From 6e4f68feb242c11f1ee9ba7ccf895e93321394ad Mon Sep 17 00:00:00 2001 From: Aly Badr Date: Wed, 17 Apr 2019 11:06:46 +0200 Subject: [PATCH] circulation: possibility to check-out in-transit items * FIX Fixes #230: allows checkout of in-transit items. Signed-off-by: Aly Badr --- rero_ils/modules/items/api.py | 42 +++++++--- rero_ils/modules/items/api_views.py | 36 ++++++++- tests/api/test_items_rest.py | 117 ++++++++++++++++++++++++++++ ui/src/app/circulation/items.ts | 6 +- 4 files changed, 186 insertions(+), 15 deletions(-) diff --git a/rero_ils/modules/items/api.py b/rero_ils/modules/items/api.py index dd3ffcc7b0..b083386e7b 100644 --- a/rero_ils/modules/items/api.py +++ b/rero_ils/modules/items/api.py @@ -125,11 +125,18 @@ def wrapper(item, *args, **kwargs): if not patron_pid: raise CirculationException( "Patron PID not specified") - data = { - 'item_pid': item.pid, - 'patron_pid': patron_pid - } - loan = Loan.create(data, dbcommit=True, reindex=True) + if function.__name__ == 'checkout': + request = get_request_by_item_pid_by_patron_pid( + item_pid=item.pid, patron_pid=patron_pid) + if request: + loan = Loan.get_record_by_pid(request.get('loan_pid')) + + if not loan: + data = { + 'item_pid': item.pid, + 'patron_pid': patron_pid + } + loan = Loan.create(data, dbcommit=True, reindex=True) else: raise CirculationException( "Loan PID not specified") @@ -378,7 +385,10 @@ def action_filter(self, action, loan): patron_type_pid, self.item_type_pid ) - action_validated = True + data = { + 'action_validated': True, + 'new_action': None + } if action == 'extend': extension_count = loan.get('extension_count', 0) if not ( @@ -390,11 +400,20 @@ def action_filter(self, action, loan): self.library_pid ) ): - action_validated = False + data['action_validated'] = False if action == 'checkout': if not circ_policy.get('allow_checkout'): - action_validated = False - return action_validated + data['action_validated'] = False + + if action == 'receive': + if ( + circ_policy.get('allow_checkout') and + loan.get('state') == 'ITEM_IN_TRANSIT_FOR_PICKUP' and + loan.get('patron_pid') == patron_pid + ): + data['action_validated'] = False + data['new_action'] = 'checkout' + return data @property def actions(self): @@ -405,8 +424,11 @@ def actions(self): if loan: for transition in transitions.get(loan.get('state')): action = transition.get('trigger') - if self.action_filter(action, loan): + data = self.action_filter(action, loan) + if data.get('action_validated'): actions.add(action) + if data.get('new_action'): + actions.add(data.get('new_action')) # default actions if not loan: for transition in transitions.get('CREATED'): diff --git a/rero_ils/modules/items/api_views.py b/rero_ils/modules/items/api_views.py index 1748e279e3..719bdd169e 100644 --- a/rero_ils/modules/items/api_views.py +++ b/rero_ils/modules/items/api_views.py @@ -122,6 +122,25 @@ def librarian_request(item, data): return item.request(**data) +def prior_checkout_actions(item, data): + """Actions executed prior to a checkout.""" + if data.get('loan_pid'): + loan = Loan.get_record_by_pid(data.get('loan_pid')) + if ( + loan.get('state') == 'ITEM_IN_TRANSIT_FOR_PICKUP' and + loan.get('patron_pid') == data.get('patron_pid') + ): + item.receive(**data) + if loan.get('state') == 'ITEM_IN_TRANSIT_TO_HOUSE': + item.cancel_loan(loan_pid=loan.get('loan_pid')) + del data['loan_pid'] + else: + loan = get_loan_for_item(item.pid) + if loan: + item.cancel_loan(loan_pid=loan.get('loan_pid')) + return data + + @api_blueprint.route('/checkout', methods=['POST']) @check_authentication @jsonify_action @@ -130,7 +149,8 @@ def checkout(item, data): required_parameters: patron_pid, item_pid """ - return item.checkout(**data) + new_data = prior_checkout_actions(item, data) + return item.checkout(**new_data) @api_blueprint.route("/checkin", methods=['POST']) @@ -286,7 +306,19 @@ def item(item_barcode): new_actions = [] for action in actions: if action == 'checkout' and circ_policy.get('allow_checkout'): - new_actions.append(action) + if item.number_of_requests() > 0: + patron_barcode = Patron.get_record_by_pid( + patron_pid).get('barcode') + if item.patron_request_rank(patron_barcode) == 1: + new_actions.append(action) + else: + new_actions.append(action) + if ( + action == 'receive' and + circ_policy.get('allow_checkout') and + item.number_of_requests() == 0 + ): + new_actions.append('checkout') item_dumps['actions'] = new_actions return jsonify({ 'metadata': { diff --git a/tests/api/test_items_rest.py b/tests/api/test_items_rest.py index bd2197cc81..0c964cd1a8 100644 --- a/tests/api/test_items_rest.py +++ b/tests/api/test_items_rest.py @@ -1366,3 +1366,120 @@ def test_items_extend_end_date(client, user_librarian_no_email, content_type='application/json', ) assert res.status_code == 200 + + +def test_items_in_transit(client, user_librarian_no_email, + user_patron_no_email, + location, item_type, store_location, + item_on_shelf, json_header, + circ_policy): + """.""" + login_user_via_session(client, user_librarian_no_email.user) + item = item_on_shelf + item_pid = item.pid + patron_pid = user_patron_no_email.pid + + # request to pick at another location + res = client.post( + url_for('api_item.librarian_request'), + data=json.dumps( + dict( + item_pid=item_pid, + pickup_location_pid=store_location.pid, + patron_pid=patron_pid + ) + ), + content_type='application/json', + ) + assert res.status_code == 200 + data = get_json(res) + item_data = data.get('metadata') + actions = data.get('action_applied') + assert item_data.get('status') == ItemStatus.ON_SHELF + assert actions.get(LoanAction.REQUEST) + loan_pid = actions[LoanAction.REQUEST].get('loan_pid') + item = Item.get_record_by_pid(item_pid) + + # validate (send) request + res = client.post( + url_for('api_item.validate_request'), + data=json.dumps( + dict( + item_pid=item_pid, + loan_pid=loan_pid + ) + ), + content_type='application/json', + ) + assert res.status_code == 200 + data = get_json(res) + item_data = data.get('metadata') + actions = data.get('action_applied') + assert item_data.get('status') == ItemStatus.IN_TRANSIT + assert actions.get(LoanAction.VALIDATE) + + # checkout action to req patron is possible without the receive action + res = client.get( + url_for( + 'api_item.item', + item_barcode=item.get('barcode'), + patron_pid=patron_pid + ) + ) + assert res.status_code == 200 + data = get_json(res) + actions = data.get('metadata').get('item').get('actions') + assert 'checkout' in actions + + # checkout + res = client.post( + url_for('api_item.checkout'), + data=json.dumps( + dict( + item_pid=item_pid, + patron_pid=patron_pid, + loan_pid=loan_pid + ) + ), + content_type='application/json', + ) + assert res.status_code == 200 + data = get_json(res) + assert Item.get_record_by_pid(item_pid).get('status') == ItemStatus.ON_LOAN + + # checkin at location other than item location + res = client.post( + url_for('api_item.checkin'), + data=json.dumps( + dict( + item_pid=item_pid, + loan_pid=loan_pid, + transaction_location_pid=store_location.pid + ) + ), + content_type='application/json', + ) + assert res.status_code == 200 + data = get_json(res) + item_data = data.get('metadata') + actions = data.get('action_applied') + assert item_data.get('status') == ItemStatus.IN_TRANSIT + assert actions.get(LoanAction.CHECKIN) + loan_pid = actions[LoanAction.CHECKIN].get('loan_pid') + loan = actions[LoanAction.CHECKIN] + assert loan.get('state') == 'ITEM_IN_TRANSIT_TO_HOUSE' + + # a new checkout + res = client.post( + url_for('api_item.checkout'), + data=json.dumps( + dict( + item_pid=item_pid, + patron_pid=patron_pid + ) + ), + content_type='application/json', + ) + assert res.status_code == 200 + data = get_json(res) + assert Item.get_record_by_pid(item_pid).get('status') == ItemStatus.ON_LOAN diff --git a/ui/src/app/circulation/items.ts b/ui/src/app/circulation/items.ts index 3ce2035d61..fb6bb6f799 100644 --- a/ui/src/app/circulation/items.ts +++ b/ui/src/app/circulation/items.ts @@ -133,17 +133,17 @@ export class Item { } return ItemAction.no; } - + // should rely on backend and delete this function public canLoan(patron) { if (!this.available) { - if (this.status === ItemStatus.AT_DESK + if ((this.status === ItemStatus.AT_DESK || this.status === ItemStatus.IN_TRANSIT) && patron && this.pending_loans && this.pending_loans.length && this.pending_loans[0].patron_pid === patron.pid) { return true; } - return false; + return true; } // available return true;