diff --git a/pos_container/README.rst b/pos_container/README.rst new file mode 100644 index 00000000..28f80f30 --- /dev/null +++ b/pos_container/README.rst @@ -0,0 +1,81 @@ +============= +POS Container +============= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github + :target: https://github.com/OCA/pos/tree/12.0/pos_container + :alt: OCA/pos +.. |badge3| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pos-12-0/pos-12-0-pos_container + :alt: Translate me on Weblate +.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/184/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| + +This module allows to handle use of reusable containers in POS, +this is useful to handle selling product in bulk without having to calculate +the tare of the container. + +Each container is identified by a barcode, the weight is stored in Odoo. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +You have to create a Barcode Nomenclature to handle containers before using the +module. + +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Coop IT Easy SCRLfs + +Contributors +~~~~~~~~~~~~ + + * Pierrick Brun + * Robin Keunen + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/pos `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pos_container/__init__.py b/pos_container/__init__.py new file mode 100644 index 00000000..02179fb0 --- /dev/null +++ b/pos_container/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import models diff --git a/pos_container/__manifest__.py b/pos_container/__manifest__.py new file mode 100644 index 00000000..e0c9c6a3 --- /dev/null +++ b/pos_container/__manifest__.py @@ -0,0 +1,36 @@ +# Copyright 2019 Coop IT Easy SCRLfs +# Robin Keunen +# Pierrick Brun +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).{ +{ + 'name': "POS Container", + 'version': '12.0.1.0.0', + + 'summary': """ + Allows managing pre-weighted containers for bulk shop""", + + "author": "Coop IT Easy SCRLfs, " + "Odoo Community Association (OCA)", + 'license': "AGPL-3", + 'website': "https://github.com/OCA/pos/", + + 'category': 'Point of Sale', + + 'depends': ['point_of_sale'], + + 'data': [ + 'data/product.xml', + 'views/container.xml', + 'templates/templates.xml', + 'security/ir.model.access.csv', + ], + 'demo': [ + 'demo/demo.xml', + ], + + 'qweb': [ + 'static/src/xml/pos.xml', + ], + + 'installable': True, +} diff --git a/pos_container/data/product.xml b/pos_container/data/product.xml new file mode 100644 index 00000000..d09932ee --- /dev/null +++ b/pos_container/data/product.xml @@ -0,0 +1,20 @@ + + + Container without product + + CONTAINER + + + + + This product is used to describe POS order lines having a container but no product yet + + 0 + + + + + + + + diff --git a/pos_container/demo/demo.xml b/pos_container/demo/demo.xml new file mode 100644 index 00000000..ed417ae9 --- /dev/null +++ b/pos_container/demo/demo.xml @@ -0,0 +1,19 @@ + + + Container 1 + 0498765456789 + 0.123 + + + + Container 2 + 0490987654356 + 0.234 + + + + Container 3 + 0490987654398 + 0.567 + + diff --git a/pos_container/i18n/fr.po b/pos_container/i18n/fr.po new file mode 100644 index 00000000..4368977e --- /dev/null +++ b/pos_container/i18n/fr.po @@ -0,0 +1,384 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_container +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-11-19 09:34+0000\n" +"PO-Revision-Date: 2019-11-19 10:42+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: fr\n" +"X-Generator: Poedit 2.2.4\n" + +#. module: pos_container +#: model:product.product,description:pos_container.temporary_container_product +#: model:product.template,description:pos_container.temporary_container_product_product_template +msgid "" +"\n" +" This product is used to describe POS order lines having a container but no product yet\n" +" " +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:161 +#: code:addons/pos_container/static/src/xml/pos.xml:202 +#: code:addons/pos_container/static/src/xml/pos.xml:252 +#, python-format +msgid "% discount" +msgstr "% de réduction" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:127 +#, python-format +msgid "Add a container" +msgstr "Ajouter un contenant" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Alias" +msgstr "Alias" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:184 +#: code:addons/pos_container/static/src/xml/pos.xml:318 +#, python-format +msgid "Automatic Weighing" +msgstr "Pesée automatique" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:124 +#, python-format +msgid "Back" +msgstr "Retour" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:48 +#: model:ir.model.fields,field_description:pos_container.field_pos_container__barcode +#, python-format +msgid "Barcode" +msgstr "Code Barre" + +#. module: pos_container +#: model:ir.model,name:pos_container.model_barcode_rule +msgid "Barcode Rule" +msgstr "Règle de code barre" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:22 +#, python-format +msgid "Cancel" +msgstr "Annuler" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Cashier" +msgstr "Caissier" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Client" +msgstr "Client" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/js/container.js:374 +#: code:addons/pos_container/static/src/xml/pos.xml:6 +#: code:addons/pos_container/static/src/xml/pos.xml:135 +#: model:ir.model.fields,field_description:pos_container.field_pos_order_line__container_id +#, python-format +msgid "Container" +msgstr "Contenants" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_order_line__container_weight +msgid "Container Weight" +msgstr "Poids du contenant" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/js/container.js:80 +#, python-format +msgid "Container deletion" +msgstr "Suppression de contenant" + +#. module: pos_container +#: model:ir.model,name:pos_container.model_pos_container +msgid "Container for bulk items" +msgstr "Contenant pour produits en vrac" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:134 +#, python-format +msgid "Container name:" +msgstr "Nom du contenant:" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Container unit" +msgstr "Unité de contenant" + +#. module: pos_container +#: model:product.product,name:pos_container.temporary_container_product +#: model:product.template,name:pos_container.temporary_container_product_product_template +msgid "Container without product" +msgstr "Contenant sans produit" + +#. module: pos_container +#: model:ir.actions.act_window,name:pos_container.pos_container_action_window +#: model:ir.ui.menu,name:pos_container.pos_container_menu +msgid "Containers" +msgstr "Contenants" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:30 +#, python-format +msgid "Delete container" +msgstr "Supprimer le contenant" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:295 +#: code:addons/pos_container/static/src/xml/pos.xml:335 +#: code:addons/pos_container/static/src/xml/pos.xml:383 +#, python-format +msgid "Discount:" +msgstr "Réduction:" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Discounted Product" +msgstr "Article en promotion" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/js/container.js:81 +#, python-format +msgid "Do you want to delete this container ?\n" +msgstr "Voulez-vous supprimer ce contenant ?\n" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/js/container.js:144 +#, python-format +msgid "Error: Could not Save Changes" +msgstr "Erreur: Impossible de sauvegarder les changements" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:84 +#: code:addons/pos_container/static/src/xml/pos.xml:101 +#, python-format +msgid "Gross :" +msgstr "Poids Brut :" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__id +msgid "ID" +msgstr "ID" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Location" +msgstr "Lieu" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Lot" +msgstr "Lot" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:234 +#: code:addons/pos_container/static/src/xml/pos.xml:366 +#, python-format +msgid "Manual Input" +msgstr "Saisie manuelle" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:47 +#: model:ir.model.fields,field_description:pos_container.field_pos_container__name +#, python-format +msgid "Name" +msgstr "Nom" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__owner_id +msgid "Owner" +msgstr "Propriétaire" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Package" +msgstr "Colis" + +#. module: pos_container +#: model:ir.model,name:pos_container.model_pos_order_line +msgid "Point of Sale Order Lines" +msgstr "Lignes de Commande en Point de Vente" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Priced Product" +msgstr "Article à prix fixe" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:137 +#, python-format +msgid "Save" +msgstr "Sauvegarder" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:26 +#, python-format +msgid "Search Containers" +msgstr "Recherche contenant" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:34 +#, python-format +msgid "Select a container" +msgstr "Sélection contenant" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:12 +#: model:ir.model.fields,field_description:pos_container.field_pos_order_line__tare +#, python-format +msgid "Tare" +msgstr "Tare" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:215 +#: code:addons/pos_container/static/src/xml/pos.xml:266 +#: code:addons/pos_container/static/src/xml/pos.xml:356 +#: code:addons/pos_container/static/src/xml/pos.xml:404 +#, python-format +msgid "Tare :" +msgstr "PT :" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_barcode_rule__type +msgid "Type" +msgstr "Type" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Unit Product" +msgstr "Unité de produit" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:49 +#, python-format +msgid "Weight" +msgstr "Poids" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__weight +msgid "Weight (g)" +msgstr "Poids (kg)" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Weighted Product" +msgstr "Article pesé" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:160 +#: code:addons/pos_container/static/src/xml/pos.xml:201 +#: code:addons/pos_container/static/src/xml/pos.xml:251 +#, python-format +msgid "With a" +msgstr "Avec un" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/js/container.js:138 +#, python-format +msgid "Your Internet connection is probably down." +msgstr "Votre connexion internet est probablement coupée." + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:85 +#: code:addons/pos_container/static/src/xml/pos.xml:102 +#: model:product.product,uom_name:pos_container.temporary_container_product +#: model:product.product,weight_uom_name:pos_container.temporary_container_product +#: model:product.template,uom_name:pos_container.temporary_container_product_product_template +#: model:product.template,weight_uom_name:pos_container.temporary_container_product_product_template +#, python-format +msgid "kg" +msgstr "kg" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:90 +#, python-format +msgid "" +"kg \n" +" -" +msgstr "" +"kg\n" +" -" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:107 +#, python-format +msgid "" +"kg \n" +" - Manual tare" +msgstr "" +"kg \n" +" - Tare manuelle" diff --git a/pos_container/i18n/pos_container.pot b/pos_container/i18n/pos_container.pot new file mode 100644 index 00000000..b863edce --- /dev/null +++ b/pos_container/i18n/pos_container.pot @@ -0,0 +1,377 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_container +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-11-19 09:33+0000\n" +"PO-Revision-Date: 2019-11-19 09:33+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_container +#: model:product.product,description:pos_container.temporary_container_product +#: model:product.template,description:pos_container.temporary_container_product_product_template +msgid "\n" +" This product is used to describe POS order lines having a container but no product yet\n" +" " +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:161 +#: code:addons/pos_container/static/src/xml/pos.xml:202 +#: code:addons/pos_container/static/src/xml/pos.xml:252 +#, python-format +msgid "% discount" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:127 +#, python-format +msgid "Add a container" +msgstr "" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Alias" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:184 +#: code:addons/pos_container/static/src/xml/pos.xml:318 +#, python-format +msgid "Automatic Weighing" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:124 +#, python-format +msgid "Back" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:48 +#: model:ir.model.fields,field_description:pos_container.field_pos_container__barcode +#, python-format +msgid "Barcode" +msgstr "" + +#. module: pos_container +#: model:ir.model,name:pos_container.model_barcode_rule +msgid "Barcode Rule" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:22 +#, python-format +msgid "Cancel" +msgstr "" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Cashier" +msgstr "" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Client" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/js/container.js:374 +#: code:addons/pos_container/static/src/xml/pos.xml:6 +#: code:addons/pos_container/static/src/xml/pos.xml:135 +#: model:ir.model.fields,field_description:pos_container.field_pos_order_line__container_id +#, python-format +msgid "Container" +msgstr "" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_order_line__container_weight +msgid "Container Weight" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/js/container.js:80 +#, python-format +msgid "Container deletion" +msgstr "" + +#. module: pos_container +#: model:ir.model,name:pos_container.model_pos_container +msgid "Container for bulk items" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:134 +#, python-format +msgid "Container name:" +msgstr "" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Container unit" +msgstr "" + +#. module: pos_container +#: model:product.product,name:pos_container.temporary_container_product +#: model:product.template,name:pos_container.temporary_container_product_product_template +msgid "Container without product" +msgstr "" + +#. module: pos_container +#: model:ir.actions.act_window,name:pos_container.pos_container_action_window +#: model:ir.ui.menu,name:pos_container.pos_container_menu +msgid "Containers" +msgstr "" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__create_uid +msgid "Created by" +msgstr "" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__create_date +msgid "Created on" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:30 +#, python-format +msgid "Delete container" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:295 +#: code:addons/pos_container/static/src/xml/pos.xml:335 +#: code:addons/pos_container/static/src/xml/pos.xml:383 +#, python-format +msgid "Discount:" +msgstr "" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Discounted Product" +msgstr "" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__display_name +msgid "Display Name" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/js/container.js:81 +#, python-format +msgid "Do you want to delete this container ?\n" +"" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/js/container.js:144 +#, python-format +msgid "Error: Could not Save Changes" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:84 +#: code:addons/pos_container/static/src/xml/pos.xml:101 +#, python-format +msgid "Gross :" +msgstr "" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__id +msgid "ID" +msgstr "" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container____last_update +msgid "Last Modified on" +msgstr "" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__write_date +msgid "Last Updated on" +msgstr "" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Location" +msgstr "" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Lot" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:234 +#: code:addons/pos_container/static/src/xml/pos.xml:366 +#, python-format +msgid "Manual Input" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:47 +#: model:ir.model.fields,field_description:pos_container.field_pos_container__name +#, python-format +msgid "Name" +msgstr "" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__owner_id +msgid "Owner" +msgstr "" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Package" +msgstr "" + +#. module: pos_container +#: model:ir.model,name:pos_container.model_pos_order_line +msgid "Point of Sale Order Lines" +msgstr "" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Priced Product" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:137 +#, python-format +msgid "Save" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:26 +#, python-format +msgid "Search Containers" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:34 +#, python-format +msgid "Select a container" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:12 +#: model:ir.model.fields,field_description:pos_container.field_pos_order_line__tare +#, python-format +msgid "Tare" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:215 +#: code:addons/pos_container/static/src/xml/pos.xml:266 +#: code:addons/pos_container/static/src/xml/pos.xml:356 +#: code:addons/pos_container/static/src/xml/pos.xml:404 +#, python-format +msgid "Tare :" +msgstr "" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_barcode_rule__type +msgid "Type" +msgstr "" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Unit Product" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:49 +#, python-format +msgid "Weight" +msgstr "" + +#. module: pos_container +#: model:ir.model.fields,field_description:pos_container.field_pos_container__weight +msgid "Weight (g)" +msgstr "" + +#. module: pos_container +#: selection:barcode.rule,type:0 +msgid "Weighted Product" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:160 +#: code:addons/pos_container/static/src/xml/pos.xml:201 +#: code:addons/pos_container/static/src/xml/pos.xml:251 +#, python-format +msgid "With a" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/js/container.js:138 +#, python-format +msgid "Your Internet connection is probably down." +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:85 +#: code:addons/pos_container/static/src/xml/pos.xml:102 +#: model:product.product,uom_name:pos_container.temporary_container_product +#: model:product.product,weight_uom_name:pos_container.temporary_container_product +#: model:product.template,uom_name:pos_container.temporary_container_product_product_template +#: model:product.template,weight_uom_name:pos_container.temporary_container_product_product_template +#, python-format +msgid "kg" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:90 +#, python-format +msgid "kg \n" +" -" +msgstr "" + +#. module: pos_container +#. openerp-web +#: code:addons/pos_container/static/src/xml/pos.xml:107 +#, python-format +msgid "kg \n" +" - Manual tare" +msgstr "" + diff --git a/pos_container/models/__init__.py b/pos_container/models/__init__.py new file mode 100644 index 00000000..b662490e --- /dev/null +++ b/pos_container/models/__init__.py @@ -0,0 +1,3 @@ +from . import container +from . import barcode +from . import pos_order_line diff --git a/pos_container/models/barcode.py b/pos_container/models/barcode.py new file mode 100644 index 00000000..78e1ab03 --- /dev/null +++ b/pos_container/models/barcode.py @@ -0,0 +1,13 @@ +# Copyright 2019 Coop IT Easy SCRLfs +# Pierrick Brun +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models, fields + + +class BarcodeRule(models.Model): + _inherit = 'barcode.rule' + + type = fields.Selection( + selection_add=[('container', 'Container unit')], + ) diff --git a/pos_container/models/container.py b/pos_container/models/container.py new file mode 100644 index 00000000..79abd6dc --- /dev/null +++ b/pos_container/models/container.py @@ -0,0 +1,45 @@ +# Copyright 2019 Coop IT Easy SCRLfs +# Robin Keunen +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models, fields, api + + +class Container(models.Model): + _name = 'pos.container' + _description = 'Container for bulk items' + + name = fields.Char( + string='Name', + ) + barcode = fields.Char( + 'Barcode', + size=13, + ) + weight = fields.Float( + string='Weight (kg)', + ) + owner_id = fields.Many2one( + comodel_name='res.partner', + inverse_name='container_ids', + string='Owner', + ) + + _sql_constraints = [ + ('barcode_uniq', + 'unique(barcode)', + "A barcode can only be assigned to one container !"), + ] + + @api.model + def create_from_ui(self, containers): + # retourne la liste des ids dans le même ordre que la liste fournie + container_ids = [] + for container in containers: + container_id = container.pop('id', False) + if container_id: # Modifying existing container + self.browse(container_id).write(container) + else: + container_id = self.create(container).id + container_ids.append(container_id) + return container_ids diff --git a/pos_container/models/pos_order_line.py b/pos_container/models/pos_order_line.py new file mode 100644 index 00000000..1a0899ca --- /dev/null +++ b/pos_container/models/pos_order_line.py @@ -0,0 +1,15 @@ +# Copyright 2019 Coop IT Easy SCRLfs +# @author Pierrick Brun +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class PosOrderLine(models.Model): + _inherit = 'pos.order.line' + + tare = fields.Float('Tare') + container_weight = fields.Float('Container Weight') + container_id = fields.Many2one( + 'pos.container', + 'Container') diff --git a/pos_container/readme/CONTRIBUTORS.rst b/pos_container/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..872a7eaf --- /dev/null +++ b/pos_container/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ + * Pierrick Brun + * Robin Keunen diff --git a/pos_container/readme/DESCRIPTION.rst b/pos_container/readme/DESCRIPTION.rst new file mode 100644 index 00000000..1bba872c --- /dev/null +++ b/pos_container/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This module allows to handle use of reusable containers in POS, +this is useful to handle selling product in bulk without having to calculate +the tare of the container. + +Each container is identified by a barcode, the weight is stored in Odoo. diff --git a/pos_container/readme/USAGE.rst b/pos_container/readme/USAGE.rst new file mode 100644 index 00000000..f5a5c7ae --- /dev/null +++ b/pos_container/readme/USAGE.rst @@ -0,0 +1,2 @@ +You have to create a Barcode Nomenclature to handle containers before using the +module. diff --git a/pos_container/security/ir.model.access.csv b/pos_container/security/ir.model.access.csv new file mode 100644 index 00000000..9bac7aa8 --- /dev/null +++ b/pos_container/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_pos_container_pos_user,access_pos_container_pos_user,model_pos_container,point_of_sale.group_pos_user,1,1,1,1 +access_pos_container_pos_manager,access_pos_container_pos_manager,model_pos_container,point_of_sale.group_pos_manager,1,1,1,1 diff --git a/pos_container/static/description/index.html b/pos_container/static/description/index.html new file mode 100644 index 00000000..a141c557 --- /dev/null +++ b/pos_container/static/description/index.html @@ -0,0 +1,431 @@ + + + + + + +POS Container + + + +
+

POS Container

+ + +

Beta OCA/pos Translate me on Weblate Try me on Runbot

+

This module allows to handle use of reusable containers in POS, +this is useful to handle selling product in bulk without having to calculate +the tare of the container.

+

Each container is identified by a barcode, the weight is stored in Odoo.

+

Table of contents

+ +
+

Usage

+

You have to create a Barcode Nomenclature to handle containers before using the +module.

+
+
+

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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Coop IT Easy SCRLfs
  • +
+
+
+

Contributors

+
+ +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/pos project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/pos_container/static/src/css/container.css b/pos_container/static/src/css/container.css new file mode 100644 index 00000000..17260fd2 --- /dev/null +++ b/pos_container/static/src/css/container.css @@ -0,0 +1,191 @@ +/* firefox seems to ignore the relative positionning of the subwindow-container + * putting this inside subwindow-container fixes it. + */ +.pos .containerlist-screen .window, +.pos .containerlist-screen .full-content .subwindow{ + display: block; +} +.pos .containerlist-screen .full-content .subwindow-container{ + display: block; + height: 100%; +} +.pos .containerlist-screen .full-content .subwindow.collapsed, +.pos .containerlist-screen .full-content .subwindow-container.collapsed{ + height: auto; +} + +/* The Scale Container Screen */ +.pos .scale-screen .add-container{ + text-align: center; + font-size: 32px; + background: rgb(110,200,155); + color: white; + border-radius: 3px; + padding: 16px; + margin: 16px; + cursor: pointer; +} + +.pos .scale-screen .container-name,.container-barcode{ + text-align: center; + font-size: 25px; + border-radius: 3px; + padding-top: 10px; + padding-bottom:10px; + margin-top: 5px; +} + +/* e) The Container List Screen */ + +.pos .containerlist-screen .container-list{ + font-size: 16px; + width: 100%; + line-height: 40px; +} +.pos .containerlist-screen .container-list th, +.pos .containerlist-screen .container-list td { + padding: 0px 8px; +} +.pos .containerlist-screen .container-list tr{ + transition: all 150ms linear; + background: rgb(230,230,230); +} +.pos .containerlist-screen .container-list thead > tr, +.pos .containerlist-screen .container-list tr:nth-child(even) { + background: rgb(247,247,247); +} +.pos .containerlist-screen .container-list tr.highlight{ + transition: all 150ms linear; + background: rgb(110,200,155) !important; + color: white; +} +.pos .containerlist-screen .container-list tr.lowlight{ + transition: all 150ms linear; + background: rgb(216, 238, 227); +} +.pos .containerlist-screen .container-list tr.lowlight:nth-child(even){ + transition: all 150ms linear; + background: rgb(227, 246, 237); +} +.pos .containerlist-screen .container-details{ + padding: 16px; + border-bottom: solid 5px rgb(110,200,155); +} +.pos .containerlist-screen .container-picture{ + height: 64px; + width: 64px; + border-radius: 32px; + overflow: hidden; + text-align: center; + float: left; + margin-right: 16px; + background: white; + position: relative; +} +.pos .containerlist-screen .container-picture > img { + position: absolute; + top: -9999px; + bottom: -9999px; + right: -9999px; + left: -9999px; + max-height: 64px; + margin: auto; +} +.pos .containerlist-screen .container-picture > .fa { + line-height: 64px; + font-size: 32px; +} +.pos .containerlist-screen .container-picture .image-uploader { + position: absolute; + z-index: 1000; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0; + cursor: pointer; +} +.pos .containerlist-screen .container-name { + font-size: 32px; + line-height: 64px; + margin-bottom:16px; +} +.pos .containerlist-screen .edit-buttons { + position: absolute; + right: 16px; + top: 10px; +} +.pos .containerlist-screen .edit-buttons .button{ + display: inline-block; + margin-left: 16px; + color: rgb(128,128,128); + cursor: pointer; + font-size: 36px; +} +.pos .containerlist-screen .container-details-box{ + position: relative; + font-size: 16px; +} +.pos .containerlist-screen .container-details-left{ + width: 50%; + float: left; +} +.pos .containerlist-screen .container-details-right{ + width: 50%; + float: left; +} +.pos .containerlist-screen .container-detail{ + line-height: 24px; +} +.pos .containerlist-screen .container-detail > .label{ + font-weight: bold; + display: inline-block; + width: 75px; + text-align: right; + margin-right: 8px; +} +.pos .containerlist-screen .container-details input, +.pos .containerlist-screen .container-details select +{ + padding: 4px; + border-radius: 3px; + border: solid 1px #cecbcb; + margin-bottom: 4px; + background: white; + font-family: "Lato","Lucida Grande", Helvetica, Verdana, Arial; + color: #555555; + width: 340px; + font-size: 14px; + box-sizing: border-box; +} +.pos .containerlist-screen .container-details input.container-name { + font-size: 24px; + line-height: 24px; + margin: 18px 6px; + width: 340px; +} +.pos .containerlist-screen .container-detail > .empty{ + opacity: 0.3; +} +.pos .containerlist-screen .searchbox{ + right: auto; + margin-left: -90px; + margin-top:8px; + left: 45%; +} +.pos .containerlist-screen .searchbox input{ + width: 120px; +} +.pos .containerlist-screen .button.delete-container { + left: 50%; + margin-left: 120px; +} + +/* Container Action buttons */ +.pos .control-button.main { + width: 75%; +} +.pos .control-button.second { + width: 15%; + flex-grow: 0; +} diff --git a/pos_container/static/src/js/container.js b/pos_container/static/src/js/container.js new file mode 100644 index 00000000..9f55da23 --- /dev/null +++ b/pos_container/static/src/js/container.js @@ -0,0 +1,431 @@ +/* + Copyright 2019 Coop IT Easy SCRLfs + Robin Keunen + Pierrick Brun + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +*/ + + +odoo.define('pos_container.container', function (require) { + "use strict"; + + var models_and_db = require('pos_container.models_and_db'); + + var screens = require('point_of_sale.screens'); + var gui = require('point_of_sale.gui'); + var models = require('point_of_sale.models'); + + var core = require('web.core'); + var rpc = require('web.rpc'); + var QWeb = core.qweb; + var _t = core._t; + + + var TareButton = screens.ActionButtonWidget.extend({ + template: 'TareButton', + }); + + screens.define_action_button({ + 'name': 'tare', + 'widget': TareButton, + }); + + screens.NumpadWidget.include({ + // to put selected-mode on tare button outside the numpadwidget + changedMode: function() { + this._super(); + if (this.state.get('mode') === 'tare'){ + $('.mode-button[data-mode="tare"]').addClass('selected-mode'); + } + }, + }); + + var ContainerButton = screens.ActionButtonWidget.extend({ + template: 'ContainerButton', + button_click: function(){ + this.gui.show_screen('containerlist'); + } + }); + + screens.define_action_button({ + 'name': 'container', + 'widget': ContainerButton, + }); + + var ContainerListScreenWidget = screens.ScreenWidget.extend({ + template: 'ContainerListScreenWidget', + + init: function(parent, options){ + this._super(parent, options); + this.container_cache = new screens.DomCache(); + }, + + show: function(){ + var self = this; + this._super(); + + this.renderElement(); + this.$('.back').click(function(){ + self.gui.back(); + }); + + this.$('.next').click(function(){ + self.save_changes(); + self.gui.show_screen('products'); + }); + + this.$('.delete-container').click(function(){ + if (self.container){ + self.gui.show_popup('confirm', { + 'title': _t('Container deletion'), + 'body': _t( + 'Do you want to delete this container ?\n').concat( + self.container.name), + confirm: function(){ + self.delete_selected_container(); + }, + }); + } + }); + + var containers = this.pos.db.get_containers_sorted(1000); + this.render_list(containers); + + this.reload_containers(); + + this.$('.container-list-contents').delegate('.container-line', + 'click', function(event){ + self.line_select(event,$(this),$(this).data('id')); + }); + + var search_timeout = null; + + if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){ + this.chrome.widget.keyboard.connect( + this.$('.searchbox input') + ); + } + + this.$('.searchbox input').on('keyup',function(event){ + clearTimeout(search_timeout); + + var query = this.value; + + search_timeout = setTimeout(function(){ + self.perform_search(query,event.which === 13); + },70); + }); + + this.$('.searchbox .search-clear').click(function(){ + self.clear_search(); + }); + }, + delete_selected_container: function(){ + var self = this; + + if (!self.container.id){ + self.deleted_container(self.container.barcode) + } + else { + rpc.query({ + model: 'pos.container', + method: 'unlink', + args: [self.container.id], + }).then(function(){ + self.deleted_container(self.container.barcode); + },function(err,ev){ + ev.preventDefault(); + var error_body = _t('Your Internet connection is probably down.'); + if (err.data) { + var except = err.data; + error_body = except.arguments && except.arguments[0] || except.message || error_body; + } + self.gui.show_popup('error',{ + 'title': _t('Error: Could not Save Changes'), + 'body': error_body, + }); + } + ); + } + }, + deleted_container: function(barcode){ + var self = this; + this.pos.db.remove_containers([barcode]); + this.$('.container-list .highlight').remove(); + this.container = null; + }, + perform_search: function(query, associate_result){ + if(query){ + var containers = this.pos.db.search_container(query); + if ( associate_result && containers.length === 1){ + this.container = containers[0]; + this.save_changes(); + this.gui.back(); + } + this.render_list(containers); + }else{ + var containers = this.pos.db.get_containers_sorted(); + this.render_list(containers); + } + }, + clear_search: function(){ + var containers = this.pos.db.get_containers_sorted(1000); + this.render_list(containers); + this.$('.searchbox input')[0].value = ''; + this.$('.searchbox input').focus(); + }, + render_list: function(containers) { + var contents = this.$el[0].querySelector('.container-list-contents'); + contents.innerHTML = ""; + for(var i = 0, len = Math.min(containers.length,1000); i < len; i++){ + var container = containers[i]; + var containerline_html = QWeb.render('ContainerLine',{widget: this, container:containers[i]}); + var containerline = document.createElement('tbody'); + containerline.innerHTML = containerline_html; + containerline = containerline.childNodes[1]; + + if(containers === this.container) { + containerline.classList.add('highlight'); + } else { + containerline.classList.remove('highlight'); + } + + contents.appendChild(containerline); + } + }, + save_changes: function(){ + this.pos.get_order().add_container(this.container); + }, + toggle_delete_button: function(){ + var $button = this.$('.button.delete-container'); + $button.toggleClass('oe_hidden', !this.container); + }, + toggle_save_button: function(){ + var $button = this.$('.button.next'); + if (this.container) { + $button.text('Set Container'); + } + $button.toggleClass('oe_hidden', !this.container); + }, + line_select: function(event,$line,barcode){ + var container = this.pos.db.get_container_by_barcode(barcode); + this.$('.container-list .lowlight').removeClass('lowlight'); + if ( $line.hasClass('highlight') ){ + $line.removeClass('highlight'); + $line.addClass('lowlight'); + this.toggle_delete_button(); + this.toggle_save_button(); + }else{ + this.$('.container-list .highlight').removeClass('highlight'); + $line.addClass('highlight'); + var y = event.pageY - $line.parent().offset().top; + this.container = container; + this.toggle_delete_button(); + this.toggle_save_button(); + } + }, + + // This fetches container changes on the server, and in case of changes, + // rerenders the affected views + reload_containers: function(){ + var self = this; + return this.pos.load_new_containers().then(function(){ + // containers may have changed in the backend + self.container_cache = new screens.DomCache(); + + self.render_list(self.pos.db.get_containers_sorted(1000)); + + var last_orderline = self.pos.get_order().get_last_orderline(); + + if(last_orderline) { + // update the currently assigned container if it has been changed in db. + var curr_container = last_orderline.get_container(); + } + + if (curr_container) { + last_orderline.set_container( + self.pos.db.get_container_by_barcode(curr_container.barcode)); + } + }); + }, + + close: function(){ + this._super(); + if (this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard) { + this.chrome.widget.keyboard.hide(); + } + }, + + auto_back: true, + }); + + gui.define_screen({ + name:'containerlist', + widget: ContainerListScreenWidget + }); + + // add container barcode scan + screens.ScreenWidget.include({ + barcode_container_action: function(code){ + var self = this; + if (self.pos.scan_container(code)) { + // nothing to do now, the container is added + // as an orderline if found. + } else { + self.gui.show_screen('containerscale', {barcode: code.base_code}); + } + }, + show: function(){ + var self = this; + this._super(); + this.pos.barcode_reader.set_action_callback({ + 'container': _.bind(self.barcode_container_action, self), + }); + }, + }); + + screens.ProductScreenWidget.include({ + // to use Tare Button from outside the NumpadWidget + start: function(){ + this._super(); + var tare_button = $('.mode-button[data-mode="tare"]'); + tare_button.click(_.bind(this.numpad.clickChangeMode, this.numpad)); + }, + // to add a product to a container orderline + click_product: function(product) { + var order = this.pos.get_order(); + var selected_orderline = order.get_selected_orderline(); + if (product.to_weight && selected_orderline && + selected_orderline.product === this.pos.get_container_product()){ + var container = selected_orderline.get_container(); + this.gui.show_screen( + 'scale', + {product: product, + container: container, + old_orderline: selected_orderline}); + } else { + this._super(product); + } + }, + }); + + screens.ScaleScreenWidget.include({ + order_product: function(){ + // Replace the orderline if the product is the placeholder + // container product. + var container = this.gui.get_current_screen_param('container'); + if (container){ + var order = this.pos.get_order(); + order.add_product(this.get_product(),{ quantity: this.weight, price: this.price }); + var orderline = order.get_selected_orderline(); + orderline.set_container(container); + var old_orderline = this.gui.get_current_screen_param( + 'old_orderline'); + if (old_orderline){ + order.remove_orderline(old_orderline); + } + orderline.set_quantity(this.weight); + orderline.set_gross_weight(this.weight + container.weight); + orderline.set_tare_mode('AUTO'); + orderline.trigger('change', orderline); + } else { + this._super(); + var orderline = this.pos.get_order().get_selected_orderline(); + orderline.set_tare_mode('AUTO'); + } + }, + }); + + var ContainerScaleScreenWidget = screens.ScaleScreenWidget.extend({ + template: 'ContainerScaleScreenWidget', + + next_screen: 'products', + previous_screen: 'products', + + init: function(parent, options){ + this._super(parent, options); + }, + + show: function(){ + this._super(); + var self = this; + + this.$('.next,.add-container').click(function(){ + self.create_container(); + }); + + if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){ + this.chrome.widget.keyboard.connect($(this.el.querySelector('.container-name input'))); + } + }, + get_product: function(){ + return this.pos.get_container_product(); + }, + create_container: function(){ + var self = this; + var fields = {}; + + fields['weight'] = this.weight; + + this.$('.container-name .detail').each(function(idx,el){ + fields['name'] = el.value; + + }); + + fields.barcode = this.gui.get_current_screen_param('barcode') || false; + fields.name = fields.name || _t('Container'); + + this.pos.push_container(fields).then( + this.pushed_container(fields["barcode"]) + ); + }, + pushed_container: function(barcode){ + var self = this; + self.gui.show_screen(self.next_screen); + }, + close: function(){ + this._super(); + if (this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard) { + this.chrome.widget.keyboard.hide(); + } + }, + }); + + gui.define_screen({ + name:'containerscale', + widget: ContainerScaleScreenWidget, + }); + + screens.ProductListWidget.include({ + render_product: function(product){ + if(product.barcode != 'CONTAINER'){ + return this._super(product); + } else { + return document.createElement('div'); + } + } + }); + + screens.OrderWidget.include({ + set_value: function(val) { + this._super(val); + var order = this.pos.get_order(); + if (order.get_selected_orderline()) { + var oline = order.get_selected_orderline(); + var mode = this.numpad_state.get('mode'); + if( mode === 'tare'){ + oline.set_tare(val); + } + if( mode === 'quantity' && oline.container) { + oline.set_gross_weight(parseFloat(val) + oline.container.weight); + } + oline.set_tare_mode('MAN'); + } + }, + }); + + return { + ContainerScaleScreenWidget: ContainerScaleScreenWidget, + }; + +}); diff --git a/pos_container/static/src/js/models_and_db.js b/pos_container/static/src/js/models_and_db.js new file mode 100644 index 00000000..883e97a7 --- /dev/null +++ b/pos_container/static/src/js/models_and_db.js @@ -0,0 +1,619 @@ +/* + Copyright 2019 Coop IT Easy SCRLfs + Pierrick Brun + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +*/ + + +odoo.define('pos_container.models_and_db', function (require) { + "use strict"; + + var PosDB = require('point_of_sale.DB'); + var models = require('point_of_sale.models'); + var rpc = require('web.rpc'); + var config = require('web.config'); + + var core = require('web.core'); + var QWeb = core.qweb; + + var utils = require('web.utils'); + var field_utils = require('web.field_utils'); + var round_di = utils.round_decimals; + var round_pr = utils.round_precision; + + // include not available => extend + models.PosModel = models.PosModel.extend({ + get_container_product: function(){ + // assign value if not already assigned. + // Avoids rewriting init function + if (!this.container_product){ + this.container_product = this.db.get_product_by_barcode( + 'CONTAINER'); + } + return this.container_product + }, + scan_container: function(parsed_code){ + var selected_order = this.get_order(); + var container = this.db.get_container_by_barcode( + parsed_code.base_code); + + if(!container){ + return false; + } + + selected_order.add_container(container); + return true; + }, + // reload the list of container, returns as a deferred that resolves if there were + // updated containers, and fails if not + load_new_containers: function(){ + var self = this; + var def = new $.Deferred(); + var fields = _.find(this.models,function(model){ + return model.model === 'pos.container'; + }).fields; + var domain = []; + rpc.query({ + model: 'pos.container', + method: 'search_read', + args: [domain, fields], + }, { + timeout: 3000, + shadow: true, + }) + .then(function(containers){ + if (self.db.add_containers(containers)) { + // check if the partners we got were real updates + def.resolve(); + } else { + def.reject(); + } + }, function(type,err){ def.reject(); }); + return def; + }, + // load placeholder product for containers. + // it is done here to load it even if inactivated. + load_placeholder_product: function(){ + var self = this; + var fields = _.find(this.models,function(model){ + return model.model === 'product.product'; + }).fields; + var domain = [['barcode', '=', 'CONTAINER'], ['active', '=', false]]; + // no need to load it when active because it is already done in standard + return rpc.query({ + model: 'product.product', + method: 'search_read', + args: [domain, fields], + }).then(function(products){ + self.db.add_products(_.map(products, function (product) { + return new models.Product({}, product); + })); + }); + }, + + // saves the container locally and try to send it to the backend. + // it returns a deferred that succeeds after having tried to send the container and all the other pending containers. + push_container: function(container, opts) { + opts = opts || {}; + var self = this; + + if(container){ + this.db.add_containers([container]); + } + + var pushed = new $.Deferred(); + + this.flush_mutex.exec(function(){ + var flushed = self._save_containers_to_server(self.db.get_containers_sorted(), opts); + + flushed.always(function(ids){ + pushed.resolve(); + }); + + return flushed; + }); + return pushed; + }, + + // send an array of containers to the server + // available options: + // - timeout: timeout for the rpc call in ms + // returns a deferred that resolves with the list of + // server generated ids for the sent containers + _save_containers_to_server: function (containers, options) { + var self = this; + var containers= containers.filter(container => !( "id" in container)) + if (!containers || !containers.length) { + var result = $.Deferred(); + result.resolve([]); + return result; + } + + options = options || {}; + var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * containers.length; + + return rpc.query({ + model: 'pos.container', + method: 'create_from_ui', + args: containers, + }, { + timeout: timeout, + }) + .then(function (server_ids) { + //self.db.remove_containers(containers); + _.each(containers, function(container, key){ + container["id"] = server_ids[key] + }); + //self.db.add_containers(containers) + self.set('failed',false); + return server_ids; + }).fail(function (type, error){ + if(error.code === 200 ){ // Business Logic Error, not a connection problem + //if warning do not need to display traceback!! + if (error.data.exception_type == 'warning') { + delete error.data.debug; + } + + // Hide error if already shown before ... + if ((!self.get('failed') || options.show_error) && !options.to_invoice) { + self.gui.show_popup('error-traceback',{ + 'title': error.data.message, + 'body': error.data.debug + }); + } + self.set('failed',error); + } + console.error('Failed to send containers:', containers); + }); + }, + + // wrapper around the _save_to_server that updates the synch status widget + // it is modified to send containers before orders + _flush_orders: function(orders, options) { + var self = this; + this.set('synch',{ state: 'connecting', pending: orders.length}); + + return self._save_containers_to_server(self.db.get_containers_sorted()) + .then(function(container_ids) { + for (var i=0; i < orders.length; i++){ + if (orders[i].data.lines) { + for (var j=0; j < orders[i].data.lines[0].length; j++){ + var orderline = orders[i].data.lines[0][j] + if ( !orderline.container_id && orderline.container_barcode) { + orderline.container_id = self.db.get_container_by_barcode(orderline.container_barcode).id; + delete orderline["container_barcode"] + } + } + } + } + return self._save_to_server(orders, options) + }).done(function (server_ids) { + var pending = self.db.get_orders().length; + + self.set('synch', { + state: pending ? 'connecting' : 'connected', + pending: pending + }); + + return server_ids; + }).fail(function(error, event){ + var pending = self.db.get_orders().length; + if (self.get('failed')) { + self.set('synch', { state: 'error', pending: pending }); + } else { + self.set('synch', { state: 'disconnected', pending: pending }); + } + }); + }, + }); + + models.Order = models.Order.extend({ + add_container: function(container, options){ + if(this._printed){ + this.destroy(); + return this.pos.get_order().add_container(container, options); + } + options = options || {}; + var attr = JSON.parse(JSON.stringify(container)); + attr.pos = this.pos; + attr.order = this; + var product = this.pos.get_container_product(); + var line = new models.Orderline({}, { + pos: this.pos, order: this, product: product}); + + line.set_container(container); + line.set_quantity(0); + this.orderlines.add(line); + + this.select_orderline(this.get_last_orderline()); + }, + has_tare_line: function(mode){ + var orderlines = this.orderlines.models + for(var i=0; i < orderlines.length; i++){ + var line = orderlines[i]; + if(line && line.get_tare_mode() === mode){ + return true; + } + } + return false; + }, + export_for_printing: function(){ + var orderlines = []; + var self = this; + + this.orderlines.each(function(orderline){ + orderlines.push(orderline.export_for_printing()); + }); + + var paymentlines = []; + this.paymentlines.each(function(paymentline){ + paymentlines.push(paymentline.export_for_printing()); + }); + var client = this.get('client'); + var cashier = this.pos.get_cashier(); + var company = this.pos.company; + var shop = this.pos.shop; + var date = new Date(); + + function is_xml(subreceipt){ + return subreceipt ? (subreceipt.split('\n')[0].indexOf('= 0) : false; + } + + function render_xml(subreceipt){ + if (!is_xml(subreceipt)) { + return subreceipt; + } else { + subreceipt = subreceipt.split('\n').slice(1).join('\n'); + var qweb = new QWeb2.Engine(); + qweb.debug = config.debug; + qweb.default_dict = _.clone(QWeb.default_dict); + qweb.add_template(''+subreceipt+''); + + return qweb.render('subreceipt',{'pos':self.pos,'widget':self.pos.chrome,'order':self, 'receipt': receipt}) ; + } + } + + var receipt = { + orderlines: orderlines, + paymentlines: paymentlines, + subtotal: this.get_subtotal(), + total_with_tax: this.get_total_with_tax(), + total_without_tax: this.get_total_without_tax(), + total_tax: this.get_total_tax(), + total_paid: this.get_total_paid(), + total_discount: this.get_total_discount(), + tax_details: this.get_tax_details(), + change: this.get_change(), + name : this.get_name(), + client: client ? client.name : null , + invoice_id: null, //TODO + cashier: cashier ? cashier.name : null, + precision: { + price: 2, + money: 2, + quantity: 3, + }, + date: { + year: date.getFullYear(), + month: date.getMonth(), + date: date.getDate(), // day of the month + day: date.getDay(), // day of the week + hour: date.getHours(), + minute: date.getMinutes() , + isostring: date.toISOString(), + localestring: date.toLocaleString(), + }, + company:{ + email: company.email, + website: company.website, + company_registry: company.company_registry, + contact_address: company.partner_id[1], + vat: company.vat, + vat_label: company.country && company.country.vat_label || '', + name: company.name, + phone: company.phone, + logo: this.pos.company_logo_base64, + }, + shop:{ + name: shop.name, + }, + currency: this.pos.currency, + //custom here + has_tare_mode: { + auto: this.has_tare_line('AUTO'), + manual: this.has_tare_line('MAN'), + } + //custom end + }; + + if (is_xml(this.pos.config.receipt_header)){ + receipt.header = ''; + receipt.header_xml = render_xml(this.pos.config.receipt_header); + } else { + receipt.header = this.pos.config.receipt_header || ''; + } + + if (is_xml(this.pos.config.receipt_footer)){ + receipt.footer = ''; + receipt.footer_xml = render_xml(this.pos.config.receipt_footer); + } else { + receipt.footer = this.pos.config.receipt_footer || ''; + } + + return receipt; + }, + }); + + // Add container to order line + models.Orderline = models.Orderline.extend({ + get_container: function(){ + return this.container; + }, + set_container: function(container){ + this.container = container; + }, + set_tare_mode: function(mode){ + if (['MAN', 'AUTO'].indexOf(mode) != -1){ + this.tare_mode = mode; + this.trigger('change', this); + } + }, + get_tare_mode: function() { + return this.tare_mode; + }, + set_tare: function(tare){ + this.tare = this.get_value_rounded(tare).toFixed(3); + this.container = null; + if (this.gross_weight && this.gross_weight != 'NaN'){ + this.set_quantity(this.gross_weight - parseFloat(this.tare)); + } + else{ + this.set_gross_weight(this.quantity); + this.set_quantity(this.quantity - parseFloat(this.tare)); + } + this.trigger('change', this); + }, + get_tare: function(){ + return this.tare || 0; + }, + get_gross_weight: function(){ + return this.gross_weight; + }, + set_gross_weight: function(weight){ + this.gross_weight = this.get_value_rounded(weight).toFixed(3); + this.trigger('change', this); + }, + set_quantity: function(quantity, keep_price){ + // copied from odoo core + this.order.assert_editable(); + if(quantity === 'remove'){ + this.order.remove_orderline(this); + return; + }else{ + var quant = parseFloat(quantity) || 0; + var unit = this.get_unit(); + if(unit){ + if (unit.rounding) { + this.quantity = round_pr(quant, unit.rounding); + var decimals = this.pos.dp['Product Unit of Measure']; + this.quantity = round_di(this.quantity, decimals) + this.quantityStr = field_utils.format.float(this.quantity, {digits: [69, decimals]}); + } else { + this.quantity = round_pr(quant, 1); + this.quantityStr = this.quantity.toFixed(0); + } + }else{ + this.quantity = quant; + this.quantityStr = '' + this.quantity; + } + } + // just like in sale.order changing the quantity will recompute the unit price + if(! keep_price && ! this.price_manually_set){ + this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity())); + this.order.fix_tax_included_price(this); + } + + // surcharge starts here + if (this.tare){ + this.set_gross_weight(this.quantity + parseFloat(this.tare)); + } + this.trigger('change', this); + }, + get_value_rounded: function(value){ + var value = parseFloat(value) || 0; + var unit = this.get_unit(); + if(unit){ + if (unit.rounding) { + value = round_pr(value, unit.rounding); + var decimals = this.pos.dp['Product Unit of Measure']; + value = round_di(value, decimals) + } else { + value = round_pr(value, 1); + } + } + return value; + }, + export_as_JSON: function(){ + var pack_lot_ids = []; + if (this.has_product_lot){ + this.pack_lot_lines.each(_.bind( function(item) { + return pack_lot_ids.push([0, 0, item.export_as_JSON()]); + }, this)); + } + return { + qty: this.get_quantity(), + price_unit: this.get_unit_price(), + price_subtotal: this.get_price_without_tax(), + price_subtotal_incl: this.get_price_with_tax(), + discount: this.get_discount(), + product_id: this.get_product().id, + tax_ids: [[6, false, _.map(this.get_applicable_taxes(), function(tax){ return tax.id; })]], + id: this.id, + pack_lot_ids: pack_lot_ids, + //custom starts here + tare: this.get_tare() ? this.get_tare() : null, + container_id: this.get_container() ? this.get_container().id : null, + container_barcode: this.get_container() ? this.get_container().barcode : null, + container_weight: this.get_container() ? this.get_container().weight : null, + }; + }, + //used to create a json of the ticket, to be sent to the printer + export_for_printing: function(){ + return { + quantity: this.get_quantity(), + unit_name: this.get_unit().name, + price: this.get_unit_price(), + discount: this.get_discount(), + product_name: this.get_product().display_name, + product_name_wrapped: this.generate_wrapped_product_name(), + price_display : this.get_display_price(), + price_with_tax : this.get_price_with_tax(), + price_without_tax: this.get_price_without_tax(), + tax: this.get_tax(), + product_description: this.get_product().description, + product_description_sale: this.get_product().description_sale, + // extension starts here + container: this.get_container(), + tare: this.get_tare(), + tare_mode: this.get_tare_mode(), + gross_weight: this.get_gross_weight(), + product_barcode: this.get_product().barcode, + }; + }, + }); + + PosDB.include({ + init: function(parent, options) { + this._super(parent, options); + + this.container_sorted = []; + this.container_by_id = {}; + this.container_by_barcode = {}; + this.container_search_string = ""; + this.container_write_date = null; + }, + _container_search_string: function(container){ + + var str = ''; + + if(container.barcode){ + str += '|' + container.barcode; + } + if(container.name) { + str += '|' + container.name; + } + var id = container.id || 0; + str = '' + id + ':' + str.replace(':','') + '\n'; + + return str; + }, + add_containers: function(containers) { + var updated_count = 0; + var new_write_date = ''; + for(var i = 0, len = containers.length; i < len; i++) { + var container = containers[i]; + + if (this.container_write_date && + this.container_by_barcode[container.barcode] && + new Date(this.container_write_date).getTime() + 1000 >= + new Date(container.write_date).getTime() ) { + // FIXME: The write_date is stored with milisec precision in the database + // but the dates we get back are only precise to the second. This means when + // you read containers modified strictly after time X, you get back containers that were + // modified X - 1 sec ago. + continue; + } else if ( new_write_date < container.write_date ) { + new_write_date = container.write_date; + } + if (!this.container_by_barcode[container.barcode]) { + this.container_sorted.push(container.barcode); + } + this.container_by_barcode[container.barcode] = container; + + updated_count += 1; + } + + this.container_write_date = new_write_date || this.container_write_date; + + if (updated_count) { + // If there were updates, we need to completely + // rebuild the search string and the id indexing + + this.container_search_string = ""; + this.container_by_id = {}; + + for (var barcode in this.container_by_barcode) { + var container = this.container_by_barcode[barcode]; + + if(container.id){ + this.container_by_id[container.id] = container; + } + this.container_search_string += this._container_search_string(container); + } + } + return updated_count; + }, + remove_containers: function(barcodes){ + for(var i = 0; i < barcodes.length; i++) { + var container = this.container_by_barcode[barcodes[i]]; + if (container){ + var index_s = this.container_sorted.indexOf(container.barcode); + this.container_sorted.splice(index_s, 1); + delete this.container_by_id[container.id]; + delete this.container_by_barcode[container.barcode]; + } + } + }, + get_container_write_date: function(){ + return this.container_write_date; + }, + get_container_by_id: function(id){ + return this.container_by_id[id]; + }, + get_container_by_barcode: function(barcode){ + return this.container_by_barcode[barcode]; + }, + get_containers_sorted: function(max_count){ + max_count = max_count ? Math.min(this.container_sorted.length, max_count) : this.container_sorted.length; + var containers = []; + for (var i = 0; i < max_count; i++) { + containers.push(this.container_by_barcode[this.container_sorted[i]]); + } + + return containers; + }, + search_container: function(query) { + try { + query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.'); + query = query.replace(' ','.+'); + var re = RegExp("([0-9]+):\\|([0-9]*"+query+"[0-9]*\\|\|[0-9]*\\|.*?"+query+")","gi"); + } catch(e) { + return []; + } + var results = []; + for(var i = 0; i < this.limit; i++) { + var r = re.exec(this.container_search_string); + if(r) { + // r[1] = id, r[2] = barcode + var barcode = r[2].substring(0, r[2].indexOf("\|")); + results.push(this.get_container_by_barcode(barcode)); + } else { + break; + } + } + + return results; + }, + + }); + + models.load_models({ + model: 'pos.container', + fields: ['name','barcode', 'weight'], + loaded: function(self, containers){ + self.db.add_containers(containers); + return self.load_placeholder_product(); + }, + }); + +}); diff --git a/pos_container/static/src/js/tests.js b/pos_container/static/src/js/tests.js new file mode 100644 index 00000000..ec07e100 --- /dev/null +++ b/pos_container/static/src/js/tests.js @@ -0,0 +1,244 @@ +odoo.define('pos_container.tour.tare', function (require) { + "use strict"; + + var Tour = require('web_tour.tour'); + + function click_numpad(num) { + return { + content: "click on numpad button '" + num + "'", + trigger: ".input-button.number-char:contains('"+num+"')" + } + } + + function scan(barcode) { + return { + content: "Scanning barcode " + barcode, + trigger: "input.ean", + run: "text " + barcode + } + } + + function confirm_scan() { + return { + content: "Confirm barcode", + trigger: ".button.barcode", + } + } + + function set_weight(weight) { + return { + content: "Setting weight " + weight, + trigger: "input.weight", + run: "text " + weight + } + } + + function confirm_weight() { + return { + content: "Confirm weight", + trigger: ".button.set_weight", + } + } + + function check_selected_orderline(message, check) { + return { + content: message, + trigger: ".orderline.selected " + check, + run: function () {}, // it's a check + } + } + + + var steps = [{ + content: 'waiting for loading to finish', + trigger: '.o_main_content:has(.loader:hidden)', + run: function () {}, + }, + scan('0499999999998'), + confirm_scan(), + set_weight(0.1), + confirm_weight(), + { + content: "Click on save", + trigger: ".add-container", + }, + // Test a second time with a custom name + scan('0499999999981'), + confirm_scan(), + set_weight(0.2), + confirm_weight(), + { + content: "Set a custom name", + trigger:"input.container-name", + run:"text TOTO", + }, { + content: "Click on save", + trigger: ".add-container", + }, + // Scan du premier contenant + scan('0499999999998'), + confirm_scan(), + check_selected_orderline("Check: empty container in the orderline", ".product-name:contains('Container without product')"), + check_selected_orderline("Check: the name is 'Container'", ".info:contains('Container')"), + check_selected_orderline("Check: the quantity is 0", ".info em:contains('0.000')"), + { + content: "select product", + trigger: ".product:contains('Whiteboard Pen')", //UoM = kg + }, + set_weight(0.2), + confirm_weight(), + { + content: "validate weight", + trigger: ".buy-product", + }, + check_selected_orderline("Check: the name is 'Container'", ".info:contains('Container')"), + check_selected_orderline("Check: orderline in AUTO tare mode", ".pos-right-align:contains('AUTO')"), + check_selected_orderline("Check: orderline's product is the Pen", ".product-name:contains('Whiteboard Pen')"), + check_selected_orderline("Check: the quantity is the tared weight", ".info:contains('0.200')"), + { + content: "click container button", + trigger: ".control-button.o_container_button", + }, { + content: "Search Container TOTO", + trigger: ".searchbox input", + run: "text TOTO", + }, { + content: "Select container TOTO", + trigger: ".container-line:contains('TOTO')", + }, { + content: "Click delete", + trigger: ".button.delete-container", + }, { + content: "Click cancel", + trigger: ".button.cancel", + }, { + content: "Click delete", + trigger: ".button.delete-container", + }, { + content: "Click confirm", + trigger: ".button.confirm", + }, { + content: "Search by barcode", + trigger: ".searchbox input", + run: "text 0499999999998", + }, { + content: "select the searched container", + trigger: ".container-line:contains('Container')", + }, { + content: "confirm selection", + trigger: ".containerlist-screen .next", + }, { + content: "remove orderline quantity", + trigger: ".input-button.numpad-backspace", + }, { + content: "delete orderline", + trigger: ".input-button.numpad-backspace", + }, { + content: "select another product", + trigger: ".product:contains('Desk Organizer')", //UoM = kg + }, + set_weight(0.5), + confirm_weight(), + { + content: "confirm purchase", + trigger: ".buy-product", + }, { + content: "switch numpad to tare mode", + trigger: ".control-button.o_tare_button", + }, + click_numpad(0), + click_numpad('.'), + click_numpad(2), + check_selected_orderline("Check: orderline in MAN tare mode", ".pos-right-align:contains('MAN')"), + check_selected_orderline("Check: orderline's product is the Organizer", ".product-name:contains('Desk Organizer')"), + check_selected_orderline("Check: the quantity is the tared weight", ".info:contains('0.300')"), + { + content: "click orderline auto", + trigger: ".orderline .pos-right-align:contains('AUTO')", + }, { + content: "switch numpad to tare mode", + trigger: ".control-button.o_tare_button", + }, + click_numpad(0), + click_numpad('.'), + click_numpad(2), + check_selected_orderline("Check: orderline in MAN tare mode", ".pos-right-align:contains('MAN')"), + check_selected_orderline("Check: orderline's product is the Pen", ".product-name:contains('Whiteboard Pen')"), + check_selected_orderline("Check: the quantity is the tared weight", ".info em:contains('0.100')"), + { + content: "switch numpad to quantity mode", + trigger: ".mode-button[data-mode='quantity']", + }, + click_numpad(0), + click_numpad('.'), + click_numpad(6), + check_selected_orderline("Check: orderline in MAN tare mode", ".pos-right-align:contains('MAN')"), + check_selected_orderline("Check: orderline's product is the Pen", ".product-name:contains('Whiteboard Pen')"), + check_selected_orderline("Check: the quantity is 0.6", ".info em:contains('0.600')"), + check_selected_orderline("Check: the tare is unchanged", ".info:contains('0.2')"), + check_selected_orderline("Check: the gross weight is the tare + the quantity", ".info:contains('Gross : 0.8 kg')"), + { + content: "Add a unit product", + trigger: ".product:contains('Large Cabinet')", + }, { + content: "click discount", + trigger: ".mode-button[data-mode='discount']", + }, + click_numpad(1), + click_numpad(0), + check_selected_orderline("Check: orderline in MAN tare mode", ".pos-right-align:contains('MAN')"), + check_selected_orderline("Check: the undiscounted price is still 320", ".info:contains('320.00')"), + { + content: "Add a unit product", + trigger: ".product:contains('Large Cabinet')", + }, { + content: "click price change", + trigger: ".mode-button[data-mode='price']", + }, + click_numpad(2), + click_numpad(0), + click_numpad(0), + check_selected_orderline("Check: orderline in MAN tare mode", ".pos-right-align:contains('MAN')"), + // Ajouter une ligne en AUTO + scan('0499999999998'), + confirm_scan(), + { + content: "select product", + trigger: ".product:contains('Whiteboard Pen')", //UoM = kg + }, + set_weight(0.3), + confirm_weight(), + { + content: "validate weight", + trigger: ".buy-product", + }, + { + content: "pay", + trigger: ".button.pay", + }, + click_numpad(2), + click_numpad(0), + click_numpad(0), + click_numpad(0), + { + content: "validate", + trigger: ".button.next", + }]; + + + var autre = [{ + content: "relancer POS", + trigger: "", + }, { + content: "click commandes", + trigger: "", + }, { + content: "sélectionner dernière commande", + trigger: "", + }, { + content: "click reprint", + trigger: "", + }]; + + Tour.register('pos_container', { test: true, url: '/pos/web' }, steps); +}); diff --git a/pos_container/static/src/xml/pos.xml b/pos_container/static/src/xml/pos.xml new file mode 100644 index 00000000..fcd142a2 --- /dev/null +++ b/pos_container/static/src/xml/pos.xml @@ -0,0 +1,442 @@ + + + + + + + + + + + + +
+
+
+ + + Cancel + + + + + + + + + + + + Select a container + + +
+
+
+
+
+
+ + + + + + + + + + +
NameBarcodeWeight
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + +
    +
      + +
    • + Gross : kg +
    • +
      +
    • + : + kg + - + +
    • +
    +
+
+ +
    +
      + +
    • + Gross : kg +
    • +
      +
    • + : + kg + - Manual tare +
    • +
    +
+
+
+ +
+
+
+ + +
+
+
+ + + Back + +

Add a container

+
+
+
+ +
+
+ Container name:
+ +
+
+ Save + +
+
+
+
+
+ + + + + + + + + + + + + + + + +
+ + +
+ With a % discount +
+
+
+ + + +
+ + + + + + + + + +
+
------------------------
+
Automatic Weighing
+
------------------------
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ With a % discount +
+
+
+ + +
+
+ + x + / +
+
+
+ Tare : + +
+
+
+ Tare : + +
+
+ + + + + + + + + +
+
------------------------
+
Manual Input
+
------------------------
+
+
+ + + + + + + + + + + + + + + + + + + + +
+ + +
+ With a % discount +
+
+
+ + + +
+
+ Tare : + +
+
+
+
+ + + + + + + + + + + + + + + + + + Discount: % + + + + + + + + + + x + + + + + + + + + + + +
------------------
+ + Automatic Weighing +
------------------
+ + + + + + + + + + + + + + + Discount: % + + + + + + + + + + x + + + + + + + + + + + + Tare : + + + + + + + Tare : + + + + + + +
------------------
+
+ + Manual Input +
------------------
+ + + + + + + + + + + + + + + Discount: % + + + + + + + + + + x + + + + + + + + + + + + Tare : + + + + + + + + + + +
diff --git a/pos_container/templates/templates.xml b/pos_container/templates/templates.xml new file mode 100644 index 00000000..a7828db8 --- /dev/null +++ b/pos_container/templates/templates.xml @@ -0,0 +1,17 @@ + + + + +