Current behavior: Confirming a Purchase Order with more than 50 lines takes too much time to be processed. In the case of the client they had PO with more than 200 lines which makes it impossible for them to confirm them. Step to reproduce: - Install mrp and mrp_subcontracting - Create PO with more than 50 order lines or more - Try to confirm it - Take a long time or timeout Benchmark (made in 16): | No. of PO lines | Before | After | |-----------------|:-------:|:------:| | 9 | 1s30 | 1s30 | | 91 | 1min | 16s | | 273 | 4min | 50s | | 405 | 4min30s | 1min6s | Fix: Batch more actions and records to reduce the number of queries generated by the ORM. opw-3625892 closes odoo/odoo#149830 X-original-commit: 7d9d7917948df966ada2055dcfead0cabaaf0651 Signed-off-by: Arnold Moyaux (arm) <arm@odoo.com>
184 lines
9.6 KiB
Python
184 lines
9.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
from collections import defaultdict
|
|
from datetime import timedelta
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools.float_utils import float_compare
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
|
|
class StockPicking(models.Model):
|
|
_name = 'stock.picking'
|
|
_inherit = 'stock.picking'
|
|
|
|
move_line_ids_without_package = fields.One2many(
|
|
domain=['&', '|', ('location_dest_id.usage', '!=', 'production'), ('move_id.picking_code', '!=', 'outgoing'),
|
|
'|', ('package_level_id', '=', False), ('picking_type_entire_packs', '=', False)])
|
|
display_action_record_components = fields.Selection(
|
|
[('hide', 'Hide'), ('facultative', 'Facultative'), ('mandatory', 'Mandatory')],
|
|
compute='_compute_display_action_record_components')
|
|
|
|
@api.depends('state', 'move_ids')
|
|
def _compute_display_action_record_components(self):
|
|
self.display_action_record_components = 'hide'
|
|
for picking in self:
|
|
# Hide if not encoding state or it is not a subcontracting picking
|
|
if picking.state in ('draft', 'cancel', 'done') or not picking._is_subcontract():
|
|
continue
|
|
subcontracted_moves = picking.move_ids.filtered(lambda m: m.is_subcontract)
|
|
if subcontracted_moves._subcontrating_should_be_record():
|
|
picking.display_action_record_components = 'mandatory'
|
|
continue
|
|
if subcontracted_moves._subcontrating_can_be_record():
|
|
picking.display_action_record_components = 'facultative'
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Action methods
|
|
# -------------------------------------------------------------------------
|
|
def _action_done(self):
|
|
res = super(StockPicking, self)._action_done()
|
|
for move in self.move_ids:
|
|
if not move.is_subcontract:
|
|
continue
|
|
# Auto set qty_producing/lot_producing_id of MO wasn't recorded
|
|
# manually (if the flexible + record_component or has tracked component)
|
|
productions = move._get_subcontract_production()
|
|
recorded_productions = productions.filtered(lambda p: p._has_been_recorded())
|
|
recorded_qty = sum(recorded_productions.mapped('qty_producing'))
|
|
sm_done_qty = sum(productions._get_subcontract_move().filtered(lambda m: m.picked).mapped('quantity'))
|
|
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
if float_compare(move.product_uom_qty, move.quantity, precision_digits=rounding) > 0 and self._context.get('cancel_backorder'):
|
|
move._update_subcontract_order_qty(move.quantity)
|
|
if float_compare(recorded_qty, sm_done_qty, precision_digits=rounding) >= 0:
|
|
continue
|
|
production = productions - recorded_productions
|
|
if not production:
|
|
continue
|
|
if len(production) > 1:
|
|
raise UserError(_("There shouldn't be multiple productions to record for the same subcontracted move."))
|
|
# Manage additional quantities
|
|
quantity_done_move = move.product_uom._compute_quantity(move.quantity, production.product_uom_id)
|
|
if float_compare(production.product_qty, quantity_done_move, precision_rounding=production.product_uom_id.rounding) == -1:
|
|
change_qty = self.env['change.production.qty'].create({
|
|
'mo_id': production.id,
|
|
'product_qty': quantity_done_move
|
|
})
|
|
change_qty.with_context(skip_activity=True).change_prod_qty()
|
|
# Create backorder MO for each move lines
|
|
amounts = [move_line.quantity for move_line in move.move_line_ids]
|
|
len_amounts = len(amounts)
|
|
productions = production._split_productions({production: amounts}, set_consumed_qty=True)
|
|
productions.move_finished_ids.move_line_ids.write({'quantity': 0})
|
|
for production, move_line in zip(productions, move.move_line_ids):
|
|
if move_line.lot_id:
|
|
production.lot_producing_id = move_line.lot_id
|
|
production.qty_producing = production.product_qty
|
|
production._set_qty_producing()
|
|
productions[:len_amounts].subcontracting_has_been_recorded = True
|
|
|
|
for picking in self:
|
|
productions_to_done = picking._get_subcontract_production()._subcontracting_filter_to_done()
|
|
productions_to_done._subcontract_sanity_check()
|
|
if not productions_to_done:
|
|
continue
|
|
productions_to_done = productions_to_done.sudo()
|
|
production_ids_backorder = []
|
|
if not self.env.context.get('cancel_backorder'):
|
|
production_ids_backorder = productions_to_done.filtered(lambda mo: mo.state == "progress").ids
|
|
productions_to_done.with_context(mo_ids_to_backorder=production_ids_backorder).button_mark_done()
|
|
# For concistency, set the date on production move before the date
|
|
# on picking. (Traceability report + Product Moves menu item)
|
|
minimum_date = min(picking.move_line_ids.mapped('date'))
|
|
production_moves = productions_to_done.move_raw_ids | productions_to_done.move_finished_ids
|
|
production_moves.write({'date': minimum_date - timedelta(seconds=1)})
|
|
production_moves.move_line_ids.write({'date': minimum_date - timedelta(seconds=1)})
|
|
|
|
return res
|
|
|
|
def action_record_components(self):
|
|
self.ensure_one()
|
|
move_subcontracted = self.move_ids.filtered(lambda m: m.is_subcontract)
|
|
for move in move_subcontracted:
|
|
production = move._subcontrating_should_be_record()
|
|
if production:
|
|
return move._action_record_components()
|
|
for move in move_subcontracted:
|
|
production = move._subcontrating_can_be_record()
|
|
if production:
|
|
return move._action_record_components()
|
|
raise UserError(_("Nothing to record"))
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Subcontract helpers
|
|
# -------------------------------------------------------------------------
|
|
def _is_subcontract(self):
|
|
self.ensure_one()
|
|
return self.picking_type_id.code == 'incoming' and any(m.is_subcontract for m in self.move_ids)
|
|
|
|
def _get_subcontract_production(self):
|
|
return self.move_ids._get_subcontract_production()
|
|
|
|
def _get_warehouse(self, subcontract_move):
|
|
return subcontract_move.warehouse_id or self.picking_type_id.warehouse_id or subcontract_move.move_dest_ids.picking_type_id.warehouse_id
|
|
|
|
def _prepare_subcontract_mo_vals(self, subcontract_move, bom):
|
|
subcontract_move.ensure_one()
|
|
group = self.env['procurement.group'].create({
|
|
'name': self.name,
|
|
'partner_id': self.partner_id.id,
|
|
})
|
|
product = subcontract_move.product_id
|
|
warehouse = self._get_warehouse(subcontract_move)
|
|
vals = {
|
|
'company_id': subcontract_move.company_id.id,
|
|
'procurement_group_id': group.id,
|
|
'subcontractor_id': subcontract_move.picking_id.partner_id.commercial_partner_id.id,
|
|
'picking_ids': [subcontract_move.picking_id.id],
|
|
'product_id': product.id,
|
|
'product_uom_id': subcontract_move.product_uom.id,
|
|
'bom_id': bom.id,
|
|
'location_src_id': subcontract_move.picking_id.partner_id.with_company(subcontract_move.company_id).property_stock_subcontractor.id,
|
|
'location_dest_id': subcontract_move.picking_id.partner_id.with_company(subcontract_move.company_id).property_stock_subcontractor.id,
|
|
'product_qty': subcontract_move.product_qty or subcontract_move.quantity,
|
|
'picking_type_id': warehouse.subcontracting_type_id.id,
|
|
'date_start': subcontract_move.date - relativedelta(days=bom.produce_delay)
|
|
}
|
|
return vals
|
|
|
|
def _subcontracted_produce(self, subcontract_details):
|
|
self.ensure_one()
|
|
group_move = defaultdict(list)
|
|
group_by_company = defaultdict(list)
|
|
for move, bom in subcontract_details:
|
|
# do not create extra production for move that have their quantity updated
|
|
if move.move_orig_ids.production_id:
|
|
continue
|
|
quantity = move.product_qty or move.quantity
|
|
if float_compare(quantity, 0, precision_rounding=move.product_uom.rounding) <= 0:
|
|
# If a subcontracted amount is decreased, don't create a MO that would be for a negative value.
|
|
continue
|
|
|
|
mo_subcontract = self._prepare_subcontract_mo_vals(move, bom)
|
|
# Link the move to the id of the MO's procurement group
|
|
group_move[mo_subcontract['procurement_group_id']] = move
|
|
# Group the MO by company
|
|
group_by_company[move.company_id.id].append(mo_subcontract)
|
|
|
|
all_mo = set()
|
|
for company, group in group_by_company.items():
|
|
grouped_mo = self.env['mrp.production'].with_company(company).create(group)
|
|
all_mo.update(grouped_mo.ids)
|
|
|
|
all_mo = self.env['mrp.production'].browse(sorted(all_mo))
|
|
all_mo.action_confirm()
|
|
|
|
for mo in all_mo:
|
|
move = group_move[mo.procurement_group_id.id][0]
|
|
mo.write({'date_finished': move.date})
|
|
finished_move = mo.move_finished_ids.filtered(lambda m: m.product_id == move.product_id)
|
|
finished_move.write({'move_dest_ids': [(4, move.id, False)]})
|
|
|
|
all_mo.action_assign()
|