diff --git a/packages/server/src/models/CashflowTransaction.ts b/packages/server/src/models/CashflowTransaction.ts index c5aadbccb..dca2355f2 100644 --- a/packages/server/src/models/CashflowTransaction.ts +++ b/packages/server/src/models/CashflowTransaction.ts @@ -95,6 +95,17 @@ export default class CashflowTransaction extends TenantModel { return !!this.uncategorizedTransaction; } + /** + * Model modifiers. + */ + static get modifiers() { + return { + published(query) { + query.whereNot('published_at', null); + }, + }; + } + /** * Relationship mapping. */ diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionBillsTransformer.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionBillsTransformer.ts index f5dd9eaa0..4f833c599 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionBillsTransformer.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionBillsTransformer.ts @@ -17,6 +17,9 @@ export class GetMatchedTransactionBillsTransformer extends Transformer { 'transactionNo', 'transactionType', 'transsactionTypeFormatted', + 'transactionNormal', + 'referenceId', + 'referenceType', ]; }; @@ -100,4 +103,29 @@ export class GetMatchedTransactionBillsTransformer extends Transformer { protected transsactionTypeFormatted() { return 'Bill'; } + + /** + * Retrieves the bill transaction normal (debit or credit). + * @returns {string} + */ + protected transactionNormal() { + return 'credit'; + } + + /** + * Retrieve the match transaction reference id. + * @param bill + * @returns {number} + */ + protected referenceId(bill) { + return bill.id; + } + + /** + * Retrieve the match transaction referenece type. + * @returns {string} + */ + protected referenceType() { + return 'Bill'; + } } diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionCashflowTransformer.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionCashflowTransformer.ts index 7beb6ae99..cd40951ff 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionCashflowTransformer.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionCashflowTransformer.ts @@ -17,6 +17,7 @@ export class GetMatchedTransactionCashflowTransformer extends Transformer { 'transactionNo', 'transactionType', 'transsactionTypeFormatted', + 'transactionNormal', 'referenceId', 'referenceType', ]; @@ -113,10 +114,28 @@ export class GetMatchedTransactionCashflowTransformer extends Transformer { return transaction.transactionTypeFormatted; } + /** + * Retrieve the cashflow transaction normal (credit or debit). + * @param transaction + * @returns {string} + */ + protected transactionNormal(transaction) { + return transaction.isCashCredit ? 'credit' : 'debit'; + } + + /** + * Retrieves the cashflow transaction reference id. + * @param transaction + * @returns {number} + */ protected referenceId(transaction) { return transaction.id; } + /** + * Retrieves the cashflow transaction reference type. + * @returns {string} + */ protected referenceType() { return 'CashflowTransaction'; } diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionExpensesTransformer.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionExpensesTransformer.ts index d6f71e705..a77dcffb0 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionExpensesTransformer.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionExpensesTransformer.ts @@ -17,6 +17,9 @@ export class GetMatchedTransactionExpensesTransformer extends Transformer { 'transactionNo', 'transactionType', 'transsactionTypeFormatted', + 'transactionNormal', + 'referenceType', + 'referenceId', ]; }; @@ -111,4 +114,29 @@ export class GetMatchedTransactionExpensesTransformer extends Transformer { protected transsactionTypeFormatted() { return 'Expense'; } + + /** + * Retrieve the expense transaction normal (credit or debit). + * @returns {string} + */ + protected transactionNormal() { + return 'credit'; + } + + /** + * Retrieve the transaction reference type. + * @returns {string} + */ + protected referenceType() { + return 'Expense'; + } + + /** + * Retrieve the transaction reference id. + * @param transaction + * @returns {number} + */ + protected referenceId(transaction) { + return transaction.id; + } } diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionInvoicesTransformer.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionInvoicesTransformer.ts index 66814f7b6..dd5de9bfb 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionInvoicesTransformer.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionInvoicesTransformer.ts @@ -17,6 +17,9 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer { 'transactionNo', 'transactionType', 'transsactionTypeFormatted', + 'transactionNormal', + 'referenceType', + 'referenceId' ]; }; @@ -108,4 +111,28 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer { protected transsactionTypeFormatted(invoice) { return 'Sale invoice'; } + + /** + * Retrieve the transaction normal of invoice (credit or debit). + * @returns {string} + */ + protected transactionNormal() { + return 'debit'; + } + + /** + * Retrieve the transaction reference type. + * @returns {string} + */ protected referenceType() { + return 'SaleInvoice'; + } + + /** + * Retrieve the transaction reference id. + * @param transaction + * @returns {number} + */ + protected referenceId(transaction) { + return transaction.id; + } } diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionManualJournalsTransformer.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionManualJournalsTransformer.ts index 9b19b01a0..11ab194a0 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionManualJournalsTransformer.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionManualJournalsTransformer.ts @@ -1,4 +1,6 @@ +import { sumBy } from 'lodash'; import { Transformer } from '@/lib/Transformer/Transformer'; +import { AccountNormal } from '@/interfaces'; export class GetMatchedTransactionManualJournalsTransformer extends Transformer { /** @@ -17,6 +19,9 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer 'transactionNo', 'transactionType', 'transsactionTypeFormatted', + 'transactionNormal', + 'referenceType', + 'referenceId', ]; }; @@ -37,13 +42,20 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer return manualJournal.referenceNo; } + protected total(manualJournal) { + const credit = sumBy(manualJournal?.entries, 'credit'); + const debit = sumBy(manualJournal?.entries, 'debit'); + + return debit - credit; + } + /** * Retrieves the manual journal amount. * @param manualJournal * @returns {number} */ protected amount(manualJournal) { - return manualJournal.amount; + return Math.abs(this.total(manualJournal)); } /** @@ -107,5 +119,31 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer protected transsactionTypeFormatted() { return 'Manual Journal'; } -} + /** + * Retrieve the manual journal transaction normal (credit or debit). + * @returns {string} + */ + protected transactionNormal(transaction) { + const amount = this.total(transaction); + + return amount >= 0 ? AccountNormal.DEBIT : AccountNormal.CREDIT; + } + + /** + * Retrieve the manual journal reference type. + * @returns {string} + */ + protected referenceType() { + return 'ManualJournal'; + } + + /** + * Retrieves the manual journal reference id. + * @param transaction + * @returns {number} + */ + protected referenceId(transaction) { + return transaction.id; + } +} diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts index 32673fe4d..f0cfdcaed 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactions.ts @@ -9,6 +9,7 @@ import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactions import HasTenancyService from '@/services/Tenancy/TenancyService'; import { sortClosestMatchTransactions } from './_utils'; import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow'; +import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices'; @Service() export class GetMatchedTransactions { @@ -16,7 +17,7 @@ export class GetMatchedTransactions { private tenancy: HasTenancyService; @Inject() - private getMatchedInvoicesService: GetMatchedTransactionsByExpenses; + private getMatchedInvoicesService: GetMatchedTransactionsByInvoices; @Inject() private getMatchedBillsService: GetMatchedTransactionsByBills; diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByBills.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByBills.ts index 4796f7598..394fecba5 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByBills.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByBills.ts @@ -1,4 +1,5 @@ import { Inject, Service } from 'typedi'; +import { initialize } from 'objection'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer'; import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types'; @@ -22,10 +23,25 @@ export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType tenantId: number, filter: GetMatchedTransactionsFilter ) { - const { Bill } = this.tenancy.models(tenantId); + const { Bill, MatchedBankTransaction } = this.tenancy.models(tenantId); + const knex = this.tenancy.knex(tenantId); + // Initialize the models metadata. + await initialize(knex, [Bill, MatchedBankTransaction]); + + // Retrieves the bill matches. const bills = await Bill.query().onBuild((q) => { - q.whereNotExists(Bill.relatedQuery('matchedBankTransaction')); + q.withGraphJoined('matchedBankTransaction'); + q.whereNull('matchedBankTransaction.id'); + q.modify('published'); + + if (filter.fromDate) { + q.where('billDate', '>=', filter.fromDate); + } + if (filter.toDate) { + q.where('billDate', '<=', filter.toDate); + } + q.orderBy('billDate', 'DESC'); }); return this.transformer.transform( diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByCashflow.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByCashflow.ts index b59a44a40..1db6dd04f 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByCashflow.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByCashflow.ts @@ -27,9 +27,22 @@ export class GetMatchedTransactionsByCashflow extends GetMatchedTransactionsByTy // Initialize the ORM models metadata. await initialize(knex, [CashflowTransaction, MatchedBankTransaction]); - const transactions = await CashflowTransaction.query() - .withGraphJoined('matchedBankTransaction') - .whereNull('matchedBankTransaction.id'); + const transactions = await CashflowTransaction.query().onBuild((q) => { + // Not matched to bank transaction. + q.withGraphJoined('matchedBankTransaction'); + q.whereNull('matchedBankTransaction.id'); + + // Published. + q.modify('published'); + + if (filter.fromDate) { + q.where('date', '>=', filter.fromDate); + } + if (filter.toDate) { + q.where('date', '<=', filter.toDate); + } + q.orderBy('date', 'DESC'); + }); return this.transformer.transform( tenantId, diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByExpenses.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByExpenses.ts index 8996c4b91..39db88cf1 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByExpenses.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByExpenses.ts @@ -1,4 +1,5 @@ import { Inject, Service } from 'typedi'; +import { initialize } from 'objection'; import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import HasTenancyService from '@/services/Tenancy/TenancyService'; @@ -23,22 +24,34 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy tenantId: number, filter: GetMatchedTransactionsFilter ) { - const { Expense } = this.tenancy.models(tenantId); + const { Expense, MatchedBankTransaction } = this.tenancy.models(tenantId); + const knex = this.tenancy.knex(tenantId); + // Initialize the models metadata. + await initialize(knex, [Expense, MatchedBankTransaction]); + + // Retrieve the expense matches. const expenses = await Expense.query().onBuild((query) => { - query.whereNotExists(Expense.relatedQuery('matchedBankTransaction')); + // Filter out the not matched to bank transactions. + query.withGraphJoined('matchedBankTransaction'); + query.whereNull('matchedBankTransaction.id'); + + // Filter the published onyl + query.modify('filterByPublished'); + if (filter.fromDate) { - query.where('payment_date', '>=', filter.fromDate); + query.where('paymentDate', '>=', filter.fromDate); } if (filter.toDate) { - query.where('payment_date', '<=', filter.toDate); + query.where('paymentDate', '<=', filter.toDate); } if (filter.minAmount) { - query.where('total_amount', '>=', filter.minAmount); + query.where('totalAmount', '>=', filter.minAmount); } if (filter.maxAmount) { - query.where('total_amount', '<=', filter.maxAmount); + query.where('totalAmount', '<=', filter.maxAmount); } + query.orderBy('paymentDate', 'DESC'); }); return this.transformer.transform( tenantId, diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByInvoices.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByInvoices.ts index 141d22fe1..88cc72c1e 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByInvoices.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByInvoices.ts @@ -1,3 +1,5 @@ +import { Inject, Service } from 'typedi'; +import { initialize } from 'objection'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer'; import { @@ -6,7 +8,6 @@ import { MatchedTransactionsPOJO, } from './types'; import HasTenancyService from '@/services/Tenancy/TenancyService'; -import { Inject, Service } from 'typedi'; import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType'; @Service() @@ -27,10 +28,27 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy tenantId: number, filter: GetMatchedTransactionsFilter ): Promise { - const { SaleInvoice } = this.tenancy.models(tenantId); + const { SaleInvoice, MatchedBankTransaction } = + this.tenancy.models(tenantId); + const knex = this.tenancy.knex(tenantId); + // Initialize the models metadata. + await initialize(knex, [SaleInvoice, MatchedBankTransaction]); + + // Retrieve the invoices that not matched, unpaid. const invoices = await SaleInvoice.query().onBuild((q) => { - q.whereNotExists(SaleInvoice.relatedQuery('matchedBankTransaction')); + q.withGraphJoined('matchedBankTransaction'); + q.whereNull('matchedBankTransaction.id'); + q.modify('unpaid'); + q.modify('published'); + + if (filter.fromDate) { + q.where('invoiceDate', '>=', filter.fromDate); + } + if (filter.toDate) { + q.where('invoiceDate', '<=', filter.toDate); + } + q.orderBy('invoiceDate', 'DESC'); }); return this.transformer.transform( diff --git a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByManualJournals.ts b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByManualJournals.ts index 2aa6341af..42dae0988 100644 --- a/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByManualJournals.ts +++ b/packages/server/src/services/Banking/Matching/GetMatchedTransactionsByManualJournals.ts @@ -1,4 +1,5 @@ import { Inject, Service } from 'typedi'; +import { initialize } from 'objection'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer'; import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType'; @@ -19,12 +20,26 @@ export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactio tenantId: number, filter: Omit ) { - const { ManualJournal } = this.tenancy.models(tenantId); + const { ManualJournal, ManualJournalEntry, MatchedBankTransaction } = + this.tenancy.models(tenantId); + const knex = this.tenancy.knex(tenantId); + + await initialize(knex, [ + ManualJournal, + ManualJournalEntry, + MatchedBankTransaction, + ]); + const accountId = 1000; const manualJournals = await ManualJournal.query().onBuild((query) => { - query.whereNotExists( - ManualJournal.relatedQuery('matchedBankTransaction') - ); + query.withGraphJoined('matchedBankTransaction'); + query.whereNull('matchedBankTransaction.id'); + + query.withGraphJoined('entries'); + query.where('entries.accountId', accountId); + + query.modify('filterByPublished'); + if (filter.fromDate) { query.where('date', '>=', filter.fromDate); } diff --git a/packages/server/src/services/Banking/Matching/MatchTransactions.ts b/packages/server/src/services/Banking/Matching/MatchTransactions.ts index c91cb152d..a85fb6a98 100644 --- a/packages/server/src/services/Banking/Matching/MatchTransactions.ts +++ b/packages/server/src/services/Banking/Matching/MatchTransactions.ts @@ -1,4 +1,4 @@ -import { isEmpty, sumBy } from 'lodash'; +import { isEmpty } from 'lodash'; import { Knex } from 'knex'; import { Inject, Service } from 'typedi'; import { PromisePool } from '@supercharge/promise-pool'; @@ -14,6 +14,7 @@ import { } from './types'; import { MatchTransactionsTypes } from './MatchTransactionsTypes'; import { ServiceError } from '@/exceptions'; +import { sumMatchTranasctions } from './_utils'; @Service() export class MatchBankTransactions { @@ -90,9 +91,8 @@ export class MatchBankTransactions { throw new ServiceError(error); } // Calculate the total given matching transactions. - const totalMatchedTranasctions = sumBy( - validatationResult.results, - 'amount' + const totalMatchedTranasctions = sumMatchTranasctions( + validatationResult.results ); // Validates the total given matching transcations whether is not equal // uncategorized transaction amount. diff --git a/packages/server/src/services/Banking/Matching/_utils.ts b/packages/server/src/services/Banking/Matching/_utils.ts index 89a316a4b..67e7b0042 100644 --- a/packages/server/src/services/Banking/Matching/_utils.ts +++ b/packages/server/src/services/Banking/Matching/_utils.ts @@ -20,3 +20,12 @@ export const sortClosestMatchTransactions = ( ), ])(matches); }; + +export const sumMatchTranasctions = (transactions: Array) => { + return transactions.reduce( + (total, item) => + total + + (item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount), + 0 + ); +}; diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx index 7203085f1..bc228471d 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/CategorizeTransactionAside.tsx @@ -8,16 +8,26 @@ import { } from '../withBankingActions'; import { CategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot'; import { withBanking } from '../withBanking'; +import { useEffect } from 'react'; interface CategorizeTransactionAsideProps extends WithBankingActionsProps {} function CategorizeTransactionAsideRoot({ // #withBankingActions closeMatchingTransactionAside, + closeReconcileMatchingTransaction, // #withBanking selectedUncategorizedTransactionId, }: CategorizeTransactionAsideProps) { + // + useEffect( + () => () => { + closeReconcileMatchingTransaction(); + }, + [closeReconcileMatchingTransaction], + ); + const handleClose = () => { closeMatchingTransactionAside(); }; diff --git a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.tsx b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.tsx index 7255fb726..3ae33fd68 100644 --- a/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.tsx +++ b/packages/webapp/src/containers/CashFlow/CategorizeTransactionAside/MatchingReconcileTransactionAside/MatchingReconcileTransactionForm.tsx @@ -1,7 +1,14 @@ // @ts-nocheck import * as R from 'ramda'; import { Button, Intent, Position, Tag } from '@blueprintjs/core'; -import { Form, Formik, FormikValues, useFormikContext } from 'formik'; +import { + Form, + Formik, + FormikHelpers, + FormikValues, + useFormikContext, +} from 'formik'; +import moment from 'moment'; import { AccountsSelect, AppToaster, @@ -26,6 +33,7 @@ import { useCreateCashflowTransaction } from '@/hooks/query'; import { useAccountTransactionsContext } from '../../AccountTransactions/AccountTransactionsProvider'; import { MatchingReconcileFormSchema } from './MatchingReconcileTransactionForm.schema'; import { initialValues, transformToReq } from './_utils'; +import { withBanking } from '../../withBanking'; interface MatchingReconcileTransactionFormProps { onSubmitSuccess?: (values: any) => void; @@ -33,6 +41,7 @@ interface MatchingReconcileTransactionFormProps { function MatchingReconcileTransactionFormRoot({ closeReconcileMatchingTransaction, + reconcileMatchingTransactionPendingAmount, // #props¿ onSubmitSuccess, @@ -43,12 +52,17 @@ function MatchingReconcileTransactionFormRoot({ const { accountId } = useAccountTransactionsContext(); + // Handles the aside close. const handleAsideClose = () => { closeReconcileMatchingTransaction(); }; + // Handle the form submitting. const handleSubmit = ( values: MatchingReconcileTransactionValues, - { setSubmitting }: FormikValues, + { + setSubmitting, + setErrors, + }: FormikHelpers, ) => { setSubmitting(true); const _values = transformToReq(values, accountId); @@ -67,14 +81,31 @@ function MatchingReconcileTransactionFormRoot({ }) .catch((error) => { setSubmitting(false); - - AppToaster.show({ - message: 'Something went wrong.', - intent: Intent.DANGER, - }); + if ( + error.response.data?.errors?.find( + (e) => e.type === 'BRANCH_ID_REQUIRED', + ) + ) { + setErrors({ + branchId: 'The branch is required.', + }); + } else { + AppToaster.show({ + message: 'Something went wrong.', + intent: Intent.DANGER, + }); + } }); }; + const _initialValues = { + ...initialValues, + amount: Math.abs(reconcileMatchingTransactionPendingAmount) || 0, + date: moment().format('YYYY-MM-DD'), + type: + reconcileMatchingTransactionPendingAmount > 0 ? 'deposit' : 'withdrawal', + }; + return (