mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +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": [
|
"watch": [
|
||||||
"src",
|
"src",
|
||||||
".env"
|
".env"
|
||||||
],
|
],
|
||||||
"ext": "js,ts,json",
|
|
||||||
"ignore": [
|
"ignore": [
|
||||||
|
".git",
|
||||||
"src/**/*.spec.ts"
|
"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": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
"start": "cross-env NODE_PATH=./src nodemon",
|
"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>",
|
"author": "Ahmed Bouhuolia, <a.bouhuolia@gmail.com>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
"node-cache": "^4.2.1",
|
"node-cache": "^4.2.1",
|
||||||
"nodemailer": "^6.3.0",
|
"nodemailer": "^6.3.0",
|
||||||
"nodemon": "^1.19.1",
|
"nodemon": "^1.19.1",
|
||||||
|
"object-hash": "^2.0.3",
|
||||||
"objection": "^2.0.10",
|
"objection": "^2.0.10",
|
||||||
"objection-filter": "^4.0.1",
|
"objection-filter": "^4.0.1",
|
||||||
"objection-soft-delete": "^1.0.7",
|
"objection-soft-delete": "^1.0.7",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Response, Request, NextFunction } from 'express';
|
|||||||
import { matchedData, validationResult } from "express-validator";
|
import { matchedData, validationResult } from "express-validator";
|
||||||
import { camelCase, snakeCase, omit } from "lodash";
|
import { camelCase, snakeCase, omit } from "lodash";
|
||||||
import { mapKeysDeep } from 'utils'
|
import { mapKeysDeep } from 'utils'
|
||||||
|
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||||
|
|
||||||
export default class BaseController {
|
export default class BaseController {
|
||||||
|
|
||||||
@@ -63,4 +64,8 @@ export default class BaseController {
|
|||||||
transfromToResponse(data: any) {
|
transfromToResponse(data: any) {
|
||||||
return mapKeysDeep(data, (v, k) => snakeCase(k));
|
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);
|
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
|
||||||
}
|
}
|
||||||
try {
|
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({
|
return res.status(200).send({
|
||||||
manual_journals: manualJournals,
|
manual_journals: manualJournals,
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export default () => {
|
|||||||
dashboard.use('/items', Container.get(Items).router());
|
dashboard.use('/items', Container.get(Items).router());
|
||||||
dashboard.use('/item_categories', Container.get(ItemCategories).router());
|
dashboard.use('/item_categories', Container.get(ItemCategories).router());
|
||||||
dashboard.use('/expenses', Container.get(Expenses).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('/customers', Container.get(Customers).router());
|
||||||
dashboard.use('/vendors', Container.get(Vendors).router());
|
dashboard.use('/vendors', Container.get(Vendors).router());
|
||||||
dashboard.use('/sales', Container.get(Sales).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',
|
normal: 'debit',
|
||||||
root_type: 'expenses',
|
root_type: 'expenses',
|
||||||
child_type: 'expenses',
|
child_type: 'expenses',
|
||||||
balance_sheet: true,
|
balance_sheet: false,
|
||||||
income_sheet: false,
|
income_sheet: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 8,
|
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 {
|
export interface IAccount {
|
||||||
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
code: string,
|
code: string,
|
||||||
|
index: number,
|
||||||
description: string,
|
description: string,
|
||||||
accountTypeId: number,
|
accountTypeId: number,
|
||||||
parentAccountId: number,
|
parentAccountId: number,
|
||||||
@@ -20,6 +22,8 @@ export interface IAccount {
|
|||||||
predefined: boolean,
|
predefined: boolean,
|
||||||
amount: number,
|
amount: number,
|
||||||
currencyCode: string,
|
currencyCode: string,
|
||||||
|
transactions?: any[],
|
||||||
|
type?: any[],
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IAccountsFilter extends IDynamicListFilterDTO {
|
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,
|
shippingAddressState?: string,
|
||||||
};
|
};
|
||||||
export interface IContact extends IContactAddress{
|
export interface IContact extends IContactAddress{
|
||||||
|
id?: number,
|
||||||
contactService: 'customer' | 'vendor',
|
contactService: 'customer' | 'vendor',
|
||||||
contactType: string,
|
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 {
|
export interface IJournalEntry {
|
||||||
|
id: number,
|
||||||
index?: number,
|
index?: number,
|
||||||
|
|
||||||
date: Date,
|
date: Date,
|
||||||
@@ -18,6 +19,8 @@ export interface IJournalEntry {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface IJournalPoster {
|
export interface IJournalPoster {
|
||||||
|
entries: IJournalEntry[],
|
||||||
|
|
||||||
credit(entry: IJournalEntry): void;
|
credit(entry: IJournalEntry): void;
|
||||||
debit(entry: IJournalEntry): void;
|
debit(entry: IJournalEntry): void;
|
||||||
|
|
||||||
@@ -26,6 +29,9 @@ export interface IJournalPoster {
|
|||||||
saveEntries(): void;
|
saveEntries(): void;
|
||||||
saveBalance(): void;
|
saveBalance(): void;
|
||||||
deleteEntries(): void;
|
deleteEntries(): void;
|
||||||
|
|
||||||
|
getAccountBalance(accountId: number, closingDate?: Date | string, dateType?: string): number;
|
||||||
|
getAccountEntries(accountId: number): IJournalEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TEntryType = 'credit' | 'debit';
|
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 './Currency';
|
||||||
export * from './ExchangeRate';
|
export * from './ExchangeRate';
|
||||||
export * from './Media';
|
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 ViewRepository from 'repositories/ViewRepository';
|
||||||
import ViewRoleRepository from 'repositories/ViewRoleRepository';
|
import ViewRoleRepository from 'repositories/ViewRoleRepository';
|
||||||
import ContactRepository from 'repositories/ContactRepository';
|
import ContactRepository from 'repositories/ContactRepository';
|
||||||
|
import AccountTransactionsRepository from 'repositories/AccountTransactionRepository';
|
||||||
|
|
||||||
export default (tenantId: number) => {
|
export default (tenantId: number) => {
|
||||||
return {
|
return {
|
||||||
accountRepository: new AccountRepository(tenantId),
|
accountRepository: new AccountRepository(tenantId),
|
||||||
|
transactionsRepository: new AccountTransactionsRepository(tenantId),
|
||||||
accountTypeRepository: new AccountTypeRepository(tenantId),
|
accountTypeRepository: new AccountTypeRepository(tenantId),
|
||||||
customerRepository: new CustomerRepository(tenantId),
|
customerRepository: new CustomerRepository(tenantId),
|
||||||
vendorRepository: new VendorRepository(tenantId),
|
vendorRepository: new VendorRepository(tenantId),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default class AccountTransaction extends TenantModel {
|
|||||||
* @param {number[]} accountsIds
|
* @param {number[]} accountsIds
|
||||||
*/
|
*/
|
||||||
filterAccounts(query, accountsIds) {
|
filterAccounts(query, accountsIds) {
|
||||||
if (accountsIds.length > 0) {
|
if (Array.isArray(accountsIds) && accountsIds.length > 0) {
|
||||||
query.whereIn('account_id', accountsIds);
|
query.whereIn('account_id', accountsIds);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -63,9 +63,11 @@ export default class AccountTransaction extends TenantModel {
|
|||||||
q.where('credit', '<=', toAmount);
|
q.where('credit', '<=', toAmount);
|
||||||
q.orWhere('debit', '<=', toAmount);
|
q.orWhere('debit', '<=', toAmount);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sumationCreditDebit(query) {
|
sumationCreditDebit(query) {
|
||||||
|
query.select(['accountId']);
|
||||||
|
|
||||||
query.sum('credit as credit');
|
query.sum('credit as credit');
|
||||||
query.sum('debit as debit');
|
query.sum('debit as debit');
|
||||||
query.groupBy('account_id');
|
query.groupBy('account_id');
|
||||||
@@ -76,6 +78,16 @@ export default class AccountTransaction extends TenantModel {
|
|||||||
filterContactIds(query, contactIds) {
|
filterContactIds(query, contactIds) {
|
||||||
query.whereIn('contact_id', 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 TenantRepository from 'repositories/TenantRepository';
|
||||||
import { IAccount } from 'interfaces';
|
import { IAccount } from 'interfaces';
|
||||||
import { Account } from 'models';
|
|
||||||
|
|
||||||
export default class AccountRepository extends TenantRepository {
|
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.
|
* Retrieve accounts dependency graph.
|
||||||
* @returns {}
|
* @returns {}
|
||||||
@@ -27,8 +9,9 @@ export default class AccountRepository extends TenantRepository {
|
|||||||
async getDependencyGraph() {
|
async getDependencyGraph() {
|
||||||
const { Account } = this.models;
|
const { Account } = this.models;
|
||||||
const accounts = await this.allAccounts();
|
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);
|
return Account.toDependencyGraph(accounts);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -37,10 +20,13 @@ export default class AccountRepository extends TenantRepository {
|
|||||||
* Retrieve all accounts on the storage.
|
* Retrieve all accounts on the storage.
|
||||||
* @return {IAccount[]}
|
* @return {IAccount[]}
|
||||||
*/
|
*/
|
||||||
allAccounts(): IAccount[] {
|
allAccounts(withRelations?: string|string[]): IAccount[] {
|
||||||
const { Account } = this.models;
|
const { Account } = this.models;
|
||||||
return this.cache.get('accounts', async () => {
|
const cacheKey = this.getCacheKey('accounts.depGraph', withRelations);
|
||||||
return Account.query();
|
|
||||||
|
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 {
|
getBySlug(slug: string): IAccount {
|
||||||
const { Account } = this.models;
|
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);
|
return Account.query().findOne('slug', slug);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -63,7 +51,9 @@ export default class AccountRepository extends TenantRepository {
|
|||||||
*/
|
*/
|
||||||
findById(id: number): IAccount {
|
findById(id: number): IAccount {
|
||||||
const { Account } = this.models;
|
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);
|
return Account.query().findById(id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -75,7 +65,11 @@ export default class AccountRepository extends TenantRepository {
|
|||||||
*/
|
*/
|
||||||
findByIds(accountsIds: number[]) {
|
findByIds(accountsIds: number[]) {
|
||||||
const { Account } = this.models;
|
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.
|
* Flush repository cache.
|
||||||
*/
|
*/
|
||||||
flushCache(): void {
|
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';
|
import { IAccountType } from 'interfaces';
|
||||||
|
|
||||||
export default class AccountTypeRepository extends TenantRepository {
|
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.
|
* Retrieve all accounts types.
|
||||||
* @return {IAccountType[]}
|
* @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';
|
import { IContact } from 'interfaces';
|
||||||
|
|
||||||
export default class ContactRepository extends TenantRepository {
|
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.
|
* Retrieve the given contact model.
|
||||||
* @param {number} contactId
|
* @param {number} contactId
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import TenantRepository from "./TenantRepository";
|
import TenantRepository from "./TenantRepository";
|
||||||
|
|
||||||
export default class CustomerRepository extends TenantRepository {
|
export default class CustomerRepository extends TenantRepository {
|
||||||
models: any;
|
all() {
|
||||||
cache: any;
|
const { Contact } = this.models;
|
||||||
|
|
||||||
/**
|
return this.cache.get('customers', () => {
|
||||||
* Constructor method.
|
return Contact.query().modify('customer');
|
||||||
* @param {number} tenantId
|
});
|
||||||
*/
|
|
||||||
constructor(tenantId: number) {
|
|
||||||
super(tenantId);
|
|
||||||
|
|
||||||
this.models = this.tenancy.models(tenantId);
|
|
||||||
this.cache = this.tenancy.cache(tenantId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,21 +3,6 @@ import { IExpense } from 'interfaces';
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
export default class ExpenseRepository extends TenantRepository {
|
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.
|
* Retrieve the given expense by id.
|
||||||
* @param {number} expenseId
|
* @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 { Container } from 'typedi';
|
||||||
import TenancyService from 'services/Tenancy/TenancyService';
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
|
import CachableRepository from './CachableRepository';
|
||||||
|
|
||||||
export default class TenantRepository {
|
export default class TenantRepository extends CachableRepository {
|
||||||
|
repositoryName: string;
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
tenancy: TenancyService;
|
tenancy: TenancyService;
|
||||||
|
modelsInstance: any;
|
||||||
|
repositoriesInstance: any;
|
||||||
|
cacheInstance: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
*/
|
*/
|
||||||
constructor(tenantId: number) {
|
constructor(tenantId: number) {
|
||||||
|
super();
|
||||||
|
|
||||||
this.tenantId = tenantId;
|
this.tenantId = tenantId;
|
||||||
this.tenancy = Container.get(TenancyService);
|
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 {
|
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.
|
* Retrieve vendor details of the given id.
|
||||||
@@ -68,7 +55,6 @@ export default class VendorRepository extends TenantRepository {
|
|||||||
[changeMethod]('balance', Math.abs(amount));
|
[changeMethod]('balance', Math.abs(amount));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async changeDiffBalance(
|
async changeDiffBalance(
|
||||||
vendorId: number,
|
vendorId: number,
|
||||||
amount: number,
|
amount: number,
|
||||||
|
|||||||
@@ -2,22 +2,6 @@ import { IView } from 'interfaces';
|
|||||||
import TenantRepository from 'repositories/TenantRepository';
|
import TenantRepository from 'repositories/TenantRepository';
|
||||||
|
|
||||||
export default class ViewRepository extends 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.
|
* Retrieve view model by the given id.
|
||||||
|
|||||||
@@ -2,23 +2,6 @@ import { omit } from 'lodash';
|
|||||||
import TenantRepository from 'repositories/TenantRepository';
|
import TenantRepository from 'repositories/TenantRepository';
|
||||||
|
|
||||||
export default class ViewRoleRepository extends 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) {
|
allByView(viewId: number) {
|
||||||
const { ViewRole } = this.models;
|
const { ViewRole } = this.models;
|
||||||
|
|||||||
@@ -1,207 +1,19 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { IJournalPoster } from 'interfaces';
|
||||||
|
|
||||||
export default class JournalFinancial {
|
export default class JournalFinancial {
|
||||||
accountsBalanceTable: { [key: number]: number; } = {};
|
journal: IJournalPoster;
|
||||||
|
|
||||||
|
accountsDepGraph: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the closing balance for the given account and closing date.
|
* Journal poster.
|
||||||
* @param {Number} accountId -
|
* @param {IJournalPoster} journal
|
||||||
* @param {Date} closingDate -
|
|
||||||
* @param {string} dataType? -
|
|
||||||
* @return {number}
|
|
||||||
*/
|
*/
|
||||||
getClosingBalance(
|
constructor(journal: IJournalPoster) {
|
||||||
accountId: number,
|
this.journal = journal;
|
||||||
closingDate: Date|string,
|
this.accountsDepGraph = this.journal.accountsDepGraph;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { omit } from 'lodash';
|
import { omit, get } from 'lodash';
|
||||||
|
import moment from 'moment';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import JournalEntry from 'services/Accounting/JournalEntry';
|
import JournalEntry from 'services/Accounting/JournalEntry';
|
||||||
import TenancyService from 'services/Tenancy/TenancyService';
|
import TenancyService from 'services/Tenancy/TenancyService';
|
||||||
@@ -22,18 +23,25 @@ export default class JournalPoster implements IJournalPoster {
|
|||||||
balancesChange: IAccountsChange = {};
|
balancesChange: IAccountsChange = {};
|
||||||
accountsDepGraph: IAccountsChange = {};
|
accountsDepGraph: IAccountsChange = {};
|
||||||
|
|
||||||
|
accountsBalanceTable: { [key: number]: number; } = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Journal poster constructor.
|
* Journal poster constructor.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
|
accountsGraph?: any,
|
||||||
) {
|
) {
|
||||||
this.initTenancy();
|
this.initTenancy();
|
||||||
|
|
||||||
this.tenantId = tenantId;
|
this.tenantId = tenantId;
|
||||||
this.models = this.tenancy.models(tenantId);
|
this.models = this.tenancy.models(tenantId);
|
||||||
this.repositories = this.tenancy.repositories(tenantId);
|
this.repositories = this.tenancy.repositories(tenantId);
|
||||||
|
|
||||||
|
if (accountsGraph) {
|
||||||
|
this.accountsDepGraph = accountsGraph;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,10 +62,13 @@ export default class JournalPoster implements IJournalPoster {
|
|||||||
* @private
|
* @private
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
private async initializeAccountsDepGraph(): Promise<void> {
|
public async initAccountsDepGraph(): Promise<void> {
|
||||||
const { accountRepository } = this.repositories;
|
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>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
public async saveBalance() {
|
public async saveBalance() {
|
||||||
await this.initializeAccountsDepGraph();
|
await this.initAccountsDepGraph();
|
||||||
|
|
||||||
const { Account } = this.models;
|
const { Account } = this.models;
|
||||||
const accountsChange = this.balanceChangeWithDepends(this.balancesChange);
|
const accountsChange = this.balanceChangeWithDepends(this.balancesChange);
|
||||||
@@ -311,15 +322,17 @@ export default class JournalPoster implements IJournalPoster {
|
|||||||
* Load fetched accounts journal entries.
|
* Load fetched accounts journal entries.
|
||||||
* @param {IJournalEntry[]} entries -
|
* @param {IJournalEntry[]} entries -
|
||||||
*/
|
*/
|
||||||
loadEntries(entries: IJournalEntry[]): void {
|
fromTransactions(transactions) {
|
||||||
entries.forEach((entry: IJournalEntry) => {
|
transactions.forEach((transaction) => {
|
||||||
this.entries.push({
|
this.entries.push({
|
||||||
...entry,
|
...transaction,
|
||||||
account: entry.account ? entry.account.id : entry.accountId,
|
account: transaction.accountId,
|
||||||
|
accountNormal: get(transaction, 'account.type.normal'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the entries balance change.
|
* Calculates the entries balance change.
|
||||||
* @public
|
* @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
|
* @param {number[]} accountsIds
|
||||||
* @return {IAccount[]}
|
* @return {IAccount[]}
|
||||||
*/
|
*/
|
||||||
private async getAccountsOrThrowError(
|
public async getAccountsOrThrowError(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
accountsIds: number[]
|
accountsIds: number[]
|
||||||
): Promise<IAccount[]> {
|
): Promise<IAccount[]> {
|
||||||
@@ -521,8 +521,7 @@ export default class AccountsService {
|
|||||||
* -----------
|
* -----------
|
||||||
* Precedures.
|
* Precedures.
|
||||||
* -----------
|
* -----------
|
||||||
* - Transfer the given account transactions to another account
|
* - Transfer the given account transactions to another account with the same root type.
|
||||||
* with the same root type.
|
|
||||||
* - Delete the given account.
|
* - Delete the given account.
|
||||||
* -------
|
* -------
|
||||||
* @param {number} tenantId -
|
* @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.
|
// Triggers `onManualJournalCreated` event.
|
||||||
this.eventDispatcher.dispatch(events.manualJournals.onCreated, {
|
this.eventDispatcher.dispatch(events.manualJournals.onCreated, {
|
||||||
tenantId,
|
tenantId,
|
||||||
manualJournal,
|
manualJournal: { ...manualJournal, entries: manualJournalObj.entries },
|
||||||
});
|
});
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
'[manual_journal] the manual journal inserted successfully.',
|
'[manual_journal] the manual journal inserted successfully.',
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default class HasTenancyService {
|
|||||||
setI18nLocals(tenantId: number, locals: any) {
|
setI18nLocals(tenantId: number, locals: any) {
|
||||||
return this.singletonService(tenantId, 'i18n', () => {
|
return this.singletonService(tenantId, 'i18n', () => {
|
||||||
return locals;
|
return locals;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -211,6 +211,18 @@ const convertEmptyStringToNull = (value) => {
|
|||||||
: 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 {
|
export {
|
||||||
hashPassword,
|
hashPassword,
|
||||||
origin,
|
origin,
|
||||||
@@ -229,4 +241,5 @@ export {
|
|||||||
getDefinedOptions,
|
getDefinedOptions,
|
||||||
entriesAmountDiff,
|
entriesAmountDiff,
|
||||||
convertEmptyStringToNull,
|
convertEmptyStringToNull,
|
||||||
|
formatNumber
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user