From bf650fa07a723a9733bad0f96678e5d5842d08e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20van=20der=20Essen?= Date: Thu, 26 Dec 2013 17:56:20 +0100 Subject: [PATCH] [IMP] hw_escpos: basic support for japanese receipt and open cashdrawer bzr revid: fva@openerp.com-20131226165620-6262zfqyxxyryxo0 --- addons/hw_escpos/controllers/main.py | 192 ++++++++++--------- addons/hw_escpos/escpos/constants.py | 74 ++++++- addons/hw_escpos/escpos/escpos.py | 78 ++++++-- addons/point_of_sale/static/src/js/models.js | 5 + 4 files changed, 248 insertions(+), 101 deletions(-) diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index 00d8f7a2195..5cb77720994 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -7,6 +7,7 @@ import base64 import openerp import time import random +import math import openerp.addons.hw_proxy.controllers.main as hw_proxy import subprocess import usb.core @@ -28,21 +29,46 @@ class EscposDriver(hw_proxy.Proxy): { 'vendor' : 0x04b8, 'product' : 0x0e03, 'name' : 'Epson TM-T20' } ] + def get_escpos_printer(self): + printers = self.connected_usb_devices(self.supported_printers) + if len(printers) > 0: + return escpos.printer.Usb(printers[0]['vendor'], printers[0]['product']) + else: + return None + @http.route('/hw_proxy/open_cashbox', type='json', auth='admin') def open_cashbox(self): print 'ESC/POS: OPEN CASHBOX' - + eprint = self.get_escpos_printer() + if eprint != None: + eprint.cashdraw(2) + eprint.cashdraw(5) + @http.route('/hw_proxy/print_receipt', type='json', auth='admin') def print_receipt(self, receipt): - print 'ESC/POS: PRINT RECEIPT ' + str(receipt) - - printers = self.connected_usb_devices(self.supported_printers) + print 'ESC/POS: PRINT RECEIPT' + eprint = self.get_escpos_printer() + if eprint != None: + self.print_receipt_body(eprint,receipt) + eprint.cut() + + def print_receipt_body(self,eprint,receipt): def check(string): return string != True and bool(string) and string.strip() def price(amount): - return "{0:.2f}".format(amount) + return ("{0:."+str(receipt['precision']['price'])+"f}").format(amount) + + def money(amount): + return ("{0:."+str(receipt['precision']['money'])+"f}").format(amount) + + def quantity(amount): + if math.floor(amount) != amount: + return ("{0:."+str(receipt['precision']['quantity'])+"f}").format(amount) + else: + return str(amount) + def printline(left, right='', width=40, ratio=0.5, indent=0): lwidth = int(width * ratio) @@ -61,7 +87,6 @@ class EscposDriver(hw_proxy.Proxy): logo = None - if receipt['company']['logo']: img = receipt['company']['logo'] img = img[img.find(',')+1:] @@ -76,92 +101,85 @@ class EscposDriver(hw_proxy.Proxy): height = int(logo_rgba.size[1]*wfac) logo = logo.resize((width,height), Image.ANTIALIAS) - if len(printers) > 0: - printer = printers[0] + # Receipt Header + if logo: + eprint._convert_image(logo) + eprint.text('\n') + else: + eprint.set(align='center',type='b',height=2,width=2) + eprint.text(receipt['company']['name'] + '\n') - eprint = escpos.printer.Usb(printer['vendor'], printer['product']) - - # Receipt Header - if logo: - eprint._convert_image(logo) - eprint.text('\n') + eprint.set(align='center',type='b') + if check(receipt['shop']['name']): + eprint.text(receipt['shop']['name'] + '\n') + if check(receipt['company']['contact_address']): + eprint.text(receipt['company']['contact address'] + '\n') + if check(receipt['company']['phone']): + eprint.text('Tel:' + receipt['company']['phone'] + '\n') + if check(receipt['company']['vat']): + eprint.text('VAT:' + receipt['company']['vat'] + '\n') + if check(receipt['company']['email']): + eprint.text(receipt['company']['email'] + '\n') + if check(receipt['company']['website']): + eprint.text(receipt['company']['website'] + '\n') + + if check(receipt['cashier']): + eprint.text('-'*32+'\n') + eprint.text('Served by '+receipt['cashier']+'\n') + + # Orderlines + eprint.text('\n\n') + eprint.set(align='center') + for line in receipt['orderlines']: + pricestr = price(line['price_display']) + if line['discount'] == 0 and line['unit_name'] == 'Unit(s)' and line['quantity'] == 1: + eprint.text(printline(line['product_name'],pricestr,ratio=0.6)) else: - eprint.set(align='center',type='b',height=2,width=2) - eprint.text(receipt['company']['name'] + '\n') - - eprint.set(align='center',type='b') - if check(receipt['shop']['name']): - eprint.text(receipt['shop']['name'] + '\n') - if check(receipt['company']['contact_address']): - eprint.text(receipt['company']['contact address'] + '\n') - if check(receipt['company']['phone']): - eprint.text('Tel:' + receipt['company']['phone'] + '\n') - if check(receipt['company']['vat']): - eprint.text('VAT:' + receipt['company']['vat'] + '\n') - if check(receipt['company']['email']): - eprint.text(receipt['company']['email'] + '\n') - if check(receipt['company']['website']): - eprint.text(receipt['company']['website'] + '\n') - - if check(receipt['cashier']): - eprint.text('-'*32+'\n') - eprint.text('Served by '+receipt['cashier']+'\n') - - # Orderlines - eprint.text('\n\n') - eprint.set(align='center') - for line in receipt['orderlines']: - pricestr = price(line['price_display']) - if line['discount'] == 0 and line['unit_name'] == 'Unit(s)' and line['quantity'] == 1: - eprint.text(printline(line['product_name'],pricestr,ratio=0.6)) + eprint.text(printline(line['product_name'],ratio=0.6)) + if line['discount'] != 0: + eprint.text(printline('Discount: '+str(line['discount'])+'%', ratio=0.6, indent=2)) + if line['unit_name'] == 'Unit(s)': + eprint.text( printline( quantity(line['quantity']) + ' x ' + price(line['price']), pricestr, ratio=0.6, indent=2)) else: - eprint.text(printline(line['product_name'],ratio=0.6)) - if line['discount'] != 0: - eprint.text(printline('Discount: '+str(line['discount'])+'%', ratio=0.6, indent=2)) - if line['unit_name'] == 'Unit(s)': - eprint.text( printline( str(line['quantity']) + ' x ' + price(line['price']), pricestr, ratio=0.6, indent=2)) - else: - eprint.text( printline( str(line['quantity']) + line['unit_name'] + ' x ' + price(line['price']), pricestr, ratio=0.6, indent=2)) + eprint.text( printline( quantity(line['quantity']) + line['unit_name'] + ' x ' + price(line['price']), pricestr, ratio=0.6, indent=2)) - # Subtotal if the taxes are not included - taxincluded = True - if price(receipt['subtotal']) != price(receipt['total_with_tax']): - eprint.text(printline('','-------')); - eprint.text(printline('Subtotal',price(receipt['subtotal']),width=40, ratio=0.6)) - eprint.text(printline('Taxes',price(receipt['total_tax']),width=40, ratio=0.6)) - taxincluded = False - - - # Total + # Subtotal if the taxes are not included + taxincluded = True + if money(receipt['subtotal']) != money(receipt['total_with_tax']): eprint.text(printline('','-------')); - eprint.set(align='center',height=2) - eprint.text(printline(' TOTAL',price(receipt['total_with_tax']),width=40, ratio=0.6)) - eprint.text('\n\n'); - - # Paymentlines - eprint.set(align='center') - for line in receipt['paymentlines']: - eprint.text(printline(line['journal'], price(line['amount']), ratio=0.6)) + eprint.text(printline('Subtotal',money(receipt['subtotal']),width=40, ratio=0.6)) + eprint.text(printline('Taxes',money(receipt['total_tax']),width=40, ratio=0.6)) + taxincluded = False - eprint.text('\n'); - eprint.set(align='center',height=2) - eprint.text(printline(' CHANGE',price(receipt['change']),width=40, ratio=0.6)) - eprint.set(align='center') - eprint.text('\n'); - # Extra Payment info - if receipt['total_discount'] != 0: - eprint.text(printline('Discounts',price(receipt['total_discount']),width=40, ratio=0.6)) - if taxincluded: - eprint.text(printline('Taxes',price(receipt['total_tax']),width=40, ratio=0.6)) - - # Footer - eprint.text(receipt['name']+'\n') - eprint.text( str(receipt['date']['date']).zfill(2) - +'/'+ str(receipt['date']['month']+1).zfill(2) - +'/'+ str(receipt['date']['year']).zfill(4) - +' '+ str(receipt['date']['hour']).zfill(2) - +':'+ str(receipt['date']['minute']).zfill(2) ) - eprint.cut() - return + # Total + eprint.text(printline('','-------')); + eprint.set(align='center',height=2) + eprint.text(printline(' TOTAL',money(receipt['total_with_tax']),width=40, ratio=0.6)) + eprint.text('\n\n'); + + # Paymentlines + eprint.set(align='center') + for line in receipt['paymentlines']: + eprint.text(printline(line['journal'], money(line['amount']), ratio=0.6)) + + eprint.text('\n'); + eprint.set(align='center',height=2) + eprint.text(printline(' CHANGE',money(receipt['change']),width=40, ratio=0.6)) + eprint.set(align='center') + eprint.text('\n'); + + # Extra Payment info + if receipt['total_discount'] != 0: + eprint.text(printline('Discounts',money(receipt['total_discount']),width=40, ratio=0.6)) + if taxincluded: + eprint.text(printline('Taxes',money(receipt['total_tax']),width=40, ratio=0.6)) + + # Footer + eprint.text(receipt['name']+'\n') + eprint.text( str(receipt['date']['date']).zfill(2) + +'/'+ str(receipt['date']['month']+1).zfill(2) + +'/'+ str(receipt['date']['year']).zfill(4) + +' '+ str(receipt['date']['hour']).zfill(2) + +':'+ str(receipt['date']['minute']).zfill(2) ) diff --git a/addons/hw_escpos/escpos/constants.py b/addons/hw_escpos/escpos/constants.py index 0be873f8284..ebffc62729e 100644 --- a/addons/hw_escpos/escpos/constants.py +++ b/addons/hw_escpos/escpos/constants.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + """ ESC/POS Commands (Constants) """ # Feed control sequences @@ -30,6 +32,7 @@ TXT_FONT_B = '\x1b\x4d\x01' # Font type B TXT_ALIGN_LT = '\x1b\x61\x00' # Left justification TXT_ALIGN_CT = '\x1b\x61\x01' # Centering TXT_ALIGN_RT = '\x1b\x61\x02' # Right justification + # Text Encoding TXT_ENC_PC437 = '\x1b\x74\x00' # PC437 USA @@ -46,9 +49,78 @@ TXT_ENC_PC866 = '\x1b\x74\x11' # PC866 Cyrillic #2 TXT_ENC_PC852 = '\x1b\x74\x12' # PC852 Latin2 TXT_ENC_PC858 = '\x1b\x74\x13' # PC858 Euro +TXT_ENC_KATAKANA_MAP = { + # Maps UTF-8 Katakana symbols to KATAKANA Page Codes + # Half-Width Katakanas + '\xef\xbd\xa1':'\xa1', # 。 + '\xef\xbd\xa2':'\xa2', # 「 + '\xef\xbd\xa3':'\xa3', # 」 + '\xef\xbd\xa4':'\xa4', # 、 + '\xef\xbd\xa5':'\xa5', # ・ -# Barcode format + '\xef\xbd\xa6':'\xa6', # ヲ + '\xef\xbd\xa7':'\xa7', # ァ + '\xef\xbd\xa8':'\xa8', # ィ + '\xef\xbd\xa9':'\xa9', # ゥ + '\xef\xbd\xaa':'\xaa', # ェ + '\xef\xbd\xab':'\xab', # ォ + '\xef\xbd\xac':'\xac', # ャ + '\xef\xbd\xad':'\xad', # ュ + '\xef\xbd\xae':'\xae', # ョ + '\xef\xbd\xaf':'\xaf', # ッ + '\xef\xbd\xb0':'\xb0', # ー + '\xef\xbd\xb1':'\xb1', # ア + '\xef\xbd\xb2':'\xb2', # イ + '\xef\xbd\xb3':'\xb3', # ウ + '\xef\xbd\xb4':'\xb4', # エ + '\xef\xbd\xb5':'\xb5', # オ + '\xef\xbd\xb6':'\xb6', # カ + '\xef\xbd\xb7':'\xb7', # キ + '\xef\xbd\xb8':'\xb8', # ク + '\xef\xbd\xb9':'\xb9', # ケ + '\xef\xbd\xba':'\xba', # コ + '\xef\xbd\xbb':'\xbb', # サ + '\xef\xbd\xbc':'\xbc', # シ + '\xef\xbd\xbd':'\xbd', # ス + '\xef\xbd\xbe':'\xbe', # セ + '\xef\xbd\xbf':'\xbf', # ソ + '\xef\xbe\x80':'\xc0', # タ + '\xef\xbe\x81':'\xc1', # チ + '\xef\xbe\x82':'\xc2', # ツ + '\xef\xbe\x83':'\xc3', # テ + '\xef\xbe\x84':'\xc4', # ト + '\xef\xbe\x85':'\xc5', # ナ + '\xef\xbe\x86':'\xc6', # ニ + '\xef\xbe\x87':'\xc7', # ヌ + '\xef\xbe\x88':'\xc8', # ネ + '\xef\xbe\x89':'\xc9', # ノ + '\xef\xbe\x8a':'\xca', # ハ + '\xef\xbe\x8b':'\xcb', # ヒ + '\xef\xbe\x8c':'\xcc', # フ + '\xef\xbe\x8d':'\xcd', # ヘ + '\xef\xbe\x8e':'\xce', # ホ + '\xef\xbe\x8f':'\xcf', # マ + '\xef\xbe\x90':'\xd0', # ミ + '\xef\xbe\x91':'\xd1', # ム + '\xef\xbe\x92':'\xd2', # メ + '\xef\xbe\x93':'\xd3', # モ + '\xef\xbe\x94':'\xd4', # ヤ + '\xef\xbe\x95':'\xd5', # ユ + '\xef\xbe\x96':'\xd6', # ヨ + '\xef\xbe\x97':'\xd7', # ラ + '\xef\xbe\x98':'\xd8', # リ + '\xef\xbe\x99':'\xd9', # ル + '\xef\xbe\x9a':'\xda', # レ + '\xef\xbe\x9b':'\xdb', # ロ + '\xef\xbe\x9c':'\xdc', # ワ + '\xef\xbe\x9d':'\xdd', # ン + + '\xef\xbe\x9e':'\xde', # ゙ + '\xef\xbe\x9f':'\xdf', # ゚ +} + +# Barcod format BARCODE_TXT_OFF = '\x1d\x48\x00' # HRI barcode chars OFF BARCODE_TXT_ABV = '\x1d\x48\x01' # HRI barcode chars above BARCODE_TXT_BLW = '\x1d\x48\x02' # HRI barcode chars below diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py index a18faa1487f..9296568b4fc 100644 --- a/addons/hw_escpos/escpos/escpos.py +++ b/addons/hw_escpos/escpos/escpos.py @@ -11,6 +11,13 @@ import qrcode import time import copy +try: + import jcconv +except: + jcconv = None + print 'ESC/POS: please install jcconv for improved Japanese receipt printing:' + print ' # pip install jcconv' + from constants import * from exceptions import * @@ -174,6 +181,8 @@ class Escpos: def text(self,txt): """ Print Utf8 encoded alpha-numeric text """ + if not txt: + return try: txt = txt.decode('utf-8') except: @@ -182,9 +191,16 @@ class Escpos: except: pass - def encode_char(char): + self.extra_chars = 0 + + def encode_char(char): + """ + Encodes a single utf-8 character into a sequence of + esc-pos code page change instructions and character declarations + """ + char_utf8 = char.encode('utf-8') encoded = '' - encoding = self.encoding + encoding = self.encoding # we reuse the last encoding to prevent code page switches at every character encodings = { 'cp437': TXT_ENC_PC437, 'cp850': TXT_ENC_PC850, @@ -194,38 +210,74 @@ class Escpos: 'cp863': TXT_ENC_PC863, 'cp865': TXT_ENC_PC865, 'cp866': TXT_ENC_PC866, + 'katakana' : TXT_ENC_KATAKANA, + # TODO Support other encodings not natively supported by python } remaining = copy.copy(encodings) if not encoding : encoding = 'cp437' - while True: + while True: # Trying all encoding until one succeeds try: - encoded = char.encode(encoding) - break - except ValueError: + if encoding == 'katakana': # Japanese characters + if jcconv: + # try to convert japanese text to a half-katakanas + kata = jcconv.kata2half(jcconv.hira2kata(char_utf8)) + if kata != char_utf8: + self.extra_chars += len(kata.decode('utf-8')) - 1 + # the conversion may result in multiple characters + return encode_str(kata.decode('utf-8')) + else: + kata = char_utf8 + + if kata in TXT_ENC_KATAKANA_MAP: + encoded = TXT_ENC_KATAKANA_MAP[kata] + break + else: + raise ValueError() + else: + encoded = char.encode(encoding) + break + + except ValueError: #the encoding failed, select another one and retry if encoding in remaining: del remaining[encoding] if len(remaining) >= 1: encoding = remaining.items()[0][0] else: - print 'COULD NOT ENCODE:',char, char.__repr__() - encoded = '\xA8' # could not encode, output error character + encoding = 'cp437' + encoded = '\xb1' # could not encode, output error character break; if encoding != self.encoding: + # if the encoding changed, remember it and prefix the character with + # the esc-pos encoding change sequence self.encoding = encoding encoded = encodings[encoding] + encoded return encoded + + def encode_str(txt): + buffer = '' + for c in txt: + buffer += encode_char(c) + return buffer - buffer = '' - for char in txt: - buffer += encode_char(char) + txt = encode_str(txt) - if buffer: - self._raw(buffer) + # if the utf-8 -> codepage conversion inserted extra characters, + # remove double spaces to try to restore the original string length + # and prevent printing alignment issues + while self.extra_chars > 0: + dspace = txt.find(' ') + if dspace > 0: + txt = txt[:dspace] + txt[dspace+1:] + self.extra_chars -= 1 + else: + break + + self._raw(txt) def set(self, align='left', font='a', type='normal', width=1, height=1): """ Set text properties """ diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 97475215469..2d2965f3117 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -869,6 +869,11 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal client: client ? client.name : null , invoice_id: null, //TODO cashier: cashier ? cashier.name : null, + precision: { + price: 2, + money: 2, + quantity: 3, + }, date: { year: date.getFullYear(), month: date.getMonth(),