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 @@
@@ -136,25 +121,7 @@
Join this mailing list to follow or participate to this discussion.
:
-
-
-
+
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 @@
- Mailing Lists
-
-
+
@@ -130,16 +140,18 @@
From a4afb910e9a7207fde8ad02d22731c60ba9a12de Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?=
Date: Thu, 20 Mar 2014 17:45:30 +0100
Subject: [PATCH 07/18] [IMP] hw_escpos / point_of_sale: print receipt with an
xml template
bzr revid: fva@openerp.com-20140320164530-odpeuf1igh7wkrms
---
addons/hw_escpos/escpos/escpos.py | 23 +--
addons/point_of_sale/static/src/js/devices.js | 41 +----
addons/point_of_sale/static/src/js/models.js | 1 +
addons/point_of_sale/static/src/js/screens.js | 5 +-
addons/point_of_sale/static/src/xml/pos.xml | 149 ++++++++++++++++++
5 files changed, 160 insertions(+), 59 deletions(-)
diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py
index cefa00fca96..7e1b18ca7a3 100644
--- a/addons/hw_escpos/escpos/escpos.py
+++ b/addons/hw_escpos/escpos/escpos.py
@@ -177,7 +177,6 @@ class XmlSerializer:
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(' ')
@@ -186,9 +185,7 @@ class XmlSerializer:
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')
@@ -197,9 +194,7 @@ class XmlSerializer:
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:
@@ -218,7 +213,6 @@ class XmlSerializer:
text = text.strip()
text = re.sub('\s+',' ',text)
if text:
- print 'printing text:'+text
self.dirty = True
self.escpos.text(text)
@@ -254,7 +248,6 @@ class XmlLineSerializer:
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)]
@@ -267,7 +260,6 @@ class XmlLineSerializer:
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(' ')
@@ -286,7 +278,6 @@ class XmlLineSerializer:
text = text.strip()
text = re.sub('\s+',' ',text)
if text:
- print 'LINE:printing text:'+text
self._txt(text)
def linebreak(self):
@@ -300,11 +291,6 @@ class XmlLineSerializer:
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
@@ -551,15 +537,11 @@ class Escpos:
formatstr = "{:"+str(width)+"."+str(decimals)+"f}"
- 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':
@@ -674,8 +656,9 @@ class Escpos:
serializer.linebreak()
elif elem.tag == 'img':
- if src in elem.attrib and 'data:' in elem.attrib['src']:
- self.print_base64_image(elem.attrib['src'])
+ pass
+ #if 'src' in elem.attrib and 'data:' in elem.attrib['src']:
+ # self.print_base64_image(elem.attrib['src'])
elif elem.tag == 'barcode' and 'encoding' in elem.attrib:
serializer.start_block(stylestack)
diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js
index 5db91924bce..f46b156371c 100644
--- a/addons/point_of_sale/static/src/js/devices.js
+++ b/addons/point_of_sale/static/src/js/devices.js
@@ -511,43 +511,8 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
return this.message('open_cashbox');
},
- /* ask the printer to print a receipt
- * receipt is a JSON object with the following specs:
- * receipt{
- * - orderlines : list of orderlines :
- * {
- * quantity: (number) the number of items, or the weight,
- * unit_name: (string) the name of the item's unit (kg, dozen, ...)
- * price: (number) the price of one unit of the item before discount
- * discount: (number) the discount on the product in % [0,100]
- * product_name: (string) the name of the product
- * price_with_tax: (number) the price paid for this orderline, tax included
- * price_without_tax: (number) the price paid for this orderline, without taxes
- * tax: (number) the price paid in taxes on this orderline
- * product_description: (string) generic description of the product
- * product_description_sale: (string) sales related information of the product
- * }
- * - paymentlines : list of paymentlines :
- * {
- * amount: (number) the amount paid
- * journal: (string) the name of the journal on wich the payment has been made
- * }
- * - total_with_tax: (number) the total of the receipt tax included
- * - total_without_tax: (number) the total of the receipt without taxes
- * - total_tax: (number) the total amount of taxes paid
- * - total_paid: (number) the total sum paid by the client
- * - change: (number) the amount of change given back to the client
- * - name: (string) a unique name for this order
- * - client: (string) name of the client. or null if no client is logged
- * - cashier: (string) the name of the cashier
- * - date: { the date at wich the payment has been done
- * year: (number) the year [2012, ...]
- * month: (number) the month [0,11]
- * date: (number) the day of the month [1,31]
- * day: (number) the day of the week [0,6]
- * hour: (number) the hour [0,23]
- * minute: (number) the minute [0,59]
- * }
+ /*
+ * ask the printer to print a receipt
*/
print_receipt: function(receipt){
var self = this;
@@ -558,7 +523,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
function send_printing_job(){
if (self.receipt_queue.length > 0){
var r = self.receipt_queue.shift();
- self.message('print_receipt',{ receipt: r },{ timeout: 5000 })
+ self.message('print_xml_receipt',{ receipt: r },{ timeout: 5000 })
.then(function(){
send_printing_job();
},function(){
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index c8c87f1b3af..8de82b4dc0a 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -987,6 +987,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
hour: date.getHours(),
minute: date.getMinutes() ,
isostring: date.toISOString(),
+ localestring: date.toLocaleString(),
},
company:{
email: company.email,
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 937ee3f88d4..1d6f018ae1b 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -1172,7 +1172,10 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}else{
this.pos.push_order(currentOrder)
if(this.pos.config.iface_print_via_proxy){
- this.pos.proxy.print_receipt(currentOrder.export_for_printing());
+ var receipt = currentOrder.export_for_printing();
+ this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{
+ receipt: receipt
+ }));
this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
}else{
this.pos_widget.screen_selector.set_current_screen(this.next_screen);
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index d0cd945d4f4..baea81becb7 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -319,6 +319,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tel:
+
+
+ VAT:
+
+
+
+
+
+
+
+
+
+
+
+ --------------------------------
+ Served by
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Discount: %
+
+
+
+
+
+
+
+
+
+ x
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ --------
+ Subtotal receipt.subtotal
+
+
+
+
+
+
+
+
+
+
+ -------
+
+ TOTAL
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CHANGE
+
+
+
+
+
+
+
+
+ Discounts
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From b206261dd5fab792f7f0da4ddd16f966e6012bbf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?=
Date: Thu, 20 Mar 2014 18:09:55 +0100
Subject: [PATCH 08/18] [IMP] hw_escpos: re-enabled image printing in xml
receipts
bzr revid: fva@openerp.com-20140320170955-ekgya8pcv2wljhl8
---
addons/hw_escpos/escpos/escpos.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py
index 7e1b18ca7a3..84b52f99d8c 100644
--- a/addons/hw_escpos/escpos/escpos.py
+++ b/addons/hw_escpos/escpos/escpos.py
@@ -656,9 +656,8 @@ class Escpos:
serializer.linebreak()
elif elem.tag == 'img':
- pass
- #if 'src' in elem.attrib and 'data:' in elem.attrib['src']:
- # self.print_base64_image(elem.attrib['src'])
+ if 'src' in elem.attrib and 'data:' in elem.attrib['src']:
+ self.print_base64_image(elem.attrib['src'])
elif elem.tag == 'barcode' and 'encoding' in elem.attrib:
serializer.start_block(stylestack)
From 0e251dbc8e136a81e7710863ec8be9b96c6cb140 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?=
Date: Fri, 21 Mar 2014 14:46:23 +0100
Subject: [PATCH 09/18] [IMP] posbox: add a homepage to the root controller on
the posbox that explains what it is and where to find information
bzr revid: fva@openerp.com-20140321134623-eaj56nrh8hkagvym
---
addons/hw_posbox_homepage/__init__.py | 25 ++++++++
addons/hw_posbox_homepage/__openerp__.py | 47 ++++++++++++++
.../controllers/__init__.py | 3 +
addons/hw_posbox_homepage/controllers/main.py | 63 +++++++++++++++++++
4 files changed, 138 insertions(+)
create mode 100644 addons/hw_posbox_homepage/__init__.py
create mode 100644 addons/hw_posbox_homepage/__openerp__.py
create mode 100644 addons/hw_posbox_homepage/controllers/__init__.py
create mode 100644 addons/hw_posbox_homepage/controllers/main.py
diff --git a/addons/hw_posbox_homepage/__init__.py b/addons/hw_posbox_homepage/__init__.py
new file mode 100644
index 00000000000..a208bc1c551
--- /dev/null
+++ b/addons/hw_posbox_homepage/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# 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 controllers
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+
diff --git a/addons/hw_posbox_homepage/__openerp__.py b/addons/hw_posbox_homepage/__openerp__.py
new file mode 100644
index 00000000000..7c0cac0f370
--- /dev/null
+++ b/addons/hw_posbox_homepage/__openerp__.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL ().
+#
+# 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 .
+#
+##############################################################################
+
+
+{
+ 'name': 'PosBox Homepage',
+ 'version': '1.0',
+ 'category': 'Hardware Drivers',
+ 'sequence': 6,
+ 'summary': 'A homepage for the PosBox',
+ 'description': """
+PosBox Homepage
+===============
+
+This module overrides openerp web interface to display a simple
+Homepage that explains what's the posbox and show the status,
+and where to find documentation.
+
+If you activate this module, you won't be able to access the
+regular openerp interface anymore.
+
+""",
+ 'author': 'OpenERP SA',
+ 'depends': ['hw_proxy'],
+ 'installable': False,
+ 'auto_install': False,
+}
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/hw_posbox_homepage/controllers/__init__.py b/addons/hw_posbox_homepage/controllers/__init__.py
new file mode 100644
index 00000000000..b5f0bcc9ec6
--- /dev/null
+++ b/addons/hw_posbox_homepage/controllers/__init__.py
@@ -0,0 +1,3 @@
+import main
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+
diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py
new file mode 100644
index 00000000000..bab94be060c
--- /dev/null
+++ b/addons/hw_posbox_homepage/controllers/main.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+import logging
+import os
+import time
+from os import listdir
+
+import openerp
+from openerp import http
+from openerp.http import request
+from openerp.tools.translate import _
+
+_logger = logging.getLogger(__name__)
+
+index_template = """
+
+
+
+ OpenERP's PosBox
+
+
+
+ Your PosBox is up and running
+
+ The PosBox is an hardware adapter that allows you to use
+ receipt printers and barcode scanners with OpenERP's Point of
+ Sale, version 8.0 or later. You can start an online free trial,
+ or download and install it yourself.
+
+
+ For more information on how to setup the Point of Sale with
+ the PosBox, please refer to the manual
+
+
+ To see the status of the connected hardware, please refer
+ to the hardware status page
+
+
+ The PosBox software installed on this posbox is version 6,
+ the posbox version number is independent from OpenERP. You can upgrade
+ the software on the upgrade page
+
+ For any other question, please contact the OpenERP support at support@openerp.com
+
+
+
+
+"""
+
+
+class PosboxHomepage(openerp.addons.web.controllers.main.Home):
+ @http.route('/', type='http', auth='none', website=True)
+ def index(self):
+ #return request.render('hw_posbox_homepage.index',mimetype='text/html')
+ return index_template
+
From e7ba075c595fce7391e3175b797ac952da3deb02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?=
Date: Fri, 21 Mar 2014 14:52:45 +0100
Subject: [PATCH 10/18] [IMP] hw_proxy: make the posbox doc publicly available
bzr revid: fva@openerp.com-20140321135245-2kcswqfijr1favwv
---
addons/hw_proxy/static/doc/manual.pdf | Bin 0 -> 252150 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 addons/hw_proxy/static/doc/manual.pdf
diff --git a/addons/hw_proxy/static/doc/manual.pdf b/addons/hw_proxy/static/doc/manual.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..5a55c56654dd357d96cefa45f5e930ad57a371da
GIT binary patch
literal 252150
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