From d45831337ef9a23baa4d2490f9115bd6617a98b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Alix?= Date: Thu, 21 Mar 2024 16:51:57 +0100 Subject: [PATCH] sale_delivery_date: add cache to improve performance This avoids a lot of SQL requests (several hundreds on a SO with 20-30 lines), and reduce the time needed to open orders. --- sale_delivery_date/models/__init__.py | 1 + sale_delivery_date/models/res_partner.py | 12 ++-- sale_delivery_date/models/resource.py | 55 ++++++++++++++++ sale_delivery_date/models/sale_order_line.py | 6 +- sale_delivery_date/tests/__init__.py | 1 + .../tests/test_sale_order_line_cache.py | 66 +++++++++++++++++++ 6 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 sale_delivery_date/models/resource.py create mode 100644 sale_delivery_date/tests/test_sale_order_line_cache.py diff --git a/sale_delivery_date/models/__init__.py b/sale_delivery_date/models/__init__.py index d3fdea4ab39..918c52e216c 100644 --- a/sale_delivery_date/models/__init__.py +++ b/sale_delivery_date/models/__init__.py @@ -5,3 +5,4 @@ from . import stock_move from . import stock_picking from . import stock_warehouse +from . import resource diff --git a/sale_delivery_date/models/res_partner.py b/sale_delivery_date/models/res_partner.py index 82c9a56d4cc..342673820d2 100644 --- a/sale_delivery_date/models/res_partner.py +++ b/sale_delivery_date/models/res_partner.py @@ -4,7 +4,7 @@ from pytz import timezone, utc -from odoo import _, fields, models +from odoo import _, fields, models, tools from odoo.exceptions import UserError from odoo.tools.date_utils import date_range @@ -91,6 +91,12 @@ def get_next_workdays_datetime(self, from_datetime, to_datetime): if date.weekday() < 5 ] + @tools.ormcache("weekday_number") + def _get_weekday(self, weekday_number): + return self.env["time.weekday"].search( + [("name", "=", weekday_number)], limit=1 + ) + def get_next_windows_start_datetime(self, from_datetime, to_datetime): """Get all delivery windows start time. @@ -117,9 +123,7 @@ def get_next_windows_start_datetime(self, from_datetime, to_datetime): from_datetime_tz, to_datetime_tz, timedelta(days=1) ): this_weekday_number = this_datetime.weekday() - this_weekday = self.env["time.weekday"].search( - [("name", "=", this_weekday_number)], limit=1 - ) + this_weekday = self._get_weekday(this_weekday_number) # Sort by start time to ensure the window we'll find will be the first # one for the weekday this_weekday_windows = self.delivery_time_window_ids.filtered( diff --git a/sale_delivery_date/models/resource.py b/sale_delivery_date/models/resource.py new file mode 100644 index 00000000000..7185200f12e --- /dev/null +++ b/sale_delivery_date/models/resource.py @@ -0,0 +1,55 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import api, models + + +class ResourceCalendar(models.Model): + _inherit = "resource.calendar" + + def write(self, vals): + res = super().write(vals) + # Clear cache to ensure 'ormcache' decorated methods on 'sale.order.line' + # returns the expected results if calendar is updated + self.clear_caches() + return res + + +class ResourceCalendarAttendance(models.Model): + _inherit = "resource.calendar.attendance" + + @api.model_create_multi + @api.returns('self', lambda value: value.id) + def create(self, vals_list): + res = super().create(vals_list) + # Clear cache to ensure 'ormcache' decorated methods on 'sale.order.line' + # returns the expected results if attendances are created + self.clear_caches() + return res + + def write(self, vals): + res = super().write(vals) + # Clear cache to ensure 'ormcache' decorated methods on 'sale.order.line' + # returns the expected results if attendances are updated + self.clear_caches() + return res + + +class ResourceCalendarLeaves(models.Model): + _inherit = "resource.calendar.leaves" + + @api.model_create_multi + @api.returns('self', lambda value: value.id) + def create(self, vals_list): + res = super().create(vals_list) + # Clear cache to ensure 'ormcache' decorated methods on 'sale.order.line' + # returns the expected results if leaves are created + self.clear_caches() + return res + + def write(self, vals): + res = super().write(vals) + # Clear cache to ensure 'ormcache' decorated methods on 'sale.order.line' + # returns the expected results if leaves are updated + self.clear_caches() + return res diff --git a/sale_delivery_date/models/sale_order_line.py b/sale_delivery_date/models/sale_order_line.py index 48d3db60554..fc07dcf9787 100644 --- a/sale_delivery_date/models/sale_order_line.py +++ b/sale_delivery_date/models/sale_order_line.py @@ -7,7 +7,7 @@ import pytz from pytz import UTC, timezone -from odoo import api, models +from odoo import api, models, tools from odoo.addons.partner_tz.tools import tz_utils @@ -368,6 +368,7 @@ def _preparation_date_from_expedition_date( # ====== @api.model + @tools.ormcache("date_from", "delay", "calendar") def _add_delay(self, date_from, delay, calendar=False): if calendar: # Plan days is expecting a number of days, not a delay. @@ -377,6 +378,7 @@ def _add_delay(self, date_from, delay, calendar=False): return date_from + timedelta(days=delay) @api.model + @tools.ormcache("date_from", "delay", "calendar") def _deduct_delay(self, date_from, delay, calendar=False): if calendar: days = self._delay_to_days(delay) @@ -399,6 +401,7 @@ def _apply_cutoff(self, date_order, cutoff, keep_same_day=False): return self._get_utc_cutoff_datetime(cutoff, date_order, keep_same_day) @api.model + @tools.ormcache("date_start", "calendar") def _postpone_to_working_day(self, date_start, calendar=False): """Returns the nearest calendar's working day""" if calendar: @@ -415,6 +418,7 @@ def _apply_customer_window(self, delivery_date, partner): return partner.next_delivery_window_start_datetime(from_date=delivery_date) @api.model + @tools.ormcache("earliest_work_end", "latest_expedition_date", "calendar") def _get_latest_work_end_from_date_range( self, earliest_work_end, latest_expedition_date, calendar=False ): diff --git a/sale_delivery_date/tests/__init__.py b/sale_delivery_date/tests/__init__.py index 71dd9c8705c..87ad4178362 100644 --- a/sale_delivery_date/tests/__init__.py +++ b/sale_delivery_date/tests/__init__.py @@ -7,3 +7,4 @@ from . import test_delivery_date_in_the_past from . import test_methods from . import test_backorder_date +from . import test_sale_order_line_cache diff --git a/sale_delivery_date/tests/test_sale_order_line_cache.py b/sale_delivery_date/tests/test_sale_order_line_cache.py new file mode 100644 index 00000000000..012beb49069 --- /dev/null +++ b/sale_delivery_date/tests/test_sale_order_line_cache.py @@ -0,0 +1,66 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from freezegun import freeze_time +import mock + +from odoo import fields +from odoo.tests.common import SavepointCase + +MONDAY = fields.Datetime.from_string("2024-03-18") +TUESDAY = fields.Datetime.from_string("2024-03-19") +WEDNESDAY = fields.Datetime.from_string("2024-03-20") + + +class TestSaleOrderLineCache(SavepointCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + @freeze_time(MONDAY) + def test_cache_invalidation(self): + calendar = self.env.ref("resource.resource_calendar_std") + sol_model = self.env["sale.order.line"] + mock_args = (type(calendar), "plan_days") + mock_kwargs = {"side_effect": calendar.plan_days} + # First call computes the date + with mock.patch.object(*mock_args, **mock_kwargs) as mocked: + date_ = sol_model._add_delay(MONDAY, delay=1, calendar=calendar) + self.assertEqual(date_.date(), TUESDAY) + mocked.assert_called() + # Second call get it from the cache + with mock.patch.object(*mock_args, **mock_kwargs) as mocked: + date_ = sol_model._add_delay(MONDAY, delay=1, calendar=calendar) + self.assertEqual(date_.date(), TUESDAY) + mocked.assert_not_called() + # Update the calendar data to invalidate the cache so the date is + # computed from the calendar again + with mock.patch.object(*mock_args, **mock_kwargs) as mocked: + calendar.write({}) + date_ = sol_model._add_delay(MONDAY, delay=1, calendar=calendar) + self.assertEqual(date_.date(), TUESDAY) + mocked.assert_called() + # Same by updating the attendances to invalidate the cache + with mock.patch.object(*mock_args, **mock_kwargs) as mocked: + calendar.attendance_ids.write({}) + date_ = sol_model._add_delay(MONDAY, delay=1, calendar=calendar) + self.assertEqual(date_.date(), TUESDAY) + mocked.assert_called() + # Same by updating the leaves to invalidate the cache + with mock.patch.object(*mock_args, **mock_kwargs) as mocked: + calendar.leave_ids.write({}) + date_ = sol_model._add_delay(MONDAY, delay=1, calendar=calendar) + self.assertEqual(date_.date(), TUESDAY) + mocked.assert_called() + # Using the cache again + with mock.patch.object(*mock_args, **mock_kwargs) as mocked: + date_ = sol_model._add_delay(MONDAY, delay=1, calendar=calendar) + self.assertEqual(date_.date(), TUESDAY) + mocked.assert_not_called() + # Not using the cache with different parameters + with mock.patch.object(*mock_args, **mock_kwargs) as mocked: + date_ = sol_model._add_delay(MONDAY, delay=2, calendar=calendar) + self.assertEqual(date_.date(), WEDNESDAY) + mocked.assert_called()