mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
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:
@@ -1,11 +1,17 @@
|
||||
{
|
||||
"restartable": "rs",
|
||||
"watch": [
|
||||
"src",
|
||||
".env"
|
||||
],
|
||||
"ext": "js,ts,json",
|
||||
"ignore": [
|
||||
".git",
|
||||
"src/**/*.spec.ts"
|
||||
],
|
||||
"exec": "ts-node --transpile-only ./src/server.ts"
|
||||
"execMap": {
|
||||
"ts": "node --inspect -r ts-node/register/transpile-only ./src/server.ts"
|
||||
},
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"start": "cross-env NODE_PATH=./src nodemon",
|
||||
"inspect": "cross-env NODE_PATH=./src nodemon --inspect src/server.ts"
|
||||
"inspect": "cross-env NODE_PATH=./src nodemon src/server.ts"
|
||||
},
|
||||
"author": "Ahmed Bouhuolia, <a.bouhuolia@gmail.com>",
|
||||
"license": "ISC",
|
||||
@@ -59,6 +59,7 @@
|
||||
"node-cache": "^4.2.1",
|
||||
"nodemailer": "^6.3.0",
|
||||
"nodemon": "^1.19.1",
|
||||
"object-hash": "^2.0.3",
|
||||
"objection": "^2.0.10",
|
||||
"objection-filter": "^4.0.1",
|
||||
"objection-soft-delete": "^1.0.7",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Response, Request, NextFunction } from 'express';
|
||||
import { matchedData, validationResult } from "express-validator";
|
||||
import { camelCase, snakeCase, omit } from "lodash";
|
||||
import { mapKeysDeep } from 'utils'
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
|
||||
export default class BaseController {
|
||||
|
||||
@@ -63,4 +64,8 @@ export default class BaseController {
|
||||
transfromToResponse(data: any) {
|
||||
return mapKeysDeep(data, (v, k) => snakeCase(k));
|
||||
}
|
||||
|
||||
asyncMiddleware(callback) {
|
||||
return asyncMiddleware(callback);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import express from 'express';
|
||||
|
||||
import BalanceSheetController from './FinancialStatements/BalanceSheet';
|
||||
import TrialBalanceSheetController from './FinancialStatements/TrialBalanceSheet';
|
||||
import GeneralLedgerController from './FinancialStatements/generalLedger';
|
||||
import JournalSheetController from './FinancialStatements/JournalSheet';
|
||||
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
||||
import ReceivableAgingSummary from './FinancialStatements/ReceivableAgingSummary';
|
||||
import PayableAgingSummary from './FinancialStatements/PayableAgingSummary';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/balance_sheet', BalanceSheetController.router());
|
||||
router.use('/profit_loss_sheet', ProfitLossController.router());
|
||||
router.use('/general_ledger', GeneralLedgerController.router());
|
||||
router.use('/trial_balance_sheet', TrialBalanceSheetController.router());
|
||||
router.use('/journal', JournalSheetController.router());
|
||||
router.use('/receivable_aging_summary', ReceivableAgingSummary.router());
|
||||
router.use('/payable_aging_summary', PayableAgingSummary.router());
|
||||
|
||||
return router;
|
||||
},
|
||||
};
|
||||
30
server/src/api/controllers/FinancialStatements.ts
Normal file
30
server/src/api/controllers/FinancialStatements.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Router } from 'express';
|
||||
import { Container, Service } from 'typedi';
|
||||
|
||||
import BalanceSheetController from './FinancialStatements/BalanceSheet';
|
||||
import TrialBalanceSheetController from './FinancialStatements/TrialBalanceSheet';
|
||||
import GeneralLedgerController from './FinancialStatements/GeneralLedger';
|
||||
import JournalSheetController from './FinancialStatements/JournalSheet';
|
||||
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
||||
import ReceivableAgingSummary from './FinancialStatements/ARAgingSummary';
|
||||
// import PayableAgingSummary from './FinancialStatements/PayableAgingSummary';
|
||||
|
||||
@Service()
|
||||
export default class FinancialStatementsService {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/balance_sheet', Container.get(BalanceSheetController).router());
|
||||
router.use('/profit_loss_sheet', Container.get(ProfitLossController).router());
|
||||
router.use('/general_ledger', Container.get(GeneralLedgerController).router());
|
||||
router.use('/trial_balance_sheet', Container.get(TrialBalanceSheetController).router());
|
||||
router.use('/journal', Container.get(JournalSheetController).router());
|
||||
router.use('/receivable_aging_summary', Container.get(ReceivableAgingSummary).router());
|
||||
// router.use('/payable_aging_summary', PayableAgingSummary.router());
|
||||
|
||||
return router;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { query, oneOf } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import ARAgingSummaryService from 'services/FinancialStatements/AgingSummary/ARAgingSummaryService';
|
||||
|
||||
@Service()
|
||||
export default class ARAgingSummaryReportController extends BaseController {
|
||||
@Inject()
|
||||
ARAgingSummaryService: ARAgingSummaryService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.receivableAgingSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receivable aging summary validation roles.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
query('as_date').optional().isISO8601(),
|
||||
query('aging_days_before').optional().isNumeric().toInt(),
|
||||
query('aging_periods').optional().isNumeric().toInt(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||
oneOf(
|
||||
[
|
||||
query('customer_ids').optional().isArray({ min: 1 }),
|
||||
query('customer_ids.*').isNumeric().toInt(),
|
||||
],
|
||||
[query('customer_ids').optional().isNumeric().toInt()]
|
||||
),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve receivable aging summary report.
|
||||
*/
|
||||
async receivableAgingSummary(req: Request, res: Response) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const {
|
||||
data,
|
||||
columns
|
||||
} = await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
import express from 'express';
|
||||
import { query, validationResult } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { pick, omit, sumBy } from 'lodash';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import { dateRangeCollection, itemsStartWith, getTotalDeep } from 'utils';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import { formatNumberClosure } from './FinancialStatementMixin';
|
||||
import BalanceSheetStructure from 'data/BalanceSheetStructure';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.balanceSheet.validation,
|
||||
asyncMiddleware(this.balanceSheet.handler)
|
||||
);
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet.
|
||||
*/
|
||||
balanceSheet: {
|
||||
validation: [
|
||||
query('accounting_method').optional().isIn(['cash', 'accural']),
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
query('display_columns_type').optional().isIn(['date_periods', 'total']),
|
||||
query('display_columns_by')
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
query('account_ids').isArray().optional(),
|
||||
query('account_ids.*').isNumeric().toInt(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { Account, AccountType } = req.models;
|
||||
|
||||
const filter = {
|
||||
display_columns_type: 'total',
|
||||
display_columns_by: '',
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
none_zero: false,
|
||||
basis: 'cash',
|
||||
account_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.account_ids)) {
|
||||
filter.account_ids = [filter.account_ids];
|
||||
}
|
||||
// Account balance formmatter based on the given query.
|
||||
const amountFormatter = formatNumberClosure(filter.number_format);
|
||||
const comparatorDateType =
|
||||
filter.display_columns_type === 'total'
|
||||
? 'day'
|
||||
: filter.display_columns_by;
|
||||
|
||||
const balanceSheetTypes = await AccountType.query().where(
|
||||
'balance_sheet',
|
||||
true
|
||||
);
|
||||
// Fetch all balance sheet accounts from the storage.
|
||||
const accounts = await Account.query()
|
||||
.whereIn(
|
||||
'account_type_id',
|
||||
balanceSheetTypes.map((a) => a.id)
|
||||
)
|
||||
.modify('filterAccounts', filter.account_ids)
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions')
|
||||
.modifyGraph('transactions', (builder) => {
|
||||
builder.modify('filterDateRange', null, filter.to_date);
|
||||
});
|
||||
// Accounts dependency graph.
|
||||
const accountsGraph = Account.toDependencyGraph(accounts);
|
||||
// Load all entries that associated to the given accounts.
|
||||
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
||||
const journalEntries = new JournalPoster(accountsGraph);
|
||||
journalEntries.loadEntries(journalEntriesCollected);
|
||||
|
||||
// Date range collection.
|
||||
const dateRangeSet =
|
||||
filter.display_columns_type === 'date_periods'
|
||||
? dateRangeCollection(
|
||||
filter.from_date,
|
||||
filter.to_date,
|
||||
comparatorDateType
|
||||
)
|
||||
: [];
|
||||
// Gets the date range set from start to end date.
|
||||
const getAccountTotalPeriods = (account) => ({
|
||||
total_periods: dateRangeSet.map((date) => {
|
||||
const amount = journalEntries.getAccountBalance(
|
||||
account.id,
|
||||
date,
|
||||
comparatorDateType
|
||||
);
|
||||
return {
|
||||
amount,
|
||||
date,
|
||||
formatted_amount: amountFormatter(amount),
|
||||
};
|
||||
}),
|
||||
});
|
||||
// Retrieve accounts total periods.
|
||||
const getAccountsTotalPeriods = (_accounts) =>
|
||||
Object.values(
|
||||
dateRangeSet.reduce((acc, date, index) => {
|
||||
const amount = sumBy(_accounts, `total_periods[${index}].amount`);
|
||||
acc[date] = {
|
||||
date,
|
||||
amount,
|
||||
formatted_amount: amountFormatter(amount),
|
||||
};
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
// Retrieve account total and total periods with account meta.
|
||||
const getAccountTotal = (account) => {
|
||||
const closingBalance = journalEntries.getAccountBalance(
|
||||
account.id,
|
||||
filter.to_date
|
||||
);
|
||||
const totalPeriods =
|
||||
(filter.display_columns_type === 'date_periods' &&
|
||||
getAccountTotalPeriods(account)) ||
|
||||
null;
|
||||
|
||||
return {
|
||||
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
|
||||
...(totalPeriods && { totalPeriods }),
|
||||
total: {
|
||||
amount: closingBalance,
|
||||
formatted_amount: amountFormatter(closingBalance),
|
||||
date: filter.to_date,
|
||||
},
|
||||
};
|
||||
};
|
||||
// Get accounts total of the given structure section
|
||||
const getAccountsSectionTotal = (_accounts) => {
|
||||
const total = getTotalDeep(_accounts, 'children', 'total.amount');
|
||||
return {
|
||||
total: {
|
||||
total,
|
||||
formatted_amount: amountFormatter(total),
|
||||
},
|
||||
};
|
||||
};
|
||||
// Strcuture accounts related mapper.
|
||||
const structureAccountsRelatedMapper = (accountsTypes) => {
|
||||
const filteredAccounts = accounts
|
||||
// Filter accounts that have no transaction when `none_zero` is on.
|
||||
.filter(
|
||||
(account) => account.transactions.length > 0 || !filter.none_zero
|
||||
)
|
||||
// Filter accounts that associated to the section accounts types.
|
||||
.filter(
|
||||
(account) => accountsTypes.indexOf(account.type.childType) !== -1
|
||||
)
|
||||
.map(getAccountTotal);
|
||||
// Gets total amount of the given accounts.
|
||||
const totalAmount = sumBy(filteredAccounts, 'total.amount');
|
||||
|
||||
return {
|
||||
children: Account.toNestedArray(filteredAccounts),
|
||||
total: {
|
||||
amount: totalAmount,
|
||||
formatted_amount: amountFormatter(totalAmount),
|
||||
},
|
||||
...(filter.display_columns_type === 'date_periods'
|
||||
? {
|
||||
total_periods: getAccountsTotalPeriods(filteredAccounts),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
// Structure section mapper.
|
||||
const structureSectionMapper = (structure) => {
|
||||
const result = {
|
||||
...omit(structure, itemsStartWith(Object.keys(structure), '_')),
|
||||
...(structure.children
|
||||
? {
|
||||
children: balanceSheetWalker(structure.children),
|
||||
}
|
||||
: {}),
|
||||
...(structure._accounts_types_related
|
||||
? {
|
||||
...structureAccountsRelatedMapper(
|
||||
structure._accounts_types_related
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
return {
|
||||
...result,
|
||||
...(!structure._accounts_types_related
|
||||
? getAccountsSectionTotal(result.children)
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
const balanceSheetWalker = (reportStructure) =>
|
||||
reportStructure.map(structureSectionMapper).filter(
|
||||
// Filter the structure sections that have no children.
|
||||
(structure) => structure.children.length > 0 || structure._forceShow
|
||||
);
|
||||
|
||||
// Response.
|
||||
return res.status(200).send({
|
||||
query: { ...filter },
|
||||
columns: { ...dateRangeSet },
|
||||
balance_sheet: [...balanceSheetWalker(BalanceSheetStructure)],
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { castArray } from 'lodash';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import BaseController from '../BaseController';
|
||||
import BalanceSheetStatementService from 'services/FinancialStatements/BalanceSheet/BalanceSheetService';
|
||||
|
||||
@Service()
|
||||
export default class BalanceSheetStatementController extends BaseController{
|
||||
@Inject()
|
||||
balanceSheetService: BalanceSheetStatementService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.balanceSheetValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.balanceSheet.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get balanceSheetValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('accounting_method')
|
||||
.optional()
|
||||
.isIn(['cash', 'accural']),
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
query('display_columns_type')
|
||||
.optional()
|
||||
.isIn(['date_periods', 'total']),
|
||||
query('display_columns_by')
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
query('number_format.no_cents')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
query('number_format.divide_1000')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
query('account_ids').isArray().optional(),
|
||||
query('account_ids.*').isNumeric().toInt(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet.
|
||||
*/
|
||||
async balanceSheet(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
filter = {
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
const organizationName = settings.get({ group: 'organization', key: 'name' });
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
|
||||
try {
|
||||
const {
|
||||
data,
|
||||
columns,
|
||||
query
|
||||
} = await this.balanceSheetService.balanceSheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
organization_name: organizationName,
|
||||
base_currency: baseCurrency,
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
})
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
|
||||
export const formatNumberClosure = (filter) => (balance) => {
|
||||
let formattedBalance = parseFloat(balance);
|
||||
|
||||
if (filter.no_cents) {
|
||||
formattedBalance = parseInt(formattedBalance, 10);
|
||||
}
|
||||
if (filter.divide_1000) {
|
||||
formattedBalance /= 1000;
|
||||
}
|
||||
return formattedBalance;
|
||||
};
|
||||
@@ -1,165 +0,0 @@
|
||||
import express from 'express';
|
||||
import { query, validationResult } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { pick, difference } from 'lodash';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import { formatNumberClosure } from './FinancialStatementMixin';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import DependencyGraph from 'lib/DependencyGraph';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/',
|
||||
this.generalLedger.validation,
|
||||
asyncMiddleware(this.generalLedger.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the general ledger financial statement.
|
||||
*/
|
||||
generalLedger: {
|
||||
validation: [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('basis').optional(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('accounts_ids').optional(),
|
||||
query('accounts_ids.*').isNumeric().toInt(),
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { AccountTransaction, Account } = req.models;
|
||||
|
||||
const filter = {
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
basis: 'cash',
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
none_zero: false,
|
||||
accounts_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.accounts_ids)) {
|
||||
filter.accounts_ids = [filter.accounts_ids];
|
||||
}
|
||||
filter.accounts_ids = filter.accounts_ids.map((id) => parseInt(id, 10));
|
||||
|
||||
const errorReasons = [];
|
||||
|
||||
if (filter.accounts_ids.length > 0) {
|
||||
const accounts = await Account.query().whereIn('id', filter.accounts_ids);
|
||||
const accountsIds = accounts.map((a) => a.id);
|
||||
|
||||
if (difference(filter.accounts_ids, accountsIds).length > 0) {
|
||||
errorReasons.push({ type: 'FILTER.ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||
}
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ error: errorReasons });
|
||||
}
|
||||
const accounts = await Account.query()
|
||||
// .remember('general_ledger_accounts')
|
||||
.orderBy('index', 'DESC')
|
||||
.modify('filterAccounts', filter.accounts_ids)
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions')
|
||||
.modifyGraph('transactions', (builder) => {
|
||||
builder.modify('filterDateRange', filter.from_date, filter.to_date);
|
||||
});
|
||||
|
||||
// Accounts dependency graph.
|
||||
const accountsGraph = DependencyGraph.fromArray(
|
||||
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
|
||||
);
|
||||
|
||||
const openingBalanceTransactions = await AccountTransaction.query()
|
||||
// .remember()
|
||||
.modify('filterDateRange', null, filter.from_date)
|
||||
.modify('sumationCreditDebit')
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
const closingBalanceTransactions = await AccountTransaction.query()
|
||||
// .remember()
|
||||
.modify('filterDateRange', null, filter.to_date)
|
||||
.modify('sumationCreditDebit')
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
const opeingBalanceCollection = new JournalPoster(accountsGraph);
|
||||
const closingBalanceCollection = new JournalPoster(accountsGraph);
|
||||
|
||||
opeingBalanceCollection.loadEntries(openingBalanceTransactions);
|
||||
closingBalanceCollection.loadEntries(closingBalanceTransactions);
|
||||
|
||||
// Transaction amount formatter based on the given query.
|
||||
const formatNumber = formatNumberClosure(filter.number_format);
|
||||
|
||||
const accountsResponse = accounts
|
||||
.filter((account) => (
|
||||
account.transactions.length > 0 || !filter.none_zero
|
||||
))
|
||||
.map((account) => ({
|
||||
...pick(account, ['id', 'name', 'code', 'index', 'parentAccountId']),
|
||||
transactions: [
|
||||
...account.transactions.map((transaction) => {
|
||||
let amount = 0;
|
||||
|
||||
if (account.type.normal === 'credit') {
|
||||
amount += transaction.credit - transaction.debit;
|
||||
} else if (account.type.normal === 'debit') {
|
||||
amount += transaction.debit - transaction.credit;
|
||||
}
|
||||
return {
|
||||
...pick(transaction, ['id', 'note', 'transactionType', 'referenceType',
|
||||
'referenceId', 'date', 'createdAt']),
|
||||
amount,
|
||||
formatted_amount: formatNumber(amount),
|
||||
};
|
||||
}),
|
||||
],
|
||||
opening: (() => {
|
||||
const openingAmount = opeingBalanceCollection.getAccountBalance(account.id);
|
||||
|
||||
return {
|
||||
date: filter.from_date,
|
||||
amount: openingAmount,
|
||||
formatted_amount: formatNumber(openingAmount),
|
||||
}
|
||||
})(),
|
||||
closing: (() => {
|
||||
const closingAmount = closingBalanceCollection.getAccountBalance(account.id);
|
||||
|
||||
return {
|
||||
date: filter.to_date,
|
||||
amount: closingAmount,
|
||||
formatted_amount: formatNumber(closingAmount),
|
||||
}
|
||||
})(),
|
||||
}));
|
||||
|
||||
return res.status(200).send({
|
||||
query: { ...filter },
|
||||
accounts: Account.toNestedArray(accountsResponse),
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import BaseController from '../BaseController';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import GeneralLedgerService from 'services/FinancialStatements/GeneralLedger/GeneralLedgerService';
|
||||
|
||||
@Service()
|
||||
export default class GeneralLedgerReportController extends BaseController{
|
||||
|
||||
@Inject()
|
||||
generalLedgetService: GeneralLedgerService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/',
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.generalLedger.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('basis').optional(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
query('accounts_ids').optional(),
|
||||
query('accounts_ids.*').isNumeric().toInt(),
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the general ledger financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async generalLedger(req: Request, res: Response) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
const organizationName = settings.get({ group: 'organization', key: 'name' });
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
|
||||
try {
|
||||
const {
|
||||
data,
|
||||
query,
|
||||
} = await this.generalLedgetService.generalLedger(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
organization_name: organizationName,
|
||||
base_currency: baseCurrency,
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import express from 'express';
|
||||
import { query, oneOf, validationResult } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { groupBy } from 'lodash';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import { formatNumberClosure } from './FinancialStatementMixin';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/',
|
||||
this.journal.validation,
|
||||
asyncMiddleware(this.journal.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the ledger report of the given account.
|
||||
*/
|
||||
journal: {
|
||||
validation: [
|
||||
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(),
|
||||
]),
|
||||
query('from_range').optional().isNumeric().toInt(),
|
||||
query('to_range').optional().isNumeric().toInt(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { AccountTransaction } = req.models;
|
||||
|
||||
const filter = {
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
from_range: null,
|
||||
to_range: null,
|
||||
account_ids: [],
|
||||
transaction_types: [],
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.transaction_types)) {
|
||||
filter.transaction_types = [filter.transaction_types];
|
||||
}
|
||||
if (!Array.isArray(filter.account_ids)) {
|
||||
filter.account_ids = [filter.account_ids];
|
||||
}
|
||||
filter.account_ids = filter.account_ids.map((id) => parseInt(id, 10));
|
||||
|
||||
const accountsJournalEntries = await AccountTransaction.query()
|
||||
// .remember()
|
||||
.modify('filterDateRange', filter.from_date, filter.to_date)
|
||||
.modify('filterAccounts', filter.account_ids)
|
||||
.modify('filterTransactionTypes', filter.transaction_types)
|
||||
.modify('filterAmountRange', filter.from_range, filter.to_range)
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
const formatNumber = formatNumberClosure(filter.number_format);
|
||||
|
||||
const journalGrouped = groupBy(accountsJournalEntries,
|
||||
(entry) => `${entry.referenceId}-${entry.referenceType}`);
|
||||
|
||||
const journal = Object.keys(journalGrouped).map((key) => {
|
||||
const transactionsGroup = journalGrouped[key];
|
||||
|
||||
const journalPoster = new JournalPoster();
|
||||
journalPoster.loadEntries(transactionsGroup);
|
||||
|
||||
const trialBalance = journalPoster.getTrialBalance();
|
||||
|
||||
return {
|
||||
id: key,
|
||||
entries: transactionsGroup,
|
||||
|
||||
credit: trialBalance.credit,
|
||||
debit: trialBalance.debit,
|
||||
|
||||
formatted_credit: formatNumber(trialBalance.credit),
|
||||
formatted_debit: formatNumber(trialBalance.debit),
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
query: { ...filter },
|
||||
journal,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Request, Response, Router } from 'express';
|
||||
import { castArray } from 'lodash';
|
||||
import { query, oneOf } from 'express-validator';
|
||||
import JournalSheetService from 'services/FinancialStatements/JournalSheet/JournalSheetService';
|
||||
import BaseController from '../BaseController';
|
||||
|
||||
@Service()
|
||||
export default class JournalSheetController extends BaseController {
|
||||
@Inject()
|
||||
journalService: JournalSheetService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/',
|
||||
this.journalValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.journal.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get journalValidationSchema() {
|
||||
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(),
|
||||
]),
|
||||
query('from_range').optional().isNumeric().toInt(),
|
||||
query('to_range').optional().isNumeric().toInt(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the ledger report of the given account.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async journal(req: Request, res: Response) {
|
||||
const { tenantId, settings } = req;
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
filter = {
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
const organizationName = settings.get({ group: 'organization', key: 'name' });
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
|
||||
try {
|
||||
const data = await this.journalService.journalSheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
organization_name: organizationName,
|
||||
base_currency: baseCurrency,
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
import express from 'express';
|
||||
import { query, oneOf, validationResult } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { pick, sumBy } from 'lodash';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import { dateRangeCollection } from 'utils';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import { formatNumberClosure } from './FinancialStatementMixin';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.profitLossSheet.validation,
|
||||
asyncMiddleware(this.profitLossSheet.handler)
|
||||
);
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve profit/loss financial statement.
|
||||
*/
|
||||
profitLossSheet: {
|
||||
validation: [
|
||||
query('basis').optional(),
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('number_format.no_cents').optional().isBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean(),
|
||||
query('basis').optional(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('account_ids').isArray().optional(),
|
||||
query('account_ids.*').isNumeric().toInt(),
|
||||
query('display_columns_type').optional().isIn(['total', 'date_periods']),
|
||||
query('display_columns_by')
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { Account, AccountType } = req.models;
|
||||
const filter = {
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
basis: 'accural',
|
||||
none_zero: false,
|
||||
display_columns_type: 'total',
|
||||
display_columns_by: 'month',
|
||||
account_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.account_ids)) {
|
||||
filter.account_ids = [filter.account_ids];
|
||||
}
|
||||
const incomeStatementTypes = await AccountType.query().where(
|
||||
'income_sheet',
|
||||
true
|
||||
);
|
||||
// Fetch all income accounts from storage.
|
||||
const accounts = await Account.query()
|
||||
// .remember('profit_loss_accounts')
|
||||
.modify('filterAccounts', filter.account_ids)
|
||||
.whereIn(
|
||||
'account_type_id',
|
||||
incomeStatementTypes.map((t) => t.id)
|
||||
)
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions');
|
||||
|
||||
// Accounts dependency graph.
|
||||
const accountsGraph = Account.toDependencyGraph(accounts);
|
||||
|
||||
// Filter all none zero accounts if it was enabled.
|
||||
const filteredAccounts = accounts.filter(
|
||||
(account) => account.transactions.length > 0 || !filter.none_zero
|
||||
);
|
||||
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
||||
const journalEntries = new JournalPoster(accountsGraph);
|
||||
journalEntries.loadEntries(journalEntriesCollected);
|
||||
|
||||
// Account balance formmatter based on the given query.
|
||||
const numberFormatter = formatNumberClosure(filter.number_format);
|
||||
const comparatorDateType =
|
||||
filter.display_columns_type === 'total'
|
||||
? 'day'
|
||||
: filter.display_columns_by;
|
||||
// Gets the date range set from start to end date.
|
||||
const dateRangeSet = dateRangeCollection(
|
||||
filter.from_date,
|
||||
filter.to_date,
|
||||
comparatorDateType
|
||||
);
|
||||
const accountsMapper = (incomeExpenseAccounts) =>
|
||||
incomeExpenseAccounts.map((account) => ({
|
||||
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
|
||||
// Total closing balance of the account.
|
||||
...(filter.display_columns_type === 'total' && {
|
||||
total: (() => {
|
||||
const amount = journalEntries.getAccountBalance(
|
||||
account.id,
|
||||
filter.to_date
|
||||
);
|
||||
return {
|
||||
amount,
|
||||
date: filter.to_date,
|
||||
formatted_amount: numberFormatter(amount),
|
||||
};
|
||||
})(),
|
||||
}),
|
||||
// Date periods when display columns type `periods`.
|
||||
...(filter.display_columns_type === 'date_periods' && {
|
||||
periods: dateRangeSet.map((date) => {
|
||||
const type = comparatorDateType;
|
||||
const amount = journalEntries.getAccountBalance(
|
||||
account.id,
|
||||
date,
|
||||
type
|
||||
);
|
||||
return {
|
||||
date,
|
||||
amount,
|
||||
formatted_amount: numberFormatter(amount),
|
||||
};
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
const accountsIncome = Account.toNestedArray(
|
||||
accountsMapper(
|
||||
filteredAccounts.filter((account) => account.type.normal === 'credit')
|
||||
)
|
||||
);
|
||||
const accountsExpenses = Account.toNestedArray(
|
||||
accountsMapper(
|
||||
filteredAccounts.filter((account) => account.type.normal === 'debit')
|
||||
)
|
||||
);
|
||||
const totalPeriodsMapper = (incomeExpenseAccounts) =>
|
||||
Object.values(
|
||||
dateRangeSet.reduce((acc, date, index) => {
|
||||
let amount = sumBy(
|
||||
incomeExpenseAccounts,
|
||||
`periods[${index}].amount`
|
||||
);
|
||||
acc[date] = {
|
||||
date,
|
||||
amount,
|
||||
formatted_amount: numberFormatter(amount),
|
||||
};
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
// Total income - Total expenses = Net income
|
||||
const netIncomePeriodsMapper = (
|
||||
totalIncomeAcocunts,
|
||||
totalExpenseAccounts
|
||||
) =>
|
||||
dateRangeSet.map((date, index) => {
|
||||
const totalIncome = totalIncomeAcocunts[index];
|
||||
const totalExpenses = totalExpenseAccounts[index];
|
||||
|
||||
let amount = totalIncome.amount || 0;
|
||||
amount -= totalExpenses.amount || 0;
|
||||
return { date, amount, formatted_amount: numberFormatter(amount) };
|
||||
});
|
||||
|
||||
// @return {Object}
|
||||
const netIncomeTotal = (totalIncome, totalExpenses) => {
|
||||
const netIncomeAmount = totalIncome.amount - totalExpenses.amount;
|
||||
return {
|
||||
amount: netIncomeAmount,
|
||||
formatted_amount: netIncomeAmount,
|
||||
date: filter.to_date,
|
||||
};
|
||||
};
|
||||
|
||||
const incomeResponse = {
|
||||
entry_normal: 'credit',
|
||||
accounts: accountsIncome,
|
||||
...(filter.display_columns_type === 'total' &&
|
||||
(() => {
|
||||
const totalIncomeAccounts = sumBy(accountsIncome, 'total.amount');
|
||||
return {
|
||||
total: {
|
||||
amount: totalIncomeAccounts,
|
||||
date: filter.to_date,
|
||||
formatted_amount: numberFormatter(totalIncomeAccounts),
|
||||
},
|
||||
};
|
||||
})()),
|
||||
...(filter.display_columns_type === 'date_periods' && {
|
||||
total_periods: [...totalPeriodsMapper(accountsIncome)],
|
||||
}),
|
||||
};
|
||||
const expenseResponse = {
|
||||
entry_normal: 'debit',
|
||||
accounts: accountsExpenses,
|
||||
...(filter.display_columns_type === 'total' &&
|
||||
(() => {
|
||||
const totalExpensesAccounts = sumBy(
|
||||
accountsExpenses,
|
||||
'total.amount'
|
||||
);
|
||||
return {
|
||||
total: {
|
||||
amount: totalExpensesAccounts,
|
||||
date: filter.to_date,
|
||||
formatted_amount: numberFormatter(totalExpensesAccounts),
|
||||
},
|
||||
};
|
||||
})()),
|
||||
...(filter.display_columns_type === 'date_periods' && {
|
||||
total_periods: [...totalPeriodsMapper(accountsExpenses)],
|
||||
}),
|
||||
};
|
||||
const netIncomeResponse = {
|
||||
...(filter.display_columns_type === 'total' && {
|
||||
total: {
|
||||
...netIncomeTotal(incomeResponse.total, expenseResponse.total),
|
||||
},
|
||||
}),
|
||||
...(filter.display_columns_type === 'date_periods' && {
|
||||
total_periods: [
|
||||
...netIncomePeriodsMapper(
|
||||
incomeResponse.total_periods,
|
||||
expenseResponse.total_periods
|
||||
),
|
||||
],
|
||||
}),
|
||||
};
|
||||
return res.status(200).send({
|
||||
query: { ...filter },
|
||||
columns: [...dateRangeSet],
|
||||
profitLoss: {
|
||||
income: incomeResponse,
|
||||
expenses: expenseResponse,
|
||||
net_income: netIncomeResponse,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import ProfitLossSheetService from 'services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService';
|
||||
|
||||
@Service()
|
||||
export default class ProfitLossSheetController extends BaseController {
|
||||
@Inject()
|
||||
profitLossSheetService: ProfitLossSheetService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.profitLossSheet.bind(this)),
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('basis').optional(),
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('number_format.no_cents').optional().isBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean(),
|
||||
query('basis').optional(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
query('accounts_ids').isArray().optional(),
|
||||
query('accounts_ids.*').isNumeric().toInt(),
|
||||
query('display_columns_type').optional().isIn(['total', 'date_periods']),
|
||||
query('display_columns_by')
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve profit/loss financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async profitLossSheet(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
const organizationName = settings.get({ group: 'organization', key: 'name' });
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
|
||||
try {
|
||||
const {
|
||||
data,
|
||||
columns,
|
||||
query,
|
||||
} = await this.profitLossSheetService.profitLossSheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
organization_name: organizationName,
|
||||
base_currency: baseCurrency,
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import express from 'express';
|
||||
import { query, oneOf } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import AgingReport from 'api/controllers/FinancialStatements/AgingReport';
|
||||
import moment from 'moment';
|
||||
|
||||
export default class ReceivableAgingSummary extends AgingReport {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
static router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.receivableAgingSummaryRoles,
|
||||
this.validateResults,
|
||||
asyncMiddleware(this.validateCustomersIds.bind(this)),
|
||||
asyncMiddleware(this.receivableAgingSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the report customers ids query.
|
||||
*/
|
||||
static async validateCustomersIds(req, res, next) {
|
||||
const { Customer } = req.models;
|
||||
|
||||
const filter = {
|
||||
customer_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.customer_ids)) {
|
||||
filter.customer_ids = [filter.customer_ids];
|
||||
}
|
||||
if (filter.customer_ids.length > 0) {
|
||||
const storedCustomers = await Customer.query().whereIn(
|
||||
'id',
|
||||
filter.customer_ids
|
||||
);
|
||||
const storedCustomersIds = storedCustomers.map((c) => parseInt(c.id, 10));
|
||||
const notStoredCustomersIds = difference(
|
||||
filter.customer_ids.map(a => parseInt(a, 10)),
|
||||
storedCustomersIds
|
||||
);
|
||||
|
||||
if (notStoredCustomersIds.length) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'CUSTOMERS.IDS.NOT.FOUND',
|
||||
code: 300,
|
||||
ids: notStoredCustomersIds,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receivable aging summary validation roles.
|
||||
*/
|
||||
static get receivableAgingSummaryRoles() {
|
||||
return [
|
||||
query('as_date').optional().isISO8601(),
|
||||
query('aging_days_before').optional().isNumeric().toInt(),
|
||||
query('aging_periods').optional().isNumeric().toInt(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||
oneOf(
|
||||
[
|
||||
query('customer_ids').optional().isArray({ min: 1 }),
|
||||
query('customer_ids.*').isNumeric().toInt(),
|
||||
],
|
||||
[query('customer_ids').optional().isNumeric().toInt()]
|
||||
),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve receivable aging summary report.
|
||||
*/
|
||||
static async receivableAgingSummary(req, res) {
|
||||
const { Customer, Account, AccountTransaction, AccountType } = req.models;
|
||||
|
||||
const filter = {
|
||||
as_date: moment().format('YYYY-MM-DD'),
|
||||
aging_days_before: 30,
|
||||
aging_periods: 3,
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
customer_ids: [],
|
||||
none_zero: false,
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.customer_ids)) {
|
||||
filter.customer_ids = [filter.customer_ids];
|
||||
}
|
||||
|
||||
const storedCustomers = await Customer.query().onBuild((builder) => {
|
||||
if (filter.customer_ids.length > 0) {
|
||||
builder.modify('filterCustomerIds', filter.customer_ids);
|
||||
}
|
||||
return builder;
|
||||
});
|
||||
|
||||
const accountsReceivableType = await AccountType.query()
|
||||
.where('key', 'accounts_receivable')
|
||||
.first();
|
||||
|
||||
const accountsReceivable = await Account.query()
|
||||
.where('account_type_id', accountsReceivableType.id)
|
||||
.remember()
|
||||
.first();
|
||||
|
||||
const transactions = await AccountTransaction.query().onBuild((query) => {
|
||||
query.modify('filterDateRange', null, filter.as_date)
|
||||
query.where('account_id', accountsReceivable.id)
|
||||
query.modify('filterContactType', 'customer');
|
||||
|
||||
if (filter.customer_ids.length> 0) {
|
||||
query.modify('filterContactIds', filter.customer_ids)
|
||||
}
|
||||
query.remember();
|
||||
return query;
|
||||
});
|
||||
|
||||
const journalPoster = new JournalPoster();
|
||||
journalPoster.loadEntries(transactions);
|
||||
|
||||
const agingPeriods = this.agingRangePeriods(
|
||||
filter.as_date,
|
||||
filter.aging_days_before,
|
||||
filter.aging_periods
|
||||
);
|
||||
// Total amount formmatter based on the given query.
|
||||
const totalFormatter = this.formatNumberClosure(filter.number_format);
|
||||
|
||||
const customers = storedCustomers.map((customer) => {
|
||||
// Calculate the trial balance total of the given customer.
|
||||
const customerBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
customer.id,
|
||||
'customer'
|
||||
);
|
||||
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||
// Calculate the trial balance between the given date period.
|
||||
const agingTrialBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
customer.id,
|
||||
'customer',
|
||||
agingPeriod.from_period
|
||||
);
|
||||
return {
|
||||
...agingPeriod,
|
||||
closingBalance: agingTrialBalance.debit,
|
||||
};
|
||||
});
|
||||
const aging = this.contactAgingBalance(
|
||||
agingClosingBalance,
|
||||
customerBalance.credit
|
||||
);
|
||||
return {
|
||||
customer_name: customer.displayName,
|
||||
aging: aging.map((item) => ({
|
||||
...item,
|
||||
formatted_total: totalFormatter(item.total),
|
||||
})),
|
||||
total: customerBalance.balance,
|
||||
formatted_total: totalFormatter(customerBalance.balance),
|
||||
};
|
||||
});
|
||||
|
||||
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||
const closingTrialBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
null,
|
||||
'customer',
|
||||
agingPeriod.from_period
|
||||
);
|
||||
return {
|
||||
...agingPeriod,
|
||||
closingBalance: closingTrialBalance.balance,
|
||||
};
|
||||
});
|
||||
|
||||
const totalClosingBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
null,
|
||||
'customer'
|
||||
);
|
||||
const agingTotal = this.contactAgingBalance(
|
||||
agingClosingBalance,
|
||||
totalClosingBalance.credit
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
columns: [...agingPeriods],
|
||||
aging: {
|
||||
customers,
|
||||
total: [
|
||||
...agingTotal.map((item) => ({
|
||||
...item,
|
||||
formatted_total: totalFormatter(item.total),
|
||||
})),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import express from 'express';
|
||||
import { query, validationResult } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import JournalPoster from 'services/Accounting/JournalPoster';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import DependencyGraph from 'lib/DependencyGraph';
|
||||
import { formatNumberClosure }from './FinancialStatementMixin';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/',
|
||||
this.trialBalanceSheet.validation,
|
||||
asyncMiddleware(this.trialBalanceSheet.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the trial balance sheet.
|
||||
*/
|
||||
trialBalanceSheet: {
|
||||
validation: [
|
||||
query('basis').optional(),
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||
query('account_ids').isArray().optional(),
|
||||
query('account_ids.*').isNumeric().toInt(),
|
||||
query('basis').optional(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { Account } = req.models;
|
||||
const filter = {
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
basis: 'accural',
|
||||
none_zero: false,
|
||||
account_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.account_ids)) {
|
||||
filter.account_ids = [filter.account_ids];
|
||||
}
|
||||
|
||||
const accounts = await Account.query()
|
||||
// .remember('trial_balance_accounts')
|
||||
.modify('filterAccounts', filter.account_ids)
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions')
|
||||
.modifyGraph('transactions', (builder) => {
|
||||
builder.modify('sumationCreditDebit');
|
||||
builder.modify('filterDateRange', filter.from_date, filter.to_date);
|
||||
});
|
||||
|
||||
// Accounts dependency graph.
|
||||
const accountsGraph = DependencyGraph.fromArray(
|
||||
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
|
||||
);
|
||||
|
||||
const journalEntriesCollect = Account.collectJournalEntries(accounts);
|
||||
const journalEntries = new JournalPoster(accountsGraph);
|
||||
journalEntries.loadEntries(journalEntriesCollect);
|
||||
|
||||
// Account balance formmatter based on the given query.
|
||||
const balanceFormatter = formatNumberClosure(filter.number_format);
|
||||
|
||||
const accountsResponse = accounts
|
||||
.filter((account) => (
|
||||
account.transactions.length > 0 || !filter.none_zero
|
||||
))
|
||||
.map((account) => {
|
||||
const trial = journalEntries.getTrialBalanceWithDepands(account.id);
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
parentAccountId: account.parentAccountId,
|
||||
name: account.name,
|
||||
code: account.code,
|
||||
accountNormal: account.type.normal,
|
||||
|
||||
credit: trial.credit,
|
||||
debit: trial.debit,
|
||||
balance: trial.balance,
|
||||
|
||||
formatted_credit: balanceFormatter(trial.credit),
|
||||
formatted_debit: balanceFormatter(trial.debit),
|
||||
formatted_balance: balanceFormatter(trial.balance),
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
query: { ...filter },
|
||||
accounts: [...Account.toNestedArray(accountsResponse) ],
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
import BaseController from '../BaseController';
|
||||
import TrialBalanceSheetService from 'services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService';
|
||||
import { castArray } from 'lodash';
|
||||
|
||||
@Service()
|
||||
export default class TrialBalanceSheetController extends BaseController {
|
||||
@Inject()
|
||||
trialBalanceSheetService: TrialBalanceSheetService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/',
|
||||
this.trialBalanceSheetValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.trialBalanceSheet.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
get trialBalanceSheetValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('basis').optional(),
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||
query('account_ids').isArray().optional(),
|
||||
query('account_ids.*').isNumeric().toInt(),
|
||||
query('basis').optional(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the trial balance sheet.
|
||||
*/
|
||||
public async trialBalanceSheet(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
filter = {
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
const organizationName = settings.get({ group: 'organization', key: 'name' });
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
|
||||
try {
|
||||
const { data, query } = await this.trialBalanceSheetService
|
||||
.trialBalanceSheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
organization_name: organizationName,
|
||||
base_currency: baseCurrency,
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query)
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,7 +338,11 @@ export default class ManualJournalsController extends BaseController {
|
||||
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
|
||||
}
|
||||
try {
|
||||
const { manualJournals, pagination, filterMeta } = await this.manualJournalsService.getManualJournals(tenantId, filter);
|
||||
const {
|
||||
manualJournals,
|
||||
pagination,
|
||||
filterMeta
|
||||
} = await this.manualJournalsService.getManualJournals(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
manual_journals: manualJournals,
|
||||
|
||||
@@ -91,7 +91,7 @@ export default () => {
|
||||
dashboard.use('/items', Container.get(Items).router());
|
||||
dashboard.use('/item_categories', Container.get(ItemCategories).router());
|
||||
dashboard.use('/expenses', Container.get(Expenses).router());
|
||||
dashboard.use('/financial_statements', FinancialStatements.router());
|
||||
dashboard.use('/financial_statements', Container.get(FinancialStatements).router());
|
||||
dashboard.use('/customers', Container.get(Customers).router());
|
||||
dashboard.use('/vendors', Container.get(Vendors).router());
|
||||
dashboard.use('/sales', Container.get(Sales).router());
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
|
||||
export default [
|
||||
{
|
||||
name: 'Assets',
|
||||
section_type: 'assets',
|
||||
type: 'section',
|
||||
children: [
|
||||
{
|
||||
name: 'Current Asset',
|
||||
type: 'section',
|
||||
_accounts_types_related: ['current_asset'],
|
||||
},
|
||||
{
|
||||
name: 'Fixed Asset',
|
||||
type: 'section',
|
||||
_accounts_types_related: ['fixed_asset'],
|
||||
},
|
||||
{
|
||||
name: 'Other Asset',
|
||||
type: 'section',
|
||||
_accounts_types_related: ['other_asset'],
|
||||
},
|
||||
],
|
||||
_forceShow: true,
|
||||
},
|
||||
{
|
||||
name: 'Liabilities and Equity',
|
||||
section_type: 'liabilities_equity',
|
||||
type: 'section',
|
||||
children: [
|
||||
{
|
||||
name: 'Liabilities',
|
||||
section_type: 'liability',
|
||||
type: 'section',
|
||||
children: [
|
||||
{
|
||||
name: 'Current Liability',
|
||||
type: 'section',
|
||||
_accounts_types_related: ['current_liability'],
|
||||
},
|
||||
{
|
||||
name: 'Long Term Liability',
|
||||
type: 'section',
|
||||
_accounts_types_related: ['long_term_liability'],
|
||||
},
|
||||
{
|
||||
name: 'Other Liability',
|
||||
type: 'section',
|
||||
_accounts_types_related: ['other_liability'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Equity',
|
||||
section_type: 'equity',
|
||||
type: 'section',
|
||||
_accounts_types_related: ['equity'],
|
||||
},
|
||||
],
|
||||
_forceShow: true,
|
||||
},
|
||||
];
|
||||
65
server/src/data/BalanceSheetStructure.ts
Normal file
65
server/src/data/BalanceSheetStructure.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { IBalanceSheetStructureSection } from 'interfaces';
|
||||
|
||||
const balanceSheetStructure: IBalanceSheetStructureSection[] = [
|
||||
{
|
||||
name: 'Assets',
|
||||
sectionType: 'assets',
|
||||
type: 'section',
|
||||
children: [
|
||||
{
|
||||
name: 'Current Asset',
|
||||
type: 'accounts_section',
|
||||
_accountsTypesRelated: ['current_asset'],
|
||||
},
|
||||
{
|
||||
name: 'Fixed Asset',
|
||||
type: 'accounts_section',
|
||||
_accountsTypesRelated: ['fixed_asset'],
|
||||
},
|
||||
{
|
||||
name: 'Other Asset',
|
||||
type: 'accounts_section',
|
||||
_accountsTypesRelated: ['other_asset'],
|
||||
},
|
||||
],
|
||||
_forceShow: true,
|
||||
},
|
||||
{
|
||||
name: 'Liabilities and Equity',
|
||||
sectionType: 'liabilities_equity',
|
||||
type: 'section',
|
||||
children: [
|
||||
{
|
||||
name: 'Liabilities',
|
||||
sectionType: 'liability',
|
||||
type: 'section',
|
||||
children: [
|
||||
{
|
||||
name: 'Current Liability',
|
||||
type: 'accounts_section',
|
||||
_accountsTypesRelated: ['current_liability'],
|
||||
},
|
||||
{
|
||||
name: 'Long Term Liability',
|
||||
type: 'accounts_section',
|
||||
_accountsTypesRelated: ['long_term_liability'],
|
||||
},
|
||||
{
|
||||
name: 'Other Liability',
|
||||
type: 'accounts_section',
|
||||
_accountsTypesRelated: ['other_liability'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Equity',
|
||||
sectionType: 'equity',
|
||||
type: 'accounts_section',
|
||||
_accountsTypesRelated: ['equity'],
|
||||
},
|
||||
],
|
||||
_forceShow: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default balanceSheetStructure;
|
||||
@@ -111,8 +111,8 @@ exports.up = function (knex) {
|
||||
normal: 'debit',
|
||||
root_type: 'expenses',
|
||||
child_type: 'expenses',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
|
||||
45
server/src/interfaces/ARAgingSummaryReport.ts
Normal file
45
server/src/interfaces/ARAgingSummaryReport.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
|
||||
|
||||
export interface IARAgingSummaryQuery {
|
||||
asDate: Date | string,
|
||||
agingDaysBefore: number,
|
||||
agingPeriods: number,
|
||||
numberFormat: {
|
||||
noCents: number,
|
||||
divideOn1000: number,
|
||||
},
|
||||
customersIds: number[],
|
||||
noneZero: boolean,
|
||||
}
|
||||
|
||||
export interface IAgingPeriod {
|
||||
fromPeriod: Date,
|
||||
toPeriod: Date,
|
||||
beforeDays: number,
|
||||
toDays: number,
|
||||
};
|
||||
|
||||
export interface IAgingPeriodClosingBalance extends IAgingPeriod {
|
||||
closingBalance: number,
|
||||
};
|
||||
|
||||
export interface IAgingPeriodTotal extends IAgingPeriod {
|
||||
total: number,
|
||||
};
|
||||
|
||||
export interface ARAgingSummaryCustomerPeriod {
|
||||
|
||||
}
|
||||
|
||||
export interface ARAgingSummaryCustomerTotal {
|
||||
amount: number,
|
||||
formattedAmount: string,
|
||||
currencyCode: string,
|
||||
}
|
||||
|
||||
export interface ARAgingSummaryCustomer {
|
||||
customerName: string,
|
||||
aging: IAgingPeriodTotal[],
|
||||
total: ARAgingSummaryCustomerTotal,
|
||||
};
|
||||
@@ -10,9 +10,11 @@ export interface IAccountDTO {
|
||||
};
|
||||
|
||||
export interface IAccount {
|
||||
id: number,
|
||||
name: string,
|
||||
slug: string,
|
||||
code: string,
|
||||
index: number,
|
||||
description: string,
|
||||
accountTypeId: number,
|
||||
parentAccountId: number,
|
||||
@@ -20,6 +22,8 @@ export interface IAccount {
|
||||
predefined: boolean,
|
||||
amount: number,
|
||||
currencyCode: string,
|
||||
transactions?: any[],
|
||||
type?: any[],
|
||||
};
|
||||
|
||||
export interface IAccountsFilter extends IDynamicListFilterDTO {
|
||||
|
||||
74
server/src/interfaces/BalanceSheet.ts
Normal file
74
server/src/interfaces/BalanceSheet.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
|
||||
export interface IBalanceSheetQuery{
|
||||
displayColumnsType: 'total' | 'date_periods',
|
||||
displayColumnsBy: string,
|
||||
fromDate: Date|string,
|
||||
toDate: Date|string,
|
||||
numberFormat: {
|
||||
noCents: boolean,
|
||||
divideOn1000: boolean,
|
||||
},
|
||||
noneZero: boolean,
|
||||
noneTransactions: boolean,
|
||||
basis: 'cash' | 'accural',
|
||||
accountIds: number[],
|
||||
}
|
||||
|
||||
export interface IBalanceSheetStatementService {
|
||||
balanceSheet(tenantId: number, query: IBalanceSheetQuery): Promise<IBalanceSheetStatement>;
|
||||
}
|
||||
|
||||
export interface IBalanceSheetStatementColumns {
|
||||
|
||||
}
|
||||
|
||||
export interface IBalanceSheetStatementData {
|
||||
|
||||
}
|
||||
|
||||
export interface IBalanceSheetStatement {
|
||||
query: IBalanceSheetQuery,
|
||||
columns: IBalanceSheetStatementColumns,
|
||||
data: IBalanceSheetStatementData,
|
||||
}
|
||||
|
||||
export interface IBalanceSheetStructureSection {
|
||||
name: string,
|
||||
sectionType?: string,
|
||||
type: 'section' | 'accounts_section',
|
||||
children?: IBalanceSheetStructureSection[],
|
||||
_accountsTypesRelated?: string[],
|
||||
_forceShow?: boolean,
|
||||
}
|
||||
|
||||
export interface IBalanceSheetAccountTotal {
|
||||
amount: number,
|
||||
formattedAmount: string,
|
||||
currencyCode: string,
|
||||
date?: string|Date,
|
||||
}
|
||||
|
||||
export interface IBalanceSheetAccount {
|
||||
id: number,
|
||||
index: number,
|
||||
name: string,
|
||||
code: string,
|
||||
parentAccountId: number,
|
||||
type: 'account',
|
||||
hasTransactions: boolean,
|
||||
children?: IBalanceSheetAccount[],
|
||||
total: IBalanceSheetAccountTotal,
|
||||
totalPeriods?: IBalanceSheetAccountTotal[],
|
||||
}
|
||||
|
||||
export interface IBalanceSheetSection {
|
||||
name: string,
|
||||
sectionType?: string,
|
||||
type: 'section' | 'accounts_section',
|
||||
children: IBalanceSheetAccount[] | IBalanceSheetSection[],
|
||||
total: IBalanceSheetAccountTotal,
|
||||
totalPeriods?: IBalanceSheetAccountTotal[];
|
||||
|
||||
_accountsTypesRelated?: string[],
|
||||
_forceShow?: boolean,
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export interface IContactAddressDTO {
|
||||
shippingAddressState?: string,
|
||||
};
|
||||
export interface IContact extends IContactAddress{
|
||||
id?: number,
|
||||
contactService: 'customer' | 'vendor',
|
||||
contactType: string,
|
||||
|
||||
|
||||
2
server/src/interfaces/FinancialStatements.ts
Normal file
2
server/src/interfaces/FinancialStatements.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
61
server/src/interfaces/GeneralLedgerSheet.ts
Normal file
61
server/src/interfaces/GeneralLedgerSheet.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
|
||||
export interface IGeneralLedgerSheetQuery {
|
||||
fromDate: Date | string,
|
||||
toDate: Date | string,
|
||||
basis: string,
|
||||
numberFormat: {
|
||||
noCents: boolean,
|
||||
divideOn1000: boolean,
|
||||
},
|
||||
noneTransactions: boolean,
|
||||
accountsIds: number[],
|
||||
};
|
||||
|
||||
export interface IGeneralLedgerSheetAccountTransaction {
|
||||
id: number,
|
||||
amount: number,
|
||||
formattedAmount: string,
|
||||
currencyCode: string,
|
||||
note?: string,
|
||||
transactionType?: string,
|
||||
referenceId?: number,
|
||||
referenceType?: string,
|
||||
date: Date|string,
|
||||
};
|
||||
|
||||
export interface IGeneralLedgerSheetAccountBalance {
|
||||
date: Date|string,
|
||||
amount: number,
|
||||
formattedAmount: string,
|
||||
currencyCode: string,
|
||||
}
|
||||
|
||||
export interface IGeneralLedgerSheetAccount {
|
||||
id: number,
|
||||
name: string,
|
||||
code: string,
|
||||
index: number,
|
||||
parentAccountId: number,
|
||||
transactions: IGeneralLedgerSheetAccountTransaction[],
|
||||
opening: IGeneralLedgerSheetAccountBalance,
|
||||
closing: IGeneralLedgerSheetAccountBalance,
|
||||
}
|
||||
|
||||
export interface IAccountTransaction {
|
||||
id: number,
|
||||
index: number,
|
||||
draft: boolean,
|
||||
note: string,
|
||||
accountId: number,
|
||||
transactionType: string,
|
||||
referenceType: string,
|
||||
referenceId: number,
|
||||
contactId: number,
|
||||
contactType: string,
|
||||
credit: number,
|
||||
debit: number,
|
||||
date: string|Date,
|
||||
createdAt: string|Date,
|
||||
updatedAt: string|Date,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
|
||||
export interface IJournalEntry {
|
||||
id: number,
|
||||
index?: number,
|
||||
|
||||
date: Date,
|
||||
@@ -18,6 +19,8 @@ export interface IJournalEntry {
|
||||
};
|
||||
|
||||
export interface IJournalPoster {
|
||||
entries: IJournalEntry[],
|
||||
|
||||
credit(entry: IJournalEntry): void;
|
||||
debit(entry: IJournalEntry): void;
|
||||
|
||||
@@ -26,6 +29,9 @@ export interface IJournalPoster {
|
||||
saveEntries(): void;
|
||||
saveBalance(): void;
|
||||
deleteEntries(): void;
|
||||
|
||||
getAccountBalance(accountId: number, closingDate?: Date | string, dateType?: string): number;
|
||||
getAccountEntries(accountId: number): IJournalEntry[];
|
||||
}
|
||||
|
||||
export type TEntryType = 'credit' | 'debit';
|
||||
|
||||
28
server/src/interfaces/JournalReport.ts
Normal file
28
server/src/interfaces/JournalReport.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { IJournalEntry } from './Journal';
|
||||
|
||||
export interface IJournalReportQuery {
|
||||
fromDate: Date | string,
|
||||
toDate: Date | string,
|
||||
numberFormat: {
|
||||
noCents: boolean,
|
||||
divideOn1000: boolean,
|
||||
},
|
||||
transactionTypes: string | string[],
|
||||
accountsIds: number | number[],
|
||||
fromRange: number,
|
||||
toRange: number,
|
||||
}
|
||||
|
||||
export interface IJournalReportEntriesGroup {
|
||||
id: string,
|
||||
entries: IJournalEntry[],
|
||||
currencyCode: string,
|
||||
credit: number,
|
||||
debit: number,
|
||||
formattedCredit: string,
|
||||
formattedDebit: string,
|
||||
}
|
||||
|
||||
export interface IJournalReport {
|
||||
entries: IJournalReportEntriesGroup[],
|
||||
}
|
||||
58
server/src/interfaces/ProfitLossSheet.ts
Normal file
58
server/src/interfaces/ProfitLossSheet.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
|
||||
|
||||
export interface IProfitLossSheetQuery {
|
||||
basis: string,
|
||||
fromDate: Date | string,
|
||||
toDate: Date | string,
|
||||
numberFormat: {
|
||||
noCents: boolean,
|
||||
divideOn1000: boolean,
|
||||
},
|
||||
noneZero: boolean,
|
||||
noneTransactions: boolean,
|
||||
accountsIds: number[],
|
||||
displayColumnsType: 'total' | 'date_periods',
|
||||
displayColumnsBy: string,
|
||||
};
|
||||
|
||||
export interface IProfitLossSheetTotal {
|
||||
amount: number,
|
||||
formattedAmount: string,
|
||||
currencyCode: string,
|
||||
date?: Date|string,
|
||||
};
|
||||
|
||||
export interface IProfitLossSheetAccount {
|
||||
id: number,
|
||||
index: number,
|
||||
name: string,
|
||||
code: string,
|
||||
parentAccountId: number,
|
||||
hasTransactions: boolean,
|
||||
total: IProfitLossSheetTotal,
|
||||
totalPeriods: IProfitLossSheetTotal[],
|
||||
};
|
||||
|
||||
export interface IProfitLossSheetAccountsSection {
|
||||
sectionTitle: string,
|
||||
entryNormal: 'credit',
|
||||
accounts: IProfitLossSheetAccount[],
|
||||
total: IProfitLossSheetTotal,
|
||||
totalPeriods?: IProfitLossSheetTotal[],
|
||||
};
|
||||
|
||||
export interface IProfitLossSheetTotalSection {
|
||||
total: IProfitLossSheetTotal,
|
||||
totalPeriods?: IProfitLossSheetTotal[],
|
||||
};
|
||||
|
||||
export interface IProfitLossSheetStatement {
|
||||
income: IProfitLossSheetAccountsSection,
|
||||
costOfSales: IProfitLossSheetAccountsSection,
|
||||
expenses: IProfitLossSheetAccountsSection,
|
||||
otherExpenses: IProfitLossSheetAccountsSection,
|
||||
|
||||
netIncome: IProfitLossSheetTotalSection;
|
||||
operatingProfit: IProfitLossSheetTotalSection;
|
||||
grossProfit: IProfitLossSheetTotalSection;
|
||||
};
|
||||
37
server/src/interfaces/TrialBalanceSheet.ts
Normal file
37
server/src/interfaces/TrialBalanceSheet.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
export interface ITrialBalanceSheetQuery {
|
||||
fromDate: Date|string,
|
||||
toDate: Date|string,
|
||||
numberFormat: {
|
||||
noCents: boolean,
|
||||
divideOn1000: boolean,
|
||||
},
|
||||
basis: 'cash' | 'accural',
|
||||
noneZero: boolean,
|
||||
noneTransactions: boolean,
|
||||
accountIds: number[],
|
||||
}
|
||||
|
||||
export interface ITrialBalanceAccount {
|
||||
id: number,
|
||||
parentAccountId: number,
|
||||
name: string,
|
||||
code: string,
|
||||
accountNormal: string,
|
||||
hasTransactions: boolean,
|
||||
|
||||
credit: number,
|
||||
debit: number,
|
||||
balance: number,
|
||||
|
||||
formattedCredit: string,
|
||||
formattedDebit: string,
|
||||
formattedBalance: string,
|
||||
}
|
||||
|
||||
export type ITrialBalanceSheetData = IBalanceSheetSection[];
|
||||
|
||||
export interface ITrialBalanceStatement {
|
||||
data: ITrialBalanceSheetData,
|
||||
query: ITrialBalanceSheetQuery,
|
||||
}
|
||||
@@ -28,4 +28,12 @@ export * from './ManualJournal';
|
||||
export * from './Currency';
|
||||
export * from './ExchangeRate';
|
||||
export * from './Media';
|
||||
export * from './SaleEstimate';
|
||||
export * from './SaleEstimate';
|
||||
export * from './FinancialStatements';
|
||||
export * from './BalanceSheet';
|
||||
export * from './TrialBalanceSheet';
|
||||
export * from './GeneralLedgerSheet'
|
||||
export * from './ProfitLossSheet';
|
||||
export * from './JournalReport';
|
||||
|
||||
export * from './ARAgingSummaryReport';
|
||||
@@ -6,10 +6,12 @@ import ExpenseRepository from 'repositories/ExpenseRepository';
|
||||
import ViewRepository from 'repositories/ViewRepository';
|
||||
import ViewRoleRepository from 'repositories/ViewRoleRepository';
|
||||
import ContactRepository from 'repositories/ContactRepository';
|
||||
import AccountTransactionsRepository from 'repositories/AccountTransactionRepository';
|
||||
|
||||
export default (tenantId: number) => {
|
||||
return {
|
||||
accountRepository: new AccountRepository(tenantId),
|
||||
transactionsRepository: new AccountTransactionsRepository(tenantId),
|
||||
accountTypeRepository: new AccountTypeRepository(tenantId),
|
||||
customerRepository: new CustomerRepository(tenantId),
|
||||
vendorRepository: new VendorRepository(tenantId),
|
||||
|
||||
@@ -28,7 +28,7 @@ export default class AccountTransaction extends TenantModel {
|
||||
* @param {number[]} accountsIds
|
||||
*/
|
||||
filterAccounts(query, accountsIds) {
|
||||
if (accountsIds.length > 0) {
|
||||
if (Array.isArray(accountsIds) && accountsIds.length > 0) {
|
||||
query.whereIn('account_id', accountsIds);
|
||||
}
|
||||
},
|
||||
@@ -63,9 +63,11 @@ export default class AccountTransaction extends TenantModel {
|
||||
q.where('credit', '<=', toAmount);
|
||||
q.orWhere('debit', '<=', toAmount);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
sumationCreditDebit(query) {
|
||||
query.select(['accountId']);
|
||||
|
||||
query.sum('credit as credit');
|
||||
query.sum('debit as debit');
|
||||
query.groupBy('account_id');
|
||||
@@ -76,6 +78,16 @@ export default class AccountTransaction extends TenantModel {
|
||||
filterContactIds(query, contactIds) {
|
||||
query.whereIn('contact_id', contactIds);
|
||||
},
|
||||
|
||||
openingBalance(query, fromDate) {
|
||||
query.modify('filterDateRange', null, fromDate)
|
||||
query.modify('sumationCreditDebit')
|
||||
},
|
||||
|
||||
closingBalance(query, toDate) {
|
||||
query.modify('filterDateRange', null, toDate)
|
||||
query.modify('sumationCreditDebit')
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,7 @@
|
||||
import TenantRepository from 'repositories/TenantRepository';
|
||||
import { IAccount } from 'interfaces';
|
||||
import { Account } from 'models';
|
||||
|
||||
export default class AccountRepository extends TenantRepository {
|
||||
models: any;
|
||||
repositories: any;
|
||||
cache: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
) {
|
||||
super(tenantId);
|
||||
|
||||
this.models = this.tenancy.models(tenantId);
|
||||
this.cache = this.tenancy.cache(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts dependency graph.
|
||||
* @returns {}
|
||||
@@ -27,8 +9,9 @@ export default class AccountRepository extends TenantRepository {
|
||||
async getDependencyGraph() {
|
||||
const { Account } = this.models;
|
||||
const accounts = await this.allAccounts();
|
||||
const cacheKey = this.getCacheKey('accounts.depGraph');
|
||||
|
||||
return this.cache.get('accounts.depGraph', async () => {
|
||||
return this.cache.get(cacheKey, async () => {
|
||||
return Account.toDependencyGraph(accounts);
|
||||
});
|
||||
}
|
||||
@@ -37,10 +20,13 @@ export default class AccountRepository extends TenantRepository {
|
||||
* Retrieve all accounts on the storage.
|
||||
* @return {IAccount[]}
|
||||
*/
|
||||
allAccounts(): IAccount[] {
|
||||
allAccounts(withRelations?: string|string[]): IAccount[] {
|
||||
const { Account } = this.models;
|
||||
return this.cache.get('accounts', async () => {
|
||||
return Account.query();
|
||||
const cacheKey = this.getCacheKey('accounts.depGraph', withRelations);
|
||||
|
||||
return this.cache.get(cacheKey, async () => {
|
||||
return Account.query()
|
||||
.withGraphFetched(withRelations);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,7 +37,9 @@ export default class AccountRepository extends TenantRepository {
|
||||
*/
|
||||
getBySlug(slug: string): IAccount {
|
||||
const { Account } = this.models;
|
||||
return this.cache.get(`accounts.slug.${slug}`, () => {
|
||||
const cacheKey = this.getCacheKey('accounts.slug', slug);
|
||||
|
||||
return this.cache.get(cacheKey, () => {
|
||||
return Account.query().findOne('slug', slug);
|
||||
});
|
||||
}
|
||||
@@ -63,7 +51,9 @@ export default class AccountRepository extends TenantRepository {
|
||||
*/
|
||||
findById(id: number): IAccount {
|
||||
const { Account } = this.models;
|
||||
return this.cache.get(`accounts.id.${id}`, () => {
|
||||
const cacheKey = this.getCacheKey('accounts.id', id);
|
||||
|
||||
return this.cache.get(cacheKey, () => {
|
||||
return Account.query().findById(id);
|
||||
});
|
||||
}
|
||||
@@ -75,7 +65,11 @@ export default class AccountRepository extends TenantRepository {
|
||||
*/
|
||||
findByIds(accountsIds: number[]) {
|
||||
const { Account } = this.models;
|
||||
return Account.query().whereIn('id', accountsIds);
|
||||
const cacheKey = this.getCacheKey('accounts.id', accountsIds);
|
||||
|
||||
return this.cache.get(cacheKey, () => {
|
||||
return Account.query().whereIn('id', accountsIds);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,6 +137,6 @@ export default class AccountRepository extends TenantRepository {
|
||||
* Flush repository cache.
|
||||
*/
|
||||
flushCache(): void {
|
||||
this.cache.delStartWith('accounts');
|
||||
this.cache.delStartWith(this.repositoryName);
|
||||
}
|
||||
}
|
||||
60
server/src/repositories/AccountTransactionRepository.ts
Normal file
60
server/src/repositories/AccountTransactionRepository.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
import { QueryBuilder } from 'knex';
|
||||
import { AccountTransaction } from 'models';
|
||||
import hashObject from 'object-hash';
|
||||
import TenantRepository from 'repositories/TenantRepository';
|
||||
|
||||
|
||||
interface IJournalTransactionsFilter {
|
||||
fromDate: string | Date,
|
||||
toDate: string | Date,
|
||||
accountsIds: number[],
|
||||
sumationCreditDebit: boolean,
|
||||
fromAmount: number,
|
||||
toAmount: number,
|
||||
contactsIds?: number[],
|
||||
contactType?: string,
|
||||
};
|
||||
|
||||
export default class AccountTransactionsRepository extends TenantRepository {
|
||||
|
||||
journal(filter: IJournalTransactionsFilter) {
|
||||
const { AccountTransaction } = this.models;
|
||||
const cacheKey = this.getCacheKey('transactions.journal', filter);
|
||||
|
||||
return this.cache.get(cacheKey, () => {
|
||||
return AccountTransaction.query()
|
||||
.modify('filterAccounts', filter.accountsIds)
|
||||
.modify('filterDateRange', filter.fromDate, filter.toDate)
|
||||
.withGraphFetched('account.type')
|
||||
.onBuild((query) => {
|
||||
if (filter.sumationCreditDebit) {
|
||||
query.modify('sumationCreditDebit');
|
||||
}
|
||||
if (filter.fromAmount || filter.toAmount) {
|
||||
query.modify('filterAmountRange', filter.fromAmount, filter.toAmount);
|
||||
}
|
||||
if (filter.contactsIds) {
|
||||
query.modify('filterContactIds', filter.contactsIds);
|
||||
}
|
||||
if (filter.contactType) {
|
||||
query.where('contact_type', filter.contactType);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
openingBalance(fromDate) {
|
||||
return this.cache.get('transaction.openingBalance', () => {
|
||||
return AccountTransaction.query()
|
||||
.modify('openingBalance', fromDate);
|
||||
})
|
||||
}
|
||||
|
||||
closingOpening(toDate) {
|
||||
return this.cache.get('transaction.closingBalance', () => {
|
||||
return AccountTransaction.query()
|
||||
.modify('closingBalance', toDate);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,6 @@ import TenantRepository from 'repositories/TenantRepository';
|
||||
import { IAccountType } from 'interfaces';
|
||||
|
||||
export default class AccountTypeRepository extends TenantRepository {
|
||||
cache: any;
|
||||
models: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
) {
|
||||
super(tenantId);
|
||||
|
||||
this.models = this.tenancy.models(tenantId);
|
||||
this.cache = this.tenancy.cache(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all accounts types.
|
||||
* @return {IAccountType[]}
|
||||
|
||||
19
server/src/repositories/CachableRepository.ts
Normal file
19
server/src/repositories/CachableRepository.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import hashObject from 'object-hash';
|
||||
|
||||
|
||||
export default class CachableRepository {
|
||||
repositoryName: string;
|
||||
|
||||
/**
|
||||
* Retrieve the cache key of the method name and arguments.
|
||||
* @param {string} method
|
||||
* @param {...any} args
|
||||
* @return {string}
|
||||
*/
|
||||
getCacheKey(method, ...args) {
|
||||
const hashArgs = hashObject({ ...args });
|
||||
const repositoryName = this.repositoryName;
|
||||
|
||||
return `${repositoryName}-${method}-${hashArgs}`;
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,6 @@ import TenantRepository from 'repositories/TenantRepository';
|
||||
import { IContact } from 'interfaces';
|
||||
|
||||
export default class ContactRepository extends TenantRepository {
|
||||
cache: any;
|
||||
models: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
) {
|
||||
super(tenantId);
|
||||
|
||||
this.models = this.tenancy.models(tenantId);
|
||||
this.cache = this.tenancy.cache(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given contact model.
|
||||
* @param {number} contactId
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import TenantRepository from "./TenantRepository";
|
||||
|
||||
export default class CustomerRepository extends TenantRepository {
|
||||
models: any;
|
||||
cache: any;
|
||||
all() {
|
||||
const { Contact } = this.models;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
constructor(tenantId: number) {
|
||||
super(tenantId);
|
||||
|
||||
this.models = this.tenancy.models(tenantId);
|
||||
this.cache = this.tenancy.cache(tenantId);
|
||||
return this.cache.get('customers', () => {
|
||||
return Contact.query().modify('customer');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,21 +3,6 @@ import { IExpense } from 'interfaces';
|
||||
import moment from "moment";
|
||||
|
||||
export default class ExpenseRepository extends TenantRepository {
|
||||
models: any;
|
||||
repositories: any;
|
||||
cache: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
constructor(tenantId: number) {
|
||||
super(tenantId);
|
||||
|
||||
this.models = this.tenancy.models(tenantId);
|
||||
this.cache = this.tenancy.cache(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given expense by id.
|
||||
* @param {number} expenseId
|
||||
|
||||
18
server/src/repositories/JournalRepository.ts
Normal file
18
server/src/repositories/JournalRepository.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { IBalanceSheetQuery } from 'interfaces';
|
||||
import TenantRepository from 'repositories/TenantRepository';
|
||||
|
||||
|
||||
export default class JournalRepository extends TenantRepository {
|
||||
|
||||
balanceSheet(query: IBalanceSheetQuery) {
|
||||
|
||||
// Accounts dependency graph.
|
||||
const accountsGraph = Account.toDependencyGraph(balanceSheetAccounts);
|
||||
|
||||
// Load all entries that associated to the given accounts.
|
||||
const journalEntriesCollected = Account.collectJournalEntries(balanceSheetAccounts);
|
||||
|
||||
const journalEntries = new JournalPoster(accountsGraph);
|
||||
journalEntries.loadEntries(journalEntriesCollected);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,45 @@
|
||||
import { Container } from 'typedi';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import CachableRepository from './CachableRepository';
|
||||
|
||||
export default class TenantRepository {
|
||||
export default class TenantRepository extends CachableRepository {
|
||||
repositoryName: string;
|
||||
tenantId: number;
|
||||
tenancy: TenancyService;
|
||||
|
||||
modelsInstance: any;
|
||||
repositoriesInstance: any;
|
||||
cacheInstance: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
constructor(tenantId: number) {
|
||||
super();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.tenancy = Container.get(TenancyService);
|
||||
this.repositoryName = this.constructor.name;
|
||||
}
|
||||
|
||||
get models() {
|
||||
if (!this.modelsInstance) {
|
||||
this.modelsInstance = this.tenancy.models(this.tenantId);
|
||||
}
|
||||
return this.modelsInstance;
|
||||
}
|
||||
|
||||
get repositories() {
|
||||
if (!this.repositoriesInstance) {
|
||||
this.repositoriesInstance = this.tenancy.repositories(this.tenantId);
|
||||
}
|
||||
return this.repositoriesInstance;
|
||||
}
|
||||
|
||||
get cache() {
|
||||
if (!this.cacheInstance) {
|
||||
this.cacheInstance = this.tenancy.cache(this.tenantId);
|
||||
}
|
||||
return this.cacheInstance;
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,6 @@ import TenantRepository from "./TenantRepository";
|
||||
|
||||
|
||||
export default class VendorRepository extends TenantRepository {
|
||||
models: any;
|
||||
cache: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
constructor(tenantId: number) {
|
||||
super(tenantId);
|
||||
|
||||
this.models = this.tenancy.models(tenantId);
|
||||
this.cache = this.tenancy.cache(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve vendor details of the given id.
|
||||
@@ -68,7 +55,6 @@ export default class VendorRepository extends TenantRepository {
|
||||
[changeMethod]('balance', Math.abs(amount));
|
||||
}
|
||||
|
||||
|
||||
async changeDiffBalance(
|
||||
vendorId: number,
|
||||
amount: number,
|
||||
|
||||
@@ -2,22 +2,6 @@ import { IView } from 'interfaces';
|
||||
import TenantRepository from 'repositories/TenantRepository';
|
||||
|
||||
export default class ViewRepository extends TenantRepository {
|
||||
models: any;
|
||||
cache: any;
|
||||
repositories: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
) {
|
||||
super(tenantId);
|
||||
|
||||
this.models = this.tenancy.models(tenantId);
|
||||
this.cache = this.tenancy.cache(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve view model by the given id.
|
||||
|
||||
@@ -2,23 +2,6 @@ import { omit } from 'lodash';
|
||||
import TenantRepository from 'repositories/TenantRepository';
|
||||
|
||||
export default class ViewRoleRepository extends TenantRepository {
|
||||
models: any;
|
||||
cache: any;
|
||||
repositories: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId - The given tenant id.
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
) {
|
||||
super(tenantId);
|
||||
|
||||
this.models = this.tenancy.models(tenantId);
|
||||
this.cache = this.tenancy.cache(tenantId);
|
||||
this.repositories = this.tenancy.cache(tenantId);
|
||||
}
|
||||
|
||||
allByView(viewId: number) {
|
||||
const { ViewRole } = this.models;
|
||||
|
||||
@@ -1,207 +1,19 @@
|
||||
import moment from 'moment';
|
||||
import { IJournalPoster } from 'interfaces';
|
||||
|
||||
export default class JournalFinancial {
|
||||
accountsBalanceTable: { [key: number]: number; } = {};
|
||||
journal: IJournalPoster;
|
||||
|
||||
accountsDepGraph: any;
|
||||
|
||||
/**
|
||||
* Retrieve the closing balance for the given account and closing date.
|
||||
* @param {Number} accountId -
|
||||
* @param {Date} closingDate -
|
||||
* @param {string} dataType? -
|
||||
* @return {number}
|
||||
* Journal poster.
|
||||
* @param {IJournalPoster} journal
|
||||
*/
|
||||
getClosingBalance(
|
||||
accountId: number,
|
||||
closingDate: Date|string,
|
||||
dateType: string = 'day'
|
||||
): number {
|
||||
let closingBalance = 0;
|
||||
const momentClosingDate = moment(closingDate);
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
// Can not continue if not before or event same closing date.
|
||||
if (
|
||||
(!momentClosingDate.isAfter(entry.date, dateType) &&
|
||||
!momentClosingDate.isSame(entry.date, dateType)) ||
|
||||
(entry.account !== accountId && accountId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (entry.accountNormal === 'credit') {
|
||||
closingBalance += entry.credit ? entry.credit : -1 * entry.debit;
|
||||
} else if (entry.accountNormal === 'debit') {
|
||||
closingBalance += entry.debit ? entry.debit : -1 * entry.credit;
|
||||
}
|
||||
});
|
||||
return closingBalance;
|
||||
constructor(journal: IJournalPoster) {
|
||||
this.journal = journal;
|
||||
this.accountsDepGraph = this.journal.accountsDepGraph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given account balance with dependencies accounts.
|
||||
* @param {Number} accountId
|
||||
* @param {Date} closingDate
|
||||
* @param {String} dateType
|
||||
* @return {Number}
|
||||
*/
|
||||
getAccountBalance(accountId: number, closingDate: Date|string, dateType: string) {
|
||||
const accountNode = this.accountsDepGraph.getNodeData(accountId);
|
||||
const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId);
|
||||
const depAccounts = depAccountsIds
|
||||
.map((id) => this.accountsDepGraph.getNodeData(id));
|
||||
|
||||
let balance: number = 0;
|
||||
|
||||
[...depAccounts, accountNode].forEach((account) => {
|
||||
const closingBalance = this.getClosingBalance(
|
||||
account.id,
|
||||
closingDate,
|
||||
dateType
|
||||
);
|
||||
this.accountsBalanceTable[account.id] = closingBalance;
|
||||
balance += this.accountsBalanceTable[account.id];
|
||||
});
|
||||
return balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the credit/debit sumation for the given account and date.
|
||||
* @param {Number} account -
|
||||
* @param {Date|String} closingDate -
|
||||
*/
|
||||
getTrialBalance(accountId, closingDate, dateType) {
|
||||
const momentClosingDate = moment(closingDate);
|
||||
const result = {
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
balance: 0,
|
||||
};
|
||||
this.entries.forEach((entry) => {
|
||||
if (
|
||||
(!momentClosingDate.isAfter(entry.date, dateType) &&
|
||||
!momentClosingDate.isSame(entry.date, dateType)) ||
|
||||
(entry.account !== accountId && accountId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
result.credit += entry.credit;
|
||||
result.debit += entry.debit;
|
||||
|
||||
if (entry.accountNormal === 'credit') {
|
||||
result.balance += entry.credit - entry.debit;
|
||||
} else if (entry.accountNormal === 'debit') {
|
||||
result.balance += entry.debit - entry.credit;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve trial balance of the given account with depends.
|
||||
* @param {Number} accountId
|
||||
* @param {Date} closingDate
|
||||
* @param {String} dateType
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
getTrialBalanceWithDepands(accountId: number, closingDate: Date, dateType: string) {
|
||||
const accountNode = this.accountsDepGraph.getNodeData(accountId);
|
||||
const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId);
|
||||
const depAccounts = depAccountsIds.map((id) =>
|
||||
this.accountsDepGraph.getNodeData(id)
|
||||
);
|
||||
const trialBalance = { credit: 0, debit: 0, balance: 0 };
|
||||
|
||||
[...depAccounts, accountNode].forEach((account) => {
|
||||
const _trialBalance = this.getTrialBalance(
|
||||
account.id,
|
||||
closingDate,
|
||||
dateType
|
||||
);
|
||||
|
||||
trialBalance.credit += _trialBalance.credit;
|
||||
trialBalance.debit += _trialBalance.debit;
|
||||
trialBalance.balance += _trialBalance.balance;
|
||||
});
|
||||
return trialBalance;
|
||||
}
|
||||
|
||||
getContactTrialBalance(
|
||||
accountId: number,
|
||||
contactId: number,
|
||||
contactType: string,
|
||||
closingDate: Date|string,
|
||||
openingDate: Date|string,
|
||||
) {
|
||||
const momentClosingDate = moment(closingDate);
|
||||
const momentOpeningDate = moment(openingDate);
|
||||
const trial = {
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
balance: 0,
|
||||
};
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
if (
|
||||
(closingDate &&
|
||||
!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||
(openingDate &&
|
||||
!momentOpeningDate.isBefore(entry.date, 'day') &&
|
||||
!momentOpeningDate.isSame(entry.date)) ||
|
||||
(accountId && entry.account !== accountId) ||
|
||||
(contactId && entry.contactId !== contactId) ||
|
||||
entry.contactType !== contactType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (entry.credit) {
|
||||
trial.balance -= entry.credit;
|
||||
trial.credit += entry.credit;
|
||||
}
|
||||
if (entry.debit) {
|
||||
trial.balance += entry.debit;
|
||||
trial.debit += entry.debit;
|
||||
}
|
||||
});
|
||||
return trial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total balnace of the given customer/vendor contact.
|
||||
* @param {Number} accountId
|
||||
* @param {Number} contactId
|
||||
* @param {String} contactType
|
||||
* @param {Date} closingDate
|
||||
*/
|
||||
getContactBalance(
|
||||
accountId: number,
|
||||
contactId: number,
|
||||
contactType: string,
|
||||
closingDate: Date,
|
||||
openingDate: Date,
|
||||
) {
|
||||
const momentClosingDate = moment(closingDate);
|
||||
let balance = 0;
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
if (
|
||||
(closingDate &&
|
||||
!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||
(entry.account !== accountId && accountId) ||
|
||||
(contactId && entry.contactId !== contactId) ||
|
||||
entry.contactType !== contactType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (entry.credit) {
|
||||
balance -= entry.credit;
|
||||
}
|
||||
if (entry.debit) {
|
||||
balance += entry.debit;
|
||||
}
|
||||
});
|
||||
return balance;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { omit } from 'lodash';
|
||||
import { omit, get } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { Container } from 'typedi';
|
||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
@@ -22,18 +23,25 @@ export default class JournalPoster implements IJournalPoster {
|
||||
balancesChange: IAccountsChange = {};
|
||||
accountsDepGraph: IAccountsChange = {};
|
||||
|
||||
accountsBalanceTable: { [key: number]: number; } = {};
|
||||
|
||||
/**
|
||||
* Journal poster constructor.
|
||||
* @param {number} tenantId -
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
accountsGraph?: any,
|
||||
) {
|
||||
this.initTenancy();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.models = this.tenancy.models(tenantId);
|
||||
this.repositories = this.tenancy.repositories(tenantId);
|
||||
|
||||
if (accountsGraph) {
|
||||
this.accountsDepGraph = accountsGraph;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,10 +62,13 @@ export default class JournalPoster implements IJournalPoster {
|
||||
* @private
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private async initializeAccountsDepGraph(): Promise<void> {
|
||||
public async initAccountsDepGraph(): Promise<void> {
|
||||
const { accountRepository } = this.repositories;
|
||||
const accountsDepGraph = await accountRepository.getDependencyGraph();
|
||||
this.accountsDepGraph = accountsDepGraph;
|
||||
|
||||
if (!this.accountsDepGraph) {
|
||||
const accountsDepGraph = await accountRepository.getDependencyGraph();
|
||||
this.accountsDepGraph = accountsDepGraph;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,7 +187,7 @@ export default class JournalPoster implements IJournalPoster {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async saveBalance() {
|
||||
await this.initializeAccountsDepGraph();
|
||||
await this.initAccountsDepGraph();
|
||||
|
||||
const { Account } = this.models;
|
||||
const accountsChange = this.balanceChangeWithDepends(this.balancesChange);
|
||||
@@ -311,15 +322,17 @@ export default class JournalPoster implements IJournalPoster {
|
||||
* Load fetched accounts journal entries.
|
||||
* @param {IJournalEntry[]} entries -
|
||||
*/
|
||||
loadEntries(entries: IJournalEntry[]): void {
|
||||
entries.forEach((entry: IJournalEntry) => {
|
||||
fromTransactions(transactions) {
|
||||
transactions.forEach((transaction) => {
|
||||
this.entries.push({
|
||||
...entry,
|
||||
account: entry.account ? entry.account.id : entry.accountId,
|
||||
...transaction,
|
||||
account: transaction.accountId,
|
||||
accountNormal: get(transaction, 'account.type.normal'),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the entries balance change.
|
||||
* @public
|
||||
@@ -334,4 +347,216 @@ export default class JournalPoster implements IJournalPoster {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static fromTransactions(entries, ...args: [number, ...any]) {
|
||||
const journal = new this(...args);
|
||||
journal.fromTransactions(entries);
|
||||
|
||||
return journal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the closing balance for the given account and closing date.
|
||||
* @param {Number} accountId -
|
||||
* @param {Date} closingDate -
|
||||
* @param {string} dataType? -
|
||||
* @return {number}
|
||||
*/
|
||||
getClosingBalance(
|
||||
accountId: number,
|
||||
closingDate: Date|string,
|
||||
dateType: string = 'day'
|
||||
): number {
|
||||
let closingBalance = 0;
|
||||
const momentClosingDate = moment(closingDate);
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
// Can not continue if not before or event same closing date.
|
||||
if (
|
||||
(!momentClosingDate.isAfter(entry.date, dateType) &&
|
||||
!momentClosingDate.isSame(entry.date, dateType)) ||
|
||||
(entry.account !== accountId && accountId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (entry.accountNormal === 'credit') {
|
||||
closingBalance += entry.credit ? entry.credit : -1 * entry.debit;
|
||||
} else if (entry.accountNormal === 'debit') {
|
||||
closingBalance += entry.debit ? entry.debit : -1 * entry.credit;
|
||||
}
|
||||
});
|
||||
return closingBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the given account balance with dependencies accounts.
|
||||
* @param {Number} accountId -
|
||||
* @param {Date} closingDate -
|
||||
* @param {String} dateType -
|
||||
* @return {Number}
|
||||
*/
|
||||
getAccountBalance(accountId: number, closingDate: Date|string, dateType: string) {
|
||||
const accountNode = this.accountsDepGraph.getNodeData(accountId);
|
||||
const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId);
|
||||
const depAccounts = depAccountsIds
|
||||
.map((id) => this.accountsDepGraph.getNodeData(id));
|
||||
|
||||
let balance: number = 0;
|
||||
|
||||
[...depAccounts, accountNode].forEach((account) => {
|
||||
const closingBalance = this.getClosingBalance(
|
||||
account.id,
|
||||
closingDate,
|
||||
dateType
|
||||
);
|
||||
this.accountsBalanceTable[account.id] = closingBalance;
|
||||
balance += this.accountsBalanceTable[account.id];
|
||||
});
|
||||
return balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the credit/debit sumation for the given account and date.
|
||||
* @param {Number} account -
|
||||
* @param {Date|String} closingDate -
|
||||
*/
|
||||
getTrialBalance(accountId, closingDate, dateType) {
|
||||
const momentClosingDate = moment(closingDate);
|
||||
const result = {
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
balance: 0,
|
||||
};
|
||||
this.entries.forEach((entry) => {
|
||||
if (
|
||||
(!momentClosingDate.isAfter(entry.date, dateType) &&
|
||||
!momentClosingDate.isSame(entry.date, dateType)) ||
|
||||
(entry.account !== accountId && accountId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
result.credit += entry.credit;
|
||||
result.debit += entry.debit;
|
||||
|
||||
if (entry.accountNormal === 'credit') {
|
||||
result.balance += entry.credit - entry.debit;
|
||||
} else if (entry.accountNormal === 'debit') {
|
||||
result.balance += entry.debit - entry.credit;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve trial balance of the given account with depends.
|
||||
* @param {Number} accountId
|
||||
* @param {Date} closingDate
|
||||
* @param {String} dateType
|
||||
* @return {Number}
|
||||
*/
|
||||
|
||||
getTrialBalanceWithDepands(accountId: number, closingDate: Date, dateType: string) {
|
||||
const accountNode = this.accountsDepGraph.getNodeData(accountId);
|
||||
const depAccountsIds = this.accountsDepGraph.dependenciesOf(accountId);
|
||||
const depAccounts = depAccountsIds.map((id) =>
|
||||
this.accountsDepGraph.getNodeData(id)
|
||||
);
|
||||
const trialBalance = { credit: 0, debit: 0, balance: 0 };
|
||||
|
||||
[...depAccounts, accountNode].forEach((account) => {
|
||||
const _trialBalance = this.getTrialBalance(
|
||||
account.id,
|
||||
closingDate,
|
||||
dateType
|
||||
);
|
||||
|
||||
trialBalance.credit += _trialBalance.credit;
|
||||
trialBalance.debit += _trialBalance.debit;
|
||||
trialBalance.balance += _trialBalance.balance;
|
||||
});
|
||||
return trialBalance;
|
||||
}
|
||||
|
||||
getContactTrialBalance(
|
||||
accountId: number,
|
||||
contactId: number,
|
||||
contactType: string,
|
||||
closingDate: Date|string,
|
||||
openingDate: Date|string,
|
||||
) {
|
||||
const momentClosingDate = moment(closingDate);
|
||||
const momentOpeningDate = moment(openingDate);
|
||||
const trial = {
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
balance: 0,
|
||||
};
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
if (
|
||||
(closingDate &&
|
||||
!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||
(openingDate &&
|
||||
!momentOpeningDate.isBefore(entry.date, 'day') &&
|
||||
!momentOpeningDate.isSame(entry.date)) ||
|
||||
(accountId && entry.account !== accountId) ||
|
||||
(contactId && entry.contactId !== contactId) ||
|
||||
entry.contactType !== contactType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (entry.credit) {
|
||||
trial.balance -= entry.credit;
|
||||
trial.credit += entry.credit;
|
||||
}
|
||||
if (entry.debit) {
|
||||
trial.balance += entry.debit;
|
||||
trial.debit += entry.debit;
|
||||
}
|
||||
});
|
||||
return trial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total balnace of the given customer/vendor contact.
|
||||
* @param {Number} accountId
|
||||
* @param {Number} contactId
|
||||
* @param {String} contactType
|
||||
* @param {Date} closingDate
|
||||
*/
|
||||
getContactBalance(
|
||||
accountId: number,
|
||||
contactId: number,
|
||||
contactType: string,
|
||||
closingDate: Date,
|
||||
openingDate: Date,
|
||||
) {
|
||||
const momentClosingDate = moment(closingDate);
|
||||
let balance = 0;
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
if (
|
||||
(closingDate &&
|
||||
!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||
(entry.account !== accountId && accountId) ||
|
||||
(contactId && entry.contactId !== contactId) ||
|
||||
entry.contactType !== contactType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (entry.credit) {
|
||||
balance -= entry.credit;
|
||||
}
|
||||
if (entry.debit) {
|
||||
balance += entry.debit;
|
||||
}
|
||||
});
|
||||
return balance;
|
||||
}
|
||||
|
||||
getAccountEntries(accountId: number) {
|
||||
return this.entries.filter((entry) => entry.account === accountId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ export default class AccountsService {
|
||||
* @param {number[]} accountsIds
|
||||
* @return {IAccount[]}
|
||||
*/
|
||||
private async getAccountsOrThrowError(
|
||||
public async getAccountsOrThrowError(
|
||||
tenantId: number,
|
||||
accountsIds: number[]
|
||||
): Promise<IAccount[]> {
|
||||
@@ -521,8 +521,7 @@ export default class AccountsService {
|
||||
* -----------
|
||||
* Precedures.
|
||||
* -----------
|
||||
* - Transfer the given account transactions to another account
|
||||
* with the same root type.
|
||||
* - Transfer the given account transactions to another account with the same root type.
|
||||
* - Delete the given account.
|
||||
* -------
|
||||
* @param {number} tenantId -
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
|
||||
export default class PayableAgingSummaryService {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import FinancialSheet from "../FinancialSheet";
|
||||
|
||||
|
||||
|
||||
export default class APAgingSummarySheet extends FinancialSheet {
|
||||
|
||||
|
||||
|
||||
reportData() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IARAgingSummaryQuery } from 'interfaces';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import Journal from 'services/Accounting/JournalPoster';
|
||||
import ARAgingSummarySheet from './ARAgingSummarySheet';
|
||||
|
||||
@Service()
|
||||
export default class ARAgingSummaryService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Default report query.
|
||||
*/
|
||||
get defaultQuery() {
|
||||
return {
|
||||
asDate: moment().format('YYYY-MM-DD'),
|
||||
agingDaysBefore: 30,
|
||||
agingPeriods: 3,
|
||||
numberFormat: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
customersIds: [],
|
||||
noneZero: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreive th accounts receivable aging summary data and columns.
|
||||
* @param {number} tenantId
|
||||
* @param query
|
||||
*/
|
||||
async ARAgingSummary(tenantId: number, query: IARAgingSummaryQuery) {
|
||||
const {
|
||||
customerRepository,
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
accountTypeRepository
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
this.logger.info('[AR_Aging_Summary] try to calculate the report.', { tenantId, filter });
|
||||
|
||||
// Settings tenant service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
|
||||
// Retrieve all accounts graph on the storage.
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieve all customers from the storage.
|
||||
const customers = await customerRepository.all();
|
||||
|
||||
// Retrieve AR account type.
|
||||
const ARType = await accountTypeRepository.getByKey('accounts_receivable');
|
||||
|
||||
// Retreive AR account.
|
||||
const ARAccount = await Account.query().findOne('account_type_id', ARType.id);
|
||||
|
||||
// Retrieve journal transactions based on the given query.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
toDate: filter.asDate,
|
||||
contactType: 'customer',
|
||||
contactsIds: customers.map(customer => customer.id),
|
||||
});
|
||||
// Converts transactions array to journal collection.
|
||||
const journal = Journal.fromTransactions(transactions, tenantId, accountsGraph);
|
||||
|
||||
// AR aging summary report instnace.
|
||||
const ARAgingSummaryReport = new ARAgingSummarySheet(
|
||||
tenantId,
|
||||
filter,
|
||||
customers,
|
||||
journal,
|
||||
ARAccount,
|
||||
baseCurrency
|
||||
);
|
||||
// AR aging summary report data and columns.
|
||||
const data = ARAgingSummaryReport.reportData();
|
||||
const columns = ARAgingSummaryReport.reportColumns();
|
||||
|
||||
return { data, columns };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
ICustomer,
|
||||
IARAgingSummaryQuery,
|
||||
ARAgingSummaryCustomer,
|
||||
IAgingPeriodClosingBalance,
|
||||
IAgingPeriodTotal,
|
||||
IJournalPoster,
|
||||
IAccount,
|
||||
IAgingPeriod
|
||||
} from "interfaces";
|
||||
import AgingSummaryReport from './AgingSummary';
|
||||
|
||||
|
||||
export default class ARAgingSummarySheet extends AgingSummaryReport {
|
||||
tenantId: number;
|
||||
query: IARAgingSummaryQuery;
|
||||
customers: ICustomer[];
|
||||
journal: IJournalPoster;
|
||||
ARAccount: IAccount;
|
||||
agingPeriods: IAgingPeriod[];
|
||||
baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
* @param {IARAgingSummaryQuery} query
|
||||
* @param {ICustomer[]} customers
|
||||
* @param {IJournalPoster} journal
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
query: IARAgingSummaryQuery,
|
||||
customers: ICustomer[],
|
||||
journal: IJournalPoster,
|
||||
ARAccount: IAccount,
|
||||
baseCurrency: string,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.customers = customers;
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.journal = journal;
|
||||
this.ARAccount = ARAccount;
|
||||
this.baseCurrency = baseCurrency;
|
||||
|
||||
this.initAgingPeriod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the aging periods.
|
||||
*/
|
||||
private initAgingPeriod() {
|
||||
this.agingPeriods = this.agingRangePeriods(
|
||||
this.query.asDate,
|
||||
this.query.agingDaysBefore,
|
||||
this.query.agingPeriods
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ICustomer} customer
|
||||
* @param {IAgingPeriod} agingPeriod
|
||||
*/
|
||||
private agingPeriodCloser(
|
||||
customer: ICustomer,
|
||||
agingPeriod: IAgingPeriod,
|
||||
): IAgingPeriodClosingBalance {
|
||||
// Calculate the trial balance between the given date period.
|
||||
const agingTrialBalance = this.journal.getContactTrialBalance(
|
||||
this.ARAccount.id,
|
||||
customer.id,
|
||||
'customer',
|
||||
agingPeriod.fromPeriod,
|
||||
);
|
||||
return {
|
||||
...agingPeriod,
|
||||
closingBalance: agingTrialBalance.debit,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ICustomer} customer
|
||||
*/
|
||||
private getCustomerAging(customer: ICustomer, totalReceivable: number): IAgingPeriodTotal[] {
|
||||
const agingClosingBalance = this.agingPeriods
|
||||
.map((agingPeriod: IAgingPeriod) => this.agingPeriodCloser(customer, agingPeriod));
|
||||
|
||||
const aging = this.contactAgingBalance(
|
||||
agingClosingBalance,
|
||||
totalReceivable
|
||||
);
|
||||
return aging;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping aging customer.
|
||||
* @param {ICustomer} customer -
|
||||
* @return {ARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customerMapper(customer: ICustomer): ARAgingSummaryCustomer {
|
||||
// Calculate the trial balance total of the given customer.
|
||||
const trialBalance = this.journal.getContactTrialBalance(
|
||||
this.ARAccount.id,
|
||||
customer.id,
|
||||
'customer'
|
||||
);
|
||||
const amount = trialBalance.balance;
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
const currencyCode = this.baseCurrency;
|
||||
|
||||
return {
|
||||
customerName: customer.displayName,
|
||||
aging: this.getCustomerAging(customer, trialBalance.balance),
|
||||
total: {
|
||||
amount,
|
||||
formattedAmount,
|
||||
currencyCode,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve customers walker.
|
||||
* @param {ICustomer[]} customers
|
||||
* @return {ARAgingSummaryCustomer[]}
|
||||
*/
|
||||
private customersWalker(customers: ICustomer[]): ARAgingSummaryCustomer[] {
|
||||
return customers
|
||||
.map((customer: ICustomer) => this.customerMapper(customer))
|
||||
|
||||
// Filter customers that have zero total amount when `noneZero` is on.
|
||||
.filter((customer: ARAgingSummaryCustomer) =>
|
||||
!(customer.total.amount === 0 && this.query.noneZero),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve AR. aging summary report data.
|
||||
*/
|
||||
public reportData() {
|
||||
return this.customersWalker(this.customers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve AR aging summary report columns.
|
||||
*/
|
||||
reportColumns() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import moment from 'moment';
|
||||
import { omit, reverse } from 'lodash';
|
||||
import { IAgingPeriod, IAgingPeriodClosingBalance, IAgingPeriodTotal } from 'interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
export default class AgingSummaryReport extends FinancialSheet{
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Array} agingPeriods
|
||||
* @param {Numeric} customerBalance
|
||||
*/
|
||||
contactAgingBalance(
|
||||
agingPeriods: IAgingPeriodClosingBalance[],
|
||||
receivableTotalCredit: number,
|
||||
): IAgingPeriodTotal[] {
|
||||
let prevAging = 0;
|
||||
let receivableCredit = receivableTotalCredit;
|
||||
let diff = receivableCredit;
|
||||
|
||||
const periods = reverse(agingPeriods).map((agingPeriod) => {
|
||||
const agingAmount = (agingPeriod.closingBalance - prevAging);
|
||||
const subtract = Math.min(diff, agingAmount);
|
||||
diff -= Math.min(agingAmount, diff);
|
||||
|
||||
const total = Math.max(agingAmount - subtract, 0);
|
||||
|
||||
const output = {
|
||||
...omit(agingPeriod, ['closingBalance']),
|
||||
total,
|
||||
};
|
||||
prevAging = agingPeriod.closingBalance;
|
||||
return output;
|
||||
});
|
||||
return reverse(periods);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} asDay
|
||||
* @param {*} agingDaysBefore
|
||||
* @param {*} agingPeriodsFreq
|
||||
*/
|
||||
agingRangePeriods(asDay, agingDaysBefore, agingPeriodsFreq): IAgingPeriod[] {
|
||||
const totalAgingDays = agingDaysBefore * agingPeriodsFreq;
|
||||
const startAging = moment(asDay).startOf('day');
|
||||
const endAging = startAging.clone().subtract('days', totalAgingDays).endOf('day');
|
||||
|
||||
const agingPeriods: IAgingPeriod[] = [];
|
||||
const startingAging = startAging.clone();
|
||||
|
||||
let beforeDays = 1;
|
||||
let toDays = 0;
|
||||
|
||||
while (startingAging > endAging) {
|
||||
const currentAging = startingAging.clone();
|
||||
startingAging.subtract('days', agingDaysBefore).endOf('day');
|
||||
toDays += agingDaysBefore;
|
||||
|
||||
agingPeriods.push({
|
||||
fromPeriod: moment(currentAging).toDate(),
|
||||
toPeriod: moment(startingAging).toDate(),
|
||||
beforeDays: beforeDays === 1 ? 0 : beforeDays,
|
||||
toDays: toDays,
|
||||
...(startingAging.valueOf() === endAging.valueOf()) ? {
|
||||
toPeriod: null,
|
||||
toDays: null,
|
||||
} : {},
|
||||
});
|
||||
beforeDays += agingDaysBefore;
|
||||
}
|
||||
return agingPeriods;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import { sumBy, pick } from 'lodash';
|
||||
import {
|
||||
IBalanceSheetQuery,
|
||||
IBalanceSheetStructureSection,
|
||||
IBalanceSheetAccountTotal,
|
||||
IBalanceSheetAccount,
|
||||
IBalanceSheetSection,
|
||||
IAccount,
|
||||
IJournalPoster,
|
||||
IAccountType,
|
||||
} from 'interfaces';
|
||||
import {
|
||||
dateRangeCollection,
|
||||
flatToNestedArray,
|
||||
} from 'utils';
|
||||
import BalanceSheetStructure from 'data/BalanceSheetStructure';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
export default class BalanceSheetStatement extends FinancialSheet {
|
||||
query: IBalanceSheetQuery;
|
||||
tenantId: number;
|
||||
accounts: IAccount & { type: IAccountType }[];
|
||||
journalFinancial: IJournalPoster;
|
||||
comparatorDateType: string;
|
||||
dateRangeSet: string[];
|
||||
baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId -
|
||||
* @param {IBalanceSheetQuery} query -
|
||||
* @param {IAccount[]} accounts -
|
||||
* @param {IJournalFinancial} journalFinancial -
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
query: IBalanceSheetQuery,
|
||||
accounts: IAccount & { type: IAccountType }[],
|
||||
journalFinancial: IJournalPoster,
|
||||
baseCurrency: string,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.accounts = accounts;
|
||||
this.journalFinancial = journalFinancial;
|
||||
this.baseCurrency = baseCurrency;
|
||||
|
||||
this.comparatorDateType = query.displayColumnsType === 'total'
|
||||
? 'day'
|
||||
: query.displayColumnsBy;
|
||||
|
||||
this.initDateRangeCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize date range set.
|
||||
*/
|
||||
initDateRangeCollection() {
|
||||
if (this.query.displayColumnsType === 'date_periods') {
|
||||
this.dateRangeSet = dateRangeCollection(
|
||||
this.query.fromDate,
|
||||
this.query.toDate,
|
||||
this.comparatorDateType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates accounts total deeply of the given accounts graph.
|
||||
* @param {IBalanceSheetSection[]} sections -
|
||||
* @return {IBalanceSheetAccountTotal}
|
||||
*/
|
||||
private getSectionTotal(sections: IBalanceSheetSection[]): IBalanceSheetAccountTotal {
|
||||
const amount = sumBy(sections, 'total.amount');
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
const currencyCode = this.baseCurrency;
|
||||
|
||||
return { amount, formattedAmount, currencyCode };
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve accounts total periods.
|
||||
* @param {IBalanceSheetAccount[]} sections -
|
||||
* @return {IBalanceSheetAccountTotal[]}
|
||||
*/
|
||||
private getSectionTotalPeriods(sections: IBalanceSheetAccount[]): IBalanceSheetAccountTotal[] {
|
||||
return this.dateRangeSet.map((date, index) => {
|
||||
const amount = sumBy(sections, `totalPeriods[${index}].amount`);
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
const currencyCode = this.baseCurrency;
|
||||
|
||||
return { date, amount, formattedAmount, currencyCode };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the date range set from start to end date.
|
||||
* @param {IAccount} account
|
||||
* @return {IBalanceSheetAccountTotal[]}
|
||||
*/
|
||||
private getAccountTotalPeriods (account: IAccount): IBalanceSheetAccountTotal[] {
|
||||
return this.dateRangeSet.map((date) => {
|
||||
const amount = this.journalFinancial.getAccountBalance(
|
||||
account.id,
|
||||
date,
|
||||
this.comparatorDateType
|
||||
);
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
const currencyCode = this.baseCurrency;
|
||||
|
||||
return { amount, formattedAmount, currencyCode, date };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account total and total periods with account meta.
|
||||
* @param {IAccount} account -
|
||||
* @param {IBalanceSheetQuery} query -
|
||||
* @return {IBalanceSheetAccount}
|
||||
*/
|
||||
private balanceSheetAccountMapper(account: IAccount): IBalanceSheetAccount {
|
||||
// Calculates the closing balance of the given account in the specific date point.
|
||||
const amount = this.journalFinancial.getAccountBalance(
|
||||
account.id, this.query.toDate,
|
||||
);
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
|
||||
// Retrieve all entries that associated to the given account.
|
||||
const entries = this.journalFinancial.getAccountEntries(account.id)
|
||||
|
||||
return {
|
||||
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
|
||||
type: 'account',
|
||||
hasTransactions: entries.length > 0,
|
||||
// Total date periods.
|
||||
...this.query.displayColumnsType === 'date_periods' && ({
|
||||
totalPeriods: this.getAccountTotalPeriods(account),
|
||||
}),
|
||||
total: {
|
||||
amount,
|
||||
formattedAmount,
|
||||
currencyCode: this.baseCurrency,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Strcuture accounts related mapper.
|
||||
* @param {string[]} sectionAccountsTypes -
|
||||
* @param {IAccount[]} accounts -
|
||||
* @param {IBalanceSheetQuery} query -
|
||||
*/
|
||||
private structureRelatedAccountsMapper(
|
||||
sectionAccountsTypes: string[],
|
||||
accounts: IAccount & { type: IAccountType }[],
|
||||
): {
|
||||
children: IBalanceSheetAccount[],
|
||||
total: IBalanceSheetAccountTotal,
|
||||
} {
|
||||
const filteredAccounts = accounts
|
||||
// Filter accounts that associated to the section accounts types.
|
||||
.filter(
|
||||
(account) => sectionAccountsTypes.indexOf(account.type.childType) !== -1
|
||||
)
|
||||
.map((account) => this.balanceSheetAccountMapper(account))
|
||||
// Filter accounts that have no transaction when `noneTransactions` is on.
|
||||
.filter(
|
||||
(section: IBalanceSheetAccount) =>
|
||||
!(!section.hasTransactions && this.query.noneTransactions),
|
||||
)
|
||||
// Filter accounts that have zero total amount when `noneZero` is on.
|
||||
.filter(
|
||||
(section: IBalanceSheetAccount) =>
|
||||
!(section.total.amount === 0 && this.query.noneZero)
|
||||
);
|
||||
|
||||
// Gets total amount of the given accounts.
|
||||
const totalAmount = sumBy(filteredAccounts, 'total.amount');
|
||||
|
||||
return {
|
||||
children: flatToNestedArray(
|
||||
filteredAccounts,
|
||||
{ id: 'id', parentId: 'parentAccountId' }
|
||||
),
|
||||
total: {
|
||||
amount: totalAmount,
|
||||
formattedAmount: this.formatNumber(totalAmount),
|
||||
currencyCode: this.baseCurrency,
|
||||
},
|
||||
...(this.query.displayColumnsType === 'date_periods'
|
||||
? {
|
||||
totalPeriods: this.getSectionTotalPeriods(filteredAccounts),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Balance sheet structure mapper.
|
||||
* @param {IBalanceSheetStructureSection} structure -
|
||||
* @return {IBalanceSheetSection}
|
||||
*/
|
||||
private balanceSheetStructureMapper(
|
||||
structure: IBalanceSheetStructureSection,
|
||||
accounts: IAccount & { type: IAccountType }[],
|
||||
): IBalanceSheetSection {
|
||||
const result = {
|
||||
name: structure.name,
|
||||
sectionType: structure.sectionType,
|
||||
type: structure.type,
|
||||
...(structure.type === 'accounts_section'
|
||||
? {
|
||||
...this.structureRelatedAccountsMapper(
|
||||
structure._accountsTypesRelated,
|
||||
accounts,
|
||||
),
|
||||
}
|
||||
: (() => {
|
||||
const children = this.balanceSheetStructureWalker(
|
||||
structure.children,
|
||||
accounts,
|
||||
);
|
||||
return {
|
||||
children,
|
||||
total: this.getSectionTotal(children),
|
||||
};
|
||||
})()),
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance sheet structure walker.
|
||||
* @param {IBalanceSheetStructureSection[]} reportStructure -
|
||||
* @return {IBalanceSheetSection}
|
||||
*/
|
||||
private balanceSheetStructureWalker(
|
||||
reportStructure: IBalanceSheetStructureSection[],
|
||||
balanceSheetAccounts: IAccount & { type: IAccountType }[],
|
||||
): IBalanceSheetSection[] {
|
||||
return reportStructure
|
||||
.map((structure: IBalanceSheetStructureSection) =>
|
||||
this.balanceSheetStructureMapper(structure, balanceSheetAccounts)
|
||||
)
|
||||
// Filter the structure sections that have no children.
|
||||
.filter((structure: IBalanceSheetSection) =>
|
||||
structure.children.length > 0 || structure._forceShow
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve date range columns of the given query.
|
||||
* @param {IBalanceSheetQuery} query
|
||||
* @return {string[]}
|
||||
*/
|
||||
private dateRangeColumns(): string[] {
|
||||
return this.dateRangeSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve balance sheet columns in different display columns types.
|
||||
* @return {string[]}
|
||||
*/
|
||||
public reportColumns(): string[] {
|
||||
// Date range collection.
|
||||
return this.query.displayColumnsType === 'date_periods'
|
||||
? this.dateRangeColumns()
|
||||
: ['total'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve balance sheet statement data.
|
||||
* @return {IBalanceSheetSection[]}
|
||||
*/
|
||||
public reportData(): IBalanceSheetSection[] {
|
||||
return this.balanceSheetStructureWalker(
|
||||
BalanceSheetStructure,
|
||||
this.accounts,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
IBalanceSheetStatementService,
|
||||
IBalanceSheetQuery,
|
||||
IBalanceSheetStatement,
|
||||
} from 'interfaces';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import Journal from 'services/Accounting/JournalPoster';
|
||||
import BalanceSheetStatement from './BalanceSheet';
|
||||
|
||||
@Service()
|
||||
export default class BalanceSheetStatementService
|
||||
implements IBalanceSheetStatementService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
get defaultQuery(): IBalanceSheetQuery {
|
||||
return {
|
||||
displayColumnsType: 'total',
|
||||
displayColumnsBy: 'day',
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
numberFormat: {
|
||||
noCents: false,
|
||||
divideOn1000: false,
|
||||
},
|
||||
noneZero: false,
|
||||
noneTransactions: false,
|
||||
basis: 'cash',
|
||||
accountIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve balance sheet statement.
|
||||
* -------------
|
||||
* @param {number} tenantId
|
||||
* @param {IBalanceSheetQuery} query
|
||||
*
|
||||
* @return {IBalanceSheetStatement}
|
||||
*/
|
||||
public async balanceSheet(
|
||||
tenantId: number,
|
||||
query: IBalanceSheetQuery
|
||||
): Promise<IBalanceSheetStatement> {
|
||||
const {
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Settings tenant service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
this.logger.info('[balance_sheet] trying to calculate the report.', { filter, tenantId });
|
||||
|
||||
// Retrieve all accounts on the storage.
|
||||
const accounts = await accountRepository.allAccounts('type');
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieve all journal transactions based on the given query.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
fromDate: query.toDate,
|
||||
});
|
||||
// Transform transactions to journal collection.
|
||||
const transactionsJournal = Journal.fromTransactions(
|
||||
transactions,
|
||||
tenantId,
|
||||
accountsGraph,
|
||||
);
|
||||
// Balance sheet report instance.
|
||||
const balanceSheetInstanace = new BalanceSheetStatement(
|
||||
tenantId,
|
||||
filter,
|
||||
accounts,
|
||||
transactionsJournal,
|
||||
baseCurrency
|
||||
);
|
||||
// Balance sheet data.
|
||||
const balanceSheetData = balanceSheetInstanace.reportData();
|
||||
|
||||
// Retrieve balance sheet columns.
|
||||
const balanceSheetColumns = balanceSheetInstanace.reportColumns();
|
||||
|
||||
return {
|
||||
data: balanceSheetData,
|
||||
columns: balanceSheetColumns,
|
||||
query: filter,
|
||||
};
|
||||
}
|
||||
}
|
||||
16
server/src/services/FinancialStatements/FinancialSheet.ts
Normal file
16
server/src/services/FinancialStatements/FinancialSheet.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
formatNumber
|
||||
} from 'utils';
|
||||
|
||||
export default class FinancialSheet {
|
||||
numberFormat: { noCents: boolean, divideOn1000: boolean };
|
||||
|
||||
/**
|
||||
* Formating amount based on the given report query.
|
||||
* @param {number} number
|
||||
* @return {string}
|
||||
*/
|
||||
protected formatNumber(number): string {
|
||||
return formatNumber(number, this.numberFormat);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { pick } from 'lodash';
|
||||
import {
|
||||
IGeneralLedgerSheetQuery,
|
||||
IGeneralLedgerSheetAccount,
|
||||
IGeneralLedgerSheetAccountBalance,
|
||||
IGeneralLedgerSheetAccountTransaction,
|
||||
IAccount,
|
||||
IJournalPoster,
|
||||
IAccountType,
|
||||
IJournalEntry
|
||||
} from 'interfaces';
|
||||
import FinancialSheet from "../FinancialSheet";
|
||||
|
||||
export default class GeneralLedgerSheet extends FinancialSheet {
|
||||
tenantId: number;
|
||||
accounts: IAccount[];
|
||||
query: IGeneralLedgerSheetQuery;
|
||||
openingBalancesJournal: IJournalPoster;
|
||||
closingBalancesJournal: IJournalPoster;
|
||||
transactions: IJournalPoster;
|
||||
baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId -
|
||||
* @param {IAccount[]} accounts -
|
||||
* @param {IJournalPoster} transactions -
|
||||
* @param {IJournalPoster} openingBalancesJournal -
|
||||
* @param {IJournalPoster} closingBalancesJournal -
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
query: IGeneralLedgerSheetQuery,
|
||||
accounts: IAccount[],
|
||||
transactions: IJournalPoster,
|
||||
openingBalancesJournal: IJournalPoster,
|
||||
closingBalancesJournal: IJournalPoster,
|
||||
baseCurrency: string,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.accounts = accounts;
|
||||
this.transactions = transactions;
|
||||
this.openingBalancesJournal = openingBalancesJournal;
|
||||
this.closingBalancesJournal = closingBalancesJournal;
|
||||
this.baseCurrency = baseCurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping the account transactions to general ledger transactions of the given account.
|
||||
* @param {IAccount} account
|
||||
* @return {IGeneralLedgerSheetAccountTransaction[]}
|
||||
*/
|
||||
private accountTransactionsMapper(
|
||||
account: IAccount & { type: IAccountType }
|
||||
): 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', 'date']),
|
||||
amount,
|
||||
formattedAmount,
|
||||
currencyCode: this.baseCurrency,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account opening balance.
|
||||
* @param {IAccount} account
|
||||
* @return {IGeneralLedgerSheetAccountBalance}
|
||||
*/
|
||||
private accountOpeningBalance(account: IAccount): IGeneralLedgerSheetAccountBalance {
|
||||
const amount = this.openingBalancesJournal.getAccountBalance(account.id);
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
const currencyCode = this.baseCurrency;
|
||||
const date = this.query.fromDate;
|
||||
|
||||
return { amount, formattedAmount, currencyCode, date };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve account closing balance.
|
||||
* @param {IAccount} account
|
||||
* @return {IGeneralLedgerSheetAccountBalance}
|
||||
*/
|
||||
private accountClosingBalance(account: IAccount): IGeneralLedgerSheetAccountBalance {
|
||||
const amount = this.closingBalancesJournal.getAccountBalance(account.id);
|
||||
const formattedAmount = this.formatNumber(amount);
|
||||
const currencyCode = this.baseCurrency;
|
||||
const date = this.query.toDate;
|
||||
|
||||
return { amount, formattedAmount, currencyCode, date };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreive general ledger accounts sections.
|
||||
* @param {IAccount} account
|
||||
* @return {IGeneralLedgerSheetAccount}
|
||||
*/
|
||||
private accountMapper(
|
||||
account: IAccount & { type: IAccountType },
|
||||
): IGeneralLedgerSheetAccount {
|
||||
return {
|
||||
...pick(account, ['id', 'name', 'code', 'index', 'parentAccountId']),
|
||||
opening: this.accountOpeningBalance(account),
|
||||
transactions: this.accountTransactionsMapper(account),
|
||||
closing: this.accountClosingBalance(account),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve mapped accounts with general ledger transactions and opeing/closing balance.
|
||||
* @param {IAccount[]} accounts -
|
||||
* @return {IGeneralLedgerSheetAccount[]}
|
||||
*/
|
||||
private accountsWalker(
|
||||
accounts: IAccount & { type: IAccountType }[]
|
||||
): IGeneralLedgerSheetAccount[] {
|
||||
return accounts
|
||||
.map((account: IAccount & { type: IAccountType }) => this.accountMapper(account))
|
||||
|
||||
// Filter general ledger accounts that have no transactions when `noneTransactions` is on.
|
||||
.filter((generalLedgerAccount: IGeneralLedgerSheetAccount) => (
|
||||
!(generalLedgerAccount.transactions.length === 0 && this.query.noneTransactions)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve general ledger report data.
|
||||
* @return {IGeneralLedgerSheetAccount[]}
|
||||
*/
|
||||
public reportData(): IGeneralLedgerSheetAccount[] {
|
||||
return this.accountsWalker(this.accounts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Service, Inject } from "typedi";
|
||||
import moment from 'moment';
|
||||
import { ServiceError } from "exceptions";
|
||||
import { difference } from 'lodash';
|
||||
import { IGeneralLedgerSheetQuery } from 'interfaces';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import Journal from "services/Accounting/JournalPoster";
|
||||
import GeneralLedgerSheet from 'services/FinancialStatements/GeneralLedger/GeneralLedger';
|
||||
|
||||
const ERRORS = {
|
||||
ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND',
|
||||
};
|
||||
|
||||
@Service()
|
||||
export default class GeneralLedgerService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Defaults general ledger report filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
get defaultQuery() {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
basis: 'cash',
|
||||
numberFormat: {
|
||||
noCents: false,
|
||||
divideOn1000: false,
|
||||
},
|
||||
noneZero: false,
|
||||
accountsIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* ----------
|
||||
* @param {number} tenantId
|
||||
* @param {IGeneralLedgerSheetQuery} query
|
||||
* @return {IGeneralLedgerStatement}
|
||||
*/
|
||||
async generalLedger(tenantId: number, query: IGeneralLedgerSheetQuery):
|
||||
Promise<{
|
||||
data: any,
|
||||
query: IGeneralLedgerSheetQuery,
|
||||
}> {
|
||||
const {
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
this.logger.info('[general_ledger] trying to calculate the report.', { tenantId, filter })
|
||||
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
|
||||
// Retrieve all accounts from the storage.
|
||||
const accounts = await accountRepository.allAccounts('type');
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retreive journal transactions from/to the given date.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
fromDate: filter.fromDate,
|
||||
toDate: filter.toDate,
|
||||
});
|
||||
// Retreive opening balance credit/debit sumation.
|
||||
const openingBalanceTrans = await transactionsRepository.journal({
|
||||
toDate: filter.fromDate,
|
||||
sumationCreditDebit: true,
|
||||
});
|
||||
// Retreive closing balance credit/debit sumation.
|
||||
const closingBalanceTrans = await transactionsRepository.journal({
|
||||
toDate: filter.toDate,
|
||||
sumationCreditDebit: true,
|
||||
});
|
||||
|
||||
// Transform array transactions to journal collection.
|
||||
const transactionsJournal = Journal.fromTransactions(transactions, tenantId, accountsGraph);
|
||||
const openingTransJournal = Journal.fromTransactions(openingBalanceTrans, tenantId, accountsGraph);
|
||||
const closingTransJournal = Journal.fromTransactions(closingBalanceTrans, tenantId, accountsGraph);
|
||||
|
||||
// General ledger report instance.
|
||||
const generalLedgerInstance = new GeneralLedgerSheet(
|
||||
tenantId,
|
||||
filter,
|
||||
accounts,
|
||||
transactionsJournal,
|
||||
openingTransJournal,
|
||||
closingTransJournal,
|
||||
baseCurrency
|
||||
);
|
||||
// Retrieve general ledger report data.
|
||||
const reportData = generalLedgerInstance.reportData();
|
||||
|
||||
return {
|
||||
data: reportData,
|
||||
query: filter,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { sumBy, chain } from 'lodash';
|
||||
import {
|
||||
IJournalEntry,
|
||||
IJournalPoster,
|
||||
IJournalReportEntriesGroup,
|
||||
IJournalReportQuery,
|
||||
IJournalReport
|
||||
} from "interfaces";
|
||||
import FinancialSheet from "../FinancialSheet";
|
||||
|
||||
export default class JournalSheet extends FinancialSheet {
|
||||
tenantId: number;
|
||||
journal: IJournalPoster;
|
||||
query: IJournalReportQuery;
|
||||
baseCurrency: string;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
* @param {IJournalPoster} journal
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
query: IJournalReportQuery,
|
||||
journal: IJournalPoster,
|
||||
baseCurrency: string,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.journal = journal;
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.baseCurrency = baseCurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping journal entries groups.
|
||||
* @param {IJournalEntry[]} entriesGroup -
|
||||
* @param {string} key -
|
||||
* @return {IJournalReportEntriesGroup}
|
||||
*/
|
||||
entriesGroupMapper(
|
||||
entriesGroup: IJournalEntry[],
|
||||
key: string,
|
||||
): IJournalReportEntriesGroup {
|
||||
const totalCredit = sumBy(entriesGroup, 'credit');
|
||||
const totalDebit = sumBy(entriesGroup, 'debit');
|
||||
|
||||
return {
|
||||
id: key,
|
||||
entries: entriesGroup,
|
||||
|
||||
currencyCode: this.baseCurrency,
|
||||
|
||||
credit: totalCredit,
|
||||
debit: totalDebit,
|
||||
|
||||
formattedCredit: this.formatNumber(totalCredit),
|
||||
formattedDebit: this.formatNumber(totalDebit),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping the journal entries to entries groups.
|
||||
* @param {IJournalEntry[]} entries
|
||||
* @return {IJournalReportEntriesGroup[]}
|
||||
*/
|
||||
entriesWalker(entries: IJournalEntry[]): IJournalReportEntriesGroup[] {
|
||||
return chain(entries)
|
||||
.groupBy((entry) => `${entry.referenceId}-${entry.referenceType}`)
|
||||
.map((
|
||||
entriesGroup: IJournalEntry[],
|
||||
key: string
|
||||
) => this.entriesGroupMapper(entriesGroup, key))
|
||||
.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve journal report.
|
||||
* @return {IJournalReport}
|
||||
*/
|
||||
reportData(): IJournalReport {
|
||||
return {
|
||||
entries: this.entriesWalker(this.journal.entries),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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";
|
||||
|
||||
@Service()
|
||||
export default class JournalSheetService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Default journal sheet filter queyr.
|
||||
*/
|
||||
get defaultQuery() {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
fromRange: null,
|
||||
toRange: null,
|
||||
accountsIds: [],
|
||||
transactionTypes: [],
|
||||
numberFormat: {
|
||||
noCents: false,
|
||||
divideOn1000: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Journal sheet.
|
||||
* @param {number} tenantId
|
||||
* @param {IJournalSheetFilterQuery} query
|
||||
*/
|
||||
async journalSheet(
|
||||
tenantId: number,
|
||||
query: IJournalReportQuery,
|
||||
) {
|
||||
const {
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
this.logger.info('[journal] trying to calculate the report.', { tenantId, filter });
|
||||
|
||||
// Settings service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
|
||||
// Retrieve all accounts on the storage.
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieve all journal transactions based on the given query.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
fromDate: filter.fromDate,
|
||||
toDate: filter.toDate,
|
||||
transactionsTypes: filter.transactionTypes,
|
||||
fromAmount: filter.fromRange,
|
||||
toAmount: filter.toRange,
|
||||
});
|
||||
|
||||
// Transform the transactions array to journal collection.
|
||||
const transactionsJournal = Journal.fromTransactions(transactions, tenantId, accountsGraph);
|
||||
|
||||
// Journal report instance.
|
||||
const journalSheetInstance = new JournalSheet(
|
||||
tenantId,
|
||||
filter,
|
||||
transactionsJournal,
|
||||
baseCurrency
|
||||
);
|
||||
// Retrieve journal report columns.
|
||||
const journalSheetData = journalSheetInstance.reportData();
|
||||
|
||||
return journalSheetData;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import moment from 'moment';
|
||||
import Journal from 'services/Accounting/JournalPoster';
|
||||
import { IProfitLossSheetQuery } from 'interfaces';
|
||||
import ProfitLossSheet from './ProfitLossSheet';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import AccountsService from 'services/Accounts/AccountsService';
|
||||
|
||||
// Profit/Loss sheet service.
|
||||
@Service()
|
||||
export default class ProfitLossSheetService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
accountsService: AccountsService;
|
||||
|
||||
/**
|
||||
* Default sheet filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
get defaultQuery(): IProfitLossSheetQuery {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
numberFormat: {
|
||||
noCents: false,
|
||||
divideOn1000: false,
|
||||
},
|
||||
basis: 'accural',
|
||||
noneZero: false,
|
||||
noneTransactions: false,
|
||||
displayColumnsType: 'total',
|
||||
displayColumnsBy: 'month',
|
||||
accountsIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve profit/loss sheet statement.
|
||||
* @param {number} tenantId
|
||||
* @param {IProfitLossSheetQuery} query
|
||||
* @return { }
|
||||
*/
|
||||
async profitLossSheet(tenantId: number, query: IProfitLossSheetQuery) {
|
||||
const {
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
this.logger.info('[profit_loss_sheet] trying to calculate the report.', { tenantId, filter });
|
||||
|
||||
// Get the given accounts or throw not found service error.
|
||||
if (filter.accountsIds.length > 0) {
|
||||
await this.accountsService.getAccountsOrThrowError(tenantId, filter.accountsIds);
|
||||
}
|
||||
// Settings tenant service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
|
||||
|
||||
// Retrieve all accounts on the storage.
|
||||
const accounts = await accountRepository.allAccounts('type');
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieve all journal transactions based on the given query.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
fromDate: query.fromDate,
|
||||
toDate: query.toDate,
|
||||
});
|
||||
// Transform transactions to journal collection.
|
||||
const transactionsJournal = Journal.fromTransactions(transactions, tenantId, accountsGraph);
|
||||
|
||||
// Profit/Loss report instance.
|
||||
const profitLossInstance = new ProfitLossSheet(
|
||||
tenantId,
|
||||
filter,
|
||||
accounts,
|
||||
transactionsJournal,
|
||||
baseCurrency
|
||||
);
|
||||
// Profit/loss report data and collumns.
|
||||
const profitLossData = profitLossInstance.reportData();
|
||||
const profitLossColumns = profitLossInstance.reportColumns();
|
||||
|
||||
return {
|
||||
data: profitLossData,
|
||||
columns: profitLossColumns,
|
||||
query: filter,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
|
||||
import {
|
||||
ITrialBalanceSheetQuery,
|
||||
ITrialBalanceAccount,
|
||||
IAccount,
|
||||
IAccountType,
|
||||
} from 'interfaces';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
import { flatToNestedArray } from 'utils';
|
||||
|
||||
export default class TrialBalanceSheet extends FinancialSheet{
|
||||
tenantId: number;
|
||||
query: ITrialBalanceSheetQuery;
|
||||
accounts: IAccount & { type: IAccountType }[];
|
||||
journalFinancial: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {number} tenantId
|
||||
* @param {ITrialBalanceSheetQuery} query
|
||||
* @param {IAccount[]} accounts
|
||||
* @param journalFinancial
|
||||
*/
|
||||
constructor(
|
||||
tenantId: number,
|
||||
query: ITrialBalanceSheetQuery,
|
||||
accounts: IAccount & { type: IAccountType }[],
|
||||
journalFinancial: any
|
||||
) {
|
||||
super();
|
||||
|
||||
this.tenantId = tenantId;
|
||||
this.query = query;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
this.accounts = accounts;
|
||||
this.journalFinancial = journalFinancial;
|
||||
this.numberFormat = this.query.numberFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Account mapper.
|
||||
* @param {IAccount} account
|
||||
*/
|
||||
private accountMapper(account: IAccount & { type: IAccountType }): ITrialBalanceAccount {
|
||||
const trial = this.journalFinancial.getTrialBalanceWithDepands(account.id);
|
||||
|
||||
// Retrieve all entries that associated to the given account.
|
||||
const entries = this.journalFinancial.getAccountEntries(account.id)
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
parentAccountId: account.parentAccountId,
|
||||
name: account.name,
|
||||
code: account.code,
|
||||
accountNormal: account.type.normal,
|
||||
hasTransactions: entries.length > 0,
|
||||
|
||||
credit: trial.credit,
|
||||
debit: trial.debit,
|
||||
balance: trial.balance,
|
||||
|
||||
formattedCredit: this.formatNumber(trial.credit),
|
||||
formattedDebit: this.formatNumber(trial.debit),
|
||||
formattedBalance: this.formatNumber(trial.balance),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounts walker.
|
||||
* @param {IAccount[]} accounts
|
||||
*/
|
||||
private accountsWalker(
|
||||
accounts: IAccount & { type: IAccountType }[]
|
||||
): ITrialBalanceAccount[] {
|
||||
const flattenAccounts = accounts
|
||||
// Mapping the trial balance accounts sections.
|
||||
.map((account: IAccount & { type: IAccountType }) => this.accountMapper(account))
|
||||
|
||||
// Filter accounts that have no transaction when `noneTransactions` is on.
|
||||
.filter((trialAccount: ITrialBalanceAccount): boolean =>
|
||||
!(!trialAccount.hasTransactions && this.query.noneTransactions),
|
||||
)
|
||||
// Filter accounts that have zero total amount when `noneZero` is on.
|
||||
.filter(
|
||||
(trialAccount: ITrialBalanceAccount): boolean =>
|
||||
!(trialAccount.credit === 0 && trialAccount.debit === 0 && this.query.noneZero)
|
||||
);
|
||||
|
||||
return flatToNestedArray(
|
||||
flattenAccounts,
|
||||
{ id: 'id', parentId: 'parentAccountId' },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve trial balance sheet statement data.
|
||||
*/
|
||||
public reportData() {
|
||||
return this.accountsWalker(this.accounts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Service, Inject } from "typedi";
|
||||
import moment from 'moment';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ITrialBalanceSheetQuery, ITrialBalanceStatement } from 'interfaces';
|
||||
import TrialBalanceSheet from "./TrialBalanceSheet";
|
||||
import Journal from 'services/Accounting/JournalPoster';
|
||||
|
||||
@Service()
|
||||
export default class TrialBalanceSheetService {
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
/**
|
||||
* Defaults trial balance sheet filter query.
|
||||
* @return {IBalanceSheetQuery}
|
||||
*/
|
||||
get defaultQuery(): ITrialBalanceSheetQuery {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
numberFormat: {
|
||||
noCents: false,
|
||||
divideOn1000: false,
|
||||
},
|
||||
basis: 'accural',
|
||||
noneZero: false,
|
||||
noneTransactions: false,
|
||||
accountIds: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve trial balance sheet statement.
|
||||
* -------------
|
||||
* @param {number} tenantId
|
||||
* @param {IBalanceSheetQuery} query
|
||||
*
|
||||
* @return {IBalanceSheetStatement}
|
||||
*/
|
||||
public async trialBalanceSheet(
|
||||
tenantId: number,
|
||||
query: ITrialBalanceSheetQuery,
|
||||
): Promise<ITrialBalanceStatement> {
|
||||
|
||||
const filter = {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
const {
|
||||
accountRepository,
|
||||
transactionsRepository,
|
||||
} = this.tenancy.repositories(tenantId);
|
||||
|
||||
this.logger.info('[trial_balance_sheet] trying to calcualte the report.', { tenantId, filter });
|
||||
|
||||
// Retrieve all accounts on the storage.
|
||||
const accounts = await accountRepository.allAccounts('type');
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Retrieve all journal transactions based on the given query.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
fromDate: query.fromDate,
|
||||
toDate: query.toDate,
|
||||
sumationCreditDebit: true,
|
||||
});
|
||||
// Transform transactions array to journal collection.
|
||||
const transactionsJournal = Journal.fromTransactions(transactions, tenantId, accountsGraph);
|
||||
|
||||
// Trial balance report instance.
|
||||
const trialBalanceInstance = new TrialBalanceSheet(
|
||||
tenantId,
|
||||
filter,
|
||||
accounts,
|
||||
transactionsJournal,
|
||||
);
|
||||
// Trial balance sheet data.
|
||||
const trialBalanceSheetData = trialBalanceInstance.reportData();
|
||||
|
||||
return {
|
||||
data: trialBalanceSheetData,
|
||||
query: filter,
|
||||
}
|
||||
}
|
||||
}
|
||||
13
server/src/services/FinancialStatements/utils.ts
Normal file
13
server/src/services/FinancialStatements/utils.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
export const formatNumber = (balance, { noCents, divideOn1000 }): string => {
|
||||
let formattedBalance: number = parseFloat(balance);
|
||||
|
||||
if (noCents) {
|
||||
formattedBalance = parseInt(formattedBalance, 10);
|
||||
}
|
||||
if (divideOn1000) {
|
||||
formattedBalance /= 1000;
|
||||
}
|
||||
return formattedBalance;
|
||||
};
|
||||
@@ -356,7 +356,7 @@ export default class ManualJournalsService implements IManualJournalsService {
|
||||
// Triggers `onManualJournalCreated` event.
|
||||
this.eventDispatcher.dispatch(events.manualJournals.onCreated, {
|
||||
tenantId,
|
||||
manualJournal,
|
||||
manualJournal: { ...manualJournal, entries: manualJournalObj.entries },
|
||||
});
|
||||
this.logger.info(
|
||||
'[manual_journal] the manual journal inserted successfully.',
|
||||
|
||||
@@ -81,7 +81,7 @@ export default class HasTenancyService {
|
||||
setI18nLocals(tenantId: number, locals: any) {
|
||||
return this.singletonService(tenantId, 'i18n', () => {
|
||||
return locals;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -211,6 +211,18 @@ const convertEmptyStringToNull = (value) => {
|
||||
: value;
|
||||
};
|
||||
|
||||
const formatNumber = (balance, { noCents = false, divideOn1000 = false }) => {
|
||||
let formattedBalance = parseFloat(balance);
|
||||
|
||||
if (noCents) {
|
||||
formattedBalance = parseInt(formattedBalance, 10);
|
||||
}
|
||||
if (divideOn1000) {
|
||||
formattedBalance /= 1000;
|
||||
}
|
||||
return formattedBalance + '';
|
||||
};
|
||||
|
||||
export {
|
||||
hashPassword,
|
||||
origin,
|
||||
@@ -229,4 +241,5 @@ export {
|
||||
getDefinedOptions,
|
||||
entriesAmountDiff,
|
||||
convertEmptyStringToNull,
|
||||
formatNumber
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user