def test_loan_replace_item_wo_params( app, json_headers, params, indexed_loans ): """Test that no Loan is returned for the given Item if only pendings.""" item_pid = "item_on_loan_2" loan = get_loan_for_item(item_pid) payload = {} res, data = _post(app, json_headers, loan, payload) assert res.status_code == 400
def test_loan_replace_item(app, json_headers, params, indexed_loans): """Test that no Loan is returned for the given Item if only pendings.""" item_pid = "item_on_loan_2" loan = get_loan_for_item(item_pid) payload = {"item_pid": "new_item_pid"} res, data = _post(app, json_headers, loan, payload) assert res.status == "202 ACCEPTED" assert data["metadata"]["item_pid"] == payload["item_pid"] # the ref won't change as it is pointing back to loan (it will # resolve by loan endpoint) assert data["metadata"]["item"]["ref"] == loan["loan_pid"]
def get_item_end_date(self, format='short_date'): """Get item due date for a given item.""" loan = get_loan_for_item(self.pid) if loan: end_date = loan['end_date'] due_date = format_date_filter( end_date, format=format, locale=current_i18n.locale.language, ) return due_date return None
def item(item_barcode): """HTTP GET request for requested loans for a library.""" item = Item.get_item_by_barcode(item_barcode) if not item: abort(404) loan = get_loan_for_item(item.pid) if loan: loan = Loan.get_record_by_pid(loan['loan_pid']).dumps_for_circulation() return jsonify( {'metadata': { 'item': item.dumps_for_circulation(), 'loan': loan }})
def test_multiple_active_loans(app, db, indexed_loans): """Test that raises if there are multiple active Loans for given Item.""" multiple_loans_pid = "item_multiple_pending_on_loan_7" test_loan_data = { "item_pid": "item_multiple_pending_on_loan_7", "patron_pid": "2", "state": "ITEM_ON_LOAN", "transaction_date": "2018-06-26", "transaction_location_pid": "loc_pid", "transaction_user_pid": "user_pid", "start_date": "2018-07-24", "end_date": "2018-08-23" } pid, loan = create_loan(test_loan_data) db.session.commit() RecordIndexer().index(loan) current_search.flush_and_refresh(index="loans") with pytest.raises(MultipleLoansOnItemError): get_loan_for_item(multiple_loans_pid)
def loan_for_item_resolver(item_pid): """Return the loan for the given item.""" loan = get_loan_for_item(item_pid) or {} return { "loan_pid": loan.get("pid"), "patron_pid": loan.get("patron_pid"), "document_pid": loan.get("document_pid"), "item_pid": loan.get("item_pid"), "state": loan.get("state"), "start_date": loan.get("start_date"), "end_date": loan.get("end_date"), "request_expire_date": loan.get("request_expire_date"), "extension_count": loan.get("extension_count"), }
def prior_checkout_actions(self, action_params): """Actions executed prior to a checkout.""" if action_params.get('pid'): loan = Loan.get_record_by_pid(action_params.get('pid')) if (loan.get('state') == 'ITEM_IN_TRANSIT_FOR_PICKUP' and loan.get('patron_pid') == action_params.get('patron_pid')): self.receive(**action_params) if loan.get('state') == 'ITEM_IN_TRANSIT_TO_HOUSE': self.cancel_loan(pid=loan.get('pid')) del action_params['pid'] else: loan = get_loan_for_item(self.pid) if (loan and loan.get('state') != 'ITEM_AT_DESK'): self.cancel_loan(pid=loan.get('pid')) return action_params
def prior_checkout_actions(item, data): """Actions executed prior to a checkout.""" if data.get('pid'): loan = Loan.get_record_by_pid(data.get('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(pid=loan.get('pid')) del data['pid'] else: loan = get_loan_for_item(item.pid) if loan: item.cancel_loan(pid=loan.get('pid')) return data
def create_loan(barcode, transaction_type, loanable_items): """Create loans transactions.""" item = next(loanable_items) patron = Patron.get_patron_by_barcode(barcode=barcode) start_date, end_date = get_loan_dates(transaction_type, item, patron) transaction_date = datetime.now(timezone.utc).isoformat() user_pid, user_location = get_random_librarian_and_transaction_location( patron) item.checkout( patron_pid=patron.pid, transaction_user_pid=user_pid, transaction_location_pid=user_location, transaction_date=transaction_date, document_pid=item.replace_refs()['document']['pid'], item_pid=item.pid, ) if transaction_type == 'extended': loan = get_loan_for_item(item.pid) loan_pid = loan.get('pid') user_pid, user_location = \ get_random_librarian_and_transaction_location(patron) item.extend_loan( pid=loan_pid, patron_pid=patron.pid, transaction_location_pid=user_location, transaction_user_pid=user_pid, transaction_date=transaction_date, document_pid=item.replace_refs()['document']['pid'], item_pid=item.pid, ) if transaction_type == 'requested_by_others': requested_patron = get_random_patron(barcode) user_pid, user_location = \ get_random_librarian_and_transaction_location(patron) circ_policy = CircPolicy.provide_circ_policy( item.library_pid, requested_patron.patron_type_pid, item.item_type_pid) if circ_policy.get('allow_requests'): item.request( patron_pid=requested_patron.pid, transaction_location_pid=user_location, transaction_user_pid=user_pid, transaction_date=transaction_date, pickup_location_pid=get_random_pickup_location( requested_patron.pid), document_pid=item.replace_refs()['document']['pid'], ) return item['barcode']
def get_item_end_date(self): """Get item due date a given item.""" loan = get_loan_for_item(self.pid) if loan: end_date = loan['end_date'] # due_date = datetime.strptime(end_date, '%Y-%m-%d') from ...filter import format_date_filter from invenio_i18n.ext import current_i18n due_date = format_date_filter( end_date, format='short_date', locale=current_i18n.locale.language, ) return due_date return None
def loan_for_item_resolver(item_pid): """Return the loan for the given item.""" loan = get_loan_for_item(item_pid) if not loan: return {} else: return { "loan_pid": loan.get("pid"), "patron_pid": loan.get("patron_pid"), "document_pid": loan.get("document_pid"), "item_pid": loan.get("item_pid"), "state": loan.get("state"), "start_date": loan.get("start_date"), "end_date": loan.get("end_date"), "extension_count": loan.get("extension_count"), }
def loan_for_item_resolver(item_pid): """Return the loan for the given item.""" loan = get_loan_for_item(dict(value=item_pid, type=ITEM_PID_TYPE)) if not loan: return {} else: return { "loan_pid": loan["pid"], "patron_pid": loan["patron_pid"], "patron": loan["patron"], "document_pid": loan["document_pid"], "item_pid": loan.get("item_pid"), "state": loan["state"], "start_date": loan["start_date"], "end_date": loan["end_date"], "extension_count": loan.get("extension_count", 0), "is_overdue": loan.get("is_overdue", False), }
def item(item_barcode): """HTTP GET request for requested loans for a library item and patron.""" item = Item.get_item_by_barcode(item_barcode, current_organisation.pid) if not item: abort(404) loan = get_loan_for_item(item_pid_to_object(item.pid)) if loan: loan = Loan.get_record_by_pid(loan.get('pid')).dumps_for_circulation() item_dumps = item.dumps_for_circulation() patron_pid = flask_request.args.get('patron_pid') if patron_pid: patron = Patron.get_record_by_pid(patron_pid) circ_policy = CircPolicy.provide_circ_policy( item.library_pid, patron.patron_type_pid, item.item_type_pid ) new_actions = [] # If circulation policy doesn't allow checkout operation no need to # perform special check describe below. if circ_policy.get('allow_checkout', False): for action in item_dumps.get('actions', []): if action == 'checkout': if item.number_of_requests() > 0: patron_barcode = patron.get('patron', {})\ .get('barcode') if item.patron_request_rank(patron_barcode) == 1: new_actions.append(action) else: new_actions.append(action) elif action == 'receive' and item.number_of_requests() == 0: new_actions.append('checkout') item_dumps['actions'] = new_actions return jsonify({ 'metadata': { 'item': item_dumps, 'loan': loan } })
def actions(self): """Get all available actions.""" transitions = current_app.config.get('CIRCULATION_LOAN_TRANSITIONS') loan = get_loan_for_item(self.pid) actions = set() if loan: for transition in transitions.get(loan.get('state')): action = transition.get('trigger') 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'): action = transition.get('trigger') actions.add(action) # remove unsupported action for action in ['cancel', 'request']: try: actions.remove(action) # not yet supported # actions.add('cancel_loan') except KeyError: pass # rename try: actions.remove('extend') actions.add('extend_loan') except KeyError: pass # if self['status'] == ItemStatus.MISSING: # actions.add('return_missing') # else: # actions.add('lose') return actions
def get_patron_from_checkout_item_pid(item_pid): """Get patron from a checkout item pid.""" from invenio_circulation.api import get_loan_for_item patron_pid = get_loan_for_item(item_pid)['patron_pid'] return Patron.get_record_by_pid(patron_pid)
def get_checkout_loan_for_item(item_pid): """Get patron from a checkout item pid.""" from invenio_circulation.api import get_loan_for_item return get_loan_for_item(item_pid)
def test_api_circulation_item_not_found(app, indexed_loans): """Test API GET call to check circulation item not found.""" loan = get_loan_for_item("") assert loan is None
def test_api_circulation_item_on_loan(app, indexed_loans): """Test that Loan is returned for the given Item if one active.""" multiple_loans_pid = "item_multiple_pending_on_loan_7" loan = get_loan_for_item(multiple_loans_pid) assert loan['state'] == 'ITEM_ON_LOAN' assert loan['item_pid'] == multiple_loans_pid
def test_api_circulation_item_loan_pending(app, indexed_loans): """Test that no Loan is returned for the given Item if only pendings.""" pending_item_pid = {"type": "itemid", "value": "item_pending_1"} loan = get_loan_for_item(pending_item_pid) assert loan is None
def test_api_circulation_item_no_loan(app, indexed_loans): """Test that no Loan is returned for the given Item if no loans.""" not_loaned_item_pid = {"type": "itemid", "value": "item_not_loaned"} loan = get_loan_for_item(not_loaned_item_pid) assert loan is None
def _loan_for_item_resolver(pid_value): """Return the loan for the given item.""" loan = get_loan_for_item(pid_value) or {} if loan.get("item"): del loan["item"] return loan
def test_checkout_item_transit(client, item2_lib_martigny, librarian_martigny_no_email, librarian_saxon_no_email, patron_martigny_no_email, loc_public_saxon, lib_martigny, loc_public_martigny, circulation_policies): """Test checkout of an item in transit.""" assert item2_lib_martigny.available # request login_user_via_session(client, librarian_martigny_no_email.user) loc_public_martigny['notification_email'] = '*****@*****.**' loc_public_martigny['send_notification'] = True loc_public_martigny.update( loc_public_martigny.dumps(), dbcommit=True, reindex=True ) res, data = postdata( client, 'api_item.librarian_request', dict( item_pid=item2_lib_martigny.pid, pickup_location_pid=loc_public_saxon.pid, patron_pid=patron_martigny_no_email.pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny_no_email.pid ) ) assert res.status_code == 200 actions = data.get('action_applied') loan_pid = actions[LoanAction.REQUEST].get('pid') assert not item2_lib_martigny.available loan = Loan.get_record_by_pid(loan_pid) assert loan['state'] == LoanState.PENDING # reset the location del loc_public_martigny['notification_email'] del loc_public_martigny['send_notification'] loc_public_martigny.update( loc_public_martigny.dumps(), dbcommit=True, reindex=True ) # validate request res, _ = postdata( client, 'api_item.validate_request', dict( item_pid=item2_lib_martigny.pid, pid=loan_pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny_no_email.pid ) ) assert res.status_code == 200 assert not item2_lib_martigny.available item = Item.get_record_by_pid(item2_lib_martigny.pid) assert not item.available loan = Loan.get_record_by_pid(loan_pid) assert loan['state'] == LoanState.ITEM_IN_TRANSIT_FOR_PICKUP login_user_via_session(client, librarian_saxon_no_email.user) # receive res, _ = postdata( client, 'api_item.receive', dict( item_pid=item2_lib_martigny.pid, pid=loan_pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny_no_email.pid ) ) assert res.status_code == 200 assert not item2_lib_martigny.available item = Item.get_record_by_pid(item2_lib_martigny.pid) assert not item.available loan_before_checkout = get_loan_for_item(item_pid_to_object(item.pid)) assert loan_before_checkout.get('state') == LoanState.ITEM_AT_DESK # checkout res, _ = postdata( client, 'api_item.checkout', dict( item_pid=item2_lib_martigny.pid, patron_pid=patron_martigny_no_email.pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny_no_email.pid, ) ) assert res.status_code == 200 item = Item.get_record_by_pid(item2_lib_martigny.pid) loan_after_checkout = get_loan_for_item(item_pid_to_object(item.pid)) assert loan_after_checkout.get('state') == LoanState.ITEM_ON_LOAN assert loan_before_checkout.get('pid') == loan_after_checkout.get('pid')
def test_checkout_item_transit(client, item2_lib_martigny, librarian_martigny_no_email, librarian_saxon_no_email, patron_martigny_no_email, loc_public_saxon, circulation_policies): """Test checkout of an item in transit.""" assert item2_lib_martigny.available # request login_user_via_session(client, librarian_martigny_no_email.user) res = client.post( url_for('api_item.librarian_request'), data=json.dumps( dict(item_pid=item2_lib_martigny.pid, pickup_location_pid=loc_public_saxon.pid, patron_pid=patron_martigny_no_email.pid)), content_type='application/json', ) assert res.status_code == 200 data = get_json(res) actions = data.get('action_applied') loan_pid = actions[LoanAction.REQUEST].get('pid') assert not item2_lib_martigny.available loan = Loan.get_record_by_pid(loan_pid) assert loan.get('state') == 'PENDING' # validate request res = client.post( url_for('api_item.validate_request'), data=json.dumps(dict(item_pid=item2_lib_martigny.pid, pid=loan_pid)), content_type='application/json', ) assert res.status_code == 200 assert not item2_lib_martigny.available item = Item.get_record_by_pid(item2_lib_martigny.pid) assert not item.available loan = Loan.get_record_by_pid(loan_pid) assert loan.get('state') == 'ITEM_IN_TRANSIT_FOR_PICKUP' login_user_via_session(client, librarian_saxon_no_email.user) # receive res = client.post( url_for('api_item.receive'), data=json.dumps(dict(item_pid=item2_lib_martigny.pid, pid=loan_pid)), content_type='application/json', ) assert res.status_code == 200 assert not item2_lib_martigny.available item = Item.get_record_by_pid(item2_lib_martigny.pid) assert not item.available loan_before_checkout = get_loan_for_item(item.pid) assert loan_before_checkout.get('state') == 'ITEM_AT_DESK' # checkout res = client.post( url_for('api_item.checkout'), data=json.dumps( dict(item_pid=item2_lib_martigny.pid, patron_pid=patron_martigny_no_email.pid)), content_type='application/json', ) assert res.status_code == 200 item = Item.get_record_by_pid(item2_lib_martigny.pid) loan_after_checkout = get_loan_for_item(item.pid) assert loan_after_checkout.get('state') == 'ITEM_ON_LOAN' assert loan_before_checkout.get('pid') == loan_after_checkout.get('pid')
def create_loan(barcode, transaction_type, loanable_items, verbose=False, debug=False): """Create loans transactions.""" try: item = next(loanable_items) patron = Patron.get_patron_by_barcode(barcode=barcode) transaction_date = datetime.now(timezone.utc).isoformat() user_pid, user_location = \ get_random_librarian_and_transaction_location(patron) item.checkout( patron_pid=patron.pid, transaction_user_pid=user_pid, transaction_location_pid=user_location, transaction_date=transaction_date, document_pid=item.replace_refs()['document']['pid'], item_pid=item.pid, ) loan = get_loan_for_item(item_pid_to_object(item.pid)) loan_pid = loan.get('pid') loan = Loan.get_record_by_pid(loan_pid) if transaction_type == 'overdue_active': end_date = datetime.now(timezone.utc) - timedelta(days=2) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) loan.create_notification(notification_type='due_soon') end_date = datetime.now(timezone.utc) - timedelta(days=70) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) loan.create_notification(notification_type='overdue') elif transaction_type == 'overdue_paid': end_date = datetime.now(timezone.utc) - timedelta(days=2) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) loan.create_notification(notification_type='due_soon') end_date = datetime.now(timezone.utc) - timedelta(days=70) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) notif = loan.create_notification(notification_type='overdue') patron_transaction = [ record for record in notif.patron_transactions ][0] user = get_random_librarian(patron).replace_refs() payment = create_payment_record( patron_transaction, user_pid, random.choice(user['libraries'])['pid']) PatronTransactionEvent.create(payment, dbcommit=True, reindex=True, update_parent=True) elif transaction_type == 'extended': user_pid, user_location = \ get_random_librarian_and_transaction_location(patron) item.extend_loan( pid=loan_pid, patron_pid=patron.pid, transaction_location_pid=user_location, transaction_user_pid=user_pid, transaction_date=transaction_date, document_pid=item.replace_refs()['document']['pid'], item_pid=item.pid, ) elif transaction_type == 'requested_by_others': requested_patron = get_random_patron(barcode) user_pid, user_location = \ get_random_librarian_and_transaction_location(patron) circ_policy = CircPolicy.provide_circ_policy( item.library_pid, requested_patron.patron_type_pid, item.item_type_pid) if circ_policy.get('allow_requests'): item.request( patron_pid=requested_patron.pid, transaction_location_pid=user_location, transaction_user_pid=user_pid, transaction_date=transaction_date, pickup_location_pid=get_random_pickup_location( requested_patron.pid, item), document_pid=item.replace_refs()['document']['pid'], ) loan.create_notification(notification_type='recall') return item['barcode'] except Exception as err: if verbose: click.secho('\tException loan {transaction_type}: {err}'.format( transaction_type=transaction_type, err=err), fg='red') if debug: traceback.print_exc() return None
def create_loan(barcode, transaction_type, loanable_items, verbose=False, debug=False): """Create loans transactions.""" notification_pids = [] try: item = next(loanable_items) patron = Patron.get_patron_by_barcode(barcode=barcode) transaction_date = datetime.now(timezone.utc).isoformat() user_pid, user_location = \ get_random_librarian_and_transaction_location(patron) item.checkout( patron_pid=patron.pid, transaction_user_pid=user_pid, transaction_location_pid=user_location, transaction_date=transaction_date, document_pid=extracted_data_from_ref(item.get('document')), item_pid=item.pid, ) loan = get_loan_for_item(item_pid_to_object(item.pid)) loan_pid = loan.get('pid') loan = Loan.get_record_by_pid(loan_pid) if transaction_type == 'overdue_active': end_date = datetime.now(timezone.utc) - timedelta(days=2) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) notifications = loan.create_notification( _type=NotificationType.DUE_SOON) for notif in notifications: notification_pids.append(notif['pid']) end_date = datetime.now(timezone.utc) - timedelta(days=70) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) notifications = loan.create_notification( _type=NotificationType.OVERDUE) for notif in notifications: notification_pids.append(notif['pid']) elif transaction_type == 'overdue_paid': end_date = datetime.now(timezone.utc) - timedelta(days=2) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) notifications = loan.create_notification( _type=NotificationType.DUE_SOON) for notif in notifications: notification_pids.append(notif['pid']) end_date = datetime.now(timezone.utc) - timedelta(days=70) loan['end_date'] = end_date.isoformat() loan.update(loan, dbcommit=True, reindex=True) notifications = loan.create_notification( _type=NotificationType.OVERDUE) for notif in notifications: notification_pids.append(notif['pid']) patron_transaction = next(notif.patron_transactions) user = get_random_librarian(patron).replace_refs() payment = create_payment_record( patron_transaction, user_pid, random.choice(user['libraries'])['pid']) PatronTransactionEvent.create(payment, dbcommit=True, reindex=True, update_parent=True) elif transaction_type == 'extended': user_pid, user_location = \ get_random_librarian_and_transaction_location(patron) item.extend_loan( pid=loan_pid, patron_pid=patron.pid, transaction_location_pid=user_location, transaction_user_pid=user_pid, transaction_date=transaction_date, document_pid=extracted_data_from_ref(item.get('document')), item_pid=item.pid, ) elif transaction_type == 'requested_by_others': requested_patron = get_random_patron(barcode) user_pid, user_location = \ get_random_librarian_and_transaction_location(patron) circ_policy = CircPolicy.provide_circ_policy( item.organisation_pid, item.library_pid, requested_patron.patron_type_pid, item.item_type_circulation_category_pid) if circ_policy.get('allow_requests'): item.request( patron_pid=requested_patron.pid, transaction_location_pid=user_location, transaction_user_pid=user_pid, transaction_date=transaction_date, pickup_location_pid=get_random_pickup_location( requested_patron.pid, item), document_pid=extracted_data_from_ref(item.get('document')), ) notifications = loan.create_notification( _type=NotificationType.RECALL) for notif in notifications: notification_pids.append(notif['pid']) Dispatcher.dispatch_notifications(notification_pids, verbose=verbose) return item['barcode'] except Exception as err: if verbose: click.secho(f'\tException loan {transaction_type}:{err}', fg='red') if debug: traceback.print_exc() return None, []
def test_api_circulation_item_no_loan(app, indexed_loans): """Test that no Loan is returned for the given Item if no loans.""" not_loaned_item_pid = "item_not_loaned" loan = get_loan_for_item(not_loaned_item_pid) assert loan is None
def get_extension_count(self): """Get item renewal count.""" loan = get_loan_for_item(self.pid) if loan: return loan.get('extension_count', 0) return 0
def test_api_circulation_item_loan_pending(app, indexed_loans): """Test that no Loan is returned for the given Item if only pendings.""" pending_item_pid = "item_pending_1" loan = get_loan_for_item(pending_item_pid) assert loan is None