Merge pull request #273 from Tecnativa/11.0-add-mrp_production_grouped_by_product

[ADD] mrp_production_grouped_by_product: New Module
This commit is contained in:
Pedro M. Baeza
2018-06-13 12:54:08 +02:00
committed by GitHub
15 changed files with 388 additions and 0 deletions

View File

@@ -0,0 +1 @@
**This file is going to be generated by oca-gen-addon-readme.**

View File

@@ -0,0 +1,2 @@
from . import models

View File

@@ -0,0 +1,20 @@
# Copyright 2018 Tecnativa - David Vidal
# Copyright 2018 Tecnativa - Pedro M. Baeza
# 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': [
'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

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

View File

@@ -0,0 +1,91 @@
# Copyright 2018 Tecnativa - David Vidal
# Copyright 2018 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from dateutil.relativedelta import relativedelta
from odoo import api, fields, 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 _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'),
]
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):
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)

View File

@@ -0,0 +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 models
class ProcurementRule(models.Model):
_inherit = 'procurement.rule'
def _run_manufacture(self, product_id, product_qty, product_uom,
location_id, name, origin, values):
return super(
ProcurementRule, self.with_context(group_mo_by_product=True),
)._run_manufacture(
product_id, product_qty, product_uom, location_id, name, origin,
values,
)

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

@@ -0,0 +1,2 @@
from . import test_mrp_production_grouped_by_product

View File

@@ -0,0 +1,102 @@
# 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 exceptions
from odoo.tests import common
class TestProductionGroupedByProduct(common.SavepointCase):
at_install = False
post_install = True
@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',
'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.stock_picking_type = cls.env.ref('stock.picking_type_out')
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)
# Add an MTO move
cls.move = cls.env['stock.move'].create({
'name': cls.product1.name,
'product_id': cls.product1.id,
'product_uom_qty': 10,
'product_uom': cls.product1.uom_id.id,
'location_id': cls.warehouse.lot_stock_id.id,
'location_dest_id': (
cls.env.ref('stock.stock_location_customers').id
),
'procure_method': 'make_to_order',
'warehouse_id': cls.warehouse.id,
'date': '2018-06-01 18:00:00',
})
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, 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, 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>