def test_notification_create(es_clear, dummy_notification, loan_validated_martigny, mailbox): """Test notification creation.""" notif = deepcopy(dummy_notification) notif_data = { 'loan_url': 'https://ils.rero.ch/api/loans/', 'pid': loan_validated_martigny.get('pid') } loan_ref = '{loan_url}{pid}'.format(**notif_data) notif['loan'] = {"$ref": loan_ref} notification = Notification.create(notif, dbcommit=True, delete_pid=True, reindex=True) assert notification == notif pid = notification.get('pid') notification = Notification.get_record_by_pid(pid) assert notification == notif fetched_pid = fetcher(notification.id, notification) assert fetched_pid.pid_value == pid assert fetched_pid.pid_type == 'notif' notification.dispatch(enqueue=False, verbose=True) assert len(mailbox) == 1
def test_recall_notification_without_email( client, patron_sion_without_email, lib_martigny, json_header, patron2_martigny_no_email, item3_lib_martigny, librarian_martigny_no_email, circulation_policies, loc_public_martigny, mailbox): """Test recall notification.""" # process all notifications still in the queue Notification.process_notifications() mailbox.clear() login_user_via_session(client, librarian_martigny_no_email.user) res, data = postdata( client, 'api_item.checkout', dict( item_pid=item3_lib_martigny.pid, patron_pid=patron_sion_without_email.pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny_no_email.pid, )) assert res.status_code == 200 loan_pid = data.get('action_applied')[LoanAction.CHECKOUT].get('pid') loan = Loan.get_record_by_pid(loan_pid) assert not loan.is_notified(notification_type='recall') # test notification res, data = postdata( client, 'api_item.librarian_request', dict(item_pid=item3_lib_martigny.pid, pickup_location_pid=loc_public_martigny.pid, patron_pid=patron2_martigny_no_email.pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny_no_email.pid)) assert res.status_code == 200 request_loan_pid = data.get('action_applied')[LoanAction.REQUEST].get( 'pid') flush_index(NotificationsSearch.Meta.index) assert loan.is_notified(notification_type='recall') notification = get_recall_notification(loan) assert notification.loan_pid == loan.pid assert not loan.is_notified(notification_type='availability') assert not get_availability_notification(loan) assert Notification.process_notifications() == { 'send': 1, 'reject': 0, 'error': 0 } # no email as the patron does not have an email assert len(mailbox) == 0 mailbox.clear()
def test_loan(app, notification_schema, dummy_notification): """Test loan for notification jsonschemas.""" validate(dummy_notification, notification_schema) with pytest.raises(ValidationError): dummy_notification['context']['loan'] = 25 validate(dummy_notification, notification_schema) with pytest.raises(ValidationError): notif = Notification(dummy_notification) del notif['context']['loan'] notif.validate()
def test_notification_es_mapping(dummy_notification, loan_validated_martigny): """Test notification elasticsearch mapping.""" search = NotificationsSearch() mapping = get_mapping(search.Meta.index) assert mapping notif = deepcopy(dummy_notification) validated_pid = loan_validated_martigny.get('pid') loan_ref = f'https://bib.rero.ch/api/loans/{validated_pid}' notif['context']['loan']['$ref'] = loan_ref Notification.create(notif, dbcommit=True, delete_pid=True, reindex=True) assert mapping == get_mapping(search.Meta.index)
def test_notification_es_mapping(dummy_notification, loan_validated_martigny): """Test notification elasticsearch mapping.""" search = NotificationsSearch() mapping = get_mapping(search.Meta.index) assert mapping notif = deepcopy(dummy_notification) notif_data = { 'loan_url': 'https://ils.rero.ch/api/loans/', 'pid': loan_validated_martigny.get('pid') } loan_ref = '{loan_url}{pid}'.format(**notif_data) notif['loan'] = {"$ref": loan_ref} Notification.create(notif, dbcommit=True, delete_pid=True, reindex=True) assert mapping == get_mapping(search.Meta.index)
def test_notifications_post_put_delete( client, dummy_notification, loan_validated_martigny, json_header): """Test record delete and update.""" record = deepcopy(dummy_notification) del record['pid'] notif_data = { 'loan_url': 'https://ils.rero.ch/api/loans/', 'pid': loan_validated_martigny.get('pid') } loan_ref = '{loan_url}{pid}'.format(**notif_data) record['loan'] = {"$ref": loan_ref} notif = Notification.create( record, dbcommit=True, reindex=True, delete_pid=True ) assert notif == record flush_index(NotificationsSearch.Meta.index) pid = notif.get('pid') item_url = url_for('invenio_records_rest.notif_item', pid_value=pid) list_url = url_for('invenio_records_rest.notif_list', q='pid:pid') new_record = deepcopy(record) # Create record / POST new_record['pid'] = 'x' res, data = postdata( client, 'invenio_records_rest.notif_list', new_record ) assert res.status_code == 201 flush_index(NotificationsSearch.Meta.index) # Check that the returned record matches the given data assert data['metadata'] == new_record res = client.get(item_url) assert res.status_code == 200 data = get_json(res) assert notif == data['metadata'] # Update record/PUT data['notification_type'] = 'due_soon' res = client.put( item_url, data=json.dumps(data), headers=json_header ) assert res.status_code == 200 # assert res.headers['ETag'] != '"{}"'.format(librarie.revision_id) # Check that the returned record matches the given data data = get_json(res) assert data['metadata']['notification_type'] == 'due_soon' res = client.get(item_url) assert res.status_code == 200 data = get_json(res) assert data['metadata']['notification_type'] == 'due_soon' res = client.get(list_url) assert res.status_code == 200 # Delete record/DELETE res = client.delete(item_url) assert res.status_code == 204 res = client.get(item_url) assert res.status_code == 410 links = notif.get_links_to_me() assert links == {} assert notif.can_delete reasons = notif.reasons_not_to_delete() assert reasons == {} notif.delete(dbcommit=True, delindex=True)
def test_cancel_notifications( client, patron_martigny, lib_martigny, item_lib_martigny, librarian_martigny, loc_public_martigny, circulation_policies, mailbox ): """Test cancel notifications.""" login_user_via_session(client, librarian_martigny.user) # CREATE and VALIDATE a request ... res, data = postdata( client, 'api_item.librarian_request', dict( item_pid=item_lib_martigny.pid, pickup_location_pid=loc_public_martigny.pid, patron_pid=patron_martigny.pid, transaction_library_pid=lib_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 200 request_loan_pid = data.get( 'action_applied')[LoanAction.REQUEST].get('pid') flush_index(NotificationsSearch.Meta.index) res, data = postdata( client, 'api_item.validate_request', dict( pid=request_loan_pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid ) ) assert res.status_code == 200 # At this time, an AVAILABILITY notification should be create but not yet # dispatched loan = Loan.get_record_by_pid(request_loan_pid) notification = get_notification(loan, NotificationType.AVAILABILITY) assert notification \ and notification['status'] == NotificationStatus.CREATED # BORROW the requested item res, data = postdata( client, 'api_item.checkout', dict( item_pid=item_lib_martigny.pid, patron_pid=patron_martigny.pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid, ) ) assert res.status_code == 200 loan_pid = data.get( 'action_applied')[LoanAction.CHECKOUT].get('pid') loan = Loan.get_record_by_pid(loan_pid) # Try to dispatch pending availability notifications. # As the item is now checkout, then the availability notification is not # yet relevant. mailbox.clear() process_notifications(NotificationType.AVAILABILITY) notification = get_notification(loan, NotificationType.AVAILABILITY) assert notification and \ notification['status'] == NotificationStatus.CANCELLED assert len(mailbox) == 0 # restore to initial state res, data = postdata( client, 'api_item.checkin', dict( item_pid=item_lib_martigny.pid, # patron_pid=patron_martigny.pid, transaction_location_pid=loc_public_martigny.pid, transaction_user_pid=librarian_martigny.pid, ) ) assert res.status_code == 200 mailbox.clear() # Test REMINDERS notifications. # reminders notification check about the end_date. As the loan end_date # is not yet over, the notification could be cancelled. notification = loan.create_notification(_type=NotificationType.DUE_SOON)[0] can_cancel, _ = notification.can_be_cancelled() assert not can_cancel process_notifications(NotificationType.DUE_SOON) notification = Notification.get_record_by_pid(notification.pid) assert notification['status'] == NotificationStatus.DONE flush_index(NotificationsSearch.Meta.index) # try to create a new DUE_SOON notification for the same loan record = { 'creation_date': datetime.now(timezone.utc).isoformat(), 'notification_type': NotificationType.DUE_SOON, 'context': { 'loan': {'$ref': get_ref_for_pid('loans', loan.pid)}, 'reminder_counter': 0 } } notification = Notification.create(record) can_cancel, _ = notification.can_be_cancelled() assert can_cancel Dispatcher.dispatch_notifications([notification.pid]) notification = Notification.get_record_by_pid(notification.pid) assert notification['status'] == NotificationStatus.CANCELLED
def test_notifications_post_put_delete( client, dummy_notification, loan_validated_martigny, json_header): """Test record delete and update.""" record = deepcopy(dummy_notification) del record['pid'] loan_ref = get_ref_for_pid('loans', loan_validated_martigny.get('pid')) record['context']['loan'] = {'$ref': loan_ref} notif = Notification.create( record, dbcommit=True, reindex=True, delete_pid=True ) assert notif == record flush_index(NotificationsSearch.Meta.index) pid = notif.get('pid') item_url = url_for('invenio_records_rest.notif_item', pid_value=pid) list_url = url_for('invenio_records_rest.notif_list', q='pid:pid') new_record = deepcopy(record) # Create record / POST new_record['pid'] = 'x' res, data = postdata( client, 'invenio_records_rest.notif_list', new_record ) assert res.status_code == 201 flush_index(NotificationsSearch.Meta.index) # Check that the returned record matches the given data assert data['metadata'] == new_record res = client.get(item_url) assert res.status_code == 200 data = get_json(res) assert notif == data['metadata'] # Update record/PUT data = data['metadata'] data['notification_type'] = NotificationType.DUE_SOON res = client.put( item_url, data=json.dumps(data), headers=json_header ) assert res.status_code == 200 # Check that the returned record matches the given data data = get_json(res) assert data['metadata']['notification_type'] == NotificationType.DUE_SOON res = client.get(item_url) assert res.status_code == 200 data = get_json(res) assert data['metadata']['notification_type'] == NotificationType.DUE_SOON res = client.get(list_url) assert res.status_code == 200 # Delete record/DELETE res = client.delete(item_url) assert res.status_code == 204 res = client.get(item_url) assert res.status_code == 410 can, reasons = notif.can_delete assert can assert reasons == {} notif.delete(dbcommit=True, delindex=True)
def test_send_order(app, client, librarian_martigny, lib_martigny, acq_order_fiction_martigny, acq_order_line_fiction_martigny, vendor_martigny, acq_order_line2_fiction_martigny, acq_order_line3_fiction_martigny, mailbox): """Test send order notification API.""" login_user_via_session(client, librarian_martigny.user) acor = acq_order_fiction_martigny address = vendor_martigny.get('default_contact').get('email') emails = [{'type': 'cc', 'address': address}] mailbox.clear() # test when parent order is not in database res, data = postdata(client, 'api_order.send_order', data=dict(emails=emails), url_data=dict(order_pid='toto')) assert res.status_code == 404 # test when email data is not provided res, data = postdata(client, 'api_order.send_order', url_data=dict(order_pid=acor.pid)) assert res.status_code == 400 # test when email data provided but empty res, data = postdata(client, 'api_order.send_order', data=dict(emails=[]), url_data=dict(order_pid=acor.pid)) assert res.status_code == 400 # test when email data provided and has no "to" email address res, data = postdata(client, 'api_order.send_order', data=dict(emails=emails), url_data=dict(order_pid=acor.pid)) assert res.status_code == 400 assert 'required' in data['message'] and '`to`' in data['message'] # have an order line with a status different than approved and ensure it # will not be ordered l3 = AcqOrderLine.get_record_by_pid(acq_order_line3_fiction_martigny.pid) l3['is_cancelled'] = True l3.update(l3, dbcommit=True, reindex=True) # test send order with correct input parameters emails = [{ 'type': 'to', 'address': address }, { 'type': 'reply_to', 'address': lib_martigny.get('email') }] res, data = postdata(client, 'api_order.send_order', data=dict(emails=emails), url_data=dict(order_pid=acor.pid)) data = get_json(res) assert res.status_code == 200 # Reload the order and related order lines to check updated fields # * the order status should be ORDERED # * each order lines should be ORDERED (except for l3 = CANCELLED) acor = AcqOrder.get_record_by_pid(acor.pid) l1 = AcqOrderLine.get_record_by_pid(acq_order_line_fiction_martigny.pid) l2 = AcqOrderLine.get_record_by_pid(acq_order_line2_fiction_martigny.pid) l3 = AcqOrderLine.get_record_by_pid(acq_order_line3_fiction_martigny.pid) assert l1.status == AcqOrderLineStatus.ORDERED assert l1.order_date assert l2.status == AcqOrderLineStatus.ORDERED assert l2.order_date assert l3.status == AcqOrderLineStatus.CANCELLED assert not l3.order_date assert acor.status == AcqOrderStatus.ORDERED # ensure that created notification is well constructed from the associated # order and vendor notification_pid = data.get('data').get('pid') notif = Notification.get_record_by_pid(notification_pid) assert notif.organisation_pid == acor.organisation_pid assert notif.aggregation_key == str(notif.id) assert notif.type == NotificationType.ACQUISITION_ORDER assert notif.status == NotificationStatus.DONE assert notif.acq_order_pid == acor.pid assert notif.library_pid == acor.library_pid assert notif.can_be_cancelled() == (False, None) assert notif.get_communication_channel() == NotificationChannel.EMAIL assert notif.get_language_to_use() == \ vendor_martigny.get('communication_language') assert address in notif.get_recipients(RecipientType.TO) # Check mail content message = mailbox[-1] shipping = lib_martigny['acquisition_settings']['shipping_informations'] assert shipping.get('extra') and shipping.get('extra') in message.body
def test_acquisition_reception_workflow( client, rero_json_header, org_martigny, lib_martigny, lib_saxon, budget_2020_martigny, vendor_martigny, librarian_martigny, document ): """Test complete acquisition workflow.""" def assert_account_data(accounts): """assert account informations.""" for acc, (balance, expenditure, encumbrance) in accounts.items(): assert acc.expenditure_amount == expenditure assert acc.encumbrance_amount == encumbrance assert acc.remaining_balance == balance # STEP 1 :: Create account structures # * Create accounts for Martigny library. # - MTY.0000.00' (root) # --> MTY.000b.00 # --> MTY.000s.00 # * Create accounts for Saxon library. # - SXN.0000.00 (root) # --> SXN.000b.00 # --> SXN.000s.00 data = { 'name': 'Martigny root account', 'number': 'MTY.0000.00', 'allocated_amount': 10000, 'budget': {'$ref': get_ref_for_pid('budg', budget_2020_martigny.pid)}, 'library': {'$ref': get_ref_for_pid('lib', lib_martigny.pid)} } m_root_acc = _make_resource(client, 'acac', data) data = { 'name': 'Martigny Books child account', 'number': 'MTY.000b.00', 'allocated_amount': 2000, 'budget': {'$ref': get_ref_for_pid('budg', budget_2020_martigny.pid)}, 'library': {'$ref': get_ref_for_pid('lib', lib_martigny.pid)}, 'parent': {'$ref': get_ref_for_pid('acac', m_root_acc.pid)}, } m_books_acc = _make_resource(client, 'acac', data) data = { 'name': 'Martigny Serials child account', 'number': 'MTY.000s.00', 'allocated_amount': 3000, 'budget': {'$ref': get_ref_for_pid('budg', budget_2020_martigny.pid)}, 'library': {'$ref': get_ref_for_pid('lib', lib_martigny.pid)}, 'parent': {'$ref': get_ref_for_pid('acac', m_root_acc.pid)}, } m_serials_acc = _make_resource(client, 'acac', data) data = { 'name': 'Saxon root account', 'number': 'SXN.0000.00', 'allocated_amount': 20000, 'budget': {'$ref': get_ref_for_pid('budg', budget_2020_martigny.pid)}, 'library': {'$ref': get_ref_for_pid('lib', lib_saxon.pid)} } s_root_acc = _make_resource(client, 'acac', data) data = { 'name': 'Saxon Books chid account', 'number': 'SXN.000b.00', 'allocated_amount': 2500, 'budget': {'$ref': get_ref_for_pid('budg', budget_2020_martigny.pid)}, 'library': {'$ref': get_ref_for_pid('lib', lib_saxon.pid)}, 'parent': {'$ref': get_ref_for_pid('acac', s_root_acc.pid)}, } s_books_acc = _make_resource(client, 'acac', data) data = { 'name': 'Saxon Serials chid account', 'number': 'SXN.000s.00', 'allocated_amount': 4000, 'budget': {'$ref': get_ref_for_pid('budg', budget_2020_martigny.pid)}, 'library': {'$ref': get_ref_for_pid('lib', lib_saxon.pid)}, 'parent': {'$ref': get_ref_for_pid('acac', s_root_acc.pid)}, } s_serials_acc = _make_resource(client, 'acac', data) # For each account check data. the dict values are tuples. Each tuple # define `balance`, `expenditure`, `encumbrance` manual_controls = { m_root_acc: ((5000, 10000), (0, 0), (0, 0)), m_books_acc: ((2000, 2000), (0, 0), (0, 0)), m_serials_acc: ((3000, 3000), (0, 0), (0, 0)), s_root_acc: ((13500, 20000), (0, 0), (0, 0)), s_books_acc: ((2500, 2500), (0, 0), (0, 0)), s_serials_acc: ((4000, 4000), (0, 0), (0, 0)) } assert_account_data(manual_controls) # STEP 2 :: CREATE REAL ORDER # * Create an order for Martigny library # * Adds 6 order lines for this order # martigny_books_account: # line_1: quantity: 5 of amount 10 = 50 # line_2: quantity: 2 of amount 50 = 100 # line_3: quantity: 3 of amount 100 = 300 # Total = 450 # martigny_serials_account: # line_1: quantity: 3 of amount 15 = 45 # line_2: quantity: 1 of amount 150 = 150 # line_3: quantity: 10 of amount 7 = 70 # Total = 265 # Items quantity = 24: order total amount = 715 login_user_via_session(client, librarian_martigny.user) data = { 'vendor': {'$ref': get_ref_for_pid('vndr', vendor_martigny.pid)}, 'library': {'$ref': get_ref_for_pid('lib', lib_martigny.pid)}, 'type': 'monograph', } order = _make_resource(client, 'acor', data) assert order['reference'] == f'ORDER-{order.pid}' assert order.get_order_provisional_total_amount() == 0 assert order.status == AcqOrderStatus.PENDING assert order.can_delete data = { 'acq_account': {'$ref': get_ref_for_pid('acac', m_books_acc.pid)}, 'acq_order': {'$ref': get_ref_for_pid('acor', order.pid)}, 'document': {'$ref': get_ref_for_pid('doc', document.pid)}, 'quantity': 5, 'amount': 10 } order_line_1 = _make_resource(client, 'acol', data) order_line_1_ref = get_ref_for_pid('acol', order_line_1.pid) assert order_line_1.get('total_amount') == 50 assert order_line_1.quantity == 5 assert order_line_1.received_quantity == 0 assert order_line_1.unreceived_quantity == 5 assert not order_line_1.is_cancelled assert order_line_1.status == AcqOrderLineStatus.APPROVED data = { 'acq_account': {'$ref': get_ref_for_pid('acac', m_books_acc.pid)}, 'acq_order': {'$ref': get_ref_for_pid('acor', order.pid)}, 'document': {'$ref': get_ref_for_pid('doc', document.pid)}, 'quantity': 2, 'amount': 50 } order_line_2 = _make_resource(client, 'acol', data) order_line_2_ref = get_ref_for_pid('acol', order_line_2.pid) assert order_line_2.get('total_amount') == 100 assert order_line_2.quantity == 2 assert order_line_2.received_quantity == 0 assert order_line_2.unreceived_quantity == 2 assert not order_line_2.is_cancelled assert order_line_2.status == AcqOrderLineStatus.APPROVED data = { 'acq_account': {'$ref': get_ref_for_pid('acac', m_books_acc.pid)}, 'acq_order': {'$ref': get_ref_for_pid('acor', order.pid)}, 'document': {'$ref': get_ref_for_pid('doc', document.pid)}, 'quantity': 3, 'amount': 100 } order_line_3 = _make_resource(client, 'acol', data) order_line_3_ref = get_ref_for_pid('acol', order_line_3.pid) assert order_line_3.get('total_amount') == 300 assert order_line_3.quantity == 3 assert order_line_3.received_quantity == 0 assert order_line_3.unreceived_quantity == 3 assert not order_line_3.is_cancelled assert order_line_3.status == AcqOrderLineStatus.APPROVED data = { 'acq_account': {'$ref': get_ref_for_pid('acac', m_serials_acc.pid)}, 'acq_order': {'$ref': get_ref_for_pid('acor', order.pid)}, 'document': {'$ref': get_ref_for_pid('doc', document.pid)}, 'quantity': 3, 'amount': 15 } order_line_4 = _make_resource(client, 'acol', data) order_line_4_ref = get_ref_for_pid('acol', order_line_4.pid) assert order_line_4.get('total_amount') == 45 assert order_line_4.quantity == 3 assert order_line_4.received_quantity == 0 assert order_line_4.unreceived_quantity == 3 assert not order_line_4.is_cancelled assert order_line_4.status == AcqOrderLineStatus.APPROVED data = { 'acq_account': {'$ref': get_ref_for_pid('acac', m_serials_acc.pid)}, 'acq_order': {'$ref': get_ref_for_pid('acor', order.pid)}, 'document': {'$ref': get_ref_for_pid('doc', document.pid)}, 'quantity': 1, 'amount': 150 } order_line_5 = _make_resource(client, 'acol', data) order_line_5_ref = get_ref_for_pid('acol', order_line_5.pid) assert order_line_5.get('total_amount') == 150 assert order_line_5.quantity == 1 assert order_line_5.received_quantity == 0 assert order_line_5.unreceived_quantity == 1 assert not order_line_5.is_cancelled assert order_line_5.status == AcqOrderLineStatus.APPROVED data = { 'acq_account': {'$ref': get_ref_for_pid('acac', m_serials_acc.pid)}, 'acq_order': {'$ref': get_ref_for_pid('acor', order.pid)}, 'document': {'$ref': get_ref_for_pid('doc', document.pid)}, 'quantity': 10, 'amount': 7 } order_line_6 = _make_resource(client, 'acol', data) order_line_6_ref = get_ref_for_pid('acol', order_line_6.pid) assert order_line_6.get('total_amount') == 70 assert order_line_6.quantity == 10 assert order_line_6.received_quantity == 0 assert order_line_6.unreceived_quantity == 10 assert not order_line_6.is_cancelled assert order_line_6.status == AcqOrderLineStatus.APPROVED # test order after adding lines assert order.get_order_provisional_total_amount() == 715 assert order.status == AcqOrderStatus.PENDING # TODO: fix links to me for the order resource, this should fail assert order.can_delete assert not order.order_date assert order.item_quantity == 24 assert order.item_received_quantity == 0 manual_controls = { m_root_acc: ((5000, 9285), (0, 0), (0, 715)), m_books_acc: ((1550, 1550), (0, 0), (450, 0)), m_serials_acc: ((2735, 2735), (0, 0), (265, 0)), s_root_acc: ((13500, 20000), (0, 0), (0, 0)), s_books_acc: ((2500, 2500), (0, 0), (0, 0)), s_serials_acc: ((4000, 4000), (0, 0), (0, 0)) } assert_account_data(manual_controls) # STEP 3 :: UPDATE ORDER LINES # * Cancel some order lines and change some quantities --> make sure # calculations still good # martigny_books_account: # line_1: quantity: 5 of amount 10 = 50 # line_2: quantity: 6 of amount 50 = 300 (quantity: 2 --> 6) # line_3: quantity: 3 of amount 100 = 300 but cancelled ! # Total = 350 # martigny_serials_account: # line_1: quantity: 3 of amount 15 = 45 # line_2: quantity: 2 of amount 150 = 300 (quantity: 1 --> 2) # line_3: quantity: 10 of amount 7 = 70 but cancelled # Total = 345 # Items quantity = 16: order total amount = 695 order_line_2['quantity'] = 6 order_line_2.update(order_line_2, dbcommit=True, reindex=True) order_line_3['is_cancelled'] = True order_line_3.update(order_line_3, dbcommit=True, reindex=True) order_line_5['quantity'] = 2 order_line_5.update(order_line_5, dbcommit=True, reindex=True) order_line_6['is_cancelled'] = True order_line_6.update(order_line_6, dbcommit=True, reindex=True) # ensure correct calculations and status again manual_controls = { m_root_acc: ((5000, 9305), (0, 0), (0, 695)), m_books_acc: ((1650, 1650), (0, 0), (350, 0)), m_serials_acc: ((2655, 2655), (0, 0), (345, 0)), s_root_acc: ((13500, 20000), (0, 0), (0, 0)), s_books_acc: ((2500, 2500), (0, 0), (0, 0)), s_serials_acc: ((4000, 4000), (0, 0), (0, 0)) } assert_account_data(manual_controls) assert order_line_6.get('total_amount') == 70 assert order_line_6.is_cancelled assert order_line_6.quantity == 10 assert order_line_6.received_quantity == 0 assert order_line_6.unreceived_quantity == 10 assert order_line_6.status == AcqOrderLineStatus.CANCELLED assert order_line_5.get('total_amount') == 300 assert not order_line_5.is_cancelled assert order_line_5.quantity == 2 assert order_line_5.received_quantity == 0 assert order_line_5.unreceived_quantity == 2 assert order_line_5.status == AcqOrderLineStatus.APPROVED assert order_line_4.get('total_amount') == 45 assert not order_line_4.is_cancelled assert order_line_4.quantity == 3 assert order_line_4.received_quantity == 0 assert order_line_4.unreceived_quantity == 3 assert order_line_4.status == AcqOrderLineStatus.APPROVED assert order_line_3.get('total_amount') == 300 assert order_line_3.is_cancelled assert order_line_3.quantity == 3 assert order_line_3.received_quantity == 0 assert order_line_3.unreceived_quantity == 3 assert order_line_3.status == AcqOrderLineStatus.CANCELLED assert order_line_2.get('total_amount') == 300 assert not order_line_2.is_cancelled assert order_line_2.quantity == 6 assert order_line_2.received_quantity == 0 assert order_line_2.unreceived_quantity == 6 assert order_line_2.status == AcqOrderLineStatus.APPROVED assert order_line_1.get('total_amount') == 50 assert not order_line_1.is_cancelled assert order_line_1.quantity == 5 assert order_line_1.received_quantity == 0 assert order_line_1.unreceived_quantity == 5 assert order_line_1.status == AcqOrderLineStatus.APPROVED # STEP 4 :: SEND THE ORDER # * Test send order and make sure statuses are up to date. # - check order lines (status, order-date) # - check order (status) # - check notification address = vendor_martigny.get('default_contact').get('email') emails = [ {'type': 'to', 'address': address}, {'type': 'reply_to', 'address': lib_martigny.get('email')} ] res, data = postdata( client, 'api_order.send_order', data=dict(emails=emails), url_data=dict(order_pid=order.pid) ) assert res.status_code == 200 for order_line in [ {'line': order_line_1, 'status': AcqOrderLineStatus.ORDERED}, {'line': order_line_2, 'status': AcqOrderLineStatus.ORDERED}, {'line': order_line_3, 'status': AcqOrderLineStatus.CANCELLED}, {'line': order_line_4, 'status': AcqOrderLineStatus.ORDERED}, {'line': order_line_5, 'status': AcqOrderLineStatus.ORDERED}, {'line': order_line_6, 'status': AcqOrderLineStatus.CANCELLED}, ]: line = AcqOrderLine.get_record_by_pid(order_line.get('line').pid) assert line.status == order_line.get('status') if line.status == AcqOrderLineStatus.CANCELLED: assert not line.order_date else: assert line.order_date # check order order = AcqOrder.get_record_by_pid(order.pid) assert order.status == AcqOrderStatus.ORDERED # notification testing notification_pid = data.get('data').get('pid') notif = Notification.get_record_by_pid(notification_pid) assert notif.organisation_pid == order.organisation_pid assert notif.aggregation_key == str(notif.id) assert notif.type == NotificationType.ACQUISITION_ORDER assert notif.status == NotificationStatus.DONE assert notif.acq_order_pid == order.pid assert notif.library_pid == order.library_pid assert notif.can_be_cancelled() == (False, None) assert notif.get_communication_channel() == NotificationChannel.EMAIL assert notif.get_language_to_use() == \ vendor_martigny.get('communication_language') assert address in notif.get_recipients(RecipientType.TO) # STEP 5 :: CREATE A RECEIPT # * create a receipt without any order lines yet # * but with some adjustments ref_acc_book = get_ref_for_pid('acac', m_books_acc.pid) ref_acc_serial = get_ref_for_pid('acac', m_serials_acc.pid) data = { 'acq_order': {'$ref': get_ref_for_pid('acor', order.pid)}, 'exchange_rate': 1, 'amount_adjustments': [ { 'label': 'handling fees', 'amount': 2.0, 'acq_account': {'$ref': ref_acc_book} }, { 'label': 'discount', 'amount': -1.0, 'acq_account': {'$ref': ref_acc_book} }, { 'label': 'handling fees', 'amount': 10, 'acq_account': {'$ref': ref_acc_serial} } ], 'library': {'$ref': get_ref_for_pid('lib', lib_martigny.pid)}, 'organisation': {'$ref': get_ref_for_pid('org', org_martigny.pid)} } receipt_1 = _make_resource(client, 'acre', data) assert receipt_1.total_amount == 11 # 2 - 1 + 10 assert receipt_1.can_delete manual_controls = { m_root_acc: ((5000, 9294), (0, 11), (0, 695)), m_books_acc: ((1649, 1649), (1, 0), (350, 0)), m_serials_acc: ((2645, 2645), (10, 0), (345, 0)), s_root_acc: ((13500, 20000), (0, 0), (0, 0)), s_books_acc: ((2500, 2500), (0, 0), (0, 0)), s_serials_acc: ((4000, 4000), (0, 0), (0, 0)) } assert_account_data(manual_controls) # STEP 6 :: RECEIVE SOME ORDER LINES # Received 2 items from the order_line_1. We need to ensure : # * the order_line status is PARTIALLY_RECEIVED # * the order status is PARTIALLY_RECEIVED too # * the related account amounts are correctly assigned # CHECK :: Not possible to receive quantity more than what you ordered res, data = postdata( client, 'api_receipt.lines', data=[{ 'acq_order_line': {'$ref': order_line_1_ref}, 'amount': 10, 'quantity': 12, 'receipt_date': '2021-11-01' }], url_data=dict(receipt_pid=receipt_1.pid) ) assert res.status_code == 200 response = data.get('response') assert response[0]['status'] == AcqReceiptLineCreationStatus.FAILURE # partially receive one order with few quantities in receipt_1 res, data = postdata( client, 'api_receipt.lines', data=[{ 'acq_order_line': {'$ref': order_line_1_ref}, 'amount': 10, 'quantity': 2, 'vat_rate': 6, 'receipt_date': '2021-11-01' }], url_data=dict(receipt_pid=receipt_1.pid) ) assert res.status_code == 200 response = data.get('response') assert response[0]['status'] == AcqReceiptLineCreationStatus.SUCCESS # Test order and order lines for order_line in [{ 'line': order_line_1, 'status': AcqOrderLineStatus.PARTIALLY_RECEIVED, 'received': 2 }, { 'line': order_line_2, 'status': AcqOrderLineStatus.ORDERED, 'received': 0 }, { 'line': order_line_3, 'status': AcqOrderLineStatus.CANCELLED, 'received': 0 }, { 'line': order_line_4, 'status': AcqOrderLineStatus.ORDERED, 'received': 0 }, { 'line': order_line_5, 'status': AcqOrderLineStatus.ORDERED, 'received': 0 }, { 'line': order_line_6, 'status': AcqOrderLineStatus.CANCELLED, 'received': 0 }]: line = AcqOrderLine.get_record_by_pid(order_line.get('line').pid) assert line.status == order_line.get('status') assert line.received_quantity == order_line.get('received') order = AcqOrder.get_record_by_pid(order.pid) assert order.status == AcqOrderStatus.PARTIALLY_RECEIVED manual_controls = { m_root_acc: ((5000, 9292.8), (0, 32.2), (0, 675)), m_books_acc: ((1647.8, 1647.8), (22.2, 0), (330, 0)), m_serials_acc: ((2645, 2645), (10, 0), (345, 0)), s_root_acc: ((13500, 20000), (0, 0), (0, 0)), s_books_acc: ((2500, 2500), (0, 0), (0, 0)), s_serials_acc: ((4000, 4000), (0, 0), (0, 0)) } assert_account_data(manual_controls) # STEP 7 :: CREATE NEW RECEIVE AND RECEIVE ALL PENDING ORDER LINES # * Create a second receipt for the same order with no adjustments. # * To to receive all pending order lines BUT with a mistake for # `order_line_5` (try to receive more than ordered items) ==> all lines # except `order_line_5` should have the RECEIVED STATUS # * complete the order reception to receive the `order_line_5` data = { 'exchange_rate': 1, 'acq_order': {'$ref': get_ref_for_pid('acor', order.pid)}, 'library': {'$ref': get_ref_for_pid('lib', lib_martigny.pid)}, 'organisation': {'$ref': get_ref_for_pid('org', org_martigny.pid)} } receipt_2 = _make_resource(client, 'acre', data) data = [{ 'acq_order_line': {'$ref': order_line_1_ref}, 'amount': 10, 'quantity': 3, 'receipt_date': '2021-11-01' }, { 'acq_order_line': {'$ref': order_line_2_ref}, 'amount': 50, 'quantity': 6, 'receipt_date': '2021-11-01' }, { 'acq_order_line': {'$ref': order_line_4_ref}, 'amount': 15, 'quantity': 3, 'receipt_date': '2021-11-01' }, { 'acq_order_line': {'$ref': order_line_5_ref}, 'amount': 150, 'quantity': 12, # too many items ! Max quantity should be 2 'receipt_date': '2021-11-01' }] res, data = postdata( client, 'api_receipt.lines', data=data, url_data=dict(receipt_pid=receipt_2.pid) ) assert res.status_code == 200 response = data.get('response') assert response[0]['status'] == AcqReceiptLineCreationStatus.SUCCESS assert response[1]['status'] == AcqReceiptLineCreationStatus.SUCCESS assert response[2]['status'] == AcqReceiptLineCreationStatus.SUCCESS assert response[3]['status'] == AcqReceiptLineCreationStatus.FAILURE # Test order and order lines for order_line in [ {'line': order_line_1, 'status': AcqOrderLineStatus.RECEIVED}, {'line': order_line_2, 'status': AcqOrderLineStatus.RECEIVED}, {'line': order_line_3, 'status': AcqOrderLineStatus.CANCELLED}, {'line': order_line_4, 'status': AcqOrderLineStatus.RECEIVED}, {'line': order_line_5, 'status': AcqOrderLineStatus.ORDERED}, {'line': order_line_6, 'status': AcqOrderLineStatus.CANCELLED} ]: line = AcqOrderLine.get_record_by_pid(order_line.get('line').pid) assert line.status == order_line.get('status') # Receive the last pending order_line data = [{ 'acq_order_line': {'$ref': order_line_5_ref}, 'amount': 150, 'quantity': 2, 'receipt_date': '2021-11-01' }] res, data = postdata( client, 'api_receipt.lines', data=data, url_data=dict(receipt_pid=receipt_2.pid) ) assert res.status_code == 200 response = data.get('response') assert response[0]['status'] == AcqReceiptLineCreationStatus.SUCCESS order_line = AcqOrderLine.get_record_by_pid(order_line_5.pid) assert order_line.status == AcqOrderLineStatus.RECEIVED # check than order is now fully received order = AcqOrder.get_record_by_pid(order.pid) assert order.status == AcqOrderStatus.RECEIVED # check account amounts manual_controls = { m_root_acc: ((5000, 9292.8), (0, 707.2), (0, 0)), m_books_acc: ((1647.8, 1647.8), (352.2, 0), (0, 0)), m_serials_acc: ((2645, 2645), (355, 0), (0, 0)), s_root_acc: ((13500, 20000), (0, 0), (0, 0)), s_books_acc: ((2500, 2500), (0, 0), (0, 0)), s_serials_acc: ((4000, 4000), (0, 0), (0, 0)) } assert_account_data(manual_controls) # TEST 8: DELETE RECEIPTS # * Delete the second receipt. This will also delete the related receipt # lines. The order status must remain to PARTIALLY_RECEIVED. # * Delete the first receipt. The order status should remain to ORDERED # and account amount should be the same than STEP#3. # Check links between object # * check some order lines (cancelled or not); not need to test all lines # * check receipt links links = order.get_links_to_me(get_pids=True) for pid in [order_line_3.pid, order_line_4.pid, order_line_5.pid]: assert pid in links['order_lines'] for pid in [receipt_1.pid, receipt_2.pid]: assert pid in links['receipts'] # DELETE `RECEIPT_2` ---------- receipt_line_pids = receipt_2.get_receipt_lines(output='pids') url = url_for('invenio_records_rest.acre_item', pid_value=receipt_2.pid) client.delete(url) # Check all resources related to `receipt_2` is deleted for pid in receipt_line_pids: line = AcqReceiptLine.get_record_by_pid(pid) assert line is None assert AcqReceipt.get_record_by_pid(receipt_2.pid) is None # Check ES is up-to-date response = AcqReceiptLinesSearch()\ .filter('terms', pid=receipt_line_pids).execute() assert response.hits.total.value == 0 response = AcqReceiptsSearch() \ .filter('term', pid=receipt_2.pid).execute() assert response.hits.total.value == 0 # Check order status order = AcqOrder.get_record_by_pid(order.pid) assert order.status == AcqOrderStatus.PARTIALLY_RECEIVED # DELETE `RECEIPT_1` ---------- receipt_line_pids = receipt_1.get_receipt_lines(output='pids') url = url_for('invenio_records_rest.acre_item', pid_value=receipt_1.pid) client.delete(url) # Check all resources related to `receipt_1` is deleted for pid in receipt_line_pids: line = AcqReceiptLine.get_record_by_pid(pid) assert line is None assert AcqReceipt.get_record_by_pid(receipt_1.pid) is None # Check ES is up-to-date response = AcqReceiptLinesSearch() \ .filter('terms', pid=receipt_line_pids).execute() assert response.hits.total.value == 0 response = AcqReceiptsSearch() \ .filter('term', pid=receipt_1.pid).execute() assert response.hits.total.value == 0 # Check order status order = AcqOrder.get_record_by_pid(order.pid) assert order.status == AcqOrderStatus.ORDERED # ensure correct calculations and status again manual_controls = { m_root_acc: ((5000, 9305), (0, 0), (0, 695)), m_books_acc: ((1650, 1650), (0, 0), (350, 0)), m_serials_acc: ((2655, 2655), (0, 0), (345, 0)), s_root_acc: ((13500, 20000), (0, 0), (0, 0)), s_books_acc: ((2500, 2500), (0, 0), (0, 0)), s_serials_acc: ((4000, 4000), (0, 0), (0, 0)) } assert_account_data(manual_controls)