Browse Source
Merge pull request #10 from akretion/8.0-201407-pos-code-sprint
Merge pull request #10 from akretion/8.0-201407-pos-code-sprint
[8.0] Add support for Customer LCD, credit card reader and check printerpull/11/head
Pedro M. Baeza
10 years ago
28 changed files with 1850 additions and 0 deletions
-
1.travis.yml
-
24hw_customer_display/__init__.py
-
85hw_customer_display/__openerp__.py
-
24hw_customer_display/controllers/__init__.py
-
178hw_customer_display/controllers/main.py
-
68hw_customer_display/test-scripts/customer-display-test.py
-
24hw_telium_payment_terminal/__init__.py
-
83hw_telium_payment_terminal/__openerp__.py
-
24hw_telium_payment_terminal/controllers/__init__.py
-
284hw_telium_payment_terminal/controllers/main.py
-
219hw_telium_payment_terminal/test-scripts/telium-test.py
-
1pos_customer_display/__init__.py
-
57pos_customer_display/__openerp__.py
-
16pos_customer_display/customer_display_view.xml
-
87pos_customer_display/i18n/es.po
-
86pos_customer_display/i18n/fr.po
-
86pos_customer_display/i18n/pos_customer_display.pot
-
44pos_customer_display/pos_customer_display.py
-
10pos_customer_display/pos_customer_display.xml
-
229pos_customer_display/static/src/js/customer_display.js
-
1pos_payment_terminal/__init__.py
-
56pos_payment_terminal/__openerp__.py
-
38pos_payment_terminal/pos_payment_terminal.py
-
18pos_payment_terminal/pos_payment_terminal.xml
-
28pos_payment_terminal/pos_payment_terminal_view.xml
-
12pos_payment_terminal/static/src/css/pos_payment_terminal.css
-
50pos_payment_terminal/static/src/js/pos_payment_terminal.js
-
17pos_payment_terminal/static/src/xml/pos_payment_terminal.xml
@ -0,0 +1,24 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Hardware Customer Display module for Odoo |
||||
|
# Copyright (C) 2014 Akretion (http://www.akretion.com) |
||||
|
# @author Alexis de Lattre <alexis.delattre@akretion.com> |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
from . import controllers |
@ -0,0 +1,85 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Hardware Customer Display module for Odoo |
||||
|
# Copyright (C) 2014 Akretion (http://www.akretion.com) |
||||
|
# @author Alexis de Lattre <alexis.delattre@akretion.com> |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
{ |
||||
|
'name': 'Hardware Customer Display', |
||||
|
'version': '0.1', |
||||
|
'category': 'Hardware Drivers', |
||||
|
'license': 'AGPL-3', |
||||
|
'summary': 'Adds support for Customer Display in the Point of Sale', |
||||
|
'description': """ |
||||
|
Hardware Customer Display |
||||
|
========================= |
||||
|
|
||||
|
This module adds support for Customer Display in the Point of Sale. |
||||
|
This module is designed to be installed on the *POSbox* (i.e. the |
||||
|
proxy on which the USB devices are connected) and not on the main |
||||
|
Odoo server. On the main Odoo server, you should install the module |
||||
|
*pos_customer_display*. |
||||
|
|
||||
|
The configuration of the hardware is done in the configuration file of |
||||
|
the Odoo server of the POSbox. You should add the following entries in |
||||
|
the configuration file: |
||||
|
|
||||
|
* customer_display_device_name (default = /dev/ttyUSB0) |
||||
|
* customer_display_device_rate (default = 9600) |
||||
|
* customer_display_device_timeout (default = 2 seconds) |
||||
|
|
||||
|
The number of cols of the Customer Display (usually 20) should be |
||||
|
configured on the main Odoo server, in the menu Point of Sale > |
||||
|
Configuration > Point of Sales. The number of rows is supposed to be 2. |
||||
|
|
||||
|
It should support most serial and USB-serial LCD displays out-of-the-box |
||||
|
or with inheritance of a few functions. |
||||
|
|
||||
|
It has been tested with: |
||||
|
|
||||
|
* Bixolon BCD-1100 (Datasheet : |
||||
|
http://www.bixolon.com/html/en/product/product_detail.xhtml?prod_id=61) |
||||
|
* Bixolon BCD-1000 |
||||
|
|
||||
|
To setup the BCD-1100 on Linux, you will find some technical instructions |
||||
|
on this page: |
||||
|
http://techtuxwords.blogspot.fr/2012/12/linux-and-bixolon-bcd-1100.html |
||||
|
|
||||
|
If you have a kernel >= 3.12, you should also read this: |
||||
|
http://www.leniwiec.org/en/2014/06/25/ubuntu-14-04lts-how-to-pass-id-ven |
||||
|
dor-and-id-product-to-ftdi_sio-driver/ |
||||
|
|
||||
|
This module has been developped during a POS code sprint at Akretion |
||||
|
France from July 7th to July 10th 2014. This module is part of the POS |
||||
|
project of the Odoo Community Association http://odoo-community.org/. |
||||
|
You are invited to become a member and/or get involved in the |
||||
|
Association ! |
||||
|
|
||||
|
This module has been written by Alexis de Lattre from Akretion |
||||
|
<alexis.delattre@akretion.com>. |
||||
|
""", |
||||
|
'author': 'Akretion', |
||||
|
'website': 'http://www.akretion.com', |
||||
|
'depends': ['hw_proxy'], |
||||
|
'external_dependencies': { |
||||
|
'python': ['serial', 'unidecode'], |
||||
|
}, |
||||
|
'data': [], |
||||
|
} |
@ -0,0 +1,24 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Hardware Customer Display module for Odoo |
||||
|
# Copyright (C) 2014 Akretion (http://www.akretion.com) |
||||
|
# @author Alexis de Lattre <alexis.delattre@akretion.com> |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
from . import main |
@ -0,0 +1,178 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Hardware Customer Display module for Odoo |
||||
|
# Copyright (C) 2014 Akretion (http://www.akretion.com) |
||||
|
# @author Alexis de Lattre <alexis.delattre@akretion.com> |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
import logging |
||||
|
import simplejson |
||||
|
import time |
||||
|
from threading import Thread, Lock |
||||
|
from Queue import Queue |
||||
|
from unidecode import unidecode |
||||
|
from serial import Serial |
||||
|
import openerp.addons.hw_proxy.controllers.main as hw_proxy |
||||
|
from openerp import http |
||||
|
from openerp.tools.config import config |
||||
|
|
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class CustomerDisplayDriver(Thread): |
||||
|
def __init__(self): |
||||
|
Thread.__init__(self) |
||||
|
self.queue = Queue() |
||||
|
self.lock = Lock() |
||||
|
self.status = {'status': 'connecting', 'messages': []} |
||||
|
self.device_name = config.get( |
||||
|
'customer_display_device_name', '/dev/ttyUSB0') |
||||
|
self.device_rate = int(config.get( |
||||
|
'customer_display_device_rate', 9600)) |
||||
|
self.device_timeout = int(config.get( |
||||
|
'customer_display_device_timeout', 2)) |
||||
|
self.serial = False |
||||
|
|
||||
|
def get_status(self): |
||||
|
self.push_task('status') |
||||
|
return self.status |
||||
|
|
||||
|
def set_status(self, status, message=None): |
||||
|
if status == self.status['status']: |
||||
|
if message is not None and message != self.status['messages'][-1]: |
||||
|
self.status['messages'].append(message) |
||||
|
else: |
||||
|
self.status['status'] = status |
||||
|
if message: |
||||
|
self.status['messages'] = [message] |
||||
|
else: |
||||
|
self.status['messages'] = [] |
||||
|
|
||||
|
if status == 'error' and message: |
||||
|
logger.error('Display Error: '+message) |
||||
|
elif status == 'disconnected' and message: |
||||
|
logger.warning('Disconnected Display: '+message) |
||||
|
|
||||
|
def lockedstart(self): |
||||
|
with self.lock: |
||||
|
if not self.isAlive(): |
||||
|
self.daemon = True |
||||
|
self.start() |
||||
|
|
||||
|
def push_task(self, task, data=None): |
||||
|
self.lockedstart() |
||||
|
self.queue.put((time.time(), task, data)) |
||||
|
|
||||
|
def move_cursor(self, col, row): |
||||
|
# Bixolon spec : 11. "Move Cursor to Specified Position" |
||||
|
self.cmd_serial_write('\x1B\x6C' + chr(col) + chr(row)) |
||||
|
|
||||
|
def display_text(self, lines): |
||||
|
logger.debug( |
||||
|
"Preparing to send the following lines to LCD: %s" % lines) |
||||
|
# We don't check the number of rows/cols here, because it has already |
||||
|
# been checked in the POS client in the JS code |
||||
|
lines_ascii = [] |
||||
|
for line in lines: |
||||
|
lines_ascii.append(unidecode(line)) |
||||
|
row = 0 |
||||
|
for dline in lines_ascii: |
||||
|
row += 1 |
||||
|
self.move_cursor(1, row) |
||||
|
self.serial_write(dline) |
||||
|
|
||||
|
def setup_customer_display(self): |
||||
|
'''Set LCD cursor to off |
||||
|
If your LCD has different setup instruction(s), you should |
||||
|
inherit this function''' |
||||
|
# Bixolon spec : 35. "Set Cursor On/Off" |
||||
|
self.cmd_serial_write('\x1F\x43\x00') |
||||
|
logger.debug('LCD cursor set to off') |
||||
|
|
||||
|
def clear_customer_display(self): |
||||
|
'''If your LCD has different clearing instruction, you should inherit |
||||
|
this function''' |
||||
|
# Bixolon spec : 12. "Clear Display Screen and Clear String Mode" |
||||
|
self.cmd_serial_write('\x0C') |
||||
|
logger.debug('Customer display cleared') |
||||
|
|
||||
|
def cmd_serial_write(self, command): |
||||
|
'''If your LCD requires a prefix and/or suffix on all commands, |
||||
|
you should inherit this function''' |
||||
|
assert isinstance(command, str), 'command must be a string' |
||||
|
self.serial_write(command) |
||||
|
|
||||
|
def serial_write(self, text): |
||||
|
assert isinstance(text, str), 'text must be a string' |
||||
|
self.serial.write(text) |
||||
|
|
||||
|
def send_text_customer_display(self, text_to_display): |
||||
|
'''This function sends the data to the serial/usb port. |
||||
|
We open and close the serial connection on every message display. |
||||
|
Why ? |
||||
|
1. Because it is not a problem for the customer display |
||||
|
2. Because it is not a problem for performance, according to my tests |
||||
|
3. Because it allows recovery on errors : you can unplug/replug the |
||||
|
customer display and it will work again on the next message without |
||||
|
problem |
||||
|
''' |
||||
|
lines = simplejson.loads(text_to_display) |
||||
|
assert isinstance(lines, list), 'lines_list should be a list' |
||||
|
try: |
||||
|
logger.debug( |
||||
|
'Opening serial port %s for customer display with baudrate %d' |
||||
|
% (self.device_name, self.device_rate)) |
||||
|
self.serial = Serial( |
||||
|
self.device_name, self.device_rate, |
||||
|
timeout=self.device_timeout) |
||||
|
logger.debug('serial.is_open = %s' % self.serial.isOpen()) |
||||
|
self.setup_customer_display() |
||||
|
self.clear_customer_display() |
||||
|
self.display_text(lines) |
||||
|
except Exception, e: |
||||
|
logger.error('Exception in serial connection: %s' % str(e)) |
||||
|
finally: |
||||
|
if self.serial: |
||||
|
logger.debug('Closing serial port for customer display') |
||||
|
self.serial.close() |
||||
|
|
||||
|
def run(self): |
||||
|
while True: |
||||
|
try: |
||||
|
timestamp, task, data = self.queue.get(True) |
||||
|
if task == 'display': |
||||
|
self.send_text_customer_display(data) |
||||
|
elif task == 'status': |
||||
|
pass |
||||
|
except Exception as e: |
||||
|
self.set_status('error', str(e)) |
||||
|
|
||||
|
driver = CustomerDisplayDriver() |
||||
|
|
||||
|
hw_proxy.drivers['customer_display'] = driver |
||||
|
|
||||
|
|
||||
|
class CustomerDisplayProxy(hw_proxy.Proxy): |
||||
|
@http.route( |
||||
|
'/hw_proxy/send_text_customer_display', type='json', auth='none', |
||||
|
cors='*') |
||||
|
def send_text_customer_display(self, text_to_display): |
||||
|
logger.debug('LCD: Call send_text_customer_display') |
||||
|
driver.push_task('display', text_to_display) |
@ -0,0 +1,68 @@ |
|||||
|
#! /usr/bin/python |
||||
|
# -*- encoding: utf-8 -*- |
||||
|
# Author : Alexis de Lattre <alexis.delattre@akretion.com> |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# This is a test script, that you can use if you want to test/play |
||||
|
# with the customer display independantly from the Odoo server |
||||
|
# It has been tested with a Bixolon BCD-1100 |
||||
|
|
||||
|
from serial import Serial |
||||
|
from unidecode import unidecode |
||||
|
import sys |
||||
|
|
||||
|
DEVICE = '/dev/ttyUSB0' |
||||
|
DEVICE_RATE = 9600 |
||||
|
DEVICE_COLS = 20 |
||||
|
|
||||
|
|
||||
|
def display_text(ser, line1, line2): |
||||
|
print "convert to ascii" |
||||
|
line1 = unidecode(line1) |
||||
|
line2 = unidecode(line2) |
||||
|
print "set lines to the right lenght (%s)" % DEVICE_COLS |
||||
|
for line in [line1, line2]: |
||||
|
if len(line) < DEVICE_COLS: |
||||
|
line += ' ' * (DEVICE_COLS - len(line)) |
||||
|
elif len(line) > DEVICE_COLS: |
||||
|
line = line[0:DEVICE_COLS] |
||||
|
assert len(line) == DEVICE_COLS, 'Wrong length' |
||||
|
print "try to clear display" |
||||
|
ser.write('\x0C') |
||||
|
print "clear done" |
||||
|
print "try to position at start of 1st line" |
||||
|
ser.write('\x1B\x6C' + chr(1) + chr(1)) |
||||
|
print "position done" |
||||
|
print "try to write 1st line" |
||||
|
ser.write(line1) |
||||
|
print "write 1st line done" |
||||
|
print "try to position at start of 2nd line" |
||||
|
ser.write('\x1B\x6C' + chr(1) + chr(2)) |
||||
|
print "position done" |
||||
|
print "try to write 2nd line" |
||||
|
ser.write(line2) |
||||
|
print "write done" |
||||
|
|
||||
|
|
||||
|
def open_close_display(line1, line2): |
||||
|
ser = False |
||||
|
try: |
||||
|
print "open serial port" |
||||
|
ser = Serial(DEVICE, DEVICE_RATE, timeout=2) |
||||
|
print "serial port open =", ser.isOpen() |
||||
|
print "try to set cursor to off" |
||||
|
ser.write('\x1F\x43\x00') |
||||
|
print "cursor set to off" |
||||
|
display_text(ser, line1, line2) |
||||
|
except Exception, e: |
||||
|
print "EXCEPTION e=", e |
||||
|
sys.exit(1) |
||||
|
finally: |
||||
|
if ser: |
||||
|
print "close serial port" |
||||
|
ser.close() |
||||
|
|
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
line1 = u'POS Code Sprint' |
||||
|
line2 = u'@ Akretion 2014/07' |
||||
|
open_close_display(line1, line2) |
@ -0,0 +1,24 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Hardware Telium Payment Terminal module for Odoo |
||||
|
# Copyright (C) 2014 Akretion (http://www.akretion.com) |
||||
|
# @author Alexis de Lattre <alexis.delattre@akretion.com> |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
from . import controllers |
@ -0,0 +1,83 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Hardware Telium Payment Terminal module for Odoo |
||||
|
# Copyright (C) 2014 Akretion (http://www.akretion.com) |
||||
|
# @author Alexis de Lattre <alexis.delattre@akretion.com> |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
{ |
||||
|
'name': 'Hardware Telium Payment Terminal', |
||||
|
'version': '0.1', |
||||
|
'category': 'Hardware Drivers', |
||||
|
'license': 'AGPL-3', |
||||
|
'summary': 'Adds support for Payment Terminals using Telium protocol', |
||||
|
'description': """ |
||||
|
Hardware Telium Payment Terminal |
||||
|
================================ |
||||
|
|
||||
|
This module adds support for credit card reader and checks printers |
||||
|
using Telium protocol in the Point of Sale. This module is designed to |
||||
|
be installed on the *POSbox* (i.e. the proxy on which the USB devices |
||||
|
are connected) and not on the main Odoo server. On the main Odoo server, |
||||
|
you should install the module *pos_payment_terminal*. |
||||
|
|
||||
|
The configuration of the hardware is done in the configuration file of |
||||
|
the Odoo server of the POSbox. You should add the following entries in |
||||
|
the configuration file: |
||||
|
|
||||
|
* payment_terminal_device_name (default = /dev/ttyACM0) |
||||
|
* payment_terminal_device_rate (default = 9600) |
||||
|
|
||||
|
The Telium protocol is used by Ingenico and Sagem payment terminals. It |
||||
|
is based on the Concert protocol, so it can probably work with payment |
||||
|
terminals from other brands. This module implements the protocol E+ (and |
||||
|
not the protocol E), so it requires a Telium Manager version 37783600 |
||||
|
or superior. To get the version of the Telium Manager on an Ingenico |
||||
|
terminal press F > 0-TELIUM MANAGER > 2-Consultation > 4-Configuration |
||||
|
> 2-Software > 1-TERMINAL > On Display > Telium Manager and then read |
||||
|
the field *M20S*. |
||||
|
|
||||
|
You will need to configure your payment terminal to accept commands |
||||
|
from the POS. On an Ingenico terminal press F > 0-TELIUM MANAGER > |
||||
|
5-Initialization > 1-Parameters > Cash Connection and then select *On* |
||||
|
and then *USB*. After that, you should reboot the terminal. |
||||
|
|
||||
|
This module has been successfully tested with: |
||||
|
|
||||
|
* Ingenico EFTSmart4S |
||||
|
* Ingenico EFTSmart2 2640 with Telim Manager version 37784503 |
||||
|
* Ingenico i2200 cheque reader and writer |
||||
|
|
||||
|
This module has been developped during a POS code sprint at Akretion |
||||
|
France from July 7th to July 10th 2014. This module is part of the POS |
||||
|
project of the Odoo Community Association http://odoo-community.org/. |
||||
|
You are invited to become a member and/or get involved in the |
||||
|
Association ! |
||||
|
|
||||
|
This module has been written by Alexis de Lattre |
||||
|
<alexis.delattre@akretion.com> from Akretion. |
||||
|
""", |
||||
|
'author': 'Akretion', |
||||
|
'website': 'http://www.akretion.com', |
||||
|
'depends': ['hw_proxy'], |
||||
|
'external_dependencies': { |
||||
|
'python': ['serial', 'pycountry'], |
||||
|
}, |
||||
|
'data': [], |
||||
|
} |
@ -0,0 +1,24 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Hardware Telium Payment Terminal module for Odoo |
||||
|
# Copyright (C) 2014 Akretion (http://www.akretion.com) |
||||
|
# @author Alexis de Lattre <alexis.delattre@akretion.com> |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
from . import main |
@ -0,0 +1,284 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Hardware Telium Payment Terminal module for Odoo |
||||
|
# Copyright (C) 2014 Akretion (http://www.akretion.com) |
||||
|
# @author Alexis de Lattre <alexis.delattre@akretion.com> |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
import logging |
||||
|
import simplejson |
||||
|
import time |
||||
|
import curses.ascii |
||||
|
from threading import Thread, Lock |
||||
|
from Queue import Queue |
||||
|
from serial import Serial |
||||
|
import pycountry |
||||
|
import openerp.addons.hw_proxy.controllers.main as hw_proxy |
||||
|
from openerp import http |
||||
|
from openerp.tools.config import config |
||||
|
|
||||
|
|
||||
|
logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class TeliumPaymentTerminalDriver(Thread): |
||||
|
def __init__(self): |
||||
|
Thread.__init__(self) |
||||
|
self.queue = Queue() |
||||
|
self.lock = Lock() |
||||
|
self.status = {'status': 'connecting', 'messages': []} |
||||
|
self.device_name = config.get( |
||||
|
'telium_terminal_device_name', '/dev/ttyACM0') |
||||
|
self.device_rate = int(config.get( |
||||
|
'telium_terminal_device_rate', 9600)) |
||||
|
self.serial = False |
||||
|
|
||||
|
def get_status(self): |
||||
|
self.push_task('status') |
||||
|
return self.status |
||||
|
|
||||
|
def set_status(self, status, message=None): |
||||
|
if status == self.status['status']: |
||||
|
if message is not None and message != self.status['messages'][-1]: |
||||
|
self.status['messages'].append(message) |
||||
|
else: |
||||
|
self.status['status'] = status |
||||
|
if message: |
||||
|
self.status['messages'] = [message] |
||||
|
else: |
||||
|
self.status['messages'] = [] |
||||
|
|
||||
|
if status == 'error' and message: |
||||
|
logger.error('Payment Terminal Error: '+message) |
||||
|
elif status == 'disconnected' and message: |
||||
|
logger.warning('Disconnected Terminal: '+message) |
||||
|
|
||||
|
def lockedstart(self): |
||||
|
with self.lock: |
||||
|
if not self.isAlive(): |
||||
|
self.daemon = True |
||||
|
self.start() |
||||
|
|
||||
|
def push_task(self, task, data=None): |
||||
|
self.lockedstart() |
||||
|
self.queue.put((time.time(), task, data)) |
||||
|
|
||||
|
def serial_write(self, text): |
||||
|
assert isinstance(text, str), 'text must be a string' |
||||
|
self.serial.write(text) |
||||
|
|
||||
|
def initialize_msg(self): |
||||
|
max_attempt = 3 |
||||
|
attempt_nr = 0 |
||||
|
while attempt_nr < max_attempt: |
||||
|
attempt_nr += 1 |
||||
|
self.send_one_byte_signal('ENQ') |
||||
|
if self.get_one_byte_answer('ACK'): |
||||
|
return True |
||||
|
else: |
||||
|
logger.warning("Terminal : SAME PLAYER TRY AGAIN") |
||||
|
self.send_one_byte_signal('EOT') |
||||
|
# Wait 1 sec between each attempt |
||||
|
time.sleep(1) |
||||
|
return False |
||||
|
|
||||
|
def send_one_byte_signal(self, signal): |
||||
|
ascii_names = curses.ascii.controlnames |
||||
|
assert signal in ascii_names, 'Wrong signal' |
||||
|
char = ascii_names.index(signal) |
||||
|
self.serial_write(chr(char)) |
||||
|
logger.debug('Signal %s sent to terminal' % signal) |
||||
|
|
||||
|
def get_one_byte_answer(self, expected_signal): |
||||
|
ascii_names = curses.ascii.controlnames |
||||
|
one_byte_read = self.serial.read(1) |
||||
|
expected_char = ascii_names.index(expected_signal) |
||||
|
if one_byte_read == chr(expected_char): |
||||
|
logger.debug("%s received from terminal" % expected_signal) |
||||
|
return True |
||||
|
else: |
||||
|
return False |
||||
|
|
||||
|
def prepare_data_to_send(self, payment_info_dict): |
||||
|
amount = payment_info_dict['amount'] |
||||
|
if payment_info_dict['payment_mode'] == 'check': |
||||
|
payment_mode = 'C' |
||||
|
elif payment_info_dict['payment_mode'] == 'card': |
||||
|
payment_mode = '1' |
||||
|
else: |
||||
|
logger.error( |
||||
|
"The payment mode '%s' is not supported" |
||||
|
% payment_info_dict['payment_mode']) |
||||
|
return False |
||||
|
cur_iso_letter = payment_info_dict['currency_iso'].upper() |
||||
|
try: |
||||
|
cur = pycountry.currencies.get(letter=cur_iso_letter) |
||||
|
cur_numeric = str(cur.numeric) |
||||
|
except: |
||||
|
logger.error("Currency %s is not recognized" % cur_iso_letter) |
||||
|
return False |
||||
|
data = { |
||||
|
'pos_number': str(1).zfill(2), |
||||
|
'answer_flag': '0', |
||||
|
'transaction_type': '0', |
||||
|
'payment_mode': payment_mode, |
||||
|
'currency_numeric': cur_numeric.zfill(3), |
||||
|
'private': ' ' * 10, |
||||
|
'delay': 'A011', |
||||
|
'auto': 'B010', |
||||
|
'amount_msg': ('%.0f' % (amount * 100)).zfill(8), |
||||
|
} |
||||
|
return data |
||||
|
|
||||
|
def generate_lrc(self, real_msg_with_etx): |
||||
|
lrc = 0 |
||||
|
for char in real_msg_with_etx: |
||||
|
lrc ^= ord(char) |
||||
|
return lrc |
||||
|
|
||||
|
def send_message(self, data): |
||||
|
'''We use protocol E+''' |
||||
|
ascii_names = curses.ascii.controlnames |
||||
|
real_msg = ( |
||||
|
data['pos_number'] |
||||
|
+ data['amount_msg'] |
||||
|
+ data['answer_flag'] |
||||
|
+ data['payment_mode'] |
||||
|
+ data['transaction_type'] |
||||
|
+ data['currency_numeric'] |
||||
|
+ data['private'] |
||||
|
+ data['delay'] |
||||
|
+ data['auto'] |
||||
|
) |
||||
|
logger.debug('Real message to send = %s' % real_msg) |
||||
|
assert len(real_msg) == 34, 'Wrong length for protocol E+' |
||||
|
real_msg_with_etx = real_msg + chr(ascii_names.index('ETX')) |
||||
|
lrc = self.generate_lrc(real_msg_with_etx) |
||||
|
message = chr(ascii_names.index('STX')) + real_msg_with_etx + chr(lrc) |
||||
|
self.serial_write(message) |
||||
|
logger.info('Message sent to terminal') |
||||
|
|
||||
|
def compare_data_vs_answer(self, data, answer_data): |
||||
|
for field in [ |
||||
|
'pos_number', 'amount_msg', |
||||
|
'currency_numeric', 'private']: |
||||
|
if data[field] != answer_data[field]: |
||||
|
logger.warning( |
||||
|
"Field %s has value '%s' in data and value '%s' in answer" |
||||
|
% (field, data[field], answer_data[field])) |
||||
|
|
||||
|
def parse_terminal_answer(self, real_msg, data): |
||||
|
answer_data = { |
||||
|
'pos_number': real_msg[0:2], |
||||
|
'transaction_result': real_msg[2], |
||||
|
'amount_msg': real_msg[3:11], |
||||
|
'payment_mode': real_msg[11], |
||||
|
'currency_numeric': real_msg[12:15], |
||||
|
'private': real_msg[15:26], |
||||
|
} |
||||
|
logger.debug('answer_data = %s' % answer_data) |
||||
|
self.compare_data_vs_answer(data, answer_data) |
||||
|
return answer_data |
||||
|
|
||||
|
def get_answer_from_terminal(self, data): |
||||
|
ascii_names = curses.ascii.controlnames |
||||
|
full_msg_size = 1+2+1+8+1+3+10+1+1 |
||||
|
msg = self.serial.read(size=full_msg_size) |
||||
|
logger.debug('%d bytes read from terminal' % full_msg_size) |
||||
|
assert len(msg) == full_msg_size, 'Answer has a wrong size' |
||||
|
if msg[0] != chr(ascii_names.index('STX')): |
||||
|
logger.error( |
||||
|
'The first byte of the answer from terminal should be STX') |
||||
|
if msg[-2] != chr(ascii_names.index('ETX')): |
||||
|
logger.error( |
||||
|
'The byte before final of the answer from terminal ' |
||||
|
'should be ETX') |
||||
|
lrc = msg[-1] |
||||
|
computed_lrc = chr(self.generate_lrc(msg[1:-1])) |
||||
|
if computed_lrc != lrc: |
||||
|
logger.error( |
||||
|
'The LRC of the answer from terminal is wrong') |
||||
|
real_msg = msg[1:-2] |
||||
|
logger.debug('Real answer received = %s' % real_msg) |
||||
|
return self.parse_terminal_answer(real_msg, data) |
||||
|
|
||||
|
def transaction_start(self, payment_info): |
||||
|
'''This function sends the data to the serial/usb port. |
||||
|
''' |
||||
|
payment_info_dict = simplejson.loads(payment_info) |
||||
|
assert isinstance(payment_info_dict, dict), \ |
||||
|
'payment_info_dict should be a dict' |
||||
|
logger.debug("payment_info_dict = %s" % payment_info_dict) |
||||
|
try: |
||||
|
logger.debug( |
||||
|
'Opening serial port %s for payment terminal with baudrate %d' |
||||
|
% (self.device_name, self.device_rate)) |
||||
|
# IMPORTANT : don't modify timeout=3 seconds |
||||
|
# This parameter is very important ; the Telium spec say |
||||
|
# that we have to wait to up 3 seconds to get LRC |
||||
|
self.serial = Serial( |
||||
|
self.device_name, self.device_rate, |
||||
|
timeout=3) |
||||
|
logger.debug('serial.is_open = %s' % self.serial.isOpen()) |
||||
|
if self.initialize_msg(): |
||||
|
data = self.prepare_data_to_send(payment_info_dict) |
||||
|
if not data: |
||||
|
return |
||||
|
self.send_message(data) |
||||
|
if self.get_one_byte_answer('ACK'): |
||||
|
self.send_one_byte_signal('EOT') |
||||
|
|
||||
|
logger.info("Now expecting answer from Terminal") |
||||
|
if self.get_one_byte_answer('ENQ'): |
||||
|
self.send_one_byte_signal('ACK') |
||||
|
self.get_answer_from_terminal(data) |
||||
|
self.send_one_byte_signal('ACK') |
||||
|
if self.get_one_byte_answer('EOT'): |
||||
|
logger.info("Answer received from Terminal") |
||||
|
|
||||
|
except Exception, e: |
||||
|
logger.error('Exception in serial connection: %s' % str(e)) |
||||
|
finally: |
||||
|
if self.serial: |
||||
|
logger.debug('Closing serial port for payment terminal') |
||||
|
self.serial.close() |
||||
|
|
||||
|
def run(self): |
||||
|
while True: |
||||
|
try: |
||||
|
timestamp, task, data = self.queue.get(True) |
||||
|
if task == 'transaction_start': |
||||
|
self.transaction_start(data) |
||||
|
elif task == 'status': |
||||
|
pass |
||||
|
except Exception as e: |
||||
|
self.set_status('error', str(e)) |
||||
|
|
||||
|
driver = TeliumPaymentTerminalDriver() |
||||
|
|
||||
|
hw_proxy.drivers['telium_payment_terminal'] = driver |
||||
|
|
||||
|
|
||||
|
class TeliumPaymentTerminalProxy(hw_proxy.Proxy): |
||||
|
@http.route( |
||||
|
'/hw_proxy/payment_terminal_transaction_start', |
||||
|
type='json', auth='none', cors='*') |
||||
|
def payment_terminal_transaction_start(self, payment_info): |
||||
|
logger.debug('Telium: Call payment_terminal_transaction_start') |
||||
|
driver.push_task('transaction_start', payment_info) |
@ -0,0 +1,219 @@ |
|||||
|
#! /usr/bin/python |
||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Hardware Telium Test script |
||||
|
# Copyright (C) 2014 Akretion (http://www.akretion.com) |
||||
|
# @author Alexis de Lattre <alexis.delattre@akretion.com> |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
from serial import Serial |
||||
|
import curses.ascii |
||||
|
import time |
||||
|
import pycountry |
||||
|
|
||||
|
|
||||
|
DEVICE = '/dev/ttyACM0' |
||||
|
DEVICE_RATE = 9600 |
||||
|
PAYMENT_MODE = 'card' # 'card' ou 'check' |
||||
|
CURRENCY_ISO = 'EUR' |
||||
|
AMOUNT = 12.42 |
||||
|
|
||||
|
|
||||
|
def serial_write(serial, text): |
||||
|
assert isinstance(text, str), 'text must be a string' |
||||
|
serial.write(text) |
||||
|
|
||||
|
|
||||
|
def initialize_msg(serial): |
||||
|
max_attempt = 3 |
||||
|
attempt_nr = 0 |
||||
|
while attempt_nr < max_attempt: |
||||
|
attempt_nr += 1 |
||||
|
send_one_byte_signal(serial, 'ENQ') |
||||
|
if get_one_byte_answer(serial, 'ACK'): |
||||
|
return True |
||||
|
else: |
||||
|
print "Terminal : SAME PLAYER TRY AGAIN" |
||||
|
send_one_byte_signal(serial, 'EOT') |
||||
|
# Wait 1 sec between each attempt |
||||
|
time.sleep(1) |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def send_one_byte_signal(serial, signal): |
||||
|
ascii_names = curses.ascii.controlnames |
||||
|
assert signal in ascii_names, 'Wrong signal' |
||||
|
char = ascii_names.index(signal) |
||||
|
serial_write(serial, chr(char)) |
||||
|
print 'Signal %s sent to terminal' % signal |
||||
|
|
||||
|
|
||||
|
def get_one_byte_answer(serial, expected_signal): |
||||
|
ascii_names = curses.ascii.controlnames |
||||
|
one_byte_read = serial.read(1) |
||||
|
expected_char = ascii_names.index(expected_signal) |
||||
|
if one_byte_read == chr(expected_char): |
||||
|
print "%s received from terminal" % expected_signal |
||||
|
return True |
||||
|
else: |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def prepare_data_to_send(): |
||||
|
if PAYMENT_MODE == 'check': |
||||
|
payment_mode = 'C' |
||||
|
elif PAYMENT_MODE == 'card': |
||||
|
payment_mode = '1' |
||||
|
else: |
||||
|
print "The payment mode '%s' is not supported" % PAYMENT_MODE |
||||
|
return False |
||||
|
cur_iso_letter = CURRENCY_ISO.upper() |
||||
|
try: |
||||
|
cur = pycountry.currencies.get(letter=cur_iso_letter) |
||||
|
cur_numeric = str(cur.numeric) |
||||
|
except: |
||||
|
print "Currency %s is not recognized" % cur_iso_letter |
||||
|
return False |
||||
|
data = { |
||||
|
'pos_number': str(1).zfill(2), |
||||
|
'answer_flag': '0', |
||||
|
'transaction_type': '0', |
||||
|
'payment_mode': payment_mode, |
||||
|
'currency_numeric': cur_numeric.zfill(3), |
||||
|
'private': ' ' * 10, |
||||
|
'delay': 'A011', |
||||
|
'auto': 'B010', |
||||
|
'amount_msg': ('%.0f' % (AMOUNT * 100)).zfill(8), |
||||
|
} |
||||
|
return data |
||||
|
|
||||
|
|
||||
|
def generate_lrc(real_msg_with_etx): |
||||
|
lrc = 0 |
||||
|
for char in real_msg_with_etx: |
||||
|
lrc ^= ord(char) |
||||
|
return lrc |
||||
|
|
||||
|
|
||||
|
def send_message(serial, data): |
||||
|
'''We use protocol E+''' |
||||
|
ascii_names = curses.ascii.controlnames |
||||
|
real_msg = ( |
||||
|
data['pos_number'] |
||||
|
+ data['amount_msg'] |
||||
|
+ data['answer_flag'] |
||||
|
+ data['payment_mode'] |
||||
|
+ data['transaction_type'] |
||||
|
+ data['currency_numeric'] |
||||
|
+ data['private'] |
||||
|
+ data['delay'] |
||||
|
+ data['auto'] |
||||
|
) |
||||
|
print 'Real message to send = %s' % real_msg |
||||
|
assert len(real_msg) == 34, 'Wrong length for protocol E+' |
||||
|
real_msg_with_etx = real_msg + chr(ascii_names.index('ETX')) |
||||
|
lrc = generate_lrc(real_msg_with_etx) |
||||
|
message = chr(ascii_names.index('STX')) + real_msg_with_etx + chr(lrc) |
||||
|
serial_write(serial, message) |
||||
|
print 'Message sent to terminal' |
||||
|
|
||||
|
|
||||
|
def compare_data_vs_answer(data, answer_data): |
||||
|
for field in [ |
||||
|
'pos_number', 'amount_msg', |
||||
|
'currency_numeric', 'private']: |
||||
|
if data[field] != answer_data[field]: |
||||
|
print ( |
||||
|
"Field %s has value '%s' in data and value '%s' in answer" |
||||
|
% (field, data[field], answer_data[field])) |
||||
|
|
||||
|
|
||||
|
def parse_terminal_answer(real_msg, data): |
||||
|
answer_data = { |
||||
|
'pos_number': real_msg[0:2], |
||||
|
'transaction_result': real_msg[2], |
||||
|
'amount_msg': real_msg[3:11], |
||||
|
'payment_mode': real_msg[11], |
||||
|
'currency_numeric': real_msg[12:15], |
||||
|
'private': real_msg[15:26], |
||||
|
} |
||||
|
print 'answer_data = %s' % answer_data |
||||
|
compare_data_vs_answer(data, answer_data) |
||||
|
return answer_data |
||||
|
|
||||
|
|
||||
|
def get_answer_from_terminal(serial, data): |
||||
|
ascii_names = curses.ascii.controlnames |
||||
|
full_msg_size = 1+2+1+8+1+3+10+1+1 |
||||
|
msg = serial.read(size=full_msg_size) |
||||
|
print '%d bytes read from terminal' % full_msg_size |
||||
|
assert len(msg) == full_msg_size, 'Answer has a wrong size' |
||||
|
if msg[0] != chr(ascii_names.index('STX')): |
||||
|
print 'The first byte of the answer from terminal should be STX' |
||||
|
if msg[-2] != chr(ascii_names.index('ETX')): |
||||
|
print 'The byte before final of the answer from terminal should be ETX' |
||||
|
lrc = msg[-1] |
||||
|
computed_lrc = chr(generate_lrc(msg[1:-1])) |
||||
|
if computed_lrc != lrc: |
||||
|
print 'The LRC of the answer from terminal is wrong' |
||||
|
real_msg = msg[1:-2] |
||||
|
print 'Real answer received = %s' % real_msg |
||||
|
return parse_terminal_answer(real_msg, data) |
||||
|
|
||||
|
|
||||
|
def transaction_start(): |
||||
|
'''This function sends the data to the serial/usb port. |
||||
|
''' |
||||
|
serial = False |
||||
|
try: |
||||
|
print( |
||||
|
'Opening serial port %s for payment terminal with ' |
||||
|
'baudrate %d' % (DEVICE, DEVICE_RATE)) |
||||
|
# IMPORTANT : don't modify timeout=3 seconds |
||||
|
# This parameter is very important ; the Telium spec say |
||||
|
# that we have to wait to up 3 seconds to get LRC |
||||
|
serial = Serial( |
||||
|
DEVICE, DEVICE_RATE, timeout=3) |
||||
|
print 'serial.is_open = %s' % serial.isOpen() |
||||
|
if initialize_msg(serial): |
||||
|
data = prepare_data_to_send() |
||||
|
if not data: |
||||
|
return |
||||
|
send_message(serial, data) |
||||
|
if get_one_byte_answer(serial, 'ACK'): |
||||
|
send_one_byte_signal(serial, 'EOT') |
||||
|
|
||||
|
print "Now expecting answer from Terminal" |
||||
|
if get_one_byte_answer(serial, 'ENQ'): |
||||
|
send_one_byte_signal(serial, 'ACK') |
||||
|
get_answer_from_terminal(serial, data) |
||||
|
send_one_byte_signal(serial, 'ACK') |
||||
|
if get_one_byte_answer(serial, 'EOT'): |
||||
|
print "Answer received from Terminal" |
||||
|
|
||||
|
except Exception, e: |
||||
|
print 'Exception in serial connection: %s' % str(e) |
||||
|
finally: |
||||
|
if serial: |
||||
|
print 'Closing serial port for payment terminal' |
||||
|
serial.close() |
||||
|
|
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
transaction_start() |
@ -0,0 +1 @@ |
|||||
|
from . import pos_customer_display |
@ -0,0 +1,57 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# POS Customer Display module for Odoo |
||||
|
# Copyright (C) 2014 Aurélien DUMAINE |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
{ |
||||
|
'name': 'POS Customer Display', |
||||
|
'version': '0.1', |
||||
|
'category': 'Point Of Sale', |
||||
|
'summary': 'Manage Customer Display device from POS front end', |
||||
|
'description': """ |
||||
|
POS Customer Display |
||||
|
==================== |
||||
|
|
||||
|
This module adds support for Customer Display in the Point of Sale. This |
||||
|
module is designed to be installed on the *main Odoo server*. On the |
||||
|
*POSbox*, you should install the module *hw_customer_display*. |
||||
|
|
||||
|
The number of rows and cols of the Customer Display (usually 2 x 20) |
||||
|
should be configured on the main Odoo server, in the menu Point of Sale |
||||
|
> Configuration > Point of Sales. |
||||
|
|
||||
|
It has been tested with a Bixolon BCD-1100 |
||||
|
(http://www.bixolon.com/html/en/product/product_detail.xhtml?prod_id=61), |
||||
|
but should support most serial and USB-serial LCD displays |
||||
|
out-of-the-box, cf the module *hw_customer_display* for more info. |
||||
|
|
||||
|
This module has been developped during a POS code sprint at Akretion |
||||
|
France from July 7th to July 10th 2014. This module is part of the POS |
||||
|
project of the Odoo Community Association http://odoo-community.org/. |
||||
|
You are invited to become a member and/or get involved in the |
||||
|
Association ! |
||||
|
""", |
||||
|
'author': 'Aurélien DUMAINE', |
||||
|
'depends': ['point_of_sale'], |
||||
|
'data': [ |
||||
|
'pos_customer_display.xml', |
||||
|
'customer_display_view.xml', |
||||
|
], |
||||
|
} |
@ -0,0 +1,16 @@ |
|||||
|
<?xml version="1.0"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
<record id="view_pos_config_form2" model="ir.ui.view"> |
||||
|
<field name="name">pos.config.form.view.inherit</field> |
||||
|
<field name="model">pos.config</field> |
||||
|
<field name="inherit_id" ref="point_of_sale.view_pos_config_form"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<field name="iface_cashdrawer" position="after"> |
||||
|
<field name="iface_customer_display"/> |
||||
|
<field name="customer_display_line_length"/> |
||||
|
</field> |
||||
|
</field> |
||||
|
</record> |
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,87 @@ |
|||||
|
# Translation of Odoo Server. |
||||
|
# This file contains the translation of the following modules: |
||||
|
# * pos_customer_display |
||||
|
# |
||||
|
msgid "" |
||||
|
msgstr "" |
||||
|
"Project-Id-Version: Odoo Server 8.0\n" |
||||
|
"Report-Msgid-Bugs-To: \n" |
||||
|
"POT-Creation-Date: 2014-11-17 11:00+0000\n" |
||||
|
"PO-Revision-Date: 2014-11-17 18:33+0100\n" |
||||
|
"Last-Translator: <>\n" |
||||
|
"Language-Team: \n" |
||||
|
"MIME-Version: 1.0\n" |
||||
|
"Content-Type: text/plain; charset=UTF-8\n" |
||||
|
"Content-Transfer-Encoding: 8bit\n" |
||||
|
"Plural-Forms: \n" |
||||
|
"Language: es\n" |
||||
|
"X-Generator: Poedit 1.6.10\n" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:56 |
||||
|
#, python-format |
||||
|
msgid "Cancel Payment" |
||||
|
msgstr "Pago Cancelado" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: field:pos.config,iface_customer_display:0 |
||||
|
msgid "Customer display" |
||||
|
msgstr "Visor cliente" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:41 |
||||
|
#, python-format |
||||
|
msgid "Delete Item" |
||||
|
msgstr "Producto Eliminado" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: help:pos.config,iface_customer_display:0 |
||||
|
msgid "Display data on the customer display" |
||||
|
msgstr "Mostrar información en el visor de cliente" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: help:pos.config,customer_display_line_length:0 |
||||
|
msgid "Length of the LEDs lines of the customer display" |
||||
|
msgstr "Longitud de las líneas LED en el visor de cliente" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: field:pos.config,customer_display_line_length:0 |
||||
|
msgid "Line length" |
||||
|
msgstr "Longitud de línea " |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:69 |
||||
|
#, python-format |
||||
|
msgid "Next Customer" |
||||
|
msgstr "Próximo Cliente" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:81 |
||||
|
#, python-format |
||||
|
msgid "Point of Sale Closed" |
||||
|
msgstr "Punto Venta Cerrado" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:75 |
||||
|
#, python-format |
||||
|
msgid "Point of Sale Open" |
||||
|
msgstr "Punto Venta Abierto" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:48 |
||||
|
#, python-format |
||||
|
msgid "TOTAL: " |
||||
|
msgstr "TOTAL: " |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:63 |
||||
|
#, python-format |
||||
|
msgid "Your Change:" |
||||
|
msgstr "Su Cambio:" |
@ -0,0 +1,86 @@ |
|||||
|
# Translation of Odoo Server. |
||||
|
# This file contains the translation of the following modules: |
||||
|
# * pos_customer_display |
||||
|
# |
||||
|
msgid "" |
||||
|
msgstr "" |
||||
|
"Project-Id-Version: Odoo Server 8.0\n" |
||||
|
"Report-Msgid-Bugs-To: \n" |
||||
|
"POT-Creation-Date: 2014-10-24 13:45+0000\n" |
||||
|
"PO-Revision-Date: 2014-10-24 13:45+0000\n" |
||||
|
"Last-Translator: <>\n" |
||||
|
"Language-Team: \n" |
||||
|
"MIME-Version: 1.0\n" |
||||
|
"Content-Type: text/plain; charset=UTF-8\n" |
||||
|
"Content-Transfer-Encoding: \n" |
||||
|
"Plural-Forms: \n" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:56 |
||||
|
#, python-format |
||||
|
msgid "Cancel Payment" |
||||
|
msgstr "Paiement annulé" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: field:pos.config,iface_customer_display:0 |
||||
|
msgid "Customer display" |
||||
|
msgstr "Afficheur client" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:41 |
||||
|
#, python-format |
||||
|
msgid "Delete Item" |
||||
|
msgstr "Article supprimé" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: help:pos.config,iface_customer_display:0 |
||||
|
msgid "Display data on the customer display" |
||||
|
msgstr "Utiliser l'afficheur client" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: help:pos.config,customer_display_line_length:0 |
||||
|
msgid "Length of the LEDs lines of the customer display" |
||||
|
msgstr "Longueur des lignes de l'afficheur client: nombre de caractères" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: field:pos.config,customer_display_line_length:0 |
||||
|
msgid "Line length" |
||||
|
msgstr "Longueur des lignes" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:69 |
||||
|
#, python-format |
||||
|
msgid "Next Customer" |
||||
|
msgstr "Client suivant" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:81 |
||||
|
#, python-format |
||||
|
msgid "Point of Sale Closed" |
||||
|
msgstr "Caisse fermée" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:75 |
||||
|
#, python-format |
||||
|
msgid "Point of Sale Open" |
||||
|
msgstr "Caisse ouverte" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:48 |
||||
|
#, python-format |
||||
|
msgid "TOTAL: " |
||||
|
msgstr "TOTAL : " |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:63 |
||||
|
#, python-format |
||||
|
msgid "Your Change:" |
||||
|
msgstr "Monnaie à rendre :" |
||||
|
|
@ -0,0 +1,86 @@ |
|||||
|
# Translation of Odoo Server. |
||||
|
# This file contains the translation of the following modules: |
||||
|
# * pos_customer_display |
||||
|
# |
||||
|
msgid "" |
||||
|
msgstr "" |
||||
|
"Project-Id-Version: Odoo Server 8.0\n" |
||||
|
"Report-Msgid-Bugs-To: \n" |
||||
|
"POT-Creation-Date: 2014-10-24 13:44+0000\n" |
||||
|
"PO-Revision-Date: 2014-10-24 13:44+0000\n" |
||||
|
"Last-Translator: <>\n" |
||||
|
"Language-Team: \n" |
||||
|
"MIME-Version: 1.0\n" |
||||
|
"Content-Type: text/plain; charset=UTF-8\n" |
||||
|
"Content-Transfer-Encoding: \n" |
||||
|
"Plural-Forms: \n" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:56 |
||||
|
#, python-format |
||||
|
msgid "Cancel Payment" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: field:pos.config,iface_customer_display:0 |
||||
|
msgid "Customer display" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:41 |
||||
|
#, python-format |
||||
|
msgid "Delete Item" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: help:pos.config,iface_customer_display:0 |
||||
|
msgid "Display data on the customer display" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: help:pos.config,customer_display_line_length:0 |
||||
|
msgid "Length of the LEDs lines of the customer display" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#: field:pos.config,customer_display_line_length:0 |
||||
|
msgid "Line length" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:69 |
||||
|
#, python-format |
||||
|
msgid "Next Customer" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:81 |
||||
|
#, python-format |
||||
|
msgid "Point of Sale Closed" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:75 |
||||
|
#, python-format |
||||
|
msgid "Point of Sale Open" |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:48 |
||||
|
#, python-format |
||||
|
msgid "TOTAL: " |
||||
|
msgstr "" |
||||
|
|
||||
|
#. module: pos_customer_display |
||||
|
#. openerp-web |
||||
|
#: code:addons/pos_customer_display/static/src/js/customer_display.js:63 |
||||
|
#, python-format |
||||
|
msgid "Your Change:" |
||||
|
msgstr "" |
||||
|
|
@ -0,0 +1,44 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# POS Customer Display module for Odoo |
||||
|
# Copyright (C) 2014 Aurélien DUMAINE |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
import logging |
||||
|
from openerp.osv import fields, orm |
||||
|
|
||||
|
_logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class pos_config(orm.Model): |
||||
|
_name = 'pos.config' |
||||
|
_inherit = 'pos.config' |
||||
|
|
||||
|
_columns = { |
||||
|
'iface_customer_display': fields.boolean( |
||||
|
'Customer display', help="Display data on the customer display"), |
||||
|
'customer_display_line_length': fields.integer( |
||||
|
'Line length', |
||||
|
help="Length of the LEDs lines of the customer display"), |
||||
|
} |
||||
|
|
||||
|
_defaults = { |
||||
|
'customer_display_line_length': 20, |
||||
|
} |
||||
|
|
||||
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
@ -0,0 +1,10 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
<template id="assets_backend" name="point_of_sale assets2" inherit_id="web.assets_backend"> |
||||
|
<xpath expr="." position="inside"> |
||||
|
<script type="text/javascript" src="/pos_customer_display/static/src/js/customer_display.js"></script> |
||||
|
</xpath> |
||||
|
</template> |
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,229 @@ |
|||||
|
/* |
||||
|
POS Customer display module for Odoo |
||||
|
Copyright (C) 2014 Aurélien DUMAINE |
||||
|
Copyright (C) 2014 Barroux Abbey (www.barroux.org) |
||||
|
@author: Aurélien DUMAINE |
||||
|
@author: Alexis de Lattre <alexis.delattre@akretion.com> |
||||
|
@author: Father Odilon (Barroux Abbey) |
||||
|
The licence is in the file __openerp__.py |
||||
|
*/ |
||||
|
|
||||
|
openerp.pos_customer_display = function(instance){ |
||||
|
module = instance.point_of_sale; |
||||
|
|
||||
|
var _t = instance.web._t; |
||||
|
|
||||
|
|
||||
|
module.PosModel = module.PosModel.extend({ |
||||
|
prepare_text_customer_display: function(type, data){ |
||||
|
if (this.config.iface_customer_display != true) |
||||
|
return; |
||||
|
var line_length = this.config.customer_display_line_length || 20; |
||||
|
var currency_rounding = Math.ceil(Math.log(1.0 / this.currency.rounding) / Math.log(10)); |
||||
|
|
||||
|
if (type == 'addProduct'){ |
||||
|
// in order to not recompute qty in options..., we assume that the new ordeLine is the last of the collection
|
||||
|
// addOrderline exists but is not called by addProduct, should we handle it ?
|
||||
|
var line = this.get('selectedOrder').getLastOrderline(); |
||||
|
var price_unit = line.get_unit_price() * (1.0 - (line.get_discount() / 100.0)); |
||||
|
price_unit = price_unit.toFixed(currency_rounding); |
||||
|
var l21 = line.get_quantity_str_with_unit() + ' x ' + price_unit; |
||||
|
var l22 = ' ' + line.get_display_price().toFixed(currency_rounding); |
||||
|
var lines_to_send = new Array( |
||||
|
this.proxy.align_left(line.get_product().display_name, line_length), |
||||
|
this.proxy.align_left(l21, line_length - l22.length) + l22 |
||||
|
); |
||||
|
|
||||
|
} else if (type == 'removeOrderline') { |
||||
|
// first click on the backspace button set the amount to 0 => we can't precise the deleted qunatity and price
|
||||
|
var line = data['line']; |
||||
|
var lines_to_send = new Array( |
||||
|
this.proxy.align_left(_t("Delete Item"), line_length), |
||||
|
this.proxy.align_right(line.get_product().display_name, line_length) |
||||
|
); |
||||
|
|
||||
|
} else if (type == 'addPaymentline') { |
||||
|
var total = this.get('selectedOrder').getTotalTaxIncluded().toFixed(currency_rounding); |
||||
|
var lines_to_send = new Array( |
||||
|
this.proxy.align_left(_t("TOTAL: "), line_length), |
||||
|
this.proxy.align_right(total, line_length) |
||||
|
); |
||||
|
|
||||
|
} else if (type == 'removePaymentline') { |
||||
|
var line = data['line']; |
||||
|
var amount = line.get_amount().toFixed(currency_rounding); |
||||
|
var lines_to_send = new Array( |
||||
|
this.proxy.align_left(_t("Cancel Payment"), line_length), |
||||
|
this.proxy.align_right(line.cashregister.journal_id[1] , line_length - 1 - amount.length) + ' ' + amount |
||||
|
); |
||||
|
|
||||
|
} else if (type == 'update_payment') { |
||||
|
var change = data['change']; |
||||
|
var lines_to_send = new Array( |
||||
|
this.proxy.align_left(_t("Your Change:"), line_length), |
||||
|
this.proxy.align_right(change, line_length) |
||||
|
); |
||||
|
|
||||
|
} else if (type == 'pushOrder') { |
||||
|
var lines_to_send = new Array( |
||||
|
this.proxy.align_center(_t("Next Customer"), line_length), |
||||
|
this.proxy.align_left(' ', line_length) |
||||
|
); |
||||
|
|
||||
|
} else if (type == 'openPOS') { |
||||
|
var lines_to_send = new Array( |
||||
|
this.proxy.align_center(_t("Point of Sale Open"), line_length), |
||||
|
this.proxy.align_left(' ', line_length) |
||||
|
); |
||||
|
|
||||
|
} else if (type = 'closePOS') { |
||||
|
var lines_to_send = new Array( |
||||
|
this.proxy.align_center(_t("Point of Sale Closed"), line_length), |
||||
|
this.proxy.align_left(' ', line_length) |
||||
|
); |
||||
|
} else { |
||||
|
console.warn('Unknown message type'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.proxy.send_text_customer_display(lines_to_send, line_length); |
||||
|
//console.log('prepare_text_customer_display type=' + type + ' | l1=' + lines_to_send[0] + ' | l2=' + lines_to_send[1]);
|
||||
|
}, |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
|
||||
|
module.ProxyDevice = module.ProxyDevice.extend({ |
||||
|
send_text_customer_display: function(data, line_length){ |
||||
|
//FIXME : this function is call twice. The first time, it is not called by prepare_text_customer_display : WHY ?
|
||||
|
if (_.isEmpty(data) || data.length != 2 || data[0].length != line_length || data[1].length != line_length){ |
||||
|
console.warn("send_text_customer_display: Bad Data argument. Data=" + data + ' line_length=' + line_length); |
||||
|
} else { |
||||
|
// alert(JSON.stringify(data));
|
||||
|
return this.message('send_text_customer_display', {'text_to_display' : JSON.stringify(data)}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
align_left: function(string, length){ |
||||
|
if (string) { |
||||
|
if (string.length > length) |
||||
|
{ |
||||
|
return string.substring(0,length); |
||||
|
} |
||||
|
else if (string.length < length) |
||||
|
{ |
||||
|
while(string.length < length) |
||||
|
string = string + ' '; |
||||
|
return string; |
||||
|
} |
||||
|
} |
||||
|
return string; |
||||
|
}, |
||||
|
|
||||
|
align_right: function(string, length){ |
||||
|
if (string) { |
||||
|
if (string.length > length) |
||||
|
{ |
||||
|
return string.substring(0,length); |
||||
|
} |
||||
|
else if (string.length < length) |
||||
|
{ |
||||
|
while(string.length < length) |
||||
|
string = ' ' + string; |
||||
|
return string; |
||||
|
} |
||||
|
} |
||||
|
return string; |
||||
|
}, |
||||
|
|
||||
|
align_center: function(string, length){ |
||||
|
if (string) { |
||||
|
if (string.length > length) |
||||
|
{ |
||||
|
return string.substring(0, length); |
||||
|
} |
||||
|
else if (string.length < length) |
||||
|
{ |
||||
|
ini = (length - string.length) / 2; |
||||
|
while(string.length < length - ini) |
||||
|
string = ' ' + string; |
||||
|
while(string.length < length) |
||||
|
string = string + ' '; |
||||
|
return string; |
||||
|
} |
||||
|
} |
||||
|
return string; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
var _super_addProduct_ = module.Order.prototype.addProduct; |
||||
|
module.Order.prototype.addProduct = function(product, options){ |
||||
|
res = _super_addProduct_.call(this, product, options); |
||||
|
if (product) { |
||||
|
this.pos.prepare_text_customer_display('addProduct', {'product' : product, 'options' : options}); |
||||
|
} |
||||
|
return res; |
||||
|
}; |
||||
|
|
||||
|
var _super_removeOrderline_ = module.Order.prototype.removeOrderline; |
||||
|
module.Order.prototype.removeOrderline = function(line){ |
||||
|
if (line) { |
||||
|
this.pos.prepare_text_customer_display('removeOrderline', {'line' : line}); |
||||
|
} |
||||
|
return _super_removeOrderline_.call(this, line); |
||||
|
}; |
||||
|
|
||||
|
var _super_removePaymentline_ = module.Order.prototype.removePaymentline; |
||||
|
module.Order.prototype.removePaymentline = function(line){ |
||||
|
if (line) { |
||||
|
this.pos.prepare_text_customer_display('removePaymentline', {'line' : line}); |
||||
|
} |
||||
|
return _super_removePaymentline_.call(this, line); |
||||
|
}; |
||||
|
|
||||
|
var _super_addPaymentline_ = module.Order.prototype.addPaymentline; |
||||
|
module.Order.prototype.addPaymentline = function(cashregister){ |
||||
|
res = _super_addPaymentline_.call(this, cashregister); |
||||
|
if (cashregister) { |
||||
|
this.pos.prepare_text_customer_display('addPaymentline', {'cashregister' : cashregister}); |
||||
|
} |
||||
|
return res; |
||||
|
}; |
||||
|
|
||||
|
var _super_pushOrder_ = module.PosModel.prototype.push_order; |
||||
|
module.PosModel.prototype.push_order = function(order){ |
||||
|
res = _super_pushOrder_.call(this, order); |
||||
|
if (order) { |
||||
|
this.prepare_text_customer_display('pushOrder', {'order' : order}); |
||||
|
} |
||||
|
return res; |
||||
|
}; |
||||
|
|
||||
|
var _super_update_payment_summary_ = module.PaymentScreenWidget.prototype.update_payment_summary; |
||||
|
module.PaymentScreenWidget.prototype.update_payment_summary = function(){ |
||||
|
res = _super_update_payment_summary_.call(this); |
||||
|
var currentOrder = this.pos.get('selectedOrder'); |
||||
|
var paidTotal = currentOrder.getPaidTotal(); |
||||
|
var dueTotal = currentOrder.getTotalTaxIncluded(); |
||||
|
var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0; |
||||
|
if (change) { |
||||
|
change_rounded = change.toFixed(2); |
||||
|
this.pos.prepare_text_customer_display('update_payment', {'change': change_rounded}); |
||||
|
} |
||||
|
return res; |
||||
|
}; |
||||
|
|
||||
|
var _super_closePOS_ = module.PosWidget.prototype.close; |
||||
|
module.PosWidget.prototype.close = function(){ |
||||
|
this.pos.prepare_text_customer_display('closePOS', {}); |
||||
|
return _super_closePOS_.call(this); |
||||
|
}; |
||||
|
|
||||
|
var _super_proxy_start_ = module.ProxyStatusWidget.prototype.start; |
||||
|
module.ProxyStatusWidget.prototype.start = function(){ |
||||
|
res = _super_proxy_start_.call(this); |
||||
|
this.pos.prepare_text_customer_display('openPOS', {}); |
||||
|
return res; |
||||
|
}; |
||||
|
|
||||
|
}; |
@ -0,0 +1 @@ |
|||||
|
from . import pos_payment_terminal |
@ -0,0 +1,56 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# POS Payment Terminal module for Odoo |
||||
|
# Copyright (C) 2014 Aurélien DUMAINE |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
{ |
||||
|
'name': 'POS Payment Terminal', |
||||
|
'version': '0.1', |
||||
|
'category': 'Point Of Sale', |
||||
|
'summary': 'Manage Payment Terminal device from POS front end', |
||||
|
'description': """ |
||||
|
POS Payment Terminal |
||||
|
==================== |
||||
|
|
||||
|
This module adds support for credit card reader and checks printer |
||||
|
in the Point of Sale. This module is designed to be installed on the |
||||
|
*main Odoo server*. On the *POSbox*, you should install the module |
||||
|
*hw_x* depending on the protocol implemented in your device. Ingenico |
||||
|
and Sagem devices support the Telium protocol implemented in the |
||||
|
*hw_telium_payment_terminal* module. |
||||
|
|
||||
|
This module support two payment methods : cards and checks. The payment |
||||
|
method should be configured on the main Odoo server, in the menu Point |
||||
|
of Sale > Configuration > Payment Methods. |
||||
|
|
||||
|
This module has been developped during a POS code sprint at Akretion |
||||
|
France from July 7th to July 10th 2014. This module is part of the POS |
||||
|
project of the Odoo Community Association http://odoo-community.org/. |
||||
|
You are invited to become a member and/or get involved in the |
||||
|
Association ! |
||||
|
""", |
||||
|
'author': 'Aurélien DUMAINE', |
||||
|
'depends': ['point_of_sale'], |
||||
|
'data': [ |
||||
|
'pos_payment_terminal.xml', |
||||
|
'pos_payment_terminal_view.xml', |
||||
|
], |
||||
|
'qweb': ['static/src/xml/pos_payment_terminal.xml'], |
||||
|
} |
@ -0,0 +1,38 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# POS Payment Terminal module for Odoo |
||||
|
# Copyright (C) 2014 Aurélien DUMAINE |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
from openerp import models, fields |
||||
|
|
||||
|
|
||||
|
class account_journal(models.Model): |
||||
|
_inherit = 'account.journal' |
||||
|
|
||||
|
payment_mode = fields.Selection( |
||||
|
(('card', 'Card'), ('check', 'Check')), 'Payment mode', |
||||
|
help="Select the payment mode sent to the payment terminal") |
||||
|
|
||||
|
|
||||
|
class pos_config(models.Model): |
||||
|
_inherit = 'pos.config' |
||||
|
|
||||
|
iface_payment_terminal = fields.Boolean( |
||||
|
'Payment Terminal', |
||||
|
help="A payment terminal is available on the Proxy") |
@ -0,0 +1,18 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
|
||||
|
<template id="assets_backend" name="point_of_sale assets" inherit_id="web.assets_backend"> |
||||
|
<xpath expr="." position="inside"> |
||||
|
<script type="text/javascript" src="/pos_payment_terminal/static/src/js/pos_payment_terminal.js"></script> |
||||
|
</xpath> |
||||
|
</template> |
||||
|
|
||||
|
<template id="index" name="pos_payment_terminal index" inherit_id="point_of_sale.index"> |
||||
|
<xpath expr="//link[@id='pos-stylesheet']" position="after"> |
||||
|
<link rel="stylesheet" href="/pos_payment_terminal/static/src/css/pos_payment_terminal.css" id="pos_payment_terminal-stylesheet"/> |
||||
|
</xpath> |
||||
|
</template> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,28 @@ |
|||||
|
<?xml version="1.0"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
|
||||
|
<record id="view_pos_config_form" model="ir.ui.view"> |
||||
|
<field name="name">pos.payment.terminal.config.form</field> |
||||
|
<field name="model">pos.config</field> |
||||
|
<field name="inherit_id" ref="point_of_sale.view_pos_config_form"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<field name="iface_cashdrawer" position="after"> |
||||
|
<field name="iface_payment_terminal"/> |
||||
|
</field> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record id="view_account_journal_pos_user_form" model="ir.ui.view"> |
||||
|
<field name="name">pos.payment.terminal.journal.form</field> |
||||
|
<field name="model">account.journal</field> |
||||
|
<field name="inherit_id" ref="point_of_sale.view_account_journal_pos_user_form"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<field name="self_checkout_payment_method" position="after"> |
||||
|
<field name="payment_mode"/> |
||||
|
</field> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,12 @@ |
|||||
|
.pos .payment-terminal-transaction-start button { |
||||
|
width: 150px; |
||||
|
height: 60px; |
||||
|
font-size: 18px; |
||||
|
cursor: pointer; |
||||
|
text-align:center; |
||||
|
box-sizing: border-box; |
||||
|
-moz-box-sizing: border-box; |
||||
|
left: 105%; |
||||
|
bottom: 10px; |
||||
|
position: absolute; |
||||
|
} |
@ -0,0 +1,50 @@ |
|||||
|
openerp.pos_payment_terminal = function(instance){ |
||||
|
module = instance.point_of_sale; |
||||
|
|
||||
|
module.ProxyDevice = module.ProxyDevice.extend({ |
||||
|
payment_terminal_transaction_start: function(line, currency_iso){ |
||||
|
var data = {'amount' : line.get_amount(), |
||||
|
'currency_iso' : currency_iso, |
||||
|
'payment_mode' : line.cashregister.journal.payment_mode}; |
||||
|
// alert(JSON.stringify(data));
|
||||
|
this.message('payment_terminal_transaction_start', {'payment_info' : JSON.stringify(data)}); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
//TODO make the button bigger and with better name
|
||||
|
|
||||
|
var _super_PaymentScreenWidget_init_ = module.PaymentScreenWidget.prototype.init; |
||||
|
module.PaymentScreenWidget.prototype.init = function(parent, options){ |
||||
|
_super_PaymentScreenWidget_init_.call(this, parent, options); |
||||
|
var self = this; |
||||
|
this.payment_terminal_transaction_start = function(event){ |
||||
|
var node = this; |
||||
|
while (node && !node.classList.contains('paymentline')){ |
||||
|
node = node.parentNode; |
||||
|
} |
||||
|
if (node && !_.isEmpty(node.line) && self.pos.config.iface_payment_terminal){ |
||||
|
self.pos.proxy.payment_terminal_transaction_start(node.line, self.pos.currency.name); |
||||
|
} |
||||
|
event.stopPropagation(); |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
var _super_renderPaymentline_ = module.PaymentScreenWidget.prototype.render_paymentline; |
||||
|
module.PaymentScreenWidget.prototype.render_paymentline = function(line){ |
||||
|
var el_node = _super_renderPaymentline_.call(this, line); |
||||
|
if (line.cashregister.journal.payment_mode && this.pos.config.iface_payment_terminal){ |
||||
|
if (!this.pos.currency.name){ |
||||
|
var self = this; |
||||
|
var currencies = new instance.web.Model('res.currency').query(['name']) |
||||
|
.filter([['id','=',this.pos.currency.id]]) |
||||
|
.all().then(function (currency) { |
||||
|
self.pos.currency.name = currency[0].name; |
||||
|
}); |
||||
|
} |
||||
|
el_node.querySelector('.payment-terminal-transaction-start') |
||||
|
.addEventListener('click', this.payment_terminal_transaction_start); |
||||
|
} |
||||
|
return el_node; |
||||
|
}; |
||||
|
|
||||
|
}; |
@ -0,0 +1,17 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<templates id="template" xml:space="preserve"> |
||||
|
<t t-extend="Paymentline" > |
||||
|
<t t-jquery=".paymentline-input" t-operation="append"> |
||||
|
<t t-if="line.cashregister.journal.payment_mode"> |
||||
|
<!-- <t t-if="pos.config.iface_payment_terminal">--> |
||||
|
<span class="payment-terminal-transaction-start"> |
||||
|
<button> |
||||
|
Start transaction |
||||
|
<!--<t t-esc="line.cashregister.journal.payment_mode"/> --> |
||||
|
</button> |
||||
|
</span> |
||||
|
<!-- </t> --> |
||||
|
</t> |
||||
|
</t> |
||||
|
</t> |
||||
|
</templates> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue