mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
feat: journal and general ledger report.
This commit is contained in:
@@ -71,6 +71,13 @@ export default class JournalPoster implements IJournalPoster {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public isEmpty() {
|
||||
return this.entries.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the credit entry for the given account.
|
||||
* @param {IJournalEntry} entry -
|
||||
|
||||
@@ -184,7 +184,7 @@ export default class BalanceSheetStatement extends FinancialSheet {
|
||||
const filteredAccounts = accounts
|
||||
// Filter accounts that associated to the section accounts types.
|
||||
.filter(
|
||||
(account) => sectionAccountsTypes.indexOf(account.type.childType) !== -1
|
||||
(account) => sectionAccountsTypes.indexOf(account.type.key) !== -1
|
||||
)
|
||||
.map((account) => this.balanceSheetAccountMapper(account))
|
||||
// Filter accounts that have no transaction when `noneTransactions` is on.
|
||||
@@ -258,7 +258,7 @@ export default class BalanceSheetStatement extends FinancialSheet {
|
||||
type: structure.type,
|
||||
...(structure.type === 'accounts_section'
|
||||
? this.structureRelatedAccountsMapper(
|
||||
structure.accountsTypesRelated,
|
||||
structure.accountsTypes,
|
||||
accounts
|
||||
)
|
||||
: this.structureSectionMapper(structure, accounts)),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pick } from 'lodash';
|
||||
import { pick, get, last } from 'lodash';
|
||||
import {
|
||||
IGeneralLedgerSheetQuery,
|
||||
IGeneralLedgerSheetAccount,
|
||||
@@ -8,9 +8,13 @@ import {
|
||||
IJournalPoster,
|
||||
IAccountType,
|
||||
IJournalEntry,
|
||||
IContact,
|
||||
} from 'interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
/**
|
||||
* General ledger sheet.
|
||||
*/
|
||||
export default class GeneralLedgerSheet extends FinancialSheet {
|
||||
tenantId: number;
|
||||
accounts: IAccount[];
|
||||
@@ -18,6 +22,7 @@ export default class GeneralLedgerSheet extends FinancialSheet {
|
||||
openingBalancesJournal: IJournalPoster;
|
||||
closingBalancesJournal: IJournalPoster;
|
||||
transactions: IJournalPoster;
|
||||
contactsMap: Map<number, IContact>;
|
||||
baseCurrency: string;
|
||||
|
||||
/**
|
||||
@@ -32,6 +37,7 @@ export default class GeneralLedgerSheet extends FinancialSheet {
|
||||
tenantId: number,
|
||||
query: IGeneralLedgerSheetQuery,
|
||||
accounts: IAccount[],
|
||||
contactsByIdMap: Map<number, IContact>,
|
||||
transactions: IJournalPoster,
|
||||
openingBalancesJournal: IJournalPoster,
|
||||
closingBalancesJournal: IJournalPoster,
|
||||
@@ -43,48 +49,100 @@ export default class GeneralLedgerSheet extends FinancialSheet {
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.accounts = accounts;
|
||||
this.contactsMap = contactsByIdMap;
|
||||
this.transactions = transactions;
|
||||
this.openingBalancesJournal = openingBalancesJournal;
|
||||
this.closingBalancesJournal = closingBalancesJournal;
|
||||
this.baseCurrency = baseCurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the transaction amount.
|
||||
* @param {number} credit - Credit amount.
|
||||
* @param {number} debit - Debit amount.
|
||||
* @param {string} normal - Credit or debit.
|
||||
*/
|
||||
getAmount(credit: number, debit: number, normal: string) {
|
||||
return normal === 'credit' ? credit - debit : debit - credit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry mapper.
|
||||
* @param {IJournalEntry} entry -
|
||||
* @return {IGeneralLedgerSheetAccountTransaction}
|
||||
*/
|
||||
entryReducer(
|
||||
entries: IGeneralLedgerSheetAccountTransaction[],
|
||||
entry: IJournalEntry,
|
||||
index: number
|
||||
): IGeneralLedgerSheetAccountTransaction[] {
|
||||
const lastEntry = last(entries);
|
||||
const openingBalance = 0;
|
||||
|
||||
const contact = this.contactsMap.get(entry.contactId);
|
||||
const amount = this.getAmount(
|
||||
entry.credit,
|
||||
entry.debit,
|
||||
entry.accountNormal
|
||||
);
|
||||
const runningBalance =
|
||||
(entries.length === 0
|
||||
? openingBalance
|
||||
: lastEntry
|
||||
? lastEntry.runningBalance
|
||||
: 0) + amount;
|
||||
|
||||
const newEntry = {
|
||||
date: entry.date,
|
||||
entryId: entry.id,
|
||||
|
||||
referenceType: entry.referenceType,
|
||||
referenceId: entry.referenceId,
|
||||
referenceTypeFormatted: entry.referenceTypeFormatted,
|
||||
|
||||
contactName: get(contact, 'displayName'),
|
||||
contactType: get(contact, 'contactService'),
|
||||
|
||||
transactionType: entry.transactionType,
|
||||
index: entry.index,
|
||||
note: entry.note,
|
||||
|
||||
credit: entry.credit,
|
||||
debit: entry.debit,
|
||||
amount,
|
||||
runningBalance,
|
||||
|
||||
formattedAmount: this.formatNumber(amount),
|
||||
formattedCredit: this.formatNumber(entry.credit),
|
||||
formattedDebit: this.formatNumber(entry.debit),
|
||||
formattedRunningBalance: this.formatNumber(runningBalance),
|
||||
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
entries.push(newEntry);
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping the account transactions to general ledger transactions of the given account.
|
||||
* @param {IAccount} account
|
||||
* @return {IGeneralLedgerSheetAccountTransaction[]}
|
||||
*/
|
||||
private accountTransactionsMapper(
|
||||
account: IAccount & { type: IAccountType }
|
||||
account: IAccount & { type: IAccountType },
|
||||
openingBalance: number
|
||||
): IGeneralLedgerSheetAccountTransaction[] {
|
||||
const entries = this.transactions.getAccountEntries(account.id);
|
||||
|
||||
return entries.map(
|
||||
(transaction: IJournalEntry): IGeneralLedgerSheetAccountTransaction => {
|
||||
let amount = 0;
|
||||
|
||||
if (account.type.normal === 'credit') {
|
||||
amount += transaction.credit - transaction.debit;
|
||||
} else if (account.type.normal === 'debit') {
|
||||
amount += transaction.debit - transaction.credit;
|
||||
}
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
|
||||
return {
|
||||
...pick(transaction, [
|
||||
'id',
|
||||
'note',
|
||||
'transactionType',
|
||||
'referenceType',
|
||||
'referenceId',
|
||||
'referenceTypeFormatted',
|
||||
'date',
|
||||
]),
|
||||
amount,
|
||||
formattedAmount,
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
}
|
||||
return entries.reduce(
|
||||
(
|
||||
entries: IGeneralLedgerSheetAccountTransaction[],
|
||||
entry: IJournalEntry
|
||||
) => {
|
||||
return this.entryReducer(entries, entry, openingBalance);
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,11 +186,21 @@ export default class GeneralLedgerSheet extends FinancialSheet {
|
||||
private accountMapper(
|
||||
account: IAccount & { type: IAccountType }
|
||||
): IGeneralLedgerSheetAccount {
|
||||
const openingBalance = this.accountOpeningBalance(account);
|
||||
const closingBalance = this.accountClosingBalance(account);
|
||||
|
||||
return {
|
||||
...pick(account, ['id', 'name', 'code', 'index', 'parentAccountId']),
|
||||
opening: this.accountOpeningBalance(account),
|
||||
transactions: this.accountTransactionsMapper(account),
|
||||
closing: this.accountClosingBalance(account),
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
code: account.code,
|
||||
index: account.index,
|
||||
parentAccountId: account.parentAccountId,
|
||||
openingBalance,
|
||||
transactions: this.accountTransactionsMapper(
|
||||
account,
|
||||
openingBalance.amount
|
||||
),
|
||||
closingBalance,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -149,7 +217,8 @@ export default class GeneralLedgerSheet extends FinancialSheet {
|
||||
.map((account: IAccount & { type: IAccountType }) =>
|
||||
this.accountMapper(account)
|
||||
)
|
||||
// Filter general ledger accounts that have no transactions when `noneTransactions` is on.
|
||||
// Filter general ledger accounts that have no transactions
|
||||
// when`noneTransactions` is on.
|
||||
.filter(
|
||||
(generalLedgerAccount: IGeneralLedgerSheetAccount) =>
|
||||
!(
|
||||
|
||||
@@ -7,6 +7,8 @@ import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import Journal from 'services/Accounting/JournalPoster';
|
||||
import GeneralLedgerSheet from 'services/FinancialStatements/GeneralLedger/GeneralLedger';
|
||||
|
||||
import { transformToMap } from 'utils';
|
||||
|
||||
const ERRORS = {
|
||||
ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND',
|
||||
};
|
||||
@@ -70,6 +72,7 @@ export default class GeneralLedgerService {
|
||||
const {
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
contactRepository
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
@@ -89,6 +92,10 @@ export default class GeneralLedgerService {
|
||||
const accounts = await accountRepository.all('type');
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// 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,
|
||||
@@ -127,6 +134,7 @@ export default class GeneralLedgerService {
|
||||
tenantId,
|
||||
filter,
|
||||
accounts,
|
||||
contactsByIdMap,
|
||||
transactionsJournal,
|
||||
openingTransJournal,
|
||||
closingTransJournal,
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { sumBy, chain, omit } from 'lodash';
|
||||
import { sumBy, chain, get, head } from 'lodash';
|
||||
import {
|
||||
IJournalEntry,
|
||||
IJournalPoster,
|
||||
IJournalReportEntriesGroup,
|
||||
IJournalReportQuery,
|
||||
IJournalReport,
|
||||
IContact,
|
||||
} from 'interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { AccountTransaction } from 'models';
|
||||
|
||||
export default class JournalSheet extends FinancialSheet {
|
||||
tenantId: number;
|
||||
journal: IJournalPoster;
|
||||
query: IJournalReportQuery;
|
||||
baseCurrency: string;
|
||||
readonly contactsById: Map<number | string, IContact>;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
@@ -24,6 +25,8 @@ export default class JournalSheet extends FinancialSheet {
|
||||
tenantId: number,
|
||||
query: IJournalReportQuery,
|
||||
journal: IJournalPoster,
|
||||
accountsGraph: any,
|
||||
contactsById: Map<number | string, IContact>,
|
||||
baseCurrency: string
|
||||
) {
|
||||
super();
|
||||
@@ -32,22 +35,48 @@ export default class JournalSheet extends FinancialSheet {
|
||||
this.journal = journal;
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.accountsGraph = accountsGraph;
|
||||
this.contactsById = contactsById;
|
||||
this.baseCurrency = baseCurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappes the journal entries.
|
||||
* @param {IJournalEntry[]} entries -
|
||||
* Entry mapper.
|
||||
* @param {IJournalEntry} entry
|
||||
*/
|
||||
entriesMapper(
|
||||
entries: IJournalEntry[],
|
||||
) {
|
||||
return entries.map((entry: IJournalEntry) => {
|
||||
return {
|
||||
...omit(entry, 'account'),
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
})
|
||||
entryMapper(entry: IJournalEntry) {
|
||||
const account = this.accountsGraph.getNodeData(entry.accountId);
|
||||
const contact = this.contactsById.get(entry.contactId);
|
||||
|
||||
return {
|
||||
entryId: entry.id,
|
||||
index: entry.index,
|
||||
note: entry.note,
|
||||
|
||||
contactName: get(contact, 'displayName'),
|
||||
contactType: get(contact, 'contactService'),
|
||||
|
||||
accountName: account.name,
|
||||
accountCode: account.code,
|
||||
transactionNumber: entry.transactionNumber,
|
||||
|
||||
currencyCode: this.baseCurrency,
|
||||
formattedCredit: this.formatNumber(entry.credit),
|
||||
formattedDebit: this.formatNumber(entry.debit),
|
||||
|
||||
credit: entry.credit,
|
||||
debit: entry.debit,
|
||||
|
||||
createdAt: entry.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappes the journal entries.
|
||||
* @param {IJournalEntry[]} entries -
|
||||
*/
|
||||
entriesMapper(entries: IJournalEntry[]) {
|
||||
return entries.map(this.entryMapper.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,13 +87,17 @@ export default class JournalSheet extends FinancialSheet {
|
||||
*/
|
||||
entriesGroupsMapper(
|
||||
entriesGroup: IJournalEntry[],
|
||||
key: string
|
||||
groupEntry: IJournalEntry
|
||||
): IJournalReportEntriesGroup {
|
||||
const totalCredit = sumBy(entriesGroup, 'credit');
|
||||
const totalDebit = sumBy(entriesGroup, 'debit');
|
||||
|
||||
return {
|
||||
id: key,
|
||||
date: groupEntry.date,
|
||||
referenceType: groupEntry.referenceType,
|
||||
referenceId: groupEntry.referenceId,
|
||||
referenceTypeFormatted: groupEntry.referenceTypeFormatted,
|
||||
|
||||
entries: this.entriesMapper(entriesGroup),
|
||||
|
||||
currencyCode: this.baseCurrency,
|
||||
@@ -72,8 +105,8 @@ export default class JournalSheet extends FinancialSheet {
|
||||
credit: totalCredit,
|
||||
debit: totalDebit,
|
||||
|
||||
formattedCredit: this.formatNumber(totalCredit),
|
||||
formattedDebit: this.formatNumber(totalDebit),
|
||||
formattedCredit: this.formatTotalNumber(totalCredit),
|
||||
formattedDebit: this.formatTotalNumber(totalDebit),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,9 +118,10 @@ export default class JournalSheet extends FinancialSheet {
|
||||
entriesWalker(entries: IJournalEntry[]): IJournalReportEntriesGroup[] {
|
||||
return chain(entries)
|
||||
.groupBy((entry) => `${entry.referenceId}-${entry.referenceType}`)
|
||||
.map((entriesGroup: IJournalEntry[], key: string) =>
|
||||
this.entriesGroupsMapper(entriesGroup, key)
|
||||
)
|
||||
.map((entriesGroup: IJournalEntry[], key: string) => {
|
||||
const headEntry = head(entriesGroup);
|
||||
return this.entriesGroupsMapper(entriesGroup, headEntry);
|
||||
})
|
||||
.value();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { IJournalReportQuery } from 'interfaces';
|
||||
import moment from 'moment';
|
||||
|
||||
import JournalSheet from './JournalSheet';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import Journal from 'services/Accounting/JournalPoster';
|
||||
|
||||
import { transformToMap } from 'utils';
|
||||
|
||||
@Service()
|
||||
export default class JournalSheetService {
|
||||
@Inject()
|
||||
@@ -40,6 +43,7 @@ export default class JournalSheetService {
|
||||
const {
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
contactRepository,
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
|
||||
const filter = {
|
||||
@@ -50,7 +54,6 @@ export default class JournalSheetService {
|
||||
tenantId,
|
||||
filter,
|
||||
});
|
||||
|
||||
// Settings service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({
|
||||
@@ -60,6 +63,10 @@ export default class JournalSheetService {
|
||||
// Retrieve all accounts on the storage.
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieve all contacts on the storage.
|
||||
const contacts = await contactRepository.all();
|
||||
const contactsByIdMap = transformToMap(contacts, 'id');
|
||||
|
||||
// Retrieve all journal transactions based on the given query.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
fromDate: filter.fromDate,
|
||||
@@ -79,6 +86,8 @@ export default class JournalSheetService {
|
||||
tenantId,
|
||||
filter,
|
||||
transactionsJournal,
|
||||
accountsGraph,
|
||||
contactsByIdMap,
|
||||
baseCurrency
|
||||
);
|
||||
// Retrieve journal report columns.
|
||||
|
||||
@@ -50,6 +50,10 @@ export default class ProfitLossSheet extends FinancialSheet {
|
||||
this.initDateRangeCollection();
|
||||
}
|
||||
|
||||
get otherIncomeAccounts() {
|
||||
return this.accounts.filter((a) => a.type.key === 'other_income');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtering income accounts.
|
||||
* @return {IAccount & { type: IAccountType }[]}
|
||||
@@ -235,6 +239,14 @@ export default class ProfitLossSheet extends FinancialSheet {
|
||||
};
|
||||
}
|
||||
|
||||
private get otherIncomeSection(): any {
|
||||
return {
|
||||
name: 'Other Income',
|
||||
entryNormal: 'credit',
|
||||
...this.sectionMapper(this.otherIncomeAccounts)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreive expenses section.
|
||||
* @return {IProfitLossSheetLossSection}
|
||||
@@ -343,10 +355,14 @@ export default class ProfitLossSheet extends FinancialSheet {
|
||||
* @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);
|
||||
@@ -356,7 +372,6 @@ export default class ProfitLossSheet extends FinancialSheet {
|
||||
expenses,
|
||||
costOfSales,
|
||||
]);
|
||||
|
||||
// - Net income = Operating profit - Other expenses.
|
||||
const netIncome = this.getSummarySection(operatingProfit, otherExpenses);
|
||||
|
||||
@@ -365,6 +380,7 @@ export default class ProfitLossSheet extends FinancialSheet {
|
||||
costOfSales,
|
||||
grossProfit,
|
||||
expenses,
|
||||
otherIncome,
|
||||
otherExpenses,
|
||||
netIncome,
|
||||
operatingProfit,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ITrialBalanceAccount,
|
||||
IAccount,
|
||||
ITrialBalanceTotal,
|
||||
ITrialBalanceSheetData,
|
||||
IAccountType,
|
||||
} from 'interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
@@ -49,6 +50,7 @@ export default class TrialBalanceSheet extends FinancialSheet {
|
||||
/**
|
||||
* Account mapper.
|
||||
* @param {IAccount} account
|
||||
* @return {ITrialBalanceAccount}
|
||||
*/
|
||||
private accountMapper(
|
||||
account: IAccount & { type: IAccountType }
|
||||
@@ -80,6 +82,7 @@ export default class TrialBalanceSheet extends FinancialSheet {
|
||||
/**
|
||||
* Accounts walker.
|
||||
* @param {IAccount[]} accounts
|
||||
* @return {ITrialBalanceAccount[]}
|
||||
*/
|
||||
private accountsWalker(
|
||||
accounts: IAccount & { type: IAccountType }[]
|
||||
@@ -136,8 +139,15 @@ export default class TrialBalanceSheet extends FinancialSheet {
|
||||
|
||||
/**
|
||||
* Retrieve trial balance sheet statement data.
|
||||
* Note: Retruns null in case there is no transactions between the given date periods.
|
||||
*
|
||||
* @return {ITrialBalanceSheetData}
|
||||
*/
|
||||
public reportData() {
|
||||
public reportData(): ITrialBalanceSheetData {
|
||||
// Don't return noting if the journal has no transactions.
|
||||
if (this.journalFinancial.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
const accounts = this.accountsWalker(this.accounts);
|
||||
const total = this.tatalSection(accounts);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user