diff --git a/invenio_circulation/config.py b/invenio_circulation/config.py index 33b9b12..15ab205 100644 --- a/invenio_circulation/config.py +++ b/invenio_circulation/config.py @@ -15,11 +15,13 @@ from .search import LoansSearch from .transitions.transitions import CreatedToItemOnLoan, CreatedToPending, \ ItemAtDeskToItemOnLoan, ItemInTransitHouseToItemReturned, \ - ItemOnLoanToItemInTransitHouse, ItemOnLoanToItemReturned, \ - PendingToItemAtDesk, PendingToItemInTransitPickup -from .utils import get_default_loan_duration, is_item_available, \ - is_loan_duration_valid, item_exists, item_location_retriever, \ - patron_exists + ItemOnLoanToItemInTransitHouse, ItemOnLoanToItemOnLoan, \ + ItemOnLoanToItemReturned, PendingToItemAtDesk, \ + PendingToItemInTransitPickup +from .utils import get_default_extension_duration, \ + get_default_extension_max_count, get_default_loan_duration, \ + is_item_available, is_loan_duration_valid, item_exists, \ + item_location_retriever, patron_exists _CIRCULATION_LOAN_PID_TYPE = 'loan_pid' """.""" @@ -72,6 +74,8 @@ dict(dest='ITEM_RETURNED', transition=ItemOnLoanToItemReturned), dict(dest='ITEM_IN_TRANSIT_TO_HOUSE', transition=ItemOnLoanToItemInTransitHouse), + dict(dest='ITEM_ON_LOAN', transition=ItemOnLoanToItemOnLoan, + trigger='extend'), dict(dest='CANCELLED', trigger='cancel') ], 'ITEM_IN_TRANSIT_TO_HOUSE': [ @@ -102,6 +106,11 @@ duration_validate=is_loan_duration_valid, item_available=is_item_available ), + extension=dict( + from_end_date=True, + duration_default=get_default_extension_duration, + max_count=get_default_extension_max_count + ), ) """.""" diff --git a/invenio_circulation/transitions/transitions.py b/invenio_circulation/transitions/transitions.py index 32c03c0..fdccf5b 100644 --- a/invenio_circulation/transitions/transitions.py +++ b/invenio_circulation/transitions/transitions.py @@ -8,7 +8,7 @@ """Invenio Circulation custom transitions.""" -from datetime import timedelta +from datetime import datetime, timedelta from flask import current_app from invenio_db import db @@ -59,6 +59,34 @@ def _update_document_pending_request_for_item(item_pid): # TODO: index loan again? +def _ensure_valid_extension(loan): + """Validate end dates for a extended loan.""" + get_extension_max_count = current_app.config['CIRCULATION_POLICIES'][ + 'extension']['max_count'] + extension_max_count = get_extension_max_count(loan) + + extension_count = loan.get('extension_count', 0) + extension_count += 1 + if extension_count > extension_max_count + 1: + msg = 'Max extension count reached `{0}`'.format(extension_max_count) + raise TransitionConstraintsViolation(msg=msg) + + loan['extension_count'] = extension_count + + get_extension_duration = current_app.config['CIRCULATION_POLICIES'][ + 'extension']['duration_default'] + number_of_days = get_extension_duration(loan) + get_extension_from_end_date = current_app.config[ + 'CIRCULATION_POLICIES']['extension']['from_end_date'] + + end_date = parse_date(loan['end_date']) + if not get_extension_from_end_date: + end_date = loan.get('transaction_date') + + end_date += timedelta(days=number_of_days) + loan['end_date'] = end_date.isoformat() + + class CreatedToPending(Transition): """Action to request to loan an item.""" @@ -165,6 +193,20 @@ def after(self, loan): super(ItemAtDeskToItemOnLoan, self).after(loan) +class ItemOnLoanToItemOnLoan(Transition): + """Extend action to perform a item loan extension.""" + + def before(self, loan, **kwargs): + """Validate extension action.""" + super(ItemOnLoanToItemOnLoan, self).before(loan, **kwargs) + + _ensure_valid_extension(loan) + + def after(self, loan): + """.""" + super(ItemOnLoanToItemOnLoan, self).after(loan) + + class ItemOnLoanToItemInTransitHouse(Transition): """Check-in action when returning an item not to its belonging location.""" diff --git a/invenio_circulation/utils.py b/invenio_circulation/utils.py index c7182ba..1adae64 100644 --- a/invenio_circulation/utils.py +++ b/invenio_circulation/utils.py @@ -38,6 +38,16 @@ def get_default_loan_duration(loan): return 30 +def get_default_extension_duration(loan): + """Return a default extension duration in number of days.""" + return 30 + + +def get_default_extension_max_count(loan): + """Return a default extensions max count.""" + return float("inf") + + def is_loan_duration_valid(loan): """Validate the loan duration.""" return loan['end_date'] > loan['start_date'] and \ diff --git a/tests/test_loan_transitions.py b/tests/test_loan_transitions.py index 91ab3cd..14e3b4e 100644 --- a/tests/test_loan_transitions.py +++ b/tests/test_loan_transitions.py @@ -12,6 +12,7 @@ import mock import pytest +from flask import current_app from helpers import SwappedConfig, SwappedNestedConfig from invenio_circulation.api import Loan, is_item_available @@ -60,6 +61,61 @@ def test_loan_request(loan_created, db, params): assert loan['state'] == 'PENDING' +def test_loan_extend(loan_created, db, params, + mock_is_item_available): + """Test loan extend action.""" + + def get_max_count_1(loan): + return 1 + + loan = current_circulation.circulation.trigger( + loan_created, **dict(params, trigger='checkout') + ) + db.session.commit() + end_date = parse_date(loan['end_date']) + + loan = current_circulation.circulation.trigger( + loan, **dict(params, trigger='extend') + ) + db.session.commit() + new_end_date = parse_date(loan['end_date']) + assert new_end_date == end_date + timedelta(days=30) + assert loan['extension_count'] == 1 + loan = current_circulation.circulation.trigger( + loan, **dict(params, trigger='extend') + ) + db.session.commit() + + # test to manny extensions + current_app.config['CIRCULATION_POLICIES']['extension'][ + 'max_count'] = get_max_count_1 + with pytest.raises(TransitionConstraintsViolation): + loan = current_circulation.circulation.trigger( + loan, **dict(params, trigger='extend') + ) + + +def test_loan_extend_from_enddate(loan_created, db, params, + mock_is_item_available): + """Test loan extend action from transaction date.""" + + loan = current_circulation.circulation.trigger( + loan_created, **dict(params, trigger='checkout') + ) + db.session.commit() + extension_date = parse_date(loan.get('transaction_date')) + current_app.config['CIRCULATION_POLICIES']['extension'][ + 'from_end_date'] = False + + loan = current_circulation.circulation.trigger( + loan, **dict(params, trigger='extend') + ) + db.session.commit() + new_end_date = parse_date(loan['end_date']) + assert new_end_date == extension_date + timedelta(days=30) + assert loan['extension_count'] == 1 + + def test_cancel_action(loan_created, db, params, mock_is_item_available): """Test should pass when calling `cancel` from `ITEM_ON_LOAN`."""