3838from rero_ils .modules .decorators import check_authentication , check_permission
3939from rero_ils .modules .documents .views import record_library_pickup_locations
4040from rero_ils .modules .errors import NoCirculationAction , NoCirculationActionIsPermitted
41+ from rero_ils .modules .item_types .api import ItemType
4142from rero_ils .modules .libraries .api import Library
4243from rero_ils .modules .loans .api import Loan
4344from rero_ils .modules .loans .dumpers import CirculationDumper as LoanCirculationDumper
4647 search_action as op_log_search_action ,
4748)
4849from rero_ils .modules .patrons .api import Patron , current_librarian
50+ from rero_ils .modules .utils import extracted_data_from_ref
4951from rero_ils .permissions import request_item_permission
5052
5153from ...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+
7588def 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
248333def 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
270356def 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
307394def 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
326414def 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
343432def return_missing (item , data ):
344433 """HTTP POST request for Item return_missing action.
345434
0 commit comments