Compare commits

...

8 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
59168bc691 fix: Plaid transactions syncing 2024-07-12 23:43:20 +02:00
Ahmed Bouhuolia
81b26c6f13 fix(hotfix): uniqid import 2024-07-12 20:15:28 +02:00
Ahmed Bouhuolia
da435d85d9 Merge pull request #524 from bigcapitalhq/fix-cashflow-transactions-type
fix: Cashflow transactions types
2024-07-09 14:57:43 +02:00
Ahmed Bouhuolia
d096e49d45 Merge pull request #523 from bigcapitalhq/matching-transactions-fixes
fix: Matching transactions bugs
2024-07-08 22:18:12 +02:00
Ahmed Bouhuolia
73acdb6240 fix: add bank rule categories 2024-07-08 21:48:16 +02:00
Ahmed Bouhuolia
38d4122d11 fix: matching transactions bugs 2024-07-08 19:37:11 +02:00
Ahmed Bouhuolia
24a77c81b3 fix: unexpected char in cashflow transactions report 2024-07-08 15:25:28 +02:00
Ahmed Bouhuolia
7f41b4280e fix: the database migration schema 2024-07-08 15:18:58 +02:00
36 changed files with 464 additions and 146 deletions

View File

@@ -5,7 +5,8 @@ exports.up = function (knex) {
.integer('uncategorized_transaction_id')
.unsigned()
.references('id')
.inTable('uncategorized_cashflow_transactions');
.inTable('uncategorized_cashflow_transactions')
.withKeyName('recognizedBankTransactionsUncategorizedTransIdForeign');
table
.integer('bank_rule_id')
.unsigned()

View File

@@ -1,6 +1,11 @@
exports.up = function (knex) {
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) {
return knex.schema.createTable('matched_bank_transactions', (table) => {
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.integer('reference_id').unsigned();
table.decimal('amount');

View File

@@ -1,3 +1,4 @@
import { Knex } from 'knex';
import {
IFinancialSheetCommonMeta,
INumberFormatQuery,
@@ -257,7 +258,6 @@ export interface IUncategorizedCashflowTransaction {
categorized: boolean;
}
export interface CreateUncategorizedTransactionDTO {
date: Date | string;
accountId: number;
@@ -269,3 +269,16 @@ export interface CreateUncategorizedTransactionDTO {
plaidTransactionId?: string | null;
batch?: string;
}
export interface IUncategorizedTransactionCreatingEventPayload {
tenantId: number;
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
trx: Knex.Transaction;
}
export interface IUncategorizedTransactionCreatedEventPayload {
tenantId: number;
uncategorizedTransaction: any;
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
trx: Knex.Transaction;
}

View File

@@ -130,8 +130,9 @@ export interface ICommandCashflowDeletedPayload {
export interface ICashflowTransactionCategorizedPayload {
tenantId: number;
cashflowTransactionId: number;
uncategorizedTransaction: any;
cashflowTransaction: ICashflowTransaction;
categorizeDTO: any;
trx: Knex.Transaction;
}
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 { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
export default () => {
return new EventPublisher();
@@ -262,6 +263,7 @@ export const susbcribers = () => {
UnlinkBankRuleOnDeleteBankRule,
DecrementUncategorizedTransactionOnMatching,
DecrementUncategorizedTransactionOnExclude,
DecrementUncategorizedTransactionOnCategorize,
// Validate matching
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(
uncategorizedTransactionId
);
//
await Account.query(trx)
.findById(transaction.accountId)
.decrement('uncategorizedTransactions', 1);
@@ -60,7 +59,6 @@ export class DecrementUncategorizedTransactionOnMatching {
const transaction = await UncategorizedCashflowTransaction.query().findById(
uncategorizedTransactionId
);
//
await Account.query(trx)
.findById(transaction.accountId)
.increment('uncategorizedTransactions', 1);

View File

@@ -17,7 +17,7 @@ import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTra
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
import { Knex } from 'knex';
import { uniqid } from 'uniqid';
import uniqid from 'uniqid';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@@ -148,7 +148,6 @@ export class PlaidSyncDb {
*/
public async syncAccountsTransactions(
tenantId: number,
batchNo: string,
plaidAccountsTransactions: PlaidTransaction[],
trx?: Knex.Transaction
): Promise<void> {
@@ -161,7 +160,6 @@ export class PlaidSyncDb {
return this.syncAccountTranactions(
tenantId,
plaidAccountId,
batchNo,
plaidTransactions,
trx
);

View File

@@ -37,7 +37,6 @@ export const transformPlaidAccountToCreateAccount = R.curry(
export const transformPlaidTrxsToCashflowCreate = R.curry(
(
cashflowAccountId: number,
creditAccountId: number,
plaidTranasction: PlaidTransaction
): CreateUncategorizedTransactionDTO => {
return {

View File

@@ -18,11 +18,11 @@ export class RegonizeTransactionsJob {
* Triggers sending invoice mail.
*/
private handler = async (job, done: Function) => {
const { tenantId } = job.attrs.data;
const { tenantId, batch } = job.attrs.data;
const regonizeTransactions = Container.get(RecognizeTranasctionsService);
try {
await regonizeTransactions.recognizeTransactions(tenantId);
await regonizeTransactions.recognizeTransactions(tenantId, batch);
done();
} catch (error) {
console.log(error);

View File

@@ -5,6 +5,8 @@ import {
IBankRuleEventDeletedPayload,
IBankRuleEventEditedPayload,
} from '../../Rules/types';
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
import { Import } from '@/system/models';
@Service()
export class TriggerRecognizedTransactions {
@@ -27,6 +29,10 @@ export class TriggerRecognizedTransactions {
events.bankRules.onDeleted,
this.recognizedTransactionsOnRuleDeleted.bind(this)
);
bus.subscribe(
events.import.onImportCommitted,
this.triggerRecognizeTransactionsOnImportCommitted.bind(this)
);
}
/**
@@ -73,4 +79,20 @@ export class TriggerRecognizedTransactions {
const payload = { tenantId };
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
);
// Updates the uncategorized transaction as categorized.
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
uncategorizedTransactionId,
{
categorized: true,
categorizeRefType: 'CashflowTransaction',
categorizeRefId: cashflowTransaction.id,
}
);
const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
uncategorizedTransactionId,
{
categorized: true,
categorizeRefType: 'CashflowTransaction',
categorizeRefId: cashflowTransaction.id,
}
);
// Triggers `onCashflowTransactionCategorized` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionCategorized,
{
tenantId,
// cashflowTransaction,
cashflowTransaction,
uncategorizedTransaction,
categorizeDTO,
trx,
} as ICashflowTransactionCategorizedPayload
);

View File

@@ -2,7 +2,13 @@ import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import {
CreateUncategorizedTransactionDTO,
IUncategorizedTransactionCreatedEventPayload,
IUncategorizedTransactionCreatingEventPayload,
} from '@/interfaces';
@Service()
export class CreateUncategorizedTransaction {
@@ -12,6 +18,9 @@ export class CreateUncategorizedTransaction {
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates an uncategorized cashflow transaction.
* @param {number} tenantId
@@ -19,7 +28,7 @@ export class CreateUncategorizedTransaction {
*/
public create(
tenantId: number,
createDTO: CreateUncategorizedTransactionDTO,
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
trx?: Knex.Transaction
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
@@ -27,12 +36,30 @@ export class CreateUncategorizedTransaction {
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
const transaction = await UncategorizedCashflowTransaction.query(
trx
).insertAndFetch({
...createDTO,
});
return transaction;
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionUncategorizedCreated,
{
tenantId,
createUncategorizedTransactionDTO,
trx,
} as IUncategorizedTransactionCreatingEventPayload
);
const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query(trx).insertAndFetch({
...createUncategorizedTransactionDTO,
});
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionUncategorizedCreated,
{
tenantId,
uncategorizedTransaction,
createUncategorizedTransactionDTO,
trx,
} as IUncategorizedTransactionCreatedEventPayload
);
return uncategorizedTransaction;
},
trx
);

View File

@@ -1,6 +1,7 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import * as yup from 'yup';
import uniqid from 'uniqid';
import { Importable } from '../Import/Importable';
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
@@ -15,6 +16,7 @@ export class UncategorizedTransactionsImportable extends Importable {
@Inject()
private tenancy: HasTenancyService;
/**
* Passing the sheet DTO to create uncategorized transaction.
* @param {number} tenantId
@@ -43,6 +45,7 @@ export class UncategorizedTransactionsImportable extends Importable {
return {
...createDTO,
accountId: context.import.paramsParsed.accountId,
batch: context.import.paramsParsed.batch,
};
}
@@ -54,6 +57,9 @@ export class UncategorizedTransactionsImportable extends Importable {
return BankTransactionsSampleData;
}
// ------------------
// # Params
// ------------------
/**
* Params validation schema.
* @returns {ValidationSchema[]}
@@ -79,4 +85,17 @@ export class UncategorizedTransactionsImportable extends Importable {
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,78 @@
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)
);
bus.subscribe(
events.cashflow.onTransactionUncategorizedCreated,
this.incrementUncategoirzedTransactionsOnCreated.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);
}
/**
* Increments uncategorized transactions count once creating a new transaction.
* @param {ICommandCashflowCreatedPayload} payload -
*/
public async incrementUncategoirzedTransactionsOnCreated({
tenantId,
uncategorizedTransaction,
trx,
}: any) {
const { Account } = this.tenancy.models(tenantId);
if (!uncategorizedTransaction.accountId) return;
await Account.query(trx)
.findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1);
}
}

View File

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

View File

@@ -15,14 +15,10 @@ import { ServiceError } from '@/exceptions';
import { getUniqueImportableValue, trimObject } from './_utils';
import { ImportableResources } from './ImportableResources';
import ResourceService from '../Resource/ResourceService';
import HasTenancyService from '../Tenancy/TenancyService';
import { Import } from '@/system/models';
@Service()
export class ImportFileCommon {
@Inject()
private tenancy: HasTenancyService;
@Inject()
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 { ImportSampleService } from './ImportSample';
import { ImportFileMeta } from './ImportFileMeta';
import { ImportFileProcessCommit } from './ImportFileProcessCommit';
@Inject()
export class ImportResourceApplication {
@@ -27,6 +28,9 @@ export class ImportResourceApplication {
@Inject()
private importMetaService: ImportFileMeta;
@Inject()
private importProcessCommit: ImportFileProcessCommit;
/**
* Reads the imported file and stores the import file meta under unqiue id.
* @param {number} tenantId -
@@ -74,12 +78,12 @@ export class ImportResourceApplication {
* @returns {Promise<ImportFilePreviewPOJO>}
*/
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.
* @param {number} tenantId -
* @param {number} tenantId -
* @param {string} importId - Import id.
* @returns {}
*/

View File

@@ -399,6 +399,9 @@ export default {
onTransactionCategorizing: 'onTransactionCategorizing',
onTransactionCategorized: 'onCashflowTransactionCategorized',
onTransactionUncategorizedCreating: 'onTransactionUncategorizedCreating',
onTransactionUncategorizedCreated: 'onTransactionUncategorizedCreated',
onTransactionUncategorizing: 'onTransactionUncategorizing',
onTransactionUncategorized: 'onTransactionUncategorized',
@@ -647,4 +650,9 @@ export default {
onUnexcluding: 'onBankTransactionUnexcluding',
onUnexcluded: 'onBankTransactionUnexcluded',
},
// Import files.
import: {
onImportCommitted: 'onImportFileCommitted',
},
};

View File

@@ -39,3 +39,12 @@ export const TRANSACRIONS_TYPE = [
'OtherExpense',
'TransferToAccount',
];
export const MoneyCategoryPerCreditAccountRootType = {
OwnerContribution: ['equity'],
OtherIncome: ['income'],
OwnerDrawing: ['equity'],
OtherExpense: ['expense'],
TransferToAccount: ['asset'],
TransferFromAccount: ['asset'],
};

View File

@@ -1,4 +1,5 @@
// @ts-nocheck
import { useCallback, useMemo } from 'react';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import { Button, Classes, Intent, Radio, Tag } from '@blueprintjs/core';
import * as R from 'ramda';
@@ -16,11 +17,11 @@ import {
} from '@/components';
import { useCreateBankRule, useEditBankRule } from '@/hooks/query/bank-rules';
import {
AssignTransactionTypeOptions,
FieldCondition,
Fields,
RuleFormValues,
TransactionTypeOptions,
getAccountRootFromMoneyCategory,
initialValues,
} from './_utils';
import { useRuleFormDialogBoot } from './RuleFormBoot';
@@ -31,6 +32,11 @@ import {
} from '@/utils';
import withDialogActions from '@/containers/Dialog/withDialogActions';
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({
// #withDialogActions
@@ -47,7 +53,6 @@ function RuleFormContentFormRoot({
...initialValues,
...transformToForm(transformToCamelCase(bankRule), initialValues),
};
// Handles the form submitting.
const handleSubmit = (
values: RuleFormValues,
@@ -92,8 +97,9 @@ function RuleFormContentFormRoot({
label={'Rule Name'}
labelInfo={<Tag minimal>Required</Tag>}
style={{ maxWidth: 300 }}
fastField
>
<FInputGroup name={'name'} />
<FInputGroup name={'name'} fastField />
</FFormGroup>
<FFormGroup
@@ -101,29 +107,22 @@ function RuleFormContentFormRoot({
label={'Apply the rule to account'}
labelInfo={<Tag minimal>Required</Tag>}
style={{ maxWidth: 350 }}
fastField
>
<AccountsSelect
name={'applyIfAccountId'}
items={accounts}
filterByTypes={['cash', 'bank']}
fastField
/>
</FFormGroup>
<FFormGroup
name={'applyIfTransactionType'}
label={'Apply to transactions are'}
style={{ maxWidth: 350 }}
>
<FSelect
name={'applyIfTransactionType'}
items={TransactionTypeOptions}
popoverProps={{ minimal: true, inline: false }}
/>
</FFormGroup>
<RuleApplyIfTransactionTypeField />
<FFormGroup
name={'conditionsType'}
label={'Categorize the transactions when'}
fastField
>
<FRadioGroup name={'conditionsType'}>
<Radio value={'and'} label={'All the following criteria matches'} />
@@ -139,34 +138,16 @@ function RuleFormContentFormRoot({
Then Assign
</h3>
<FFormGroup
name={'assignCategory'}
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>
<RuleAssignCategoryField />
<RuleAssignCategoryAccountField />
<FFormGroup
name={'assignRef'}
label={'Reference'}
style={{ maxWidth: 300 }}
fastField
>
<FInputGroup name={'assignRef'} />
<FInputGroup name={'assignRef'} fastField />
</FFormGroup>
<RuleFormActions />
@@ -203,11 +184,13 @@ function RuleFormConditions() {
name={`conditions[${index}].field`}
label={'Field'}
style={{ marginBottom: 0, flex: '1 0' }}
fastField
>
<FSelect
name={`conditions[${index}].field`}
items={Fields}
popoverProps={{ minimal: true, inline: false }}
fastField
/>
</FFormGroup>
@@ -215,11 +198,13 @@ function RuleFormConditions() {
name={`conditions[${index}].comparator`}
label={'Condition'}
style={{ marginBottom: 0, flex: '1 0' }}
fastField
>
<FSelect
name={`conditions[${index}].comparator`}
items={FieldCondition}
popoverProps={{ minimal: true, inline: false }}
fastField
/>
</FFormGroup>
@@ -227,8 +212,9 @@ function RuleFormConditions() {
name={`conditions[${index}].value`}
label={'Value'}
style={{ marginBottom: 0, flex: '1 0 ', width: '40%' }}
fastField
>
<FInputGroup name={`conditions[${index}].value`} />
<FInputGroup name={`conditions[${index}].value`} fastField />
</FFormGroup>
</Group>
))}
@@ -284,3 +270,104 @@ function 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 = {
name: '',
order: 0,
applyIfAccountId: '',
applyIfTransactionType: '',
applyIfTransactionType: 'deposit',
conditionsType: 'and',
conditions: [
{
@@ -47,3 +50,9 @@ export const FieldCondition = [
export const AssignTransactionTypeOptions = [
{ 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,
Position,
Tooltip,
MenuDivider,
} from '@blueprintjs/core';
import { Box, FormatDateCell, Icon, MaterialProgressBar } from '@/components';
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
@@ -213,9 +212,8 @@ export function useAccountUncategorizedTransactionsColumns() {
{
id: 'reference_number',
Header: 'Ref.#',
accessor: 'reference_number',
accessor: 'reference_no',
width: 50,
className: 'reference_number',
clickable: true,
textOverview: true,
},

View File

@@ -6,6 +6,7 @@ import {
FFormGroup,
FInputGroup,
FTextArea,
Icon,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOtherIncome() {
popoverProps={{ position: Position.BOTTOM, minimal: true }}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
inputProps={{ fill: true }}
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
/>
</FFormGroup>

View File

@@ -6,6 +6,7 @@ import {
FFormGroup,
FInputGroup,
FTextArea,
Icon,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOwnerContribution() {
popoverProps={{ position: Position.BOTTOM, minimal: true }}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
inputProps={{ fill: true }}
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
/>
</FFormGroup>

View File

@@ -6,6 +6,7 @@ import {
FFormGroup,
FInputGroup,
FTextArea,
Icon,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
@@ -21,7 +22,7 @@ export default function CategorizeTransactionTransferFrom() {
popoverProps={{ position: Position.BOTTOM, minimal: true }}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
inputProps={{ fill: true }}
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
/>
</FFormGroup>

View File

@@ -6,6 +6,7 @@ import {
FFormGroup,
FInputGroup,
FTextArea,
Icon,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOtherExpense() {
popoverProps={{ position: Position.BOTTOM, minimal: true }}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
inputProps={{ fill: true }}
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
/>
</FFormGroup>

View File

@@ -6,6 +6,7 @@ import {
FFormGroup,
FInputGroup,
FTextArea,
Icon,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
@@ -21,7 +22,7 @@ export default function CategorizeTransactionOwnerDrawings() {
popoverProps={{ position: Position.BOTTOM, minimal: true }}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
inputProps={{ fill: true }}
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
/>
</FFormGroup>

View File

@@ -6,6 +6,7 @@ import {
FFormGroup,
FInputGroup,
FTextArea,
Icon,
} from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
@@ -21,7 +22,7 @@ export default function CategorizeTransactionToAccount() {
popoverProps={{ position: Position.BOTTOM, minimal: true }}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
inputProps={{ fill: true }}
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
/>
</FFormGroup>

View File

@@ -4,7 +4,7 @@ export const MatchingReconcileFormSchema = Yup.object().shape({
type: Yup.string().required().label('Type'),
date: Yup.string().required().label('Date'),
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 #'),
category: Yup.string().required().label('Categogry'),
});

View File

@@ -1,14 +1,9 @@
// @ts-nocheck
import * as R from 'ramda';
import { Button, Intent, Position, Tag } from '@blueprintjs/core';
import {
Form,
Formik,
FormikHelpers,
FormikValues,
useFormikContext,
} from 'formik';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import moment from 'moment';
import { round } from 'lodash';
import {
AccountsSelect,
AppToaster,
@@ -19,6 +14,7 @@ import {
FInputGroup,
FMoneyInputGroup,
Group,
Icon,
} from '@/components';
import { Aside } from '@/components/Aside/Aside';
import { momentFormatter } from '@/utils';
@@ -100,7 +96,7 @@ function MatchingReconcileTransactionFormRoot({
const _initialValues = {
...initialValues,
amount: Math.abs(reconcileMatchingTransactionPendingAmount) || 0,
amount: round(Math.abs(reconcileMatchingTransactionPendingAmount), 2) || 0,
date: moment().format('YYYY-MM-DD'),
type:
reconcileMatchingTransactionPendingAmount > 0 ? 'deposit' : 'withdrawal',
@@ -179,7 +175,7 @@ function CreateReconcileTransactionContent() {
},
boundary: 'viewport',
}}
inputProps={{ fill: true }}
inputProps={{ fill: true, leftElement: <Icon icon={'date-range'} /> }}
fill
fastField
/>

View File

@@ -235,6 +235,12 @@ export function useExcludeUncategorizedTransaction(
queryClient.invalidateQueries(
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,
},
@@ -282,6 +288,12 @@ export function useUnexcludeUncategorizedTransaction(
queryClient.invalidateQueries(
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,
},
@@ -323,6 +335,13 @@ export function useMatchUncategorizedTransaction(
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,
});
@@ -362,6 +381,13 @@ export function useUnmatchMatchedUncategorizedTransaction(
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,
});

View File

@@ -253,6 +253,9 @@ export function useCategorizeTransaction(props) {
queryClient.invalidateQueries(
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
);
// Invalidate bank account summary.
queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META');
},
...props,
},
@@ -276,6 +279,9 @@ export function useUncategorizeTransaction(props) {
queryClient.invalidateQueries(
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
);
// Invalidate bank account summary.
queryClient.invalidateQueries('BANK_ACCOUNT_SUMMARY_META');
},
...props,
},