diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py index 8864d1e7bbf..c2381aed0a7 100644 --- a/addons/stock/models/stock_orderpoint.py +++ b/addons/stock/models/stock_orderpoint.py @@ -1,14 +1,19 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import logging from datetime import datetime, time -from json import dumps - from dateutil import relativedelta +from json import dumps +from psycopg2 import OperationalError -from odoo import _, api, fields, models +from odoo import SUPERUSER_ID, _, api, fields, models, registry +from odoo.addons.stock.models.stock_rule import ProcurementException from odoo.exceptions import UserError, ValidationError from odoo.osv import expression +from odoo.tools import float_compare, split_every + +_logger = logging.getLogger(__name__) class StockWarehouseOrderpoint(models.Model): @@ -159,5 +164,73 @@ class StockWarehouseOrderpoint(models.Model): 'group_id': group or self.group_id, } + def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=None): + """ Create procurements based on orderpoints. + :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing + 1000 orderpoints. + This is appropriate for batch jobs only. + """ + self = self.with_company(company_id) + orderpoints_noprefetch = self.read(['id']) + orderpoints_noprefetch = [orderpoint['id'] for orderpoint in orderpoints_noprefetch] + + for orderpoints_batch in split_every(1000, orderpoints_noprefetch): + if use_new_cursor: + cr = registry(self._cr.dbname).cursor() + self = self.with_env(self.env(cr=cr)) + orderpoints_batch = self.env['stock.warehouse.orderpoint'].browse(orderpoints_batch) + orderpoints_exceptions = [] + while orderpoints_batch: + procurements = [] + for orderpoint in orderpoints_batch: + if float_compare(orderpoint.qty_to_order, 0.0, precision_rounding=orderpoint.product_uom.rounding) == 1: + date = datetime.combine(orderpoint.lead_days_date, time.min) + values = orderpoint._prepare_procurement_values(orderpoint.qty_to_order, date=date) + procurements.append(self.env['procurement.group'].Procurement( + orderpoint.product_id, orderpoint.qty_to_order, orderpoint.product_uom, + orderpoint.location_id, orderpoint.name, orderpoint.name, + orderpoint.company_id, values)) + + try: + with self.env.cr.savepoint(): + self.env['procurement.group'].with_context(from_orderpoint=True).run(procurements, raise_user_error=False) + except ProcurementException as errors: + for procurement, error_msg in errors.procurement_exceptions: + orderpoints_exceptions += [(procurement.values.get('orderpoint_id'), error_msg)] + failed_orderpoints = self.env['stock.warehouse.orderpoint'].concat(*[o[0] for o in orderpoints_exceptions]) + if not failed_orderpoints: + _logger.error('Unable to process orderpoints') + break + orderpoints_batch -= failed_orderpoints + + except OperationalError: + if use_new_cursor: + cr.rollback() + continue + else: + raise + else: + orderpoints_batch._post_process_scheduler() + break + + # Log an activity on product template for failed orderpoints. + for orderpoint, error_msg in orderpoints_exceptions: + existing_activity = self.env['mail.activity'].search([ + ('res_id', '=', orderpoint.product_id.product_tmpl_id.id), + ('res_model_id', '=', self.env.ref('product.model_product_template').id), + ('note', '=', error_msg)]) + if not existing_activity: + orderpoint.product_id.product_tmpl_id.activity_schedule( + 'mail.mail_activity_data_warning', + note=error_msg, + user_id=orderpoint.product_id.responsible_id.id or SUPERUSER_ID, + ) + + if use_new_cursor: + cr.commit() + cr.close() + + return {} + def _post_process_scheduler(self): return True diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py index ddc37eccbcf..96652ed970e 100644 --- a/addons/stock/models/stock_rule.py +++ b/addons/stock/models/stock_rule.py @@ -3,15 +3,13 @@ import logging from collections import defaultdict, namedtuple -from datetime import datetime, time from dateutil.relativedelta import relativedelta -from psycopg2 import OperationalError -from odoo import SUPERUSER_ID, _, api, fields, models, registry +from odoo import _, api, fields, models, registry from odoo.exceptions import UserError from odoo.osv import expression -from odoo.tools import float_compare, float_is_zero, float_round, frozendict, html_escape +from odoo.tools import float_compare, float_is_zero, html_escape from odoo.tools.misc import split_every _logger = logging.getLogger(__name__) @@ -502,7 +500,9 @@ class ProcurementGroup(models.Model): @api.model def _run_scheduler_tasks(self, use_new_cursor=False, company_id=False): # Minimum stock rules - self.sudo()._procure_orderpoint_confirm(use_new_cursor=use_new_cursor, company_id=company_id) + domain = self._get_orderpoint_domain(company_id=company_id) + orderpoints = self.env['stock.warehouse.orderpoint'].search(domain) + orderpoints.sudo()._procure_orderpoint_confirm(use_new_cursor=use_new_cursor, company_id=company_id) # Search all confirmed stock_moves and try to assign them domain = self._get_moves_to_assign_domain() @@ -548,102 +548,3 @@ class ProcurementGroup(models.Model): domain = [('company_id', '=', company_id)] if company_id else [] domain += [('product_id.active', '=', True)] return domain - - @api.model - def _procure_orderpoint_confirm(self, use_new_cursor=False, company_id=None): - """ Create procurements based on orderpoints. - :param bool use_new_cursor: if set, use a dedicated cursor and auto-commit after processing - 1000 orderpoints. - This is appropriate for batch jobs only. - """ - self = self.with_company(company_id) - domain = self._get_orderpoint_domain(company_id=company_id) - orderpoints_noprefetch = self.env['stock.warehouse.orderpoint'].search_read( - domain, fields=['id'], order=self._procurement_from_orderpoint_get_order()) - orderpoints_noprefetch = [orderpoint['id'] for orderpoint in orderpoints_noprefetch] - - for orderpoints_batch in split_every(1000, orderpoints_noprefetch): - if use_new_cursor: - cr = registry(self._cr.dbname).cursor() - self = self.with_env(self.env(cr=cr)) - orderpoints_batch = self.env['stock.warehouse.orderpoint'].browse(orderpoints_batch) - orderpoints_exceptions = [] - while orderpoints_batch: - - # Calculate groups that can be executed together - orderpoints_contexts = defaultdict(lambda: self.env['stock.warehouse.orderpoint']) - - procurements = [] - for orderpoint in orderpoints_batch: - orderpoint_context = orderpoint._get_product_context() - product_context = frozendict({**self.env.context, **orderpoint_context}) - orderpoints_contexts[product_context] |= orderpoint - - for orderpoint_context, orderpoints_by_context in orderpoints_contexts.items(): - substract_quantity = orderpoints_by_context._quantity_in_progress() - product_quantity = orderpoints_by_context.product_id.with_context(orderpoint_context)._product_available() - - for orderpoint in orderpoints_by_context: - op_product_virtual = product_quantity[orderpoint.product_id.id]['virtual_available'] - if op_product_virtual is None: - continue - if float_compare(op_product_virtual, orderpoint.product_min_qty, precision_rounding=orderpoint.product_uom.rounding) <= 0: - qty = max(orderpoint.product_min_qty, orderpoint.product_max_qty) - op_product_virtual - remainder = orderpoint.qty_multiple > 0 and qty % orderpoint.qty_multiple or 0.0 - - if float_compare(remainder, 0.0, precision_rounding=orderpoint.product_uom.rounding) > 0: - qty += orderpoint.qty_multiple - remainder - - if float_compare(qty, 0.0, precision_rounding=orderpoint.product_uom.rounding) < 0: - continue - - qty -= substract_quantity[orderpoint.id] - qty_rounded = float_round(qty, precision_rounding=orderpoint.product_uom.rounding) - if qty_rounded > 0: - date = datetime.combine(orderpoint_context.get('to_date'), time.min) - values = orderpoint._prepare_procurement_values(qty_rounded, date=date) - procurements.append(self.env['procurement.group'].Procurement( - orderpoint.product_id, qty_rounded, orderpoint.product_uom, - orderpoint.location_id, orderpoint.name, orderpoint.name, - orderpoint.company_id, values)) - - try: - with self.env.cr.savepoint(): - self.env['procurement.group'].with_context(from_orderpoint=True).run(procurements, raise_user_error=False) - except ProcurementException as errors: - for procurement, error_msg in errors.procurement_exceptions: - orderpoints_exceptions += [(procurement.values.get('orderpoint_id'), error_msg)] - failed_orderpoints = self.env['stock.warehouse.orderpoint'].concat(*[o[0] for o in orderpoints_exceptions]) - if not failed_orderpoints: - _logger.error('Unable to process orderpoints') - break - orderpoints_batch -= failed_orderpoints - - except OperationalError: - if use_new_cursor: - cr.rollback() - continue - else: - raise - else: - orderpoints_batch._post_process_scheduler() - break - - # Log an activity on product template for failed orderpoints. - for orderpoint, error_msg in orderpoints_exceptions: - existing_activity = self.env['mail.activity'].search([ - ('res_id', '=', orderpoint.product_id.product_tmpl_id.id), - ('res_model_id', '=', self.env.ref('product.model_product_template').id), - ('note', '=', error_msg)]) - if not existing_activity: - orderpoint.product_id.product_tmpl_id.activity_schedule( - 'mail.mail_activity_data_warning', - note=error_msg, - user_id=orderpoint.product_id.responsible_id.id or SUPERUSER_ID, - ) - - if use_new_cursor: - cr.commit() - cr.close() - - return {}