[IMP] mrp: multiple finished product lots

This commit allows the user in charge of manufacturing products to
split his production into different lots if this production is done via
workorders. Using a lot in a workorder will adds constraint to all the
other ones. It will also prefil the final lot field and matching quantity
to produce for futur workorders.

This commit uses workorder lines (in another one2many as standard workorder_line_ids)
to register lot/serial number and produced quantity. Prefill lots and
quantities are all read from those lines

Task : 1891864
This commit is contained in:
William Henrotin
2019-04-04 11:13:41 +00:00
parent 108ee65af2
commit 7556938fc7
5 changed files with 177 additions and 42 deletions
@@ -28,6 +28,12 @@ class MrpAbstractWorkorder(models.AbstractModel):
@api.onchange('qty_producing')
def _onchange_qty_producing(self):
""" Modify the qty currently producing will modify the existing
workorder line in order to match the new quantity to consume for each
component and their reserved quantity.
"""
if self.qty_producing <= 0:
raise UserError(_('You have to produce at least one %s.') % self.product_uom_id.name)
line_values = self._update_workorder_lines()
for vals in line_values['to_create']:
self.workorder_line_ids |= self.workorder_line_ids.new(vals)
+159 -35
View File
@@ -3,6 +3,7 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.exceptions import UserError
@@ -103,21 +104,60 @@ class MrpWorkorder(models.Model):
capacity = fields.Float(
'Capacity', default=1.0,
help="Number of pieces that can be produced in parallel.")
workorder_line_ids = fields.One2many('mrp.workorder.line', 'workorder_id', string='Workorder lines')
final_lot_domain = fields.One2many('stock.production.lot', compute="_compute_final_lot_domain")
workorder_line_ids = fields.One2many(
'mrp.workorder.line',
'workorder_id',
string='Workorder lines',
domain=[('is_finished', '=', False)]
)
finished_workorder_line_ids = fields.One2many(
'mrp.workorder.line',
'workorder_id',
string='Final Lot lines',
domain=[('is_finished', '=', True)]
)
allowed_lots_domain = fields.One2many(comodel_name='stock.production.lot', compute="_compute_allowed_lots_domain")
@api.depends('production_id')
def _compute_final_lot_domain(self):
for wo in self:
# check if self is not the first workorder in the list
if self.env['mrp.workorder'].search([('next_work_order_id', '=', wo.id)]):
wo.final_lot_domain = self.env['stock.production.lot'].search([
('use_next_on_work_order_id', '=', wo.id),
]).ids
@api.onchange('final_lot_id')
def _onchange_final_lot_id(self):
"""When the user changes the lot being currently produced, suggest
a quantity to produce consistent with the previous workorders. """
previous_wo = self.env['mrp.workorder'].search([
('next_work_order_id', '=', self.id)
])
if previous_wo:
line = previous_wo.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.final_lot_id)
if line:
self.qty_producing = line.qty_done
def _compute_allowed_lots_domain(self):
""" browse already created lot and respective quantities"""
productions = self.mapped('production_id')
for production in productions:
if production.product_id.tracking == 'none':
continue
rounding = production.product_uom_id.rounding
finished_workorder_lines = production.workorder_ids.mapped('finished_workorder_line_ids')
qties_done_per_lot = defaultdict(list)
for finished_workorder_line in finished_workorder_lines:
qties_done_per_lot[finished_workorder_line.lot_id.id].append(finished_workorder_line.qty_done)
qty_to_produce = production.product_qty
allowed_lot_ids = self.env['stock.production.lot']
qty_produced = sum([max(qty_dones) for qty_dones in qties_done_per_lot.values()])
if float_compare(qty_produced, qty_to_produce, precision_rounding=rounding) < 0:
# If we haven't produced enough, all lots are available
allowed_lot_ids = self.env['stock.production.lot'].search([('product_id', '=', production.product_id.id)])
else:
wo.final_lot_domain = self.env['stock.production.lot'].search([
('product_id', '=', wo.product_id.id),
]).ids
# If we produced enough, only the already produced lots are available
allowed_lot_ids = self.env['stock.production.lot'].browse(qties_done_per_lot.keys())
workorders = production.workorder_ids.filtered(lambda wo: wo.state not in ('done', 'cancel'))
for workorder in workorders:
if workorder.product_tracking == 'serial':
workorder.allowed_lots_domain = allowed_lot_ids - workorder.finished_workorder_line_ids.mapped('lot_id')
else:
workorder.allowed_lots_domain = allowed_lot_ids
@api.multi
def name_get(self):
@@ -195,9 +235,38 @@ class MrpWorkorder(models.Model):
line_values = self._generate_lines_values(move, qty_to_consume)
self.workorder_line_ids |= self.env['mrp.workorder.line'].create(line_values)
def _assign_default_final_lot_id(self):
self.final_lot_id = self.env['stock.production.lot'].search([('use_next_on_work_order_id', '=', self.id)],
order='create_date, id', limit=1)
def _apply_update_workorder_lines(self):
""" update existing line on the workorder. It could be trigger manually
after a modification of qty_producing.
"""
self.ensure_one()
line_values = self._update_workorder_lines()
self.workorder_line_ids |= self.workorder_line_ids.create(line_values['to_create'])
if line_values['to_delete']:
line_values['to_delete'].unlink()
for line, vals in line_values['to_update'].items():
line.write(vals)
def _defaults_from_finished_workorder_line(self, reference_lot_lines):
for r_line in reference_lot_lines:
# see which lot we could suggest and its related qty_producing
if not r_line.lot_id:
continue
candidates = self.finished_workorder_line_ids.filtered(lambda line: line.lot_id == r_line.lot_id)
rounding = self.product_uom_id.rounding
if not candidates:
self.write({
'final_lot_id': r_line.lot_id.id,
'qty_producing': r_line.qty_done,
})
return True
elif float_compare(candidates.qty_done, r_line.qty_done, precision_rounding=rounding) < 0:
self.write({
'final_lot_id': r_line.lot_id.id,
'qty_producing': r_line.qty_done - candidates.qty_done,
})
return True
return False
def _get_byproduct_move_line(self, by_product_move, quantity):
return {
@@ -216,7 +285,7 @@ class MrpWorkorder(models.Model):
return True
self.ensure_one()
if self.qty_producing <= 0:
if float_compare(self.qty_producing, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.'))
# One a piece is produced, you can launch the next work order
@@ -224,39 +293,93 @@ class MrpWorkorder(models.Model):
self.next_work_order_id.state = 'ready'
# If last work order, then post lots used
# TODO: should be same as checking if for every workorder something has been done?
if not self.next_work_order_id:
self._update_finished_move()
# Transfer quantities from temporary to final move line or make them final
self._update_raw_moves()
# Transfert lot and quantity produced to a finished workorder line
if self.product_tracking != 'none' and self.final_lot_id:
self._create_or_update_finished_line()
# Update workorder quantity produced
self.qty_produced += self.qty_producing
if self.final_lot_id:
self.final_lot_id.use_next_on_work_order_id = self.next_work_order_id
self.final_lot_id = False
# Suggest a finished lot on the next workorder
if self.next_work_order_id and self.production_id.product_id.tracking != 'none' and not self.next_work_order_id.final_lot_id:
self.next_work_order_id._defaults_from_finished_workorder_line(self.finished_workorder_line_ids)
# As we may have changed the quantity to produce on the next workorder,
# make sure to update its wokorder lines
self.next_work_order_id._apply_update_workorder_lines()
# Set a qty producing
# Test if the production is done
rounding = self.production_id.product_uom_id.rounding
if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0:
self.qty_producing = 0
elif self.production_id.product_id.tracking == 'serial':
self._assign_default_final_lot_id()
self.qty_producing = 1.0
self.generate_wo_lines()
if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) < 0:
previous_wo = self.env['mrp.workorder']
if self.product_tracking != 'none':
previous_wo = self.env['mrp.workorder'].search([
('next_work_order_id', '=', self.id)
])
candidate_found_in_previous_wo = False
if previous_wo:
candidate_found_in_previous_wo = self._defaults_from_finished_workorder_line(previous_wo.finished_workorder_line_ids)
if not candidate_found_in_previous_wo:
# self is the first workorder
self.qty_producing = self.qty_remaining
self.final_lot_id = False
if self.product_tracking == 'serial':
self.qty_producing = 1
self._apply_update_workorder_lines()
else:
self.qty_producing = float_round(self.production_id.product_qty - self.qty_produced, precision_rounding=rounding)
self.generate_wo_lines()
if self.next_work_order_id and self.production_id.product_id.tracking != 'none':
self.next_work_order_id._assign_default_final_lot_id()
if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0:
self.qty_producing = 0
self.button_finish()
return True
def _create_or_update_finished_line(self):
"""
1. Check that the final lot and the quantity producing is valid regarding
other workorders of this production
2. Save final lot and quantity producing to suggest on next workorder
"""
self.ensure_one()
final_lot_quantity = self.qty_production
rounding = self.product_uom_id.rounding
# Get the max quantity possible for current lot in other workorders
for workorder in (self.production_id.workorder_ids - self):
# We add the remaining quantity to the produced quantity for the
# current lot. For 5 finished products: if in the first wo it
# creates 4 lot A and 1 lot B and in the second it create 3 lot A
# and it remains 2 units to product, it could produce 5 lot A.
# In this case we select 4 since it would conflict with the first
# workorder otherwise.
line = workorder.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.final_lot_id)
if line and float_compare(line.qty_done + workorder.qty_remaining, final_lot_quantity, precision_rounding=rounding) <= 0:
final_lot_quantity = line.qty_done + workorder.qty_remaining
elif float_compare(workorder.qty_remaining, final_lot_quantity, precision_rounding=rounding) < 0:
final_lot_quantity = workorder.qty_remaining
# final lot line for this lot on this workorder.
current_lot_lines = self.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.final_lot_id)
# this lot has already been produced
if float_compare(final_lot_quantity, current_lot_lines.qty_done + self.qty_producing, precision_rounding=rounding) < 0:
raise UserError(_('You have produced %s %s of lot %s in the previous workorder. You are trying to produce %s in this one') %
(final_lot_quantity, self.product_id.uom_id.name, self.final_lot_id.name, current_lot_lines.qty_done + self.qty_producing))
# Update workorder line that regiter final lot created
if not current_lot_lines:
current_lot_lines = self.env['mrp.workorder.line'].create({
'workorder_id': self.id,
'is_finished': True,
'product_id': self.product_id.id,
'lot_id': self.final_lot_id.id,
'qty_done': self.qty_producing,
})
else:
current_lot_lines.qty_done += self.qty_producing
@api.multi
def button_start(self):
self.ensure_one()
@@ -391,6 +514,7 @@ class MrpWorkorderLine(models.Model):
_description = "Workorder move line"
workorder_id = fields.Many2one('mrp.workorder', 'Workorder')
is_finished = fields.Boolean('Finished Lot Line', default=False)
def _get_final_lot(self):
return self.workorder_id.final_lot_id
+1 -5
View File
@@ -1,17 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo import models, _
from odoo.exceptions import UserError
class StockProductionLot(models.Model):
_inherit = 'stock.production.lot'
use_next_on_work_order_id = fields.Many2one('mrp.workorder',
string="Next Work Order to Use",
help='Technical: used to figure out default serial number on work orders')
def _check_create(self):
active_mo_id = self.env.context.get('active_mo_id')
if active_mo_id:
+2
View File
@@ -691,6 +691,7 @@ class TestMrpOrder(TestMrpCommon):
with self.assertRaises(UserError):
# try adding another line for a bom product to increase the quantity
produce_form.qty_producing = 1
with produce_form.workorder_line_ids.new() as line:
line.product_id = p1
line.qty_done = 1
@@ -709,6 +710,7 @@ class TestMrpOrder(TestMrpCommon):
'active_id': mo.id,
'active_ids': [mo.id],
}))
produce_form.qty_producing = 1
with produce_form.workorder_line_ids.new() as line:
line.product_id = self.product_4
line.qty_done = 1
+9 -2
View File
@@ -105,7 +105,7 @@
<field name="is_user_working" invisible="1"/>
<field name="working_state" invisible="1"/>
<field name="production_state" invisible="1"/>
<field name="final_lot_domain" invisible="1"/>
<field name="allowed_lots_domain" invisible="1"/>
<header>
<button name="button_finish" type="object" string="Finish Order" attrs="{'invisible': ['|', ('state', '!=', 'progress'), ('is_produced', '=', False)]}" class="btn-info"/>
<button name="button_start" type="object" string="Start Working" attrs="{'invisible': ['|', ('working_state', '=', 'blocked'), ('state', '!=', 'pending')]}"/>
@@ -149,7 +149,7 @@
<group>
<group>
<field name="qty_producing" string="Quantity in Production" attrs="{'readonly': ['|', ('product_tracking', '=', 'serial'), ('state', 'in', ('done', 'cancel'))]}"/>
<field name="final_lot_id" context="{'default_product_id': product_id}" domain="[('id', 'in', final_lot_domain)]" attrs="{'invisible': [('product_tracking', '=', 'none')]}" groups="stock.group_production_lot"/>
<field name="final_lot_id" context="{'default_product_id': product_id}" domain="[('id', 'in', allowed_lots_domain)]" attrs="{'invisible': [('product_tracking', '=', 'none')]}" groups="stock.group_production_lot"/>
<field name="product_tracking" invisible="1"/>
</group>
</group>
@@ -166,6 +166,13 @@
<field name="move_id" invisible="1"/>
</tree>
</field>
<field name="finished_workorder_line_ids" attrs="{'invisible': [('finished_workorder_line_ids', '=', [])]}" readonly="1">
<tree editable="bottom" create="0" delete="0">
<field name="product_id"/>
<field name="lot_id"/>
<field name="qty_done" string="Done"/>
</tree>
</field>
</page>
<page string="Time Tracking" groups="mrp.group_mrp_manager">
<group>