Compare commits

..

1 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
031ccc4a0b fix: Edit the payment received transactions with attachments 2024-06-10 13:41:10 +02:00
14 changed files with 224 additions and 598 deletions

View File

@@ -25,7 +25,6 @@
"@casl/ability": "^5.4.3", "@casl/ability": "^5.4.3",
"@hapi/boom": "^7.4.3", "@hapi/boom": "^7.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0", "@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
"@supercharge/promise-pool": "^3.2.0",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/i18n": "^0.8.7", "@types/i18n": "^0.8.7",
"@types/knex": "^0.16.1", "@types/knex": "^0.16.1",

View File

@@ -4,7 +4,6 @@ import color from 'colorette';
import argv from 'getopts'; import argv from 'getopts';
import Knex from 'knex'; import Knex from 'knex';
import { knexSnakeCaseMappers } from 'objection'; import { knexSnakeCaseMappers } from 'objection';
import { PromisePool } from '@supercharge/promise-pool';
import '../before'; import '../before';
import config from '../config'; import config from '../config';
@@ -29,7 +28,7 @@ function initSystemKnex() {
}); });
} }
function initTenantKnex(organizationId: string = '') { function initTenantKnex(organizationId) {
return Knex({ return Knex({
client: config.tenant.db_client, client: config.tenant.db_client,
connection: { connection: {
@@ -72,12 +71,10 @@ function getAllSystemTenants(knex) {
return knex('tenants'); return knex('tenants');
} }
function getAllInitializedTenants(knex) { function getAllInitializedSystemTenants(knex) {
return knex('tenants').whereNotNull('initializedAt'); return knex('tenants').whereNotNull('initializedAt');
} }
const MIGRATION_CONCURRENCY = 10;
// module.exports = { // module.exports = {
// log, // log,
// success, // success,
@@ -94,7 +91,6 @@ const MIGRATION_CONCURRENCY = 10;
// - bigcapital tenants:migrate:make // - bigcapital tenants:migrate:make
// - bigcapital system:migrate:make // - bigcapital system:migrate:make
// - bigcapital tenants:list // - bigcapital tenants:list
// - bigcapital tenants:list --all
commander commander
.command('system:migrate:rollback') .command('system:migrate:rollback')
@@ -153,13 +149,10 @@ commander
commander commander
.command('tenants:list') .command('tenants:list')
.description('Retrieve a list of all system tenants databases.') .description('Retrieve a list of all system tenants databases.')
.option('-a, --all', 'All tenants even are not initialized.')
.action(async (cmd) => { .action(async (cmd) => {
try { try {
const sysKnex = await initSystemKnex(); const sysKnex = await initSystemKnex();
const tenants = cmd?.all const tenants = await getAllSystemTenants(sysKnex);
? await getAllSystemTenants(sysKnex)
: await getAllInitializedTenants(sysKnex);
tenants.forEach((tenant) => { tenants.forEach((tenant) => {
const dbName = `${config.tenant.db_name_prefix}${tenant.organizationId}`; const dbName = `${config.tenant.db_name_prefix}${tenant.organizationId}`;
@@ -190,20 +183,18 @@ commander
commander commander
.command('tenants:migrate:latest') .command('tenants:migrate:latest')
.description('Migrate all tenants or the given tenant id.') .description('Migrate all tenants or the given tenant id.')
.option( .option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.')
'-t, --tenant_id [tenant_id]',
'Which organization id do you migrate.'
)
.action(async (cmd) => { .action(async (cmd) => {
try { try {
const sysKnex = await initSystemKnex(); const sysKnex = await initSystemKnex();
const tenants = await getAllInitializedTenants(sysKnex); const tenants = await getAllInitializedSystemTenants(sysKnex);
const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId); const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId);
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) { if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
exit(`The given tenant id ${cmd.tenant_id} is not exists.`); exit(`The given tenant id ${cmd.tenant_id} is not exists.`);
} }
// Validate the tenant id exist first of all. // Validate the tenant id exist first of all.
const migrateOpers = [];
const migrateTenant = async (organizationId) => { const migrateTenant = async (organizationId) => {
try { try {
const tenantKnex = await initTenantKnex(organizationId); const tenantKnex = await initTenantKnex(organizationId);
@@ -225,17 +216,17 @@ commander
} }
}; };
if (!cmd.tenant_id) { if (!cmd.tenant_id) {
await PromisePool.withConcurrency(MIGRATION_CONCURRENCY) tenants.forEach((tenant) => {
.for(tenants) const oper = migrateTenant(tenant.organizationId);
.process((tenant, index, pool) => { migrateOpers.push(oper);
return migrateTenant(tenant.organizationId); });
})
.then(() => {
success('All tenants are migrated.');
});
} else { } else {
await migrateTenant(cmd.tenant_id); const oper = migrateTenant(cmd.tenant_id);
migrateOpers.push(oper);
} }
Promise.all(migrateOpers).then(() => {
success('All tenants are migrated.');
});
} catch (error) { } catch (error) {
exit(error); exit(error);
} }
@@ -244,21 +235,19 @@ commander
commander commander
.command('tenants:migrate:rollback') .command('tenants:migrate:rollback')
.description('Rollback the last batch of tenants migrations.') .description('Rollback the last batch of tenants migrations.')
.option( .option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.')
'-t, --tenant_id [tenant_id]',
'Which organization id do you migrate.'
)
.action(async (cmd) => { .action(async (cmd) => {
try { try {
const sysKnex = await initSystemKnex(); const sysKnex = await initSystemKnex();
const tenants = await getAllInitializedTenants(sysKnex); const tenants = await getAllSystemTenants(sysKnex);
const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId); const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId);
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) { if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
exit(`The given tenant id ${cmd.tenant_id} is not exists.`); exit(`The given tenant id ${cmd.tenant_id} is not exists.`);
} }
const migrateTenant = async (organizationId: string) => { const migrateOpers = [];
const migrateTenant = async (organizationId) => {
try { try {
const tenantKnex = await initTenantKnex(organizationId); const tenantKnex = await initTenantKnex(organizationId);
const [batchNo, _log] = await tenantKnex.migrate.rollback(); const [batchNo, _log] = await tenantKnex.migrate.rollback();
@@ -279,17 +268,17 @@ commander
}; };
if (!cmd.tenant_id) { if (!cmd.tenant_id) {
await PromisePool.withConcurrency(MIGRATION_CONCURRENCY) tenants.forEach((tenant) => {
.for(tenants) const oper = migrateTenant(tenant.organizationId);
.process((tenant, index, pool) => { migrateOpers.push(oper);
return migrateTenant(tenant.organizationId); });
})
.then(() => {
success('All tenants are rollbacked.');
});
} else { } else {
await migrateTenant(cmd.tenant_id); const oper = migrateTenant(cmd.tenant_id);
migrateOpers.push(oper);
} }
Promise.all(migrateOpers).then(() => {
success('All tenants are rollbacked.');
});
} catch (error) { } catch (error) {
exit(error); exit(error);
} }

View File

@@ -56,8 +56,6 @@ 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

@@ -274,14 +274,4 @@ 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,4 +1,6 @@
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

@@ -3,6 +3,7 @@ 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,
@@ -11,6 +12,7 @@ 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,31 +1,29 @@
import { isEmpty, get, last, sumBy, first, head } from 'lodash'; import { isEmpty, get, last, sumBy } from 'lodash';
import moment from 'moment';
import * as R from 'ramda';
import { import {
IGeneralLedgerSheetQuery, IGeneralLedgerSheetQuery,
IGeneralLedgerSheetAccount, IGeneralLedgerSheetAccount,
IGeneralLedgerSheetAccountBalance, IGeneralLedgerSheetAccountBalance,
IGeneralLedgerSheetAccountTransaction, IGeneralLedgerSheetAccountTransaction,
IAccount, IAccount,
ILedgerEntry, IJournalPoster,
IJournalEntry,
IContact,
} from '@/interfaces'; } from '@/interfaces';
import FinancialSheet from '../FinancialSheet'; import FinancialSheet from '../FinancialSheet';
import { GeneralLedgerRepository } from './GeneralLedgerRepository'; import moment from 'moment';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { flatToNestedArray } from '@/utils';
import Ledger from '@/services/Accounting/Ledger';
import { calculateRunningBalance } from './_utils';
/** /**
* General ledger sheet. * General ledger sheet.
*/ */
export default class GeneralLedgerSheet extends R.compose( export default class GeneralLedgerSheet extends FinancialSheet {
FinancialSheetStructure tenantId: number;
)(FinancialSheet) { accounts: IAccount[];
private query: IGeneralLedgerSheetQuery; query: IGeneralLedgerSheetQuery;
private baseCurrency: string; openingBalancesJournal: IJournalPoster;
private i18n: any; transactions: IJournalPoster;
private repository: GeneralLedgerRepository; contactsMap: Map<number, IContact>;
baseCurrency: string;
i18n: any;
/** /**
* Constructor method. * Constructor method.
@@ -36,59 +34,63 @@ export default class GeneralLedgerSheet extends R.compose(
* @param {IJournalPoster} closingBalancesJournal - * @param {IJournalPoster} closingBalancesJournal -
*/ */
constructor( constructor(
tenantId: number,
query: IGeneralLedgerSheetQuery, query: IGeneralLedgerSheetQuery,
repository: GeneralLedgerRepository, accounts: IAccount[],
contactsByIdMap: Map<number, IContact>,
transactions: IJournalPoster,
openingBalancesJournal: IJournalPoster,
baseCurrency: string,
i18n i18n
) { ) {
super(); super();
this.tenantId = tenantId;
this.query = query; this.query = query;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
this.repository = repository; this.accounts = accounts;
this.baseCurrency = this.repository.tenant.metadata.currencyCode; this.contactsMap = contactsByIdMap;
this.transactions = transactions;
this.openingBalancesJournal = openingBalancesJournal;
this.baseCurrency = baseCurrency;
this.i18n = i18n; this.i18n = i18n;
} }
/** /**
* Entry mapper. * Retrieve the transaction amount.
* @param {ILedgerEntry} entry - * @param {number} credit - Credit amount.
* @return {IGeneralLedgerSheetAccountTransaction} * @param {number} debit - Debit amount.
* @param {string} normal - Credit or debit.
*/ */
private getEntryRunningBalance( getAmount(credit: number, debit: number, normal: string) {
entry: ILedgerEntry, return normal === 'credit' ? credit - debit : debit - credit;
openingBalance: number,
runningBalance?: number
): number {
const lastRunningBalance = runningBalance || openingBalance;
const amount = Ledger.getAmount(
entry.credit,
entry.debit,
entry.accountNormal
);
return calculateRunningBalance(amount, lastRunningBalance);
} }
/** /**
* Maps the given ledger entry to G/L transaction. * Entry mapper.
* @param {ILedgerEntry} entry * @param {IJournalEntry} entry -
* @param {number} runningBalance * @return {IGeneralLedgerSheetAccountTransaction}
* @returns {IGeneralLedgerSheetAccountTransaction}
*/ */
private transactionMapper( entryReducer(
entry: ILedgerEntry, entries: IGeneralLedgerSheetAccountTransaction[],
runningBalance: number entry: IJournalEntry,
): IGeneralLedgerSheetAccountTransaction { openingBalance: number
const contact = this.repository.contactsById.get(entry.contactId); ): IGeneralLedgerSheetAccountTransaction[] {
const amount = Ledger.getAmount( const lastEntry = last(entries);
const contact = this.contactsMap.get(entry.contactId);
const amount = this.getAmount(
entry.credit, entry.credit,
entry.debit, entry.debit,
entry.accountNormal entry.accountNormal
); );
return { const runningBalance =
id: entry.id, amount + (!isEmpty(entries) ? lastEntry.runningBalance : openingBalance);
const newEntry = {
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,
@@ -107,15 +109,16 @@ export default class GeneralLedgerSheet extends R.compose(
amount, amount,
runningBalance, runningBalance,
formattedAmount: this.formatNumber(amount, { excerptZero: false }), formattedAmount: this.formatNumber(amount),
formattedCredit: this.formatNumber(entry.credit, { excerptZero: false }), formattedCredit: this.formatNumber(entry.credit),
formattedDebit: this.formatNumber(entry.debit, { excerptZero: false }), formattedDebit: this.formatNumber(entry.debit),
formattedRunningBalance: this.formatNumber(runningBalance, { formattedRunningBalance: this.formatNumber(runningBalance),
excerptZero: false,
}),
currencyCode: this.baseCurrency, currencyCode: this.baseCurrency,
} as IGeneralLedgerSheetAccountTransaction; };
entries.push(newEntry);
return entries;
} }
/** /**
@@ -127,48 +130,28 @@ export default class GeneralLedgerSheet extends R.compose(
account: IAccount, account: IAccount,
openingBalance: number openingBalance: number
): IGeneralLedgerSheetAccountTransaction[] { ): IGeneralLedgerSheetAccountTransaction[] {
const entries = this.repository.transactionsLedger const entries = this.transactions.getAccountEntries(account.id);
.whereAccountId(account.id)
.getEntries();
return entries return entries.reduce(
.reduce((prev: Array<[number, ILedgerEntry]>, current: ILedgerEntry) => { (
const prevEntry = last(prev); entries: IGeneralLedgerSheetAccountTransaction[],
const prevRunningBalance = head(prevEntry) as number; entry: IJournalEntry
const amount = this.getEntryRunningBalance( ) => {
current, return this.entryReducer(entries, entry, openingBalance);
openingBalance, },
prevRunningBalance []
); );
return [...prev, [amount, current]];
}, [])
.map((entryPair: [number, ILedgerEntry]) => {
const [runningBalance, entry] = entryPair;
return this.transactionMapper(entry, runningBalance);
});
} }
/** /**
* Retrieves the given account opening balance. * Retrieve 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 accountOpeningBalanceTotal( private accountOpeningBalance(
accountId: number account: IAccount
): IGeneralLedgerSheetAccountBalance { ): IGeneralLedgerSheetAccountBalance {
const amount = this.accountOpeningBalance(accountId); const amount = this.openingBalancesJournal.getAccountBalance(account.id);
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;
@@ -177,31 +160,15 @@ export default class GeneralLedgerSheet extends R.compose(
} }
/** /**
* Retrieves the given account closing balance. * Retrieve 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 accountClosingBalanceTotal( private accountClosingBalance(
accountId: number openingBalance: number,
transactions: IGeneralLedgerSheetAccountTransaction[]
): IGeneralLedgerSheetAccountBalance { ): IGeneralLedgerSheetAccountBalance {
const amount = this.accountClosingBalance(accountId); const amount = this.calcClosingBalance(openingBalance, transactions);
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;
@@ -209,78 +176,31 @@ export default class GeneralLedgerSheet extends R.compose(
return { amount, formattedAmount, currencyCode, date }; return { amount, formattedAmount, currencyCode, date };
} }
/** private calcClosingBalance(
* Retrieves the given account closing balance with subaccounts. openingBalance: number,
* @param {number} accountId transactions: IGeneralLedgerSheetAccountTransaction[]
* @returns {number} ) {
*/ return openingBalance + sumBy(transactions, (trans) => trans.amount);
private accountClosingBalanceWithSubaccounts = ( }
accountId: number
): number => {
const depsAccountsIds =
this.repository.accountsGraph.dependenciesOf(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;
};
/**
* Retrieves the closing balance with subaccounts total node.
* @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 };
};
/**
* Detarmines whether the closing balance subaccounts node should be exist.
* @param {number} accountId
* @returns {boolean}
*/
private isAccountNodeIncludesClosingSubaccounts = (accountId: number) => {
// Retrun early if there is no accounts in the filter so
// return closing subaccounts in all cases.
if (isEmpty(this.query.accountsIds)) {
return true;
}
// Returns true if the given account id includes transactions.
return this.repository.accountNodesIncludeTransactions.includes(accountId);
};
/** /**
* 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.accountOpeningBalanceTotal(account.id); const openingBalance = this.accountOpeningBalance(account);
const transactions = this.accountTransactionsMapper( const transactions = this.accountTransactionsMapper(
account, account,
openingBalance.amount openingBalance.amount
); );
const closingBalance = this.accountClosingBalanceTotal(account.id); const closingBalance = this.accountClosingBalance(
const closingBalanceSubaccounts = openingBalance.amount,
this.accountClosingBalanceWithSubaccountsTotal(account.id); transactions
);
const initialNode = { return {
id: account.id, id: account.id,
name: account.name, name: account.name,
code: account.code, code: account.code,
@@ -290,90 +210,34 @@ export default class GeneralLedgerSheet extends R.compose(
transactions, transactions,
closingBalance, closingBalance,
}; };
}
return R.compose(
R.when(
() => this.isAccountNodeIncludesClosingSubaccounts(account.id),
R.assoc('closingBalanceSubaccounts', closingBalanceSubaccounts)
)
)(initialNode);
};
/** /**
* Maps over deep nodes to retrieve the G/L account node. * Retrieve mapped accounts with general ledger transactions and opeing/closing balance.
* @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 filterAccountNodesByTransactionsFilter = (
nodes: IGeneralLedgerSheetAccount[]
): IGeneralLedgerSheetAccount[] => {
return this.filterNodesDeep(
nodes,
(account: IGeneralLedgerSheetAccount) =>
!(account.transactions.length === 0 && this.query.noneTransactions)
);
};
/**
* Filters account nodes by the acounts filter.
* @param {IAccount[]} nodes
* @returns {IAccount[]}
*/
private filterAccountNodesByAccountsFilter = (
nodes: IAccount[]
): IAccount[] => {
return this.filterNodesDeep(nodes, (node: IGeneralLedgerSheetAccount) => {
if (R.isEmpty(this.query.accountsIds)) {
return true;
}
// Returns true if the given account id exists in the filter.
return this.repository.accountNodeInclude?.includes(node.id);
});
};
/**
* 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 R.compose( return (
R.defaultTo([]), accounts
this.filterAccountNodesByTransactionsFilter, .map((account: IAccount) => this.accountMapper(account))
this.accountNodesDeepMap, // Filter general ledger accounts that have no transactions
R.defaultTo([]), // when`noneTransactions` is on.
this.filterAccountNodesByAccountsFilter, .filter(
this.nestedAccountsNode (generalLedgerAccount: IGeneralLedgerSheetAccount) =>
)(accounts); !(
generalLedgerAccount.transactions.length === 0 &&
this.query.noneTransactions
)
)
);
} }
/** /**
* Retrieves general ledger report data. * Retrieve general ledger report data.
* @return {IGeneralLedgerSheetAccount[]} * @return {IGeneralLedgerSheetAccount[]}
*/ */
public reportData(): IGeneralLedgerSheetAccount[] { public reportData(): IGeneralLedgerSheetAccount[] {
return this.accountsWalker(this.repository.accounts); return this.accountsWalker(this.accounts);
} }
} }

View File

@@ -1,180 +0,0 @@
import moment from 'moment';
import * as R from 'ramda';
import {
IAccount,
IAccountTransaction,
IContact,
IGeneralLedgerSheetQuery,
ITenant,
} from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
import { transformToMap } from '@/utils';
import { Tenant } from '@/system/models';
import { flatten, isEmpty, uniq } from 'lodash';
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;
public accountNodesIncludeTransactions: Array<number> = [];
public accountNodeInclude: Array<number> = [];
/**
* 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.initAccountsOpeningBalance();
this.initAccountNodesIncludeTransactions();
await this.initTransactions();
this.initAccountNodesIncluded();
}
/**
* 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()
.orderBy('name', 'ASC');
}
/**
* 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,
})
.orderBy('date', 'ASC')
.onBuild((query) => {
if (this.filter.accountsIds?.length > 0) {
query.whereIn('accountId', this.accountNodesIncludeTransactions);
}
});
// 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
);
}
/**
* Initialize the account nodes that should include transactions.
* @returns {void}
*/
public initAccountNodesIncludeTransactions() {
if (isEmpty(this.filter.accountsIds)) {
return;
}
const childrenNodeIds = this.filter.accountsIds?.map(
(accountId: number) => {
return this.accountsGraph.dependenciesOf(accountId);
}
);
const nodeIds = R.concat(this.filter.accountsIds, childrenNodeIds);
this.accountNodesIncludeTransactions = uniq(flatten(nodeIds));
}
/**
* Initialize the account node ids should be included,
* if the filter by acounts is presented.
* @returns {void}
*/
public initAccountNodesIncluded() {
if (isEmpty(this.filter.accountsIds)) {
return;
}
const nodeIds = this.filter.accountsIds.map((accountId) => {
const childrenIds = this.accountsGraph.dependenciesOf(accountId);
const parentIds = this.accountsGraph.dependantsOf(accountId);
return R.concat(childrenIds, parentIds);
});
this.accountNodeInclude = R.compose(
R.uniq,
R.flatten,
R.concat(this.filter.accountsIds)
)(nodeIds);
}
}

View File

@@ -1,10 +1,18 @@
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 {
@@ -32,13 +40,29 @@ 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 {Promise<IGeneralLedgerStatement>} * @return {IGeneralLedgerStatement}
*/ */
public async generalLedger( async generalLedger(
tenantId: number, tenantId: number,
query: IGeneralLedgerSheetQuery query: IGeneralLedgerSheetQuery
): Promise<{ ): Promise<{
@@ -46,24 +70,60 @@ export class GeneralLedgerService {
query: IGeneralLedgerSheetQuery; query: IGeneralLedgerSheetQuery;
meta: IGeneralLedgerMeta; meta: IGeneralLedgerMeta;
}> { }> {
const repositories = this.tenancy.repositories(tenantId); const { accountRepository, transactionsRepository, contactRepository } =
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,
}; };
const genealLedgerRepository = new GeneralLedgerRepository( // Retrieve all accounts with associated type from the storage.
repositories, const accounts = await accountRepository.all();
query, const accountsGraph = await accountRepository.getDependencyGraph();
tenantId
);
await genealLedgerRepository.asyncInitialize();
// 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
);
// General ledger report instance. // General ledger report instance.
const generalLedgerInstance = new GeneralLedgerSheet( const generalLedgerInstance = new GeneralLedgerSheet(
tenantId,
filter, filter,
genealLedgerRepository, accounts,
contactsByIdMap,
transactionsJournal,
openingTransJournal,
tenant.metadata.baseCurrency,
i18n i18n
); );
// Retrieve general ledger report data. // Retrieve general ledger report data.

View File

@@ -83,8 +83,8 @@ export class GeneralLedgerTable extends R.compose(
*/ */
private openingBalanceColumnsAccessors(): IColumnMapperMeta[] { private openingBalanceColumnsAccessors(): IColumnMapperMeta[] {
return [ return [
{ key: 'date', value: 'Opening Balance' }, { key: 'date', value: this.meta.fromDate },
{ key: 'account_name', value: '' }, { key: 'account_name', value: 'Opening Balance' },
{ key: 'reference_type', accessor: '_empty_' }, { key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' }, { key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: 'description' }, { key: 'description', accessor: 'description' },
@@ -97,15 +97,12 @@ export class GeneralLedgerTable extends R.compose(
/** /**
* Closing balance row column accessors. * Closing balance row column accessors.
* @param {IGeneralLedgerSheetAccount} account -
* @returns {ITableColumnAccessor[]} * @returns {ITableColumnAccessor[]}
*/ */
private closingBalanceColumnAccessors( private closingBalanceColumnAccessors(): IColumnMapperMeta[] {
account: IGeneralLedgerSheetAccount
): IColumnMapperMeta[] {
return [ return [
{ key: 'date', value: `Closing balance for ${account.name}` }, { key: 'date', value: this.meta.toDate },
{ key: 'account_name', value: `` }, { key: 'account_name', value: 'Closing Balance' },
{ key: 'reference_type', accessor: '_empty_' }, { key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' }, { key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: '_empty_' }, { key: 'description', accessor: '_empty_' },
@@ -116,36 +113,6 @@ export class GeneralLedgerTable extends R.compose(
]; ];
} }
/**
* Closing balance row column accessors.
* @param {IGeneralLedgerSheetAccount} account -
* @returns {ITableColumnAccessor[]}
*/
private closingBalanceWithSubaccountsColumnAccessors(
account: IGeneralLedgerSheetAccount
): IColumnMapperMeta[] {
return [
{
key: 'date',
value: `Closing Balance for ${account.name} with sub-accounts`,
},
{
key: 'account_name',
value: ``,
},
{ 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[]}
@@ -217,22 +184,7 @@ export class GeneralLedgerTable extends R.compose(
* @returns {ITableRow} * @returns {ITableRow}
*/ */
private closingBalanceMapper = (account: IGeneralLedgerSheetAccount) => { private closingBalanceMapper = (account: IGeneralLedgerSheetAccount) => {
const columns = this.closingBalanceColumnAccessors(account); const columns = this.closingBalanceColumnAccessors();
const meta = {
rowTypes: [ROW_TYPE.CLOSING_BALANCE],
};
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(account);
const meta = { const meta = {
rowTypes: [ROW_TYPE.CLOSING_BALANCE], rowTypes: [ROW_TYPE.CLOSING_BALANCE],
}; };
@@ -269,27 +221,8 @@ 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);
// Appends the closing balance with sub-accounts row if the account return R.assoc('children', transactions)(row);
// has children accounts and the node is define.
const isAppendClosingSubaccounts = () =>
account.children?.length > 0 && !!account.closingBalanceSubaccounts;
const children = R.compose(
R.when(
isAppendClosingSubaccounts,
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);
}; };
/** /**
@@ -300,7 +233,7 @@ export class GeneralLedgerTable extends R.compose(
private accountsMapper = ( private accountsMapper = (
accounts: IGeneralLedgerSheetAccount[] accounts: IGeneralLedgerSheetAccount[]
): ITableRow[] => { ): ITableRow[] => {
return this.mapNodesDeepReverse(accounts, this.accountMapper); return this.mapNodesDeep(accounts, this.accountMapper);
}; };
/** /**
@@ -317,6 +250,7 @@ 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);
} }
} }

View File

@@ -1,13 +0,0 @@
/**
* Calculate the running balance.
* @param {number} amount - Transaction amount.
* @param {number} lastRunningBalance - Last running balance.
* @param {number} openingBalance - Opening balance.
* @return {number} Running balance.
*/
export function calculateRunningBalance(
amount: number,
lastRunningBalance: number
): number {
return amount + lastRunningBalance;
}

View File

@@ -146,6 +146,7 @@ export class EditPaymentReceive {
paymentReceiveId, paymentReceiveId,
paymentReceive, paymentReceive,
oldPaymentReceive, oldPaymentReceive,
paymentReceiveDTO,
authorizedUser, authorizedUser,
trx, trx,
} as IPaymentReceiveEditedPayload); } as IPaymentReceiveEditedPayload);

View File

@@ -96,19 +96,12 @@ const GeneralLedgerDataTable = styled(ReportDataTable)`
} }
} }
} }
&:not(:first-child).is-expanded .td {
border-top: 1px solid #ddd;
}
} }
&--OPENING_BALANCE, &--OPENING_BALANCE,
&--CLOSING_BALANCE { &--CLOSING_BALANCE {
.td {
color: #000;
}
.date {
font-weight: 500;
.cell-inner {
position: absolute;
}
}
.amount { .amount {
font-weight: 500; font-weight: 500;
} }
@@ -117,9 +110,6 @@ const GeneralLedgerDataTable = styled(ReportDataTable)`
.name { .name {
font-weight: 500; font-weight: 500;
} }
.td {
border-top: 1px solid #ddd;
}
} }
} }
} }

10
pnpm-lock.yaml generated
View File

@@ -50,9 +50,6 @@ importers:
'@lemonsqueezy/lemonsqueezy.js': '@lemonsqueezy/lemonsqueezy.js':
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.2.0 version: 2.2.0
'@supercharge/promise-pool':
specifier: ^3.2.0
version: 3.2.0
'@types/express': '@types/express':
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
@@ -5754,11 +5751,6 @@ packages:
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
dev: false dev: false
/@supercharge/promise-pool@3.2.0:
resolution: {integrity: sha512-pj0cAALblTZBPtMltWOlZTQSLT07jIaFNeM8TWoJD1cQMgDB9mcMlVMoetiB35OzNJpqQ2b+QEtwiR9f20mADg==}
engines: {node: '>=8'}
dev: false
/@surma/rollup-plugin-off-main-thread@2.2.3: /@surma/rollup-plugin-off-main-thread@2.2.3:
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
dependencies: dependencies:
@@ -17390,7 +17382,6 @@ packages:
/memory-pager@1.5.0: /memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
requiresBuild: true
dev: false dev: false
/memorystream@0.3.1: /memorystream@0.3.1:
@@ -23481,7 +23472,6 @@ packages:
/sparse-bitfield@3.0.3: /sparse-bitfield@3.0.3:
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
requiresBuild: true
dependencies: dependencies:
memory-pager: 1.5.0 memory-pager: 1.5.0
dev: false dev: false