[IMP] hw_escpos: basic support for japanese receipt and open cashdrawer

bzr revid: fva@openerp.com-20131226165620-6262zfqyxxyryxo0
This commit is contained in:
Frédéric van der Essen 2013-12-26 17:56:20 +01:00
parent 5a8040d909
commit bf650fa07a
4 changed files with 248 additions and 101 deletions

View File

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

View File

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

View File

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

View File

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