feat: general ledger sub-accounts

This commit is contained in:
Ahmed Bouhuolia
2024-06-05 21:45:01 +02:00
parent 6afe1a09c6
commit 044f11ff74
5 changed files with 249 additions and 78 deletions

View File

@@ -56,6 +56,8 @@ export interface IGeneralLedgerSheetAccount {
transactions: IGeneralLedgerSheetAccountTransaction[]; transactions: IGeneralLedgerSheetAccountTransaction[];
openingBalance: IGeneralLedgerSheetAccountBalance; openingBalance: IGeneralLedgerSheetAccountBalance;
closingBalance: IGeneralLedgerSheetAccountBalance; closingBalance: IGeneralLedgerSheetAccountBalance;
closingBalanceSubaccounts: IGeneralLedgerSheetAccountBalance;
children?: IGeneralLedgerSheetAccount[];
} }
export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[]; export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[];

View File

@@ -51,7 +51,7 @@ export default class Ledger implements ILedger {
/** /**
* Filters entries by the given accounts ids then returns a new ledger. * Filters entries by the given accounts ids then returns a new ledger.
* @param {number[]} accountIds * @param {number[]} accountIds
* @returns {ILedger} * @returns {ILedger}
*/ */
public whereAccountsIds(accountIds: number[]): ILedger { public whereAccountsIds(accountIds: number[]): ILedger {
@@ -274,4 +274,14 @@ export default class Ledger implements ILedger {
const entries = Ledger.mappingTransactions(transactions); const entries = Ledger.mappingTransactions(transactions);
return new Ledger(entries); return new Ledger(entries);
} }
/**
* Retrieve the transaction amount.
* @param {number} credit - Credit amount.
* @param {number} debit - Debit amount.
* @param {string} normal - Credit or debit.
*/
static getAmount(credit: number, debit: number, normal: string) {
return normal === 'credit' ? credit - debit : debit - credit;
}
} }

View File

@@ -1,6 +1,4 @@
import * as R from 'ramda'; import * as R from 'ramda';
import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod';
import { FinancialHorizTotals } from '../FinancialHorizTotals';
import { FinancialSheetStructure } from '../FinancialSheetStructure'; import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { import {
BALANCE_SHEET_SCHEMA_NODE_TYPE, BALANCE_SHEET_SCHEMA_NODE_TYPE,

View File

@@ -1,5 +1,6 @@
import { isEmpty, get, last, sumBy } from 'lodash'; import { isEmpty, get, last, sumBy, first, head } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import * as R from 'ramda';
import { import {
IGeneralLedgerSheetQuery, IGeneralLedgerSheetQuery,
IGeneralLedgerSheetAccount, IGeneralLedgerSheetAccount,
@@ -10,11 +11,16 @@ import {
} from '@/interfaces'; } from '@/interfaces';
import FinancialSheet from '../FinancialSheet'; import FinancialSheet from '../FinancialSheet';
import { GeneralLedgerRepository } from './GeneralLedgerRepository'; import { GeneralLedgerRepository } from './GeneralLedgerRepository';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { flatToNestedArray } from '@/utils';
import Ledger from '@/services/Accounting/Ledger';
/** /**
* General ledger sheet. * General ledger sheet.
*/ */
export default class GeneralLedgerSheet extends FinancialSheet { export default class GeneralLedgerSheet extends R.compose(
FinancialSheetStructure
)(FinancialSheet) {
tenantId: number; tenantId: number;
query: IGeneralLedgerSheetQuery; query: IGeneralLedgerSheetQuery;
baseCurrency: string; baseCurrency: string;
@@ -46,13 +52,14 @@ export default class GeneralLedgerSheet extends FinancialSheet {
} }
/** /**
* Retrieve the transaction amount. * Calculate the running balance.
* @param {number} credit - Credit amount. * @param {number} amount - Transaction amount.
* @param {number} debit - Debit amount. * @param {number} lastRunningBalance - Last running balance.
* @param {string} normal - Credit or debit. * @param {number} openingBalance - Opening balance.
* @return {number} Running balance.
*/ */
getAmount(credit: number, debit: number, normal: string) { calculateRunningBalance(amount: number, lastRunningBalance: number): number {
return normal === 'credit' ? credit - debit : debit - credit; return amount + lastRunningBalance;
} }
/** /**
@@ -60,26 +67,38 @@ export default class GeneralLedgerSheet extends FinancialSheet {
* @param {ILedgerEntry} entry - * @param {ILedgerEntry} entry -
* @return {IGeneralLedgerSheetAccountTransaction} * @return {IGeneralLedgerSheetAccountTransaction}
*/ */
entryReducer( private getEntryRunningBalance(
entries: IGeneralLedgerSheetAccountTransaction[],
entry: ILedgerEntry, entry: ILedgerEntry,
openingBalance: number openingBalance: number,
): IGeneralLedgerSheetAccountTransaction[] { runningBalance?: number
const lastEntry = last(entries); ): number {
const lastRunningBalance = runningBalance || openingBalance;
const contact = this.repository.contactsById.get(entry.contactId); const amount = Ledger.getAmount(
const amount = this.getAmount(
entry.credit, entry.credit,
entry.debit, entry.debit,
entry.accountNormal entry.accountNormal
); );
const runningBalance = return this.calculateRunningBalance(amount, lastRunningBalance);
amount + (!isEmpty(entries) ? lastEntry.runningBalance : openingBalance); }
const newEntry = { /**
*
* @param entry
* @param runningBalance
* @returns
*/
private entryMapper(entry: ILedgerEntry, runningBalance: number) {
const contact = this.repository.contactsById.get(entry.contactId);
const amount = Ledger.getAmount(
entry.credit,
entry.debit,
entry.accountNormal
);
return {
id: entry.id,
date: entry.date, date: entry.date,
dateFormatted: moment(entry.date).format('YYYY MMM DD'), dateFormatted: moment(entry.date).format('YYYY MMM DD'),
entryId: entry.id,
transactionNumber: entry.transactionNumber, transactionNumber: entry.transactionNumber,
referenceType: entry.referenceType, referenceType: entry.referenceType,
@@ -104,10 +123,7 @@ export default class GeneralLedgerSheet extends FinancialSheet {
formattedRunningBalance: this.formatNumber(runningBalance), formattedRunningBalance: this.formatNumber(runningBalance),
currencyCode: this.baseCurrency, currencyCode: this.baseCurrency,
}; } as IGeneralLedgerSheetAccountTransaction;
entries.push(newEntry);
return entries;
} }
/** /**
@@ -123,28 +139,40 @@ export default class GeneralLedgerSheet extends FinancialSheet {
.whereAccountId(account.id) .whereAccountId(account.id)
.getEntries(); .getEntries();
return entries.reduce( return entries
( .reduce((prev: Array<[number, ILedgerEntry]>, current: ILedgerEntry) => {
entries: IGeneralLedgerSheetAccountTransaction[], const amount = this.getEntryRunningBalance(
entry: ILedgerEntry current,
) => { openingBalance,
return this.entryReducer(entries, entry, openingBalance); head(last(prev)) as number
}, );
[] return new Array([amount, current]);
); }, [])
.map(([runningBalance, entry]: [number, ILedgerEntry]) =>
this.entryMapper(entry, runningBalance)
);
} }
/** /**
* Retrieve account opening balance. * Retrieves the given account opening balance.
* @param {number} accountId
* @returns {number}
*/
private accountOpeningBalance(accountId: number): number {
return this.repository.openingBalanceTransactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
}
/**
* Retrieve the given account opening balance.
* @param {IAccount} account * @param {IAccount} account
* @return {IGeneralLedgerSheetAccountBalance} * @return {IGeneralLedgerSheetAccountBalance}
*/ */
private accountOpeningBalance( private accountOpeningBalanceTotal(
account: IAccount accountId: number
): IGeneralLedgerSheetAccountBalance { ): IGeneralLedgerSheetAccountBalance {
const amount = this.repository.openingBalanceTransactionsLedger const amount = this.accountOpeningBalance(accountId);
.whereAccountId(account.id)
.getClosingBalance();
const formattedAmount = this.formatTotalNumber(amount); const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency; const currencyCode = this.baseCurrency;
const date = this.query.fromDate; const date = this.query.fromDate;
@@ -153,15 +181,31 @@ export default class GeneralLedgerSheet extends FinancialSheet {
} }
/** /**
* Retrieve account closing balance. * Retrieves the given account closing balance.
* @param {number} accountId
* @returns {number}
*/
private accountClosingBalance(accountId: number): number {
const openingBalance = this.repository.openingBalanceTransactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
const transactionsBalance = this.repository.transactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
return openingBalance + transactionsBalance;
}
/**
* Retrieves the given account closing balance.
* @param {IAccount} account * @param {IAccount} account
* @return {IGeneralLedgerSheetAccountBalance} * @return {IGeneralLedgerSheetAccountBalance}
*/ */
private accountClosingBalance( private accountClosingBalanceTotal(
openingBalance: number, accountId: number
transactions: IGeneralLedgerSheetAccountTransaction[]
): IGeneralLedgerSheetAccountBalance { ): IGeneralLedgerSheetAccountBalance {
const amount = this.calcClosingBalance(openingBalance, transactions); const amount = this.accountClosingBalance(accountId);
const formattedAmount = this.formatTotalNumber(amount); const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency; const currencyCode = this.baseCurrency;
const date = this.query.toDate; const date = this.query.toDate;
@@ -169,29 +213,63 @@ export default class GeneralLedgerSheet extends FinancialSheet {
return { amount, formattedAmount, currencyCode, date }; return { amount, formattedAmount, currencyCode, date };
} }
private calcClosingBalance( /**
openingBalance: number, * Retrieves the given account closing balance with subaccounts.
transactions: IGeneralLedgerSheetAccountTransaction[] * @param {number} accountId
) { * @returns {number}
return openingBalance + sumBy(transactions, (trans) => trans.amount); */
} private accountClosingBalanceWithSubaccounts = (
accountId: number
): number => {
const depsAccountsIds =
this.repository.accountsGraph.dependenciesOf(accountId);
console.log([...depsAccountsIds, accountId]);
const openingBalance = this.repository.openingBalanceTransactionsLedger
.whereAccountsIds([...depsAccountsIds, accountId])
.getClosingBalance();
const transactionsBalanceWithSubAccounts =
this.repository.transactionsLedger
.whereAccountsIds([...depsAccountsIds, accountId])
.getClosingBalance();
const closingBalance = openingBalance + transactionsBalanceWithSubAccounts;
return closingBalance;
};
/**
*
* @param {number} accountId
* @returns {IGeneralLedgerSheetAccountBalance}
*/
private accountClosingBalanceWithSubaccountsTotal = (
accountId: number
): IGeneralLedgerSheetAccountBalance => {
const amount = this.accountClosingBalanceWithSubaccounts(accountId);
const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency;
const date = this.query.toDate;
return { amount, formattedAmount, currencyCode, date };
};
/** /**
* Retreive general ledger accounts sections. * Retreive general ledger accounts sections.
* @param {IAccount} account * @param {IAccount} account
* @return {IGeneralLedgerSheetAccount} * @return {IGeneralLedgerSheetAccount}
*/ */
private accountMapper(account: IAccount): IGeneralLedgerSheetAccount { private accountMapper = (account: IAccount): IGeneralLedgerSheetAccount => {
const openingBalance = this.accountOpeningBalance(account); const openingBalance = this.accountOpeningBalanceTotal(account.id);
const transactions = this.accountTransactionsMapper( const transactions = this.accountTransactionsMapper(
account, account,
openingBalance.amount openingBalance.amount
); );
const closingBalance = this.accountClosingBalance( const closingBalance = this.accountClosingBalanceTotal(account.id);
openingBalance.amount, const closingBalanceSubaccounts =
transactions this.accountClosingBalanceWithSubaccountsTotal(account.id);
);
return { return {
id: account.id, id: account.id,
@@ -202,32 +280,65 @@ export default class GeneralLedgerSheet extends FinancialSheet {
openingBalance, openingBalance,
transactions, transactions,
closingBalance, closingBalance,
closingBalanceSubaccounts,
}; };
} };
/** /**
* Retrieve mapped accounts with general ledger transactions and opeing/closing balance. * Maps over deep nodes to retrieve the G/L account node.
* @param {IAccount[]} accounts
* @returns {IGeneralLedgerSheetAccount[]}
*/
private accountNodesDeepMap = (
accounts: IAccount[]
): IGeneralLedgerSheetAccount[] => {
return this.mapNodesDeep(accounts, this.accountMapper);
};
/**
* Transformes the flatten nodes to nested nodes.
*/
private nestedAccountsNode = (flattenAccounts: IAccount[]): IAccount[] => {
return flatToNestedArray(flattenAccounts, {
id: 'id',
parentId: 'parentAccountId',
});
};
/**
* Filters account nodes.
* @param {IGeneralLedgerSheetAccount[]} nodes
* @returns {IGeneralLedgerSheetAccount[]}
*/
private filterAccountNodes = (
nodes: IGeneralLedgerSheetAccount[]
): IGeneralLedgerSheetAccount[] => {
return this.filterNodesDeep(
nodes,
(generalLedgerAccount: IGeneralLedgerSheetAccount) =>
!(
generalLedgerAccount.transactions.length === 0 &&
this.query.noneTransactions
)
);
};
/**
* Retrieves mapped accounts with general ledger transactions and
* opeing/closing balance.
* @param {IAccount[]} accounts - * @param {IAccount[]} accounts -
* @return {IGeneralLedgerSheetAccount[]} * @return {IGeneralLedgerSheetAccount[]}
*/ */
private accountsWalker(accounts: IAccount[]): IGeneralLedgerSheetAccount[] { private accountsWalker(accounts: IAccount[]): IGeneralLedgerSheetAccount[] {
return ( return R.compose(
accounts this.filterAccountNodes,
.map((account: IAccount) => this.accountMapper(account)) this.accountNodesDeepMap,
// Filter general ledger accounts that have no transactions this.nestedAccountsNode
// when`noneTransactions` is on. )(accounts);
.filter(
(generalLedgerAccount: IGeneralLedgerSheetAccount) =>
!(
generalLedgerAccount.transactions.length === 0 &&
this.query.noneTransactions
)
)
);
} }
/** /**
* Retrieve general ledger report data. * Retrieves general ledger report data.
* @return {IGeneralLedgerSheetAccount[]} * @return {IGeneralLedgerSheetAccount[]}
*/ */
public reportData(): IGeneralLedgerSheetAccount[] { public reportData(): IGeneralLedgerSheetAccount[] {

View File

@@ -113,6 +113,27 @@ export class GeneralLedgerTable extends R.compose(
]; ];
} }
/**
* Closing balance row column accessors.
* @returns {ITableColumnAccessor[]}
*/
private closingBalanceWithSubaccountsColumnAccessors(): IColumnMapperMeta[] {
return [
{ key: 'date', value: this.meta.toDate },
{ key: 'account_name', value: 'Closing Balance with sub-accounts' },
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: '_empty_' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'closingBalanceSubaccounts.formattedAmount' },
{
key: 'running_balance',
accessor: 'closingBalanceSubaccounts.formattedAmount',
},
];
}
/** /**
* Retrieves the common table columns. * Retrieves the common table columns.
* @returns {ITableColumn[]} * @returns {ITableColumn[]}
@@ -191,6 +212,21 @@ export class GeneralLedgerTable extends R.compose(
return tableRowMapper(account, columns, meta); return tableRowMapper(account, columns, meta);
}; };
/**
* Maps the given account node to opening balance table row.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private closingBalanceWithSubaccountsMapper = (
account: IGeneralLedgerSheetAccount
): ITableRow => {
const columns = this.closingBalanceWithSubaccountsColumnAccessors();
const meta = {
rowTypes: [ROW_TYPE.CLOSING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/** /**
* Maps the given account node to transactions table rows. * Maps the given account node to transactions table rows.
* @param {IGeneralLedgerSheetAccount} account * @param {IGeneralLedgerSheetAccount} account
@@ -221,8 +257,23 @@ export class GeneralLedgerTable extends R.compose(
rowTypes: [ROW_TYPE.ACCOUNT], rowTypes: [ROW_TYPE.ACCOUNT],
}; };
const row = tableRowMapper(account, columns, meta); const row = tableRowMapper(account, columns, meta);
const closingBalanceWithSubaccounts =
this.closingBalanceWithSubaccountsMapper(account);
return R.assoc('children', transactions)(row); const children = R.compose(
// Appends the closing balance with sub-accounts row if the account has children accounts.
R.when(
() => account.children?.length > 0,
R.append(closingBalanceWithSubaccounts)
),
R.concat(R.defaultTo([], transactions)),
R.when(
() => account?.children?.length > 0,
R.concat(R.defaultTo([], account.children))
)
)([]);
return R.assoc('children', children)(row);
}; };
/** /**
@@ -233,7 +284,7 @@ export class GeneralLedgerTable extends R.compose(
private accountsMapper = ( private accountsMapper = (
accounts: IGeneralLedgerSheetAccount[] accounts: IGeneralLedgerSheetAccount[]
): ITableRow[] => { ): ITableRow[] => {
return this.mapNodesDeep(accounts, this.accountMapper); return this.mapNodesDeepReverse(accounts, this.accountMapper);
}; };
/** /**
@@ -250,7 +301,6 @@ export class GeneralLedgerTable extends R.compose(
*/ */
public tableColumns(): ITableColumn[] { public tableColumns(): ITableColumn[] {
const columns = this.commonColumns(); const columns = this.commonColumns();
return R.compose(this.tableColumnsCellIndexing)(columns); return R.compose(this.tableColumnsCellIndexing)(columns);
} }
} }