[MIG] improved_mo_origin to 18.0
This commit is contained in:
parent
1a69864176
commit
d428d673bc
11 changed files with 426 additions and 0 deletions
1
improved_mo_origin/__init__.py
Normal file
1
improved_mo_origin/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
38
improved_mo_origin/__manifest__.py
Normal file
38
improved_mo_origin/__manifest__.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the GNU Lesser General Public License,
|
||||
# version 3.
|
||||
#
|
||||
# For full license details, see https://www.gnu.org/licenses/lgpl-3.0.en.html.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
"name": "Improved Manufacturing Origin",
|
||||
"version": "18.0.1.0.0",
|
||||
"summary": "Adds better origin information for multi-tiered manufacturing.",
|
||||
"category": "Manufacturing/Manufacturing",
|
||||
"author": "Bemade Inc.",
|
||||
"website": "http://www.bemade.org",
|
||||
"license": "LGPL-3",
|
||||
"depends": ["sale_mrp"],
|
||||
"data": ["views/mrp_production_views.xml"],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"improved_mo_origin/static/src/**/*.xml",
|
||||
"improved_mo_origin/static/src/**/*.js",
|
||||
]
|
||||
},
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
}
|
||||
1
improved_mo_origin/models/__init__.py
Normal file
1
improved_mo_origin/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import mrp_production
|
||||
131
improved_mo_origin/models/mrp_production.py
Normal file
131
improved_mo_origin/models/mrp_production.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_inherit = "mrp.production"
|
||||
|
||||
customer_ids = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
compute="_compute_customers",
|
||||
string="Customers",
|
||||
store=True,
|
||||
compute_sudo=True,
|
||||
)
|
||||
source_sale_orders = fields.Char(
|
||||
compute="_compute_source_sale_orders",
|
||||
string="Sale Order(s)",
|
||||
store=True,
|
||||
compute_sudo=True,
|
||||
)
|
||||
source_sale_ids = fields.Many2many(
|
||||
comodel_name="sale.order",
|
||||
compute="_compute_source_sale_ids",
|
||||
compute_sudo=True,
|
||||
)
|
||||
|
||||
def _get_related_sales(self):
|
||||
self.ensure_one()
|
||||
# Try multiple paths to find related sale orders
|
||||
sales = self.env['sale.order']
|
||||
|
||||
# Path 1: Through procurement group
|
||||
if self.procurement_group_id:
|
||||
sales |= (
|
||||
self.procurement_group_id
|
||||
.mapped("mrp_production_ids")
|
||||
.mapped("move_dest_ids")
|
||||
.mapped("group_id")
|
||||
.mapped("sale_id")
|
||||
)
|
||||
|
||||
# Path 2: Through move_dest_ids directly
|
||||
sales |= (
|
||||
self.mapped("move_dest_ids")
|
||||
.mapped("group_id")
|
||||
.mapped("sale_id")
|
||||
)
|
||||
|
||||
# Path 3: Through move_finished_ids -> move_dest_ids
|
||||
sales |= (
|
||||
self.mapped("move_finished_ids")
|
||||
.mapped("move_dest_ids")
|
||||
.mapped("group_id")
|
||||
.mapped("sale_id")
|
||||
)
|
||||
|
||||
return sales
|
||||
|
||||
def _get_sources(self):
|
||||
"""Get source MOs that might have sale order information"""
|
||||
self.ensure_one()
|
||||
# Look for parent MOs through stock moves
|
||||
parent_mos = self.env['mrp.production']
|
||||
|
||||
# Check raw material moves for parent MOs
|
||||
for move in self.move_raw_ids:
|
||||
if move.move_orig_ids:
|
||||
parent_mos |= move.move_orig_ids.mapped('production_id')
|
||||
|
||||
# Also check if this MO is a source for others (for merged MOs)
|
||||
# Find MOs that have this MO as a source through move_orig_ids
|
||||
source_mos = self.env['mrp.production'].search([
|
||||
('move_raw_ids.move_orig_ids.production_id', '=', self.id)
|
||||
])
|
||||
|
||||
return parent_mos | source_mos
|
||||
|
||||
@api.depends(
|
||||
"procurement_group_id",
|
||||
"procurement_group_id.mrp_production_ids",
|
||||
"procurement_group_id.mrp_production_ids.move_dest_ids",
|
||||
"procurement_group_id.stock_move_ids.move_dest_ids",
|
||||
"move_raw_ids.move_orig_ids",
|
||||
"move_raw_ids.move_orig_ids.production_id",
|
||||
)
|
||||
def _compute_customers(self):
|
||||
for rec in self:
|
||||
customers = rec._get_related_sales().mapped("partner_id")
|
||||
|
||||
# If no direct customers, check source MOs (without recursion)
|
||||
if not customers:
|
||||
sources = rec._get_sources()
|
||||
if sources:
|
||||
# Get customers from source MOs directly
|
||||
for source in sources:
|
||||
source_sales = source._get_related_sales()
|
||||
customers |= source_sales.mapped("partner_id")
|
||||
|
||||
rec.customer_ids = customers
|
||||
|
||||
@api.depends(
|
||||
"procurement_group_id",
|
||||
"procurement_group_id.mrp_production_ids",
|
||||
"procurement_group_id.mrp_production_ids.move_dest_ids",
|
||||
"procurement_group_id.stock_move_ids.move_dest_ids",
|
||||
"move_raw_ids.move_orig_ids",
|
||||
"move_raw_ids.move_orig_ids.production_id",
|
||||
)
|
||||
def _compute_source_sale_ids(self):
|
||||
for rec in self:
|
||||
source_sale_ids = rec._get_related_sales()
|
||||
|
||||
# If no direct sales, check source MOs (without recursion)
|
||||
if not source_sale_ids:
|
||||
sources = rec._get_sources()
|
||||
if sources:
|
||||
# Get sale orders from source MOs directly
|
||||
for source in sources:
|
||||
source_sales = source._get_related_sales()
|
||||
source_sale_ids |= source_sales
|
||||
|
||||
rec.source_sale_ids = source_sale_ids
|
||||
|
||||
@api.depends("source_sale_ids")
|
||||
def _compute_source_sale_orders(self):
|
||||
for rec in self:
|
||||
rec.source_sale_orders = (
|
||||
", ".join(rec.source_sale_ids.mapped("name"))
|
||||
if rec.source_sale_ids
|
||||
else ""
|
||||
)
|
||||
|
||||
25
improved_mo_origin/static/src/mrp_display/mrp_display.esm.js
Normal file
25
improved_mo_origin/static/src/mrp_display/mrp_display.esm.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {MrpDisplay} from "@mrp_workorder/mrp_display/mrp_display";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
|
||||
patch(MrpDisplay.prototype, {
|
||||
_makeModelParams() {
|
||||
var params = super._makeModelParams();
|
||||
const customerFields = this.props.models.find(
|
||||
(m) => m.resModel === "res.partner"
|
||||
).fields;
|
||||
params.config.activeFields.customer_ids.related = {
|
||||
fields: customerFields,
|
||||
activeFields: customerFields,
|
||||
};
|
||||
const saleOrderFields = this.props.models.find(
|
||||
(m) => m.resModel === "sale.order"
|
||||
).fields;
|
||||
params.config.activeFields.source_sale_ids.related = {
|
||||
fields: saleOrderFields,
|
||||
activeFields: saleOrderFields,
|
||||
};
|
||||
return params;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {MrpDisplayAction} from "@mrp_workorder/mrp_display/mrp_display_action";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
|
||||
patch(MrpDisplayAction.prototype, {
|
||||
get fieldsStructure() {
|
||||
const vals = super.fieldsStructure;
|
||||
vals["mrp.production"].push("customer_ids", "source_sale_ids");
|
||||
vals["res.partner"] = ["id", "display_name"];
|
||||
vals["sale.order"] = ["id", "display_name"];
|
||||
return vals;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {Many2ManyTagsField} from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
import {MrpDisplayRecord} from "@mrp_workorder/mrp_display/mrp_display_record";
|
||||
|
||||
MrpDisplayRecord.components = {
|
||||
...MrpDisplayRecord.components,
|
||||
Many2ManyTagsField,
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserver">
|
||||
<t t-inherit="mrp_workorder.MrpDisplayRecord" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('card-header')]" position="inside">
|
||||
<t t-set="customers" t-value="props.production.data.customer_ids" />
|
||||
<t t-if="customers && customers.records">
|
||||
<Many2ManyTagsField
|
||||
name="'customer_ids'"
|
||||
record="props.production"
|
||||
canCreate="false"
|
||||
canCreateEdit="false"
|
||||
canQuickCreate="false"
|
||||
readonly="true"
|
||||
/>
|
||||
</t>
|
||||
<t t-set="sale_orders" t-value="props.production.data.source_sale_ids" />
|
||||
<t t-if="sale_orders && sale_orders.records">
|
||||
<Many2ManyTagsField
|
||||
name="'source_sale_ids'"
|
||||
record="props.production"
|
||||
canCreate="false"
|
||||
canCreateEdit="false"
|
||||
canQuickCreate="false"
|
||||
readonly="true"
|
||||
/>
|
||||
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
1
improved_mo_origin/tests/__init__.py
Normal file
1
improved_mo_origin/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_mrp_production
|
||||
151
improved_mo_origin/tests/test_mrp_production.py
Normal file
151
improved_mo_origin/tests/test_mrp_production.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
from odoo.addons.sale_mrp.tests.test_sale_mrp_flow import TestSaleMrpFlowCommon
|
||||
from odoo.tests import Form, tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestMrpProduction(TestSaleMrpFlowCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
def _setup_products(self):
|
||||
route_manufacture = self.company_data[
|
||||
"default_warehouse"
|
||||
].manufacture_pull_id.route_id
|
||||
route_mto = self.company_data["default_warehouse"].mto_pull_id.route_id
|
||||
product_a = self._cls_create_product(
|
||||
"Product A",
|
||||
self.uom_unit,
|
||||
routes=[route_manufacture, route_mto],
|
||||
)
|
||||
product_c = self._cls_create_product(
|
||||
"Product C",
|
||||
self.uom_kg,
|
||||
)
|
||||
product_b = self._cls_create_product(
|
||||
"Product B",
|
||||
self.uom_dozen,
|
||||
routes=[route_manufacture, route_mto],
|
||||
)
|
||||
product_d = self._cls_create_product(
|
||||
"Product D",
|
||||
self.uom_unit,
|
||||
routes=[route_manufacture, route_mto],
|
||||
)
|
||||
# Bill of materials for Product A.
|
||||
with Form(self.env["mrp.bom"]) as form:
|
||||
form.product_tmpl_id = product_a.product_tmpl_id
|
||||
form.product_id = product_a
|
||||
form.product_qty = 2
|
||||
form.product_uom_id = self.uom_dozen
|
||||
form.save()
|
||||
with form.bom_line_ids.new() as line:
|
||||
line.product_id = product_b
|
||||
line.product_qty = 3
|
||||
line.product_uom_id = self.uom_unit
|
||||
with form.bom_line_ids.new() as line:
|
||||
line.product_id = product_c
|
||||
line.product_qty = 300.0
|
||||
line.product_uom_id = self.uom_gm
|
||||
with form.bom_line_ids.new() as line:
|
||||
line.product_id = product_d
|
||||
line.product_qty = 4
|
||||
line.product_uom_id = self.uom_unit
|
||||
# Bill of materials for Product B.
|
||||
with Form(self.env["mrp.bom"]) as form:
|
||||
form.product_tmpl_id = product_b.product_tmpl_id
|
||||
form.product_id = product_b
|
||||
form.product_qty = 1
|
||||
form.product_uom_id = self.uom_dozen
|
||||
form.save()
|
||||
with form.bom_line_ids.new() as line:
|
||||
line.product_id = product_d
|
||||
line.product_qty = 2
|
||||
line.product_uom_id = self.uom_unit
|
||||
return product_a
|
||||
|
||||
def _create_order(self, partner_name: str, product):
|
||||
order_form = Form(self.env["sale.order"])
|
||||
order_form.partner_id = self.env["res.partner"].create({"name": partner_name})
|
||||
with order_form.order_line.new() as line:
|
||||
line.product_id = product
|
||||
line.product_uom = self.uom_dozen
|
||||
line.product_uom_qty = 10
|
||||
return order_form.save()
|
||||
|
||||
def _setup_test_mo_single_client(self):
|
||||
product_a = self._setup_products()
|
||||
|
||||
order = self._create_order("Test Partner", product_a)
|
||||
order.action_confirm()
|
||||
# A sales order creating an MO should have its partner listed in the MO's customer_ids field
|
||||
|
||||
self.assertNotEqual(order.mrp_production_count, 0)
|
||||
base_mos = order.mrp_production_ids | order.mrp_production_ids
|
||||
child_mos = self.env["mrp.production"]
|
||||
for mo in base_mos:
|
||||
child_mos |= mo._get_children()
|
||||
return order, base_mos | child_mos
|
||||
|
||||
def test_mo_single_client_correctly_listed(self):
|
||||
order, all_mos = self._setup_test_mo_single_client()
|
||||
for mo in all_mos:
|
||||
self.assertIn(order.partner_id, mo.customer_ids)
|
||||
|
||||
def test_mo_single_client_so_correctly_set(self):
|
||||
order, all_mos = self._setup_test_mo_single_client()
|
||||
for mo in all_mos:
|
||||
self.assertEqual(order.name, mo.source_sale_orders)
|
||||
|
||||
def _setup_test_mo_multiple_so(self):
|
||||
product_a = self._setup_products()
|
||||
order1 = self._create_order("Test Partner 1", product_a)
|
||||
order2 = self._create_order("Test Partner 2", product_a)
|
||||
order1.action_confirm()
|
||||
order2.action_confirm()
|
||||
|
||||
# Run scheduler to create manufacturing orders with proper BOMs
|
||||
self.env["procurement.group"].run_scheduler()
|
||||
|
||||
# Get top-level MOs for Product A only (not child MOs for components)
|
||||
top_level_mos_order1 = order1.mrp_production_ids.filtered(
|
||||
lambda mo: mo.product_id == product_a
|
||||
)
|
||||
top_level_mos_order2 = order2.mrp_production_ids.filtered(
|
||||
lambda mo: mo.product_id == product_a
|
||||
)
|
||||
|
||||
# Merge only the top-level MOs for the same product
|
||||
(top_level_mos_order1 | top_level_mos_order2).action_merge()
|
||||
self.env.invalidate_all(flush=True)
|
||||
|
||||
# After merge, find the merged MO (original MOs are deleted, new one created)
|
||||
merged_mos = self.env['mrp.production'].search([
|
||||
('product_id', '=', product_a.id),
|
||||
('state', 'in', ['draft', 'confirmed', 'progress', 'to_close']),
|
||||
])
|
||||
|
||||
self.assertTrue(merged_mos, "Should find the merged MO")
|
||||
self.assertEqual(len(merged_mos), 1, "Should have exactly one merged MO")
|
||||
merged_mo = merged_mos[0]
|
||||
|
||||
# Collect all MOs (merged top-level + all children)
|
||||
all_mos = merged_mo
|
||||
for mo in merged_mo:
|
||||
all_mos |= mo._get_children()
|
||||
|
||||
return order1, order2, all_mos
|
||||
|
||||
def test_mo_multiple_so_clients_correctly_listed(self):
|
||||
order1, order2, all_mos = self._setup_test_mo_multiple_so()
|
||||
# MOs are merged so we should get both partners on each MO
|
||||
for mo in all_mos:
|
||||
self.assertIn(order1.partner_id, mo.customer_ids)
|
||||
self.assertIn(order2.partner_id, mo.customer_ids)
|
||||
|
||||
def test_mo_multiple_so_sales_correctly_set(self):
|
||||
order1, order2, all_mos = self._setup_test_mo_multiple_so()
|
||||
# MOs are merged so both sale_ids should be set on all MOs
|
||||
for mo in all_mos:
|
||||
self.assertIn(order1.name, mo.source_sale_orders)
|
||||
self.assertIn(order2.name, mo.source_sale_orders)
|
||||
25
improved_mo_origin/views/mrp_production_views.xml
Normal file
25
improved_mo_origin/views/mrp_production_views.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="mrp_production_view_form_customer_name" model="ir.ui.view">
|
||||
<field name="name">mrp.production.view.form.customer_name</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_production_form_view" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='group_extra_info']//field[@name='user_id']">
|
||||
<field name="customer_ids" widget="many2many_tags" />
|
||||
<field name="source_sale_orders" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mrp_production_view_tree_customer_name" model="ir.ui.view">
|
||||
<field name="name">mrp.production.view.form.customer_name</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_production_tree_view" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="after">
|
||||
<field name="customer_ids" optional="show" widget="many2many_tags" />
|
||||
<field name="source_sale_orders" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in a new issue