Merge pull request #113 from cyrilgdn/mrp_bom_dismantling

[ADD] new module mrp_bom_dismantling
This commit is contained in:
Alexandre Fayolle
2016-04-27 12:54:55 +02:00
21 changed files with 998 additions and 1 deletions

View File

@@ -0,0 +1,59 @@
.. 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
===============
BOM Dismantling
===============
This module adds the ability to create a dismantling BOM by reversing a BOM.
Usage
=====
* On BOM form view, click on "Create dismantling BOM" button and it will reverse your BOM.
* 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".
.. 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
<https://github.com/OCA/manufacture/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
<https://github.com/OCA/
manufacture/issues/new?body=module:%20
mrp_bom_dismantling%0Aversion:%20
9.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Contributors
------------
* Camptocamp - Cyril Gaudin <cyril.gaudin@camptocamp.com>
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.

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models
from . import wizards

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "BOM Dismantling",
"summary": "Ability to create a dismantling BOM by reversing a BOM.",
"version": "9.0.1.0.0",
"category": "Manufacturing",
"website": "http://www.camptocamp.com/",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
'mrp_byproduct',
"stock_available_mrp",
],
"data": [
"views/mrp_bom.xml",
"views/product_template.xml",
"wizards/mrp_product_produce.xml",
],
}

View File

@@ -0,0 +1,60 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mrp_bom_dismantling
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-04-04 11:23+0200\n"
"PO-Revision-Date: 2016-04-06 15:55+0200\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.8.4\n"
"Last-Translator: \n"
"Language: de\n"
#. module: mrp_bom_dismantling
#: model:ir.model,name:mrp_bom_dismantling.model_mrp_bom
msgid "Bill of Material"
msgstr "Stücklisten"
#. module: mrp_bom_dismantling
#: model:ir.ui.view,arch_db:mrp_bom_dismantling.mrp_bom_form_view
msgid "Create Manufacturing Order"
msgstr "Fertigungsauftrag erstellen"
#. module: mrp_bom_dismantling
#: model:ir.ui.view,arch_db:mrp_bom_dismantling.mrp_bom_form_view
msgid "Create dismantling BoM"
msgstr "Stücklisten umrüsten erstellen"
#. module: mrp_bom_dismantling
#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantled_product_id
msgid "Dismantled product"
msgstr "Produkt zu umrüsten"
#. module: mrp_bom_dismantling
#: model:ir.actions.act_window,name:mrp_bom_dismantling.mrp_bom_dismantling_form_action
#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantling
#: model:ir.ui.menu,name:mrp_bom_dismantling.menu_mrp_bom_dismantling
msgid "Dismantling"
msgstr "Umrüsten"
#. module: mrp_bom_dismantling
#: sql_constraint:mrp.bom:0
msgid "Dismantling BoM should have a dismantled product."
msgstr ""
#. module: mrp_bom_dismantling
#: model:ir.model,name:mrp_bom_dismantling.model_product_product
msgid "Product"
msgstr "Produkt"
#. module: mrp_bom_dismantling
#: model:ir.model,name:mrp_bom_dismantling.model_product_template
msgid "Product Template"
msgstr "Produktvorlage"

View File

@@ -0,0 +1,59 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mrp_bom_dismantling
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-04-04 09:17+0000\n"
"PO-Revision-Date: 2016-04-04 09:17+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: mrp_bom_dismantling
#: model:ir.model,name:mrp_bom_dismantling.model_mrp_bom
msgid "Bill of Material"
msgstr ""
#. module: mrp_bom_dismantling
#: model:ir.ui.view,arch_db:mrp_bom_dismantling.mrp_bom_form_view
msgid "Create Manufacturing Order"
msgstr ""
#. module: mrp_bom_dismantling
#: model:ir.ui.view,arch_db:mrp_bom_dismantling.mrp_bom_form_view
msgid "Create dismantling BoM"
msgstr ""
#. module: mrp_bom_dismantling
#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantled_product_id
msgid "Dismantled product"
msgstr ""
#. module: mrp_bom_dismantling
#: model:ir.actions.act_window,name:mrp_bom_dismantling.mrp_bom_dismantling_form_action
#: model:ir.model.fields,field_description:mrp_bom_dismantling.field_mrp_bom_dismantling
#: model:ir.ui.menu,name:mrp_bom_dismantling.menu_mrp_bom_dismantling
msgid "Dismantling"
msgstr ""
#. module: mrp_bom_dismantling
#: sql_constraint:mrp.bom:0
msgid "Dismantling BoM should have a dismantled product."
msgstr ""
#. module: mrp_bom_dismantling
#: model:ir.model,name:mrp_bom_dismantling.model_product_product
msgid "Product"
msgstr ""
#. module: mrp_bom_dismantling
#: model:ir.model,name:mrp_bom_dismantling.model_product_template
msgid "Product Template"
msgstr ""

View File

@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import mrp_bom
from . import product_product
from . import product_template
from . import stock_move

View File

@@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import _, api, exceptions, fields, models
class MrpBom(models.Model):
_inherit = 'mrp.bom'
dismantling = fields.Boolean(string='Dismantling', default=False)
dismantled_product_id = fields.Many2one(
comodel_name='product.product',
string='Dismantled product'
)
_sql_constraints = [
('bom_dismantled_product_id',
'CHECK(dismantled_product_id is not null = dismantling)',
"Dismantling BoM should have a dismantled product."),
]
@api.multi
def create_mrp_production(self):
""" Create a manufacturing order from this BoM
"""
self.ensure_one()
product = self._get_bom_product()
production = self.env['mrp.production'].create({
'bom_id': self.id,
'product_id': product.id,
'product_qty': self.product_qty,
'product_uom': self.product_uom.id,
})
return self._get_form_view('mrp.production', production)
@api.multi
def create_dismantling_bom(self):
""" Create a dismantling BoM based on this BoM
"""
self.ensure_one()
self._check_bom_validity(check_dismantling=True)
product = self._get_bom_product()
components = self._get_components_tuples()
# Create the BoM on first component (sorted by Id)
first_component, first_component_needs = components.pop(0)
dismantling_bom = self.create({
'product_tmpl_id': first_component.product_tmpl_id.id,
'product_id': first_component.id,
'dismantling': True,
'dismantled_product_id': product.id,
'product_qty': first_component_needs,
})
# Create BoM line for self.product_tmpl_id
self.env['mrp.bom.line'].create({
'bom_id': dismantling_bom.id,
'product_id': product.id,
'product_qty': self.product_qty,
'product_uom': self.product_uom.id,
})
# Add others component as By-products
subproduct_model = self.env['mrp.subproduct']
for component, needs in components:
subproduct_model.create({
'bom_id': dismantling_bom.id,
'product_id': component.id,
'product_qty': needs,
'product_uom': self.env.ref('product.product_uom_unit').id,
})
return self._get_form_view('mrp.bom', dismantling_bom)
def _get_form_view(self, model_name, entity):
return {
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': model_name,
'target': 'current',
'res_id': entity.id,
'context': self.env.context
}
def _check_bom_validity(self, check_dismantling=False):
""" Ensure this BoM can be use for creating a dismantling BoM
or a manufacturing order.
:type check_dismantling: bool
:raise exceptions.UserError: If this BoM is not valid.
"""
warning = None
if check_dismantling and self.dismantling:
warning = 'This BoM is already a dismantling Bom.'
if not len(self.bom_line_ids):
warning = 'This BoM does not have components.'
if not self.product_id \
and len(self.product_tmpl_id.product_variant_ids) > 1:
warning = 'This product has several variants: ' \
'you need to specify one.'
if warning:
raise exceptions.UserError(_(warning))
def _get_components_tuples(self):
""" Return this BoM components and their needed qties
sorted by component id.
The result is like [(component_1, 1), (component_2, 5), ...]
:rtype: list of tuple
"""
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
def _get_bom_product(self):
""" Get the product of this BoM.
If BoM does not have product_id, return first template variant.
:rtype: product_product
"""
if not self.product_id:
product = self.product_tmpl_id.product_variant_ids[0]
else:
product = self.product_id
return product

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import models
class ProductProduct(models.Model):
_inherit = 'product.product'
def action_view_bom(self, cr, uid, ids, context=None):
""" Override parent method to add a domain which filter out
dismantling BoM
"""
result = super(ProductProduct, self).action_view_bom(
cr, uid, ids, context
)
result['domain'] = [('dismantling', '=', False)]
return result

View File

@@ -0,0 +1,22 @@
# -*- 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 ProductTemplate(models.Model):
_inherit = 'product.template'
bom_count = fields.Integer(compute='_bom_count',
string='# Bill of Material')
@api.multi
def _bom_count(self):
""" Override parent method to filter out dismantling bom.
"""
for template in self:
template.bom_count = self.env['mrp.bom'].search_count([
('product_tmpl_id', '=', template.id),
('dismantling', '=', False),
])

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import api, models
class StockMove(models.Model):
_inherit = 'stock.move'
@api.multi
def action_consume(self, product_qty, location_id=False,
restrict_lot_id=False, restrict_partner_id=False,
consumed_for=False):
""" Override restrict_lot_id if user define one for this move's
product in wizard.
"""
# If user define a lot_id for this move's product we override
restrict_lot_id = self.env.context.get('mapping_move_lot', {}).pop(
self.id, restrict_lot_id
)
return super(StockMove, self).action_consume(
product_qty, location_id, restrict_lot_id,
restrict_partner_id, consumed_for
)

View File

@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_bom
from . import test_product
from . import test_product_produce
from . import test_template

View File

@@ -0,0 +1,249 @@
# -*- coding: utf-8 -*-
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import exceptions
from openerp.tests import TransactionCase
class TestBom(TransactionCase):
def setUp(self):
super(TestBom, self).setUp()
self.product_model = self.env['product.product']
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.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):
entity_id = result.pop('res_id')
self.assertEqual({
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': model_name,
'target': 'current',
'context': self.env.context
}, result)
return self.env[model_name].browse(entity_id)
def create_bom(self, product, qty=1, uom=None,
phantom=False, components=None):
bom = self.bom_model.create({
'product_tmpl_id': product.product_tmpl_id.id,
'product_id': product.id,
'product_qty': qty,
'product_uom': self.unit_uom.id if uom is None else uom.id,
'type': 'phantom' if phantom else 'normal',
})
if components:
for component in components:
self.create_bom_line(bom, component)
return bom
def create_bom_line(self, bom, component, qty=1, uom=None):
self.bom_line_model.create({
'bom_id': bom.id,
'product_id': component.id,
'product_qty': qty,
'product_uom': self.unit_uom.id if uom is None else uom.id,
})
def test_dismantling_no_components(self):
p1 = self.product_model.create({'name': 'Test P1'})
p2 = self.product_model.create({'name': 'Test P1'})
p1_bom = self.create_bom(p1)
with self.assertRaises(exceptions.UserError):
p1_bom.create_dismantling_bom()
self.create_bom_line(p1_bom, p2)
p1_bom.create_dismantling_bom()
def test_dismantling_on_dismantling_bom(self):
p1 = self.product_model.create({'name': 'Test P1'})
p2 = self.product_model.create({'name': 'Test P1'})
p1_bom = self.create_bom(p1, components=[p2])
p1_bom.write({
'dismantling': True,
'dismantled_product_id': p2.id
})
with self.assertRaises(exceptions.UserError):
p1_bom.create_dismantling_bom()
def test_dismantling_bom_no_product_id__multiple_vaiants(self):
p1 = self.product_model.create({'name': 'Test P1'})
p1_var = self.product_model.create({
'product_tmpl_id': p1.product_tmpl_id.id,
})
p2 = self.product_model.create({'name': 'Test P2'})
# P1 BoM: Need one P2
p1_bom = self.bom_model.create({
'product_tmpl_id': p1.product_tmpl_id.id,
'product_id': None,
})
self.create_bom_line(p1_bom, p2)
# No variant specified (and template has multiple variants)
with self.assertRaises(exceptions.UserError):
p1_bom.create_dismantling_bom()
# Variant specified
p1_bom.product_id = p1_var
result = p1_bom.create_dismantling_bom()
self.check_result_and_load_entity('mrp.bom', result)
def test_dismantling_bom_no_product_id(self):
# Same tests but BoM only have a product_tmpl_id, no product_id
# (Seems to be the standard case)
p1 = self.product_model.create({'name': 'Test P1'})
p2 = self.product_model.create({'name': 'Test P2'})
# P1 BoM: Need one P2
p1_bom = self.bom_model.create({
'product_tmpl_id': p1.product_tmpl_id.id,
'product_id': None,
})
self.create_bom_line(p1_bom, p2)
result = p1_bom.create_dismantling_bom()
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)
self.assertEqual(p1.id, dmtl_bom.dismantled_product_id.id)
self.assertEqual(p2.product_tmpl_id.id, dmtl_bom.product_tmpl_id.id)
# Consume p1
self.assertEqual(1, len(dmtl_bom.bom_line_ids))
self.assertEqual(p1.id, dmtl_bom.bom_line_ids[0].product_id.id)
def test_dismantling_simple_case(self):
p1 = self.product_model.create({'name': 'Test P1'})
p2 = self.product_model.create({'name': 'Test P2'})
p3 = self.product_model.create({'name': 'Test P3'})
# P1 BoM: Need one P2 and one P3
p1_bom = self.create_bom(p1, components=[p2, p3])
result = p1_bom.create_dismantling_bom()
dmtl_bom = self.check_result_and_load_entity('mrp.bom', result)
self.assertEqual(p2.id, dmtl_bom.product_id.id)
self.assertEqual(p2.product_tmpl_id.id, dmtl_bom.product_tmpl_id.id)
self.assertEqual(True, dmtl_bom.dismantling)
self.assertEqual(p1.id, dmtl_bom.dismantled_product_id.id)
# Consume p1
self.assertEqual(1, len(dmtl_bom.bom_line_ids))
self.assertEqual(p1.id, dmtl_bom.bom_line_ids[0].product_id.id)
# P3 in by-products
self.assertEqual(1, len(dmtl_bom.sub_products))
self.assertEqual(p3.id, dmtl_bom.sub_products[0].product_id.id)
def test_phantom_bom(self):
p1 = self.product_model.create({'name': 'Test P1'})
p2 = self.product_model.create({'name': 'Test P2'})
sub_p2 = self.product_model.create({'name': 'Test sub P2'})
p3 = self.product_model.create({'name': 'Test P3'})
sub_p3 = self.product_model.create({'name': 'Test sub P3'})
# P1 BoM: Need one P2 and one P3
# P2 has a phantom BoM which need one sub P2
# P3 has a normal Bom which need one sub p3
p1_bom = self.create_bom(p1, components=[p2, p3])
self.create_bom(p2, phantom=True, components=[sub_p2])
self.create_bom(p3, components=[sub_p3])
result = p1_bom.create_dismantling_bom()
dmtl_bom = self.check_result_and_load_entity('mrp.bom', result)
self.assertEqual(sub_p2.id, dmtl_bom.product_id.id)
self.assertEqual(sub_p2.product_tmpl_id.id,
dmtl_bom.product_tmpl_id.id)
self.assertEqual(True, dmtl_bom.dismantling)
self.assertEqual(p1.id, dmtl_bom.dismantled_product_id.id)
# Consume p1self.assertEqual(1, len(dmtl_bom.bom_line_ids))
self.assertEqual(p1.id, dmtl_bom.bom_line_ids[0].product_id.id)
# Sub P3 in by-products
self.assertEqual(1, len(dmtl_bom.sub_products))
self.assertEqual(p3.id, dmtl_bom.sub_products[0].product_id.id)
def test_multi_unit_components(self):
p1 = self.product_model.create({'name': 'Test P1'})
p2 = self.product_model.create({'name': 'Test P2'})
p3 = self.product_model.create({'name': 'Test P3'})
p4 = self.product_model.create({'name': 'Test P4'})
# P1 BoM (produced 1 dozen): Needs 2 P2, 4 P3 and 2 dozen P4
# P2 has a phantom BoM which need one Dozen of P3
#
# => Dismantling BoM:
# Product: P3 (produced 28 unit)
# Component: 1 dozen P1
p1_bom = self.create_bom(p1, qty=1, uom=self.dozen_uom)
self.create_bom_line(p1_bom, p2, qty=2)
self.create_bom_line(p1_bom, p3, qty=4)
self.create_bom_line(p1_bom, p4, qty=2, uom=self.dozen_uom)
p2_bom = self.create_bom(p2, phantom=True)
self.create_bom_line(p2_bom, p3, qty=1, uom=self.dozen_uom)
result = p1_bom.create_dismantling_bom()
dmtl_bom = self.check_result_and_load_entity('mrp.bom', result)
self.assertEqual(p3.id, dmtl_bom.product_id.id)
self.assertEqual(28, dmtl_bom.product_qty)
self.assertEqual(self.unit_uom, dmtl_bom.product_uom)
self.assertEqual(True, dmtl_bom.dismantling)
self.assertEqual(p1.id, dmtl_bom.dismantled_product_id.id)
# Consume 1 dozen p1
self.assertEqual(1, len(dmtl_bom.bom_line_ids))
dmtl_bom_line = dmtl_bom.bom_line_ids[0]
self.assertEqual(p1.id, dmtl_bom_line.product_id.id)
self.assertEqual(1, dmtl_bom_line.product_qty)
self.assertEqual(self.dozen_uom, dmtl_bom_line.product_uom)
# Byproducts
self.assertEqual(1, len(dmtl_bom.sub_products))
dmtl_sub_product = dmtl_bom.sub_products[0]
self.assertEqual(p4.id, dmtl_sub_product.product_id.id)
self.assertEqual(24, dmtl_sub_product.product_qty)
self.assertEqual(self.unit_uom, dmtl_sub_product.product_uom)
def test_create_mrp_production(self):
p1 = self.product_model.create({'name': 'Test P1'})
p2 = self.product_model.create({'name': 'Test P2'})
bom = self.create_bom(p1, qty=2, uom=self.dozen_uom, components=[p2])
self.assertEqual(
0,
self.mrp_production_model.search_count([('bom_id', '=', bom.id)])
)
result = bom.create_mrp_production()
mrp_prod = self.check_result_and_load_entity('mrp.production', result)
self.assertEqual(bom, mrp_prod.bom_id)
self.assertEqual(p1, mrp_prod.product_id)
self.assertEqual(2, mrp_prod.product_qty)
self.assertEqual(2, mrp_prod.product_qty)
self.assertEqual(self.dozen_uom, mrp_prod.product_uom)

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp.tests import TransactionCase
class TestProduct(TransactionCase):
def test_action_view_bom(self):
# Covering test
p1 = self.env['product.product'].create({'name': 'Test P1'})
result = p1.action_view_bom()
self.assertIn(('dismantling', '=', False), result['domain'])

View File

@@ -0,0 +1,96 @@
# -*- coding: utf8 -*-
from openerp.tests import TransactionCase
class TestProductProduce(TransactionCase):
def test_produced_products_lots(self):
produce_model = self.env['mrp.product.produce']
product_model = self.env['product.product']
lot_model = self.env['stock.production.lot']
quant_model = self.env['stock.quant']
unit_uom = self.browse_ref('product.product_uom_unit')
wh_main = self.browse_ref('stock.warehouse0')
# Create simple bom with by products
p1 = product_model.create({'name': 'Test P1'})
p2 = product_model.create({'name': 'Test P2'})
p3 = product_model.create({'name': 'Test P3'})
# We have 1 P2 in stock
inventory = self.env['stock.inventory'].create({
'name': 'P2 inventory',
'location_id': wh_main.lot_stock_id.id,
'filter': 'partial'
})
inventory.prepare_inventory()
self.env['stock.inventory.line'].create({
'inventory_id': inventory.id,
'product_id': p2.id,
'location_id': wh_main.lot_stock_id.id,
'product_qty': 1
})
inventory.action_done()
# P1 need P2 and generates one byproduct P3
bom = self.env['mrp.bom'].create({
'product_tmpl_id': p1.product_tmpl_id.id,
'product_id': p1.id,
'product_qty': 1,
'product_uom': unit_uom.id,
})
self.env['mrp.bom.line'].create({
'bom_id': bom.id,
'product_id': p2.id,
'product_qty': 1,
'product_uom': unit_uom.id,
})
self.env['mrp.subproduct'].create({
'bom_id': bom.id,
'product_id': p3.id,
'product_qty': 1,
'product_uom': unit_uom.id,
})
# Create MRP Order
mrp_order_id = bom.create_mrp_production()['res_id']
mrp_order = self.env['mrp.production'].browse(mrp_order_id)
mrp_order.action_confirm()
mrp_order.action_assign()
# Wizard simulation
wizard = produce_model.with_context(active_id=mrp_order_id).create({})
wizard.on_change_product_id()
self.assertEqual(2, len(wizard.move_lot_ids))
self.assertEqual([p1, p3], [x.product_id for x in wizard.move_lot_ids])
lot_p1 = lot_model.create({'name': 'LOT_01', 'product_id': p1.id})
wizard.move_lot_ids[0].lot_id = lot_p1
lot_p3 = lot_model.create({'name': 'LOT_03', 'product_id': p3.id})
wizard.move_lot_ids[1].lot_id = lot_p3
wizard.do_produce()
# Check created move in mrp.production
mrp_order.refresh()
self.assertEqual(lot_p1,
mrp_order.move_created_ids2[0].restrict_lot_id)
self.assertEqual(lot_p3,
mrp_order.move_created_ids2[1].restrict_lot_id)
# Check stock.quants
p1_quants = quant_model.search([('product_id', '=', p1.id)])
self.assertEqual(1, len(p1_quants))
self.assertEqual(lot_p1, p1_quants.lot_id)
self.assertEqual(1, p1_quants.qty)
p3_quants = quant_model.search([('product_id', '=', p3.id)])
self.assertEqual(1, len(p3_quants))
self.assertEqual(1, p3_quants.qty)

View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp.tests import TransactionCase
class TestTemplate(TransactionCase):
def test_bom_count(self):
tmpl_model = self.env['product.template']
tmpl1 = tmpl_model.create({'name': 'Template 1'})
self.assertEqual(0, tmpl1.bom_count)
# Create a BoM for this template
bom_model = self.env['mrp.bom']
bom_model.create({'product_tmpl_id': tmpl1.id})
self.assertEqual(1, tmpl1.bom_count)
# Create a dismantling BoM
other_product = self.env['product.product'].create({
'name': 'Other product'
})
bom_model.create({
'product_tmpl_id': tmpl1.id,
'dismantling': True,
'dismantled_product_id': other_product.id
})
self.assertEqual(1, tmpl1.bom_count)
# Check count on another template
tmpl2 = tmpl_model.create({'name': 'Template 2'})
self.assertEqual(0, tmpl2
.bom_count)
# And on dismantled product
self.assertEqual(0, other_product.product_tmpl_id.bom_count)

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Menu and action-->
<record id="mrp_bom_dismantling_form_action" model="ir.actions.act_window">
<field name="name">Dismantling</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">mrp.bom</field>
<field name="domain">[('dismantling', '=', True)]</field>
<field name="context">{'default_dismantling': True}</field>
<field name="view_type">form</field>
</record>
<menuitem id="menu_mrp_bom_dismantling"
action="mrp_bom_dismantling_form_action"
parent="mrp.menu_mrp_bom"
sequence="100"/>
<record id="mrp.mrp_bom_form_action" model="ir.actions.act_window">
<field name="domain">[('dismantling', '=', False)]</field>
</record>
<!-- Form view -->
<record id="mrp_bom_form_view" model="ir.ui.view">
<field name="name">mrp_bom_form</field>
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.mrp_bom_form_view"/>
<field name="arch" type="xml">
<xpath expr="/form/sheet" position="before">
<header>
<!-- Create Manufacturing Order -->
<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"
attrs="{'invisible': [('dismantling', '=', True)]}"/>
</header>
</xpath>
<field name="product_tmpl_id" position="before">
<field name="dismantling" invisible="True"/>
<field name="dismantled_product_id"
attrs="{'invisible': [('dismantling', '=', False)], 'required': [('dismantling', '=', True)]}"/>
</field>
</field>
</record>
<!-- Tree view -->
<record id="mrp_bom_tree_view" model="ir.ui.view">
<field name="name">mrp_bom_tree</field>
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.mrp_bom_tree_view"/>
<field name="arch" type="xml">
<field name="product_tmpl_id" position="before">
<field name="dismantled_product_id" invisible="not context.get('default_dismantling')"/>
</field>
</field>
</record>
<!-- Searches -->
<record id="view_mrp_bom_filter" model="ir.ui.view">
<field name="name">mrp.bom.select</field>
<field name="model">mrp.bom</field>
<field name="inherit_id" ref="mrp.view_mrp_bom_filter"/>
<field name="arch" type="xml">
<field name="code" position="before">
<field name="dismantled_product_id" invisible="not context.get('default_dismantling')"/>
</field>
</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Bill of Materials action override -->
<record id="mrp.template_open_bom" model="ir.actions.act_window">
<field name="domain">[('dismantling', '=', False)]</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# © 2016 Cyril Gaudin (Camptocamp)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import mrp_product_produce

View File

@@ -0,0 +1,51 @@
# -*- 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 MrpByProductLine(models.TransientModel):
_name = "mrp.product.produced.line"
produce_id = fields.Many2one('mrp.product.produce', required=True,
string="Produce")
move_id = fields.Many2one('stock.move', required=True)
product_id = fields.Many2one('product.product',
related='move_id.product_id')
lot_id = fields.Many2one('stock.production.lot', string='Lot')
class MrpProductProduce(models.TransientModel):
_inherit = "mrp.product.produce"
move_lot_ids = fields.One2many(
'mrp.product.produced.line',
inverse_name='produce_id',
)
@api.onchange("product_id")
def on_change_product_id(self):
""" Listen to product_id changes just for filling byproducts_lot_ids.
"""
if not self.move_lot_ids:
mrp_prod = self.env["mrp.production"].browse(
self.env.context['active_id']
)
self.move_lot_ids = [
(0, None, {'move_id': move})
for move in mrp_prod.move_created_ids
]
@api.multi
def do_produce(self):
""" Stock produced products lot_id and call parent do_produce
"""
mapping_move_lot = {}
for move_lot in self.move_lot_ids:
if move_lot.lot_id:
mapping_move_lot[move_lot.move_id.id] = move_lot.lot_id.id
super(MrpProductProduce, self.with_context(
mapping_move_lot=mapping_move_lot
)).do_produce()

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Manage sub products lot_id -->
<record id="view_mrp_product_produce_wizard" model="ir.ui.view">
<field name="name">MRP Product Produce</field>
<field name="model">mrp.product.produce</field>
<field name="inherit_id" ref="mrp.view_mrp_product_produce_wizard"/>
<field name="arch" type="xml">
<!-- Hide lot_id field -->
<field name="lot_id" position="attributes">
<attribute name="invisible">True</attribute>
</field>
<!-- Add produced products list for managing lot_id -->
<xpath expr="//group" position="after">
<group string="Products to produce lots" groups="stock.group_production_lot">
<field name="move_lot_ids" nolabel="1">
<tree editable="bottom" create="false" delete="false">
<field name="move_id" invisible="True"/>
<field name="product_id" readonly="True"/>
<field name="lot_id" domain="[('product_id', '=', product_id)]"
context="{'default_product_id':product_id}"/>
</tree>
</field>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,3 +1,4 @@
# List the OCA project dependencies, one per line
# Add a repository url and branch if you need a forked version
product-attribute
product-attribute
stock-logistics-warehouse