From 8cd3a6c48d2087538318d7db9c2a8ab3c4699f63 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 22 Jul 2024 20:40:15 +0200 Subject: [PATCH] feat: advanced payments --- .../api/controllers/Sales/PaymentReceives.ts | 11 +- ...plied_column_to_payments_received_table.js | 19 ++ .../src/database/seeds/data/accounts.js | 20 +++ .../server/src/interfaces/PaymentReceive.ts | 5 +- packages/server/src/loaders/eventEmitter.ts | 6 + .../Bills/AutoApplyPrepardExpenses.ts | 73 ++++++++ .../AutoApplyPrepardExpensesOnBillCreated.ts | 36 ++++ .../AutoApplyUnearnedRevenue.ts | 75 ++++++++ .../PaymentReceives/CreatePaymentReceive.ts | 8 +- .../PaymentReceiveDTOTransformer.ts | 5 +- .../PaymentReceiveGLEntries.ts | 168 +++--------------- .../PaymentReceivedGLCommon.ts | 123 +++++++++++++ .../PaymentReceivedUnearnedGLEntries.ts | 110 ++++++++++++ ...utoApplyUnearnedRevenueOnInvoiceCreated.ts | 34 ++++ .../PaymentReceiveForm/PaymentReceiveForm.tsx | 11 -- .../PaymentReceiveHeaderFields.tsx | 4 +- .../PaymentReceiveForm/utils.tsx | 2 +- 17 files changed, 548 insertions(+), 162 deletions(-) create mode 100644 packages/server/src/database/migrations/20240722085204_amount_applied_column_to_payments_received_table.js create mode 100644 packages/server/src/services/Purchases/Bills/AutoApplyPrepardExpenses.ts create mode 100644 packages/server/src/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/AutoApplyUnearnedRevenue.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/PaymentReceivedGLCommon.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries.ts create mode 100644 packages/server/src/services/Sales/PaymentReceives/events/AutoApplyUnearnedRevenueOnInvoiceCreated.ts diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts index 5acd359e4..4181b2fe7 100644 --- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts @@ -151,6 +151,8 @@ export default class PaymentReceivesController extends BaseController { check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), check('payment_date').exists(), + check('amount').exists().isNumeric().toFloat(), + check('reference_no').optional(), check('deposit_account_id').exists().isNumeric().toInt(), check('payment_receive_no').optional({ nullable: true }).trim().escape(), @@ -158,15 +160,20 @@ export default class PaymentReceivesController extends BaseController { check('branch_id').optional({ nullable: true }).isNumeric().toInt(), - check('entries').isArray({ min: 1 }), + check('entries').isArray(), check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(), check('entries.*.index').optional().isNumeric().toInt(), check('entries.*.invoice_id').exists().isNumeric().toInt(), - check('entries.*.payment_amount').exists().isNumeric().toFloat(), + check('entries.*.amount_applied').exists().isNumeric().toFloat(), check('attachments').isArray().optional(), check('attachments.*.key').exists().isString(), + + check('unearned_revenue_account_id') + .optional({ nullable: true }) + .isNumeric() + .toInt(), ]; } diff --git a/packages/server/src/database/migrations/20240722085204_amount_applied_column_to_payments_received_table.js b/packages/server/src/database/migrations/20240722085204_amount_applied_column_to_payments_received_table.js new file mode 100644 index 000000000..32b8ae9fb --- /dev/null +++ b/packages/server/src/database/migrations/20240722085204_amount_applied_column_to_payments_received_table.js @@ -0,0 +1,19 @@ +exports.up = function (knex) { + return knex.schema.table('payment_receives', (table) => { + table.decimal('unapplied_amount', 13, 3).defaultTo(0); + table.decimal('used_amount', 13, 3).defaultTo(0); + table + .integer('unearned_revenue_account_id') + .unsigned() + .references('id') + .inTable('accounts'); + }); +}; + +exports.down = function (knex) { + return knex.schema.table('payment_receives', (table) => { + table.dropColumn('unapplied_amount'); + table.dropColumn('used_amount'); + table.dropColumn('unearned_revenue_account_id'); + }); +}; diff --git a/packages/server/src/database/seeds/data/accounts.js b/packages/server/src/database/seeds/data/accounts.js index a5f7182ba..9721997e8 100644 --- a/packages/server/src/database/seeds/data/accounts.js +++ b/packages/server/src/database/seeds/data/accounts.js @@ -323,4 +323,24 @@ export default [ index: 1, predefined: 0, }, + { + name: 'Unearned Revenue', + slug: 'unearned-revenue', + account_type: 'other-income', + parent_account_id: null, + code: '50005', + active: true, + index: 1, + predefined: true, + }, + { + name: 'Prepaid Expenses', + slug: 'prepaid-expenses', + account_type: 'prepaid-expenses', + parent_account_id: null, + code: '', + active: true, + index: 1, + predefined: true, + } ]; diff --git a/packages/server/src/interfaces/PaymentReceive.ts b/packages/server/src/interfaces/PaymentReceive.ts index 68ecb0eb6..31f36f8f8 100644 --- a/packages/server/src/interfaces/PaymentReceive.ts +++ b/packages/server/src/interfaces/PaymentReceive.ts @@ -25,6 +25,7 @@ export interface IPaymentReceive { updatedAt: Date; localAmount?: number; branchId?: number; + unearnedRevenueAccountId?: number; } export interface IPaymentReceiveCreateDTO { customerId: number; @@ -39,6 +40,8 @@ export interface IPaymentReceiveCreateDTO { branchId?: number; attachments?: AttachmentLinkDTO[]; + + unearnedRevenueAccountId?: number; } export interface IPaymentReceiveEditDTO { @@ -69,7 +72,7 @@ export interface IPaymentReceiveEntryDTO { index: number; paymentReceiveId: number; invoiceId: number; - paymentAmount: number; + amountApplied: number; } export interface IPaymentReceivesFilter extends IDynamicListFilterDTO { diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index 8595bed55..12efacc1e 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -113,6 +113,8 @@ import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/ import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch'; import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude'; import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize'; +import { AutoApplyUnearnedRevenueOnInvoiceCreated } from '@/services/Sales/PaymentReceives/events/AutoApplyUnearnedRevenueOnInvoiceCreated'; +import { AutoApplyPrepardExpensesOnBillCreated } from '@/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated'; export default () => { return new EventPublisher(); @@ -274,5 +276,9 @@ export const susbcribers = () => { // Plaid RecognizeSyncedBankTranasctions, + + // Advanced Payments + AutoApplyUnearnedRevenueOnInvoiceCreated, + AutoApplyPrepardExpensesOnBillCreated ]; }; diff --git a/packages/server/src/services/Purchases/Bills/AutoApplyPrepardExpenses.ts b/packages/server/src/services/Purchases/Bills/AutoApplyPrepardExpenses.ts new file mode 100644 index 000000000..88f3a0c4c --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/AutoApplyPrepardExpenses.ts @@ -0,0 +1,73 @@ +import { Knex } from 'knex'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import PromisePool from '@supercharge/promise-pool'; +import { Inject, Service } from 'typedi'; + +@Service() +export class AutoApplyPrepardExpenses { + @Inject() + private tenancy: HasTenancyService; + + /** + * Auto apply prepard expenses to the given bill. + * @param {number} tenantId + * @param {number} billId + * @returns {Promise} + */ + async autoApplyPrepardExpensesToBill( + tenantId: number, + billId: number, + trx?: Knex.Transaction + ): Promise { + const { PaymentMade, Bill } = this.tenancy.models(tenantId); + + const unappliedPayments = await PaymentMade.query(trx).where( + 'unappliedAmount', + '>', + 0 + ); + const bill = Bill.query(trx).findById(billId).throwIfNotFound(); + + await PromisePool.withConcurrency(1) + .for(unappliedPayments) + .process(async (unappliedPayment: any) => { + const appliedAmount = 1; + + await this.applyBillToPaymentMade( + tenantId, + unappliedPayment.id, + bill.id, + appliedAmount, + trx + ); + }); + + // Increase the paid amount of the purchase invoice. + await Bill.changePaymentAmount(billId, 0, trx); + } + + /** + * Apply the given bill to payment made transaction. + * @param {number} tenantId + * @param {number} billPaymentId + * @param {number} billId + * @param {number} appliedAmount + * @param {Knex.Transaction} trx + */ + public applyBillToPaymentMade = async ( + tenantId: number, + billPaymentId: number, + billId: number, + appliedAmount: number, + trx?: Knex.Transaction + ) => { + const { BillPaymentEntry, BillPayment } = this.tenancy.models(tenantId); + + await BillPaymentEntry.query(trx).insert({ + billPaymentId, + billId, + paymentAmount: appliedAmount, + }); + await BillPayment.query().increment('usedAmount', appliedAmount); + }; +} diff --git a/packages/server/src/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated.ts b/packages/server/src/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated.ts new file mode 100644 index 000000000..ea53fc1ad --- /dev/null +++ b/packages/server/src/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated.ts @@ -0,0 +1,36 @@ +import { Inject, Service } from 'typedi'; +import { AutoApplyPrepardExpenses } from '../AutoApplyPrepardExpenses'; +import events from '@/subscribers/events'; +import { IBillCreatedPayload } from '@/interfaces'; + +@Service() +export class AutoApplyPrepardExpensesOnBillCreated { + @Inject() + private autoApplyPrepardExpenses: AutoApplyPrepardExpenses; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.saleInvoice.onCreated, + this.handleAutoApplyPrepardExpensesOnBillCreated.bind(this) + ); + } + + /** + * Handles the auto apply prepard expenses on bill created. + * @param {IBillCreatedPayload} payload - + */ + private async handleAutoApplyPrepardExpensesOnBillCreated({ + tenantId, + billId, + trx, + }: IBillCreatedPayload) { + await this.autoApplyPrepardExpenses.autoApplyPrepardExpensesToBill( + tenantId, + billId, + trx + ); + } +} diff --git a/packages/server/src/services/Sales/PaymentReceives/AutoApplyUnearnedRevenue.ts b/packages/server/src/services/Sales/PaymentReceives/AutoApplyUnearnedRevenue.ts new file mode 100644 index 000000000..7f1be4e09 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/AutoApplyUnearnedRevenue.ts @@ -0,0 +1,75 @@ +import { Inject, Service } from 'typedi'; +import { Knex } from 'knex'; +import PromisePool from '@supercharge/promise-pool'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; + +@Service() +export class AutoApplyUnearnedRevenue { + @Inject() + private tenancy: HasTenancyService; + /** + * Auto apply invoice to advanced payment received transactions. + * @param {number} tenantId + * @param {number} invoiceId + */ + public async autoApplyUnearnedRevenueToInvoice( + tenantId: number, + invoiceId: number, + trx?: Knex.Transaction + ) { + const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId); + + const unappliedPayments = await PaymentReceive.query(trx).where( + 'unappliedAmount', + '>', + 0 + ); + const invoice = await SaleInvoice.query(trx) + .findById(invoiceId) + .throwIfNotFound(); + + let unappliedAmount = invoice.balance; + + await PromisePool.withConcurrency(1) + .for(unappliedPayments) + .process(async (unappliedPayment: any) => { + const appliedAmount = unappliedAmount; + + await this.applyInvoiceToPaymentReceived( + tenantId, + unappliedPayment.id, + invoice.id, + appliedAmount, + trx + ); + }); + // Increase the paid amount of the sale invoice. + await SaleInvoice.changePaymentAmount(invoiceId, unappliedAmount, trx); + } + + /** + * Apply the given invoice to payment received transaction. + * @param {number} tenantId + * @param {number} paymentReceivedId + * @param {number} invoiceId + * @param {number} appliedAmount + * @param {Knex.Transaction} trx + */ + public applyInvoiceToPaymentReceived = async ( + tenantId: number, + paymentReceiveId: number, + invoiceId: number, + appliedAmount: number, + trx?: Knex.Transaction + ) => { + const { PaymentReceiveEntry, PaymentReceive } = + this.tenancy.models(tenantId); + + await PaymentReceiveEntry.query(trx).insert({ + paymentReceiveId, + invoiceId, + paymentAmount: appliedAmount, + }); + await PaymentReceive.query(trx).increment('usedAmount', appliedAmount); + }; +} diff --git a/packages/server/src/services/Sales/PaymentReceives/CreatePaymentReceive.ts b/packages/server/src/services/Sales/PaymentReceives/CreatePaymentReceive.ts index 7649177d3..da2d9785f 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/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts index 8d0357e04..df47ab14a 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveDTOTransformer.ts @@ -36,7 +36,8 @@ export class PaymentReceiveDTOTransformer { paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO, oldPaymentReceive?: IPaymentReceive ): Promise { - const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); + const appliedAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); + const unappliedAmount = paymentReceiveDTO.amount - appliedAmount; // Retreive the next invoice number. const autoNextNumber = @@ -54,7 +55,7 @@ export class PaymentReceiveDTOTransformer { ...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [ 'paymentDate', ]), - amount: paymentAmount, + unappliedAmount, currencyCode: customer.currencyCode, ...(paymentReceiveNo ? { paymentReceiveNo } : {}), exchangeRate: paymentReceiveDTO.exchangeRate || 1, diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveGLEntries.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveGLEntries.ts index e804733c0..059f8bd43 100644 --- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveGLEntries.ts +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveGLEntries.ts @@ -1,19 +1,14 @@ import { Service, Inject } from 'typedi'; -import { sumBy } from 'lodash'; import { Knex } from 'knex'; import Ledger from '@/services/Accounting/Ledger'; import TenancyService from '@/services/Tenancy/TenancyService'; -import { - IPaymentReceive, - ILedgerEntry, - AccountNormal, - IPaymentReceiveGLCommonEntry, -} from '@/interfaces'; +import { IPaymentReceive, ILedgerEntry, AccountNormal } from '@/interfaces'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import { TenantMetadata } from '@/system/models'; +import { PaymentReceivedGLCommon } from './PaymentReceivedGLCommon'; @Service() -export class PaymentReceiveGLEntries { +export class PaymentReceiveGLEntries extends PaymentReceivedGLCommon { @Inject() private tenancy: TenancyService; @@ -22,9 +17,9 @@ export class PaymentReceiveGLEntries { /** * Writes payment GL entries to the storage. - * @param {number} tenantId - * @param {number} paymentReceiveId - * @param {Knex.Transaction} trx + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveId - Payment received id. + * @param {Knex.Transaction} trx - Knex transaction. * @returns {Promise} */ public writePaymentGLEntries = async ( @@ -34,14 +29,19 @@ export class PaymentReceiveGLEntries { ): Promise => { const { PaymentReceive } = this.tenancy.models(tenantId); - // Retrieves the given tenant metadata. - const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); - // Retrieves the payment receive with associated entries. const paymentReceive = await PaymentReceive.query(trx) .findById(paymentReceiveId) .withGraphFetched('entries.invoice'); + // Cannot continue if the received payment is unearned revenue type, + // that type of transactions have different type of GL entries. + if (paymentReceive.unearnedRevenueAccountId) { + return; + } + // Retrieves the given tenant metadata. + const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + // Retrives the payment receive ledger. const ledger = await this.getPaymentReceiveGLedger( tenantId, @@ -53,25 +53,6 @@ export class PaymentReceiveGLEntries { await this.ledgerStorage.commit(tenantId, ledger, trx); }; - /** - * Reverts the given payment receive GL entries. - * @param {number} tenantId - * @param {number} paymentReceiveId - * @param {Knex.Transaction} trx - */ - public revertPaymentGLEntries = async ( - tenantId: number, - paymentReceiveId: number, - trx?: Knex.Transaction - ) => { - await this.ledgerStorage.deleteByReference( - tenantId, - paymentReceiveId, - 'PaymentReceive', - trx - ); - }; - /** * Rewrites the given payment receive GL entries. * @param {number} tenantId @@ -92,10 +73,10 @@ export class PaymentReceiveGLEntries { /** * Retrieves the payment receive general ledger. - * @param {number} tenantId - - * @param {IPaymentReceive} paymentReceive - - * @param {string} baseCurrencyCode - - * @param {Knex.Transaction} trx - + * @param {number} tenantId - + * @param {IPaymentReceive} paymentReceive - + * @param {string} baseCurrencyCode - + * @param {Knex.Transaction} trx - * @returns {Ledger} */ public getPaymentReceiveGLedger = async ( @@ -126,100 +107,9 @@ export class PaymentReceiveGLEntries { return new Ledger(ledgerEntries); }; - /** - * Calculates the payment total exchange gain/loss. - * @param {IBillPayment} paymentReceive - Payment receive with entries. - * @returns {number} - */ - private getPaymentExGainOrLoss = ( - paymentReceive: IPaymentReceive - ): number => { - return sumBy(paymentReceive.entries, (entry) => { - const paymentLocalAmount = - entry.paymentAmount * paymentReceive.exchangeRate; - const invoicePayment = entry.paymentAmount * entry.invoice.exchangeRate; - - return paymentLocalAmount - invoicePayment; - }); - }; - - /** - * Retrieves the common entry of payment receive. - * @param {IPaymentReceive} paymentReceive - * @returns {} - */ - private getPaymentReceiveCommonEntry = ( - paymentReceive: IPaymentReceive - ): IPaymentReceiveGLCommonEntry => { - return { - debit: 0, - credit: 0, - - currencyCode: paymentReceive.currencyCode, - exchangeRate: paymentReceive.exchangeRate, - - transactionId: paymentReceive.id, - transactionType: 'PaymentReceive', - - transactionNumber: paymentReceive.paymentReceiveNo, - referenceNumber: paymentReceive.referenceNo, - - date: paymentReceive.paymentDate, - userId: paymentReceive.userId, - createdAt: paymentReceive.createdAt, - - branchId: paymentReceive.branchId, - }; - }; - - /** - * Retrieves the payment exchange gain/loss entry. - * @param {IPaymentReceive} paymentReceive - - * @param {number} ARAccountId - - * @param {number} exchangeGainOrLossAccountId - - * @param {string} baseCurrencyCode - - * @returns {ILedgerEntry[]} - */ - private getPaymentExchangeGainLossEntry = ( - paymentReceive: IPaymentReceive, - ARAccountId: number, - exchangeGainOrLossAccountId: number, - baseCurrencyCode: string - ): ILedgerEntry[] => { - const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive); - const gainOrLoss = this.getPaymentExGainOrLoss(paymentReceive); - const absGainOrLoss = Math.abs(gainOrLoss); - - return gainOrLoss - ? [ - { - ...commonJournal, - currencyCode: baseCurrencyCode, - exchangeRate: 1, - debit: gainOrLoss > 0 ? absGainOrLoss : 0, - credit: gainOrLoss < 0 ? absGainOrLoss : 0, - accountId: ARAccountId, - contactId: paymentReceive.customerId, - index: 3, - accountNormal: AccountNormal.CREDIT, - }, - { - ...commonJournal, - currencyCode: baseCurrencyCode, - exchangeRate: 1, - credit: gainOrLoss > 0 ? absGainOrLoss : 0, - debit: gainOrLoss < 0 ? absGainOrLoss : 0, - accountId: exchangeGainOrLossAccountId, - index: 3, - accountNormal: AccountNormal.DEBIT, - }, - ] - : []; - }; - /** * Retrieves the payment deposit GL entry. - * @param {IPaymentReceive} paymentReceive + * @param {IPaymentReceive} paymentReceive * @returns {ILedgerEntry} */ private getPaymentDepositGLEntry = ( @@ -238,8 +128,8 @@ export class PaymentReceiveGLEntries { /** * Retrieves the payment receivable entry. - * @param {IPaymentReceive} paymentReceive - * @param {number} ARAccountId + * @param {IPaymentReceive} paymentReceive + * @param {number} ARAccountId * @returns {ILedgerEntry} */ private getPaymentReceivableEntry = ( @@ -262,15 +152,15 @@ export class PaymentReceiveGLEntries { * Records payment receive journal transactions. * * Invoice payment journals. - * -------- - * - Account receivable -> Debit - * - Payment account [current asset] -> Credit + * ------------ + * - Account Receivable -> Debit + * - Payment Account [current asset] -> Credit * - * @param {number} tenantId - * @param {IPaymentReceive} paymentRecieve - Payment receive model. - * @param {number} ARAccountId - A/R account id. - * @param {number} exGainOrLossAccountId - Exchange gain/loss account id. - * @param {string} baseCurrency - Base currency code. + * @param {number} tenantId + * @param {IPaymentReceive} paymentRecieve - Payment receive model. + * @param {number} ARAccountId - A/R account id. + * @param {number} exGainOrLossAccountId - Exchange gain/loss account id. + * @param {string} baseCurrency - Base currency code. * @returns {Promise} */ public getPaymentReceiveGLEntries = ( diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedGLCommon.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedGLCommon.ts new file mode 100644 index 000000000..ba03cb512 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedGLCommon.ts @@ -0,0 +1,123 @@ +import { sumBy } from 'lodash'; +import { + AccountNormal, + ILedgerEntry, + IPaymentReceive, + IPaymentReceiveGLCommonEntry, +} from '@/interfaces'; +import { Knex } from 'knex'; +import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; + +export class PaymentReceivedGLCommon { + private ledgerStorage: LedgerStorageService; + + /** + * Retrieves the common entry of payment receive. + * @param {IPaymentReceive} paymentReceive + * @returns {IPaymentReceiveGLCommonEntry} + */ + protected getPaymentReceiveCommonEntry = ( + paymentReceive: IPaymentReceive + ): IPaymentReceiveGLCommonEntry => { + return { + debit: 0, + credit: 0, + + currencyCode: paymentReceive.currencyCode, + exchangeRate: paymentReceive.exchangeRate, + + transactionId: paymentReceive.id, + transactionType: 'PaymentReceive', + + transactionNumber: paymentReceive.paymentReceiveNo, + referenceNumber: paymentReceive.referenceNo, + + date: paymentReceive.paymentDate, + userId: paymentReceive.userId, + createdAt: paymentReceive.createdAt, + + branchId: paymentReceive.branchId, + }; + }; + + /** + * Retrieves the payment exchange gain/loss entry. + * @param {IPaymentReceive} paymentReceive - + * @param {number} ARAccountId - + * @param {number} exchangeGainOrLossAccountId - + * @param {string} baseCurrencyCode - + * @returns {ILedgerEntry[]} + */ + protected getPaymentExchangeGainLossEntry = ( + paymentReceive: IPaymentReceive, + ARAccountId: number, + exchangeGainOrLossAccountId: number, + baseCurrencyCode: string + ): ILedgerEntry[] => { + const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive); + const gainOrLoss = this.getPaymentExGainOrLoss(paymentReceive); + const absGainOrLoss = Math.abs(gainOrLoss); + + return gainOrLoss + ? [ + { + ...commonJournal, + currencyCode: baseCurrencyCode, + exchangeRate: 1, + debit: gainOrLoss > 0 ? absGainOrLoss : 0, + credit: gainOrLoss < 0 ? absGainOrLoss : 0, + accountId: ARAccountId, + contactId: paymentReceive.customerId, + index: 3, + accountNormal: AccountNormal.CREDIT, + }, + { + ...commonJournal, + currencyCode: baseCurrencyCode, + exchangeRate: 1, + credit: gainOrLoss > 0 ? absGainOrLoss : 0, + debit: gainOrLoss < 0 ? absGainOrLoss : 0, + accountId: exchangeGainOrLossAccountId, + index: 3, + accountNormal: AccountNormal.DEBIT, + }, + ] + : []; + }; + + /** + * Calculates the payment total exchange gain/loss. + * @param {IBillPayment} paymentReceive - Payment receive with entries. + * @returns {number} + */ + private getPaymentExGainOrLoss = ( + paymentReceive: IPaymentReceive + ): number => { + return sumBy(paymentReceive.entries, (entry) => { + const paymentLocalAmount = + entry.paymentAmount * paymentReceive.exchangeRate; + const invoicePayment = entry.paymentAmount * entry.invoice.exchangeRate; + + return paymentLocalAmount - invoicePayment; + }); + }; + + /** + * Reverts the given payment receive GL entries. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @param {Knex.Transaction} trx + */ + public revertPaymentGLEntries = async ( + tenantId: number, + paymentReceiveId: number, + trx?: Knex.Transaction + ) => { + await this.ledgerStorage.deleteByReference( + tenantId, + paymentReceiveId, + 'PaymentReceive', + trx + ); + }; +} diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries.ts new file mode 100644 index 000000000..f6036a4e6 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries.ts @@ -0,0 +1,110 @@ +import * as R from 'ramda'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import { TenantMetadata } from '@/system/models'; +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; +import { PaymentReceivedGLCommon } from './PaymentReceivedGLCommon'; +import { + AccountNormal, + ILedgerEntry, + IPaymentReceive, + IPaymentReceiveEntry, +} from '@/interfaces'; + +@Service() +export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon { + @Inject() + private tenancy: HasTenancyService; + + /** + * Writes payment GL entries to the storage. + * @param {number} tenantId + * @param {number} paymentReceiveId + * @param {Knex.Transaction} trx + * @returns {Promise} + */ + public writePaymentGLEntries = async ( + tenantId: number, + paymentReceiveId: number, + trx?: Knex.Transaction + ): Promise => { + const { PaymentReceive } = this.tenancy.models(tenantId); + + // Retrieves the given tenant metadata. + const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); + + // Retrieves the payment receive with associated entries. + const paymentReceive = await PaymentReceive.query(trx) + .findById(paymentReceiveId) + .withGraphFetched('entries.invoice'); + + if (paymentReceive.unearnedRevenueAccountId) { + return; + } + + }; + + private getPaymentGLEntries = (paymentReceive: IPaymentReceive) => {}; + + private getPaymentUnearnedGLEntries = R.curry(() => {}); + + /** + * + * @param {IPaymentReceiveEntry} paymentReceivedEntry - + * @param {IPaymentReceive} paymentReceive - + */ + private getPaymentEntryGLEntries = R.curry( + ( + paymentReceivedEntry: IPaymentReceiveEntry, + paymentReceive: IPaymentReceive + ) => { + const depositEntry = this.getPaymentDepositGLEntry( + paymentReceivedEntry.paymentAmount, + paymentReceive + ); + } + ); + + /** + * Retrieves the payment deposit GL entry. + * @param {IPaymentReceive} paymentReceive + * @returns {ILedgerEntry} + */ + private getPaymentDepositGLEntry = ( + amount: number, + paymentReceive: IPaymentReceive + ): ILedgerEntry => { + const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive); + + return { + ...commonJournal, + credit: amount, + accountId: paymentReceive.unearnedRevenueAccountId, + accountNormal: AccountNormal.CREDIT, + index: 2, + }; + }; + + /** + * Retrieves the payment receivable entry. + * @param {IPaymentReceive} paymentReceive + * @param {number} ARAccountId + * @returns {ILedgerEntry} + */ + private getPaymentReceivableEntry = ( + amount: number, + paymentReceive: IPaymentReceive, + ARAccountId: number + ): ILedgerEntry => { + const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive); + + return { + ...commonJournal, + debit: amount, + contactId: paymentReceive.customerId, + accountId: ARAccountId, + accountNormal: AccountNormal.DEBIT, + index: 1, + }; + }; +} diff --git a/packages/server/src/services/Sales/PaymentReceives/events/AutoApplyUnearnedRevenueOnInvoiceCreated.ts b/packages/server/src/services/Sales/PaymentReceives/events/AutoApplyUnearnedRevenueOnInvoiceCreated.ts new file mode 100644 index 000000000..44e7d5de2 --- /dev/null +++ b/packages/server/src/services/Sales/PaymentReceives/events/AutoApplyUnearnedRevenueOnInvoiceCreated.ts @@ -0,0 +1,34 @@ +import { Inject, Service } from 'typedi'; +import events from '@/subscribers/events'; +import { AutoApplyUnearnedRevenue } from '../AutoApplyUnearnedRevenue'; + +@Service() +export class AutoApplyUnearnedRevenueOnInvoiceCreated { + @Inject() + private autoApplyUnearnedRevenue: AutoApplyUnearnedRevenue; + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.saleInvoice.onCreated, + this.handleAutoApplyUnearnedRevenueOnInvoiceCreated.bind(this) + ); + } + + /** + * Handles the auto apply unearned revenue on invoice creating. + * @param + */ + private async handleAutoApplyUnearnedRevenueOnInvoiceCreated({ + tenantId, + saleInvoice, + trx, + }) { + await this.autoApplyUnearnedRevenue.autoApplyUnearnedRevenueToInvoice( + tenantId, + saleInvoice.id, + trx + ); + } +} diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx index a0c84bacb..5d80d89f3 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveForm.tsx @@ -102,17 +102,6 @@ function PaymentReceiveForm({ ) => { setSubmitting(true); - // Calculates the total payment amount of entries. - const totalPaymentAmount = sumBy(values.entries, 'payment_amount'); - - if (totalPaymentAmount <= 0) { - AppToaster.show({ - message: intl.get('you_cannot_make_payment_with_zero_total_amount'), - intent: Intent.DANGER, - }); - setSubmitting(false); - return; - } // Transformes the form values to request body. const form = transformFormToRequest(values); diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx index 9ed1e5503..c94623a5c 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.tsx @@ -124,7 +124,7 @@ export default function PaymentReceiveHeaderFields() { {/* ------------ Full amount ------------ */} - + {({ form: { setFieldValue, @@ -146,7 +146,7 @@ export default function PaymentReceiveHeaderFields() { { - setFieldValue('full_amount', value); + setFieldValue('amount', value); }} onBlurValue={onFullAmountBlur} /> diff --git a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx index 71f75280a..dfc9fcf1f 100644 --- a/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx +++ b/packages/webapp/src/containers/Sales/PaymentReceives/PaymentReceiveForm/utils.tsx @@ -42,7 +42,7 @@ export const defaultPaymentReceive = { // Holds the payment number that entered manually only. payment_receive_no_manually: '', statement: '', - full_amount: '', + amount: '', currency_code: '', branch_id: '', exchange_rate: 1,