mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
1
mrp_production_split/README.rst
Normal file
1
mrp_production_split/README.rst
Normal file
@@ -0,0 +1 @@
|
||||
# TO BE GENERATED
|
||||
2
mrp_production_split/__init__.py
Normal file
2
mrp_production_split/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizards
|
||||
21
mrp_production_split/__manifest__.py
Normal file
21
mrp_production_split/__manifest__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright 2023 Camptocamp SA (https://www.camptocamp.com).
|
||||
# @author Iván Todorovich <ivan.todorovich@camptocamp.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "MRP Production Split",
|
||||
"summary": "Split Manufacturing Orders into smaller ones",
|
||||
"version": "15.0.1.0.0",
|
||||
"author": "Camptocamp, Odoo Community Association (OCA)",
|
||||
"maintainers": ["ivantodorovich"],
|
||||
"website": "https://github.com/OCA/manufacture",
|
||||
"license": "AGPL-3",
|
||||
"category": "Manufacturing",
|
||||
"depends": ["mrp"],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"templates/messages.xml",
|
||||
"views/mrp_production.xml",
|
||||
"wizards/mrp_production_split_wizard.xml",
|
||||
],
|
||||
}
|
||||
1
mrp_production_split/models/__init__.py
Normal file
1
mrp_production_split/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import mrp_production
|
||||
34
mrp_production_split/models/mrp_production.py
Normal file
34
mrp_production_split/models/mrp_production.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Copyright 2023 Camptocamp SA (https://www.camptocamp.com).
|
||||
# @author Iván Todorovich <ivan.todorovich@camptocamp.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_inherit = "mrp.production"
|
||||
|
||||
def copy_data(self, default=None):
|
||||
# OVERRIDE copy the date_planned_start and date_planned_end when splitting
|
||||
# productions, as they are not copied by default (copy=False).
|
||||
[data] = super().copy_data(default=default)
|
||||
data.setdefault("date_planned_start", self.date_planned_start)
|
||||
data.setdefault("date_planned_finished", self.date_planned_finished)
|
||||
return [data]
|
||||
|
||||
def action_split(self):
|
||||
self.ensure_one()
|
||||
self._check_company()
|
||||
if self.state in ("draft", "done", "to_close", "cancel"):
|
||||
raise UserError(
|
||||
_(
|
||||
"Cannot split a manufacturing order that is in '%s' state.",
|
||||
self._fields["state"].convert_to_export(self.state, self),
|
||||
)
|
||||
)
|
||||
action = self.env["ir.actions.actions"]._for_xml_id(
|
||||
"mrp_production_split.action_mrp_production_split_wizard"
|
||||
)
|
||||
action["context"] = {"default_production_id": self.id}
|
||||
return action
|
||||
3
mrp_production_split/readme/CONTRIBUTORS.rst
Normal file
3
mrp_production_split/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1,3 @@
|
||||
* `Camptocamp <https://www.camptocamp.com>`_
|
||||
|
||||
* Iván Todorovich <ivan.todorovich@camptocamp.com>
|
||||
12
mrp_production_split/readme/DESCRIPTION.rst
Normal file
12
mrp_production_split/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
This module adds a "Split" button to Manufacturing Orders.
|
||||
|
||||
Manufacturing Orders can be split as long as they haven't been completed yet.
|
||||
|
||||
For products tracked by "Serial Number", it allows to choose the Quantity to extract
|
||||
from the original MO, and it'll create one MO per single unit.
|
||||
|
||||
For other products, more options are available, that will let you do things like:
|
||||
|
||||
* Extract 10 units from a MO into 5 MOs of 2 units each.
|
||||
* Extract 10 units from a MO into a single new MOs.
|
||||
* Extract 10 units from a MO into multiple MOs of different quantities.
|
||||
5
mrp_production_split/readme/USAGE.rst
Normal file
5
mrp_production_split/readme/USAGE.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
#. Create a Manufacturing Order.
|
||||
#. Confirm it.
|
||||
#. Click on the "Split" button.
|
||||
#. Choose the desired split options.
|
||||
#. Confirm.
|
||||
2
mrp_production_split/security/ir.model.access.csv
Normal file
2
mrp_production_split/security/ir.model.access.csv
Normal file
@@ -0,0 +1,2 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_mrp_production_split_wizard,access.mrp.production.split.wizard,model_mrp_production_split_wizard,mrp.group_mrp_user,1,1,1,1
|
||||
|
28
mrp_production_split/templates/messages.xml
Normal file
28
mrp_production_split/templates/messages.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2023 Camptocamp SA (https://www.camptocamp.com).
|
||||
@author Iván Todorovich <ivan.todorovich@camptocamp.com>
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<template id="message_order_split">
|
||||
<p>
|
||||
The following <t
|
||||
t-esc="self.env['ir.model']._get(records._name).name.lower()"
|
||||
/>(s)
|
||||
have been created as a result of a <strong>split</strong> operation:
|
||||
</p>
|
||||
<ul>
|
||||
<li t-foreach="records" t-as="o">
|
||||
<a
|
||||
href="#"
|
||||
t-att-data-oe-model="o._name"
|
||||
t-att-data-oe-id="o.id"
|
||||
t-esc="o.display_name"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
1
mrp_production_split/tests/__init__.py
Normal file
1
mrp_production_split/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_mrp_production_split
|
||||
92
mrp_production_split/tests/common.py
Normal file
92
mrp_production_split/tests/common.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# Copyright 2023 Camptocamp SA (https://www.camptocamp.com).
|
||||
# @author Iván Todorovich <ivan.todorovich@camptocamp.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import Form, TransactionCase
|
||||
|
||||
|
||||
class CommonCase(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
|
||||
# Create bom, product and components
|
||||
cls.component = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Component",
|
||||
"detailed_type": "product",
|
||||
}
|
||||
)
|
||||
cls.product = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Product",
|
||||
"detailed_type": "product",
|
||||
"tracking": "lot",
|
||||
}
|
||||
)
|
||||
cls.product_bom = cls.env["mrp.bom"].create(
|
||||
{
|
||||
"product_tmpl_id": cls.product.product_tmpl_id.id,
|
||||
"product_qty": 1.0,
|
||||
"product_uom_id": cls.product.uom_id.id,
|
||||
"bom_line_ids": [
|
||||
Command.create(
|
||||
{
|
||||
"product_id": cls.component.id,
|
||||
"product_qty": 1.0,
|
||||
"product_uom_id": cls.component.uom_id.id,
|
||||
}
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
# Create some initial stocks
|
||||
cls.location_stock = cls.env.ref("stock.stock_location_stock")
|
||||
cls.env["stock.quant"].create(
|
||||
{
|
||||
"product_id": cls.component.id,
|
||||
"product_uom_id": cls.component.uom_id.id,
|
||||
"location_id": cls.location_stock.id,
|
||||
"quantity": 10.00,
|
||||
}
|
||||
)
|
||||
# Create the MO
|
||||
cls.production = cls._create_mrp_production(
|
||||
product=cls.product,
|
||||
bom=cls.product_bom,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _create_mrp_production(
|
||||
cls, product=None, bom=None, quantity=5.0, confirm=False
|
||||
):
|
||||
if product is None: # pragma: no cover
|
||||
product = cls.product
|
||||
if bom is None: # pragma: no cover
|
||||
bom = cls.product_bom
|
||||
mo_form = Form(cls.env["mrp.production"])
|
||||
mo_form.product_id = product
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_qty = quantity
|
||||
mo_form.product_uom_id = product.uom_id
|
||||
mo = mo_form.save()
|
||||
if confirm: # pragma: no cover
|
||||
mo.action_confirm()
|
||||
return mo
|
||||
|
||||
def _mrp_production_set_quantity_done(self, order):
|
||||
for line in order.move_raw_ids.move_line_ids:
|
||||
line.qty_done = line.product_uom_qty
|
||||
order.move_raw_ids._recompute_state()
|
||||
order.qty_producing = order.product_qty
|
||||
|
||||
def _mrp_production_split(self, order, **vals):
|
||||
action = order.action_split()
|
||||
Wizard = self.env[action["res_model"]]
|
||||
Wizard = Wizard.with_context(active_model=order._name, active_id=order.id)
|
||||
Wizard = Wizard.with_context(**action["context"])
|
||||
wizard = Wizard.create(vals)
|
||||
res = wizard.apply()
|
||||
records = self.env[res["res_model"]].search(res["domain"])
|
||||
return records
|
||||
178
mrp_production_split/tests/test_mrp_production_split.py
Normal file
178
mrp_production_split/tests/test_mrp_production_split.py
Normal file
@@ -0,0 +1,178 @@
|
||||
# Copyright 2023 Camptocamp SA (https://www.camptocamp.com).
|
||||
# @author Iván Todorovich <ivan.todorovich@camptocamp.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from .common import CommonCase
|
||||
|
||||
|
||||
class TestMrpProductionSplit(CommonCase):
|
||||
def test_mrp_production_split_draft(self):
|
||||
with self.assertRaisesRegex(UserError, r"Cannot split.*"):
|
||||
self._mrp_production_split(self.production)
|
||||
|
||||
def test_mrp_production_split_done(self):
|
||||
self.production.action_confirm()
|
||||
self.production.action_generate_serial()
|
||||
self._mrp_production_set_quantity_done(self.production)
|
||||
self.production.button_mark_done()
|
||||
with self.assertRaisesRegex(UserError, r"Cannot split.*"):
|
||||
self._mrp_production_split(self.production)
|
||||
|
||||
def test_mrp_production_split_cancel(self):
|
||||
self.production.action_cancel()
|
||||
with self.assertRaisesRegex(UserError, r"Cannot split.*"):
|
||||
self._mrp_production_split(self.production)
|
||||
|
||||
def test_mrp_production_split_lot_simple(self):
|
||||
self.production.action_confirm()
|
||||
self.production.action_generate_serial()
|
||||
mos = self._mrp_production_split(self.production, split_qty=2.0)
|
||||
self.assertRecordValues(mos, [dict(product_qty=3.0), dict(product_qty=2.0)])
|
||||
|
||||
def test_mrp_production_split_lot_simple_copy_date_planned(self):
|
||||
dt_start = datetime.now() + timedelta(days=5)
|
||||
dt_finished = dt_start + timedelta(hours=1)
|
||||
self.production.date_planned_start = dt_start
|
||||
self.production.date_planned_finished = dt_finished
|
||||
self.production.action_confirm()
|
||||
self.production.action_generate_serial()
|
||||
mos = self._mrp_production_split(self.production, split_qty=2.0)
|
||||
self.assertRecordValues(
|
||||
mos,
|
||||
[
|
||||
dict(
|
||||
product_qty=3.0,
|
||||
date_planned_start=dt_start,
|
||||
date_planned_finished=dt_finished,
|
||||
),
|
||||
dict(
|
||||
product_qty=2.0,
|
||||
date_planned_start=dt_start,
|
||||
date_planned_finished=dt_finished,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def test_mrp_production_split_lot_simple_zero_qty(self):
|
||||
self.production.action_confirm()
|
||||
self.production.action_generate_serial()
|
||||
with self.assertRaisesRegex(UserError, r"Nothing to split.*"):
|
||||
self._mrp_production_split(self.production, split_qty=0.0)
|
||||
|
||||
def test_mrp_production_split_lot_simple_with_qty_producing_exceeded(self):
|
||||
self.production.action_confirm()
|
||||
self.production.action_generate_serial()
|
||||
self.production.qty_producing = 3.0
|
||||
with self.assertRaisesRegex(UserError, r"You can't split.*"):
|
||||
self._mrp_production_split(self.production, split_qty=4.0)
|
||||
|
||||
def test_mrp_production_split_lot_equal(self):
|
||||
self.production.action_confirm()
|
||||
self.production.action_generate_serial()
|
||||
mos = self._mrp_production_split(
|
||||
self.production,
|
||||
split_mode="equal",
|
||||
split_qty=4.0,
|
||||
split_equal_qty=2.0,
|
||||
)
|
||||
self.assertRecordValues(
|
||||
mos,
|
||||
[
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=2.0),
|
||||
dict(product_qty=2.0),
|
||||
],
|
||||
)
|
||||
|
||||
def test_mrp_production_split_lot_custom(self):
|
||||
self.production.action_confirm()
|
||||
self.production.action_generate_serial()
|
||||
mos = self._mrp_production_split(
|
||||
self.production,
|
||||
split_mode="custom",
|
||||
custom_quantities="1 2 1 1",
|
||||
)
|
||||
self.assertRecordValues(
|
||||
mos,
|
||||
[
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=2.0),
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=1.0),
|
||||
],
|
||||
)
|
||||
|
||||
def test_mrp_production_split_lot_custom_incomplete(self):
|
||||
self.production.action_confirm()
|
||||
self.production.action_generate_serial()
|
||||
mos = self._mrp_production_split(
|
||||
self.production,
|
||||
split_mode="custom",
|
||||
custom_quantities="1 2",
|
||||
)
|
||||
self.assertRecordValues(
|
||||
mos,
|
||||
[
|
||||
dict(product_qty=2.0),
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=2.0),
|
||||
],
|
||||
)
|
||||
|
||||
def test_mrp_production_split_lot_custom_float(self):
|
||||
self.production.action_confirm()
|
||||
self.production.action_generate_serial()
|
||||
mos = self._mrp_production_split(
|
||||
self.production,
|
||||
split_mode="custom",
|
||||
custom_quantities="1.0 2.0 1.0 1.0",
|
||||
)
|
||||
self.assertRecordValues(
|
||||
mos,
|
||||
[
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=2.0),
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=1.0),
|
||||
],
|
||||
)
|
||||
|
||||
def test_mrp_production_split_lot_custom_float_locale(self):
|
||||
lang = self.env["res.lang"]._lang_get(self.env.user.lang)
|
||||
lang.decimal_point = ","
|
||||
lang.thousands_sep = ""
|
||||
self.production.action_confirm()
|
||||
self.production.action_generate_serial()
|
||||
mos = self._mrp_production_split(
|
||||
self.production,
|
||||
split_mode="custom",
|
||||
custom_quantities="1,0 2,0 1,0 1,0",
|
||||
)
|
||||
self.assertRecordValues(
|
||||
mos,
|
||||
[
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=2.0),
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=1.0),
|
||||
],
|
||||
)
|
||||
|
||||
def test_mrp_production_split_serial(self):
|
||||
self.product.tracking = "serial"
|
||||
self.production.action_confirm()
|
||||
mos = self._mrp_production_split(self.production)
|
||||
self.assertRecordValues(
|
||||
mos,
|
||||
[
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=1.0),
|
||||
dict(product_qty=1.0),
|
||||
],
|
||||
)
|
||||
30
mrp_production_split/views/mrp_production.xml
Normal file
30
mrp_production_split/views/mrp_production.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2023 Camptocamp SA (https://www.camptocamp.com).
|
||||
@author Iván Todorovich <ivan.todorovich@camptocamp.com>
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="mrp_production_form_view" model="ir.ui.view">
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_production_form_view" />
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_serial_mass_produce_wizard" position="before">
|
||||
<button
|
||||
name="action_split"
|
||||
string="Split"
|
||||
type="object"
|
||||
attrs="{
|
||||
'invisible': [
|
||||
'|',
|
||||
('state', 'in', ('draft', 'done', 'to_close', 'cancel')),
|
||||
('product_qty', '<=', 1),
|
||||
],
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
1
mrp_production_split/wizards/__init__.py
Normal file
1
mrp_production_split/wizards/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import mrp_production_split_wizard
|
||||
167
mrp_production_split/wizards/mrp_production_split_wizard.py
Normal file
167
mrp_production_split/wizards/mrp_production_split_wizard.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# Copyright 2023 Camptocamp SA (https://www.camptocamp.com).
|
||||
# @author Iván Todorovich <ivan.todorovich@camptocamp.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from typing import List, Union
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class MrpProductionSplitWizard(models.TransientModel):
|
||||
_name = "mrp.production.split.wizard"
|
||||
|
||||
production_id = fields.Many2one(
|
||||
"mrp.production",
|
||||
"Production",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
split_mode = fields.Selection(
|
||||
[
|
||||
("simple", "Extract a quantity from the original MO"),
|
||||
("equal", "Extract a quantity into several MOs with equal quantities"),
|
||||
("custom", "Custom"),
|
||||
],
|
||||
required=True,
|
||||
default="simple",
|
||||
)
|
||||
split_qty = fields.Float(
|
||||
string="Quantity",
|
||||
digits="Product Unit of Measure",
|
||||
help="Total quantity to extract from the original MO.",
|
||||
)
|
||||
split_equal_qty = fields.Float(
|
||||
string="Equal Quantity",
|
||||
digits="Product Unit of Measure",
|
||||
help="Used to split the MO into several MOs with equal quantities.",
|
||||
default=1,
|
||||
)
|
||||
custom_quantities = fields.Char(
|
||||
string="Split Quantities",
|
||||
help="Space separated list of quantities to split:\n"
|
||||
"e.g. '3 2 5' will result in 3 MOs with 3, 2 and 5 units respectively.\n"
|
||||
"If the sum of the quantities is less than the original MO's quantity, the "
|
||||
"remaining quantity will remain in the original MO.",
|
||||
)
|
||||
product_tracking = fields.Selection(related="production_id.product_id.tracking")
|
||||
product_uom_id = fields.Many2one(related="production_id.product_uom_id")
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
active_model = self.env.context.get("active_model")
|
||||
active_id = self.env.context.get("active_id")
|
||||
# Auto-complete production_id from context
|
||||
if "production_id" in fields_list and active_model == "mrp.production":
|
||||
res["production_id"] = active_id
|
||||
# Auto-complete split_mode from production_id
|
||||
if "split_mode" in fields_list and res.get("production_id"):
|
||||
production = self.env["mrp.production"].browse(res["production_id"])
|
||||
if production.product_tracking == "serial":
|
||||
res["split_mode"] = "equal"
|
||||
# Auto-complete split_qty from production_id
|
||||
if "split_qty" in fields_list and res.get("production_id"):
|
||||
production = self.env["mrp.production"].browse(res["production_id"])
|
||||
res["split_qty"] = production._get_quantity_to_backorder()
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _parse_float(self, value: Union[float, int, str]) -> float:
|
||||
"""Parse a float number from a string, with the user's language settings."""
|
||||
if isinstance(value, (float, int)): # pragma: no cover
|
||||
return float(value)
|
||||
lang = self.env["res.lang"]._lang_get(self.env.user.lang)
|
||||
try:
|
||||
return float(
|
||||
value.replace(lang.thousands_sep, "")
|
||||
.replace(lang.decimal_point, ".")
|
||||
.strip()
|
||||
)
|
||||
except ValueError as e: # pragma: no cover
|
||||
raise UserError(_("%s is not a number.", value)) from e
|
||||
|
||||
@api.model
|
||||
def _parse_float_list(self, value: str) -> List[float]:
|
||||
"""Parse a list of float numbers from a string."""
|
||||
return [self._parse_float(v) for v in value.split()]
|
||||
|
||||
@api.onchange("custom_quantities")
|
||||
def _onchange_custom_quantities_check(self):
|
||||
"""Check that the custom quantities are valid."""
|
||||
if self.custom_quantities: # pragma: no cover
|
||||
try:
|
||||
self._parse_float_list(self.custom_quantities)
|
||||
except UserError:
|
||||
return {
|
||||
"warning": {
|
||||
"title": _("Invalid quantities"),
|
||||
"message": _("Please enter a space separated list of numbers."),
|
||||
}
|
||||
}
|
||||
|
||||
def _get_split_quantities(self) -> List[float]:
|
||||
"""Return the quantities to split, according to the settings."""
|
||||
production = self.production_id
|
||||
rounding = production.product_uom_id.rounding
|
||||
if self.split_mode == "simple":
|
||||
if (
|
||||
fields.Float.compare(
|
||||
self.split_qty, production._get_quantity_to_backorder(), rounding
|
||||
)
|
||||
> 0
|
||||
):
|
||||
raise UserError(_("You can't split quantities already in production."))
|
||||
if fields.Float.is_zero(self.split_qty, precision_rounding=rounding):
|
||||
raise UserError(_("Nothing to split."))
|
||||
return [production.product_qty - self.split_qty, self.split_qty]
|
||||
elif self.split_mode == "equal":
|
||||
split_total = min(production._get_quantity_to_backorder(), self.split_qty)
|
||||
split_count = int(split_total // self.split_equal_qty)
|
||||
split_rest = production.product_qty - split_total
|
||||
split_rest += split_total % self.split_equal_qty
|
||||
quantities = [self.split_equal_qty] * split_count
|
||||
if not fields.Float.is_zero(split_rest, precision_rounding=rounding):
|
||||
quantities = [split_rest] + quantities
|
||||
return quantities
|
||||
elif self.split_mode == "custom":
|
||||
quantities = self._parse_float_list(self.custom_quantities)
|
||||
split_total = sum(quantities)
|
||||
split_rest = production.product_qty - split_total
|
||||
if not fields.Float.is_zero(split_rest, precision_rounding=rounding):
|
||||
quantities = [split_rest] + quantities
|
||||
return quantities
|
||||
else: # pragma: no cover
|
||||
raise UserError(_("Invalid Split Mode: '%s'", self.split_mode))
|
||||
|
||||
def _apply(self):
|
||||
self.ensure_one()
|
||||
records = self.production_id.with_context(
|
||||
copy_date_planned=True
|
||||
)._split_productions(
|
||||
amounts={self.production_id: self._get_split_quantities()},
|
||||
cancel_remaning_qty=False,
|
||||
set_consumed_qty=False,
|
||||
)
|
||||
new_records = records - self.production_id
|
||||
for record in new_records:
|
||||
record.message_post_with_view(
|
||||
"mail.message_origin_link",
|
||||
values=dict(self=record, origin=self.production_id),
|
||||
message_log=True,
|
||||
)
|
||||
if new_records:
|
||||
self.production_id.message_post_with_view(
|
||||
"mrp_production_split.message_order_split",
|
||||
values=dict(self=self.production_id, records=new_records),
|
||||
message_log=True,
|
||||
)
|
||||
return records
|
||||
|
||||
def apply(self):
|
||||
records = self._apply()
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"mrp.mrp_production_action"
|
||||
)
|
||||
action["domain"] = [("id", "in", records.ids)]
|
||||
return action
|
||||
57
mrp_production_split/wizards/mrp_production_split_wizard.xml
Normal file
57
mrp_production_split/wizards/mrp_production_split_wizard.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2023 Camptocamp SA (https://www.camptocamp.com).
|
||||
@author Iván Todorovich <ivan.todorovich@camptocamp.com>
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_mrp_production_split_wizard_form" model="ir.ui.view">
|
||||
<field name="model">mrp.production.split.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="production_id" invisible="1" />
|
||||
<field name="product_tracking" invisible="1" />
|
||||
<field
|
||||
name="split_mode"
|
||||
widget="radio"
|
||||
attrs="{'invisible': [('product_tracking', '=', 'serial')]}"
|
||||
/>
|
||||
<field
|
||||
name="split_qty"
|
||||
attrs="{'invisible': [('split_mode', 'not in', ('simple', 'equal'))]}"
|
||||
/>
|
||||
<field
|
||||
name="split_equal_qty"
|
||||
string="In orders of"
|
||||
attrs="{'invisible': ['|', ('product_tracking', '=', 'serial'), ('split_mode', '!=', 'equal')]}"
|
||||
/>
|
||||
<field
|
||||
name="custom_quantities"
|
||||
attrs="{'invisible': [('split_mode', '!=', 'custom')]}"
|
||||
/>
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
name="apply"
|
||||
type="object"
|
||||
data-hotkey="q"
|
||||
string="Apply"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button special="cancel" data-hotkey="z" string="Cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mrp_production_split_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Split Manufacturing Order</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">mrp.production.split.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
1
setup/mrp_production_split/odoo/addons/mrp_production_split
Symbolic link
1
setup/mrp_production_split/odoo/addons/mrp_production_split
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../mrp_production_split
|
||||
6
setup/mrp_production_split/setup.py
Normal file
6
setup/mrp_production_split/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
Reference in New Issue
Block a user