refactor: GL entries

This commit is contained in:
Ahmed Bouhuolia
2024-12-31 14:57:24 +02:00
parent 1b15261adb
commit a819d6c1ba
54 changed files with 2669 additions and 2298 deletions

View File

@@ -20,6 +20,8 @@ import { SaleReceiptIncrement } from './commands/SaleReceiptIncrement.service';
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module';
import { SaleReceiptsController } from './SaleReceipts.controller';
import { SaleReceiptGLEntriesSubscriber } from './subscribers/SaleReceiptGLEntriesSubscriber';
import { SaleReceiptGLEntries } from './ledger/SaleReceiptGLEntries';
@Module({
controllers: [SaleReceiptsController],
@@ -46,6 +48,8 @@ import { SaleReceiptsController } from './SaleReceipts.controller';
SaleReceiptDTOTransformer,
SaleReceiptBrandingTemplate,
SaleReceiptIncrement,
SaleReceiptGLEntries,
SaleReceiptGLEntriesSubscriber
],
})
export class SaleReceiptsModule {}

View File

@@ -1,184 +0,0 @@
// import { Knex } from 'knex';
// import { Service, Inject } from 'typedi';
// import * as R from 'ramda';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
// import {
// AccountNormal,
// ILedgerEntry,
// ISaleReceipt,
// IItemEntry,
// } from '@/interfaces';
// import Ledger from '@/services/Accounting/Ledger';
// @Service()
// export class SaleReceiptGLEntries {
// @Inject()
// private tenancy: HasTenancyService;
// @Inject()
// private ledgerStorage: LedgerStorageService;
// /**
// * Creates income GL entries.
// * @param {number} tenantId
// * @param {number} saleReceiptId
// * @param {Knex.Transaction} trx
// */
// public writeIncomeGLEntries = async (
// tenantId: number,
// saleReceiptId: number,
// trx?: Knex.Transaction
// ): Promise<void> => {
// const { SaleReceipt } = this.tenancy.models(tenantId);
// const saleReceipt = await SaleReceipt.query(trx)
// .findById(saleReceiptId)
// .withGraphFetched('entries.item');
// // Retrieve the income entries ledger.
// const incomeLedger = this.getIncomeEntriesLedger(saleReceipt);
// // Commits the ledger entries to the storage.
// await this.ledgerStorage.commit(tenantId, incomeLedger, trx);
// };
// /**
// * Reverts the receipt GL entries.
// * @param {number} tenantId
// * @param {number} saleReceiptId
// * @param {Knex.Transaction} trx
// * @returns {Promise<void>}
// */
// public revertReceiptGLEntries = async (
// tenantId: number,
// saleReceiptId: number,
// trx?: Knex.Transaction
// ): Promise<void> => {
// await this.ledgerStorage.deleteByReference(
// tenantId,
// saleReceiptId,
// 'SaleReceipt',
// trx
// );
// };
// /**
// * Rewrites the receipt GL entries.
// * @param {number} tenantId
// * @param {number} saleReceiptId
// * @param {Knex.Transaction} trx
// * @returns {Promise<void>}
// */
// public rewriteReceiptGLEntries = async (
// tenantId: number,
// saleReceiptId: number,
// trx?: Knex.Transaction
// ): Promise<void> => {
// // Reverts the receipt GL entries.
// await this.revertReceiptGLEntries(tenantId, saleReceiptId, trx);
// // Writes the income GL entries.
// await this.writeIncomeGLEntries(tenantId, saleReceiptId, trx);
// };
// /**
// * Retrieves the income GL ledger.
// * @param {ISaleReceipt} saleReceipt
// * @returns {Ledger}
// */
// private getIncomeEntriesLedger = (saleReceipt: ISaleReceipt): Ledger => {
// const entries = this.getIncomeGLEntries(saleReceipt);
// return new Ledger(entries);
// };
// /**
// * Retireves the income GL common entry.
// * @param {ISaleReceipt} saleReceipt -
// */
// private getIncomeGLCommonEntry = (saleReceipt: ISaleReceipt) => {
// return {
// currencyCode: saleReceipt.currencyCode,
// exchangeRate: saleReceipt.exchangeRate,
// transactionType: 'SaleReceipt',
// transactionId: saleReceipt.id,
// date: saleReceipt.receiptDate,
// transactionNumber: saleReceipt.receiptNumber,
// referenceNumber: saleReceipt.referenceNo,
// createdAt: saleReceipt.createdAt,
// credit: 0,
// debit: 0,
// userId: saleReceipt.userId,
// branchId: saleReceipt.branchId,
// };
// };
// /**
// * Retrieve receipt income item GL entry.
// * @param {ISaleReceipt} saleReceipt -
// * @param {IItemEntry} entry -
// * @param {number} index -
// * @returns {ILedgerEntry}
// */
// private getReceiptIncomeItemEntry = R.curry(
// (
// saleReceipt: ISaleReceipt,
// entry: IItemEntry,
// index: number
// ): ILedgerEntry => {
// const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
// const itemIncome = entry.amount * saleReceipt.exchangeRate;
// return {
// ...commonEntry,
// credit: itemIncome,
// accountId: entry.item.sellAccountId,
// note: entry.description,
// index: index + 2,
// itemId: entry.itemId,
// itemQuantity: entry.quantity,
// accountNormal: AccountNormal.CREDIT,
// };
// }
// );
// /**
// * Retrieves the receipt deposit GL deposit entry.
// * @param {ISaleReceipt} saleReceipt
// * @returns {ILedgerEntry}
// */
// private getReceiptDepositEntry = (
// saleReceipt: ISaleReceipt
// ): ILedgerEntry => {
// const commonEntry = this.getIncomeGLCommonEntry(saleReceipt);
// return {
// ...commonEntry,
// debit: saleReceipt.localAmount,
// accountId: saleReceipt.depositAccountId,
// index: 1,
// accountNormal: AccountNormal.DEBIT,
// };
// };
// /**
// * Retrieves the income GL entries.
// * @param {ISaleReceipt} saleReceipt -
// * @returns {ILedgerEntry[]}
// */
// private getIncomeGLEntries = (saleReceipt: ISaleReceipt): ILedgerEntry[] => {
// const getItemEntry = this.getReceiptIncomeItemEntry(saleReceipt);
// const creditEntries = saleReceipt.entries.map(getItemEntry);
// const depositEntry = this.getReceiptDepositEntry(saleReceipt);
// return [depositEntry, ...creditEntries];
// };
// }

View File

@@ -0,0 +1,167 @@
import * as R from 'ramda';
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
import { AccountNormal } from '@/modules/Accounts/Accounts.types';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Ledger } from '@/modules/Ledger/Ledger';
import { SaleReceipt } from '../models/SaleReceipt';
import { ItemEntry } from '@/modules/Items/models/ItemEntry';
export class SaleReceiptGL {
private saleReceipt: SaleReceipt;
private discountAccountId: number;
private otherChargesAccountId: number;
/**
* Constructor method.
* @param {SaleReceipt} saleReceipt - Sale receipt.
*/
constructor(saleReceipt: SaleReceipt) {
this.saleReceipt = saleReceipt;
}
/**
* Sets the discount account id.
* @param {number} discountAccountId - Discount account id.
*/
setDiscountAccountId(discountAccountId: number) {
this.discountAccountId = discountAccountId;
return this;
}
/**
* Sets the other charges account id.
* @param {number} otherChargesAccountId - Other charges account id.
*/
setOtherChargesAccountId(otherChargesAccountId: number) {
this.otherChargesAccountId = otherChargesAccountId;
return this;
}
/**
* Retrieves the income GL common entry.
*/
private getIncomeGLCommonEntry = () => {
return {
currencyCode: this.saleReceipt.currencyCode,
exchangeRate: this.saleReceipt.exchangeRate,
transactionType: 'SaleReceipt',
transactionId: this.saleReceipt.id,
date: this.saleReceipt.receiptDate,
transactionNumber: this.saleReceipt.receiptNumber,
referenceNumber: this.saleReceipt.referenceNo,
createdAt: this.saleReceipt.createdAt,
credit: 0,
debit: 0,
userId: this.saleReceipt.userId,
branchId: this.saleReceipt.branchId,
};
};
/**
* Retrieve receipt income item G/L entry.
* @param {ItemEntry} entry - Item entry.
* @param {number} index - Index.
* @returns {ILedgerEntry}
*/
private getReceiptIncomeItemEntry = R.curry(
(entry: ItemEntry, index: number): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry();
const totalLocal =
entry.totalExcludingTax * this.saleReceipt.exchangeRate;
return {
...commonEntry,
credit: totalLocal,
accountId: entry.item.sellAccountId,
note: entry.description,
index: index + 2,
itemId: entry.itemId,
itemQuantity: entry.quantity,
accountNormal: AccountNormal.CREDIT,
};
},
);
/**
* Retrieves the receipt deposit GL deposit entry.
* @returns {ILedgerEntry}
*/
private getReceiptDepositEntry = (): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry();
return {
...commonEntry,
debit: this.saleReceipt.totalLocal,
accountId: this.saleReceipt.depositAccountId,
index: 1,
accountNormal: AccountNormal.DEBIT,
};
};
/**
* Retrieves the discount GL entry.
* @returns {ILedgerEntry}
*/
private getDiscountEntry = (): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry();
return {
...commonEntry,
debit: this.saleReceipt.discountAmountLocal,
accountId: this.discountAccountId,
index: 1,
accountNormal: AccountNormal.CREDIT,
};
};
/**
* Retrieves the adjustment GL entry.
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (): ILedgerEntry => {
const commonEntry = this.getIncomeGLCommonEntry();
const adjustmentAmount = Math.abs(this.saleReceipt.adjustmentLocal);
return {
...commonEntry,
debit: this.saleReceipt.adjustmentLocal < 0 ? adjustmentAmount : 0,
credit: this.saleReceipt.adjustmentLocal > 0 ? adjustmentAmount : 0,
accountId: this.otherChargesAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieves the income GL entries.
* @returns {ILedgerEntry[]}
*/
public getIncomeGLEntries = (): ILedgerEntry[] => {
const getItemEntry = this.getReceiptIncomeItemEntry;
const creditEntries = this.saleReceipt.entries.map((e, index) =>
getItemEntry(e, index),
);
const depositEntry = this.getReceiptDepositEntry();
const discountEntry = this.getDiscountEntry();
const adjustmentEntry = this.getAdjustmentEntry();
return [depositEntry, ...creditEntries, discountEntry, adjustmentEntry];
};
/**
* Retrieves the income GL ledger.
* @returns {ILedger}
*/
public getIncomeLedger = (): ILedger => {
const entries = this.getIncomeGLEntries();
return new Ledger(entries);
};
}

View File

@@ -0,0 +1,83 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
import { SaleReceipt } from '../models/SaleReceipt';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { SaleReceiptGL } from './SaleReceiptGL';
@Injectable()
export class SaleReceiptGLEntries {
constructor(
private readonly ledgerStorage: LedgerStorageService,
private readonly accountRepository: AccountRepository,
@Inject(SaleReceipt.name)
private readonly saleReceiptModel: typeof SaleReceipt,
) {}
/**
* Creates income GL entries.
* @param {number} saleReceiptId
* @param {Knex.Transaction} trx
*/
public writeIncomeGLEntries = async (
saleReceiptId: number,
trx?: Knex.Transaction
): Promise<void> => {
const saleReceipt = await this.saleReceiptModel.query(trx)
.findById(saleReceiptId)
.withGraphFetched('entries.item');
// Find or create the discount expense account.
const discountAccount = await this.accountRepository.findOrCreateDiscountAccount(
{},
trx
);
// Find or create the other charges account.
const otherChargesAccount =
await this.accountRepository.findOrCreateOtherChargesAccount({}, trx);
// Retrieves the income ledger.
const incomeLedger = new SaleReceiptGL(saleReceipt)
.setDiscountAccountId(discountAccount.id)
.setOtherChargesAccountId(otherChargesAccount.id)
.getIncomeLedger();
// Commits the ledger entries to the storage.
await this.ledgerStorage.commit(incomeLedger, trx);
};
/**
* Reverts the receipt GL entries.
* @param {number} saleReceiptId - Sale receipt id.
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public revertReceiptGLEntries = async (
saleReceiptId: number,
trx?: Knex.Transaction
): Promise<void> => {
await this.ledgerStorage.deleteByReference(
saleReceiptId,
'SaleReceipt',
trx
);
};
/**
* Rewrites the receipt GL entries.
* @param {number} saleReceiptId
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public rewriteReceiptGLEntries = async (
saleReceiptId: number,
trx?: Knex.Transaction
): Promise<void> => {
// Reverts the receipt GL entries.
await this.revertReceiptGLEntries(saleReceiptId, trx);
// Writes the income GL entries.
await this.writeIncomeGLEntries(saleReceiptId, trx);
};
}

View File

@@ -6,6 +6,11 @@ import { Model, mixin } from 'objection';
// import { DEFAULT_VIEWS } from '@/services/Sales/Receipts/constants';
// import ModelSearchable from './ModelSearchable';
import { BaseModel } from '@/models/Model';
import { ItemEntry } from '@/modules/Items/models/ItemEntry';
import { Customer } from '@/modules/Customers/models/Customer';
import { AccountTransaction } from '@/modules/Accounts/models/AccountTransaction.model';
import { Branch } from '@/modules/Branches/models/Branch.model';
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
export class SaleReceipt extends BaseModel {
amount: number;
@@ -27,6 +32,12 @@ export class SaleReceipt extends BaseModel {
createdAt: Date;
updatedAt: Date | null;
customer!: Customer;
entries!: ItemEntry[];
transactions!: AccountTransaction[];
branch!: Branch;
warehouse!: Warehouse;
/**
* Table name
*/

View File

@@ -0,0 +1,63 @@
import { Inject, Injectable } from '@nestjs/common';
import {
ISaleReceiptCreatedPayload,
ISaleReceiptEditedPayload,
ISaleReceiptEventDeletedPayload,
} from '../types/SaleReceipts.types';
import { SaleReceiptGLEntries } from '../ledger/SaleReceiptGLEntries';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
@Injectable()
export class SaleReceiptGLEntriesSubscriber {
constructor(private readonly saleReceiptGLEntries: SaleReceiptGLEntries) {}
/**
* Handles writing sale receipt income journal entries once created.
* @param {ISaleReceiptCreatedPayload} payload -
*/
@OnEvent(events.saleReceipt.onCreated)
@OnEvent(events.saleReceipt.onClosed)
public async handleWriteReceiptIncomeJournalEntrieOnCreate({
saleReceiptId,
saleReceipt,
trx,
}: ISaleReceiptCreatedPayload) {
// Can't continue if the sale receipt is not closed yet.
if (!saleReceipt.closedAt) return null;
// Writes the sale receipt income journal entries.
await this.saleReceiptGLEntries.writeIncomeGLEntries(saleReceiptId, trx);
}
/**
* Handles sale receipt revert jouranl entries once be deleted.
* @param {ISaleReceiptEventDeletedPayload} payload -
*/
@OnEvent(events.saleReceipt.onDeleted)
public async handleRevertReceiptJournalEntriesOnDeleted({
saleReceiptId,
trx,
}: ISaleReceiptEventDeletedPayload) {
await this.saleReceiptGLEntries.revertReceiptGLEntries(saleReceiptId, trx);
}
/**
* Handles writing sale receipt income journal entries once be edited.
* @param {ISaleReceiptEditedPayload} payload -
*/
@OnEvent(events.saleReceipt.onEdited)
public async handleWriteReceiptIncomeJournalEntrieOnEdited({
saleReceipt,
trx,
}: ISaleReceiptEditedPayload) {
// Can't continue if the sale receipt is not closed yet.
if (!saleReceipt.closedAt) return null;
// Writes the sale receipt income journal entries.
await this.saleReceiptGLEntries.rewriteReceiptGLEntries(
saleReceipt.id,
trx,
);
}
}