Migrate and improve mrp_to_mto_with_stock to version 10

This commit is contained in:
Florian da Costa
2017-07-27 10:46:26 +02:00
parent 7571492df0
commit 588511765c
9 changed files with 381 additions and 128 deletions

View File

@@ -7,9 +7,16 @@ MRP MTO with Stock
==================
This module extends the functionality of Manufacturing to support the creation
of procurements when there is no stock available. This allow you to pull from
stock until the quantity on hand is zero, and then create a procurement
to fulfill the MO requirements.
of procurements only for a part of the raw material.
It has 2 modes. The default one allow you to pull
from stock until the quantity on hand is zero, and then create a procurement
to fulfill the MO requirements. In this mode, the created procurements must
be the ones fulfilling the MO that has generated it.
The other mode is based on the forecast quantity. It will allow to pull from
stock until the forecast quantity is zero and then create a procurement for
the missing products. In this mode, there is no link between the procurement
created and MO that has generated it. The procurement may be used to fulfill
another MO.
Configuration
=============
@@ -21,17 +28,23 @@ To configure this module, you need to:
MTO/MTS Locations*. Any other location not specified here will have the
standard behavior.
If you want to use the second mode, based on forecast quantity
#. Go to the warehouse you want to follow this behaviour.
#. In the view form go to the tab *Warehouse Configuration* and set the
*MRP MTO with forecast stock*. You still need to configure the products
like described in last step.
Usage
=====
To use this module, you need to:
#. Go to *Manufacturing* and create a Manufacturing Order.
#. Click on *Confirm Production*.
#. Click on *Check availability*.
.. 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/9.0
:target: https://runbot.odoo-community.org/runbot/129/10.0
Bug Tracker
===========
@@ -54,6 +67,7 @@ Contributors
* John Walsh <John.Walsh@interclean.com>
* Lois Rilo <lois.rilo@eficent.com>
* Florian da Costa <florian.dacosta@akretion.com>
Maintainer
----------

View File

@@ -10,10 +10,14 @@
"author": "John Walsh, Eficent, Odoo Community Association (OCA)",
"website": "https://odoo-community.org/",
"category": "Manufacturing",
"version": "9.0.1.0.0",
"version": "10.0.1.0.0",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": ["mrp"],
"data": ['views/product_template_view.xml'],
"data": [
'views/product_template_view.xml',
'views/stock_warehouse.xml',
],
"demo": ['demo/product.xml'],
}

View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2017 - Today: GRAP (http://www.grap.coop)
@author Sylvain LE GAL (https://twitter.com/legalsylvain)
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<odoo> <data noupdate="1">
<record id="product_product_manufacture_1" model="product.product">
<field name="name">TOP</field>
<field name="categ_id" ref="product.product_category_3"/>
<field name="standard_price">600.00</field>
<field name="list_price">400.00</field>
<field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/>
<field name="description">TODO</field>
<field name="default_code">MANUF</field>
<field name="route_ids" eval="[(6, 0, [ref('stock.route_warehouse0_mto'), ref('mrp.route_warehouse0_manufacture')])]"/>
</record>
<record id="product_product_manufacture_1_1" model="product.product">
<field name="name">Subproduct 1</field>
<field name="categ_id" ref="product.product_category_3"/>
<field name="standard_price">300.00</field>
<field name="list_price">100.00</field>
<field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/>
<field name="description">TODO</field>
<field name="default_code">MANUF 1-1</field>
<field name="route_ids" eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"/>
<field name="mrp_mts_mto_location_ids" eval="[(6, 0, [ref('stock.stock_location_stock')])]"/>
</record>
<record id="product_product_manufacture_1_2" model="product.product">
<field name="name">Subproduct 2</field>
<field name="categ_id" ref="product.product_category_3"/>
<field name="standard_price">100.00</field>
<field name="list_price">30.00</field>
<field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/>
<field name="description">TODO</field>
<field name="default_code">MANUF 1-2</field>
<field name="route_ids" eval="[(6, 0, [ref('mrp.route_warehouse0_manufacture')])]"/>
<field name="mrp_mts_mto_location_ids" eval="[(6, 0, [ref('stock.stock_location_stock')])]"/>
</record>
<record id="product_product_manufacture_1_1_1" model="product.product">
<field name="name">Subproduct 1-1</field>
<field name="categ_id" ref="product.product_category_3"/>
<field name="standard_price">10.00</field>
<field name="list_price">3.00</field>
<field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/>
<field name="description">TODO</field>
<field name="default_code">MANUF 1-1-1</field>
</record>
<record id="product_product_manufacture_1_2_1" model="product.product">
<field name="name">Subproduct 2-1</field>
<field name="categ_id" ref="product.product_category_3"/>
<field name="standard_price">10.00</field>
<field name="list_price">3.00</field>
<field name="type">product</field>
<field name="uom_id" ref="product.product_uom_unit"/>
<field name="uom_po_id" ref="product.product_uom_unit"/>
<field name="description">TODO</field>
<field name="default_code">MANUF 1-2-1</field>
</record>
<record id="mrp_bom_manuf_1" model="mrp.bom">
<field name="product_tmpl_id" ref="product_product_manufacture_1_product_template"/>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">10</field>
</record>
<record id="mrp_bom_line_manuf_1_1" model="mrp.bom.line">
<field name="product_id" ref="product_product_manufacture_1_1"/>
<field name="product_qty">5</field>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">1</field>
<field name="bom_id" ref="mrp_bom_manuf_1"/>
</record>
<record id="mrp_bom_line_manuf_1_2" model="mrp.bom.line">
<field name="product_id" ref="product_product_manufacture_1_2"/>
<field name="product_qty">2</field>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">1</field>
<field name="bom_id" ref="mrp_bom_manuf_1"/>
</record>
<record id="mrp_bom_manuf_1_1" model="mrp.bom">
<field name="product_tmpl_id" ref="product_product_manufacture_1_1_product_template"/>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">10</field>
</record>
<record id="mrp_bom_line_manuf_1_1_1" model="mrp.bom.line">
<field name="product_id" ref="product_product_manufacture_1_1_1"/>
<field name="product_qty">2</field>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">1</field>
<field name="bom_id" ref="mrp_bom_manuf_1_1"/>
</record>
<record id="mrp_bom_manuf_1_2" model="mrp.bom">
<field name="product_tmpl_id" ref="product_product_manufacture_1_2_product_template"/>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">10</field>
</record>
<record id="mrp_bom_line_manuf_1_2_1" model="mrp.bom.line">
<field name="product_id" ref="product_product_manufacture_1_2_1"/>
<field name="product_qty">4</field>
<field name="product_uom_id" ref="product.product_uom_unit"/>
<field name="sequence">1</field>
<field name="bom_id" ref="mrp_bom_manuf_1_2"/>
</record>
</data> </odoo>

View File

@@ -5,3 +5,4 @@
from . import mrp_production
from . import product_template
from . import stock_warehouse

View File

@@ -3,7 +3,7 @@
# Copyright 2015 John Walsh
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import api, models
from odoo import api, models
import logging
_logger = logging.getLogger(__name__)
@@ -11,32 +11,69 @@ _logger = logging.getLogger(__name__)
class MrpProduction(models.Model):
_inherit = 'mrp.production'
@api.one
@api.multi
def _adjust_procure_method(self):
# Si location => By pass method...
super(MrpProduction, self)._adjust_procure_method()
@api.multi
def action_assign(self):
"""Reserves available products to the production order but also creates
procurements for more items if we cannot reserve enough (MTO with
stock).
@returns list of ids"""
@returns True"""
# reserve all that is available (standard behaviour):
res = super(MrpProduction, self).action_assign()
# try to create procurements:
for move in self.move_lines:
if (move.state == 'confirmed' and move.location_id in
move.product_id.mrp_mts_mto_location_ids):
domain = [('product_id', '=', move.product_id.id),
('move_dest_id', '=', move.id)]
if move.group_id:
domain.append(('group_id', '=', move.group_id.id))
procurement = self.env['procurement.order'].search(domain)
if not procurement:
qty_to_procure = (move.remaining_qty -
move.reserved_availability)
proc_dict = self._prepare_mto_procurement(
move, qty_to_procure)
self.env['procurement.order'].create(proc_dict)
move_obj = self.env['stock.move']
for production in self:
warehouse = production.location_src_id.get_warehouse()
mto_with_no_move_dest_id = warehouse.mrp_mto_mts_forecast_qty
for move in self.move_raw_ids:
if (move.state == 'confirmed' and move.location_id in
move.product_id.mrp_mts_mto_location_ids and not
mto_with_no_move_dest_id):
domain = [('product_id', '=', move.product_id.id),
('move_dest_id', '=', move.id)]
if move.group_id:
domain.append(('group_id', '=', move.group_id.id))
procurement = self.env['procurement.order'].search(domain)
if not procurement:
# We have to split the move because we can't have
# a part of the move that have ancestors and not the
# other else it won't ever be reserved.
qty_to_procure = (move.remaining_qty -
move.reserved_availability)
if qty_to_procure < move.product_uom_qty:
move.do_unreserve()
new_move_id = move.split(
qty_to_procure,
restrict_lot_id=move.restrict_lot_id,
restrict_partner_id=move.restrict_partner_id)
new_move = move_obj.browse(
new_move_id)
move.action_assign()
else:
new_move = move
proc_dict = self._prepare_mto_procurement(
new_move, qty_to_procure,
mto_with_no_move_dest_id)
self.env['procurement.order'].create(proc_dict)
if (move.state == 'confirmed' and move.location_id in
move.product_id.mrp_mts_mto_location_ids and
move.procure_method == 'make_to_stock' and
mto_with_no_move_dest_id):
qty_to_procure = production.get_mto_qty_to_procure(move)
if qty_to_procure > 0.0:
proc_dict = self._prepare_mto_procurement(
move, qty_to_procure, mto_with_no_move_dest_id)
proc_dict.pop('move_dest_id', None)
self.env['procurement.order'].create(proc_dict)
return res
def _prepare_mto_procurement(self, move, qty):
def _prepare_mto_procurement(self, move, qty, mto_with_no_move_dest_id):
"""Prepares a procurement for a MTO product."""
origin = ((move.group_id and move.group_id.name + ":") or "") + \
((move.name and move.name + ":") or "") + 'MTO -> Production'
@@ -44,7 +81,7 @@ class MrpProduction(models.Model):
route_ids = self.env.ref('stock.route_warehouse0_mto')
warehouse_id = (move.warehouse_id.id or (move.picking_type_id and
move.picking_type_id.warehouse_id.id or False))
return {
vals = {
'name': move.name + ':' + str(move.id),
'origin': origin,
'company_id': move.company_id and move.company_id.id or False,
@@ -53,9 +90,26 @@ class MrpProduction(models.Model):
'product_qty': qty,
'product_uom': move.product_uom.id,
'location_id': move.location_id.id,
'move_dest_id': move.id,
'group_id': group_id,
'route_ids': [(6, 0, route_ids.ids)],
'warehouse_id': warehouse_id,
'priority': move.priority,
}
if not mto_with_no_move_dest_id:
vals['move_dest_id'] = move.id
return vals
@api.multi
def get_mto_qty_to_procure(self, move):
self.ensure_one()
stock_location_id = move.location_id.id
move_location = move.with_context(location=stock_location_id)
virtual_available = move_location.product_id.virtual_available
qty_available = move.product_id.uom_id._compute_quantity(
virtual_available, move.product_uom)
if qty_available >= 0:
return 0.0
else:
if abs(qty_available) < move.product_uom_qty:
return abs(qty_available)
return move.product_uom_qty

View File

@@ -2,7 +2,7 @@
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import fields, models
from odoo import fields, models
class ProductTemplate(models.Model):

View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class StockWarehouse(models.Model):
_inherit = 'stock.warehouse'
mrp_mto_mts_forecast_qty = fields.Boolean(
string="MRP MTO with forecast stock",
help="When you use Mrp_mto_with_stock, the procurement creation is "
"based on reservable stock by default. Check this option if "
"you prefer base it on the forecast stock. In this case, the "
"created procurements won't be linked to the raw material moves")

View File

@@ -2,11 +2,11 @@
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp.tests.common import TransactionCase
from openerp import fields
from odoo.tests.common import TransactionCase
class TestMrpMtoWithStock(TransactionCase):
def setUp(self, *args, **kwargs):
super(TestMrpMtoWithStock, self).setUp(*args, **kwargs)
self.production_model = self.env['mrp.production']
@@ -15,55 +15,27 @@ class TestMrpMtoWithStock(TransactionCase):
self.manufacture_route = self.env.ref(
'mrp.route_warehouse0_manufacture')
self.uom_unit = self.env.ref('product.product_uom_unit')
self.warehouse = self.env.ref('stock.warehouse0')
self.product_fp = self.env['product.product'].create({
'name': 'FP',
'type': 'product',
'uom_id': self.uom_unit.id,
'route_ids': [(4, self.manufacture_route.id)]
})
self.product_c1 = self.env['product.product'].create({
'name': 'C1',
'type': 'product',
'uom_id': self.uom_unit.id,
'route_ids': [(4, self.manufacture_route.id)]
})
self.product_c2 = self.env['product.product'].create({
'name': 'C2',
'type': 'product',
'uom_id': self.uom_unit.id,
})
self._update_product_qty(self.product_c2,
self.stock_location_stock, 10)
self.top_product = self.env.ref(
'mrp_mto_with_stock.product_product_manufacture_1')
self.subproduct1 = self.env.ref(
'mrp_mto_with_stock.product_product_manufacture_1_1')
self.subproduct2 = self.env.ref(
'mrp_mto_with_stock.product_product_manufacture_1_2')
self.subproduct_1_1 = self.env.ref(
'mrp_mto_with_stock.product_product_manufacture_1_1_1')
self.bom_fp = self.env['mrp.bom'].create({
'product_id': self.product_fp.id,
'product_tmpl_id': self.product_fp.product_tmpl_id.id,
'bom_line_ids': ([
(0, 0, {
'product_id': self.product_c1.id,
'product_qty': 1,
'product_uom': self.uom_unit.id
}),
(0, 0, {
'product_id': self.product_c2.id,
'product_qty': 1,
'product_uom': self.uom_unit.id
}),
])
})
self.main_bom = self.env.ref(
'mrp_mto_with_stock.mrp_bom_manuf_1')
self.bom_c1 = self.env['mrp.bom'].create({
'product_id': self.product_c1.id,
'product_tmpl_id': self.product_c1.product_tmpl_id.id,
'bom_line_ids': ([(0, 0, {
'product_id': self.product_c2.id,
'product_qty': 1,
'product_uom': self.uom_unit.id
})])
})
self.product_c1.mrp_mts_mto_location_ids = [
(6, 0, [self.stock_location_stock.id])]
def _get_production_vals(self):
return {
'product_id': self.top_product.id,
'product_qty': 1,
'product_uom_id': self.uom_unit.id,
'bom_id': self.main_bom.id,
}
def _update_product_qty(self, product, location, quantity):
"""Update Product quantity."""
@@ -75,65 +47,112 @@ class TestMrpMtoWithStock(TransactionCase):
product_qty.change_product_qty()
return product_qty
def create_procurement(self, name, product):
values = {
'name': name,
'date_planned': fields.Datetime.now(),
'product_id': product.id,
'product_qty': 4.0,
'product_uom': product.uom_id.id,
'warehouse_id': self.env.ref('stock.warehouse0').id,
'location_id': self.stock_location_stock.id,
'route_ids': [
(4, self.env.ref('mrp.route_warehouse0_manufacture').id, 0)],
}
return self.env['procurement.order'].create(values)
def test_manufacture_with_forecast_stock(self):
"""
Test Manufacture mto with stock based on forecast quantity
and no link between sub assemblies MO's and Main MO raw material
"""
def test_manufacture(self):
self.warehouse.mrp_mto_mts_forecast_qty = True
procurement_fp = self.create_procurement('TEST/01', self.product_fp)
production_fp = procurement_fp.production_id
self.assertEqual(production_fp.state, 'confirmed')
self._update_product_qty(self.subproduct1, self.stock_location_stock,
2)
self._update_product_qty(self.subproduct2, self.stock_location_stock,
4)
production_fp.action_assign()
self.assertEqual(production_fp.state, 'confirmed')
self.production = self.production_model.create(
self._get_production_vals())
procurement_c1 = self.env['procurement.order'].search(
[('product_id', '=', self.product_c1.id),
('move_dest_id', 'in', production_fp.move_lines.ids)], limit=1)
self.assertEquals(len(procurement_c1), 1)
# Create MO and check it create sub assemblie MO.
self.production.action_assign()
procurement_c2 = self.env['procurement.order'].search(
[('product_id', '=', self.product_c2.id),
('move_dest_id', 'in', production_fp.move_lines.ids)], limit=1)
self.assertEquals(len(procurement_c2), 0)
self.assertEqual(self.production.availability, 'partially_available')
procurement_c1.run()
production_c1 = procurement_c1.production_id
self.assertEqual(production_c1.state, 'confirmed')
self.assertEquals(self.subproduct1.virtual_available, 0)
production_c1.action_assign()
self.assertEqual(production_c1.state, 'ready')
procurement_subproduct1 = self.env['procurement.order'].search(
[('product_id', '=', self.subproduct1.id),
('group_id', '=', self.production.procurement_group_id.id)])
procurement_c2 = self.env['procurement.order'].search(
[('product_id', '=', self.product_c2.id),
('move_dest_id', 'in', production_c1.move_lines.ids)], limit=1)
self.assertEquals(len(procurement_c2), 0)
self.assertEquals(len(procurement_subproduct1), 1)
self.assertEquals(procurement_subproduct1.product_qty, 3)
wizard = self.env['mrp.product.produce'].create({
'product_id': self.product_c1.id,
'product_qty': 1,
})
self.env['mrp.production'].action_produce(
production_c1.id, 1, 'consume_produce', wizard)
production_c1.refresh()
self.assertEqual(production_fp.state, 'confirmed')
production_sub1 = procurement_subproduct1.production_id
self.assertEqual(production_sub1.state, 'confirmed')
self.assertEqual(production_sub1.product_qty, 3)
wizard = self.env['mrp.product.produce'].create({
'product_id': self.product_c1.id,
'product_qty': 3,
})
self.env['mrp.production'].action_produce(
production_c1.id, 3, 'consume_produce', wizard)
production_c1.refresh()
self.assertEqual(production_fp.state, 'ready')
self._update_product_qty(self.subproduct1, self.stock_location_stock,
7)
# Create second MO and check it does not create procurement
self.production2 = self.production_model.create(
self._get_production_vals())
self.production2.action_assign()
procurement_subproduct1_2 = self.env['procurement.order'].search(
[('product_id', '=', self.subproduct1.id),
('group_id', '=', self.production2.procurement_group_id.id)])
self.assertEquals(len(procurement_subproduct1_2), 0)
self.assertEquals(self.production2.availability, 'assigned')
self.production2.do_unreserve()
self.assertEquals(self.subproduct1.virtual_available, 0)
self.production.action_assign()
# We check if first MO is able to assign it self even if it has
# previously generate procurements, it would not be the case in the
# other mode (without mrp_mto_mts_reservable_stock on warehouse)
self.assertEquals(self.production.availability, 'assigned')
self.assertEquals(self.subproduct1.virtual_available, 0)
def test_manufacture_with_reservable_stock(self):
"""
Test Manufacture mto with stock based on reservable stock
and there is a link between sub assemblies MO's and Main MO raw
materi al
"""
self._update_product_qty(self.subproduct1, self.stock_location_stock,
2)
self._update_product_qty(self.subproduct2, self.stock_location_stock,
4)
self.production = self.production_model.create(
self._get_production_vals())
self._update_product_qty(self.subproduct_1_1,
self.stock_location_stock, 50)
# Create MO and check it create sub assemblie MO.
self.production.action_assign()
self.assertEqual(self.production.state, 'confirmed')
procurement_sub1 = self.env['procurement.order'].search(
[('product_id', '=', self.subproduct1.id),
('move_dest_id', 'in', self.production.move_raw_ids.ids)])
self.assertEquals(len(procurement_sub1), 1)
procurement_sub2 = self.env['procurement.order'].search(
[('product_id', '=', self.subproduct2.id),
('move_dest_id', 'in', self.production.move_raw_ids.ids)])
self.assertEquals(len(procurement_sub2), 0)
production_sub1 = procurement_sub1.production_id
self.assertEqual(production_sub1.product_qty, 3)
production_sub1.action_assign()
self.assertEqual(production_sub1.availability, 'assigned')
wizard_obj = self.env['mrp.product.produce']
default_fields = ['lot_id', 'product_id', 'product_uom_id',
'product_tracking', 'consume_line_ids',
'production_id', 'product_qty', 'serial']
wizard_vals = wizard_obj.with_context(active_id=production_sub1.id).\
default_get(default_fields)
wizard = wizard_obj.create(wizard_vals)
wizard.do_produce()
self.assertTrue(production_sub1.check_to_done)
self.assertEquals(self.subproduct1.qty_available, 2)
production_sub1.button_mark_done()
self.assertEquals(self.subproduct1.qty_available, 5)
self.assertEqual(self.production.availability, 'assigned')

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Akretion
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_warehouse_mrp_with_stock" model="ir.ui.view">
<field name="model">stock.warehouse</field>
<field name="inherit_id" ref="mrp.view_warehouse_inherited"/>
<field name="arch" type="xml">
<field name="manufacture_to_resupply" position="after">
<field name="mrp_mto_mts_forecast_qty"/>
</field>
</field>
</record>
</odoo>