From 77d61949a0e66774b8b2d3c5193c012ffb5a9d3e Mon Sep 17 00:00:00 2001 From: Cyril Gaudin Date: Mon, 18 Apr 2016 13:16:21 +0200 Subject: [PATCH] dismantling bom: allow user to choose main component. --- mrp_bom_dismantling/README.rst | 1 + mrp_bom_dismantling/__openerp__.py | 2 + mrp_bom_dismantling/i18n/de.po | 139 +++++++++++++++++- mrp_bom_dismantling/models/__init__.py | 1 + mrp_bom_dismantling/models/mrp_bom.py | 60 ++++++-- mrp_bom_dismantling/models/res_config.py | 30 ++++ mrp_bom_dismantling/tests/test_bom.py | 123 +++++++++++++++- mrp_bom_dismantling/views/mrp_bom.xml | 2 +- mrp_bom_dismantling/views/res_config.xml | 14 ++ mrp_bom_dismantling/wizards/__init__.py | 1 + .../wizards/dismantling_product_choice.py | 48 ++++++ .../wizards/dismantling_product_choice.xml | 17 +++ 12 files changed, 413 insertions(+), 25 deletions(-) create mode 100644 mrp_bom_dismantling/models/res_config.py create mode 100644 mrp_bom_dismantling/views/res_config.xml create mode 100644 mrp_bom_dismantling/wizards/dismantling_product_choice.py create mode 100644 mrp_bom_dismantling/wizards/dismantling_product_choice.xml diff --git a/mrp_bom_dismantling/README.rst b/mrp_bom_dismantling/README.rst index ff3c7b758..7be37077b 100644 --- a/mrp_bom_dismantling/README.rst +++ b/mrp_bom_dismantling/README.rst @@ -14,6 +14,7 @@ Usage * On BOM form view, click on "Create dismantling BOM" button and it will reverse your BOM. +* Configure in settings if you want choose main component when reversing BOM or not. * In Manufacturing -> Products, there is a new menu "Dismantling". * In dismantling tree view, you can search by dismantled product. * On BOM form view, there is a new button "Create Manufacturing Order". diff --git a/mrp_bom_dismantling/__openerp__.py b/mrp_bom_dismantling/__openerp__.py index 53f0dc284..f0739421f 100644 --- a/mrp_bom_dismantling/__openerp__.py +++ b/mrp_bom_dismantling/__openerp__.py @@ -18,6 +18,8 @@ "data": [ "views/mrp_bom.xml", "views/product_template.xml", + "views/res_config.xml", + "wizards/dismantling_product_choice.xml", "wizards/mrp_product_produce.xml", ], } diff --git a/mrp_bom_dismantling/i18n/de.po b/mrp_bom_dismantling/i18n/de.po index b8a721957..63015d9b6 100644 --- a/mrp_bom_dismantling/i18n/de.po +++ b/mrp_bom_dismantling/i18n/de.po @@ -23,6 +23,37 @@ msgstr "" msgid "Bill of Material" msgstr "Stücklisten" +#. module: mrp_bom_dismantling +#: model:ir.ui.view,arch_db:mrp_bom_dismantling.view_mrp_config +msgid "Bill of Materials" +msgstr "Stücklisten" + +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantling_product_choice_bom_id +msgid "Bom id" +msgstr "Bom id" + +#. module: mrp_bom_dismantling +#: model:ir.ui.view,arch_db:mrp_bom_dismantling.view_mrp_bom_dismantling_product_choice_wizard +msgid "Cancel" +msgstr "Abbrechen" + +#. module: mrp_bom_dismantling +#: code:addons/mrp_bom_dismantling/models/mrp_bom.py:49 +#, python-format +msgid "Choose main compoment" +msgstr "Hauptkomponente wählen" + +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantling_product_choice_component_id +msgid "Component id" +msgstr "Component id" + +#. module: mrp_bom_dismantling +#: model:ir.ui.view,arch_db:mrp_bom_dismantling.view_mrp_bom_dismantling_product_choice_wizard +msgid "Confirm" +msgstr "Erstellen" + #. module: mrp_bom_dismantling #: model:ir.ui.view,arch_db:mrp_bom_dismantling.mrp_bom_form_view msgid "Create Manufacturing Order" @@ -34,14 +65,16 @@ msgid "Create dismantling BoM" msgstr "Zerlegung Stücklisten erstellen" #. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantling_product_choice_create_uid #: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produced_line_create_uid msgid "Created by" -msgstr "" +msgstr "Angelegt von" #. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantling_product_choice_create_date #: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produced_line_create_date msgid "Created on" -msgstr "" +msgstr "Angelegt am" #. module: mrp_bom_dismantling #: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantled_product_id @@ -55,10 +88,75 @@ msgstr "Produkt zu zerlegen" msgid "Dismantling" msgstr "Zerlegen" +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_config_settings_dismantling_product_choice +msgid "Dismantling BOM" +msgstr "Zerlegung Stücklisten" + #. module: mrp_bom_dismantling #: sql_constraint:mrp.bom:0 msgid "Dismantling BoM should have a dismantled product." -msgstr "" +msgstr "Dismantling BoM should have a dismantled product." + +#. module: mrp_bom_dismantling +#: model:ir.ui.view,arch_db:mrp_bom_dismantling.view_mrp_bom_dismantling_product_choice_wizard +msgid "Dismantling product choice" +msgstr "Dismantling product choice" + +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantling_product_choice_display_name +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produced_line_display_name +msgid "Display Name" +msgstr "Angezeigter Name" + +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantling_product_choice_id +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produced_line_id +msgid "ID" +msgstr "ID" + +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantling_product_choice___last_update +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produced_line___last_update +msgid "Last Modified on" +msgstr "Zuletzt geändert am" + +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantling_product_choice_write_uid +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produced_line_write_uid +msgid "Last Updated by" +msgstr "Zuletzt aktualisiert durch" + +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantling_product_choice_write_date +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produced_line_write_date +msgid "Last Updated on" +msgstr "Zuletzt aktualisiert am" + +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produced_line_lot_id +msgid "Lot" +msgstr "Fertigungslos" + +#. module: mrp_bom_dismantling +#: selection:mrp.config.settings,dismantling_product_choice:0 +msgid "Main BOM product will be set randomly" +msgstr "Main BOM product will be set randomly" + +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produced_line_move_id +msgid "Move id" +msgstr "Move id" + +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produce_move_lot_ids +msgid "Move lot ids" +msgstr "Move lot ids" + +#. module: mrp_bom_dismantling +#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produced_line_produce_id +msgid "Produce" +msgstr "Produziere" #. module: mrp_bom_dismantling #: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_product_produced_line_display_name @@ -114,7 +212,7 @@ msgstr "Produkt" #. module: mrp_bom_dismantling #: model:ir.model,name:mrp_bom_dismantling.model_mrp_product_produce msgid "Product Produce" -msgstr "" +msgstr "Produkt fertigen" #. module: mrp_bom_dismantling #: model:ir.model,name:mrp_bom_dismantling.model_product_template @@ -124,14 +222,41 @@ msgstr "Produktvorlage" #. module: mrp_bom_dismantling #: model:ir.ui.view,arch_db:mrp_bom_dismantling.view_mrp_product_produce_wizard msgid "Products to produce lots" -msgstr "" +msgstr "Products to produce lots" #. module: mrp_bom_dismantling #: model:ir.model,name:mrp_bom_dismantling.model_stock_move msgid "Stock Move" -msgstr "" +msgstr "Lagerbuchung" + +#. module: mrp_bom_dismantling +#: code:addons/mrp_bom_dismantling/wizards/dismantling_product_choice.py:34 +#, python-format +msgid "This BoM does not have components." +msgstr "This BoM does not have components." + +#. module: mrp_bom_dismantling +#: selection:mrp.config.settings,dismantling_product_choice:0 +msgid "User have to choose which component to set as main BOM product" +msgstr "User have to choose which component to set as main BOM product" + +#. module: mrp_bom_dismantling +#: model:ir.model,name:mrp_bom_dismantling.model_mrp_bom_dismantling_product_choice +msgid "mrp.bom.dismantling_product_choice" +msgstr "mrp.bom.dismantling_product_choice" + +#. module: mrp_bom_dismantling +#: model:ir.model,name:mrp_bom_dismantling.model_mrp_bom_dismantling_product_choice_line +msgid "mrp.bom.dismantling_product_choice.line" +msgstr "mrp.bom.dismantling_product_choice.line" + +#. module: mrp_bom_dismantling +#: model:ir.model,name:mrp_bom_dismantling.model_mrp_config_settings +msgid "mrp.config.settings" +msgstr "mrp.config.settings" #. module: mrp_bom_dismantling #: model:ir.model,name:mrp_bom_dismantling.model_mrp_product_produced_line msgid "mrp.product.produced.line" -msgstr "" +msgstr "mrp.product.produced.line" + diff --git a/mrp_bom_dismantling/models/__init__.py b/mrp_bom_dismantling/models/__init__.py index 07cc00ac6..c5462f06d 100644 --- a/mrp_bom_dismantling/models/__init__.py +++ b/mrp_bom_dismantling/models/__init__.py @@ -5,4 +5,5 @@ from . import mrp_bom from . import product_product from . import product_template +from . import res_config from . import stock_move diff --git a/mrp_bom_dismantling/models/mrp_bom.py b/mrp_bom_dismantling/models/mrp_bom.py index 991df667c..8a7792224 100644 --- a/mrp_bom_dismantling/models/mrp_bom.py +++ b/mrp_bom_dismantling/models/mrp_bom.py @@ -38,24 +38,56 @@ class MrpBom(models.Model): return self._get_form_view('mrp.production', production) @api.multi - def create_dismantling_bom(self): + def action_create_dismantling_bom(self): + """ Check dismantling_product_choice config and open choice wizard + if needed or directly call create_dismantling_bom. + """ + config_name = 'mrp.bom.dismantling.product_choice' + if self.env['ir.config_parameter'].get_param(config_name): + return { + 'type': 'ir.actions.act_window', + 'name': _('Choose main compoment'), + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'mrp.bom.dismantling_product_choice', + 'target': 'new', + 'context': self.env.context + } + + else: + return self.create_dismantling_bom() + + @api.multi + def create_dismantling_bom(self, main_component=None): """ Create a dismantling BoM based on this BoM + + If *main_component* is not None, this component will be set as main + product in dismantling bom. + + Else first component will be taken (sorted by Id). + + :type main_component: product_product + :rtype: dict """ self.ensure_one() self._check_bom_validity(check_dismantling=True) product = self._get_bom_product() - components = self._get_components_tuples() + components = self._get_components_needs() - # Create the BoM on first component (sorted by Id) - first_component, first_component_needs = components.pop(0) + # If no main component, take first sorted by Id + if not main_component: + main_component = sorted(components.keys(), key=lambda c: c.id)[0] + + # Create the BoM on main component + main_component_needs = components.pop(main_component) dismantling_bom = self.create({ - 'product_tmpl_id': first_component.product_tmpl_id.id, - 'product_id': first_component.id, + 'product_tmpl_id': main_component.product_tmpl_id.id, + 'product_id': main_component.id, 'dismantling': True, 'dismantled_product_id': product.id, - 'product_qty': first_component_needs, + 'product_qty': main_component_needs, }) # Create BoM line for self.product_tmpl_id @@ -68,7 +100,7 @@ class MrpBom(models.Model): # Add others component as By-products subproduct_model = self.env['mrp.subproduct'] - for component, needs in components: + for component, needs in components.items(): subproduct_model.create({ 'bom_id': dismantling_bom.id, 'product_id': component.id, @@ -111,19 +143,17 @@ class MrpBom(models.Model): if warning: raise exceptions.UserError(_(warning)) - def _get_components_tuples(self): - """ Return this BoM components and their needed qties - sorted by component id. + def _get_components_needs(self): + """ Return this BoM components and their needed qties. - The result is like [(component_1, 1), (component_2, 5), ...] + The result is like {component_1: 1, component_2: 5, ...} - :rtype: list of tuple + :rtype: dict(product_product, float) """ components = self.product_id._get_components_needs( product=self.product_id, bom=self ) - components = sorted(components.items(), key=lambda t: t[0].id) - return components + return dict(components) def _get_bom_product(self): """ Get the product of this BoM. diff --git a/mrp_bom_dismantling/models/res_config.py b/mrp_bom_dismantling/models/res_config.py new file mode 100644 index 000000000..1232db966 --- /dev/null +++ b/mrp_bom_dismantling/models/res_config.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# © 2016 Cyril Gaudin (Camptocamp) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import api, fields, models + + +class MrpConfigSettings(models.TransientModel): + """ Add settings for dismantling BOM. + """ + _inherit = 'mrp.config.settings' + + dismantling_product_choice = fields.Selection([ + (0, "Main BOM product will be set randomly"), + (1, "User have to choose which component to set as main BOM product") + ], "Dismantling BOM") + + @api.multi + def get_default_dismantling_product_choice(self, fields): + product_choice = self.env["ir.config_parameter"].get_param( + 'mrp.bom.dismantling.product_choice', default=0 + ) + return {'dismantling_product_choice': product_choice} + + @api.multi + def set_dismantling_product_choice(self): + self.env["ir.config_parameter"].set_param( + 'mrp.bom.dismantling.product_choice', + self.dismantling_product_choice + ) diff --git a/mrp_bom_dismantling/tests/test_bom.py b/mrp_bom_dismantling/tests/test_bom.py index f53c6469b..a5dcffb15 100644 --- a/mrp_bom_dismantling/tests/test_bom.py +++ b/mrp_bom_dismantling/tests/test_bom.py @@ -3,6 +3,8 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from openerp import exceptions + +from openerp.exceptions import UserError from openerp.tests import TransactionCase @@ -15,11 +17,12 @@ class TestBom(TransactionCase): self.bom_model = self.env['mrp.bom'] self.bom_line_model = self.env['mrp.bom.line'] self.mrp_production_model = self.env['mrp.production'] + self.config_param_model = self.env['ir.config_parameter'] self.unit_uom = self.browse_ref('product.product_uom_unit') self.dozen_uom = self.browse_ref('product.product_uom_dozen') - def check_result_and_load_entity(self, model_name, result): + def check_result_and_load_entity(self, model_name, result, context=None): entity_id = result.pop('res_id') self.assertEqual({ 'type': 'ir.actions.act_window', @@ -27,7 +30,7 @@ class TestBom(TransactionCase): 'view_mode': 'form', 'res_model': model_name, 'target': 'current', - 'context': self.env.context + 'context': context or self.env.context, }, result) return self.env[model_name].browse(entity_id) @@ -247,3 +250,119 @@ class TestBom(TransactionCase): self.assertEqual(2, mrp_prod.product_qty) self.assertEqual(2, mrp_prod.product_qty) self.assertEqual(self.dozen_uom, mrp_prod.product_uom) + + def test_action_create_dismantling_bom(self): + # Set component automatically choosen. + self.config_param_model.set_param( + 'mrp.bom.dismantling.product_choice', False + ) + + p1 = self.product_model.create({'name': 'Test P1'}) + p2 = self.product_model.create({'name': 'Test P2'}) + + p1_bom = self.create_bom(p1, components=[p2]) + + dismantled_p2_domain = [ + ('product_id', '=', p2.id), + ('dismantling', '=', True), + ] + + # Non dismantling bom + self.assertEqual(0, self.bom_model.search_count(dismantled_p2_domain)) + + result = p1_bom.action_create_dismantling_bom() + self.assertEqual(1, self.bom_model.search_count(dismantled_p2_domain)) + + dmtl_bom = self.check_result_and_load_entity('mrp.bom', result) + self.assertEqual(p2.id, dmtl_bom.product_id.id) + self.assertEqual(True, dmtl_bom.dismantling) + + # Component must be choose by user + self.config_param_model.set_param( + 'mrp.bom.dismantling.product_choice', '1' + ) + + result = p1_bom.action_create_dismantling_bom() + + # No new dismantling bom created + self.assertEqual(1, self.bom_model.search_count(dismantled_p2_domain)) + + # Response opened wizard + self.assertEqual({ + 'type': 'ir.actions.act_window', + 'name': 'Choose main compoment', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'mrp.bom.dismantling_product_choice', + 'target': 'new', + 'context': self.env.context + }, result) + + def test_res_config(self): + # Coverage test for res_config methods + self.config_param_model.set_param( + 'mrp.bom.dismantling.product_choice', None + ) + + mrp_config = self.env['mrp.config.settings'].create({ + # Bypass default_get bug: https://github.com/odoo/odoo/pull/10373 + 'group_product_variant': 0 + }) + self.assertEqual( + False, mrp_config.read( + ['dismantling_product_choice'] + )[0]['dismantling_product_choice'] + ) + + mrp_config.write({'dismantling_product_choice': 1}) + mrp_config.execute() + + self.assertEqual('1', self.config_param_model.get_param( + 'mrp.bom.dismantling.product_choice' + )) + + def test_product_choice_wizard(self): + wizard_model = self.env['mrp.bom.dismantling_product_choice'] + + p1 = self.product_model.create({'name': 'Test P1'}) + p2 = self.product_model.create({'name': 'Test P2'}) + p3 = self.product_model.create({'name': 'Test P3'}) + + bom = self.create_bom(p1) + + # No active ID + with self.assertRaises(KeyError): + wizard_model.create({}) + + # Cannot really test full workflow => call methods manually. + wizard = wizard_model.with_context(active_id=bom.id).new({}) + self.assertEqual(bom, wizard._get_bom_id()) + + wizard.bom_id = bom + + # No component + with self.assertRaises(UserError): + wizard.on_change_bom_id() + + self.create_bom_line(bom, p2) + self.create_bom_line(bom, p3) + + bom.refresh() + wizard.bom_id = bom + result = wizard.on_change_bom_id() + self.assertEqual({ + 'domain': { + 'component_id': [('id', 'in', [p2.id, p3.id])], + } + }, result) + + wizard.component_id = p3 + wizard.write({}) + result = wizard.create_bom() + + # Dismantling BOM main product is P3 + dmtl_bom = self.check_result_and_load_entity( + 'mrp.bom', result, context={'active_id': bom.id} + ) + self.assertEqual(p3.id, dmtl_bom.product_id.id) + self.assertEqual(True, dmtl_bom.dismantling) diff --git a/mrp_bom_dismantling/views/mrp_bom.xml b/mrp_bom_dismantling/views/mrp_bom.xml index 8e8ceead2..5a7f8dd5a 100644 --- a/mrp_bom_dismantling/views/mrp_bom.xml +++ b/mrp_bom_dismantling/views/mrp_bom.xml @@ -31,7 +31,7 @@