diff --git a/cypress/e2e/finance/funds/expense-class-inactive-totals-displayed-correctly.cy.js b/cypress/e2e/finance/funds/expense-class-inactive-totals-displayed-correctly.cy.js new file mode 100644 index 0000000000..bd8a1dc007 --- /dev/null +++ b/cypress/e2e/finance/funds/expense-class-inactive-totals-displayed-correctly.cy.js @@ -0,0 +1,298 @@ +import { Permissions } from '../../../support/dictionary'; +import { + APPLICATION_NAMES, + EXPENSE_CLASS_STATUSES, + FUND_DISTRIBUTION_TYPES, + INVOICE_STATUSES, + ORDER_STATUSES, +} from '../../../support/constants'; +import { ExpenseClasses } from '../../../support/fragments/settings/finance'; +import { + Budgets, + FundDetails, + FinanceHelper, + Funds, + GroupDetails, + Groups, +} from '../../../support/fragments/finance'; +import FinanceDetails from '../../../support/fragments/finance/financeDetails'; +import { NewOrganization, Organizations } from '../../../support/fragments/organizations'; +import { BasicOrderLine, NewOrder, Orders } from '../../../support/fragments/orders'; +import { Invoices } from '../../../support/fragments/invoices'; +import TopMenu from '../../../support/fragments/topMenu'; +import TopMenuNavigation from '../../../support/fragments/topMenuNavigation'; +import Users from '../../../support/fragments/users/users'; + +describe('Finance', () => { + describe('Funds', () => { + const testData = { + expenseClass: ExpenseClasses.getDefaultExpenseClass(), + organization: NewOrganization.getDefaultOrganization(), + user: {}, + }; + + before('Create test data', () => { + cy.getAdminToken().then(() => { + ExpenseClasses.createExpenseClassViaApi(testData.expenseClass); + + const group = Groups.getDefaultGroup(); + Groups.createViaApi(group).then((groupResponse) => { + testData.group = groupResponse; + + const { fiscalYear, fund, budget } = Budgets.createBudgetWithFundLedgerAndFYViaApi({ + budget: { + allocated: 1000, + statusExpenseClasses: [ + { + status: EXPENSE_CLASS_STATUSES.ACTIVE, + expenseClassId: testData.expenseClass.id, + }, + ], + }, + }); + + testData.fiscalYear = fiscalYear; + testData.fund = fund; + testData.budget = budget; + + Funds.getFundsViaApi({ query: `id=="${fund.id}"` }).then(({ funds }) => { + Funds.updateFundViaApi(funds[0], [group.id]); + }); + + Organizations.createOrganizationViaApi(testData.organization); + + // Order #1 with expense class, linked to Fund A + const order1 = { + ...NewOrder.getDefaultOrder({ vendorId: testData.organization.id }), + reEncumber: true, + }; + const orderLine1 = BasicOrderLine.getDefaultOrderLine({ + listUnitPrice: 10, + fundDistribution: [ + { + code: fund.code, + fundId: fund.id, + expenseClassId: testData.expenseClass.id, + distributionType: FUND_DISTRIBUTION_TYPES.PERCENTAGE, + value: 100, + }, + ], + }); + + Orders.createOrderWithOrderLineViaApi(order1, orderLine1).then((createdOrder1) => { + testData.order1 = createdOrder1; + testData.orderLine1 = orderLine1; + Orders.updateOrderViaApi({ ...createdOrder1, workflowStatus: ORDER_STATUSES.OPEN }); + }); + + // Invoice #1 linked to Order #1 + cy.then(() => { + Invoices.createInvoiceWithInvoiceLineViaApi({ + vendorId: testData.organization.id, + poLineId: testData.orderLine1.id, + exportToAccounting: false, + fundDistributions: [ + { + code: fund.code, + fundId: fund.id, + expenseClassId: testData.expenseClass.id, + distributionType: FUND_DISTRIBUTION_TYPES.PERCENTAGE, + value: 100, + }, + ], + subTotal: 10, + releaseEncumbrance: true, + }).then((invoice1) => { + testData.invoice1 = invoice1; + }); + }); + + // Approve and Pay Invoice #1 + cy.then(() => { + Invoices.changeInvoiceStatusViaApi({ + invoice: testData.invoice1, + status: INVOICE_STATUSES.APPROVED, + }); + }); + cy.then(() => { + Invoices.changeInvoiceStatusViaApi({ + invoice: testData.invoice1, + status: INVOICE_STATUSES.PAID, + }); + }); + + // Set expense class to Inactive on the budget + cy.then(() => { + Budgets.getBudgetByIdViaApi(testData.budget.id).then((budgetResp) => { + Budgets.updateBudgetViaApi({ + ...budgetResp, + statusExpenseClasses: budgetResp.statusExpenseClasses.map((ec) => ({ + ...ec, + status: EXPENSE_CLASS_STATUSES.INACTIVE, + })), + }); + }); + }); + + // Order #2 without expense class, linked to Fund A + const order2 = { + ...NewOrder.getDefaultOrder({ vendorId: testData.organization.id }), + reEncumber: true, + }; + const orderLine2 = BasicOrderLine.getDefaultOrderLine({ + listUnitPrice: 20, + fundDistribution: [ + { + code: fund.code, + fundId: fund.id, + distributionType: FUND_DISTRIBUTION_TYPES.PERCENTAGE, + value: 100, + }, + ], + }); + + cy.then(() => { + Orders.createOrderWithOrderLineViaApi(order2, orderLine2).then((createdOrder2) => { + testData.order2 = createdOrder2; + testData.orderLine2 = orderLine2; + Orders.updateOrderViaApi({ ...createdOrder2, workflowStatus: ORDER_STATUSES.OPEN }); + }); + }); + + // Invoice #2 linked to Order #2 + cy.then(() => { + Invoices.createInvoiceWithInvoiceLineViaApi({ + vendorId: testData.organization.id, + poLineId: testData.orderLine2.id, + exportToAccounting: false, + fundDistributions: [ + { + code: fund.code, + fundId: fund.id, + distributionType: FUND_DISTRIBUTION_TYPES.PERCENTAGE, + value: 100, + }, + ], + subTotal: 20, + releaseEncumbrance: true, + }).then((invoice2) => { + testData.invoice2 = invoice2; + }); + }); + + // Approve and Pay Invoice #2 + cy.then(() => { + Invoices.changeInvoiceStatusViaApi({ + invoice: testData.invoice2, + status: INVOICE_STATUSES.APPROVED, + }); + }); + cy.then(() => { + Invoices.changeInvoiceStatusViaApi({ + invoice: testData.invoice2, + status: INVOICE_STATUSES.PAID, + }); + }); + }); + }); + + cy.createTempUser([ + Permissions.uiFinanceViewFundAndBudget.gui, + Permissions.uiFinanceViewFiscalYear.gui, + Permissions.uiFinanceViewGroups.gui, + ]).then((userProperties) => { + testData.user = userProperties; + + cy.login(testData.user.username, testData.user.password, { + path: TopMenu.fundPath, + waiter: Funds.waitLoading, + }); + }); + }); + + after('Delete test data', () => { + cy.getAdminToken().then(() => { + Users.deleteViaApi(testData.user.userId); + Organizations.deleteOrganizationViaApi(testData.organization.id); + }); + }); + + it( + 'C805786 Totals for transactions created after expense class was set to inactive are displayed correctly in the expense class summary (thunderjet)', + { tags: ['criticalPath', 'thunderjet', 'C805786'] }, + () => { + Funds.searchByName(testData.fund.name); + Funds.selectFund(testData.fund.name); + FundDetails.checkFundDetails({ + currentExpenseClasses: [ + { + name: 'Unassigned', + encumbered: '$0.00', + awaitingPayment: '$0.00', + expended: '$20.00', + percentExpended: '66.67%', + }, + { + name: testData.expenseClass.name, + encumbered: '$0.00', + awaitingPayment: '$0.00', + expended: '$10.00', + percentExpended: '33.33%', + status: EXPENSE_CLASS_STATUSES.INACTIVE, + }, + ], + }); + FinanceDetails.checkUnassignedExpenseClassTooltip('currentExpenseClasses'); + + const BudgetDetails = FundDetails.openCurrentBudgetDetails(); + BudgetDetails.checkBudgetDetails({ + expenseClasses: [ + { + name: 'Unassigned', + encumbered: '$0.00', + awaitingPayment: '$0.00', + expended: '$20.00', + percentExpended: '66.67%', + }, + { + name: testData.expenseClass.name, + encumbered: '$0.00', + awaitingPayment: '$0.00', + expended: '$10.00', + percentExpended: '33.33%', + status: EXPENSE_CLASS_STATUSES.INACTIVE, + }, + ], + }); + FinanceDetails.checkUnassignedExpenseClassTooltip('expense-classes'); + + BudgetDetails.closeBudgetDetails(); + TopMenuNavigation.navigateToApp(APPLICATION_NAMES.FINANCE); + FinanceHelper.selectGroupsNavigation(); + Groups.waitLoading(); + Groups.searchByName(testData.group.name); + Groups.selectGroupByName(testData.group.name); + Groups.checkFYInGroup(testData.fiscalYear.code); + GroupDetails.checkGroupDetails({ + expenseClasses: [ + { + name: 'Unassigned', + encumbered: '$0.00', + awaitingPayment: '$0.00', + expended: '$20.00', + percentExpended: '66.67%', + }, + { + name: testData.expenseClass.name, + encumbered: '$0.00', + awaitingPayment: '$0.00', + expended: '$10.00', + percentExpended: '33.33%', + }, + ], + }); + FinanceDetails.checkUnassignedExpenseClassTooltip('expenseClasses'); + }, + ); + }); +}); diff --git a/cypress/e2e/finance/funds/validation-in-the-move-allocation-modal-for-allocation-transaction-exceeding-total-allocated.cy.js b/cypress/e2e/finance/funds/validation-in-the-move-allocation-modal-for-allocation-transaction-exceeding-total-allocated.cy.js new file mode 100644 index 0000000000..97019e46e2 --- /dev/null +++ b/cypress/e2e/finance/funds/validation-in-the-move-allocation-modal-for-allocation-transaction-exceeding-total-allocated.cy.js @@ -0,0 +1,123 @@ +import { + APPLICATION_NAMES, + FUND_STATUSES, + FUNDING_INFORMATION_NAMES, +} from '../../../support/constants'; +import Permissions from '../../../support/dictionary/permissions'; +import { + Budgets, + FinanceHelper, + FiscalYears, + Funds, + Ledgers, +} from '../../../support/fragments/finance'; +import BudgetDetails from '../../../support/fragments/finance/budgets/budgetDetails'; +import TopMenuNavigation from '../../../support/fragments/topMenuNavigation'; +import Users from '../../../support/fragments/users/users'; +import getRandomPostfix from '../../../support/utils/stringTools'; + +describe('Finance', () => { + describe('Funds', () => { + const firstFiscalYear = { ...FiscalYears.defaultUiFiscalYear }; + const defaultLedger = { ...Ledgers.defaultUiLedger }; + const fundA = { ...Funds.defaultUiFund }; + const fundB = { + name: `autotest_fund2_${getRandomPostfix()}`, + code: getRandomPostfix(), + externalAccountNo: getRandomPostfix(), + fundStatus: FUND_STATUSES.ACTIVE, + description: `This is fund created by E2E test automation script_${getRandomPostfix()}`, + }; + + const budgetA = { + ...Budgets.getDefaultBudget(), + allocated: 200, + }; + const budgetB = { + ...Budgets.getDefaultBudget(), + allocated: 100, + }; + let user; + + before('Create test data', () => { + cy.getAdminToken().then(() => { + FiscalYears.createViaApi(firstFiscalYear).then((firstFiscalYearResponse) => { + firstFiscalYear.id = firstFiscalYearResponse.id; + budgetA.fiscalYearId = firstFiscalYearResponse.id; + budgetB.fiscalYearId = firstFiscalYearResponse.id; + defaultLedger.fiscalYearOneId = firstFiscalYear.id; + Ledgers.createViaApi(defaultLedger).then((ledgerResponse) => { + defaultLedger.id = ledgerResponse.id; + fundA.ledgerId = defaultLedger.id; + fundB.ledgerId = defaultLedger.id; + + Funds.createViaApi(fundB).then((fundBResponse) => { + fundB.id = fundBResponse.fund.id; + budgetB.fundId = fundBResponse.fund.id; + Budgets.createViaApi(budgetB); + fundA.allocatedToIds = [fundBResponse.fund.id]; + Funds.createViaApi(fundA).then((fundAResponse) => { + fundA.id = fundAResponse.fund.id; + budgetA.fundId = fundAResponse.fund.id; + Budgets.createViaApi(budgetA); + }); + }); + }); + }); + }); + + cy.createTempUser([ + Permissions.uiFinanceCreateAllocations.gui, + Permissions.uiFinanceViewFundAndBudget.gui, + ]).then((userProperties) => { + user = userProperties; + + cy.login(userProperties.username, userProperties.password); + TopMenuNavigation.navigateToApp(APPLICATION_NAMES.FINANCE); + Funds.waitLoading(); + }); + }); + + after('Delete test data', () => { + cy.getAdminToken(); + Users.deleteViaApi(user.userId); + }); + + it( + 'C825298 Validation in the "Move allocation" modal for allocation transaction exceeding total allocated (thunderjet) (TaaS)', + { tags: ['criticalPath', 'thunderjet', 'C825298'] }, + () => { + FinanceHelper.searchByName(fundA.name); + Funds.selectFund(fundA.name); + Funds.checkBudgetDetails([{ ...budgetA, available: budgetA.allocated }]); + Funds.selectBudgetDetails(); + + const addTransferModal = BudgetDetails.clickMoveAllocationButton(); + addTransferModal.verifyFromFieldValue(''); + addTransferModal.verifyToFieldValue(fundA.name); + + addTransferModal.fillTransferDetails({ fromFund: fundB.name, amount: '200' }); + addTransferModal.expectErrorPresent('Total allocation cannot be less than zero'); + addTransferModal.verifyConfirmButtonDisabled(true); + addTransferModal.verifyCancelButtonDisabled(false); + + addTransferModal.clickSwapButton(); + addTransferModal.verifyFromFieldValue(fundA.name); + addTransferModal.verifyToFieldValue(fundB.name); + addTransferModal.verifyConfirmButtonDisabled(false); + + addTransferModal.clickSwapButton(); + addTransferModal.verifyFromFieldValue(fundB.name); + addTransferModal.verifyToFieldValue(fundA.name); + addTransferModal.expectErrorPresent('Total allocation cannot be less than zero'); + addTransferModal.verifyConfirmButtonDisabled(true); + + addTransferModal.fillTransferDetails({ amount: '100' }); + addTransferModal.clickConfirmButton({ ammountAllocated: true, transferCreated: false }); + BudgetDetails.checkBudgetDetails({ + summary: [{ key: FUNDING_INFORMATION_NAMES.TOTAL_ALLOCATED, value: '$300.00' }], + }); + }, + ); + }); +}); diff --git a/cypress/support/fragments/finance/budgets/budgetDetails.js b/cypress/support/fragments/finance/budgets/budgetDetails.js index c633af043b..db4edd7142 100644 --- a/cypress/support/fragments/finance/budgets/budgetDetails.js +++ b/cypress/support/fragments/finance/budgets/budgetDetails.js @@ -27,7 +27,13 @@ export default { cy.wait(ms); cy.expect(budgetPane.exists()); }, - checkBudgetDetails({ summary = [], information = [], balance = {}, expenseClass } = {}) { + checkBudgetDetails({ + summary = [], + information = [], + balance = {}, + expenseClass, + expenseClasses, + } = {}) { cy.wait(4000); summary.forEach(({ key, value }) => { cy.expect( @@ -48,10 +54,11 @@ export default { this.checkBalance({ name: 'Available', value: balance.available }); } - if (expenseClass) { + const classes = expenseClasses || (expenseClass ? [expenseClass] : null); + if (classes) { FinanceDetails.checkExpenseClassesTableContent({ section: expenseClassSection, - items: [expenseClass], + items: classes, }); } }, diff --git a/cypress/support/fragments/finance/financeDetails.js b/cypress/support/fragments/finance/financeDetails.js index 21f792e66b..467ab314fa 100644 --- a/cypress/support/fragments/finance/financeDetails.js +++ b/cypress/support/fragments/finance/financeDetails.js @@ -1,4 +1,5 @@ import { + Button, HTML, KeyValue, MultiColumnListCell, @@ -141,6 +142,17 @@ export default { }); }); }, + checkUnassignedExpenseClassTooltip(sectionId) { + const section = Section({ id: sectionId }); + const unassignedCell = section.find( + MultiColumnListCell({ column: 'Expense class', content: including('Unassigned') }), + ); + + cy.do(unassignedCell.find(Button({ icon: 'info' })).click()); + cy.contains( + 'applies to transactions completed prior to the inclusion of expense classes on the budget, if there are any that exist', + ).should('be.visible'); + }, checkLedgersDetails(ledgers = []) { this.checkTableContent({ section: ledgersSection, items: ledgers }); }, diff --git a/cypress/support/fragments/finance/funds/funds.js b/cypress/support/fragments/finance/funds/funds.js index bfe27cd74c..9fc296334e 100644 --- a/cypress/support/fragments/finance/funds/funds.js +++ b/cypress/support/fragments/finance/funds/funds.js @@ -395,7 +395,7 @@ export default { }, checkAmountInputError: (errorMessage) => { - cy.do(amountTextField.has({ error: errorMessage })); + cy.expect(amountTextField.has({ error: errorMessage })); }, checkCreatedFund: (fundName) => { diff --git a/cypress/support/fragments/finance/groups/groupDetails.js b/cypress/support/fragments/finance/groups/groupDetails.js index f4efa93e91..83cc0033fa 100644 --- a/cypress/support/fragments/finance/groups/groupDetails.js +++ b/cypress/support/fragments/finance/groups/groupDetails.js @@ -15,7 +15,7 @@ export default { verifyGroupName: (title) => { cy.expect(groupDetailsPane.find(groupDetailsPaneHeader).has({ text: including(title) })); }, - checkGroupDetails({ information, financialSummary, funds, expenseClass } = {}) { + checkGroupDetails({ information, financialSummary, funds, expenseClass, expenseClasses } = {}) { if (information) { FinanceDetails.checkInformation(information); } @@ -25,10 +25,11 @@ export default { if (funds) { FinanceDetails.checkFundsDetails(funds); } - if (expenseClass) { + const classes = expenseClasses || (expenseClass ? [expenseClass] : null); + if (classes) { FinanceDetails.checkExpenseClassesTableContent({ section: expenseClassSection, - items: [expenseClass], + items: classes, }); } }, diff --git a/cypress/support/fragments/finance/modals/addTransferModal.js b/cypress/support/fragments/finance/modals/addTransferModal.js index 697c74c5c8..a8e02f1097 100644 --- a/cypress/support/fragments/finance/modals/addTransferModal.js +++ b/cypress/support/fragments/finance/modals/addTransferModal.js @@ -29,6 +29,7 @@ const descriptionTextArea = addTransferModal.find(TextArea({ name: 'description' const cancelButton = addTransferModal.find(Button('Cancel')); const confirmButton = addTransferModal.find(Button('Confirm')); +const feedbackErrorSelector = '[class*="feedbackError"]'; export default { verifyModalView({ header = TRANSFER_ACTIONS.TRANSFER } = {}) { @@ -53,6 +54,7 @@ export default { SelectionList().filter(fromFund), SelectionList().select(including(fromFund)), ]); + cy.wait(2000); } if (toFund) { cy.do([ @@ -127,7 +129,7 @@ export default { }, expectErrorPresent(message) { - cy.get('[class*="feedbackError"]').should('contain', message); + cy.get(feedbackErrorSelector).should('contain', message); }, clickSwapButton() { @@ -167,6 +169,10 @@ export default { cy.expect(confirmButton.has({ disabled: isDisabled })); }, + verifyCancelButtonDisabled(isDisabled = true) { + cy.expect(cancelButton.has({ disabled: isDisabled })); + }, + addNewTag(tagName) { cy.do([tagsMultiSelect.toggle(), tagsMultiSelect.filter(tagName)]); cy.wait(500);