From d8ae935e7482abbc6476096108ca79dc7933867c Mon Sep 17 00:00:00 2001
From: Josse Colpaert <jco@odoo.com>
Date: Mon, 25 Sep 2017 18:26:25 +0200
Subject: [PATCH] [IMP] mrp: traceability of quantity of non-tracked product

When producing a finished product with a lot, also the raw materials
without lot tracking should split the move lines to at least know the
quantities consumed for every lot produced.  This way, the traceability
report will not confuse the user.
---
 addons/mrp/data/mrp_demo.xml             |  2 +-
 addons/mrp/models/mrp_production.py      |  4 +-
 addons/mrp/models/mrp_workorder.py       | 12 +++--
 addons/mrp/models/stock_move.py          | 41 ++++++++++++++++-
 addons/mrp/tests/test_unbuild.py         | 58 +++++++++++++++++++++++-
 addons/mrp/views/stock_move_views.xml    |  7 +--
 addons/mrp/wizard/mrp_product_produce.py | 53 +++++++---------------
 7 files changed, 128 insertions(+), 49 deletions(-)

diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml
index 33cc801b9fcd..b15716264b4a 100644
--- a/addons/mrp/data/mrp_demo.xml
+++ b/addons/mrp/data/mrp_demo.xml
@@ -232,7 +232,7 @@
             <field name="default_code">E-COM92</field>
         </record>
 
-        <record id="product_product_build_kit_product_template" model="product.template">
+         <record id="product_product_build_kit_product_template" model="product.template">
             <field name="route_ids" eval="[(6, 0, [ref('stock.route_warehouse0_mto'), ref('mrp.route_warehouse0_manufacture')])]"/>
         </record>
 
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 1f574c1d24e6..59bafc27487d 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -549,8 +549,10 @@ class MrpProduction(models.Model):
             consume_move_lines = moves_to_do.mapped('active_move_line_ids')
             for moveline in moves_to_finish.mapped('active_move_line_ids'):
                 if moveline.move_id.has_tracking != 'none':
+                    if any([not ml.lot_produced_id for ml in consume_move_lines]):
+                        raise UserError(_('You can not consume without telling for which lot you consumed it'))
                     # Link all movelines in the consumed with same lot_produced_id false or the correct lot_produced_id
-                    filtered_lines = consume_move_lines.filtered(lambda x: x.lot_produced_id == moveline.lot_id or not x.lot_produced_id)
+                    filtered_lines = consume_move_lines.filtered(lambda x: x.lot_produced_id == moveline.lot_id)
                     moveline.write({'consume_line_ids': [(6, 0, [x for x in filtered_lines.ids])]})
                 else:
                     # Link with everything
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index 18329432edd4..61e7865d5cc5 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -294,11 +294,15 @@ class MrpWorkorder(models.Model):
         # For each untracked component without any 'temporary' move lines,
         # (the new workorder tablet view allows registering consumed quantities for untracked components)
         # we assume that only the theoretical quantity was used
-        raw_moves = self.move_raw_ids.filtered(lambda x: (x.has_tracking == 'none') and (x.state not in ('done', 'cancel')) and x.bom_line_id)
-        for move in raw_moves:
-            if move.unit_factor and not move.move_line_ids.filtered(lambda m: not m.done_wo):
+        for move in self.move_raw_ids:
+            if move.has_tracking == 'none' and (move.state not in ('done', 'cancel')) and move.bom_line_id\
+                        and move.unit_factor and not move.move_line_ids.filtered(lambda ml: not ml.done_wo):
                 rounding = move.product_uom.rounding
-                move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)
+                if self.product_id.tracking != 'none':
+                    qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)
+                    move._generate_consumed_move_line(qty_to_add, self.final_lot_id)
+                else:
+                    move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)
 
         # Transfer quantities from temporary to final move lots or make them final
         for move_line in self.active_move_line_ids:
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index 966e3365b6c4..4eb94b2379a3 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -68,6 +68,7 @@ class StockMove(models.Model):
         help='Technical Field to order moves')
     needs_lots = fields.Boolean('Tracking', compute='_compute_needs_lots')
     order_finished_lot_ids = fields.Many2many('stock.production.lot', compute='_compute_order_finished_lot_ids')
+    finished_lots_exist = fields.Boolean('Finished Lots Exist', compute='_compute_order_finished_lot_ids')
 
     @api.depends('active_move_line_ids.qty_done', 'active_move_line_ids.product_uom_id')
     def _compute_done_quantity(self):
@@ -76,8 +77,13 @@ class StockMove(models.Model):
     @api.depends('raw_material_production_id.move_finished_ids.move_line_ids.lot_id')
     def _compute_order_finished_lot_ids(self):
         for move in self:
-            if move.product_id.tracking != 'none' and move.raw_material_production_id:
-                move.order_finished_lot_ids = move.raw_material_production_id.move_finished_ids.mapped('move_line_ids.lot_id').ids
+            if move.raw_material_production_id.move_finished_ids:
+                finished_lots_ids = move.raw_material_production_id.move_finished_ids.mapped('move_line_ids.lot_id').ids
+                if finished_lots_ids:
+                    move.order_finished_lot_ids = finished_lots_ids
+                    move.finished_lots_exist = True
+                else:
+                    move.finished_lots_exist = False
 
     @api.depends('product_id.tracking')
     def _compute_needs_lots(self):
@@ -178,6 +184,37 @@ class StockMove(models.Model):
             })
         return self.env['stock.move']
 
+    def _generate_consumed_move_line(self, qty_to_add, final_lot, lot=False):
+        if lot:
+            ml = self.move_line_ids.filtered(lambda ml: ml.lot_id == lot and not ml.lot_produced_id)
+        else:
+            ml = self.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_id)
+        if ml:
+            new_quantity_done = (ml.qty_done + qty_to_add)
+            if new_quantity_done >= ml.product_uom_qty:
+                ml.write({'qty_done': new_quantity_done, 'lot_produced_id': final_lot.id})
+            else:
+                new_qty_reserved = ml.product_uom_qty - new_quantity_done
+                default = {'product_uom_qty': new_quantity_done,
+                           'qty_done': new_quantity_done,
+                           'lot_produced_id': final_lot.id}
+                ml.copy(default=default)
+                ml.with_context(bypass_reservation_update=True).write({'product_uom_qty': new_qty_reserved, 'qty_done': 0})
+        else:
+            vals = {
+                'move_id': self.id,
+                'product_id': self.product_id.id,
+                'location_id': self.location_id.id,
+                'location_dest_id': self.location_dest_id.id,
+                'product_uom_qty': 0,
+                'product_uom_id': self.product_uom.id,
+                'qty_done': qty_to_add,
+                'lot_produced_id': final_lot.id,
+            }
+            if lot:
+                vals.update({'lot_id': lot.id})
+            self.env['stock.move.line'].create(vals)
+
 
 class PushedFlow(models.Model):
     _inherit = "stock.location.path"
diff --git a/addons/mrp/tests/test_unbuild.py b/addons/mrp/tests/test_unbuild.py
index 8c53593ab0fc..0c98b64b3866 100644
--- a/addons/mrp/tests/test_unbuild.py
+++ b/addons/mrp/tests/test_unbuild.py
@@ -269,8 +269,6 @@ class TestUnbuild(TestMrpCommon):
         self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100, lot_id=lot_1)
         self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5, lot_id=lot_2)
         mo.action_assign()
-        for ml in mo.move_raw_ids.mapped('move_line_ids'):
-            ml.qty_done = ml.product_qty
 
         produce_wizard = self.env['mrp.product.produce'].with_context({
             'active_id': mo.id,
@@ -279,6 +277,8 @@ class TestUnbuild(TestMrpCommon):
             'product_qty': 5.0,
             'lot_id': lot_final.id,
         })
+        for pl in produce_wizard.produce_line_ids:
+            pl.qty_done = pl.qty_to_consume
         produce_wizard.do_produce()
 
         mo.button_mark_done()
@@ -399,3 +399,57 @@ class TestUnbuild(TestMrpCommon):
         self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_1), 1, 'You should have get your product with lot 1 in stock')
         self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have the 3 basic product for lot 2 in stock')
         self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_3), 2, 'You should have get one product back for lot 3')
+        
+        
+    def test_production_links_with_non_tracked_lots(self):
+        """ This test produces an MO in two times and checks that the move lines are linked in a correct way
+        """
+        mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_1='none', tracking_base_2='lot')
+        lot_1 = self.env['stock.production.lot'].create({
+            'name': 'lot_1',
+            'product_id': p2.id,
+        })
+        
+        self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 3, lot_id=lot_1)
+        lot_finished_1 = self.env['stock.production.lot'].create({
+            'name': 'lot_finished_1',
+            'product_id': p_final.id,
+        })
+        
+        produce_wizard = self.env['mrp.product.produce'].with_context({
+            'active_id': mo.id,
+            'active_ids': [mo.id],
+        }).create({
+            'product_qty': 3.0,
+            'lot_id': lot_finished_1.id,
+        })
+        
+        produce_wizard.produce_line_ids[0].lot_id = lot_1.id
+        produce_wizard.do_produce()
+        
+        lot_2 = self.env['stock.production.lot'].create({
+            'name': 'lot_2',
+            'product_id': p2.id,
+        })
+        
+        self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 4, lot_id=lot_2)
+        lot_finished_2 = self.env['stock.production.lot'].create({
+            'name': 'lot_finished_2',
+            'product_id': p_final.id,
+        })
+        
+        produce_wizard = self.env['mrp.product.produce'].with_context({
+            'active_id': mo.id,
+            'active_ids': [mo.id],
+        }).create({
+            'product_qty': 2.0,
+            'lot_id': lot_finished_2.id,
+        })
+        
+        produce_wizard.produce_line_ids[0].lot_id = lot_2.id
+        produce_wizard.do_produce()
+        mo.button_mark_done()
+        ml = mo.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and m.lot_produced_id == lot_finished_1)
+        self.assertEqual(ml.qty_done, 12.0, 'Should have consumed 12 for the first lot')
+        ml = mo.finished_move_line_ids[1].consume_line_ids.filtered(lambda m: m.product_id == p1 and m.lot_produced_id == lot_finished_2)
+        self.assertEqual(ml.qty_done, 8.0, 'Should have consumed 8 for the second lot')
\ No newline at end of file
diff --git a/addons/mrp/views/stock_move_views.xml b/addons/mrp/views/stock_move_views.xml
index 9a1e0970836a..879f576f3f9e 100644
--- a/addons/mrp/views/stock_move_views.xml
+++ b/addons/mrp/views/stock_move_views.xml
@@ -26,7 +26,7 @@
                             </div>
                             <label for="quantity_done"/>
                             <div class="o_row">
-                                <span><field name="quantity_done" attrs="{'readonly': ['|', ('is_locked', '=', True), ('has_tracking', '!=', 'none')]}" nolabel="1"/></span>
+                                <span><field name="quantity_done" attrs="{'readonly': ['|', ('is_locked', '=', True), '|', ('finished_lots_exist', '=', True), ('has_tracking', '!=', 'none')]}" nolabel="1"/></span>
                                 <span> / </span>
                                 <span><field name="reserved_availability" nolabel="1"/></span>
                                 <!-- <span><field name="product_uom" readonly="1" nolabel="1"/></span>-->
@@ -41,11 +41,12 @@
                             <field name="name" invisible="1"/>
                             <field name="has_tracking" invisible="1"/>
                             <field name="order_finished_lot_ids" invisible="1"/>
+                            <field name="finished_lots_exist" invisible="1"/>
                         </group>
                     </group>
-                    <field name="active_move_line_ids" attrs="{'readonly': [('is_locked', '=', True)], 'invisible': [('has_tracking', '=', 'none')]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id,  'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}">
+                    <field name="active_move_line_ids" attrs="{'readonly': [('is_locked', '=', True)], 'invisible': [('finished_lots_exist', '=', False)]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id,  'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}">
                         <tree editable="bottom" decoration-success="product_qty==qty_done" decoration-danger="(product_qty &gt; 0) and (qty_done&gt;product_qty)">
-                            <field name="lot_id" domain="[('product_id', '=', parent.product_id)]" context="{'default_product_id': parent.product_id}"/>
+                            <field name="lot_id" attrs="{'column_invisible': [('parent.has_tracking', '=', 'none')]}" domain="[('product_id', '=', parent.product_id)]" context="{'default_product_id': parent.product_id}"/>
                             <field name="lot_produced_id" options="{'no_open': True, 'no_create': True}" domain="[('id', 'in', parent.order_finished_lot_ids)]" invisible="not context.get('final_lots')"/>
                             <field name="product_qty" string="Reserved" readonly="1"/>
                             <field name="qty_done"/>
diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py
index dd8d08635245..eef65170bb6d 100644
--- a/addons/mrp/wizard/mrp_product_produce.py
+++ b/addons/mrp/wizard/mrp_product_produce.py
@@ -42,7 +42,7 @@ class MrpProductProduce(models.TransientModel):
                     for move_line in move.move_line_ids:
                         if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0:
                             break
-                        if float_compare(move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0:
+                        if move_line.lot_produced_id or float_compare(move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0:
                             continue
                         to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty)
                         lines.append({
@@ -96,7 +96,11 @@ class MrpProductProduce(models.TransientModel):
             # TODO currently not possible to guess if the user updated quantity by hand or automatically by the produce wizard.
             if move.product_id.tracking == 'none' and move.state not in ('done', 'cancel') and move.unit_factor:
                 rounding = move.product_uom.rounding
-                move.quantity_done += float_round(quantity * move.unit_factor, precision_rounding=rounding)
+                if self.product_id.tracking != 'none':
+                    qty_to_add = float_round(quantity * move.unit_factor, precision_rounding=rounding)
+                    move._generate_consumed_move_line(qty_to_add, self.lot_id)
+                else:
+                    move.quantity_done += float_round(quantity * move.unit_factor, precision_rounding=rounding)
         for move in self.production_id.move_finished_ids:
             if move.product_id.tracking == 'none' and move.state not in ('done', 'cancel'):
                 rounding = move.product_uom.rounding
@@ -132,7 +136,7 @@ class MrpProductProduce(models.TransientModel):
                   'product_uom_id': produce_move.product_uom.id,
                   'qty_done': self.product_qty,
                   'lot_id': self.lot_id.id,
-                  'location_id': produce_move.location_id.id, 
+                  'location_id': produce_move.location_id.id,
                   'location_dest_id': produce_move.location_dest_id.id,
                 }
                 self.env['stock.move.line'].create(vals)
@@ -150,39 +154,16 @@ class MrpProductProduce(models.TransientModel):
                         # create a move and put it in there
                         order = self.production_id
                         pl.move_id = self.env['stock.move'].create({
-                                        'name': order.name,
-                                        'product_id': pl.product_id.id,
-                                        'product_uom': pl.product_uom_id.id,
-                                        'location_id': order.location_src_id.id,
-                                        'location_dest_id': self.product_id.property_stock_production.id,
-                                        'raw_material_production_id': order.id,
-                                        'origin': order.name,
-                                        'group_id': order.procurement_group_id.id,
-                                        'state': 'confirmed',
-                                    })
-                ml = pl.move_id.move_line_ids.filtered(lambda ml: ml.lot_id == pl.lot_id and not ml.lot_produced_id)
-                if ml:
-                    if (ml.qty_done + pl.qty_done) >= ml.product_uom_qty:
-                        ml.write({'qty_done': ml.qty_done + pl.qty_done, 'lot_produced_id': self.lot_id.id})
-                    else:
-                        new_qty_todo = ml.product_uom_qty - (ml.qty_done + pl.qty_done)
-                        default = {'product_uom_qty': ml.qty_done + pl.qty_done,
-                                   'qty_done': ml.qty_done + pl.qty_done,
-                                   'lot_produced_id': self.lot_id.id}
-                        ml.copy(default=default)
-                        ml.with_context(bypass_reservation_update=True).write({'product_uom_qty': new_qty_todo, 'qty_done': 0})
-                else:
-                    self.env['stock.move.line'].create({
-                        'move_id': pl.move_id.id,
-                        'product_id': pl.product_id.id,
-                        'location_id': pl.move_id.location_id.id,
-                        'location_dest_id': pl.move_id.location_dest_id.id,
-                        'product_uom_qty': 0,
-                        'product_uom_id': pl.product_uom_id.id,
-                        'qty_done': pl.qty_done,
-                        'lot_id': pl.lot_id.id,
-                        'lot_produced_id': self.lot_id.id,
-                    })
+                                    'name': order.name,
+                                    'product_id': pl.product_id.id,
+                                    'product_uom': pl.product_uom_id.id,
+                                    'location_id': order.location_src_id.id,
+                                    'location_dest_id': self.product_id.property_stock_production.id,
+                                    'raw_material_production_id': order.id,
+                                    'group_id': order.procurement_group_id.id,
+                                    'origin': order.name,
+                                    'state': 'confirmed'})
+                pl.move_id._generate_consumed_move_line(pl.qty_done, self.lot_id, lot=pl.lot_id)
         return True
 
 
-- 
GitLab