Skip to content

Commit

Permalink
sale_delivery_date: add cache to improve performance
Browse files Browse the repository at this point in the history
This avoids a lot of SQL requests (several hundreds on a SO with 20-30 lines),
and reduce the time needed to open orders.
  • Loading branch information
sebalix committed Mar 22, 2024
1 parent e81f274 commit fc0c9ab
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 5 deletions.
1 change: 1 addition & 0 deletions sale_delivery_date/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
from . import stock_move
from . import stock_picking
from . import stock_warehouse
from . import resource
12 changes: 8 additions & 4 deletions sale_delivery_date/models/res_partner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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(
Expand Down
55 changes: 55 additions & 0 deletions sale_delivery_date/models/resource.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion sale_delivery_date/models/sale_order_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -368,6 +368,7 @@ def _preparation_date_from_expedition_date(
# ======

@api.model
@tools.ormcache("date_from", "delay", "calendar.id")
def _add_delay(self, date_from, delay, calendar=False):
if calendar:
# Plan days is expecting a number of days, not a delay.
Expand All @@ -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.id")
def _deduct_delay(self, date_from, delay, calendar=False):
if calendar:
days = self._delay_to_days(delay)
Expand All @@ -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.id")
def _postpone_to_working_day(self, date_start, calendar=False):
"""Returns the nearest calendar's working day"""
if calendar:
Expand All @@ -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.id")
def _get_latest_work_end_from_date_range(
self, earliest_work_end, latest_expedition_date, calendar=False
):
Expand Down
1 change: 1 addition & 0 deletions sale_delivery_date/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
66 changes: 66 additions & 0 deletions sale_delivery_date/tests/test_sale_order_line_cache.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit fc0c9ab

Please sign in to comment.