diff --git a/pos_sale_picking_keep/README.rst b/pos_sale_picking_keep/README.rst index fd25b9428f..f63b76bb51 100644 --- a/pos_sale_picking_keep/README.rst +++ b/pos_sale_picking_keep/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - =========================== Keep sale pickings from PoS =========================== @@ -17,7 +13,7 @@ Keep sale pickings from PoS .. |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 +.. |badge2| image:: https://img.shields.io/badge/licence-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 @@ -70,6 +66,18 @@ Authors * Tecnativa +Contributors +------------ + +- `Tecnativa `__ + + - Pedro M. Baeza + - Víctor Martínez + +- `ACSONE SA/NV `__: + + - Denis Roussel + Maintainers ----------- diff --git a/pos_sale_picking_keep/__manifest__.py b/pos_sale_picking_keep/__manifest__.py index 5301bfc033..c8cbaef513 100644 --- a/pos_sale_picking_keep/__manifest__.py +++ b/pos_sale_picking_keep/__manifest__.py @@ -10,6 +10,7 @@ "license": "AGPL-3", "installable": True, "depends": ["pos_sale"], + "data": [], "assets": { "web.assets_tests": [ "pos_sale_picking_keep/static/tests/tours/**/*", diff --git a/pos_sale_picking_keep/models/__init__.py b/pos_sale_picking_keep/models/__init__.py index 1315735d03..d2893a12d2 100644 --- a/pos_sale_picking_keep/models/__init__.py +++ b/pos_sale_picking_keep/models/__init__.py @@ -2,3 +2,5 @@ from . import pos_order from . import pos_session from . import sale_order_line +from . import pos_order_line +from . import stock_picking diff --git a/pos_sale_picking_keep/models/pos_order.py b/pos_sale_picking_keep/models/pos_order.py index 653aab9477..74a12aad48 100644 --- a/pos_sale_picking_keep/models/pos_order.py +++ b/pos_sale_picking_keep/models/pos_order.py @@ -1,7 +1,6 @@ # Copyright 2025 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import api, models -from odoo.tools import config class PosOrder(models.Model): @@ -32,16 +31,3 @@ def sync_from_ui(self, orders): res = super().sync_from_ui(orders) pickings._compute_state() return res - - def _create_order_picking(self): - # Nullify the creation of the pickings at this level - # We cannot use self.env.context.get("test_pos_sale_picking_keep") because - # the tours that run in the tests do not allow that context to be maintained. - # Therefore, we use self.config_id.name. - if ( - config["test_enable"] - and self.config_id.name != "test_pos_sale_picking_keep" - ): - # For not breaking tests of other modules - return super()._create_order_picking() - return True diff --git a/pos_sale_picking_keep/models/pos_order_line.py b/pos_sale_picking_keep/models/pos_order_line.py new file mode 100644 index 0000000000..7fcd3b4832 --- /dev/null +++ b/pos_sale_picking_keep/models/pos_order_line.py @@ -0,0 +1,18 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import models + + +class PosOrderLine(models.Model): + _inherit = "pos.order.line" + + def _launch_stock_rule_from_pos_order_lines(self): + """ + Launch stock rules for pos order lines that are not linked to a sale order line + """ + lines_to_launch = self.filtered(lambda line: not line.sale_order_line_id) + if lines_to_launch: + super( + PosOrderLine, lines_to_launch + )._launch_stock_rule_from_pos_order_lines() + return True diff --git a/pos_sale_picking_keep/models/stock_picking.py b/pos_sale_picking_keep/models/stock_picking.py new file mode 100644 index 0000000000..8a4a53951e --- /dev/null +++ b/pos_sale_picking_keep/models/stock_picking.py @@ -0,0 +1,24 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + @api.model + def _create_picking_from_pos_order_lines( + self, location_dest_id, lines, picking_type, partner=False + ): + """ + Avoid cancelling existing pickings and re-launching stock rules from + POS line if linked to a sale order line and with a strategy + to keep sale pickings. + """ + lines_without_create = lines.filtered(lambda line: line.sale_order_line_id) + return super()._create_picking_from_pos_order_lines( + location_dest_id=location_dest_id, + lines=(lines - lines_without_create), + picking_type=picking_type, + partner=partner, + ) diff --git a/pos_sale_picking_keep/readme/CONTRIBUTORS.md b/pos_sale_picking_keep/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..c0fea7d6e8 --- /dev/null +++ b/pos_sale_picking_keep/readme/CONTRIBUTORS.md @@ -0,0 +1,6 @@ +- [Tecnativa](https://www.tecnativa.com) + - Pedro M. Baeza + - Víctor Martínez + +- [ACSONE SA/NV](https://acsone.eu): + - Denis Roussel \<\> \ No newline at end of file diff --git a/pos_sale_picking_keep/static/description/index.html b/pos_sale_picking_keep/static/description/index.html index 2226b9650b..b68419ba91 100644 --- a/pos_sale_picking_keep/static/description/index.html +++ b/pos_sale_picking_keep/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Keep sale pickings from PoS -
+
+

Keep sale pickings from PoS

- - -Odoo Community Association - -
-

Keep sale pickings from PoS

-

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

+

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

This module inhibits the manipulation that the point of sale mades over the sales orders pickings, and the creation of new pickings under the PoS picking type.

@@ -385,13 +380,14 @@

Keep sale pickings from PoS

  • Bug Tracker
  • Credits
  • -

    Use Cases / Context

    +

    Use Cases / Context

    In some scenarios, you may not want that the point of sale (PoS) handles the pickings of the products you are paying:

      @@ -401,7 +397,7 @@

      Use Cases / Context

      In that cases, it’s better to keep the original sales pickings.

    -

    Bug Tracker

    +

    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 @@ -409,15 +405,29 @@

    Bug Tracker

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Tecnativa
    +
    +

    Contributors

    + +
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -432,6 +442,5 @@

    Maintainers

    -
    diff --git a/pos_sale_picking_keep/static/tests/tours/pos_sale_picking_keep.esm.js b/pos_sale_picking_keep/static/tests/tours/pos_sale_picking_keep.esm.js index 63ef17f323..2f3fc360c4 100644 --- a/pos_sale_picking_keep/static/tests/tours/pos_sale_picking_keep.esm.js +++ b/pos_sale_picking_keep/static/tests/tours/pos_sale_picking_keep.esm.js @@ -30,3 +30,18 @@ registry.category("web_tour.tours").add("PosSalePickingKeep2", { PaymentScreen.clickValidate(), ].flat(), }); + +registry.category("web_tour.tours").add("PosSalePickingKeepMixed", { + steps: () => + [ + Chrome.startPoS(), + Dialog.confirm("Open Register"), + PosSale.settleNthOrder(1), + ProductScreen.selectedOrderlineHas("Test Product", "1.00"), + ProductScreen.addOrderline("Test Product 2"), + ProductScreen.clickPayButton(), + PaymentScreen.clickPaymentMethod("Bank", true, {remaining: "0.0"}), + PaymentScreen.clickValidate(), + ReceiptScreen.isShown(), + ].flat(), +}); diff --git a/pos_sale_picking_keep/tests/__init__.py b/pos_sale_picking_keep/tests/__init__.py index bdfa733c4b..054f42ef8c 100644 --- a/pos_sale_picking_keep/tests/__init__.py +++ b/pos_sale_picking_keep/tests/__init__.py @@ -1 +1,2 @@ from . import test_pos_sale_picking_keep +from . import test_pos_sale_picking_keep_realtime diff --git a/pos_sale_picking_keep/tests/common.py b/pos_sale_picking_keep/tests/common.py new file mode 100644 index 0000000000..526600a5c2 --- /dev/null +++ b/pos_sale_picking_keep/tests/common.py @@ -0,0 +1,33 @@ +# Copyright 2026 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import odoo.tests + +from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon + + +@odoo.tests.tagged("post_install", "-at_install") +class PosSalePickingKeepCommon(TestPointOfSaleHttpCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.customer = cls.env["res.partner"].create({"name": "Test partner"}) + cls.warehouse = cls.env["stock.warehouse"].search( + [("company_id", "=", cls.env.company.id)], limit=1 + ) + cls.product = cls.env["product.product"].create( + { + "name": "Test Product", + "available_in_pos": True, + "is_storable": True, + "lst_price": 10.0, + } + ) + cls.product_2 = cls.env["product.product"].create( + { + "name": "Test Product 2", + "available_in_pos": True, + "is_storable": True, + "lst_price": 10.0, + } + ) + cls.main_pos_config.name = "test_pos_sale_picking_keep" diff --git a/pos_sale_picking_keep/tests/test_pos_sale_picking_keep.py b/pos_sale_picking_keep/tests/test_pos_sale_picking_keep.py index b85b122233..4ef909cba5 100644 --- a/pos_sale_picking_keep/tests/test_pos_sale_picking_keep.py +++ b/pos_sale_picking_keep/tests/test_pos_sale_picking_keep.py @@ -1,30 +1,15 @@ # Copyright 2026 Tecnativa - Víctor Martínez # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -import odoo.tests from odoo.tests import Form -from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon +from .common import PosSalePickingKeepCommon -@odoo.tests.tagged("post_install", "-at_install") -class TestPosSalePickingKeep(TestPointOfSaleHttpCommon): +class TestPosSalePickingKeep(PosSalePickingKeepCommon): @classmethod def setUpClass(cls): super().setUpClass() cls.env.company.point_of_sale_update_stock_quantities = "closing" - cls.customer = cls.env["res.partner"].create({"name": "Test partner"}) - cls.warehouse = cls.env["stock.warehouse"].search( - [("company_id", "=", cls.env.company.id)], limit=1 - ) - cls.product = cls.env["product.product"].create( - { - "name": "Test Product", - "available_in_pos": True, - "is_storable": True, - "lst_price": 10.0, - } - ) - cls.main_pos_config.name = "test_pos_sale_picking_keep" def test_sale_order_pos_order_done(self): self.env["stock.quant"]._update_available_quantity( @@ -39,8 +24,7 @@ def test_sale_order_pos_order_done(self): sol = sale_order.order_line self.assertEqual(sol.qty_delivered, 0) self.main_pos_config.open_ui() - self.start_tour( - "/pos/ui?config_id=%d" % self.main_pos_config.id, + self.start_pos_tour( "PosSalePickingKeep1", login="accountman", ) @@ -58,8 +42,7 @@ def test_sale_order_pos_order_done(self): def test_pos_order_flow(self): self.main_pos_config.open_ui() - self.start_tour( - "/pos/ui?config_id=%d" % self.main_pos_config.id, + self.start_pos_tour( "PosSalePickingKeep2", login="accountman", ) diff --git a/pos_sale_picking_keep/tests/test_pos_sale_picking_keep_realtime.py b/pos_sale_picking_keep/tests/test_pos_sale_picking_keep_realtime.py new file mode 100644 index 0000000000..38e9163b97 --- /dev/null +++ b/pos_sale_picking_keep/tests/test_pos_sale_picking_keep_realtime.py @@ -0,0 +1,77 @@ +# Copyright 2026 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo.tests import Form + +from .common import PosSalePickingKeepCommon + + +class TestPosSalePickingKeepRealtime(PosSalePickingKeepCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env.company.point_of_sale_update_stock_quantities = "real" + + def test_sale_order_pos_order_done(self): + self.env["stock.quant"]._update_available_quantity( + self.product, self.warehouse.lot_stock_id, 1 + ) + self.env["stock.quant"]._update_available_quantity( + self.product_2, self.warehouse.lot_stock_id, 10 + ) + order_form = Form(self.env["sale.order"]) + order_form.partner_id = self.customer + order_form.client_order_ref = "test_pos_sale_picking_keep" + with order_form.order_line.new() as line_form: + line_form.product_id = self.product + sale_order = order_form.save() + sol = sale_order.order_line + self.assertEqual(sol.qty_delivered, 0) + self.main_pos_config.open_ui() + self.start_pos_tour( + "PosSalePickingKeepMixed", + login="accountman", + ) + self.assertEqual(sale_order.state, "sale") + self.assertEqual(len(sale_order.picking_ids), 1) + pos_order = sol.pos_order_line_ids.order_id + self.assertEqual(pos_order.state, "paid") + self.assertTrue(pos_order.picking_ids) + so_picking = sale_order.picking_ids + self.assertEqual(so_picking.state, "assigned") + self.assertEqual(sol.qty_delivered, 0) + sale_order.picking_ids.button_validate() + self.assertEqual(so_picking.state, "done") + self.assertEqual(sol.qty_delivered, 1) + + def test_sale_order_pos_order_done_and_pos_picking(self): + self.env["stock.quant"]._update_available_quantity( + self.product, self.warehouse.lot_stock_id, 1 + ) + self.env["stock.quant"]._update_available_quantity( + self.product_2, self.warehouse.lot_stock_id, 10 + ) + order_form = Form(self.env["sale.order"]) + order_form.partner_id = self.customer + order_form.client_order_ref = "test_pos_sale_picking_keep" + with order_form.order_line.new() as line_form: + line_form.product_id = self.product + sale_order = order_form.save() + sol = sale_order.order_line + self.assertEqual(sol.qty_delivered, 0) + self.main_pos_config.open_ui() + self.start_pos_tour( + "PosSalePickingKeepMixed", + login="accountman", + ) + self.assertEqual(sale_order.state, "sale") + self.assertEqual(len(sale_order.picking_ids), 1) + pos_order = sol.pos_order_line_ids.order_id + self.assertEqual(pos_order.state, "paid") + self.assertTrue(pos_order.picking_ids) + self.assertEqual(pos_order.picking_ids.state, "done") + so_picking = sale_order.picking_ids + self.assertEqual(so_picking.state, "assigned") + self.assertEqual(sol.qty_delivered, 0) + sale_order.picking_ids.button_validate() + self.assertEqual(so_picking.state, "done") + self.assertEqual(sol.qty_delivered, 1)