diff --git a/addons/mrp/tests/__init__.py b/addons/mrp/tests/__init__.py index ba967b5b60e..9664aebc5b1 100644 --- a/addons/mrp/tests/__init__.py +++ b/addons/mrp/tests/__init__.py @@ -8,3 +8,4 @@ from . import test_workorder_operation from . import test_procurement from . import test_unbuild from . import test_oee +from . import test_traceability diff --git a/addons/mrp/tests/test_traceability.py b/addons/mrp/tests/test_traceability.py new file mode 100644 index 00000000000..f3e3747e6fc --- /dev/null +++ b/addons/mrp/tests/test_traceability.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import Form, tagged +from odoo.addons.mrp.tests.common import TestMrpCommon +import uuid + +class TestTraceability(TestMrpCommon): + TRACKING_TYPES = ['none', 'serial', 'lot'] + + def _create_product(self, tracking): + return self.env['product.product'].create({ + 'name': 'Product %s' % tracking, + 'type': 'product', + 'tracking': tracking, + 'categ_id': self.env.ref('product.product_category_all').id, + }) + + def test_tracking_types_on_mo(self): + finished_no_track = self._create_product('none') + finished_lot = self._create_product('lot') + finished_serial = self._create_product('serial') + consumed_no_track = self._create_product('none') + consumed_lot = self._create_product('lot') + consumed_serial = self._create_product('serial') + stock_id = self.env.ref('stock.stock_location_stock').id + inventory_adjustment = self.env['stock.inventory'].create({ + 'name': 'Initial Inventory', + 'location_id': stock_id, + 'filter': 'partial', + }) + inventory_adjustment.action_start() + inventory_adjustment.write({ + 'line_ids': [ + (0,0, {'product_id': consumed_no_track.id, 'product_qty': 3, 'location_id': stock_id}), + (0,0, {'product_id': consumed_lot.id, 'product_qty': 3, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'L1', 'product_id': consumed_lot.id}).id, 'location_id': stock_id}), + (0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S1', 'product_id': consumed_serial.id}).id, 'location_id': stock_id}), + (0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S2', 'product_id': consumed_serial.id}).id, 'location_id': stock_id}), + (0,0, {'product_id': consumed_serial.id, 'product_qty': 1, 'prod_lot_id': self.env['stock.production.lot'].create({'name': 'S3', 'product_id': consumed_serial.id}).id, 'location_id': stock_id}), + ] + }) + inventory_adjustment.action_validate() + for finished_product in [finished_no_track, finished_lot, finished_serial]: + bom = self.env['mrp.bom'].create({ + 'product_id': finished_product.id, + 'product_tmpl_id': finished_product.product_tmpl_id.id, + 'product_uom_id': self.env.ref('uom.product_uom_unit').id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': consumed_no_track.id, 'product_qty': 1}), + (0, 0, {'product_id': consumed_lot.id, 'product_qty': 1}), + (0, 0, {'product_id': consumed_serial.id, 'product_qty': 1}), + ], + }) + + mo = self.env['mrp.production'].create({ + 'name': 'MO %s' % finished_product.tracking, + 'product_id': finished_product.id, + 'product_uom_id': self.env.ref('uom.product_uom_unit').id, + 'product_qty': 1, + 'bom_id': bom.id, + }) + + mo.action_assign() + + # Start MO production + produce_form = Form(self.env['mrp.product.produce'].with_context({ + 'active_id': mo.id, + 'active_ids': [mo.id], + })) + + if finished_product.tracking != 'serial': + produce_form.product_qty = 1 + + if finished_product.tracking != 'none': + produce_form.lot_id = self.env['stock.production.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id}) + produce_wizard = produce_form.save() + + produce_wizard.do_produce() + mo.button_mark_done() + + self.assertEqual(mo.state, 'done', "Production order should be in done state.") + + # Check results of traceability + context = ({ + 'active_id': mo.id, + 'model': 'mrp.production', + }) + lines = self.env['stock.traceability.report'].with_context(context).get_lines() + + self.assertEqual(len(lines), 1, "Should always return 1 line : the final product") + + final_product = lines[0] + self.assertEqual(final_product['unfoldable'], True, "Final product should always be unfoldable") + + # Find parts of the final products + lines = self.env['stock.traceability.report'].get_lines(final_product['id'], **{ + 'level': final_product['level'], + 'model_id': final_product['model_id'], + 'model_name': final_product['model'], + }) + + self.assertEqual(len(lines), 3, "There should be 3 lines. 1 for untracked, 1 for lot, and 1 for serial") + + for line in lines: + tracking = line['columns'][1].split(' ')[1] + self.assertEqual( + line['columns'][-1], "1.000 Unit(s)", 'Part with tracking type "%s", should have quantity = 1' % (tracking) + ) + unfoldable = False if tracking == 'none' else True + self.assertEqual( + line['unfoldable'], + unfoldable, + 'Parts with tracking type "%s", should have be unfoldable : %s' % (tracking, unfoldable) + ) + diff --git a/addons/stock/models/stock_traceability.py b/addons/stock/models/stock_traceability.py index add1b9bd6e5..9470499c102 100644 --- a/addons/stock/models/stock_traceability.py +++ b/addons/stock/models/stock_traceability.py @@ -68,7 +68,7 @@ class MrpStockReport(models.TransientModel): if model == 'stock.picking': lines = record.move_lines.mapped('move_line_ids').filtered(lambda m: m.lot_id and m.state == 'done') else: - lines = record.move_finished_ids.mapped('move_line_ids').filtered(lambda m: m.lot_id and m.state == 'done') + lines = record.move_finished_ids.mapped('move_line_ids').filtered(lambda m: m.state == 'done') move_line_vals = self._lines(line_id, model_id=rec_id, model=model, level=level, move_lines=lines) final_vals = sorted(move_line_vals, key=lambda v: v['date'], reverse=True) lines = self._final_vals_to_lines(final_vals, level) @@ -112,7 +112,6 @@ class MrpStockReport(models.TransientModel): def _make_dict_move(self, level, parent_id, move_line, unfoldable=False): res_model, res_id, ref = self._get_reference(move_line) dummy, is_used = self._get_linked_move_lines(move_line) - unfoldable = False if not move_line.lot_id else unfoldable data = [{ 'level': level, 'unfoldable': unfoldable, @@ -126,8 +125,8 @@ class MrpStockReport(models.TransientModel): 'product_qty_uom': "%s %s" % (self._quantity_to_str(move_line.product_uom_id, move_line.product_id.uom_id, move_line.qty_done), move_line.product_id.uom_id.name), 'lot_name': move_line.lot_id.name, 'lot_id': move_line.lot_id.id, - 'location_source': move_line.location_id.name if not unfoldable or level == 1 else False, - 'location_destination': move_line.location_dest_id.name if not unfoldable or level == 1 else False, + 'location_source': move_line.location_id.name, + 'location_destination': move_line.location_dest_id.name, 'reference_id': ref, 'res_id': res_id, 'res_model': res_model}] @@ -181,7 +180,9 @@ class MrpStockReport(models.TransientModel): for line in move_line: final_vals += self._make_dict_move(level, parent_id=line_id, move_line=line) for line in lines: - unfoldable = bool(line.produce_line_ids or line.consume_line_ids) + unfoldable = False + if line.consume_line_ids or ( line.lot_id and self._get_move_lines(line) and model != "stock.production.lot"): + unfoldable = True final_vals += self._make_dict_move(level, parent_id=line_id, move_line=line, unfoldable=unfoldable) return final_vals