refactoring: balance sheet report.

refactoring: trial balance sheet report.
refactoring: general ledger report.
refactoring: journal report.
refactoring: P&L report.
This commit is contained in:
Ahmed Bouhuolia
2020-12-10 13:04:49 +02:00
parent e8f329e29e
commit d49992a6d7
71 changed files with 3203 additions and 1571 deletions

View File

@@ -0,0 +1,369 @@
import { flatten, pick, sumBy } from 'lodash';
import { IProfitLossSheetQuery } from "interfaces/ProfitLossSheet";
import FinancialSheet from "../FinancialSheet";
import {
IAccount,
IAccountType,
IJournalPoster,
IProfitLossSheetAccount,
IProfitLossSheetTotal,
IProfitLossSheetStatement,
IProfitLossSheetAccountsSection,
IProfitLossSheetTotalSection,
} from 'interfaces';
import { flatToNestedArray, dateRangeCollection } from 'utils';
export default class ProfitLossSheet extends FinancialSheet {
tenantId: number;
query: IProfitLossSheetQuery;
accounts: IAccount & { type: IAccountType }[];
journal: IJournalPoster;
dateRangeSet: string[];
comparatorDateType: string;
baseCurrency: string;
/**
* Constructor method.
* @param {number} tenantId -
* @param {IProfitLossSheetQuery} query -
* @param {IAccount[]} accounts -
* @param {IJournalPoster} transactionsJournal -
*/
constructor(
tenantId: number,
query: IProfitLossSheetQuery,
accounts: IAccount & { type: IAccountType }[],
journal: IJournalPoster,
baseCurrency: string,
) {
super();
this.tenantId = tenantId;
this.query = query;
this.numberFormat = this.query.numberFormat;
this.accounts = accounts;
this.journal = journal;
this.baseCurrency = baseCurrency;
this.comparatorDateType = query.displayColumnsType === 'total'
? 'day'
: query.displayColumnsBy;
this.initDateRangeCollection();
}
/**
* Filtering income accounts.
* @return {IAccount & { type: IAccountType }[]}
*/
get incomeAccounts() {
return this.accounts.filter(a => a.type.key === 'income');
}
/**
* Filtering expenses accounts.
* @return {IAccount & { type: IAccountType }[]}
*/
get expensesAccounts() {
return this.accounts.filter(a => a.type.key === 'expense');
}
/**
* Filter other expenses accounts.
* @return {IAccount & { type: IAccountType }[]}}
*/
get otherExpensesAccounts() {
return this.accounts.filter(a => a.type.key === 'other_expense');
}
/**
* Filtering cost of sales accounts.
* @return {IAccount & { type: IAccountType }[]}
*/
get costOfSalesAccounts() {
return this.accounts.filter(a => a.type.key === 'cost_of_goods_sold');
}
/**
* Initialize date range set.
*/
initDateRangeCollection() {
if (this.query.displayColumnsType === 'date_periods') {
this.dateRangeSet = dateRangeCollection(
this.query.fromDate,
this.query.toDate,
this.comparatorDateType
);
}
}
/**
* Retrieve account total in the query date.
* @param {IAccount} account -
* @return {IProfitLossSheetTotal}
*/
private getAccountTotal(account: IAccount): IProfitLossSheetTotal {
const amount = this.journal.getAccountBalance(
account.id,
this.query.toDate,
this.comparatorDateType,
);
const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency;
return { amount, formattedAmount, currencyCode };
}
/**
* Retrieve account total periods.
* @param {IAccount} account -
* @return {IProfitLossSheetTotal[]}
*/
private getAccountTotalPeriods(account: IAccount): IProfitLossSheetTotal[] {
return this.dateRangeSet.map((date) => {
const amount = this.journal.getAccountBalance(
account.id,
date,
this.comparatorDateType,
);
const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency;
return { date, amount, formattedAmount, currencyCode };
})
}
/**
* Mapping the given account to total result with account metadata.
* @param {IAccount} account -
* @return {IProfitLossSheetAccount}
*/
private accountMapper(account: IAccount): IProfitLossSheetAccount {
const entries = this.journal.getAccountEntries(account.id);
return {
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
hasTransactions: entries.length > 0,
total: this.getAccountTotal(account),
// Date periods when display columns type `periods`.
...(this.query.displayColumnsType === 'date_periods' && {
totalPeriods: this.getAccountTotalPeriods(account),
}),
};
}
/**
*
* @param {IAccount[]} accounts -
* @return {IProfitLossSheetAccount[]}
*/
private accountsWalker(accounts: IAccount & { type: IAccountType }[]): IProfitLossSheetAccount[] {
const flattenAccounts = accounts
.map(this.accountMapper.bind(this))
// Filter accounts that have no transaction when `noneTransactions` is on.
.filter((account: IProfitLossSheetAccount) =>
!(!account.hasTransactions && this.query.noneTransactions),
)
// Filter accounts that have zero total amount when `noneZero` is on.
.filter((account: IProfitLossSheetAccount) =>
!(account.total.amount === 0 && this.query.noneZero)
);
return flatToNestedArray(
flattenAccounts,
{ id: 'id', parentId: 'parentAccountId' },
);
}
/**
* Retreive the report total section.
* @param {IAccount[]} accounts -
* @return {IProfitLossSheetTotal}
*/
private gatTotalSection(accounts: IProfitLossSheetAccount[]): IProfitLossSheetTotal {
const amount = sumBy(accounts, 'total.amount');
const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency;
return { amount, formattedAmount, currencyCode };
}
/**
* Retrieve the report total section in periods display type.
* @param {IAccount} accounts -
* @return {IProfitLossSheetTotal[]}
*/
private getTotalPeriodsSection(accounts: IProfitLossSheetAccount[]): IProfitLossSheetTotal[] {
return this.dateRangeSet.map((date, index) => {
const amount = sumBy(accounts, `totalPeriods[${index}].amount`);
const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency;
return { amount, formattedAmount, currencyCode };
});
}
sectionMapper(sectionAccounts) {
const accounts = this.accountsWalker(sectionAccounts);
const total = this.gatTotalSection(accounts);
return {
accounts,
total,
...(this.query.displayColumnsType === 'date_periods' && {
totalPeriods: this.getTotalPeriodsSection(accounts),
}),
}
}
/**
* Retrieve income section.
* @return {IProfitLossSheetIncomeSection}
*/
private get incomeSection(): IProfitLossSheetAccountsSection {
return {
sectionTitle: 'Income accounts',
entryNormal: 'credit',
...this.sectionMapper(this.incomeAccounts),
};
}
/**
* Retreive expenses section.
* @return {IProfitLossSheetLossSection}
*/
private get expensesSection(): IProfitLossSheetAccountsSection {
return {
sectionTitle: 'Expense accounts',
entryNormal: 'debit',
...this.sectionMapper(this.expensesAccounts),
};
}
/**
* Retrieve other expenses section.
* @return {IProfitLossSheetAccountsSection}
*/
private get otherExpensesSection(): IProfitLossSheetAccountsSection {
return {
sectionTitle: 'Other expenses accounts',
entryNormal: 'debit',
...this.sectionMapper(this.otherExpensesAccounts),
};
}
/**
* Cost of sales section.
* @return {IProfitLossSheetAccountsSection}
*/
private get costOfSalesSection(): IProfitLossSheetAccountsSection {
return {
sectionTitle: 'Cost of sales',
entryNormal: 'debit',
...this.sectionMapper(this.costOfSalesAccounts),
};
}
private getSummarySectionDatePeriods(
positiveSections: IProfitLossSheetTotalSection[],
minesSections: IProfitLossSheetTotalSection[],
) {
return this.dateRangeSet.map((date, index: number) => {
const totalPositive = sumBy(positiveSections, `totalPeriods[${index}].amount`);
const totalMines = sumBy(minesSections, `totalPeriods[${index}].amount`);
const amount = totalPositive - totalMines;
const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency;
return { date, amount, formattedAmount, currencyCode };
});
};
private getSummarySectionTotal(
positiveSections: IProfitLossSheetTotalSection[],
minesSections: IProfitLossSheetTotalSection[],
) {
const totalPositiveSections = sumBy(positiveSections, 'total.amount');
const totalMinesSections = sumBy(minesSections, 'total.amount');
const amount = totalPositiveSections - totalMinesSections;
const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency;
return { amount, formattedAmount, currencyCode };
}
/**
* Retrieve the summary section
* @param
*/
private getSummarySection(
sections: IProfitLossSheetTotalSection|IProfitLossSheetTotalSection[],
subtractSections: IProfitLossSheetTotalSection|IProfitLossSheetTotalSection[]
): IProfitLossSheetTotalSection {
const positiveSections = Array.isArray(sections) ? sections : [sections];
const minesSections = Array.isArray(subtractSections) ? subtractSections : [subtractSections];
return {
total: this.getSummarySectionTotal(positiveSections, minesSections),
...(this.query.displayColumnsType === 'date_periods' && {
totalPeriods: [
...this.getSummarySectionDatePeriods(
positiveSections,
minesSections,
),
],
}),
}
}
/**
* Retrieve date range columns of the given query.
* @param {IBalanceSheetQuery} query
* @return {string[]}
*/
private dateRangeColumns(): string[] {
return this.dateRangeSet;
}
/**
* Retrieve profit/loss report data.
* @return {IProfitLossSheetStatement}
*/
public reportData(): IProfitLossSheetStatement {
const income = this.incomeSection;
const costOfSales = this.costOfSalesSection;
const expenses = this.expensesSection;
const otherExpenses = this.otherExpensesSection;
// - Gross profit = Total income - COGS.
const grossProfit = this.getSummarySection(income, costOfSales);
// - Operating profit = Gross profit - Expenses.
const operatingProfit = this.getSummarySection(grossProfit, [expenses, costOfSales]);
// - Net income = Operating profit - Other expenses.
const netIncome = this.getSummarySection(operatingProfit, otherExpenses);
return {
income,
costOfSales,
grossProfit,
expenses,
otherExpenses,
netIncome,
operatingProfit,
};
}
/**
* Retrieve profit/loss report columns.
*/
public reportColumns() {
// Date range collection.
return this.query.displayColumnsType === 'date_periods'
? this.dateRangeColumns()
: ['total'];
}
}