3030 get_all_expiring_or_overdue_loans_by_patron_pid ,
3131)
3232from 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+
173219def 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
235332def 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 ):
0 commit comments