From 82a6a3ea7a548883663c3567bf313680cc1a96e9 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 10 May 2018 20:22:27 +0200 Subject: [PATCH 1/3] [ADD] mrp_production_grouped_by_product: New Module --- mrp_production_grouped_by_product/README.rst | 56 ++++++++++++ mrp_production_grouped_by_product/__init__.py | 2 + .../__manifest__.py | 18 ++++ .../models/__init__.py | 3 + .../models/mrp_production.py | 15 ++++ .../models/procurement.py | 44 ++++++++++ .../tests/__init__.py | 2 + .../test_mrp_production_grouped_by_product.py | 85 +++++++++++++++++++ 8 files changed, 225 insertions(+) create mode 100644 mrp_production_grouped_by_product/README.rst create mode 100644 mrp_production_grouped_by_product/__init__.py create mode 100644 mrp_production_grouped_by_product/__manifest__.py create mode 100644 mrp_production_grouped_by_product/models/__init__.py create mode 100644 mrp_production_grouped_by_product/models/mrp_production.py create mode 100644 mrp_production_grouped_by_product/models/procurement.py create mode 100644 mrp_production_grouped_by_product/tests/__init__.py create mode 100644 mrp_production_grouped_by_product/tests/test_mrp_production_grouped_by_product.py diff --git a/mrp_production_grouped_by_product/README.rst b/mrp_production_grouped_by_product/README.rst new file mode 100644 index 000000000..1833e03d7 --- /dev/null +++ b/mrp_production_grouped_by_product/README.rst @@ -0,0 +1,56 @@ +.. 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 +============================= + +Groups pending productions by product. + +Configuration +============= + + +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 + +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 +------------ + +* David Vidal + +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. diff --git a/mrp_production_grouped_by_product/__init__.py b/mrp_production_grouped_by_product/__init__.py new file mode 100644 index 000000000..a9e337226 --- /dev/null +++ b/mrp_production_grouped_by_product/__init__.py @@ -0,0 +1,2 @@ + +from . import models diff --git a/mrp_production_grouped_by_product/__manifest__.py b/mrp_production_grouped_by_product/__manifest__.py new file mode 100644 index 000000000..3815ef003 --- /dev/null +++ b/mrp_production_grouped_by_product/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2018 Tecnativa - David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Production Grouped By Product', + 'version': '11.0.1.0.0', + 'category': 'MRP', + 'author': 'Tecnativa, ' + 'Odoo Community Association (OCA)', + 'website': 'https://github.com/oca/manufacture', + 'license': 'AGPL-3', + 'depends': [ + 'mrp', + ], + 'data': [ + ], + 'installable': True, +} diff --git a/mrp_production_grouped_by_product/models/__init__.py b/mrp_production_grouped_by_product/models/__init__.py new file mode 100644 index 000000000..29b455422 --- /dev/null +++ b/mrp_production_grouped_by_product/models/__init__.py @@ -0,0 +1,3 @@ + +from . import mrp_production +from . import procurement diff --git a/mrp_production_grouped_by_product/models/mrp_production.py b/mrp_production_grouped_by_product/models/mrp_production.py new file mode 100644 index 000000000..d6e97b1a0 --- /dev/null +++ b/mrp_production_grouped_by_product/models/mrp_production.py @@ -0,0 +1,15 @@ +# Copyright 2018 Tecnativa - David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + + @api.model + def create(self, vals): + if not self._context.get('merge_products_into_mo'): + return super(MrpProduction, self).create(vals) + # We return the MO to merge into + return self._context.get('merge_products_into_mo') diff --git a/mrp_production_grouped_by_product/models/procurement.py b/mrp_production_grouped_by_product/models/procurement.py new file mode 100644 index 000000000..de99e50d7 --- /dev/null +++ b/mrp_production_grouped_by_product/models/procurement.py @@ -0,0 +1,44 @@ +# Copyright 2018 Tecnativa - David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class ProcuermentRule(models.Model): + _inherit = 'procurement.rule' + + def _run_manufacture(self, product_id, product_qty, product_uom, + location_id, name, origin, values): + bom = self._get_matching_bom(product_id, values) + # Send to super for exception + if not bom: + return super(ProcuermentRule, self)._run_manufacture( + product_id, product_qty, product_uom, location_id, + name, origin, values) + open_mo = self._find_equal_open_mo( + product_id, bom, location_id, values) + # Create mo as usual + if not open_mo: + return super(ProcuermentRule, self)._run_manufacture( + product_id, product_qty, product_uom, location_id, + name, origin, values) + # Add product qty to mo + self.env['change.production.qty'].create({ + 'mo_id': open_mo.id, + 'product_qty': open_mo.product_qty + product_qty, + }).change_prod_qty() + # We pass the record in the context so the chatter is correctly written + additional_context={'merge_products_into_mo': open_mo} + return super(ProcuermentRule, self.with_context(**additional_context) + )._run_manufacture(product_id, product_qty, product_uom, + location_id,name, origin, values) + + def _find_equal_open_mo(self, product_id, bom, location_id, values): + """Returns the first occurrence according to conditions""" + return self.env['mrp.production'].search([ + ('state', 'not in', ['progress', 'done', 'cancel']), + ('product_id', '=', product_id.id), + ('bom_id', '=', bom.id), + ('location_dest_id', '=', location_id.id), + ('company_id', '=', values.get('company_id').id), + ], limit=1) diff --git a/mrp_production_grouped_by_product/tests/__init__.py b/mrp_production_grouped_by_product/tests/__init__.py new file mode 100644 index 000000000..66f012923 --- /dev/null +++ b/mrp_production_grouped_by_product/tests/__init__.py @@ -0,0 +1,2 @@ + +from . import test_mrp_production_grouped_by_product 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 new file mode 100644 index 000000000..7ffa9fc64 --- /dev/null +++ b/mrp_production_grouped_by_product/tests/test_mrp_production_grouped_by_product.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Tecnativa - David Vidal +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests import common + + +class TestProductionGroupedByProduct(common.SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestProductionGroupedByProduct, cls).setUpClass() + cls.product1 = cls.env['product.product'].create({ + 'name': 'TEST Muffin', + 'route_ids': [(6, 0, [ + cls.env.ref('mrp.route_warehouse0_manufacture').id])], + 'type': 'product', + }) + cls.product2 = cls.env['product.product'].create({ + 'name': 'TEST Paper muffin cup', + 'type': 'product', + }) + cls.product3 = cls.env['product.product'].create({ + 'name': 'TEST Muffin paset', + 'type': 'product', + }) + cls.bom = cls.env['mrp.bom'].create({ + 'product_id': cls.product1.id, + 'product_tmpl_id': cls.product1.product_tmpl_id.id, + 'type': 'normal', + 'bom_line_ids': [(0, 0, { + 'product_id': cls.product2.id, + 'product_qty': 1, + }), (0, 0, { + 'product_id': cls.product3.id, + 'product_qty': 0.2, + })] + }) + cls.env['stock.change.product.qty'].create({ + 'product_id': cls.product2.id, + 'new_quantity': 100.0, + }).change_product_qty() + cls.env['stock.change.product.qty'].create({ + 'product_id': cls.product3.id, + 'new_quantity': 100.0, + }).change_product_qty() + 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, + }) + + def test_mo_by_product(self): + self.env['procurement.group'].run_scheduler() + mo = self.env['mrp.production'].search([ + ('product_id', '=', self.product1.id), + ]) + self.assertTrue(mo) + # + picking = self.env['stock.picking'].create({ + 'picking_type_id': self.stock_picking_type.id, + 'location_id': self.env.ref('stock.stock_location_stock').id, + 'location_dest_id': self.env.ref( + 'stock.stock_location_customers').id, + }) + move = self.env['stock.move'].create({ + 'name': self.product1.name, + 'product_id': self.product1.id, + 'product_uom_qty': 1000, + 'product_uom': self.product1.uom_id.id, + 'picking_id': picking.id, + 'picking_type_id': self.stock_picking_type.id, + 'location_id': picking.location_id.id, + 'location_dest_id': picking.location_id.id, + }) + move.quantity_done = 1000 + picking.action_assign() + self.product1.virtual_available = -500 + self.env['procurement.group'].run_scheduler() + mo = self.env['mrp.production'].search([ + ('product_id', '=', self.product1.id), + ]) + self.assertEqual(len(mo), 1) From d13de574caaa10345cd2c0bea046be85c06fe8a8 Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Thu, 10 May 2018 21:03:59 +0200 Subject: [PATCH 2/3] [IMP] mrp_production_grouped_by_company: Context evaluation on mrp.production + tests --- mrp_production_grouped_by_product/README.rst | 23 ++++-- .../__manifest__.py | 1 + .../models/mrp_production.py | 46 ++++++++++-- .../models/procurement.py | 44 +++--------- .../test_mrp_production_grouped_by_product.py | 70 ++++++++++--------- 5 files changed, 105 insertions(+), 79 deletions(-) diff --git a/mrp_production_grouped_by_product/README.rst b/mrp_production_grouped_by_product/README.rst index 1833e03d7..40d01b606 100644 --- a/mrp_production_grouped_by_product/README.rst +++ b/mrp_production_grouped_by_product/README.rst @@ -6,11 +6,14 @@ Production Grouped By Product ============================= -Groups pending productions by product. - -Configuration -============= +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 ===== @@ -19,6 +22,11 @@ Usage :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 =========== @@ -38,7 +46,12 @@ Images Contributors ------------ -* David Vidal +* Tecnativa _ + + * David Vidal + * Pedro M. Baeza + +Do not contact contributors directly about support or help with technical issues. Maintainer ---------- diff --git a/mrp_production_grouped_by_product/__manifest__.py b/mrp_production_grouped_by_product/__manifest__.py index 3815ef003..78138907c 100644 --- a/mrp_production_grouped_by_product/__manifest__.py +++ b/mrp_production_grouped_by_product/__manifest__.py @@ -1,4 +1,5 @@ # Copyright 2018 Tecnativa - David Vidal +# Copyright 2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { diff --git a/mrp_production_grouped_by_product/models/mrp_production.py b/mrp_production_grouped_by_product/models/mrp_production.py index d6e97b1a0..f9572f142 100644 --- a/mrp_production_grouped_by_product/models/mrp_production.py +++ b/mrp_production_grouped_by_product/models/mrp_production.py @@ -1,15 +1,51 @@ # Copyright 2018 Tecnativa - David Vidal +# Copyright 2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import api, models +from odoo.tools import config class MrpProduction(models.Model): _inherit = 'mrp.production' + def _post_mo_merging_adjustments(self, vals): + """Called when a new MO is merged onto existing one for adjusting the + needed values according this merging. + + :param self: Single record of the target record where merging. + :param vals: Dictionary with the new record values. + """ + self.ensure_one() + new_vals = { + 'origin': (self.origin or '') + ",%s" % vals['origin'], + } + if vals.get('move_dest_ids'): + new_vals['move_dest_ids'] = vals['move_dest_ids'] + 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([ + ('product_id', '=', vals['product_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 + @api.model def create(self, vals): - if not self._context.get('merge_products_into_mo'): - return super(MrpProduction, self).create(vals) - # We return the MO to merge into - return self._context.get('merge_products_into_mo') + context = self.env.context + if (context.get('group_mo_by_product') and + (not config['test_enable'] or context.get('test_group_mo'))): + mo = self._find_grouping_target(vals) + if mo: + self.env['change.production.qty'].create({ + 'mo_id': mo.id, + 'product_qty': mo.product_qty + vals['product_qty'], + }).change_prod_qty() + mo._post_mo_merging_adjustments(vals) + return mo + return super(MrpProduction, self).create(vals) diff --git a/mrp_production_grouped_by_product/models/procurement.py b/mrp_production_grouped_by_product/models/procurement.py index de99e50d7..19368adc0 100644 --- a/mrp_production_grouped_by_product/models/procurement.py +++ b/mrp_production_grouped_by_product/models/procurement.py @@ -1,44 +1,18 @@ # Copyright 2018 Tecnativa - David Vidal +# Copyright 2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import models -class ProcuermentRule(models.Model): +class ProcurementRule(models.Model): _inherit = 'procurement.rule' def _run_manufacture(self, product_id, product_qty, product_uom, location_id, name, origin, values): - bom = self._get_matching_bom(product_id, values) - # Send to super for exception - if not bom: - return super(ProcuermentRule, self)._run_manufacture( - product_id, product_qty, product_uom, location_id, - name, origin, values) - open_mo = self._find_equal_open_mo( - product_id, bom, location_id, values) - # Create mo as usual - if not open_mo: - return super(ProcuermentRule, self)._run_manufacture( - product_id, product_qty, product_uom, location_id, - name, origin, values) - # Add product qty to mo - self.env['change.production.qty'].create({ - 'mo_id': open_mo.id, - 'product_qty': open_mo.product_qty + product_qty, - }).change_prod_qty() - # We pass the record in the context so the chatter is correctly written - additional_context={'merge_products_into_mo': open_mo} - return super(ProcuermentRule, self.with_context(**additional_context) - )._run_manufacture(product_id, product_qty, product_uom, - location_id,name, origin, values) - - def _find_equal_open_mo(self, product_id, bom, location_id, values): - """Returns the first occurrence according to conditions""" - return self.env['mrp.production'].search([ - ('state', 'not in', ['progress', 'done', 'cancel']), - ('product_id', '=', product_id.id), - ('bom_id', '=', bom.id), - ('location_dest_id', '=', location_id.id), - ('company_id', '=', values.get('company_id').id), - ], limit=1) + return super( + ProcurementRule, self.with_context(group_mo_by_product=True), + )._run_manufacture( + product_id, product_qty, product_uom, location_id, name, origin, + values, + ) 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 7ffa9fc64..517d9cfc6 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 @@ -1,11 +1,13 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Tecnativa - David Vidal +# Copyright 2018 Tecnativa - David Vidal +# Copyright 2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo.tests import common class TestProductionGroupedByProduct(common.SavepointCase): + at_install = False + post_install = True @classmethod def setUpClass(cls): @@ -36,14 +38,6 @@ class TestProductionGroupedByProduct(common.SavepointCase): 'product_qty': 0.2, })] }) - cls.env['stock.change.product.qty'].create({ - 'product_id': cls.product2.id, - 'new_quantity': 100.0, - }).change_product_qty() - cls.env['stock.change.product.qty'].create({ - 'product_id': cls.product3.id, - 'new_quantity': 100.0, - }).change_product_qty() cls.stock_picking_type = cls.env.ref('stock.picking_type_out') cls.procurement_rule = cls.env['stock.warehouse.orderpoint'].create({ 'name': 'XXX/00000', @@ -51,35 +45,43 @@ class TestProductionGroupedByProduct(common.SavepointCase): 'product_min_qty': 10, 'product_max_qty': 100, }) + cls.mo = cls.env['mrp.production'].create({ + 'bom_id': cls.bom.id, + 'product_id': cls.product1.id, + 'product_qty': 2, + 'product_uom_id': cls.product1.uom_id.id, + }) + 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.env['procurement.group'].run_scheduler() - mo = self.env['mrp.production'].search([ - ('product_id', '=', self.product1.id), - ]) - self.assertTrue(mo) - # - picking = self.env['stock.picking'].create({ - 'picking_type_id': self.stock_picking_type.id, - 'location_id': self.env.ref('stock.stock_location_stock').id, - 'location_dest_id': self.env.ref( - 'stock.stock_location_customers').id, - }) + 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, - 'product_uom_qty': 1000, + 'product_uom_qty': 10, 'product_uom': self.product1.uom_id.id, - 'picking_id': picking.id, - 'picking_type_id': self.stock_picking_type.id, - 'location_id': picking.location_id.id, - 'location_dest_id': picking.location_id.id, + 'location_id': self.warehouse.lot_stock_id.id, + 'location_dest_id': ( + self.env.ref('stock.stock_location_customers').id + ), + 'procure_method': 'make_to_order', + 'warehouse_id': self.warehouse.id, }) - move.quantity_done = 1000 - picking.action_assign() - self.product1.virtual_available = -500 - self.env['procurement.group'].run_scheduler() - mo = self.env['mrp.production'].search([ - ('product_id', '=', self.product1.id), - ]) + 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) + # 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) From befc20195a8a001051a52e4e7be7f1f24c54f656 Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Mon, 4 Jun 2018 01:29:31 +0200 Subject: [PATCH 3/3] [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 + + + + + + + + + +