[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:
Josse Colpaert
2017-10-03 13:57:31 +02:00
committed by amoyaux
parent b7a07d3385
commit d8ae935e74
7 changed files with 128 additions and 49 deletions
+1 -1
View File
@@ -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>
+3 -1
View File
@@ -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
+8 -4
View File
@@ -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:
+39 -2
View File
@@ -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"
+56 -2
View File
@@ -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')
+4 -3
View File
@@ -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"/>
+17 -36
View File
@@ -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