Skip to content

Commit

Permalink
circulation: fix checkout end date
Browse files Browse the repository at this point in the history
* Fixes the checkout end date calculation between the winter and summer
  time transition.
* Closes #2748.

Co-Authored-by: Johnny Mariéthoz <[email protected]>
Co-Authored-by: Bertrand Zuchuat <[email protected]>
  • Loading branch information
jma and Garfield-fr committed Mar 15, 2022
1 parent 944650a commit b734790
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 14 deletions.
25 changes: 14 additions & 11 deletions rero_ils/modules/loans/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,17 @@ def get_circ_policy(loan, checkout_location=False):

def get_default_loan_duration(loan, initial_loan):
"""Return calculated checkout duration in number of days."""
# TODO: case when 'now' is not sysdate.
now = datetime.utcnow()
now_in_utc = datetime.now(timezone.utc)

# Get library (to check opening hours and get timezone)
library = Library.get_record_by_pid(loan.library_pid)

# Process difference between now and end of day in term of hours/minutes
# - use hours and minutes from now
# - check regarding end of day (eod), 23:59
# - correct the hours/date regarding library timezone
eod = timedelta(hours=23, minutes=59, seconds=00, milliseconds=000)
aware_eod = eod - library.get_timezone().utcoffset(now, is_dst=True)
time_to_eod = aware_eod - timedelta(hours=now.hour, minutes=now.minute)
now_in_library_timezone = now_in_utc.astimezone(tz=library.get_timezone())

# Due date should be defined differently from checkout_duration
# For that we use:
# - expected due date (now + checkout_duration)
# - we apply a -1 day correction because the next open day is not today
# - next library open date (the eve of expected due date is used)
# We finally make the difference between next library open date and now.
# We apply a correction for hour/minute to be 23:59 (end of day).
Expand All @@ -84,11 +78,20 @@ def get_default_loan_duration(loan, initial_loan):
# method. This was not the place for this ; this function should
# only return the loan duration.
policy = get_circ_policy(loan)
due_date_eve = now \
due_date_eve = now_in_library_timezone \
+ timedelta(days=policy.get('checkout_duration', 0)) \
- timedelta(days=1)
next_open_date = library.next_open(date=due_date_eve)
return timedelta(days=(next_open_date - now).days) + time_to_eod
# all libraries are closed at 23h59
# the next_open returns UTC.
end_date_in_library_timezone = next_open_date.astimezone(
library.get_timezone()).replace(
hour=23,
minute=59,
second=0,
microsecond=0
)
return end_date_in_library_timezone - now_in_library_timezone


def get_extension_params(loan=None, initial_loan=None, parameter_name=None):
Expand Down
65 changes: 62 additions & 3 deletions tests/ui/loans/test_loans_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ def test_loans_create(loan_pending_martigny):

def test_item_loans_default_duration(
item_lib_martigny, librarian_martigny, patron_martigny,
loc_public_martigny, circulation_policies):
loc_public_martigny, circulation_policies, lib_martigny):
"""Test default loan duration."""

# create a loan with request is easy
item, actions = item_lib_martigny.request(
pickup_location_pid=loc_public_martigny.pid,
patron_pid=patron_martigny.pid,
Expand All @@ -71,10 +72,68 @@ def test_item_loans_default_duration(
)
loan_pid = actions['request']['pid']
loan = Loan.get_record_by_pid(loan_pid)
# a new loan without transaction location
new_loan = deepcopy(loan)
del new_loan['transaction_location_pid']
assert get_default_loan_duration(new_loan, None) == \
get_default_loan_duration(loan, None)
# should have the same duration
with freeze_time():
assert get_default_loan_duration(new_loan, None) == \
get_default_loan_duration(loan, None)

policy = get_circ_policy(loan)
# the checkout duration should be enougth long
assert policy.get('checkout_duration', 0) > 3
# now in UTC
for now_str in [
# winter time
'2021-12-28 06:00:00', '2022-12-28 20:00:00',
# winter to summer time
'2022-03-22 06:00:00', '2022-03-22 20:00:00',
# summer time
'2022-06-28 05:00:00', '2022-06-28 19:00:00',
# summer to winter time
'2022-10-25 05:00:00', '2022-10-25 19:00:00'
]:
with freeze_time(now_str, tz_offset=0):
# get loan duration
duration = get_default_loan_duration(loan, None)
# now in datetime object
now = datetime.now(timezone.utc)
utc_end_date = now + duration
# computed end date at the library timezone
end_date = utc_end_date.astimezone(
tz=lib_martigny.get_timezone())
expected_utc_end_date = now + timedelta(
days=policy['checkout_duration'])
# expected end date at the library timezone
expected_end_date = expected_utc_end_date.astimezone(
lib_martigny.get_timezone())
assert end_date.strftime('%Y-%m-%d') == \
expected_end_date.strftime('%Y-%m-%d')
assert end_date.hour == 23
assert end_date.minute == 59

# test library closed days
now_str = '2022-02-04 14:00:00'
with freeze_time(now_str, tz_offset=0):
# get loan duration
duration = get_default_loan_duration(loan, None)
# now in datetime object
now = datetime.now(timezone.utc)

utc_end_date = now + duration
# computed end date at the library timezone
end_date = utc_end_date.astimezone(tz=lib_martigny.get_timezone())
# saturday and sunday is closed (+2)
expected_utc_end_date = now + timedelta(
days=(policy['checkout_duration'] + 2))
# expected end date at the library timezone
expected_end_date = expected_utc_end_date.astimezone(
lib_martigny.get_timezone())
assert end_date.strftime('%Y-%m-%d') == \
expected_end_date.strftime('%Y-%m-%d')
assert end_date.hour == 23
assert end_date.minute == 59
item_lib_martigny.cancel_item_request(
pid=loan.pid,
transaction_location_pid=loc_public_martigny.pid,
Expand Down

0 comments on commit b734790

Please sign in to comment.