Browse Source

Merge pull request #10 from akretion/8.0-201407-pos-code-sprint

[8.0] Add support for Customer LCD, credit card reader and check printer
pull/11/head
Pedro M. Baeza 10 years ago
parent
commit
b372076674
  1. 1
      .travis.yml
  2. 24
      hw_customer_display/__init__.py
  3. 85
      hw_customer_display/__openerp__.py
  4. 24
      hw_customer_display/controllers/__init__.py
  5. 178
      hw_customer_display/controllers/main.py
  6. 68
      hw_customer_display/test-scripts/customer-display-test.py
  7. 24
      hw_telium_payment_terminal/__init__.py
  8. 83
      hw_telium_payment_terminal/__openerp__.py
  9. 24
      hw_telium_payment_terminal/controllers/__init__.py
  10. 284
      hw_telium_payment_terminal/controllers/main.py
  11. 219
      hw_telium_payment_terminal/test-scripts/telium-test.py
  12. 1
      pos_customer_display/__init__.py
  13. 57
      pos_customer_display/__openerp__.py
  14. 16
      pos_customer_display/customer_display_view.xml
  15. 87
      pos_customer_display/i18n/es.po
  16. 86
      pos_customer_display/i18n/fr.po
  17. 86
      pos_customer_display/i18n/pos_customer_display.pot
  18. 44
      pos_customer_display/pos_customer_display.py
  19. 10
      pos_customer_display/pos_customer_display.xml
  20. 229
      pos_customer_display/static/src/js/customer_display.js
  21. 1
      pos_payment_terminal/__init__.py
  22. 56
      pos_payment_terminal/__openerp__.py
  23. 38
      pos_payment_terminal/pos_payment_terminal.py
  24. 18
      pos_payment_terminal/pos_payment_terminal.xml
  25. 28
      pos_payment_terminal/pos_payment_terminal_view.xml
  26. 12
      pos_payment_terminal/static/src/css/pos_payment_terminal.css
  27. 50
      pos_payment_terminal/static/src/js/pos_payment_terminal.js
  28. 17
      pos_payment_terminal/static/src/xml/pos_payment_terminal.xml

1
.travis.yml

@ -17,6 +17,7 @@ install:
- export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH}
- travis_install_nightly - travis_install_nightly
- printf '[options]\n\nrunning_env = dev' > ${HOME}/.openerp_serverrc - printf '[options]\n\nrunning_env = dev' > ${HOME}/.openerp_serverrc
- pip install unidecode pyserial pycountry
script: script:
- travis_run_tests - travis_run_tests

24
hw_customer_display/__init__.py

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

85
hw_customer_display/__openerp__.py

@ -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': [],
}

24
hw_customer_display/controllers/__init__.py

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

178
hw_customer_display/controllers/main.py

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

68
hw_customer_display/test-scripts/customer-display-test.py

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

24
hw_telium_payment_terminal/__init__.py

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

83
hw_telium_payment_terminal/__openerp__.py

@ -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': [],
}

24
hw_telium_payment_terminal/controllers/__init__.py

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

284
hw_telium_payment_terminal/controllers/main.py

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

219
hw_telium_payment_terminal/test-scripts/telium-test.py

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

1
pos_customer_display/__init__.py

@ -0,0 +1 @@
from . import pos_customer_display

57
pos_customer_display/__openerp__.py

@ -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',
],
}

16
pos_customer_display/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>

87
pos_customer_display/i18n/es.po

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

86
pos_customer_display/i18n/fr.po

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

86
pos_customer_display/i18n/pos_customer_display.pot

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

44
pos_customer_display/pos_customer_display.py

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

10
pos_customer_display/pos_customer_display.xml

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

229
pos_customer_display/static/src/js/customer_display.js

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

1
pos_payment_terminal/__init__.py

@ -0,0 +1 @@
from . import pos_payment_terminal

56
pos_payment_terminal/__openerp__.py

@ -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'],
}

38
pos_payment_terminal/pos_payment_terminal.py

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

18
pos_payment_terminal/pos_payment_terminal.xml

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

28
pos_payment_terminal/pos_payment_terminal_view.xml

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

12
pos_payment_terminal/static/src/css/pos_payment_terminal.css

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

50
pos_payment_terminal/static/src/js/pos_payment_terminal.js

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

17
pos_payment_terminal/static/src/xml/pos_payment_terminal.xml

@ -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>
Loading…
Cancel
Save