diff --git a/mrp_mto_with_stock/README.rst b/mrp_mto_with_stock/README.rst new file mode 100644 index 000000000..b004f5b12 --- /dev/null +++ b/mrp_mto_with_stock/README.rst @@ -0,0 +1,73 @@ +.. 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 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. + +Configuration +============= + +To configure this module, you need to: + +#. Activate the developer mode. +#. 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. + +Usage +===== + +To use this module, you need to: + +#. Go to *Manufacturing* and create a Manufacturing Order. +#. Click on *Confirm Production*. +#. Click on *Reserve*. + +.. 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 + +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 +------------ + +* John Walsh +* Lois Rilo + +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_mto_with_stock/__init__.py b/mrp_mto_with_stock/__init__.py new file mode 100644 index 000000000..a7129c69a --- /dev/null +++ b/mrp_mto_with_stock/__init__.py @@ -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 diff --git a/mrp_mto_with_stock/__openerp__.py b/mrp_mto_with_stock/__openerp__.py new file mode 100644 index 000000000..08fb4650a --- /dev/null +++ b/mrp_mto_with_stock/__openerp__.py @@ -0,0 +1,22 @@ +# -*- 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": "9.0.1.0.0", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": ["mrp"], + "data": [ + "views/product_template_view.xml", + "views/mrp_production_view.xml", + ], +} diff --git a/mrp_mto_with_stock/models/__init__.py b/mrp_mto_with_stock/models/__init__.py new file mode 100644 index 000000000..c4a684fa2 --- /dev/null +++ b/mrp_mto_with_stock/models/__init__.py @@ -0,0 +1,7 @@ +# -*- 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 diff --git a/mrp_mto_with_stock/models/mrp_production.py b/mrp_mto_with_stock/models/mrp_production.py new file mode 100644 index 000000000..b8eabe2eb --- /dev/null +++ b/mrp_mto_with_stock/models/mrp_production.py @@ -0,0 +1,87 @@ +# -*- 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 openerp import api, models, _ +from openerp.exceptions import UserError +import logging +_logger = logging.getLogger(__name__) + + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + + @api.multi + def action_mass_assign(self): + if any([x != 'confirmed' for x in self.mapped('state')]): + raise UserError(_( + "All Manufacturing Orders must be confirmed.")) + return self.action_assign() + + @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""" + # 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: + for move in production.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: + # 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_obj.split( + move, + 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) + self.env['procurement.order'].create(proc_dict) + return res + + def _prepare_mto_procurement(self, move, qty): + """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)) + return { + '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, + 'move_dest_id': move.id, + 'group_id': group_id, + 'route_ids': [(6, 0, route_ids.ids)], + 'warehouse_id': warehouse_id, + 'priority': move.priority, + } diff --git a/mrp_mto_with_stock/models/product_template.py b/mrp_mto_with_stock/models/product_template.py new file mode 100644 index 000000000..11099600d --- /dev/null +++ b/mrp_mto_with_stock/models/product_template.py @@ -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 openerp 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.') diff --git a/mrp_mto_with_stock/tests/__init__.py b/mrp_mto_with_stock/tests/__init__.py new file mode 100644 index 000000000..f8065ee2e --- /dev/null +++ b/mrp_mto_with_stock/tests/__init__.py @@ -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 diff --git a/mrp_mto_with_stock/tests/test_mrp_mto_with_stock.py b/mrp_mto_with_stock/tests/test_mrp_mto_with_stock.py new file mode 100644 index 000000000..464195aa9 --- /dev/null +++ b/mrp_mto_with_stock/tests/test_mrp_mto_with_stock.py @@ -0,0 +1,139 @@ +# -*- 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 openerp.tests.common import TransactionCase +from openerp import fields + + +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.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.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.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 _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 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(self): + + procurement_fp = self.create_procurement('TEST/01', self.product_fp) + production_fp = procurement_fp.production_id + self.assertEqual(production_fp.state, 'confirmed') + + production_fp.action_assign() + self.assertEqual(production_fp.state, 'confirmed') + + 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) + + 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) + + procurement_c1.run() + production_c1 = procurement_c1.production_id + self.assertEqual(production_c1.state, 'confirmed') + + production_c1.action_assign() + self.assertEqual(production_c1.state, 'ready') + + 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) + + 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') + + 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') diff --git a/mrp_mto_with_stock/views/mrp_production_view.xml b/mrp_mto_with_stock/views/mrp_production_view.xml new file mode 100644 index 000000000..6d25e52e1 --- /dev/null +++ b/mrp_mto_with_stock/views/mrp_production_view.xml @@ -0,0 +1,28 @@ + + + + + + + Reserve MO + True + ir.actions.server + + code + self.action_mass_assign(cr, uid, context.get('active_ids', []), context=context) + + + + mrp.production.action - mass assign + + + action + + mrp.production + client_action_multi + + + diff --git a/mrp_mto_with_stock/views/product_template_view.xml b/mrp_mto_with_stock/views/product_template_view.xml new file mode 100644 index 000000000..cbe221f8f --- /dev/null +++ b/mrp_mto_with_stock/views/product_template_view.xml @@ -0,0 +1,20 @@ + + + + + + + product.template.form - mrp_mto_with_stock + extension + product.template + + + + + + + + +