From b73479048b3221d4d08fb58c803133ed54eebbf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Mari=C3=A9thoz?= Date: Mon, 14 Mar 2022 14:34:43 +0100 Subject: [PATCH] circulation: fix checkout end date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixes the checkout end date calculation between the winter and summer time transition. * Closes #2748. Co-Authored-by: Johnny MariƩthoz Co-Authored-by: Bertrand Zuchuat --- rero_ils/modules/loans/utils.py | 25 ++++++------ tests/ui/loans/test_loans_api.py | 65 ++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/rero_ils/modules/loans/utils.py b/rero_ils/modules/loans/utils.py index e709f560f4..a274d0dc87 100644 --- a/rero_ils/modules/loans/utils.py +++ b/rero_ils/modules/loans/utils.py @@ -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). @@ -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): diff --git a/tests/ui/loans/test_loans_api.py b/tests/ui/loans/test_loans_api.py index b416672918..2d2be23d28 100644 --- a/tests/ui/loans/test_loans_api.py +++ b/tests/ui/loans/test_loans_api.py @@ -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, @@ -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,