Browse Source

[ADD] pos_container

See Readme for more information.

I'm not the author, just the commiter. Credits to :
- @robin.keunen
- @PierrickBrun
- @vvrossem
pull/498/head
Rémy Taymans 4 years ago
parent
commit
9fbdfc0845
  1. 81
      pos_container/README.rst
  2. 2
      pos_container/__init__.py
  3. 36
      pos_container/__manifest__.py
  4. 20
      pos_container/data/product.xml
  5. 19
      pos_container/demo/demo.xml
  6. 384
      pos_container/i18n/fr.po
  7. 377
      pos_container/i18n/pos_container.pot
  8. 3
      pos_container/models/__init__.py
  9. 13
      pos_container/models/barcode.py
  10. 45
      pos_container/models/container.py
  11. 15
      pos_container/models/pos_order_line.py
  12. 2
      pos_container/readme/CONTRIBUTORS.rst
  13. 5
      pos_container/readme/DESCRIPTION.rst
  14. 2
      pos_container/readme/USAGE.rst
  15. 3
      pos_container/security/ir.model.access.csv
  16. 431
      pos_container/static/description/index.html
  17. 191
      pos_container/static/src/css/container.css
  18. 431
      pos_container/static/src/js/container.js
  19. 619
      pos_container/static/src/js/models_and_db.js
  20. 244
      pos_container/static/src/js/tests.js
  21. 442
      pos_container/static/src/xml/pos.xml
  22. 17
      pos_container/templates/templates.xml
  23. 1
      pos_container/tests/__init__.py
  24. 14
      pos_container/tests/test_pos_container.py
  25. 43
      pos_container/views/container.xml

81
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 <https://github.com/OCA/pos/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 <https://github.com/OCA/pos/issues/new?body=module:%20pos_container%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Coop IT Easy SCRLfs
Contributors
~~~~~~~~~~~~
* Pierrick Brun <pierrick.brun@akretion.com>
* Robin Keunen <robin.keunen@coopiteasy.be>
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 <https://github.com/OCA/pos/tree/12.0/pos_container>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

2
pos_container/__init__.py

@ -0,0 +1,2 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models

36
pos_container/__manifest__.py

@ -0,0 +1,36 @@
# Copyright 2019 Coop IT Easy SCRLfs
# Robin Keunen <robin@coopiteasy.be>
# Pierrick Brun <pierrick.brun@akretion.com>
# 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,
}

20
pos_container/data/product.xml

@ -0,0 +1,20 @@
<odoo>
<record id="temporary_container_product" model="product.product">
<field name="name">Container without product</field>
<!-- The barcode is used to find it from the POS -->
<field name="barcode">CONTAINER</field>
<field name="uom_id" ref="uom.product_uom_kgm"/>
<field name="uom_po_id" ref="uom.product_uom_kgm"/>
<field name="purchase_ok" eval="False"/>
<field name="description">
This product is used to describe POS order lines having a container but no product yet
</field>
<field name="list_price">0</field>
<field name="available_in_pos" eval="True"/>
<field name="to_weight" eval="True"/>
<field name="active" eval="False"/>
</record>
<record id="temporary_container_product_product_template" model="product.template">
<field name="active" eval="False"/>
</record>
</odoo>

19
pos_container/demo/demo.xml

@ -0,0 +1,19 @@
<odoo>
<record id="container_1" model="pos.container">
<field name="name">Container 1</field>
<field name="barcode">0498765456789</field>
<field name="weight">0.123</field>
</record>
<record id="container_2" model="pos.container">
<field name="name">Container 2</field>
<field name="barcode">0490987654356</field>
<field name="weight">0.234</field>
</record>
<record id="container_3" model="pos.container">
<field name="name">Container 3</field>
<field name="barcode">0490987654398</field>
<field name="weight">0.567</field>
</record>
</odoo>

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

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

3
pos_container/models/__init__.py

@ -0,0 +1,3 @@
from . import container
from . import barcode
from . import pos_order_line

13
pos_container/models/barcode.py

@ -0,0 +1,13 @@
# Copyright 2019 Coop IT Easy SCRLfs
# Pierrick Brun <pierrick.brun@akretion.com>
# 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')],
)

45
pos_container/models/container.py

@ -0,0 +1,45 @@
# Copyright 2019 Coop IT Easy SCRLfs
# Robin Keunen <robin@coopiteasy.be>
# 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

15
pos_container/models/pos_order_line.py

@ -0,0 +1,15 @@
# Copyright 2019 Coop IT Easy SCRLfs
# @author Pierrick Brun <pierrick.brun@akretion.com>
# 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')

2
pos_container/readme/CONTRIBUTORS.rst

@ -0,0 +1,2 @@
* Pierrick Brun <pierrick.brun@akretion.com>
* Robin Keunen <robin.keunen@coopiteasy.be>

5
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.

2
pos_container/readme/USAGE.rst

@ -0,0 +1,2 @@
You have to create a Barcode Nomenclature to handle containers before using the
module.

3
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

431
pos_container/static/description/index.html

@ -0,0 +1,431 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.14: http://docutils.sourceforge.net/" />
<title>POS Container</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="pos-container">
<h1 class="title">POS Container</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="https://github.com/OCA/pos/tree/12.0/pos_container"><img alt="OCA/pos" src="https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/pos-12-0/pos-12-0-pos_container"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/184/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>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.</p>
<p>Each container is identified by a barcode, the weight is stored in Odoo.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="id1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>You have to create a Barcode Nomenclature to handle containers before using the
module.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/pos/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/pos/issues/new?body=module:%20pos_container%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id4">Authors</a></h2>
<ul class="simple">
<li>Coop IT Easy SCRLfs</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id5">Contributors</a></h2>
<blockquote>
<ul class="simple">
<li>Pierrick Brun &lt;<a class="reference external" href="mailto:pierrick.brun&#64;akretion.com">pierrick.brun&#64;akretion.com</a>&gt;</li>
<li>Robin Keunen &lt;<a class="reference external" href="mailto:robin.keunen&#64;coopiteasy.be">robin.keunen&#64;coopiteasy.be</a>&gt;</li>
</ul>
</blockquote>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/pos/tree/12.0/pos_container">OCA/pos</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

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

431
pos_container/static/src/js/container.js

@ -0,0 +1,431 @@
/*
Copyright 2019 Coop IT Easy SCRLfs
Robin Keunen <robin@coopiteasy.be>
Pierrick Brun <pierrick.brun@akretion.com>
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,
};
});

619
pos_container/static/src/js/models_and_db.js

@ -0,0 +1,619 @@
/*
Copyright 2019 Coop IT Easy SCRLfs
Pierrick Brun <pierrick.brun@akretion.com>
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('<!DOCTYPE QWEB') >= 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('<templates><t t-name="subreceipt">'+subreceipt+'</t></templates>');
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();
},
});
});

244
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);
});

442
pos_container/static/src/xml/pos.xml

@ -0,0 +1,442 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ContainerButton">
<button class="control-button main o_container_button">
<i class="fa fa-beer" role="img" aria-label="Container" title="Container"/>
Container
</button>
</t>
<t t-name="TareButton">
<button class="control-button second mode-button o_tare_button" data-mode="tare">
Tare
</button>
</t>
<t t-name="ContainerListScreenWidget">
<div class="containerlist-screen screen">
<div class="screen-content">
<section class="top-content">
<span class="button back">
<i class="fa fa-angle-double-left"/>
Cancel
</span>
<span class="searchbox">
<input placeholder="Search Containers"/>
<span class="search-clear"/>
</span>
<span class="searchbox"/>
<span class="button delete-container oe_hidden" role="img" aria-label="Delete container" title="Delete container">
<i class="fa fa-beer"/>
<i class="fa fa-trash"/>
</span>
<span class="button next oe_hidden highlight">
Select a container
<i class="fa fa-angle-double-right"/>
</span>
</section>
<section class="full-content">
<div class="window">
<section class="subwindow">
<div class="subwindow-container">
<div class="subwindow-container-fix touch-scrollable scrollable-y">
<table class="container-list">
<thead>
<tr>
<th>Name</th>
<th>Barcode</th>
<th>Weight</th>
</tr>
</thead>
<tbody class="container-list-contents">
</tbody>
</table>
</div>
</div>
</section>
</div>
</section>
</div>
</div>
</t>
<t t-name="ContainerLine">
<tr class="container-line" t-att-data-id="container.barcode">
<td>
<t t-esc="container.name"/>
</td>
<td>
<t t-esc="container.barcode"/>
</td>
<td>
<t t-esc="container.weight"/>
</td>
</tr>
</t>
<t t-extend='Orderline'>
<t t-jquery='.info-list:last-child' t-operation='prepend'>
<t t-if="line.get_container()">
<ul class="info-list">
<ul class="info-list">
<i><t t-if="line.get_gross_weight()">
<li class="info">
Gross : <t t-esc="line.get_gross_weight()"/> kg
</li>
</t>
<li class="info">
<i class='fa fa-beer'/> :
<t t-esc="line.get_container().weight"/> kg
-
<t t-esc="line.get_container().name"/>
</li></i>
</ul>
</ul>
</t>
<t t-if="line.get_tare()">
<ul class="info-list">
<ul class="info-list">
<i><t t-if="line.get_gross_weight()">
<li class="info">
Gross : <t t-esc="line.get_gross_weight()"/> kg
</li>
</t>
<li class="info">
<i class='fa fa-beer'/> :
<t t-esc="line.get_tare()"/> kg
- Manual tare
</li></i>
</ul>
</ul>
</t>
<div class="pos-right-align">
<t t-esc="line.get_tare_mode()"/>
</div>
</t>
</t>
<t t-name="ContainerScaleScreenWidget">
<div class="scale-screen screen">
<div class="screen-content">
<div class="top-content">
<span class='button back'>
<i class='fa fa-angle-double-left'></i>
Back
</span>
<h1 class='product-name'>Add a container</h1>
</div>
<div class="centered-content">
<div class='weight js-weight'>
<t t-esc="widget.get_product_weight_string()" />
</div>
<div class='container-name'>
<span class='label'>Container name: </span><br/>
<input class='detail container-name' name='container_name' placeholder="Container"></input>
</div>
<div class='add-container'>
Save
<i class='fa fa-angle-double-right'></i>
</div>
</div>
</div>
</div>
</t>
<t t-extend="PosTicket">
<t t-jquery='.receipt-orderlines' t-operation='inner'>
<table>
<colgroup>
<col width='50%' />
<col width='25%' />
<col width='25%' />
</colgroup>
<tr t-foreach="orderlines" t-as="orderline">
<t t-if="orderline.get_tare_mode() == undefined
and orderline.product.barcode != 'CONTAINER'">
<td>
<t t-esc="orderline.get_product().display_name"/>
<t t-if="orderline.get_discount() > 0">
<div class="pos-disc-font">
With a <t t-esc="orderline.get_discount()"/>% discount
</div>
</t>
</td>
<td class="pos-right-align">
<t t-esc="orderline.get_quantity_str_with_unit()"/>
</td>
<td class="pos-right-align">
<t t-esc="widget.format_currency(orderline.get_display_price())"/>
</td>
</t>
</tr>
</table>
<t t-if="order.has_tare_line('AUTO')">
<table>
<colgroup>
<col width='20%' />
<col width='60%' />
<col width='20%' />
</colgroup>
<td></td>
<td>
<center>------------------------</center>
<center>Automatic Weighing</center>
<center>------------------------</center>
</td>
</table>
</t>
<table>
<colgroup>
<col width='50%' />
<col width='25%' />
<col width='25%' />
</colgroup>
<tr t-foreach="orderlines" t-as="orderline">
<t t-if="orderline.get_tare_mode() == 'AUTO'">
<tr>
<td>
<t t-esc="orderline.get_product().display_name"/>
<t t-if="orderline.get_discount() > 0">
<div class="pos-disc-font">
With a <t t-esc="orderline.get_discount()"/>% discount
</div>
</t>
</td>
<td/>
<td class="pos-right-align">
<t t-esc="widget.format_currency(orderline.get_display_price())"/>
</td>
</tr>
<tr>
<td>
<div style="margin-left:5%">
<t t-esc="orderline.get_quantity_str_with_unit()"/>
x
<t t-esc="widget.format_currency(orderline.get_unit_display_price())"/>/<t t-esc="orderline.get_unit().name"/>
</div>
</td>
</tr>
<t t-if="orderline.get_container()">
<tr>
<td>
<div style="margin-left:5%">
Tare : <t t-esc="orderline.get_container().weight"/>
<t t-esc="orderline.get_unit().name"/>
</div>
</td>
</tr>
</t>
<t t-if="orderline.get_tare()">
<tr>
<td>
<div style="margin-left:5%">
Tare : <t t-esc="orderline.get_tare()"/>
<t t-esc="orderline.get_unit().name"/>
</div>
</td>
</tr>
</t>
</t>
</tr>
</table>
<t t-if="order.has_tare_line('MAN')">
<table>
<colgroup>
<col width='20%' />
<col width='60%' />
<col width='20%' />
</colgroup>
<td></td>
<td>
<center>------------------------</center>
<center>Manual Input</center>
<center>------------------------</center>
</td>
</table>
</t>
<table>
<colgroup>
<col width='50%' />
<col width='25%' />
<col width='25%' />
</colgroup>
<tr t-foreach="orderlines" t-as="orderline">
<t t-if="orderline.get_tare_mode() == 'MAN'">
<tr>
<td>
<t t-esc="orderline.get_product().display_name"/>
<t t-if="orderline.get_discount() > 0">
<div class="pos-disc-font">
With a <t t-esc="orderline.get_discount()"/>% discount
</div>
</t>
</td>
<td class="pos-right-align">
<t t-esc="orderline.get_quantity_str_with_unit()"/>
</td>
<td class="pos-right-align">
<t t-esc="widget.format_currency(orderline.get_display_price())"/>
</td>
</tr>
<t t-if="orderline.get_tare()">
<tr>
<td>
<div style="margin-left:5%">
Tare : <t t-esc="orderline.tare"/>
<t t-esc="orderline.get_unit().name"/>
</div>
</td>
</tr>
</t>
</t>
</tr>
</table>
</t>
</t>
<t t-extend="XmlReceipt">
<t t-jquery='.orderlines' t-operation='inner'>
<t t-foreach='receipt.orderlines' t-as='line'>
<t t-if="line.tare_mode == undefined and line.product_barcode != 'CONTAINER'">
<t t-set='simple' t-value='line.discount === 0 and line.unit_name === "Unit(s)" and line.quantity === 1' />
<t t-if='simple'>
<line>
<left><t t-esc='line.product_name_wrapped[0]' /></left>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value></right>
</line>
<t t-call="XmlReceiptWrappedProductNameLines"/>
</t>
<t t-if='!simple'>
<line><left><t t-esc='line.product_name_wrapped[0]' /></left></line>
<t t-call="XmlReceiptWrappedProductNameLines"/>
<t t-if='line.discount !== 0'>
<line indent='1'><left>Discount: <t t-esc='line.discount' />%</left></line>
</t>
<line indent='1'>
<left>
<value t-att-value-decimals='pos.dp["Product Unit of Measure"]' value-autoint='on'>
<t t-esc='line.quantity' />
</value>
<t t-if='line.unit_name !== "Unit(s)"'>
<t t-esc='line.unit_name' />
</t>
x
<value t-att-value-decimals='pos.dp["Product Price"]'>
<t t-esc='line.price' />
</value>
</left>
<right>
<value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value>
</right>
</line>
</t>
</t>
</t>
<div>------------------</div>
<t t-if="receipt.has_tare_mode.auto">
Automatic Weighing
<div>------------------</div>
<t t-foreach='receipt.orderlines' t-as='line'>
<t t-if="line.tare_mode == 'AUTO'">
<t t-set='simple' t-value='line.discount === 0 and line.unit_name === "Unit(s)" and line.quantity === 1' />
<t t-if='simple'>
<line>
<left><t t-esc='line.product_name_wrapped[0]' /></left>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value></right>
</line>
<t t-call="XmlReceiptWrappedProductNameLines"/>
</t>
<t t-if='!simple'>
<line><left><t t-esc='line.product_name_wrapped[0]' /></left></line>
<t t-call="XmlReceiptWrappedProductNameLines"/>
<t t-if='line.discount !== 0'>
<line indent='1'><left>Discount: <t t-esc='line.discount' />%</left></line>
</t>
<line indent='1'>
<left>
<value t-att-value-decimals='pos.dp["Product Unit of Measure"]' value-autoint='on'>
<t t-esc='line.quantity' />
</value>
<t t-if='line.unit_name !== "Unit(s)"'>
<t t-esc='line.unit_name' />
</t>
x
<value t-att-value-decimals='pos.dp["Product Price"]'>
<t t-esc='line.price' />
</value>
</left>
<right>
<value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value>
</right>
</line>
<t t-if='line.container'>
<line indent='1'>
<left>
Tare : <t t-esc='line.container.weight' /><t t-esc='line.unit_name' />
</left>
</line>
</t>
<t t-if='line.tare'>
<line indent='1'>
<left>
Tare : <t t-esc='line.tare' /><t t-esc='line.unit_name' />
</left>
</line>
</t>
</t>
</t>
</t>
<div>------------------</div>
</t>
<t t-if="receipt.has_tare_mode.manual">
Manual Input
<div>------------------</div>
<t t-foreach='receipt.orderlines' t-as='line'>
<t t-if="line.tare_mode == 'MAN'">
<t t-set='simple' t-value='line.discount === 0 and line.unit_name === "Unit(s)" and line.quantity === 1' />
<t t-if='simple'>
<line>
<left><t t-esc='line.product_name_wrapped[0]' /></left>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value></right>
</line>
<t t-call="XmlReceiptWrappedProductNameLines"/>
</t>
<t t-if='!simple'>
<line><left><t t-esc='line.product_name_wrapped[0]' /></left></line>
<t t-call="XmlReceiptWrappedProductNameLines"/>
<t t-if='line.discount !== 0'>
<line indent='1'><left>Discount: <t t-esc='line.discount' />%</left></line>
</t>
<line indent='1'>
<left>
<value t-att-value-decimals='pos.dp["Product Unit of Measure"]' value-autoint='on'>
<t t-esc='line.quantity' />
</value>
<t t-if='line.unit_name !== "Unit(s)"'>
<t t-esc='line.unit_name' />
</t>
x
<value t-att-value-decimals='pos.dp["Product Price"]'>
<t t-esc='line.price' />
</value>
</left>
<right>
<value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value>
</right>
</line>
<t t-if='line.tare'>
<line indent='1'>
<left>
Tare : <t t-esc='line.tare' /><t t-esc='line.unit_name' />
</left>
</line>
</t>
</t>
</t>
</t>
</t>
</t>
</t>
</templates>

17
pos_container/templates/templates.xml

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 Coop IT Easy SCRLfs
Robin Keunen <robin@coopiteasy.be>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<odoo>
<template id="assets_frontend" inherit_id="point_of_sale.assets">
<xpath expr="." position="inside">
<link href="/pos_container/static/src/css/container.css" rel="stylesheet"/>
<script type="text/javascript" src="/pos_container/static/src/js/models_and_db.js"/>
<script type="text/javascript" src="/pos_container/static/src/js/container.js"/>
<script type="text/javascript" src="/pos_container/static/src/js/tests.js"/>
</xpath>
</template>
</odoo>

1
pos_container/tests/__init__.py

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

14
pos_container/tests/test_pos_container.py

@ -0,0 +1,14 @@
import odoo.tests
@odoo.tests.tagged('pos_install', '-at-install')
class TestUi(odoo.tests.HttpCase):
def test_01_pos_container_tour(self):
self.phantom_js(
"/web",
"odoo.__DEBUG__.services['web_tour.tour']" +
".run('pos_container_tour')",
"odoo.__DEBUG__.services['web_tour.tour']" +
".tours.pos_container_tour.ready",
login="admin")

43
pos_container/views/container.xml

@ -0,0 +1,43 @@
<odoo>
<record id="pos_container_tree_view" model="ir.ui.view">
<field name="name">pos.container.tree</field>
<field name="model">pos.container</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="barcode"/>
<field name="weight"/>
</tree>
</field>
</record>
<record id="pos_container_form_view" model="ir.ui.view">
<field name="name">pos.container.form</field>
<field name="model">pos.container</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
<field name="barcode"/>
<field name="weight"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="pos_container_action_window" model="ir.actions.act_window" >
<field name="name">Containers</field>
<field name="res_model">pos.container</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="pos_container_menu"
name="Containers"
parent="point_of_sale.pos_config_menu_catalog"
sequence="13"
action="pos_container_action_window"
groups="point_of_sale.group_pos_manager,point_of_sale.group_pos_user"
/>
</odoo>
Loading…
Cancel
Save