[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:
@@ -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)
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user