def test_notes(app, acq_order_line_schema, acq_order_line_fiction_martigny_data_tmp): """Test notes acq order lines jsonschemas.""" order_line_data = acq_order_line_fiction_martigny_data_tmp order_line_data['notes'] = [ { 'type': AcqOrderLineNoteType.STAFF, 'content': 'note content' }, { 'type': AcqOrderLineNoteType.VENDOR, 'content': 'note content 2' }, ] validate(order_line_data, acq_order_line_schema) with pytest.raises(ValidationError): order_line_data['notes'] = [ { 'type': AcqOrderLineNoteType.STAFF, 'content': 'note content' }, { 'type': AcqOrderLineNoteType.STAFF, 'content': 'note content 2' }, ] AcqOrderLine.validate(AcqOrderLine(order_line_data))
def test_acq_order_lines_es_mapping(es_clear, db, document, acq_account_fiction_martigny, acq_order_fiction_martigny, acq_order_line_fiction_martigny_data): """Test aquisition order line elasticsearch mapping.""" search = AcqOrderLinesSearch() mapping = get_mapping(search.Meta.index) assert mapping AcqOrderLine.create(acq_order_line_fiction_martigny_data, dbcommit=True, reindex=True, delete_pid=True) assert mapping == get_mapping(search.Meta.index)
def test_order_line_validation_extension(acq_order_line_fiction_martigny_data, acq_account_fiction_martigny, ebook_1): """Test order line validation extension.""" data = deepcopy(acq_order_line_fiction_martigny_data) del data['pid'] # An order line cannot be linked to an harvested document ebook_ref = get_ref_for_pid('doc', ebook_1.pid) test_data = deepcopy(data) test_data['document']['$ref'] = ebook_ref with pytest.raises(ValidationError) as error: AcqOrderLine.create(test_data, delete_pid=True) assert 'Cannot link to an harvested document' in str(error.value)
def acq_order_line_fiction_sion(app, acq_account_fiction_sion, acq_order_fiction_sion, acq_order_line_fiction_sion_data): """Load acq_order_line lib sion fiction record.""" acol = AcqOrderLine.create(data=acq_order_line_fiction_sion_data, delete_pid=False, dbcommit=True, reindex=True) flush_index(AcqOrderLinesSearch.Meta.index) return acol
def acq_order_line3_fiction_martigny(app, acq_account_fiction_martigny, document, acq_order_fiction_martigny, acq_order_line3_fiction_martigny_data): """Load acq_order_line lib martigny fiction record.""" acol = AcqOrderLine.create(data=acq_order_line3_fiction_martigny_data, delete_pid=False, dbcommit=True, reindex=True) flush_index(AcqOrderLinesSearch.Meta.index) return acol
def enrich_acq_order_line_data(sender, json=None, record=None, index=None, doc_type=None, arguments=None, **dummy_kwargs): """Signal sent before a record is indexed. :param json: The dumped record dictionary which can be modified. :param record: The record being indexed. :param index: The index in which the record will be indexed. :param doc_type: The doc_type for the record. """ if index.split('-')[0] == AcqOrderLinesSearch.Meta.index: if not isinstance(record, AcqOrderLine): record = AcqOrderLine.get_record_by_pid(record.get('pid')) unreceived_quantity = record.unreceived_quantity # other dynamic keys json['total_unreceived_amount'] = \ unreceived_quantity * record['amount'] json['status'] = record.status json['received_quantity'] = record.received_quantity
def test_acquisition_order(client, rero_json_header, org_martigny, lib_martigny, budget_2020_martigny, vendor_martigny, librarian_martigny, document): """Scenario to test orders creation.""" login_user_via_session(client, librarian_martigny.user) # STEP 0 :: Create the account tree basic_data = { 'allocated_amount': 1000, 'budget': { '$ref': get_ref_for_pid('budg', budget_2020_martigny.pid) }, 'library': { '$ref': get_ref_for_pid('lib', lib_martigny.pid) } } account_a = dict(name='A', allocated_amount=2000) account_a = {**basic_data, **account_a} account_a = _make_resource(client, 'acac', account_a) account_a_ref = {'$ref': get_ref_for_pid('acac', account_a.pid)} account_b = dict(name='B', allocated_amount=500, parent=account_a_ref) account_b = {**basic_data, **account_b} account_b = _make_resource(client, 'acac', account_b) account_b_ref = {'$ref': get_ref_for_pid('acac', account_b.pid)} # TEST 1 :: Create an order and add some order lines on it. # * The creation of the order will be successful # * We create first order line linked to account B. After this creation, # we can check the encumbrance of this account and its parent account. order_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', order_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 basic_data = { 'acq_account': account_b_ref, 'acq_order': { '$ref': get_ref_for_pid('acor', order.pid) }, 'document': { '$ref': get_ref_for_pid('doc', document.pid) }, 'quantity': 4, 'amount': 25 } order_line_1 = _make_resource(client, 'acol', basic_data) assert order_line_1.get('total_amount') == 100 assert account_b.encumbrance_amount[0] == 100 assert account_b.remaining_balance[0] == 400 # 500 - 100 assert account_a.encumbrance_amount == (0, 100) assert account_a.remaining_balance[0] == 1500 assert account_a.expenditure_amount == (0, 0) # TEST 2 :: update the number of received item from the order line. # * The encumbrance amount account should be decrease by quantity # received * amount. # field received_quantity is now dynamically calculated at the receive of # receipt_lines assert order_line_1.received_quantity == 0 # TEST 3 :: add a new cancelled order line. # * As this new order line has CANCELLED status, its amount is not # calculated into the encumbrance_amount basic_data = { 'acq_account': account_b_ref, 'acq_order': { '$ref': get_ref_for_pid('acor', order.pid) }, 'document': { '$ref': get_ref_for_pid('doc', document.pid) }, 'quantity': 2, 'amount': 10, 'is_cancelled': True } order_line_1_1 = _make_resource(client, 'acol', basic_data) assert order_line_1_1.get('total_amount') == 20 assert account_b.encumbrance_amount[0] == 100 assert account_b.remaining_balance[0] == 400 # 500 - 100 assert account_a.encumbrance_amount == (0, 100) assert account_a.remaining_balance[0] == 1500 assert account_a.expenditure_amount == (0, 0) # TEST 4 :: new order line raises the limit of account available money. # * Create a new order line on the same account ; but the total amount # of the line must be larger than account available money --> should # be raise an ValidationError # * Update the first order line to raise the limit and check than the # same validation error occurs. # * Update the first order line to reach the limit without exceeding it order_line_2 = dict(quantity=50) order_line_2 = {**basic_data, **order_line_2} with pytest.raises(Exception) as excinfo: _make_resource(client, 'acol', order_line_2) assert 'Parent account available amount too low' in str(excinfo.value) order_line_1['quantity'] = 50 with pytest.raises(Exception) as excinfo: order_line_1.update(order_line_1, dbcommit=True, reindex=True) assert 'Parent account available amount too low' in str(excinfo.value) order_line_1['quantity'] = 20 order_line_1 = order_line_1.update(order_line_1, dbcommit=True, reindex=True) assert account_b.encumbrance_amount[0] == 500 assert account_b.remaining_balance[0] == 0 assert account_a.encumbrance_amount == (0, 500) assert account_a.remaining_balance[0] == 1500 # TEST 5 :: Update the account encumbrance exceedance and test it. # * At this time, the account B doesn't have any available money to # place any nex order line. Try to add an other item to existing order # line will raise a ValidationError # * Update the account 'encumbrance_exceedance' setting to allow more # encumbrance and try to add an item to order_line. It will be OK order_line_1['quantity'] += 1 with pytest.raises(Exception) as excinfo: order_line_1.update(order_line_1, dbcommit=True, reindex=True) assert 'Parent account available amount too low' in str(excinfo.value) account_b['encumbrance_exceedance'] = 5 # 5% of 500 = 25 account_b = account_b.update(account_b, dbcommit=True, reindex=True) order_line_1 = order_line_1.update(order_line_1, dbcommit=True, reindex=True) assert account_b.encumbrance_amount[0] == 525 assert account_b.remaining_balance[0] == -25 assert account_a.encumbrance_amount == (0, 525) assert account_a.remaining_balance[0] == 1500 # Test cascade deleting of order lines when attempting to delete a # PENDING order. order_line_1 = AcqOrderLine.get_record_by_pid(order_line_1.pid) order_line_1['is_cancelled'] = True order_line_1.update(order_line_1, dbcommit=True, reindex=True) order = AcqOrder.get_record_by_pid(order.pid) assert order.status == AcqOrderStatus.CANCELLED # Delete CANCELLED order is not permitted with pytest.raises(IlsRecordError.NotDeleted): _del_resource(client, 'acor', order.pid) order_line_1['is_cancelled'] = False order_line_1.update(order_line_1, dbcommit=True, reindex=True) order = AcqOrder.get_record_by_pid(order.pid) assert order.status == AcqOrderStatus.PENDING # DELETE created resources _del_resource(client, 'acor', order.pid) # Deleting the parent PENDING order does delete all of its order lines order_line_1 = AcqOrderLine.get_record_by_pid(order_line_1.pid) order_line_1_1 = AcqOrderLine.get_record_by_pid(order_line_1_1.pid) assert not order_line_1 assert not order_line_1_1 _del_resource(client, 'acac', account_b.pid) _del_resource(client, 'acac', account_a.pid)
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)