Compare commits

...

9 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
f67c63a4fa Merge pull request #569 from bigcapitalhq/fix-edit-bank-rule-recognized
fix: Recognize transactions on editing bank rule
2024-08-08 00:26:18 +02:00
Ahmed Bouhuolia
0025dcf8d4 fix: add recognize jobs 2024-08-08 00:23:47 +02:00
Ahmed Bouhuolia
81995dc94f fix: recognize transactions on editing bank rule 2024-08-08 00:20:17 +02:00
Ahmed Bouhuolia
3fcc70c1d8 Merge pull request #568 from bigcapitalhq/fix-banking-api-query
fix: Banking API account and page query.
2024-08-07 20:17:49 +02:00
Ahmed Bouhuolia
a986c7a250 fix: Banking api account and page query. 2024-08-07 20:08:59 +02:00
allcontributors[bot]
37e25a8061 docs: add mittalsam98 as a contributor for bug (#567)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-08-07 18:58:37 +02:00
Ahmed Bouhuolia
cfba628465 Merge pull request #562 from mittalsam98/fix/wrong-due-amount
fix: the wrong invoice due amouynt.
2024-08-07 18:56:28 +02:00
Ahmed Bouhuolia
3d200f4d7d fix: wrong invoice due amount 2024-08-07 18:52:36 +02:00
Sachin
8cab012324 fix: due Amount on edit page is calculated wrong with "Exclusive of Tax" Invoice mode 2024-08-03 23:56:02 +05:30
24 changed files with 350 additions and 172 deletions

View File

@@ -141,6 +141,15 @@
"contributions": [ "contributions": [
"bug" "bug"
] ]
},
{
"login": "mittalsam98",
"name": "Sachin Mittal",
"avatar_url": "https://avatars.githubusercontent.com/u/42431274?v=4",
"profile": "https://myself.vercel.app/",
"contributions": [
"bug"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@@ -128,6 +128,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
</tr> </tr>
<tr> <tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oleynikd"><img src="https://avatars.githubusercontent.com/u/3976868?v=4?s=100" width="100px;" alt="Denis"/><br /><sub><b>Denis</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aoleynikd" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/oleynikd"><img src="https://avatars.githubusercontent.com/u/3976868?v=4?s=100" width="100px;" alt="Denis"/><br /><sub><b>Denis</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aoleynikd" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://myself.vercel.app/"><img src="https://avatars.githubusercontent.com/u/42431274?v=4?s=100" width="100px;" alt="Sachin Mittal"/><br /><sub><b>Sachin Mittal</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Amittalsam98" title="Bug reports">🐛</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -43,8 +43,6 @@ export class BankingRulesController extends BaseController {
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 }),
body('recognition').isBoolean().toBoolean().optional({ nullable: true }),
]; ];
} }

View File

@@ -42,7 +42,11 @@ export class ExcludeBankTransactionsController extends BaseController {
); );
router.get( router.get(
'/excluded', '/excluded',
[], [
query('account_id').optional().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
],
this.validationResult, this.validationResult,
this.getExcludedBankTransactions.bind(this) this.getExcludedBankTransactions.bind(this)
); );
@@ -177,7 +181,7 @@ export class ExcludeBankTransactionsController extends BaseController {
next: NextFunction next: NextFunction
): Promise<Response | void> { ): Promise<Response | void> {
const { tenantId } = req; const { tenantId } = req;
const filter = this.matchedBodyData(req); const filter = this.matchedQueryData(req);
try { try {
const data = const data =

View File

@@ -1,5 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { NextFunction, Request, Response, Router } from 'express'; import { NextFunction, Request, Response, Router } from 'express';
import { query } from 'express-validator';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication'; import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@@ -14,7 +15,16 @@ export class RecognizedTransactionsController extends BaseController {
router() { router() {
const router = Router(); const router = Router();
router.get('/', this.getRecognizedTransactions.bind(this)); router.get(
'/',
[
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('account_id').optional().isNumeric().toInt(),
],
this.validationResult,
this.getRecognizedTransactions.bind(this)
);
router.get( router.get(
'/transactions/:uncategorizedTransactionId', '/transactions/:uncategorizedTransactionId',
this.getRecognizedTransaction.bind(this) this.getRecognizedTransaction.bind(this)

View File

@@ -13,7 +13,9 @@ import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentRecei
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob'; import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob'; import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob'; import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob';
import { RegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/RecognizeTransactionsJob'; import { ReregonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RerecognizeTransactionsJob';
import { RegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RecognizeTransactionsJob';
import { RevertRegonizeTransactionsJob } from '@/services/Banking/RegonizeTranasctions/jobs/RevertRecognizedTransactionsJob';
export default ({ agenda }: { agenda: Agenda }) => { export default ({ agenda }: { agenda: Agenda }) => {
new ResetPasswordMailJob(agenda); new ResetPasswordMailJob(agenda);
@@ -31,6 +33,8 @@ export default ({ agenda }: { agenda: Agenda }) => {
new ImportDeleteExpiredFilesJobs(agenda); new ImportDeleteExpiredFilesJobs(agenda);
new SendVerifyMailJob(agenda); new SendVerifyMailJob(agenda);
new RegonizeTransactionsJob(agenda); new RegonizeTransactionsJob(agenda);
new ReregonizeTransactionsJob(agenda);
new RevertRegonizeTransactionsJob(agenda);
agenda.start().then(() => { agenda.start().then(() => {
agenda.every('1 hours', 'delete-expired-imported-files', {}); agenda.every('1 hours', 'delete-expired-imported-files', {});

View File

@@ -35,7 +35,8 @@ export class RecognizeSyncedBankTranasctions extends EventSubscriber {
runAfterTransaction(trx, async () => { runAfterTransaction(trx, async () => {
await this.recognizeTranasctionsService.recognizeTransactions( await this.recognizeTranasctionsService.recognizeTransactions(
tenantId, tenantId,
batch null,
{ batch }
); );
}); });
}; };

View File

@@ -1,11 +1,13 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { castArray, isEmpty } from 'lodash';
import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction'; import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { transformToMapBy } from '@/utils'; import { transformToMapBy } from '@/utils';
import { PromisePool } from '@supercharge/promise-pool'; import { PromisePool } from '@supercharge/promise-pool';
import { BankRule } from '@/models/BankRule'; import { BankRule } from '@/models/BankRule';
import { bankRulesMatchTransaction } from './_utils'; import { bankRulesMatchTransaction } from './_utils';
import { RecognizeTransactionsCriteria } from './_types';
@Service() @Service()
export class RecognizeTranasctionsService { export class RecognizeTranasctionsService {
@@ -48,24 +50,42 @@ export class RecognizeTranasctionsService {
/** /**
* Regonized the uncategorized transactions. * Regonized the uncategorized transactions.
* @param {number} tenantId - * @param {number} tenantId -
* @param {number|Array<number>} ruleId - The target rule id/ids.
* @param {RecognizeTransactionsCriteria}
* @param {Knex.Transaction} trx - * @param {Knex.Transaction} trx -
*/ */
public async recognizeTransactions( public async recognizeTransactions(
tenantId: number, tenantId: number,
batch: string = '', ruleId?: number | Array<number>,
transactionsCriteria?: RecognizeTransactionsCriteria,
trx?: Knex.Transaction trx?: Knex.Transaction
) { ) {
const { UncategorizedCashflowTransaction, BankRule } = const { UncategorizedCashflowTransaction, BankRule } =
this.tenancy.models(tenantId); this.tenancy.models(tenantId);
const uncategorizedTranasctions = const uncategorizedTranasctions =
await UncategorizedCashflowTransaction.query().onBuild((query) => { await UncategorizedCashflowTransaction.query(trx).onBuild((query) => {
query.where('recognized_transaction_id', null); query.modify('notRecognized');
query.where('categorized', false); query.modify('notCategorized');
if (batch) query.where('batch', batch); // Filter the transactions based on the given criteria.
if (transactionsCriteria?.batch) {
query.where('batch', transactionsCriteria.batch);
}
if (transactionsCriteria?.accountId) {
query.where('accountId', transactionsCriteria.accountId);
}
}); });
const bankRules = await BankRule.query().withGraphFetched('conditions');
const bankRules = await BankRule.query(trx).onBuild((q) => {
const rulesIds = castArray(ruleId);
if (!isEmpty(rulesIds)) {
q.whereIn('id', rulesIds);
}
q.withGraphFetched('conditions');
});
const bankRulesByAccountId = transformToMapBy( const bankRulesByAccountId = transformToMapBy(
bankRules, bankRules,
'applyIfAccountId' 'applyIfAccountId'

View File

@@ -0,0 +1,72 @@
import { Inject, Service } from 'typedi';
import { castArray } from 'lodash';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { RevertRecognizedTransactionsCriteria } from './_types';
@Service()
export class RevertRecognizedTransactions {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/**
* Revert and unlinks the recognized transactions based on the given bank rule
* and transactions criteria..
* @param {number} tenantId - Tenant id.
* @param {number|Array<number>} bankRuleId - Bank rule id.
* @param {RevertRecognizedTransactionsCriteria} transactionsCriteria -
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public async revertRecognizedTransactions(
tenantId: number,
ruleId?: number | Array<number>,
transactionsCriteria?: RevertRecognizedTransactionsCriteria,
trx?: Knex.Transaction
): Promise<void> {
const { UncategorizedCashflowTransaction, RecognizedBankTransaction } =
this.tenancy.models(tenantId);
const rulesIds = castArray(ruleId);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Retrieves all the recognized transactions of the banbk rule.
const uncategorizedTransactions =
await UncategorizedCashflowTransaction.query(trx).onBuild((q) => {
q.withGraphJoined('recognizedTransaction');
q.whereNotNull('recognizedTransaction.id');
if (rulesIds.length > 0) {
q.whereIn('recognizedTransaction.bankRuleId', rulesIds);
}
if (transactionsCriteria?.accountId) {
q.where('accountId', transactionsCriteria.accountId);
}
if (transactionsCriteria?.batch) {
q.where('batch', transactionsCriteria.batch);
}
});
const uncategorizedTransactionIds = uncategorizedTransactions.map(
(r) => r.id
);
// Unlink the recongized transactions out of uncategorized transactions.
await UncategorizedCashflowTransaction.query(trx)
.whereIn('id', uncategorizedTransactionIds)
.patch({
recognizedTransactionId: null,
});
// Delete the recognized bank transactions that assocaited to bank rule.
await RecognizedBankTransaction.query(trx)
.whereIn('uncategorizedTransactionId', uncategorizedTransactionIds)
.delete();
},
trx
);
}
}

View File

@@ -0,0 +1,11 @@
export interface RevertRecognizedTransactionsCriteria {
batch?: string;
accountId?: number;
}
export interface RecognizeTransactionsCriteria {
batch?: string;
accountId?: number;
}

View File

@@ -41,14 +41,10 @@ export class TriggerRecognizedTransactions {
*/ */
private async recognizedTransactionsOnRuleCreated({ private async recognizedTransactionsOnRuleCreated({
tenantId, tenantId,
createRuleDTO, bankRule,
}: IBankRuleEventCreatedPayload) { }: IBankRuleEventCreatedPayload) {
const payload = { tenantId }; const payload = { tenantId, ruleId: bankRule.id };
// Cannot run recognition if the option is not enabled.
if (createRuleDTO.recognition) {
return;
}
await this.agenda.now('recognize-uncategorized-transactions-job', payload); await this.agenda.now('recognize-uncategorized-transactions-job', payload);
} }
@@ -59,14 +55,14 @@ export class TriggerRecognizedTransactions {
private async recognizedTransactionsOnRuleEdited({ private async recognizedTransactionsOnRuleEdited({
tenantId, tenantId,
editRuleDTO, editRuleDTO,
ruleId,
}: IBankRuleEventEditedPayload) { }: IBankRuleEventEditedPayload) {
const payload = { tenantId }; const payload = { tenantId, ruleId };
// Cannot run recognition if the option is not enabled. await this.agenda.now(
if (!editRuleDTO.recognition) { 'rerecognize-uncategorized-transactions-job',
return; payload
} );
await this.agenda.now('recognize-uncategorized-transactions-job', payload);
} }
/** /**
@@ -75,9 +71,13 @@ export class TriggerRecognizedTransactions {
*/ */
private async recognizedTransactionsOnRuleDeleted({ private async recognizedTransactionsOnRuleDeleted({
tenantId, tenantId,
ruleId,
}: IBankRuleEventDeletedPayload) { }: IBankRuleEventDeletedPayload) {
const payload = { tenantId }; const payload = { tenantId, ruleId };
await this.agenda.now('recognize-uncategorized-transactions-job', payload); await this.agenda.now(
'revert-recognized-uncategorized-transactions-job',
payload
);
} }
/** /**
@@ -91,7 +91,7 @@ export class TriggerRecognizedTransactions {
}: IImportFileCommitedEventPayload) { }: IImportFileCommitedEventPayload) {
const importFile = await Import.query().findOne({ importId }); const importFile = await Import.query().findOne({ importId });
const batch = importFile.paramsParsed.batch; const batch = importFile.paramsParsed.batch;
const payload = { tenantId, batch }; const payload = { tenantId, transactionsCriteria: { batch } };
await this.agenda.now('recognize-uncategorized-transactions-job', payload); await this.agenda.now('recognize-uncategorized-transactions-job', payload);
} }

View File

@@ -1,5 +1,5 @@
import Container, { Service } from 'typedi'; import Container, { Service } from 'typedi';
import { RecognizeTranasctionsService } from './RecognizeTranasctionsService'; import { RecognizeTranasctionsService } from '../RecognizeTranasctionsService';
@Service() @Service()
export class RegonizeTransactionsJob { export class RegonizeTransactionsJob {
@@ -18,11 +18,15 @@ 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, batch } = job.attrs.data; const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
const regonizeTransactions = Container.get(RecognizeTranasctionsService); const regonizeTransactions = Container.get(RecognizeTranasctionsService);
try { try {
await regonizeTransactions.recognizeTransactions(tenantId, batch); await regonizeTransactions.recognizeTransactions(
tenantId,
ruleId,
transactionsCriteria
);
done(); done();
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -0,0 +1,45 @@
import Container, { Service } from 'typedi';
import { RecognizeTranasctionsService } from '../RecognizeTranasctionsService';
import { RevertRecognizedTransactions } from '../RevertRecognizedTransactions';
@Service()
export class ReregonizeTransactionsJob {
/**
* Constructor method.
*/
constructor(agenda) {
agenda.define(
'rerecognize-uncategorized-transactions-job',
{ priority: 'high', concurrency: 2 },
this.handler
);
}
/**
* Triggers sending invoice mail.
*/
private handler = async (job, done: Function) => {
const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
const regonizeTransactions = Container.get(RecognizeTranasctionsService);
const revertRegonizedTransactions = Container.get(
RevertRecognizedTransactions
);
try {
await revertRegonizedTransactions.revertRecognizedTransactions(
tenantId,
ruleId,
transactionsCriteria
);
await regonizeTransactions.recognizeTransactions(
tenantId,
ruleId,
transactionsCriteria
);
done();
} catch (error) {
console.log(error);
done(error);
}
};
}

View File

@@ -0,0 +1,38 @@
import Container, { Service } from 'typedi';
import { RevertRecognizedTransactions } from '../RevertRecognizedTransactions';
@Service()
export class RevertRegonizeTransactionsJob {
/**
* Constructor method.
*/
constructor(agenda) {
agenda.define(
'revert-recognized-uncategorized-transactions-job',
{ priority: 'high', concurrency: 2 },
this.handler
);
}
/**
* Triggers sending invoice mail.
*/
private handler = async (job, done: Function) => {
const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
const revertRegonizedTransactions = Container.get(
RevertRecognizedTransactions
);
try {
await revertRegonizedTransactions.revertRecognizedTransactions(
tenantId,
ruleId,
transactionsCriteria
);
done();
} catch (error) {
console.log(error);
done(error);
}
};
}

View File

@@ -62,6 +62,7 @@ export class CreateBankRuleService {
await this.eventPublisher.emitAsync(events.bankRules.onCreated, { await this.eventPublisher.emitAsync(events.bankRules.onCreated, {
tenantId, tenantId,
createRuleDTO, createRuleDTO,
bankRule,
trx, trx,
} as IBankRuleEventCreatedPayload); } as IBankRuleEventCreatedPayload);

View File

@@ -1,54 +0,0 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
@Service()
export class UnlinkBankRuleRecognizedTransactions {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
/**
* Unlinks the given bank rule out of recognized transactions.
* @param {number} tenantId - Tenant id.
* @param {number} bankRuleId - Bank rule id.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public async unlinkBankRuleOutRecognizedTransactions(
tenantId: number,
bankRuleId: number,
trx?: Knex.Transaction
): Promise<void> {
const { UncategorizedCashflowTransaction, RecognizedBankTransaction } =
this.tenancy.models(tenantId);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Retrieves all the recognized transactions of the banbk rule.
const recognizedTransactions = await RecognizedBankTransaction.query(
trx
).where('bankRuleId', bankRuleId);
const uncategorizedTransactionIds = recognizedTransactions.map(
(r) => r.uncategorizedTransactionId
);
// Unlink the recongized transactions out of uncategorized transactions.
await UncategorizedCashflowTransaction.query(trx)
.whereIn('id', uncategorizedTransactionIds)
.patch({
recognizedTransactionId: null,
});
// Delete the recognized bank transactions that assocaited to bank rule.
await RecognizedBankTransaction.query(trx)
.where({ bankRuleId })
.delete();
},
trx
);
}
}

View File

@@ -1,12 +1,12 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { UnlinkBankRuleRecognizedTransactions } from '../UnlinkBankRuleRecognizedTransactions';
import { IBankRuleEventDeletingPayload } from '../types'; import { IBankRuleEventDeletingPayload } from '../types';
import { RevertRecognizedTransactions } from '../../RegonizeTranasctions/RevertRecognizedTransactions';
@Service() @Service()
export class UnlinkBankRuleOnDeleteBankRule { export class UnlinkBankRuleOnDeleteBankRule {
@Inject() @Inject()
private unlinkBankRule: UnlinkBankRuleRecognizedTransactions; private revertRecognizedTransactionsService: RevertRecognizedTransactions;
/** /**
* Constructor method. * Constructor method.
@@ -26,7 +26,7 @@ export class UnlinkBankRuleOnDeleteBankRule {
tenantId, tenantId,
ruleId, ruleId,
}: IBankRuleEventDeletingPayload) { }: IBankRuleEventDeletingPayload) {
await this.unlinkBankRule.unlinkBankRuleOutRecognizedTransactions( await this.revertRecognizedTransactionsService.revertRecognizedTransactions(
tenantId, tenantId,
ruleId ruleId
); );

View File

@@ -30,6 +30,7 @@ export enum BankRuleApplyIfTransactionType {
} }
export interface IBankRule { export interface IBankRule {
id?: number;
name: string; name: string;
order?: number; order?: number;
applyIfAccountId: number; applyIfAccountId: number;
@@ -71,8 +72,6 @@ export interface IBankRuleCommonDTO {
assignAccountId: number; assignAccountId: number;
assignPayee?: string; assignPayee?: string;
assignMemo?: string; assignMemo?: string;
recognition?: boolean;
} }
export interface ICreateBankRuleDTO extends IBankRuleCommonDTO {} export interface ICreateBankRuleDTO extends IBankRuleCommonDTO {}
@@ -86,6 +85,7 @@ export interface IBankRuleEventCreatingPayload {
export interface IBankRuleEventCreatedPayload { export interface IBankRuleEventCreatedPayload {
tenantId: number; tenantId: number;
createRuleDTO: ICreateBankRuleDTO; createRuleDTO: ICreateBankRuleDTO;
bankRule: IBankRule;
trx?: Knex.Transaction; trx?: Knex.Transaction;
} }

View File

@@ -43,6 +43,7 @@ function RecognizedTransactionsTableBoot({
hasNextPage: hasUncategorizedTransactionsNextPage, hasNextPage: hasUncategorizedTransactionsNextPage,
} = useRecognizedBankTransactionsInfinity({ } = useRecognizedBankTransactionsInfinity({
page_size: 50, page_size: 50,
account_id: accountId,
}); });
// Memorized the cashflow account transactions. // Memorized the cashflow account transactions.
const recognizedTransactions = React.useMemo( const recognizedTransactions = React.useMemo(

View File

@@ -10,18 +10,16 @@ import {
TotalLineBorderStyle, TotalLineBorderStyle,
TotalLineTextStyle, TotalLineTextStyle,
} from '@/components'; } from '@/components';
import { useInvoiceAggregatedTaxRates, useInvoiceTotals } from './utils'; import { useInvoiceAggregatedTaxRates } from './utils';
import { TaxType } from '@/interfaces/TaxRates'; import { TaxType } from '@/interfaces/TaxRates';
import {
InvoiceDueAmountFormatted,
InvoicePaidAmountFormatted,
InvoiceSubTotalFormatted,
InvoiceTotalFormatted,
} from './components';
export function InvoiceFormFooterRight() { export function InvoiceFormFooterRight() {
// Calculate the total due amount of invoice entries.
const {
formattedSubtotal,
formattedTotal,
formattedDueTotal,
formattedPaymentTotal,
} = useInvoiceTotals();
const { const {
values: { inclusive_exclusive_tax, currency_code }, values: { inclusive_exclusive_tax, currency_code },
} = useFormikContext(); } = useFormikContext();
@@ -38,7 +36,7 @@ export function InvoiceFormFooterRight() {
: 'Subtotal'} : 'Subtotal'}
</> </>
} }
value={formattedSubtotal} value={<InvoiceSubTotalFormatted />}
/> />
{taxEntries.map((tax, index) => ( {taxEntries.map((tax, index) => (
<TotalLine <TotalLine
@@ -50,18 +48,18 @@ export function InvoiceFormFooterRight() {
))} ))}
<TotalLine <TotalLine
title={`Total (${currency_code})`} title={`Total (${currency_code})`}
value={formattedTotal} value={<InvoiceTotalFormatted />}
borderStyle={TotalLineBorderStyle.SingleDark} borderStyle={TotalLineBorderStyle.SingleDark}
textStyle={TotalLineTextStyle.Bold} textStyle={TotalLineTextStyle.Bold}
/> />
<TotalLine <TotalLine
title={<T id={'invoice_form.label.payment_amount'} />} title={<T id={'invoice_form.label.payment_amount'} />}
value={formattedPaymentTotal} value={<InvoicePaidAmountFormatted />}
borderStyle={TotalLineBorderStyle.None} borderStyle={TotalLineBorderStyle.None}
/> />
<TotalLine <TotalLine
title={<T id={'invoice_form.label.due_amount'} />} title={<T id={'invoice_form.label.due_amount'} />}
value={formattedDueTotal} value={<InvoiceDueAmountFormatted />}
textStyle={TotalLineTextStyle.Bold} textStyle={TotalLineTextStyle.Bold}
/> />
</InvoiceTotalLines> </InvoiceTotalLines>

View File

@@ -8,7 +8,7 @@ import InvoiceFormHeaderFields from './InvoiceFormHeaderFields';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { PageFormBigNumber } from '@/components'; import { PageFormBigNumber } from '@/components';
import { useInvoiceSubtotal } from './utils'; import { useInvoiceDueAmount } from './utils';
/** /**
* Invoice form header section. * Invoice form header section.
@@ -32,7 +32,7 @@ function InvoiceFormBigTotal() {
} = useFormikContext(); } = useFormikContext();
// Calculate the total due amount of invoice entries. // Calculate the total due amount of invoice entries.
const totalDueAmount = useInvoiceSubtotal(); const totalDueAmount = useInvoiceDueAmount();
return ( return (
<PageFormBigNumber <PageFormBigNumber

View File

@@ -4,14 +4,21 @@ import intl from 'react-intl-universal';
import * as R from 'ramda'; import * as R from 'ramda';
import { Button } from '@blueprintjs/core'; import { Button } from '@blueprintjs/core';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { ExchangeRateInputGroup } from '@/components'; import { ExchangeRateInputGroup, FormatNumber } from '@/components';
import { useCurrentOrganization } from '@/hooks/state'; import { useCurrentOrganization } from '@/hooks/state';
import { useInvoiceIsForeignCustomer, useInvoiceTotal } from './utils'; import {
import withSettings from '@/containers/Settings/withSettings'; useInvoiceCurrencyCode,
useInvoiceDueAmount,
useInvoiceIsForeignCustomer,
useInvoicePaidAmount,
useInvoiceSubtotal,
useInvoiceTotal,
} from './utils';
import { useUpdateEffect } from '@/hooks'; import { useUpdateEffect } from '@/hooks';
import { transactionNumber } from '@/utils'; import { transactionNumber } from '@/utils';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { import {
useSyncExRateToForm, useSyncExRateToForm,
withExchangeRateFetchingLoading, withExchangeRateFetchingLoading,
@@ -109,3 +116,47 @@ export const InvoiceExchangeRateSync = R.compose(withDialogActions)(
return null; return null;
}, },
); );
/**
*Renders the invoice formatted total.
* @returns {JSX.Element}
*/
export const InvoiceTotalFormatted = () => {
const currencyCode = useInvoiceCurrencyCode();
const total = useInvoiceTotal();
return <FormatNumber value={total} currency={currencyCode} />;
};
/**
* Renders the invoice formatted subtotal.
* @returns {JSX.Element}
*/
export const InvoiceSubTotalFormatted = () => {
const currencyCode = useInvoiceCurrencyCode();
const subTotal = useInvoiceSubtotal();
return <FormatNumber value={subTotal} currency={currencyCode} />;
};
/**
* Renders the invoice formatted due amount.
* @returns {JSX.Element}
*/
export const InvoiceDueAmountFormatted = () => {
const currencyCode = useInvoiceCurrencyCode();
const dueAmount = useInvoiceDueAmount();
return <FormatNumber value={dueAmount} currency={currencyCode} />;
};
/**
* Renders the invoice formatted paid amount.
* @returns {JSX.Element}
*/
export const InvoicePaidAmountFormatted = () => {
const currencyCode = useInvoiceCurrencyCode();
const paidAmount = useInvoicePaidAmount();
return <FormatNumber value={paidAmount} currency={currencyCode} />;
};

View File

@@ -269,59 +269,6 @@ export const useInvoiceSubtotal = () => {
return React.useMemo(() => getEntriesTotal(entries), [entries]); return React.useMemo(() => getEntriesTotal(entries), [entries]);
}; };
/**
* Retreives the invoice totals.
*/
export const useInvoiceTotals = () => {
const {
values: { entries, currency_code: currencyCode },
} = useFormikContext();
// Retrieves the invoice entries total.
const total = React.useMemo(() => getEntriesTotal(entries), [entries]);
const total_ = useInvoiceTotal();
// Retrieves the formatted total money.
const formattedTotal = React.useMemo(
() => formattedAmount(total_, currencyCode),
[total_, currencyCode],
);
// Retrieves the formatted subtotal.
const formattedSubtotal = React.useMemo(
() => formattedAmount(total, currencyCode, { money: false }),
[total, currencyCode],
);
// Retrieves the payment total.
const paymentTotal = React.useMemo(() => 0, []);
// Retireves the formatted payment total.
const formattedPaymentTotal = React.useMemo(
() => formattedAmount(paymentTotal, currencyCode),
[paymentTotal, currencyCode],
);
// Retrieves the formatted due total.
const dueTotal = React.useMemo(
() => total - paymentTotal,
[total, paymentTotal],
);
// Retrieves the formatted due total.
const formattedDueTotal = React.useMemo(
() => formattedAmount(dueTotal, currencyCode),
[dueTotal, currencyCode],
);
return {
total,
paymentTotal,
dueTotal,
formattedTotal,
formattedSubtotal,
formattedPaymentTotal,
formattedDueTotal,
};
};
/** /**
* Detarmines whether the invoice has foreign customer. * Detarmines whether the invoice has foreign customer.
* @returns {boolean} * @returns {boolean}
@@ -409,14 +356,25 @@ export const useInvoiceTotal = () => {
); );
}; };
/**
* Retrieves the paid amount of the invoice.
* @returns {number}
*/
export const useInvoicePaidAmount = () => {
const { invoice } = useInvoiceFormContext();
return invoice?.payment_amount || 0;
};
/** /**
* Retreives the invoice due amount. * Retreives the invoice due amount.
* @returns {number} * @returns {number}
*/ */
export const useInvoiceDueAmount = () => { export const useInvoiceDueAmount = () => {
const total = useInvoiceTotal(); const total = useInvoiceTotal();
const paidAmount = useInvoicePaidAmount();
return total; return Math.max(total - paidAmount, 0);
}; };
/** /**
@@ -438,3 +396,13 @@ export const useIsInvoiceTaxExclusive = () => {
return values.inclusive_exclusive_tax === TaxType.Exclusive; return values.inclusive_exclusive_tax === TaxType.Exclusive;
}; };
/**
* Retrieves the invoice currency code.
* @returns {string}
*/
export const useInvoiceCurrencyCode = () => {
const { values } = useFormikContext();
return values.currency_code;
};

View File

@@ -276,6 +276,11 @@ const onValidateExcludeUncategorizedTransaction = (queryClient) => {
// invalidate bank account summary. // invalidate bank account summary.
queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META); queryClient.invalidateQueries(BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META);
// Invalidate the recognized transactions.
queryClient.invalidateQueries([
BANK_QUERY_KEY.RECOGNIZED_BANK_TRANSACTIONS_INFINITY,
]);
}; };
type ExcludeUncategorizedTransactionValue = number; type ExcludeUncategorizedTransactionValue = number;
@@ -312,10 +317,6 @@ export function useExcludeUncategorizedTransaction(
{ {
onSuccess: (res, id) => { onSuccess: (res, id) => {
onValidateExcludeUncategorizedTransaction(queryClient); onValidateExcludeUncategorizedTransaction(queryClient);
queryClient.invalidateQueries([
BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META,
id,
]);
}, },
...options, ...options,
}, },
@@ -357,10 +358,6 @@ export function useUnexcludeUncategorizedTransaction(
{ {
onSuccess: (res, id) => { onSuccess: (res, id) => {
onValidateExcludeUncategorizedTransaction(queryClient); onValidateExcludeUncategorizedTransaction(queryClient);
queryClient.invalidateQueries([
BANK_QUERY_KEY.BANK_ACCOUNT_SUMMARY_META,
id,
]);
}, },
...options, ...options,
}, },
@@ -649,7 +646,6 @@ export function useRecognizedBankTransactionsInfinity(
getPreviousPageParam: (firstPage) => firstPage.pagination.page - 1, getPreviousPageParam: (firstPage) => firstPage.pagination.page - 1,
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {
const { pagination } = lastPage; const { pagination } = lastPage;
return pagination.total > pagination.page_size * pagination.page return pagination.total > pagination.page_size * pagination.page
? lastPage.pagination.page + 1 ? lastPage.pagination.page + 1
: undefined; : undefined;