From d5a730a0b31ad3967e418dc12ae91d922d09e67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Mon, 17 Mar 2014 14:29:06 +0100 Subject: [PATCH 01/18] [WIP] hw_escpos: printing client-side defined xml templates bzr revid: fva@openerp.com-20140317132906-uyr7h1oce8pptny9 --- addons/hw_escpos/controllers/main.py | 9 +- addons/hw_escpos/escpos/escpos.py | 178 ++++++++++++++++++++++++++- 2 files changed, 181 insertions(+), 6 deletions(-) diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index e950950a985..52036fc1b56 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -118,6 +118,8 @@ class EscposDriver(Thread): self.open_cashbox(printer) elif task == 'printstatus': self.print_status(printer) + elif task == 'testprint': + printer.receipt(testreceipt) elif task == 'status': pass @@ -280,7 +282,7 @@ driver = EscposDriver() hw_proxy.drivers['escpos'] = driver -driver.push_task('printstatus') +driver.push_task('testprint') class EscposProxy(hw_proxy.Proxy): @@ -293,4 +295,9 @@ class EscposProxy(hw_proxy.Proxy): def print_receipt(self, receipt): _logger.info('ESC/POS: PRINT RECEIPT') driver.push_task('receipt',receipt) + + @http.route('/hw_proxy/print_xml_receipt', type='json', auth='none', cors='*') + def print_receipt(self, receipt): + _logger.info('ESC/POS: PRINT XML RECEIPT') + driver.push_task('xml_receipt',receipt) diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py index a4942019f8c..08864ec0268 100644 --- a/addons/hw_escpos/escpos/escpos.py +++ b/addons/hw_escpos/escpos/escpos.py @@ -17,6 +17,8 @@ import io import base64 import math import md5 +import re +import xml.etree.ElementTree as ET from PIL import Image @@ -30,13 +32,20 @@ except ImportError: from constants import * from exceptions import * +# class RBuffer: +# def __init__(self,width=40,margin=10,tabwidth=2,indent=0): +# self.width = width +# self.margin = margin +# self.tabwidth = tabwidth +# self.indent = indent +# self.lines = [] + class Escpos: """ ESC/POS Printer object """ device = None encoding = None img_cache = {} - def _check_image_size(self, size): """ Check and fix the size of the image to 32 bits """ if size % 32 == 0: @@ -48,7 +57,6 @@ class Escpos: else: return (image_border / 2, (image_border / 2) + 1) - def _print_image(self, line, size): """ Print formatted image """ i = 0 @@ -101,7 +109,6 @@ class Escpos: return raw - def _convert_image(self, im): """ Parse image and prepare it to a printable format """ pixels = [] @@ -197,8 +204,7 @@ class Escpos: # Convert the RGB image in printable image self._convert_image(im) - - def barcode(self, code, bc, width, height, pos, font): + def barcode(self, code, bc, width=255, height=2, pos='below', font='a'): """ Print Barcode """ # Align Bar Code() self._raw(TXT_ALIGN_CT) @@ -249,6 +255,168 @@ class Escpos: else: raise exception.BarcodeCodeError() + def receipt(self,xml): + root = ET.fromstring(xml) + open_cashdrawer = False + cut_receipt = True + + width = 48 + ratio = 0.5 + indent = 0 + tabwidth = 2 + + if root.tag != 'receipt': + print 'Error: expecting receipt xml and not '+str(root.tag) + return + if 'open-cashdrawer' in root.attrib: + open_cashdrawer = root.attrib['open-cashdrawer'] == 'true' + if 'cut' in root.attrib: + cut_receipt = root.attrib['cut'] == 'true' + +# def justify(string,width): +# words = string.split() +# lines = [] +# line = '' +# linew = 0 +# for w in words: +# if len(w) <= width - len(line): +# if len(line) > 0: +# line += ' ' +# line += w +# else: +# if len(line) > 0: +# lines.append(line.ljust(width)) +# line = '' +# line += w +# if len(line) > 0: +# lines.append(line.ljust(width)) +# return lines +# +# def strindent(indent,string): +# ind = tabwidth * indent +# rem = width - ind +# lines = justify(string,rem) +# txt = '' +# for l in lines: +# txt += ' '*ind + '\n' +# +# return txt + + def strclean(string): + if not string: + string = '' + string = string.strip() + string = re.sub('\s+',' ',string) + return string + + + def print_ul(elem, indent=0): + bullets = ['-','-'] + bullet = ' '+bullets[indent % 2]+' ' + if elem.tag == 'ul': + for lelem in elem: + if lelem.tag == 'li': + self.text(' ' * indent * tabwidth + bullet + strclean(lelem.text)+'\n') + elif lelem.tag == 'ul': + print_ul(lelem,indent+1) + elif lelem.tag == 'ol': + print_old(lelem,indent+1) + + def print_ol(elem, indent=0): + cwidth = len(str(len(elem))) + 2 + i = 1 + for lelem in elem: + if lelem.tag == 'li': + self.text(' ' * indent * tabwidth + ' ' + (str(i)+'.').ljust(cwidth)+strclean(lelem.text)+'\n') + i += 1 + elif lelem.tag == 'ul': + print_ul(lelem,indent+1) + elif lelem.tag == 'ol': + print_ol(lelem,indent+1) + + + for elem in root: + if elem.tag == 'line': + left = strclean(elem.text) + right = '' + for lelem in elem: + if lelem.tag == 'left': + left = strclean(lelem.text) + print 'left:'+left + elif lelem.tag == 'right': + right = strclean(lelem.text) + print 'right:'+right + lwidth = int(width * ratio) + rwidth = width - lwidth + lwidth = lwidth - indent + + left = left[:lwidth] + if len(left) != lwidth: + left = left + ' ' * (lwidth - len(left)) + + right = right[-rwidth:] + if len(right) != rwidth: + right = ' ' * (rwidth - len(right)) + right + line = ' ' * indent + left + right + '\n' + print line + self.text(line) + elif elem.tag == 'img': + if src in elem.attrib and 'data:' in elem.attrib['src']: + self.print_base64_image(elem.attrib['src']) + elif elem.tag == 'p': + self.text(strclean(elem.text)+'\n') + elif elem.tag == 'h1': + self.set(align='left', font='a', type='b', width=2, height=2) + self._raw('\x1b\x21\x30') + self._raw('\x1b\x45\x01') + self.text(strclean(elem.text)+'\n') + self.set() + elif elem.tag == 'h2': + self.set(align='left', font='a', type='bu', width=1, height=2) + self._raw('\x1b\x21\x30') + self.text(strclean(elem.text)+'\n') + self.set() + elif elem.tag == 'h3': + self.set(align='left', font='a', type='u', width=1, height=2) + self._raw('\x1b\x45\x01') + self.text(strclean(elem.text)+'\n') + self.set() + elif elem.tag == 'h4': + self.set(align='left', font='a', type='bu', width=1, height=2) + self.text(strclean(elem.text)+'\n') + self.set() + elif elem.tag == 'h5': + self.set(align='left', font='a', type='u', width=1, height=1) + self._raw('\x1b\x45\x01') + self.text(strclean(elem.text)+'\n') + self.set() + elif elem.tag == 'pre': + self.text(elem.text) + elif elem.tag == 'cut': + self.cut() + elif elem.tag == 'ul': + print_ul(elem) + elif elem.tag == 'ol': + print_ol(elem) + elif elem.tag == 'hr': + self.text('-'*width+'\n') + elif elem.tag == 'br': + self.text('\n') + elif elem.tag == 'barcode' and 'encoding' in elem.attrib: + self.barcode(strclean(elem.text),elem.attrib['encoding']) + elif elem.tag == 'partialcut': + self.cut(mode='part') + elif elem.tag == 'cashdraw': + self.cashdraw(2) + self.cashdraw(5) + + if cut_receipt: + self.cut() + if open_cashdrawer: + self.cashdraw(2) + self.cashdraw(5) + + def text(self,txt): """ Print Utf8 encoded alpha-numeric text """ if not txt: From 97a6372937bace64d037af58afb04ab5945befd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 20 Mar 2014 10:09:31 +0100 Subject: [PATCH 02/18] [CLEAN] website_mail: use already-existing template for subscription instead of copy-and-pasting everything. Please re-use existing things -_-. bzr revid: tde@openerp.com-20140320090931-r17typzrvvsx03ay --- .../views/website_mail_group.xml | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/addons/website_mail_group/views/website_mail_group.xml b/addons/website_mail_group/views/website_mail_group.xml index fb18ee9f3fd..d1e1fbde024 100644 --- a/addons/website_mail_group/views/website_mail_group.xml +++ b/addons/website_mail_group/views/website_mail_group.xml @@ -18,22 +18,7 @@
- -
- - - unsubscribe - subscribe - - -
+
@@ -136,25 +121,7 @@ Join this mailing list to follow or participate to this discussion.
:

- -
-
- - - unsubscribe - subscribe - - -
-
- + From 53f02309ed9fbd85e58c49f10ed0843c10b5c486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Thu, 20 Mar 2014 12:27:20 +0100 Subject: [PATCH 03/18] [IMP] hw_escpos: print_xml_receipt() controller allows you to print an xml defined receipt bzr revid: fva@openerp.com-20140320112720-ldshe2764z3fyfvi --- addons/hw_escpos/controllers/main.py | 11 +- addons/hw_escpos/escpos/constants.py | 1 + addons/hw_escpos/escpos/escpos.py | 585 ++++++++++++++++++++------- 3 files changed, 448 insertions(+), 149 deletions(-) diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index 52036fc1b56..56c56a158f2 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -12,6 +12,7 @@ import math import md5 import openerp.addons.hw_proxy.controllers.main as hw_proxy import subprocess +import traceback from threading import Thread, Lock from Queue import Queue, Empty @@ -113,19 +114,21 @@ class EscposDriver(Thread): if timestamp >= time.time() - 1 * 60 * 60: self.print_receipt_body(printer,data) printer.cut() + elif task == 'xml_receipt': + if timestamp >= time.time() - 1 * 60 * 60: + printer.receipt(data) elif task == 'cashbox': if timestamp >= time.time() - 12: self.open_cashbox(printer) elif task == 'printstatus': self.print_status(printer) - elif task == 'testprint': - printer.receipt(testreceipt) elif task == 'status': pass except Exception as e: self.set_status('error', str(e)) - _logger.error(e); + errmsg = str(e) + '\n' + '-'*60+'\n' + traceback.format_exc() + '-'*60 + '\n' + _logger.error(errmsg); def push_task(self,task, data = None): self.lockedstart() @@ -282,8 +285,6 @@ driver = EscposDriver() hw_proxy.drivers['escpos'] = driver -driver.push_task('testprint') - class EscposProxy(hw_proxy.Proxy): @http.route('/hw_proxy/open_cashbox', type='json', auth='none', cors='*') diff --git a/addons/hw_escpos/escpos/constants.py b/addons/hw_escpos/escpos/constants.py index 723c67013f0..6f0be0bb135 100644 --- a/addons/hw_escpos/escpos/constants.py +++ b/addons/hw_escpos/escpos/constants.py @@ -22,6 +22,7 @@ PAPER_PART_CUT = '\x1d\x56\x01' # Partial cut paper TXT_NORMAL = '\x1b\x21\x00' # Normal text TXT_2HEIGHT = '\x1b\x21\x10' # Double height text TXT_2WIDTH = '\x1b\x21\x20' # Double width text +TXT_DOUBLE = '\x1b\x21\x30' # Double height & Width TXT_UNDERL_OFF = '\x1b\x2d\x00' # Underline font OFF TXT_UNDERL_ON = '\x1b\x2d\x01' # Underline font 1-dot ON TXT_UNDERL2_ON = '\x1b\x2d\x02' # Underline font 2-dot ON diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py index 08864ec0268..cefa00fca96 100644 --- a/addons/hw_escpos/escpos/escpos.py +++ b/addons/hw_escpos/escpos/escpos.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +# -*- coding: utf-8 -*- ''' @author: Manuel F Martinez @organization: Bashlinux @@ -18,7 +18,9 @@ import base64 import math import md5 import re +import traceback import xml.etree.ElementTree as ET +import xml.dom.minidom as minidom from PIL import Image @@ -32,13 +34,279 @@ except ImportError: from constants import * from exceptions import * -# class RBuffer: -# def __init__(self,width=40,margin=10,tabwidth=2,indent=0): -# self.width = width -# self.margin = margin -# self.tabwidth = tabwidth -# self.indent = indent -# self.lines = [] +def utfstr(stuff): + """ converts stuff to string and does without failing if stuff is a utf8 string """ + if isinstance(stuff,basestring): + return stuff + else: + return str(stuff) + +class StyleStack: + """ + The stylestack is used by the xml receipt serializer to compute the active styles along the xml + document. Styles are just xml attributes, there is no css mechanism. But the style applied by + the attributes are inherited by deeper nodes. + """ + def __init__(self): + self.stack = [] + self.defaults = { # default style values + 'align': 'left', + 'underline': 'off', + 'bold': 'off', + 'size': 'normal', + 'font' : 'a', + 'width': 48, + 'indent': 0, + 'tabwidth': 2, + 'bullet': ' - ', + 'line-ratio':0.5, + + 'value-decimals': 2, + 'value-symbol': '', + 'value-symbol-position': 'after', + 'value-autoint': 'off', + 'value-decimals-separator': '.', + 'value-thousands-separator': ',', + 'value-width': 0, + + } + + self.types = { # attribute types, default is string and can be ommitted + 'width': 'int', + 'indent': 'int', + 'tabwidth': 'int', + 'line-ratio': 'float', + 'value-decimals': 'int', + 'value-width': 'int', + } + + self.cmds = { + # translation from styles to escpos commands + # some style do not correspond to escpos command are used by + # the serializer instead + 'align': { + 'left': TXT_ALIGN_LT, + 'right': TXT_ALIGN_RT, + 'center': TXT_ALIGN_CT, + }, + 'underline': { + 'off': TXT_UNDERL_OFF, + 'on': TXT_UNDERL_ON, + 'double': TXT_UNDERL2_ON, + }, + 'bold': { + 'off': TXT_BOLD_OFF, + 'on': TXT_BOLD_ON, + }, + 'font': { + 'a': TXT_FONT_A, + 'b': TXT_FONT_B, + }, + 'size': { + 'normal': TXT_NORMAL, + 'double-height': TXT_2HEIGHT, + 'double-width': TXT_2WIDTH, + 'double': TXT_DOUBLE, + } + } + + self.push(self.defaults) + + def get(self,style): + """ what's the value of a style at the current stack level""" + level = len(self.stack) -1 + while level >= 0: + if style in self.stack[level]: + return self.stack[level][style] + else: + level = level - 1 + return None + + def enforce_type(self, attr, val): + """converts a value to the attribute's type""" + if not attr in self.types: + return utfstr(val) + elif self.types[attr] == 'int': + return int(float(val)) + elif self.types[attr] == 'float': + return float(val) + else: + return utfstr(val) + + def push(self, style={}): + """push a new level on the stack with a style dictionnary containing style:value pairs""" + _style = {} + for attr in style: + if attr in self.cmds and not style[attr] in self.cmds[attr]: + print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr) + else: + _style[attr] = self.enforce_type(attr, style[attr]) + self.stack.append(_style) + + def set(self, style={}): + """overrides style values at the current stack level""" + _style = {} + for attr in style: + if attr in self.cmds and not style[attr] in self.cmds[attr]: + print 'WARNING: ESC/POS PRINTING: ignoring invalid value: '+utfstr(style[attr])+' for style: '+utfstr(attr) + else: + self.stack[-1][attr] = self.enforce_type(attr, style[attr]) + + def pop(self): + """ pop a style stack level """ + if len(self.stack) > 1 : + self.stack = self.stack[:-1] + + def to_escpos(self): + """ converts the current style to an escpos command string """ + cmd = '' + for style in self.cmds: + cmd += self.cmds[style][self.get(style)] + return cmd + +class XmlSerializer: + """ + Converts the xml inline / block tree structure to a string, + keeping track of newlines and spacings. + The string is outputted asap to the provided escpos driver. + """ + def __init__(self,escpos): + self.escpos = escpos + self.stack = ['block'] + self.dirty = False + + def start_inline(self,stylestack=None): + """ starts an inline entity with an optional style definition """ + print 'start_inline' + self.stack.append('inline') + if self.dirty: + self.escpos._raw(' ') + if stylestack: + self.style(stylestack) + + def start_block(self,stylestack=None): + """ starts a block entity with an optional style definition """ + print 'start_block' + if self.dirty: + print 'cleanup before block' + self.escpos._raw('\n') + self.dirty = False + self.stack.append('block') + if stylestack: + self.style(stylestack) + + def end_entity(self): + """ ends the entity definition. (but does not cancel the active style!) """ + print 'end_entity' + if self.stack[-1] == 'block' and self.dirty: + print 'cleanup after block' + self.escpos._raw('\n') + self.dirty = False + if len(self.stack) > 1: + self.stack = self.stack[:-1] + + def pre(self,text): + """ puts a string of text in the entity keeping the whitespace intact """ + if text: + self.escpos.text(text) + self.dirty = True + + def text(self,text): + """ puts text in the entity. Whitespace and newlines are stripped to single spaces. """ + if text: + text = utfstr(text) + text = text.strip() + text = re.sub('\s+',' ',text) + if text: + print 'printing text:'+text + self.dirty = True + self.escpos.text(text) + + def linebreak(self): + """ inserts a linebreak in the entity """ + self.dirty = False + self.escpos._raw('\n') + + def style(self,stylestack): + """ apply a style to the entity (only applies to content added after the definition) """ + self.raw(stylestack.to_escpos()) + + def raw(self,raw): + """ puts raw text or escpos command in the entity without affecting the state of the serializer """ + self.escpos._raw(raw) + +class XmlLineSerializer: + """ + This is used to convert a xml tree into a single line, with a left and a right part. + The content is not output to escpos directly, and is intended to be fedback to the + XmlSerializer as the content of a block entity. + """ + def __init__(self, indent=0, tabwidth=2, width=48, ratio=0.5): + self.tabwidth = tabwidth + self.indent = indent + self.width = max(0, width - int(tabwidth*indent)) + self.lwidth = int(self.width*ratio) + self.rwidth = max(0, self.width - self.lwidth) + self.clwidth = 0 + self.crwidth = 0 + self.lbuffer = '' + self.rbuffer = '' + self.left = True + + def _txt(self,txt): + print '_txt: ',txt + if self.left: + if self.clwidth < self.lwidth: + txt = txt[:max(0, self.lwidth - self.clwidth)] + self.lbuffer += txt + self.clwidth += len(txt) + else: + if self.crwidth < self.rwidth: + txt = txt[:max(0, self.rwidth - self.crwidth)] + self.rbuffer += txt + self.crwidth += len(txt) + + def start_inline(self,stylestack=None): + print 'LINE:start_entity' + if (self.left and self.clwidth) or (not self.left and self.crwidth): + self._txt(' ') + + def start_block(self,stylestack=None): + self.start_inline(stylestack) + + def end_entity(self): + pass + + def pre(self,text): + if text: + self._txt(text) + def text(self,text): + if text: + text = utfstr(text) + text = text.strip() + text = re.sub('\s+',' ',text) + if text: + print 'LINE:printing text:'+text + self._txt(text) + + def linebreak(self): + pass + def style(self,stylestack): + pass + def raw(self,raw): + pass + + def start_right(self): + self.left = False + + def get_line(self): + print 'LBUFFER: '+self.lbuffer + print self.clwidth + print 'RBUFFER: '+self.rbuffer + print self.crwidth + + return ' ' * self.indent * self.tabwidth + self.lbuffer + ' ' * (self.width - self.clwidth - self.crwidth) + self.rbuffer + class Escpos: """ ESC/POS Printer object """ @@ -256,51 +524,9 @@ class Escpos: raise exception.BarcodeCodeError() def receipt(self,xml): - root = ET.fromstring(xml) - open_cashdrawer = False - cut_receipt = True - - width = 48 - ratio = 0.5 - indent = 0 - tabwidth = 2 - - if root.tag != 'receipt': - print 'Error: expecting receipt xml and not '+str(root.tag) - return - if 'open-cashdrawer' in root.attrib: - open_cashdrawer = root.attrib['open-cashdrawer'] == 'true' - if 'cut' in root.attrib: - cut_receipt = root.attrib['cut'] == 'true' - -# def justify(string,width): -# words = string.split() -# lines = [] -# line = '' -# linew = 0 -# for w in words: -# if len(w) <= width - len(line): -# if len(line) > 0: -# line += ' ' -# line += w -# else: -# if len(line) > 0: -# lines.append(line.ljust(width)) -# line = '' -# line += w -# if len(line) > 0: -# lines.append(line.ljust(width)) -# return lines -# -# def strindent(indent,string): -# ind = tabwidth * indent -# rem = width - ind -# lines = justify(string,rem) -# txt = '' -# for l in lines: -# txt += ' '*ind + '\n' -# -# return txt + """ + Prints an xml based receipt definition + """ def strclean(string): if not string: @@ -309,113 +535,184 @@ class Escpos: string = re.sub('\s+',' ',string) return string + def format_value(value, decimals=3, width=0, decimals_separator='.', thousands_separator=',', autoint=False, symbol='', position='after'): + decimals = max(0,int(decimals)) + width = max(0,int(width)) + value = float(value) - def print_ul(elem, indent=0): - bullets = ['-','-'] - bullet = ' '+bullets[indent % 2]+' ' - if elem.tag == 'ul': - for lelem in elem: - if lelem.tag == 'li': - self.text(' ' * indent * tabwidth + bullet + strclean(lelem.text)+'\n') - elif lelem.tag == 'ul': - print_ul(lelem,indent+1) - elif lelem.tag == 'ol': - print_old(lelem,indent+1) + if autoint and math.floor(value) == value: + decimals = 0 + if width == 0: + width = '' - def print_ol(elem, indent=0): - cwidth = len(str(len(elem))) + 2 - i = 1 - for lelem in elem: - if lelem.tag == 'li': - self.text(' ' * indent * tabwidth + ' ' + (str(i)+'.').ljust(cwidth)+strclean(lelem.text)+'\n') - i += 1 - elif lelem.tag == 'ul': - print_ul(lelem,indent+1) - elif lelem.tag == 'ol': - print_ol(lelem,indent+1) - - - for elem in root: - if elem.tag == 'line': - left = strclean(elem.text) - right = '' - for lelem in elem: - if lelem.tag == 'left': - left = strclean(lelem.text) - print 'left:'+left - elif lelem.tag == 'right': - right = strclean(lelem.text) - print 'right:'+right - lwidth = int(width * ratio) - rwidth = width - lwidth - lwidth = lwidth - indent + if thousands_separator: + formatstr = "{:"+str(width)+",."+str(decimals)+"f}" + else: + formatstr = "{:"+str(width)+"."+str(decimals)+"f}" - left = left[:lwidth] - if len(left) != lwidth: - left = left + ' ' * (lwidth - len(left)) - right = right[-rwidth:] - if len(right) != rwidth: - right = ' ' * (rwidth - len(right)) + right - line = ' ' * indent + left + right + '\n' - print line - self.text(line) + print formatstr + print value + ret = formatstr.format(value) + print ret + ret = ret.replace(',','COMMA') + ret = ret.replace('.','DOT') + ret = ret.replace('COMMA',thousands_separator) + ret = ret.replace('DOT',decimals_separator) + print 'RET '+ret + + if symbol: + if position == 'after': + ret = ret + symbol + else: + ret = symbol + ret + return ret + + def print_elem(stylestack, serializer, elem, indent=0): + + elem_styles = { + 'h1': {'bold': 'on', 'size':'double'}, + 'h2': {'size':'double'}, + 'h3': {'bold': 'on', 'size':'double-height'}, + 'h4': {'size': 'double-height'}, + 'h5': {'bold': 'on'}, + 'em': {'font': 'b'}, + 'b': {'bold': 'on'}, + } + + stylestack.push() + if elem.tag in elem_styles: + stylestack.set(elem_styles[elem.tag]) + stylestack.set(elem.attrib) + + if elem.tag in ('p','div','section','article','receipt','header','footer','li','h1','h2','h3','h4','h5'): + serializer.start_block(stylestack) + serializer.text(elem.text) + for child in elem: + print_elem(stylestack,serializer,child) + serializer.start_inline(stylestack) + serializer.text(child.tail) + serializer.end_entity() + serializer.end_entity() + + elif elem.tag in ('span','em','b','left','right'): + serializer.start_inline(stylestack) + serializer.text(elem.text) + for child in elem: + print_elem(stylestack,serializer,child) + serializer.start_inline(stylestack) + serializer.text(child.tail) + serializer.end_entity() + serializer.end_entity() + + elif elem.tag == 'value': + serializer.start_inline(stylestack) + serializer.pre(format_value( + elem.text, + decimals=stylestack.get('value-decimals'), + width=stylestack.get('value-width'), + decimals_separator=stylestack.get('value-decimals-separator'), + thousands_separator=stylestack.get('value-thousands-separator'), + autoint=(stylestack.get('autoint') == 'on'), + symbol=stylestack.get('value-symbol'), + position=stylestack.get('value-symbol-position') + )) + serializer.end_entity() + + elif elem.tag == 'line': + width = stylestack.get('width') + if stylestack.get('size') in ('double', 'double-width'): + width = width / 2 + + lineserializer = XmlLineSerializer(stylestack.get('indent')+indent,stylestack.get('tabwidth'),width,stylestack.get('line-ratio')) + serializer.start_block(stylestack) + for child in elem: + if child.tag == 'left': + print_elem(stylestack,lineserializer,child,indent=indent) + elif child.tag == 'right': + lineserializer.start_right() + print_elem(stylestack,lineserializer,child,indent=indent) + serializer.pre(lineserializer.get_line()) + serializer.end_entity() + + elif elem.tag == 'ul': + serializer.start_block(stylestack) + bullet = stylestack.get('bullet') + for child in elem: + if child.tag == 'li': + serializer.style(stylestack) + serializer.raw(' ' * indent * stylestack.get('tabwidth') + bullet) + print_elem(stylestack,serializer,child,indent=indent+1) + serializer.end_entity() + + elif elem.tag == 'ol': + cwidth = len(str(len(elem))) + 2 + i = 1 + serializer.start_block(stylestack) + for child in elem: + if child.tag == 'li': + serializer.style(stylestack) + serializer.raw(' ' * indent * stylestack.get('tabwidth') + ' ' + (str(i)+')').ljust(cwidth)) + i = i + 1 + print_elem(stylestack,serializer,child,indent=indent+1) + serializer.end_entity() + + elif elem.tag == 'pre': + serializer.start_block(stylestack) + serializer.pre(elem.text) + serializer.end_entity() + + elif elem.tag == 'hr': + width = stylestack.get('width') + if stylestack.get('size') in ('double', 'double-width'): + width = width / 2 + serializer.start_block(stylestack) + serializer.text('-'*width) + serializer.end_entity() + + elif elem.tag == 'br': + serializer.linebreak() + elif elem.tag == 'img': if src in elem.attrib and 'data:' in elem.attrib['src']: self.print_base64_image(elem.attrib['src']) - elif elem.tag == 'p': - self.text(strclean(elem.text)+'\n') - elif elem.tag == 'h1': - self.set(align='left', font='a', type='b', width=2, height=2) - self._raw('\x1b\x21\x30') - self._raw('\x1b\x45\x01') - self.text(strclean(elem.text)+'\n') - self.set() - elif elem.tag == 'h2': - self.set(align='left', font='a', type='bu', width=1, height=2) - self._raw('\x1b\x21\x30') - self.text(strclean(elem.text)+'\n') - self.set() - elif elem.tag == 'h3': - self.set(align='left', font='a', type='u', width=1, height=2) - self._raw('\x1b\x45\x01') - self.text(strclean(elem.text)+'\n') - self.set() - elif elem.tag == 'h4': - self.set(align='left', font='a', type='bu', width=1, height=2) - self.text(strclean(elem.text)+'\n') - self.set() - elif elem.tag == 'h5': - self.set(align='left', font='a', type='u', width=1, height=1) - self._raw('\x1b\x45\x01') - self.text(strclean(elem.text)+'\n') - self.set() - elif elem.tag == 'pre': - self.text(elem.text) + + elif elem.tag == 'barcode' and 'encoding' in elem.attrib: + serializer.start_block(stylestack) + self.barcode(strclean(elem.text),elem.attrib['encoding']) + serializer.end_entity() + elif elem.tag == 'cut': self.cut() - elif elem.tag == 'ul': - print_ul(elem) - elif elem.tag == 'ol': - print_ol(elem) - elif elem.tag == 'hr': - self.text('-'*width+'\n') - elif elem.tag == 'br': - self.text('\n') - elif elem.tag == 'barcode' and 'encoding' in elem.attrib: - self.barcode(strclean(elem.text),elem.attrib['encoding']) elif elem.tag == 'partialcut': self.cut(mode='part') elif elem.tag == 'cashdraw': self.cashdraw(2) self.cashdraw(5) - if cut_receipt: - self.cut() - if open_cashdrawer: - self.cashdraw(2) - self.cashdraw(5) + stylestack.pop() + try: + stylestack = StyleStack() + serializer = XmlSerializer(self) + root = ET.fromstring(xml) + + self._raw(stylestack.to_escpos()) + + print_elem(stylestack,serializer,root) + + if 'open-cashdrawer' in root.attrib and root.attrib['open-cashdrawer'] == 'true': + self.cashdraw(2) + self.cashdraw(5) + if not 'cut' in root.attrib or root.attrib['cut'] == 'true' : + self.cut() + + except Exception as e: + errmsg = str(e)+'\n'+'-'*48+'\n'+traceback.format_exc() + '-'*48+'\n' + self.text(errmsg) + self.cut() + + raise e def text(self,txt): """ Print Utf8 encoded alpha-numeric text """ From 2bc87a67ebf282aa33b6164fc832ce5f6b3ca241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 20 Mar 2014 16:08:55 +0100 Subject: [PATCH 04/18] [IMP] mail_group: as they are now mailing list, avoid having automatic creation message, this is quite ugly. bzr revid: tde@openerp.com-20140320150855-a0ich2zgzn320wj0 --- addons/mail/mail_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py index 5a9f8751f2d..4ae47a96774 100644 --- a/addons/mail/mail_group.py +++ b/addons/mail/mail_group.py @@ -126,7 +126,7 @@ class mail_group(osv.Model): vals['menu_id'] = menu_id # Create group and alias - create_context = dict(context, alias_model_name=self._name, alias_parent_model_name=self._name) + create_context = dict(context, alias_model_name=self._name, alias_parent_model_name=self._name, mail_create_nolog=True) mail_group_id = super(mail_group, self).create(cr, uid, vals, context=create_context) group = self.browse(cr, uid, mail_group_id, context=context) self.pool.get('mail.alias').write(cr, uid, [group.alias_id.id], {"alias_force_thread_id": mail_group_id, 'alias_parent_thread_id': mail_group_id}, context) From 6af6575efb9738438dd40f53ad153eb426b8269a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 20 Mar 2014 16:10:04 +0100 Subject: [PATCH 05/18] [IMP] website_mail: added not stored function field on mail.message that is a short description of the message, either the subject if present, of the beginning of the plain-text version of the body. Used whenever messages 'subject' for all messages is necessary. bzr revid: tde@openerp.com-20140320151004-qq57s9r8qcpue0rs --- addons/website_mail/models/mail_message.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/addons/website_mail/models/mail_message.py b/addons/website_mail/models/mail_message.py index 12c35f3e83f..a376d391a33 100644 --- a/addons/website_mail/models/mail_message.py +++ b/addons/website_mail/models/mail_message.py @@ -19,6 +19,7 @@ # ############################################################################## +from openerp.tools import html2plaintext from openerp.tools.translate import _ from openerp.osv import osv, fields @@ -26,7 +27,21 @@ from openerp.osv import osv, fields class MailMessage(osv.Model): _inherit = 'mail.message' + def _get_description_short(self, cr, uid, ids, name, arg, context=None): + res = dict.fromkeys(ids, False) + for message in self.browse(cr, uid, ids, context=context): + if message.subject: + res[message.id] = message.subject + else: + plaintext_ct = html2plaintext(message.body) + res[message.id] = plaintext_ct + '%s' % (' [...]' if len(plaintext_ct) >= 20 else '') + return res + _columns = { + 'description': fields.function( + _get_description_short, type='char', + help='Message description: either the subject, or the beginning of the body' + ), 'website_published': fields.boolean( 'Published', help="Visible on the website as a comment" ), From 50ca5202a0e9722c131079774138125e47d3a396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 20 Mar 2014 16:11:14 +0100 Subject: [PATCH 06/18] [REF] website_mail_group: refactored controllers and views - now using post arguments instead of hardcoded path items like mode; - unfucked the date archives that was basically now working; - keep user choices through the navigation (mode, dates); - various small fixes in the views bzr revid: tde@openerp.com-20140320151114-1mmk9x48i1lwc6qv --- addons/website_mail_group/controllers/main.py | 78 +++++++++---------- .../views/website_mail_group.xml | 36 ++++++--- 2 files changed, 63 insertions(+), 51 deletions(-) diff --git a/addons/website_mail_group/controllers/main.py b/addons/website_mail_group/controllers/main.py index fe133434abc..2ec32f40f01 100644 --- a/addons/website_mail_group/controllers/main.py +++ b/addons/website_mail_group/controllers/main.py @@ -1,34 +1,29 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2013-Today OpenERP SA (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## +import datetime + +from openerp import tools from openerp.addons.web import http +from openerp.addons.website.models.website import slug from openerp.addons.web.http import request class MailGroup(http.Controller): - _thread_per_page = 10 + _thread_per_page = 2 - @http.route([ - "/groups", - ], type='http', auth="public", website=True) + def _get_archives(self, group_id): + MailMessage = request.registry['mail.message'] + groups = MailMessage.read_group( + request.cr, request.uid, [('model', '=', 'mail.group'), ('res_id', '=', group_id)], ['subject', 'date'], + groupby="date", orderby="date asc", context=request.context) + for group in groups: + begin_date = datetime.datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATETIME_FORMAT).date() + end_date = datetime.datetime.strptime(group['__domain'][1][2], tools.DEFAULT_SERVER_DATETIME_FORMAT).date() + group['date_begin'] = '%s' % datetime.date.strftime(begin_date, tools.DEFAULT_SERVER_DATE_FORMAT) + group['date_end'] = '%s' % datetime.date.strftime(end_date, tools.DEFAULT_SERVER_DATE_FORMAT) + return groups + + @http.route("/groups", type='http', auth="public", website=True) def view(self, **post): cr, uid, context = request.cr, request.uid, request.context group_obj = request.registry.get('mail.group') @@ -37,7 +32,7 @@ class MailGroup(http.Controller): return request.website.render('website_mail_group.mail_groups', values) @http.route(["/groups/subscription/"], type='json', auth="user") - def subscription(self, group_id=0, action=False ,**post): + def subscription(self, group_id=0, action=False, **post): cr, uid, context = request.cr, request.uid, request.context group_obj = request.registry.get('mail.group') if action: @@ -47,45 +42,50 @@ class MailGroup(http.Controller): return [] @http.route([ - "/groups//", - "/groups///page/" + "/groups/", + "/groups//page//" ], type='http', auth="public", website=True) - def thread(self, group, mode='thread', page=1, **post): + def thread_headers(self, group, page=1, mode='thread', date_begin=None, date_end=None, **post): cr, uid, context = request.cr, request.uid, request.context - thread_obj = request.registry.get('mail.message') - domain = [('model','=','mail.group'), ('res_id','=',group.id)] - if mode=='thread': - domain.append(('parent_id','=',False)) + + domain = [('model', '=', 'mail.group'), ('res_id', '=', group.id)] + if mode == 'thread': + domain += [('parent_id', '=', False)] + if date_begin and date_end: + domain += [('date', '>=', date_begin), ('date', '<=', date_end)] + thread_count = thread_obj.search_count(cr, uid, domain, context=context) pager = request.website.pager( - url='/groups/%s/%s/' % (group.id, mode), + url='/groups/%s/' % slug(group), total=thread_count, page=page, step=self._thread_per_page, + url_args={'mode': mode, 'date_begin': date_begin or '', 'date_end': date_end or ''}, ) thread_ids = thread_obj.search(cr, uid, domain, limit=self._thread_per_page, offset=pager['offset']) - messages = thread_obj.browse(cr, uid, thread_ids, context) - for m in messages: - print m.subject values = { 'messages': messages, 'group': group, 'pager': pager, - 'mode': mode + 'mode': mode, + 'archives': self._get_archives(group.id), + 'date_begin': date_begin, + 'date_end': date_end, } return request.website.render('website_mail_group.group_messages', values) @http.route([ - "/groups//message/", + "/groups//", ], type='http', auth="public", website=True) - def get_thread(self, group, message, mode='thread', page=1, **post): + def thread_discussion(self, group, message, mode='thread', date_begin=None, date_end=None, **post): cr, uid, context = request.cr, request.uid, request.context values = { 'message': message, 'group': group, 'mode': mode, - 'page': page, + 'date_begin': date_begin, + 'date_end': date_end, } return request.website.render('website_mail_group.group_message', values) diff --git a/addons/website_mail_group/views/website_mail_group.xml b/addons/website_mail_group/views/website_mail_group.xml index d1e1fbde024..33af5c0c58d 100644 --- a/addons/website_mail_group/views/website_mail_group.xml +++ b/addons/website_mail_group/views/website_mail_group.xml @@ -16,7 +16,7 @@
- +
@@ -48,10 +48,20 @@
@@ -71,7 +81,7 @@