Skip to content

Commit e41fe2f

Browse files
committed
self-checkout: provide ad-hoc endpoints
* define new ad-hoc search and checkout endpoints, to be able to have better control on contraints and input and output payloads * use delivery methods to store when the checkout is a self-checkout * closes CERNDocumentServer/cds-ils#927
1 parent 79a415b commit e41fe2f

File tree

14 files changed

+626
-142
lines changed

14 files changed

+626
-142
lines changed

invenio_app_ils/circulation/api.py

Lines changed: 123 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@
3030
get_all_expiring_or_overdue_loans_by_patron_pid,
3131
)
3232
from invenio_app_ils.errors import (
33+
DocumentOverbookedError,
3334
IlsException,
3435
InvalidLoanExtendError,
3536
InvalidParameterError,
37+
ItemCannotCirculateError,
38+
ItemHasActiveLoanError,
39+
ItemNotFoundError,
3640
MissingRequiredParameterError,
41+
MultipleItemsBarcodeFoundError,
3742
PatronHasLoanOnDocumentError,
3843
PatronHasLoanOnItemError,
3944
PatronHasRequestOnDocumentError,
@@ -119,7 +124,7 @@ def request_loan(
119124
patron_pid,
120125
transaction_location_pid,
121126
transaction_user_pid=None,
122-
**kwargs
127+
**kwargs,
123128
):
124129
"""Create a new loan and trigger the first transition to PENDING."""
125130
loan_cls = current_circulation.loan_record_cls
@@ -170,13 +175,54 @@ def patron_has_active_loan_on_item(patron_pid, item_pid):
170175
return search_result.hits.total.value > 0
171176

172177

178+
def _checkout_loan(
179+
item_pid,
180+
patron_pid,
181+
transaction_location_pid,
182+
trigger="checkout",
183+
transaction_user_pid=None,
184+
delivery=None,
185+
**kwargs,
186+
):
187+
"""Checkout a loan."""
188+
transaction_user_pid = transaction_user_pid or str(current_user.id)
189+
loan_cls = current_circulation.loan_record_cls
190+
# create a new loan
191+
record_uuid = uuid.uuid4()
192+
new_loan = dict(
193+
patron_pid=patron_pid,
194+
transaction_location_pid=transaction_location_pid,
195+
transaction_user_pid=transaction_user_pid,
196+
)
197+
198+
if delivery:
199+
new_loan["delivery"] = delivery
200+
# check if there is an existing request
201+
loan = patron_has_request_on_document(patron_pid, kwargs.get("document_pid"))
202+
if loan:
203+
loan = loan_cls.get_record_by_pid(loan.pid)
204+
pid = IlsCirculationLoanIdProvider.get(loan["pid"]).pid
205+
loan.update(new_loan)
206+
else:
207+
pid = ils_circulation_loan_pid_minter(record_uuid, data=new_loan)
208+
loan = loan_cls.create(data=new_loan, id_=record_uuid)
209+
210+
params = deepcopy(loan)
211+
params.update(item_pid=item_pid, **kwargs)
212+
213+
loan = current_circulation.circulation.trigger(
214+
loan, **dict(params, trigger=trigger)
215+
)
216+
return pid, loan
217+
218+
173219
def checkout_loan(
174220
item_pid,
175221
patron_pid,
176222
transaction_location_pid,
177223
transaction_user_pid=None,
178224
force=False,
179-
**kwargs
225+
**kwargs,
180226
):
181227
"""Create a new loan and trigger the first transition to ITEM_ON_LOAN.
182228
@@ -191,7 +237,7 @@ def checkout_loan(
191237
the checkout. If False, the checkout will fail when the item cannot
192238
circulate.
193239
"""
194-
loan_cls = current_circulation.loan_record_cls
240+
195241
if patron_has_active_loan_on_item(patron_pid=patron_pid, item_pid=item_pid):
196242
raise PatronHasLoanOnItemError(patron_pid, item_pid)
197243
optional_delivery = kwargs.get("delivery")
@@ -201,35 +247,86 @@ def checkout_loan(
201247
if force:
202248
_set_item_to_can_circulate(item_pid)
203249

204-
transaction_user_pid = transaction_user_pid or str(current_user.id)
205-
206-
# create a new loan
207-
record_uuid = uuid.uuid4()
208-
new_loan = dict(
209-
patron_pid=patron_pid,
210-
transaction_location_pid=transaction_location_pid,
250+
return _checkout_loan(
251+
item_pid,
252+
patron_pid,
253+
transaction_location_pid,
211254
transaction_user_pid=transaction_user_pid,
255+
**kwargs,
212256
)
213257

214-
# check if there is an existing request
215-
loan = patron_has_request_on_document(patron_pid, kwargs.get("document_pid"))
216-
if loan:
217-
loan = loan_cls.get_record_by_pid(loan.pid)
218-
pid = IlsCirculationLoanIdProvider.get(loan["pid"]).pid
219-
loan.update(new_loan)
220-
else:
221-
pid = ils_circulation_loan_pid_minter(record_uuid, data=new_loan)
222-
loan = loan_cls.create(data=new_loan, id_=record_uuid)
223258

224-
params = deepcopy(loan)
225-
params.update(item_pid=item_pid, **kwargs)
259+
def _ensure_item_loanable_via_self_checkout(item_pid):
260+
"""Self-checkout: return loanable item or raise when not loanable.
226261
227-
# trigger the transition to request
228-
loan = current_circulation.circulation.trigger(
229-
loan, **dict(params, trigger="checkout")
262+
Implements the self-checkout rules to loan an item.
263+
"""
264+
item = current_app_ils.item_record_cls.get_record_by_pid(item_pid)
265+
item_dict = item.replace_refs()
266+
267+
if item_dict["status"] != "CAN_CIRCULATE":
268+
raise ItemCannotCirculateError()
269+
270+
circulation_state = item_dict["circulation"].get("state")
271+
has_active_loan = (
272+
circulation_state and circulation_state in CIRCULATION_STATES_LOAN_ACTIVE
230273
)
274+
if has_active_loan:
275+
raise ItemHasActiveLoanError(loan_pid=item_dict["circulation"]["loan_pid"])
231276

232-
return pid, loan
277+
document = current_app_ils.document_record_cls.get_record_by_pid(
278+
item_dict["document_pid"]
279+
)
280+
document_dict = document.replace_refs()
281+
if document_dict["circulation"].get("overbooked", False):
282+
raise DocumentOverbookedError(
283+
f"Cannot self-checkout the overbooked document {item_dict['document_pid']}"
284+
)
285+
286+
return item
287+
288+
289+
def self_checkout_get_item_by_barcode(barcode):
290+
"""Search for an item by barcode.
291+
292+
:param barcode: the barcode of the item to search for
293+
:return item: the item that was found, or raise in case of errors
294+
"""
295+
item_search = current_app_ils.item_search_cls()
296+
items = item_search.search_by_barcode(barcode).execute()
297+
if items.hits.total.value == 0:
298+
raise ItemNotFoundError(barcode=barcode)
299+
if items.hits.total.value > 1:
300+
raise MultipleItemsBarcodeFoundError(barcode)
301+
302+
item_pid = items.hits[0].pid
303+
item = _ensure_item_loanable_via_self_checkout(item_pid)
304+
return item_pid, item
305+
306+
307+
def self_checkout(
308+
item_pid, patron_pid, transaction_location_pid, transaction_user_pid=None, **kwargs
309+
):
310+
"""Perform self-checkout.
311+
312+
:param item_pid: a dict containing `value` and `type` fields to
313+
uniquely identify the item.
314+
:param patron_pid: the PID value of the patron
315+
:param transaction_location_pid: the PID value of the location where the
316+
checkout is performed
317+
:param transaction_user_pid: the PID value of the user that performed the
318+
checkout
319+
"""
320+
_ensure_item_loanable_via_self_checkout(item_pid["value"])
321+
return _checkout_loan(
322+
item_pid,
323+
patron_pid,
324+
transaction_location_pid,
325+
transaction_user_pid=transaction_user_pid,
326+
trigger="self_checkout",
327+
delivery=dict(method="SELF-CHECKOUT"),
328+
**kwargs,
329+
)
233330

234331

235332
def bulk_extend_loans(patron_pid, **kwargs):
@@ -253,7 +350,7 @@ def bulk_extend_loans(patron_pid, **kwargs):
253350
params,
254351
trigger="extend",
255352
transition_kwargs=dict(send_notification=False),
256-
)
353+
),
257354
)
258355
extended_loans.append(extended_loan)
259356
except (CirculationException, InvalidLoanExtendError):

invenio_app_ils/circulation/config.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
PatronOwnerPermission,
4545
authenticated_user_permission,
4646
backoffice_permission,
47-
loan_checkout_permission,
4847
loan_extend_circulation_permission,
4948
patron_owner_permission,
5049
superuser_permission,
@@ -84,6 +83,7 @@
8483
ILS_CIRCULATION_DELIVERY_METHODS = {
8584
"PICKUP": "Pick it up at the library desk",
8685
"DELIVERY": "Have it delivered to my office",
86+
"SELF-CHECKOUT": "Self-checkout",
8787
}
8888

8989
# Notification message creator for loan notifications
@@ -162,7 +162,13 @@
162162
dest="ITEM_ON_LOAN",
163163
trigger="checkout",
164164
transition=ILSToItemOnLoan,
165-
permission_factory=loan_checkout_permission,
165+
permission_factory=backoffice_permission,
166+
),
167+
dict(
168+
dest="ITEM_ON_LOAN",
169+
trigger="self_checkout",
170+
transition=ILSToItemOnLoan,
171+
permission_factory=authenticated_user_permission,
166172
),
167173
],
168174
"PENDING": [
@@ -172,6 +178,12 @@
172178
transition=ILSToItemOnLoan,
173179
permission_factory=backoffice_permission,
174180
),
181+
dict(
182+
dest="ITEM_ON_LOAN",
183+
trigger="self_checkout",
184+
transition=ILSToItemOnLoan,
185+
permission_factory=authenticated_user_permission,
186+
),
175187
dict(
176188
dest="CANCELLED",
177189
trigger="cancel",

invenio_app_ils/circulation/loaders/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
from .schemas.json.bulk_extend import BulkExtendLoansSchemaV1
1313
from .schemas.json.loan_checkout import LoanCheckoutSchemaV1
1414
from .schemas.json.loan_request import LoanRequestSchemaV1
15+
from .schemas.json.loan_self_checkout import LoanSelfCheckoutSchemaV1
1516
from .schemas.json.loan_update_dates import LoanUpdateDatesSchemaV1
1617

1718
loan_request_loader = ils_marshmallow_loader(LoanRequestSchemaV1)
1819
loan_checkout_loader = ils_marshmallow_loader(LoanCheckoutSchemaV1)
20+
loan_self_checkout_loader = ils_marshmallow_loader(LoanSelfCheckoutSchemaV1)
1921
loan_update_dates_loader = ils_marshmallow_loader(LoanUpdateDatesSchemaV1)
2022
loans_bulk_update_loader = ils_marshmallow_loader(BulkExtendLoansSchemaV1)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) 2019 CERN.
4+
#
5+
# invenio-app-ils is free software; you can redistribute it and/or modify
6+
# it under the terms of the MIT License; see LICENSE file for more details.
7+
8+
"""Invenio App ILS circulation Loan Checkout loader JSON schema."""
9+
10+
from invenio_circulation.records.loaders.schemas.json import LoanItemPIDSchemaV1
11+
from marshmallow import fields
12+
13+
from .base import LoanBaseSchemaV1
14+
15+
16+
class LoanSelfCheckoutSchemaV1(LoanBaseSchemaV1):
17+
"""Loan self-checkout schema."""
18+
19+
item_pid = fields.Nested(LoanItemPIDSchemaV1, required=True)

invenio_app_ils/circulation/notifications/messages.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class NotificationLoanMsg(NotificationMsg):
2525
request="request.html",
2626
request_no_items="request_no_items.html",
2727
checkout="checkout.html",
28+
self_checkout="self_checkout.html",
2829
checkin="checkin.html",
2930
extend="extend.html",
3031
cancel="cancel.html",

invenio_app_ils/circulation/serializers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# under the terms of the MIT License; see LICENSE file for more details.
77

88
"""Loan serializers."""
9+
910
from invenio_records_rest.serializers.response import search_responsify
1011

1112
from invenio_app_ils.records.schemas.json import ILSRecordSchemaJSONV1

invenio_app_ils/circulation/serializers/response.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# under the terms of the MIT License; see LICENSE file for more details.
77

88
"""Response serializers for circulation module."""
9+
910
import json
1011

1112
from flask import current_app
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{% block title %}
2+
InvenioILS: loan started for "{{ document.title|safe }}"
3+
{% endblock %}
4+
5+
{% block body_plain %}
6+
Dear {{ patron.name }},
7+
8+
your self-checkout loan for "{{ document.full_title }}" <{{ spa_routes.HOST }}{{ spa_routes.PATHS['literature']|format(pid=document.pid) }}> has started.
9+
10+
The due date is {{ loan.end_date }}.
11+
{% endblock %}
12+
13+
{% block body_html %}
14+
Dear {{ patron.name }}, <br/><br/>
15+
16+
your self-checkout loan for <a href="{{ spa_routes.HOST }}{{ spa_routes.PATHS['literature']|format(pid=document.pid) }}">"{{ document.full_title }}"</a> has <b>started</b>. <br/><br/>
17+
18+
<b>The due date is {{ loan.end_date }}</b>.<br/>
19+
{% endblock %}

0 commit comments

Comments
 (0)