fix: Closing balance in general ledger report does not sum the negative figures.

This commit is contained in:
Ahmed Bouhuolia
2024-06-04 21:26:46 +02:00
parent 909a70e2c5
commit 6afe1a09c6
4 changed files with 152 additions and 97 deletions

View File

@@ -3,7 +3,6 @@ import * as R from 'ramda';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
IAccount,
IAccountTransactionsGroupBy, IAccountTransactionsGroupBy,
IBalanceSheetQuery, IBalanceSheetQuery,
ILedger, ILedger,
@@ -12,7 +11,6 @@ import { transformToMapBy } from 'utils';
import Ledger from '@/services/Accounting/Ledger'; import Ledger from '@/services/Accounting/Ledger';
import { BalanceSheetQuery } from './BalanceSheetQuery'; import { BalanceSheetQuery } from './BalanceSheetQuery';
import { FinancialDatePeriods } from '../FinancialDatePeriods'; import { FinancialDatePeriods } from '../FinancialDatePeriods';
import { ACCOUNT_PARENT_TYPE, ACCOUNT_TYPE } from '@/data/AccountTypes';
import { BalanceSheetRepositoryNetIncome } from './BalanceSheetRepositoryNetIncome'; import { BalanceSheetRepositoryNetIncome } from './BalanceSheetRepositoryNetIncome';
@Service() @Service()

View File

@@ -1,29 +1,25 @@
import { isEmpty, get, last, sumBy } from 'lodash'; import { isEmpty, get, last, sumBy } from 'lodash';
import moment from 'moment';
import { import {
IGeneralLedgerSheetQuery, IGeneralLedgerSheetQuery,
IGeneralLedgerSheetAccount, IGeneralLedgerSheetAccount,
IGeneralLedgerSheetAccountBalance, IGeneralLedgerSheetAccountBalance,
IGeneralLedgerSheetAccountTransaction, IGeneralLedgerSheetAccountTransaction,
IAccount, IAccount,
IJournalPoster, ILedgerEntry,
IJournalEntry,
IContact,
} from '@/interfaces'; } from '@/interfaces';
import FinancialSheet from '../FinancialSheet'; import FinancialSheet from '../FinancialSheet';
import moment from 'moment'; import { GeneralLedgerRepository } from './GeneralLedgerRepository';
/** /**
* General ledger sheet. * General ledger sheet.
*/ */
export default class GeneralLedgerSheet extends FinancialSheet { export default class GeneralLedgerSheet extends FinancialSheet {
tenantId: number; tenantId: number;
accounts: IAccount[];
query: IGeneralLedgerSheetQuery; query: IGeneralLedgerSheetQuery;
openingBalancesJournal: IJournalPoster;
transactions: IJournalPoster;
contactsMap: Map<number, IContact>;
baseCurrency: string; baseCurrency: string;
i18n: any; i18n: any;
repository: GeneralLedgerRepository;
/** /**
* Constructor method. * Constructor method.
@@ -36,11 +32,7 @@ export default class GeneralLedgerSheet extends FinancialSheet {
constructor( constructor(
tenantId: number, tenantId: number,
query: IGeneralLedgerSheetQuery, query: IGeneralLedgerSheetQuery,
accounts: IAccount[], repository: GeneralLedgerRepository,
contactsByIdMap: Map<number, IContact>,
transactions: IJournalPoster,
openingBalancesJournal: IJournalPoster,
baseCurrency: string,
i18n i18n
) { ) {
super(); super();
@@ -48,11 +40,8 @@ export default class GeneralLedgerSheet extends FinancialSheet {
this.tenantId = tenantId; this.tenantId = tenantId;
this.query = query; this.query = query;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
this.accounts = accounts; this.repository = repository;
this.contactsMap = contactsByIdMap; this.baseCurrency = this.repository.tenant.metadata.currencyCode;
this.transactions = transactions;
this.openingBalancesJournal = openingBalancesJournal;
this.baseCurrency = baseCurrency;
this.i18n = i18n; this.i18n = i18n;
} }
@@ -68,17 +57,17 @@ export default class GeneralLedgerSheet extends FinancialSheet {
/** /**
* Entry mapper. * Entry mapper.
* @param {IJournalEntry} entry - * @param {ILedgerEntry} entry -
* @return {IGeneralLedgerSheetAccountTransaction} * @return {IGeneralLedgerSheetAccountTransaction}
*/ */
entryReducer( entryReducer(
entries: IGeneralLedgerSheetAccountTransaction[], entries: IGeneralLedgerSheetAccountTransaction[],
entry: IJournalEntry, entry: ILedgerEntry,
openingBalance: number openingBalance: number
): IGeneralLedgerSheetAccountTransaction[] { ): IGeneralLedgerSheetAccountTransaction[] {
const lastEntry = last(entries); const lastEntry = last(entries);
const contact = this.contactsMap.get(entry.contactId); const contact = this.repository.contactsById.get(entry.contactId);
const amount = this.getAmount( const amount = this.getAmount(
entry.credit, entry.credit,
entry.debit, entry.debit,
@@ -130,12 +119,14 @@ export default class GeneralLedgerSheet extends FinancialSheet {
account: IAccount, account: IAccount,
openingBalance: number openingBalance: number
): IGeneralLedgerSheetAccountTransaction[] { ): IGeneralLedgerSheetAccountTransaction[] {
const entries = this.transactions.getAccountEntries(account.id); const entries = this.repository.transactionsLedger
.whereAccountId(account.id)
.getEntries();
return entries.reduce( return entries.reduce(
( (
entries: IGeneralLedgerSheetAccountTransaction[], entries: IGeneralLedgerSheetAccountTransaction[],
entry: IJournalEntry entry: ILedgerEntry
) => { ) => {
return this.entryReducer(entries, entry, openingBalance); return this.entryReducer(entries, entry, openingBalance);
}, },
@@ -151,7 +142,9 @@ export default class GeneralLedgerSheet extends FinancialSheet {
private accountOpeningBalance( private accountOpeningBalance(
account: IAccount account: IAccount
): IGeneralLedgerSheetAccountBalance { ): IGeneralLedgerSheetAccountBalance {
const amount = this.openingBalancesJournal.getAccountBalance(account.id); const amount = this.repository.openingBalanceTransactionsLedger
.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;
@@ -238,6 +231,6 @@ export default class GeneralLedgerSheet extends FinancialSheet {
* @return {IGeneralLedgerSheetAccount[]} * @return {IGeneralLedgerSheetAccount[]}
*/ */
public reportData(): IGeneralLedgerSheetAccount[] { public reportData(): IGeneralLedgerSheetAccount[] {
return this.accountsWalker(this.accounts); return this.accountsWalker(this.repository.accounts);
} }
} }

View File

@@ -0,0 +1,123 @@
import moment from 'moment';
import {
IAccount,
IAccountTransaction,
IContact,
IGeneralLedgerSheetQuery,
ITenant,
} from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
import { transformToMap } from '@/utils';
import { Tenant } from '@/system/models';
export class GeneralLedgerRepository {
public filter: IGeneralLedgerSheetQuery;
public accounts: IAccount[];
public transactions: IAccountTransaction[];
public openingBalanceTransactions: IAccountTransaction[];
public transactionsLedger: Ledger;
public openingBalanceTransactionsLedger: Ledger;
public repositories: any;
public models: any;
public accountsGraph: any;
public contacts: IContact;
public contactsById: Map<number, IContact>;
public tenantId: number;
public tenant: ITenant;
/**
* Constructor method.
* @param models
* @param repositories
* @param filter
*/
constructor(
repositories: any,
filter: IGeneralLedgerSheetQuery,
tenantId: number
) {
this.filter = filter;
this.repositories = repositories;
this.tenantId = tenantId;
}
/**
* Initialize the G/L report.
*/
public async asyncInitialize() {
await this.initTenant();
await this.initAccounts();
await this.initAccountsGraph();
await this.initContacts();
await this.initTransactions();
await this.initAccountsOpeningBalance();
}
/**
* Initialize the tenant.
*/
public async initTenant() {
this.tenant = await Tenant.query()
.findById(this.tenantId)
.withGraphFetched('metadata');
}
/**
* Initialize the accounts.
*/
public async initAccounts() {
this.accounts = await this.repositories.accountRepository.all();
}
/**
* Initialize the accounts graph.
*/
public async initAccountsGraph() {
this.accountsGraph =
await this.repositories.accountRepository.getDependencyGraph();
}
/**
* Initialize the contacts.
*/
public async initContacts() {
this.contacts = await this.repositories.contactRepository.all();
this.contactsById = transformToMap(this.contacts, 'id');
}
/**
* Initialize the G/L transactions from/to the given date.
*/
public async initTransactions() {
this.transactions = await this.repositories.transactionsRepository.journal({
fromDate: this.filter.fromDate,
toDate: this.filter.toDate,
branchesIds: this.filter.branchesIds,
});
// Transform array transactions to journal collection.
this.transactionsLedger = Ledger.fromTransactions(this.transactions);
}
/**
* Initialize the G/L accounts opening balance.
*/
public async initAccountsOpeningBalance() {
// Retreive opening balance credit/debit sumation.
this.openingBalanceTransactions =
await this.repositories.transactionsRepository.journal({
toDate: moment(this.filter.fromDate).subtract(1, 'day'),
sumationCreditDebit: true,
branchesIds: this.filter.branchesIds,
});
// Accounts opening transactions.
this.openingBalanceTransactionsLedger = Ledger.fromTransactions(
this.openingBalanceTransactions
);
}
}

View File

@@ -1,18 +1,10 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { ServiceError } from '@/exceptions';
import { difference } from 'lodash';
import { IGeneralLedgerSheetQuery, IGeneralLedgerMeta } from '@/interfaces'; import { IGeneralLedgerSheetQuery, IGeneralLedgerMeta } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import Journal from '@/services/Accounting/JournalPoster';
import GeneralLedgerSheet from '@/services/FinancialStatements/GeneralLedger/GeneralLedger'; import GeneralLedgerSheet from '@/services/FinancialStatements/GeneralLedger/GeneralLedger';
import { transformToMap } from 'utils';
import { Tenant } from '@/system/models';
import { GeneralLedgerMeta } from './GeneralLedgerMeta'; import { GeneralLedgerMeta } from './GeneralLedgerMeta';
import { GeneralLedgerRepository } from './GeneralLedgerRepository';
const ERRORS = {
ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND',
};
@Service() @Service()
export class GeneralLedgerService { export class GeneralLedgerService {
@@ -40,29 +32,13 @@ export class GeneralLedgerService {
}; };
} }
/**
* Validates accounts existance on the storage.
* @param {number} tenantId
* @param {number[]} accountsIds
*/
async validateAccountsExistance(tenantId: number, accountsIds: number[]) {
const { Account } = this.tenancy.models(tenantId);
const storedAccounts = await Account.query().whereIn('id', accountsIds);
const storedAccountsIds = storedAccounts.map((a) => a.id);
if (difference(accountsIds, storedAccountsIds).length > 0) {
throw new ServiceError(ERRORS.ACCOUNTS_NOT_FOUND);
}
}
/** /**
* Retrieve general ledger report statement. * Retrieve general ledger report statement.
* @param {number} tenantId * @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query * @param {IGeneralLedgerSheetQuery} query
* @return {IGeneralLedgerStatement} * @return {Promise<IGeneralLedgerStatement>}
*/ */
async generalLedger( public async generalLedger(
tenantId: number, tenantId: number,
query: IGeneralLedgerSheetQuery query: IGeneralLedgerSheetQuery
): Promise<{ ): Promise<{
@@ -70,60 +46,25 @@ export class GeneralLedgerService {
query: IGeneralLedgerSheetQuery; query: IGeneralLedgerSheetQuery;
meta: IGeneralLedgerMeta; meta: IGeneralLedgerMeta;
}> { }> {
const { accountRepository, transactionsRepository, contactRepository } = const repositories = this.tenancy.repositories(tenantId);
this.tenancy.repositories(tenantId);
const i18n = this.tenancy.i18n(tenantId); const i18n = this.tenancy.i18n(tenantId);
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const filter = { const filter = {
...this.defaultQuery, ...this.defaultQuery,
...query, ...query,
}; };
// Retrieve all accounts with associated type from the storage. const genealLedgerRepository = new GeneralLedgerRepository(
const accounts = await accountRepository.all(); repositories,
const accountsGraph = await accountRepository.getDependencyGraph(); query,
tenantId
// Retrieve all contacts on the storage.
const contacts = await contactRepository.all();
const contactsByIdMap = transformToMap(contacts, 'id');
// Retreive journal transactions from/to the given date.
const transactions = await transactionsRepository.journal({
fromDate: filter.fromDate,
toDate: filter.toDate,
branchesIds: filter.branchesIds,
});
// Retreive opening balance credit/debit sumation.
const openingBalanceTrans = await transactionsRepository.journal({
toDate: moment(filter.fromDate).subtract(1, 'day'),
sumationCreditDebit: true,
branchesIds: filter.branchesIds,
});
// Transform array transactions to journal collection.
const transactionsJournal = Journal.fromTransactions(
transactions,
tenantId,
accountsGraph
);
// Accounts opening transactions.
const openingTransJournal = Journal.fromTransactions(
openingBalanceTrans,
tenantId,
accountsGraph
); );
await genealLedgerRepository.asyncInitialize();
// General ledger report instance. // General ledger report instance.
const generalLedgerInstance = new GeneralLedgerSheet( const generalLedgerInstance = new GeneralLedgerSheet(
tenantId, tenantId,
filter, filter,
accounts, genealLedgerRepository,
contactsByIdMap,
transactionsJournal,
openingTransJournal,
tenant.metadata.baseCurrency,
i18n i18n
); );
// Retrieve general ledger report data. // Retrieve general ledger report data.