Merge pull request #216 from akretion/10-migrate-mrp_mto_with_stock

[10] migrate mrp mto with stock
This commit is contained in:
Pedro M. Baeza
2017-09-20 18:05:06 +02:00
committed by GitHub
12 changed files with 596 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
.. 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
==================
MRP MTO with Stock
==================
This module extends the functionality of Manufacturing to support the creation
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
=============
To configure this module, you need to:
#. Go to the products you want to follow this behaviour.
#. In the view form go to the tab *Inventory* and set the *Manufacturing
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 *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/10.0
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
------------
* John Walsh <John.Walsh@interclean.com>
* Lois Rilo <lois.rilo@eficent.com>
* Florian da Costa <florian.dacosta@akretion.com>
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.

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# Copyright 2015 John Walsh
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# Copyright 2015 John Walsh
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "MRP MTO with Stock",
"summary": "Fix Manufacturing orders to pull from stock until qty is "
"zero, and then create a procurement for them.",
"author": "John Walsh, Eficent, Odoo Community Association (OCA)",
"website": "https://odoo-community.org/",
"category": "Manufacturing",
"version": "10.0.1.0.0",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": ["mrp"],
"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

@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# Copyright 2015 John Walsh
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import mrp_production
from . import product_template
from . import stock_warehouse

View File

@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# Copyright 2015 John Walsh
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models
import logging
_logger = logging.getLogger(__name__)
class MrpProduction(models.Model):
_inherit = 'mrp.production'
@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 True"""
# reserve all that is available (standard behaviour):
res = super(MrpProduction, self).action_assign()
# try to create procurements:
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, 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'
group_id = move.group_id and move.group_id.id or False
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))
vals = {
'name': move.name + ':' + str(move.id),
'origin': origin,
'company_id': move.company_id and move.company_id.id or False,
'date_planned': move.date,
'product_id': move.product_id.id,
'product_qty': qty,
'product_uom': move.product_uom.id,
'location_id': move.location_id.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

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
mrp_mts_mto_location_ids = fields.Many2many(
comodel_name='stock.location',
string='Manufacturing MTO/MTS Locations',
help='These manufacturing locations will create procurements when '
'there is no stock availale in the source location.')

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

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_mrp_mto_with_stock

View File

@@ -0,0 +1,158 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
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']
self.bom_model = self.env['mrp.bom']
self.stock_location_stock = self.env.ref('stock.stock_location_stock')
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.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.main_bom = self.env.ref(
'mrp_mto_with_stock.mrp_bom_manuf_1')
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."""
product_qty = self.env['stock.change.product.qty'].create({
'location_id': location.id,
'product_id': product.id,
'new_quantity': quantity,
})
product_qty.change_product_qty()
return product_qty
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
"""
self.warehouse.mrp_mto_mts_forecast_qty = True
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())
# Create MO and check it create sub assemblie MO.
self.production.action_assign()
self.assertEqual(self.production.availability, 'partially_available')
self.assertEquals(self.subproduct1.virtual_available, 0)
procurement_subproduct1 = self.env['procurement.order'].search(
[('product_id', '=', self.subproduct1.id),
('group_id', '=', self.production.procurement_group_id.id)])
self.assertEquals(len(procurement_subproduct1), 1)
self.assertEquals(procurement_subproduct1.product_qty, 3)
production_sub1 = procurement_subproduct1.production_id
self.assertEqual(production_sub1.state, 'confirmed')
self.assertEqual(production_sub1.product_qty, 3)
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,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_template_property_form" model="ir.ui.view">
<field name="name">product.template.form - mrp_mto_with_stock
extension</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="stock.view_template_property_form"/>
<field name="arch" type="xml">
<field name="property_stock_inventory" position="after">
<field name="mrp_mts_mto_location_ids" widget="many2many_tags"
options="{'no_create': True}"/>
</field>
</field>
</record>
</odoo>

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>