diff --git a/setup/stock_reserve_sale_mrp/odoo/addons/stock_reserve_sale_mrp b/setup/stock_reserve_sale_mrp/odoo/addons/stock_reserve_sale_mrp new file mode 120000 index 000000000..fed31ff44 --- /dev/null +++ b/setup/stock_reserve_sale_mrp/odoo/addons/stock_reserve_sale_mrp @@ -0,0 +1 @@ +../../../../stock_reserve_sale_mrp \ No newline at end of file diff --git a/setup/stock_reserve_sale_mrp/setup.py b/setup/stock_reserve_sale_mrp/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/stock_reserve_sale_mrp/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_reserve_sale_mrp/README.rst b/stock_reserve_sale_mrp/README.rst new file mode 100644 index 000000000..95dd788ee --- /dev/null +++ b/stock_reserve_sale_mrp/README.rst @@ -0,0 +1,77 @@ +======================= +Stock Reserve Sales MRP +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a556e23bd74738cc418b2d0c9b018d6793c1c2cc56130b9274a823020588d5bd + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fstock--logistics--warehouse-lightgray.png?logo=github + :target: https://github.com/OCA/stock-logistics-warehouse/tree/13.0/stock_reserve_sale_mrp + :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-13-0/stock-logistics-warehouse-13-0-stock_reserve_sale_mrp + :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=13.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Extends the functionality of `sale_reserve_sale` to allow te possibility to also make stock reservations on the components of a KIT product. + +**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 +~~~~~~~ + +* Forgeflow + +Contributors +~~~~~~~~~~~~ + +* Jaume Bernaus + +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_reserve_sale_mrp/__init__.py b/stock_reserve_sale_mrp/__init__.py new file mode 100644 index 000000000..40272379f --- /dev/null +++ b/stock_reserve_sale_mrp/__init__.py @@ -0,0 +1 @@ +from . import wizard diff --git a/stock_reserve_sale_mrp/__manifest__.py b/stock_reserve_sale_mrp/__manifest__.py new file mode 100644 index 000000000..36724e922 --- /dev/null +++ b/stock_reserve_sale_mrp/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2023 ForgeFlow S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Stock Reserve Sales MRP", + "version": "13.0.1.1.0", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "category": "Warehouse", + "license": "AGPL-3", + "complexity": "normal", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "depends": ["stock_reserve_sale", "mrp"], + "data": [], + "installable": True, + "auto_install": True, +} diff --git a/stock_reserve_sale_mrp/tests/__init__.py b/stock_reserve_sale_mrp/tests/__init__.py new file mode 100644 index 000000000..e62f63206 --- /dev/null +++ b/stock_reserve_sale_mrp/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_reserve_sale_mrp diff --git a/stock_reserve_sale_mrp/tests/test_stock_reserve_sale_mrp.py b/stock_reserve_sale_mrp/tests/test_stock_reserve_sale_mrp.py new file mode 100644 index 000000000..15590fa94 --- /dev/null +++ b/stock_reserve_sale_mrp/tests/test_stock_reserve_sale_mrp.py @@ -0,0 +1,63 @@ +# Copyright 2023 ForgeFlow S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo.tests import Form + +from odoo.addons.stock_reserve_sale.tests.test_stock_reserve_sale import ( + TestStockReserveSale, +) + + +class TestStockReserveSaleMRP(TestStockReserveSale): + def setUp(self): + super().setUp() + product_form = Form(self.env["product.product"]) + product_form.name = "KIT Product" + product_form.type = "product" + self.kit_product = product_form.save() + + bom_form = Form(self.env["mrp.bom"]) + bom_form.product_tmpl_id = self.kit_product.product_tmpl_id + bom_form.product_qty = 1.0 + bom_form.type = "phantom" + with bom_form.bom_line_ids.new() as component_form: + component_form.product_id = self.product_1 + component_form.product_qty = 1 + with bom_form.bom_line_ids.new() as component_form: + component_form.product_id = self.product_2 + component_form.product_qty = 1 + self.bom = bom_form.save() + + def test_reserve_01_kit_bom(self): + sale_order_form = Form(self.env["sale.order"]) + sale_order_form.partner_id = self.partner + with sale_order_form.order_line.new() as order_line_form: + order_line_form.product_id = self.kit_product + order_line_form.product_uom_qty = 3 + so = sale_order_form.save() + wiz = Form( + self.env["sale.stock.reserve"].with_context( + active_model="sale.order", active_id=so.id, active_ids=so.ids + ) + ).save() + wiz.button_reserve() + self.assertEquals(self.product_1.virtual_available, 7) + self.assertEquals(self.product_2.virtual_available, 7) + + def test_reserve_02_mixed_products(self): + sale_order_form = Form(self.env["sale.order"]) + sale_order_form.partner_id = self.partner + with sale_order_form.order_line.new() as order_line_form: + order_line_form.product_id = self.kit_product + order_line_form.product_uom_qty = 3 + with sale_order_form.order_line.new() as order_line_form: + order_line_form.product_id = self.product_1 + order_line_form.product_uom_qty = 4 + so = sale_order_form.save() + wiz = Form( + self.env["sale.stock.reserve"].with_context( + active_model="sale.order", active_id=so.id, active_ids=so.ids + ) + ).save() + wiz.button_reserve() + self.assertEquals(self.product_1.virtual_available, 3) + self.assertEquals(self.product_2.virtual_available, 7) diff --git a/stock_reserve_sale_mrp/wizard/__init__.py b/stock_reserve_sale_mrp/wizard/__init__.py new file mode 100644 index 000000000..d7bd1ed96 --- /dev/null +++ b/stock_reserve_sale_mrp/wizard/__init__.py @@ -0,0 +1 @@ +from . import sale_stock_reserve diff --git a/stock_reserve_sale_mrp/wizard/sale_stock_reserve.py b/stock_reserve_sale_mrp/wizard/sale_stock_reserve.py new file mode 100644 index 000000000..9b7443341 --- /dev/null +++ b/stock_reserve_sale_mrp/wizard/sale_stock_reserve.py @@ -0,0 +1,52 @@ +# Copyright 2023 ForgeFlow S.L. +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import models + + +class SaleStockReserve(models.TransientModel): + _inherit = "sale.stock.reserve" + + def _prepare_stock_reservation_bom_line(self, line, bom_line, bom_vals): + self.ensure_one() + return { + "product_id": bom_line.product_id.id, + "product_uom": bom_line.bom_id.product_uom_id.id, + "product_uom_qty": bom_vals["qty"], + "date_validity": self.date_validity, + "name": "{} ({})".format(line.order_id.name, line.name), + "location_id": self.location_id.id, + "location_dest_id": self.location_dest_id.id, + "note": self.note, + "sale_line_id": line.id, + "restrict_partner_id": self.owner_id.id, + } + + def stock_reserve(self, line_ids): + self.ensure_one() + lines = self.env["sale.order.line"].browse(line_ids) + lines_without_kit = self.env["sale.order.line"] + vals_list = [] + for line in lines: + bom_kit = self.env["mrp.bom"]._bom_find( + product=line.product_id, + company_id=line.company_id.id, + bom_type="phantom", + ) + if not bom_kit: + lines_without_kit |= line + else: + qty = line.product_uom._compute_quantity( + line.product_uom_qty, bom_kit.product_uom_id, round=False + ) + qty_to_produce = qty / bom_kit.product_qty + boms, bom_sub_lines = bom_kit.explode(line.product_id, qty_to_produce) + for bom_line, bom_vals in bom_sub_lines: + vals_list.append( + self._prepare_stock_reservation_bom_line( + line, bom_line, bom_vals + ) + ) + if vals_list: + reserves = self.env["stock.reservation"].create(vals_list) + reserves.reserve() + return super().stock_reserve(lines_without_kit.ids)