From 411db0a9634ea64f2a28a63ae5d492217e221455 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Mon, 6 Aug 2018 11:09:46 +0200 Subject: [PATCH] [ADD] Select BoM by properties on the sale order line (feature removed from Odoo 9.0) --- mrp_property/README.rst | 79 +++++++++++++++++++ mrp_property/__init__.py | 2 + mrp_property/__manifest__.py | 27 +++++++ mrp_property/hooks.py | 13 ++++ mrp_property/models/__init__.py | 6 ++ mrp_property/models/mrp_bom.py | 53 +++++++++++++ mrp_property/models/mrp_property.py | 26 +++++++ mrp_property/models/mrp_property_group.py | 13 ++++ mrp_property/models/procurement_order.py | 22 ++++++ mrp_property/models/sale_order_line.py | 36 +++++++++ mrp_property/models/stock_move.py | 27 +++++++ mrp_property/readme/CONTRIBUTORS.rst | 2 + mrp_property/readme/CREDITS.rst | 1 + mrp_property/readme/DESCRIPTION.rst | 3 + mrp_property/security/ir.model.access.csv | 5 ++ mrp_property/tests/__init__.py | 1 + mrp_property/tests/test_mrp_property.py | 92 +++++++++++++++++++++++ mrp_property/views/mrp_bom.xml | 14 ++++ mrp_property/views/mrp_property.xml | 55 ++++++++++++++ mrp_property/views/mrp_property_group.xml | 41 ++++++++++ mrp_property/views/procurement_order.xml | 12 +++ mrp_property/views/sale_order.xml | 12 +++ 22 files changed, 542 insertions(+) create mode 100644 mrp_property/README.rst create mode 100644 mrp_property/__init__.py create mode 100644 mrp_property/__manifest__.py create mode 100644 mrp_property/hooks.py create mode 100644 mrp_property/models/__init__.py create mode 100644 mrp_property/models/mrp_bom.py create mode 100644 mrp_property/models/mrp_property.py create mode 100644 mrp_property/models/mrp_property_group.py create mode 100644 mrp_property/models/procurement_order.py create mode 100644 mrp_property/models/sale_order_line.py create mode 100644 mrp_property/models/stock_move.py create mode 100644 mrp_property/readme/CONTRIBUTORS.rst create mode 100644 mrp_property/readme/CREDITS.rst create mode 100644 mrp_property/readme/DESCRIPTION.rst create mode 100644 mrp_property/security/ir.model.access.csv create mode 100644 mrp_property/tests/__init__.py create mode 100644 mrp_property/tests/test_mrp_property.py create mode 100644 mrp_property/views/mrp_bom.xml create mode 100644 mrp_property/views/mrp_property.xml create mode 100644 mrp_property/views/mrp_property_group.xml create mode 100644 mrp_property/views/procurement_order.xml create mode 100644 mrp_property/views/sale_order.xml diff --git a/mrp_property/README.rst b/mrp_property/README.rst new file mode 100644 index 000000000..4801173d0 --- /dev/null +++ b/mrp_property/README.rst @@ -0,0 +1,79 @@ +================================== +MRP Properties on Sale Order Lines +================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge2| image:: https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github + :target: https://github.com/OCA/manufacture/tree/10.0/mrp_property + :alt: OCA/manufacture +.. |badge3| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/manufacture-10-0/manufacture-10-0-mrp_property + :alt: Translate me on Weblate +.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/129/10.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| + +This module allows sales users to add properties to sale order lines. The properties are propagated to the procurement order and then used to control the selection of the BoM. If there are properties set on a sale order line, the first BoM that has all those properties will be selected for the production order. If there are properties set on a sale order line and no such BoM can be found, but there is a BoM without any properties, that BoM will be returned. If no properties are set on a sale order line, no filter on properties will be applied even if there are BoMs both with and without properties for the given product or product variant. + +Installation of this module will activate the sale order line form view. + +**Table of contents** + +.. contents:: + :local: + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Odoo S.A. +* Opener B.V. + +Contributors +~~~~~~~~~~~~ + +* Odoo S.A. +* Stefan Rijnhart + +Other credits +~~~~~~~~~~~~~ + +This functionality was forward ported from Odoo 9.0. It was removed from Odoo in the development cycle of Odoo 10.0, in this commit: https://github.com/odoo/odoo/commit/2ddc35a530 + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/manufacture `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mrp_property/__init__.py b/mrp_property/__init__.py new file mode 100644 index 000000000..cc6b6354a --- /dev/null +++ b/mrp_property/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import post_init_hook diff --git a/mrp_property/__manifest__.py b/mrp_property/__manifest__.py new file mode 100644 index 000000000..b328ef7fe --- /dev/null +++ b/mrp_property/__manifest__.py @@ -0,0 +1,27 @@ +# coding: utf-8 +# Copyright 2008-2016 Odoo S.A. +# Copyright 2018 Opener B.V. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +{ + 'name': "MRP Properties on Sale Order Lines", + 'summary': 'Control BoM selection from properties on sale order lines', + 'version': '10.0.1.0.0', + 'category': 'Manufacturing', + 'author': "Odoo S.A.,Opener B.V.,Odoo Community Association (OCA)", + 'development_status': 'stable', + 'website': 'https://github.com/oca/manufacture', + 'license': 'LGPL-3', + 'depends': [ + 'sale_mrp', + ], + 'data': [ + 'views/mrp_bom.xml', + 'views/mrp_property.xml', + 'views/mrp_property_group.xml', + 'views/procurement_order.xml', + 'views/sale_order.xml', + 'security/ir.model.access.csv', + ], + 'installable': True, + 'post_init_hook': 'post_init_hook', +} diff --git a/mrp_property/hooks.py b/mrp_property/hooks.py new file mode 100644 index 000000000..0d9d27048 --- /dev/null +++ b/mrp_property/hooks.py @@ -0,0 +1,13 @@ +# coding: utf-8 +# Copyright 2018 Opener B.V. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import SUPERUSER_ID, api + + +def post_init_hook(cr, pool): + """ Add everyone to the properties group by default upon installation + """ + env = api.Environment(cr, SUPERUSER_ID, {}) + config = env['sale.config.settings'].create({}) + config.group_mrp_properties = 1 + config.execute() diff --git a/mrp_property/models/__init__.py b/mrp_property/models/__init__.py new file mode 100644 index 000000000..04ff5e196 --- /dev/null +++ b/mrp_property/models/__init__.py @@ -0,0 +1,6 @@ +from . import mrp_bom +from . import mrp_property +from . import mrp_property_group +from . import procurement_order +from . import sale_order_line +from . import stock_move diff --git a/mrp_property/models/mrp_bom.py b/mrp_property/models/mrp_bom.py new file mode 100644 index 000000000..c504ba63a --- /dev/null +++ b/mrp_property/models/mrp_bom.py @@ -0,0 +1,53 @@ +# coding: utf-8 +# Copyright 2008 - 2016 Odoo S.A. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import api, models, fields + + +class MrpBom(models.Model): + _inherit = 'mrp.bom' + + property_ids = fields.Many2many( + 'mrp.property', + 'mrp_bom_mrp_property_rel', + 'mrp_bom_id', 'mrp_property_id', + string='Properties', + help=("If a production product is manufactured for a sale order, the " + "BoM that has the same properties as the sale order line will " + "be selected (a BoM with no properties at all could be selected " + "as a fallback.")) + + @api.model + def search(self, args, offset=0, limit=None, order=None, count=False): + """ If limit is set to 1 and property_ids is set in the context, search + a BoM which has all properties specified, or if you can not find one, + you return a BoM without any properties with the lowest sequence. """ + check_properties = False + if limit == 1 and self.env.context.get('property_ids'): + check_properties = True + limit = None + boms = super(MrpBom, self).search( + args, offset=offset, limit=limit, order=order, count=count) + if check_properties: + bom_empty_prop = self.env['mrp.bom'] + property_ids = set(self.env.context['property_ids']) + for bom in boms: + if bom.property_ids: + if not set(bom.property_ids.ids) - property_ids: + return bom + elif not bom_empty_prop: + bom_empty_prop = bom + return bom_empty_prop + return boms + + @api.model + def _bom_find(self, product_tmpl=None, product=None, picking_type=None, + company_id=False): + """ If property_ids are set in the context at this point, add an + additional value in the context that triggers the filter on these + properties in this model's search method """ + if self.env.context.get('property_ids'): + self = self.with_context(check_properties=True) + return super(MrpBom, self)._bom_find( + product_tmpl=product_tmpl, product=product, + picking_type=picking_type, company_id=company_id) diff --git a/mrp_property/models/mrp_property.py b/mrp_property/models/mrp_property.py new file mode 100644 index 000000000..5fbe79da5 --- /dev/null +++ b/mrp_property/models/mrp_property.py @@ -0,0 +1,26 @@ +# coding: utf-8 +# Copyright 2008 - 2016 Odoo S.A. +# Copyright 2018 Opener B.V. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class MrpProperty(models.Model): + """ Property to control BOM selection from the sale order """ + _name = 'mrp.property' + _description = 'MRP Property' + + name = fields.Char(required=True) + group_id = fields.Many2one( + 'mrp.property.group', 'Property Group', required=True) + description = fields.Text() + + @api.multi + def unlink(self): + """ Restrict the removal of properties that are in use """ + if self.env['sale.order.line'].sudo().search( + [('property_ids', 'in', self.ids)]): + raise UserError('You cannot delete this property, because it has ' + 'been assigned to a sale order line.') + return super(MrpProperty, self).unlink() diff --git a/mrp_property/models/mrp_property_group.py b/mrp_property/models/mrp_property_group.py new file mode 100644 index 000000000..265574b55 --- /dev/null +++ b/mrp_property/models/mrp_property_group.py @@ -0,0 +1,13 @@ +# coding: utf-8 +# Copyright 2008 - 2016 Odoo S.A. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import fields, models + + +class MrpPropertyGroup(models.Model): + """ Group of mrp properties """ + _name = 'mrp.property.group' + _description = 'Property Group' + + name = fields.Char(required=True) + description = fields.Text() diff --git a/mrp_property/models/procurement_order.py b/mrp_property/models/procurement_order.py new file mode 100644 index 000000000..68d65a9b4 --- /dev/null +++ b/mrp_property/models/procurement_order.py @@ -0,0 +1,22 @@ +# coding: utf-8 +# Copyright 2008 - 2016 Odoo S.A. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import api, fields, models + + +class ProcurementOrder(models.Model): + _inherit = 'procurement.order' + + property_ids = fields.Many2many( + 'mrp.property', 'procurement_property_rel', + 'procurement_id', 'property_id', + string='Properties', + help=("The BoM that has the same properties as this procurement will " + "be selected unless there is a BoM with no properties at all.")) + + @api.multi + def _get_matching_bom(self): + """ Inject property ids in the context, to be honoured in the + production model's search method """ + return super(ProcurementOrder, self.with_context( + property_ids=self.property_ids.ids))._get_matching_bom() diff --git a/mrp_property/models/sale_order_line.py b/mrp_property/models/sale_order_line.py new file mode 100644 index 000000000..975b3f5f2 --- /dev/null +++ b/mrp_property/models/sale_order_line.py @@ -0,0 +1,36 @@ +# coding: utf-8 +# Copyright 2008 - 2016 Odoo S.A. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + property_ids = fields.Many2many( + 'mrp.property', 'sale_order_line_property_rel', 'order_id', + 'property_id', 'Properties', readonly=True, + help=("If a production product is manufactured for this sale order, " + "the BoM that has the same properties as the sale order line " + "will be selected (a BoM with no properties at all could be " + "selected as a fallback"), + states={'draft': [('readonly', False)], + 'sent': [('readonly', False)]}) + + @api.multi + def _prepare_order_line_procurement(self, group_id=False): + """ Add the properties of the sale order line to the procurement + that is generated from it """ + vals = super(SaleOrderLine, self)._prepare_order_line_procurement( + group_id=group_id) + vals['property_ids'] = [(6, 0, self.property_ids.ids)] + return vals + + @api.multi + def _get_delivered_qty(self): + """ Make sure that the correct phantom bom is selected in the super + method (if any) """ + self.ensure_one() + if self.property_ids: + self = self.with_context(property_ids=self.property_ids.ids) + return super(SaleOrderLine, self)._get_delivered_qty() diff --git a/mrp_property/models/stock_move.py b/mrp_property/models/stock_move.py new file mode 100644 index 000000000..3ad5f66b8 --- /dev/null +++ b/mrp_property/models/stock_move.py @@ -0,0 +1,27 @@ +# coding: utf-8 +# Copyright 2008 - 2016 Odoo S.A. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import api, models + + +class StockMove(models.Model): + _inherit = 'stock.move' + + @api.multi + def _prepare_procurement_from_move(self): + """ The procurement that creates the MO might not be the original + procurement. Therefore, propagate the properties further down. """ + res = super(StockMove, self)._prepare_procurement_from_move() + if self.procurement_id.property_ids: + res['property_ids'] = [ + (6, 0, self.procurement_id.property_ids.ids)] + return res + + @api.multi + def action_explode(self): + """ Pass the properties from the procurement in the context for the + selection of the right BoM """ + properties = self.procurement_id.sale_line_id.property_ids + if properties: + self = self.with_context(property_ids=properties.ids) + return super(StockMove, self).action_explode() diff --git a/mrp_property/readme/CONTRIBUTORS.rst b/mrp_property/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..a7d5077ec --- /dev/null +++ b/mrp_property/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Odoo S.A. +* Stefan Rijnhart diff --git a/mrp_property/readme/CREDITS.rst b/mrp_property/readme/CREDITS.rst new file mode 100644 index 000000000..55a8ce351 --- /dev/null +++ b/mrp_property/readme/CREDITS.rst @@ -0,0 +1 @@ +This functionality was forward ported from Odoo 9.0. It was removed from Odoo in the development cycle of Odoo 10.0, in this commit: https://github.com/odoo/odoo/commit/2ddc35a530 diff --git a/mrp_property/readme/DESCRIPTION.rst b/mrp_property/readme/DESCRIPTION.rst new file mode 100644 index 000000000..5826cb634 --- /dev/null +++ b/mrp_property/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module allows sales users to add properties to sale order lines. The properties are propagated to the procurement order and then used to control the selection of the BoM. If there are properties set on a sale order line, the first BoM that has all those properties will be selected for the production order. If there are properties set on a sale order line and no such BoM can be found, but there is a BoM without any properties, that BoM will be returned. If no properties are set on a sale order line, no filter on properties will be applied even if there are BoMs both with and without properties for the given product or product variant. + +Installation of this module will activate the sale order line form view. diff --git a/mrp_property/security/ir.model.access.csv b/mrp_property/security/ir.model.access.csv new file mode 100644 index 000000000..298b7b642 --- /dev/null +++ b/mrp_property/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mrp_property_group_manager,mrp.property.group,model_mrp_property_group,stock.group_stock_manager,1,1,1,1 +access_mrp_property_manager,mrp.property,model_mrp_property,stock.group_stock_manager,1,1,1,1 +access_mrp_property_group,mrp.property.group,model_mrp_property_group,base.group_user,1,0,0,0 +access_mrp_property,mrp.property,model_mrp_property,base.group_user,1,0,0,0 diff --git a/mrp_property/tests/__init__.py b/mrp_property/tests/__init__.py new file mode 100644 index 000000000..8d2bb9b7f --- /dev/null +++ b/mrp_property/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mrp_property diff --git a/mrp_property/tests/test_mrp_property.py b/mrp_property/tests/test_mrp_property.py new file mode 100644 index 000000000..b816b8ab4 --- /dev/null +++ b/mrp_property/tests/test_mrp_property.py @@ -0,0 +1,92 @@ +# coding: utf-8 +# Copyright 2018 Opener B.V. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.tests.common import SavepointCase +from odoo.exceptions import UserError + + +class TestBomProperty(SavepointCase): + def setUp(self): + super(TestBomProperty, self).setUp() + self.product = self.env.ref('product.product_product_3') + self.group = self.env['mrp.property.group'].create({'name': __name__}) + self.property1 = self.env['mrp.property'].create({ + 'name': 'prop1', + 'group_id': self.group.id, + }) + self.property2 = self.env['mrp.property'].create({ + 'name': 'prop2', + 'group_id': self.group.id, + }) + self.bom_without_properties = self.env.ref('mrp.mrp_bom_manufacture') + self.bom_with_properties = self.env.ref( + 'mrp.mrp_bom_manufacture').copy({ + 'sequence': '999', + 'property_ids': + [(6, 0, [self.property1.id, self.property2.id])]}) + partner = self.env.ref('base.res_partner_address_7') + self.order = self.env['sale.order'].create({ + 'partner_id': partner.id, + 'partner_invoice_id': partner.id, + 'partner_shipping_id': partner.id, + 'pricelist_id': self.env.ref('product.list0').id, + }) + self.line = self.env['sale.order.line'].create({ + 'order_id': self.order.id, + 'product_id': self.product.id, + 'product_uom_qty': 2, + }) + self.last_production_id = ( + self.env['mrp.production'].search( + [('product_id', '=', self.product.id)], + order='id desc', limit=1).id or 0) + + def test_01_no_properties(self): + """ BoM with the lowest sequence is selected for a line w/o properties + """ + self.order.action_confirm() + production = self.env['mrp.production'].search( + [('product_id', '=', self.product.id), + ('id', '>', self.last_production_id)]) + self.assertTrue(production) + self.assertEqual(production.bom_id, self.bom_without_properties) + + def test_02_one_property(self): + """ Without a BoM with all the properties, the fallback is selected """ + self.line.property_ids = self.property1 + self.order.action_confirm() + production = self.env['mrp.production'].search( + [('product_id', '=', self.product.id), + ('id', '>', self.last_production_id)]) + self.assertTrue(production) + self.assertEqual( + production.bom_id, self.bom_without_properties) + + def test_03_two_properties(self): + """ The BoM with all the properties is selected """ + self.line.property_ids = self.property1 + self.property2 + self.order.action_confirm() + production = self.env['mrp.production'].search( + [('product_id', '=', self.product.id), + ('id', '>', self.last_production_id)]) + self.assertTrue(production) + self.assertEqual( + production.bom_id, self.bom_with_properties) + + def test_04_one_property_no_fallback(self): + """ Without a fallback BoM, the procurement cannot proceed """ + self.line.property_ids = self.property1 + self.bom_without_properties.active = False + self.order.action_confirm() + production = self.env['mrp.production'].search( + [('product_id', '=', self.product.id), + ('id', '>', self.last_production_id)]) + self.assertFalse(production) + + def test_05_delete_properties(self): + """ Cannot delete properties that are in use """ + self.line.property_ids = self.property1 + self.property2 + with self.assertRaises(UserError): + self.property2.unlink() + self.line.property_ids = self.property1 + self.property2.unlink() diff --git a/mrp_property/views/mrp_bom.xml b/mrp_property/views/mrp_bom.xml new file mode 100644 index 000000000..2118be40d --- /dev/null +++ b/mrp_property/views/mrp_bom.xml @@ -0,0 +1,14 @@ + + + + mrp.bom + + + + + + + + + diff --git a/mrp_property/views/mrp_property.xml b/mrp_property/views/mrp_property.xml new file mode 100644 index 000000000..9f923cad7 --- /dev/null +++ b/mrp_property/views/mrp_property.xml @@ -0,0 +1,55 @@ + + + + mrp.property + + + + + + + + + + + mrp.property.search + mrp.property + + + + + + + + + + + + + Properties + ir.actions.act_window + mrp.property + form + tree + + +

+ Click to create a new property. +

+ The Properties in Odoo are used to select the right bill of + materials for manufacturing a product when you have different + ways of building the same product. You can assign several + properties to each bill of materials. When a salesperson + creates a sales order, they can relate it to several properties + and Odoo will automatically select the BoM to use according + the needs. +

+
+
+ + +
diff --git a/mrp_property/views/mrp_property_group.xml b/mrp_property/views/mrp_property_group.xml new file mode 100644 index 000000000..b0d5a6220 --- /dev/null +++ b/mrp_property/views/mrp_property_group.xml @@ -0,0 +1,41 @@ + + + + mrp.property.group + + + + + + + + + + Property Groups + ir.actions.act_window + mrp.property.group + form + tree + +

+ Click to create a group of properties. +

+ Define specific property groups that can be assigned to your + bill of materials and sales orders. Properties allows Odoo + to automatically select the right bill of materials according + to properties selected in the sales order by salesperson. +

+ For instance, in the property group "Warranty", you an have + two properties: 1 year warranty, 3 years warranty. Depending + on the propoerties selected in the sales order, Odoo will + schedule a production using the matching bill of materials. +

+
+
+ + +
diff --git a/mrp_property/views/procurement_order.xml b/mrp_property/views/procurement_order.xml new file mode 100644 index 000000000..a3f681c7d --- /dev/null +++ b/mrp_property/views/procurement_order.xml @@ -0,0 +1,12 @@ + + + + procurement.order + + + + + + + + diff --git a/mrp_property/views/sale_order.xml b/mrp_property/views/sale_order.xml new file mode 100644 index 000000000..2fdc1de72 --- /dev/null +++ b/mrp_property/views/sale_order.xml @@ -0,0 +1,12 @@ + + + + sale.order + + + + + + + +