diff --git a/pos_customer_display/README.rst b/pos_customer_display/README.rst index 958b0ffb..91b56cb8 100644 --- a/pos_customer_display/README.rst +++ b/pos_customer_display/README.rst @@ -1,90 +1,8 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 - -==================== -POS Customer Display -==================== - -This module adds support for Customer Display in the Point of Sale. As incredible as it may seem, the Odoo POS doesn't have native support for a customer display, even on Odoo 10. So, if the customer cannot see the screen of the cashier, he will not be able to check the price of each product scanned by the cashier, he won't be able to see the total amount, etc. This module provides a solution to this problem by adding support for good old POS LCDs, which are often made of 2 lines of 20 caracters. - -This module is designed to be installed on the *main Odoo server*. On the -*POSbox*, you should install the module *hw_customer_display*. But you will certainly prefer to use `pywebdriver `__ instead of the POSbox. Compared to the POSbox, Pywebdriver has several advantages: - -* smaller footprint: no need to have a full-blown Odoo with PostgreSQL on the computer of the cashier (or his small Linux-based PC connected to the hardware, like the RaspberryPi for the POSbox), -* availability of an Ubuntu package, for easier deployment, -* native support for the customer display, payment terminal, etc. -* nice test/diagnosis Web interface. - -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 or with minor adaptations in the source code: - -* of the module *hw_customer_display* if you use the POSbox, -* or of the Python lib `pyposdisplay `__ if you use `pywebdriver `__. - -This module has been developped during a POS code sprint at -`Akretion France `_ from July 7th to July 10th 2014. - -Configuration -============= - -To configure this module, go to the menu *Point of Sale > Configuration > Point -of Sale* and edit the point of sale for which you want to enable the LCD: - -* make sure you have configured the *IP address* and port of the POSbox or pywebdriver in the section *Hardware Proxy / PosBox*, -* activate the option *Customer Display*, -* configure the number of caracters on each line of your LCD (20 by default). - -At the end of the page, in the *Customer Display* section, you can customize the *Next customer* message and the *POS closed* message. - -Usage -===== - -Once everything is configured, just start the POS as usual. You will see messages on the LCD when: - -* you start the POS, -* you add or remove a product, -* you press the Payment button: the LCD will display the total amount, -* you enter the amount of cash you receive: the LCD will display the amount of the change to give back, -* you validate an order and go to the next customer, -* you close the POS. - -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/184/10.0 - -Bug Tracker -=========== - -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smashing it by providing a detailed and welcomed feedback. - -Credits -======= - -Contributors ------------- - -* Aurélien Dumaine -* Alexis de Lattre -* Father Odilon (`Barroux Abbey `_) -* Daniel Kraft - -Maintainer ----------- - -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -This module is maintained by the OCA. - -OCA, or the Odoo Community Association, is a nonprofit organization whose -mission is to support the collaborative development of Odoo features and -promote its widespread use. - -To contribute to this module, please visit https://odoo-community.org. +==================================== +Point of Sale - LCD Customer Display +==================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! diff --git a/pos_customer_display/__init__.py b/pos_customer_display/__init__.py index cde864ba..0650744f 100644 --- a/pos_customer_display/__init__.py +++ b/pos_customer_display/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from . import models diff --git a/pos_customer_display/__manifest__.py b/pos_customer_display/__manifest__.py index ed1728a9..95b346e2 100644 --- a/pos_customer_display/__manifest__.py +++ b/pos_customer_display/__manifest__.py @@ -1,20 +1,19 @@ -# -*- coding: utf-8 -*- # © 2014-2016 Aurélien DUMAINE # © 2014-2016 Akretion (Alexis de Lattre ) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - "name": "POS Customer Display", - "version": "10.0.1.0.1", + "name": "Point of Sale - LCD Customer Display", + "version": "12.0.1.0.0", "category": "Point Of Sale", - "summary": "Manage Customer Display device from POS front end", - "author": "Aurélien DUMAINE,Akretion,Odoo Community Association (OCA)", + "summary": "Manage LCD Customer Display device from POS front end", + "author": "Aurélien DUMAINE,GRAP,Akretion,Odoo Community Association (OCA)", "license": "AGPL-3", "depends": ["point_of_sale"], "data": [ - "views/pos_customer_display.xml", - "views/customer_display_view.xml", + "views/assets.xml", + "views/view_pos_config.xml", ], - "demo": ["demo/pos_customer_display_demo.xml"], + "demo": ["demo/pos_config.xml"], "installable": True, } diff --git a/pos_customer_display/demo/pos_config.xml b/pos_customer_display/demo/pos_config.xml new file mode 100644 index 00000000..fae6ffe2 --- /dev/null +++ b/pos_customer_display/demo/pos_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/pos_customer_display/demo/pos_customer_display_demo.xml b/pos_customer_display/demo/pos_customer_display_demo.xml deleted file mode 100644 index 1e34870a..00000000 --- a/pos_customer_display/demo/pos_customer_display_demo.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/pos_customer_display/i18n/pos_customer_display.pot b/pos_customer_display/i18n/pos_customer_display.pot index 000a7099..01f70599 100644 --- a/pos_customer_display/i18n/pos_customer_display.pot +++ b/pos_customer_display/i18n/pos_customer_display.pot @@ -4,8 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 10.0\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-01-11 13:43+0000\n" +"PO-Revision-Date: 2020-01-11 13:43+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -14,130 +16,158 @@ msgstr "" "Plural-Forms: \n" #. module: pos_customer_display -#: model:ir.ui.view,arch_db:pos_customer_display.view_pos_config_form -msgid "Bottom Line" +#: selection:pos.config,customer_display_format:0 +msgid "2 Lines of 20 Characters" msgstr "" #. module: pos_customer_display -#: model:ir.model.fields,help:pos_customer_display.field_pos_config_customer_display_msg_next_l2 -msgid "Bottom line of the message on the customer display which is displayed after starting POS and also after validation of an order" +#. openerp-web +#: code:addons/pos_customer_display/static/src/js/customer_display_2_20.js:93 +#, python-format +msgid "Customer Account" msgstr "" #. module: pos_customer_display -#: model:ir.model.fields,help:pos_customer_display.field_pos_config_customer_display_msg_closed_l2 -msgid "Bottom line of the message on the customer display which is displayed when POS is closed" +#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config__customer_display_format +msgid "Customer Display Size" msgstr "" #. module: pos_customer_display #. openerp-web -#: code:addons/pos_customer_display/static/src/js/customer_display.js:74 +#: code:addons/pos_customer_display/static/src/js/customer_display_2_20.js:60 #, python-format -msgid "Cancel Payment" +msgid "Deleting Line ..." msgstr "" #. module: pos_customer_display -#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config_iface_customer_display -#: model:ir.ui.view,arch_db:pos_customer_display.view_pos_config_form -msgid "Customer Display" +#: model:ir.model.fields,help:pos_customer_display.field_pos_config__iface_customer_display +msgid "Display data on the customer display" msgstr "" #. module: pos_customer_display -#. openerp-web -#: code:addons/pos_customer_display/static/src/js/customer_display.js:59 -#, python-format -msgid "Delete Item" +#: model:ir.model.fields,help:pos_customer_display.field_pos_config__customer_display_msg_next_l1 +msgid "First line of the message on the customer display which is displayed after starting POS and also after validation of an order" msgstr "" #. module: pos_customer_display -#: model:ir.model.fields,help:pos_customer_display.field_pos_config_iface_customer_display -msgid "Display data on the customer display" +#: model:ir.model.fields,help:pos_customer_display.field_pos_config__customer_display_msg_closed_l1 +msgid "First line of the message on the customer display which is displayed when POS is closed" +msgstr "" + +#. module: pos_customer_display +#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config__iface_customer_display +#: model_terms:ir.ui.view,arch_db:pos_customer_display.view_pos_config_form +msgid "LED Customer Display" msgstr "" #. module: pos_customer_display -#: model:ir.model.fields,help:pos_customer_display.field_pos_config_customer_display_line_length +#: model:ir.model.fields,help:pos_customer_display.field_pos_config__customer_display_line_length msgid "Length of the LEDs lines of the customer display" msgstr "" #. module: pos_customer_display -#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config_customer_display_line_length -msgid "Line Length of the Customer Display" +#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config__customer_display_line_length +#: model_terms:ir.ui.view,arch_db:pos_customer_display.view_pos_config_form +msgid "Line Length" msgstr "" #. module: pos_customer_display -#: code:addons/pos_customer_display/models/pos_customer_display.py:46 -#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config_customer_display_msg_next_l2 -#, python-format -msgid "Next Customer (bottom line)" +#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config__customer_display_msg_next_l1 +#: model_terms:ir.ui.view,arch_db:pos_customer_display.view_pos_config_form +msgid "Next Customer (Line 1)" +msgstr "" + +#. module: pos_customer_display +#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config__customer_display_msg_next_l2 +#: model_terms:ir.ui.view,arch_db:pos_customer_display.view_pos_config_form +msgid "Next Customer (Line 2)" msgstr "" #. module: pos_customer_display -#: code:addons/pos_customer_display/models/pos_customer_display.py:44 -#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config_customer_display_msg_next_l1 +#. openerp-web +#: code:addons/pos_customer_display/static/src/js/customer_display_2_20.js:98 #, python-format -msgid "Next Customer (top line)" +msgid "No Customer Account" +msgstr "" + +#. module: pos_customer_display +#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config__customer_display_msg_closed_l1 +#: model_terms:ir.ui.view,arch_db:pos_customer_display.view_pos_config_form +msgid "PoS Closed (Line 1)" msgstr "" #. module: pos_customer_display -#: model:ir.ui.view,arch_db:pos_customer_display.view_pos_config_form -msgid "Next Customer Message" +#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config__customer_display_msg_closed_l2 +msgid "PoS Closed (Line 2)" msgstr "" #. module: pos_customer_display -#: code:addons/pos_customer_display/models/pos_customer_display.py:50 -#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config_customer_display_msg_closed_l2 +#: code:addons/pos_customer_display/models/pos_config.py:64 #, python-format -msgid "POS Closed (bottom line)" +msgid "Point of Sale Closed" +msgstr "" + +#. module: pos_customer_display +#: model:ir.model,name:pos_customer_display.model_pos_config +msgid "Point of Sale Configuration" msgstr "" #. module: pos_customer_display -#: code:addons/pos_customer_display/models/pos_customer_display.py:48 -#: model:ir.model.fields,field_description:pos_customer_display.field_pos_config_customer_display_msg_closed_l1 +#: code:addons/pos_customer_display/models/pos_config.py:60 #, python-format -msgid "POS Closed (top line)" +msgid "Point of Sale Open" msgstr "" #. module: pos_customer_display -#: model:ir.ui.view,arch_db:pos_customer_display.view_pos_config_form -msgid "POS Closed Message" +#: model_terms:ir.ui.view,arch_db:pos_customer_display.view_pos_config_form +msgid "Pos Closed (Line 2)" msgstr "" #. module: pos_customer_display #. openerp-web -#: code:addons/pos_customer_display/static/src/js/customer_display.js:66 +#: code:addons/pos_customer_display/static/src/js/customer_display_2_20.js:80 #, python-format -msgid "TOTAL: " +msgid "Returned: " msgstr "" #. module: pos_customer_display -#: code:addons/pos_customer_display/models/pos_customer_display.py:55 -#, python-format -msgid "The message for customer display '%s' is too long: it has %d chars whereas the maximum is %d chars." +#: model:ir.model.fields,help:pos_customer_display.field_pos_config__customer_display_msg_next_l2 +msgid "Second line of the message on the customer display which is displayed after starting POS and also after validation of an order" msgstr "" #. module: pos_customer_display -#: model:ir.ui.view,arch_db:pos_customer_display.view_pos_config_form -msgid "Top Line" +#: model:ir.model.fields,help:pos_customer_display.field_pos_config__customer_display_msg_closed_l2 +msgid "Second line of the message on the customer display which is displayed when POS is closed" msgstr "" #. module: pos_customer_display -#: model:ir.model.fields,help:pos_customer_display.field_pos_config_customer_display_msg_next_l1 -msgid "Top line of the message on the customer display which is displayed after starting POS and also after validation of an order" +#: code:addons/pos_customer_display/models/pos_config.py:66 +#, python-format +msgid "See you soon!" msgstr "" #. module: pos_customer_display -#: model:ir.model.fields,help:pos_customer_display.field_pos_config_customer_display_msg_closed_l1 -msgid "Top line of the message on the customer display which is displayed when POS is closed" +#: code:addons/pos_customer_display/models/pos_config.py:93 +#, python-format +msgid "The message for customer display '%s' is too long: it has %d chars whereas the maximum is %d chars." msgstr "" #. module: pos_customer_display #. openerp-web -#: code:addons/pos_customer_display/static/src/js/customer_display.js:81 +#: code:addons/pos_customer_display/static/src/js/customer_display_2_20.js:78 #, python-format -msgid "Your Change:" +msgid "To Pay: " msgstr "" #. module: pos_customer_display -#: model:ir.model,name:pos_customer_display.model_pos_config -msgid "pos.config" +#. openerp-web +#: code:addons/pos_customer_display/static/src/js/customer_display_2_20.js:85 +#, python-format +msgid "Total" msgstr "" +#. module: pos_customer_display +#: code:addons/pos_customer_display/models/pos_config.py:62 +#, python-format +msgid "Welcome!" +msgstr "" diff --git a/pos_customer_display/models/__init__.py b/pos_customer_display/models/__init__.py index 22f410a1..db8634ad 100644 --- a/pos_customer_display/models/__init__.py +++ b/pos_customer_display/models/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - -from . import pos_customer_display +from . import pos_config diff --git a/pos_customer_display/models/pos_config.py b/pos_customer_display/models/pos_config.py new file mode 100644 index 00000000..357b5fc8 --- /dev/null +++ b/pos_customer_display/models/pos_config.py @@ -0,0 +1,102 @@ +# © 2014-2016 Aurélien DUMAINE +# © 2015-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class PosConfig(models.Model): + _inherit = "pos.config" + + _CUSTOMER_DISPLAY_FORMAT_SELECTION = [ + ("2_20", "2 Lines of 20 Characters"), + ] + + iface_customer_display = fields.Boolean( + string="LED Customer Display", + help="Display data on the customer display" + ) + + customer_display_format = fields.Selection( + selection=_CUSTOMER_DISPLAY_FORMAT_SELECTION, + string="Customer Display Format", + default="2_20", required=True) + + customer_display_line_length = fields.Integer( + string="Line Length", + compute="_customer_display_line_length", + store=True, + help="Length of the LEDs lines of the customer display", + ) + customer_display_msg_next_l1 = fields.Char( + string="Next Customer (Line 1)", + default=lambda x: x._default_customer_display_msg("next_l1"), + help="First line of the message on the customer display which is " + "displayed after starting POS and also after validation of an order", + ) + customer_display_msg_next_l2 = fields.Char( + string="Next Customer (Line 2)", + default=lambda x: x._default_customer_display_msg("next_l2"), + help="Second line of the message on the customer display which is " + "displayed after starting POS and also after validation of an order", + ) + customer_display_msg_closed_l1 = fields.Char( + string="PoS Closed (Line 1)", + default=lambda x: x._default_customer_display_msg("closed_l1"), + help="First line of the message on the customer display which " + "is displayed when POS is closed", + ) + customer_display_msg_closed_l2 = fields.Char( + string="PoS Closed (Line 2)", + default=lambda x: x._default_customer_display_msg("closed_l1"), + help="Second line of the message on the customer display which " + "is displayed when POS is closed", + ) + + @api.model + def _default_customer_display_msg(self, line): + if line == "next_l1": + return _("Point of Sale Open") + elif line == "next_l2": + return _("Welcome!") + elif line == "closed_l1": + return _("Point of Sale Closed") + elif line == "closed_l2": + return _("See you soon!") + + @api.depends("customer_display_format") + def _compute_customer_display_line_length(self): + for config in self: + config.customer_display_line_length = int( + config.customer_display_format.split("_")[1]) + + @api.constrains( + "iface_customer_display", + "customer_display_format", + "customer_display_msg_next_l1", + "customer_display_msg_next_l2", + "customer_display_msg_closed_l1", + "customer_display_msg_closed_l2", + ) + def _check_customer_display_length(self): + for config in self.filtered(lambda x: x.customer_display_line_length): + maxsize = config.customer_display_line_length + fields_to_check = [ + x for x in self._fields.keys() + if 'customer_display_msg_' in x + ] + for field_name in fields_to_check: + value = getattr(config, field_name) + if value and len(value) > maxsize: + raise ValidationError( + _( + "The message for customer display '%s' is too " + "long: it has %d chars whereas the maximum " + "is %d chars." + ) + % ( + self._fields[field_name].string, + len(value), + maxsize) + ) diff --git a/pos_customer_display/models/pos_customer_display.py b/pos_customer_display/models/pos_customer_display.py deleted file mode 100644 index 845ebca8..00000000 --- a/pos_customer_display/models/pos_customer_display.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -# © 2014-2016 Aurélien DUMAINE -# © 2015-2016 Akretion (Alexis de Lattre ) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import models, fields, api, _ -from odoo.exceptions import ValidationError - - -class PosConfig(models.Model): - _inherit = "pos.config" - - iface_customer_display = fields.Boolean( - string="Customer Display", help="Display data on the customer display" - ) - customer_display_line_length = fields.Integer( - string="Line Length of the Customer Display", - default=20, - help="Length of the LEDs lines of the customer display", - ) - customer_display_msg_next_l1 = fields.Char( - string="Next Customer (top line)", - default="Welcome!", - help="Top line of the message on the customer display which is " - "displayed after starting POS and also after validation of an order", - ) - customer_display_msg_next_l2 = fields.Char( - string="Next Customer (bottom line)", - default="Point of Sale Open", - help="Bottom line of the message on the customer display which is " - "displayed after starting POS and also after validation of an order", - ) - customer_display_msg_closed_l1 = fields.Char( - string="POS Closed (top line)", - default="Point of Sale Closed", - help="Top line of the message on the customer display which " - "is displayed when POS is closed", - ) - customer_display_msg_closed_l2 = fields.Char( - string="POS Closed (bottom line)", - default="See you soon!", - help="Bottom line of the message on the customer display which " - "is displayed when POS is closed", - ) - - @api.constrains( - "customer_display_line_length", - "customer_display_msg_next_l1", - "customer_display_msg_next_l2", - "customer_display_msg_closed_l1", - "customer_display_msg_closed_l2", - ) - def _check_customer_display_length(self): - self.ensure_one() - if self.customer_display_line_length: - maxsize = self.customer_display_line_length - to_check = { - _( - "Next Customer (top line)" - ): self.customer_display_msg_next_l1, - _( - "Next Customer (bottom line)" - ): self.customer_display_msg_next_l2, - _( - "POS Closed (top line)" - ): self.customer_display_msg_closed_l1, - _( - "POS Closed (bottom line)" - ): self.customer_display_msg_closed_l2, - } - for field, msg in to_check.iteritems(): - if msg and len(msg) > maxsize: - raise ValidationError( - _( - "The message for customer display '%s' is too " - "long: it has %d chars whereas the maximum " - "is %d chars." - ) - % (field, len(msg), maxsize) - ) diff --git a/pos_customer_display/readme/CONFIGURE.rst b/pos_customer_display/readme/CONFIGURE.rst new file mode 100644 index 00000000..4a6c99b9 --- /dev/null +++ b/pos_customer_display/readme/CONFIGURE.rst @@ -0,0 +1,8 @@ +To configure this module, +* go to the menu Point of Sale > Configuration > Point of Sale +* edit the point of sale for which you want to enable the LCD: + +* In the IotBox section, activate the option *LCD Customer Display*, +* configure the format of your LCD screen. (2 lines of 20 characters, by default) + +* optionaly, you can customize the *Next customer* message and the *POS closed* message diff --git a/pos_customer_display/readme/CONTRIBUTORS.rst b/pos_customer_display/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..26af69d3 --- /dev/null +++ b/pos_customer_display/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* Aurélien Dumaine +* Alexis de Lattre +* Father Odilon (`Barroux Abbey `_) +* Daniel Kraft +* Sylvain LE GAL ((https://twitter.com/legalsylvain)) diff --git a/pos_customer_display/readme/DESCRIPTION.rst b/pos_customer_display/readme/DESCRIPTION.rst new file mode 100644 index 00000000..4989b1af --- /dev/null +++ b/pos_customer_display/readme/DESCRIPTION.rst @@ -0,0 +1,10 @@ +This module adds support for LCD Customer Display in the Point of Sale. + +It has been tested with + +* Bixolon BCD-1100 (http://www.bixolon.com/html/en/product/product_detail.xhtml?prod_id=61), +* Aures OCD 300 (https://www.aures.com/point-de-vente-equipment-solutions-systemes/ecrans-tactiles-afficheurs/ocd-300-350-afficheur-client-graphique) + +But it should support most serial and USB-serial LCD displays out-of-the-box or with minor adaptations in the source code +* of the module *hw_customer_display* if you use the POSbox, +* or of the Python lib `pyposdisplay `__ if you use `pywebdriver `__. diff --git a/pos_customer_display/readme/INSTALL.rst b/pos_customer_display/readme/INSTALL.rst new file mode 100644 index 00000000..1eb1b989 --- /dev/null +++ b/pos_customer_display/readme/INSTALL.rst @@ -0,0 +1,7 @@ +This module is designed to be installed on the *main Odoo server*. On the +*POSbox*, you should install the module *hw_customer_display*. But you will certainly prefer to use `pywebdriver `__ instead of the POSbox. Compared to the POSbox, Pywebdriver has several advantages: + +* smaller footprint: no need to have a full-blown Odoo with PostgreSQL on the computer of the cashier (or his small Linux-based PC connected to the hardware, like the RaspberryPi for the POSbox), +* availability of an Ubuntu package, for easier deployment, +* native support for the customer display, payment terminal, etc. +* nice test/diagnosis Web interface. diff --git a/pos_customer_display/readme/ROADMAP.rst b/pos_customer_display/readme/ROADMAP.rst new file mode 100644 index 00000000..e69de29b diff --git a/pos_customer_display/readme/USAGE.rst b/pos_customer_display/readme/USAGE.rst new file mode 100644 index 00000000..71279049 --- /dev/null +++ b/pos_customer_display/readme/USAGE.rst @@ -0,0 +1,9 @@ +Once everything is configured, just start the POS as usual. You will see messages on the LCD when: + +* you start the POS, +* you add or remove a product, +* you select or deselect a customer, +* you press the Payment button: the LCD will display the total amount, +* you enter the amount of cash you receive: the LCD will display the amount of the change to give back, +* you validate an order and go to the next customer, +* you close the POS. diff --git a/pos_customer_display/static/description/icon.png b/pos_customer_display/static/description/icon.png index 3a0328b5..b0c338e7 100644 Binary files a/pos_customer_display/static/description/icon.png and b/pos_customer_display/static/description/icon.png differ diff --git a/pos_customer_display/static/src/js/customer_display.js b/pos_customer_display/static/src/js/customer_display.js deleted file mode 100644 index e7ed780b..00000000 --- a/pos_customer_display/static/src/js/customer_display.js +++ /dev/null @@ -1,309 +0,0 @@ -/* - © 2014-2016 Aurélien DUMAINE - © 2014-2016 Barroux Abbey (www.barroux.org) - © 2014-2016 Akretion (www.akretion.com) - @author: Aurélien DUMAINE - @author: Alexis de Lattre - @author: Father Odilon (Barroux Abbey) - License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -*/ - -odoo.define('pos_customer_display', function(require) { - "use strict"; - var chrome = require('point_of_sale.chrome'); - var core = require('web.core'); - var devices = require('point_of_sale.devices'); - var gui = require('point_of_sale.gui'); - var models = require('point_of_sale.models'); - var screens = require('point_of_sale.screens'); - var _t = core._t; - var PosModelSuper = models.PosModel; - - models.PosModel = models.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 = this.currency.decimals; - - if (type == 'add_update_line'){ - var line = data['line']; - var price_unit = line.get_unit_price(); - var discount = line.get_discount(); - if (discount) { - price_unit = price_unit * (1.0 - (discount / 100.0)); - } - price_unit = price_unit.toFixed(currency_rounding); - var qty = line.get_quantity(); - // only display decimals when qty is not an integer - if (qty.toFixed(0) == qty) { - qty = qty.toFixed(0); - } - // only display unit when != Unit(s) - var unit = line.get_unit(); - var unit_display = ''; - if (unit && !unit.is_unit) { - unit_display = unit.name; - } - var l21 = qty + unit_display + ' 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 == 'remove_orderline') { - // 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 == 'add_paymentline') { - var total = this.get('selectedOrder').get_total_with_tax().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 == 'remove_paymentline') { - 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 == 'push_order') { - var lines_to_send = new Array( - this.proxy.align_center(this.config.customer_display_msg_next_l1, line_length), - this.proxy.align_center(this.config.customer_display_msg_next_l2, line_length) - ); - - } else if (type == 'openPOS') { - var lines_to_send = new Array( - this.proxy.align_center(this.config.customer_display_msg_next_l1, line_length), - this.proxy.align_center(this.config.customer_display_msg_next_l2, line_length) - ); - - } else if (type = 'closePOS') { - var lines_to_send = new Array( - this.proxy.align_center(this.config.customer_display_msg_closed_l1, line_length), - this.proxy.align_center(this.config.customer_display_msg_closed_l2, line_length) - ); - } else { - console.warn('Unknown message type'); - return; - } - - this.proxy.send_text_customer_display(lines_to_send, line_length); - }, - - push_order: function(order){ - var res = PosModelSuper.prototype.push_order.call(this, order); - if (order) { - this.prepare_text_customer_display('push_order', {'order' : order}); - } - return res; - }, - - }); - - devices.ProxyDevice = devices.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) - { - string = string.substring(0,length); - } - else if (string.length < length) - { - while(string.length < length) - string = string + ' '; - } - } - else { - string = ' ' - while(string.length < length) - string = ' ' + string; - } - return string; - }, - - align_right: function(string, length){ - if (string) { - if (string.length > length) - { - string = string.substring(0,length); - } - else if (string.length < length) - { - while(string.length < length) - string = ' ' + string; - } - } - else { - string = ' ' - while(string.length < length) - string = ' ' + string; - } - return string; - }, - - align_center: function(string, length){ - if (string) { - if (string.length > length) - { - string = string.substring(0, length); - } - else if (string.length < length) - { - var ini = (length - string.length) / 2; - while(string.length < length - ini) - string = ' ' + string; - while(string.length < length) - string = string + ' '; - } - } - else { - string = ' ' - while(string.length < length) - string = ' ' + string; - } - return string; - }, - }); - - var OrderlineSuper = models.Orderline; - - models.Orderline = models.Orderline.extend({ - /* set_quantity() is called when you force the qty via the dedicated button - AND when you create a new order line via add_product(). - So, when you add a product, we call prepare_text_customer_display() twice... - but I haven't found any good solution to avoid this -- Alexis */ - set_quantity: function(quantity){ - var res = OrderlineSuper.prototype.set_quantity.call(this, quantity); - if (quantity != 'remove') { - var line = this; - if(this.selected){ - this.pos.prepare_text_customer_display('add_update_line', {'line': line}); - } - } - return res; - }, - - set_discount: function(discount){ - var res = OrderlineSuper.prototype.set_discount.call(this, discount); - if (discount) { - var line = this; - if(this.selected){ - this.pos.prepare_text_customer_display('add_update_line', {'line': line}); - } - } - return res; - }, - - set_unit_price: function(price){ - var res = OrderlineSuper.prototype.set_unit_price.call(this, price); - var line = this; - if(this.selected){ - this.pos.prepare_text_customer_display('add_update_line', {'line': line}); - } - return res; - }, - - }); - - var OrderSuper = models.Order; - - models.Order = models.Order.extend({ - add_product: function(product, options){ - var res = OrderSuper.prototype.add_product.call(this, product, options); - if (product) { - var line = this.get_last_orderline(); - this.pos.prepare_text_customer_display('add_update_line', {'line' : line}); - } - return res; - }, - - remove_orderline: function(line){ - if (line) { - this.pos.prepare_text_customer_display('remove_orderline', {'line' : line}); - } - return OrderSuper.prototype.remove_orderline.call(this, line); - }, - - remove_paymentline: function(line){ - if (line) { - this.pos.prepare_text_customer_display('remove_paymentline', {'line' : line}); - } - return OrderSuper.prototype.remove_paymentline.call(this, line); - }, - - add_paymentline: function(cashregister){ - var res = OrderSuper.prototype.add_paymentline.call(this, cashregister); - if (cashregister) { - this.pos.prepare_text_customer_display('add_paymentline', {'cashregister' : cashregister}); - } - return res; - }, - - }); - - screens.PaymentScreenWidget.include({ - render_paymentlines: function(){ - var res = this._super(); - var currentOrder = this.pos.get_order(); - if (currentOrder) { - var paidTotal = currentOrder.get_total_paid(); - var dueTotal = currentOrder.get_total_with_tax(); - var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0; - if (change) { - var change_rounded = change.toFixed(2); - this.pos.prepare_text_customer_display('update_payment', {'change': change_rounded}); - } - } - return res; - }, - }); - - gui.Gui.include({ - close: function(){ - this._super(); - this.pos.prepare_text_customer_display('closePOS', {}); - }, - }); - - chrome.ProxyStatusWidget.include({ - start: function(){ - this._super(); - this.pos.prepare_text_customer_display('openPOS', {}); - }, - }); - - screens.PaymentScreenWidget.include({ - show: function(){ - this._super(); - this.pos.prepare_text_customer_display('add_paymentline', {}); - }, - }); - -}); diff --git a/pos_customer_display/static/src/js/customer_display_2_20.js b/pos_customer_display/static/src/js/customer_display_2_20.js new file mode 100644 index 00000000..a183989b --- /dev/null +++ b/pos_customer_display/static/src/js/customer_display_2_20.js @@ -0,0 +1,111 @@ +/* +Copyright (C) 2015-Today GRAP (http://www.grap.coop) +@author: Sylvain LE GAL (https://twitter.com/legalsylvain) + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +*/ + + +odoo.define('pos_customer_display.customer_display_2_20', function (require) { + "use strict"; + + var core = require('web.core'); + var _t = core._t; + + var CustomerDisplay_2_20 = function(proxy_device){ + this.pos = proxy_device.pos; + this._prepare_line = proxy_device._prepare_line; + + this._prepare_message_welcome = function(){ + return new Array( + this._prepare_line(this.pos.config.customer_display_msg_next_l1, ""), + this._prepare_line(this.pos.config.customer_display_msg_next_l2, ""), + ); + }; + + this._prepare_message_close = function(){ + return new Array( + this._prepare_line(this.pos.config.customer_display_msg_closed_l1, ""), + this._prepare_line(this.pos.config.customer_display_msg_closed_l2, ""), + ); + }; + + this._prepare_message_orderline = function(order_line, action){ + var currency_rounding = this.pos.currency.decimals; + + var product_name = order_line.product.display_name; + var unit_price_str = order_line.get_unit_price().toFixed(currency_rounding); + var total_amount_str = order_line.get_display_price().toFixed(currency_rounding); + var qty = order_line.get_quantity(); + // only display decimals when qty is not an integer + if (qty.toFixed(0) == qty) { + qty = qty.toFixed(0); + } + var discount = order_line.get_discount(); + var discount_str = ""; + if ([ + 'add_line', + 'update_quantity', + 'update_unit_price', + 'update_discount', + ].indexOf(action) !== -1){ + var second_line = String(qty) + " * " + unit_price_str; + if (discount){ + discount_str = " -" + String(discount) + "%"; + } + return new Array( + this._prepare_line(product_name, discount_str), + this._prepare_line(second_line, total_amount_str), + ); + } else if (action === 'delete_line'){ + return new Array( + this._prepare_line(_t("Deleting Line ..."), ""), + this._prepare_line(order_line.product.display_name, ""), + ); + } + }; + + this._prepare_message_payment = function(action){ + var currency_rounding = this.pos.currency.decimals; + var order = this.pos.get_order() + var total = order.get_total_with_tax().toFixed(currency_rounding); + var total_paid = order.get_total_paid().toFixed(currency_rounding); + var total_change = order.get_due().toFixed(currency_rounding); + var total_to_pay = (total - total_paid).toFixed(currency_rounding); + + var remaining_operation_str = ""; + + if (total_paid != 0) { + if (total_to_pay > 0) { + remaining_operation_str = _t("To Pay: ") + String(total_to_pay); + } else if (total_change < 0) { + remaining_operation_str = _t("Returned: ") + String(- total_change); + } + } + + return new Array( + this._prepare_line(_t("Total"), String(total)), + this._prepare_line(remaining_operation_str, ""), + ); + }; + + this._prepare_message_client = function(client){ + if ( client ) { + return new Array( + this._prepare_line(_t("Customer Account"), ""), + this._prepare_line(client.name, ""), + ); + } else { + return new Array( + this._prepare_line(_t("No Customer Account"), ""), + this._prepare_line("", ""), + ); + } + }; + + }; + + return { + CustomerDisplay_2_20: CustomerDisplay_2_20, + }; + +}); diff --git a/pos_customer_display/static/src/js/devices.js b/pos_customer_display/static/src/js/devices.js new file mode 100644 index 00000000..0571dee2 --- /dev/null +++ b/pos_customer_display/static/src/js/devices.js @@ -0,0 +1,90 @@ +/* +Copyright (C) 2015-Today GRAP (http://www.grap.coop) +@author: Sylvain LE GAL (https://twitter.com/legalsylvain) + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +*/ + + +odoo.define('pos_customer_display.devices', function (require) { + "use strict"; + + var devices = require('point_of_sale.devices'); + + var customer_display_2_20 = require('pos_customer_display.customer_display_2_20'); + + var ProxyDeviceSuper = devices.ProxyDevice; + + devices.ProxyDevice = devices.ProxyDevice.extend({ + + + init: function(parent, options){ + var res = ProxyDeviceSuper.prototype.init.call(this, parent, options); + this.customer_display_proxy = false; + return res; + }, + + load_customer_display_format_file: function(){ + // console.log(this.config.customer_display_format); + if (this.pos.config.customer_display_format == "2_20") { + this.customer_display_proxy = new customer_display_2_20.CustomerDisplay_2_20(this); + } else { + console.warn("No Javascript file found for the Customer Display format" + this.config.customer_display_format); + } + }, + + send_text_customer_display: function(data){ + console.log(data); + if (this.customer_display_proxy) { + return this.message( + 'send_text_customer_display', + {'text_to_display' : JSON.stringify(data)} + ); + } + }, + + _prepare_line: function(left_part, right_part){ + var line_length = this.pos.config.customer_display_line_length; + var max_left_length = line_length; + if (right_part.length !== 0) { + max_left_length -= right_part.length; + } + var result = left_part.substring(0, max_left_length - 1); + result = result.padEnd(max_left_length); + if (right_part.length !== 0) { + result += right_part.padStart(line_length - result.length); + } + return result; + }, + + prepare_message_orderline: function(order_line, action){ + if (this.customer_display_proxy) { + return this.customer_display_proxy._prepare_message_orderline(order_line, action); + } + }, + + prepare_message_payment: function(action){ + if (this.customer_display_proxy) { + return this.customer_display_proxy._prepare_message_payment(action); + } + }, + + prepare_message_welcome: function(){ + if (this.customer_display_proxy) { + return this.customer_display_proxy._prepare_message_welcome(); + } + }, + + prepare_message_close: function(){ + if (this.customer_display_proxy) { + return this.customer_display_proxy._prepare_message_close(); + } + }, + + prepare_message_client: function(client){ + if (this.customer_display_proxy) { + return this.customer_display_proxy._prepare_message_client(client); + } + } + + }); +}); diff --git a/pos_customer_display/static/src/js/gui.js b/pos_customer_display/static/src/js/gui.js new file mode 100644 index 00000000..983a662f --- /dev/null +++ b/pos_customer_display/static/src/js/gui.js @@ -0,0 +1,24 @@ +/* +Copyright (C) 2020-Today GRAP (http://www.grap.coop) +@author: Sylvain LE GAL (https://twitter.com/legalsylvain) + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +*/ + + +odoo.define('pos_customer_display.gui', function (require) { + "use strict"; + + var gui = require('point_of_sale.gui'); + + gui.Gui.include({ + + close: function(){ + this.pos.proxy.send_text_customer_display( + this.pos.proxy.prepare_message_closed() + ); + return this._super(); + }, + + }); + +}); diff --git a/pos_customer_display/static/src/js/models.js b/pos_customer_display/static/src/js/models.js new file mode 100644 index 00000000..9f0916e2 --- /dev/null +++ b/pos_customer_display/static/src/js/models.js @@ -0,0 +1,128 @@ +/* +Copyright (C) 2015-Today GRAP (http://www.grap.coop) +@author: Sylvain LE GAL (https://twitter.com/legalsylvain) + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +*/ + +odoo.define('pos_customer_display.models', function (require) { + "use strict"; + + var models = require('point_of_sale.models'); + var OrderSuper = models.Order; + var OrderlineSuper = models.Orderline; + var PosModelSuper = models.PosModel; + + // ////////////////////////////////// + // Overload models.PosModel + // ////////////////////////////////// + models.PosModel = models.PosModel.extend({ + + initialize: function(session, attributes) { + var res = PosModelSuper.prototype.initialize.call(this, session, attributes); + // send_message_customer_display is a variable that allow + // to disable the send of the message to the customer display + // This desing is due to the current design of Odoo PoS. + // 1) during the call_of_init_from_JSON, a call to + // add_product and remove_orderline is done. + // 2) during the call of add_product, a call + // to set_price, set_discount, set_quantity is done. + // To avoid multiple useless messages sends. + // it fixed this @via-alexis comment: + // https://github.com/OCA/pos/blob/10.0/pos_customer_display/ + // static/src/js/customer_display.js#L198 + this.send_message_customer_display = true; + return res; + }, + + after_load_server_data: function(){ + this.proxy.load_customer_display_format_file(); + return PosModelSuper.prototype.after_load_server_data.call(this); + }, + }); + + + // ////////////////////////////////// + // Overload models.Order + // ////////////////////////////////// + models.Order = models.Order.extend({ + + init_from_JSON: function(json) { + this.pos.send_message_customer_display = false; + var res = OrderSuper.prototype.init_from_JSON.call(this, json); + this.pos.send_message_customer_display = true; + return res; + }, + + add_product: function(product, options){ + var send_message = this.pos.send_message_customer_display; + this.pos.send_message_customer_display = false; + var res = OrderSuper.prototype.add_product.call(this, product, options); + if (send_message){ + this.pos.proxy.send_text_customer_display( + this.pos.proxy.prepare_message_orderline( + this.get_last_orderline(), 'add_line')); + this.pos.send_message_customer_display = true; + } + return res; + }, + + }); + + ////////////////////////////////// + // Overload models.Orderline + ////////////////////////////////// + models.Orderline = models.Orderline.extend({ + + set_quantity: function(quantity, keep_price){ + var send_message = this.pos.send_message_customer_display; + var message; + this.pos.send_message_customer_display = false; + + // In the current Odoo design, set_quantity is call to remove line + // so we prepare the message before because after the call + // of super, the line is delete. + if (send_message){ + if (quantity === 'remove') { + message = this.pos.proxy.prepare_message_orderline(this, 'delete_line'); + } + } + var res = OrderlineSuper.prototype.set_quantity.call(this, quantity, keep_price); + if (send_message) { + if (quantity !== 'remove'){ + message = this.pos.proxy.prepare_message_orderline(this, 'update_quantity'); + } + this.pos.send_message_customer_display = true; + this.pos.proxy.send_text_customer_display(message); + } + return res; + }, + + set_discount: function(discount){ + var send_message = this.pos.send_message_customer_display; + this.pos.send_message_customer_display = false; + + var res = OrderlineSuper.prototype.set_discount.call(this, discount); + if (send_message) { + this.pos.send_message_customer_display = true; + this.pos.proxy.send_text_customer_display( + this.pos.proxy.prepare_message_orderline(this, 'update_discount')); + } + return res; + }, + + set_unit_price: function(price){ + var send_message = this.pos.send_message_customer_display; + this.pos.send_message_customer_display = false; + + var res = OrderlineSuper.prototype.set_unit_price.call(this, price); + if (send_message) { + this.pos.send_message_customer_display = true; + this.pos.proxy.send_text_customer_display( + this.pos.proxy.prepare_message_orderline(this, 'update_unit_price')); + } + return res; + }, + + }); + +}); diff --git a/pos_customer_display/static/src/js/screens.js b/pos_customer_display/static/src/js/screens.js new file mode 100644 index 00000000..5db24048 --- /dev/null +++ b/pos_customer_display/static/src/js/screens.js @@ -0,0 +1,51 @@ +/* +Copyright (C) 2015-Today GRAP (http://www.grap.coop) +@author: Sylvain LE GAL (https://twitter.com/legalsylvain) + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +*/ + +odoo.define('pos_customer_display.screens', function (require) { + "use strict"; + + var screens = require('point_of_sale.screens'); + + screens.PaymentScreenWidget.include({ + + render_paymentlines: function() { + if (this.pos.get_order().get_total_with_tax() === 0) { + // Render payment is called each time a new order is created + // (and so when lauching the PoS) + // in that case, we display the welcome message + this.pos.proxy.send_text_customer_display( + this.pos.proxy.prepare_message_welcome() + ); + } else { + this.pos.proxy.send_text_customer_display( + this.pos.proxy.prepare_message_payment() + ); + } + return this._super(); + }, + + }); + + screens.ClientListScreenWidget.include({ + + save_changes: function(){ + if(this.has_client_changed()){ + this.pos.proxy.send_text_customer_display( + this.pos.proxy.prepare_message_client(this.new_client) + ); + } + // we disable the send of message, during the call of _super() + // because when selecting customer, all lines are recomputed + // and so a message is sent for each lines + // causing useless flashes + this.pos.send_message_customer_display = false; + var res = this._super(); + this.pos.send_message_customer_display = true; + return res; + }, + }); + +}); diff --git a/pos_customer_display/views/assets.xml b/pos_customer_display/views/assets.xml new file mode 100644 index 00000000..b0ed39f9 --- /dev/null +++ b/pos_customer_display/views/assets.xml @@ -0,0 +1,14 @@ + + + + - - diff --git a/pos_customer_display/views/view_pos_config.xml b/pos_customer_display/views/view_pos_config.xml new file mode 100644 index 00000000..3a424015 --- /dev/null +++ b/pos_customer_display/views/view_pos_config.xml @@ -0,0 +1,41 @@ + + + + + pos.config + + + +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+ +