Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mrp_sale_info: Materialize link between sale order lines and created MOs #1353

Open
wants to merge 3 commits into
base: 16.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions mrp_sale_info/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

from . import mrp_production
from . import mrp_workorder
from . import sale_order_line
from . import stock_move
from . import stock_rule
65 changes: 65 additions & 0 deletions mrp_sale_info/models/sale_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Copyright 2024 Camptocamp SA
# Copyright 2024 Simone Rubino - Aion Tech
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
import operator
from functools import reduce

from odoo import api, fields, models


class SaleOrderLine(models.Model):
_inherit = "sale.order.line"

created_production_ids = fields.Many2many(
"mrp.production",
compute="_compute_created_production_ids",
store=True,
)

@api.depends(lambda sol: sol._created_prod_move_deps())
def _compute_created_production_ids(self):
move_to_production_ids = dict()
for move in self.move_ids:
productions = move._get_orig_created_production_ids()
productions |= productions.procurement_group_id.mrp_production_ids
move_to_production_ids[move] = productions

for line in self:
delivery_moves = line.move_ids
productions = reduce(
operator.or_,
[
move_to_production_ids[delivery_move]
for delivery_move in delivery_moves
],
self.env["mrp.production"].browse(),
)
line.created_production_ids = productions

def _created_prod_move_deps(self):
fnames = [
"move_ids",
"move_ids.created_production_id",
"move_ids.created_production_id.procurement_group_id.mrp_production_ids",
]
# Allow customizing how many levels of recursive moves must be considered to
# compute the created_production_ids field on sale.order.line.
# This value must be at least equal to the number of:
# delivery steps + post manufacturing operations of the warehouse - 1.
# FIXME: Check if there's a way to recompute the dependency without having to
# restart Odoo.
try:
levels = int(
self.env["ir.config_parameter"]
.sudo()
.get_param("mrp_sale_info.compute.created_prod_ids_move_dep_levels", 3)
)
except ValueError:
levels = 3

Check warning on line 58 in mrp_sale_info/models/sale_order_line.py

View check run for this annotation

Codecov / codecov/patch

mrp_sale_info/models/sale_order_line.py#L57-L58

Added lines #L57 - L58 were not covered by tests
for num in range(1, levels + 1):
fnames.append("move_ids.%screated_production_id" % ("move_orig_ids." * num))
fnames.append(
"move_ids.%screated_production_id.procurement_group_id.mrp_production_ids"
% ("move_orig_ids." * num)
)
return fnames
25 changes: 25 additions & 0 deletions mrp_sale_info/models/stock_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import models


class StockMove(models.Model):
_inherit = "stock.move"

def _get_orig_created_production_ids(self, already_scanned_ids=None):
self.ensure_one()
if self.created_production_id:
return self.created_production_id
if not already_scanned_ids:
# 'already_scanned_ids' is used to avoid infinite loop errors,
# we only call this method recursively for moves that haven't been handled yet.
already_scanned_ids = []
mrp_production_ids = set()
for move in self.move_orig_ids:
if move.id in already_scanned_ids:
return self.env["mrp.production"]

Check warning on line 20 in mrp_sale_info/models/stock_move.py

View check run for this annotation

Codecov / codecov/patch

mrp_sale_info/models/stock_move.py#L20

Added line #L20 was not covered by tests
already_scanned_ids.append(move.id)
mrp_production_ids.update(
move._get_orig_created_production_ids(already_scanned_ids).ids
)
return self.env["mrp.production"].browse(mrp_production_ids)
87 changes: 78 additions & 9 deletions mrp_sale_info/tests/test_mrp_sale_info.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# Copyright 2020 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.tests import common
from odoo.tests import RecordCapturer, common


class TestMrpSaleInfo(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
warehouse = cls.env.ref("stock.warehouse0")
warehouse.write(
{"delivery_steps": "pick_pack_ship", "manufacture_steps": "pbm_sam"}
)
route_manufacture_1 = cls.env.ref("mrp.route_warehouse0_manufacture")
route_manufacture_2 = cls.env.ref("stock.route_warehouse0_mto")
route_manufacture_2.active = True
Expand All @@ -20,6 +24,26 @@ def setUpClass(cls):
],
}
)
cls.product_kit = cls.env["product.product"].create(
{
"name": "Test kit",
"type": "product",
"route_ids": [
(4, route_manufacture_1.id),
(4, route_manufacture_2.id),
],
}
)
cls.product_kit_comp = cls.env["product.product"].create(
{
"name": "Test kit comp",
"type": "product",
"route_ids": [
(4, route_manufacture_1.id),
(4, route_manufacture_2.id),
],
}
)
cls.bom = cls.env["mrp.bom"].create(
{
"product_tmpl_id": cls.product.product_tmpl_id.id,
Expand All @@ -35,6 +59,28 @@ def setUpClass(cls):
],
}
)
cls.bom_kit = cls.env["mrp.bom"].create(
{
"product_tmpl_id": cls.product_kit.product_tmpl_id.id,
"type": "phantom",
"bom_line_ids": [
(
0,
0,
{
"product_id": cls.product.id,
},
),
(
0,
0,
{
"product_id": cls.product_kit_comp.id,
},
),
],
}
)
cls.partner = cls.env["res.partner"].create({"name": "Test client"})
cls.sale_order = cls.env["sale.order"].create(
{
Expand All @@ -55,19 +101,42 @@ def setUpClass(cls):
)

def test_mrp_sale_info(self):
prev_productions = self.env["mrp.production"].search([])
self.sale_order.action_confirm()
production = self.env["mrp.production"].search([]) - prev_productions
with RecordCapturer(self.env["mrp.production"], []) as capture:
self.sale_order.action_confirm()
production = capture.records
self.assertEqual(production.sale_id, self.sale_order)
self.assertEqual(production.partner_id, self.partner)
self.assertEqual(production.client_order_ref, self.sale_order.client_order_ref)
self.assertEqual(self.sale_order.order_line.created_production_ids, production)

def test_mrp_workorder(self):
prev_workorders = self.env["mrp.workorder"].search([])
self.sale_order.action_confirm()
workorder = (
self.env["mrp.production"].search([]).workorder_ids - prev_workorders
)
with RecordCapturer(self.env["mrp.workorder"], []) as capture:
self.sale_order.action_confirm()
workorder = capture.records
self.assertEqual(workorder.sale_id, self.sale_order)
self.assertEqual(workorder.partner_id, self.partner)
self.assertEqual(workorder.client_order_ref, self.sale_order.client_order_ref)

def test_kit(self):
self.sale_order.write(
{
"order_line": [
(
0,
0,
{
"product_id": self.product_kit.id,
"product_uom_qty": 2,
"price_unit": 1,
},
)
]
}
)
with RecordCapturer(self.env["mrp.production"], []) as capture:
self.sale_order.action_confirm()
productions = capture.records
for order_line in self.sale_order.order_line:
for created_prod in order_line.created_production_ids:
self.assertIn(created_prod, productions)
self.assertEqual(created_prod.product_qty, order_line.product_uom_qty)
Loading