diff --git a/setup/stock_request_bom/odoo/addons/stock_request_bom b/setup/stock_request_bom/odoo/addons/stock_request_bom new file mode 120000 index 000000000..2680c074a --- /dev/null +++ b/setup/stock_request_bom/odoo/addons/stock_request_bom @@ -0,0 +1 @@ +../../../../stock_request_bom \ No newline at end of file diff --git a/setup/stock_request_bom/setup.py b/setup/stock_request_bom/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/stock_request_bom/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_request_bom/README.rst b/stock_request_bom/README.rst new file mode 100644 index 000000000..35565ea32 --- /dev/null +++ b/stock_request_bom/README.rst @@ -0,0 +1,88 @@ +================= +Stock Request BOM +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d20525ad991c601bd8d4376af55d594a40d78800669d3dde725c6acfccda8081 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-warehouse/tree/15.0/stock_request_bom + :alt: OCA/stock-logistics-warehouse +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/stock-logistics-warehouse-15-0/stock-logistics-warehouse-15-0-stock_request_bom + :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/stock-logistics-warehouse&target_branch=15.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module enhances the stock request functionality by integrating with the Bill of Materials (BOM). + +Key Features: +- Adds Product BOM and Quantity BOM fields to stock request orders. +- Auto-completes stock request lines based on the selected BOM and quantity. +- Allows updating and clearing BOM selections. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Select a Product BOM and enter a Quantity BOM. +2. Stock request lines will auto-fill with BOM components. +3. Adjust quantity BOM as needed. + +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 +~~~~~~~ + +* ForgeFlow + +Contributors +~~~~~~~~~~~~ + +* Joan Sisquella Andrés + +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/stock-logistics-warehouse `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/stock_request_bom/__init__.py b/stock_request_bom/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/stock_request_bom/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_request_bom/__manifest__.py b/stock_request_bom/__manifest__.py new file mode 100644 index 000000000..f5d28e08d --- /dev/null +++ b/stock_request_bom/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +{ + "name": "Stock Request BOM", + "summary": "Stock Request with BOM Integration", + "version": "15.0.1.0.0", + "license": "LGPL-3", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "category": "Warehouse Management", + "depends": ["stock_request", "mrp"], + "data": [ + "views/stock_request_order_views.xml", + ], + "installable": True, + "auto_install": True, +} diff --git a/stock_request_bom/models/__init__.py b/stock_request_bom/models/__init__.py new file mode 100644 index 000000000..48b33d209 --- /dev/null +++ b/stock_request_bom/models/__init__.py @@ -0,0 +1 @@ +from . import stock_request_order diff --git a/stock_request_bom/models/stock_request_order.py b/stock_request_bom/models/stock_request_order.py new file mode 100644 index 000000000..0097ea3af --- /dev/null +++ b/stock_request_bom/models/stock_request_order.py @@ -0,0 +1,80 @@ +# Copyright 2024 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class StockRequestOrder(models.Model): + _inherit = "stock.request.order" + + product_bom_id = fields.Many2one( + "product.product", + string="Product BOM", + domain="[('bom_ids', '!=', False)]", + help="Select a product with a BOM to auto-complete stock request lines.", + ) + quantity_bom = fields.Float( + string="Quantity BOM", + help="Specify the quantity for the Product BOM.", + default=1.0, + ) + + @api.constrains("quantity_bom") + def _check_quantity_bom(self): + for order in self: + if order.quantity_bom <= 0: + raise ValidationError(_("The quantity BOM must be a positive value.")) + + def _prepare_bom_line(self, product, line, line_qty): + """Prepare a dictionary for a BOM line.""" + return { + "product_id": product.id, + "product_uom_id": line.product_uom_id.id, + "product_uom_qty": line_qty, + "warehouse_id": self.warehouse_id.id, + "location_id": self.location_id.id, + "company_id": self.company_id.id, + "requested_by": self.requested_by.id, + "expected_date": self.expected_date, + } + + def _get_bom_lines_recursive(self, bom, quantity): + """Recursively fetch BOM lines for a given BOM and quantity.""" + bom_lines = [] + bom_done, lines_done = bom.explode(self.product_bom_id, quantity) + for line, line_data in lines_done: + product = line.product_id + line_qty = line_data["qty"] + sub_bom = self.env["mrp.bom"]._bom_find(product).get(product) + if sub_bom: + # Recursively fetch lines from the sub-BOM + bom_lines += self._get_bom_lines_recursive( + sub_bom, line_qty / sub_bom.product_qty + ) + else: + # Add the line directly if no sub-BOM is found + bom_lines.append( + ( + 0, + 0, + self._prepare_bom_line(product, line, line_qty), + ) + ) + return bom_lines + + @api.onchange("product_bom_id", "quantity_bom") + def _onchange_product_bom(self): + if not self.product_bom_id: + self.stock_request_ids = [(5, 0, 0)] + return + + bom = ( + self.env["mrp.bom"]._bom_find(self.product_bom_id).get(self.product_bom_id) + ) + if not bom: + raise UserError(_("No BOM found for the selected product.")) + # Clear the existing stock request lines + self.stock_request_ids = [(5, 0, 0)] + bom_lines = self._get_bom_lines_recursive(bom, self.quantity_bom) + self.stock_request_ids = bom_lines diff --git a/stock_request_bom/readme/CONTRIBUTORS.rst b/stock_request_bom/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..02c39fd18 --- /dev/null +++ b/stock_request_bom/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Joan Sisquella Andrés diff --git a/stock_request_bom/readme/DESCRIPTION.rst b/stock_request_bom/readme/DESCRIPTION.rst new file mode 100644 index 000000000..7277e8e29 --- /dev/null +++ b/stock_request_bom/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module enhances the stock request functionality by integrating with the Bill of Materials (BOM). + +Key Features: +- Adds Product BOM and Quantity BOM fields to stock request orders. +- Auto-completes stock request lines based on the selected BOM and quantity. +- Allows updating and clearing BOM selections. diff --git a/stock_request_bom/readme/USAGE.rst b/stock_request_bom/readme/USAGE.rst new file mode 100644 index 000000000..40360d25c --- /dev/null +++ b/stock_request_bom/readme/USAGE.rst @@ -0,0 +1,3 @@ +1. Select a Product BOM and enter a Quantity BOM. +2. Stock request lines will auto-fill with BOM components. +3. Adjust quantity BOM as needed. diff --git a/stock_request_bom/static/description/index.html b/stock_request_bom/static/description/index.html new file mode 100644 index 000000000..c6b3f7bcd --- /dev/null +++ b/stock_request_bom/static/description/index.html @@ -0,0 +1,433 @@ + + + + + +Stock Request BOM + + + +
+

Stock Request BOM

+ + +

Beta License: LGPL-3 OCA/stock-logistics-warehouse Translate me on Weblate Try me on Runboat

+

This module enhances the stock request functionality by integrating with the Bill of Materials (BOM).

+

Key Features: +- Adds Product BOM and Quantity BOM fields to stock request orders. +- Auto-completes stock request lines based on the selected BOM and quantity. +- Allows updating and clearing BOM selections.

+

Table of contents

+ +
+

Usage

+
    +
  1. Select a Product BOM and enter a Quantity BOM.
  2. +
  3. Stock request lines will auto-fill with BOM components.
  4. +
  5. Adjust quantity BOM as needed.
  6. +
+
+
+

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

+
    +
  • ForgeFlow
  • +
+
+
+

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/stock-logistics-warehouse project on GitHub.

+

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

+
+
+
+ + diff --git a/stock_request_bom/tests/__init__.py b/stock_request_bom/tests/__init__.py new file mode 100644 index 000000000..9b96587d5 --- /dev/null +++ b/stock_request_bom/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_request_bom diff --git a/stock_request_bom/tests/test_stock_request_bom.py b/stock_request_bom/tests/test_stock_request_bom.py new file mode 100644 index 000000000..32ac1d7a3 --- /dev/null +++ b/stock_request_bom/tests/test_stock_request_bom.py @@ -0,0 +1,99 @@ +# Copyright 2024 ForgeFlow S.L. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0). + +from odoo.exceptions import UserError, ValidationError +from odoo.tests import Form + +from odoo.addons.stock_request.tests.test_stock_request import TestStockRequest + + +class TestStockRequestBOM(TestStockRequest): + def setUp(self): + super().setUp() + self.mrp_user_group = self.env.ref("mrp.group_mrp_user") + self.stock_request_user.write({"groups_id": [(4, self.mrp_user_group.id)]}) + self.stock_request_manager.write({"groups_id": [(4, self.mrp_user_group.id)]}) + self.route_manufacture = self.warehouse.manufacture_pull_id.route_id + self.product.write({"route_ids": [(6, 0, self.route_manufacture.ids)]}) + + self.raw_1 = self._create_product("SL", "Sole", False) + self.raw_2 = self._create_product("LC", "Lace", False) + self.raw_3 = self._create_product("BT", "Button", False) + + self._update_qty_in_location(self.warehouse.lot_stock_id, self.raw_1, 10) + self._update_qty_in_location(self.warehouse.lot_stock_id, self.raw_2, 10) + self._update_qty_in_location(self.warehouse.lot_stock_id, self.raw_3, 10) + + self.bom = self._create_mrp_bom(self.product, [self.raw_1, self.raw_2]) + self.bom_raw_2 = self._create_mrp_bom(self.raw_2, [self.raw_3]) + + def _update_qty_in_location(self, location, product, quantity): + self.env["stock.quant"]._update_available_quantity(product, location, quantity) + + def _create_mrp_bom(self, product_id, raw_materials): + bom = self.env["mrp.bom"].create( + { + "product_id": product_id.id, + "product_tmpl_id": product_id.product_tmpl_id.id, + "product_uom_id": product_id.uom_id.id, + "product_qty": 1.0, + "type": "normal", + } + ) + for raw_mat in raw_materials: + self.env["mrp.bom.line"].create( + {"bom_id": bom.id, "product_id": raw_mat.id, "product_qty": 1} + ) + return bom + + def test_01_create_stock_request_with_bom(self): + order_form = Form(self.request_order.with_user(self.stock_request_user)) + order_form.product_bom_id = self.product + order_form.quantity_bom = 2 + order = order_form.save() + self.assertEqual(len(order.stock_request_ids), 2) + + product_ids = order.stock_request_ids.mapped("product_id") + expected_products = {self.raw_1.id, self.raw_3.id} + actual_products = {product.id for product in product_ids} + self.assertEqual(expected_products, actual_products) + + def test_02_update_quantity_bom(self): + order_form = Form(self.request_order.with_user(self.stock_request_user)) + order_form.product_bom_id = self.product + order_form.quantity_bom = 1 + order = order_form.save() + for line in order.stock_request_ids: + self.assertEqual(line.product_uom_qty, 1) + + order_form = Form(order) + order_form.quantity_bom = 3 + order = order_form.save() + for line in order.stock_request_ids: + self.assertEqual(line.product_uom_qty, 3) + + def test_03_clear_product_bom(self): + order_form = Form(self.request_order.with_user(self.stock_request_user)) + order_form.product_bom_id = self.product + order_form.quantity_bom = 1 + order = order_form.save() + self.assertEqual(len(order.stock_request_ids), 2) + self.request_order.write({"product_bom_id": False}) + order_form = Form(order) + order_form.product_bom_id = self.env["product.product"] + order = order_form.save() + self.assertEqual(len(order.stock_request_ids), 0) + + def test_04_invalid_quantity_bom(self): + order_form = Form(self.request_order.with_user(self.stock_request_user)) + order_form.product_bom_id = self.product + order_form.quantity_bom = -1 + + with self.assertRaises(ValidationError): + order_form.save() + + def test_05_product_without_bom(self): + product_without_bom = self._create_product("PWB", "Product Without BOM", False) + order_form = Form(self.request_order.with_user(self.stock_request_user)) + with self.assertRaises(UserError): + order_form.product_bom_id = product_without_bom diff --git a/stock_request_bom/views/stock_request_order_views.xml b/stock_request_bom/views/stock_request_order_views.xml new file mode 100644 index 000000000..a4a5f231e --- /dev/null +++ b/stock_request_bom/views/stock_request_order_views.xml @@ -0,0 +1,19 @@ + + + + + stock.request.order.form.inherit.bom + stock.request.order + + + + + + + + +