From 67da2e9260e74afe0974760a63b6e737a1298d67 Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Mon, 4 Jun 2018 01:29:31 +0200 Subject: [PATCH] [IMP] mrp_production_grouped_by_product: Time frames Introduce time frames for grouping. --- mrp_production_grouped_by_product/README.rst | 70 +------------------ .../__manifest__.py | 1 + mrp_production_grouped_by_product/i18n/es.po | 63 +++++++++++++++++ .../models/__init__.py | 1 + .../models/mrp_production.py | 50 +++++++++++-- .../models/stock_picking_type.py | 37 ++++++++++ .../readme/CONFIGURE.rst | 17 +++++ .../readme/CONTRIBUTORS.rst | 4 ++ .../readme/DESCRIPTION.rst | 9 +++ .../readme/ROADMAP.rst | 1 + .../test_mrp_production_grouped_by_product.py | 65 ++++++++++------- .../views/stock_picking_type_views.xml | 17 +++++ 12 files changed, 236 insertions(+), 99 deletions(-) create mode 100644 mrp_production_grouped_by_product/i18n/es.po create mode 100644 mrp_production_grouped_by_product/models/stock_picking_type.py create mode 100644 mrp_production_grouped_by_product/readme/CONFIGURE.rst create mode 100644 mrp_production_grouped_by_product/readme/CONTRIBUTORS.rst create mode 100644 mrp_production_grouped_by_product/readme/DESCRIPTION.rst create mode 100644 mrp_production_grouped_by_product/readme/ROADMAP.rst create mode 100644 mrp_production_grouped_by_product/views/stock_picking_type_views.xml diff --git a/mrp_production_grouped_by_product/README.rst b/mrp_production_grouped_by_product/README.rst index 40d01b606..3f6262529 100644 --- a/mrp_production_grouped_by_product/README.rst +++ b/mrp_production_grouped_by_product/README.rst @@ -1,69 +1 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 - -============================= -Production Grouped By Product -============================= - -When you have several sales orders with make to (MTO) order products that -require to be manufactured, you end up with one manufacturing order for each of -these sales orders, which is very bad for the management. - -With this module, each time an MTO manufacturing order is required to be -created, it first checks that there's no other existing order not yet started -for the same product and bill of materials, and if there's one, then the -quantity of that order is increased instead of creating a new one. - -Usage -===== - -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/129/11.0 - -Known issues / Roadmap -====================== - -* Add a check in the product form for excluding it from being grouped. - -Bug Tracker -=========== - -Bugs are tracked on `GitHub Issues -`_. In case of trouble, -please check there if your issue has already been reported. If you spotted it -first, help us smash it by providing detailed and welcomed feedback. - -Credits -======= - -Images ------- - -* Odoo Community Association: `Icon `_. - -Contributors ------------- - -* Tecnativa _ - - * David Vidal - * Pedro M. Baeza - -Do not contact contributors directly about support or help with technical issues. - -Maintainer ----------- - -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -This module is maintained by the OCA. - -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. - -To contribute to this module, please visit https://odoo-community.org. +**This file is going to be generated by oca-gen-addon-readme.** diff --git a/mrp_production_grouped_by_product/__manifest__.py b/mrp_production_grouped_by_product/__manifest__.py index 78138907c..d1ecdd69a 100644 --- a/mrp_production_grouped_by_product/__manifest__.py +++ b/mrp_production_grouped_by_product/__manifest__.py @@ -14,6 +14,7 @@ 'mrp', ], 'data': [ + 'views/stock_picking_type_views.xml', ], 'installable': True, } diff --git a/mrp_production_grouped_by_product/i18n/es.po b/mrp_production_grouped_by_product/i18n/es.po new file mode 100644 index 000000000..e9c4e2b95 --- /dev/null +++ b/mrp_production_grouped_by_product/i18n/es.po @@ -0,0 +1,63 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mrp_production_grouped_by_product +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-06-03 23:16+0000\n" +"PO-Revision-Date: 2018-06-03 23:16+0000\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: mrp_production_grouped_by_product +#: model:ir.model.fields,field_description:mrp_production_grouped_by_product.field_stock_picking_type_mo_grouping_interval +msgid "MO grouping interval (days)" +msgstr "Intervalo de agrupación de OFs (días)" + +#. module: mrp_production_grouped_by_product +#: model:ir.model.fields,field_description:mrp_production_grouped_by_product.field_stock_picking_type_mo_grouping_max_hour +msgid "MO grouping max. hour (UTC)" +msgstr "Hora máx. agrupación OFs (UTC)" + +#. module: mrp_production_grouped_by_product +#: model:ir.model,name:mrp_production_grouped_by_product.model_mrp_production +msgid "Manufacturing Order" +msgstr "Orden de fabricación" + +#. module: mrp_production_grouped_by_product +#: model:ir.model,name:mrp_production_grouped_by_product.model_procurement_rule +msgid "Procurement Rule" +msgstr "Regla de abastecimiento" + +#. module: mrp_production_grouped_by_product +#: model:ir.model.fields,help:mrp_production_grouped_by_product.field_stock_picking_type_mo_grouping_max_hour +msgid "The maximum hour (between 0 and 23) for considering new manufacturing orders inside the same interval period, and thus being grouped on the same MO. IMPORTANT: The hour should be expressed in UTC." +msgstr "La hora máxima (entre 0 y 23) para considerar nuevas órdenes de fabricación dentro del mismo periodo de tiempo, y por tanto siendo agrupadas dentro de la misma OF. IMPORTANTE: La hora debe expresarse en UTC." + +#. module: mrp_production_grouped_by_product +#: model:ir.model.fields,help:mrp_production_grouped_by_product.field_stock_picking_type_mo_grouping_interval +msgid "The number of days for grouping together on the same manufacturing order." +msgstr "El número de días para agrupar juntas las órdenes de fabricación." + +#. module: mrp_production_grouped_by_product +#: model:ir.model,name:mrp_production_grouped_by_product.model_stock_picking_type +msgid "The operation type determines the picking view" +msgstr "El tipo de operación determina la vista de la operación" + +#. module: mrp_production_grouped_by_product +#: code:addons/mrp_production_grouped_by_product/models/stock_picking_type.py:36 +#, python-format +msgid "You have to enter a positive value for interval." +msgstr "Debe introducir un valor positivo para el intervalo." + +#. module: mrp_production_grouped_by_product +#: code:addons/mrp_production_grouped_by_product/models/stock_picking_type.py:29 +#, python-format +msgid "You have to enter a valid hour between 0 and 23." +msgstr "Debe introducir una hora válida entre 0 y 23." diff --git a/mrp_production_grouped_by_product/models/__init__.py b/mrp_production_grouped_by_product/models/__init__.py index 29b455422..b4b7e408a 100644 --- a/mrp_production_grouped_by_product/models/__init__.py +++ b/mrp_production_grouped_by_product/models/__init__.py @@ -1,3 +1,4 @@ from . import mrp_production from . import procurement +from . import stock_picking_type diff --git a/mrp_production_grouped_by_product/models/mrp_production.py b/mrp_production_grouped_by_product/models/mrp_production.py index f9572f142..5dd54a23c 100644 --- a/mrp_production_grouped_by_product/models/mrp_production.py +++ b/mrp_production_grouped_by_product/models/mrp_production.py @@ -2,7 +2,8 @@ # Copyright 2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, models +from dateutil.relativedelta import relativedelta +from odoo import api, fields, models from odoo.tools import config @@ -25,15 +26,54 @@ class MrpProduction(models.Model): self.move_finished_ids.move_dest_ids = vals['move_dest_ids'] self.write(new_vals) - def _find_grouping_target(self, vals): - mo = self.env['mrp.production'].search([ + def _get_grouping_target_domain(self, vals): + """Get the domain for searching manufacturing orders that can match + with the criteria we want to use. + + :param vals: Values dictionary of the MO to be created. + + :return: Odoo domain. + """ + domain = [ ('product_id', '=', vals['product_id']), + ('picking_type_id', '=', vals['picking_type_id']), ('bom_id', '=', vals.get('bom_id', False)), ('routing_id', '=', vals.get('routing_id', False)), ('company_id', '=', vals.get('company_id', False)), ('state', '=', 'confirmed'), - ], limit=1) - return mo + ] + if not vals.get('date_planned_finished'): + return domain + date = fields.Datetime.from_string(vals['date_planned_finished']) + pt = self.env['stock.picking.type'].browse(vals['picking_type_id']) + if date.hour < pt.mo_grouping_max_hour: + date_end = date.replace( + hour=pt.mo_grouping_max_hour, minute=0, second=0, + ) + else: + date_end = date.replace( + day=date.day + 1, hour=pt.mo_grouping_max_hour, minute=0, + second=0, + ) + date_start = date_end - relativedelta(days=pt.mo_grouping_interval) + domain += [ + ('date_planned_finished', '>', + fields.Datetime.to_string(date_start)), + ('date_planned_finished', '<=', + fields.Datetime.to_string(date_end)), + ] + return domain + + def _find_grouping_target(self, vals): + """Return the matching order for grouping. + + :param vals: Values dictionary of the MO to be created. + + :return: Target manufacturing order record (or empty record). + """ + return self.env['mrp.production'].search( + self._get_grouping_target_domain(vals), limit=1, + ) @api.model def create(self, vals): diff --git a/mrp_production_grouped_by_product/models/stock_picking_type.py b/mrp_production_grouped_by_product/models/stock_picking_type.py new file mode 100644 index 000000000..f5b9c3bd5 --- /dev/null +++ b/mrp_production_grouped_by_product/models/stock_picking_type.py @@ -0,0 +1,37 @@ +# Copyright 2018 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, exceptions, fields, models + + +class StockPickingType(models.Model): + _inherit = 'stock.picking.type' + + mo_grouping_max_hour = fields.Integer( + string="MO grouping max. hour (UTC)", + help="The maximum hour (between 0 and 23) for considering new " + "manufacturing orders inside the same interval period, and thus " + "being grouped on the same MO. IMPORTANT: The hour should be " + "expressed in UTC.", + default=19, + ) + mo_grouping_interval = fields.Integer( + string="MO grouping interval (days)", + help="The number of days for grouping together on the same " + "manufacturing order.", + default=1, + ) + + @api.constrains('mo_grouping_max_hour') + def _check_mo_grouping_max_hour(self): + if self.mo_grouping_max_hour < 0 or self.mo_grouping_max_hour > 23: + raise exceptions.ValidationError( + _("You have to enter a valid hour between 0 and 23."), + ) + + @api.constrains('mo_grouping_interval') + def _check_mo_grouping_interval(self): + if self.mo_grouping_interval < 0: + raise exceptions.ValidationError( + _("You have to enter a positive value for interval."), + ) diff --git a/mrp_production_grouped_by_product/readme/CONFIGURE.rst b/mrp_production_grouped_by_product/readme/CONFIGURE.rst new file mode 100644 index 000000000..163bf6c3b --- /dev/null +++ b/mrp_production_grouped_by_product/readme/CONFIGURE.rst @@ -0,0 +1,17 @@ +To configure the time frame for grouping manufacturing order: + +#. Go to *Inventory > Configuration > Warehouse Management > Operation Types* +#. Locate the manufacturing type you are using (default one is called + "Manufacturing"). +#. Open it and change these 2 values: + + * MO grouping max. hour (UTC): The maximum hour (between 0 and 23) for + considering new manufacturing orders inside the same interval period, and + thus being grouped on the same MO. IMPORTANT: The hour should be expressed + in UTC. + * MO grouping interval (days): The number of days for grouping together on + the same manufacturing order. + + Example: If you leave the default values 19 and 1, all the planned orders + between 19:00:01 of the previous day and 20:00:00 of the target date will + be grouped together. diff --git a/mrp_production_grouped_by_product/readme/CONTRIBUTORS.rst b/mrp_production_grouped_by_product/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..cfa3cdf10 --- /dev/null +++ b/mrp_production_grouped_by_product/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* Tecnativa __ + + * David Vidal + * Pedro M. Baeza diff --git a/mrp_production_grouped_by_product/readme/DESCRIPTION.rst b/mrp_production_grouped_by_product/readme/DESCRIPTION.rst new file mode 100644 index 000000000..50962a392 --- /dev/null +++ b/mrp_production_grouped_by_product/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +When you have several sales orders with make to order (MTO) products that +require to be manufactured, you end up with one manufacturing order for each of +these sales orders, which is very bad for the management. + +With this module, each time an MTO manufacturing order is required to be +created, it first checks that there's no other existing order not yet started +for the same product and bill of materials inside the specied time frame , and +if there's one, then the quantity of that order is increased instead of +creating a new one. diff --git a/mrp_production_grouped_by_product/readme/ROADMAP.rst b/mrp_production_grouped_by_product/readme/ROADMAP.rst new file mode 100644 index 000000000..7845179c4 --- /dev/null +++ b/mrp_production_grouped_by_product/readme/ROADMAP.rst @@ -0,0 +1 @@ +* Add a check in the product form for excluding it from being grouped. diff --git a/mrp_production_grouped_by_product/tests/test_mrp_production_grouped_by_product.py b/mrp_production_grouped_by_product/tests/test_mrp_production_grouped_by_product.py index 517d9cfc6..28afaa0f8 100644 --- a/mrp_production_grouped_by_product/tests/test_mrp_production_grouped_by_product.py +++ b/mrp_production_grouped_by_product/tests/test_mrp_production_grouped_by_product.py @@ -2,6 +2,7 @@ # Copyright 2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import exceptions from odoo.tests import common @@ -12,11 +13,17 @@ class TestProductionGroupedByProduct(common.SavepointCase): @classmethod def setUpClass(cls): super(TestProductionGroupedByProduct, cls).setUpClass() + cls.ProcurementGroup = cls.env['procurement.group'] + cls.MrpProduction = cls.env['mrp.production'] + cls.env.user.company_id.manufacturing_lead = 0 + cls.env.user.tz = False # Make sure there's no timezone in user + cls.picking_type = cls.env.ref('mrp.picking_type_manufacturing') cls.product1 = cls.env['product.product'].create({ 'name': 'TEST Muffin', 'route_ids': [(6, 0, [ cls.env.ref('mrp.route_warehouse0_manufacture').id])], 'type': 'product', + 'produce_delay': 0, }) cls.product2 = cls.env['product.product'].create({ 'name': 'TEST Paper muffin cup', @@ -39,49 +46,57 @@ class TestProductionGroupedByProduct(common.SavepointCase): })] }) cls.stock_picking_type = cls.env.ref('stock.picking_type_out') - cls.procurement_rule = cls.env['stock.warehouse.orderpoint'].create({ - 'name': 'XXX/00000', - 'product_id': cls.product1.id, - 'product_min_qty': 10, - 'product_max_qty': 100, - }) - cls.mo = cls.env['mrp.production'].create({ + cls.mo = cls.MrpProduction.create({ 'bom_id': cls.bom.id, 'product_id': cls.product1.id, 'product_qty': 2, 'product_uom_id': cls.product1.uom_id.id, + 'date_planned_finished': '2018-06-01 15:00:00', + 'date_planned_start': '2018-06-01 15:00:00', }) cls.warehouse = cls.env['stock.warehouse'].search([ ('company_id', '=', cls.env.user.company_id.id), ], limit=1) - cls.ProcurementGroup = cls.env['procurement.group'] - cls.MrpProduction = cls.env['mrp.production'] - - def test_mo_by_product(self): - self.ProcurementGroup.with_context(test_group_mo=True).run_scheduler() - mo = self.MrpProduction.search([('product_id', '=', self.product1.id)]) - self.assertEqual(len(mo), 1) - self.assertEqual(mo.product_qty, 100) # Add an MTO move - move = self.env['stock.move'].create({ - 'name': self.product1.name, - 'product_id': self.product1.id, + cls.move = cls.env['stock.move'].create({ + 'name': cls.product1.name, + 'product_id': cls.product1.id, 'product_uom_qty': 10, - 'product_uom': self.product1.uom_id.id, - 'location_id': self.warehouse.lot_stock_id.id, + 'product_uom': cls.product1.uom_id.id, + 'location_id': cls.warehouse.lot_stock_id.id, 'location_dest_id': ( - self.env.ref('stock.stock_location_customers').id + cls.env.ref('stock.stock_location_customers').id ), 'procure_method': 'make_to_order', - 'warehouse_id': self.warehouse.id, + 'warehouse_id': cls.warehouse.id, + 'date': '2018-06-01 18:00:00', }) - move.with_context(test_group_mo=True)._action_confirm(merge=False) + + def test_mo_by_product(self): + self.move.with_context(test_group_mo=True)._action_confirm(merge=False) self.ProcurementGroup.with_context(test_group_mo=True).run_scheduler() mo = self.MrpProduction.search([('product_id', '=', self.product1.id)]) self.assertEqual(len(mo), 1) - self.assertEqual(mo.product_qty, 110) + self.assertEqual(mo.product_qty, 12) # Run again the scheduler to see if quantities are altered self.ProcurementGroup.with_context(test_group_mo=True).run_scheduler() mo = self.MrpProduction.search([('product_id', '=', self.product1.id)]) self.assertEqual(len(mo), 1) - self.assertEqual(mo.product_qty, 110) + self.assertEqual(mo.product_qty, 12) + + def test_mo_other_date(self): + self.move.date = '2018-06-01 20:01:00' + self.move.with_context(test_group_mo=True)._action_confirm(merge=False) + self.ProcurementGroup.with_context(test_group_mo=True).run_scheduler() + mo = self.MrpProduction.search([('product_id', '=', self.product1.id)]) + self.assertEqual(len(mo), 2) + + def test_check_mo_grouping_max_hour(self): + with self.assertRaises(exceptions.ValidationError): + self.picking_type.mo_grouping_max_hour = 25 + with self.assertRaises(exceptions.ValidationError): + self.picking_type.mo_grouping_max_hour = -1 + + def test_check_mo_grouping_interval(self): + with self.assertRaises(exceptions.ValidationError): + self.picking_type.mo_grouping_interval = -1 diff --git a/mrp_production_grouped_by_product/views/stock_picking_type_views.xml b/mrp_production_grouped_by_product/views/stock_picking_type_views.xml new file mode 100644 index 000000000..2b24b05e5 --- /dev/null +++ b/mrp_production_grouped_by_product/views/stock_picking_type_views.xml @@ -0,0 +1,17 @@ + + + + + + stock.picking.type + + + + + + + + + +