mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 05:40:31 +00:00
feat: Cachable and date session model.
This commit is contained in:
@@ -42,8 +42,8 @@ exports.seed = (knex) => {
|
||||
name: 'Equity',
|
||||
normal: 'credit',
|
||||
root_type: 'equity',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
|
||||
@@ -192,7 +192,7 @@ export default {
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const { Account } = req.models;
|
||||
const account = await Account.query().where('id', id).first();
|
||||
const account = await Account.query().remember().where('id', id).first();
|
||||
|
||||
if (!account) {
|
||||
return res.boom.notFound();
|
||||
@@ -273,6 +273,7 @@ export default {
|
||||
const errorReasons = [];
|
||||
|
||||
const accountsResource = await Resource.query()
|
||||
.remember()
|
||||
.where('name', 'accounts')
|
||||
.withGraphFetched('fields')
|
||||
.first();
|
||||
@@ -294,6 +295,8 @@ export default {
|
||||
builder.withGraphFetched('roles.field');
|
||||
builder.withGraphFetched('columns');
|
||||
builder.first();
|
||||
|
||||
builder.remember();
|
||||
});
|
||||
const dynamicFilter = new DynamicFilter(Account.tableName);
|
||||
|
||||
@@ -335,13 +338,15 @@ export default {
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const accounts = await Account.query().onBuild((builder) => {
|
||||
builder.modify('filterAccountTypes', filter.account_types);
|
||||
builder.withGraphFetched('type');
|
||||
builder.withGraphFetched('balance');
|
||||
const accounts = await Account.query()
|
||||
.remember()
|
||||
.onBuild((builder) => {
|
||||
builder.modify('filterAccountTypes', filter.account_types);
|
||||
builder.withGraphFetched('type');
|
||||
builder.withGraphFetched('balance');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
});
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
});
|
||||
|
||||
const nestedAccounts = new NestedSet(accounts, { parentId: 'parentAccountId' });
|
||||
const nestedSetAccounts = nestedAccounts.toTree();
|
||||
|
||||
@@ -107,6 +107,7 @@ export default {
|
||||
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)
|
||||
@@ -197,6 +198,7 @@ export default {
|
||||
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')
|
||||
@@ -206,11 +208,13 @@ export default {
|
||||
});
|
||||
|
||||
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');
|
||||
@@ -226,7 +230,7 @@ export default {
|
||||
|
||||
const items = accounts
|
||||
.filter((account) => (
|
||||
account.transactions.length > 0 || filter.none_zero
|
||||
account.transactions.length > 0 || !filter.none_zero
|
||||
))
|
||||
.map((account) => ({
|
||||
...pick(account, ['id', 'name', 'code', 'index']),
|
||||
@@ -248,11 +252,11 @@ export default {
|
||||
],
|
||||
opening: {
|
||||
date: filter.from_date,
|
||||
amount: opeingBalanceCollection.getClosingBalance(account.id),
|
||||
amount: formatNumber(opeingBalanceCollection.getClosingBalance(account.id)),
|
||||
},
|
||||
closing: {
|
||||
date: filter.to_date,
|
||||
amount: closingBalanceCollection.getClosingBalance(account.id),
|
||||
amount: formatNumber(closingBalanceCollection.getClosingBalance(account.id)),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -276,6 +280,8 @@ export default {
|
||||
.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) {
|
||||
@@ -298,13 +304,20 @@ export default {
|
||||
},
|
||||
none_zero: false,
|
||||
basis: 'cash',
|
||||
account_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.account_ids)) {
|
||||
filter.account_ids = [filter.account_ids];
|
||||
}
|
||||
|
||||
const balanceSheetTypes = await AccountType.query().where('balance_sheet', true);
|
||||
|
||||
// Fetch all balance sheet accounts.
|
||||
const accounts = await Account.query()
|
||||
.remember('balance_sheet_accounts')
|
||||
.whereIn('account_type_id', balanceSheetTypes.map((a) => a.id))
|
||||
.modify('filterAccounts', filter.account_ids)
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions')
|
||||
.modifyGraph('transactions', (builder) => {
|
||||
@@ -313,6 +326,7 @@ export default {
|
||||
|
||||
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
||||
const journalEntries = new JournalPoster();
|
||||
|
||||
journalEntries.loadEntries(journalEntriesCollected);
|
||||
|
||||
// Account balance formmatter based on the given query.
|
||||
@@ -378,10 +392,12 @@ export default {
|
||||
accounts: [
|
||||
{
|
||||
name: 'Assets',
|
||||
type: 'assets',
|
||||
children: [...assets],
|
||||
},
|
||||
{
|
||||
name: 'Liabilities & Equity',
|
||||
type: 'liabilities_equity',
|
||||
children: [...liabilitiesEquity],
|
||||
},
|
||||
],
|
||||
@@ -426,6 +442,7 @@ export default {
|
||||
};
|
||||
|
||||
const accounts = await Account.query()
|
||||
.remember('trial_balance_accounts')
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions')
|
||||
.modifyGraph('transactions', (builder) => {
|
||||
@@ -474,7 +491,7 @@ export default {
|
||||
query('number_format.no_cents').optional().isBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean(),
|
||||
query('basis').optional(),
|
||||
query('none_zero').optional(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('display_columns_type').optional().isIn([
|
||||
'total', 'date_periods',
|
||||
]),
|
||||
@@ -507,6 +524,7 @@ export default {
|
||||
|
||||
// Fetch all income accounts from storage.
|
||||
const accounts = await Account.query()
|
||||
.remember('profit_loss_accounts')
|
||||
.whereIn('account_type_id', incomeStatementTypes.map((t) => t.id))
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions');
|
||||
|
||||
@@ -45,10 +45,10 @@ export default async (req, res, next) => {
|
||||
req.knex = knex;
|
||||
req.organizationId = organizationId;
|
||||
req.models = {
|
||||
...Object.values(models).reduce((acc, model) => {
|
||||
if (model.resource
|
||||
&& model.resource.default
|
||||
&& Object.getPrototypeOf(model.resource.default) === TenantModel) {
|
||||
...Object.values(models).reduce((acc, model) => {
|
||||
if (typeof model.resource.default.requestModel === 'function' &&
|
||||
model.resource.default.requestModel() &&
|
||||
model.name !== 'TenantModel') {
|
||||
acc[model.name] = model.resource.default.bindKnex(knex);
|
||||
}
|
||||
return acc;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import BaseModel from '@/models/Model';
|
||||
import CacheService from '@/services/Cache';
|
||||
|
||||
export default (Model) => {
|
||||
return class CachableModel extends Model{
|
||||
static flushCache(key) {
|
||||
const modelName = this.name;
|
||||
|
||||
if (key) {
|
||||
CacheService.del(`${modelName}.${key}`);
|
||||
} else {
|
||||
CacheService.delStartWith(modelName);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
69
server/src/lib/Cachable/CachableQueryBuilder.js
Normal file
69
server/src/lib/Cachable/CachableQueryBuilder.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { QueryBuilder } from 'objection';
|
||||
import crypto from 'crypto';
|
||||
import CacheService from '@/services/Cache';
|
||||
|
||||
export default class CachableQueryBuilder extends QueryBuilder{
|
||||
|
||||
async then(...args) {
|
||||
// Flush model cache after insert, delete or update transaction.
|
||||
if (this.isInsert() || this.isDelete() || this.isUpdate()) {
|
||||
this.modelClass().flushCache();
|
||||
}
|
||||
if (this.cacheTag && this.isFind()) {
|
||||
this.setCacheKey();
|
||||
return this.getOrStoreCache().then(...args);
|
||||
} else {
|
||||
const promise = this.execute();
|
||||
|
||||
return promise.then((result) => {
|
||||
this.setCache(result);
|
||||
return result;
|
||||
}).then(...args);
|
||||
}
|
||||
}
|
||||
|
||||
getOrStoreCache() {
|
||||
const storeFunction = () => this.execute();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
CacheService.get(this.cacheKey, storeFunction)
|
||||
.then((result) => { resolve(result); });
|
||||
});
|
||||
}
|
||||
|
||||
setCache(results) {
|
||||
CacheService.set(`${this.cacheKey}`, results, this.cacheSeconds);
|
||||
}
|
||||
|
||||
generateCacheKey() {
|
||||
const knexSql = this.toKnexQuery().toSQL();
|
||||
const hashedQuery = crypto.createHash('md5').update(knexSql.sql).digest("hex");
|
||||
|
||||
return hashedQuery;
|
||||
}
|
||||
|
||||
remember(key, seconds) {
|
||||
const modelName = this.modelClass().name;
|
||||
|
||||
this.cacheSeconds = seconds;
|
||||
this.cacheTag = (key) ? `${modelName}.${key}` : modelName;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
withGraphFetched(relation, settings) {
|
||||
if (!this.graphAppends) {
|
||||
this.graphAppends = [relation];
|
||||
} else {
|
||||
this.graphAppends.push(relation);
|
||||
}
|
||||
return super.withGraphFetched(relation, settings);
|
||||
}
|
||||
|
||||
setCacheKey() {
|
||||
const hashedQuery = this.generateCacheKey();
|
||||
const appends = (this.graphAppends || []).join(this.graphAppends, ',');
|
||||
|
||||
this.cacheKey = `${this.cacheTag}.${hashedQuery}.${appends}`;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
/* eslint-disable global-require */
|
||||
import { Model } from 'objection';
|
||||
import { Model, mixin } from 'objection';
|
||||
import { flatten } from 'lodash';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import {
|
||||
buildFilterQuery,
|
||||
buildSortColumnQuery,
|
||||
} from '@/lib/ViewRolesBuilder';
|
||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||
import DateSession from '@/models/DateSession';
|
||||
|
||||
export default class Account extends TenantModel {
|
||||
|
||||
export default class Account extends mixin(TenantModel, [CachableModel, DateSession]) {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -15,6 +19,13 @@ export default class Account extends TenantModel {
|
||||
return 'accounts';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return CachableQueryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Model } from 'objection';
|
||||
import moment from 'moment';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||
|
||||
|
||||
export default class AccountTransaction extends TenantModel {
|
||||
/**
|
||||
@@ -10,6 +12,13 @@ export default class AccountTransaction extends TenantModel {
|
||||
return 'accounts_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return CachableQueryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
|
||||
30
server/src/models/DateSession.js
Normal file
30
server/src/models/DateSession.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export default (Model) => {
|
||||
return class DateSession extends Model {
|
||||
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
$beforeUpdate(opt, context) {
|
||||
const maybePromise = super.$beforeUpdate(opt, context);
|
||||
|
||||
return Promise.resolve(maybePromise).then(() => {
|
||||
if (DateSession.timestamps[1]) {
|
||||
this[DateSession.timestamps[1]] = moment().format('YYYY/MM/DD HH:mm:ss');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$beforeInsert(context) {
|
||||
const maybePromise = super.$beforeInsert(context);
|
||||
|
||||
return Promise.resolve(maybePromise).then(() => {
|
||||
if (DateSession.timestamps[0]) {
|
||||
this[DateSession.timestamps[0]] = moment().format('YYYY/MM/DD HH:mm:ss');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { mixin } from 'objection';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import MetableCollection from '@/lib/Metable/MetableCollection';
|
||||
import definedOptions from '@/data/options';
|
||||
|
||||
export default class Option extends mixin(TenantModel, [mixin]) {
|
||||
|
||||
export default class Option extends TenantModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Model } from 'objection';
|
||||
import { Model, mixin } from 'objection';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||
|
||||
export default class Resource extends TenantModel {
|
||||
export default class Resource extends mixin(TenantModel, [CachableModel]) {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
@@ -9,6 +11,13 @@ export default class Resource extends TenantModel {
|
||||
return 'resources';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return CachableQueryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
|
||||
@@ -7,4 +7,11 @@ export default class TenantModel extends BaseModel {
|
||||
}
|
||||
return super.bindKnex(this.knexBinded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow to embed models to express request.
|
||||
*/
|
||||
static requestModel() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Model } from 'objection';
|
||||
import { Model, mixin } from 'objection';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||
|
||||
export default class View extends TenantModel {
|
||||
export default class View extends mixin(TenantModel, [CachableModel]) {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
@@ -9,6 +11,13 @@ export default class View extends TenantModel {
|
||||
return 'views';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return CachableQueryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
import AccountBalance from '@/models/AccountBalance';
|
||||
import {promiseSerial} from '@/utils';
|
||||
import Account from '../../models/Account';
|
||||
|
||||
export default class JournalPoster {
|
||||
/**
|
||||
@@ -85,6 +86,8 @@ export default class JournalPoster {
|
||||
const balanceFindOneOpers = [];
|
||||
let balanceAccounts = [];
|
||||
|
||||
const effectAccountsOpers = [];
|
||||
|
||||
balancesList.forEach((balance) => {
|
||||
const oper = AccountBalance.tenant()
|
||||
.query().findOne('account_id', balance.account_id);
|
||||
@@ -113,12 +116,67 @@ export default class JournalPoster {
|
||||
});
|
||||
balanceInsertOpers.push(query);
|
||||
}
|
||||
|
||||
const effectedAccountsOper = this.effectAssociatedAccountsBalance(
|
||||
balance.accountId, amount, 'USD', method,
|
||||
);
|
||||
effectAccountsOpers.push(effectedAccountsOper);
|
||||
});
|
||||
await Promise.all([
|
||||
...balanceUpdateOpers, ...balanceInsertOpers,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Effect associated descendants and parent accounts
|
||||
* of the given account id.
|
||||
* @param {Number} accountId
|
||||
* @param {Number} amount
|
||||
* @param {String} currencyCode
|
||||
* @param {*} method
|
||||
*/
|
||||
async effectAssociatedAccountsBalance(accountId, amount, currencyCode = 'USD', method) {
|
||||
const accounts = await Account.query().withGraphFetched('balance');
|
||||
|
||||
const accountsDecendences = accounts.getDescendants();
|
||||
|
||||
const asyncOpers = [];
|
||||
const accountsInsertBalance = [];
|
||||
const accountsUpdateBalance = [];
|
||||
|
||||
accounts.forEach((account) => {
|
||||
const accountBalances = account.balance;
|
||||
const currencyBalance = accountBalances
|
||||
.find(balance => balance.currencyCode === currencyCode);
|
||||
|
||||
if (currencyBalance) {
|
||||
accountsInsertBalance.push(account.id);
|
||||
} else {
|
||||
accountsUpdateBalance.push(account.id);
|
||||
}
|
||||
});
|
||||
|
||||
accountsInsertBalance.forEach((accountId) => {
|
||||
const oper = AccountBalance.tenant().query().insert({
|
||||
account_id: accountId,
|
||||
amount: method === 'decrement' ? amount * -1 : amount,
|
||||
currency_code: currencyCode,
|
||||
});
|
||||
asyncOpers.push(oper);
|
||||
});
|
||||
|
||||
if (accountsUpdateBalance.length > 0) {
|
||||
const oper = AccountBalance.tenant().query()
|
||||
.whereIn('account_id', accountsUpdateBalance);
|
||||
[method]('amount', Math.abs(amount))
|
||||
.where('currency_code', currencyCode);
|
||||
|
||||
asyncOpers.push(oper);
|
||||
}
|
||||
await Promise.all(asyncOpers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the stacked journal entries to the storage.
|
||||
*/
|
||||
@@ -233,9 +291,9 @@ export default class JournalPoster {
|
||||
result.debit += entry.debit;
|
||||
|
||||
if (entry.accountNormal === 'credit') {
|
||||
result.balance += (entry.credit) ? entry.credit : -1 * entry.debit;
|
||||
result.balance += entry.credit - entry.debit;
|
||||
} else if (entry.accountNormal === 'debit') {
|
||||
result.balance += (entry.debit) ? entry.debit : -1 * entry.credit;
|
||||
result.balance += entry.debit - entry.credit;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
|
||||
52
server/src/services/Cache/index.js
Normal file
52
server/src/services/Cache/index.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import NodeCache from 'node-cache';
|
||||
|
||||
class Cache {
|
||||
|
||||
constructor() {
|
||||
this.cache = new NodeCache({
|
||||
// stdTTL: 9999999,
|
||||
// checkperiod: 9999999 * 0.2,
|
||||
useClones: false,
|
||||
});
|
||||
}
|
||||
|
||||
get(key, storeFunction) {
|
||||
const value = this.cache.get(key);
|
||||
|
||||
if (value) {
|
||||
return Promise.resolve(value);
|
||||
}
|
||||
return storeFunction().then((result) => {
|
||||
this.cache.set(key, result);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
set(key, results) {
|
||||
this.cache.set(key, results);
|
||||
}
|
||||
|
||||
del(keys) {
|
||||
this.cache.del(keys);
|
||||
}
|
||||
|
||||
delStartWith(startStr = '') {
|
||||
if (!startStr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = this.cache.keys();
|
||||
for (const key of keys) {
|
||||
if (key.indexOf(startStr) === 0) {
|
||||
this.del(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
this.cache.flushAll();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default new Cache();
|
||||
@@ -2,7 +2,7 @@ import { mixin } from 'objection';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import MetableCollection from '@/lib/Metable/MetableCollection';
|
||||
|
||||
export default class Option extends mixin(SystemModel, [mixin]) {
|
||||
export default class Option extends SystemModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
|
||||
32
server/tests/lib/CachableModel.test.js
Normal file
32
server/tests/lib/CachableModel.test.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
request,
|
||||
expect,
|
||||
} from '~/testInit';
|
||||
import Account from '@/models/Account';
|
||||
import {
|
||||
tenantWebsite,
|
||||
tenantFactory,
|
||||
loginRes
|
||||
} from '~/dbInit';
|
||||
import { times } from 'lodash';
|
||||
|
||||
describe.only('CachableModel', () => {
|
||||
describe('remember()', () => {
|
||||
it('Should retrieve the data from the storage.', async () => {
|
||||
|
||||
for (let i = 0; i < 1; i++) {
|
||||
const account = await Account.tenant().query()
|
||||
.remember()
|
||||
.where('id', 1);
|
||||
|
||||
const account2 = await Account.tenant().query()
|
||||
.remember()
|
||||
.withGraphFetched('balance');
|
||||
|
||||
console.log(account2);
|
||||
// \\\
|
||||
}
|
||||
// Account.flushCache();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,49 +2,56 @@ import moment from 'moment';
|
||||
import {
|
||||
request,
|
||||
expect,
|
||||
login,
|
||||
createTenantFactory,
|
||||
createTenant,
|
||||
dropTenant,
|
||||
} from '~/testInit';
|
||||
import {
|
||||
tenantWebsite,
|
||||
tenantFactory,
|
||||
loginRes
|
||||
} from '~/dbInit';
|
||||
|
||||
let tenantWebsite;
|
||||
let tenantFactory;
|
||||
let loginRes;
|
||||
let creditAccount;
|
||||
let debitAccount;
|
||||
let incomeType;
|
||||
|
||||
describe('routes: `/financial_statements`', () => {
|
||||
beforeEach(async () => {
|
||||
tenantWebsite = await createTenant();
|
||||
tenantFactory = createTenantFactory(tenantWebsite.tenantDb);
|
||||
|
||||
loginRes = await login(tenantWebsite);
|
||||
|
||||
// Balance sheet types.
|
||||
const creditAccType = await tenantFactory.create('account_type', { normal: 'credit', balance_sheet: true });
|
||||
const debitAccType = await tenantFactory.create('account_type', { normal: 'debit', balance_sheet: true });
|
||||
const assetType = await tenantFactory.create('account_type', { normal: 'debit', balance_sheet: true });
|
||||
const liabilityType = await tenantFactory.create('account_type', { normal: 'credit', balance_sheet: true });
|
||||
|
||||
// Income statement types.
|
||||
const incomeType = await tenantFactory.create('account_type', { normal: 'credit', income_sheet: true });
|
||||
incomeType = await tenantFactory.create('account_type', { normal: 'credit', income_sheet: true });
|
||||
const expenseType = await tenantFactory.create('account_type', { normal: 'debit', income_sheet: true });
|
||||
|
||||
// Assets & liabilites accounts.
|
||||
creditAccount = await tenantFactory.create('account', { account_type_id: creditAccType.id });
|
||||
debitAccount = await tenantFactory.create('account', { account_type_id: debitAccType.id });
|
||||
creditAccount = await tenantFactory.create('account', { account_type_id: liabilityType.id });
|
||||
debitAccount = await tenantFactory.create('account', { account_type_id: assetType.id });
|
||||
|
||||
// Income && expenses accounts.
|
||||
const incomeAccount = await tenantFactory.create('account', { account_type_id: incomeType.id });
|
||||
const expenseAccount = await tenantFactory.create('account', { account_type_id: expenseType.id });
|
||||
const income2Account = await tenantFactory.create('account', { account_type_id: incomeType.id });
|
||||
// const income2Account = await tenantFactory.create('account', { account_type_id: incomeType.id });
|
||||
|
||||
const accountTransactionMixied = { date: '2020-1-10' };
|
||||
|
||||
// Expense --
|
||||
// 1000 Credit - Credit account
|
||||
// 1000 Debit - expense account.
|
||||
await tenantFactory.create('account_transaction', {
|
||||
credit: 1000, debit: 0, account_id: creditAccount.id, referenceType: 'Expense', ...accountTransactionMixied,
|
||||
credit: 1000, debit: 0, account_id: debitAccount.id, referenceType: 'Expense',
|
||||
referenceId: 1, ...accountTransactionMixied,
|
||||
});
|
||||
await tenantFactory.create('account_transaction', {
|
||||
credit: 1000, debit: 0, account_id: creditAccount.id, ...accountTransactionMixied,
|
||||
credit: 0, debit: 1000, account_id: expenseAccount.id, referenceType: 'Expense',
|
||||
referenceId: 1, ...accountTransactionMixied,
|
||||
});
|
||||
|
||||
// Jounral
|
||||
// 4000 Credit - liability account.
|
||||
// 2000 Debit - Asset account
|
||||
// 2000 Debit - Asset account
|
||||
await tenantFactory.create('account_transaction', {
|
||||
credit: 4000, debit: 0, account_id: creditAccount.id, ...accountTransactionMixied,
|
||||
});
|
||||
await tenantFactory.create('account_transaction', {
|
||||
debit: 2000, credit: 0, account_id: debitAccount.id, ...accountTransactionMixied,
|
||||
@@ -52,16 +59,22 @@ describe('routes: `/financial_statements`', () => {
|
||||
await tenantFactory.create('account_transaction', {
|
||||
debit: 2000, credit: 0, account_id: debitAccount.id, ...accountTransactionMixied,
|
||||
});
|
||||
await tenantFactory.create('account_transaction', { credit: 2000, account_id: incomeAccount.id, ...accountTransactionMixied });
|
||||
await tenantFactory.create('account_transaction', { debit: 6000, account_id: expenseAccount.id, ...accountTransactionMixied });
|
||||
|
||||
// Income Journal.
|
||||
// 2000 Credit - Income account.
|
||||
// 2000 Debit - Asset account.
|
||||
await tenantFactory.create('account_transaction', {
|
||||
credit: 2000, account_id: incomeAccount.id, ...accountTransactionMixied
|
||||
});
|
||||
await tenantFactory.create('account_transaction', {
|
||||
debit: 2000, credit: 0, account_id: debitAccount.id, ...accountTransactionMixied,
|
||||
});
|
||||
|
||||
// -----------------------------------------
|
||||
// Assets account balance = 5000 | Libility account balance = 4000
|
||||
// Expense account balance = 1000 | Income account balance = 2000
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await dropTenant(tenantWebsite);
|
||||
|
||||
loginRes = null;
|
||||
tenantFactory = null;
|
||||
});
|
||||
|
||||
describe('routes: `/financial_statements/journal`', () => {
|
||||
it('Should response unauthorized in case the user was not authorized.', async () => {
|
||||
@@ -72,7 +85,7 @@ describe('routes: `/financial_statements`', () => {
|
||||
expect(res.status).equals(401);
|
||||
});
|
||||
|
||||
it('Should retrieve ledger transactions grouped by reference type and id.', async () => {
|
||||
it('Should retrieve ledger sheet transactions grouped by reference type and id.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/journal')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
@@ -115,7 +128,10 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.journal.length).equals(2);
|
||||
expect(res.body.journal[0].entries.length).equals(1);
|
||||
expect(res.body.journal[0].entries[0].account_id).equals(creditAccount.id);
|
||||
|
||||
expect(res.body.journal.length).equals(1);
|
||||
});
|
||||
|
||||
it('Should retrieve tranasactions with the given types.', async () => {
|
||||
@@ -138,13 +154,14 @@ describe('routes: `/financial_statements`', () => {
|
||||
.query({
|
||||
from_range: 2000,
|
||||
to_range: 2000,
|
||||
});
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.journal[0].credit).satisfy((credit) => {
|
||||
return credit === 0 || credit === 2000;
|
||||
return credit === 0 || credit >= 2000;
|
||||
});
|
||||
expect(res.body.journal[0].debit).satisfy((debit) => {
|
||||
return debit === 0 || debit === 2000;
|
||||
return debit === 0 || debit >= 2000;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,7 +184,7 @@ describe('routes: `/financial_statements`', () => {
|
||||
const journal = res.body.journal.find((j) => j.id === '1-Expense');
|
||||
|
||||
expect(journal.credit).equals(1);
|
||||
expect(journal.debit).equals(0);
|
||||
expect(journal.debit).equals(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -209,10 +226,10 @@ describe('routes: `/financial_statements`', () => {
|
||||
expect(res.body.accounts[0].code).to.be.a('string');
|
||||
expect(res.body.accounts[0].transactions).to.be.a('array');
|
||||
expect(res.body.accounts[0].opening).to.be.a('object');
|
||||
expect(res.body.accounts[0].opening.balance).to.be.a('number');
|
||||
expect(res.body.accounts[0].opening.amount).to.be.a('number');
|
||||
expect(res.body.accounts[0].opening.date).to.be.a('string');
|
||||
expect(res.body.accounts[0].closing).to.be.a('object');
|
||||
expect(res.body.accounts[0].closing.balance).to.be.a('number');
|
||||
expect(res.body.accounts[0].closing.amount).to.be.a('number');
|
||||
expect(res.body.accounts[0].closing.date).to.be.a('string');
|
||||
});
|
||||
|
||||
@@ -227,10 +244,10 @@ describe('routes: `/financial_statements`', () => {
|
||||
|
||||
expect(targetAccount).to.be.an('object');
|
||||
expect(targetAccount.opening).to.deep.equal({
|
||||
balance: 0, date: '2020-01-01',
|
||||
amount: 0, date: '2020-01-01',
|
||||
});
|
||||
expect(targetAccount.closing).to.deep.equal({
|
||||
balance: 2000, date: '2020-12-31',
|
||||
amount: 4000, date: '2020-12-31',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -240,20 +257,22 @@ describe('routes: `/financial_statements`', () => {
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.query({
|
||||
from_date: '2020-01-20',
|
||||
from_date: '2018-01-01',
|
||||
to_date: '2020-03-30',
|
||||
none_zero: true,
|
||||
// none_zero: true,
|
||||
})
|
||||
.send();
|
||||
|
||||
console.log(res.body);
|
||||
|
||||
const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id);
|
||||
|
||||
expect(targetAccount).to.be.an('object');
|
||||
expect(targetAccount.opening).to.deep.equal({
|
||||
balance: 2000, date: '2020-01-20',
|
||||
amount: 4000, date: '2020-01-01',
|
||||
});
|
||||
expect(targetAccount.closing).to.deep.equal({
|
||||
balance: 2000, date: '2020-03-30',
|
||||
amount: 4000, date: '2020-03-30',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -267,17 +286,7 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id);
|
||||
|
||||
expect(targetAccount.transactions[0].amount).equals(1000);
|
||||
expect(targetAccount.transactions[1].amount).equals(1000);
|
||||
|
||||
expect(targetAccount.transactions[1].id).to.be.an('number');
|
||||
// expect(targetAccount.transactions[1].note).to.be.an('string');
|
||||
// expect(targetAccount.transactions[1].transactionType).to.be.an('string');
|
||||
// expect(targetAccount.transactions[1].referenceType).to.be.an('string');
|
||||
// expect(targetAccount.transactions[1].referenceId).to.be.an('number');
|
||||
expect(targetAccount.transactions[1].date).to.be.an('string');
|
||||
|
||||
})
|
||||
|
||||
it('Should retrieve accounts transactions only that between date range.', async () => {
|
||||
@@ -292,8 +301,6 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id);
|
||||
expect(targetAccount.transactions.length).equals(0);
|
||||
});
|
||||
|
||||
it('Should not retrieve all accounts that have no transactions in the given date range when `none_zero` is `false`.', async () => {
|
||||
@@ -345,10 +352,27 @@ describe('routes: `/financial_statements`', () => {
|
||||
},
|
||||
})
|
||||
.send();
|
||||
|
||||
const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id);
|
||||
expect(targetAccount.transactions[0].amount).equals(1);
|
||||
expect(targetAccount.transactions[1].amount).equals(1);
|
||||
|
||||
expect(res.body.accounts).include.something.deep.equals({
|
||||
id: creditAccount.id,
|
||||
name: creditAccount.name,
|
||||
code: creditAccount.code,
|
||||
index: null,
|
||||
transactions: [
|
||||
{
|
||||
id: 1002,
|
||||
note: null,
|
||||
transactionType: null,
|
||||
referenceType: null,
|
||||
referenceId: null,
|
||||
date: '2020-01-09T22:00:00.000Z',
|
||||
createdAt: null,
|
||||
amount: 4
|
||||
}
|
||||
],
|
||||
opening: { date: '2020-01-01', amount: 0 },
|
||||
closing: { date: '2020-03-30', amount: 4 }
|
||||
});
|
||||
});
|
||||
|
||||
it('Should amount transactions rounded with no decimals when `number_format.no_cents` is `true`.', async () => {
|
||||
@@ -371,7 +395,7 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.accounts[0].transactions[2].amount).equal(0);
|
||||
expect(res.body.accounts[0].transactions[2].amount).equal(2);
|
||||
});
|
||||
|
||||
it('Should retrieve only accounts that given in the query.', async () => {
|
||||
@@ -432,8 +456,11 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts).to.be.a('array');
|
||||
expect(res.body.balance_sheet.liabilities_equity.accounts).to.be.a('array');
|
||||
expect(res.body.accounts[0].children).to.be.a('array');
|
||||
expect(res.body.accounts[1].children).to.be.a('array');
|
||||
|
||||
expect(res.body.accounts[0].children.length).is.not.equals(0);
|
||||
expect(res.body.accounts[1].children.length).is.not.equals(0);
|
||||
});
|
||||
|
||||
it('Should retrieve assets/liabilities total balance between the given date range.', async () => {
|
||||
@@ -448,13 +475,20 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
console.log(res.body.balance_sheet.assets.accounts);
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].total).deep.equals({
|
||||
amount: 4000, formatted_amount: 4000, date: '2032-02-02',
|
||||
expect(res.body.accounts[0].children).include.something.deep.equals({
|
||||
id: 1001,
|
||||
index: null,
|
||||
name: debitAccount.name,
|
||||
code: debitAccount.code,
|
||||
total: { formatted_amount: 5000, amount: 5000, date: '2032-02-02' }
|
||||
});
|
||||
expect(res.body.balance_sheet.liabilities_equity.accounts[0].total).deep.equals({
|
||||
amount: 2000, formatted_amount: 2000, date: '2032-02-02',
|
||||
|
||||
expect(res.body.accounts[1].children).include.something.deep.equals({
|
||||
id: 1000,
|
||||
index: null,
|
||||
name: creditAccount.name,
|
||||
code: creditAccount.code,
|
||||
total: { formatted_amount: 4000, amount: 4000, date: '2032-02-02' }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -465,15 +499,16 @@ describe('routes: `/financial_statements`', () => {
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.query({
|
||||
display_columns_by: 'year',
|
||||
display_columns_type: 'date_periods',
|
||||
from_date: '2012-01-01',
|
||||
to_date: '2018-02-02',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(7);
|
||||
expect(res.body.balance_sheet.liabilities_equity.accounts[0].periods_balance.length).equals(7);
|
||||
expect(res.body.accounts[0].children[0].total_periods.length).equals(7);
|
||||
expect(res.body.accounts[1].children[0].total_periods.length).equals(7);
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
expect(res.body.accounts[0].children[0].total_periods).deep.equals([
|
||||
{
|
||||
amount: 0,
|
||||
formatted_amount: 0,
|
||||
@@ -519,18 +554,40 @@ describe('routes: `/financial_statements`', () => {
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.query({
|
||||
display_columns_by: 'day',
|
||||
display_columns_type: 'date_periods',
|
||||
from_date: '2020-01-08',
|
||||
to_date: '2020-01-12',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2020-01-10', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-01-11', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-01-12', formatted_amount: 4000, amount: 4000 },
|
||||
]);
|
||||
expect(res.body.accounts[0].children).include.something.deep.equals({
|
||||
id: debitAccount.id,
|
||||
index: debitAccount.index,
|
||||
name: debitAccount.name,
|
||||
code: debitAccount.code,
|
||||
total_periods: [
|
||||
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2020-01-10', formatted_amount: 5000, amount: 5000 },
|
||||
{ date: '2020-01-11', formatted_amount: 5000, amount: 5000 },
|
||||
{ date: '2020-01-12', formatted_amount: 5000, amount: 5000 },
|
||||
],
|
||||
total: { formatted_amount: 5000, amount: 5000, date: '2020-01-12' }
|
||||
});
|
||||
expect(res.body.accounts[1].children).include.something.deep.equals({
|
||||
id: creditAccount.id,
|
||||
index: creditAccount.index,
|
||||
name: creditAccount.name,
|
||||
code: creditAccount.code,
|
||||
total_periods: [
|
||||
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2020-01-10', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-01-11', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-01-12', formatted_amount: 4000, amount: 4000 }
|
||||
],
|
||||
total: { formatted_amount: 4000, amount: 4000, date: '2020-01-12' }
|
||||
});
|
||||
});
|
||||
|
||||
it('Should retrieve the balance sheet with display columns by `month`.', async () => {
|
||||
@@ -540,28 +597,33 @@ describe('routes: `/financial_statements`', () => {
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.query({
|
||||
display_columns_by: 'month',
|
||||
display_columns_type: 'date_periods',
|
||||
from_date: '2019-07-01',
|
||||
to_date: '2020-06-30',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(12);
|
||||
expect(res.body.balance_sheet.liabilities_equity.accounts[0].periods_balance.length).equals(12);
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
{ date: '2019-07', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-08', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-09', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-10', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-11', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-12', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2020-01', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-02', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-03', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-04', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-05', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-06', formatted_amount: 4000, amount: 4000 },
|
||||
]);
|
||||
expect(res.body.accounts[0].children).include.something.deep.equals({
|
||||
id: debitAccount.id,
|
||||
index: debitAccount.index,
|
||||
name: debitAccount.name,
|
||||
code: debitAccount.code,
|
||||
total_periods: [
|
||||
{ date: '2019-07', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-08', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-09', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-10', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-11', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-12', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2020-01', formatted_amount: 5000, amount: 5000 },
|
||||
{ date: '2020-02', formatted_amount: 5000, amount: 5000 },
|
||||
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
|
||||
{ date: '2020-04', formatted_amount: 5000, amount: 5000 },
|
||||
{ date: '2020-05', formatted_amount: 5000, amount: 5000 },
|
||||
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
|
||||
],
|
||||
total: { formatted_amount: 5000, amount: 5000, date: '2020-06-30' }
|
||||
});
|
||||
});
|
||||
|
||||
it('Should retrieve the balance sheet with display columns `quarter`.', async () => {
|
||||
@@ -571,18 +633,25 @@ describe('routes: `/financial_statements`', () => {
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.query({
|
||||
display_columns_by: 'quarter',
|
||||
display_columns_type: 'date_periods',
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2020-12-31',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(4);
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
{ date: '2020-03', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-06', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-09', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-12', formatted_amount: 4000, amount: 4000 },
|
||||
]);
|
||||
expect(res.body.accounts[0].children).include.something.deep.equals({
|
||||
id: debitAccount.id,
|
||||
index: debitAccount.index,
|
||||
name: debitAccount.name,
|
||||
code: debitAccount.code,
|
||||
total_periods: [
|
||||
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
|
||||
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
|
||||
{ date: '2020-09', formatted_amount: 5000, amount: 5000 },
|
||||
{ date: '2020-12', formatted_amount: 5000, amount: 5000 },
|
||||
],
|
||||
total: { formatted_amount: 5000, amount: 5000, date: '2020-12-31' },
|
||||
});
|
||||
});
|
||||
|
||||
it('Should retrieve the balance sheet amounts without cents.', async () => {
|
||||
@@ -595,6 +664,7 @@ describe('routes: `/financial_statements`', () => {
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.query({
|
||||
display_columns_by: 'quarter',
|
||||
display_columns_type: 'date_periods',
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2020-12-31',
|
||||
number_format: {
|
||||
@@ -603,13 +673,19 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(4);
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
{ date: '2020-03', formatted_amount: 4000, amount: 4000.25 },
|
||||
{ date: '2020-06', formatted_amount: 4000, amount: 4000.25 },
|
||||
{ date: '2020-09', formatted_amount: 4000, amount: 4000.25 },
|
||||
{ date: '2020-12', formatted_amount: 4000, amount: 4000.25 },
|
||||
]);
|
||||
expect(res.body.accounts[0].children).include.something.deep.equals({
|
||||
id: debitAccount.id,
|
||||
index: debitAccount.index,
|
||||
name: debitAccount.name,
|
||||
code: debitAccount.code,
|
||||
total_periods: [
|
||||
{ date: '2020-03', formatted_amount: 5000, amount: 5000.25 },
|
||||
{ date: '2020-06', formatted_amount: 5000, amount: 5000.25 },
|
||||
{ date: '2020-09', formatted_amount: 5000, amount: 5000.25 },
|
||||
{ date: '2020-12', formatted_amount: 5000, amount: 5000.25 },
|
||||
],
|
||||
total: { formatted_amount: 5000, amount: 5000.25, date: '2020-12-31' },
|
||||
});
|
||||
});
|
||||
|
||||
it('Should retrieve the balance sheet amounts divided on 1000.', async () => {
|
||||
@@ -619,6 +695,7 @@ describe('routes: `/financial_statements`', () => {
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.query({
|
||||
display_columns_by: 'quarter',
|
||||
display_columns_type: 'date_periods',
|
||||
from_date: '2020',
|
||||
to_date: '2021',
|
||||
number_format: {
|
||||
@@ -626,14 +703,21 @@ describe('routes: `/financial_statements`', () => {
|
||||
},
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
{ date: '2020-03', formatted_amount: 4, amount: 4000 },
|
||||
{ date: '2020-06', formatted_amount: 4, amount: 4000 },
|
||||
{ date: '2020-09', formatted_amount: 4, amount: 4000 },
|
||||
{ date: '2020-12', formatted_amount: 4, amount: 4000 },
|
||||
{ date: '2021-03', formatted_amount: 4, amount: 4000 },
|
||||
]);
|
||||
|
||||
expect(res.body.accounts[0].children).include.something.deep.equals({
|
||||
id: debitAccount.id,
|
||||
index: debitAccount.index,
|
||||
name: debitAccount.name,
|
||||
code: debitAccount.code,
|
||||
total_periods: [
|
||||
{ date: '2020-03', formatted_amount: 5, amount: 5000 },
|
||||
{ date: '2020-06', formatted_amount: 5, amount: 5000 },
|
||||
{ date: '2020-09', formatted_amount: 5, amount: 5000 },
|
||||
{ date: '2020-12', formatted_amount: 5, amount: 5000 },
|
||||
{ date: '2021-03', formatted_amount: 5, amount: 5000 },
|
||||
],
|
||||
total: { formatted_amount: 5, amount: 5000, date: '2021' },
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not retrieve accounts has no transactions between the given date range in case query none_zero is true.', async () => {
|
||||
@@ -652,8 +736,8 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts.length).equals(0);
|
||||
expect(res.body.balance_sheet.liabilities_equity.accounts.length).equals(0);
|
||||
expect(res.body.accounts[0].children.length).equals(0);
|
||||
expect(res.body.accounts[1].children.length).equals(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -673,12 +757,24 @@ describe('routes: `/financial_statements`', () => {
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send();
|
||||
|
||||
const foundCreditAccount = res.body.items.find((item) => {
|
||||
return item.account_id === creditAccount.id;
|
||||
expect(res.body.accounts).include.something.deep.equals({
|
||||
account_id: debitAccount.id,
|
||||
name: debitAccount.name,
|
||||
code: debitAccount.code,
|
||||
accountNormal: 'debit',
|
||||
credit: 1000,
|
||||
debit: 6000,
|
||||
balance: 5000,
|
||||
});
|
||||
expect(res.body.accounts).include.something.deep.equals({
|
||||
account_id: creditAccount.id,
|
||||
name: creditAccount.name,
|
||||
code: creditAccount.code,
|
||||
accountNormal: 'credit',
|
||||
credit: 4000,
|
||||
debit: 0,
|
||||
balance: 4000,
|
||||
});
|
||||
expect(foundCreditAccount.credit).equals(2000);
|
||||
expect(foundCreditAccount.debit).equals(0);
|
||||
expect(foundCreditAccount.balance).equals(2000);
|
||||
});
|
||||
|
||||
it('Should not retrieve accounts has no transactions between the given date range.', async () => {
|
||||
@@ -694,7 +790,7 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.items.length).equals(0);
|
||||
expect(res.body.accounts.length).equals(0);
|
||||
});
|
||||
|
||||
it('Should retrieve trial balance of accounts between the given date range.', async () => {
|
||||
@@ -706,15 +802,19 @@ describe('routes: `/financial_statements`', () => {
|
||||
// There is no transactions between these dates.
|
||||
from_date: '2020-01-05',
|
||||
to_date: '2020-01-10',
|
||||
none_zero: true,
|
||||
})
|
||||
.send();
|
||||
|
||||
const foundCreditAccount = res.body.items.find((item) => {
|
||||
return item.account_id === creditAccount.id;
|
||||
expect(res.body.accounts).include.something.deep.equals({
|
||||
account_id: creditAccount.id,
|
||||
name: creditAccount.name,
|
||||
code: creditAccount.code,
|
||||
accountNormal: 'credit',
|
||||
credit: 4000,
|
||||
debit: 0,
|
||||
balance: 4000
|
||||
});
|
||||
expect(foundCreditAccount.credit).equals(2000);
|
||||
expect(foundCreditAccount.debit).equals(0);
|
||||
expect(foundCreditAccount.balance).equals(2000);
|
||||
});
|
||||
|
||||
it('Should credit, debit and balance amount be divided on 1000.', async () => {
|
||||
@@ -732,12 +832,15 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
const foundCreditAccount = res.body.items.find((item) => {
|
||||
return item.account_id === creditAccount.id;
|
||||
expect(res.body.accounts).include.something.deep.equals({
|
||||
account_id: creditAccount.id,
|
||||
name: creditAccount.name,
|
||||
code: creditAccount.code,
|
||||
accountNormal: 'credit',
|
||||
credit: 4,
|
||||
debit: 0,
|
||||
balance: 4
|
||||
});
|
||||
expect(foundCreditAccount.credit).equals(2);
|
||||
expect(foundCreditAccount.debit).equals(0);
|
||||
expect(foundCreditAccount.balance).equals(2);
|
||||
});
|
||||
|
||||
it('Should credit, debit and balance amount rounded without cents.', async () => {
|
||||
@@ -768,7 +871,7 @@ describe('routes: `/financial_statements`', () => {
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(401);
|
||||
expect(res.body.message).equals('unauthorized');
|
||||
expect(res.body.message).equals('Unauthorized');
|
||||
});
|
||||
|
||||
it('Should retrieve columns when display type `date_periods` and columns by `month` between date range.', async () => {
|
||||
@@ -844,6 +947,8 @@ describe('routes: `/financial_statements`', () => {
|
||||
});
|
||||
|
||||
it('Should retrieve all income accounts even it has no transactions.', async () => {
|
||||
const zeroAccount = await tenantFactory.create('account', { account_type_id: incomeType.id });
|
||||
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/profit_loss_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
@@ -853,11 +958,17 @@ describe('routes: `/financial_statements`', () => {
|
||||
to_date: moment('2020-01-01').endOf('month').format('YYYY-MM-DD'),
|
||||
display_columns_type: 'total',
|
||||
display_columns_by: 'month',
|
||||
none_zero: false,
|
||||
})
|
||||
.send();
|
||||
|
||||
const zeroAccount = res.body.income.accounts.filter((a) => a.total.amount === 0);
|
||||
expect(zeroAccount.length).not.equals(0);
|
||||
expect(res.body.profitLoss.income.accounts).include.something.deep.equals({
|
||||
id: zeroAccount.id,
|
||||
index: zeroAccount.index,
|
||||
name: zeroAccount.name,
|
||||
code: zeroAccount.code,
|
||||
total: { amount: 0, date: '2020-01-31', formatted_amount: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it('Should retrieve total of each income account when display columns by `total`.', async () => {
|
||||
@@ -872,14 +983,14 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.income.accounts).to.be.an('array');
|
||||
expect(res.body.income.accounts.length).not.equals(0);
|
||||
expect(res.body.income.accounts[0].id).to.be.an('number');
|
||||
expect(res.body.income.accounts[0].name).to.be.an('string');
|
||||
expect(res.body.income.accounts[0].total).to.be.an('object');
|
||||
expect(res.body.income.accounts[0].total.amount).to.be.an('number');
|
||||
expect(res.body.income.accounts[0].total.formatted_amount).to.be.an('number');
|
||||
expect(res.body.income.accounts[0].total.date).equals(toDate);
|
||||
expect(res.body.profitLoss.income.accounts).to.be.an('array');
|
||||
expect(res.body.profitLoss.income.accounts.length).not.equals(0);
|
||||
expect(res.body.profitLoss.income.accounts[0].id).to.be.an('number');
|
||||
expect(res.body.profitLoss.income.accounts[0].name).to.be.an('string');
|
||||
expect(res.body.profitLoss.income.accounts[0].total).to.be.an('object');
|
||||
expect(res.body.profitLoss.income.accounts[0].total.amount).to.be.an('number');
|
||||
expect(res.body.profitLoss.income.accounts[0].total.formatted_amount).to.be.an('number');
|
||||
expect(res.body.profitLoss.income.accounts[0].total.date).equals(toDate);
|
||||
});
|
||||
|
||||
it('Should retrieve credit sumation of income accounts.', async () => {
|
||||
@@ -893,10 +1004,10 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.income.total).to.be.an('object');
|
||||
expect(res.body.income.total.amount).equals(2000);
|
||||
expect(res.body.income.total.formatted_amount).equals(2000);
|
||||
expect(res.body.income.total.date).equals('2021-01-01');
|
||||
expect(res.body.profitLoss.income.total).to.be.an('object');
|
||||
expect(res.body.profitLoss.income.total.amount).equals(2000);
|
||||
expect(res.body.profitLoss.income.total.formatted_amount).equals(2000);
|
||||
expect(res.body.profitLoss.income.total.date).equals('2021-01-01');
|
||||
});
|
||||
|
||||
it('Should retrieve debit sumation of expenses accounts.', async () => {
|
||||
@@ -910,10 +1021,10 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.expenses.total).to.be.an('object');
|
||||
expect(res.body.expenses.total.amount).equals(6000);
|
||||
expect(res.body.expenses.total.formatted_amount).equals(6000);
|
||||
expect(res.body.expenses.total.date).equals('2021-01-01');
|
||||
expect(res.body.profitLoss.expenses.total).to.be.an('object');
|
||||
expect(res.body.profitLoss.expenses.total.amount).equals(1000);
|
||||
expect(res.body.profitLoss.expenses.total.formatted_amount).equals(1000);
|
||||
expect(res.body.profitLoss.expenses.total.date).equals('2021-01-01');
|
||||
});
|
||||
|
||||
it('Should retrieve credit total of income accounts with `date_periods` columns between the given date range.', async () => {
|
||||
@@ -929,9 +1040,9 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.income.total_periods[0].amount).equals(0);
|
||||
expect(res.body.income.total_periods[1].amount).equals(2000);
|
||||
expect(res.body.income.total_periods[2].amount).equals(2000);
|
||||
expect(res.body.profitLoss.income.total_periods[0].amount).equals(0);
|
||||
expect(res.body.profitLoss.income.total_periods[1].amount).equals(2000);
|
||||
expect(res.body.profitLoss.income.total_periods[2].amount).equals(2000);
|
||||
});
|
||||
|
||||
it('Should retrieve debit total of expenses accounts with `date_periods` columns between the given date range.', async () => {
|
||||
@@ -947,9 +1058,9 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.expenses.total_periods[0].amount).equals(0);
|
||||
expect(res.body.expenses.total_periods[1].amount).equals(6000);
|
||||
expect(res.body.expenses.total_periods[2].amount).equals(6000);
|
||||
expect(res.body.profitLoss.expenses.total_periods[0].amount).equals(0);
|
||||
expect(res.body.profitLoss.expenses.total_periods[1].amount).equals(1000);
|
||||
expect(res.body.profitLoss.expenses.total_periods[2].amount).equals(1000);
|
||||
});
|
||||
|
||||
it('Should retrieve total net income with `total column display between the given date range.', async () => {
|
||||
@@ -964,9 +1075,9 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.net_income.total.amount).equals(-4000);
|
||||
expect(res.body.net_income.total.formatted_amount).equals(-4000);
|
||||
expect(res.body.net_income.total.date).equals('2020-12-01');
|
||||
expect(res.body.profitLoss.net_income.total.amount).equals(1000);
|
||||
expect(res.body.profitLoss.net_income.total.formatted_amount).equals(1000);
|
||||
expect(res.body.profitLoss.net_income.total.date).equals('2020-12-01');
|
||||
});
|
||||
|
||||
it('Should retrieve total net income with `date_periods` columns between the given date range.', async () => {
|
||||
@@ -982,14 +1093,15 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.net_income.total_periods.length).equals(5);
|
||||
expect(res.body.net_income.total_periods[0].amount).equals(0);
|
||||
expect(res.body.net_income.total_periods[0].formatted_amount).equal(0);
|
||||
expect(res.body.net_income.total_periods[0].date).equals('2019-12');
|
||||
|
||||
expect(res.body.net_income.total_periods[1].amount).equals(-4000);
|
||||
expect(res.body.net_income.total_periods[1].formatted_amount).equal(-4000);
|
||||
expect(res.body.net_income.total_periods[1].date).equals('2020-03');
|
||||
expect(res.body.profitLoss.net_income).deep.equals({
|
||||
total_periods: [
|
||||
{ date: '2019-12', amount: 0, formatted_amount: 0 },
|
||||
{ date: '2020-03', amount: 1000, formatted_amount: 1000 },
|
||||
{ date: '2020-06', amount: 1000, formatted_amount: 1000 },
|
||||
{ date: '2020-09', amount: 1000, formatted_amount: 1000 },
|
||||
{ date: '2020-12', amount: 1000, formatted_amount: 1000 }
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not retrieve income or expenses accounts that has no transactions between the given date range in case none_zero equals true.', async () => {
|
||||
@@ -1001,11 +1113,12 @@ describe('routes: `/financial_statements`', () => {
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2021-01-01',
|
||||
display_columns_by: 'month',
|
||||
display_columns_type: 'date_periods',
|
||||
none_zero: true,
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.income.accounts.length).equals(1);
|
||||
expect(res.body.profitLoss.income.accounts.length).equals(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user