[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.
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user