[MERGE] point_of_sale & posbox: new posbox homepage, posbox software upgrade system, printing xml based receipt

bzr revid: fva@openerp.com-20140326134236-fuwrlti8356dmyp9
This commit is contained in:
Frédéric van der Essen 2014-03-26 14:42:36 +01:00
commit 6ad8096210
17 changed files with 1010 additions and 53 deletions

View File

@ -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
@ -36,6 +37,7 @@ from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
class EscposDriver(Thread):
def __init__(self):
Thread.__init__(self)
@ -45,6 +47,7 @@ class EscposDriver(Thread):
def connected_usb_devices(self):
connected = []
for device in supported_devices.device_list:
if usb.core.find(idVendor=device['vendor'], idProduct=device['product']) != None:
connected.append(device)
@ -74,6 +77,8 @@ class EscposDriver(Thread):
self.push_task('status')
return self.status
def open_cashbox(self,printer):
printer.cashdraw(2)
printer.cashdraw(5)
@ -113,6 +118,9 @@ 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)
@ -123,7 +131,8 @@ class EscposDriver(Thread):
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()
@ -142,12 +151,15 @@ class EscposDriver(Thread):
if len(ips) == 0:
eprint.text('ERROR: Could not connect to LAN\n\nPlease check that the PosBox is correc-\ntly connected with a network cable,\n that the LAN is setup with DHCP, and\nthat network addresses are available')
elif len(ips) == 1:
eprint.text('IP Address\n'+ips[0]+'\n')
eprint.text('IP Address:\n'+ips[0]+'\n')
else:
eprint.text('IP Addresses\n')
eprint.text('IP Addresses:\n')
for ip in ips:
eprint.text(ip+'\n')
if len(ips) >= 1:
eprint.text('\nHomepage:\nhttp://'+ips[0]+':8069\n')
eprint.text('\n\n')
eprint.cut()
@ -278,10 +290,10 @@ class EscposDriver(Thread):
driver = EscposDriver()
driver.push_task('printstatus')
hw_proxy.drivers['escpos'] = driver
driver.push_task('printstatus')
class EscposProxy(hw_proxy.Proxy):
@http.route('/hw_proxy/open_cashbox', type='json', auth='none', cors='*')
@ -293,4 +305,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)

View File

@ -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

View File

@ -1,4 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
'''
@author: Manuel F Martinez <manpaz@bashlinux.com>
@organization: Bashlinux
@ -17,6 +17,10 @@ import io
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
@ -30,13 +34,272 @@ except ImportError:
from constants import *
from exceptions import *
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 """
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 """
if self.dirty:
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!) """
if self.stack[-1] == 'block' and self.dirty:
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:
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):
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):
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:
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):
return ' ' * self.indent * self.tabwidth + self.lbuffer + ' ' * (self.width - self.clwidth - self.crwidth) + self.rbuffer
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 +311,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 +363,6 @@ class Escpos:
return raw
def _convert_image(self, im):
""" Parse image and prepare it to a printable format """
pixels = []
@ -197,8 +458,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 +509,193 @@ class Escpos:
else:
raise exception.BarcodeCodeError()
def receipt(self,xml):
"""
Prints an xml based receipt definition
"""
def strclean(string):
if not string:
string = ''
string = string.strip()
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)
if autoint and math.floor(value) == value:
decimals = 0
if width == 0:
width = ''
if thousands_separator:
formatstr = "{:"+str(width)+",."+str(decimals)+"f}"
else:
formatstr = "{:"+str(width)+"."+str(decimals)+"f}"
ret = formatstr.format(value)
ret = ret.replace(',','COMMA')
ret = ret.replace('.','DOT')
ret = ret.replace('COMMA',thousands_separator)
ret = ret.replace('DOT',decimals_separator)
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 == '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 == 'partialcut':
self.cut(mode='part')
elif elem.tag == 'cashdraw':
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 """
if not txt:

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import controllers
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'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:

View File

@ -0,0 +1,3 @@
import main
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -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 = """
<!DOCTYPE HTML>
<html>
<head>
<title>OpenERP's PosBox</title>
<style>
body {
width: 480px;
margin: 60px auto;
font-family: sans-serif;
text-align: justify;
color: #6B6B6B;
}
</style>
</head>
<body>
<h1>Your PosBox is up and running</h1>
<p>
The PosBox is an hardware adapter that allows you to use
receipt printers and barcode scanners with OpenERP's Point of
Sale, <b>version 8.0 or later</b>. You can start an <a href='https://www.openerp.com/start'>online free trial</a>,
or <a href='https://www.openerp.com/start?download'>download and install</a> it yourself.
</p>
<p>
For more information on how to setup the Point of Sale with
the PosBox, please refer to <a href='/hw_proxy/static/doc/manual.pdf'>the manual</a>
</p>
<p>
To see the status of the connected hardware, please refer
to the <a href='/hw_proxy/status'>hardware status page</a>
</p>
<p>
The PosBox software installed on this posbox is <b>version 6</b>,
the posbox version number is independent from OpenERP. You can upgrade
the software on the <a href='/hw_proxy/upgrade/'>upgrade page</a>
</p>
<p>For any other question, please contact the OpenERP support at <a href='mailto:support@openerp.com'>support@openerp.com</a>
</p>
</body>
</html>
"""
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

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import controllers
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'PosBox Software Upgrader',
'version': '1.0',
'category': 'Hardware Drivers',
'sequence': 6,
'summary': 'Allows to remotely upgrade the PosBox software',
'description': """
PosBox Software Upgrader
========================
This module allows to remotely upgrade the PosBox software to a
new version. This module is specific to the PosBox setup and environment
and should not be installed on regular openerp servers.
""",
'author': 'OpenERP SA',
'depends': ['hw_proxy'],
'test': [
],
'installable': False,
'auto_install': False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,3 @@
import main
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
import logging
import os
import time
import openerp
import openerp.addons.hw_proxy.controllers.main as hw_proxy
import threading
from openerp import http
from openerp.http import request
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
upgrade_template = """
<!DOCTYPE HTML>
<html>
<head>
<title>OpenERP's PosBox - Software Upgrade</title>
<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
<script>
$(function(){
var upgrading = false;
$('#upgrade').click(function(){
console.log('click');
if(!upgrading){
upgrading = true;
$('#upgrade').text('Upgrading, Please Wait');
$.ajax({
url:'/hw_proxy/perform_upgrade/'
}).then(function(status){
$('#upgrade').html('Upgrade Successful<br \\>Click to Restart the PosBox');
$('#upgrade').off('click');
$('#upgrade').click(function(){
$.ajax({ url:'hw_proxy/perform_restart' })
$('#upgrade').text('Restarting');
$('#upgrade').off('click');
setTimeout(function(){
window.location = '/'
},30*1000);
});
},function(){
$('#upgrade').text('Upgrade Failed');
});
}
});
});
</script>
<style>
body {
width: 480px;
margin: 60px auto;
font-family: sans-serif;
text-align: justify;
color: #6B6B6B;
}
.centering{
text-align: center;
}
#upgrade {
padding: 20px;
background: rgb(121, 197, 107);
color: white;
border-radius: 3px;
text-align: center;
margin: 30px;
text-decoration: none;
display: inline-block;
}
</style>
</head>
<body>
<h1>PosBox Software Upgrade</h1>
<p>
This tool will help you perform an upgrade of the PosBox's software.
However the preferred method to upgrade the posbox is to flash the sd-card with
the <a href='http://nightly.openerp.com/trunk/posbox/'>latest image</a>. The upgrade
procedure is explained into to the <a href='/hw_proxy/static/doc/manual.pdf'>PosBox manual</a>
</p>
<p>
To upgrade the posbox, click on the upgrade button. The upgrade will take a few minutes. <b>Do not reboot</b> the PosBox during the upgrade.
</p>
<div class='centering'>
<a href='#' id='upgrade'>Upgrade</a>
</div>
</body>
</html>
"""
class PosboxUpgrader(hw_proxy.Proxy):
def __init__(self):
super(PosboxUpgrader,self).__init__()
self.upgrading = threading.Lock()
self.last_upgrade = 0
@http.route('/hw_proxy/upgrade', type='http', auth='none', )
def upgrade(self):
return upgrade_template
@http.route('/hw_proxy/perform_upgrade', type='http', auth='none')
def perform_upgrade(self):
self.upgrading.acquire()
if time.time() - self.last_upgrade < 30:
self.upgrading.release()
return 'UPTODATE'
else:
os.system('/bin/bash /home/pi/openerp/update.sh')
self.last_upgrade = time.time()
self.upgrading.release()
return 'SUCCESS'
@http.route('/hw_proxy/perform_restart', type='http', auth='none')
def perform_restart(self):
self.upgrading.acquire()
if time.time() - self.last_upgrade < 30:
self.upgrading.release()
return 'RESTARTED'
else:
os.system('/bin/bash /home/pi/openerp/restart.sh')
self.last_upgrade = time.time()
self.upgrading.release()
return 'SUCCESS'

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import logging
import commands
import simplejson
import os
import os.path
@ -43,7 +44,32 @@ class Proxy(http.Controller):
@http.route('/hw_proxy/status', type='http', auth='none', cors='*')
def status_http(self):
resp = '<html>\n<body>\n<h1>Hardware Proxy Status</h1>\n'
resp = """
<!DOCTYPE HTML>
<html>
<head>
<title>OpenERP's PosBox</title>
<style>
body {
width: 480px;
margin: 60px auto;
font-family: sans-serif;
text-align: justify;
color: #6B6B6B;
}
.device {
border-bottom: solid 1px rgb(216,216,216);
padding: 9px;
}
.device:nth-child(2n) {
background:rgb(240,240,240);
}
</style>
</head>
<body>
<h1>Hardware Status</h1>
<p>The list of enabled drivers and their status</p>
"""
statuses = self.get_status()
for driver in statuses:
@ -56,12 +82,22 @@ class Proxy(http.Controller):
else:
color = 'red'
resp += "<h2 style='color:"+color+";'>"+driver+' : '+status['status']+"</h2>\n"
resp += "<h3 style='color:"+color+";'>"+driver+' : '+status['status']+"</h3>\n"
resp += "<ul>\n"
for msg in status['messages']:
resp += '<li>'+msg+'</li>\n'
resp += "</ul>\n"
resp += "<script>\n\tsetTimeout(function(){window.location.reload();},30000);\n</script>\n</body>\n</html>\n\n"
resp += """
<h2>Connected Devices</h2>
<p>The list of connected USB devices as seen by the posbox</p>
"""
devices = commands.getoutput("lsusb").split('\n')
resp += "<div class='devices'>\n"
for device in devices:
device_name = device[device.find('ID')+2:]
resp+= "<div class='device' data-device='"+device+"'>"+device_name+"</div>\n"
resp += "</div>\n"
resp += "</body>\n</html>\n\n"
return request.make_response(resp,{
'Cache-Control': 'no-cache',

Binary file not shown.

View File

@ -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(){

View File

@ -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,

View File

@ -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);

View File

@ -319,6 +319,155 @@
</div>
</t>
<t t-name="XmlReceipt">
<receipt align='center' width='40' value-thousands-separator='' >
<t t-if='receipt.company.logo'>
<img t-att-src='receipt.company.logo' />
<br/>
</t>
<t t-if='!receipt.company.logo'>
<h1><t t-esc='receipt.company.name' /></h1>
<br/>
</t>
<div font='b'>
<t t-if='receipt.shop.name'>
<div><t t-esc='receipt.shop.name' /></div>
</t>
<t t-if='receipt.company.contact_address'>
<div><t t-esc='receipt.company.contact_address' /></div>
</t>
<t t-if='receipt.company.phone'>
<div>Tel:<t t-esc='receipt.company.phone' /></div>
</t>
<t t-if='receipt.company.vat'>
<div>VAT:<t t-esc='receipt.company.vat' /></div>
</t>
<t t-if='receipt.company.email'>
<div><t t-esc='receipt.company.email' /></div>
</t>
<t t-if='receipt.company.website'>
<div><t t-esc='receipt.company.website' /></div>
</t>
<t t-if='receipt.header'>
<div><t t-esc='receipt.header' /></div>
</t>
<t t-if='receipt.cashier'>
<div>--------------------------------</div>
<div>Served by <t t-esc='receipt.cashier' /></div>
</t>
</div>
<br /><br />
<!-- Orderlines -->
<div line-ratio='0.6'>
<t t-foreach='receipt.orderlines' t-as='line'>
<t t-set='simple' t-value='line.discount === 0 and line.unit_name === "Unit(s)" and line.quantity === 1' />
<t t-if='simple'>
<line>
<left><t t-esc='line.product_name' /></left>
<right><value><t t-esc='line.price_display' /></value></right>
</line>
</t>
<t t-if='!simple'>
<line><left><t t-esc='line.product_name' /></left></line>
<t t-if='line.discount !== 0'>
<line indent='1'><left>Discount: <t t-esc='line.discount' />%</left></line>
</t>
<line indent='1'>
<left>
<value value-decimals='3' autoint='on'>
<t t-esc='line.quantity' />
</value>
<t t-if='line.unit_name !== "Unit(s)"'>
<t t-esc='line.unit_name' />
</t>
x
<value value-decimals='2'>
<t t-esc='line.price' />
</value>
</left>
<right>
<value><t t-esc='line.price_display' /></value>
</right>
</line>
</t>
</t>
</div>
<!-- Subtotal -->
<t t-set='taxincluded' t-value='Math.abs(receipt.subtotal - receipt.total_with_tax) &lt;= 0.000001' />
<t t-if='!taxincluded'>
<line><right>--------</right></line>
<line><left>Subtotal</left><right> <value>receipt.subtotal</value></right></line>
<t t-foreach='receipt.tax_details' t-as='tax'>
<line>
<left><t t-esc='tax.name' /></left>
<right><value><t t-esc='tax.amount' /></value></right>
</line>
</t>
</t>
<!-- Total -->
<line><right>-------</right></line>
<line size='double-height'>
<left><pre> TOTAL</pre></left>
<right><value><t t-esc='receipt.total_with_tax' /></value></right>
</line>
<br/><br/>
<!-- Payment Lines -->
<t t-foreach='receipt.paymentlines' t-as='line'>
<line>
<left><t t-esc='line.journal' /></left>
<right><value><t t-esc='line.amount'/></value></right>
</line>
</t>
<br/>
<line size='double-height'>
<left><pre> CHANGE</pre></left>
<right><value><t t-esc='receipt.change' /></value></right>
</line>
<br/>
<!-- Extra Payment Info -->
<t t-if='receipt.total_discount'>
<line>
<left>Discounts</left>
<right><value><t t-esc='receipt.total_discount'/></value></right>
</line>
</t>
<t t-if='taxincluded'>
<t t-foreach='receipt.tax_details' t-as='tax'>
<line>
<left><t t-esc='tax.name' /></left>
<right><value><t t-esc='tax.amount' /></value></right>
</line>
</t>
</t>
<!-- Footer -->
<t t-if='receipt.footer'>
<br/>
<pre><t t-esc='receipt.footer' /></pre>
<br/>
<br/>
</t>
<br/>
<div font='b'>
<div><t t-esc='receipt.name' /></div>
<div><t t-esc='receipt.date.localestring' /></div>
</div>
</receipt>
</t>
<t t-name="WelcomeScreenWidget">
<div class="welcome-screen screen">
<header class='rightpane-header'><h2>Welcome</h2></header>