Merge pull request #523 from bigcapitalhq/matching-transactions-fixes

fix: Matching transactions bugs
This commit is contained in:
Ahmed Bouhuolia
2024-07-08 22:18:12 +02:00
committed by GitHub
32 changed files with 389 additions and 133 deletions

View File

@@ -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()

View File

@@ -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');
}); });
}; };

View File

@@ -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');

View File

@@ -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 {

View File

@@ -0,0 +1,8 @@
import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
export interface IImportFileCommitedEventPayload {
tenantId: number;
importId: number;
meta: ImportFilePreviewPOJO;
}

View File

@@ -112,6 +112,7 @@ import { RecognizeSyncedBankTranasctions } from '@/services/Banking/Plaid/subscr
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 { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude'; 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();
@@ -262,6 +263,7 @@ export const susbcribers = () => {
UnlinkBankRuleOnDeleteBankRule, UnlinkBankRuleOnDeleteBankRule,
DecrementUncategorizedTransactionOnMatching, DecrementUncategorizedTransactionOnMatching,
DecrementUncategorizedTransactionOnExclude, DecrementUncategorizedTransactionOnExclude,
DecrementUncategorizedTransactionOnCategorize,
// Validate matching // Validate matching
ValidateMatchingOnCashflowDelete, ValidateMatchingOnCashflowDelete,

View File

@@ -184,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);
}
} }

View File

@@ -39,7 +39,6 @@ export class DecrementUncategorizedTransactionOnMatching {
const transaction = await UncategorizedCashflowTransaction.query().findById( const transaction = await UncategorizedCashflowTransaction.query().findById(
uncategorizedTransactionId uncategorizedTransactionId
); );
//
await Account.query(trx) await Account.query(trx)
.findById(transaction.accountId) .findById(transaction.accountId)
.decrement('uncategorizedTransactions', 1); .decrement('uncategorizedTransactions', 1);
@@ -60,7 +59,6 @@ export class DecrementUncategorizedTransactionOnMatching {
const transaction = await UncategorizedCashflowTransaction.query().findById( const transaction = await UncategorizedCashflowTransaction.query().findById(
uncategorizedTransactionId uncategorizedTransactionId
); );
//
await Account.query(trx) await Account.query(trx)
.findById(transaction.accountId) .findById(transaction.accountId)
.increment('uncategorizedTransactions', 1); .increment('uncategorizedTransactions', 1);

View File

@@ -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);

View File

@@ -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);
}
} }

View File

@@ -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
); );

View File

@@ -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,
};
}
} }

View File

@@ -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);
}
}

View File

@@ -74,7 +74,7 @@ export class CashflowAccountTransactionReport extends FinancialSheet {
const firstMatchedTrans = first(matchedTrans); const firstMatchedTrans = first(matchedTrans);
return ( return (
(firstCategorizedTrans?.id || firstCategorizedTrans?.id ||
firstMatchedTrans?.uncategorizedTransactionId || firstMatchedTrans?.uncategorizedTransactionId ||
null null
); );

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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,7 +78,7 @@ 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);
} }
/** /**

View File

@@ -647,4 +647,9 @@ export default {
onUnexcluding: 'onBankTransactionUnexcluding', onUnexcluding: 'onBankTransactionUnexcluding',
onUnexcluded: 'onBankTransactionUnexcluded', onUnexcluded: 'onBankTransactionUnexcluded',
}, },
// Import files.
import: {
onImportCommitted: 'onImportFileCommitted',
},
}; };

View File

@@ -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'],
};

View File

@@ -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>
);
}

View File

@@ -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) || [];
};

View File

@@ -9,7 +9,6 @@ import {
PopoverInteractionKind, PopoverInteractionKind,
Position, Position,
Tooltip, Tooltip,
MenuDivider,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { Box, FormatDateCell, Icon, MaterialProgressBar } from '@/components'; import { Box, FormatDateCell, Icon, MaterialProgressBar } from '@/components';
import { useAccountTransactionsContext } from './AccountTransactionsProvider'; import { useAccountTransactionsContext } from './AccountTransactionsProvider';
@@ -213,9 +212,8 @@ export function useAccountUncategorizedTransactionsColumns() {
{ {
id: 'reference_number', id: 'reference_number',
Header: 'Ref.#', Header: 'Ref.#',
accessor: 'reference_number', accessor: 'reference_no',
width: 50, width: 50,
className: 'reference_number',
clickable: true, clickable: true,
textOverview: true, textOverview: true,
}, },

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'),
}); });

View File

@@ -1,14 +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 { import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
Form,
Formik,
FormikHelpers,
FormikValues,
useFormikContext,
} from 'formik';
import moment from 'moment'; import moment from 'moment';
import { round } from 'lodash';
import { import {
AccountsSelect, AccountsSelect,
AppToaster, AppToaster,
@@ -19,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';
@@ -100,7 +96,7 @@ function MatchingReconcileTransactionFormRoot({
const _initialValues = { const _initialValues = {
...initialValues, ...initialValues,
amount: Math.abs(reconcileMatchingTransactionPendingAmount) || 0, amount: round(Math.abs(reconcileMatchingTransactionPendingAmount), 2) || 0,
date: moment().format('YYYY-MM-DD'), date: moment().format('YYYY-MM-DD'),
type: type:
reconcileMatchingTransactionPendingAmount > 0 ? 'deposit' : 'withdrawal', reconcileMatchingTransactionPendingAmount > 0 ? 'deposit' : 'withdrawal',
@@ -179,7 +175,7 @@ function CreateReconcileTransactionContent() {
}, },
boundary: 'viewport', boundary: 'viewport',
}} }}
inputProps={{ fill: true }} inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
fill fill
fastField fastField
/> />

View File

@@ -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,
}, },
@@ -323,6 +335,13 @@ export function useMatchUncategorizedTransaction(
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
); );
queryClient.invalidateQueries(t.CASHFLOW_ACCOUNT_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,
}); });
@@ -362,6 +381,13 @@ export function useUnmatchMatchedUncategorizedTransaction(
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY, t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
); );
queryClient.invalidateQueries(t.CASHFLOW_ACCOUNT_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,
}); });

View File

@@ -253,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,
}, },
@@ -276,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,
}, },