[IMP] hw_escpos: basic support for japanese receipt and open cashdrawer
bzr revid: fva@openerp.com-20131226165620-6262zfqyxxyryxo0
This commit is contained in:
parent
5a8040d909
commit
bf650fa07a
|
@ -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) )
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 """
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Reference in New Issue