From 4b7ed8e45a14e1debc37f2ccf1076c5e19c1debc Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 5 May 2026 08:39:08 +0200 Subject: [PATCH 1/5] [IMP] pos_sale_picking_keep: Avoid unwanted recomputations --- pos_sale_picking_keep/models/pos_order.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pos_sale_picking_keep/models/pos_order.py b/pos_sale_picking_keep/models/pos_order.py index 4c40b68a6d..58fdbc8880 100644 --- a/pos_sale_picking_keep/models/pos_order.py +++ b/pos_sale_picking_keep/models/pos_order.py @@ -27,9 +27,18 @@ def sync_from_ui(self, orders): # Fake the pickings state before calling super for avoiding the move quantity # reduction that is done upstream that effectively cancels the SO pickings pickings = so_lines.move_ids.picking_id - pickings.state = "draft" - res = super().sync_from_ui(orders) - pickings._compute_state() + state_field = self.env["stock.picking"]._fields["state"] + picking_values = {} + # Save picking state values + for picking in pickings: + picking_values[picking.id] = picking.state + # Don't mark the concerned pickings state as dirty to avoid + # unwanted recomputations + with self.env.protecting([state_field], pickings): + pickings.state = "draft" + res = super().sync_from_ui(orders) + for picking in pickings: + picking.state = picking_values[picking.id] return res def _create_order_picking(self): From cfb40d8314a79bb7e7a4d6d29022d985ef5d6e85 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 5 May 2026 16:09:51 +0200 Subject: [PATCH 2/5] [IMP] pos_sale_picking_keep: Allow to choose a strategy to keep or not pickings As in some cases, you want to mix in POS orders the payment of an existing sale and added products in the same POS order, you still want to keep both pickings linked to sale orders AND pos orders. This introduces the keep picking strategy that allows to choose per POS configuration the strategy that fits the best to your case. --- pos_sale_picking_keep/README.rst | 6 +- pos_sale_picking_keep/__manifest__.py | 1 + pos_sale_picking_keep/models/__init__.py | 4 + pos_sale_picking_keep/models/pos_config.py | 16 +++ pos_sale_picking_keep/models/pos_order.py | 15 +-- .../models/pos_order_line.py | 26 +++++ .../models/res_config_settings.py | 12 ++ .../models/sale_order_line.py | 46 ++++++-- pos_sale_picking_keep/models/stock_picking.py | 28 +++++ .../static/description/index.html | 24 ++-- .../tests/tours/pos_sale_picking_keep.esm.js | 15 +++ pos_sale_picking_keep/tests/__init__.py | 1 + .../tests/test_pos_sale_picking_keep.py | 1 + .../test_pos_sale_picking_keep_realtime.py | 104 ++++++++++++++++++ .../views/res_config_settings.xml | 24 ++++ 15 files changed, 281 insertions(+), 42 deletions(-) create mode 100644 pos_sale_picking_keep/models/pos_config.py create mode 100644 pos_sale_picking_keep/models/pos_order_line.py create mode 100644 pos_sale_picking_keep/models/res_config_settings.py create mode 100644 pos_sale_picking_keep/models/stock_picking.py create mode 100644 pos_sale_picking_keep/tests/test_pos_sale_picking_keep_realtime.py create mode 100644 pos_sale_picking_keep/views/res_config_settings.xml diff --git a/pos_sale_picking_keep/README.rst b/pos_sale_picking_keep/README.rst index f1d682f12a..44337f9e6b 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 diff --git a/pos_sale_picking_keep/__manifest__.py b/pos_sale_picking_keep/__manifest__.py index f4cb506b9a..fdc90ac33f 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": ["views/res_config_settings.xml"], "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..ed8a4e604f 100644 --- a/pos_sale_picking_keep/models/__init__.py +++ b/pos_sale_picking_keep/models/__init__.py @@ -2,3 +2,7 @@ from . import pos_order from . import pos_session from . import sale_order_line +from . import pos_config +from . import pos_order_line +from . import stock_picking +from . import res_config_settings diff --git a/pos_sale_picking_keep/models/pos_config.py b/pos_sale_picking_keep/models/pos_config.py new file mode 100644 index 0000000000..cf2d620098 --- /dev/null +++ b/pos_sale_picking_keep/models/pos_config.py @@ -0,0 +1,16 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class PosConfig(models.Model): + _inherit = "pos.config" + + picking_keep_strategy = fields.Selection( + selection=[ + ("keep_sale_pickings", "Keep Sale Pickings Only"), + ("keep_sale_pos_pickings", "Keep Sale and POS Pickings"), + ], + help="When loading sale orders in POS, Odoo cancels the sale pickings." + "Change the strategy here.", + ) diff --git a/pos_sale_picking_keep/models/pos_order.py b/pos_sale_picking_keep/models/pos_order.py index 58fdbc8880..b7eba80763 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): @@ -42,14 +41,6 @@ def sync_from_ui(self, orders): 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 + if self.config_id.picking_keep_strategy == "keep_sale_pickings": + return True + return super()._create_order_picking() 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..b4c5086903 --- /dev/null +++ b/pos_sale_picking_keep/models/pos_order_line.py @@ -0,0 +1,26 @@ +# 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 and when the strategy is to keep both pos and sale order pickings + """ + lines_to_launch = self.filtered( + lambda line: not line.order_id.pos_config.picking_keep_strategy + or ( + line.order_id.pos_config.picking_keep_strategy + == "keep_sale_pos_pickings" + and 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/res_config_settings.py b/pos_sale_picking_keep/models/res_config_settings.py new file mode 100644 index 0000000000..9f5e1b1363 --- /dev/null +++ b/pos_sale_picking_keep/models/res_config_settings.py @@ -0,0 +1,12 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + pos_picking_keep_strategy = fields.Selection( + related="pos_config_id.picking_keep_strategy", + readonly=False, + ) diff --git a/pos_sale_picking_keep/models/sale_order_line.py b/pos_sale_picking_keep/models/sale_order_line.py index f44e3837a2..2c7943a976 100644 --- a/pos_sale_picking_keep/models/sale_order_line.py +++ b/pos_sale_picking_keep/models/sale_order_line.py @@ -1,19 +1,45 @@ # Copyright 2026 Tecnativa - Víctor Martínez # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import api, models +from odoo import models class SaleOrderLine(models.Model): _inherit = "sale.order.line" - # TODO: Delete if merged https://github.com/odoo/odoo/pull/253333 def _compute_qty_delivered(self): - self = self.with_context(from_qty_delivered=True) - return super()._compute_qty_delivered() + res = super()._compute_qty_delivered() - # TODO: Delete if merged https://github.com/odoo/odoo/pull/253333 - @api.model - def _convert_qty(self, sale_line, qty, direction): - if self.env.context.get("from_qty_delivered"): - return 0 - return super()._convert_qty(sale_line=sale_line, qty=qty, direction=direction) + # Mimic what is done at pos_sale level but lowering the quantity + # TODO: Delete if merged https://github.com/odoo/odoo/pull/253333 + def update_qty_delivered_from_pickings(sale_line, pos_lines): + if all( + picking.state == "done" for picking in pos_lines.order_id.picking_ids + ): + sale_line.qty_delivered -= sum( + ( + self._convert_qty(sale_line, pos_line.qty, "p2s") + for pos_line in pos_lines + if sale_line.product_id.type != "service" + ), + 0, + ) + + for sale_line in self: + if ( + sale_line.pos_order_line_ids.order_id.config_id.picking_keep_strategy + in ("keep_sale_pickings", "keep_sale_pos_pickings") + ): + pos_lines = sale_line.pos_order_line_ids.filtered( + lambda order_line: order_line.order_id.state + not in ["cancel", "draft"] + ) + update_qty_delivered_from_pickings(sale_line, pos_lines) + + refund_lines = ( + sale_line.pos_order_line_ids.refund_orderline_ids.filtered( + lambda order_line: order_line.order_id.state + not in ["cancel", "draft"] + ) + ) + update_qty_delivered_from_pickings(sale_line, refund_lines) + return res 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..9742d70886 --- /dev/null +++ b/pos_sale_picking_keep/models/stock_picking.py @@ -0,0 +1,28 @@ +# 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 + and line.order_id.config_id.picking_keep_strategy + in ("keep_sale_pickings", "keep_sale_pos_pickings") + ) + 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/static/description/index.html b/pos_sale_picking_keep/static/description/index.html index 61487a9e41..236044e76c 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.

@@ -391,7 +386,7 @@

Keep sale pickings from PoS

-

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 +396,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 +404,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Tecnativa
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -432,6 +427,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/test_pos_sale_picking_keep.py b/pos_sale_picking_keep/tests/test_pos_sale_picking_keep.py index b85b122233..6db001886b 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 @@ -12,6 +12,7 @@ class TestPosSalePickingKeep(TestPointOfSaleHttpCommon): def setUpClass(cls): super().setUpClass() cls.env.company.point_of_sale_update_stock_quantities = "closing" + cls.main_pos_config.picking_keep_strategy = "keep_sale_pickings" 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 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..94d6298387 --- /dev/null +++ b/pos_sale_picking_keep/tests/test_pos_sale_picking_keep_realtime.py @@ -0,0 +1,104 @@ +# 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 + + +@odoo.tests.tagged("post_install", "-at_install") +class TestPosSalePickingKeep(TestPointOfSaleHttpCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env.company.point_of_sale_update_stock_quantities = "real" + cls.main_pos_config.picking_keep_strategy = "keep_sale_pickings" + 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" + + 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_tour( + "/pos/ui?config_id=%d" % self.main_pos_config.id, + "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.assertFalse(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.main_pos_config.picking_keep_strategy = "keep_sale_pos_pickings" + 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_tour( + "/pos/ui?config_id=%d" % self.main_pos_config.id, + "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) diff --git a/pos_sale_picking_keep/views/res_config_settings.xml b/pos_sale_picking_keep/views/res_config_settings.xml new file mode 100644 index 0000000000..0ee623df6d --- /dev/null +++ b/pos_sale_picking_keep/views/res_config_settings.xml @@ -0,0 +1,24 @@ + + + + + res.config.settings.view.form.inherit.pos_sale_picking_keep + res.config.settings + + + + + + + + + + + From f94560832f1a87bc4625e9be25dc6545d77459d6 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 5 May 2026 16:13:08 +0200 Subject: [PATCH 3/5] [IMP] pos_sale_picking_keep: Add contributors --- pos_sale_picking_keep/README.rst | 12 ++++++++++++ pos_sale_picking_keep/readme/CONTRIBUTORS.md | 6 ++++++ .../static/description/index.html | 19 +++++++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 pos_sale_picking_keep/readme/CONTRIBUTORS.md diff --git a/pos_sale_picking_keep/README.rst b/pos_sale_picking_keep/README.rst index 44337f9e6b..c563f084d3 100644 --- a/pos_sale_picking_keep/README.rst +++ b/pos_sale_picking_keep/README.rst @@ -66,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/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 236044e76c..25d7e2c4bb 100644 --- a/pos_sale_picking_keep/static/description/index.html +++ b/pos_sale_picking_keep/static/description/index.html @@ -380,7 +380,8 @@

Keep sale pickings from PoS

  • Bug Tracker
  • Credits
  • @@ -411,8 +412,22 @@

    Authors

  • Tecnativa
  • +
    +

    Contributors

    + +
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association From f32ffd4b923c5687c7725f7e098027f74261b551 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Wed, 6 May 2026 15:25:02 +0200 Subject: [PATCH 4/5] [IMP] pos_sale_picking_keep: Simplify the configuration The behaviour should be the same when the configuration is applied for both POS pickings and sale orders one. --- pos_sale_picking_keep/models/pos_config.py | 6 +----- pos_sale_picking_keep/models/pos_order.py | 5 ----- pos_sale_picking_keep/models/pos_order_line.py | 2 +- pos_sale_picking_keep/models/res_config_settings.py | 4 ++-- pos_sale_picking_keep/models/sale_order_line.py | 5 +---- pos_sale_picking_keep/models/stock_picking.py | 3 +-- pos_sale_picking_keep/tests/test_pos_sale_picking_keep.py | 2 +- .../tests/test_pos_sale_picking_keep_realtime.py | 5 ++--- pos_sale_picking_keep/views/res_config_settings.xml | 2 +- 9 files changed, 10 insertions(+), 24 deletions(-) diff --git a/pos_sale_picking_keep/models/pos_config.py b/pos_sale_picking_keep/models/pos_config.py index cf2d620098..e459473749 100644 --- a/pos_sale_picking_keep/models/pos_config.py +++ b/pos_sale_picking_keep/models/pos_config.py @@ -6,11 +6,7 @@ class PosConfig(models.Model): _inherit = "pos.config" - picking_keep_strategy = fields.Selection( - selection=[ - ("keep_sale_pickings", "Keep Sale Pickings Only"), - ("keep_sale_pos_pickings", "Keep Sale and POS Pickings"), - ], + keep_picking = fields.Boolean( help="When loading sale orders in POS, Odoo cancels the sale pickings." "Change the strategy here.", ) diff --git a/pos_sale_picking_keep/models/pos_order.py b/pos_sale_picking_keep/models/pos_order.py index b7eba80763..c5b3c8f563 100644 --- a/pos_sale_picking_keep/models/pos_order.py +++ b/pos_sale_picking_keep/models/pos_order.py @@ -39,8 +39,3 @@ def sync_from_ui(self, orders): for picking in pickings: picking.state = picking_values[picking.id] return res - - def _create_order_picking(self): - if self.config_id.picking_keep_strategy == "keep_sale_pickings": - return True - return super()._create_order_picking() diff --git a/pos_sale_picking_keep/models/pos_order_line.py b/pos_sale_picking_keep/models/pos_order_line.py index b4c5086903..c6462463f8 100644 --- a/pos_sale_picking_keep/models/pos_order_line.py +++ b/pos_sale_picking_keep/models/pos_order_line.py @@ -12,7 +12,7 @@ def _launch_stock_rule_from_pos_order_lines(self): line and when the strategy is to keep both pos and sale order pickings """ lines_to_launch = self.filtered( - lambda line: not line.order_id.pos_config.picking_keep_strategy + lambda line: not line.order_id.pos_config.keep_picking or ( line.order_id.pos_config.picking_keep_strategy == "keep_sale_pos_pickings" diff --git a/pos_sale_picking_keep/models/res_config_settings.py b/pos_sale_picking_keep/models/res_config_settings.py index 9f5e1b1363..35e2dcb4de 100644 --- a/pos_sale_picking_keep/models/res_config_settings.py +++ b/pos_sale_picking_keep/models/res_config_settings.py @@ -6,7 +6,7 @@ class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" - pos_picking_keep_strategy = fields.Selection( - related="pos_config_id.picking_keep_strategy", + pos_keep_picking = fields.Boolean( + related="pos_config_id.keep_picking", readonly=False, ) diff --git a/pos_sale_picking_keep/models/sale_order_line.py b/pos_sale_picking_keep/models/sale_order_line.py index 2c7943a976..22ba2c5d38 100644 --- a/pos_sale_picking_keep/models/sale_order_line.py +++ b/pos_sale_picking_keep/models/sale_order_line.py @@ -25,10 +25,7 @@ def update_qty_delivered_from_pickings(sale_line, pos_lines): ) for sale_line in self: - if ( - sale_line.pos_order_line_ids.order_id.config_id.picking_keep_strategy - in ("keep_sale_pickings", "keep_sale_pos_pickings") - ): + if sale_line.pos_order_line_ids.order_id.config_id.keep_picking: pos_lines = sale_line.pos_order_line_ids.filtered( lambda order_line: order_line.order_id.state not in ["cancel", "draft"] diff --git a/pos_sale_picking_keep/models/stock_picking.py b/pos_sale_picking_keep/models/stock_picking.py index 9742d70886..5b4fbea475 100644 --- a/pos_sale_picking_keep/models/stock_picking.py +++ b/pos_sale_picking_keep/models/stock_picking.py @@ -17,8 +17,7 @@ def _create_picking_from_pos_order_lines( """ lines_without_create = lines.filtered( lambda line: line.sale_order_line_id - and line.order_id.config_id.picking_keep_strategy - in ("keep_sale_pickings", "keep_sale_pos_pickings") + and line.order_id.config_id.keep_picking ) return super()._create_picking_from_pos_order_lines( location_dest_id=location_dest_id, 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 6db001886b..295e1c7f6a 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 @@ -12,7 +12,7 @@ class TestPosSalePickingKeep(TestPointOfSaleHttpCommon): def setUpClass(cls): super().setUpClass() cls.env.company.point_of_sale_update_stock_quantities = "closing" - cls.main_pos_config.picking_keep_strategy = "keep_sale_pickings" + cls.main_pos_config.keep_picking = True 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 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 index 94d6298387..5cadc92a2c 100644 --- 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 @@ -12,7 +12,7 @@ class TestPosSalePickingKeep(TestPointOfSaleHttpCommon): def setUpClass(cls): super().setUpClass() cls.env.company.point_of_sale_update_stock_quantities = "real" - cls.main_pos_config.picking_keep_strategy = "keep_sale_pickings" + cls.main_pos_config.keep_picking = True 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 @@ -60,7 +60,7 @@ def test_sale_order_pos_order_done(self): self.assertEqual(len(sale_order.picking_ids), 1) pos_order = sol.pos_order_line_ids.order_id self.assertEqual(pos_order.state, "paid") - self.assertFalse(pos_order.picking_ids) + self.assertTrue(pos_order.picking_ids) so_picking = sale_order.picking_ids self.assertEqual(so_picking.state, "assigned") self.assertEqual(sol.qty_delivered, 0) @@ -69,7 +69,6 @@ def test_sale_order_pos_order_done(self): self.assertEqual(sol.qty_delivered, 1) def test_sale_order_pos_order_done_and_pos_picking(self): - self.main_pos_config.picking_keep_strategy = "keep_sale_pos_pickings" self.env["stock.quant"]._update_available_quantity( self.product, self.warehouse.lot_stock_id, 1 ) diff --git a/pos_sale_picking_keep/views/res_config_settings.xml b/pos_sale_picking_keep/views/res_config_settings.xml index 0ee623df6d..d6344e0775 100644 --- a/pos_sale_picking_keep/views/res_config_settings.xml +++ b/pos_sale_picking_keep/views/res_config_settings.xml @@ -16,7 +16,7 @@ string="Keep Picking Strategy" help="Choose a keep picking strategy." > - + From 2d435608df8cbf0618828f483acc984b13f8e87d Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Wed, 6 May 2026 15:31:13 +0200 Subject: [PATCH 5/5] [IMP] pos_sale_picking_keep: Add common tests class --- pos_sale_picking_keep/tests/common.py | 34 +++++++++++++++++++ .../tests/test_pos_sale_picking_keep.py | 20 ++--------- .../test_pos_sale_picking_keep_realtime.py | 28 ++------------- 3 files changed, 38 insertions(+), 44 deletions(-) create mode 100644 pos_sale_picking_keep/tests/common.py diff --git a/pos_sale_picking_keep/tests/common.py b/pos_sale_picking_keep/tests/common.py new file mode 100644 index 0000000000..c1de50cbd7 --- /dev/null +++ b/pos_sale_picking_keep/tests/common.py @@ -0,0 +1,34 @@ +# 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.main_pos_config.keep_picking = True + 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 295e1c7f6a..1823c6328f 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,31 +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.main_pos_config.keep_picking = True - 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( 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 index 5cadc92a2c..3242f538b0 100644 --- 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 @@ -1,39 +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 TestPosSalePickingKeepRealtime(PosSalePickingKeepCommon): @classmethod def setUpClass(cls): super().setUpClass() cls.env.company.point_of_sale_update_stock_quantities = "real" - cls.main_pos_config.keep_picking = True - 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" def test_sale_order_pos_order_done(self): self.env["stock.quant"]._update_available_quantity(