[IMP] hw_proxy, hw_escpos: it is now possible to add support for new printers from...
[odoo/odoo.git] / addons / hw_escpos / controllers / main.py
1 # -*- coding: utf-8 -*-
2 import commands
3 import logging
4 import simplejson
5 import os
6 import os.path
7 import io
8 import base64
9 import openerp
10 import time
11 import random
12 import math
13 import md5
14 import openerp.addons.hw_proxy.controllers.main as hw_proxy
15 import pickle
16 import re
17 import subprocess
18 import traceback
19 from threading import Thread, Lock
20 from Queue import Queue, Empty
21
22 try:
23     import usb.core
24 except ImportError:
25     usb = None
26
27 try:
28     from .. import escpos
29     from ..escpos import printer
30     from ..escpos import supported_devices
31 except ImportError:
32     escpos = printer = None
33
34 from PIL import Image
35
36 from openerp import http
37 from openerp.http import request
38 from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template
39 from openerp.tools.translate import _
40
41 _logger = logging.getLogger(__name__)
42
43
44 class EscposDriver(Thread):
45     def __init__(self):
46         Thread.__init__(self)
47         self.queue = Queue()
48         self.lock  = Lock()
49         self.status = {'status':'connecting', 'messages':[]}
50
51     def supported_devices(self):
52         if not os.path.isfile('escpos_devices.pickle'):
53             return supported_devices.device_list
54         else:
55             try:
56                 f = open('escpos_devices.pickle','r')
57                 return pickle.load(f)
58                 f.close()
59             except Exception as e:
60                 self.set_status('error',str(e))
61                 return supported_devices.device_list
62
63     def add_supported_device(self,device_string):
64         r = re.compile('[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}');
65         match = r.search(device_string)
66         if match:
67             match = match.group().split(':')
68             vendor = int(match[0],16)
69             product = int(match[1],16)
70             name = device_string.split('ID')
71             if len(name) >= 2:
72                 name = name[1]
73             else:
74                 name = name[0]
75             _logger.info('ESC/POS: adding support for device: '+match[0]+':'+match[1]+' '+name)
76             
77             device_list = supported_devices.device_list[:]
78             if os.path.isfile('escpos_devices.pickle'):
79                 try:
80                     f = open('escpos_devices.pickle','r')
81                     device_list = pickle.load(f)
82                     f.close()
83                 except Exception as e:
84                     self.set_status('error',str(e))
85             device_list.append({
86                 'vendor': vendor,
87                 'product': product,
88                 'name': name,
89             })
90
91             try:
92                 f = open('escpos_devices.pickle','w+')
93                 f.seek(0)
94                 pickle.dump(device_list,f)
95                 f.close()
96             except Exception as e:
97                 self.set_status('error',str(e))
98
99     def connected_usb_devices(self):
100         connected = []
101         
102         for device in self.supported_devices():
103             if usb.core.find(idVendor=device['vendor'], idProduct=device['product']) != None:
104                 connected.append(device)
105         return connected
106
107     def lockedstart(self):
108         with self.lock:
109             if not self.isAlive():
110                 self.daemon = True
111                 self.start()
112     
113     def get_escpos_printer(self):
114         try:
115             printers = self.connected_usb_devices()
116             if len(printers) > 0:
117                 self.set_status('connected','Connected to '+printers[0]['name'])
118                 return escpos.printer.Usb(printers[0]['vendor'], printers[0]['product'])
119             else:
120                 self.set_status('disconnected','Printer Not Found')
121                 return None
122         except Exception as e:
123             self.set_status('error',str(e))
124             return None
125
126     def get_status(self):
127         self.push_task('status')
128         return self.status
129
130
131
132     def open_cashbox(self,printer):
133         printer.cashdraw(2)
134         printer.cashdraw(5)
135
136     def set_status(self, status, message = None):
137         _logger.info(status+' : '+ (message or 'no message'))
138         if status == self.status['status']:
139             if message != None and (len(self.status['messages']) == 0 or message != self.status['messages'][-1]):
140                 self.status['messages'].append(message)
141         else:
142             self.status['status'] = status
143             if message:
144                 self.status['messages'] = [message]
145             else:
146                 self.status['messages'] = []
147
148         if status == 'error' and message:
149             _logger.error('ESC/POS Error: '+message)
150         elif status == 'disconnected' and message:
151             _logger.warning('ESC/POS Device Disconnected: '+message)
152
153     def run(self):
154         if not escpos:
155             _logger.error('ESC/POS cannot initialize, please verify system dependencies.')
156             return
157         while True:
158             try:
159                 timestamp, task, data = self.queue.get(True)
160
161                 printer = self.get_escpos_printer()
162
163                 if printer == None:
164                     if task != 'status':
165                         self.queue.put((timestamp,task,data))
166                     time.sleep(5)
167                     continue
168                 elif task == 'receipt': 
169                     if timestamp >= time.time() - 1 * 60 * 60:
170                         self.print_receipt_body(printer,data)
171                         printer.cut()
172                 elif task == 'xml_receipt':
173                     if timestamp >= time.time() - 1 * 60 * 60:
174                         printer.receipt(data)
175                 elif task == 'cashbox':
176                     if timestamp >= time.time() - 12:
177                         self.open_cashbox(printer)
178                 elif task == 'printstatus':
179                     self.print_status(printer)
180                 elif task == 'status':
181                     pass
182
183             except Exception as e:
184                 self.set_status('error', str(e))
185                 errmsg = str(e) + '\n' + '-'*60+'\n' + traceback.format_exc() + '-'*60 + '\n'
186                 _logger.error(errmsg);
187
188     def push_task(self,task, data = None):
189         self.lockedstart()
190         self.queue.put((time.time(),task,data))
191
192     def print_status(self,eprint):
193         localips = ['0.0.0.0','127.0.0.1','127.0.1.1']
194         ips =  [ c.split(':')[1].split(' ')[0] for c in commands.getoutput("/sbin/ifconfig").split('\n') if 'inet addr' in c ]
195         ips =  [ ip for ip in ips if ip not in localips ] 
196         eprint.text('\n\n')
197         eprint.set(align='center',type='b',height=2,width=2)
198         eprint.text('PosBox Status\n')
199         eprint.text('\n')
200         eprint.set(align='center')
201
202         if len(ips) == 0:
203             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')
204         elif len(ips) == 1:
205             eprint.text('IP Address:\n'+ips[0]+'\n')
206         else:
207             eprint.text('IP Addresses:\n')
208             for ip in ips:
209                 eprint.text(ip+'\n')
210
211         if len(ips) >= 1:
212             eprint.text('\nHomepage:\nhttp://'+ips[0]+':8069\n')
213
214         eprint.text('\n\n')
215         eprint.cut()
216
217     def print_receipt_body(self,eprint,receipt):
218
219         def check(string):
220             return string != True and bool(string) and string.strip()
221         
222         def price(amount):
223             return ("{0:."+str(receipt['precision']['price'])+"f}").format(amount)
224         
225         def money(amount):
226             return ("{0:."+str(receipt['precision']['money'])+"f}").format(amount)
227
228         def quantity(amount):
229             if math.floor(amount) != amount:
230                 return ("{0:."+str(receipt['precision']['quantity'])+"f}").format(amount)
231             else:
232                 return str(amount)
233
234         def printline(left, right='', width=40, ratio=0.5, indent=0):
235             lwidth = int(width * ratio) 
236             rwidth = width - lwidth 
237             lwidth = lwidth - indent
238             
239             left = left[:lwidth]
240             if len(left) != lwidth:
241                 left = left + ' ' * (lwidth - len(left))
242
243             right = right[-rwidth:]
244             if len(right) != rwidth:
245                 right = ' ' * (rwidth - len(right)) + right
246
247             return ' ' * indent + left + right + '\n'
248         
249         def print_taxes():
250             taxes = receipt['tax_details']
251             for tax in taxes:
252                 eprint.text(printline(tax['tax']['name'],price(tax['amount']), width=40,ratio=0.6))
253
254         # Receipt Header
255         if receipt['company']['logo']:
256             eprint.set(align='center')
257             eprint.print_base64_image(receipt['company']['logo'])
258             eprint.text('\n')
259         else:
260             eprint.set(align='center',type='b',height=2,width=2)
261             eprint.text(receipt['company']['name'] + '\n')
262
263         eprint.set(align='center',type='b')
264         if check(receipt['shop']['name']):
265             eprint.text(receipt['shop']['name'] + '\n')
266         if check(receipt['company']['contact_address']):
267             eprint.text(receipt['company']['contact_address'] + '\n')
268         if check(receipt['company']['phone']):
269             eprint.text('Tel:' + receipt['company']['phone'] + '\n')
270         if check(receipt['company']['vat']):
271             eprint.text('VAT:' + receipt['company']['vat'] + '\n')
272         if check(receipt['company']['email']):
273             eprint.text(receipt['company']['email'] + '\n')
274         if check(receipt['company']['website']):
275             eprint.text(receipt['company']['website'] + '\n')
276         if check(receipt['header']):
277             eprint.text(receipt['header']+'\n')
278         if check(receipt['cashier']):
279             eprint.text('-'*32+'\n')
280             eprint.text('Served by '+receipt['cashier']+'\n')
281
282         # Orderlines
283         eprint.text('\n\n')
284         eprint.set(align='center')
285         for line in receipt['orderlines']:
286             pricestr = price(line['price_display'])
287             if line['discount'] == 0 and line['unit_name'] == 'Unit(s)' and line['quantity'] == 1:
288                 eprint.text(printline(line['product_name'],pricestr,ratio=0.6))
289             else:
290                 eprint.text(printline(line['product_name'],ratio=0.6))
291                 if line['discount'] != 0:
292                     eprint.text(printline('Discount: '+str(line['discount'])+'%', ratio=0.6, indent=2))
293                 if line['unit_name'] == 'Unit(s)':
294                     eprint.text( printline( quantity(line['quantity']) + ' x ' + price(line['price']), pricestr, ratio=0.6, indent=2))
295                 else:
296                     eprint.text( printline( quantity(line['quantity']) + line['unit_name'] + ' x ' + price(line['price']), pricestr, ratio=0.6, indent=2))
297
298         # Subtotal if the taxes are not included
299         taxincluded = True
300         if money(receipt['subtotal']) != money(receipt['total_with_tax']):
301             eprint.text(printline('','-------'));
302             eprint.text(printline(_('Subtotal'),money(receipt['subtotal']),width=40, ratio=0.6))
303             print_taxes()
304             #eprint.text(printline(_('Taxes'),money(receipt['total_tax']),width=40, ratio=0.6))
305             taxincluded = False
306
307
308         # Total
309         eprint.text(printline('','-------'));
310         eprint.set(align='center',height=2)
311         eprint.text(printline(_('         TOTAL'),money(receipt['total_with_tax']),width=40, ratio=0.6))
312         eprint.text('\n\n');
313         
314         # Paymentlines
315         eprint.set(align='center')
316         for line in receipt['paymentlines']:
317             eprint.text(printline(line['journal'], money(line['amount']), ratio=0.6))
318
319         eprint.text('\n');
320         eprint.set(align='center',height=2)
321         eprint.text(printline(_('        CHANGE'),money(receipt['change']),width=40, ratio=0.6))
322         eprint.set(align='center')
323         eprint.text('\n');
324
325         # Extra Payment info
326         if receipt['total_discount'] != 0:
327             eprint.text(printline(_('Discounts'),money(receipt['total_discount']),width=40, ratio=0.6))
328         if taxincluded:
329             print_taxes()
330             #eprint.text(printline(_('Taxes'),money(receipt['total_tax']),width=40, ratio=0.6))
331
332         # Footer
333         if check(receipt['footer']):
334             eprint.text('\n'+receipt['footer']+'\n\n')
335         eprint.text(receipt['name']+'\n')
336         eprint.text(      str(receipt['date']['date']).zfill(2)
337                     +'/'+ str(receipt['date']['month']+1).zfill(2)
338                     +'/'+ str(receipt['date']['year']).zfill(4)
339                     +' '+ str(receipt['date']['hour']).zfill(2)
340                     +':'+ str(receipt['date']['minute']).zfill(2) )
341
342
343 driver = EscposDriver()
344
345 driver.push_task('printstatus')
346
347 hw_proxy.drivers['escpos'] = driver
348
349 class EscposProxy(hw_proxy.Proxy):
350     
351     @http.route('/hw_proxy/open_cashbox', type='json', auth='none', cors='*')
352     def open_cashbox(self):
353         _logger.info('ESC/POS: OPEN CASHBOX') 
354         driver.push_task('cashbox')
355         
356     @http.route('/hw_proxy/print_receipt', type='json', auth='none', cors='*')
357     def print_receipt(self, receipt):
358         _logger.info('ESC/POS: PRINT RECEIPT') 
359         driver.push_task('receipt',receipt)
360
361     @http.route('/hw_proxy/print_xml_receipt', type='json', auth='none', cors='*')
362     def print_xml_receipt(self, receipt):
363         _logger.info('ESC/POS: PRINT XML RECEIPT') 
364         driver.push_task('xml_receipt',receipt)
365
366     @http.route('/hw_proxy/escpos/add_supported_device', type='http', auth='none', cors='*')
367     def add_supported_device(self, device_string):
368         _logger.info('ESC/POS: ADDED NEW DEVICE:'+device_string) 
369         driver.add_supported_device(device_string)
370         return "The device:\n"+device_string+"\n has been added to the list of supported devices.<br/><a href='/hw_proxy/status'>Ok</a>"
371
372     @http.route('/hw_proxy/escpos/reset_supported_devices', type='http', auth='none', cors='*')
373     def reset_supported_devices(self):
374         try:
375             os.remove('escpos_devices.pickle')
376         except Exception as e:
377             pass
378         return 'The list of supported devices has been reset to factory defaults.<br/><a href="/hw_proxy/status">Ok</a>'
379
380