diff --git a/mrp_request_workcenter_cycle/__init__.py b/mrp_request_workcenter_cycle/__init__.py new file mode 100644 index 000000000..aee8895e7 --- /dev/null +++ b/mrp_request_workcenter_cycle/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/mrp_request_workcenter_cycle/__manifest__.py b/mrp_request_workcenter_cycle/__manifest__.py new file mode 100644 index 000000000..6a6a6de52 --- /dev/null +++ b/mrp_request_workcenter_cycle/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright 2020 David BEAL @ Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "MRP Request Workcenter Cycle", + "version": "12.0.1.0.0", + "category": "Manufacturing", + "license": "AGPL-3", + "author": "Akretion, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/manufacture/tree/12.0" + "/mrp_request_workcenter_cycle", + "depends": [ + "mrp_production_request", + ], + "development_status": "Alpha", + "data": [ + "security/ir.model.access.csv", + "views/product.xml", + "views/request.xml", + ], + "demo": [ + "demo/workcenter.xml", + "demo/request.xml", + ], + "maintainers": ["bealdav"], + "installable": True, +} diff --git a/mrp_request_workcenter_cycle/demo/request.xml b/mrp_request_workcenter_cycle/demo/request.xml new file mode 100644 index 000000000..db09b039a --- /dev/null +++ b/mrp_request_workcenter_cycle/demo/request.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/mrp_request_workcenter_cycle/demo/workcenter.xml b/mrp_request_workcenter_cycle/demo/workcenter.xml new file mode 100644 index 000000000..a7c13985b --- /dev/null +++ b/mrp_request_workcenter_cycle/demo/workcenter.xml @@ -0,0 +1,24 @@ + + + + + Oven 1 + + + Oven 2 + + + + + + 110 + 1 + + + + + 80 + 2 + + + diff --git a/mrp_request_workcenter_cycle/models/__init__.py b/mrp_request_workcenter_cycle/models/__init__.py new file mode 100644 index 000000000..bf701982f --- /dev/null +++ b/mrp_request_workcenter_cycle/models/__init__.py @@ -0,0 +1,4 @@ +from . import product +from . import product_workcenter_quantity +from . import request +from . import request_workcenter diff --git a/mrp_request_workcenter_cycle/models/product.py b/mrp_request_workcenter_cycle/models/product.py new file mode 100644 index 000000000..3673c70ff --- /dev/null +++ b/mrp_request_workcenter_cycle/models/product.py @@ -0,0 +1,16 @@ +# Copyright 2020 David BEAL @ Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from odoo import fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + qty_by_workcenter_ids = fields.One2many( + comodel_name="product.workcenter.quantity", + inverse_name="product_id", + groups="mrp_production_request.group_mrp_production_request_user", + string="Capacity by workcenter", + ) diff --git a/mrp_request_workcenter_cycle/models/product_workcenter_quantity.py b/mrp_request_workcenter_cycle/models/product_workcenter_quantity.py new file mode 100644 index 000000000..62012428a --- /dev/null +++ b/mrp_request_workcenter_cycle/models/product_workcenter_quantity.py @@ -0,0 +1,30 @@ +# Copyright 2020 David BEAL @ Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from odoo import fields, models + + +class ProductWorkcenterQuantity(models.Model): + _name = "product.workcenter.quantity" + _description = "Quantity that can be produced with a workcenter cycle" + + workcenter_id = fields.Many2one(comodel_name="mrp.workcenter", string="Workcenter") + product_id = fields.Many2one(comodel_name="product.product", string="Product") + product_qty = fields.Float( + default=1, help="Quantity of the product that the workcenter use in a cycle." + ) + workcenter_cycle_no = fields.Float( + string="Cycle Number", + default=1, + help="Default number of cycles for the workcenter set for " + "Manufacturing Request of this product.\n", + ) + + _sql_constraints = [ + ( + "product_workcenter_unique", + "UNIQUE(workcenter_id,product_id)", + "Workcenter field must be unique by product", + ) + ] diff --git a/mrp_request_workcenter_cycle/models/request.py b/mrp_request_workcenter_cycle/models/request.py new file mode 100644 index 000000000..d1a3a3440 --- /dev/null +++ b/mrp_request_workcenter_cycle/models/request.py @@ -0,0 +1,120 @@ +# Copyright 2020 David BEAL @ Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from math import floor + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +import odoo.addons.decimal_precision as dp + + +class MrpProductionRequest(models.Model): + _inherit = "mrp.production.request" + + qty_by_workcenter_ids = fields.One2many( + comodel_name="mrp.request.workcenter", + inverse_name="request_id", + string="Workcenters cycles number", + ) + workcenter_lines_count = fields.Integer( + compute="_compute_workcenter_lines_count", + help="Technical field: size of qty_by_workcenter_ids field", + ) + auto_product_qty = fields.Float( + compute="_compute_auto_product_qty", + store=True, + compute_sudo=True, + digits=dp.get_precision("Product Unit of Measure"), + ) + target_quantity = fields.Float( + digits=dp.get_precision("Product Unit of Measure"), + help="This is your initial quantity defined before any cycle adjustement.", + ) + + @api.depends("qty_by_workcenter_ids") + def _compute_auto_product_qty(self): + for rec in self: + if rec.qty_by_workcenter_ids: + if rec.product_uom_id and rec.product_uom_id != rec.product_id.uom_id: + raise UserError( + _( + "Computing quantity with different units is " + "not supported for now." + ) + ) + qty = 0 + for qty_by in rec.qty_by_workcenter_ids: + qty += qty_by.product_qty * qty_by.workcenter_cycle_no + rec.auto_product_qty = qty + # need sudo to write in a not computed field in computed method + rec.sudo().write({"product_qty": qty}) + else: + rec.auto_product_qty = 0 + + @api.onchange("product_id") + def _onchange_product_id(self): + super()._onchange_product_id() + self.populate_qty_by_workcenter() + + @api.onchange("target_quantity") + def _onchange_target_quantity(self): + self.populate_qty_by_workcenter() + + def populate_qty_by_workcenter(self): + for rec in self: + cycle_factor = 1 + if rec.target_quantity and rec.product_id.qty_by_workcenter_ids: + # this factor allow us to compute the total quantity + # allocated by machine + cycle_factor = rec.target_quantity / sum( + x.product_qty * x.workcenter_cycle_no + for x in rec.product_id.qty_by_workcenter_ids + ) + rec.qty_by_workcenter_ids = [(5, 0, 0)] + rec.qty_by_workcenter_ids = [ + ( + 0, + 0, + { + "workcenter_cycle_no": x.workcenter_cycle_no * cycle_factor, + "workcenter_id": x.workcenter_id.id, + "request_id": rec.id, + "product_qty": x.product_qty, + }, + ) + for x in rec.product_id.qty_by_workcenter_ids + ] + + def _compute_workcenter_lines_count(self): + for rec in self: + rec.workcenter_lines_count = len(rec.qty_by_workcenter_ids) + + def button_create_mo_by_workcenter(self): + for rec in self: + if rec.mrp_production_ids: + raise UserError(_("MO already exists")) + if rec.qty_by_workcenter_ids: + for qty_by in rec.qty_by_workcenter_ids: + qty = floor(qty_by.workcenter_cycle_no) + while qty: + mo_qty = qty_by.product_qty + self._get_mo_from_request(mo_qty, qty_by.workcenter_id) + qty -= 1 + rest = qty_by.workcenter_cycle_no - floor( + qty_by.workcenter_cycle_no + ) + if rest: + mo_qty = qty_by.product_qty * rest + self._get_mo_from_request(mo_qty, qty_by.workcenter_id) + + @api.model + def _get_mo_from_request(self, mo_qty, workcenter): + wiz = ( + self.env["mrp.production.request.create.mo"] + .with_context(active_ids=[self.id], active_model="mrp.production.request") + .create({}) + ) + wiz.compute_product_line_ids() + wiz.mo_qty = mo_qty + wiz.product_uom_id = self.product_id.uom_id.id + wiz.with_context(workcenter=workcenter.name).create_mo() diff --git a/mrp_request_workcenter_cycle/models/request_workcenter.py b/mrp_request_workcenter_cycle/models/request_workcenter.py new file mode 100644 index 000000000..dc5ffdd36 --- /dev/null +++ b/mrp_request_workcenter_cycle/models/request_workcenter.py @@ -0,0 +1,32 @@ +# Copyright 2020 David BEAL @ Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class MrpRequestWorkcenter(models.Model): + _name = "mrp.request.workcenter" + _description = "Workcenters attached to Production Request" + + request_id = fields.Many2one( + comodel_name="mrp.production.request", string="Production Request" + ) + workcenter_id = fields.Many2one( + comodel_name="mrp.workcenter", string="Workcenter", required=True + ) + workcenter_cycle_no = fields.Float( + string="Cycles", + default="1", + help="A cycle is complete ended process for the machine.\n" + "Multiple cycles are several machines of the same kind in parallel " + "or the same machine used several times.", + ) + product_qty = fields.Float(string="Products by cycle") + + _sql_constraints = [ + ( + "request_workcenter_unique", + "UNIQUE(workcenter_id,request_id)", + "Workcenter field must be unique by manufacturing request", + ) + ] diff --git a/mrp_request_workcenter_cycle/readme/CONFIGURE.rst b/mrp_request_workcenter_cycle/readme/CONFIGURE.rst new file mode 100644 index 000000000..8d1500537 --- /dev/null +++ b/mrp_request_workcenter_cycle/readme/CONFIGURE.rst @@ -0,0 +1,8 @@ +To configure a product to automatically compute Manufacturing Requests quantity you need to: + +#. Create some workcenters: go to Manufacturing settings, tick work order and click on Workcenter. +#. Go to the products variants. +#. Go to the *Inventory* tab. +#. Fill **Capacity by workcenter** section. + +.. figure:: ../static/description/settings.png diff --git a/mrp_request_workcenter_cycle/readme/CONTRIBUTORS.rst b/mrp_request_workcenter_cycle/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..1d990af2f --- /dev/null +++ b/mrp_request_workcenter_cycle/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +Akretion: + +* David Beal diff --git a/mrp_request_workcenter_cycle/readme/DESCRIPTION.rst b/mrp_request_workcenter_cycle/readme/DESCRIPTION.rst new file mode 100644 index 000000000..ee852f249 --- /dev/null +++ b/mrp_request_workcenter_cycle/readme/DESCRIPTION.rst @@ -0,0 +1,7 @@ +This module extends the functionality of Manufacturing Request (MR) to allow you +to: + +* compute MR quantity from workcenter capacity: this capacity is defined by product +* create manufacturing orders according to selected workcenters + +Note: this module use Workcenters without Work Orders. diff --git a/mrp_request_workcenter_cycle/readme/ROADMAP.rst b/mrp_request_workcenter_cycle/readme/ROADMAP.rst new file mode 100644 index 000000000..27ed0a3dc --- /dev/null +++ b/mrp_request_workcenter_cycle/readme/ROADMAP.rst @@ -0,0 +1 @@ +* Support heterogenous units between product and manufacturing request diff --git a/mrp_request_workcenter_cycle/readme/USAGE.rst b/mrp_request_workcenter_cycle/readme/USAGE.rst new file mode 100644 index 000000000..ad7825350 --- /dev/null +++ b/mrp_request_workcenter_cycle/readme/USAGE.rst @@ -0,0 +1,12 @@ +To use this module, you need to: + +#. Go to *Manufacturing > Manufacturing Requests*. +#. Create a manufacturing request using the product configured with workcenter (Wood Panel in demo data) +#. Fill Target Quantity field and save MR +#. Click on 'Request Approval' button +#. 'Approve' the Manufacturing Request +#. Click on 'Create Manufacturing Orders' button + +Check created MOs + +.. figure:: ../static/description/request.png diff --git a/mrp_request_workcenter_cycle/security/ir.model.access.csv b/mrp_request_workcenter_cycle/security/ir.model.access.csv new file mode 100644 index 000000000..6318e95c2 --- /dev/null +++ b/mrp_request_workcenter_cycle/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_prd_wkcenter_qty_user,product.workcenter.qty.user,model_product_workcenter_quantity,base.group_user,1,0,0,0 +access_prd_wkcenter_qty_manager,product.workcenter.qty.manager,model_product_workcenter_quantity,mrp_production_request.group_mrp_production_request_manager,1,1,1,1 +access_produc_request_wkcenter_qty_user,production.request.workcenter.user,model_mrp_request_workcenter,mrp_production_request.group_mrp_production_request_user,1,1,1,1 diff --git a/mrp_request_workcenter_cycle/static/description/request.png b/mrp_request_workcenter_cycle/static/description/request.png new file mode 100644 index 000000000..2cf0ccd8b Binary files /dev/null and b/mrp_request_workcenter_cycle/static/description/request.png differ diff --git a/mrp_request_workcenter_cycle/static/description/settings.png b/mrp_request_workcenter_cycle/static/description/settings.png new file mode 100644 index 000000000..5327d5e0c Binary files /dev/null and b/mrp_request_workcenter_cycle/static/description/settings.png differ diff --git a/mrp_request_workcenter_cycle/tests/__init__.py b/mrp_request_workcenter_cycle/tests/__init__.py new file mode 100644 index 000000000..cc656da64 --- /dev/null +++ b/mrp_request_workcenter_cycle/tests/__init__.py @@ -0,0 +1 @@ +from . import test_cycle diff --git a/mrp_request_workcenter_cycle/tests/test_cycle.py b/mrp_request_workcenter_cycle/tests/test_cycle.py new file mode 100644 index 000000000..3ffadd22c --- /dev/null +++ b/mrp_request_workcenter_cycle/tests/test_cycle.py @@ -0,0 +1,31 @@ +# copyright 2020 David BEAL @ Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests.common import SavepointCase + + +class TestMrpRequestCycle(SavepointCase): + def setUp(self, *args, **kwargs): + super().setUp(*args, **kwargs) + + def test_pool_of_mo(self): + product = self.env.ref("mrp.product_product_wood_panel") + request = self.env["mrp.production.request"].create( + { + "product_id": product.id, + "bom_id": self.env["mrp.bom"] + .search([("product_tmpl_id", "=", product.product_tmpl_id.id)], limit=1) + .id, + "name": "test", + } + ) + request.populate_qty_by_workcenter() + assert request.auto_product_qty == 270 + assert request.product_qty == 270 + request.button_to_approve() + request.button_approved() + request.button_create_mo_by_workcenter() + assert request.mrp_production_count == 3 + qties = [x.product_qty for x in request.mrp_production_ids] + qties.sort() + assert qties == [80, 80, 110] diff --git a/mrp_request_workcenter_cycle/views/product.xml b/mrp_request_workcenter_cycle/views/product.xml new file mode 100644 index 000000000..03497e0d9 --- /dev/null +++ b/mrp_request_workcenter_cycle/views/product.xml @@ -0,0 +1,24 @@ + + + + + product.product + + + + + + + + + + + + + + + + + + + diff --git a/mrp_request_workcenter_cycle/views/request.xml b/mrp_request_workcenter_cycle/views/request.xml new file mode 100644 index 000000000..242d4e7fe --- /dev/null +++ b/mrp_request_workcenter_cycle/views/request.xml @@ -0,0 +1,47 @@ + + + + + mrp.production.request + + + + {'invisible': ['|', '|', ('workcenter_lines_count', '>', 0), ('state', '!=', 'approved'), ('mrp_production_count', '>', 0)]} + False + oe_highlight + + + + + + + + + {'readonly': ['|', ('state', 'not in', 'draft'), ('workcenter_lines_count', '>', 0)]} + + + + + + + + + + + + + + + + One manufacturing order is created by cycle. + + + + + + + + diff --git a/mrp_request_workcenter_cycle/wizards/__init__.py b/mrp_request_workcenter_cycle/wizards/__init__.py new file mode 100644 index 000000000..442319f76 --- /dev/null +++ b/mrp_request_workcenter_cycle/wizards/__init__.py @@ -0,0 +1 @@ +from . import request_create_mo diff --git a/mrp_request_workcenter_cycle/wizards/request_create_mo.py b/mrp_request_workcenter_cycle/wizards/request_create_mo.py new file mode 100644 index 000000000..31f1669a3 --- /dev/null +++ b/mrp_request_workcenter_cycle/wizards/request_create_mo.py @@ -0,0 +1,16 @@ +# Copyright 2020 David BEAL @ Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class MrpProductionRequestCreateMo(models.TransientModel): + _inherit = "mrp.production.request.create.mo" + + def _prepare_manufacturing_order(self): + vals = super()._prepare_manufacturing_order() + vals["origin"] = "%s %s" % ( + vals.get("origin") or "", + self.env.context.get("workcenter", ""), + ) + return vals