diff --git a/pos_stock_available_online/README.rst b/pos_stock_available_online/README.rst new file mode 100644 index 0000000000..d6056cae32 --- /dev/null +++ b/pos_stock_available_online/README.rst @@ -0,0 +1,123 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +==================================== +Point of Sale Stock Available Online +==================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d544301c362e728a3cab17fa70bc08796bb02c4435dd0edf4ed2df4a4376d1c1 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github + :target: https://github.com/OCA/pos/tree/19.0/pos_stock_available_online + :alt: OCA/pos +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/pos-19-0/pos-19-0-pos_stock_available_online + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/pos&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to display product quantities in selected locations +in real time. Quantities are displayed directly on product tiles: |Pos +Quantity| + +Once a product quantity is changed it will be simultaneously updated in +all active POS. + +This module depends on stock_available module which is available in +https://github.com/OCA/stock-logistics-availability repo. + +.. |Pos Quantity| image:: https://raw.githubusercontent.com/OCA/pos/19.0/pos_stock_available_online/static/img/pos_quantity.png + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +In "Point of Sale" configuration "Product Quantity" section activate +"Display Product Quantity" feature: + +|Pos Config| + +By default quantity is displayed for the warehouse that is used in the +POS stock operation type. + +You can add additional warehouses to show quantity in by adding them +into "Additional Warehouses" field. + +In this case the following information will be displayed on product +tiles: + +- Total quantity = quantity in the default warehouse + quantity in the + additional warehouses +- Quantity in the default warehouse +- Quantity in the additional warehouses. + +.. |Pos Config| image:: https://raw.githubusercontent.com/OCA/pos/19.0/pos_stock_available_online/static/img/pos_config.png + +Known issues / Roadmap +====================== + +This module requires connection to update quantities and doesn't support +offline mode. Warehouses must belong to the same company as POS. Offline +mode support (probably additional module). + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Cetmix + +Contributors +------------ + +- Cetmix <`https://cetmix.com/\\> >`__ +- Dinar Gabbasov +- Nikul Chaudhary + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/pos `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pos_stock_available_online/__init__.py b/pos_stock_available_online/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/pos_stock_available_online/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_stock_available_online/__manifest__.py b/pos_stock_available_online/__manifest__.py new file mode 100644 index 0000000000..f869bd94d4 --- /dev/null +++ b/pos_stock_available_online/__manifest__.py @@ -0,0 +1,23 @@ +{ + "name": "Point of Sale Stock Available Online", + "version": "19.0.1.0.0", + "category": "Sales/Point of Sale", + "summary": "Show the available quantity of products in the Point of Sale ", + "depends": [ + "point_of_sale", + "stock_available", + "base_automation", + ], + "website": "https://github.com/OCA/pos", + "author": "Cetmix, Odoo Community Association (OCA)", + "images": ["static/description/banner.png"], + "installable": True, + "data": ["views/res_config_settings_view.xml"], + "assets": { + # customer_display_assets + "point_of_sale._assets_pos": [ + "pos_stock_available_online/static/src/app/**/*", + ], + }, + "license": "AGPL-3", +} diff --git a/pos_stock_available_online/i18n/es.po b/pos_stock_available_online/i18n/es.po new file mode 100644 index 0000000000..79ec9a561b --- /dev/null +++ b/pos_stock_available_online/i18n/es.po @@ -0,0 +1,126 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_stock_available_online +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-10-24 11:06+0000\n" +"Last-Translator: Daniel Duque \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: pos_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__additional_warehouse_ids +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_additional_warehouse_ids +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Additional Warehouses" +msgstr "Almacenes Adicionales" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de Configuración" + +#. module: pos_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__display_product_quantity +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_display_product_quantity +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Display Product Quantity" +msgstr "Mostrar Cantidad de Productos" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Display the quantity of available products in the POS for" +msgstr "Mostrar la cantidad de productos disponibles en el TPV para" + +#. module: pos_stock_available_online +#: model:ir.model.fields,help:pos_stock_available_online.field_pos_config__additional_warehouse_ids +#: model:ir.model.fields,help:pos_stock_available_online.field_res_config_settings__pos_additional_warehouse_ids +msgid "" +"For the selected warehouses will be displayed quantity of available products " +"in the POS" +msgstr "" +"Para los almacenes seleccionados se mostrará la cantidad de productos " +"disponibles en el TPV" + +#. module: pos_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__main_warehouse_id +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_main_warehouse_id +msgid "Main Warehouse" +msgstr "" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Mark quantity red if below or equal this value." +msgstr "Marque la cantidad en rojo si es inferior o igual a este valor." + +#. module: pos_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__minimum_product_quantity_alert +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_minimum_product_quantity_alert +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Minimum Product Quantity Alert" +msgstr "Alerta de Cantidad Mínima de Producto" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_pos_config +msgid "Point of Sale Configuration" +msgstr "Configuración del Punto de Venta" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_pos_session +msgid "Point of Sale Session" +msgstr "Sesiones del Punto de Venta" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Product Quantity" +msgstr "Cantidad de Productos" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_quant +msgid "Quants" +msgstr "Quants" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Show quantity in the following warehouses in additional to" +msgstr "Indicar la cantidad en los siguientes almacenes además de" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_notifier_pos_mixin +msgid "Stock Notifier POS Mixin" +msgstr "" + +#. module: pos_stock_available_online +#. odoo-javascript +#: code:addons/pos_stock_available_online/static/src/xml/Screens/ProductScreen/ProductItem.xml:0 +#, python-format +msgid "Total" +msgstr "Total" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_warehouse +msgid "Warehouse" +msgstr "Almacén" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Warehouses must belong to the same company as POS." +msgstr "Los almacenes deben pertenecer a la misma compañía que el TPV." + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "warehouse" +msgstr "almacén" diff --git a/pos_stock_available_online/i18n/it.po b/pos_stock_available_online/i18n/it.po new file mode 100644 index 0000000000..bca2a20fd1 --- /dev/null +++ b/pos_stock_available_online/i18n/it.po @@ -0,0 +1,126 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_stock_available_online +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-02-21 18:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: pos_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__additional_warehouse_ids +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_additional_warehouse_ids +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Additional Warehouses" +msgstr "Magazzini aggiuntivi" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_res_config_settings +msgid "Config Settings" +msgstr "Impostazioni configurazione" + +#. module: pos_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__display_product_quantity +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_display_product_quantity +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Display Product Quantity" +msgstr "Visualizza quantità prdotto" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Display the quantity of available products in the POS for" +msgstr "Visualizza la quantità dei prodotti disponibili nel POS per" + +#. module: pos_stock_available_online +#: model:ir.model.fields,help:pos_stock_available_online.field_pos_config__additional_warehouse_ids +#: model:ir.model.fields,help:pos_stock_available_online.field_res_config_settings__pos_additional_warehouse_ids +msgid "" +"For the selected warehouses will be displayed quantity of available products " +"in the POS" +msgstr "" +"Per il magazzino selezionato verrà visualizzata nel POS la quantità " +"disponibile del prodotto" + +#. module: pos_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__main_warehouse_id +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_main_warehouse_id +msgid "Main Warehouse" +msgstr "Magazzino principale" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Mark quantity red if below or equal this value." +msgstr "Evidenzia in rosso la quantità se inferiore o uguale a questo valore." + +#. module: pos_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__minimum_product_quantity_alert +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_minimum_product_quantity_alert +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Minimum Product Quantity Alert" +msgstr "Allerta quantità minima prodotto" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_pos_config +msgid "Point of Sale Configuration" +msgstr "Configurazione punto vendita" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_pos_session +msgid "Point of Sale Session" +msgstr "Sessione punto vendita" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Product Quantity" +msgstr "Quantità prodotto" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_quant +msgid "Quants" +msgstr "Quanti" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Show quantity in the following warehouses in additional to" +msgstr "Mostra le quantità nei magazzini seguenti in aggiunta a" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_move +msgid "Stock Move" +msgstr "Movimento di magazzino" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_notifier_pos_mixin +msgid "Stock Notifier POS Mixin" +msgstr "Mixin POS avvisatore giacenza" + +#. module: pos_stock_available_online +#. odoo-javascript +#: code:addons/pos_stock_available_online/static/src/xml/Screens/ProductScreen/ProductItem.xml:0 +#, python-format +msgid "Total" +msgstr "Totale" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_warehouse +msgid "Warehouse" +msgstr "Magazzino" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Warehouses must belong to the same company as POS." +msgstr "Il magazzino deve appartenere alla stessa azienda del POS." + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "warehouse" +msgstr "magazzino" diff --git a/pos_stock_available_online/i18n/pos_stock_available_online.pot b/pos_stock_available_online/i18n/pos_stock_available_online.pot new file mode 100644 index 0000000000..6562ec70c8 --- /dev/null +++ b/pos_stock_available_online/i18n/pos_stock_available_online.pot @@ -0,0 +1,121 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * pos_stock_available_online +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \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_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__additional_warehouse_ids +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_additional_warehouse_ids +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Additional Warehouses" +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__display_product_quantity +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_display_product_quantity +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Display Product Quantity" +msgstr "" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Display the quantity of available products in the POS for" +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model.fields,help:pos_stock_available_online.field_pos_config__additional_warehouse_ids +#: model:ir.model.fields,help:pos_stock_available_online.field_res_config_settings__pos_additional_warehouse_ids +msgid "" +"For the selected warehouses will be displayed quantity of available products" +" in the POS" +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__main_warehouse_id +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_main_warehouse_id +msgid "Main Warehouse" +msgstr "" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Mark quantity red if below or equal this value." +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model.fields,field_description:pos_stock_available_online.field_pos_config__minimum_product_quantity_alert +#: model:ir.model.fields,field_description:pos_stock_available_online.field_res_config_settings__pos_minimum_product_quantity_alert +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Minimum Product Quantity Alert" +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_pos_config +msgid "Point of Sale Configuration" +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_pos_session +msgid "Point of Sale Session" +msgstr "" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Product Quantity" +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_quant +msgid "Quants" +msgstr "" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Show quantity in the following warehouses in additional to" +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_notifier_pos_mixin +msgid "Stock Notifier POS Mixin" +msgstr "" + +#. module: pos_stock_available_online +#. odoo-javascript +#: code:addons/pos_stock_available_online/static/src/xml/Screens/ProductScreen/ProductItem.xml:0 +#, python-format +msgid "Total" +msgstr "" + +#. module: pos_stock_available_online +#: model:ir.model,name:pos_stock_available_online.model_stock_warehouse +msgid "Warehouse" +msgstr "" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "Warehouses must belong to the same company as POS." +msgstr "" + +#. module: pos_stock_available_online +#: model_terms:ir.ui.view,arch_db:pos_stock_available_online.res_config_settings_view_form +msgid "warehouse" +msgstr "" diff --git a/pos_stock_available_online/models/__init__.py b/pos_stock_available_online/models/__init__.py new file mode 100644 index 0000000000..277775b373 --- /dev/null +++ b/pos_stock_available_online/models/__init__.py @@ -0,0 +1,7 @@ +from . import pos_config +from . import product +from . import res_config_settings +from . import stock_notifier_pos_mixin +from . import stock_quant +from . import stock_warehouse +from . import stock_move diff --git a/pos_stock_available_online/models/pos_config.py b/pos_stock_available_online/models/pos_config.py new file mode 100644 index 0000000000..de72392ecf --- /dev/null +++ b/pos_stock_available_online/models/pos_config.py @@ -0,0 +1,57 @@ +import logging + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class PosConfig(models.Model): + _inherit = "pos.config" + + display_product_quantity = fields.Boolean( + default=True, + ) + main_warehouse_id = fields.Many2one( + "stock.warehouse", + string="Main Warehouse", + related="warehouse_id", + store=True, + ) + additional_warehouse_ids = fields.Many2many( + "stock.warehouse", + "pos_config_stock_warehouse_rel", + "pos_config_id", + "warehouse_id", + string="Additional Warehouses", + domain="[('company_id', '=', company_id)]", + help="For the selected warehouses will be displayed " + "quantity of available products in the POS", + ) + minimum_product_quantity_alert = fields.Float( + default=0.0, + ) + + def _load_pos_data_read(self, records, config): + data = super()._load_pos_data_read(records, config) + values_by_id = { + record["id"]: record + for record in records.read( + ["display_product_quantity", "minimum_product_quantity_alert"], + load=False, + ) + } + for record in data: + record.update(values_by_id.get(record["id"], {})) + return data + + def _notify_available_quantity(self, message): + """ + Notify POSes about product updates + """ + if not isinstance(message, list): + message = [message] + for config in self: + config._notify("PRODUCT_QUANTITY_UPDATE", message) + _logger.debug( + "POS product quantity notifications for %s: %s", self.ids, message + ) diff --git a/pos_stock_available_online/models/product.py b/pos_stock_available_online/models/product.py new file mode 100644 index 0000000000..55cd0d1867 --- /dev/null +++ b/pos_stock_available_online/models/product.py @@ -0,0 +1,45 @@ +from odoo import models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + def _process_pos_ui_product_product(self, products, config_id): + if config_id and config_id.display_product_quantity: + product_obj = self.env["product.template"] + for product_info in products: + product = product_obj.browse(product_info["id"]) + stock_product = product.product_variant_id or product + # prepared first main warehouse info + warehouse_info = [ + config_id.main_warehouse_id._prepare_vals_for_pos(stock_product) + ] + # prepared additional warehouses info + for warehouse in config_id.additional_warehouse_ids: + warehouse_info.append( + warehouse._prepare_vals_for_pos(stock_product) + ) + product_info["warehouse_info"] = warehouse_info + return super()._process_pos_ui_product_product(products, config_id) + + def get_product_info_pos( + self, price, quantity, pos_config_id, product_variant_id=False + ): + product_info = super().get_product_info_pos( + price, quantity, pos_config_id, product_variant_id=product_variant_id + ) + config = self.env["pos.config"].browse(pos_config_id) + if not config.display_product_quantity: + return product_info + + product_or_variants = ( + self.env["product.product"].browse(product_variant_id) + if product_variant_id + else self.product_variant_ids + ) + warehouses = config.main_warehouse_id | config.additional_warehouse_ids + product_info["warehouses"] = [ + warehouse._prepare_product_info_for_pos(product_or_variants) + for warehouse in warehouses + ] + return product_info diff --git a/pos_stock_available_online/models/res_config_settings.py b/pos_stock_available_online/models/res_config_settings.py new file mode 100644 index 0000000000..1cf2838cad --- /dev/null +++ b/pos_stock_available_online/models/res_config_settings.py @@ -0,0 +1,23 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + pos_display_product_quantity = fields.Boolean( + related="pos_config_id.display_product_quantity", + readonly=False, + ) + pos_main_warehouse_id = fields.Many2one( + "stock.warehouse", + related="pos_config_id.main_warehouse_id", + ) + pos_additional_warehouse_ids = fields.Many2many( + "stock.warehouse", + related="pos_config_id.additional_warehouse_ids", + readonly=False, + ) + pos_minimum_product_quantity_alert = fields.Float( + related="pos_config_id.minimum_product_quantity_alert", + readonly=False, + ) diff --git a/pos_stock_available_online/models/stock_move.py b/pos_stock_available_online/models/stock_move.py new file mode 100644 index 0000000000..b9eefc1873 --- /dev/null +++ b/pos_stock_available_online/models/stock_move.py @@ -0,0 +1,30 @@ +from odoo import models + + +class StockMove(models.Model): + _name = "stock.move" + _inherit = ["stock.move", "stock.notifier.pos.mixin"] + + def write(self, vals): + states_dict = {line.id: line.state for line in self} if "state" in vals else {} + res = super().write(vals) + if not states_dict: + return res + for line in self: + if states_dict.get(line.id) != line.state: + line._notify_pos() + return res + + def _action_done(self, cancel_backorder=False): + # As stock will be updated once move has updated its state, skip + # notification from quant + return super( + StockMove, self.with_context(skip_quant_notify_pos=True) + )._action_done(cancel_backorder=cancel_backorder) + + def _get_warehouses_to_notify(self): + warehouses = super()._get_warehouses_to_notify() + warehouses |= self.warehouse_id + warehouses |= self.location_id.warehouse_id + warehouses |= self.location_dest_id.warehouse_id + return warehouses diff --git a/pos_stock_available_online/models/stock_notifier_pos_mixin.py b/pos_stock_available_online/models/stock_notifier_pos_mixin.py new file mode 100644 index 0000000000..e249e03fac --- /dev/null +++ b/pos_stock_available_online/models/stock_notifier_pos_mixin.py @@ -0,0 +1,54 @@ +from odoo import models + + +class StockNotifierPosMixin(models.AbstractModel): + _name = "stock.notifier.pos.mixin" + _description = "Stock Notifier POS Mixin" + + def _skip_notify_pos(self): + """ + Skip notification to POS + """ + return False + + def _get_warehouses_to_notify(self): + self.ensure_one() + return self.env["stock.warehouse"] + + def _notify_pos(self): + """ + Send notification to POS + """ + pos_session_obj = self.env["pos.session"] + for record in self: + if record._skip_notify_pos(): + continue + for warehouse in record._get_warehouses_to_notify(): + configs = ( + pos_session_obj.sudo() + .search( + [ + ("state", "=", "opened"), + ("config_id.display_product_quantity", "=", True), + "|", + ( + "config_id.additional_warehouse_ids", + "in", + [warehouse.id], + ), + ("config_id.main_warehouse_id", "=", warehouse.id), + "|", + ("config_id.iface_available_categ_ids", "=", False), + ( + "config_id.iface_available_categ_ids", + "in", + record.product_id.pos_categ_ids.ids, + ), + ], + ) + .mapped("config_id") + ) + if configs: + configs._notify_available_quantity( + warehouse._prepare_vals_for_pos(record.product_id) + ) diff --git a/pos_stock_available_online/models/stock_quant.py b/pos_stock_available_online/models/stock_quant.py new file mode 100644 index 0000000000..4c6b282d77 --- /dev/null +++ b/pos_stock_available_online/models/stock_quant.py @@ -0,0 +1,23 @@ +from odoo import models + + +class StockQuant(models.Model): + _name = "stock.quant" + _inherit = ["stock.quant", "stock.notifier.pos.mixin"] + + def write(self, vals): + res = super().write(vals) + self._notify_pos() + return res + + def _skip_notify_pos(self): + self.ensure_one() + return ( + self.env.context.get("skip_quant_notify_pos", False) + or super()._skip_notify_pos() + ) + + def _get_warehouses_to_notify(self): + warehouses = super()._get_warehouses_to_notify() + warehouses |= self.warehouse_id + return warehouses diff --git a/pos_stock_available_online/models/stock_warehouse.py b/pos_stock_available_online/models/stock_warehouse.py new file mode 100644 index 0000000000..e69731e081 --- /dev/null +++ b/pos_stock_available_online/models/stock_warehouse.py @@ -0,0 +1,45 @@ +from odoo import models + + +class StockWarehouse(models.Model): + _inherit = "stock.warehouse" + + def _prepare_vals_for_pos(self, product): + """ + Prepare warehouse info data to send a POS + """ + self.ensure_one() + quantity = self.env["stock.quant"]._get_available_quantity( + product, + self.lot_stock_id, + ) + return { + "id": self.id, + "name": self.name, + "code": self.code, + "quantity": quantity, + "product_id": product.id, + "product_tmpl_id": product.product_tmpl_id.id, + "uom_id": product.uom_id.id, + } + + def _prepare_product_info_for_pos(self, products): + self.ensure_one() + quantity = sum( + self.env["stock.quant"]._get_available_quantity( + product, + self.lot_stock_id, + ) + for product in products + ) + forecasted_quantity = sum( + products.with_context(warehouse_id=self.id).mapped("virtual_available") + ) + return { + "id": self.id, + "name": self.name, + "available_quantity": quantity, + "free_qty": quantity, + "forecasted_quantity": forecasted_quantity, + "uom": products.uom_id[:1].name, + } diff --git a/pos_stock_available_online/pyproject.toml b/pos_stock_available_online/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/pos_stock_available_online/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/pos_stock_available_online/readme/CONFIGURE.md b/pos_stock_available_online/readme/CONFIGURE.md new file mode 100644 index 0000000000..8da00ae747 --- /dev/null +++ b/pos_stock_available_online/readme/CONFIGURE.md @@ -0,0 +1,17 @@ +In "Point of Sale" configuration "Product Quantity" section activate "Display Product Quantity" feature: + +![Pos Config](../static/img/pos_config.png) + +By default quantity is displayed for the warehouse that is used in the +POS stock operation type. + +You can add additional warehouses to show quantity in by adding them +into "Additional Warehouses" field. + +In this case the following information will be displayed on product +tiles: + +- Total quantity = quantity in the default warehouse + quantity in the + additional warehouses +- Quantity in the default warehouse +- Quantity in the additional warehouses. diff --git a/pos_stock_available_online/readme/CONTRIBUTORS.md b/pos_stock_available_online/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..98fd39bb57 --- /dev/null +++ b/pos_stock_available_online/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Cetmix \ +- Dinar Gabbasov +- Nikul Chaudhary \<\> diff --git a/pos_stock_available_online/readme/DESCRIPTION.md b/pos_stock_available_online/readme/DESCRIPTION.md new file mode 100644 index 0000000000..e73030f515 --- /dev/null +++ b/pos_stock_available_online/readme/DESCRIPTION.md @@ -0,0 +1,8 @@ +This module allows to display product quantities in selected locations in real time. Quantities are displayed directly on product tiles: +![Pos Quantity](../static/img/pos_quantity.png) + +Once a product quantity is changed it will be simultaneously updated in +all active POS. + +This module depends on stock_available module which is available in + repo. diff --git a/pos_stock_available_online/readme/ROADMAP.md b/pos_stock_available_online/readme/ROADMAP.md new file mode 100644 index 0000000000..66f29e172f --- /dev/null +++ b/pos_stock_available_online/readme/ROADMAP.md @@ -0,0 +1,3 @@ +This module requires connection to update quantities and doesn't support +offline mode. Warehouses must belong to the same company as POS. Offline +mode support (probably additional module). diff --git a/pos_stock_available_online/static/description/banner.png b/pos_stock_available_online/static/description/banner.png new file mode 100644 index 0000000000..a286fd8f5e Binary files /dev/null and b/pos_stock_available_online/static/description/banner.png differ diff --git a/pos_stock_available_online/static/description/icon.png b/pos_stock_available_online/static/description/icon.png new file mode 100644 index 0000000000..014c48f30f Binary files /dev/null and b/pos_stock_available_online/static/description/icon.png differ diff --git a/pos_stock_available_online/static/description/index.html b/pos_stock_available_online/static/description/index.html new file mode 100644 index 0000000000..f236e8442c --- /dev/null +++ b/pos_stock_available_online/static/description/index.html @@ -0,0 +1,462 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Point of Sale Stock Available Online

+ +

Beta License: AGPL-3 OCA/pos Translate me on Weblate Try me on Runboat

+

This module allows to display product quantities in selected locations +in real time. Quantities are displayed directly on product tiles: Pos Quantity

+

Once a product quantity is changed it will be simultaneously updated in +all active POS.

+

This module depends on stock_available module which is available in +https://github.com/OCA/stock-logistics-availability repo.

+

Table of contents

+ +
+

Configuration

+

In “Point of Sale” configuration “Product Quantity” section activate +“Display Product Quantity” feature:

+

Pos Config

+

By default quantity is displayed for the warehouse that is used in the +POS stock operation type.

+

You can add additional warehouses to show quantity in by adding them +into “Additional Warehouses” field.

+

In this case the following information will be displayed on product +tiles:

+
    +
  • Total quantity = quantity in the default warehouse + quantity in the +additional warehouses
  • +
  • Quantity in the default warehouse
  • +
  • Quantity in the additional warehouses.
  • +
+
+
+

Known issues / Roadmap

+

This module requires connection to update quantities and doesn’t support +offline mode. Warehouses must belong to the same company as POS. Offline +mode support (probably additional module).

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Cetmix
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

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

+

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

+
+
+
+
+ + diff --git a/pos_stock_available_online/static/img/pos_config.png b/pos_stock_available_online/static/img/pos_config.png new file mode 100644 index 0000000000..1e698fabea Binary files /dev/null and b/pos_stock_available_online/static/img/pos_config.png differ diff --git a/pos_stock_available_online/static/img/pos_quantity.png b/pos_stock_available_online/static/img/pos_quantity.png new file mode 100644 index 0000000000..1a3fe1d3e0 Binary files /dev/null and b/pos_stock_available_online/static/img/pos_quantity.png differ diff --git a/pos_stock_available_online/static/src/app/generic_components/product_card/product_card.scss b/pos_stock_available_online/static/src/app/generic_components/product_card/product_card.scss new file mode 100644 index 0000000000..f46ed4ee57 --- /dev/null +++ b/pos_stock_available_online/static/src/app/generic_components/product_card/product_card.scss @@ -0,0 +1,54 @@ +.pos .product-list { + .warehouse-info { + padding: 0.35rem 0.45rem; + font-weight: bold; + display: flex; + flex-direction: column; + border-top: 1px solid #efefef; + gap: 0.15rem; + line-height: 1.1; + + .warehouse { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.35rem; + width: 100%; + min-width: 0; + + .quantity { + color: black; + padding: 1px 2px; + font-size: 10px; + line-height: 1.1; + text-align: right; + overflow-wrap: anywhere; + } + + .warehouse-name { + display: block; + color: #696969; + font-size: 10px; + flex: 0 0 auto; + } + + .quantity.available { + color: #32a868; + } + + .quantity.not-available { + color: #ef5350; + } + } + + .warehouse.total .warehouse-name { + font-weight: bold; + } + + .warehouse.total { + border-bottom: 1px solid #efefef; + padding-bottom: 0.15rem; + margin-bottom: 0.05rem; + } + } +} diff --git a/pos_stock_available_online/static/src/app/generic_components/product_card/product_card.xml b/pos_stock_available_online/static/src/app/generic_components/product_card/product_card.xml new file mode 100644 index 0000000000..1751fa3e22 --- /dev/null +++ b/pos_stock_available_online/static/src/app/generic_components/product_card/product_card.xml @@ -0,0 +1,44 @@ + + + + +
+
+
+ + + + +
+
+
+ +
+ + + + + + +
+
+
+
+
+
+
diff --git a/pos_stock_available_online/static/src/app/screens/product_screen/product_list/product_list.esm.js b/pos_stock_available_online/static/src/app/screens/product_screen/product_list/product_list.esm.js new file mode 100644 index 0000000000..a68c2f73b5 --- /dev/null +++ b/pos_stock_available_online/static/src/app/screens/product_screen/product_list/product_list.esm.js @@ -0,0 +1,153 @@ +import {formatFloat, roundPrecision} from "@web/core/utils/numbers"; +import {onWillUnmount, useEffect, useState} from "@odoo/owl"; +import {PosStore} from "@point_of_sale/app/services/pos_store"; +import {ProductCard} from "@point_of_sale/app/components/product_card/product_card"; +import {ProductInfoBanner} from "@point_of_sale/app/components/product_info_banner/product_info_banner"; +import {debounce} from "@web/core/utils/timing"; +import {patch} from "@web/core/utils/patch"; +import {usePos} from "@point_of_sale/app/hooks/pos_hook"; +import {useService} from "@web/core/utils/hooks"; +import {useTrackedAsync} from "@point_of_sale/app/hooks/hooks"; + +patch(ProductCard, { + props: { + ...ProductCard.props, + warehouse_info: {type: Array, optional: true}, + }, +}); + +patch(ProductCard.prototype, { + setup() { + super.setup(...arguments); + this.pos = usePos(); + }, + format_quantity(quantity) { + const unit = this.props.product.uom_id; + var formattedQuantity = `${quantity}`; + if (unit) { + if (unit.rounding) { + const precision = this.pos.models["decimal.precision"].find( + (dp) => dp.name === "Product Unit of Measure" + ); + const decimals = precision ? precision.digits : 2; + formattedQuantity = formatFloat(quantity, { + digits: [69, decimals], + }); + } else { + formattedQuantity = roundPrecision(quantity, 1).toFixed(0); + } + } + return `${formattedQuantity}`; + }, + get display_total_quantity() { + return this.format_quantity(this.total_quantity); + }, + get total_quantity() { + return this.warehouses.reduce( + (partialSum, warehouse) => partialSum + warehouse.quantity, + 0 + ); + }, + get displayProductQuantity() { + return this.pos.config.display_product_quantity; + }, + get minimumProductQuantityAlert() { + return this.pos.config.minimum_product_quantity_alert; + }, + get warehouses() { + return ( + this.props.product.warehouse_info || + this.props.product.raw.warehouse_info || + [] + ); + }, +}); + +patch(PosStore.prototype, { + async setup() { + await super.setup(...arguments); + this.data.connectWebSocket("PRODUCT_QUANTITY_UPDATE", (payload) => { + this.updateProductQuantity(payload); + }); + }, + updateProductQuantity(payload) { + const messages = Array.isArray(payload) ? payload : [payload]; + for (const message of messages) { + const productTmplId = message.product_tmpl_id; + if (!productTmplId) { + continue; + } + const product = this.models["product.template"].get(productTmplId); + if (!product) { + continue; + } + const warehouseInfo = + product.warehouse_info || product.raw.warehouse_info || []; + const warehouse = warehouseInfo.find((wh) => wh.id === message.id); + if (warehouse) { + product.warehouse_info = warehouseInfo.map((wh) => + wh.id === message.id ? {...wh, quantity: message.quantity} : wh + ); + } else { + product.warehouse_info = [...warehouseInfo, message]; + } + } + }, +}); + +patch(ProductInfoBanner.prototype, { + setup() { + this.pos = usePos(); + this.fetchStock = useTrackedAsync( + (pt, p) => this.pos.getProductInfo(pt, 1, 0, p), + { + keepLast: true, + } + ); + this.ui = useService("ui"); + this.state = useState({ + other_warehouses: [], + available_quantity: 0, + free_qty: 0, + uom: "", + }); + + const debouncedFetchStocks = debounce(async (product, productTemplate) => { + let result = {}; + if (this.props.info) { + result = this.props.info; + } else { + await this.fetchStock.call(productTemplate, product); + if (this.fetchStock.status === "error") { + throw this.fetchStock.result; + } + result = this.fetchStock.result; + } + + if (result) { + const warehouses = result.productInfo.warehouses || []; + const totalFreeQty = warehouses.reduce( + (partialSum, warehouse) => partialSum + (warehouse.free_qty || 0), + 0 + ); + this.state.other_warehouses = warehouses; + this.state.available_quantity = totalFreeQty; + this.state.free_qty = totalFreeQty; + this.state.uom = warehouses[0]?.uom; + } + }, 500); + + useEffect( + () => { + if (this.props.productTemplate) { + debouncedFetchStocks( + this.props.product, + this.props.productTemplate + ); + } + }, + () => [this.props.product] + ); + onWillUnmount(() => debouncedFetchStocks.cancel()); + }, +}); diff --git a/pos_stock_available_online/static/src/app/screens/product_screen/product_list/product_list.xml b/pos_stock_available_online/static/src/app/screens/product_screen/product_list/product_list.xml new file mode 100644 index 0000000000..1aaf8f9e79 --- /dev/null +++ b/pos_stock_available_online/static/src/app/screens/product_screen/product_list/product_list.xml @@ -0,0 +1,16 @@ + + + + + + product.warehouse_info || product.raw.warehouse_info + + + + diff --git a/pos_stock_available_online/tests/__init__.py b/pos_stock_available_online/tests/__init__.py new file mode 100644 index 0000000000..74dcc1c043 --- /dev/null +++ b/pos_stock_available_online/tests/__init__.py @@ -0,0 +1 @@ +from . import test_pos_stock_available_online diff --git a/pos_stock_available_online/tests/test_pos_stock_available_online.py b/pos_stock_available_online/tests/test_pos_stock_available_online.py new file mode 100644 index 0000000000..46c215ddd1 --- /dev/null +++ b/pos_stock_available_online/tests/test_pos_stock_available_online.py @@ -0,0 +1,263 @@ +from unittest.mock import patch + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged("post_install", "-at_install") +class TestPosStockAvailableOnline(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company = cls.env.company + cls.main_warehouse = cls.env["stock.warehouse"].search( + [("company_id", "=", cls.company.id)], limit=1 + ) + cls.extra_warehouse = cls.env["stock.warehouse"].create( + { + "name": "POS Extra Warehouse", + "code": "PEW", + "company_id": cls.company.id, + } + ) + cls.product = cls.env["product.product"].create( + { + "name": "POS Stock Product", + "is_storable": True, + "available_in_pos": True, + "sale_ok": True, + "taxes_id": False, + } + ) + cls.env["stock.quant"]._update_available_quantity( + cls.product, cls.main_warehouse.lot_stock_id, 7.0 + ) + cls.env["stock.quant"]._update_available_quantity( + cls.product, cls.extra_warehouse.lot_stock_id, 3.0 + ) + cls.config = cls.env["pos.config"].create( + { + "name": "POS Stock Available Online", + "display_product_quantity": True, + "warehouse_id": cls.main_warehouse.id, + "additional_warehouse_ids": [(6, 0, cls.extra_warehouse.ids)], + "minimum_product_quantity_alert": 2.0, + } + ) + + def test_stock_warehouse_prepare_vals_uses_warehouse_location(self): + main_vals = self.main_warehouse._prepare_vals_for_pos(self.product) + extra_vals = self.extra_warehouse._prepare_vals_for_pos(self.product) + + self.assertEqual(main_vals["quantity"], 7.0) + self.assertEqual(extra_vals["quantity"], 3.0) + self.assertEqual(main_vals["product_id"], self.product.id) + self.assertEqual(main_vals["product_tmpl_id"], self.product.product_tmpl_id.id) + + def test_product_template_process_pos_data_adds_all_warehouses(self): + product_info = { + "id": self.product.product_tmpl_id.id, + "currency_id": self.company.currency_id.id, + "taxes_id": [], + "image_128": False, + "list_price": 0.0, + "standard_price": 0.0, + } + + self.env["product.template"]._process_pos_ui_product_product( + [product_info], self.config + ) + + self.assertEqual( + [wh["id"] for wh in product_info["warehouse_info"]], + [self.main_warehouse.id, self.extra_warehouse.id], + ) + self.assertEqual( + [wh["quantity"] for wh in product_info["warehouse_info"]], + [7.0, 3.0], + ) + + def test_product_template_process_pos_data_skips_when_disabled(self): + product_info = { + "id": self.product.product_tmpl_id.id, + "currency_id": self.company.currency_id.id, + "taxes_id": [], + "image_128": False, + "list_price": 0.0, + "standard_price": 0.0, + } + self.config.display_product_quantity = False + + self.env["product.template"]._process_pos_ui_product_product( + [product_info], self.config + ) + + self.assertNotIn("warehouse_info", product_info) + + def test_product_info_pos_uses_configured_warehouses(self): + product_info = self.product.product_tmpl_id.get_product_info_pos( + 1.0, 1.0, self.config.id, self.product.id + ) + + self.assertEqual( + [wh["id"] for wh in product_info["warehouses"]], + [self.main_warehouse.id, self.extra_warehouse.id], + ) + self.assertEqual( + [wh["free_qty"] for wh in product_info["warehouses"]], + [7.0, 3.0], + ) + self.assertEqual( + [wh["available_quantity"] for wh in product_info["warehouses"]], + [7.0, 3.0], + ) + + def test_product_info_pos_skips_when_disabled(self): + self.config.display_product_quantity = False + + product_info = self.product.product_tmpl_id.get_product_info_pos( + 1.0, 1.0, self.config.id, self.product.id + ) + + self.assertIn("warehouses", product_info) + + def test_pos_config_load_pos_data_read_adds_custom_fields(self): + data = self.config._load_pos_data_read(self.config, self.config) + config_data = data[0] + + self.assertTrue(config_data["display_product_quantity"]) + self.assertEqual(config_data["minimum_product_quantity_alert"], 2.0) + + def test_pos_config_notify_available_quantity_sends_list_payload(self): + with patch.object(type(self.config), "_notify", autospec=True) as notify: + self.config._notify_available_quantity({"product_id": self.product.id}) + + notify.assert_called_once_with( + self.config, + "PRODUCT_QUANTITY_UPDATE", + [{"product_id": self.product.id}], + ) + + def test_pos_config_notify_available_quantity_keeps_list_payload(self): + payload = [{"product_id": self.product.id}] + + with patch.object(type(self.config), "_notify", autospec=True) as notify: + self.config._notify_available_quantity(payload) + + notify.assert_called_once_with( + self.config, + "PRODUCT_QUANTITY_UPDATE", + payload, + ) + + def test_stock_quant_write_notifies_and_context_can_skip(self): + quant = self.env["stock.quant"].search( + [ + ("product_id", "=", self.product.id), + ("location_id", "=", self.main_warehouse.lot_stock_id.id), + ], + limit=1, + ) + + with patch.object(type(quant), "_notify_pos", autospec=True) as notify: + quant.write({"inventory_quantity": 7.0}) + + notify.assert_called_once() + self.assertFalse(quant._skip_notify_pos()) + self.assertTrue( + quant.with_context(skip_quant_notify_pos=True)._skip_notify_pos() + ) + self.assertIn(self.main_warehouse, quant._get_warehouses_to_notify()) + + def test_stock_move_write_notifies_only_on_state_change(self): + move = self.env["stock.move"].create( + { + "product_id": self.product.id, + "product_uom_qty": 1.0, + "product_uom": self.product.uom_id.id, + "location_id": self.main_warehouse.lot_stock_id.id, + "location_dest_id": self.extra_warehouse.lot_stock_id.id, + } + ) + + with patch.object(type(move), "_notify_pos", autospec=True) as notify: + move.write({"product_uom_qty": 2.0}) + self.assertFalse(notify.called) + move.write({"state": "cancel"}) + self.assertTrue(notify.called) + + warehouses = move._get_warehouses_to_notify() + self.assertIn(self.main_warehouse, warehouses) + self.assertIn(self.extra_warehouse, warehouses) + + def test_stock_move_action_done_sets_skip_quant_notify_context(self): + move = self.env["stock.move"].create( + { + "product_id": self.product.id, + "product_uom_qty": 1.0, + "product_uom": self.product.uom_id.id, + "location_id": self.main_warehouse.lot_stock_id.id, + "location_dest_id": self.extra_warehouse.lot_stock_id.id, + "picked": True, + "move_line_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_id": self.product.uom_id.id, + "quantity": 1.0, + "location_id": self.main_warehouse.lot_stock_id.id, + "location_dest_id": self.extra_warehouse.lot_stock_id.id, + "picked": True, + }, + ) + ], + } + ) + + with patch.object(type(move), "_notify_pos", autospec=True): + move._action_confirm() + result = move._action_done() + + self.assertIn(move, result) + self.assertEqual(move.state, "done") + + def test_notifier_pos_mixin_notify_pos(self): + self.config.iface_available_categ_ids = False + session = self.env["pos.session"].create({"config_id": self.config.id}) + session.state = "opened" + quant = self.env["stock.quant"].search( + [ + ("product_id", "=", self.product.id), + ("location_id", "=", self.extra_warehouse.lot_stock_id.id), + ], + limit=1, + ) + + with patch.object( + type(self.config), "_notify_available_quantity", autospec=True + ) as notify: + quant._notify_pos() + + self.assertTrue(notify.called) + + def test_notifier_pos_mixin_skip_notify_pos(self): + quant = ( + self.env["stock.quant"] + .search( + [ + ("product_id", "=", self.product.id), + ("location_id", "=", self.extra_warehouse.lot_stock_id.id), + ], + limit=1, + ) + .with_context(skip_quant_notify_pos=True) + ) + + with patch.object( + type(self.config), "_notify_available_quantity", autospec=True + ) as notify: + quant._notify_pos() + + self.assertFalse(notify.called) diff --git a/pos_stock_available_online/views/res_config_settings_view.xml b/pos_stock_available_online/views/res_config_settings_view.xml new file mode 100644 index 0000000000..cbc5ff676b --- /dev/null +++ b/pos_stock_available_online/views/res_config_settings_view.xml @@ -0,0 +1,49 @@ + + + + res.config.settings.view.form + res.config.settings + + + + + + + + +
+ Show quantity in the following warehouses in additional to warehouse. +
+ + +
+ + + + +
+
+
+
+