diff --git a/pos_stock_available_online/README.rst b/pos_stock_available_online/README.rst new file mode 100644 index 0000000000..0e4c3377cb --- /dev/null +++ b/pos_stock_available_online/README.rst @@ -0,0 +1 @@ +wait a bot :) 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..25da98d50c --- /dev/null +++ b/pos_stock_available_online/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "Point of Sale Stock Available Online", + "version": "16.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)", + "maintainers": ["GabbasovDinar", "CetmixGitDrone"], + "installable": True, + "data": ["views/res_config_settings_view.xml"], + "assets": { + "point_of_sale.assets": [ + "pos_stock_available_online/static/src/css/**/*.css", + "pos_stock_available_online/static/src/js/**/*.js", + "pos_stock_available_online/static/src/xml/**/*.xml", + ], + }, + "license": "AGPL-3", +} diff --git a/pos_stock_available_online/models/__init__.py b/pos_stock_available_online/models/__init__.py new file mode 100644 index 0000000000..ceb6084326 --- /dev/null +++ b/pos_stock_available_online/models/__init__.py @@ -0,0 +1,5 @@ +from . import pos_config +from . import pos_session +from . import res_config_settings +from . import stock_quant +from . import stock_warehouse 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..90c200d198 --- /dev/null +++ b/pos_stock_available_online/models/pos_config.py @@ -0,0 +1,53 @@ +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", + related="picking_type_id.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 _get_channel_name(self): + """ + Return full channel name as combination, POS Config ID and const CHANNEL + """ + self.ensure_one() + return '["{}","{}"]'.format("pos_stock_available_online", self.id) + + def _notify_available_quantity(self, message): + """ + Notify POSes about product updates + """ + if not isinstance(message, list): + message = [message] + notifications = [] + for config in self: + notifications.append( + [config._get_channel_name(), "pos.config/product_update", message] + ) + if notifications: + self.env["bus.bus"]._sendmany(notifications) + _logger.debug("POS notifications for %s: %s", self.ids, notifications) diff --git a/pos_stock_available_online/models/pos_session.py b/pos_stock_available_online/models/pos_session.py new file mode 100644 index 0000000000..cb385c7eca --- /dev/null +++ b/pos_stock_available_online/models/pos_session.py @@ -0,0 +1,22 @@ +from odoo import models + + +class PosSession(models.Model): + _inherit = "pos.session" + + def _process_pos_ui_product_product(self, products): + config = self.config_id + if config.display_product_quantity: + product_obj = self.env["product.product"] + for product_info in products: + product = product_obj.browse(product_info["id"]) + # prepared first main warehouse info + warehouse_info = [ + config.main_warehouse_id._prepare_vals_for_pos(product) + ] + # prepared additional warehouses info + for warehouse in config.additional_warehouse_ids: + warehouse_info.append(warehouse._prepare_vals_for_pos(product)) + product_info["warehouse_info"] = warehouse_info + + return super()._process_pos_ui_product_product(products) 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_quant.py b/pos_stock_available_online/models/stock_quant.py new file mode 100644 index 0000000000..8fa75e21f1 --- /dev/null +++ b/pos_stock_available_online/models/stock_quant.py @@ -0,0 +1,47 @@ +import logging + +from odoo import models + +_logger = logging.getLogger(__name__) + + +class StockQuant(models.Model): + _inherit = "stock.quant" + + def _prepare_pos_message(self): + """ + Return prepared message to send to POS + """ + self.ensure_one() + return self.warehouse_id._prepare_vals_for_pos(self.product_id) + + def _notify_pos(self): + """ + Send notification to POS + """ + pos_session_obj = self.env["pos.session"] + for quant in self: + warehouse_id = quant.warehouse_id.id + configs = pos_session_obj.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", + [quant.product_id.pos_categ_id.id], + ), + ], + ).mapped("config_id") + if configs: + configs._notify_available_quantity(quant._prepare_pos_message()) + + def write(self, vals): + res = super().write(vals) + self._notify_pos() + return res 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..e6b2d35586 --- /dev/null +++ b/pos_stock_available_online/models/stock_warehouse.py @@ -0,0 +1,18 @@ +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() + return { + "id": self.id, + "name": self.name, + "code": self.code, + "quantity": product.with_context(warehouse=self.id).immediately_usable_qty, + "product_id": product.id, + } diff --git a/pos_stock_available_online/readme/CONFIGURE.rst b/pos_stock_available_online/readme/CONFIGURE.rst new file mode 100644 index 0000000000..2ef5a641a1 --- /dev/null +++ b/pos_stock_available_online/readme/CONFIGURE.rst @@ -0,0 +1,14 @@ +In "Point of Sale" configuration "Product Quantity" section activate "Display Product Quantity" feature: + .. image:: ../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.rst b/pos_stock_available_online/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..a7269b1094 --- /dev/null +++ b/pos_stock_available_online/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Cetmix +* Dinar Gabbasov diff --git a/pos_stock_available_online/readme/DESCRIPTION.rst b/pos_stock_available_online/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..d1fc64d49e --- /dev/null +++ b/pos_stock_available_online/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module allows to display product quantities in selected locations in real time. Quantities are displayed directly on product tiles: + .. image:: ../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 https://github.com/OCA/stock-logistics-availability repo. diff --git a/pos_stock_available_online/readme/ROADMAP.rst b/pos_stock_available_online/readme/ROADMAP.rst new file mode 100644 index 0000000000..bf84463bd7 --- /dev/null +++ b/pos_stock_available_online/readme/ROADMAP.rst @@ -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/icon.png b/pos_stock_available_online/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/pos_stock_available_online/static/description/icon.png differ 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/css/pos.css b/pos_stock_available_online/static/src/css/pos.css new file mode 100644 index 0000000000..39848eda75 --- /dev/null +++ b/pos_stock_available_online/static/src/css/pos.css @@ -0,0 +1,32 @@ +.pos .product-list .warehouse-info { + padding: 0.5rem; + font-weight: bold; + display: flex; + border-top: 1px solid #efefef; + justify-content: space-between; +} +.pos .product-list .warehouse-info .warehouse { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} +.pos .product-list .warehouse-info .warehouse .quantity { + color: black; + padding: 1px 2px; + font-size: 11px; +} +.pos .product-list .warehouse-info .warehouse .warehouse-name { + display: block; + color: #696969; + font-size: 10px; +} +.pos .product-list .warehouse-info .warehouse.total .warehouse-name { + font-weight: bold; +} +.pos .product-list .warehouse-info .warehouse .quantity.available { + color: #32a868; +} +.pos .product-list .warehouse-info .warehouse .quantity.not-available { + color: #ef5350; +} diff --git a/pos_stock_available_online/static/src/js/Screens/ProductScreen/ProductItem.esm.js b/pos_stock_available_online/static/src/js/Screens/ProductScreen/ProductItem.esm.js new file mode 100644 index 0000000000..e04c9d8af0 --- /dev/null +++ b/pos_stock_available_online/static/src/js/Screens/ProductScreen/ProductItem.esm.js @@ -0,0 +1,39 @@ +/** @odoo-module **/ + +import ProductItem from "point_of_sale.ProductItem"; +import Registries from "point_of_sale.Registries"; +import {format} from "web.field_utils"; +import utils from "web.utils"; + +const StockProductItem = (ProductItem) => + class StockProductItem extends ProductItem { + format_quantity(quantity) { + const unit = this.env.pos.units_by_id[this.props.product.uom_id[0]]; + var formattedQuantity = `${quantity}`; + if (unit) { + if (unit.rounding) { + var decimals = this.env.pos.dp["Product Unit of Measure"]; + formattedQuantity = format.float(quantity, { + digits: [69, decimals], + }); + } else { + formattedQuantity = utils.round_precision(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 warehouses() { + return this.props.product.warehouse_info; + } + }; + +Registries.Component.extend(ProductItem, StockProductItem); diff --git a/pos_stock_available_online/static/src/js/Screens/ProductScreen/ProductsWidget.esm.js b/pos_stock_available_online/static/src/js/Screens/ProductScreen/ProductsWidget.esm.js new file mode 100644 index 0000000000..e802f69a19 --- /dev/null +++ b/pos_stock_available_online/static/src/js/Screens/ProductScreen/ProductsWidget.esm.js @@ -0,0 +1,63 @@ +/** @odoo-module **/ + +import ProductsWidget from "point_of_sale.ProductsWidget"; +import Registries from "point_of_sale.Registries"; + +const StockProductsWidget = (ProductsWidget) => + class StockProductsWidget extends ProductsWidget { + setup() { + super.setup(); + this.env.services.bus_service.addChannel(this._getChannelName()); + this.env.services.bus_service.addEventListener( + "notification", + this._onNotification.bind(this) + ); + } + _getChannelName() { + return JSON.stringify([ + "pos_stock_available_online", + String(this.env.pos.config.id), + ]); + } + _onNotification({detail: notifications}) { + var payloads = []; + for (const {payload, type} of notifications) { + if (type === "pos.config/product_update") { + payloads.push(payload); + } + } + this._handleNotification(payloads); + } + async _handleNotification(payloads) { + if (this.env.isDebug()) { + console.log("Payloads:", payloads); + } + const db = this.env.pos.db; + const ProductIds = []; + for (const payload of payloads) { + for (const message of payload) { + var product = db.get_product_by_id(message.product_id); + if (product) { + // Update warehouse info of the product + var warehouse = product.warehouse_info.find( + (wh) => wh.id === message.id + ); + if (warehouse) { + warehouse.quantity = message.quantity; + } else { + product.warehouse_info.push(message); + } + } else { + ProductIds.push(message.id); + } + } + } + if (ProductIds.length) { + await this.env.pos._addProducts([...new Set(ProductIds)], false); + } + // Re-render product list without category switching + this.render(true); + } + }; + +Registries.Component.extend(ProductsWidget, StockProductsWidget); diff --git a/pos_stock_available_online/static/src/xml/Screens/ProductScreen/ProductItem.xml b/pos_stock_available_online/static/src/xml/Screens/ProductScreen/ProductItem.xml new file mode 100644 index 0000000000..e2d33cd6a0 --- /dev/null +++ b/pos_stock_available_online/static/src/xml/Screens/ProductScreen/ProductItem.xml @@ -0,0 +1,44 @@ + + + + + +
+
+
+ + + + +
+
+
+ +
+ + + + + + +
+
+
+
+
+
+ +
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..19ec3c2e7b --- /dev/null +++ b/pos_stock_available_online/views/res_config_settings_view.xml @@ -0,0 +1,82 @@ + + + + + res.config.settings.view.form + res.config.settings + + + +

Product Quantity

+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
diff --git a/setup/pos_stock_available_online/odoo/addons/pos_stock_available_online b/setup/pos_stock_available_online/odoo/addons/pos_stock_available_online new file mode 120000 index 0000000000..d81820f21b --- /dev/null +++ b/setup/pos_stock_available_online/odoo/addons/pos_stock_available_online @@ -0,0 +1 @@ +../../../../pos_stock_available_online \ No newline at end of file diff --git a/setup/pos_stock_available_online/setup.py b/setup/pos_stock_available_online/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/pos_stock_available_online/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)