Merge PR #905 into 14.0

Signed-off-by simahawk
This commit is contained in:
OCA-git-bot
2023-09-13 13:45:25 +00:00
21 changed files with 1155 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
=================================
Manufacturing Order Auto-Validate
=================================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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%2Fmanufacture-lightgray.png?logo=github
:target: https://github.com/OCA/manufacture/tree/14.0/mrp_production_auto_validate
:alt: OCA/manufacture
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/manufacture-14-0/manufacture-14-0-mrp_production_auto_validate
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/129/14.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
Auto-validate manufacturing orders when the "Pick components" transfer operation
is validated. This feature has to be enabled for each Bill of Material to produce.
**Table of contents**
.. contents::
:local:
Configuration
=============
Warehouse
~~~~~~~~~
* Go to *Inventory > Configuration > Settings* to enable the *Multi-Step Routes* option
* Go to *Inventory > Configuration > Warehouses* and enable the manufacturing
with two or three steps on your warehouse (to get a *Pick components* transfer
operation generated when you create a MO).
Bill of Materials
~~~~~~~~~~~~~~~~~
* Go to *Manufacturing > Products > Bills of Materials* and enable the auto-validation
of manufacturing orders for your BoMs (*Order Auto Validation* field).
Known issues / Roadmap
======================
* Add support of auto-validation as soon as we have enough components to produce
at least one finished product. Currently Odoo doesn't support this
(`reservation_state` of MO is still set to "Waiting" in such case).
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/manufacture/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/manufacture/issues/new?body=module:%20mrp_production_auto_validate%0Aversion:%2014.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
~~~~~~~
* Camptocamp
Contributors
~~~~~~~~~~~~
* Sébastien Alix <sebastien.alix@camptocamp.com>
* Simone Orsi <simone.orsi@camptocamp.com>
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.
.. |maintainer-sebalix| image:: https://github.com/sebalix.png?size=40px
:target: https://github.com/sebalix
:alt: sebalix
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-sebalix|
This module is part of the `OCA/manufacture <https://github.com/OCA/manufacture/tree/14.0/mrp_production_auto_validate>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -0,0 +1,2 @@
from . import models
from . import wizards

View File

@@ -0,0 +1,19 @@
# Copyright 2022 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
{
"name": "Manufacturing Order Auto-Validate",
"summary": "Manufacturing Order Auto-Validation when components are picked",
"version": "14.0.1.0.0",
"license": "AGPL-3",
"website": "https://github.com/OCA/manufacture",
"author": "Camptocamp, Odoo Community Association (OCA)",
"category": "Manufacturing",
"depends": ["mrp"],
"data": [
"views/mrp_bom.xml",
"views/mrp_production.xml",
],
"installable": True,
"development_status": "Beta",
"maintainers": ["sebalix"],
}

View File

@@ -0,0 +1,4 @@
from . import mrp_bom
from . import mrp_production
from . import stock_picking
from . import stock_rule

View File

@@ -0,0 +1,38 @@
# Copyright 2022 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo import _, api, fields, models
class MrpBom(models.Model):
_inherit = "mrp.bom"
mo_auto_validation = fields.Boolean(
string="Order Auto Validation",
help=(
"Validate automatically the manufacturing order "
"when the 'Pick Components' transfer is validated.\n"
"This behavior is available only if the warehouse is configured "
"with 2 or 3 steps."
),
default=False,
)
mo_auto_validation_warning = fields.Char(
string="Order Auto Validation (warning)",
compute="_compute_mo_auto_validation_warning",
)
@api.onchange("type")
def onchange_type_auto_validation(self):
if self.type != "normal":
self.mo_auto_validation = self.mo_auto_validation_warning = False
@api.depends("mo_auto_validation")
def _compute_mo_auto_validation_warning(self):
for bom in self:
bom.mo_auto_validation_warning = False
if bom.mo_auto_validation:
bom.mo_auto_validation_warning = _(
"The Quantity To Produce of an order is now "
"restricted to the BoM Quantity."
)

View File

@@ -0,0 +1,192 @@
# Copyright 2022 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import logging
from odoo import _, api, exceptions, fields, models, tools
_logger = logging.getLogger(__name__)
class MrpProduction(models.Model):
_inherit = "mrp.production"
auto_validate = fields.Boolean(
string="Auto Validate",
compute="_compute_auto_validate",
store=True,
states={"draft": [("readonly", False)]},
)
@api.constrains("bom_id", "auto_validate", "product_qty")
def check_bom_auto_validate(self):
for mo in self:
# FIXME: Handle different UOM between BOM and MO
qty_ok = (
tools.float_compare(
mo.product_qty,
mo.bom_id.product_qty,
precision_rounding=mo.product_uom_id.rounding,
)
== 0
)
bypass_check = self.env.context.get("disable_check_mo_auto_validate")
if bypass_check:
return
if mo.bom_id and mo.auto_validate and not qty_ok:
raise exceptions.ValidationError(
_(
"The quantity to produce is restricted to {qty} "
"as the BoM is configured with the "
"'Order Auto Validation' option."
).format(qty=mo.bom_id.product_qty)
)
@api.depends("bom_id.mo_auto_validation", "state")
def _compute_auto_validate(self):
for prod in self:
if prod.state != "draft":
# Avoid recomputing the value once the MO is confirmed.
# e.g. if the value changes on the BOM but the MO was already confirmed,
# or if the user forces another value while the MO is in draft,
# we don't want to change the value after confirmation.
continue
prod.auto_validate = prod.bom_id.mo_auto_validation
def _auto_validate_after_picking(self):
self.ensure_one()
if self.state == "progress":
# If the MO is already in progress, we want to call the immediate
# wizard to handle lot/serial number automatically (if any).
action = self._action_generate_immediate_wizard()
self._handle_wiz_mrp_immediate_production(action)
res = self.button_mark_done()
if res is True:
return True
res = self.handle_mark_done_result(res)
# Each call might return a new wizard, loop until we satisfy all of them
while isinstance(res, dict):
res = self.handle_mark_done_result(res)
def handle_mark_done_result(self, res):
if res["res_model"] == "mrp.production":
# MO has been processed and returned an action to open a backorder
return True
handler_name = "_handle_wiz_" + res["res_model"].replace(".", "_")
handler = getattr(self, handler_name, None)
if not handler:
_logger.warning("'%s' wizard is not supported", res["res_model"])
return True
return handler(res)
def _handle_wiz_mrp_immediate_production(self, action):
wiz_model = self.env[action["res_model"]].with_context(
**action.get("context", {})
)
wiz = wiz_model.create({})
return wiz.process()
def _handle_wiz_mrp_production_backorder(self, action):
wiz_model = self.env[action["res_model"]].with_context(
**action.get("context", {})
)
wiz = wiz_model.create({})
return wiz.action_backorder()
@api.model_create_multi
def create(self, values_list):
new_values_list, messages_to_post = self.adapt_values_qty_for_auto_validation(
values_list
)
res = super().create(new_values_list)
if messages_to_post:
for pos, msg in messages_to_post.items():
prod = res[pos]
prod.message_post(body=msg)
return res
@api.model
def adapt_values_qty_for_auto_validation(self, values_list):
"""Adapt create values according to qty with auto validated BOM
If MOs are to be created with a BOM having auto validation, we must ensure
the quantity of the MO is equal to the quantity of the BOM.
However when MOs are created through procurements, the requested quantity
is based on the procurement quantity, so we should either
* increase the quantity to match the BOM if procurement value is lower
* split the values to create one MO into multiple values to create multiple
MOs matching the BOM quantity if procurement value is bigger
"""
messages_to_post = {}
if not self.env.context.get("_split_create_values_for_auto_validation"):
return values_list, messages_to_post
new_values_list = []
for values in values_list:
bom_id = values.get("bom_id")
if not bom_id:
new_values_list.append(values)
continue
bom = self.env["mrp.bom"].browse(bom_id)
if not bom.mo_auto_validation:
new_values_list.append(values)
continue
create_qty = values.get("product_qty")
create_uom = self.env["uom.uom"].browse(values.get("product_uom_id"))
bom_qty = bom.product_qty
bom_uom = bom.product_uom_id
if create_uom != bom_uom:
create_qty = create_uom._compute_quantity(create_qty, bom_uom)
if (
tools.float_compare(
create_qty, bom_qty, precision_rounding=bom_uom.rounding
)
== 0
):
new_values_list.append(values)
continue
elif (
tools.float_compare(
create_qty, bom_qty, precision_rounding=bom_uom.rounding
)
< 0
):
procure_qty = values.get("product_qty")
values["product_qty"] = bom_qty
values["product_uom_id"] = bom_uom.id
msg = _(
"Quantity in procurement (%s %s) was increased to %s %s due to auto "
"validation feature preventing to create an MO with a different "
"qty than defined on the BOM."
) % (
procure_qty,
create_uom.display_name,
bom_qty,
bom_uom.display_name,
)
messages_to_post[len(new_values_list)] = msg
new_values_list.append(values)
continue
# If we get here we need to split the prepared MO values
# into multiple MO values to respect BOM qty
while (
tools.float_compare(create_qty, 0, precision_rounding=bom_uom.rounding)
> 0
):
new_values = values.copy()
new_values["product_qty"] = bom_qty
new_values["product_uom_id"] = bom_uom.id
msg = _(
"Quantity in procurement (%s %s) was split to multiple production "
"orders of %s %s due to auto validation feature preventing to "
"set a quantity to produce different than the quantity defined "
"on the Bill of Materials."
) % (
values.get("product_qty"),
create_uom.display_name,
bom_qty,
bom_uom.display_name,
)
messages_to_post[len(new_values_list)] = msg
new_values_list.append(new_values)
create_qty -= bom_qty
return new_values_list, messages_to_post

View File

@@ -0,0 +1,39 @@
# Copyright 2022 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo import models
class StockPicking(models.Model):
_inherit = "stock.picking"
def _get_manufacturing_orders(self, states=None):
self.ensure_one()
if states is None:
states = ("confirmed", "progress")
return self.move_lines.move_dest_ids.raw_material_production_id.filtered(
lambda o: o.state in states
)
def _action_done(self):
res = super()._action_done()
for picking in self:
if picking.state != "done":
continue
orders = picking._get_manufacturing_orders()
if not orders:
continue
for order in orders:
# NOTE: use of 'reservation_state' doesn't allow to produce
# at least 1 finished product even if there is enough components,
# but Odoo expects to work this way.
if order.auto_validate and order.reservation_state == "assigned":
# 'stock.immediate.transfer' could set the 'skip_immediate'
# key to process the transfer. The same ctx key is used by
# MO validation methods, but they are not the same!
# Unset the key in such case.
# TODO add a test
if order.env.context.get("skip_immediate"):
order = order.with_context(skip_immediate=False)
order._auto_validate_after_picking()
return res

View File

@@ -0,0 +1,13 @@
# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import api, models
class StockRule(models.Model):
_inherit = "stock.rule"
@api.model
def _run_manufacture(self, procurements):
return super(
StockRule, self.with_context(_split_create_values_for_auto_validation=True)
)._run_manufacture(procurements)

View File

@@ -0,0 +1,13 @@
Warehouse
~~~~~~~~~
* Go to *Inventory > Configuration > Settings* to enable the *Multi-Step Routes* option
* Go to *Inventory > Configuration > Warehouses* and enable the manufacturing
with two or three steps on your warehouse (to get a *Pick components* transfer
operation generated when you create a MO).
Bill of Materials
~~~~~~~~~~~~~~~~~
* Go to *Manufacturing > Products > Bills of Materials* and enable the auto-validation
of manufacturing orders for your BoMs (*Order Auto Validation* field).

View File

@@ -0,0 +1,2 @@
* Sébastien Alix <sebastien.alix@camptocamp.com>
* Simone Orsi <simone.orsi@camptocamp.com>

View File

@@ -0,0 +1,2 @@
Auto-validate manufacturing orders when the "Pick components" transfer operation
is validated. This feature has to be enabled for each Bill of Material to produce.

View File

@@ -0,0 +1,3 @@
* Add support of auto-validation as soon as we have enough components to produce
at least one finished product. Currently Odoo doesn't support this
(`reservation_state` of MO is still set to "Waiting" in such case).

View File

@@ -0,0 +1,456 @@
<?xml version="1.0" encoding="utf-8" ?>
<!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: http://docutils.sourceforge.net/" />
<title>Manufacturing Order Auto-Validate</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/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: grey; } /* 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 {
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" id="manufacturing-order-auto-validate">
<h1 class="title">Manufacturing Order Auto-Validate</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" 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" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/manufacture/tree/14.0/mrp_production_auto_validate"><img alt="OCA/manufacture" src="https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/manufacture-14-0/manufacture-14-0-mrp_production_auto_validate"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/129/14.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>Auto-validate manufacturing orders when the “Pick components” transfer operation
is validated. This feature has to be enabled for each Bill of Material to produce.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="id1">Configuration</a><ul>
<li><a class="reference internal" href="#warehouse" id="id2">Warehouse</a></li>
<li><a class="reference internal" href="#bill-of-materials" id="id3">Bill of Materials</a></li>
</ul>
</li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id4">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id5">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id6">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id7">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id8">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id9">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id1">Configuration</a></h1>
<div class="section" id="warehouse">
<h2><a class="toc-backref" href="#id2">Warehouse</a></h2>
<ul class="simple">
<li>Go to <em>Inventory &gt; Configuration &gt; Settings</em> to enable the <em>Multi-Step Routes</em> option</li>
<li>Go to <em>Inventory &gt; Configuration &gt; Warehouses</em> and enable the manufacturing
with two or three steps on your warehouse (to get a <em>Pick components</em> transfer
operation generated when you create a MO).</li>
</ul>
</div>
<div class="section" id="bill-of-materials">
<h2><a class="toc-backref" href="#id3">Bill of Materials</a></h2>
<ul class="simple">
<li>Go to <em>Manufacturing &gt; Products &gt; Bills of Materials</em> and enable the auto-validation
of manufacturing orders for your BoMs (<em>Order Auto Validation</em> field).</li>
</ul>
</div>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id4">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Add support of auto-validation as soon as we have enough components to produce
at least one finished product. Currently Odoo doesnt support this
(<cite>reservation_state</cite> of MO is still set to “Waiting” in such case).</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id5">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/manufacture/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 smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/manufacture/issues/new?body=module:%20mrp_production_auto_validate%0Aversion:%2014.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">
<h1><a class="toc-backref" href="#id6">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id7">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id8">Contributors</a></h2>
<ul class="simple">
<li>Sébastien Alix &lt;<a class="reference external" href="mailto:sebastien.alix&#64;camptocamp.com">sebastien.alix&#64;camptocamp.com</a>&gt;</li>
<li>Simone Orsi &lt;<a class="reference external" href="mailto:simone.orsi&#64;camptocamp.com">simone.orsi&#64;camptocamp.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id9">Maintainers</a></h2>
<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>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external" href="https://github.com/sebalix"><img alt="sebalix" src="https://github.com/sebalix.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/manufacture/tree/14.0/mrp_production_auto_validate">OCA/manufacture</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>
</body>
</html>

View File

@@ -0,0 +1 @@
from . import test_auto_validate

View File

@@ -0,0 +1,197 @@
# Copyright 2022 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo.exceptions import ValidationError
from odoo.tests.common import Form, SavepointCase
class TestManufacturingOrderAutoValidate(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
# Configure the WH to manufacture in at least two steps to get a
# "pick components" transfer operation
cls.wh = cls.env.ref("stock.warehouse0")
cls.wh.manufacture_steps = "pbm"
# Configure the product to be replenished through manufacture route
cls.product_template = cls.env.ref(
"mrp.product_product_computer_desk_head_product_template"
)
cls.manufacture_route = cls.env.ref("mrp.route_warehouse0_manufacture")
cls.product_template.route_ids = [(6, 0, [cls.manufacture_route.id])]
# Configure the BoM to auto-validate manufacturing orders
# NOTE: to ease tests we take a BoM with only one component
cls.bom = cls.env.ref("mrp.mrp_bom_table_top") # Tracked by S/N
cls.bom.mo_auto_validation = True
cls.uom_unit = cls.env.ref("uom.product_uom_unit")
@classmethod
def _replenish_product(cls, product, product_qty=1, product_uom=None):
if product_uom is None:
product_uom = cls.uom_unit
wiz = (
cls.env["product.replenish"]
.with_context(default_product_id=product.id)
.create(
{
"quantity": product_qty,
"product_uom_id": product_uom.id,
}
)
)
wiz.launch_replenishment()
@classmethod
def _create_manufacturing_order(cls, bom, product_qty=1):
with Form(cls.env["mrp.production"]) as form:
form.bom_id = bom
form.product_qty = product_qty
order = form.save()
order.invalidate_cache()
return order
@classmethod
def _validate_picking(cls, picking, moves=None):
"""Validate a stock transfer.
`moves` can be set with a list of tuples [(move, quantity_done)] to
process the transfer partially.
"""
if moves is None:
moves = []
for move in picking.move_lines:
# Try to match a move to set a given qty
for move2, qty_done in moves:
if move == move2:
move.quantity_done = qty_done
break
else:
move.quantity_done = move.product_uom_qty
picking._action_done()
def test_bom_alert(self):
self.assertIn(
"restricted to the BoM Quantity", self.bom.mo_auto_validation_warning
)
def test_get_manufacturing_orders_pbm(self):
"""Get the MO from transfers in a 2 steps configuration."""
# WH already configured as 'Pick components and then manufacture (2 steps)'
order = self._create_manufacturing_order(self.bom)
order.action_confirm()
picking_pick = order.picking_ids
self.assertEqual(picking_pick._get_manufacturing_orders(), order)
def test_get_manufacturing_orders_pbm_sam(self):
"""Get the MO from transfers in a 3 steps configuration."""
self.wh.manufacture_steps = "pbm_sam"
order = self._create_manufacturing_order(self.bom)
order.action_confirm()
picking_pick = order.picking_ids.filtered(
lambda o: "Pick" in o.picking_type_id.name
)
picking_store = order.picking_ids.filtered(
lambda o: "Store" in o.picking_type_id.name
)
self.assertEqual(picking_pick._get_manufacturing_orders(), order)
self.assertFalse(picking_store._get_manufacturing_orders())
def test_create_order_too_much_qty_to_produce(self):
"""Creation of MO for 2 finished product while BoM produces 1."""
with self.assertRaisesRegex(ValidationError, r"is restricted to"):
self._create_manufacturing_order(self.bom, product_qty=2)
def test_auto_validate_disabled(self):
"""Auto-validation of MO disabled. No change in the standard behavior."""
self.bom.mo_auto_validation = False
order = self._create_manufacturing_order(self.bom)
order.action_confirm()
self.assertEqual(order.state, "confirmed")
picking = order.picking_ids
picking.action_assign()
self.assertEqual(picking.state, "assigned")
self._validate_picking(picking)
self.assertEqual(picking.state, "done")
self.assertEqual(order.state, "confirmed")
def test_auto_validate_one_qty_to_produce(self):
"""Auto-validation of MO with components for 1 finished product."""
order = self._create_manufacturing_order(self.bom)
order.action_confirm()
self.assertEqual(order.state, "confirmed")
picking = order.picking_ids
picking.action_assign()
self.assertEqual(picking.state, "assigned")
self._validate_picking(picking)
self.assertEqual(picking.state, "done")
self.assertEqual(order.state, "done")
self.assertTrue(order.lot_producing_id)
def test_auto_validate_two_qty_to_produce(self):
"""Auto-validation of MO with components for 2 finished product."""
self.bom.product_qty = 2
order = self._create_manufacturing_order(self.bom, product_qty=2)
order.action_confirm()
self.assertEqual(order.state, "confirmed")
picking = order.picking_ids
picking.action_assign()
self.assertEqual(picking.state, "assigned")
self._validate_picking(picking)
self.assertEqual(picking.state, "done")
self.assertEqual(order.state, "done")
self.assertTrue(order.lot_producing_id)
def test_auto_validate_with_not_enough_components(self):
"""MO not validated: not enough components to produce 1 finished product."""
self.bom.product_qty = 2
self.bom.bom_line_ids.product_qty = 4
order = self._create_manufacturing_order(self.bom, product_qty=2)
order.action_confirm()
self.assertEqual(order.state, "confirmed")
# Process 'Pick components' with not enough qties to process at least
# 1 finished product (2 components required but only 1 transfered)
picking = order.picking_ids
picking.action_assign()
self.assertEqual(picking.state, "assigned")
self._validate_picking(picking, moves=[(picking.move_lines, 1)])
self.assertEqual(picking.state, "done")
self.assertEqual(picking.backorder_ids.move_lines.product_uom_qty, 3)
# Check that no MO gets validated in the process
order_done = picking._get_manufacturing_orders(states=("done",))
self.assertFalse(order_done)
self.assertEqual(order.state, "confirmed")
self.assertEqual(order.product_qty, 2)
def test_keep_qty_on_replenishment(self):
existing_mos = self.env["mrp.production"].search([])
self._replenish_product(self.product_template.product_variant_id, product_qty=1)
created_mos = self.env["mrp.production"].search(
[("id", "not in", existing_mos.ids)]
)
self.assertEqual(len(created_mos), 1)
self.assertEqual(created_mos.product_qty, self.bom.product_qty)
self.assertFalse(any("split" in m.body for m in created_mos.message_ids))
self.assertFalse(any("increased" in m.body for m in created_mos.message_ids))
def test_split_qty_on_replenishment(self):
existing_mos = self.env["mrp.production"].search([])
self._replenish_product(self.product_template.product_variant_id, product_qty=3)
created_mos = self.env["mrp.production"].search(
[("id", "not in", existing_mos.ids)]
)
self.assertEqual(len(created_mos), 3)
for mo in created_mos:
self.assertEqual(mo.product_qty, self.bom.product_qty)
self.assertTrue(any("split" in m.body for m in mo.message_ids))
def test_raise_qty_on_replenishment(self):
existing_mos = self.env["mrp.production"].search([])
self.bom.product_qty = 5
self._replenish_product(self.product_template.product_variant_id, product_qty=3)
created_mos = self.env["mrp.production"].search(
[("id", "not in", existing_mos.ids)]
)
self.assertEqual(len(created_mos), 1)
self.assertEqual(created_mos.product_qty, self.bom.product_qty)
self.assertTrue(any("increased" in m.body for m in created_mos.message_ids))

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2022 Camptocamp SA
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="mrp_bom_form_view" model="ir.ui.view">
<field name="name">mrp.bom.form.inherit</field>
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.mrp_bom_form_view" />
<field name="arch" type="xml">
<field name="type" position="after">
<field
name="mo_auto_validation"
attrs="{'invisible': [('type', '!=', 'normal')]}"
/>
<field
name="mo_auto_validation_warning"
nolabel="1"
colspan="2"
class="alert alert-warning"
role="alert"
attrs="{'invisible': ['|', ('type', '!=', 'normal'), ('mo_auto_validation', '=', False)]}"
/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2022 Camptocamp SA
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="mrp_production_form_view" model="ir.ui.view">
<field name="name">mrp.production.form.inherit</field>
<field name="model">mrp.production</field>
<field name="inherit_id" ref="mrp.mrp_production_form_view" />
<field name="arch" type="xml">
<div name="button_box" position="before">
<field name="auto_validate" invisible="1" />
</div>
</field>
</record>
</odoo>

View File

@@ -0,0 +1 @@
from . import mrp_production_backorder

View File

@@ -0,0 +1,14 @@
# Copyright 2022 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo import models
class MrpProductionBackorder(models.TransientModel):
_inherit = "mrp.production.backorder"
def action_backorder(self):
# Bypass the 'auto_validate' constraint regarding qty to produce
# when creating a backorder.
self = self.with_context(disable_check_mo_auto_validate=True)
return super().action_backorder()

View File

@@ -0,0 +1 @@
../../../../mrp_production_auto_validate

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)