Skip to content

Commit d70a397

Browse files
committed
feat: auto-remove temporary item type on scan
Adds a new field `remove_temporary_item_type_on_scan` to item types that automatically removes the temporary item type from items when scanned during circulation operations (checkout, checkin, validate_request, receive, return_missing). When enabled, the temporary item type is removed BEFORE the circulation action, ensuring that the main item type and its associated circulation policy are used. The removal info is included in all API JSON responses for consistency. Changes: - Add `remove_temporary_item_type_on_scan` field to item type schema and mapping - Add comprehensive tests covering success and error scenarios - Add info to API responses when temporary item type is removed Closes https://tree.taiga.io/project/rero21-reroils/us/2905. Co-Authored-by: Pascal Repond <[email protected]>
1 parent 22f9ee0 commit d70a397

File tree

6 files changed

+447
-19
lines changed

6 files changed

+447
-19
lines changed

rero_ils/modules/item_types/jsonschemas/item_types/item_type-v0.0.1.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"type",
1111
"negative_availability",
1212
"displayed_status",
13-
"circulation_information"
13+
"circulation_information",
14+
"remove_temporary_item_type_on_scan"
1415
],
1516
"required": [
1617
"$schema",
@@ -236,6 +237,12 @@
236237
}
237238
}
238239
},
240+
"remove_temporary_item_type_on_scan": {
241+
"title": "Remove temporary item type on item scan",
242+
"description": "If enabled, scanning an item with this temporary item type will remove the temporary item type from the item.",
243+
"type": "boolean",
244+
"default": false
245+
},
239246
"organisation": {
240247
"title": "Organisation",
241248
"type": "object",

rero_ils/modules/item_types/mappings/v7/item_types/item_type-v0.0.1.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
}
4646
}
4747
},
48+
"remove_temporary_item_type_on_scan": {
49+
"type": "boolean"
50+
},
4851
"organisation": {
4952
"properties": {
5053
"type": {

rero_ils/modules/items/api/record.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22
#
33
# RERO ILS
4-
# Copyright (C) 2019-2022 RERO
4+
# Copyright (C) 2019-2026 RERO
55
#
66
# This program is free software: you can redistribute it and/or modify
77
# it under the terms of the GNU Affero General Public License as published by

rero_ils/modules/items/views/api_views.py

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from rero_ils.modules.decorators import check_authentication, check_permission
3939
from rero_ils.modules.documents.views import record_library_pickup_locations
4040
from rero_ils.modules.errors import NoCirculationAction, NoCirculationActionIsPermitted
41+
from rero_ils.modules.item_types.api import ItemType
4142
from rero_ils.modules.libraries.api import Library
4243
from rero_ils.modules.loans.api import Loan
4344
from rero_ils.modules.loans.dumpers import CirculationDumper as LoanCirculationDumper
@@ -46,6 +47,7 @@
4647
search_action as op_log_search_action,
4748
)
4849
from rero_ils.modules.patrons.api import Patron, current_librarian
50+
from rero_ils.modules.utils import extracted_data_from_ref
4951
from rero_ils.permissions import request_item_permission
5052

5153
from ...commons.exceptions import MissingDataException
@@ -72,6 +74,17 @@ def decorated_view(*args, **kwargs):
7274
return decorated_view
7375

7476

77+
def should_remove_on_scan(func):
78+
"""Mark a circulation action as requiring temporary item type removal on scan.
79+
80+
When applied to a circulation action function, this decorator indicates that
81+
the temporary_item_type should be removed before the action if the item type
82+
has remove_temporary_item_type_on_scan enabled.
83+
"""
84+
func._should_remove_on_scan = True
85+
return func
86+
87+
7588
def check_authentication_for_request(func):
7689
"""Decorator to check authentication for item requests HTTP API."""
7790

@@ -131,48 +144,119 @@ def do_item_jsonify_action(func):
131144
132145
This method for the circulation actions that required access to the item
133146
object before executing the invenio-circulation logic.
147+
148+
Important: When remove_temporary_item_type_on_scan is enabled on an item type,
149+
the temporary_item_type is removed BEFORE the circulation action for functions
150+
decorated with @should_remove_on_scan. This means:
151+
- The circulation policy will use the main item type (not temporary)
152+
- The temporary_item_type will be removed even if the action fails
153+
- The removal info is returned in JSON responses for:
154+
* Success responses
155+
* NoCirculationAction errors
156+
* NoCirculationActionIsPermitted errors
157+
* CirculationException errors
158+
* MissingRequiredParameterError errors
159+
* Generic Exception errors (catch-all)
160+
- The removal info is NOT returned for:
161+
* NotFound errors (item not found before removal is attempted)
162+
* RequestError errors (Elasticsearch errors before removal is attempted)
134163
"""
135164

165+
def _add_removed_item_type_info(error_response, removed_temp_item_type_name):
166+
"""Add removed temporary item type info to response if applicable."""
167+
if removed_temp_item_type_name:
168+
error_response["removed_temporary_item_type"] = {"name": removed_temp_item_type_name}
169+
return error_response
170+
171+
def _reindex_item_if_needed(item, was_temp_item_type_removed):
172+
"""Reindex item if temporary item type was removed but circulation failed.
173+
174+
The item is already committed before the circulation action.
175+
On success, status_update() handles reindexing.
176+
On error, we must reindex explicitly here.
177+
"""
178+
if was_temp_item_type_removed and item:
179+
item.reindex()
180+
136181
@wraps(func)
137182
def decorated_view(*args, **kwargs):
183+
item = None
184+
removed_temp_item_type_name = None
138185
try:
139186
data = deepcopy(flask_request.get_json())
140187
item = Item.get_item_record_for_ui(**data)
141188
data.pop("item_barcode", None)
142189

143190
if not item:
144191
abort(404)
192+
193+
# Check if temporary item type should be removed for circulation actions
194+
# We do not remove it for requests or other non-scan actions
195+
if getattr(func, "_should_remove_on_scan", False) and (tmp_itty := item.get("temporary_item_type")):
196+
tmp_itty_pid = extracted_data_from_ref(tmp_itty.get("$ref"))
197+
tmp_itty_record = ItemType.get_record_by_pid(tmp_itty_pid)
198+
if tmp_itty_record and tmp_itty_record.get("remove_temporary_item_type_on_scan"):
199+
removed_temp_item_type_name = tmp_itty_record.get("name")
200+
# Remove temporary item type and commit before circulation action.
201+
# The commit is required because get_circ_policy() reloads the item from DB.
202+
# Reindex is not needed here as status_update() will do it after the action.
203+
item.pop("temporary_item_type", None)
204+
item.update(item, dbcommit=True, reindex=False)
205+
206+
# Execute circulation action
145207
item_data, action_applied = func(item, data, *args, **kwargs)
146208
for action, loan in action_applied.items():
147209
if loan:
148210
action_applied[action] = loan.dumps(LoanCirculationDumper())
149211

150-
return jsonify(
151-
{
152-
"metadata": item_data.dumps(CirculationActionDumper()),
153-
"action_applied": action_applied,
154-
}
212+
response = {
213+
"metadata": item_data.dumps(CirculationActionDumper()),
214+
"action_applied": action_applied,
215+
}
216+
if removed_temp_item_type_name:
217+
response["removed_temporary_item_type"] = {"name": removed_temp_item_type_name}
218+
219+
return jsonify(response)
220+
except (NoCirculationAction, NoCirculationActionIsPermitted) as error:
221+
_reindex_item_if_needed(item, removed_temp_item_type_name)
222+
# Return error with info about removed temporary item type if applicable
223+
# NoCirculationActionIsPermitted: The circulation specs do not allow updates on some loan states.
224+
status_code = 403 if isinstance(error, NoCirculationActionIsPermitted) else 400
225+
error_response = _add_removed_item_type_info(
226+
{"status": status_code, "message": str(error)}, removed_temp_item_type_name
155227
)
156-
except NoCirculationAction as error:
157-
return jsonify({"status": f"error: {error!s}"}), 400
158-
except NoCirculationActionIsPermitted as error:
159-
# The circulation specs do not allow updates on some loan states.
160-
return jsonify({"status": f"error: {error!s}"}), 403
228+
return jsonify(error_response), status_code
161229
except MissingRequiredParameterError as error:
230+
_reindex_item_if_needed(item, removed_temp_item_type_name)
162231
# Return error 400 when there is a missing required parameter
163-
abort(400, str(error))
232+
# Return JSON response to allow including additional information like removed_temporary_item_type
233+
error_response = _add_removed_item_type_info(
234+
{"status": 400, "message": str(error)}, removed_temp_item_type_name
235+
)
236+
return jsonify(error_response), 400
164237
except CirculationException as error:
165-
abort(403, error.description or str(error))
238+
_reindex_item_if_needed(item, removed_temp_item_type_name)
239+
# Circulation exceptions (e.g., patron blocked, checkout limit reached)
240+
# Return JSON response with status code and message to allow including
241+
# additional information like removed_temporary_item_type
242+
error_response = _add_removed_item_type_info(
243+
{"status": 403, "message": error.description or str(error)}, removed_temp_item_type_name
244+
)
245+
return jsonify(error_response), 403
166246
except NotFound as error:
167247
raise error
168248
except exceptions.RequestError as error:
169249
# missing required parameters
170-
return jsonify({"status": f"error: {error}"}), 400
250+
return jsonify({"status": 400, "message": str(error)}), 400
171251
except Exception as error:
252+
_reindex_item_if_needed(item, removed_temp_item_type_name)
172253
# TODO: need to know what type of exception and document there.
173254
# raise error
174255
current_app.logger.error(f"{func.__name__}: {error!s}")
175-
return jsonify({"status": f"error: {error}"}), 400
256+
error_response = _add_removed_item_type_info(
257+
{"status": 400, "message": str(error)}, removed_temp_item_type_name
258+
)
259+
return jsonify(error_response), 400
176260

177261
return decorated_view
178262

@@ -245,6 +329,7 @@ def cancel_item_request(item, data):
245329
# @profile(sort_by='cumulative', lines_to_print=100)
246330
@check_authentication
247331
@do_item_jsonify_action
332+
@should_remove_on_scan
248333
def checkout(item, data):
249334
"""HTTP POST request for Item checkout action.
250335
@@ -267,6 +352,7 @@ def checkout(item, data):
267352
# @profile(sort_by='cumulative', lines_to_print=100)
268353
@check_authentication
269354
@do_item_jsonify_action
355+
@should_remove_on_scan
270356
def checkin(item, data):
271357
"""HTTP GET request for item return action.
272358
@@ -304,6 +390,7 @@ def update_loan_pickup_location(loan, data):
304390
@api_blueprint.route("/validate_request", methods=["POST"])
305391
@check_authentication
306392
@do_item_jsonify_action
393+
@should_remove_on_scan
307394
def validate_request(item, data):
308395
"""HTTP GET request for Item request validation action.
309396
@@ -323,6 +410,7 @@ def validate_request(item, data):
323410
@api_blueprint.route("/receive", methods=["POST"])
324411
@check_authentication
325412
@do_item_jsonify_action
413+
@should_remove_on_scan
326414
def receive(item, data):
327415
"""HTTP POST request for receive item action.
328416
@@ -340,6 +428,7 @@ def receive(item, data):
340428
@api_blueprint.route("/return_missing", methods=["POST"])
341429
@check_authentication
342430
@do_item_jsonify_action
431+
@should_remove_on_scan
343432
def return_missing(item, data):
344433
"""HTTP POST request for Item return_missing action.
345434

tests/api/circulation/test_actions_views_checkin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ def test_auto_checkin_else(
121121
},
122122
)
123123
assert res.status_code == 400
124-
assert get_json(res)["status"] == _("error: No circulation action performed: on shelf")
124+
assert get_json(res)["status"] == 400
125+
assert get_json(res)["message"] == _("No circulation action performed: on shelf")
125126
query = OperationLogsSearch().filter("term", record__type="scan_item").filter("exists", field="scan")
126127
assert query.count() == 1
127128
log_data = query.execute()[0].to_dict()

0 commit comments

Comments
 (0)