diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml index 33cc801b9fcd657a7592d640c5eb00a211c26e97..b15716264b4a61beff24fe828f6c3305c25f7ce1 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 1f574c1d24e6b9a89488247c9262f97328625ee9..59bafc27487d87befb3e03d2e3a24a75e499a890 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 18329432edd499065af0e6ddc4b7829e7e2bd486..61e7865d5cc51530fd172e57951c214673c7e14b 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 966e3365b6c4dc29140c11cb414b98f7dc71740f..4eb94b2379a3018a466709102f8768dd8570feb7 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 8c53593ab0fcb6a8c24e9d8f69ea18e8f6289b2b..0c98b64b3866500762a71eea5570c04411be7b47 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 9a1e0970836a313156f7dd1ea1a20705656ee435..879f576f3f9e16049cb6dcbc490ab36b49ca9f77 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 > 0) and (qty_done>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 dd8d08635245f2c76cb47d84f8d0dba804528461..eef65170bb6d1b269f8890f0a04c52f37c6e699b 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