From 9d2a8535ccb9aa3113c49ce2e77df083af6f2cd2 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Fri, 12 Jan 2024 10:45:03 +0100 Subject: [PATCH] [IMP] pos_product_expiry: Make it work in Offline mode --- pos_product_expiry/__manifest__.py | 5 +- pos_product_expiry/models/__init__.py | 2 +- pos_product_expiry/models/product_product.py | 32 -------- pos_product_expiry/models/stock_lot.py | 21 ++++++ .../static/src/js/app/models.esm.js | 57 +++++++------- .../tests/tours/ProductExpiry.tour.esm.js | 51 +++++++++++++ pos_product_expiry/tests/__init__.py | 1 + pos_product_expiry/tests/test_frontend.py | 75 +++++++++++++++++++ 8 files changed, 183 insertions(+), 61 deletions(-) delete mode 100644 pos_product_expiry/models/product_product.py create mode 100644 pos_product_expiry/models/stock_lot.py create mode 100644 pos_product_expiry/static/tests/tours/ProductExpiry.tour.esm.js create mode 100644 pos_product_expiry/tests/__init__.py create mode 100644 pos_product_expiry/tests/test_frontend.py diff --git a/pos_product_expiry/__manifest__.py b/pos_product_expiry/__manifest__.py index fb922be5cf..d898e1767e 100644 --- a/pos_product_expiry/__manifest__.py +++ b/pos_product_expiry/__manifest__.py @@ -10,11 +10,14 @@ "license": "AGPL-3", "author": "Dixmit,INVITU,Odoo Community Association (OCA)", "website": "https://github.com/OCA/pos", - "depends": ["point_of_sale", "product_expiry"], + "depends": ["point_of_sale", "product_expiry", "pos_lot_selection"], "assets": { "point_of_sale._assets_pos": [ "pos_product_expiry/static/src/js/**/*.js", ], + "web.assets_tests": [ + "pos_product_expiry/static/tests/tours/**/*", + ], }, "data": [ "views/res_config_settings_views.xml", diff --git a/pos_product_expiry/models/__init__.py b/pos_product_expiry/models/__init__.py index b3298cd1a9..29f0241cb2 100644 --- a/pos_product_expiry/models/__init__.py +++ b/pos_product_expiry/models/__init__.py @@ -1,4 +1,4 @@ from . import pos_session -from . import product_product from . import pos_config from . import res_config_settings +from . import stock_lot diff --git a/pos_product_expiry/models/product_product.py b/pos_product_expiry/models/product_product.py deleted file mode 100644 index b86dd85c4c..0000000000 --- a/pos_product_expiry/models/product_product.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2024 Dixmit -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import _, fields, models - - -class ProductProduct(models.Model): - _inherit = "product.product" - - def check_pos_lots(self, lots, company_id): - self.ensure_one() - found_lots = ( - self.env["stock.lot"] - .sudo() - .search( - [ - ("product_id", "=", self.id), - ("name", "in", lots), - "|", - ("company_id", "=", company_id), - ("company_id", "=", False), - ] - ) - ) - if len(lots) > len(found_lots): - return _("Some lots couldn't be found") - now = fields.Datetime.now() - if found_lots.filtered(lambda r: r.expiration_date and r.expiration_date < now): - return _( - "Some lots are expired and you are not enabled to sell expired lots" - ) - return False diff --git a/pos_product_expiry/models/stock_lot.py b/pos_product_expiry/models/stock_lot.py new file mode 100644 index 0000000000..65179c8c24 --- /dev/null +++ b/pos_product_expiry/models/stock_lot.py @@ -0,0 +1,21 @@ +# Copyright 2024 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import pytz + +from odoo import models + + +class StockLot(models.Model): + _inherit = "stock.lot" + + def _get_pos_info(self): + result = super()._get_pos_info() + if self.expiration_date: + timezone = pytz.timezone( + self._context.get("tz") or self.env.user.tz or "UTC" + ) + result["expiration_date"] = self.expiration_date.astimezone( + timezone + ).isoformat() + return result diff --git a/pos_product_expiry/static/src/js/app/models.esm.js b/pos_product_expiry/static/src/js/app/models.esm.js index e7494444ae..6e43193b1c 100644 --- a/pos_product_expiry/static/src/js/app/models.esm.js +++ b/pos_product_expiry/static/src/js/app/models.esm.js @@ -3,49 +3,52 @@ Copyright 2024 Dixmit License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) */ - -import {ConnectionLostError} from "@web/core/network/rpc_service"; +import {Orderline, Product} from "@point_of_sale/app/store/models"; import {ErrorPopup} from "@point_of_sale/app/errors/popups/error_popup"; -import {OfflineErrorPopup} from "@point_of_sale/app/errors/popups/offline_error_popup"; -import {Orderline} from "@point_of_sale/app/store/models"; import {_t} from "@web/core/l10n/translation"; import {patch} from "@web/core/utils/patch"; +patch(Product.prototype, { + async checkProductLotExpiration(lot) { + const lotData = this.available_lot_for_pos_ids.filter((availableLot) => { + return lot === availableLot.name; + }); + if (lotData.length === 0) { + await this.env.services.popup.add(ErrorPopup, { + title: _t("Problem with lots"), + body: _t("A lot was not found. No changes were applied."), + }); + return true; + } + if (new Date(lotData[0].expiration_date) < new Date()) { + await this.env.services.popup.add(ErrorPopup, { + title: _t("Problem with lots"), + body: _t( + "A lot is expired and you are not enabled to sell expired lots. No changes were applied." + ), + }); + return true; + } + return false; + }, +}); + patch(Orderline.prototype, { async setPackLotLines({modifiedPackLotLines, newPackLotLines}) { if ( this.product.use_expiration_date && this.env.services.pos.config.check_lot_expiry ) { - var lotsToCheck = []; for (const newLotLine of newPackLotLines) { - lotsToCheck.push(newLotLine.lot_name); + if (await this.product.checkProductLotExpiration(newLotLine.lot_name)) { + return; + } } for (const modifiedLotline of Object.values(modifiedPackLotLines)) { - lotsToCheck.push(modifiedLotline); - } - try { - const checked_lots_problem = await this.env.services.orm.call( - "product.product", - "check_pos_lots", - [[this.product.id], lotsToCheck, this.env.services.pos.company.id] - ); - if (checked_lots_problem) { - await this.env.services.popup.add(ErrorPopup, { - title: _t("Problem with lots"), - body: - checked_lots_problem + " " + _t("No changes were applied."), - }); - // We don't want to apply the changes in this case + if (await this.product.checkProductLotExpiration(modifiedLotline)) { return; } - } catch (error) { - if (error instanceof ConnectionLostError) { - this.env.services.popup.add(OfflineErrorPopup); - } else { - throw error; - } } } return await super.setPackLotLines(...arguments); diff --git a/pos_product_expiry/static/tests/tours/ProductExpiry.tour.esm.js b/pos_product_expiry/static/tests/tours/ProductExpiry.tour.esm.js new file mode 100644 index 0000000000..56f0f93bf3 --- /dev/null +++ b/pos_product_expiry/static/tests/tours/ProductExpiry.tour.esm.js @@ -0,0 +1,51 @@ +/** @odoo-module */ +/* + Copyright 2023 Trobz Consulting + License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +*/ + +import * as Chrome from "@point_of_sale/../tests/tours/helpers/ChromeTourMethods"; +import * as ErrorPopup from "@point_of_sale/../tests/tours/helpers/ErrorPopupTourMethods"; +import * as PaymentScreen from "@point_of_sale/../tests/tours/helpers/PaymentScreenTourMethods"; +import * as ProductScreen from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods"; +import * as ReceiptScreen from "@point_of_sale/../tests/tours/helpers/ReceiptScreenTourMethods"; +import {registry} from "@web/core/registry"; +import {selectLotNumber} from "@pos_lot_selection/../tests/tours/LotSelection.tour.esm"; + +registry.category("web_tour.tours").add("ProductExpiryNotExpired", { + test: true, + url: "/pos/ui", + steps: () => + [ + ProductScreen.confirmOpeningPopup(), + ProductScreen.clickHomeCategory(), + ProductScreen.clickDisplayedProduct("Lot Product 1"), + selectLotNumber("10120000515"), + ProductScreen.selectedOrderlineHas("Lot Product 1"), + ProductScreen.clickPayButton(), + PaymentScreen.clickPaymentMethod("Cash"), + PaymentScreen.clickValidate(), + ReceiptScreen.trackingMethodIsLot(), + Chrome.endTour(), + ].flat(), +}); + +registry.category("web_tour.tours").add("ProductExpiryExpired", { + test: true, + url: "/pos/ui", + steps: () => + [ + ProductScreen.confirmOpeningPopup(), + ProductScreen.clickHomeCategory(), + ProductScreen.clickDisplayedProduct("Lot Product 1"), + selectLotNumber("10120000516"), + ProductScreen.selectedOrderlineHas("Lot Product 1"), + ErrorPopup.isShown(), + ErrorPopup.clickConfirm(), + ProductScreen.pressNumpad("⌫"), + ProductScreen.pressNumpad("⌫"), + // We need to clean the screen + ProductScreen.orderIsEmpty(), + Chrome.endTour(), + ].flat(), +}); diff --git a/pos_product_expiry/tests/__init__.py b/pos_product_expiry/tests/__init__.py new file mode 100644 index 0000000000..ab211c0007 --- /dev/null +++ b/pos_product_expiry/tests/__init__.py @@ -0,0 +1 @@ +from . import test_frontend diff --git a/pos_product_expiry/tests/test_frontend.py b/pos_product_expiry/tests/test_frontend.py new file mode 100644 index 0000000000..946d5c3325 --- /dev/null +++ b/pos_product_expiry/tests/test_frontend.py @@ -0,0 +1,75 @@ +# Copyright 2023 Trobz Consulting +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +import odoo.tests + +from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon + + +@odoo.tests.tagged("post_install", "-at_install") +class TestLotScanning(TestPointOfSaleHttpCommon): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + now = datetime.now() + cls.lot_product_1 = cls.env["product.product"].create( + { + "name": "Lot Product 1", + "type": "product", + "tracking": "lot", + "categ_id": cls.env.ref("product.product_category_all").id, + "available_in_pos": True, + "use_expiration_date": True, + } + ) + lots = cls.env["stock.lot"].create( + [ + { + "name": "10120000515", + "product_id": cls.lot_product_1.id, + "company_id": cls.env.company.id, + "expiration_date": now + timedelta(days=1), + }, + { + "name": "10120000516", + "product_id": cls.lot_product_1.id, + "company_id": cls.env.company.id, + "expiration_date": now + timedelta(days=-1), + }, + ] + ) + location_id = cls.main_pos_config.picking_type_id.default_location_src_id.id + cls.env["stock.quant"].with_context(inventory_mode=True).create( + { + "product_id": cls.lot_product_1.id, + "inventory_quantity": 100, + "location_id": location_id, + "lot_id": lots[0].id, + } + ).action_apply_inventory() + cls.env["stock.quant"].with_context(inventory_mode=True).create( + { + "product_id": cls.lot_product_1.id, + "inventory_quantity": 100, + "location_id": location_id, + "lot_id": lots[1].id, + } + ).action_apply_inventory() + + def test_lot_not_expired(self): + self.main_pos_config.with_user(self.pos_user).open_ui() + self.start_tour( + "/pos/ui?config_id=%d" % self.main_pos_config.id, + "ProductExpiryNotExpired", + login="pos_user", + ) + + def test_lot_expired(self): + self.main_pos_config.with_user(self.pos_user).open_ui() + self.start_tour( + "/pos/ui?config_id=%d" % self.main_pos_config.id, + "ProductExpiryExpired", + login="pos_user", + )