diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py index 25537727bc..b6f2b895d6 100644 --- a/addons/delivery/models/stock_picking.py +++ b/addons/delivery/models/stock_picking.py @@ -2,6 +2,7 @@ # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details. import json +from collections import defaultdict from flectra import models, fields, api, _ from flectra.exceptions import UserError @@ -13,21 +14,31 @@ class StockQuantPackage(models.Model): @api.depends('quant_ids') def _compute_weight(self): + if self.env.context.get('picking_id'): + package_weights = defaultdict(float) + # Ordering by qty_done prevents the default ordering by groupby fields that can inject multiple Left Joins in the resulting query. + res_groups = self.env['stock.move.line'].read_group( + [('result_package_id', 'in', self.ids), ('product_id', '!=', False), ('picking_id', '=', self.env.context['picking_id'])], + ['id:count'], + ['result_package_id', 'product_id', 'product_uom_id', 'qty_done'], + lazy=False, orderby='qty_done asc' + ) + for res_group in res_groups: + product_id = self.env['product.product'].browse(res_group['product_id'][0]) + product_uom_id = self.env['uom.uom'].browse(res_group['product_uom_id'][0]) + package_weights[res_group['result_package_id'][0]] += ( + res_group['__count'] + * product_uom_id._compute_quantity(res_group['qty_done'], product_id.uom_id) + * product_id.weight + ) for package in self: - weight = 0.0 if self.env.context.get('picking_id'): - # TODO: potential bottleneck: N packages = N queries, use groupby ? - current_picking_move_line_ids = self.env['stock.move.line'].search([ - ('result_package_id', '=', package.id), - ('picking_id', '=', self.env.context['picking_id']) - ]) - for ml in current_picking_move_line_ids: - weight += ml.product_uom_id._compute_quantity( - ml.qty_done, ml.product_id.uom_id) * ml.product_id.weight + package.weight = package_weights[package.id] else: + weight = 0.0 for quant in package.quant_ids: weight += quant.quantity * quant.product_id.weight - package.weight = weight + package.weight = weight def _get_default_weight_uom(self): return self.env['product.template']._get_weight_uom_name_from_ir_config_parameter() @@ -49,19 +60,32 @@ class StockPicking(models.Model): def _compute_packages(self): for package in self: packs = set() - for move_line in package.move_line_ids: - if move_line.result_package_id: - packs.add(move_line.result_package_id.id) + if self.env['stock.move.line'].search_count([('picking_id', '=', package.id), ('result_package_id', '!=', False)]): + for move_line in package.move_line_ids: + if move_line.result_package_id: + packs.add(move_line.result_package_id.id) package.package_ids = list(packs) @api.depends('move_line_ids', 'move_line_ids.result_package_id', 'move_line_ids.product_uom_id', 'move_line_ids.qty_done') def _compute_bulk_weight(self): + picking_weights = defaultdict(float) + # Ordering by qty_done prevents the default ordering by groupby fields that can inject multiple Left Joins in the resulting query. + res_groups = self.env['stock.move.line'].read_group( + [('picking_id', 'in', self.ids), ('product_id', '!=', False), ('result_package_id', '=', False)], + ['id:count'], + ['picking_id', 'product_id', 'product_uom_id', 'qty_done'], + lazy=False, orderby='qty_done asc' + ) + for res_group in res_groups: + product_id = self.env['product.product'].browse(res_group['product_id'][0]) + product_uom_id = self.env['uom.uom'].browse(res_group['product_uom_id'][0]) + picking_weights[res_group['picking_id'][0]] += ( + res_group['__count'] + * product_uom_id._compute_quantity(res_group['qty_done'], product_id.uom_id) + * product_id.weight + ) for picking in self: - weight = 0.0 - for move_line in picking.move_line_ids: - if move_line.product_id and not move_line.result_package_id: - weight += move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.product_id.uom_id) * move_line.product_id.weight - picking.weight_bulk = weight + picking.weight_bulk = picking_weights[picking.id] @api.depends('move_line_ids.result_package_id', 'move_line_ids.result_package_id.shipping_weight', 'weight_bulk') def _compute_shipping_weight(self): diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index e6216b4add..ae4ed3d706 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -110,7 +110,7 @@ class PaymentAcquirer(models.Model): help="Capture the amount from Flectra, when the delivery is completed.") journal_id = fields.Many2one( 'account.journal', 'Payment Journal', domain="[('type', 'in', ['bank', 'cash']), ('company_id', '=', company_id)]", - help="""Journal where the successful transactions will be posted""") + help="""Journal where the successful transactions will be posted""", ondelete='restrict') check_validity = fields.Boolean(string="Verify Card Validity", help="""Trigger a transaction of 1 currency unit and its refund to check the validity of new credit cards entered in the customer portal. Without this check, the validity will be verified at the very first transaction.""") diff --git a/addons/project/models/project_task_recurrence.py b/addons/project/models/project_task_recurrence.py index b5cad51333..ad8776c1ed 100644 --- a/addons/project/models/project_task_recurrence.py +++ b/addons/project/models/project_task_recurrence.py @@ -8,8 +8,6 @@ from calendar import monthrange from dateutil.relativedelta import relativedelta from dateutil.rrule import rrule, rruleset, DAILY, WEEKLY, MONTHLY, YEARLY, MO, TU, WE, TH, FR, SA, SU -import math - MONTHS = { 'january': 31, 'february': 28, @@ -140,8 +138,9 @@ class ProjectTaskRecurrence(models.Model): @api.constrains('repeat_unit', 'repeat_on_month', 'repeat_day', 'repeat_type', 'repeat_until') def _check_repeat_until_month(self): - if self.filtered(lambda r: r.repeat_type == 'until' and r.repeat_unit == 'month' and r.repeat_until and r.repeat_on_month == 'date' and int(r.repeat_day) > r.repeat_until.day): - raise ValidationError('The end date should be after the day of the month') + if self.filtered(lambda r: r.repeat_type == 'until' and r.repeat_unit == 'month' and r.repeat_until and r.repeat_on_month == 'date' + and int(r.repeat_day) > r.repeat_until.day and monthrange(r.repeat_until.year, r.repeat_until.month)[1] != r.repeat_until.day): + raise ValidationError('The end date should be after the day of the month or the last day of the month') @api.model def _get_recurring_fields(self): @@ -179,16 +178,16 @@ class ProjectTaskRecurrence(models.Model): rrule_kwargs['freq'] = MONTHLY if repeat_on_month == 'date': start = date_start - relativedelta(days=1) - if repeat_type == 'until' and repeat_until > date_start: - delta = relativedelta(repeat_until, date_start) - count = math.floor((delta.years * 12 + delta.months) / repeat_interval) - for i in range(count): + start = start.replace(day=min(repeat_day, monthrange(start.year, start.month)[1])) + if start < date_start: + # Ensure the next recurrence is in the future + start += relativedelta(months=repeat_interval) start = start.replace(day=min(repeat_day, monthrange(start.year, start.month)[1])) - if i == 0 and start < date_start: - # Ensure the next recurrence is in the future - start += relativedelta(months=repeat_interval) + can_generate_date = (lambda: start <= repeat_until) if repeat_type == 'until' else (lambda: len(dates) < count) + while can_generate_date(): dates.append(start) start += relativedelta(months=repeat_interval) + start = start.replace(day=min(repeat_day, monthrange(start.year, start.month)[1])) return dates elif repeat_unit == 'year': rrule_kwargs['freq'] = YEARLY diff --git a/addons/project/tests/test_project_recurrence.py b/addons/project/tests/test_project_recurrence.py index 1e25110e37..ec8ff287d0 100644 --- a/addons/project/tests/test_project_recurrence.py +++ b/addons/project/tests/test_project_recurrence.py @@ -455,6 +455,69 @@ class TestProjectrecurrence(SavepointCase): self.assertEqual(dates[0], datetime(2020, 7, 3)) self.assertEqual(dates[1], datetime(2021, 1, 3)) + # Should generate a date at the last day of the current month + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2022, 2, 26), + repeat_interval=1, + repeat_unit='month', + repeat_type='until', + repeat_until=date(2022, 2, 28), + repeat_on_month='date', + repeat_on_year=False, + weekdays=False, + repeat_day=31, + repeat_week=False, + repeat_month=False, + count=5) + + self.assertEqual(len(dates), 1) + self.assertEqual(dates[0], date(2022, 2, 28)) + + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2022, 11, 26), + repeat_interval=3, + repeat_unit='month', + repeat_type='until', + repeat_until=date(2024, 2, 29), + repeat_on_month='date', + repeat_on_year=False, + weekdays=False, + repeat_day=25, + repeat_week=False, + repeat_month=False, + count=5) + + self.assertEqual(len(dates), 5) + self.assertEqual(dates[0], date(2023, 2, 25)) + self.assertEqual(dates[1], date(2023, 5, 25)) + self.assertEqual(dates[2], date(2023, 8, 25)) + self.assertEqual(dates[3], date(2023, 11, 25)) + self.assertEqual(dates[4], date(2024, 2, 25)) + + # Use the exact same parameters than the previous test but with a repeat_day that is not passed yet + # So we generate an additional date in the current month + dates = self.env['project.task.recurrence']._get_next_recurring_dates( + date_start=date(2022, 11, 26), + repeat_interval=3, + repeat_unit='month', + repeat_type='until', + repeat_until=date(2024, 2, 29), + repeat_on_month='date', + repeat_on_year=False, + weekdays=False, + repeat_day=31, + repeat_week=False, + repeat_month=False, + count=5) + + self.assertEqual(len(dates), 6) + self.assertEqual(dates[0], date(2022, 11, 30)) + self.assertEqual(dates[1], date(2023, 2, 28)) + self.assertEqual(dates[2], date(2023, 5, 31)) + self.assertEqual(dates[3], date(2023, 8, 31)) + self.assertEqual(dates[4], date(2023, 11, 30)) + self.assertEqual(dates[5], date(2024, 2, 29)) + def test_recurrence_next_dates_year(self): dates = self.env['project.task.recurrence']._get_next_recurring_dates( date_start=date(2020, 12, 1), diff --git a/addons/stock/report/stock_traceability.py b/addons/stock/report/stock_traceability.py index 2a1e207e06..b306decf52 100644 --- a/addons/stock/report/stock_traceability.py +++ b/addons/stock/report/stock_traceability.py @@ -184,7 +184,7 @@ class MrpStockReport(models.TransientModel): lines = self._get_move_lines(move_line, line_id=line_id) for line in lines: unfoldable = False - if line.consume_line_ids or ( line.lot_id and self._get_move_lines(line) and model != "stock.production.lot"): + if line.consume_line_ids or (model != "stock.production.lot" and line.lot_id and self._get_move_lines(line)): unfoldable = True final_vals += self._make_dict_move(level, parent_id=line_id, move_line=line, unfoldable=unfoldable) return final_vals