diff --git a/repair_preparation_purchase/README.rst b/repair_preparation_purchase/README.rst new file mode 100644 index 00000000..4b7be5fe --- /dev/null +++ b/repair_preparation_purchase/README.rst @@ -0,0 +1,78 @@ +=========================== +Repair Preparation Purchase +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7de56899985844b5e7af07db24664b0ad319d0b844b2d285a2afcd11fa26a331 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/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%2Frepair-lightgray.png?logo=github + :target: https://github.com/OCA/repair/tree/16.0/repair_preparation_purchase + :alt: OCA/repair +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/repair-16-0/repair-16-0-repair_preparation_purchase + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/repair&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This addon extends **``repair_preparation``** by adding a direct link +between a **Repair Order** and the **Purchase Orders** created by +procurement during the preparation stage (e.g., MTO+Buy flows). + +**Table of contents** + +.. contents:: + :local: + +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 +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ACSONE SA/NV + +Contributors +------------ + +- Souheil Bejaoui souheil.bejaoui@acsone.eu + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/repair `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/repair_preparation_purchase/__init__.py b/repair_preparation_purchase/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/repair_preparation_purchase/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/repair_preparation_purchase/__manifest__.py b/repair_preparation_purchase/__manifest__.py new file mode 100644 index 00000000..a006358e --- /dev/null +++ b/repair_preparation_purchase/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Repair Preparation Purchase", + "summary": """This addon add link""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/repair", + "depends": ["repair_preparation", "purchase_stock"], + "data": [ + "views/purchase_order.xml", + "views/repair_order.xml", + ], + "demo": [], +} diff --git a/repair_preparation_purchase/models/__init__.py b/repair_preparation_purchase/models/__init__.py new file mode 100644 index 00000000..95044c2f --- /dev/null +++ b/repair_preparation_purchase/models/__init__.py @@ -0,0 +1,3 @@ +from . import repair_order +from . import purchase_order_line +from . import purchase_order diff --git a/repair_preparation_purchase/models/purchase_order.py b/repair_preparation_purchase/models/purchase_order.py new file mode 100644 index 00000000..942ca9b7 --- /dev/null +++ b/repair_preparation_purchase/models/purchase_order.py @@ -0,0 +1,30 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class PurchaseOrder(models.Model): + + _inherit = "purchase.order" + + repair_ids = fields.Many2many( + comodel_name="repair.order", compute="_compute_repair_ids" + ) + count_repair = fields.Integer(compute="_compute_repair_ids") + + @api.depends("order_line.repair_line_id") + def _compute_repair_ids(self): + for rec in self: + rec.repair_ids = rec.order_line.repair_line_id.repair_id + rec.count_repair = len(rec.repair_ids) + + def action_view_repair_order(self): + return { + "type": "ir.actions.act_window", + "name": _("Repair Order(s)"), + "res_model": self.repair_ids._name, + "domain": [("id", "in", self.repair_ids.ids)], + "view_mode": "tree,form", + "context": self.env.context, + } diff --git a/repair_preparation_purchase/models/purchase_order_line.py b/repair_preparation_purchase/models/purchase_order_line.py new file mode 100644 index 00000000..2cf4092d --- /dev/null +++ b/repair_preparation_purchase/models/purchase_order_line.py @@ -0,0 +1,23 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class PurchaseOrderLine(models.Model): + + _inherit = "purchase.order.line" + + repair_line_id = fields.Many2one( + comodel_name="repair.line", ondelete="cascade", readonly=True + ) + + @api.model + def _prepare_purchase_order_line_from_procurement( + self, product_id, product_qty, product_uom, company_id, values, po + ): + po_line_vals = super()._prepare_purchase_order_line_from_procurement( + product_id, product_qty, product_uom, company_id, values, po + ) + po_line_vals["repair_line_id"] = values.get("repair_line_id") + return po_line_vals diff --git a/repair_preparation_purchase/models/repair_order.py b/repair_preparation_purchase/models/repair_order.py new file mode 100644 index 00000000..2336a05a --- /dev/null +++ b/repair_preparation_purchase/models/repair_order.py @@ -0,0 +1,30 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class RepairOrder(models.Model): + + _inherit = "repair.order" + + preparation_purchase_ids = fields.Many2many( + comodel_name="purchase.order", compute="_compute_preparation_purchase_ids" + ) + + @api.depends("operations") + def _compute_preparation_purchase_ids(self): + for rec in self: + rec.preparation_purchase_ids = self.env["purchase.order"].search( + [("order_line.repair_line_id", "in", rec.operations.ids)] + ) + + def action_view_preparation_purchase_order(self): + return { + "type": "ir.actions.act_window", + "name": _("Preparation Purchase Order(s)"), + "res_model": self.preparation_purchase_ids._name, + "domain": [("id", "in", self.preparation_purchase_ids.ids)], + "view_mode": "tree,form", + "context": self.env.context, + } diff --git a/repair_preparation_purchase/readme/CONTRIBUTORS.md b/repair_preparation_purchase/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..dbdd727b --- /dev/null +++ b/repair_preparation_purchase/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Souheil Bejaoui diff --git a/repair_preparation_purchase/readme/DESCRIPTION.md b/repair_preparation_purchase/readme/DESCRIPTION.md new file mode 100644 index 00000000..dab4e464 --- /dev/null +++ b/repair_preparation_purchase/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This addon extends **`repair_preparation`** by adding a direct link between +a **Repair Order** and the **Purchase Orders** created by procurement during +the preparation stage (e.g., MTO+Buy flows). diff --git a/repair_preparation_purchase/static/description/icon.png b/repair_preparation_purchase/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/repair_preparation_purchase/static/description/icon.png differ diff --git a/repair_preparation_purchase/static/description/index.html b/repair_preparation_purchase/static/description/index.html new file mode 100644 index 00000000..2de460d8 --- /dev/null +++ b/repair_preparation_purchase/static/description/index.html @@ -0,0 +1,425 @@ + + + + + +Repair Preparation Purchase + + + +
+

Repair Preparation Purchase

+ + +

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

+

This addon extends ``repair_preparation`` by adding a direct link +between a Repair Order and the Purchase Orders created by +procurement during the preparation stage (e.g., MTO+Buy flows).

+

Table of contents

+ +
+

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 +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/repair project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/repair_preparation_purchase/tests/__init__.py b/repair_preparation_purchase/tests/__init__.py new file mode 100644 index 00000000..2402eba1 --- /dev/null +++ b/repair_preparation_purchase/tests/__init__.py @@ -0,0 +1 @@ +from . import test_repair_preparation_purchase diff --git a/repair_preparation_purchase/tests/test_repair_preparation_purchase.py b/repair_preparation_purchase/tests/test_repair_preparation_purchase.py new file mode 100644 index 00000000..e44835d3 --- /dev/null +++ b/repair_preparation_purchase/tests/test_repair_preparation_purchase.py @@ -0,0 +1,170 @@ +# Copyright 2025 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestRepairPreparationPurchase(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.partner = cls.env["res.partner"].create({"name": "test partner"}) + cls.vendor = cls.env["res.partner"].create({"name": "test vendor"}) + + cls.product = cls.env["product.product"].create( + {"name": "product to repair", "type": "product"} + ) + cls.product_c = cls.env["product.product"].create( + {"name": "product to consume", "type": "product"} + ) + cls.product_c_2 = cls.env["product.product"].create( + {"name": "product to consume 2", "type": "product"} + ) + cls.env["product.supplierinfo"].create( + { + "partner_id": cls.vendor.id, + "product_tmpl_id": cls.product_c.product_tmpl_id.id, + "price": 1.0, + "min_qty": 1.0, + } + ) + cls.env["product.supplierinfo"].create( + { + "partner_id": cls.vendor.id, + "product_tmpl_id": cls.product_c_2.product_tmpl_id.id, + "price": 1.0, + "min_qty": 1.0, + } + ) + + cls.warehouse = cls.env["stock.warehouse"].create( + {"name": "WH", "code": "wh_test"} + ) + cls.stock_loc = cls.warehouse.lot_stock_id + cls.in_type = cls.warehouse.in_type_id + cls.prep_loc = cls.env["stock.location"].create( + { + "name": "Preparation", + "usage": "internal", + "location_id": cls.stock_loc.id, + "company_id": cls.warehouse.company_id.id, + } + ) + cls.prep_type = cls.env["stock.picking.type"].create( + { + "name": "Preparation", + "code": "internal", + "warehouse_id": cls.warehouse.id, + "sequence_code": "PREP", + "default_location_src_id": cls.stock_loc.id, + "default_location_dest_id": cls.prep_loc.id, + } + ) + cls.warehouse.write( + { + "repair_preparation_enabled": True, + "repair_preparation_picking_type_id": cls.prep_type.id, + } + ) + + # Pull from Stock -> Preparation, but as MTO to propagate upstream procurement + cls.prep_route = cls.env["stock.route"].create( + { + "name": "Route to Preparation (MTO)", + "product_selectable": True, + "warehouse_selectable": False, + "company_id": cls.warehouse.company_id.id, + } + ) + cls.prep_rule = cls.env["stock.rule"].create( + { + "name": "Pull to Preparation (MTO)", + "route_id": cls.prep_route.id, + "action": "pull", + "procure_method": "make_to_order", + "picking_type_id": cls.prep_type.id, + "location_src_id": cls.stock_loc.id, + "location_dest_id": cls.prep_loc.id, + "warehouse_id": cls.warehouse.id, + } + ) + + # Route: Buy to Stock (so the upstream procurement buys what is missing) + cls.buy_route = cls.env["stock.route"].create( + { + "name": "Buy to WH Stock", + "product_selectable": True, + "warehouse_selectable": False, + "company_id": cls.warehouse.company_id.id, + } + ) + cls.buy_rule = cls.env["stock.rule"].create( + { + "name": "Buy to Stock", + "route_id": cls.buy_route.id, + "action": "buy", + "warehouse_id": cls.warehouse.id, + "location_dest_id": cls.stock_loc.id, + "picking_type_id": cls.in_type.id, + } + ) + + (cls.product_c + cls.product_c_2).route_ids += cls.prep_route + cls.buy_route + cls.env["stock.quant"]._update_available_quantity( + cls.product, cls.prep_loc, 1.0 + ) + + cls.repair = cls.env["repair.order"].create( + { + "partner_id": cls.partner.id, + "product_id": cls.product.id, + "location_id": cls.prep_loc.id, + "priority": "1", + "company_id": cls.warehouse.company_id.id, + } + ) + cls.line = cls._create_repair_line(cls.product_c) + + @classmethod + def _create_repair_line(cls, product): + return cls.env["repair.line"].create( + { + "name": "replace product", + "repair_id": cls.repair.id, + "type": "add", + "price_unit": 100, + "product_id": product.id, + "product_uom_qty": 2.0, + } + ) + + @classmethod + def _get_available_qty(cls, product, location): + return cls.env["stock.quant"]._get_available_quantity(product, location) + + def test_validate_runs_procurement_creates_purchase(self): + self.repair.action_validate() + self.assertEqual(self.repair.state, "confirmed") + self.assertTrue(self.line.preparation_move_ids) + self.assertTrue(self.repair.preparation_picking_ids) + self.assertTrue(self.repair.preparation_purchase_ids) + po = self.repair.preparation_purchase_ids + po_line = po.order_line + self.assertTrue(po) + self.assertTrue(po_line) + self.assertEqual(po_line.product_id, self.line.product_id) + self.assertEqual(po_line.product_uom_qty, self.line.product_uom_qty) + + def test_create_new_line_under_repair_triggers_procurement(self): + self.test_validate_runs_procurement_creates_purchase() + self.repair.action_repair_start() + self.assertEqual(self.repair.state, "under_repair") + po = self.repair.preparation_purchase_ids + po_line = po.order_line + new_line = self._create_repair_line(self.product_c_2) + self.assertTrue(new_line.preparation_move_ids) + self.assertEqual(len(po.order_line), 2) + new_po_line = po.order_line - po_line + self.assertEqual(new_po_line.product_id, new_line.product_id) + self.assertEqual(new_po_line.product_uom_qty, new_line.product_uom_qty) diff --git a/repair_preparation_purchase/views/purchase_order.xml b/repair_preparation_purchase/views/purchase_order.xml new file mode 100644 index 00000000..5236449b --- /dev/null +++ b/repair_preparation_purchase/views/purchase_order.xml @@ -0,0 +1,30 @@ + + + + + + purchase.order + + + + + + + + + + + + diff --git a/repair_preparation_purchase/views/repair_order.xml b/repair_preparation_purchase/views/repair_order.xml new file mode 100644 index 00000000..e38451dc --- /dev/null +++ b/repair_preparation_purchase/views/repair_order.xml @@ -0,0 +1,30 @@ + + + + + + repair.order + + + + + + + + + + + diff --git a/setup/repair_preparation_purchase/odoo/addons/repair_preparation_purchase b/setup/repair_preparation_purchase/odoo/addons/repair_preparation_purchase new file mode 120000 index 00000000..f0acf64e --- /dev/null +++ b/setup/repair_preparation_purchase/odoo/addons/repair_preparation_purchase @@ -0,0 +1 @@ +../../../../repair_preparation_purchase \ No newline at end of file diff --git a/setup/repair_preparation_purchase/setup.py b/setup/repair_preparation_purchase/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/repair_preparation_purchase/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..91aea903 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +odoo-addon-repair-preparation @ git+https://github.com/OCA/repair.git@refs/pull/119/head#subdirectory=setup/repair_preparation