[REF] pos: Replace hw_escpos by PrinterDriver
ESCPOS printers are now supported as IoT Devices, just like any other printer. We now transform the receipts to JPGs, transform these JPGs into ESCPOS commands and send them via Cups, adding support for network printers. Since we now print images, we can now print special characters and therefore add support for other languages such as Arabic. The JPG is generated in the browser, with the help of html2canvas. `hw_escpos` is left untouched to allow backward compatibility and should be removed when v12 is no longer supported. In the PoS, all the code for printing has been moved from ProxyDevice to a separated JS file. TaskID: 1939455
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/python3
|
||||
import logging
|
||||
import time
|
||||
from threading import Thread, Event
|
||||
from threading import Thread, Event, Lock
|
||||
from usb import core
|
||||
from gatt import DeviceManager as Gatt_DeviceManager
|
||||
import subprocess
|
||||
@@ -353,7 +353,8 @@ class Manager(Thread):
|
||||
|
||||
def printer_loop(self):
|
||||
printer_devices = {}
|
||||
devices = conn.getDevices()
|
||||
with cups_lock:
|
||||
devices = conn.getDevices()
|
||||
for path in [printer_lo for printer_lo in devices if devices[printer_lo]['device-make-and-model'] != 'Unknown']:
|
||||
if 'uuid=' in path:
|
||||
serial = sub('[^a-zA-Z0-9 ]+', '', path.split('uuid=')[1])
|
||||
@@ -443,6 +444,7 @@ class SocketManager(Thread):
|
||||
conn = cups_connection()
|
||||
PPDs = conn.getPPDs()
|
||||
printers = conn.getPrinters()
|
||||
cups_lock = Lock() # We can only make one call to Cups at a time
|
||||
|
||||
m = Manager()
|
||||
m.daemon = True
|
||||
|
||||
@@ -2,55 +2,273 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from base64 import b64decode
|
||||
from cups import IPPError, IPP_PRINTER_IDLE, IPP_PRINTER_PROCESSING, IPP_PRINTER_STOPPED
|
||||
import dbus
|
||||
import logging
|
||||
import netifaces as ni
|
||||
import os
|
||||
import io
|
||||
import base64
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
from uuid import getnode as get_mac
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
from odoo import _
|
||||
from odoo.addons.hw_drivers.controllers.driver import Driver, PPDs, conn, printers
|
||||
from odoo import http, _
|
||||
from odoo.addons.hw_drivers.controllers.driver import event_manager, Driver, PPDs, conn, printers, cups_lock, iot_devices
|
||||
from odoo.addons.hw_proxy.controllers.main import drivers as old_drivers
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
def cups_notification_handler(message, uri, device_id, state, reason, accepting_jobs):
|
||||
if device_id in iot_devices:
|
||||
reason = reason if reason != 'none' else None
|
||||
state_value = {
|
||||
IPP_PRINTER_IDLE: 'connected',
|
||||
IPP_PRINTER_PROCESSING: 'processing',
|
||||
IPP_PRINTER_STOPPED: 'stopped'
|
||||
}
|
||||
iot_devices[device_id].update_status(state_value[state], message, reason)
|
||||
|
||||
# Create a Cups subscription if it doesn't exist yet
|
||||
try:
|
||||
conn.getSubscriptions('/printers/')
|
||||
except IPPError:
|
||||
conn.createSubscription(
|
||||
uri='/printers/',
|
||||
recipient_uri='dbus://',
|
||||
events=['printer-state-changed']
|
||||
)
|
||||
|
||||
# Listen for notifications from Cups
|
||||
bus = dbus.SystemBus()
|
||||
bus.add_signal_receiver(cups_notification_handler, signal_name="PrinterStateChanged", dbus_interface="org.cups.cupsd.Notifier")
|
||||
|
||||
|
||||
class PrinterDriver(Driver):
|
||||
connection_type = 'printer'
|
||||
status = {'status': "disconnected", 'printers': [], 'messages': []}
|
||||
|
||||
def __init__(self, device):
|
||||
super(PrinterDriver, self).__init__(device)
|
||||
self._device_type = 'printer'
|
||||
self._device_connection = self.dev['device-class'].lower()
|
||||
self._device_name = self.dev['device-make-and-model']
|
||||
self._device_identifier = self.dev['identifier']
|
||||
self.state = {
|
||||
'status': 'connecting',
|
||||
'message': 'Connecting to printer',
|
||||
'reason': None,
|
||||
}
|
||||
self.send_status()
|
||||
if 'direct' in self._device_connection:
|
||||
self.add_connected_printer()
|
||||
self.print_status()
|
||||
|
||||
@classmethod
|
||||
def supported(cls, device):
|
||||
protocol = ['dnssd', 'lpd']
|
||||
if any(x in device['url'] for x in protocol) or 'direct' in device['device-class']:
|
||||
model = cls.get_device_model(device)
|
||||
ppdFile = ''
|
||||
for device_id in [device_lo for device_lo in device['device-id'].split(';') if device['device-id']]:
|
||||
if any(x in device_id for x in ['MDL','MODEL']):
|
||||
device.update({'MDL': device_id.split(':')[1]})
|
||||
for ppd in PPDs:
|
||||
if device['MDL'] in PPDs[ppd]['ppd-product']:
|
||||
ppdFile = ppd
|
||||
if ppdFile:
|
||||
conn.addPrinter(name = device['identifier'], ppdname = ppdFile, device = device['url'])
|
||||
else:
|
||||
conn.addPrinter(name = device['identifier'], device = device['url'])
|
||||
if device['identifier'] not in printers:
|
||||
conn.setPrinterInfo(device['identifier'], device['device-make-and-model'])
|
||||
conn.enablePrinter(device['identifier'])
|
||||
conn.acceptJobs(device['identifier'])
|
||||
conn.setPrinterUsersAllowed(device['identifier'],['all'])
|
||||
else:
|
||||
device['device-make-and-model'] = printers[device['identifier']]['printer-info']
|
||||
for ppd in PPDs:
|
||||
if model and model in PPDs[ppd]['ppd-product']:
|
||||
ppdFile = ppd
|
||||
break
|
||||
with cups_lock:
|
||||
if ppdFile:
|
||||
conn.addPrinter(name=device['identifier'], ppdname=ppdFile, device=device['url'])
|
||||
else:
|
||||
conn.addPrinter(name=device['identifier'], device=device['url'])
|
||||
if device['identifier'] not in printers:
|
||||
conn.setPrinterInfo(device['identifier'], device['device-make-and-model'])
|
||||
conn.enablePrinter(device['identifier'])
|
||||
conn.acceptJobs(device['identifier'])
|
||||
conn.setPrinterUsersAllowed(device['identifier'], ['all'])
|
||||
conn.addPrinterOptionDefault(device['identifier'], "usb-no-reattach", "true")
|
||||
conn.addPrinterOptionDefault(device['identifier'], "usb-unidir", "true")
|
||||
else:
|
||||
device['device-make-and-model'] = printers[device['identifier']]['printer-info']
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_device_model(cls, device):
|
||||
device_model = ""
|
||||
if device.get('device-id'):
|
||||
for device_id in [device_lo for device_lo in device['device-id'].split(';')]:
|
||||
if any(x in device_id for x in ['MDL', 'MODEL']):
|
||||
device_model = device_id.split(':')[1]
|
||||
break
|
||||
elif device.get('device-make-and-model'):
|
||||
device_model = device['device-make-and-model']
|
||||
return re.sub("[\(].*?[\)]", "", device_model).strip()
|
||||
|
||||
@classmethod
|
||||
def get_status(cls):
|
||||
return cls.status
|
||||
|
||||
@property
|
||||
def device_identifier(self):
|
||||
return self.dev['identifier']
|
||||
|
||||
def add_connected_printer(self):
|
||||
PrinterDriver.status['status'] = "connected"
|
||||
PrinterDriver.status['printers'].append(self.device_identifier)
|
||||
|
||||
def remove_connected_printer(self):
|
||||
if self.device_identifier in PrinterDriver.status['printers']:
|
||||
PrinterDriver.status['printers'].remove(self.device_identifier)
|
||||
if not PrinterDriver.status['printers']:
|
||||
PrinterDriver.status['status'] = "disconnected"
|
||||
|
||||
def action(self, data):
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
tmp.write(b64decode(data['document']))
|
||||
tmp.flush()
|
||||
subprocess.check_call("lp -d %s %s" % (self.dev['identifier'], tmp.name), shell=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
_logger.warning(e.output)
|
||||
if data.get('action') == 'cashbox':
|
||||
self.open_cashbox()
|
||||
elif data.get('action') == 'print_receipt':
|
||||
self.print_receipt(base64.b64decode(data['receipt']))
|
||||
else:
|
||||
self.print_raw(b64decode(data['document']))
|
||||
|
||||
def disconnect(self):
|
||||
self.remove_connected_printer()
|
||||
self.update_status('disconnected', 'Printer was disconnected')
|
||||
super(PrinterDriver, self).disconnect()
|
||||
|
||||
def update_status(self, status, message, reason=None):
|
||||
"""Updates the state of the current printer.
|
||||
|
||||
Args:
|
||||
status (str): The new value of the status
|
||||
message (str): A comprehensive message describing the status
|
||||
reason (str): The reason fo the current status
|
||||
"""
|
||||
if self.state['status'] != status or self.state['reason'] != reason:
|
||||
self.state = {
|
||||
'status': status,
|
||||
'message': message,
|
||||
'reason': reason,
|
||||
}
|
||||
self.send_status()
|
||||
|
||||
def send_status(self):
|
||||
""" Sends the current status of the printer to the connected Odoo instance.
|
||||
"""
|
||||
self.data = {
|
||||
'value': '',
|
||||
'state': self.state,
|
||||
}
|
||||
event_manager.device_changed(self)
|
||||
|
||||
def print_raw(self, data):
|
||||
process = subprocess.Popen(["lp", "-d", self.device_identifier], stdin=subprocess.PIPE)
|
||||
process.communicate(data)
|
||||
|
||||
def print_receipt(self, receipt):
|
||||
im = Image.open(io.BytesIO(receipt))
|
||||
|
||||
# Convert to greyscale then to black and white
|
||||
im = im.convert("L")
|
||||
im = ImageOps.invert(im)
|
||||
im = im.convert("1")
|
||||
|
||||
'''ESC a n
|
||||
- ESC a: Select justification, in hex: "1B 61"
|
||||
- n: Justification:
|
||||
- 0: left
|
||||
- 1: center
|
||||
- 2: right
|
||||
'''
|
||||
center = b'\x1b\x61\x01'
|
||||
|
||||
'''GS v0 m x y
|
||||
- GS v0: Print raster bit image, in hex: "1D 76 30"
|
||||
- m: Density mode:
|
||||
- 0: 180dpi x 180dpi
|
||||
- 1: 180dpi x 90dpi
|
||||
- 2: 90dpi x 180 dpi
|
||||
- 3: 90dpi x 90 dpi
|
||||
- x: Length in X direction, in bytes, represented as 2 bytes in little endian
|
||||
- y: Length in Y direction, in dots, represented as 2 bytes in little endian
|
||||
'''
|
||||
width_pixels, height_pixels = im.size
|
||||
width_bytes = int((width_pixels + 7) / 8)
|
||||
print_command = b"\x1d\x76\x30" + b'\x00' + (width_bytes).to_bytes(2, 'little') + height_pixels.to_bytes(2, 'little')
|
||||
|
||||
'''GS V m
|
||||
- GS V: Cut, in hex: "1D 56"
|
||||
- m: Cut mode:
|
||||
- 0: Full cut
|
||||
- 1: Partial cut
|
||||
- 65 (0x41): Feed paper then full cut
|
||||
- 66 (0x42): Feed paper then partial cut
|
||||
'''
|
||||
cut = b'\x1d\x56' + b'\x41'
|
||||
|
||||
self.print_raw(center + print_command + im.tobytes() + cut + b'\n')
|
||||
|
||||
def print_status(self):
|
||||
"""Prints the status ticket of the IoTBox on the current printer."""
|
||||
wlan = ''
|
||||
ip = ''
|
||||
mac = ''
|
||||
homepage = ''
|
||||
|
||||
hosting_ap = os.system('pgrep hostapd') == 0
|
||||
ssid = subprocess.check_output('iwconfig 2>&1 | grep \'ESSID:"\' | sed \'s/.*"\\(.*\\)"/\\1/\'', shell=True).decode('utf-8').rstrip()
|
||||
if hosting_ap:
|
||||
wlan = 'Wireless network:\nIoTBox\n\n'
|
||||
elif ssid:
|
||||
wlan = 'Wireless network:\n%s\n\n' % ssid
|
||||
|
||||
interfaces = ni.interfaces()
|
||||
ips = []
|
||||
for iface_id in interfaces:
|
||||
iface_obj = ni.ifaddresses(iface_id)
|
||||
ifconfigs = iface_obj.get(ni.AF_INET, [])
|
||||
for conf in ifconfigs:
|
||||
if conf.get('addr') and conf.get('addr'):
|
||||
ips.append(conf.get('addr'))
|
||||
if len(ips) == 0:
|
||||
ip = '\nERROR: Could not connect to LAN\n\nPlease check that the IoTBox 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:
|
||||
ip = '\nIP Address:\n%s\n' % ips[0]
|
||||
else:
|
||||
ip = '\nIP Addresses:\n%s\n' % '\n'.join(ips)
|
||||
|
||||
mac_addr = get_mac()
|
||||
h = iter(hex(mac_addr)[2:].zfill(12))
|
||||
mac_addr = ":".join(i + next(h) for i in h)
|
||||
|
||||
if len(ips) >= 1:
|
||||
ips_filtered = [i for i in ips if i != '127.0.0.1']
|
||||
main_ips = ips_filtered and ips_filtered[0] or '127.0.0.1'
|
||||
mac = '\nMAC Address:\n%s\n' % mac_addr
|
||||
homepage = '\nHomepage:\nhttp://%s:8069\n\n' % main_ips
|
||||
|
||||
center = b'\x1b\x61\x01'
|
||||
title = b'\n\x1b\x21\x30\x1b\x4d\x01IoTBox Status\x1b\x4d\x00\x1b\x21\x00\n'
|
||||
cut = b'\x1d\x56\x41'
|
||||
|
||||
self.print_raw(center + title + wlan.encode() + mac.encode() + ip.encode() + homepage.encode() + cut + b'\n')
|
||||
|
||||
def open_cashbox(self):
|
||||
"""Sends a signal to the current printer to open the connected cashbox."""
|
||||
# ESC = --> Set peripheral device
|
||||
self.print_raw(b'\x1b\x3d\x01')
|
||||
for drawer in [b'\x1b\x70\x00', b'\x1b\x70\x01']: # Kick pin 2 and 5
|
||||
command = drawer + b'\x19\x19' # Pulse ON during 50ms then OFF during 50ms
|
||||
self.print_raw(command)
|
||||
|
||||
|
||||
class PrinterController(http.Controller):
|
||||
|
||||
@http.route('/hw_proxy/default_printer_action', type='json', auth='none', cors='*')
|
||||
def default_printer_action(self, data):
|
||||
printer = next((d for d in iot_devices if iot_devices[d].device_type == 'printer' and iot_devices[d].device_connection == 'direct'), None)
|
||||
if printer:
|
||||
iot_devices[printer].action(data)
|
||||
return True
|
||||
return False
|
||||
|
||||
old_drivers['printer'] = PrinterDriver
|
||||
|
||||
@@ -105,7 +105,14 @@ class EscposDriver(Thread):
|
||||
|
||||
printers = self.connected_usb_devices()
|
||||
if len(printers) > 0:
|
||||
print_dev = Usb(printers[0]['vendor'], printers[0]['product'])
|
||||
try:
|
||||
print_dev = Usb(printers[0]['vendor'], printers[0]['product'])
|
||||
except HandleDeviceError:
|
||||
# Escpos printers are now integrated to PrinterDriver, if the IoTBox is printing
|
||||
# through Cups at the same time, we get an USBError(16, 'Resource busy'). This means
|
||||
# that the Odoo instance connected to this IoTBox is up to date and no longer uses
|
||||
# this escpos library.
|
||||
return None
|
||||
self.set_status(
|
||||
'connected',
|
||||
"Connected to %s (in=0x%02x,out=0x%02x)" % (printers[0]['name'], print_dev.in_ep, print_dev.out_ep)
|
||||
@@ -168,8 +175,6 @@ class EscposDriver(Thread):
|
||||
elif task == 'cashbox':
|
||||
if timestamp >= time.time() - 12:
|
||||
self.open_cashbox(printer)
|
||||
elif task == 'printstatus':
|
||||
self.print_status(printer)
|
||||
elif task == 'status':
|
||||
pass
|
||||
error = False
|
||||
@@ -177,6 +182,7 @@ class EscposDriver(Thread):
|
||||
except NoDeviceError as e:
|
||||
print("No device found %s" % e)
|
||||
except HandleDeviceError as e:
|
||||
printer = None
|
||||
print("Impossible to handle the device due to previous error %s" % e)
|
||||
except TicketNotPrinted as e:
|
||||
print("The ticket does not seems to have been fully printed %s" % e)
|
||||
@@ -196,50 +202,6 @@ class EscposDriver(Thread):
|
||||
self.lockedstart()
|
||||
self.queue.put((time.time(),task,data))
|
||||
|
||||
def print_status(self,eprint):
|
||||
localips = ['0.0.0.0','127.0.0.1','127.0.1.1']
|
||||
hosting_ap = os.system('pgrep hostapd') == 0
|
||||
ssid = subprocess.check_output('iwconfig 2>&1 | grep \'ESSID:"\' | sed \'s/.*"\\(.*\\)"/\\1/\'', shell=True).decode('utf-8').rstrip()
|
||||
mac = get_mac()
|
||||
h = iter(hex(mac)[2:].zfill(12))
|
||||
mac = ":".join(i + next(h) for i in h)
|
||||
interfaces = ni.interfaces()
|
||||
ips = []
|
||||
for iface_id in interfaces:
|
||||
iface_obj = ni.ifaddresses(iface_id)
|
||||
ifconfigs = iface_obj.get(ni.AF_INET, [])
|
||||
for conf in ifconfigs:
|
||||
if conf.get('addr'):
|
||||
ips.append(conf.get('addr'))
|
||||
eprint.text('\n\n')
|
||||
eprint.set(align='center',type='b',height=2,width=2)
|
||||
eprint.text('IoTBox Status\n')
|
||||
eprint.text('\n')
|
||||
eprint.set(align='center')
|
||||
|
||||
if hosting_ap:
|
||||
eprint.text('Wireless network:\nIoTBox\n\n')
|
||||
elif ssid:
|
||||
eprint.text('Wireless network:\n' + ssid + '\n\n')
|
||||
|
||||
if len(ips) == 0:
|
||||
eprint.text('ERROR: Could not connect to LAN\n\nPlease check that the IoTBox 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')
|
||||
else:
|
||||
eprint.text('IP Addresses:\n')
|
||||
for ip in ips:
|
||||
eprint.text(ip+'\n')
|
||||
|
||||
if len(ips) >= 1:
|
||||
ips_filtered = [i for i in ips if i != '127.0.0.1']
|
||||
main_ips = ips_filtered and ips_filtered[0] or '127.0.0.1'
|
||||
eprint.text('\nMAC Address:\n' + str(mac) + '\n')
|
||||
eprint.text('\nHomepage:\nhttp://' + main_ips + ':8069\n')
|
||||
|
||||
eprint.text('\n\n')
|
||||
eprint.cut()
|
||||
|
||||
def print_receipt_body(self,eprint,receipt):
|
||||
|
||||
def check(string):
|
||||
@@ -366,8 +328,6 @@ class EscposDriver(Thread):
|
||||
|
||||
driver = EscposDriver()
|
||||
|
||||
driver.push_task('printstatus')
|
||||
|
||||
hw_proxy.drivers['escpos'] = driver
|
||||
|
||||
class EscposProxy(hw_proxy.Proxy):
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1468,6 +1468,11 @@ td {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.pos-receipts {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ var AbstractAction = require('web.AbstractAction');
|
||||
var core = require('web.core');
|
||||
var ajax = require('web.ajax');
|
||||
var CrashManager = require('web.CrashManager').CrashManager;
|
||||
var rpc = require('web.rpc');
|
||||
var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents;
|
||||
|
||||
|
||||
@@ -366,8 +367,7 @@ var ProxyStatusWidget = StatusWidget.extend({
|
||||
}
|
||||
if( this.pos.config.iface_print_via_proxy ||
|
||||
this.pos.config.iface_cashdrawer ){
|
||||
var printer = status.drivers.escpos ? status.drivers.escpos.status : false;
|
||||
if( printer != 'connected' && printer != 'connecting'){
|
||||
if(!this.is_printer_connected(status.drivers.printer)){
|
||||
warning = true;
|
||||
msg = msg ? msg + ' & ' : msg;
|
||||
msg += _t('Printer');
|
||||
@@ -388,6 +388,9 @@ var ProxyStatusWidget = StatusWidget.extend({
|
||||
this.set_status(status.status,'');
|
||||
}
|
||||
},
|
||||
is_printer_connected: function (printer) {
|
||||
return printer && printer.status === 'connected';
|
||||
},
|
||||
start: function(){
|
||||
var self = this;
|
||||
|
||||
@@ -416,7 +419,34 @@ var SaleDetailsButton = PosBaseWidget.extend({
|
||||
start: function(){
|
||||
var self = this;
|
||||
this.$el.click(function(){
|
||||
self.pos.proxy.print_sale_details();
|
||||
self.print_sale_details();
|
||||
});
|
||||
},
|
||||
|
||||
/** Print an overview of todays sales.
|
||||
*
|
||||
* By default this will print all sales of the day for current PoS config.
|
||||
*/
|
||||
print_sale_details: function () {
|
||||
var self = this;
|
||||
rpc.query({
|
||||
model: 'report.point_of_sale.report_saledetails',
|
||||
method: 'get_sale_details',
|
||||
args: [false, false, false, [this.pos.pos_session.id]],
|
||||
})
|
||||
.then(function(result){
|
||||
var env = {
|
||||
widget: new PosBaseWidget(self),
|
||||
company: self.pos.company,
|
||||
pos: self.pos,
|
||||
products: result.products,
|
||||
payments: result.payments,
|
||||
taxes: result.taxes,
|
||||
total_paid: result.total_paid,
|
||||
date: (new Date()).toLocaleString(),
|
||||
};
|
||||
var report = QWeb.render('SaleDetailsReport', env);
|
||||
self.pos.proxy.printer.print_receipt(report);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,12 +3,8 @@ odoo.define('point_of_sale.devices', function (require) {
|
||||
|
||||
var core = require('web.core');
|
||||
var mixins = require('web.mixins');
|
||||
var rpc = require('web.rpc');
|
||||
var Session = require('web.Session');
|
||||
var PosBaseWidget = require('point_of_sale.BaseWidget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
var Printer = require('point_of_sale.Printer').Printer;
|
||||
|
||||
// the JobQueue schedules a sequence of 'jobs'. each job is
|
||||
// a function returning a promise. The queue waits for each job to finish
|
||||
@@ -126,8 +122,6 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
};
|
||||
this.custom_payment_status = this.default_payment_status;
|
||||
|
||||
this.receipt_queue = [];
|
||||
|
||||
this.notifications = {};
|
||||
this.bypass_proxy = false;
|
||||
|
||||
@@ -141,8 +135,8 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
|
||||
this.on('change:status',this,function(eh,status){
|
||||
status = status.newValue;
|
||||
if(status.status === 'connected'){
|
||||
self.print_receipt();
|
||||
if(status.status === 'connected' && self.printer) {
|
||||
self.printer.print_receipt();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -175,6 +169,9 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
var self = this;
|
||||
this.connection = new Session(undefined,url, { use_cors: true});
|
||||
this.host = url;
|
||||
if (this.pos.config.iface_print_via_proxy) {
|
||||
this.connect_to_printer();
|
||||
}
|
||||
this.set_connection_status('connecting',{});
|
||||
|
||||
return this.message('handshake').then(function(response){
|
||||
@@ -192,6 +189,10 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
});
|
||||
},
|
||||
|
||||
connect_to_printer: function () {
|
||||
this.printer = new Printer(this.host, this.pos);
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a proxy and connects to it.
|
||||
*
|
||||
@@ -204,6 +205,9 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
autoconnect: function (options) {
|
||||
var self = this;
|
||||
this.set_connection_status('connecting',{});
|
||||
if (this.pos.config.iface_print_via_proxy) {
|
||||
this.connect_to_printer();
|
||||
}
|
||||
var found_url = new Promise(function () {});
|
||||
|
||||
if (options.force_ip) {
|
||||
@@ -446,67 +450,6 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
this.debug_weight = 0;
|
||||
},
|
||||
|
||||
// ask for the cashbox (the physical box where you store the cash) to be opened
|
||||
open_cashbox: function(){
|
||||
return this.message('open_cashbox');
|
||||
},
|
||||
|
||||
/*
|
||||
* ask the printer to print a receipt
|
||||
*/
|
||||
print_receipt: function(receipt){
|
||||
var self = this;
|
||||
if(receipt){
|
||||
this.receipt_queue.push(receipt);
|
||||
}
|
||||
function send_printing_job(){
|
||||
if (self.receipt_queue.length > 0){
|
||||
var r = self.receipt_queue.shift();
|
||||
self.message('print_xml_receipt',{ receipt: r },{ timeout: 5000 })
|
||||
.then(function(){
|
||||
send_printing_job();
|
||||
},function(error){
|
||||
if (error) {
|
||||
self.pos.gui.show_popup('error-traceback',{
|
||||
'title': _t('Printing Error: ') + error.data.message,
|
||||
'body': error.data.debug,
|
||||
});
|
||||
return;
|
||||
}
|
||||
self.receipt_queue.unshift(r);
|
||||
});
|
||||
}
|
||||
}
|
||||
send_printing_job();
|
||||
},
|
||||
|
||||
/** Print an overview of todays sales.
|
||||
*
|
||||
* By default this will print all sales of the day for current PoS config.
|
||||
*/
|
||||
print_sale_details: function() {
|
||||
var self = this;
|
||||
rpc.query({
|
||||
model: 'report.point_of_sale.report_saledetails',
|
||||
method: 'get_sale_details',
|
||||
args: [false, false, false, [this.pos.pos_session.id]],
|
||||
})
|
||||
.then(function(result){
|
||||
var env = {
|
||||
widget: new PosBaseWidget(self),
|
||||
company: self.pos.company,
|
||||
pos: self.pos,
|
||||
products: result.products,
|
||||
payments: result.payments,
|
||||
taxes: result.taxes,
|
||||
total_paid: result.total_paid,
|
||||
date: (new Date()).toLocaleString(),
|
||||
};
|
||||
var report = QWeb.render('SaleDetailsReport', env);
|
||||
self.print_receipt(report);
|
||||
});
|
||||
},
|
||||
|
||||
update_customer_facing_display: function(html) {
|
||||
if (this.posbox_supports_display) {
|
||||
return this.message('customer_facing_display',
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
odoo.define('point_of_sale.Printer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Session = require('web.Session');
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
|
||||
var PrinterMixin = {
|
||||
init: function () {
|
||||
this.receipt_queue = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Add the receipt to the queue of receipts to be printed and process it.
|
||||
* @param {String} receipt: The receipt to be printed, in HTML
|
||||
*/
|
||||
print_receipt: function (receipt) {
|
||||
var self = this;
|
||||
if (receipt) {
|
||||
this.receipt_queue.push(receipt);
|
||||
}
|
||||
function process_next_job() {
|
||||
if (self.receipt_queue.length > 0) {
|
||||
var r = self.receipt_queue.shift();
|
||||
self.htmlToImg(r)
|
||||
.then(self.send_printing_job.bind(self))
|
||||
.then(self._onIoTActionResult.bind(self))
|
||||
.then(process_next_job)
|
||||
.guardedCatch(self._onIoTActionFail.bind(self));
|
||||
}
|
||||
}
|
||||
process_next_job();
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the html as an image to print it
|
||||
* @param {String} receipt: The receipt to be printed, in HTML
|
||||
*/
|
||||
htmlToImg: function (receipt) {;
|
||||
$('.pos-receipts').html(receipt);
|
||||
$('.pos-receipt').addClass('pos-receipt-print');
|
||||
var promise = new Promise(function (resolve, reject) {
|
||||
html2canvas($('.pos-receipt')[0], {
|
||||
ignoreElements: function (node) {
|
||||
// By default, html2canvas copies the whole DOM even if we just capture a part
|
||||
// of it. When copying the list of products, it gets all of the product from the
|
||||
// backend. We ignore the content of <div class="pos"> to speed things up.
|
||||
if (node.className == "pos") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}).then(function (canvas) {
|
||||
$('.pos-receipts').empty();
|
||||
resolve(canvas.toDataURL('image/jpeg').replace('data:image/jpeg;base64,',''));
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
|
||||
_onIoTActionResult: function (data){
|
||||
if (this.pos && (data === false || data.result === false)) {
|
||||
this.pos.gui.show_popup('error',{
|
||||
'title': _t('Connection to the printer failed'),
|
||||
'body': _t('Please check if the printer is still connected.'),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onIoTActionFail: function () {
|
||||
if (this.pos) {
|
||||
this.pos.gui.show_popup('error',{
|
||||
'title': _t('Connection to IoT Box failed'),
|
||||
'body': _t('Please check if the IoT Box is still connected.'),
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var Printer = core.Class.extend(PrinterMixin, {
|
||||
init: function (url, pos) {
|
||||
PrinterMixin.init.call(this, arguments);
|
||||
this.pos = pos;
|
||||
this.connection = new Session(undefined, url || 'http://localhost:8069', { use_cors: true});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends a command to the connected proxy to open the cashbox
|
||||
* (the physical box where you store the cash). Updates the status of
|
||||
* the printer with the answer from the proxy.
|
||||
*/
|
||||
open_cashbox: function () {
|
||||
var self = this;
|
||||
return this.connection.rpc('/hw_proxy/default_printer_action', {
|
||||
data: {
|
||||
action: 'cashbox'
|
||||
}
|
||||
}).then(self._onIoTActionResult.bind(self))
|
||||
.guardedCatch(self._onIoTActionFail.bind(self));
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends the printing command the connected proxy
|
||||
* @param {String} img : The receipt to be printed, as an image
|
||||
*/
|
||||
send_printing_job: function (img) {
|
||||
return this.connection.rpc('/hw_proxy/default_printer_action', {
|
||||
data: {
|
||||
action: 'print_receipt',
|
||||
receipt: img,
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
PrinterMixin: PrinterMixin,
|
||||
Printer: Printer,
|
||||
}
|
||||
});
|
||||
@@ -1672,7 +1672,7 @@ var ReceiptScreenWidget = ScreenWidget.extend({
|
||||
print_xml: function() {
|
||||
var receipt = QWeb.render('XmlReceipt', this.get_receipt_render_env());
|
||||
|
||||
this.pos.proxy.print_receipt(receipt);
|
||||
this.pos.proxy.printer.print_receipt(receipt);
|
||||
this.pos.get_order()._printed = true;
|
||||
},
|
||||
print: function() {
|
||||
@@ -2062,7 +2062,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
|
||||
});
|
||||
|
||||
this.$('.js_cashdrawer').click(function(){
|
||||
self.pos.proxy.open_cashbox();
|
||||
self.pos.proxy.printer.open_cashbox();
|
||||
});
|
||||
|
||||
},
|
||||
@@ -2173,7 +2173,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
|
||||
|
||||
if (order.is_paid_with_cash() && this.pos.config.iface_cashdrawer) {
|
||||
|
||||
this.pos.proxy.open_cashbox();
|
||||
this.pos.proxy.printer.open_cashbox();
|
||||
}
|
||||
|
||||
order.initialize_validation_date();
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="Chrome">
|
||||
<div class='pos-receipts'>
|
||||
<!-- here go the receipts that will be printed -->
|
||||
</div>
|
||||
|
||||
<div class="pos">
|
||||
<div class="pos-topheader">
|
||||
<div class="pos-branding">
|
||||
|
||||
@@ -29,6 +29,9 @@ DefaultAuthType Basic
|
||||
# Web interface setting...
|
||||
WebInterface Yes
|
||||
|
||||
# Default paper size depends on the printer
|
||||
DefaultPaperSize None
|
||||
|
||||
# Restrict access to the server...
|
||||
<Location />
|
||||
Order allow,deny
|
||||
|
||||
@@ -25,6 +25,8 @@ PKGS_TO_INSTALL="
|
||||
fswebcam \
|
||||
nginx-full \
|
||||
dnsmasq \
|
||||
dbus \
|
||||
dbus-x11 \
|
||||
cups \
|
||||
printer-driver-all \
|
||||
cups-ipp-utils \
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<script type="text/javascript" src="/point_of_sale/static/lib/backbone/backbone.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/lib/waitfont.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/lib/sha1.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/lib/html2canvas.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/main.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/db.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/models.js"></script>
|
||||
@@ -21,6 +22,7 @@
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/gui.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/popups.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/screens.js"></script>
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/printers.js"></script>
|
||||
</template>
|
||||
|
||||
<template id="customer_facing_display_html" >
|
||||
|
||||
@@ -4,41 +4,20 @@ odoo.define('pos_restaurant.multiprint', function (require) {
|
||||
var models = require('point_of_sale.models');
|
||||
var screens = require('point_of_sale.screens');
|
||||
var core = require('web.core');
|
||||
var mixins = require('web.mixins');
|
||||
var Session = require('web.Session');
|
||||
var Printer = require('point_of_sale.Printer').Printer;
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var Printer = core.Class.extend(mixins.PropertiesMixin,{
|
||||
init: function(parent,options){
|
||||
mixins.PropertiesMixin.init.call(this);
|
||||
this.setParent(parent);
|
||||
options = options || {};
|
||||
var url = options.url || 'http://localhost:8069';
|
||||
this.connection = new Session(undefined,url, { use_cors: true});
|
||||
this.host = url;
|
||||
this.receipt_queue = [];
|
||||
},
|
||||
print: function(receipt){
|
||||
var self = this;
|
||||
if(receipt){
|
||||
this.receipt_queue.push(receipt);
|
||||
models.PosModel = models.PosModel.extend({
|
||||
create_printer: function (config) {
|
||||
var url = config.proxy_ip || '';
|
||||
if(url.indexOf('//') < 0) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
function send_printing_job(){
|
||||
if(self.receipt_queue.length > 0){
|
||||
var r = self.receipt_queue.shift();
|
||||
var options = {shadow: true, timeout: 5000};
|
||||
self.connection.rpc('/hw_proxy/print_xml_receipt', {receipt: r}, options)
|
||||
.then(function(){
|
||||
send_printing_job();
|
||||
},function(error, event){
|
||||
self.receipt_queue.unshift(r);
|
||||
console.log('There was an error while trying to print the order:');
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
if(url.indexOf(':', url.indexOf('//') + 2) < 0) {
|
||||
url = url + ':8069';
|
||||
}
|
||||
send_printing_job();
|
||||
return new Printer(url, this);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -58,14 +37,7 @@ models.load_models({
|
||||
|
||||
for(var i = 0; i < printers.length; i++){
|
||||
if(active_printers[printers[i].id]){
|
||||
var url = printers[i].proxy_ip || '';
|
||||
if(url.indexOf('//') < 0){
|
||||
url = 'http://'+url;
|
||||
}
|
||||
if(url.indexOf(':',url.indexOf('//')+2) < 0){
|
||||
url = url+':8069';
|
||||
}
|
||||
var printer = new Printer(self,{url:url});
|
||||
var printer = self.create_printer(printers[i]);
|
||||
printer.config = printers[i];
|
||||
self.printers.push(printer);
|
||||
|
||||
@@ -318,7 +290,7 @@ models.Order = models.Order.extend({
|
||||
var changes = this.computeChanges(printers[i].config.product_categories_ids);
|
||||
if ( changes['new'].length > 0 || changes['cancelled'].length > 0){
|
||||
var receipt = QWeb.render('OrderChangeReceipt',{changes:changes, widget:this});
|
||||
printers[i].print(receipt);
|
||||
printers[i].print_receipt(receipt);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -386,7 +358,6 @@ screens.OrderWidget.include({
|
||||
});
|
||||
|
||||
return {
|
||||
Printer: Printer,
|
||||
SubmitOrderButton: SubmitOrderButton,
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ var PrintBillButton = screens.ActionButtonWidget.extend({
|
||||
if(order.get_orderlines().length > 0){
|
||||
var receipt = order.export_for_printing();
|
||||
receipt.bill = true;
|
||||
this.pos.proxy.print_receipt(QWeb.render('BillReceipt',{
|
||||
this.pos.proxy.printer.print_receipt(QWeb.render('BillReceipt',{
|
||||
receipt: receipt, widget: this, pos: this.pos, order: order,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user