Initial commit: Odoo 18.0-20251222 extra-addons
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled

This commit is contained in:
tocmo0nlord
2026-03-13 20:43:25 +00:00
parent 36e847a7df
commit adbe430761
9472 changed files with 1265727 additions and 0 deletions

125
sale_mrp_bom/README.rst Executable file
View File

@@ -0,0 +1,125 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
============
Sale MRP BOM
============
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:63cc33af1668c7019325079317e92367d731d9ccd088c6df0049d6c5c100f4fc
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/license-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%2Fsale--workflow-lightgray.png?logo=github
:target: https://github.com/OCA/sale-workflow/tree/18.0/sale_mrp_bom
:alt: OCA/sale-workflow
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-sale_mrp_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/sale-workflow&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This modules allows to specify a Bill Of Materials directly inside a
sale order line. It is specially useful to select alternative
manufacturing and sub-contracting routings.
**Table of contents**
.. contents::
:local:
Installation
============
Configuration
=============
To be able to select a specific Bill of Materials in a sale order, the
user needs the special permission: "Allows to define a BOM on sale order
lines".
Usage
=====
When adding a new sale order line, you can eventually select a specific
Bill Of Materials.
|image1|
When confirming the sale order, if the routing is manufacturing then the
production order will be using the specified Bill Of Materials.
|image2|
.. |image1| image:: https://raw.githubusercontent.com/OCA/sale-workflow/18.0/sale_mrp_bom/static/description/sale_order_1.png
.. |image2| image:: https://raw.githubusercontent.com/OCA/sale-workflow/18.0/sale_mrp_bom/static/description/manufacturing_order_1.png
Known issues / Roadmap
======================
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/sale-workflow/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 <https://github.com/OCA/sale-workflow/issues/new?body=module:%20sale_mrp_bom%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Akretion
Contributors
------------
- Renato Lima <renato.lima@akretion.com.br>
Trobz:
- Hai Lang <hailn@trobz.com>
Other credits
-------------
The migration of this module from 12.0 to 14.0 was financially supported
by Camptocamp.
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/sale-workflow <https://github.com/OCA/sale-workflow/tree/18.0/sale_mrp_bom>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

3
sale_mrp_bom/__init__.py Executable file
View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import models

18
sale_mrp_bom/__manifest__.py Executable file
View File

@@ -0,0 +1,18 @@
# Copyright 2020 Akretion Renato Lima <renato.lima@akretion.com.br>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Sale MRP BOM",
"category": "Sale",
"license": "AGPL-3",
"author": "Akretion, Odoo Community Association (OCA)",
"version": "18.0.1.0.0",
"website": "https://github.com/OCA/sale-workflow",
"summary": "Allows define a BOM in the sales lines.",
"depends": ["mrp", "sale_stock"],
"data": [
"security/security.xml",
"views/sale_order.xml",
"views/sale_order_line.xml",
],
"installable": True,
}

46
sale_mrp_bom/i18n/es.po Executable file
View File

@@ -0,0 +1,46 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_mrp_bom
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2023-10-15 19:36+0000\n"
"Last-Translator: Ivorra78 <informatica@totmaterial.es>\n"
"Language-Team: none\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: sale_mrp_bom
#: model:res.groups,name:sale_mrp_bom.sale_mrp_bom_group
msgid "Allows to define a BOM on sale order lines"
msgstr "Permite definir una lista de materiales en líneas de órdenes de venta"
#. module: sale_mrp_bom
#: model:ir.model.fields,field_description:sale_mrp_bom.field_sale_order_line__bom_id
msgid "BoM"
msgstr "BoM"
#. module: sale_mrp_bom
#. odoo-python
#: code:addons/sale_mrp_bom/models/sale_order_line.py:0
#, python-format
msgid "Please select BoM that has matched product with the line `{}`"
msgstr ""
"Seleccione la lista de materiales que coincida con el producto de la línea "
"`{}`"
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línea de Orden de Venta"
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_stock_move
msgid "Stock Move"
msgstr "Movimiento de Existencias"

45
sale_mrp_bom/i18n/hr.po Executable file
View File

@@ -0,0 +1,45 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_mrp_bom
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-06-21 19:34+0000\n"
"Last-Translator: Bole <bole@dajmi5.com>\n"
"Language-Team: none\n"
"Language: hr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.17\n"
#. module: sale_mrp_bom
#: model:res.groups,name:sale_mrp_bom.sale_mrp_bom_group
msgid "Allows to define a BOM on sale order lines"
msgstr "Dozvoli definiranje sastavnice na stavkama prodajnog naloga"
#. module: sale_mrp_bom
#: model:ir.model.fields,field_description:sale_mrp_bom.field_sale_order_line__bom_id
msgid "BoM"
msgstr "Sastavnica"
#. module: sale_mrp_bom
#. odoo-python
#: code:addons/sale_mrp_bom/models/sale_order_line.py:0
#, python-format
msgid "Please select BoM that has matched product with the line `{}`"
msgstr "Molimo odaberite sastavnicu koja odgovara proizvodu u stavci `{}`"
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_sale_order_line
msgid "Sales Order Line"
msgstr "Stavka prodajnog naloga"
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_stock_move
msgid "Stock Move"
msgstr "Skladišno kretanje"

44
sale_mrp_bom/i18n/it.po Executable file
View File

@@ -0,0 +1,44 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_mrp_bom
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2023-12-07 18:33+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: sale_mrp_bom
#: model:res.groups,name:sale_mrp_bom.sale_mrp_bom_group
msgid "Allows to define a BOM on sale order lines"
msgstr "Consente di definire una DiBa nelle righe ordine di vendita"
#. module: sale_mrp_bom
#: model:ir.model.fields,field_description:sale_mrp_bom.field_sale_order_line__bom_id
msgid "BoM"
msgstr "DiBa"
#. module: sale_mrp_bom
#. odoo-python
#: code:addons/sale_mrp_bom/models/sale_order_line.py:0
#, python-format
msgid "Please select BoM that has matched product with the line `{}`"
msgstr "Selezionare una DiBa che ha un prodotto corrispondente con la riga `{}`"
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_sale_order_line
msgid "Sales Order Line"
msgstr "Riga ordine di vendita"
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_stock_move
msgid "Stock Move"
msgstr "Movimento di magazzino"

46
sale_mrp_bom/i18n/nl.po Executable file
View File

@@ -0,0 +1,46 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_mrp_bom
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
#. module: sale_mrp_bom
#: model:res.groups,name:sale_mrp_bom.sale_mrp_bom_group
msgid "Allows to define a BOM on sale order lines"
msgstr ""
#. module: sale_mrp_bom
#: model:ir.model.fields,field_description:sale_mrp_bom.field_sale_order_line__bom_id
msgid "BoM"
msgstr ""
#. module: sale_mrp_bom
#. odoo-python
#: code:addons/sale_mrp_bom/models/sale_order_line.py:0
msgid "Please select a BoM that matches the product %(product)s"
msgstr ""
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_procurement_group
msgid "Procurement Group"
msgstr ""
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_sale_order_line
msgid "Sales Order Line"
msgstr ""
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_stock_move
msgid "Stock Move"
msgstr ""

48
sale_mrp_bom/i18n/nl_NL.po Executable file
View File

@@ -0,0 +1,48 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_mrp_bom
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-08-09 17:25+0000\n"
"Last-Translator: Bosd <c5e2fd43-d292-4c90-9d1f-74ff3436329a@anonaddy.me>\n"
"Language-Team: none\n"
"Language: nl_NL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.4\n"
#. module: sale_mrp_bom
#: model:res.groups,name:sale_mrp_bom.sale_mrp_bom_group
msgid "Allows to define a BOM on sale order lines"
msgstr "Staat toe om een stuklijst te definiëren op verkooporderregels"
#. module: sale_mrp_bom
#: model:ir.model.fields,field_description:sale_mrp_bom.field_sale_order_line__bom_id
msgid "BoM"
msgstr "Stuklijst"
#. module: sale_mrp_bom
#. odoo-python
#: code:addons/sale_mrp_bom/models/sale_order_line.py:0
msgid "Please select a BoM that matches the product %(product)s"
msgstr "Selecteer een stuklijst die overeenkomt met het product %(product)s"
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_procurement_group
msgid "Procurement Group"
msgstr "Inkoopgroep"
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_sale_order_line
msgid "Sales Order Line"
msgstr "Verkooporderregel"
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_stock_move
msgid "Stock Move"
msgstr "Voorraadverplaatsing"

View File

@@ -0,0 +1,45 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_mrp_bom
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: sale_mrp_bom
#: model:res.groups,name:sale_mrp_bom.sale_mrp_bom_group
msgid "Allows to define a BOM on sale order lines"
msgstr ""
#. module: sale_mrp_bom
#: model:ir.model.fields,field_description:sale_mrp_bom.field_sale_order_line__bom_id
msgid "BoM"
msgstr ""
#. module: sale_mrp_bom
#. odoo-python
#: code:addons/sale_mrp_bom/models/sale_order_line.py:0
msgid "Please select a BoM that matches the product %(product)s"
msgstr ""
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_procurement_group
msgid "Procurement Group"
msgstr ""
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_sale_order_line
msgid "Sales Order Line"
msgstr ""
#. module: sale_mrp_bom
#: model:ir.model,name:sale_mrp_bom.model_stock_move
msgid "Stock Move"
msgstr ""

View File

@@ -0,0 +1,5 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import procurement_group
from . import sale_order_line
from . import stock_move

View File

@@ -0,0 +1,95 @@
# Copyright 2025 360ERP (<https://www.360erp.com>)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from odoo import api, models
class ProcurementGroup(models.Model):
_inherit = "procurement.group"
@api.model
def run(self, procurements, raise_user_error=True):
"""
Handle phantom BoM (kit) procurements linked to sale order lines.
If a procurement is for a kit product associated with a sale order line,
this method explodes the kit into its components and generates new
procurements for those components. Original procurements that are not
kits, or not linked to a sale order line with a phantom BoM, are passed through.
:param procurements: A list of Procurement namedtuples
:param raise_user_error: Whether to raise UserError on failure
:return: Result of the original run method
"""
# Collect unique sale_line_ids from procurements that have them
sale_line_ids = list(
set(
p.values.get("sale_line_id")
for p in procurements
if p.values.get("sale_line_id")
)
)
# Pre-fetch sale lines and create a mapping for quick access
sale_lines = self.env["sale.order.line"].browse(sale_line_ids)
sale_lines_map = {sl.id: sl for sl in sale_lines}
procurements_without_kit = []
for procurement in procurements:
sale_line_id = procurement.values.get("sale_line_id")
sale_line = sale_lines_map.get(sale_line_id)
bom_kit = (
sale_line.bom_id.filtered(
lambda bm, pr=procurement: bm.type == "phantom"
and (
# If BoM has product_id, match the procurement's product_id
(bm.product_id and bm.product_id == pr.product_id)
or
# Otherwise (if BoM has no product_id), match the template_id
(
not bm.product_id
and bm.product_tmpl_id == pr.product_id.product_tmpl_id
)
)
)
if sale_line
else False
)
if bom_kit:
order_qty = procurement.product_uom._compute_quantity(
procurement.product_qty, bom_kit.product_uom_id, round=False
)
qty_to_produce = order_qty / bom_kit.product_qty
_dummy, bom_sub_lines = bom_kit.explode(
procurement.product_id,
qty_to_produce,
never_attribute_values=procurement.values.get(
"never_product_template_attribute_value_ids"
),
)
for bom_line, bom_line_data in bom_sub_lines:
bom_line_uom = bom_line.product_uom_id
quant_uom = bom_line.product_id.uom_id
# recreate dict of values since each child has its own bom_line_id
values = dict(procurement.values, bom_line_id=bom_line.id)
component_qty, procurement_uom = (
bom_line_uom._adjust_uom_quantities(
bom_line_data["qty"], quant_uom
)
)
procurements_without_kit.append(
self.env["procurement.group"].Procurement(
bom_line.product_id,
component_qty,
procurement_uom,
procurement.location_id,
procurement.name,
procurement.origin,
procurement.company_id,
values,
)
)
else:
procurements_without_kit.append(procurement)
return super().run(procurements_without_kit, raise_user_error=raise_user_error)

View File

@@ -0,0 +1,36 @@
# Copyright 2020 Akretion Renato Lima <renato.lima@akretion.com.br>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
bom_id = fields.Many2one(
comodel_name="mrp.bom",
string="BoM",
domain="[('product_tmpl_id.product_variant_ids', '=', product_id),"
"'|', ('product_id', '=', product_id), "
"('product_id', '=', False)]",
)
@api.constrains("bom_id", "product_id")
def _check_match_product_variant_ids(self):
for line in self:
if not line.bom_id:
continue
bom_product = line.bom_id.product_id
bom_product_tmpl = line.bom_id.product_tmpl_id
if bom_product and bom_product == line.product_id:
continue
if not bom_product and bom_product_tmpl == line.product_template_id:
continue
raise ValidationError(
_(
"Please select a BoM that matches the product %(product)s",
product=line.product_id.display_name,
)
)

View File

@@ -0,0 +1,14 @@
# Copyright 2020 Akretion Renato Lima <renato.lima@akretion.com.br>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models
class StockMove(models.Model):
_inherit = "stock.move"
def _prepare_procurement_values(self):
values = super()._prepare_procurement_values()
if self.sale_line_id and self.sale_line_id.bom_id:
values["bom_id"] = self.sale_line_id.bom_id
return values

3
sale_mrp_bom/pyproject.toml Executable file
View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,3 @@
To be able to select a specific Bill of Materials in a sale order, the
user needs the special permission: "Allows to define a BOM on sale order
lines".

View File

@@ -0,0 +1,5 @@
- Renato Lima \<<renato.lima@akretion.com.br>\>
Trobz:
- Hai Lang \<<hailn@trobz.com>\>

2
sale_mrp_bom/readme/CREDITS.md Executable file
View File

@@ -0,0 +1,2 @@
The migration of this module from 12.0 to 14.0 was financially supported
by Camptocamp.

View File

@@ -0,0 +1,3 @@
This modules allows to specify a Bill Of Materials directly inside a
sale order line. It is specially useful to select alternative
manufacturing and sub-contracting routings.

1
sale_mrp_bom/readme/INSTALL.md Executable file
View File

@@ -0,0 +1 @@

1
sale_mrp_bom/readme/ROADMAP.md Executable file
View File

@@ -0,0 +1 @@

9
sale_mrp_bom/readme/USAGE.md Executable file
View File

@@ -0,0 +1,9 @@
When adding a new sale order line, you can eventually select a specific
Bill Of Materials.
![](../static/description/sale_order_1.png)
When confirming the sale order, if the routing is manufacturing then the
production order will be using the specified Bill Of Materials.
![](../static/description/manufacturing_order_1.png)

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="sale_mrp_bom_group" model="res.groups">
<field name="name">Allows to define a BOM on sale order lines</field>
<field name="category_id" ref="base.module_category_hidden" />
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,466 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="sale-mrp-bom">
<h1>Sale MRP BOM</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:63cc33af1668c7019325079317e92367d731d9ccd088c6df0049d6c5c100f4fc
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/sale-workflow/tree/18.0/sale_mrp_bom"><img alt="OCA/sale-workflow" src="https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-sale_mrp_bom"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This modules allows to specify a Bill Of Materials directly inside a
sale order line. It is specially useful to select alternative
manufacturing and sub-contracting routings.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#installation" id="toc-entry-1">Installation</a></li>
<li><a class="reference internal" href="#configuration" id="toc-entry-2">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-3">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-4">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-5">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-6">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-7">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-8">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="toc-entry-9">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-10">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="installation">
<h2><a class="toc-backref" href="#toc-entry-1">Installation</a></h2>
</div>
<div class="section" id="configuration">
<h2><a class="toc-backref" href="#toc-entry-2">Configuration</a></h2>
<p>To be able to select a specific Bill of Materials in a sale order, the
user needs the special permission: “Allows to define a BOM on sale order
lines”.</p>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-3">Usage</a></h2>
<p>When adding a new sale order line, you can eventually select a specific
Bill Of Materials.</p>
<p><img alt="image1" src="https://raw.githubusercontent.com/OCA/sale-workflow/18.0/sale_mrp_bom/static/description/sale_order_1.png" /></p>
<p>When confirming the sale order, if the routing is manufacturing then the
production order will be using the specified Bill Of Materials.</p>
<p><img alt="image2" src="https://raw.githubusercontent.com/OCA/sale-workflow/18.0/sale_mrp_bom/static/description/manufacturing_order_1.png" /></p>
</div>
<div class="section" id="known-issues-roadmap">
<h2><a class="toc-backref" href="#toc-entry-4">Known issues / Roadmap</a></h2>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-5">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/sale-workflow/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/sale-workflow/issues/new?body=module:%20sale_mrp_bom%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-6">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-7">Authors</a></h3>
<ul class="simple">
<li>Akretion</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-8">Contributors</a></h3>
<ul class="simple">
<li>Renato Lima &lt;<a class="reference external" href="mailto:renato.lima&#64;akretion.com.br">renato.lima&#64;akretion.com.br</a>&gt;</li>
</ul>
<p>Trobz:</p>
<ul class="simple">
<li>Hai Lang &lt;<a class="reference external" href="mailto:hailn&#64;trobz.com">hailn&#64;trobz.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
<h3><a class="toc-backref" href="#toc-entry-9">Other credits</a></h3>
<p>The migration of this module from 12.0 to 14.0 was financially supported
by Camptocamp.</p>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-10">Maintainers</a></h3>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/sale-workflow/tree/18.0/sale_mrp_bom">OCA/sale-workflow</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

4
sale_mrp_bom/tests/__init__.py Executable file
View File

@@ -0,0 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import test_sale_mrp_bom
from . import test_sale_mrp_bom_multi_line

View File

@@ -0,0 +1,142 @@
# Copyright 2020 Akretion Renato Lima <renato.lima@akretion.com.br>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase
class TestSaleMrpLink(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner = cls.env.ref("base.res_partner_2")
cls.warehouse = cls.env.ref("stock.warehouse0")
route_manufacture = cls.warehouse.manufacture_pull_id.route_id.id
route_mto = cls.warehouse.mto_pull_id.route_id.id
cls.product_a = cls._create_product(
"Product A", route_ids=[(6, 0, [route_manufacture, route_mto])]
)
cls.product_b = cls._create_product(
"Product B", route_ids=[(6, 0, [route_manufacture, route_mto])]
)
cls.component_a = cls._create_product("Component A", route_ids=[])
cls.component_b = cls._create_product("Component B", route_ids=[])
@classmethod
def _create_product(cls, name, route_ids):
return cls.env["product.product"].create(
{"name": name, "type": "consu", "route_ids": route_ids}
)
def _prepare_bom_lines(self):
# Create BOMs
bom_a_v1 = self._create_bom(self.product_a.product_tmpl_id)
self._create_bom_line(bom_a_v1, self.component_a, 1)
bom_a_v2 = self._create_bom(self.product_a.product_tmpl_id)
self._create_bom_line(bom_a_v2, self.component_a, 2)
bom_b_v1 = self._create_bom(self.product_b.product_tmpl_id)
self._create_bom_line(bom_b_v1, self.component_b, 1)
bom_b_v2 = self._create_bom(self.product_b.product_tmpl_id)
self._create_bom_line(bom_b_v2, self.component_b, 2)
bom_a, bom_b = bom_a_v2, bom_b_v2
self.boms = {
self.product_a.id: bom_a,
self.product_b.id: bom_b,
}
return bom_a, bom_b
def _prepare_so(self):
bom_a_v2, bom_b_v2 = self._prepare_bom_lines()
# Create Sale Order
so = self._create_sale_order(self.partner, "SO1")
self._create_sale_order_line(so, self.product_a, 1, 10.0, bom_a_v2)
self._create_sale_order_line(so, self.product_b, 1, 10.0, bom_b_v2)
so.action_confirm()
return so, bom_a_v2, bom_b_v2
def _create_bom(self, template):
return self.env["mrp.bom"].create(
[{"product_tmpl_id": template.id, "type": "normal"}]
)
def _create_bom_line(self, bom, product, qty):
self.env["mrp.bom.line"].create(
[{"bom_id": bom.id, "product_id": product.id, "product_qty": qty}]
)
def _create_sale_order(self, partner, client_ref):
return self.env["sale.order"].create(
[{"partner_id": partner.id, "client_order_ref": client_ref}]
)
def _create_sale_order_line(self, sale_order, product, qty, price, bom):
self.env["sale.order.line"].create(
[
{
"order_id": sale_order.id,
"product_id": product.id,
"price_unit": price,
"product_uom_qty": qty,
"bom_id": bom.id,
}
]
)
def test_define_bom_in_sale_line(self):
"""Check manufactured order is created with BOM defined in Sale."""
previous_mos = self.env["mrp.production"].search([])
_so, _bom_a, _bom_b = self._prepare_so()
# Check manufacture order
mos = self.env["mrp.production"].search([]) - previous_mos
for mo in mos:
self.assertEqual(mo.bom_id, self.boms.get(mo.product_id.id))
def test_pick_a_pack_confirm(self):
so, bom_a, bom_b = self._prepare_so()
picking, boms = so.picking_ids[0], (bom_a, bom_b)
for i, line in enumerate(picking.move_ids):
values = line._prepare_procurement_values()
self.assertEqual(values["bom_id"], boms[i])
def test_mismatch_product_variant_ids(self):
so, bom_a, bom_b = self._prepare_so()
line_a = so.order_line[0]
self.assertEqual(line_a.bom_id, bom_a)
with self.assertRaises(ValidationError):
line_a.write({"bom_id": bom_b})
def test_accept_bom_with_no_variant(self):
# make variants for template of product A
product_tmpl_a = self.product_a.product_tmpl_id
prod_att_color = self.env["product.attribute"].create({"name": "Color"})
product_attr_val_red, product_attr_val_green = self.env[
"product.attribute.value"
].create(
[
{"name": "red", "attribute_id": prod_att_color.id, "sequence": 1},
{"name": "blue", "attribute_id": prod_att_color.id, "sequence": 2},
]
)
product_tmpl_a.attribute_line_ids = [
(
0,
0,
{
"attribute_id": prod_att_color.id,
"value_ids": [
(6, 0, [product_attr_val_red.id, product_attr_val_green.id])
],
},
)
]
product_a = product_tmpl_a.product_variant_ids[0]
bom_no_variant = self._create_bom(product_tmpl_a)
so = self._create_sale_order(self.partner, "SO2")
self._create_sale_order_line(so, product_a, 2, 25, bom_no_variant)
line_a = so.order_line[0]
line_a.bom_id = bom_no_variant

View File

@@ -0,0 +1,211 @@
# Copyright 2025 360ERP (https://www.360erp.com)
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from odoo.tests.common import TransactionCase
class TestSalePhantomBomProcurementMultiLine(TransactionCase):
"""
Tests Phantom BoM explosion for Kits selected on SO Lines.
Focuses on a scenario with multiple lines for the same kit product,
each specifying a different phantom BoM referencing the same component,
to ensure component quantities are correctly exploded and aggregated
within the resulting Stock Picking moves.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(
context=dict(
cls.env.context,
mail_create_nolog=True,
mail_create_nosubscribe=True,
mail_notrack=True,
no_reset_password=True,
tracking_disable=True,
)
)
cls.company = cls.env.company
# Required groups
cls.env.user.groups_id += cls.env.ref("stock.group_adv_location")
cls.env.user.groups_id += cls.env.ref("sale_mrp_bom.sale_mrp_bom_group")
# Ensure MTO Route is Active
cls.mto_route = cls.env.ref(
"stock.route_warehouse0_mto", raise_if_not_found=True
)
if not cls.mto_route.active:
cls.mto_route.action_unarchive()
# Products
cls.product_mtokit = cls.env["product.product"].create(
[
{
"name": "MTOKIT",
"type": "consu",
"route_ids": [(6, 0, [cls.mto_route.id])],
"categ_id": cls.env.ref("product.product_category_all").id,
}
]
)
cls.product_mtocomp = cls.env["product.product"].create(
[
{
"name": "MTOCOMP",
"type": "consu",
"route_ids": [],
"categ_id": cls.env.ref("product.product_category_all").id,
}
]
)
# BoMs (Phantom)
cls.bom_kit1 = cls.env["mrp.bom"].create(
[
{
"product_tmpl_id": cls.product_mtokit.product_tmpl_id.id,
"product_qty": 1.0,
"type": "phantom",
"code": "KIT1",
}
]
)
# BoM Line 1: 1 x MTOCOMP
cls.env["mrp.bom.line"].create(
[
{
"bom_id": cls.bom_kit1.id,
"product_id": cls.product_mtocomp.id,
"product_qty": 1,
}
]
)
cls.bom_kit2 = cls.env["mrp.bom"].create(
[
{
"product_tmpl_id": cls.product_mtokit.product_tmpl_id.id,
"product_qty": 1.0,
"type": "phantom",
"code": "KIT2",
}
]
)
# BoM Line 2: 2 x MTOCOMP
cls.env["mrp.bom.line"].create(
[
{
"bom_id": cls.bom_kit2.id,
"product_id": cls.product_mtocomp.id,
"product_qty": 2,
}
]
)
cls.partner = cls.env.ref("base.res_partner_2") # Customer
cls.warehouse = cls.env.ref("stock.warehouse0")
def _create_sale_order(self, partner):
return self.env["sale.order"].create(
[
{
"partner_id": partner.id,
"partner_invoice_id": partner.id,
"partner_shipping_id": partner.id,
"warehouse_id": self.warehouse.id,
}
]
)
def _create_sale_order_line(self, sale_order, product, qty, bom):
sol = self.env["sale.order.line"].create(
[
{
"order_id": sale_order.id,
"product_id": product.id,
"product_uom_qty": qty,
"bom_id": bom.id,
"product_uom": product.uom_id.id,
"price_unit": 1,
}
]
)
return sol
def test_phantom_bom_explosion_multi_line_same_component(self):
"""
Test SO with 2 lines for MTOKIT (phantom): Line 1 uses KIT1 (1 comp),
Line 2 uses KIT2 (2 comps).
Verify that the resulting delivery picking moves contain lines for MTO_COMP
with correctly aggregated quantities based on the phantom BoM explosion.
"""
# Create SO
so = self._create_sale_order(self.partner)
qty_line1 = 5
qty_line2 = 3
# Line 1: 5 x MTOKIT using KIT1 (-> 5 * 1 = 5 MTO_COMP)
self._create_sale_order_line(so, self.product_mtokit, qty_line1, self.bom_kit1)
# Line 2: 3 x MTOKIT using KIT2 (-> 3 * 2 = 6 MTO_COMP)
self._create_sale_order_line(so, self.product_mtokit, qty_line2, self.bom_kit2)
# Confirm the Sale Order - This triggers the delivery order creation
# and the phantom BoM explosion for the delivery moves.
so.action_confirm()
# Find the picking associated with the Sale Order
pickings = so.picking_ids
self.assertEqual(
len(pickings),
1,
f"Expected one picking for {so.name}, found {len(pickings)}",
)
picking = pickings[0]
# Find stock moves within this picking
moves = picking.move_ids
# Verify no moves for the kit itself (it's phantom)
kit_product_moves = moves.filtered(
lambda m: m.product_id == self.product_mtokit
)
self.assertFalse(
kit_product_moves,
"No stock move line should be created for the parent kit product (MTOKIT).",
)
# Find the moves specifically for the component within this picking
comp_product_moves = moves.filtered(
lambda m: m.product_id == self.product_mtocomp
)
self.assertTrue(
comp_product_moves,
f"Stock move lines for {self.product_mtocomp.name} should be created.",
)
# Calculate expected *total* component quantity based on BoMs
expected_comp_qty_line1 = (
qty_line1
* self.bom_kit1.bom_line_ids.filtered(
lambda bl: bl.product_id == self.product_mtocomp
).product_qty
)
expected_comp_qty_line2 = (
qty_line2
* self.bom_kit2.bom_line_ids.filtered(
lambda bl: bl.product_id == self.product_mtocomp
).product_qty
)
expected_total_qty = (
expected_comp_qty_line1 + expected_comp_qty_line2
) # Should be 5 * 1 + 3 * 2 = 11
# Verify the total quantity demanded in the generated stock moves
# Check the initial demand planned for the picking
actual_total_move_qty = sum(comp_product_moves.mapped("product_uom_qty"))
self.assertEqual(
actual_total_move_qty,
expected_total_qty,
f"MTO_COMP: expected {expected_total_qty}, got {actual_total_move_qty}.",
)

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="sale_order_form" model="ir.ui.view">
<field name="name">sale_mrp_bom.sale.order.form</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form" />
<field name="arch" type="xml">
<xpath
expr="//field[@name='order_line']//list//field[@name='name']"
position="after"
>
<field
name="bom_id"
groups="sale_mrp_bom.sale_mrp_bom_group"
optional="show"
context="{'default_product_id': product_id, 'default_product_tmpl_id': product_template_id}"
/>
</xpath>
<xpath
expr="//field[@name='order_line']//form//field[@name='customer_lead']"
position="after"
>
<field name="product_template_id" invisible="1" />
<field
name="bom_id"
groups="sale_mrp_bom.sale_mrp_bom_group"
context="{'default_product_id': product_id, 'default_product_tmpl_id': product_template_id}"
/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="sale_order_line_tree" model="ir.ui.view">
<field name="name">sale_mrp_bom.sale.order.line.tree</field>
<field name="model">sale.order.line</field>
<field name="inherit_id" ref="sale.view_order_line_tree" />
<field name="arch" type="xml">
<field name="route_id" position="after">
<field
name="bom_id"
groups="sale_mrp_bom.sale_mrp_bom_group"
optional="show"
/>
</field>
</field>
</record>
</odoo>