[IMP] mrp_production_grouped_by_product: Time frames

Introduce time frames for grouping.
This commit is contained in:
Pedro M. Baeza
2018-06-04 01:29:31 +02:00
committed by ps-tubtim
parent 1d89c9ad1b
commit 67da2e9260
12 changed files with 236 additions and 99 deletions

View File

@@ -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
<https://github.com/OCA/manufacture/issues>`_. In case of trouble,
please check there if your issue has already been reported. If you spotted it
first, help us smash it by providing detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Tecnativa <https://www.tecnativa.com>_
* 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.**

View File

@@ -14,6 +14,7 @@
'mrp',
],
'data': [
'views/stock_picking_type_views.xml',
],
'installable': True,
}

View File

@@ -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."

View File

@@ -1,3 +1,4 @@
from . import mrp_production
from . import procurement
from . import stock_picking_type

View File

@@ -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):

View File

@@ -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."),
)

View File

@@ -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.

View File

@@ -0,0 +1,4 @@
* Tecnativa <https://www.tecnativa.com>__
* David Vidal
* Pedro M. Baeza

View File

@@ -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.

View File

@@ -0,0 +1 @@
* Add a check in the product form for excluding it from being grouped.

View File

@@ -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

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2018 Tecnativa - Pedro M. Baeza
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_picking_type_form" model="ir.ui.view">
<field name="model">stock.picking.type</field>
<field name="inherit_id" ref="stock.view_picking_type_form"/>
<field name="arch" type="xml">
<field name="code" position="after">
<field name="mo_grouping_max_hour"/>
<field name="mo_grouping_interval"/>
</field>
</field>
</record>
</odoo>