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 @@
-
diff --git a/mrp_bom_dismantling/views/res_config.xml b/mrp_bom_dismantling/views/res_config.xml
new file mode 100644
index 000000000..833cd4b63
--- /dev/null
+++ b/mrp_bom_dismantling/views/res_config.xml
@@ -0,0 +1,14 @@
+
+
+
+ mrp.config.settings
+
+
+
+
+
+
+
+
+
+
diff --git a/mrp_bom_dismantling/wizards/__init__.py b/mrp_bom_dismantling/wizards/__init__.py
index 4a94536b7..61d03e79a 100644
--- a/mrp_bom_dismantling/wizards/__init__.py
+++ b/mrp_bom_dismantling/wizards/__init__.py
@@ -2,4 +2,5 @@
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from . import dismantling_product_choice
from . import mrp_product_produce
diff --git a/mrp_bom_dismantling/wizards/dismantling_product_choice.py b/mrp_bom_dismantling/wizards/dismantling_product_choice.py
new file mode 100644
index 000000000..9249a6a34
--- /dev/null
+++ b/mrp_bom_dismantling/wizards/dismantling_product_choice.py
@@ -0,0 +1,48 @@
+# -*- 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
+from openerp.exceptions import UserError
+
+
+class DismantlingProductChoice(models.TransientModel):
+ _name = 'mrp.bom.dismantling_product_choice'
+
+ def _get_bom_id(self):
+ return self.env['mrp.bom'].browse(self.env.context['active_id'])
+
+ bom_id = fields.Many2one(
+ 'mrp.bom',
+ default=_get_bom_id
+ )
+ component_id = fields.Many2one(
+ 'product.product',
+ required=True,
+ domain=[('id', '=', False)]
+ )
+
+ @api.onchange('bom_id')
+ def on_change_bom_id(self):
+ """ Update component_id domain to include only BOM components.
+ """
+ component_ids = sorted(
+ [c.id for c in self.bom_id._get_components_needs()]
+ )
+ if not component_ids:
+ raise UserError(_('This BoM does not have components.'))
+
+ return {
+ 'domain': {
+ 'component_id': [('id', 'in', component_ids)]
+ }
+ }
+
+ @api.multi
+ def create_bom(self):
+ """ Call dismantling bom creation method with main component specified.
+ """
+ return self.bom_id.create_dismantling_bom(
+ main_component=self.component_id
+ )
diff --git a/mrp_bom_dismantling/wizards/dismantling_product_choice.xml b/mrp_bom_dismantling/wizards/dismantling_product_choice.xml
new file mode 100644
index 000000000..be22aabc1
--- /dev/null
+++ b/mrp_bom_dismantling/wizards/dismantling_product_choice.xml
@@ -0,0 +1,17 @@
+
+
+
+ BOM Dismantling product choice
+ mrp.bom.dismantling_product_choice
+
+
+
+
+