mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 07:40:32 +00:00
feat: prepard expenses of payment made transactions
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export default class CustomerTransfromer extends ContactTransfromer {
|
|||||||
'formattedOpeningBalanceAt',
|
'formattedOpeningBalanceAt',
|
||||||
'customerType',
|
'customerType',
|
||||||
'formattedCustomerType',
|
'formattedCustomerType',
|
||||||
|
'unusedCredit',
|
||||||
|
'formattedUnusedCredit',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
Reference in New Issue
Block a user