mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
412 lines
11 KiB
TypeScript
412 lines
11 KiB
TypeScript
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';
|
|
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
|
|
|
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();
|
|
}
|
|
|
|
get otherIncomeAccounts() {
|
|
return this.accounts.filter(
|
|
(a) => a.accountType === ACCOUNT_TYPE.OTHER_INCOME
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Filtering income accounts.
|
|
* @return {IAccount & { type: IAccountType }[]}
|
|
*/
|
|
get incomeAccounts() {
|
|
return this.accounts.filter((a) => a.accountType === ACCOUNT_TYPE.INCOME);
|
|
}
|
|
|
|
/**
|
|
* Filtering expenses accounts.
|
|
* @return {IAccount & { type: IAccountType }[]}
|
|
*/
|
|
get expensesAccounts() {
|
|
return this.accounts.filter((a) => a.accountType === ACCOUNT_TYPE.EXPENSE);
|
|
}
|
|
|
|
/**
|
|
* Filter other expenses accounts.
|
|
* @return {IAccount & { type: IAccountType }[]}}
|
|
*/
|
|
get otherExpensesAccounts() {
|
|
return this.accounts.filter(
|
|
(a) => a.accountType === ACCOUNT_TYPE.OTHER_EXPENSE
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Filtering cost of sales accounts.
|
|
* @return {IAccount & { type: IAccountType }[]}
|
|
*/
|
|
get costOfSalesAccounts() {
|
|
return this.accounts.filter(
|
|
(a) => a.accountType === ACCOUNT_TYPE.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.formatTotalNumber(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.formatTotalNumber(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 {
|
|
name: 'Income accounts',
|
|
entryNormal: 'credit',
|
|
...this.sectionMapper(this.incomeAccounts),
|
|
};
|
|
}
|
|
|
|
private get otherIncomeSection(): any {
|
|
return {
|
|
name: 'Other Income',
|
|
entryNormal: 'credit',
|
|
...this.sectionMapper(this.otherIncomeAccounts),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Retreive expenses section.
|
|
* @return {IProfitLossSheetLossSection}
|
|
*/
|
|
private get expensesSection(): IProfitLossSheetAccountsSection {
|
|
return {
|
|
name: 'Expense accounts',
|
|
entryNormal: 'debit',
|
|
...this.sectionMapper(this.expensesAccounts),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Retrieve other expenses section.
|
|
* @return {IProfitLossSheetAccountsSection}
|
|
*/
|
|
private get otherExpensesSection(): IProfitLossSheetAccountsSection {
|
|
return {
|
|
name: 'Other expenses accounts',
|
|
entryNormal: 'debit',
|
|
...this.sectionMapper(this.otherExpensesAccounts),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Cost of sales section.
|
|
* @return {IProfitLossSheetAccountsSection}
|
|
*/
|
|
private get costOfSalesSection(): IProfitLossSheetAccountsSection {
|
|
return {
|
|
name: '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.formatTotalNumber(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.formatTotalNumber(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 {
|
|
if (this.journal.isEmpty()) {
|
|
return null;
|
|
}
|
|
const income = this.incomeSection;
|
|
const costOfSales = this.costOfSalesSection;
|
|
const expenses = this.expensesSection;
|
|
const otherExpenses = this.otherExpensesSection;
|
|
const otherIncome = this.otherIncomeSection;
|
|
|
|
// - Gross profit = Total income - COGS.
|
|
const grossProfit = this.getSummarySection(income, costOfSales);
|
|
|
|
// - Operating profit = Gross profit - Expenses.
|
|
const operatingProfit = this.getSummarySection(grossProfit, expenses);
|
|
|
|
// Net other income = Other income - other expenses.
|
|
const netOtherIncome = this.getSummarySection(otherIncome, otherExpenses);
|
|
|
|
// - Net income = (Operating profit + other income) - Other expenses.
|
|
const netIncome = this.getSummarySection(
|
|
[operatingProfit, netOtherIncome],
|
|
[],
|
|
);
|
|
|
|
return {
|
|
income,
|
|
costOfSales,
|
|
grossProfit,
|
|
expenses,
|
|
otherIncome,
|
|
otherExpenses,
|
|
operatingProfit,
|
|
netOtherIncome,
|
|
netIncome,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Retrieve profit/loss report columns.
|
|
*/
|
|
public reportColumns() {
|
|
// Date range collection.
|
|
return this.query.displayColumnsType === 'date_periods'
|
|
? this.dateRangeColumns()
|
|
: ['total'];
|
|
}
|
|
}
|