diff --git a/packages/server/src/api/controllers/Purchases/BillsPayments.ts b/packages/server/src/api/controllers/Purchases/BillsPayments.ts index dfe46cf4d..2bfb38383 100644 --- a/packages/server/src/api/controllers/Purchases/BillsPayments.ts +++ b/packages/server/src/api/controllers/Purchases/BillsPayments.ts @@ -118,7 +118,7 @@ export default class BillsPayments extends BaseController { check('reference').optional().trim().escape(), 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.*.bill_id').exists().isNumeric().toInt(), check('entries.*.payment_amount').exists().isNumeric().toFloat(), diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index 4181b2fe7..263652e77 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -165,7 +165,7 @@ export default class PaymentReceivesController extends BaseController { check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(), check('entries.*.index').optional().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.*.key').exists().isString(), diff --git a/packages/server/src/database/migrations/20240723231630_amount_applied_column_to_payments_made_table.js b/packages/server/src/database/migrations/20240723231630_amount_applied_column_to_payments_made_table.js new file mode 100644 index 000000000..d88e14fff --- /dev/null +++ b/packages/server/src/database/migrations/20240723231630_amount_applied_column_to_payments_made_table.js @@ -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'); + }); +}; diff --git a/packages/server/src/interfaces/Bill.ts b/packages/server/src/interfaces/Bill.ts index 0ccc72723..d3379956c 100644 --- a/packages/server/src/interfaces/Bill.ts +++ b/packages/server/src/interfaces/Bill.ts @@ -166,3 +166,10 @@ export interface IBillOpenedPayload { oldBill: IBill; tenantId: number; } + + +export interface IBillPrepardExpensesAppliedEventPayload { + tenantId: number; + billId: number; + trx?: Knex.Transaction; +} diff --git a/packages/server/src/interfaces/BillPayment.ts b/packages/server/src/interfaces/BillPayment.ts index 002bf867b..2a8e37626 100644 --- a/packages/server/src/interfaces/BillPayment.ts +++ b/packages/server/src/interfaces/BillPayment.ts @@ -119,3 +119,11 @@ export enum IPaymentMadeAction { Delete = 'Delete', View = 'View', } + +export interface IPaymentPrepardExpensesAppliedEventPayload { + tenantId: number; + billPaymentId: number; + billId: number; + appliedAmount: number; + trx?: Knex.Transaction; +} diff --git a/packages/server/src/interfaces/PaymentReceive.ts b/packages/server/src/interfaces/PaymentReceive.ts index dc35c03e4..1aac795b8 100644 --- a/packages/server/src/interfaces/PaymentReceive.ts +++ b/packages/server/src/interfaces/PaymentReceive.ts @@ -72,7 +72,7 @@ export interface IPaymentReceiveEntryDTO { index: number; paymentReceiveId: number; invoiceId: number; - amountApplied: number; + paymentAmount: number; } export interface IPaymentReceivesFilter extends IDynamicListFilterDTO { diff --git a/packages/server/src/models/Bill.ts b/packages/server/src/models/Bill.ts index 422865cb0..e91f0ea33 100644 --- a/packages/server/src/models/Bill.ts +++ b/packages/server/src/models/Bill.ts @@ -525,9 +525,9 @@ export default class Bill extends mixin(TenantModel, [ return notFoundBillsIds; } - static changePaymentAmount(billId, amount) { + static changePaymentAmount(billId, amount, trx) { const changeMethod = amount > 0 ? 'increment' : 'decrement'; - return this.query() + return this.query(trx) .where('id', billId) [changeMethod]('payment_amount', Math.abs(amount)); } diff --git a/packages/server/src/services/Contacts/ContactTransformer.ts b/packages/server/src/services/Contacts/ContactTransformer.ts index ceaeeec6c..abf6ec0b5 100644 --- a/packages/server/src/services/Contacts/ContactTransformer.ts +++ b/packages/server/src/services/Contacts/ContactTransformer.ts @@ -38,4 +38,24 @@ export default class ContactTransfromer extends Transformer { ? 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 }); + }; } diff --git a/packages/server/src/services/Contacts/Customers/CustomerTransformer.ts b/packages/server/src/services/Contacts/Customers/CustomerTransformer.ts index 497430c0e..98ee39c42 100644 --- a/packages/server/src/services/Contacts/Customers/CustomerTransformer.ts +++ b/packages/server/src/services/Contacts/Customers/CustomerTransformer.ts @@ -12,6 +12,8 @@ export default class CustomerTransfromer extends ContactTransfromer { 'formattedOpeningBalanceAt', 'customerType', 'formattedCustomerType', + 'unusedCredit', + 'formattedUnusedCredit', ]; }; diff --git a/packages/server/src/services/Contacts/Customers/CustomersApplication.ts b/packages/server/src/services/Contacts/Customers/CustomersApplication.ts index 3cf222c02..ac3b1dd3b 100644 --- a/packages/server/src/services/Contacts/Customers/CustomersApplication.ts +++ b/packages/server/src/services/Contacts/Customers/CustomersApplication.ts @@ -45,9 +45,9 @@ export class CustomersApplication { /** * Creates a new customer. - * @param {number} tenantId - * @param {ICustomerNewDTO} customerDTO - * @param {ISystemUser} authorizedUser + * @param {number} tenantId + * @param {ICustomerNewDTO} customerDTO + * @param {ISystemUser} authorizedUser * @returns {Promise} */ public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => { @@ -56,9 +56,9 @@ export class CustomersApplication { /** * Edits details of the given customer. - * @param {number} tenantId - * @param {number} customerId - * @param {ICustomerEditDTO} customerDTO + * @param {number} tenantId + * @param {number} customerId + * @param {ICustomerEditDTO} customerDTO * @return {Promise} */ public editCustomer = ( @@ -75,9 +75,9 @@ export class CustomersApplication { /** * Deletes the given customer and associated transactions. - * @param {number} tenantId - * @param {number} customerId - * @param {ISystemUser} authorizedUser + * @param {number} tenantId + * @param {number} customerId + * @param {ISystemUser} authorizedUser * @returns {Promise} */ public deleteCustomer = ( @@ -94,9 +94,9 @@ export class CustomersApplication { /** * Changes the opening balance of the given customer. - * @param {number} tenantId - * @param {number} customerId - * @param {Date|string} openingBalanceEditDTO + * @param {number} tenantId + * @param {number} customerId + * @param {Date|string} openingBalanceEditDTO * @returns {Promise} */ public editOpeningBalance = ( diff --git a/packages/server/src/services/Contacts/Vendors/VendorTransformer.ts b/packages/server/src/services/Contacts/Vendors/VendorTransformer.ts index 0514392a8..7970befea 100644 --- a/packages/server/src/services/Contacts/Vendors/VendorTransformer.ts +++ b/packages/server/src/services/Contacts/Vendors/VendorTransformer.ts @@ -1,4 +1,3 @@ -import { Service } from 'typedi'; import ContactTransfromer from '../ContactTransformer'; export default class VendorTransfromer extends ContactTransfromer { @@ -10,7 +9,9 @@ export default class VendorTransfromer extends ContactTransfromer { return [ 'formattedBalance', 'formattedOpeningBalance', - 'formattedOpeningBalanceAt' + 'formattedOpeningBalanceAt', + 'unusedCredit', + 'formattedUnusedCredit', ]; }; } diff --git a/packages/server/src/services/Contacts/constants.ts b/packages/server/src/services/Contacts/constants.ts index 3bfb8771e..5e18f9f9a 100644 --- a/packages/server/src/services/Contacts/constants.ts +++ b/packages/server/src/services/Contacts/constants.ts @@ -21,7 +21,6 @@ export const DEFAULT_VIEWS = [ }, ]; - export const ERRORS = { OPENING_BALANCE_DATE_REQUIRED: 'OPENING_BALANCE_DATE_REQUIRED', CONTACT_ALREADY_INACTIVE: 'CONTACT_ALREADY_INACTIVE', diff --git a/packages/server/src/services/Purchases/Bills/AutoApplyPrepardExpenses.ts b/packages/server/src/services/Purchases/Bills/AutoApplyPrepardExpenses.ts index 88f3a0c4c..632d6dbd4 100644 --- a/packages/server/src/services/Purchases/Bills/AutoApplyPrepardExpenses.ts +++ b/packages/server/src/services/Purchases/Bills/AutoApplyPrepardExpenses.ts @@ -1,13 +1,23 @@ import { Knex } from 'knex'; -import HasTenancyService from '@/services/Tenancy/TenancyService'; -import PromisePool from '@supercharge/promise-pool'; 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() export class AutoApplyPrepardExpenses { @Inject() private tenancy: HasTenancyService; + @Inject() + private eventPublisher: EventPublisher; + /** * Auto apply prepard expenses to the given bill. * @param {number} tenantId @@ -19,31 +29,53 @@ export class AutoApplyPrepardExpenses { billId: number, trx?: Knex.Transaction ): Promise { - const { PaymentMade, Bill } = this.tenancy.models(tenantId); + const { BillPayment, Bill } = this.tenancy.models(tenantId); - const unappliedPayments = await PaymentMade.query(trx).where( - 'unappliedAmount', - '>', - 0 - ); - const bill = Bill.query(trx).findById(billId).throwIfNotFound(); + const bill = await 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 = 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) .for(unappliedPayments) - .process(async (unappliedPayment: any) => { - const appliedAmount = 1; - - await this.applyBillToPaymentMade( - tenantId, - unappliedPayment.id, - bill.id, - appliedAmount, - trx - ); - }); + .process(precessHandler); // 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, 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 + ); }; } diff --git a/packages/server/src/services/Sales/PaymentReceives/CreatePaymentReceive.ts b/packages/server/src/services/Sales/PaymentReceives/CreatePaymentReceive.ts index da2d9785f..7649177d3 100644 --- a/packages/server/src/services/Sales/PaymentReceives/CreatePaymentReceive.ts +++ b/packages/server/src/services/Sales/PaymentReceives/CreatePaymentReceive.ts @@ -78,10 +78,10 @@ export class CreatePaymentReceive { paymentReceiveDTO.entries ); // Validate invoice payment amount. - // await this.validators.validateInvoicesPaymentsAmount( - // tenantId, - // paymentReceiveDTO.entries - // ); + await this.validators.validateInvoicesPaymentsAmount( + tenantId, + paymentReceiveDTO.entries + ); // Validates the payment account currency code. this.validators.validatePaymentAccountCurrency( depositAccount.currencyCode, diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index 164fb4df0..70e99f63c 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -254,6 +254,8 @@ export default { onOpening: 'onBillOpening', onOpened: 'onBillOpened', + + onPrepardExpensesApplied: 'onBillPrepardExpensesApplied' }, /** @@ -271,6 +273,8 @@ export default { onPublishing: 'onBillPaymentPublishing', onPublished: 'onBillPaymentPublished', + + onPrepardExpensesApplied: 'onBillPaymentPrepardExpensesApplied' }, /** diff --git a/packages/webapp/src/containers/Customers/CustomersLanding/components.tsx b/packages/webapp/src/containers/Customers/CustomersLanding/components.tsx index 0de0a69b5..9e91344a3 100644 --- a/packages/webapp/src/containers/Customers/CustomersLanding/components.tsx +++ b/packages/webapp/src/containers/Customers/CustomersLanding/components.tsx @@ -160,6 +160,13 @@ export function useCustomersTableColumns() { width: 85, clickable: true, }, + { + id: 'credit_balance', + Header: 'Credit Balance', + accessor: 'formatted_unused_credit', + width: 100, + align: 'right', + }, { id: 'balance', Header: intl.get('receivable_balance'), diff --git a/packages/webapp/src/containers/Vendors/VendorsLanding/components.tsx b/packages/webapp/src/containers/Vendors/VendorsLanding/components.tsx index c6eb396e6..2a12660c5 100644 --- a/packages/webapp/src/containers/Vendors/VendorsLanding/components.tsx +++ b/packages/webapp/src/containers/Vendors/VendorsLanding/components.tsx @@ -183,6 +183,13 @@ export function useVendorsTableColumns() { width: 85, clickable: true, }, + { + id: 'credit_balance', + Header: 'Credit Balance', + accessor: 'formatted_unused_credit', + width: 100, + align: 'right', + }, { id: 'balance', Header: intl.get('receivable_balance'),