dismantling bom: allow user to choose main component.

This commit is contained in:
Cyril Gaudin
2016-04-18 13:16:21 +02:00
parent 743ffda57c
commit 77d61949a0
12 changed files with 413 additions and 25 deletions

View File

@@ -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".

View File

@@ -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",
],
}

View File

@@ -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"

View File

@@ -5,4 +5,5 @@
from . import mrp_bom
from . import product_product
from . import product_template
from . import res_config
from . import stock_move

View File

@@ -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.

View File

@@ -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
)

View File

@@ -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)

View File

@@ -31,7 +31,7 @@
<button name="create_mrp_production" type="object" string="Create Manufacturing Order" />
<!-- Create Dismantling BoM -->
<button name="create_dismantling_bom" type="object" string="Create dismantling BoM"
<button name="action_create_dismantling_bom" type="object" string="Create dismantling BoM"
attrs="{'invisible': [('dismantling', '=', True)]}"/>
</header>
</xpath>

View File

@@ -0,0 +1,14 @@
<odoo>
<!-- Adding Bill of Materials config group -->
<record id="view_mrp_config" model="ir.ui.view">
<field name="model">mrp.config.settings</field>
<field name="inherit_id" ref="mrp.view_mrp_config" />
<field name="arch" type="xml">
<xpath expr="//form/group[last()]" position="after">
<group string="Bill of Materials">
<field name="dismantling_product_choice" class="oe_inline" widget="radio"/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -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

View File

@@ -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
)

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mrp_bom_dismantling_product_choice_wizard" model="ir.ui.view">
<field name="name">BOM Dismantling product choice</field>
<field name="model">mrp.bom.dismantling_product_choice</field>
<field name="arch" type="xml">
<form string="Dismantling product choice">
<field name="bom_id" invisible="True"/>
<field name="component_id" options="{'no_create': True}"/>
<footer>
<button name="create_bom" type="object" string="Confirm" class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>