Skip to content

Commit

Permalink
[FIX] mrp: backport performance fix
Browse files Browse the repository at this point in the history
closes odoo#82215

Signed-off-by: Arnold Moyaux <arm@odoo.com>
  • Loading branch information
diagnoza committed Jan 7, 2022
1 parent 0dae344 commit e3d6691
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 81 deletions.
6 changes: 6 additions & 0 deletions addons/mrp/i18n/mrp.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5388,6 +5388,12 @@ msgstr ""
msgid "You cannot move a manufacturing order once it is cancelled or done."
msgstr ""

#. module: mrp
#: code:addons/mrp/models/mrp_production.py:0
#, python-format
msgid "Unable to split with more than the quantity to produce."
msgstr ""

#. module: mrp
#: code:addons/mrp/models/mrp_workorder.py:0
#, python-format
Expand Down
268 changes: 191 additions & 77 deletions addons/mrp/models/mrp_production.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@
import datetime
import math
import re
import warnings

from collections import defaultdict
from dateutil.relativedelta import relativedelta

from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare, float_round, float_is_zero, format_datetime
from odoo.tools.misc import format_date
from odoo.tools.misc import OrderedSet, format_date

from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES

SIZE_BACK_ORDER_NUMERING = 3


class MrpProduction(models.Model):
""" Manufacturing Orders """
_name = 'mrp.production'
Expand Down Expand Up @@ -1523,9 +1523,195 @@ def _generate_backorder_productions(self, close_mo=True):
wo.qty_producing = 1
else:
wo.qty_producing = wo.qty_remaining

return backorders

def _split_productions(self, amounts=False, cancel_remaning_qty=False):
""" Splits productions into productions smaller quantities to produce, i.e. creates
its backorders.
:param dict amounts: a dict with a production as key and a list value containing
the amounts each production split should produce including the original production,
e.g. {mrp.production(1,): [3, 2]} will result in mrp.production(1,) having a product_qty=3
and a new backorder with product_qty=2.
:return: mrp.production records in order of [orig_prod_1, backorder_prod_1,
backorder_prod_2, orig_prod_2, backorder_prod_2, etc.]
"""
def _default_amounts(production):
return [production.qty_producing, production._get_quantity_to_backorder()]

if not amounts:
amounts = {}
for production in self:
mo_amounts = amounts.get(production)
if not mo_amounts:
amounts[production] = _default_amounts(production)
continue
total_amount = sum(mo_amounts)
if total_amount < production.product_qty and not cancel_remaning_qty:
amounts[production].append(production.product_qty - total_amount)
elif total_amount > production.product_qty or production.state in ['done', 'cancel']:
raise UserError(_("Unable to split with more than the quantity to produce."))

backorder_vals_list = []
initial_qty_by_production = {}

# Create the backorders.
for production in self:
initial_qty_by_production[production] = production.product_qty
if production.backorder_sequence == 0: # Activate backorder naming
production.backorder_sequence = 1
production.name = self._get_name_backorder(production.name, production.backorder_sequence)
production.product_qty = amounts[production][0]
backorder_vals = production.copy_data(default=production._get_backorder_mo_vals())[0]
backorder_qtys = amounts[production][1:]

next_seq = max(production.procurement_group_id.mrp_production_ids.mapped("backorder_sequence"), default=1)

for qty_to_backorder in backorder_qtys:
next_seq += 1
backorder_vals_list.append(dict(
backorder_vals,
product_qty=qty_to_backorder,
name=production._get_name_backorder(production.name, next_seq),
backorder_sequence=next_seq,
state='confirmed'
))

backorders = self.env['mrp.production'].create(backorder_vals_list)

index = 0
production_to_backorders = {}
production_ids = OrderedSet()
for production in self:
number_of_backorder_created = len(amounts.get(production, _default_amounts(production))) - 1
production_backorders = backorders[index:index + number_of_backorder_created]
production_to_backorders[production] = production_backorders
production_ids.update(production.ids)
production_ids.update(production_backorders.ids)
index += number_of_backorder_created

# Split the `stock.move` among new backorders.
new_moves_vals = []
moves = []
for production in self:
for move in production.move_raw_ids | production.move_finished_ids:
if move.additional:
continue
unit_factor = move.product_uom_qty / initial_qty_by_production[production]
initial_move_vals = move.copy_data(move._get_backorder_move_vals())[0]
move.with_context(do_not_unreserve=True).product_uom_qty = production.product_qty * unit_factor

for backorder in production_to_backorders[production]:
move_vals = dict(
initial_move_vals,
product_uom_qty=backorder.product_qty * unit_factor
)
if move.raw_material_production_id:
move_vals['raw_material_production_id'] = backorder.id
else:
move_vals['production_id'] = backorder.id
new_moves_vals.append(move_vals)
moves.append(move)

backorder_moves = self.env['stock.move'].create(new_moves_vals)
# Split `stock.move.line`s. 2 options for this:
# - do_unreserve -> action_assign
# - Split the reserved amounts manually
# The first option would be easier to maintain since it's less code
# However it could be slower (due to `stock.quant` update) and could
# create inconsistencies in mass production if a new lot higher in a
# FIFO strategy arrives between the reservation and the backorder creation
move_to_backorder_moves = defaultdict(lambda: self.env['stock.move'])
for move, backorder_move in zip(moves, backorder_moves):
move_to_backorder_moves[move] |= backorder_move

move_lines_vals = []
assigned_moves = set()
partially_assigned_moves = set()
move_lines_to_unlink = set()

for initial_move, backorder_moves in move_to_backorder_moves.items():
ml_by_move = []
product_uom = initial_move.product_id.uom_id
for move_line in initial_move.move_line_ids:
available_qty = move_line.product_uom_id._compute_quantity(move_line.product_uom_qty, product_uom)
if float_compare(available_qty, 0, precision_rounding=move_line.product_uom_id.rounding) <= 0:
continue
ml_by_move.append((available_qty, move_line, move_line.copy_data()[0]))

initial_move.move_line_ids.with_context(bypass_reservation_update=True).write({'product_uom_qty': 0})
moves = list(initial_move | backorder_moves)

move = moves and moves.pop(0)
move_qty_to_reserve = move.product_qty
for quantity, move_line, ml_vals in ml_by_move:
while float_compare(quantity, 0, precision_rounding=product_uom.rounding) > 0 and move:
# Do not create `stock.move.line` if there is no initial demand on `stock.move`
taken_qty = min(move_qty_to_reserve, quantity)
taken_qty_uom = product_uom._compute_quantity(taken_qty, move_line.product_uom_id)
if move == initial_move:
move_line.with_context(bypass_reservation_update=True).product_uom_qty = taken_qty_uom
elif not float_is_zero(taken_qty_uom, precision_rounding=move_line.product_uom_id.rounding):
move_lines_vals.append(dict(
ml_vals,
product_uom_qty=taken_qty_uom,
move_id=move.id
))
quantity -= taken_qty
move_qty_to_reserve -= taken_qty

if float_compare(move_qty_to_reserve, 0, precision_rounding=move.product_uom.rounding) <= 0:
assigned_moves.add(move.id)
move = moves and moves.pop(0)
move_qty_to_reserve = move and move.product_qty or 0

# Unreserve the quantity removed from initial `stock.move.line` and
# not assigned to a move anymore. In case of a split smaller than initial
# quantity and fully reserved
if quantity:
self.env['stock.quant']._update_reserved_quantity(
move_line.product_id, move_line.location_id, -quantity,
lot_id=move_line.lot_id, package_id=move_line.package_id,
owner_id=move_line.owner_id, strict=True)

if move and move_qty_to_reserve != move.product_qty:
partially_assigned_moves.add(move.id)

move_lines_to_unlink.update(initial_move.move_line_ids.filtered(
lambda ml: not ml.product_uom_qty and not ml.qty_done).ids)

self.env['stock.move'].browse(assigned_moves).write({'state': 'assigned'})
self.env['stock.move'].browse(partially_assigned_moves).write({'state': 'partially_available'})
# Avoid triggering a useless _recompute_state
self.env['stock.move.line'].browse(move_lines_to_unlink).write({'move_id': False})
self.env['stock.move.line'].browse(move_lines_to_unlink).unlink()
self.env['stock.move.line'].create(move_lines_vals)

# We need to adapt `duration_expected` on both the original workorders and their
# backordered workorders. To do that, we use the original `duration_expected` and the
# ratio of the quantity produced and the quantity to produce.
for production in self:
initial_qty = initial_qty_by_production[production]
initial_workorder_remaining_qty = []
bo = production_to_backorders[production]

# Adapt duration
for workorder in (production | bo).workorder_ids:
workorder.duration_expected = workorder.duration_expected * workorder.production_id.product_qty / initial_qty

# Adapt quantities produced
for workorder in production.workorder_ids:
initial_workorder_remaining_qty.append(max(workorder.qty_produced - workorder.qty_production, 0))
workorder.qty_produced = min(workorder.qty_produced, workorder.qty_production)
workorders_len = len(bo.workorder_ids)
for index, workorder in enumerate(bo.workorder_ids):
remaining_qty = initial_workorder_remaining_qty[index // workorders_len]
if remaining_qty:
workorder.qty_produced = max(workorder.qty_production, remaining_qty)
initial_workorder_remaining_qty[index % workorders_len] = max(remaining_qty - workorder.qty_produced, 0)
backorders.workorder_ids._action_confirm()

return self.env['mrp.production'].browse(production_ids)

def button_mark_done(self):
self._button_mark_done_sanity_checks()

Expand Down Expand Up @@ -1896,77 +2082,5 @@ def _check_serial_mass_produce_components(self):
return (have_serial_components, have_lot_components, missing_components, multiple_lot_components)

def _generate_backorder_productions_multi(self, serial_numbers, cancel_remaining_quantities=False):
self.ensure_one()
if self.product_id.tracking != 'serial':
raise UserError(_('Expected product tracked by Serial Number.'))
if self.backorder_sequence == 0:
self.backorder_sequence = 1
result = []
production = self
for serial_number in serial_numbers:
production.qty_producing = 1
if production.product_qty > 1 and (serial_number != serial_numbers[-1] or not cancel_remaining_quantities):
backorder = production.copy(default=production._get_backorder_mo_vals())
else:
backorder = None
new_moves_origin = []
new_moves_quantity_to_split = []
new_moves_vals = []
for move in production.move_raw_ids | production.move_finished_ids:
if not move.additional:
uom_qty_to_split = move.product_uom_qty - move.unit_factor * production.qty_producing
qty_to_split = move.product_uom._compute_quantity(uom_qty_to_split, move.product_id.uom_id, rounding_method='HALF-UP')
uom_qty_to_split = float_round(uom_qty_to_split, precision_rounding=move.product_uom.rounding, rounding_method='HALF-UP')
move_vals = move._split(qty_to_split)
if backorder:
if not move_vals:
continue
if move.raw_material_production_id:
move_vals[0]['raw_material_production_id'] = backorder.id
else:
move_vals[0]['production_id'] = backorder.id
new_moves_origin.append(move)
new_moves_vals.append(move_vals[0])
new_moves_quantity_to_split.append(uom_qty_to_split)
elif move.raw_material_production_id:
move.move_line_ids.product_uom_qty = 0
move.move_line_ids[0].product_uom_qty = move.product_uom_qty
if backorder:
new_moves = self.env['stock.move'].create(new_moves_vals)
for move, quantity_to_split, new_move in zip(new_moves_origin, new_moves_quantity_to_split, new_moves):
if move.raw_material_production_id:
move.move_line_ids[0].copy({
'move_id': new_move.id,
'product_uom_qty': quantity_to_split,
'lot_id': move.move_line_ids[0].lot_id.id if move.move_line_ids[0].lot_id else 0,
})
move.with_context(bypass_reservation_update=True).move_line_ids.product_uom_qty = 0
move.with_context(bypass_reservation_update=True).move_line_ids[0].product_uom_qty = move.product_uom_qty
for old_wo, wo in zip(production.workorder_ids, backorder.workorder_ids):
wo.qty_produced = max(old_wo.qty_produced - old_wo.qty_producing, 0)
wo.qty_producing = 1
ratio = production.qty_producing / production.product_qty
for workorder in production.workorder_ids:
workorder.duration_expected = workorder.duration_expected * ratio
for workorder in backorder.workorder_ids:
workorder.duration_expected = workorder.duration_expected * (1 - ratio)
backorder.action_confirm()
production.name = self._get_name_backorder(production.name, production.backorder_sequence)
production.product_qty = 1
production.lot_producing_id = self.env['stock.production.lot'].create({
'product_id': production.product_id.id,
'company_id': production.company_id.id,
'name': serial_number,
})
qty_producing_uom = production.product_uom_id._compute_quantity(production.qty_producing, production.product_id.uom_id, rounding_method='HALF-UP')
if qty_producing_uom != 1:
production.qty_producing = production.product_id.uom_id._compute_quantity(1, production.product_uom_id, rounding_method='HALF-UP')
for move in (production.move_raw_ids | production.move_finished_ids.filtered(lambda m: m.product_id != production.product_id)):
if not move.product_uom:
continue
new_qty = float_round((production.qty_producing - production.qty_produced) * move.unit_factor, precision_rounding=move.product_uom.rounding)
move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty)
result.append((production.lot_producing_id.id, production.id))
production = backorder
return result
warnings.warn("Method '_generate_backorder_productions_multi()' is deprecated, use _split_productions() instead.", DeprecationWarning)
self._split_productions({self: [1] * len(serial_numbers)}, cancel_remaining_quantities)
11 changes: 10 additions & 1 deletion addons/mrp/models/stock_move.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, fields, models, _
from odoo import api, Command, fields, models, _
from odoo.osv import expression
from odoo.tools import float_compare, float_round, float_is_zero, OrderedSet

Expand Down Expand Up @@ -354,6 +354,15 @@ def _consuming_picking_types(self):
res.append('mrp_operation')
return res

def _get_backorder_move_vals(self):
self.ensure_one()
return {
'state': 'confirmed',
'reservation_date': self.reservation_date,
'move_orig_ids': [Command.link(m.id) for m in self.mapped('move_orig_ids')],
'move_dest_ids': [Command.link(m.id) for m in self.mapped('move_dest_ids')]
}

def _get_source_document(self):
res = super()._get_source_document()
return res or self.production_id or self.raw_material_production_id
Expand Down
24 changes: 21 additions & 3 deletions addons/mrp/wizard/stock_assign_serial_numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,30 @@ def _onchange_serial_numbers(self):
self.produced_qty = len(serial_numbers)
self.show_apply = self.produced_qty == self.expected_qty
self.show_backorders = self.produced_qty > 0 and self.produced_qty < self.expected_qty

def _assign_serial_numbers(self, cancel_remaining_quantity=False):
serial_numbers = self._get_serial_numbers()
productions = self.production_id._split_productions(
{self.production_id: [1] * len(serial_numbers)}, cancel_remaining_quantity)
production_lots_vals = []
for serial_name in serial_numbers:
production_lots_vals.append({
'product_id': self.production_id.product_id.id,
'company_id': self.production_id.company_id.id,
'name': serial_name,
})
production_lots = self.env['stock.production.lot'].create(production_lots_vals)
for production, production_lot in zip(productions, production_lots):
production.lot_producing_id = production_lot.id
production.qty_producing = production.product_qty
for workorder in production.workorder_ids:
workorder.qty_produced = workorder.qty_producing

def apply(self):
self.production_id._generate_backorder_productions_multi(self._get_serial_numbers())
self._assign_serial_numbers()

def create_backorder(self):
self.production_id._generate_backorder_productions_multi(self._get_serial_numbers(), False)
self._assign_serial_numbers(False)

def no_backorder(self):
self.production_id._generate_backorder_productions_multi(self._get_serial_numbers(), True)
self._assign_serial_numbers(True)

0 comments on commit e3d6691

Please sign in to comment.