From 9e368590f80a3c3cfbcc37672b5f58cec9f46376 Mon Sep 17 00:00:00 2001 From: Yoshi Tashiro Date: Sat, 27 Jan 2024 16:07:39 +0000 Subject: [PATCH 1/3] [4207][IMP] stock_owner_restriction, mrp_stock_owner_restriction --- .../models/mrp_production.py | 8 +++++ .../models/mrp_unbuild.py | 2 +- .../models/stock_move.py | 7 +++- purchase_order_owner/models/purchase_order.py | 8 +++++ stock_owner_restriction/models/__init__.py | 1 + stock_owner_restriction/models/stock_move.py | 24 +++++++++++-- .../models/stock_picking.py | 6 ++++ stock_owner_restriction/models/stock_rule.py | 36 +++++++++++++++++++ 8 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 stock_owner_restriction/models/stock_rule.py diff --git a/mrp_stock_owner_restriction/models/mrp_production.py b/mrp_stock_owner_restriction/models/mrp_production.py index 1bcb97ba..5664f814 100644 --- a/mrp_stock_owner_restriction/models/mrp_production.py +++ b/mrp_stock_owner_restriction/models/mrp_production.py @@ -16,3 +16,11 @@ class MrpProduction(models.Model): help="Produced products will be assigned to this owner.", ) owner_restriction = fields.Selection(related="picking_type_id.owner_restriction") + + def write(self, vals): + if "owner_id" in vals: + for production in self: + owner_restriction = self.picking_type_id.owner_restriction + if owner_restriction in ("unassigned_owner", "picking_partner"): + production.move_line_raw_ids.unlink() + return super().write(vals) diff --git a/mrp_stock_owner_restriction/models/mrp_unbuild.py b/mrp_stock_owner_restriction/models/mrp_unbuild.py index 0a94ae78..d896717b 100644 --- a/mrp_stock_owner_restriction/models/mrp_unbuild.py +++ b/mrp_stock_owner_restriction/models/mrp_unbuild.py @@ -21,6 +21,6 @@ class MrpUnbuild(models.Model): def action_validate(self): owner_restriction = self.mo_id.picking_type_id.owner_restriction owner = self.mo_id.owner_id - if owner and owner_restriction != "standard_behavior": + if owner and owner_restriction in ("unassigned_owner", "picking_partner"): self = self.with_context(force_restricted_owner_id=owner) return super().action_validate() diff --git a/mrp_stock_owner_restriction/models/stock_move.py b/mrp_stock_owner_restriction/models/stock_move.py index 46889455..f664b528 100644 --- a/mrp_stock_owner_restriction/models/stock_move.py +++ b/mrp_stock_owner_restriction/models/stock_move.py @@ -1,12 +1,17 @@ # Copyright 2023 Quartile Limited # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import models +from odoo import api, models class StockMove(models.Model): _inherit = "stock.move" + # Just to add a trigger + @api.depends("production_id.owner_id") + def _compute_restrict_partner_id(self): + return super()._compute_restrict_partner_id() + def _get_mo_to_unbuild(self): self.ensure_one() return self.consume_unbuild_id.mo_id or self.unbuild_id.mo_id diff --git a/purchase_order_owner/models/purchase_order.py b/purchase_order_owner/models/purchase_order.py index 637761a6..7dd52e18 100644 --- a/purchase_order_owner/models/purchase_order.py +++ b/purchase_order_owner/models/purchase_order.py @@ -18,6 +18,14 @@ class PurchaseOrder(models.Model): "incoming stock picking.", ) + def button_confirm(self): + # TODO: Double-check if this is necessary + for order in self: + if order.owner_id: + order = order.with_context(owner_id=order.owner_id.id) + super(PurchaseOrder, order).button_confirm() + return True + def _prepare_picking(self): res = super()._prepare_picking() res["owner_id"] = self.owner_id.id diff --git a/stock_owner_restriction/models/__init__.py b/stock_owner_restriction/models/__init__.py index 0246ca70..5c7a2a20 100644 --- a/stock_owner_restriction/models/__init__.py +++ b/stock_owner_restriction/models/__init__.py @@ -4,3 +4,4 @@ from . import stock_quant from . import stock_picking from . import stock_picking_type +from . import stock_rule diff --git a/stock_owner_restriction/models/stock_move.py b/stock_owner_restriction/models/stock_move.py index 463794cf..2e8dad15 100644 --- a/stock_owner_restriction/models/stock_move.py +++ b/stock_owner_restriction/models/stock_move.py @@ -1,15 +1,35 @@ # Copyright 2020 Carlos Dauden - Tecnativa # Copyright 2020 Sergio Teruel - Tecnativa -# Copyright 2023 Quartile Limited +# Copyright 2023-2024 Quartile Limited # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from collections import defaultdict -from odoo import models +from odoo import api, fields, models class StockMove(models.Model): _inherit = "stock.move" + restrict_partner_id = fields.Many2one( + compute="_compute_restrict_partner_id", store=True + ) + + @api.depends("picking_type_id.owner_restriction", "picking_id.owner_id") + def _compute_restrict_partner_id(self): + for move in self: + if move.picking_type_id.owner_restriction == "picking_partner": + move.restrict_partner_id = move._get_owner_for_assign() + else: + move.restrict_partner_id = False + + # pylint: disable=W8110 + @api.depends("restrict_partner_id") + def _compute_forecast_information(self): + for move in self: + if move.picking_type_id.owner_restriction == "picking_partner": + move = move.with_context(owner_id=move.restrict_partner_id.id) + super(StockMove, move)._compute_forecast_information() + def _get_moves_to_assign_with_standard_behavior(self): """This method is expected to be extended as necessary. e.g. you may not want to handle subcontracting receipts (whose picking type is normal incoming receipt diff --git a/stock_owner_restriction/models/stock_picking.py b/stock_owner_restriction/models/stock_picking.py index 28edb636..33285627 100644 --- a/stock_owner_restriction/models/stock_picking.py +++ b/stock_owner_restriction/models/stock_picking.py @@ -8,3 +8,9 @@ class StockPicking(models.Model): _inherit = "stock.picking" owner_restriction = fields.Selection(related="picking_type_id.owner_restriction") + + def write(self, vals): + if "owner_id" in vals: + # TODO: Do this only when owner_id is different to restricted_partner_id of moves + self.move_line_ids.unlink() + return super().write(vals) diff --git a/stock_owner_restriction/models/stock_rule.py b/stock_owner_restriction/models/stock_rule.py new file mode 100644 index 00000000..45f59c8a --- /dev/null +++ b/stock_owner_restriction/models/stock_rule.py @@ -0,0 +1,36 @@ +# Copyright 2024 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class StockRule(models.Model): + _inherit = "stock.rule" + + def _get_stock_move_values( + self, + product_id, + product_qty, + product_uom, + location_dest_id, + name, + origin, + company_id, + values, + ): + """Pass ristrict_partner_id to child moves created via _run_pull().""" + res = super()._get_stock_move_values( + product_id, + product_qty, + product_uom, + location_dest_id, + name, + origin, + company_id, + values, + ) + if self.picking_type_id.owner_restriction == "picking_partner": + move_dest = values.get("move_dest_ids") + if move_dest: + res["restrict_partner_id"] = move_dest.restrict_partner_id.id + return res From d4a9381ba3837780cef8c3fdd01238c2167e399a Mon Sep 17 00:00:00 2001 From: Yoshi Tashiro Date: Wed, 3 Apr 2024 08:31:47 +0000 Subject: [PATCH 2/3] sync with OCA PR 1385 --- stock_owner_restriction/models/stock_quant.py | 20 +++++++--------- .../tests/test_stock_owner_restriction.py | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/stock_owner_restriction/models/stock_quant.py b/stock_owner_restriction/models/stock_quant.py index d35d36ac..8edc5326 100644 --- a/stock_owner_restriction/models/stock_quant.py +++ b/stock_owner_restriction/models/stock_quant.py @@ -41,18 +41,14 @@ def read_group( if not owner_in_domain: restricted_owner = self.env.context.get("force_restricted_owner_id", None) if restricted_owner is not None: - domain = expression.AND( - [ - domain, - [ - ( - "owner_id", - "=", - restricted_owner and restricted_owner.id or False, - ) - ], - ] - ) + owner_domain = [ + ( + "owner_id", + "=", + restricted_owner and restricted_owner.id or False, + ), + ] + domain = expression.AND([domain, owner_domain]) return super(StockQuant, self).read_group( domain, fields, diff --git a/stock_owner_restriction/tests/test_stock_owner_restriction.py b/stock_owner_restriction/tests/test_stock_owner_restriction.py index 5faccad4..9af85dc9 100644 --- a/stock_owner_restriction/tests/test_stock_owner_restriction.py +++ b/stock_owner_restriction/tests/test_stock_owner_restriction.py @@ -130,3 +130,27 @@ def test_search_qty(self): [("id", "=", self.product.id), ("qty_available", ">", 499.00)] ) self.assertTrue(products) + + def test_update_available_quantity(self): + self.env["stock.quant"].with_context( + force_restricted_owner_id=self.owner + )._update_available_quantity( + self.product, self.picking_type_out.default_location_src_id, 100 + ) + self.assertEqual(self.product.qty_available, 500.00) + self.product.invalidate_model() + self.assertEqual( + self.product.with_context(skip_restricted_owner=True).qty_available, 1100.00 + ) + + def test_update_reserved_quantity(self): + self.env["stock.quant"].with_context( + force_restricted_owner_id=self.owner + )._update_reserved_quantity( + self.product, self.picking_type_out.default_location_src_id, 100 + ) + self.assertEqual(self.product.qty_available, 500.00) + self.product.invalidate_model() + self.assertEqual( + self.product.with_context(skip_restricted_owner=True).qty_available, 1000.00 + ) From a66ff59b6322a047563b29a2ba3accbb714b4dd5 Mon Sep 17 00:00:00 2001 From: Yoshi Tashiro Date: Wed, 3 Apr 2024 09:57:57 +0000 Subject: [PATCH 3/3] misc. improvements/adjustments --- .../readme/DESCRIPTION.rst | 2 +- .../tests/test_mrp_stock_owner_restriction.py | 58 +++++++++++-------- purchase_order_owner/models/purchase_order.py | 10 +++- stock_owner_restriction/models/__init__.py | 1 - stock_owner_restriction/models/stock_move.py | 15 +++-- .../models/stock_picking.py | 6 +- stock_owner_restriction/models/stock_rule.py | 36 ------------ 7 files changed, 54 insertions(+), 74 deletions(-) delete mode 100644 stock_owner_restriction/models/stock_rule.py diff --git a/mrp_stock_owner_restriction/readme/DESCRIPTION.rst b/mrp_stock_owner_restriction/readme/DESCRIPTION.rst index 414e84c9..c9c8783f 100644 --- a/mrp_stock_owner_restriction/readme/DESCRIPTION.rst +++ b/mrp_stock_owner_restriction/readme/DESCRIPTION.rst @@ -1,2 +1,2 @@ -This module adopts the functionality of stock_owner_restriction in the manufacturing +This module adapts the functionality of stock_owner_restriction in the manufacturing order. diff --git a/mrp_stock_owner_restriction/tests/test_mrp_stock_owner_restriction.py b/mrp_stock_owner_restriction/tests/test_mrp_stock_owner_restriction.py index 002958c6..03e79697 100644 --- a/mrp_stock_owner_restriction/tests/test_mrp_stock_owner_restriction.py +++ b/mrp_stock_owner_restriction/tests/test_mrp_stock_owner_restriction.py @@ -10,39 +10,37 @@ def setUp(self): self.company = self.env.ref("base.main_company") self.manufacture_route = self.env.ref("mrp.route_warehouse0_manufacture") - self.product = self.env["product.product"].create( - { - "name": "test product", - "type": "product", - } + self.finished_product = self.env["product.product"].create( + {"name": "test product", "type": "product"} ) - self.product_2 = self.env["product.product"].create( - {"name": "test component", "default_code": "PRD 2", "type": "product"} + self.component = self.env["product.product"].create( + {"name": "test component", "type": "product"} ) self.bom = self.env["mrp.bom"].create( { - "product_id": self.product.id, - "product_tmpl_id": self.product.product_tmpl_id.id, - "product_uom_id": self.product.uom_id.id, + "product_id": self.finished_product.id, + "product_tmpl_id": self.finished_product.product_tmpl_id.id, + "product_uom_id": self.finished_product.uom_id.id, "product_qty": 1.0, "type": "normal", } ) self.env["mrp.bom.line"].create( - {"bom_id": self.bom.id, "product_id": self.product_2.id, "product_qty": 2} + {"bom_id": self.bom.id, "product_id": self.component.id, "product_qty": 1} ) self.owner = self.env["res.partner"].create({"name": "Owner test"}) - self.product.route_ids = [(6, 0, self.manufacture_route.ids)] - self.picking_type_id = self.env["stock.picking.type"].search( + self.finished_product.route_ids = [(6, 0, self.manufacture_route.ids)] + self.picking_type = self.env["stock.picking.type"].search( [ ("code", "=", "mrp_operation"), ("warehouse_id.company_id", "=", self.company.id), ], limit=1, ) + self.picking_type.write({"owner_restriction": "picking_partner"}) quant_vals = { - "product_id": self.product_2.id, - "location_id": self.picking_type_id.default_location_src_id.id, + "product_id": self.component.id, + "location_id": self.picking_type.default_location_src_id.id, "quantity": 250.00, } # Create quants without owner @@ -51,12 +49,17 @@ def setUp(self): self.env["stock.quant"].create(dict(quant_vals, owner_id=self.owner.id)) def test_mrp_quant_assign_owner(self): + self.assertEqual(self.component.qty_available, 250) + self.component.invalidate_model(["qty_available"]) + self.assertEqual( + self.component.with_context(skip_restricted_owner=True).qty_available, 500 + ) mo = self.env["mrp.production"].create( { - "product_id": self.product.id, + "product_id": self.finished_product.id, "bom_id": self.bom.id, "product_qty": 250, - "picking_type_id": self.picking_type_id.id, + "picking_type_id": self.picking_type.id, "owner_id": self.owner.id, } ) @@ -69,17 +72,22 @@ def test_mrp_quant_assign_owner(self): action = wizard.process() # Check produced product owner and qty_available - self.assertEqual(self.product.qty_available, 0.00) - self.product.invalidate_model() + self.assertEqual(self.finished_product.qty_available, 0.00) + self.finished_product.invalidate_model(["qty_available"]) self.assertEqual( - self.product.with_context(skip_restricted_owner=True).qty_available, 250.00 + self.finished_product.with_context( + skip_restricted_owner=True + ).qty_available, + 250.00, + ) + quant = self.env["stock.quant"].search( + [("product_id", "=", self.finished_product.id)] ) - quant = self.env["stock.quant"].search([("product_id", "=", self.product.id)]) self.assertEqual(quant.owner_id, self.owner) - # Check component product qty_available - self.assertEqual(self.product_2.qty_available, 0.00) - self.product.invalidate_model() + # Confirm that component inventory with owner has been consumed + self.assertEqual(self.component.qty_available, 250) + self.component.invalidate_model(["qty_available"]) self.assertEqual( - self.product_2.with_context(skip_restricted_owner=True).qty_available, 0.00 + self.component.with_context(skip_restricted_owner=True).qty_available, 250 ) diff --git a/purchase_order_owner/models/purchase_order.py b/purchase_order_owner/models/purchase_order.py index 7dd52e18..ff464723 100644 --- a/purchase_order_owner/models/purchase_order.py +++ b/purchase_order_owner/models/purchase_order.py @@ -19,10 +19,14 @@ class PurchaseOrder(models.Model): ) def button_confirm(self): - # TODO: Double-check if this is necessary + """For subcontracting orders + + Without this, the downstream component demands may misjudge the available + quantities with owner consideration. + """ + # TODO: Double-check if the logic is optimal for order in self: - if order.owner_id: - order = order.with_context(owner_id=order.owner_id.id) + order = order.with_context(owner_id=order.owner_id.id) super(PurchaseOrder, order).button_confirm() return True diff --git a/stock_owner_restriction/models/__init__.py b/stock_owner_restriction/models/__init__.py index 5c7a2a20..0246ca70 100644 --- a/stock_owner_restriction/models/__init__.py +++ b/stock_owner_restriction/models/__init__.py @@ -4,4 +4,3 @@ from . import stock_quant from . import stock_picking from . import stock_picking_type -from . import stock_rule diff --git a/stock_owner_restriction/models/stock_move.py b/stock_owner_restriction/models/stock_move.py index 2e8dad15..2f3430dc 100644 --- a/stock_owner_restriction/models/stock_move.py +++ b/stock_owner_restriction/models/stock_move.py @@ -11,10 +11,12 @@ class StockMove(models.Model): _inherit = "stock.move" restrict_partner_id = fields.Many2one( - compute="_compute_restrict_partner_id", store=True + compute="_compute_restrict_partner_id", + store=True, + readonly=False, ) - @api.depends("picking_type_id.owner_restriction", "picking_id.owner_id") + @api.depends("picking_type_id", "picking_id.owner_id", "move_dest_ids") def _compute_restrict_partner_id(self): for move in self: if move.picking_type_id.owner_restriction == "picking_partner": @@ -52,10 +54,11 @@ def _get_owner_for_assign(self): needs to be applied to moves in manufacturing orders. """ self.ensure_one() - partner = self.move_dest_ids.picking_id.owner_id - if not partner: - partner = self.picking_id.owner_id or self.picking_id.partner_id - return partner + return ( + self.move_dest_ids.restrict_partner_id + or self.picking_id.owner_id + or self.picking_id.partner_id + ) def _action_assign(self, force_qty=False): # Split moves by picking type owner behavior restriction to process diff --git a/stock_owner_restriction/models/stock_picking.py b/stock_owner_restriction/models/stock_picking.py index 33285627..3ad5f7a7 100644 --- a/stock_owner_restriction/models/stock_picking.py +++ b/stock_owner_restriction/models/stock_picking.py @@ -11,6 +11,8 @@ class StockPicking(models.Model): def write(self, vals): if "owner_id" in vals: - # TODO: Do this only when owner_id is different to restricted_partner_id of moves - self.move_line_ids.unlink() + for pick in self: + owner_restriction = pick.picking_type_id.owner_restriction + if owner_restriction in ("unassigned_owner", "picking_partner"): + pick.move_line_ids.unlink() return super().write(vals) diff --git a/stock_owner_restriction/models/stock_rule.py b/stock_owner_restriction/models/stock_rule.py deleted file mode 100644 index 45f59c8a..00000000 --- a/stock_owner_restriction/models/stock_rule.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2024 Quartile Limited -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import models - - -class StockRule(models.Model): - _inherit = "stock.rule" - - def _get_stock_move_values( - self, - product_id, - product_qty, - product_uom, - location_dest_id, - name, - origin, - company_id, - values, - ): - """Pass ristrict_partner_id to child moves created via _run_pull().""" - res = super()._get_stock_move_values( - product_id, - product_qty, - product_uom, - location_dest_id, - name, - origin, - company_id, - values, - ) - if self.picking_type_id.owner_restriction == "picking_partner": - move_dest = values.get("move_dest_ids") - if move_dest: - res["restrict_partner_id"] = move_dest.restrict_partner_id.id - return res