diff --git a/repair_picking_after_done/models/__init__.py b/repair_picking_after_done/models/__init__.py index 3985e558..09184778 100644 --- a/repair_picking_after_done/models/__init__.py +++ b/repair_picking_after_done/models/__init__.py @@ -1 +1,2 @@ from . import repair +from . import stock_move diff --git a/repair_picking_after_done/models/stock_move.py b/repair_picking_after_done/models/stock_move.py new file mode 100644 index 00000000..77403b21 --- /dev/null +++ b/repair_picking_after_done/models/stock_move.py @@ -0,0 +1,96 @@ +from odoo import _, models +from odoo.exceptions import UserError +from odoo.tools import float_is_zero, float_round +from odoo.tools.float_utils import float_compare + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _split(self, qty, restrict_partner_id=False): + """Splits `self` quantity and return values + for a new moves to be created afterwards + + This method is based on the core Odoo `stock.move` + `_split` method with the following customization: + - Allows splitting stock moves related to repair + orders that are marked as "done", which is prevented + by the core Odoo logic. + - This change enables the creation of backorders for + these split stock moves when the associated repair is completed. + + Note: This customization overrides logic in both the `stock` + and `repair` modules to ensure that backorders can be created + for completed repairs, while adhering to the business rules of both modules. + + :param qty: float. quantity to split (given in product UoM) + :param restrict_partner_id: optional partner that can be given in order + to force the new move to restrict its choice of quants to the ones + belonging to this partner. + :returns: list of dict. stock move values""" + self.ensure_one() + + # Custom logic: Prevent splitting stock moves tied to incomplete repairs + if self.repair_id and self.repair_id.state != "done": + return [] + + # --- Core logic begins here --- + + if self.state in ("done", "cancel"): + raise UserError( + _( + "You cannot split a stock move that has been " + "set to 'Done' or 'Cancel'." + ) + ) + elif self.state == "draft": + raise UserError( + _("You cannot split a draft move. It needs to be confirmed first.") + ) + + if float_is_zero(qty, precision_rounding=self.product_id.uom_id.rounding): + return [] + + decimal_precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + + uom_qty = self.product_id.uom_id._compute_quantity( + qty, self.product_uom, rounding_method="HALF-UP" + ) + if ( + float_compare( + qty, + self.product_uom._compute_quantity( + uom_qty, self.product_id.uom_id, rounding_method="HALF-UP" + ), + precision_digits=decimal_precision, + ) + == 0 + ): + defaults = self._prepare_move_split_vals(uom_qty) + else: + defaults = self.with_context( + force_split_uom_id=self.product_id.uom_id.id + )._prepare_move_split_vals(qty) + + if restrict_partner_id: + defaults["restrict_partner_id"] = restrict_partner_id + + if self.env.context.get("source_location_id"): + defaults["location_id"] = self.env.context["source_location_id"] + new_move_vals = self.copy_data(defaults) + + new_product_qty = self.product_id.uom_id._compute_quantity( + max(0, self.product_qty - qty), self.product_uom, round=False + ) + new_product_qty = float_round( + new_product_qty, + precision_digits=self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ), + ) + self.with_context(do_not_unreserve=True).write( + {"product_uom_qty": new_product_qty} + ) + return new_move_vals diff --git a/repair_picking_after_done/tests/__init__.py b/repair_picking_after_done/tests/__init__.py index 7d52509a..d7527875 100644 --- a/repair_picking_after_done/tests/__init__.py +++ b/repair_picking_after_done/tests/__init__.py @@ -2,3 +2,4 @@ # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) from . import test_repair_transfers +from . import test_stock_move_split diff --git a/repair_picking_after_done/tests/test_stock_move_split.py b/repair_picking_after_done/tests/test_stock_move_split.py new file mode 100644 index 00000000..e3b60ffb --- /dev/null +++ b/repair_picking_after_done/tests/test_stock_move_split.py @@ -0,0 +1,58 @@ +from odoo.tests.common import TransactionCase + + +class TestStockMoveSplit(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Create a product + cls.product = cls.env["product.product"].create( + { + "name": "Test Product", + "type": "product", + } + ) + + # Create a stock location + cls.location = cls.env["stock.location"].create( + { + "name": "Test Location", + "usage": "internal", + } + ) + + # Create a repair order in 'draft' state + cls.repair = cls.env["repair.order"].create( + { + "name": "Repair Order 1", + "product_id": cls.product.id, + "state": "draft", + } + ) + + # Create a stock move linked to the repair order + cls.stock_move = cls.env["stock.move"].create( + { + "name": "Stock Move for Repair Order 1", + "product_id": cls.product.id, + "product_uom_qty": 10.0, + "product_uom": cls.product.uom_id.id, + "repair_id": cls.repair.id, + "state": "confirmed", + "location_id": cls.location.id, + "location_dest_id": cls.location.id, + } + ) + + def test_split_move_with_incomplete_repair(self): + """Ensure a stock move linked to an incomplete repair cannot be split""" + result = self.stock_move._split(5.0) + self.assertEqual( + result, [], "Move should not split as the repair is not 'done'." + ) + + def test_split_move_with_completed_repair(self): + """Ensure a stock move linked to a completed repair can be split""" + self.repair.write({"state": "done"}) + result = self.stock_move._split(5.0) + self.assertTrue(result, "Move should split as the repair is 'done'.")