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:
@@ -16,7 +16,8 @@ export default class JournalSheetController extends BaseFinancialReportControlle
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/',
|
||||
router.get(
|
||||
'/',
|
||||
this.journalValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.journal.bind(this))
|
||||
@@ -31,18 +32,20 @@ export default class JournalSheetController extends BaseFinancialReportControlle
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
oneOf([
|
||||
query('transaction_types').optional().isArray({ min: 1 }),
|
||||
query('transaction_types.*').optional().isNumeric().toInt(),
|
||||
], [
|
||||
query('transaction_types').optional().trim().escape(),
|
||||
]),
|
||||
oneOf([
|
||||
query('account_ids').optional().isArray({ min: 1 }),
|
||||
query('account_ids.*').optional().isNumeric().toInt(),
|
||||
], [
|
||||
query('account_ids').optional().isNumeric().toInt(),
|
||||
]),
|
||||
oneOf(
|
||||
[
|
||||
query('transaction_types').optional().isArray({ min: 1 }),
|
||||
query('transaction_types.*').optional().isNumeric().toInt(),
|
||||
],
|
||||
[query('transaction_types').optional().trim().escape()]
|
||||
),
|
||||
oneOf(
|
||||
[
|
||||
query('account_ids').optional().isArray({ min: 1 }),
|
||||
query('account_ids.*').optional().isNumeric().toInt(),
|
||||
],
|
||||
[query('account_ids').optional().isNumeric().toInt()]
|
||||
),
|
||||
query('from_range').optional().isNumeric().toInt(),
|
||||
query('to_range').optional().isNumeric().toInt(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
@@ -52,7 +55,7 @@ export default class JournalSheetController extends BaseFinancialReportControlle
|
||||
|
||||
/**
|
||||
* Retrieve the ledger report of the given account.
|
||||
* @param {Request} req -
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async journal(req: Request, res: Response, next: NextFunction) {
|
||||
@@ -63,11 +66,20 @@ export default class JournalSheetController extends BaseFinancialReportControlle
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
const organizationName = settings.get({ group: 'organization', key: 'name' });
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
try {
|
||||
const { data, query } = await this.journalService.journalSheet(tenantId, filter);
|
||||
const { data, query } = await this.journalService.journalSheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
organization_name: organizationName,
|
||||
@@ -79,4 +91,4 @@ export default class JournalSheetController extends BaseFinancialReportControlle
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,42 @@ const balanceSheetStructure: IBalanceSheetStructureSection[] = [
|
||||
children: [
|
||||
{
|
||||
name: 'Current Asset',
|
||||
type: 'accounts_section',
|
||||
accountsTypesRelated: ['current_asset'],
|
||||
sectionType: 'assets',
|
||||
type: 'section',
|
||||
children: [
|
||||
{
|
||||
name: 'Cash and cash equivalents',
|
||||
type: 'accounts_section',
|
||||
accountsTypes: ['cash', 'bank'],
|
||||
},
|
||||
{
|
||||
name: 'Accounts Receivable',
|
||||
type: 'accounts_section',
|
||||
accountsTypes: ['accounts_receivable'],
|
||||
},
|
||||
{
|
||||
name: 'Inventories',
|
||||
type: 'accounts_section',
|
||||
accountsTypes: ['inventory'],
|
||||
},
|
||||
{
|
||||
name: 'Other current assets',
|
||||
type: 'accounts_section',
|
||||
accountsTypes: ['other_current_asset'],
|
||||
},
|
||||
],
|
||||
alwaysShow: true,
|
||||
},
|
||||
{
|
||||
name: 'Fixed Asset',
|
||||
type: 'accounts_section',
|
||||
accountsTypesRelated: ['fixed_asset'],
|
||||
accountsTypes: ['fixed_asset'],
|
||||
},
|
||||
{
|
||||
name: 'Other Asset',
|
||||
name: 'Non-Current Assets',
|
||||
type: 'accounts_section',
|
||||
accountsTypesRelated: ['other_asset'],
|
||||
},
|
||||
accountsTypes: ['non_current_asset'],
|
||||
}
|
||||
],
|
||||
alwaysShow: true,
|
||||
},
|
||||
@@ -35,27 +58,32 @@ const balanceSheetStructure: IBalanceSheetStructureSection[] = [
|
||||
type: 'section',
|
||||
children: [
|
||||
{
|
||||
name: 'Current Liability',
|
||||
name: 'Current Liabilties',
|
||||
type: 'accounts_section',
|
||||
accountsTypesRelated: ['current_liability'],
|
||||
accountsTypes: [
|
||||
'accounts_payable',
|
||||
'tax_payable',
|
||||
'credit_card',
|
||||
'other_current_liability'
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Long Term Liability',
|
||||
name: 'Long-Term Liabilities',
|
||||
type: 'accounts_section',
|
||||
accountsTypesRelated: ['long_term_liability'],
|
||||
accountsTypes: ['long_term_liability'],
|
||||
},
|
||||
{
|
||||
name: 'Other Liability',
|
||||
name: 'Non-Current Liabilities',
|
||||
type: 'accounts_section',
|
||||
accountsTypesRelated: ['other_liability'],
|
||||
},
|
||||
accountsTypes: ['non_current_liability'],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Equity',
|
||||
sectionType: 'equity',
|
||||
type: 'accounts_section',
|
||||
accountsTypesRelated: ['equity'],
|
||||
accountsTypes: ['equity'],
|
||||
},
|
||||
],
|
||||
alwaysShow: true,
|
||||
|
||||
@@ -53,7 +53,7 @@ export default [
|
||||
key: 'non_current_asset',
|
||||
normal: 'debit',
|
||||
root_type: 'asset',
|
||||
child_type: 'non_current_asset',
|
||||
child_type: 'fixed_asset',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
@@ -81,14 +81,6 @@ export default [
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
{
|
||||
key: 'long_term_liability',
|
||||
normal: 'credit',
|
||||
root_type: 'liability',
|
||||
child_type: 'long_term_liability',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
key: 'other_current_liability',
|
||||
normal: 'credit',
|
||||
@@ -97,6 +89,22 @@ export default [
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
key: 'non_current_liability',
|
||||
normal: 'credit',
|
||||
root_type: 'liability',
|
||||
child_type: 'current_liability',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
key: 'long_term_liability',
|
||||
normal: 'credit',
|
||||
root_type: 'liability',
|
||||
child_type: 'long_term_liability',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
key: 'equity',
|
||||
normal: 'credit',
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface IBalanceSheetStructureSection {
|
||||
sectionType?: string;
|
||||
type: 'section' | 'accounts_section';
|
||||
children?: IBalanceSheetStructureSection[];
|
||||
accountsTypesRelated?: string[];
|
||||
accountsTypes?: string[];
|
||||
alwaysShow?: boolean;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,6 @@ export interface IBalanceSheetSection {
|
||||
total: IBalanceSheetAccountTotal;
|
||||
totalPeriods?: IBalanceSheetAccountTotal[];
|
||||
|
||||
accountsTypesRelated?: string[];
|
||||
accountsTypes?: string[];
|
||||
_forceShow?: boolean;
|
||||
}
|
||||
|
||||
@@ -14,13 +14,26 @@ export interface IGeneralLedgerSheetQuery {
|
||||
|
||||
export interface IGeneralLedgerSheetAccountTransaction {
|
||||
id: number,
|
||||
|
||||
amount: number,
|
||||
runningBalance: number,
|
||||
credit: number,
|
||||
debit: number,
|
||||
|
||||
formattedAmount: string,
|
||||
formattedCredit: string,
|
||||
formattedDebit: string,
|
||||
formattedRunningBalance: string,
|
||||
|
||||
currencyCode: string,
|
||||
note?: string,
|
||||
|
||||
transactionType?: string,
|
||||
transactionNumber: string,
|
||||
|
||||
referenceId?: number,
|
||||
referenceType?: string,
|
||||
|
||||
date: Date|string,
|
||||
};
|
||||
|
||||
@@ -38,8 +51,8 @@ export interface IGeneralLedgerSheetAccount {
|
||||
index: number,
|
||||
parentAccountId: number,
|
||||
transactions: IGeneralLedgerSheetAccountTransaction[],
|
||||
opening: IGeneralLedgerSheetAccountBalance,
|
||||
closing: IGeneralLedgerSheetAccountBalance,
|
||||
openingBalance: IGeneralLedgerSheetAccountBalance,
|
||||
closingBalance: IGeneralLedgerSheetAccountBalance,
|
||||
}
|
||||
|
||||
export interface IAccountTransaction {
|
||||
|
||||
@@ -50,7 +50,7 @@ export interface IProfitLossSheetStatement {
|
||||
costOfSales: IProfitLossSheetAccountsSection,
|
||||
expenses: IProfitLossSheetAccountsSection,
|
||||
otherExpenses: IProfitLossSheetAccountsSection,
|
||||
|
||||
otherIncome: IProfitLossSheetAccountsSection,
|
||||
netIncome: IProfitLossSheetTotalSection;
|
||||
operatingProfit: IProfitLossSheetTotalSection;
|
||||
grossProfit: IProfitLossSheetTotalSection;
|
||||
|
||||
@@ -58,23 +58,23 @@ export default class AccountType extends TenantModel {
|
||||
static get labels() {
|
||||
return {
|
||||
inventory: 'Inventory',
|
||||
other_current_asset: 'Other current asset',
|
||||
bank: 'Bank account',
|
||||
other_current_asset: 'Other Current Asset',
|
||||
bank: 'Bank Account',
|
||||
cash: 'Cash',
|
||||
fixed_asset: 'Fixed asset',
|
||||
non_current_asset: 'Non-current asset',
|
||||
accounts_payable: 'Accounts payable (A/P)',
|
||||
accounts_receivable: 'Accounts receivable (A/R)',
|
||||
credit_card: 'Credit card',
|
||||
long_term_liability: 'Long term liability',
|
||||
other_current_liability: 'Other current liability',
|
||||
other_liability: 'Other liability',
|
||||
fixed_asset: 'Fixed Asset',
|
||||
non_current_asset: 'Non-Current Asset',
|
||||
accounts_payable: 'Accounts Payable (A/P)',
|
||||
accounts_receivable: 'Accounts Receivable (A/R)',
|
||||
credit_card: 'Credit Card',
|
||||
long_term_liability: 'Long Term Liability',
|
||||
other_current_liability: 'Other Current Liability',
|
||||
other_liability: 'Other Liability',
|
||||
equity: "Equity",
|
||||
expense: "Expense",
|
||||
income: "Income",
|
||||
other_income: "Other income",
|
||||
other_expense: "Other expense",
|
||||
cost_of_goods_sold: "Cost of goods sold (COGS)",
|
||||
other_income: "Other Income",
|
||||
other_expense: "Other Expense",
|
||||
cost_of_goods_sold: "Cost of Goods Sold (COGS)",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -278,6 +278,15 @@ function defaultToTransform(value, defaultOrTransformedValue, defaultValue) {
|
||||
: _transfromedValue;
|
||||
}
|
||||
|
||||
const transformToMap = (objects, key) => {
|
||||
const map = new Map();
|
||||
|
||||
objects.forEach(object => {
|
||||
map.set(object[key], object);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
export {
|
||||
hashPassword,
|
||||
origin,
|
||||
@@ -299,4 +308,5 @@ export {
|
||||
formatNumber,
|
||||
isBlank,
|
||||
defaultToTransform,
|
||||
transformToMap
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user