mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
Compare commits
15 Commits
reconcile-
...
v0.18.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81b26c6f13 | ||
|
|
da435d85d9 | ||
|
|
533006b90e | ||
|
|
d096e49d45 | ||
|
|
73acdb6240 | ||
|
|
38d4122d11 | ||
|
|
24a77c81b3 | ||
|
|
7f41b4280e | ||
|
|
aa89653967 | ||
|
|
b80bc95fa5 | ||
|
|
9a5befbee7 | ||
|
|
b7487f19d3 | ||
|
|
cd9039fe16 | ||
|
|
87f60f7461 | ||
|
|
202179ec0b |
@@ -38,15 +38,7 @@ export class BankingRulesController extends BaseController {
|
|||||||
body('conditions.*.value').exists(),
|
body('conditions.*.value').exists(),
|
||||||
|
|
||||||
// Assign
|
// Assign
|
||||||
body('assign_category')
|
body('assign_category').isString(),
|
||||||
.isString()
|
|
||||||
.isIn([
|
|
||||||
'interest_income',
|
|
||||||
'other_income',
|
|
||||||
'deposit',
|
|
||||||
'expense',
|
|
||||||
'owner_drawings',
|
|
||||||
]),
|
|
||||||
body('assign_account_id').isInt({ min: 0 }),
|
body('assign_account_id').isInt({ min: 0 }),
|
||||||
body('assign_payee').isString().optional({ nullable: true }),
|
body('assign_payee').isString().optional({ nullable: true }),
|
||||||
body('assign_memo').isString().optional({ nullable: true }),
|
body('assign_memo').isString().optional({ nullable: true }),
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
|
export const CashflowTransactionTypes = {
|
||||||
|
OtherIncome: 'Other income',
|
||||||
|
OtherExpense: 'Other expense',
|
||||||
|
OwnerDrawing: 'Owner drawing',
|
||||||
|
OwnerContribution: 'Owner contribution',
|
||||||
|
TransferToAccount: 'Transfer to account',
|
||||||
|
TransferFromAccount: 'Transfer from account',
|
||||||
|
};
|
||||||
|
|
||||||
export const TransactionTypes = {
|
export const TransactionTypes = {
|
||||||
SaleInvoice: 'Sale invoice',
|
SaleInvoice: 'Sale invoice',
|
||||||
SaleReceipt: 'Sale receipt',
|
SaleReceipt: 'Sale receipt',
|
||||||
PaymentReceive: 'Payment receive',
|
PaymentReceive: 'Payment received',
|
||||||
Bill: 'Bill',
|
Bill: 'Bill',
|
||||||
BillPayment: 'Payment made',
|
BillPayment: 'Payment made',
|
||||||
VendorOpeningBalance: 'Vendor opening balance',
|
VendorOpeningBalance: 'Vendor opening balance',
|
||||||
@@ -17,12 +26,10 @@ export const TransactionTypes = {
|
|||||||
OtherExpense: 'Other expense',
|
OtherExpense: 'Other expense',
|
||||||
OwnerDrawing: 'Owner drawing',
|
OwnerDrawing: 'Owner drawing',
|
||||||
InvoiceWriteOff: 'Invoice write-off',
|
InvoiceWriteOff: 'Invoice write-off',
|
||||||
|
|
||||||
CreditNote: 'transaction_type.credit_note',
|
CreditNote: 'transaction_type.credit_note',
|
||||||
VendorCredit: 'transaction_type.vendor_credit',
|
VendorCredit: 'transaction_type.vendor_credit',
|
||||||
|
|
||||||
RefundCreditNote: 'transaction_type.refund_credit_note',
|
RefundCreditNote: 'transaction_type.refund_credit_note',
|
||||||
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
|
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
|
||||||
|
|
||||||
LandedCost: 'transaction_type.landed_cost',
|
LandedCost: 'transaction_type.landed_cost',
|
||||||
|
CashflowTransaction: CashflowTransactionTypes,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ exports.up = function (knex) {
|
|||||||
.integer('uncategorized_transaction_id')
|
.integer('uncategorized_transaction_id')
|
||||||
.unsigned()
|
.unsigned()
|
||||||
.references('id')
|
.references('id')
|
||||||
.inTable('uncategorized_cashflow_transactions');
|
.inTable('uncategorized_cashflow_transactions')
|
||||||
|
.withKeyName('recognizedBankTransactionsUncategorizedTransIdForeign');
|
||||||
table
|
table
|
||||||
.integer('bank_rule_id')
|
.integer('bank_rule_id')
|
||||||
.unsigned()
|
.unsigned()
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
exports.up = function (knex) {
|
exports.up = function (knex) {
|
||||||
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
|
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
|
||||||
table.integer('recognized_transaction_id').unsigned();
|
table
|
||||||
|
.integer('recognized_transaction_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('recognized_bank_transactions')
|
||||||
|
.withKeyName('uncategorizedCashflowTransRecognizedTranIdForeign');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
exports.up = function (knex) {
|
exports.up = function (knex) {
|
||||||
return knex.schema.createTable('matched_bank_transactions', (table) => {
|
return knex.schema.createTable('matched_bank_transactions', (table) => {
|
||||||
table.increments('id');
|
table.increments('id');
|
||||||
table.integer('uncategorized_transaction_id').unsigned();
|
table
|
||||||
|
.integer('uncategorized_transaction_id')
|
||||||
|
.unsigned()
|
||||||
|
.references('id')
|
||||||
|
.inTable('uncategorized_cashflow_transactions');
|
||||||
table.string('reference_type');
|
table.string('reference_type');
|
||||||
table.integer('reference_id').unsigned();
|
table.integer('reference_id').unsigned();
|
||||||
table.decimal('amount');
|
table.decimal('amount');
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
exports.up = function (knex) {
|
||||||
|
return knex('accounts_transactions')
|
||||||
|
.whereIn('referenceType', [
|
||||||
|
'OtherIncome',
|
||||||
|
'OtherExpense',
|
||||||
|
'OwnerDrawing',
|
||||||
|
'OwnerContribution',
|
||||||
|
'TransferToAccount',
|
||||||
|
'TransferFromAccount',
|
||||||
|
])
|
||||||
|
.update({
|
||||||
|
transactionType: knex.raw(`
|
||||||
|
CASE
|
||||||
|
WHEN REFERENCE_TYPE = 'OtherIncome' THEN 'OtherIncome'
|
||||||
|
WHEN REFERENCE_TYPE = 'OtherExpense' THEN 'OtherExpense'
|
||||||
|
WHEN REFERENCE_TYPE = 'OwnerDrawing' THEN 'OwnerDrawing'
|
||||||
|
WHEN REFERENCE_TYPE = 'OwnerContribution' THEN 'OwnerContribution'
|
||||||
|
WHEN REFERENCE_TYPE = 'TransferToAccount' THEN 'TransferToAccount'
|
||||||
|
WHEN REFERENCE_TYPE = 'TransferFromAccount' THEN 'TransferFromAccount'
|
||||||
|
END
|
||||||
|
`),
|
||||||
|
referenceType: knex.raw(`
|
||||||
|
CASE
|
||||||
|
WHEN REFERENCE_TYPE IN ('OtherIncome', 'OtherExpense', 'OwnerDrawing', 'OwnerContribution', 'TransferToAccount', 'TransferFromAccount') THEN 'CashflowTransaction'
|
||||||
|
ELSE REFERENCE_TYPE
|
||||||
|
END
|
||||||
|
`),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex('accounts_transactions')
|
||||||
|
.whereIn('transactionType', [
|
||||||
|
'OtherIncome',
|
||||||
|
'OtherExpense',
|
||||||
|
'OwnerDrawing',
|
||||||
|
'OwnerContribution',
|
||||||
|
'TransferToAccount',
|
||||||
|
'TransferFromAccount',
|
||||||
|
])
|
||||||
|
.update({
|
||||||
|
referenceType: knex.raw(`
|
||||||
|
CASE
|
||||||
|
WHEN TRANSACTION_TYPE = 'OtherIncome' THEN 'OtherIncome'
|
||||||
|
WHEN TRANSACTION_TYPE = 'OtherExpense' THEN 'OtherExpense'
|
||||||
|
WHEN TRANSACTION_TYPE = 'OwnerDrawing' THEN 'OwnerDrawing'
|
||||||
|
WHEN TRANSACTION_TYPE = 'OwnerContribution' THEN 'OwnerContribution'
|
||||||
|
WHEN TRANSACTION_TYPE = 'TransferToAccount' THEN 'TransferToAccount'
|
||||||
|
WHEN TRANSACTION_TYPE = 'TransferFromAccount' THEN 'TransferFromAccount'
|
||||||
|
ELSE REFERENCE_TYPE
|
||||||
|
END
|
||||||
|
`),
|
||||||
|
transactionType: knex.raw(`
|
||||||
|
CASE
|
||||||
|
WHEN TRANSACTION_TYPE IN ('OtherIncome', 'OtherExpense', 'OwnerDrawing', 'OwnerContribution', 'TransferToAccount', 'TransferFromAccount') THEN NULL
|
||||||
|
ELSE TRANSACTION_TYPE
|
||||||
|
END
|
||||||
|
`),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -66,7 +66,9 @@ export interface IAccountTransaction {
|
|||||||
referenceId: number;
|
referenceId: number;
|
||||||
|
|
||||||
referenceNumber?: string;
|
referenceNumber?: string;
|
||||||
|
|
||||||
transactionNumber?: string;
|
transactionNumber?: string;
|
||||||
|
transactionType?: string;
|
||||||
|
|
||||||
note?: string;
|
note?: string;
|
||||||
|
|
||||||
|
|||||||
@@ -130,8 +130,9 @@ export interface ICommandCashflowDeletedPayload {
|
|||||||
|
|
||||||
export interface ICashflowTransactionCategorizedPayload {
|
export interface ICashflowTransactionCategorizedPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
cashflowTransactionId: number;
|
uncategorizedTransaction: any;
|
||||||
cashflowTransaction: ICashflowTransaction;
|
cashflowTransaction: ICashflowTransaction;
|
||||||
|
categorizeDTO: any;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
export interface ICashflowTransactionUncategorizingPayload {
|
export interface ICashflowTransactionUncategorizingPayload {
|
||||||
|
|||||||
@@ -29,4 +29,9 @@ export interface ICashflowAccountTransaction {
|
|||||||
|
|
||||||
date: Date;
|
date: Date;
|
||||||
formattedDate: string;
|
formattedDate: string;
|
||||||
|
|
||||||
|
status: string;
|
||||||
|
formattedStatus: string;
|
||||||
|
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
}
|
}
|
||||||
|
|||||||
8
packages/server/src/interfaces/Import.ts
Normal file
8
packages/server/src/interfaces/Import.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
|
||||||
|
|
||||||
|
|
||||||
|
export interface IImportFileCommitedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
importId: number;
|
||||||
|
meta: ImportFilePreviewPOJO;
|
||||||
|
}
|
||||||
@@ -40,6 +40,8 @@ export interface ILedgerEntry {
|
|||||||
date: Date | string;
|
date: Date | string;
|
||||||
|
|
||||||
transactionType: string;
|
transactionType: string;
|
||||||
|
transactionSubType: string;
|
||||||
|
|
||||||
transactionId: number;
|
transactionId: number;
|
||||||
|
|
||||||
transactionNumber?: string;
|
transactionNumber?: string;
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ import { ValidateMatchingOnPaymentMadeDelete } from '@/services/Banking/Matching
|
|||||||
import { ValidateMatchingOnCashflowDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnCashflowDelete';
|
import { ValidateMatchingOnCashflowDelete } from '@/services/Banking/Matching/events/ValidateMatchingOnCashflowDelete';
|
||||||
import { RecognizeSyncedBankTranasctions } from '@/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions';
|
import { RecognizeSyncedBankTranasctions } from '@/services/Banking/Plaid/subscribers/RecognizeSyncedBankTransactions';
|
||||||
import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule';
|
import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/UnlinkBankRuleOnDeleteBankRule';
|
||||||
|
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
|
||||||
|
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
|
||||||
|
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return new EventPublisher();
|
return new EventPublisher();
|
||||||
@@ -258,6 +261,9 @@ export const susbcribers = () => {
|
|||||||
// Bank Rules
|
// Bank Rules
|
||||||
TriggerRecognizedTransactions,
|
TriggerRecognizedTransactions,
|
||||||
UnlinkBankRuleOnDeleteBankRule,
|
UnlinkBankRuleOnDeleteBankRule,
|
||||||
|
DecrementUncategorizedTransactionOnMatching,
|
||||||
|
DecrementUncategorizedTransactionOnExclude,
|
||||||
|
DecrementUncategorizedTransactionOnCategorize,
|
||||||
|
|
||||||
// Validate matching
|
// Validate matching
|
||||||
ValidateMatchingOnCashflowDelete,
|
ValidateMatchingOnCashflowDelete,
|
||||||
@@ -266,7 +272,7 @@ export const susbcribers = () => {
|
|||||||
ValidateMatchingOnPaymentReceivedDelete,
|
ValidateMatchingOnPaymentReceivedDelete,
|
||||||
ValidateMatchingOnPaymentMadeDelete,
|
ValidateMatchingOnPaymentMadeDelete,
|
||||||
|
|
||||||
// Plaid
|
// Plaid
|
||||||
RecognizeSyncedBankTranasctions,
|
RecognizeSyncedBankTranasctions,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default class AccountTransaction extends TenantModel {
|
|||||||
debit: number;
|
debit: number;
|
||||||
exchangeRate: number;
|
exchangeRate: number;
|
||||||
taxRate: number;
|
taxRate: number;
|
||||||
|
transactionType: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
@@ -53,7 +54,7 @@ export default class AccountTransaction extends TenantModel {
|
|||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
get referenceTypeFormatted() {
|
get referenceTypeFormatted() {
|
||||||
return getTransactionTypeLabel(this.referenceType);
|
return getTransactionTypeLabel(this.referenceType, this.transactionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
getCashflowAccountTransactionsTypes,
|
getCashflowAccountTransactionsTypes,
|
||||||
getCashflowTransactionType,
|
getCashflowTransactionType,
|
||||||
} from '@/services/Cashflow/utils';
|
} from '@/services/Cashflow/utils';
|
||||||
import AccountTransaction from './AccountTransaction';
|
|
||||||
import { CASHFLOW_DIRECTION } from '@/services/Cashflow/constants';
|
import { CASHFLOW_DIRECTION } from '@/services/Cashflow/constants';
|
||||||
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
import { getCashflowTransactionFormattedType } from '@/utils/transactions-types';
|
||||||
|
|
||||||
export default class CashflowTransaction extends TenantModel {
|
export default class CashflowTransaction extends TenantModel {
|
||||||
transactionType: string;
|
transactionType: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
@@ -64,7 +64,7 @@ export default class CashflowTransaction extends TenantModel {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
get transactionTypeFormatted() {
|
get transactionTypeFormatted() {
|
||||||
return getTransactionTypeLabel(this.transactionType);
|
return getCashflowTransactionFormattedType(this.transactionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
get typeMeta() {
|
get typeMeta() {
|
||||||
@@ -95,6 +95,34 @@ export default class CashflowTransaction extends TenantModel {
|
|||||||
return !!this.uncategorizedTransaction;
|
return !!this.uncategorizedTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model modifiers.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Filter the published transactions.
|
||||||
|
*/
|
||||||
|
published(query) {
|
||||||
|
query.whereNot('published_at', null);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the not categorized transactions.
|
||||||
|
*/
|
||||||
|
notCategorized(query) {
|
||||||
|
query.whereNull('cashflowTransactions.uncategorizedTransactionId');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the categorized transactions.
|
||||||
|
*/
|
||||||
|
categorized(query) {
|
||||||
|
query.whereNotNull('cashflowTransactions.uncategorizedTransactionId');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
@@ -131,8 +159,7 @@ export default class CashflowTransaction extends TenantModel {
|
|||||||
to: 'accounts_transactions.referenceId',
|
to: 'accounts_transactions.referenceId',
|
||||||
},
|
},
|
||||||
filter(builder) {
|
filter(builder) {
|
||||||
const referenceTypes = getCashflowAccountTransactionsTypes();
|
builder.where('reference_type', 'CashflowTransaction');
|
||||||
builder.whereIn('reference_type', referenceTypes);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -105,8 +105,34 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
* Filters the excluded transactions.
|
* Filters the excluded transactions.
|
||||||
*/
|
*/
|
||||||
excluded(query) {
|
excluded(query) {
|
||||||
query.whereNotNull('excluded_at')
|
query.whereNotNull('excluded_at');
|
||||||
}
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter out the recognized transactions.
|
||||||
|
* @param query
|
||||||
|
*/
|
||||||
|
recognized(query) {
|
||||||
|
query.whereNotNull('recognizedTransactionId');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter out the not recognized transactions.
|
||||||
|
* @param query
|
||||||
|
*/
|
||||||
|
notRecognized(query) {
|
||||||
|
query.whereNull('recognizedTransactionId');
|
||||||
|
},
|
||||||
|
|
||||||
|
categorized(query) {
|
||||||
|
query.whereNotNull('categorizeRefType');
|
||||||
|
query.whereNotNull('categorizeRefId');
|
||||||
|
},
|
||||||
|
|
||||||
|
notCategorized(query) {
|
||||||
|
query.whereNull('categorizeRefType');
|
||||||
|
query.whereNull('categorizeRefId');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,56 +184,4 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the count of uncategorized transactions for the associated account
|
|
||||||
* based on the specified operation.
|
|
||||||
* @param {QueryContext} queryContext - The query context for the transaction.
|
|
||||||
* @param {boolean} increment - Indicates whether to increment or decrement the count.
|
|
||||||
*/
|
|
||||||
private async updateUncategorizedTransactionCount(
|
|
||||||
queryContext: QueryContext,
|
|
||||||
increment: boolean,
|
|
||||||
amount: number = 1
|
|
||||||
) {
|
|
||||||
const operation = increment ? 'increment' : 'decrement';
|
|
||||||
|
|
||||||
await Account.query(queryContext.transaction)
|
|
||||||
.findById(this.accountId)
|
|
||||||
[operation]('uncategorized_transactions', amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs after insert.
|
|
||||||
* @param {QueryContext} queryContext
|
|
||||||
*/
|
|
||||||
public async $afterInsert(queryContext) {
|
|
||||||
await super.$afterInsert(queryContext);
|
|
||||||
await this.updateUncategorizedTransactionCount(queryContext, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs after update.
|
|
||||||
* @param {ModelOptions} opt
|
|
||||||
* @param {QueryContext} queryContext
|
|
||||||
*/
|
|
||||||
public async $afterUpdate(
|
|
||||||
opt: ModelOptions,
|
|
||||||
queryContext: QueryContext
|
|
||||||
): Promise<any> {
|
|
||||||
await super.$afterUpdate(opt, queryContext);
|
|
||||||
|
|
||||||
if (this.id && this.categorized) {
|
|
||||||
await this.updateUncategorizedTransactionCount(queryContext, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs after delete.
|
|
||||||
* @param {QueryContext} queryContext
|
|
||||||
*/
|
|
||||||
public async $afterDelete(queryContext: QueryContext) {
|
|
||||||
await super.$afterDelete(queryContext);
|
|
||||||
await this.updateUncategorizedTransactionCount(queryContext, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export const transformLedgerEntryToTransaction = (
|
|||||||
referenceId: entry.transactionId,
|
referenceId: entry.transactionId,
|
||||||
|
|
||||||
transactionNumber: entry.transactionNumber,
|
transactionNumber: entry.transactionNumber,
|
||||||
|
transactionType: entry.transactionSubType,
|
||||||
|
|
||||||
referenceNumber: entry.referenceNumber,
|
referenceNumber: entry.referenceNumber,
|
||||||
|
|
||||||
note: entry.note,
|
note: entry.note,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import { Server } from 'socket.io';
|
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { initialize } from 'objection';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class GetBankAccountSummary {
|
export class GetBankAccountSummary {
|
||||||
@@ -14,22 +14,43 @@ export class GetBankAccountSummary {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public async getBankAccountSummary(tenantId: number, bankAccountId: number) {
|
public async getBankAccountSummary(tenantId: number, bankAccountId: number) {
|
||||||
|
const knex = this.tenancy.knex(tenantId);
|
||||||
const {
|
const {
|
||||||
Account,
|
Account,
|
||||||
UncategorizedCashflowTransaction,
|
UncategorizedCashflowTransaction,
|
||||||
RecognizedBankTransaction,
|
RecognizedBankTransaction,
|
||||||
|
MatchedBankTransaction,
|
||||||
} = this.tenancy.models(tenantId);
|
} = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
await initialize(knex, [
|
||||||
|
UncategorizedCashflowTransaction,
|
||||||
|
RecognizedBankTransaction,
|
||||||
|
MatchedBankTransaction,
|
||||||
|
]);
|
||||||
const bankAccount = await Account.query()
|
const bankAccount = await Account.query()
|
||||||
.findById(bankAccountId)
|
.findById(bankAccountId)
|
||||||
.throwIfNotFound();
|
.throwIfNotFound();
|
||||||
|
|
||||||
// Retrieves the uncategorized transactions count of the given bank account.
|
// Retrieves the uncategorized transactions count of the given bank account.
|
||||||
const uncategorizedTranasctionsCount =
|
const uncategorizedTranasctionsCount =
|
||||||
await UncategorizedCashflowTransaction.query()
|
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
||||||
.where('accountId', bankAccountId)
|
// Include just the given account.
|
||||||
.count('id as total')
|
q.where('accountId', bankAccountId);
|
||||||
.first();
|
|
||||||
|
// Only the not excluded.
|
||||||
|
q.modify('notExcluded');
|
||||||
|
|
||||||
|
// Only the not categorized.
|
||||||
|
q.modify('notCategorized');
|
||||||
|
|
||||||
|
// Only the not matched bank transactions.
|
||||||
|
q.withGraphJoined('matchedBankTransactions');
|
||||||
|
q.whereNull('matchedBankTransactions.id');
|
||||||
|
|
||||||
|
// Count the results.
|
||||||
|
q.count('uncategorized_cashflow_transactions.id as total');
|
||||||
|
q.first();
|
||||||
|
});
|
||||||
|
|
||||||
// Retrieves the recognized transactions count of the given bank account.
|
// Retrieves the recognized transactions count of the given bank account.
|
||||||
const recognizedTransactionsCount = await RecognizedBankTransaction.query()
|
const recognizedTransactionsCount = await RecognizedBankTransaction.query()
|
||||||
@@ -43,8 +64,8 @@ export class GetBankAccountSummary {
|
|||||||
.first();
|
.first();
|
||||||
|
|
||||||
const totalUncategorizedTransactions =
|
const totalUncategorizedTransactions =
|
||||||
uncategorizedTranasctionsCount?.total;
|
uncategorizedTranasctionsCount?.total || 0;
|
||||||
const totalRecognizedTransactions = recognizedTransactionsCount?.total;
|
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: bankAccount.name,
|
name: bankAccount.name,
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|||||||
import UnitOfWork from '@/services/UnitOfWork';
|
import UnitOfWork from '@/services/UnitOfWork';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { validateTransactionNotCategorized } from './utils';
|
import { validateTransactionNotCategorized } from './utils';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import {
|
||||||
|
IBankTransactionUnexcludedEventPayload,
|
||||||
|
IBankTransactionUnexcludingEventPayload,
|
||||||
|
} from './_types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExcludeBankTransaction {
|
export class ExcludeBankTransaction {
|
||||||
@@ -11,6 +17,9 @@ export class ExcludeBankTransaction {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private uow: UnitOfWork;
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given bank transaction as excluded.
|
* Marks the given bank transaction as excluded.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -31,11 +40,23 @@ export class ExcludeBankTransaction {
|
|||||||
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
||||||
|
|
||||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
|
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluding, {
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
trx,
|
||||||
|
} as IBankTransactionUnexcludingEventPayload);
|
||||||
|
|
||||||
await UncategorizedCashflowTransaction.query(trx)
|
await UncategorizedCashflowTransaction.query(trx)
|
||||||
.findById(uncategorizedTransactionId)
|
.findById(uncategorizedTransactionId)
|
||||||
.patch({
|
.patch({
|
||||||
excludedAt: new Date(),
|
excludedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.eventPublisher.emitAsync(events.bankTransactions.onExcluded, {
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
trx,
|
||||||
|
} as IBankTransactionUnexcludedEventPayload);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|||||||
import UnitOfWork from '@/services/UnitOfWork';
|
import UnitOfWork from '@/services/UnitOfWork';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { validateTransactionNotCategorized } from './utils';
|
import { validateTransactionNotCategorized } from './utils';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import {
|
||||||
|
IBankTransactionExcludedEventPayload,
|
||||||
|
IBankTransactionExcludingEventPayload,
|
||||||
|
} from './_types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class UnexcludeBankTransaction {
|
export class UnexcludeBankTransaction {
|
||||||
@@ -11,6 +17,9 @@ export class UnexcludeBankTransaction {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private uow: UnitOfWork;
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given bank transaction as excluded.
|
* Marks the given bank transaction as excluded.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -20,7 +29,7 @@ export class UnexcludeBankTransaction {
|
|||||||
public async unexcludeBankTransaction(
|
public async unexcludeBankTransaction(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
uncategorizedTransactionId: number
|
uncategorizedTransactionId: number
|
||||||
) {
|
): Promise<void> {
|
||||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const oldUncategorizedTransaction =
|
const oldUncategorizedTransaction =
|
||||||
@@ -31,11 +40,27 @@ export class UnexcludeBankTransaction {
|
|||||||
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
validateTransactionNotCategorized(oldUncategorizedTransaction);
|
||||||
|
|
||||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.bankTransactions.onUnexcluding,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
} as IBankTransactionExcludingEventPayload
|
||||||
|
);
|
||||||
|
|
||||||
await UncategorizedCashflowTransaction.query(trx)
|
await UncategorizedCashflowTransaction.query(trx)
|
||||||
.findById(uncategorizedTransactionId)
|
.findById(uncategorizedTransactionId)
|
||||||
.patch({
|
.patch({
|
||||||
excludedAt: null,
|
excludedAt: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.bankTransactions.onUnexcluded,
|
||||||
|
{
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
} as IBankTransactionExcludedEventPayload
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,30 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
export interface ExcludedBankTransactionsQuery {
|
export interface ExcludedBankTransactionsQuery {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
accountId?: number;
|
accountId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IBankTransactionUnexcludingEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBankTransactionUnexcludedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBankTransactionExcludingEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
}
|
||||||
|
export interface IBankTransactionExcludedEventPayload {
|
||||||
|
tenantId: number;
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import {
|
||||||
|
IBankTransactionExcludedEventPayload,
|
||||||
|
IBankTransactionUnexcludedEventPayload,
|
||||||
|
} from '../_types';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class DecrementUncategorizedTransactionOnExclude {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
*/
|
||||||
|
public attach(bus) {
|
||||||
|
bus.subscribe(
|
||||||
|
events.bankTransactions.onExcluded,
|
||||||
|
this.decrementUnCategorizedTransactionsOnExclude.bind(this)
|
||||||
|
);
|
||||||
|
bus.subscribe(
|
||||||
|
events.bankTransactions.onUnexcluded,
|
||||||
|
this.incrementUnCategorizedTransactionsOnUnexclude.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||||
|
* @param {IManualJournalDeletingPayload}
|
||||||
|
*/
|
||||||
|
public async decrementUnCategorizedTransactionsOnExclude({
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
trx,
|
||||||
|
}: IBankTransactionExcludedEventPayload) {
|
||||||
|
const { UncategorizedCashflowTransaction, Account } =
|
||||||
|
this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const transaction = await UncategorizedCashflowTransaction.query(
|
||||||
|
trx
|
||||||
|
).findById(uncategorizedTransactionId);
|
||||||
|
|
||||||
|
await Account.query(trx)
|
||||||
|
.findById(transaction.accountId)
|
||||||
|
.decrement('uncategorizedTransactions', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||||
|
* @param {IManualJournalDeletingPayload}
|
||||||
|
*/
|
||||||
|
public async incrementUnCategorizedTransactionsOnUnexclude({
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
trx,
|
||||||
|
}: IBankTransactionUnexcludedEventPayload) {
|
||||||
|
const { UncategorizedCashflowTransaction, Account } =
|
||||||
|
this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||||
|
uncategorizedTransactionId
|
||||||
|
);
|
||||||
|
//
|
||||||
|
await Account.query(trx)
|
||||||
|
.findById(transaction.accountId)
|
||||||
|
.increment('uncategorizedTransactions', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ export class GetMatchedTransactionBillsTransformer extends Transformer {
|
|||||||
'transactionNo',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
|
'transactionNormal',
|
||||||
|
'referenceId',
|
||||||
|
'referenceType',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,4 +103,29 @@ export class GetMatchedTransactionBillsTransformer extends Transformer {
|
|||||||
protected transsactionTypeFormatted() {
|
protected transsactionTypeFormatted() {
|
||||||
return 'Bill';
|
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
|
||||||
|
export class GetMatchedTransactionCashflowTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to sale credit note object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return [
|
||||||
|
'referenceNo',
|
||||||
|
'amount',
|
||||||
|
'amountFormatted',
|
||||||
|
'transactionNo',
|
||||||
|
'date',
|
||||||
|
'dateFormatted',
|
||||||
|
'transactionId',
|
||||||
|
'transactionNo',
|
||||||
|
'transactionType',
|
||||||
|
'transsactionTypeFormatted',
|
||||||
|
'transactionNormal',
|
||||||
|
'referenceId',
|
||||||
|
'referenceType',
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclude all attributes.
|
||||||
|
* @returns {Array<string>}
|
||||||
|
*/
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['*'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the invoice reference number.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected referenceNo(invoice) {
|
||||||
|
return invoice.referenceNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the transaction amount.
|
||||||
|
* @param transaction
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
protected amount(transaction) {
|
||||||
|
return transaction.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the transaction formatted amount.
|
||||||
|
* @param transaction
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected amountFormatted(transaction) {
|
||||||
|
return this.formatNumber(transaction.amount, {
|
||||||
|
currencyCode: transaction.currencyCode,
|
||||||
|
money: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the date of the invoice.
|
||||||
|
* @param invoice
|
||||||
|
* @returns {Date}
|
||||||
|
*/
|
||||||
|
protected date(transaction) {
|
||||||
|
return transaction.date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the date of the invoice.
|
||||||
|
* @param invoice
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected dateFormatted(transaction) {
|
||||||
|
return this.formatDate(transaction.date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the transaction ID of the invoice.
|
||||||
|
* @param invoice
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
protected transactionId(transaction) {
|
||||||
|
return transaction.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the invoice transaction number.
|
||||||
|
* @param invoice
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected transactionNo(transaction) {
|
||||||
|
return transaction.transactionNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the invoice transaction type.
|
||||||
|
* @param invoice
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
protected transactionType(transaction) {
|
||||||
|
return transaction.transactionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the invoice formatted transaction type.
|
||||||
|
* @param invoice
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected transsactionTypeFormatted(transaction) {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ export class GetMatchedTransactionExpensesTransformer extends Transformer {
|
|||||||
'transactionNo',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
|
'transactionNormal',
|
||||||
|
'referenceType',
|
||||||
|
'referenceId',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,4 +114,29 @@ export class GetMatchedTransactionExpensesTransformer extends Transformer {
|
|||||||
protected transsactionTypeFormatted() {
|
protected transsactionTypeFormatted() {
|
||||||
return 'Expense';
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
|||||||
'transactionNo',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
|
'transactionNormal',
|
||||||
|
'referenceType',
|
||||||
|
'referenceId'
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +52,7 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
|||||||
* @param invoice
|
* @param invoice
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected formatAmount(invoice) {
|
protected amountFormatted(invoice) {
|
||||||
return this.formatNumber(invoice.dueAmount, {
|
return this.formatNumber(invoice.dueAmount, {
|
||||||
currencyCode: invoice.currencyCode,
|
currencyCode: invoice.currencyCode,
|
||||||
money: true,
|
money: true,
|
||||||
@@ -79,7 +82,7 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
|||||||
* @param invoice
|
* @param invoice
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
protected getTransactionId(invoice) {
|
protected transactionId(invoice) {
|
||||||
return invoice.id;
|
return invoice.id;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -108,4 +111,28 @@ export class GetMatchedTransactionInvoicesTransformer extends Transformer {
|
|||||||
protected transsactionTypeFormatted(invoice) {
|
protected transsactionTypeFormatted(invoice) {
|
||||||
return 'Sale 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { sumBy } from 'lodash';
|
||||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
import { AccountNormal } from '@/interfaces';
|
||||||
|
|
||||||
export class GetMatchedTransactionManualJournalsTransformer extends Transformer {
|
export class GetMatchedTransactionManualJournalsTransformer extends Transformer {
|
||||||
/**
|
/**
|
||||||
@@ -17,6 +19,9 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
|||||||
'transactionNo',
|
'transactionNo',
|
||||||
'transactionType',
|
'transactionType',
|
||||||
'transsactionTypeFormatted',
|
'transsactionTypeFormatted',
|
||||||
|
'transactionNormal',
|
||||||
|
'referenceType',
|
||||||
|
'referenceId',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,13 +42,20 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
|||||||
return manualJournal.referenceNo;
|
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.
|
* Retrieves the manual journal amount.
|
||||||
* @param manualJournal
|
* @param manualJournal
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
protected amount(manualJournal) {
|
protected amount(manualJournal) {
|
||||||
return manualJournal.amount;
|
return Math.abs(this.total(manualJournal));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,5 +119,31 @@ export class GetMatchedTransactionManualJournalsTransformer extends Transformer
|
|||||||
protected transsactionTypeFormatted() {
|
protected transsactionTypeFormatted() {
|
||||||
return 'Manual Journal';
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
|
|||||||
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { sortClosestMatchTransactions } from './_utils';
|
import { sortClosestMatchTransactions } from './_utils';
|
||||||
|
import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow';
|
||||||
|
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class GetMatchedTransactions {
|
export class GetMatchedTransactions {
|
||||||
@@ -15,7 +17,7 @@ export class GetMatchedTransactions {
|
|||||||
private tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private getMatchedInvoicesService: GetMatchedTransactionsByExpenses;
|
private getMatchedInvoicesService: GetMatchedTransactionsByInvoices;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private getMatchedBillsService: GetMatchedTransactionsByBills;
|
private getMatchedBillsService: GetMatchedTransactionsByBills;
|
||||||
@@ -26,6 +28,9 @@ export class GetMatchedTransactions {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private getMatchedExpensesService: GetMatchedTransactionsByExpenses;
|
private getMatchedExpensesService: GetMatchedTransactionsByExpenses;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getMatchedCashflowService: GetMatchedTransactionsByCashflow;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registered matched transactions types.
|
* Registered matched transactions types.
|
||||||
*/
|
*/
|
||||||
@@ -35,6 +40,7 @@ export class GetMatchedTransactions {
|
|||||||
{ type: 'Bill', service: this.getMatchedBillsService },
|
{ type: 'Bill', service: this.getMatchedBillsService },
|
||||||
{ type: 'Expense', service: this.getMatchedExpensesService },
|
{ type: 'Expense', service: this.getMatchedExpensesService },
|
||||||
{ type: 'ManualJournal', service: this.getMatchedManualJournalService },
|
{ type: 'ManualJournal', service: this.getMatchedManualJournalService },
|
||||||
|
{ type: 'Cashflow', service: this.getMatchedCashflowService },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { initialize } from 'objection';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
|
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
|
||||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
||||||
@@ -22,10 +23,25 @@ export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
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) => {
|
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(
|
return this.transformer.transform(
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { initialize } from 'objection';
|
||||||
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||||
|
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
|
||||||
|
import { GetMatchedTransactionsFilter } from './types';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GetMatchedTransactionsByCashflow extends GetMatchedTransactionsByType {
|
||||||
|
@Inject()
|
||||||
|
private transformer: TransformerInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the matched transactions of cash flow.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {GetMatchedTransactionsFilter} filter
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getMatchedTransactions(
|
||||||
|
tenantId: number,
|
||||||
|
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
|
||||||
|
) {
|
||||||
|
const { CashflowTransaction, MatchedBankTransaction } =
|
||||||
|
this.tenancy.models(tenantId);
|
||||||
|
const knex = this.tenancy.knex(tenantId);
|
||||||
|
|
||||||
|
// Initialize the ORM models metadata.
|
||||||
|
await initialize(knex, [CashflowTransaction, MatchedBankTransaction]);
|
||||||
|
|
||||||
|
const transactions = await CashflowTransaction.query().onBuild((q) => {
|
||||||
|
// Not matched to bank transaction.
|
||||||
|
q.withGraphJoined('matchedBankTransaction');
|
||||||
|
q.whereNull('matchedBankTransaction.id');
|
||||||
|
|
||||||
|
// Not categorized.
|
||||||
|
q.modify('notCategorized');
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
transactions,
|
||||||
|
new GetMatchedTransactionCashflowTransformer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the matched transaction of cash flow.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} transactionId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async getMatchedTransaction(tenantId: number, transactionId: number) {
|
||||||
|
const { CashflowTransaction, MatchedBankTransaction } =
|
||||||
|
this.tenancy.models(tenantId);
|
||||||
|
const knex = this.tenancy.knex(tenantId);
|
||||||
|
|
||||||
|
// Initialize the ORM models metadata.
|
||||||
|
await initialize(knex, [CashflowTransaction, MatchedBankTransaction]);
|
||||||
|
|
||||||
|
const transactions = await CashflowTransaction.query()
|
||||||
|
.findById(transactionId)
|
||||||
|
.withGraphJoined('matchedBankTransaction')
|
||||||
|
.whereNull('matchedBankTransaction.id')
|
||||||
|
.modify('notCategorized')
|
||||||
|
.modify('published')
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
return this.transformer.transform(
|
||||||
|
tenantId,
|
||||||
|
transactions,
|
||||||
|
new GetMatchedTransactionCashflowTransformer()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { initialize } from 'objection';
|
||||||
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
@@ -23,22 +24,34 @@ export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByTy
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
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) => {
|
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) {
|
if (filter.fromDate) {
|
||||||
query.where('payment_date', '>=', filter.fromDate);
|
query.where('paymentDate', '>=', filter.fromDate);
|
||||||
}
|
}
|
||||||
if (filter.toDate) {
|
if (filter.toDate) {
|
||||||
query.where('payment_date', '<=', filter.toDate);
|
query.where('paymentDate', '<=', filter.toDate);
|
||||||
}
|
}
|
||||||
if (filter.minAmount) {
|
if (filter.minAmount) {
|
||||||
query.where('total_amount', '>=', filter.minAmount);
|
query.where('totalAmount', '>=', filter.minAmount);
|
||||||
}
|
}
|
||||||
if (filter.maxAmount) {
|
if (filter.maxAmount) {
|
||||||
query.where('total_amount', '<=', filter.maxAmount);
|
query.where('totalAmount', '<=', filter.maxAmount);
|
||||||
}
|
}
|
||||||
|
query.orderBy('paymentDate', 'DESC');
|
||||||
});
|
});
|
||||||
return this.transformer.transform(
|
return this.transformer.transform(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { initialize } from 'objection';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
|
||||||
import {
|
import {
|
||||||
@@ -6,7 +8,6 @@ import {
|
|||||||
MatchedTransactionsPOJO,
|
MatchedTransactionsPOJO,
|
||||||
} from './types';
|
} from './types';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -27,10 +28,27 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: GetMatchedTransactionsFilter
|
filter: GetMatchedTransactionsFilter
|
||||||
): Promise<MatchedTransactionsPOJO> {
|
): Promise<MatchedTransactionsPOJO> {
|
||||||
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) => {
|
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(
|
return this.transformer.transform(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { initialize } from 'objection';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
|
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
|
||||||
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
|
||||||
@@ -19,12 +20,26 @@ export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactio
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
|
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>
|
||||||
) {
|
) {
|
||||||
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) => {
|
const manualJournals = await ManualJournal.query().onBuild((query) => {
|
||||||
query.whereNotExists(
|
query.withGraphJoined('matchedBankTransaction');
|
||||||
ManualJournal.relatedQuery('matchedBankTransaction')
|
query.whereNull('matchedBankTransaction.id');
|
||||||
);
|
|
||||||
|
query.withGraphJoined('entries');
|
||||||
|
query.where('entries.accountId', accountId);
|
||||||
|
|
||||||
|
query.modify('filterByPublished');
|
||||||
|
|
||||||
if (filter.fromDate) {
|
if (filter.fromDate) {
|
||||||
query.where('date', '>=', filter.fromDate);
|
query.where('date', '>=', filter.fromDate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isEmpty, sumBy } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { PromisePool } from '@supercharge/promise-pool';
|
import { PromisePool } from '@supercharge/promise-pool';
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { sumMatchTranasctions } from './_utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MatchBankTransactions {
|
export class MatchBankTransactions {
|
||||||
@@ -90,9 +91,8 @@ export class MatchBankTransactions {
|
|||||||
throw new ServiceError(error);
|
throw new ServiceError(error);
|
||||||
}
|
}
|
||||||
// Calculate the total given matching transactions.
|
// Calculate the total given matching transactions.
|
||||||
const totalMatchedTranasctions = sumBy(
|
const totalMatchedTranasctions = sumMatchTranasctions(
|
||||||
validatationResult.results,
|
validatationResult.results
|
||||||
'amount'
|
|
||||||
);
|
);
|
||||||
// Validates the total given matching transcations whether is not equal
|
// Validates the total given matching transcations whether is not equal
|
||||||
// uncategorized transaction amount.
|
// uncategorized transaction amount.
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { GetMatchedTransactionsByBills } from './GetMatchedTransactionsByBills';
|
|||||||
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
import { GetMatchedTransactionsByManualJournals } from './GetMatchedTransactionsByManualJournals';
|
||||||
import { MatchTransactionsTypesRegistry } from './MatchTransactionsTypesRegistry';
|
import { MatchTransactionsTypesRegistry } from './MatchTransactionsTypesRegistry';
|
||||||
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
import { GetMatchedTransactionsByInvoices } from './GetMatchedTransactionsByInvoices';
|
||||||
|
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
|
||||||
|
import { GetMatchedTransactionsByCashflow } from './GetMatchedTransactionsByCashflow';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class MatchTransactionsTypes {
|
export class MatchTransactionsTypes {
|
||||||
@@ -25,6 +27,10 @@ export class MatchTransactionsTypes {
|
|||||||
type: 'ManualJournal',
|
type: 'ManualJournal',
|
||||||
service: GetMatchedTransactionsByManualJournals,
|
service: GetMatchedTransactionsByManualJournals,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'CashflowTransaction',
|
||||||
|
service: GetMatchedTransactionsByCashflow,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export class UnmatchMatchedBankTransaction {
|
|||||||
return this.uow.withTransaction(tenantId, async (trx) => {
|
return this.uow.withTransaction(tenantId, async (trx) => {
|
||||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatching, {
|
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatching, {
|
||||||
tenantId,
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
trx,
|
trx,
|
||||||
} as IBankTransactionUnmatchingEventPayload);
|
} as IBankTransactionUnmatchingEventPayload);
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ export class UnmatchMatchedBankTransaction {
|
|||||||
|
|
||||||
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatched, {
|
await this.eventPublisher.emitAsync(events.bankMatch.onUnmatched, {
|
||||||
tenantId,
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
trx,
|
trx,
|
||||||
} as IBankTransactionUnmatchingEventPayload);
|
} as IBankTransactionUnmatchingEventPayload);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { ERRORS } from './types';
|
import { ERRORS } from './types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -18,12 +19,13 @@ export class ValidateTransactionMatched {
|
|||||||
public async validateTransactionNoMatchLinking(
|
public async validateTransactionNoMatchLinking(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
referenceType: string,
|
referenceType: string,
|
||||||
referenceId: number
|
referenceId: number,
|
||||||
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
const { MatchedBankTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const foundMatchedTransaction =
|
const foundMatchedTransaction =
|
||||||
await MatchedBankTransaction.query().findOne({
|
await MatchedBankTransaction.query(trx).findOne({
|
||||||
referenceType,
|
referenceType,
|
||||||
referenceId,
|
referenceId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,3 +20,12 @@ export const sortClosestMatchTransactions = (
|
|||||||
),
|
),
|
||||||
])(matches);
|
])(matches);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sumMatchTranasctions = (transactions: Array<any>) => {
|
||||||
|
return transactions.reduce(
|
||||||
|
(total, item) =>
|
||||||
|
total +
|
||||||
|
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import {
|
||||||
|
IBankTransactionMatchedEventPayload,
|
||||||
|
IBankTransactionUnmatchedEventPayload,
|
||||||
|
} from '../types';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class DecrementUncategorizedTransactionOnMatching {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
*/
|
||||||
|
public attach(bus) {
|
||||||
|
bus.subscribe(
|
||||||
|
events.bankMatch.onMatched,
|
||||||
|
this.decrementUnCategorizedTransactionsOnMatching.bind(this)
|
||||||
|
);
|
||||||
|
bus.subscribe(
|
||||||
|
events.bankMatch.onUnmatched,
|
||||||
|
this.incrementUnCategorizedTransactionsOnUnmatching.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||||
|
* @param {IManualJournalDeletingPayload}
|
||||||
|
*/
|
||||||
|
public async decrementUnCategorizedTransactionsOnMatching({
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
trx,
|
||||||
|
}: IBankTransactionMatchedEventPayload) {
|
||||||
|
const { UncategorizedCashflowTransaction, Account } =
|
||||||
|
this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||||
|
uncategorizedTransactionId
|
||||||
|
);
|
||||||
|
await Account.query(trx)
|
||||||
|
.findById(transaction.accountId)
|
||||||
|
.decrement('uncategorizedTransactions', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||||
|
* @param {IManualJournalDeletingPayload}
|
||||||
|
*/
|
||||||
|
public async incrementUnCategorizedTransactionsOnUnmatching({
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransactionId,
|
||||||
|
trx,
|
||||||
|
}: IBankTransactionUnmatchedEventPayload) {
|
||||||
|
const { UncategorizedCashflowTransaction, Account } =
|
||||||
|
this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const transaction = await UncategorizedCashflowTransaction.query().findById(
|
||||||
|
uncategorizedTransactionId
|
||||||
|
);
|
||||||
|
await Account.query(trx)
|
||||||
|
.findById(transaction.accountId)
|
||||||
|
.increment('uncategorizedTransactions', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { IManualJournalDeletingPayload } from '@/interfaces';
|
import { ICommandCashflowDeletingPayload, IManualJournalDeletingPayload } from '@/interfaces';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
|
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
|
||||||
|
|
||||||
@@ -24,13 +24,14 @@ export class ValidateMatchingOnCashflowDelete {
|
|||||||
*/
|
*/
|
||||||
public async validateMatchingOnCashflowDeleting({
|
public async validateMatchingOnCashflowDeleting({
|
||||||
tenantId,
|
tenantId,
|
||||||
oldManualJournal,
|
oldCashflowTransaction,
|
||||||
trx,
|
trx,
|
||||||
}: IManualJournalDeletingPayload) {
|
}: ICommandCashflowDeletingPayload) {
|
||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'ManualJournal',
|
'CashflowTransaction',
|
||||||
oldManualJournal.id
|
oldCashflowTransaction.id,
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnExpenseDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'Expense',
|
'Expense',
|
||||||
oldExpense.id
|
oldExpense.id,
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnManualJournalDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'ManualJournal',
|
'ManualJournal',
|
||||||
oldManualJournal.id
|
oldManualJournal.id,
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export class ValidateMatchingOnPaymentMadeDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'PaymentMade',
|
'PaymentMade',
|
||||||
oldBillPayment.id
|
oldBillPayment.id,
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export class ValidateMatchingOnPaymentReceivedDelete {
|
|||||||
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
|
||||||
tenantId,
|
tenantId,
|
||||||
'PaymentReceive',
|
'PaymentReceive',
|
||||||
oldPaymentReceive.id
|
oldPaymentReceive.id,
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ export interface IBankTransactionMatchedEventPayload {
|
|||||||
|
|
||||||
export interface IBankTransactionUnmatchingEventPayload {
|
export interface IBankTransactionUnmatchingEventPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBankTransactionUnmatchedEventPayload {
|
export interface IBankTransactionUnmatchedEventPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
|
uncategorizedTransactionId: number;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMatchTransactionDTO {
|
export interface IMatchTransactionDTO {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTra
|
|||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { uniqid } from 'uniqid';
|
import uniqid from 'uniqid';
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ export class RegonizeTransactionsJob {
|
|||||||
* Triggers sending invoice mail.
|
* Triggers sending invoice mail.
|
||||||
*/
|
*/
|
||||||
private handler = async (job, done: Function) => {
|
private handler = async (job, done: Function) => {
|
||||||
const { tenantId } = job.attrs.data;
|
const { tenantId, batch } = job.attrs.data;
|
||||||
const regonizeTransactions = Container.get(RecognizeTranasctionsService);
|
const regonizeTransactions = Container.get(RecognizeTranasctionsService);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await regonizeTransactions.recognizeTransactions(tenantId);
|
await regonizeTransactions.recognizeTransactions(tenantId, batch);
|
||||||
done();
|
done();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
IBankRuleEventDeletedPayload,
|
IBankRuleEventDeletedPayload,
|
||||||
IBankRuleEventEditedPayload,
|
IBankRuleEventEditedPayload,
|
||||||
} from '../../Rules/types';
|
} from '../../Rules/types';
|
||||||
|
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||||
|
import { Import } from '@/system/models';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class TriggerRecognizedTransactions {
|
export class TriggerRecognizedTransactions {
|
||||||
@@ -27,6 +29,10 @@ export class TriggerRecognizedTransactions {
|
|||||||
events.bankRules.onDeleted,
|
events.bankRules.onDeleted,
|
||||||
this.recognizedTransactionsOnRuleDeleted.bind(this)
|
this.recognizedTransactionsOnRuleDeleted.bind(this)
|
||||||
);
|
);
|
||||||
|
bus.subscribe(
|
||||||
|
events.import.onImportCommitted,
|
||||||
|
this.triggerRecognizeTransactionsOnImportCommitted.bind(this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,4 +79,20 @@ export class TriggerRecognizedTransactions {
|
|||||||
const payload = { tenantId };
|
const payload = { tenantId };
|
||||||
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the recognize bank transactions once the imported file commit.
|
||||||
|
* @param {IImportFileCommitedEventPayload} payload -
|
||||||
|
*/
|
||||||
|
private async triggerRecognizeTransactionsOnImportCommitted({
|
||||||
|
tenantId,
|
||||||
|
importId,
|
||||||
|
meta,
|
||||||
|
}: IImportFileCommitedEventPayload) {
|
||||||
|
const importFile = await Import.query().findOne({ importId });
|
||||||
|
const batch = importFile.paramsParsed.batch;
|
||||||
|
const payload = { tenantId, batch };
|
||||||
|
|
||||||
|
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { upperFirst, camelCase } from 'lodash';
|
|
||||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
import { getCashflowTransactionFormattedType } from '@/utils/transactions-types';
|
||||||
|
|
||||||
export class GetBankRulesTransformer extends Transformer {
|
export class GetBankRulesTransformer extends Transformer {
|
||||||
/**
|
/**
|
||||||
@@ -29,8 +28,7 @@ export class GetBankRulesTransformer extends Transformer {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
protected assignCategoryFormatted(bankRule: any) {
|
protected assignCategoryFormatted(bankRule: any) {
|
||||||
const assignCategory = upperFirst(camelCase(bankRule.assignCategory));
|
return getCashflowTransactionFormattedType(bankRule.assignCategory);
|
||||||
return getTransactionTypeLabel(assignCategory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,11 +34,12 @@ export default class CashflowTransactionJournalEntries {
|
|||||||
currencyCode: transaction.currencyCode,
|
currencyCode: transaction.currencyCode,
|
||||||
exchangeRate: transaction.exchangeRate,
|
exchangeRate: transaction.exchangeRate,
|
||||||
|
|
||||||
transactionType: transformCashflowTransactionType(
|
transactionType: 'CashflowTransaction',
|
||||||
transaction.transactionType
|
|
||||||
),
|
|
||||||
transactionId: transaction.id,
|
transactionId: transaction.id,
|
||||||
transactionNumber: transaction.transactionNumber,
|
transactionNumber: transaction.transactionNumber,
|
||||||
|
transactionSubType: transformCashflowTransactionType(
|
||||||
|
transaction.transactionType
|
||||||
|
),
|
||||||
referenceNumber: transaction.referenceNo,
|
referenceNumber: transaction.referenceNo,
|
||||||
|
|
||||||
note: transaction.description,
|
note: transaction.description,
|
||||||
@@ -161,12 +162,10 @@ export default class CashflowTransactionJournalEntries {
|
|||||||
cashflowTransactionId: number,
|
cashflowTransactionId: number,
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const transactionTypes = getCashflowAccountTransactionsTypes();
|
|
||||||
|
|
||||||
await this.ledgerStorage.deleteByReference(
|
await this.ledgerStorage.deleteByReference(
|
||||||
tenantId,
|
tenantId,
|
||||||
cashflowTransactionId,
|
cashflowTransactionId,
|
||||||
transactionTypes,
|
'CashflowTransaction',
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,20 +84,23 @@ export class CategorizeCashflowTransaction {
|
|||||||
cashflowTransactionDTO
|
cashflowTransactionDTO
|
||||||
);
|
);
|
||||||
// Updates the uncategorized transaction as categorized.
|
// Updates the uncategorized transaction as categorized.
|
||||||
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
|
const uncategorizedTransaction =
|
||||||
uncategorizedTransactionId,
|
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
|
||||||
{
|
uncategorizedTransactionId,
|
||||||
categorized: true,
|
{
|
||||||
categorizeRefType: 'CashflowTransaction',
|
categorized: true,
|
||||||
categorizeRefId: cashflowTransaction.id,
|
categorizeRefType: 'CashflowTransaction',
|
||||||
}
|
categorizeRefId: cashflowTransaction.id,
|
||||||
);
|
}
|
||||||
|
);
|
||||||
// Triggers `onCashflowTransactionCategorized` event.
|
// Triggers `onCashflowTransactionCategorized` event.
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.cashflow.onTransactionCategorized,
|
events.cashflow.onTransactionCategorized,
|
||||||
{
|
{
|
||||||
tenantId,
|
tenantId,
|
||||||
// cashflowTransaction,
|
cashflowTransaction,
|
||||||
|
uncategorizedTransaction,
|
||||||
|
categorizeDTO,
|
||||||
trx,
|
trx,
|
||||||
} as ICashflowTransactionCategorizedPayload
|
} as ICashflowTransactionCategorizedPayload
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export default class NewCashflowTransactionService {
|
|||||||
...fromDTO,
|
...fromDTO,
|
||||||
transactionNumber,
|
transactionNumber,
|
||||||
currencyCode: cashflowAccount.currencyCode,
|
currencyCode: cashflowAccount.currencyCode,
|
||||||
|
exchangeRate: fromDTO?.exchangeRate || 1,
|
||||||
transactionType: transformCashflowTransactionType(
|
transactionType: transformCashflowTransactionType(
|
||||||
fromDTO.transactionType
|
fromDTO.transactionType
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
|
import uniqid from 'uniqid';
|
||||||
import { Importable } from '../Import/Importable';
|
import { Importable } from '../Import/Importable';
|
||||||
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
|
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
|
||||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||||
@@ -15,6 +16,7 @@ export class UncategorizedTransactionsImportable extends Importable {
|
|||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private tenancy: HasTenancyService;
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passing the sheet DTO to create uncategorized transaction.
|
* Passing the sheet DTO to create uncategorized transaction.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -43,6 +45,7 @@ export class UncategorizedTransactionsImportable extends Importable {
|
|||||||
return {
|
return {
|
||||||
...createDTO,
|
...createDTO,
|
||||||
accountId: context.import.paramsParsed.accountId,
|
accountId: context.import.paramsParsed.accountId,
|
||||||
|
batch: context.import.paramsParsed.batch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +57,9 @@ export class UncategorizedTransactionsImportable extends Importable {
|
|||||||
return BankTransactionsSampleData;
|
return BankTransactionsSampleData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------
|
||||||
|
// # Params
|
||||||
|
// ------------------
|
||||||
/**
|
/**
|
||||||
* Params validation schema.
|
* Params validation schema.
|
||||||
* @returns {ValidationSchema[]}
|
* @returns {ValidationSchema[]}
|
||||||
@@ -79,4 +85,17 @@ export class UncategorizedTransactionsImportable extends Importable {
|
|||||||
await Account.query().findById(params.accountId).throwIfNotFound({});
|
await Account.query().findById(params.accountId).throwIfNotFound({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes the import params before storing them.
|
||||||
|
* @param {Record<string, any>} parmas
|
||||||
|
*/
|
||||||
|
public transformParams(parmas: Record<string, any>) {
|
||||||
|
const batch = uniqid();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...parmas,
|
||||||
|
batch,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import {
|
||||||
|
ICashflowTransactionCategorizedPayload,
|
||||||
|
ICashflowTransactionUncategorizedPayload,
|
||||||
|
} from '@/interfaces';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class DecrementUncategorizedTransactionOnCategorize {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
*/
|
||||||
|
public attach(bus) {
|
||||||
|
bus.subscribe(
|
||||||
|
events.cashflow.onTransactionCategorized,
|
||||||
|
this.decrementUnCategorizedTransactionsOnCategorized.bind(this)
|
||||||
|
);
|
||||||
|
bus.subscribe(
|
||||||
|
events.cashflow.onTransactionUncategorized,
|
||||||
|
this.incrementUnCategorizedTransactionsOnUncategorized.bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrement the uncategoirzed transactions on the account once categorizing.
|
||||||
|
* @param {ICashflowTransactionCategorizedPayload}
|
||||||
|
*/
|
||||||
|
public async decrementUnCategorizedTransactionsOnCategorized({
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransaction,
|
||||||
|
}: ICashflowTransactionCategorizedPayload) {
|
||||||
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
await Account.query()
|
||||||
|
.findById(uncategorizedTransaction.accountId)
|
||||||
|
.decrement('uncategorizedTransactions', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the uncategorized transaction on the given account on uncategorizing.
|
||||||
|
* @param {IManualJournalDeletingPayload}
|
||||||
|
*/
|
||||||
|
public async incrementUnCategorizedTransactionsOnUncategorized({
|
||||||
|
tenantId,
|
||||||
|
uncategorizedTransaction,
|
||||||
|
}: ICashflowTransactionUncategorizedPayload) {
|
||||||
|
const { Account } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
await Account.query()
|
||||||
|
.findById(uncategorizedTransaction.accountId)
|
||||||
|
.increment('uncategorizedTransactions', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import R from 'ramda';
|
import R from 'ramda';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { first, isEmpty } from 'lodash';
|
||||||
import {
|
import {
|
||||||
ICashflowAccountTransaction,
|
ICashflowAccountTransaction,
|
||||||
ICashflowAccountTransactionsQuery,
|
ICashflowAccountTransactionsQuery,
|
||||||
INumberFormatQuery,
|
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import FinancialSheet from '../FinancialSheet';
|
import FinancialSheet from '../FinancialSheet';
|
||||||
import { runningAmount } from 'utils';
|
import { runningAmount } from 'utils';
|
||||||
|
import { CashflowAccountTransactionsRepo } from './CashflowAccountTransactionsRepo';
|
||||||
|
import { BankTransactionStatus } from './constants';
|
||||||
|
import { formatBankTransactionsStatus } from './utils';
|
||||||
|
|
||||||
export default class CashflowAccountTransactionReport extends FinancialSheet {
|
export class CashflowAccountTransactionReport extends FinancialSheet {
|
||||||
private transactions: any;
|
|
||||||
private openingBalance: number;
|
|
||||||
private runningBalance: any;
|
private runningBalance: any;
|
||||||
private numberFormat: INumberFormatQuery;
|
|
||||||
private baseCurrency: string;
|
|
||||||
private query: ICashflowAccountTransactionsQuery;
|
private query: ICashflowAccountTransactionsQuery;
|
||||||
|
private repo: CashflowAccountTransactionsRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
@@ -23,19 +23,61 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
|||||||
* @param {ICashflowAccountTransactionsQuery} query -
|
* @param {ICashflowAccountTransactionsQuery} query -
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
transactions,
|
repo: CashflowAccountTransactionsRepo,
|
||||||
openingBalance: number,
|
|
||||||
query: ICashflowAccountTransactionsQuery
|
query: ICashflowAccountTransactionsQuery
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.transactions = transactions;
|
this.repo = repo;
|
||||||
this.openingBalance = openingBalance;
|
|
||||||
|
|
||||||
this.runningBalance = runningAmount(this.openingBalance);
|
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.numberFormat = query.numberFormat;
|
this.runningBalance = runningAmount(this.repo.openingBalance);
|
||||||
this.baseCurrency = 'USD';
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the transaction status.
|
||||||
|
* @param {} transaction
|
||||||
|
* @returns {BankTransactionStatus}
|
||||||
|
*/
|
||||||
|
private getTransactionStatus(transaction: any): BankTransactionStatus {
|
||||||
|
const categorizedTrans = this.repo.uncategorizedTransactionsMapByRef.get(
|
||||||
|
`${transaction.referenceType}-${transaction.referenceId}`
|
||||||
|
);
|
||||||
|
const matchedTrans = this.repo.matchedBankTransactionsMapByRef.get(
|
||||||
|
`${transaction.referenceType}-${transaction.referenceId}`
|
||||||
|
);
|
||||||
|
if (!isEmpty(categorizedTrans)) {
|
||||||
|
return BankTransactionStatus.Categorized;
|
||||||
|
} else if (!isEmpty(matchedTrans)) {
|
||||||
|
return BankTransactionStatus.Matched;
|
||||||
|
} else {
|
||||||
|
return BankTransactionStatus.Manual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the uncategoized transaction id from the given transaction.
|
||||||
|
* @param transaction
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
private getUncategorizedTransId(transaction: any): number {
|
||||||
|
// The given transaction would be categorized, matched or not, so we'd take a look at
|
||||||
|
// the categorized transaction first to get the id if not exist, then should look at the matched
|
||||||
|
// transaction if not exist too, so the given transaction has no uncategorized transaction id.
|
||||||
|
const categorizedTrans = this.repo.uncategorizedTransactionsMapByRef.get(
|
||||||
|
`${transaction.referenceType}-${transaction.referenceId}`
|
||||||
|
);
|
||||||
|
const matchedTrans = this.repo.matchedBankTransactionsMapByRef.get(
|
||||||
|
`${transaction.referenceType}-${transaction.referenceId}`
|
||||||
|
);
|
||||||
|
// Relation between the transaction and matching always been one-to-one.
|
||||||
|
const firstCategorizedTrans = first(categorizedTrans);
|
||||||
|
const firstMatchedTrans = first(matchedTrans);
|
||||||
|
|
||||||
|
return (
|
||||||
|
firstCategorizedTrans?.id ||
|
||||||
|
firstMatchedTrans?.uncategorizedTransactionId ||
|
||||||
|
null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +86,10 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
|||||||
* @returns {ICashflowAccountTransaction}
|
* @returns {ICashflowAccountTransaction}
|
||||||
*/
|
*/
|
||||||
private transactionNode = (transaction: any): ICashflowAccountTransaction => {
|
private transactionNode = (transaction: any): ICashflowAccountTransaction => {
|
||||||
|
const status = this.getTransactionStatus(transaction);
|
||||||
|
const uncategorizedTransactionId =
|
||||||
|
this.getUncategorizedTransId(transaction);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date: transaction.date,
|
date: transaction.date,
|
||||||
formattedDate: moment(transaction.date).format('YYYY-MM-DD'),
|
formattedDate: moment(transaction.date).format('YYYY-MM-DD'),
|
||||||
@@ -67,6 +113,9 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
|||||||
|
|
||||||
balance: 0,
|
balance: 0,
|
||||||
formattedBalance: '',
|
formattedBalance: '',
|
||||||
|
status,
|
||||||
|
formattedStatus: formatBankTransactionsStatus(status),
|
||||||
|
uncategorizedTransactionId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,6 +195,6 @@ export default class CashflowAccountTransactionReport extends FinancialSheet {
|
|||||||
* @returns {ICashflowAccountTransaction[]}
|
* @returns {ICashflowAccountTransaction[]}
|
||||||
*/
|
*/
|
||||||
public reportData(): ICashflowAccountTransaction[] {
|
public reportData(): ICashflowAccountTransaction[] {
|
||||||
return this.transactionsNode(this.transactions);
|
return this.transactionsNode(this.repo.transactions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,59 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import * as R from 'ramda';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import { ICashflowAccountTransactionsQuery } from '@/interfaces';
|
||||||
import { ICashflowAccountTransactionsQuery, IPaginationMeta } from '@/interfaces';
|
import {
|
||||||
|
groupMatchedBankTransactions,
|
||||||
|
groupUncategorizedTransactions,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
@Service()
|
export class CashflowAccountTransactionsRepo {
|
||||||
export default class CashflowAccountTransactionsRepo {
|
private models: any;
|
||||||
@Inject()
|
public query: ICashflowAccountTransactionsQuery;
|
||||||
private tenancy: HasTenancyService;
|
public transactions: any;
|
||||||
|
public uncategorizedTransactions: any;
|
||||||
|
public uncategorizedTransactionsMapByRef: Map<string, any>;
|
||||||
|
public matchedBankTransactions: any;
|
||||||
|
public matchedBankTransactionsMapByRef: Map<string, any>;
|
||||||
|
public pagination: any;
|
||||||
|
public openingBalance: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param {any} models
|
||||||
|
* @param {ICashflowAccountTransactionsQuery} query
|
||||||
|
*/
|
||||||
|
constructor(models: any, query: ICashflowAccountTransactionsQuery) {
|
||||||
|
this.models = models;
|
||||||
|
this.query = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Async initalize the resources.
|
||||||
|
*/
|
||||||
|
async asyncInit() {
|
||||||
|
await this.initCashflowAccountTransactions();
|
||||||
|
await this.initCashflowAccountOpeningBalance();
|
||||||
|
await this.initCategorizedTransactions();
|
||||||
|
await this.initMatchedTransactions();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the cashflow account transactions.
|
* Retrieve the cashflow account transactions.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {ICashflowAccountTransactionsQuery} query -
|
* @param {ICashflowAccountTransactionsQuery} query -
|
||||||
*/
|
*/
|
||||||
async getCashflowAccountTransactions(
|
async initCashflowAccountTransactions() {
|
||||||
tenantId: number,
|
const { AccountTransaction } = this.models;
|
||||||
query: ICashflowAccountTransactionsQuery
|
|
||||||
) {
|
|
||||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
return AccountTransaction.query()
|
const { results, pagination } = await AccountTransaction.query()
|
||||||
.where('account_id', query.accountId)
|
.where('account_id', this.query.accountId)
|
||||||
.orderBy([
|
.orderBy([
|
||||||
{ column: 'date', order: 'desc' },
|
{ column: 'date', order: 'desc' },
|
||||||
{ column: 'created_at', order: 'desc' },
|
{ column: 'created_at', order: 'desc' },
|
||||||
])
|
])
|
||||||
.pagination(query.page - 1, query.pageSize);
|
.pagination(this.query.page - 1, this.query.pageSize);
|
||||||
|
|
||||||
|
this.transactions = results;
|
||||||
|
this.pagination = pagination;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,22 +63,18 @@ export default class CashflowAccountTransactionsRepo {
|
|||||||
* @param {IPaginationMeta} pagination
|
* @param {IPaginationMeta} pagination
|
||||||
* @return {Promise<number>}
|
* @return {Promise<number>}
|
||||||
*/
|
*/
|
||||||
async getCashflowAccountOpeningBalance(
|
async initCashflowAccountOpeningBalance(): Promise<void> {
|
||||||
tenantId: number,
|
const { AccountTransaction } = this.models;
|
||||||
accountId: number,
|
|
||||||
pagination: IPaginationMeta
|
|
||||||
): Promise<number> {
|
|
||||||
const { AccountTransaction } = this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
// Retrieve the opening balance of credit and debit balances.
|
// Retrieve the opening balance of credit and debit balances.
|
||||||
const openingBalancesSubquery = AccountTransaction.query()
|
const openingBalancesSubquery = AccountTransaction.query()
|
||||||
.where('account_id', accountId)
|
.where('account_id', this.query.accountId)
|
||||||
.orderBy([
|
.orderBy([
|
||||||
{ column: 'date', order: 'desc' },
|
{ column: 'date', order: 'desc' },
|
||||||
{ column: 'created_at', order: 'desc' },
|
{ column: 'created_at', order: 'desc' },
|
||||||
])
|
])
|
||||||
.limit(pagination.total)
|
.limit(this.pagination.total)
|
||||||
.offset(pagination.pageSize * (pagination.page - 1));
|
.offset(this.pagination.pageSize * (this.pagination.page - 1));
|
||||||
|
|
||||||
// Sumation of credit and debit balance.
|
// Sumation of credit and debit balance.
|
||||||
const openingBalances = await AccountTransaction.query()
|
const openingBalances = await AccountTransaction.query()
|
||||||
@@ -60,6 +85,43 @@ export default class CashflowAccountTransactionsRepo {
|
|||||||
|
|
||||||
const openingBalance = openingBalances.debit - openingBalances.credit;
|
const openingBalance = openingBalances.debit - openingBalances.credit;
|
||||||
|
|
||||||
return openingBalance;
|
this.openingBalance = openingBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the uncategorized transactions of the bank account.
|
||||||
|
*/
|
||||||
|
async initCategorizedTransactions() {
|
||||||
|
const { UncategorizedCashflowTransaction } = this.models;
|
||||||
|
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
|
||||||
|
|
||||||
|
const uncategorizedTransactions =
|
||||||
|
await UncategorizedCashflowTransaction.query().whereIn(
|
||||||
|
['categorizeRefType', 'categorizeRefId'],
|
||||||
|
refs
|
||||||
|
);
|
||||||
|
|
||||||
|
this.uncategorizedTransactions = uncategorizedTransactions;
|
||||||
|
this.uncategorizedTransactionsMapByRef = groupUncategorizedTransactions(
|
||||||
|
uncategorizedTransactions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the matched bank transactions of the bank account.
|
||||||
|
*/
|
||||||
|
async initMatchedTransactions(): Promise<void> {
|
||||||
|
const { MatchedBankTransaction } = this.models;
|
||||||
|
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
|
||||||
|
|
||||||
|
const matchedBankTransactions =
|
||||||
|
await MatchedBankTransaction.query().whereIn(
|
||||||
|
['referenceType', 'referenceId'],
|
||||||
|
refs
|
||||||
|
);
|
||||||
|
this.matchedBankTransactions = matchedBankTransactions;
|
||||||
|
this.matchedBankTransactionsMapByRef = groupMatchedBankTransactions(
|
||||||
|
matchedBankTransactions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,16 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import { includes } from 'lodash';
|
|
||||||
import * as qim from 'qim';
|
import * as qim from 'qim';
|
||||||
import { ICashflowAccountTransactionsQuery, IAccount } from '@/interfaces';
|
import { ICashflowAccountTransactionsQuery, IAccount } from '@/interfaces';
|
||||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import FinancialSheet from '../FinancialSheet';
|
import FinancialSheet from '../FinancialSheet';
|
||||||
import CashflowAccountTransactionsRepo from './CashflowAccountTransactionsRepo';
|
import { CashflowAccountTransactionReport } from './CashflowAccountTransactions';
|
||||||
import CashflowAccountTransactionsReport from './CashflowAccountTransactions';
|
|
||||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
import { ERRORS } from './constants';
|
|
||||||
import I18nService from '@/services/I18n/I18nService';
|
import I18nService from '@/services/I18n/I18nService';
|
||||||
|
import { CashflowAccountTransactionsRepo } from './CashflowAccountTransactionsRepo';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CashflowAccountTransactionsService extends FinancialSheet {
|
export default class CashflowAccountTransactionsService extends FinancialSheet {
|
||||||
@Inject()
|
@Inject()
|
||||||
tenancy: TenancyService;
|
private tenancy: TenancyService;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
cashflowTransactionsRepo: CashflowAccountTransactionsRepo;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
i18nService: I18nService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defaults balance sheet filter query.
|
* Defaults balance sheet filter query.
|
||||||
@@ -50,59 +40,24 @@ export default class CashflowAccountTransactionsService extends FinancialSheet {
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
query: ICashflowAccountTransactionsQuery
|
query: ICashflowAccountTransactionsQuery
|
||||||
) {
|
) {
|
||||||
const { Account } = this.tenancy.models(tenantId);
|
const models = this.tenancy.models(tenantId);
|
||||||
const parsedQuery = { ...this.defaultQuery, ...query };
|
const parsedQuery = { ...this.defaultQuery, ...query };
|
||||||
|
|
||||||
// Retrieve the given account or throw not found service error.
|
// Initalize the bank transactions report repository.
|
||||||
const account = await Account.query().findById(parsedQuery.accountId);
|
const cashflowTransactionsRepo = new CashflowAccountTransactionsRepo(
|
||||||
|
models,
|
||||||
// Validates the cashflow account type.
|
|
||||||
this.validateCashflowAccountType(account);
|
|
||||||
|
|
||||||
// Retrieve the cashflow account transactions.
|
|
||||||
const { results: transactions, pagination } =
|
|
||||||
await this.cashflowTransactionsRepo.getCashflowAccountTransactions(
|
|
||||||
tenantId,
|
|
||||||
parsedQuery
|
|
||||||
);
|
|
||||||
// Retrieve the cashflow account opening balance.
|
|
||||||
const openingBalance =
|
|
||||||
await this.cashflowTransactionsRepo.getCashflowAccountOpeningBalance(
|
|
||||||
tenantId,
|
|
||||||
parsedQuery.accountId,
|
|
||||||
pagination
|
|
||||||
);
|
|
||||||
// Retrieve the computed report.
|
|
||||||
const report = new CashflowAccountTransactionsReport(
|
|
||||||
transactions,
|
|
||||||
openingBalance,
|
|
||||||
parsedQuery
|
parsedQuery
|
||||||
);
|
);
|
||||||
const reportTranasctions = report.reportData();
|
await cashflowTransactionsRepo.asyncInit();
|
||||||
|
|
||||||
return {
|
// Retrieve the computed report.
|
||||||
transactions: this.i18nService.i18nApply(
|
const report = new CashflowAccountTransactionReport(
|
||||||
[[qim.$each, 'formattedTransactionType']],
|
cashflowTransactionsRepo,
|
||||||
reportTranasctions,
|
parsedQuery
|
||||||
tenantId
|
);
|
||||||
),
|
const transactions = report.reportData();
|
||||||
pagination,
|
const pagination = cashflowTransactionsRepo.pagination;
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return { transactions, pagination };
|
||||||
* Validates the cashflow account type.
|
|
||||||
* @param {IAccount} account -
|
|
||||||
*/
|
|
||||||
private validateCashflowAccountType(account: IAccount) {
|
|
||||||
const cashflowTypes = [
|
|
||||||
ACCOUNT_TYPE.CASH,
|
|
||||||
ACCOUNT_TYPE.CREDIT_CARD,
|
|
||||||
ACCOUNT_TYPE.BANK,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!includes(cashflowTypes, account.accountType)) {
|
|
||||||
throw new ServiceError(ERRORS.ACCOUNT_ID_HAS_INVALID_TYPE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
|
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum BankTransactionStatus {
|
||||||
|
Categorized = 'categorized',
|
||||||
|
Matched = 'matched',
|
||||||
|
Manual = 'manual',
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
|
||||||
|
export const groupUncategorizedTransactions = (
|
||||||
|
uncategorizedTransactions: any
|
||||||
|
): Map<string, any> => {
|
||||||
|
return new Map(
|
||||||
|
R.toPairs(
|
||||||
|
R.groupBy(
|
||||||
|
(transaction) =>
|
||||||
|
`${transaction.categorizeRefType}-${transaction.categorizeRefId}`,
|
||||||
|
uncategorizedTransactions
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const groupMatchedBankTransactions = (
|
||||||
|
uncategorizedTransactions: any
|
||||||
|
): Map<string, any> => {
|
||||||
|
return new Map(
|
||||||
|
R.toPairs(
|
||||||
|
R.groupBy(
|
||||||
|
(transaction) =>
|
||||||
|
`${transaction.referenceType}-${transaction.referenceId}`,
|
||||||
|
uncategorizedTransactions
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatBankTransactionsStatus = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'categorized':
|
||||||
|
return 'Categorized';
|
||||||
|
case 'matched':
|
||||||
|
return 'Matched';
|
||||||
|
case 'manual':
|
||||||
|
return 'Manual';
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -15,14 +15,10 @@ import { ServiceError } from '@/exceptions';
|
|||||||
import { getUniqueImportableValue, trimObject } from './_utils';
|
import { getUniqueImportableValue, trimObject } from './_utils';
|
||||||
import { ImportableResources } from './ImportableResources';
|
import { ImportableResources } from './ImportableResources';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
|
||||||
import { Import } from '@/system/models';
|
import { Import } from '@/system/models';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileCommon {
|
export class ImportFileCommon {
|
||||||
@Inject()
|
|
||||||
private tenancy: HasTenancyService;
|
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private importFileValidator: ImportFileDataValidator;
|
private importFileValidator: ImportFileDataValidator;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
|
import { ImportFilePreviewPOJO } from './interfaces';
|
||||||
|
import { ImportFileProcess } from './ImportFileProcess';
|
||||||
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
import events from '@/subscribers/events';
|
||||||
|
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class ImportFileProcessCommit {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private importFile: ImportFileProcess;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits the imported file.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} importId
|
||||||
|
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||||
|
*/
|
||||||
|
public async commit(
|
||||||
|
tenantId: number,
|
||||||
|
importId: number
|
||||||
|
): Promise<ImportFilePreviewPOJO> {
|
||||||
|
const knex = this.tenancy.knex(tenantId);
|
||||||
|
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
|
||||||
|
|
||||||
|
const meta = await this.importFile.import(tenantId, importId, trx);
|
||||||
|
|
||||||
|
// Commit the successed transaction.
|
||||||
|
await trx.commit();
|
||||||
|
|
||||||
|
// Triggers `onImportFileCommitted` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.import.onImportCommitted, {
|
||||||
|
meta,
|
||||||
|
importId,
|
||||||
|
tenantId,
|
||||||
|
} as IImportFileCommitedEventPayload);
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { ImportFileProcess } from './ImportFileProcess';
|
|||||||
import { ImportFilePreview } from './ImportFilePreview';
|
import { ImportFilePreview } from './ImportFilePreview';
|
||||||
import { ImportSampleService } from './ImportSample';
|
import { ImportSampleService } from './ImportSample';
|
||||||
import { ImportFileMeta } from './ImportFileMeta';
|
import { ImportFileMeta } from './ImportFileMeta';
|
||||||
|
import { ImportFileProcessCommit } from './ImportFileProcessCommit';
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
export class ImportResourceApplication {
|
export class ImportResourceApplication {
|
||||||
@@ -27,6 +28,9 @@ export class ImportResourceApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private importMetaService: ImportFileMeta;
|
private importMetaService: ImportFileMeta;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private importProcessCommit: ImportFileProcessCommit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the imported file and stores the import file meta under unqiue id.
|
* Reads the imported file and stores the import file meta under unqiue id.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
@@ -74,12 +78,12 @@ export class ImportResourceApplication {
|
|||||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||||
*/
|
*/
|
||||||
public async process(tenantId: number, importId: number) {
|
public async process(tenantId: number, importId: number) {
|
||||||
return this.importProcessService.import(tenantId, importId);
|
return this.importProcessCommit.commit(tenantId, importId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the import meta of the given import id.
|
* Retrieves the import meta of the given import id.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {string} importId - Import id.
|
* @param {string} importId - Import id.
|
||||||
* @returns {}
|
* @returns {}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -639,4 +639,17 @@ export default {
|
|||||||
onUnmatching: 'onBankTransactionUnmathcing',
|
onUnmatching: 'onBankTransactionUnmathcing',
|
||||||
onUnmatched: 'onBankTransactionUnmathced',
|
onUnmatched: 'onBankTransactionUnmathced',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
bankTransactions: {
|
||||||
|
onExcluding: 'onBankTransactionExclude',
|
||||||
|
onExcluded: 'onBankTransactionExcluded',
|
||||||
|
|
||||||
|
onUnexcluding: 'onBankTransactionUnexcluding',
|
||||||
|
onUnexcluded: 'onBankTransactionUnexcluded',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Import files.
|
||||||
|
import: {
|
||||||
|
onImportCommitted: 'onImportFileCommitted',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,36 @@
|
|||||||
import { TransactionTypes } from '@/data/TransactionTypes';
|
import { isObject, upperFirst, camelCase } from 'lodash';
|
||||||
|
import {
|
||||||
|
TransactionTypes,
|
||||||
|
CashflowTransactionTypes,
|
||||||
|
} from '@/data/TransactionTypes';
|
||||||
|
|
||||||
export const getTransactionTypeLabel = (transactionType: string) => {
|
/**
|
||||||
return TransactionTypes[transactionType];
|
* Retrieves the formatted type of account transaction.
|
||||||
|
* @param {string} referenceType
|
||||||
|
* @param {string} transactionType
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export const getTransactionTypeLabel = (
|
||||||
|
referenceType: string,
|
||||||
|
transactionType?: string
|
||||||
|
) => {
|
||||||
|
const _referenceType = upperFirst(camelCase(referenceType));
|
||||||
|
const _transactionType = upperFirst(camelCase(transactionType));
|
||||||
|
|
||||||
|
return isObject(TransactionTypes[_referenceType])
|
||||||
|
? TransactionTypes[_referenceType][_transactionType]
|
||||||
|
: TransactionTypes[_referenceType] || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted type of cashflow transaction.
|
||||||
|
* @param {string} transactionType
|
||||||
|
* @returns {string¿}
|
||||||
|
*/
|
||||||
|
export const getCashflowTransactionFormattedType = (
|
||||||
|
transactionType: string
|
||||||
|
) => {
|
||||||
|
const _transactionType = upperFirst(camelCase(transactionType));
|
||||||
|
|
||||||
|
return CashflowTransactionTypes[_transactionType] || null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,3 +39,12 @@ export const TRANSACRIONS_TYPE = [
|
|||||||
'OtherExpense',
|
'OtherExpense',
|
||||||
'TransferToAccount',
|
'TransferToAccount',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const MoneyCategoryPerCreditAccountRootType = {
|
||||||
|
OwnerContribution: ['equity'],
|
||||||
|
OtherIncome: ['income'],
|
||||||
|
OwnerDrawing: ['equity'],
|
||||||
|
OtherExpense: ['expense'],
|
||||||
|
TransferToAccount: ['asset'],
|
||||||
|
TransferFromAccount: ['asset'],
|
||||||
|
};
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ function AccountDeleteTransactionAlert({
|
|||||||
'Cannot delete transaction converted from uncategorized transaction but you uncategorize it.',
|
'Cannot delete transaction converted from uncategorized transaction but you uncategorize it.',
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
});
|
});
|
||||||
|
} else if (
|
||||||
|
errors.find((e) => e.type === 'CANNOT_DELETE_TRANSACTION_MATCHED')
|
||||||
|
) {
|
||||||
|
AppToaster.show({
|
||||||
|
message:
|
||||||
|
'Cannot delete a transaction matched to the bank transaction',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||||
import { Button, Classes, Intent, Radio, Tag } from '@blueprintjs/core';
|
import { Button, Classes, Intent, Radio, Tag } from '@blueprintjs/core';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
@@ -16,11 +17,11 @@ import {
|
|||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useCreateBankRule, useEditBankRule } from '@/hooks/query/bank-rules';
|
import { useCreateBankRule, useEditBankRule } from '@/hooks/query/bank-rules';
|
||||||
import {
|
import {
|
||||||
AssignTransactionTypeOptions,
|
|
||||||
FieldCondition,
|
FieldCondition,
|
||||||
Fields,
|
Fields,
|
||||||
RuleFormValues,
|
RuleFormValues,
|
||||||
TransactionTypeOptions,
|
TransactionTypeOptions,
|
||||||
|
getAccountRootFromMoneyCategory,
|
||||||
initialValues,
|
initialValues,
|
||||||
} from './_utils';
|
} from './_utils';
|
||||||
import { useRuleFormDialogBoot } from './RuleFormBoot';
|
import { useRuleFormDialogBoot } from './RuleFormBoot';
|
||||||
@@ -31,6 +32,11 @@ import {
|
|||||||
} from '@/utils';
|
} from '@/utils';
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
import { getAddMoneyInOptions, getAddMoneyOutOptions } from '@/constants';
|
||||||
|
|
||||||
|
// Retrieves the add money in button options.
|
||||||
|
const MoneyInOptions = getAddMoneyInOptions();
|
||||||
|
const MoneyOutOptions = getAddMoneyOutOptions();
|
||||||
|
|
||||||
function RuleFormContentFormRoot({
|
function RuleFormContentFormRoot({
|
||||||
// #withDialogActions
|
// #withDialogActions
|
||||||
@@ -47,7 +53,6 @@ function RuleFormContentFormRoot({
|
|||||||
...initialValues,
|
...initialValues,
|
||||||
...transformToForm(transformToCamelCase(bankRule), initialValues),
|
...transformToForm(transformToCamelCase(bankRule), initialValues),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles the form submitting.
|
// Handles the form submitting.
|
||||||
const handleSubmit = (
|
const handleSubmit = (
|
||||||
values: RuleFormValues,
|
values: RuleFormValues,
|
||||||
@@ -92,8 +97,9 @@ function RuleFormContentFormRoot({
|
|||||||
label={'Rule Name'}
|
label={'Rule Name'}
|
||||||
labelInfo={<Tag minimal>Required</Tag>}
|
labelInfo={<Tag minimal>Required</Tag>}
|
||||||
style={{ maxWidth: 300 }}
|
style={{ maxWidth: 300 }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FInputGroup name={'name'} />
|
<FInputGroup name={'name'} fastField />
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
@@ -101,29 +107,22 @@ function RuleFormContentFormRoot({
|
|||||||
label={'Apply the rule to account'}
|
label={'Apply the rule to account'}
|
||||||
labelInfo={<Tag minimal>Required</Tag>}
|
labelInfo={<Tag minimal>Required</Tag>}
|
||||||
style={{ maxWidth: 350 }}
|
style={{ maxWidth: 350 }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<AccountsSelect
|
<AccountsSelect
|
||||||
name={'applyIfAccountId'}
|
name={'applyIfAccountId'}
|
||||||
items={accounts}
|
items={accounts}
|
||||||
filterByTypes={['cash', 'bank']}
|
filterByTypes={['cash', 'bank']}
|
||||||
|
fastField
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<FFormGroup
|
<RuleApplyIfTransactionTypeField />
|
||||||
name={'applyIfTransactionType'}
|
|
||||||
label={'Apply to transactions are'}
|
|
||||||
style={{ maxWidth: 350 }}
|
|
||||||
>
|
|
||||||
<FSelect
|
|
||||||
name={'applyIfTransactionType'}
|
|
||||||
items={TransactionTypeOptions}
|
|
||||||
popoverProps={{ minimal: true, inline: false }}
|
|
||||||
/>
|
|
||||||
</FFormGroup>
|
|
||||||
|
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'conditionsType'}
|
name={'conditionsType'}
|
||||||
label={'Categorize the transactions when'}
|
label={'Categorize the transactions when'}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FRadioGroup name={'conditionsType'}>
|
<FRadioGroup name={'conditionsType'}>
|
||||||
<Radio value={'and'} label={'All the following criteria matches'} />
|
<Radio value={'and'} label={'All the following criteria matches'} />
|
||||||
@@ -139,34 +138,16 @@ function RuleFormContentFormRoot({
|
|||||||
Then Assign
|
Then Assign
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<FFormGroup
|
<RuleAssignCategoryField />
|
||||||
name={'assignCategory'}
|
<RuleAssignCategoryAccountField />
|
||||||
label={'Transaction type'}
|
|
||||||
labelInfo={<Tag minimal>Required</Tag>}
|
|
||||||
style={{ maxWidth: 300 }}
|
|
||||||
>
|
|
||||||
<FSelect
|
|
||||||
name={'assignCategory'}
|
|
||||||
items={AssignTransactionTypeOptions}
|
|
||||||
popoverProps={{ minimal: true, inline: false }}
|
|
||||||
/>
|
|
||||||
</FFormGroup>
|
|
||||||
|
|
||||||
<FFormGroup
|
|
||||||
name={'assignAccountId'}
|
|
||||||
label={'Account category'}
|
|
||||||
labelInfo={<Tag minimal>Required</Tag>}
|
|
||||||
style={{ maxWidth: 300 }}
|
|
||||||
>
|
|
||||||
<AccountsSelect name={'assignAccountId'} items={accounts} />
|
|
||||||
</FFormGroup>
|
|
||||||
|
|
||||||
<FFormGroup
|
<FFormGroup
|
||||||
name={'assignRef'}
|
name={'assignRef'}
|
||||||
label={'Reference'}
|
label={'Reference'}
|
||||||
style={{ maxWidth: 300 }}
|
style={{ maxWidth: 300 }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FInputGroup name={'assignRef'} />
|
<FInputGroup name={'assignRef'} fastField />
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
<RuleFormActions />
|
<RuleFormActions />
|
||||||
@@ -203,11 +184,13 @@ function RuleFormConditions() {
|
|||||||
name={`conditions[${index}].field`}
|
name={`conditions[${index}].field`}
|
||||||
label={'Field'}
|
label={'Field'}
|
||||||
style={{ marginBottom: 0, flex: '1 0' }}
|
style={{ marginBottom: 0, flex: '1 0' }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FSelect
|
<FSelect
|
||||||
name={`conditions[${index}].field`}
|
name={`conditions[${index}].field`}
|
||||||
items={Fields}
|
items={Fields}
|
||||||
popoverProps={{ minimal: true, inline: false }}
|
popoverProps={{ minimal: true, inline: false }}
|
||||||
|
fastField
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
@@ -215,11 +198,13 @@ function RuleFormConditions() {
|
|||||||
name={`conditions[${index}].comparator`}
|
name={`conditions[${index}].comparator`}
|
||||||
label={'Condition'}
|
label={'Condition'}
|
||||||
style={{ marginBottom: 0, flex: '1 0' }}
|
style={{ marginBottom: 0, flex: '1 0' }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FSelect
|
<FSelect
|
||||||
name={`conditions[${index}].comparator`}
|
name={`conditions[${index}].comparator`}
|
||||||
items={FieldCondition}
|
items={FieldCondition}
|
||||||
popoverProps={{ minimal: true, inline: false }}
|
popoverProps={{ minimal: true, inline: false }}
|
||||||
|
fastField
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
@@ -227,8 +212,9 @@ function RuleFormConditions() {
|
|||||||
name={`conditions[${index}].value`}
|
name={`conditions[${index}].value`}
|
||||||
label={'Value'}
|
label={'Value'}
|
||||||
style={{ marginBottom: 0, flex: '1 0 ', width: '40%' }}
|
style={{ marginBottom: 0, flex: '1 0 ', width: '40%' }}
|
||||||
|
fastField
|
||||||
>
|
>
|
||||||
<FInputGroup name={`conditions[${index}].value`} />
|
<FInputGroup name={`conditions[${index}].value`} fastField />
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
@@ -284,3 +270,104 @@ function RuleFormActionsRoot({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RuleFormActions = R.compose(withDialogActions)(RuleFormActionsRoot);
|
const RuleFormActions = R.compose(withDialogActions)(RuleFormActionsRoot);
|
||||||
|
|
||||||
|
function RuleApplyIfTransactionTypeField() {
|
||||||
|
const { setFieldValue } = useFormikContext<RuleFormValues>();
|
||||||
|
|
||||||
|
const handleItemChange = useCallback(
|
||||||
|
(item: any) => {
|
||||||
|
setFieldValue('applyIfTransactionType', item.value);
|
||||||
|
setFieldValue('assignCategory', '');
|
||||||
|
setFieldValue('assignAccountId', '');
|
||||||
|
},
|
||||||
|
[setFieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FFormGroup
|
||||||
|
name={'applyIfTransactionType'}
|
||||||
|
label={'Apply to transactions are'}
|
||||||
|
style={{ maxWidth: 350 }}
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FSelect
|
||||||
|
name={'applyIfTransactionType'}
|
||||||
|
items={TransactionTypeOptions}
|
||||||
|
popoverProps={{ minimal: true, inline: false }}
|
||||||
|
onItemChange={handleItemChange}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RuleAssignCategoryField() {
|
||||||
|
const { values, setFieldValue } = useFormikContext<RuleFormValues>();
|
||||||
|
|
||||||
|
// Retrieves the transaction types if it is deposit or withdrawal.
|
||||||
|
const transactionTypes = useMemo(
|
||||||
|
() =>
|
||||||
|
values?.applyIfTransactionType === 'deposit'
|
||||||
|
? MoneyInOptions
|
||||||
|
: MoneyOutOptions,
|
||||||
|
[values?.applyIfTransactionType],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handles the select item change.
|
||||||
|
const handleItemChange = useCallback(
|
||||||
|
(item: any) => {
|
||||||
|
setFieldValue('assignCategory', item.value);
|
||||||
|
setFieldValue('assignAccountId', '');
|
||||||
|
},
|
||||||
|
[setFieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FFormGroup
|
||||||
|
name={'assignCategory'}
|
||||||
|
label={'Transaction type'}
|
||||||
|
labelInfo={<Tag minimal>Required</Tag>}
|
||||||
|
style={{ maxWidth: 300 }}
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FSelect
|
||||||
|
name={'assignCategory'}
|
||||||
|
items={transactionTypes}
|
||||||
|
popoverProps={{ minimal: true, inline: false }}
|
||||||
|
valueAccessor={'value'}
|
||||||
|
textAccessor={'name'}
|
||||||
|
onItemChange={handleItemChange}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function RuleAssignCategoryAccountField() {
|
||||||
|
const { values } = useFormikContext<RuleFormValues>();
|
||||||
|
const { accounts } = useRuleFormDialogBoot();
|
||||||
|
|
||||||
|
const accountRoot = useMemo(
|
||||||
|
() => getAccountRootFromMoneyCategory(values.assignCategory),
|
||||||
|
[values.assignCategory],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FFormGroup
|
||||||
|
name={'assignAccountId'}
|
||||||
|
label={'Account category'}
|
||||||
|
labelInfo={<Tag minimal>Required</Tag>}
|
||||||
|
style={{ maxWidth: 300 }}
|
||||||
|
fastField
|
||||||
|
shouldUpdateDeps={{ accountRoot }}
|
||||||
|
>
|
||||||
|
<AccountsSelect
|
||||||
|
name={'assignAccountId'}
|
||||||
|
items={accounts}
|
||||||
|
filterByRootTypes={accountRoot}
|
||||||
|
shouldUpdateDeps={{ accountRoot }}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { camelCase, get, upperFirst } from 'lodash';
|
||||||
|
import { MoneyCategoryPerCreditAccountRootType } from '@/constants/cashflowOptions';
|
||||||
|
|
||||||
export const initialValues = {
|
export const initialValues = {
|
||||||
name: '',
|
name: '',
|
||||||
order: 0,
|
order: 0,
|
||||||
applyIfAccountId: '',
|
applyIfAccountId: '',
|
||||||
applyIfTransactionType: '',
|
applyIfTransactionType: 'deposit',
|
||||||
conditionsType: 'and',
|
conditionsType: 'and',
|
||||||
conditions: [
|
conditions: [
|
||||||
{
|
{
|
||||||
@@ -47,3 +50,9 @@ export const FieldCondition = [
|
|||||||
export const AssignTransactionTypeOptions = [
|
export const AssignTransactionTypeOptions = [
|
||||||
{ value: 'expense', text: 'Expense' },
|
{ value: 'expense', text: 'Expense' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const getAccountRootFromMoneyCategory = (category: string): string[] => {
|
||||||
|
const _category = upperFirst(camelCase(category));
|
||||||
|
|
||||||
|
return get(MoneyCategoryPerCreditAccountRootType, _category) || [];
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DataTable,
|
DataTable,
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
TableSkeletonHeader,
|
TableSkeletonHeader,
|
||||||
TableVirtualizedListRows,
|
TableVirtualizedListRows,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
|
AppToaster,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { TABLES } from '@/constants/tables';
|
import { TABLES } from '@/constants/tables';
|
||||||
|
|
||||||
@@ -19,9 +21,11 @@ import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
|||||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||||
import { useAccountTransactionsColumns, ActionsMenu } from './components';
|
import { useAccountTransactionsColumns, ActionsMenu } from './components';
|
||||||
import { useAccountTransactionsAllContext } from './AccountTransactionsAllBoot';
|
import { useAccountTransactionsAllContext } from './AccountTransactionsAllBoot';
|
||||||
|
import { useUnmatchMatchedUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||||
import { handleCashFlowTransactionType } from './utils';
|
import { handleCashFlowTransactionType } from './utils';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
|
import { useUncategorizeTransaction } from '@/hooks/query';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account transactions data table.
|
* Account transactions data table.
|
||||||
@@ -43,14 +47,14 @@ function AccountTransactionsDataTable({
|
|||||||
const { cashflowTransactions, isCashFlowTransactionsLoading } =
|
const { cashflowTransactions, isCashFlowTransactionsLoading } =
|
||||||
useAccountTransactionsAllContext();
|
useAccountTransactionsAllContext();
|
||||||
|
|
||||||
|
const { mutateAsync: uncategorizeTransaction } = useUncategorizeTransaction();
|
||||||
|
const { mutateAsync: unmatchTransaction } =
|
||||||
|
useUnmatchMatchedUncategorizedTransaction();
|
||||||
|
|
||||||
// Local storage memorizing columns widths.
|
// Local storage memorizing columns widths.
|
||||||
const [initialColumnsWidths, , handleColumnResizing] =
|
const [initialColumnsWidths, , handleColumnResizing] =
|
||||||
useMemorizedColumnsWidths(TABLES.CASHFLOW_Transactions);
|
useMemorizedColumnsWidths(TABLES.CASHFLOW_Transactions);
|
||||||
|
|
||||||
// handle delete transaction
|
|
||||||
const handleDeleteTransaction = ({ reference_id }) => {
|
|
||||||
openAlert('account-transaction-delete', { referenceId: reference_id });
|
|
||||||
};
|
|
||||||
// Handle view details action.
|
// Handle view details action.
|
||||||
const handleViewDetailCashflowTransaction = (referenceType) => {
|
const handleViewDetailCashflowTransaction = (referenceType) => {
|
||||||
handleCashFlowTransactionType(referenceType, openDrawer);
|
handleCashFlowTransactionType(referenceType, openDrawer);
|
||||||
@@ -60,6 +64,38 @@ function AccountTransactionsDataTable({
|
|||||||
const referenceType = cell.row.original;
|
const referenceType = cell.row.original;
|
||||||
handleCashFlowTransactionType(referenceType, openDrawer);
|
handleCashFlowTransactionType(referenceType, openDrawer);
|
||||||
};
|
};
|
||||||
|
// Handles the unmatching the matched transaction.
|
||||||
|
const handleUnmatchTransaction = (transaction) => {
|
||||||
|
unmatchTransaction({ id: transaction.uncategorized_transaction_id })
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'The bank transaction has been unmatched.',
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// Handle uncategorize transaction.
|
||||||
|
const handleUncategorizeTransaction = (transaction) => {
|
||||||
|
uncategorizeTransaction(transaction.uncategorized_transaction_id)
|
||||||
|
.then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'The bank transaction has been uncategorized.',
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CashflowTransactionsTable
|
<CashflowTransactionsTable
|
||||||
@@ -87,7 +123,8 @@ function AccountTransactionsDataTable({
|
|||||||
className="table-constrant"
|
className="table-constrant"
|
||||||
payload={{
|
payload={{
|
||||||
onViewDetails: handleViewDetailCashflowTransaction,
|
onViewDetails: handleViewDetailCashflowTransaction,
|
||||||
onDelete: handleDeleteTransaction,
|
onUncategorize: handleUncategorizeTransaction,
|
||||||
|
onUnmatch: handleUnmatchTransaction,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,19 +12,17 @@ import {
|
|||||||
AppToaster,
|
AppToaster,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { TABLES } from '@/constants/tables';
|
import { TABLES } from '@/constants/tables';
|
||||||
|
import { ActionsMenu } from './UncategorizedTransactions/components';
|
||||||
|
|
||||||
import withSettings from '@/containers/Settings/withSettings';
|
import withSettings from '@/containers/Settings/withSettings';
|
||||||
import { withBankingActions } from '../withBankingActions';
|
import { withBankingActions } from '../withBankingActions';
|
||||||
|
|
||||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||||
import {
|
import { useAccountUncategorizedTransactionsColumns } from './components';
|
||||||
ActionsMenu,
|
|
||||||
useAccountUncategorizedTransactionsColumns,
|
|
||||||
} from './components';
|
|
||||||
import { useAccountUncategorizedTransactionsContext } from './AllTransactionsUncategorizedBoot';
|
import { useAccountUncategorizedTransactionsContext } from './AllTransactionsUncategorizedBoot';
|
||||||
|
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
import { useExcludeUncategorizedTransaction } from '@/hooks/query/bank-rules';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Account transactions data table.
|
* Account transactions data table.
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
|
||||||
|
import { Icon } from '@/components';
|
||||||
|
import { safeCallback } from '@/utils';
|
||||||
|
|
||||||
|
export function ActionsMenu({
|
||||||
|
payload: { onCategorize, onExclude },
|
||||||
|
row: { original },
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuItem
|
||||||
|
icon={<Icon icon="reader-18" />}
|
||||||
|
text={'Categorize'}
|
||||||
|
onClick={safeCallback(onCategorize, original)}
|
||||||
|
/>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem
|
||||||
|
text={'Exclude'}
|
||||||
|
onClick={safeCallback(onExclude, original)}
|
||||||
|
icon={<Icon icon="disable" iconSize={16} />}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,44 +5,56 @@ import {
|
|||||||
Intent,
|
Intent,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuDivider,
|
|
||||||
Tag,
|
Tag,
|
||||||
Popover,
|
|
||||||
PopoverInteractionKind,
|
PopoverInteractionKind,
|
||||||
Position,
|
Position,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import {
|
import { Box, FormatDateCell, Icon, MaterialProgressBar } from '@/components';
|
||||||
Box,
|
|
||||||
Can,
|
|
||||||
FormatDateCell,
|
|
||||||
Icon,
|
|
||||||
MaterialProgressBar,
|
|
||||||
} from '@/components';
|
|
||||||
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
||||||
import { safeCallback } from '@/utils';
|
import { safeCallback } from '@/utils';
|
||||||
|
|
||||||
export function ActionsMenu({
|
export function ActionsMenu({
|
||||||
payload: { onCategorize, onExclude },
|
payload: { onUncategorize, onUnmatch },
|
||||||
row: { original },
|
row: { original },
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuItem
|
{original.status === 'categorized' && (
|
||||||
icon={<Icon icon="reader-18" />}
|
<MenuItem
|
||||||
text={'Categorize'}
|
icon={<Icon icon="reader-18" />}
|
||||||
onClick={safeCallback(onCategorize, original)}
|
text={'Uncategorize'}
|
||||||
/>
|
onClick={safeCallback(onUncategorize, original)}
|
||||||
<MenuDivider />
|
/>
|
||||||
<MenuItem
|
)}
|
||||||
text={'Exclude'}
|
{original.status === 'matched' && (
|
||||||
onClick={safeCallback(onExclude, original)}
|
<MenuItem
|
||||||
icon={<Icon icon="disable" iconSize={16} />}
|
text={'Unmatch'}
|
||||||
/>
|
icon={<Icon icon="unlink" iconSize={16} />}
|
||||||
|
onClick={safeCallback(onUnmatch, original)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allTransactionsStatusAccessor = (transaction) => {
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
intent={
|
||||||
|
transaction.status === 'categorized'
|
||||||
|
? Intent.SUCCESS
|
||||||
|
: transaction.status === 'matched'
|
||||||
|
? Intent.SUCCESS
|
||||||
|
: Intent.NONE
|
||||||
|
}
|
||||||
|
minimal={transaction.status === 'manual'}
|
||||||
|
>
|
||||||
|
{transaction.formatted_status}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve account transctions table columns.
|
* Retrieve account transctions table columns.
|
||||||
*/
|
*/
|
||||||
@@ -70,7 +82,7 @@ export function useAccountTransactionsColumns() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'transaction_number',
|
id: 'transaction_number',
|
||||||
Header: intl.get('transaction_number'),
|
Header: 'Transaction #',
|
||||||
accessor: 'transaction_number',
|
accessor: 'transaction_number',
|
||||||
width: 160,
|
width: 160,
|
||||||
className: 'transaction_number',
|
className: 'transaction_number',
|
||||||
@@ -79,13 +91,18 @@ export function useAccountTransactionsColumns() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'reference_number',
|
id: 'reference_number',
|
||||||
Header: intl.get('reference_no'),
|
Header: 'Ref.#',
|
||||||
accessor: 'reference_number',
|
accessor: 'reference_number',
|
||||||
width: 160,
|
width: 160,
|
||||||
className: 'reference_number',
|
className: 'reference_number',
|
||||||
clickable: true,
|
clickable: true,
|
||||||
textOverview: true,
|
textOverview: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
Header: 'Status',
|
||||||
|
accessor: allTransactionsStatusAccessor,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'deposit',
|
id: 'deposit',
|
||||||
Header: intl.get('cash_flow.label.deposit'),
|
Header: intl.get('cash_flow.label.deposit'),
|
||||||
@@ -116,16 +133,6 @@ export function useAccountTransactionsColumns() {
|
|||||||
align: 'right',
|
align: 'right',
|
||||||
clickable: true,
|
clickable: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'balance',
|
|
||||||
Header: intl.get('balance'),
|
|
||||||
accessor: 'formatted_balance',
|
|
||||||
className: 'balance',
|
|
||||||
width: 150,
|
|
||||||
textOverview: true,
|
|
||||||
clickable: true,
|
|
||||||
align: 'right',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
@@ -204,10 +211,9 @@ export function useAccountUncategorizedTransactionsColumns() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'reference_number',
|
id: 'reference_number',
|
||||||
Header: intl.get('reference_no'),
|
Header: 'Ref.#',
|
||||||
accessor: 'reference_number',
|
accessor: 'reference_no',
|
||||||
width: 50,
|
width: 50,
|
||||||
className: 'reference_number',
|
|
||||||
clickable: true,
|
clickable: true,
|
||||||
textOverview: true,
|
textOverview: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
FFormGroup,
|
FFormGroup,
|
||||||
FInputGroup,
|
FInputGroup,
|
||||||
FTextArea,
|
FTextArea,
|
||||||
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOtherIncome() {
|
|||||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
formatDate={(date) => date.toLocaleDateString()}
|
formatDate={(date) => date.toLocaleDateString()}
|
||||||
parseDate={(str) => new Date(str)}
|
parseDate={(str) => new Date(str)}
|
||||||
inputProps={{ fill: true }}
|
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
FFormGroup,
|
FFormGroup,
|
||||||
FInputGroup,
|
FInputGroup,
|
||||||
FTextArea,
|
FTextArea,
|
||||||
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOwnerContribution() {
|
|||||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
formatDate={(date) => date.toLocaleDateString()}
|
formatDate={(date) => date.toLocaleDateString()}
|
||||||
parseDate={(str) => new Date(str)}
|
parseDate={(str) => new Date(str)}
|
||||||
inputProps={{ fill: true }}
|
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
FFormGroup,
|
FFormGroup,
|
||||||
FInputGroup,
|
FInputGroup,
|
||||||
FTextArea,
|
FTextArea,
|
||||||
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionTransferFrom() {
|
|||||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
formatDate={(date) => date.toLocaleDateString()}
|
formatDate={(date) => date.toLocaleDateString()}
|
||||||
parseDate={(str) => new Date(str)}
|
parseDate={(str) => new Date(str)}
|
||||||
inputProps={{ fill: true }}
|
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
FFormGroup,
|
FFormGroup,
|
||||||
FInputGroup,
|
FInputGroup,
|
||||||
FTextArea,
|
FTextArea,
|
||||||
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOtherExpense() {
|
|||||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
formatDate={(date) => date.toLocaleDateString()}
|
formatDate={(date) => date.toLocaleDateString()}
|
||||||
parseDate={(str) => new Date(str)}
|
parseDate={(str) => new Date(str)}
|
||||||
inputProps={{ fill: true }}
|
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
FFormGroup,
|
FFormGroup,
|
||||||
FInputGroup,
|
FInputGroup,
|
||||||
FTextArea,
|
FTextArea,
|
||||||
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOwnerDrawings() {
|
|||||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
formatDate={(date) => date.toLocaleDateString()}
|
formatDate={(date) => date.toLocaleDateString()}
|
||||||
parseDate={(str) => new Date(str)}
|
parseDate={(str) => new Date(str)}
|
||||||
inputProps={{ fill: true }}
|
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
FFormGroup,
|
FFormGroup,
|
||||||
FInputGroup,
|
FInputGroup,
|
||||||
FTextArea,
|
FTextArea,
|
||||||
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
|
||||||
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
|
||||||
@@ -21,7 +22,7 @@ export default function CategorizeTransactionToAccount() {
|
|||||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
formatDate={(date) => date.toLocaleDateString()}
|
formatDate={(date) => date.toLocaleDateString()}
|
||||||
parseDate={(str) => new Date(str)}
|
parseDate={(str) => new Date(str)}
|
||||||
inputProps={{ fill: true }}
|
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||||
/>
|
/>
|
||||||
</FFormGroup>
|
</FFormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,26 @@ import {
|
|||||||
} from '../withBankingActions';
|
} from '../withBankingActions';
|
||||||
import { CategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
import { CategorizeTransactionTabsBoot } from './CategorizeTransactionTabsBoot';
|
||||||
import { withBanking } from '../withBanking';
|
import { withBanking } from '../withBanking';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
interface CategorizeTransactionAsideProps extends WithBankingActionsProps {}
|
interface CategorizeTransactionAsideProps extends WithBankingActionsProps {}
|
||||||
|
|
||||||
function CategorizeTransactionAsideRoot({
|
function CategorizeTransactionAsideRoot({
|
||||||
// #withBankingActions
|
// #withBankingActions
|
||||||
closeMatchingTransactionAside,
|
closeMatchingTransactionAside,
|
||||||
|
closeReconcileMatchingTransaction,
|
||||||
|
|
||||||
// #withBanking
|
// #withBanking
|
||||||
selectedUncategorizedTransactionId,
|
selectedUncategorizedTransactionId,
|
||||||
}: CategorizeTransactionAsideProps) {
|
}: CategorizeTransactionAsideProps) {
|
||||||
|
//
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
closeReconcileMatchingTransaction();
|
||||||
|
},
|
||||||
|
[closeReconcileMatchingTransaction],
|
||||||
|
);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
closeMatchingTransactionAside();
|
closeMatchingTransactionAside();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
color: rgb(21, 82, 200),
|
color: rgb(21, 82, 200),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(.active){
|
&:hover:not(.active){
|
||||||
border-color: #c0c0c0;
|
border-color: #c0c0c0;
|
||||||
}
|
}
|
||||||
@@ -25,7 +24,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.checkbox:global(.bp4-control.bp4-checkbox) :global .bp4-control-indicator{
|
.checkbox:global(.bp4-control.bp4-checkbox) :global .bp4-control-indicator{
|
||||||
border-color: #CBCBCB;
|
box-shadow: 0 0 0 1px #CBCBCB;
|
||||||
}
|
}
|
||||||
.checkbox:global(.bp4-control.bp4-checkbox) :global .bp4-control-indicator{
|
.checkbox:global(.bp4-control.bp4-checkbox) :global .bp4-control-indicator{
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
@@ -34,9 +33,17 @@
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox:global(.bp4-control.bp4-checkbox) :global input:checked ~ .bp4-control-indicator{
|
||||||
|
box-shadow: 0 0 0 1px #0069ff;
|
||||||
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: #10161A;
|
color: #252A33;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.label :global strong {
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric:tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface MatchTransactionCheckboxProps {
|
|||||||
active?: boolean;
|
active?: boolean;
|
||||||
initialActive?: boolean;
|
initialActive?: boolean;
|
||||||
onChange?: (state: boolean) => void;
|
onChange?: (state: boolean) => void;
|
||||||
label: string;
|
label: string | React.ReactNode;
|
||||||
date: string;
|
date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ export function MatchTransactionCheckbox({
|
|||||||
position="apart"
|
position="apart"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={2}>
|
||||||
<span className={styles.label}>{label}</span>
|
<span className={styles.label}>{label}</span>
|
||||||
<Text className={styles.date}>Date: {date}</Text>
|
<Text className={styles.date}>Date: {date}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const MatchingReconcileFormSchema = Yup.object().shape({
|
|||||||
type: Yup.string().required().label('Type'),
|
type: Yup.string().required().label('Type'),
|
||||||
date: Yup.string().required().label('Date'),
|
date: Yup.string().required().label('Date'),
|
||||||
amount: Yup.string().required().label('Amount'),
|
amount: Yup.string().required().label('Amount'),
|
||||||
memo: Yup.string().required().label('Memo'),
|
memo: Yup.string().required().min(3).label('Memo'),
|
||||||
referenceNo: Yup.string().label('Refernece #'),
|
referenceNo: Yup.string().label('Refernece #'),
|
||||||
category: Yup.string().required().label('Categogry'),
|
category: Yup.string().required().label('Categogry'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { Button, Intent, Position, Tag } from '@blueprintjs/core';
|
import { Button, Intent, Position, Tag } from '@blueprintjs/core';
|
||||||
import { Form, Formik, FormikValues, useFormikContext } from 'formik';
|
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { round } from 'lodash';
|
||||||
import {
|
import {
|
||||||
AccountsSelect,
|
AccountsSelect,
|
||||||
AppToaster,
|
AppToaster,
|
||||||
@@ -12,6 +14,7 @@ import {
|
|||||||
FInputGroup,
|
FInputGroup,
|
||||||
FMoneyInputGroup,
|
FMoneyInputGroup,
|
||||||
Group,
|
Group,
|
||||||
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { Aside } from '@/components/Aside/Aside';
|
import { Aside } from '@/components/Aside/Aside';
|
||||||
import { momentFormatter } from '@/utils';
|
import { momentFormatter } from '@/utils';
|
||||||
@@ -25,29 +28,43 @@ import {
|
|||||||
import { useCreateCashflowTransaction } from '@/hooks/query';
|
import { useCreateCashflowTransaction } from '@/hooks/query';
|
||||||
import { useAccountTransactionsContext } from '../../AccountTransactions/AccountTransactionsProvider';
|
import { useAccountTransactionsContext } from '../../AccountTransactions/AccountTransactionsProvider';
|
||||||
import { MatchingReconcileFormSchema } from './MatchingReconcileTransactionForm.schema';
|
import { MatchingReconcileFormSchema } from './MatchingReconcileTransactionForm.schema';
|
||||||
import { initialValues } from './_utils';
|
import { initialValues, transformToReq } from './_utils';
|
||||||
|
import { withBanking } from '../../withBanking';
|
||||||
|
|
||||||
|
interface MatchingReconcileTransactionFormProps {
|
||||||
|
onSubmitSuccess?: (values: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
function MatchingReconcileTransactionFormRoot({
|
function MatchingReconcileTransactionFormRoot({
|
||||||
closeReconcileMatchingTransaction,
|
closeReconcileMatchingTransaction,
|
||||||
}) {
|
reconcileMatchingTransactionPendingAmount,
|
||||||
|
|
||||||
|
// #props¿
|
||||||
|
onSubmitSuccess,
|
||||||
|
}: MatchingReconcileTransactionFormProps) {
|
||||||
// Mutation create cashflow transaction.
|
// Mutation create cashflow transaction.
|
||||||
const { mutateAsync: createCashflowTransactionMutate } =
|
const { mutateAsync: createCashflowTransactionMutate } =
|
||||||
useCreateCashflowTransaction();
|
useCreateCashflowTransaction();
|
||||||
|
|
||||||
const { accountId } = useAccountTransactionsContext();
|
const { accountId } = useAccountTransactionsContext();
|
||||||
|
|
||||||
|
// Handles the aside close.
|
||||||
const handleAsideClose = () => {
|
const handleAsideClose = () => {
|
||||||
closeReconcileMatchingTransaction();
|
closeReconcileMatchingTransaction();
|
||||||
};
|
};
|
||||||
|
// Handle the form submitting.
|
||||||
const handleSubmit = (
|
const handleSubmit = (
|
||||||
values: MatchingReconcileTransactionValues,
|
values: MatchingReconcileTransactionValues,
|
||||||
{ setSubmitting }: FormikValues<MatchingReconcileTransactionValues>,
|
{
|
||||||
|
setSubmitting,
|
||||||
|
setErrors,
|
||||||
|
}: FormikHelpers<MatchingReconcileTransactionValues>,
|
||||||
) => {
|
) => {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const _values = transformToReq(values, accountId);
|
const _values = transformToReq(values, accountId);
|
||||||
|
|
||||||
createCashflowTransactionMutate(_values)
|
createCashflowTransactionMutate(_values)
|
||||||
.then(() => {
|
.then((res) => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
@@ -55,17 +72,36 @@ function MatchingReconcileTransactionFormRoot({
|
|||||||
intent: Intent.SUCCESS,
|
intent: Intent.SUCCESS,
|
||||||
});
|
});
|
||||||
closeReconcileMatchingTransaction();
|
closeReconcileMatchingTransaction();
|
||||||
|
onSubmitSuccess &&
|
||||||
|
onSubmitSuccess({ id: res.data.id, type: 'CashflowTransaction' });
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
if (
|
||||||
AppToaster.show({
|
error.response.data?.errors?.find(
|
||||||
message: 'Something went wrong.',
|
(e) => e.type === 'BRANCH_ID_REQUIRED',
|
||||||
intent: Intent.DANGER,
|
)
|
||||||
});
|
) {
|
||||||
|
setErrors({
|
||||||
|
branchId: 'The branch is required.',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
AppToaster.show({
|
||||||
|
message: 'Something went wrong.',
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _initialValues = {
|
||||||
|
...initialValues,
|
||||||
|
amount: round(Math.abs(reconcileMatchingTransactionPendingAmount), 2) || 0,
|
||||||
|
date: moment().format('YYYY-MM-DD'),
|
||||||
|
type:
|
||||||
|
reconcileMatchingTransactionPendingAmount > 0 ? 'deposit' : 'withdrawal',
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Aside
|
<Aside
|
||||||
title={'Create Reconcile Transactions'}
|
title={'Create Reconcile Transactions'}
|
||||||
@@ -75,7 +111,7 @@ function MatchingReconcileTransactionFormRoot({
|
|||||||
<MatchingReconcileTransactionBoot>
|
<MatchingReconcileTransactionBoot>
|
||||||
<Formik
|
<Formik
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
initialValues={initialValues}
|
initialValues={_initialValues}
|
||||||
validationSchema={MatchingReconcileFormSchema}
|
validationSchema={MatchingReconcileFormSchema}
|
||||||
>
|
>
|
||||||
<Form className={styles.form}>
|
<Form className={styles.form}>
|
||||||
@@ -93,11 +129,136 @@ function MatchingReconcileTransactionFormRoot({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MatchingReconcileTransactionForm = R.compose(withBankingActions)(
|
export const MatchingReconcileTransactionForm = R.compose(
|
||||||
MatchingReconcileTransactionFormRoot,
|
withBankingActions,
|
||||||
);
|
withBanking(({ reconcileMatchingTransactionPendingAmount }) => ({
|
||||||
|
reconcileMatchingTransactionPendingAmount,
|
||||||
|
})),
|
||||||
|
)(MatchingReconcileTransactionFormRoot);
|
||||||
|
|
||||||
export function MatchingReconcileTransactionFooter() {
|
function ReconcileMatchingType() {
|
||||||
|
const { setFieldValue, values } =
|
||||||
|
useFormikContext<MatchingReconcileFormValues>();
|
||||||
|
|
||||||
|
const handleChange = (value: string) => {
|
||||||
|
setFieldValue('type', value);
|
||||||
|
setFieldValue('category');
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<ContentTabs
|
||||||
|
value={values?.type || 'deposit'}
|
||||||
|
onChange={handleChange}
|
||||||
|
small
|
||||||
|
>
|
||||||
|
<ContentTabs.Tab id={'deposit'} title={'Deposit'} />
|
||||||
|
<ContentTabs.Tab id={'withdrawal'} title={'Withdrawal'} />
|
||||||
|
</ContentTabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateReconcileTransactionContent() {
|
||||||
|
const { branches } = useMatchingReconcileTransactionBoot();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={styles.content}>
|
||||||
|
<ReconcileMatchingType />
|
||||||
|
|
||||||
|
<FFormGroup label={'Date'} name={'date'} fastField>
|
||||||
|
<FDateInput
|
||||||
|
{...momentFormatter('YYYY/MM/DD')}
|
||||||
|
name={'date'}
|
||||||
|
popoverProps={{
|
||||||
|
minimal: false,
|
||||||
|
position: Position.LEFT,
|
||||||
|
modifiers: {
|
||||||
|
preventOverflow: { enabled: true },
|
||||||
|
},
|
||||||
|
boundary: 'viewport',
|
||||||
|
}}
|
||||||
|
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
|
||||||
|
fill
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<FFormGroup
|
||||||
|
label={'Amount'}
|
||||||
|
name={'amount'}
|
||||||
|
labelInfo={<Tag minimal>Required</Tag>}
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FMoneyInputGroup name={'amount'} fastField />
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<MatchingReconcileCategoryField />
|
||||||
|
|
||||||
|
<FFormGroup
|
||||||
|
label={'Memo'}
|
||||||
|
name={'memo'}
|
||||||
|
labelInfo={<Tag minimal>Required</Tag>}
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<FInputGroup name={'memo'} fastField />
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<FFormGroup label={'Reference No.'} name={'reference_no'} fastField>
|
||||||
|
<FInputGroup name={'reference_no'} />
|
||||||
|
</FFormGroup>
|
||||||
|
|
||||||
|
<FFormGroup
|
||||||
|
name={'branchId'}
|
||||||
|
label={'Branch'}
|
||||||
|
labelInfo={<Tag minimal>Required</Tag>}
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<BranchSelect
|
||||||
|
name={'branchId'}
|
||||||
|
branches={branches}
|
||||||
|
popoverProps={{
|
||||||
|
minimal: false,
|
||||||
|
position: Position.LEFT,
|
||||||
|
modifiers: {
|
||||||
|
preventOverflow: { enabled: true },
|
||||||
|
},
|
||||||
|
boundary: 'viewport',
|
||||||
|
}}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MatchingReconcileCategoryField() {
|
||||||
|
const { accounts } = useMatchingReconcileTransactionBoot();
|
||||||
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FFormGroup
|
||||||
|
label={'Category'}
|
||||||
|
name={'category'}
|
||||||
|
labelInfo={<Tag minimal>Required</Tag>}
|
||||||
|
fastField
|
||||||
|
>
|
||||||
|
<AccountsSelect
|
||||||
|
name={'category'}
|
||||||
|
items={accounts}
|
||||||
|
popoverProps={{
|
||||||
|
minimal: false,
|
||||||
|
position: Position.LEFT,
|
||||||
|
modifiers: {
|
||||||
|
preventOverflow: { enabled: true },
|
||||||
|
},
|
||||||
|
boundary: 'viewport',
|
||||||
|
}}
|
||||||
|
filterByRootTypes={values.type === 'deposit' ? 'income' : 'expense'}
|
||||||
|
fastField
|
||||||
|
/>
|
||||||
|
</FFormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MatchingReconcileTransactionFooter() {
|
||||||
const { isSubmitting } = useFormikContext();
|
const { isSubmitting } = useFormikContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,85 +276,3 @@ export function MatchingReconcileTransactionFooter() {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReconcileMatchingType() {
|
|
||||||
const { setFieldValue, values } =
|
|
||||||
useFormikContext<MatchingReconcileFormValues>();
|
|
||||||
|
|
||||||
const handleChange = (value: string) => {
|
|
||||||
setFieldValue('type', value);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<ContentTabs
|
|
||||||
value={values?.type || 'deposit'}
|
|
||||||
onChange={handleChange}
|
|
||||||
small
|
|
||||||
>
|
|
||||||
<ContentTabs.Tab id={'deposit'} title={'Deposit'} />
|
|
||||||
<ContentTabs.Tab id={'withdrawal'} title={'Withdrawal'} />
|
|
||||||
</ContentTabs>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreateReconcileTransactionContent() {
|
|
||||||
const { accounts, branches } = useMatchingReconcileTransactionBoot();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box className={styles.content}>
|
|
||||||
<ReconcileMatchingType />
|
|
||||||
|
|
||||||
<FFormGroup label={'Date'} name={'date'}>
|
|
||||||
<FDateInput
|
|
||||||
{...momentFormatter('YYYY/MM/DD')}
|
|
||||||
name={'date'}
|
|
||||||
formatDate={(date) => date.toLocaleString()}
|
|
||||||
popoverProps={{
|
|
||||||
position: Position.LEFT,
|
|
||||||
}}
|
|
||||||
inputProps={{ fill: true }}
|
|
||||||
fill
|
|
||||||
/>
|
|
||||||
</FFormGroup>
|
|
||||||
|
|
||||||
<FFormGroup
|
|
||||||
label={'Amount'}
|
|
||||||
name={'amount'}
|
|
||||||
labelInfo={<Tag minimal>Required</Tag>}
|
|
||||||
>
|
|
||||||
<FMoneyInputGroup name={'amount'} />
|
|
||||||
</FFormGroup>
|
|
||||||
|
|
||||||
<FFormGroup
|
|
||||||
label={'Category'}
|
|
||||||
name={'category'}
|
|
||||||
labelInfo={<Tag minimal>Required</Tag>}
|
|
||||||
>
|
|
||||||
<AccountsSelect
|
|
||||||
name={'category'}
|
|
||||||
items={accounts}
|
|
||||||
popoverProps={{ minimal: false, position: Position.LEFT }}
|
|
||||||
/>
|
|
||||||
</FFormGroup>
|
|
||||||
|
|
||||||
<FFormGroup
|
|
||||||
label={'Memo'}
|
|
||||||
name={'memo'}
|
|
||||||
labelInfo={<Tag minimal>Required</Tag>}
|
|
||||||
>
|
|
||||||
<FInputGroup name={'memo'} />
|
|
||||||
</FFormGroup>
|
|
||||||
|
|
||||||
<FFormGroup label={'Reference No.'} name={'reference_no'}>
|
|
||||||
<FInputGroup name={'reference_no'} />
|
|
||||||
</FFormGroup>
|
|
||||||
|
|
||||||
<FFormGroup name={'branchId'} label={'Branch'}>
|
|
||||||
<BranchSelect
|
|
||||||
name={'branchId'}
|
|
||||||
branches={branches}
|
|
||||||
popoverProps={{ minimal: true }}
|
|
||||||
/>
|
|
||||||
</FFormGroup>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
|
import { AnchorButton, Button, Intent, Tag, Text } from '@blueprintjs/core';
|
||||||
import { FastField, FastFieldProps, Formik, useFormikContext } from 'formik';
|
import { FastField, FastFieldProps, Formik, useFormikContext } from 'formik';
|
||||||
import { AppToaster, Box, FormatNumber, Group, Stack } from '@/components';
|
import { AppToaster, Box, FormatNumber, Group, Stack } from '@/components';
|
||||||
@@ -39,9 +40,6 @@ const initialValues = {
|
|||||||
function MatchingBankTransactionRoot({
|
function MatchingBankTransactionRoot({
|
||||||
// #withBankingActions
|
// #withBankingActions
|
||||||
closeMatchingTransactionAside,
|
closeMatchingTransactionAside,
|
||||||
|
|
||||||
// #withBanking
|
|
||||||
openReconcileMatchingTransaction,
|
|
||||||
}) {
|
}) {
|
||||||
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
|
const { uncategorizedTransactionId } = useCategorizeTransactionTabsBoot();
|
||||||
const { mutateAsync: matchTransaction } = useMatchUncategorizedTransaction();
|
const { mutateAsync: matchTransaction } = useMatchUncategorizedTransaction();
|
||||||
@@ -71,6 +69,18 @@ function MatchingBankTransactionRoot({
|
|||||||
closeMatchingTransactionAside();
|
closeMatchingTransactionAside();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
if (
|
||||||
|
err.response?.data.errors.find(
|
||||||
|
(e) => e.type === 'TOTAL_MATCHING_TRANSACTIONS_INVALID',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AppToaster.show({
|
||||||
|
message: `The total amount does not equal the uncategorized transaction.`,
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
message: 'Something went wrong.',
|
message: 'Something went wrong.',
|
||||||
@@ -84,25 +94,86 @@ function MatchingBankTransactionRoot({
|
|||||||
uncategorizedTransactionId={uncategorizedTransactionId}
|
uncategorizedTransactionId={uncategorizedTransactionId}
|
||||||
>
|
>
|
||||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||||
<>
|
<MatchingBankTransactionFormContent />
|
||||||
<MatchingBankTransactionContent />
|
|
||||||
|
|
||||||
{openReconcileMatchingTransaction && (
|
|
||||||
<MatchingReconcileTransactionForm />
|
|
||||||
)}
|
|
||||||
{!openReconcileMatchingTransaction && <MatchTransactionFooter />}
|
|
||||||
</>
|
|
||||||
</Formik>
|
</Formik>
|
||||||
</MatchingTransactionBoot>
|
</MatchingTransactionBoot>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MatchingBankTransaction = R.compose(
|
export const MatchingBankTransaction = R.compose(withBankingActions)(
|
||||||
|
MatchingBankTransactionRoot,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matching bank transaction form content.
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
const MatchingBankTransactionFormContent = R.compose(
|
||||||
withBankingActions,
|
withBankingActions,
|
||||||
withBanking(({ openReconcileMatchingTransaction }) => ({
|
withBanking(({ openReconcileMatchingTransaction }) => ({
|
||||||
openReconcileMatchingTransaction,
|
openReconcileMatchingTransaction,
|
||||||
})),
|
})),
|
||||||
)(MatchingBankTransactionRoot);
|
)(
|
||||||
|
({
|
||||||
|
// #withBanking
|
||||||
|
openReconcileMatchingTransaction,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
isMatchingTransactionsFetching,
|
||||||
|
isMatchingTransactionsSuccess,
|
||||||
|
matches,
|
||||||
|
} = useMatchingTransactionBoot();
|
||||||
|
const [pending, setPending] = useState<null | {
|
||||||
|
refId: number;
|
||||||
|
refType: string;
|
||||||
|
}>(null);
|
||||||
|
|
||||||
|
const { setFieldValue } = useFormikContext();
|
||||||
|
|
||||||
|
// This effect is responsible for automatically marking a transaction as matched
|
||||||
|
// when the matching process is successful and not currently fetching.
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
pending &&
|
||||||
|
isMatchingTransactionsSuccess &&
|
||||||
|
!isMatchingTransactionsFetching
|
||||||
|
) {
|
||||||
|
const foundMatch = matches?.find(
|
||||||
|
(m) =>
|
||||||
|
m.referenceType === pending?.refType &&
|
||||||
|
m.referenceId === pending?.refId,
|
||||||
|
);
|
||||||
|
if (foundMatch) {
|
||||||
|
setFieldValue(`matched.${pending.refType}-${pending.refId}`, true);
|
||||||
|
}
|
||||||
|
setPending(null);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isMatchingTransactionsFetching,
|
||||||
|
isMatchingTransactionsSuccess,
|
||||||
|
matches,
|
||||||
|
pending,
|
||||||
|
setFieldValue,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleReconcileFormSubmitSuccess = (payload) => {
|
||||||
|
setPending({ refId: payload.id, refType: payload.type });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MatchingBankTransactionContent />
|
||||||
|
|
||||||
|
{openReconcileMatchingTransaction && (
|
||||||
|
<MatchingReconcileTransactionForm
|
||||||
|
onSubmitSuccess={handleReconcileFormSubmitSuccess}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!openReconcileMatchingTransaction && <MatchTransactionFooter />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function MatchingBankTransactionContent() {
|
function MatchingBankTransactionContent() {
|
||||||
return (
|
return (
|
||||||
@@ -141,8 +212,8 @@ function PerfectMatchingTransactions() {
|
|||||||
key={index}
|
key={index}
|
||||||
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
||||||
date={match.dateFormatted}
|
date={match.dateFormatted}
|
||||||
transactionId={match.transactionId}
|
transactionId={match.referenceId}
|
||||||
transactionType={match.transactionType}
|
transactionType={match.referenceType}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -166,9 +237,6 @@ function PossibleMatchingTransactions() {
|
|||||||
<Box className={styles.matchBar}>
|
<Box className={styles.matchBar}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<h2 className={styles.matchBarTitle}>Possible Matches</h2>
|
<h2 className={styles.matchBarTitle}>Possible Matches</h2>
|
||||||
<Text style={{ fontSize: 12, color: '#5C7080' }}>
|
|
||||||
Transactions up to 20 Aug 2019
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -176,10 +244,15 @@ function PossibleMatchingTransactions() {
|
|||||||
{possibleMatches.map((match, index) => (
|
{possibleMatches.map((match, index) => (
|
||||||
<MatchTransactionField
|
<MatchTransactionField
|
||||||
key={index}
|
key={index}
|
||||||
label={`${match.transsactionTypeFormatted} for ${match.amountFormatted}`}
|
label={
|
||||||
|
<>
|
||||||
|
{`${match.transsactionTypeFormatted} for `}
|
||||||
|
<strong>{match.amountFormatted}</strong>
|
||||||
|
</>
|
||||||
|
}
|
||||||
date={match.dateFormatted}
|
date={match.dateFormatted}
|
||||||
transactionId={match.transactionId}
|
transactionId={match.referenceId}
|
||||||
transactionType={match.transactionType}
|
transactionType={match.referenceType}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -240,7 +313,7 @@ const MatchTransactionFooter = R.compose(withBankingActions)(
|
|||||||
submitForm();
|
submitForm();
|
||||||
};
|
};
|
||||||
const handleReconcileTransaction = () => {
|
const handleReconcileTransaction = () => {
|
||||||
openReconcileMatchingTransaction();
|
openReconcileMatchingTransaction(totalPending);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -258,7 +331,7 @@ const MatchTransactionFooter = R.compose(withBankingActions)(
|
|||||||
</AnchorButton>
|
</AnchorButton>
|
||||||
)}
|
)}
|
||||||
<Text
|
<Text
|
||||||
style={{ fontSize: 14, marginLeft: 'auto', color: '#5F6B7C' }}
|
style={{ fontSize: 14, marginLeft: 'auto', color: '#404854' }}
|
||||||
tagName="span"
|
tagName="span"
|
||||||
>
|
>
|
||||||
Pending <FormatNumber value={totalPending} currency={'USD'} />
|
Pending <FormatNumber value={totalPending} currency={'USD'} />
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { defaultTo } from 'lodash';
|
|
||||||
import React, { createContext } from 'react';
|
import React, { createContext } from 'react';
|
||||||
|
import { defaultTo } from 'lodash';
|
||||||
|
import * as R from 'ramda';
|
||||||
import { useGetBankTransactionsMatches } from '@/hooks/query/bank-rules';
|
import { useGetBankTransactionsMatches } from '@/hooks/query/bank-rules';
|
||||||
|
|
||||||
interface MatchingTransactionBootValues {
|
interface MatchingTransactionBootValues {
|
||||||
isMatchingTransactionsLoading: boolean;
|
isMatchingTransactionsLoading: boolean;
|
||||||
|
isMatchingTransactionsFetching: boolean;
|
||||||
|
isMatchingTransactionsSuccess: boolean;
|
||||||
possibleMatches: Array<any>;
|
possibleMatches: Array<any>;
|
||||||
perfectMatchesCount: number;
|
perfectMatchesCount: number;
|
||||||
perfectMatches: Array<any>;
|
perfectMatches: Array<any>;
|
||||||
@@ -26,13 +29,24 @@ function MatchingTransactionBoot({
|
|||||||
const {
|
const {
|
||||||
data: matchingTransactions,
|
data: matchingTransactions,
|
||||||
isLoading: isMatchingTransactionsLoading,
|
isLoading: isMatchingTransactionsLoading,
|
||||||
|
isFetching: isMatchingTransactionsFetching,
|
||||||
|
isSuccess: isMatchingTransactionsSuccess,
|
||||||
} = useGetBankTransactionsMatches(uncategorizedTransactionId);
|
} = useGetBankTransactionsMatches(uncategorizedTransactionId);
|
||||||
|
|
||||||
|
const possibleMatches = defaultTo(matchingTransactions?.possibleMatches, []);
|
||||||
|
const perfectMatchesCount = matchingTransactions?.perfectMatches?.length || 0;
|
||||||
|
const perfectMatches = defaultTo(matchingTransactions?.perfectMatches, []);
|
||||||
|
|
||||||
|
const matches = R.concat(perfectMatches, possibleMatches);
|
||||||
|
|
||||||
const provider = {
|
const provider = {
|
||||||
isMatchingTransactionsLoading,
|
isMatchingTransactionsLoading,
|
||||||
possibleMatches: defaultTo(matchingTransactions?.possibleMatches, []),
|
isMatchingTransactionsFetching,
|
||||||
perfectMatchesCount: matchingTransactions?.perfectMatches?.length || 0,
|
isMatchingTransactionsSuccess,
|
||||||
perfectMatches: defaultTo(matchingTransactions?.perfectMatches, []),
|
possibleMatches,
|
||||||
|
perfectMatchesCount,
|
||||||
|
perfectMatches,
|
||||||
|
matches,
|
||||||
} as MatchingTransactionBootValues;
|
} as MatchingTransactionBootValues;
|
||||||
|
|
||||||
return <RuleFormBootContext.Provider value={provider} {...props} />;
|
return <RuleFormBootContext.Provider value={provider} {...props} />;
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ export const useGetPendingAmountMatched = () => {
|
|||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
|
const matchedItems = [...perfectMatches, ...possibleMatches].filter(
|
||||||
(match) => {
|
(match) => {
|
||||||
const key = `${match.transactionType}-${match.transactionId}`;
|
const key = `${match.referenceType}-${match.referenceId}`;
|
||||||
return values.matched[key];
|
return values.matched[key];
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const totalMatchedAmount = matchedItems.reduce(
|
const totalMatchedAmount = matchedItems.reduce(
|
||||||
(total, item) => total + parseFloat(item.amount),
|
(total, item) =>
|
||||||
|
total +
|
||||||
|
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const amount = uncategorizedTransaction.amount;
|
const amount = uncategorizedTransaction.amount;
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ export const withBanking = (mapState) => {
|
|||||||
selectedUncategorizedTransactionId:
|
selectedUncategorizedTransactionId:
|
||||||
state.plaid.uncategorizedTransactionIdForMatching,
|
state.plaid.uncategorizedTransactionIdForMatching,
|
||||||
openReconcileMatchingTransaction:
|
openReconcileMatchingTransaction:
|
||||||
state.plaid.openReconcileMatchingTransaction,
|
state.plaid.openReconcileMatchingTransaction.isOpen,
|
||||||
|
|
||||||
|
reconcileMatchingTransactionPendingAmount:
|
||||||
|
state.plaid.openReconcileMatchingTransaction.pending,
|
||||||
};
|
};
|
||||||
return mapState ? mapState(mapped, state, props) : mapped;
|
return mapState ? mapState(mapped, state, props) : mapped;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export interface WithBankingActionsProps {
|
|||||||
setUncategorizedTransactionIdForMatching: (
|
setUncategorizedTransactionIdForMatching: (
|
||||||
uncategorizedTransactionId: number,
|
uncategorizedTransactionId: number,
|
||||||
) => void;
|
) => void;
|
||||||
openReconcileMatchingTransaction: () => void;
|
openReconcileMatchingTransaction: (pendingAmount: number) => void;
|
||||||
closeReconcileMatchingTransaction: () => void;
|
closeReconcileMatchingTransaction: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,8 +24,8 @@ const mapDipatchToProps = (dispatch: any): WithBankingActionsProps => ({
|
|||||||
dispatch(
|
dispatch(
|
||||||
setUncategorizedTransactionIdForMatching(uncategorizedTransactionId),
|
setUncategorizedTransactionIdForMatching(uncategorizedTransactionId),
|
||||||
),
|
),
|
||||||
openReconcileMatchingTransaction: () =>
|
openReconcileMatchingTransaction: (pendingAmount: number) =>
|
||||||
dispatch(openReconcileMatchingTransaction()),
|
dispatch(openReconcileMatchingTransaction({ pending: pendingAmount })),
|
||||||
closeReconcileMatchingTransaction: () =>
|
closeReconcileMatchingTransaction: () =>
|
||||||
dispatch(closeReconcileMatchingTransaction()),
|
dispatch(closeReconcileMatchingTransaction()),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ interface CreateBankRuleResponse {}
|
|||||||
/**
|
/**
|
||||||
* Creates a new bank rule.
|
* Creates a new bank rule.
|
||||||
* @param {UseMutationOptions<CreateBankRuleValues, Error, CreateBankRuleValues>} options -
|
* @param {UseMutationOptions<CreateBankRuleValues, Error, CreateBankRuleValues>} options -
|
||||||
* @returns {UseMutationResult<CreateBankRuleValues, Error, CreateBankRuleValues>}
|
* @returns {UseMutationResult<CreateBankRuleValues, Error, CreateBankRuleValues>}TCHES
|
||||||
*/
|
*/
|
||||||
export function useCreateBankRule(
|
export function useCreateBankRule(
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
@@ -235,6 +235,12 @@ export function useExcludeUncategorizedTransaction(
|
|||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||||
);
|
);
|
||||||
|
// Invalidate accounts.
|
||||||
|
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||||
|
queryClient.invalidateQueries(t.ACCOUNT);
|
||||||
|
|
||||||
|
// invalidate bank account summary.
|
||||||
|
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
@@ -282,6 +288,12 @@ export function useUnexcludeUncategorizedTransaction(
|
|||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||||
);
|
);
|
||||||
|
// Invalidate accounts.
|
||||||
|
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||||
|
queryClient.invalidateQueries(t.ACCOUNT);
|
||||||
|
|
||||||
|
// Invalidate bank account summary.
|
||||||
|
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
@@ -322,6 +334,60 @@ export function useMatchUncategorizedTransaction(
|
|||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||||
);
|
);
|
||||||
|
queryClient.invalidateQueries(t.CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY);
|
||||||
|
|
||||||
|
// Invalidate accounts.
|
||||||
|
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||||
|
queryClient.invalidateQueries(t.ACCOUNT);
|
||||||
|
|
||||||
|
// Invalidate bank account summary.
|
||||||
|
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnmatchUncategorizedTransactionValues {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
interface UnmatchUncategorizedTransactionRes {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmatch the given matched uncategorized transaction.
|
||||||
|
* @param {UseMutationOptions<UnmatchUncategorizedTransactionRes, Error, UnmatchUncategorizedTransactionValues>} props
|
||||||
|
* @returns {UseMutationResult<UnmatchUncategorizedTransactionRes, Error, UnmatchUncategorizedTransactionValues>}
|
||||||
|
*/
|
||||||
|
export function useUnmatchMatchedUncategorizedTransaction(
|
||||||
|
props?: UseMutationOptions<
|
||||||
|
UnmatchUncategorizedTransactionRes,
|
||||||
|
Error,
|
||||||
|
UnmatchUncategorizedTransactionValues
|
||||||
|
>,
|
||||||
|
): UseMutationResult<
|
||||||
|
UnmatchUncategorizedTransactionRes,
|
||||||
|
Error,
|
||||||
|
UnmatchUncategorizedTransactionValues
|
||||||
|
> {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useMutation<
|
||||||
|
UnmatchUncategorizedTransactionRes,
|
||||||
|
Error,
|
||||||
|
UnmatchUncategorizedTransactionValues
|
||||||
|
>(({ id }) => apiRequest.post(`/banking/matches/unmatch/${id}`), {
|
||||||
|
onSuccess: (res, id) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries(t.CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY);
|
||||||
|
|
||||||
|
// Invalidate accounts.
|
||||||
|
queryClient.invalidateQueries(t.ACCOUNTS);
|
||||||
|
queryClient.invalidateQueries(t.ACCOUNT);
|
||||||
|
|
||||||
|
// Invalidate bank account summary.
|
||||||
|
queryClient.invalidateQueries(QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export function useCreateCashflowTransaction(props) {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
// Invalidate queries.
|
// Invalidate queries.
|
||||||
commonInvalidateQueries(queryClient);
|
commonInvalidateQueries(queryClient);
|
||||||
|
|
||||||
|
queryClient.invalidateQueries('BANK_TRANSACTION_MATCHES');
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
@@ -251,6 +253,9 @@ export function useCategorizeTransaction(props) {
|
|||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Invalidate bank account summary.
|
||||||
|
queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META');
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
@@ -274,6 +279,9 @@ export function useUncategorizeTransaction(props) {
|
|||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(
|
||||||
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Invalidate bank account summary.
|
||||||
|
queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META');
|
||||||
},
|
},
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -629,4 +629,10 @@ export default {
|
|||||||
],
|
],
|
||||||
viewBox: '0 0 16 16',
|
viewBox: '0 0 16 16',
|
||||||
},
|
},
|
||||||
|
unlink: {
|
||||||
|
path: [
|
||||||
|
'M11.9975 0.00500107C14.2061 0.00500107 15.995 1.79388 15.995 4.0025C15.995 5.11181 15.5353 6.0912 14.8058 6.81075L14.8257 6.83074L13.8264 7.83011L13.8064 7.81012C13.2562 8.36798 12.5482 8.76807 11.7539 8.92548L10.8249 7.99643L12.4073 6.401L13.4066 5.40163L13.3966 5.39163C13.7564 5.03186 13.9963 4.54217 13.9963 3.99251C13.9963 2.8932 13.0968 1.99376 11.9975 1.99376C11.4479 1.99376 10.9582 2.23361 10.5984 2.59338L10.5884 2.58339L8.0001 5.17168L7.07559 4.24717C7.23518 3.45247 7.63943 2.74409 8.18989 2.19363L8.1699 2.17365L9.16928 1.17427L9.18926 1.19426C9.90882 0.464714 10.8982 0.00500107 11.9975 0.00500107ZM2.29289 2.29289C2.68341 1.90237 3.31657 1.90237 3.7071 2.29289L13.7071 12.2929C14.0976 12.6834 14.0976 13.3166 13.7071 13.7071C13.3166 14.0976 12.6834 14.0976 12.2929 13.7071L8.93565 10.3499C8.97565 10.562 8.99938 10.7781 8.99938 10.9981C8.99938 12.0974 8.53966 13.0868 7.81012 13.8064L7.83011 13.8263L6.83073 14.8257L6.81074 14.8057C6.09119 15.5353 5.10181 15.995 4.0025 15.995C1.79388 15.995 0.00499688 14.2061 0.00499688 11.9975C0.00499688 10.8982 0.464709 9.90879 1.19425 9.18924L1.17427 9.16925L2.17364 8.16988L2.19363 8.18986C2.91318 7.46032 3.90256 7.00061 5.00187 7.00061C5.2251 7.00061 5.44064 7.02369 5.65087 7.06509L2.29289 3.7071C1.90236 3.31658 1.90236 2.68341 2.29289 2.29289ZM8.00244 9.41666L8.707 10.1212L5.41162 13.4166L5.40162 13.4066C5.04185 13.7664 4.55216 14.0062 4.0025 14.0062C2.90319 14.0062 2.00375 13.1068 2.00375 12.0075C2.00375 11.4578 2.2436 10.9681 2.60337 10.6084L2.59338 10.5984L5.88876 7.30298L6.58333 7.99755L4.29231 10.2886C4.11243 10.4685 4.00249 10.7183 4.00249 10.9981C4.00249 11.5478 4.45221 11.9975 5.00187 11.9975C5.28169 11.9975 5.53154 11.8876 5.71143 11.7077L8.00244 9.41666ZM8.70466 5.87623L10.1238 7.29534L11.7077 5.71143C11.8876 5.53154 11.9975 5.2817 11.9975 5.00187C11.9975 4.45222 11.5478 4.0025 10.9981 4.0025C10.7183 4.0025 10.4685 4.11243 10.2886 4.29232L8.70466 5.87623Z',
|
||||||
|
],
|
||||||
|
viewBox: '0 0 16 16',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ interface StorePlaidState {
|
|||||||
plaidToken: string;
|
plaidToken: string;
|
||||||
openMatchingTransactionAside: boolean;
|
openMatchingTransactionAside: boolean;
|
||||||
uncategorizedTransactionIdForMatching: number | null;
|
uncategorizedTransactionIdForMatching: number | null;
|
||||||
openReconcileMatchingTransaction: boolean;
|
openReconcileMatchingTransaction: { isOpen: boolean; pending: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaidSlice = createSlice({
|
export const PlaidSlice = createSlice({
|
||||||
@@ -13,7 +13,10 @@ export const PlaidSlice = createSlice({
|
|||||||
plaidToken: '',
|
plaidToken: '',
|
||||||
openMatchingTransactionAside: false,
|
openMatchingTransactionAside: false,
|
||||||
uncategorizedTransactionIdForMatching: null,
|
uncategorizedTransactionIdForMatching: null,
|
||||||
openReconcileMatchingTransaction: false,
|
openReconcileMatchingTransaction: {
|
||||||
|
isOpen: false,
|
||||||
|
pending: 0,
|
||||||
|
},
|
||||||
} as StorePlaidState,
|
} as StorePlaidState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setPlaidId: (state: StorePlaidState, action: PayloadAction<string>) => {
|
setPlaidId: (state: StorePlaidState, action: PayloadAction<string>) => {
|
||||||
@@ -37,12 +40,17 @@ export const PlaidSlice = createSlice({
|
|||||||
state.uncategorizedTransactionIdForMatching = null;
|
state.uncategorizedTransactionIdForMatching = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
openReconcileMatchingTransaction: (state: StorePlaidState) => {
|
openReconcileMatchingTransaction: (
|
||||||
state.openReconcileMatchingTransaction = true;
|
state: StorePlaidState,
|
||||||
|
action: PayloadAction<{ pending: number }>,
|
||||||
|
) => {
|
||||||
|
state.openReconcileMatchingTransaction.isOpen = true;
|
||||||
|
state.openReconcileMatchingTransaction.pending = action.payload.pending;
|
||||||
},
|
},
|
||||||
|
|
||||||
closeReconcileMatchingTransaction: (state: StorePlaidState) => {
|
closeReconcileMatchingTransaction: (state: StorePlaidState) => {
|
||||||
state.openReconcileMatchingTransaction = false;
|
state.openReconcileMatchingTransaction.isOpen = false;
|
||||||
|
state.openReconcileMatchingTransaction.pending = 0;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,3 +50,9 @@ $form-check-input-indeterminate-bg-image: url("data:image/svg+xml,<svg xmlns='ht
|
|||||||
// z-indexs
|
// z-indexs
|
||||||
$zindex-dashboard-splash-screen: 39;
|
$zindex-dashboard-splash-screen: 39;
|
||||||
$zindex-toast: 40;
|
$zindex-toast: 40;
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
$control-checked-background-color: #0069ff !default;
|
||||||
|
$control-checked-background-color-hover: #0069ff !default;
|
||||||
|
$control-checked-background-color-active: #0069ff !default;
|
||||||
|
$control-box-shadow: inset 0 0 0 1px #666 !default;
|
||||||
@@ -271,203 +271,6 @@ label.bp4-label {
|
|||||||
.select-list--tooltip-items .bp4-popover-target {
|
.select-list--tooltip-items .bp4-popover-target {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin control-checked-colors($selector: ':checked') {
|
|
||||||
input#{$selector}~.#{$ns}-control-indicator {
|
|
||||||
box-shadow: none;
|
|
||||||
background-color: $control-checked-background-color;
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover input#{$selector}~.#{$ns}-control-indicator {
|
|
||||||
box-shadow: none;
|
|
||||||
background-color: $control-checked-background-color-hover;
|
|
||||||
border-color: $control-checked-background-color-active;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:not(:disabled):active#{$selector}~.#{$ns}-control-indicator {
|
|
||||||
box-shadow: none;
|
|
||||||
background-color: $control-checked-background-color-active;
|
|
||||||
border-color: $control-checked-background-color-active;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:disabled#{$selector}~.#{$ns}-control-indicator {
|
|
||||||
box-shadow: none;
|
|
||||||
background: rgba($control-checked-background-color, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///@extend
|
|
||||||
.#{$ns}-control {
|
|
||||||
input:checked~.#{$ns}-control-indicator {
|
|
||||||
box-shadow: none;
|
|
||||||
background-color: transparent;
|
|
||||||
background-image: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover input:checked~.#{$ns}-control-indicator {
|
|
||||||
box-shadow: none;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:not(:disabled):active:checked~.#{$ns}-control-indicator {
|
|
||||||
box-shadow: none;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:disabled:checked~.#{$ns}-control-indicator {
|
|
||||||
box-shadow: none;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.#{$ns}-disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
color: $pt-text-color-disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.#{$ns}-inline {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: $pt-grid-size * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.#{$ns}-control-indicator {
|
|
||||||
box-shadow: 0 0 0 transparent;
|
|
||||||
background-clip: padding-box;
|
|
||||||
background-color: transparent;
|
|
||||||
background-image: none;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bp4-large .#{$ns}-control-indicator {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .#{$ns}-control-indicator {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:not(:disabled):active~.#{$ns}-control-indicator {
|
|
||||||
box-shadow: 0 0 0 transparent;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Checkbox
|
|
||||||
|
|
||||||
Markup:
|
|
||||||
<label class="#{$ns}-control #{$ns}-checkbox {{.modifier}}">
|
|
||||||
<input type="checkbox" {{:modifier}} />
|
|
||||||
<span class="#{$ns}-control-indicator"></span>
|
|
||||||
Checkbox
|
|
||||||
</label>
|
|
||||||
|
|
||||||
:checked - Checked
|
|
||||||
:disabled - Disabled. Also add <code>.#{$ns}-disabled</code> to <code>.#{$ns}-control</code> to change text color (not shown below).
|
|
||||||
:indeterminate - Indeterminate. Note that this style can only be achieved via JavaScript
|
|
||||||
<code>input.indeterminate = true</code>.
|
|
||||||
.#{$ns}-align-right - Right-aligned indicator
|
|
||||||
.#{$ns}-large - Large
|
|
||||||
|
|
||||||
Styleguide checkbox
|
|
||||||
*/
|
|
||||||
&.#{$ns}-checkbox {
|
|
||||||
&:hover input:indeterminate~.#{$ns}-control-indicator {
|
|
||||||
// box-shadow: 0 0 0 transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin indicator-inline-icon($icon) {
|
|
||||||
&::before {
|
|
||||||
// embed SVG icon image as backgroud-image above gradient.
|
|
||||||
// the SVG image content is inlined into the CSS, so use this sparingly.
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include control-checked-colors(':checked');
|
|
||||||
|
|
||||||
// make :indeterminate look like :checked _for Checkbox only_
|
|
||||||
@include control-checked-colors(':indeterminate');
|
|
||||||
|
|
||||||
.#{$ns}-control-indicator {
|
|
||||||
border: 1px solid #c6c6c6;
|
|
||||||
border-radius: $pt-border-radius;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:checked~.#{$ns}-control-indicator {
|
|
||||||
background-image: escape-svg($form-check-input-checked-bg-image);
|
|
||||||
border-color: $form-check-input-checked-bg-color;
|
|
||||||
background-color: $form-check-input-checked-bg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:indeterminate~.#{$ns}-control-indicator {
|
|
||||||
// background-image: escape-svg($form-check-input-indeterminate-bg-image);
|
|
||||||
border-color: $form-check-input-checked-bg-color;
|
|
||||||
background-color: $form-check-input-checked-bg-color;
|
|
||||||
box-shadow: 0 0 0 0 transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Radio
|
|
||||||
|
|
||||||
Markup:
|
|
||||||
<label class="#{$ns}-control #{$ns}-radio {{.modifier}}">
|
|
||||||
<input type="radio" name="docs-radio-regular" {{:modifier}} />
|
|
||||||
<span class="#{$ns}-control-indicator"></span>
|
|
||||||
Radio
|
|
||||||
</label>
|
|
||||||
|
|
||||||
:checked - Selected
|
|
||||||
:disabled - Disabled. Also add <code>.#{$ns}-disabled</code> to <code>.#{$ns}-control</code> to change text color (not shown below).
|
|
||||||
.#{$ns}-align-right - Right-aligned indicator
|
|
||||||
.#{$ns}-large - Large
|
|
||||||
|
|
||||||
Styleguide radio
|
|
||||||
*/
|
|
||||||
&.#{$ns}-radio {
|
|
||||||
.#{$ns}-control-indicator {
|
|
||||||
border: 2px solid #cecece;
|
|
||||||
background-color: #fff;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
height: 14px;
|
|
||||||
width: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input:checked~.#{$ns}-control-indicator {
|
|
||||||
border-color: $form-check-input-checked-bg-color;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-image: radial-gradient($form-check-input-checked-bg-color 40%,
|
|
||||||
transparent 40%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input:checked:disabled~.#{$ns}-control-indicator::before {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus~.#{$ns}-control-indicator {
|
|
||||||
-moz-outline-radius: $control-indicator-size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bp4-menu-item::before,
|
.bp4-menu-item::before,
|
||||||
.bp4-menu-item>.bp4-icon {
|
.bp4-menu-item>.bp4-icon {
|
||||||
color: #4b5d6b;
|
color: #4b5d6b;
|
||||||
|
|||||||
Reference in New Issue
Block a user