feat: prepard expenses of payment made transactions

This commit is contained in:
Ahmed Bouhuolia
2024-07-24 02:18:32 +02:00
parent 341d47cc7b
commit b68d180785
17 changed files with 162 additions and 46 deletions

View File

@@ -118,7 +118,7 @@ export default class BillsPayments extends BaseController {
check('reference').optional().trim().escape(), check('reference').optional().trim().escape(),
check('branch_id').optional({ nullable: true }).isNumeric().toInt(), check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
check('entries').exists().isArray({ min: 1 }), check('entries').exists().isArray(),
check('entries.*.index').optional().isNumeric().toInt(), check('entries.*.index').optional().isNumeric().toInt(),
check('entries.*.bill_id').exists().isNumeric().toInt(), check('entries.*.bill_id').exists().isNumeric().toInt(),
check('entries.*.payment_amount').exists().isNumeric().toFloat(), check('entries.*.payment_amount').exists().isNumeric().toFloat(),

View File

@@ -165,7 +165,7 @@ export default class PaymentReceivesController extends BaseController {
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(), check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
check('entries.*.index').optional().isNumeric().toInt(), check('entries.*.index').optional().isNumeric().toInt(),
check('entries.*.invoice_id').exists().isNumeric().toInt(), check('entries.*.invoice_id').exists().isNumeric().toInt(),
check('entries.*.amount_applied').exists().isNumeric().toFloat(), check('entries.*.payment_amount').exists().isNumeric().toFloat(),
check('attachments').isArray().optional(), check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(), check('attachments.*.key').exists().isString(),

View File

@@ -0,0 +1,17 @@
exports.up = function (knex) {
return knex.schema.table('bills_payments', (table) => {
table.decimal('applied_amount', 13, 3).defaultTo(0);
table
.integer('prepard_expenses_account_id')
.unsigned()
.references('id')
.inTable('accounts');
});
};
exports.down = function (knex) {
return knex.schema.table('bills_payments', (table) => {
table.dropColumn('applied_amount');
table.dropColumn('prepard_expenses_account_id');
});
};

View File

@@ -166,3 +166,10 @@ export interface IBillOpenedPayload {
oldBill: IBill; oldBill: IBill;
tenantId: number; tenantId: number;
} }
export interface IBillPrepardExpensesAppliedEventPayload {
tenantId: number;
billId: number;
trx?: Knex.Transaction;
}

View File

@@ -119,3 +119,11 @@ export enum IPaymentMadeAction {
Delete = 'Delete', Delete = 'Delete',
View = 'View', View = 'View',
} }
export interface IPaymentPrepardExpensesAppliedEventPayload {
tenantId: number;
billPaymentId: number;
billId: number;
appliedAmount: number;
trx?: Knex.Transaction;
}

View File

@@ -72,7 +72,7 @@ export interface IPaymentReceiveEntryDTO {
index: number; index: number;
paymentReceiveId: number; paymentReceiveId: number;
invoiceId: number; invoiceId: number;
amountApplied: number; paymentAmount: number;
} }
export interface IPaymentReceivesFilter extends IDynamicListFilterDTO { export interface IPaymentReceivesFilter extends IDynamicListFilterDTO {

View File

@@ -525,9 +525,9 @@ export default class Bill extends mixin(TenantModel, [
return notFoundBillsIds; return notFoundBillsIds;
} }
static changePaymentAmount(billId, amount) { static changePaymentAmount(billId, amount, trx) {
const changeMethod = amount > 0 ? 'increment' : 'decrement'; const changeMethod = amount > 0 ? 'increment' : 'decrement';
return this.query() return this.query(trx)
.where('id', billId) .where('id', billId)
[changeMethod]('payment_amount', Math.abs(amount)); [changeMethod]('payment_amount', Math.abs(amount));
} }

View File

@@ -38,4 +38,24 @@ export default class ContactTransfromer extends Transformer {
? this.formatDate(contact.openingBalanceAt) ? this.formatDate(contact.openingBalanceAt)
: ''; : '';
}; };
/**
* Retrieves the unused credit balance.
* @param {IContact} contact
* @returns {number}
*/
protected unusedCredit = (contact: IContact): number => {
return contact.balance > 0 ? 0 : Math.abs(contact.balance);
};
/**
* Retrieves the formatted unused credit balance.
* @param {IContact} contact
* @returns {string}
*/
protected formattedUnusedCredit = (contact: IContact): string => {
const unusedCredit = this.unusedCredit(contact);
return formatNumber(unusedCredit, { currencyCode: contact.currencyCode });
};
} }

View File

@@ -12,6 +12,8 @@ export default class CustomerTransfromer extends ContactTransfromer {
'formattedOpeningBalanceAt', 'formattedOpeningBalanceAt',
'customerType', 'customerType',
'formattedCustomerType', 'formattedCustomerType',
'unusedCredit',
'formattedUnusedCredit',
]; ];
}; };

View File

@@ -45,9 +45,9 @@ export class CustomersApplication {
/** /**
* Creates a new customer. * Creates a new customer.
* @param {number} tenantId * @param {number} tenantId
* @param {ICustomerNewDTO} customerDTO * @param {ICustomerNewDTO} customerDTO
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
* @returns {Promise<ICustomer>} * @returns {Promise<ICustomer>}
*/ */
public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => { public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => {
@@ -56,9 +56,9 @@ export class CustomersApplication {
/** /**
* Edits details of the given customer. * Edits details of the given customer.
* @param {number} tenantId * @param {number} tenantId
* @param {number} customerId * @param {number} customerId
* @param {ICustomerEditDTO} customerDTO * @param {ICustomerEditDTO} customerDTO
* @return {Promise<ICustomer>} * @return {Promise<ICustomer>}
*/ */
public editCustomer = ( public editCustomer = (
@@ -75,9 +75,9 @@ export class CustomersApplication {
/** /**
* Deletes the given customer and associated transactions. * Deletes the given customer and associated transactions.
* @param {number} tenantId * @param {number} tenantId
* @param {number} customerId * @param {number} customerId
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public deleteCustomer = ( public deleteCustomer = (
@@ -94,9 +94,9 @@ export class CustomersApplication {
/** /**
* Changes the opening balance of the given customer. * Changes the opening balance of the given customer.
* @param {number} tenantId * @param {number} tenantId
* @param {number} customerId * @param {number} customerId
* @param {Date|string} openingBalanceEditDTO * @param {Date|string} openingBalanceEditDTO
* @returns {Promise<ICustomer>} * @returns {Promise<ICustomer>}
*/ */
public editOpeningBalance = ( public editOpeningBalance = (

View File

@@ -1,4 +1,3 @@
import { Service } from 'typedi';
import ContactTransfromer from '../ContactTransformer'; import ContactTransfromer from '../ContactTransformer';
export default class VendorTransfromer extends ContactTransfromer { export default class VendorTransfromer extends ContactTransfromer {
@@ -10,7 +9,9 @@ export default class VendorTransfromer extends ContactTransfromer {
return [ return [
'formattedBalance', 'formattedBalance',
'formattedOpeningBalance', 'formattedOpeningBalance',
'formattedOpeningBalanceAt' 'formattedOpeningBalanceAt',
'unusedCredit',
'formattedUnusedCredit',
]; ];
}; };
} }

View File

@@ -21,7 +21,6 @@ export const DEFAULT_VIEWS = [
}, },
]; ];
export const ERRORS = { export const ERRORS = {
OPENING_BALANCE_DATE_REQUIRED: 'OPENING_BALANCE_DATE_REQUIRED', OPENING_BALANCE_DATE_REQUIRED: 'OPENING_BALANCE_DATE_REQUIRED',
CONTACT_ALREADY_INACTIVE: 'CONTACT_ALREADY_INACTIVE', CONTACT_ALREADY_INACTIVE: 'CONTACT_ALREADY_INACTIVE',

View File

@@ -1,13 +1,23 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import PromisePool from '@supercharge/promise-pool';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import PromisePool, { ProcessHandler } from '@supercharge/promise-pool';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import {
IBillPayment,
IBillPrepardExpensesAppliedEventPayload,
IPaymentPrepardExpensesAppliedEventPayload,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service() @Service()
export class AutoApplyPrepardExpenses { export class AutoApplyPrepardExpenses {
@Inject() @Inject()
private tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Auto apply prepard expenses to the given bill. * Auto apply prepard expenses to the given bill.
* @param {number} tenantId * @param {number} tenantId
@@ -19,31 +29,53 @@ export class AutoApplyPrepardExpenses {
billId: number, billId: number,
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<void> { ): Promise<void> {
const { PaymentMade, Bill } = this.tenancy.models(tenantId); const { BillPayment, Bill } = this.tenancy.models(tenantId);
const unappliedPayments = await PaymentMade.query(trx).where( const bill = await Bill.query(trx).findById(billId).throwIfNotFound();
'unappliedAmount',
'>',
0
);
const bill = Bill.query(trx).findById(billId).throwIfNotFound();
const unappliedPayments = await BillPayment.query(trx)
.where('vendorId', bill.vendorId)
.whereRaw('amount - applied_amount > 0')
.whereNotNull('prepardExpensesAccountId');
let unappliedAmount = bill.total;
let appliedTotalAmount = 0; // Total applied amount after applying.
const precessHandler: ProcessHandler<IBillPayment, void> = async (
unappliedPayment: IBillPayment,
index: number,
pool
) => {
const appliedAmount = Math.min(unappliedAmount, unappliedPayment.amount);
unappliedAmount = unappliedAmount - appliedAmount;
appliedTotalAmount += appliedAmount;
// Stop applying once the unapplied amount reach zero or less.
if (appliedAmount <= 0) {
pool.stop();
return;
}
await this.applyBillToPaymentMade(
tenantId,
unappliedPayment.id,
bill.id,
appliedAmount,
trx
);
};
await PromisePool.withConcurrency(1) await PromisePool.withConcurrency(1)
.for(unappliedPayments) .for(unappliedPayments)
.process(async (unappliedPayment: any) => { .process(precessHandler);
const appliedAmount = 1;
await this.applyBillToPaymentMade(
tenantId,
unappliedPayment.id,
bill.id,
appliedAmount,
trx
);
});
// Increase the paid amount of the purchase invoice. // Increase the paid amount of the purchase invoice.
await Bill.changePaymentAmount(billId, 0, trx); await Bill.changePaymentAmount(billId, appliedTotalAmount, trx);
// Triggers `onBillPrepardExpensesApplied` event.
await this.eventPublisher.emitAsync(events.bill.onPrepardExpensesApplied, {
tenantId,
billId,
trx,
} as IBillPrepardExpensesAppliedEventPayload);
} }
/** /**
@@ -68,6 +100,18 @@ export class AutoApplyPrepardExpenses {
billId, billId,
paymentAmount: appliedAmount, paymentAmount: appliedAmount,
}); });
await BillPayment.query().increment('usedAmount', appliedAmount); await BillPayment.query(trx).increment('appliedAmount', appliedAmount);
// Triggers `onBillPaymentPrepardExpensesApplied` event.
await this.eventPublisher.emitAsync(
events.billPayment.onPrepardExpensesApplied,
{
tenantId,
billPaymentId,
billId,
appliedAmount,
trx,
} as IPaymentPrepardExpensesAppliedEventPayload
);
}; };
} }

View File

@@ -78,10 +78,10 @@ export class CreatePaymentReceive {
paymentReceiveDTO.entries paymentReceiveDTO.entries
); );
// Validate invoice payment amount. // Validate invoice payment amount.
// await this.validators.validateInvoicesPaymentsAmount( await this.validators.validateInvoicesPaymentsAmount(
// tenantId, tenantId,
// paymentReceiveDTO.entries paymentReceiveDTO.entries
// ); );
// Validates the payment account currency code. // Validates the payment account currency code.
this.validators.validatePaymentAccountCurrency( this.validators.validatePaymentAccountCurrency(
depositAccount.currencyCode, depositAccount.currencyCode,

View File

@@ -254,6 +254,8 @@ export default {
onOpening: 'onBillOpening', onOpening: 'onBillOpening',
onOpened: 'onBillOpened', onOpened: 'onBillOpened',
onPrepardExpensesApplied: 'onBillPrepardExpensesApplied'
}, },
/** /**
@@ -271,6 +273,8 @@ export default {
onPublishing: 'onBillPaymentPublishing', onPublishing: 'onBillPaymentPublishing',
onPublished: 'onBillPaymentPublished', onPublished: 'onBillPaymentPublished',
onPrepardExpensesApplied: 'onBillPaymentPrepardExpensesApplied'
}, },
/** /**

View File

@@ -160,6 +160,13 @@ export function useCustomersTableColumns() {
width: 85, width: 85,
clickable: true, clickable: true,
}, },
{
id: 'credit_balance',
Header: 'Credit Balance',
accessor: 'formatted_unused_credit',
width: 100,
align: 'right',
},
{ {
id: 'balance', id: 'balance',
Header: intl.get('receivable_balance'), Header: intl.get('receivable_balance'),

View File

@@ -183,6 +183,13 @@ export function useVendorsTableColumns() {
width: 85, width: 85,
clickable: true, clickable: true,
}, },
{
id: 'credit_balance',
Header: 'Credit Balance',
accessor: 'formatted_unused_credit',
width: 100,
align: 'right',
},
{ {
id: 'balance', id: 'balance',
Header: intl.get('receivable_balance'), Header: intl.get('receivable_balance'),