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_create_accounts(client, rero_json_header, org_martigny, lib_martigny, budget_2020_martigny): """Basic scenario to test account creation.""" # STEP 1 :: Create a root account root_account_data = { 'name': 'Root account', 'number': '000.0000.00', 'allocated_amount': 1000, 'budget': { '$ref': get_ref_for_pid('budg', budget_2020_martigny.pid) }, 'library': { '$ref': get_ref_for_pid('lib', lib_martigny.pid) } } root_account = _make_resource(client, 'acac', root_account_data) # STEP 2 :: Create a child account # * Try to create a child account with too much amount regarding root # account. It should be failed with a ValidationError due to # `pre_create` extension. # * Create a child account with 70% of the available amount of root # account child_account_data = { 'name': 'Chid account', 'number': '000.0001.00', 'allocated_amount': root_account['allocated_amount'] + 1, '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', root_account.pid) }, } with pytest.raises(Exception) as excinfo: _make_resource(client, 'acac', child_account_data) assert 'Parent account available amount too low.' in str(excinfo.value) amount_70 = round(root_account['allocated_amount'] * 0.7) amount_30 = root_account['allocated_amount'] - amount_70 child_account_data['allocated_amount'] = amount_70 child_account = _make_resource(client, 'acac', child_account_data) # STEP 3 :: Check accounts distribution # * Parent account should have 30% as available amount # * Parent account distribution should be 70% assert root_account.remaining_balance[0] == amount_30 assert root_account.distribution == amount_70 # STEP 4 :: Decrease the allocated amount of parent account too much root_account_data['allocated_amount'] = amount_30 + 1 with pytest.raises(Exception) as excinfo: root_account.update(root_account_data, dbcommit=True) assert 'Remaining balance too low' in str(excinfo.value) # RESET DATA _del_resource(client, 'acac', child_account.pid) _del_resource(client, 'acac', root_account.pid)
def test_acquisition_orders_serializers(client, librarian_martigny, budget_2020_martigny, lib_martigny, vendor_martigny, document, rero_json_header): """Test orders serializer.""" login_user_via_session(client, librarian_martigny.user) # STEP 0 :: Create the account with multiple order lines account_data = { 'name': 'Account A', '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 = _make_resource(client, 'acac', account_data) account_a_ref = {'$ref': get_ref_for_pid('acac', account_a.pid)} order_data = { 'vendor': { '$ref': get_ref_for_pid('vndr', vendor_martigny.pid) }, 'library': { '$ref': get_ref_for_pid('lib', lib_martigny.pid) }, 'reference': 'ORDER#1', 'type': 'monograph', } order = _make_resource(client, 'acor', order_data) order.reindex() line_data = { 'acq_account': account_a_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', line_data) # TEST ORDER SERIALIZER list_url = url_for('invenio_records_rest.acor_list') response = client.get(list_url, headers=rero_json_header) assert response.status_code == 200 data = get_json(response) record = data.get('hits', {}).get('hits', [])[0] assert record.get('metadata', {})\ .get('order_lines', [])[0] \ .get('account', {})\ .get('name') == account_a['name'] assert record.get('metadata', {}) \ .get('order_lines', [])[0] \ .get('document', {}) \ .get('pid') == document.pid # RESET RESOURCES _del_resource(client, 'acol', order_line_1.pid) _del_resource(client, 'acor', order.pid) _del_resource(client, 'acac', account_a.pid)
def test_transfer_funds_api(client, rero_json_header, org_martigny, lib_martigny, budget_2020_martigny, librarian_martigny): """Scenario to test fund transfer between both accounts.""" def _check_account(account): """Check amount available about an account.""" return account['allocated_amount'], account.remaining_balance[0] login_user_via_session(client, librarian_martigny.user) # STEP 0 :: Create account tree # Test structure account is described below. Each account are noted like # A{x, y} where : # * 'A' is the account name # * 'x' is the account allocated amount # * 'y' is the account remaining_balance # # A{2000, 500} E{200, 100} # |-- B{500, 150} +-- F{200, 100} # | |-- B1{300, 300} +-- G{100, 100} # | +-- B2{50, 50} # +-- C{1000, 700} # |-- C1{100, 100} # |-- C2{100, 30} # | |-- C21{50, 50} # | +-- C22{20, 20} # +-- C3{100, 100} 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) a_ref = {'$ref': get_ref_for_pid('acac', account_a.pid)} account_b = dict(name='B', allocated_amount=500, parent=a_ref) account_b = {**basic_data, **account_b} account_b = _make_resource(client, 'acac', account_b) b_ref = {'$ref': get_ref_for_pid('acac', account_b.pid)} account_c = dict(name='C', allocated_amount=1000, parent=a_ref) account_c = {**basic_data, **account_c} account_c = _make_resource(client, 'acac', account_c) c_ref = {'$ref': get_ref_for_pid('acac', account_c.pid)} account_b1 = dict(name='B1', allocated_amount=300, parent=b_ref) account_b1 = {**basic_data, **account_b1} account_b1 = _make_resource(client, 'acac', account_b1) account_b2 = dict(name='B2', allocated_amount=50, parent=b_ref) account_b2 = {**basic_data, **account_b2} account_b2 = _make_resource(client, 'acac', account_b2) account_c1 = dict(name='C1', allocated_amount=100, parent=c_ref) account_c1 = {**basic_data, **account_c1} account_c1 = _make_resource(client, 'acac', account_c1) account_c2 = dict(name='C2', allocated_amount=100, parent=c_ref) account_c2 = {**basic_data, **account_c2} account_c2 = _make_resource(client, 'acac', account_c2) account_c3 = dict(name='C3', allocated_amount=100, parent=c_ref) account_c3 = {**basic_data, **account_c3} account_c3 = _make_resource(client, 'acac', account_c3) c2_ref = {'$ref': get_ref_for_pid('acac', account_c2.pid)} account_c21 = dict(name='C21', allocated_amount=50, parent=c2_ref) account_c21 = {**basic_data, **account_c21} account_c21 = _make_resource(client, 'acac', account_c21) account_c22 = dict(name='C22', allocated_amount=20, parent=c2_ref) account_c22 = {**basic_data, **account_c22} account_c22 = _make_resource(client, 'acac', account_c22) account_e = dict(name='E', allocated_amount=300) account_e = {**basic_data, **account_e} account_e = _make_resource(client, 'acac', account_e) e_ref = {'$ref': get_ref_for_pid('acac', account_e.pid)} account_f = dict(name='F', allocated_amount=200, parent=e_ref) account_f = {**basic_data, **account_f} account_f = _make_resource(client, 'acac', account_f) f_ref = {'$ref': get_ref_for_pid('acac', account_f.pid)} account_g = dict(name='G', allocated_amount=100, parent=f_ref) account_g = {**basic_data, **account_g} account_g = _make_resource(client, 'acac', account_g) # TEST 0 :: Try the API with invalid arguments. res = client.get(url_for('api_acq_account.transfer_funds')) assert res.status_code == 400 assert 'argument is required' in res.get_data(as_text=True) cases_to_test = [{ 'source': 'dummy', 'target': 'dummy', 'amount': 'dummy', 'error': 'Unable to load source account' }, { 'source': account_a.pid, 'target': 'dummy', 'amount': 'dummy', 'error': 'Unable to load target account' }, { 'source': account_a.pid, 'target': account_b.pid, 'amount': 'dummy', 'error': "could not convert" }, { 'source': account_a.pid, 'target': account_b.pid, 'amount': -1.52, 'error': "'amount' should be a positive number" }, { 'source': account_a.pid, 'target': account_a.pid, 'amount': 1, 'error': "Cannot transfer fund to myself" }, { 'source': account_a.pid, 'target': account_e.pid, 'amount': 100000, 'error': "Not enough available money from source account" }] for case in cases_to_test: res = client.get( url_for('api_acq_account.transfer_funds', source=case['source'], target=case['target'], amount=case['amount'])) assert res.status_code == 400 data = get_json(res) assert case['error'] in data['message'] # STATUS BEFORE NEXT TEST # A{2000, 500} E{300, 100} # |-- B{500, 150} +-- F{200, 100} # | |-- B1{300, 300} +-- G{100, 100} # | +-- B2{50, 50} # +-- C{1000, 700} # |-- C1{100, 100} # |-- C2{100, 30} # | |-- C21{50, 50} # | +-- C22{20, 20} # +-- C3{100, 100} # TEST 1 :: Transfer to an ancestor account # Transfer 25 from C21 account to C account. After this transfer, the # C20 remaining balance should be equal to 25 ; the remaining balance for # C account should be 725 res = client.get( url_for('api_acq_account.transfer_funds', source=account_c21.pid, target=account_c.pid, amount=25)) assert res.status_code == 200 account_c21 = AcqAccount.get_record_by_pid(account_c21.pid) account_c2 = AcqAccount.get_record_by_pid(account_c2.pid) account_c = AcqAccount.get_record_by_pid(account_c.pid) assert _check_account(account_c) == (1000, 725) assert _check_account(account_c2) == (75, 30) assert _check_account(account_c21) == (25, 25) # STATUS BEFORE NEXT TEST # A{2000, 500} E{300, 100} # |-- B{500, 150} +-- F{200, 100} # | |-- B1{300, 300} +-- G{100, 100} # | +-- B2{50, 50} # +-- C{1000, 725} # |-- C1{100, 100} # |-- C2{75, 30} # | |-- C21{25, 25} # | +-- C22{20, 20} # +-- C3{100, 100} # TEST 2 :: Transfer between accounts in the same tree # Transfer 100 from A account to C22 account. After this transfer, the # C22 remaining balance should be equal to 120 ; the remaining balance # for A account should be 400. The remaining balance for intermediate # accounts (C, C2) should be the same, but allocated amount should be # increased by 100 (1100, 175) res = client.get( url_for('api_acq_account.transfer_funds', source=account_a.pid, target=account_c22.pid, amount=100)) assert res.status_code == 200 account_a = AcqAccount.get_record_by_pid(account_a.pid) account_c = AcqAccount.get_record_by_pid(account_c.pid) account_c2 = AcqAccount.get_record_by_pid(account_c2.pid) account_c22 = AcqAccount.get_record_by_pid(account_c22.pid) assert _check_account(account_a) == (2000, 400) assert _check_account(account_c) == (1100, 725) assert _check_account(account_c2) == (175, 30) assert _check_account(account_c22) == (120, 120) # STATUS BEFORE NEXT TEST # A{2000, 400} E{300, 100} # |-- B{500, 150} +-- F{200, 100} # | |-- B1{300, 300} +-- G{100, 100} # | +-- B2{50, 50} # +-- C{1100, 725} # |-- C1{100, 100} # |-- C2{175, 30} # | |-- C21{25, 25} # | +-- C22{120, 120} # +-- C3{100, 100} # TEST 3 :: Transfer 300 from B1 account to C21 account. # Same behavior than previous test, but source account isn't the common # ancestor. res = client.get( url_for('api_acq_account.transfer_funds', source=account_b1.pid, target=account_c21.pid, amount=300)) assert res.status_code == 200 account_b1 = AcqAccount.get_record_by_pid(account_b1.pid) account_b = AcqAccount.get_record_by_pid(account_b.pid) account_a = AcqAccount.get_record_by_pid(account_a.pid) account_c = AcqAccount.get_record_by_pid(account_c.pid) account_c2 = AcqAccount.get_record_by_pid(account_c2.pid) account_c21 = AcqAccount.get_record_by_pid(account_c21.pid) assert _check_account(account_b1) == (0, 0) assert _check_account(account_b) == (200, 150) assert _check_account(account_a) == (2000, 400) assert _check_account(account_c) == (1400, 725) assert _check_account(account_c2) == (475, 30) assert _check_account(account_c21) == (325, 325) # STATUS BEFORE NEXT TEST # A{2000, 400} E{300, 100} # |-- B{200, 150} +-- F{200, 100} # | |-- B1{0, 0} +-- G{100, 100} # | +-- B2{50, 50} # +-- C{1400, 725} # |-- C1{100, 100} # |-- C2{475, 30} # | |-- C21{325, 325} # | +-- C22{120, 120} # +-- C3{100, 100} # TEST 4 :: Transfer between two account from separate tree. # We transfer 100 from F account to C3 account. As both accounts aren't # in the same tree, they not exists a common ancestor. Each root tag # should be update (E will decrease, A will increase) res = client.get( url_for('api_acq_account.transfer_funds', source=account_f.pid, target=account_c3.pid, amount=100)) assert res.status_code == 200 account_f = AcqAccount.get_record_by_pid(account_f.pid) account_e = AcqAccount.get_record_by_pid(account_e.pid) account_a = AcqAccount.get_record_by_pid(account_a.pid) account_c = AcqAccount.get_record_by_pid(account_c.pid) account_c3 = AcqAccount.get_record_by_pid(account_c3.pid) assert _check_account(account_f) == (100, 0) assert _check_account(account_e) == (200, 100) assert _check_account(account_a) == (2100, 400) assert _check_account(account_c) == (1500, 725) assert _check_account(account_c3) == (200, 200) # STATUS BEFORE NEXT TEST # A{2100, 400} E{200, 100} # |-- B{200, 150} +-- F{100, 0} # | |-- B1{0, 0} +-- G{100, 100} # | +-- B2{50, 50} # +-- C{1500, 725} # |-- C1{100, 100} # |-- C2{475, 30} # | |-- C21{325, 325} # | +-- C22{120, 120} # +-- C3{200, 200} # delete accounts _del_resource(client, 'acac', account_g.pid) _del_resource(client, 'acac', account_f.pid) _del_resource(client, 'acac', account_e.pid) _del_resource(client, 'acac', account_c22.pid) _del_resource(client, 'acac', account_c21.pid) _del_resource(client, 'acac', account_c3.pid) _del_resource(client, 'acac', account_c2.pid) _del_resource(client, 'acac', account_c1.pid) _del_resource(client, 'acac', account_c.pid) _del_resource(client, 'acac', account_b2.pid) _del_resource(client, 'acac', account_b1.pid) _del_resource(client, 'acac', account_b.pid) _del_resource(client, 'acac', account_a.pid)
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)