WIP server side.

This commit is contained in:
Ahmed Bouhuolia
2020-01-22 02:09:45 +02:00
parent de905d7e7c
commit 488709088b
123 changed files with 14885 additions and 771 deletions

View File

@@ -3,6 +3,7 @@ import helmet from 'helmet';
import boom from 'express-boom';
import '../config';
import routes from '@/http';
import '@/models';
const app = express();

View File

@@ -0,0 +1,76 @@
export default class BudgetEntriesSet {
constructor() {
this.accounts = {};
this.totalSummary = {}
this.orderSize = null;
}
setZeroPlaceholder() {
if (!this.orderSize) { return; }
Object.values(this.accounts).forEach((account) => {
for (let i = 0; i <= this.orderSize.length; i++) {
if (typeof account[i] === 'undefined') {
account[i] = { amount: 0 };
}
}
});
}
static from(accounts, configs) {
const collection = new this(configs);
accounts.forEach((entry) => {
if (typeof this.accounts[entry.accountId] === 'undefined') {
collection.accounts[entry.accountId] = {};
}
if (entry.order) {
collection.accounts[entry.accountId][entry.order] = entry;
}
});
return collection;
}
toArray() {
const output = [];
Object.key(this.accounts).forEach((accountId) => {
const entries = this.accounts[accountId];
output.push({
account_id: accountId,
entries: [
...Object.key(entries).map((order) => {
const entry = entries[order];
return {
order,
amount: entry.amount,
};
}),
],
});
});
}
calcTotalSummary() {
const totalSummary = {};
for (let i = 0; i < this.orderSize.length; i++) {
Object.value(this.accounts).forEach((account) => {
if (typeof totalSummary[i] !== 'undefined') {
totalSummary[i] = { amount: 0, order: i };
}
totalSummary[i].amount += account[i].amount;
});
}
this.totalSummary = totalSummary;
}
toArrayTotalSummary() {
return Object.values(this.totalSummary);
}
}

View File

View File

@@ -0,0 +1,279 @@
export default {
METADATA_GROUP: 'default',
KEY_COLUMN: 'key',
VALUE_COLUMN: 'value',
TYPE_COLUMN: 'type',
extraColumns: [],
metadata: [],
shouldReload: true,
extraMetadataQuery: () => {},
/**
* Set the value column key to query from.
* @param {String} name -
*/
setKeyColumnName(name) {
this.KEY_COLUMN = name;
},
/**
* Set the key column name to query from.
* @param {String} name -
*/
setValueColumnName(name) {
this.VALUE_COLUMN = name;
},
/**
* Set extra columns to be added to the rows.
* @param {Array} columns -
*/
setExtraColumns(columns) {
this.extraColumns = columns;
},
/**
* Metadata database query.
* @param {Object} query -
* @param {String} groupName -
*/
whereQuery(query, key) {
const groupName = this.METADATA_GROUP;
if (groupName) {
query.where('group', groupName);
}
if (key) {
if (Array.isArray(key)) {
query.whereIn('key', key);
} else {
query.where('key', key);
}
}
},
/**
* Loads the metadata from the storage.
* @param {String|Array} key -
* @param {Boolean} force -
*/
async load(force = false) {
if (this.shouldReload || force) {
const metadataCollection = await this.query((query) => {
this.whereQuery(query);
this.extraMetadataQuery(query);
}).fetchAll();
this.shouldReload = false;
this.metadata = [];
const metadataArray = this.mapMetadataCollection(metadataCollection);
metadataArray.forEach((metadata) => { this.metadata.push(metadata); });
}
},
/**
* Fetches all the metadata that associate with the current group.
*/
async allMeta(force = false) {
await this.load(force);
return this.metadata;
},
/**
* Find the given metadata key.
* @param {String} key -
* @return {object} - Metadata object.
*/
findMeta(key) {
return this.metadata.find((meta) => meta.key === key);
},
/**
* Fetch the metadata of the current group.
* @param {*} key -
*/
async getMeta(key, defaultValue, force = false) {
await this.load(force);
const metadata = this.findMeta(key);
return metadata ? metadata.value : defaultValue || false;
},
/**
* Markes the metadata to should be deleted.
* @param {String} key -
*/
async removeMeta(key) {
await this.load();
const metadata = this.findMeta(key);
if (metadata) {
metadata.markAsDeleted = true;
}
this.shouldReload = true;
/**
* Remove all meta data of the given group.
* @param {*} group
*/
removeAllMeta(group = 'default') {
this.metdata.map((meta) => ({
...(meta.group !== group) ? { markAsDeleted: true } : {},
...meta,
}));
this.shouldReload = true;
},
/**
* Set the meta data to the stack.
* @param {String} key -
* @param {String} value -
*/
async setMeta(key, value, payload) {
if (Array.isArray(key)) {
const metadata = key;
metadata.forEach((meta) => {
this.setMeta(meta.key, meta.value);
});
return;
}
await this.load();
const metadata = this.findMeta(key);
if (metadata) {
metadata.value = value;
metadata.markAsUpdated = true;
} else {
this.metadata.push({
value, key, ...payload, markAsInserted: true,
});
}
},
/**
* Saved the modified metadata.
*/
async saveMeta() {
const inserted = this.metadata.filter((m) => (m.markAsInserted === true));
const updated = this.metadata.filter((m) => (m.markAsUpdated === true));
const deleted = this.metadata.filter((m) => (m.markAsDeleted === true));
const metadataDeletedKeys = deleted.map((m) => m.key);
const metadataInserted = inserted.map((m) => this.mapMetadata(m, 'format'));
const metadataUpdated = updated.map((m) => this.mapMetadata(m, 'format'));
const batchUpdate = (collection) => knex.transaction((trx) => {
const queries = collection.map((tuple) => {
const query = knex(this.tableName);
this.whereQuery(query, tuple.key);
this.extraMetadataQuery(query);
return query.update(tuple).transacting(trx);
});
return Promise.all(queries).then(trx.commit).catch(trx.rollback);
});
await Promise.all([
knex.insert(metadataInserted).into(this.tableName),
batchUpdate(metadataUpdated),
metadataDeletedKeys.length > 0
? this.query('whereIn', this.KEY_COLUMN, metadataDeletedKeys).destroy({
require: true,
}) : null,
]);
this.shouldReload = true;
},
/**
* Purge all the cached metadata in the memory.
*/
purgeMetadata() {
this.metadata = [];
this.shouldReload = true;
},
/**
* Parses the metadata value.
* @param {String} value -
* @param {String} valueType -
*/
parseMetaValue(value, valueType) {
let parsedValue;
switch (valueType) {
case 'integer':
parsedValue = parseInt(value, 10);
break;
case 'float':
parsedValue = parseFloat(value);
break;
case 'boolean':
parsedValue = Boolean(value);
break;
case 'json':
parsedValue = JSON.parse(parsedValue);
break;
default:
parsedValue = value;
break;
}
return parsedValue;
},
/**
* Format the metadata before saving to the database.
* @param {String|Number|Boolean} value -
* @param {String} valueType -
* @return {String|Number|Boolean} -
*/
formatMetaValue(value, valueType) {
let parsedValue;
switch (valueType) {
case 'number':
parsedValue = `${value}`;
break;
case 'boolean':
parsedValue = value ? '1' : '0';
break;
case 'json':
parsedValue = JSON.stringify(parsedValue);
break;
default:
parsedValue = value;
break;
}
return parsedValue;
},
mapMetadata(attr, parseType = 'parse') {
return {
key: attr[this.KEY_COLUMN],
value: (parseType === 'parse')
? this.parseMetaValue(
attr[this.VALUE_COLUMN],
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
)
: this.formatMetaValue(
attr[this.VALUE_COLUMN],
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
),
...this.extraColumns.map((extraCol) => ({
[extraCol]: attr[extraCol] || null,
})),
};
},
/**
* Parse the metadata collection.
* @param {Array} collection -
*/
mapMetadataCollection(collection, parseType = 'parse') {
return collection.map((model) => this.mapMetadata(model.attributes, parseType));
},
};

View File

@@ -0,0 +1,73 @@
export default class NestedSet {
/**
* Constructor method.
* @param {Object} options -
*/
constructor(items, options) {
this.options = {
parentId: 'parent_id',
id: 'id',
...options,
};
this.items = items;
this.collection = {};
this.toTree();
return this.collection;
}
/**
* Link nodes children.
*/
linkChildren() {
if (this.items.length <= 0) return false;
const map = {};
this.items.forEach((item) => {
map[item.id] = item;
map[item.id].children = [];
});
this.items.forEach((item) => {
const parentNodeId = item[this.options.parentId];
if (parentNodeId) {
map[parentNodeId].children.push(item);
}
});
return map;
}
toTree() {
const map = this.linkChildren();
const tree = {};
this.items.forEach((item) => {
const parentNodeId = item[this.options.parentId];
if (!parentNodeId) {
tree[item.id] = map[item.id];
}
});
this.collection = Object.values(tree);
}
walk() {
}
getParents() {
}
getChildren() {
}
toFlattenArray() {
}
toArray() {
}
}

View File

@@ -27,17 +27,52 @@ factory.define('password_reset', 'password_resets', async () => {
factory.define('account_type', 'account_types', async () => ({
name: faker.lorem.words(2),
normal: 'debit',
}));
factory.define('account_balance', 'account_balances', async () => {
const account = await factory.create('account');
return {
account_id: account.id,
amount: faker.random.number(),
currency_code: 'USD',
};
});
factory.define('account', 'accounts', async () => {
const accountType = await factory.create('account_type');
return {
name: faker.lorem.word(),
code: faker.random.number(),
account_type_id: accountType.id,
description: faker.lorem.paragraph(),
};
});
factory.define('account_transaction', 'accounts_transactions', async () => {
const account = await factory.create('account');
const user = await factory.create('user');
return {
account_id: account.id,
credit: faker.random.number(),
debit: 0,
user_id: user.id,
};
});
factory.define('manual_journal', 'manual_journals', async () => {
const user = await factory.create('user');
return {
reference: faker.random.number(),
amount: faker.random.number(),
// date: faker.random,
user_id: user.id,
};
});
factory.define('item_category', 'items_categories', () => ({
label: faker.name.firstName(),
description: faker.lorem.text(),
@@ -135,11 +170,13 @@ factory.define('resource_field', 'resource_fields', async () => {
return {
label_name: faker.lorem.words(),
slug: faker.lorem.slug(),
data_type: dataTypes[Math.floor(Math.random() * dataTypes.length)],
help_text: faker.lorem.words(),
default: faker.lorem.word(),
resource_id: resource.id,
active: true,
columnable: true,
predefined: false,
};
});
@@ -167,4 +204,47 @@ factory.define('view_has_columns', 'view_has_columns', async () => {
};
});
factory.define('expense', 'expenses', async () => {
const paymentAccount = await factory.create('account');
const expenseAccount = await factory.create('account');
const user = await factory.create('user');
return {
payment_account_id: paymentAccount.id,
expense_account_id: expenseAccount.id,
user_id: user.id,
amount: faker.random.number(),
currency_code: 'USD',
};
});
factory.define('option', 'options', async () => {
return {
key: faker.lorem.slug(),
value: faker.lorem.slug(),
group: faker.lorem.slug(),
};
});
factory.define('budget', 'budgets', async () => {
return {
name: faker.lorem.slug(),
fiscal_year: '2020',
period: 'month',
account_types: 'profit_loss',
};
});
factory.define('budget_entry', 'budget_entries', async () => {
const budget = await factory.create('budget');
const account = await factory.create('account');
return {
account_id: account.id,
budget_id: budget.id,
amount: 1000,
order: 1,
};
});
export default factory;

View File

@@ -1,7 +1,11 @@
import Knex from 'knex';
import { knexSnakeCaseMappers } from 'objection';
import knexfile from '@/../knexfile';
const config = knexfile[process.env.NODE_ENV];
const knex = Knex(config);
const knex = Knex({
...config,
...knexSnakeCaseMappers({ upperCase: true }),
});
export default knex;

View File

@@ -7,6 +7,8 @@ exports.up = function (knex) {
table.integer('parent_account_id');
table.string('code', 10);
table.text('description');
table.boolean('active').defaultTo(true);
table.integer('index').unsigned();
table.timestamps();
});
};

View File

@@ -1,11 +1,11 @@
exports.up = function (knex) {
return knex.schema.createTable('account_balance', (table) => {
return knex.schema.createTable('account_balances', (table) => {
table.increments();
table.integer('account_id');
table.decimal('amount');
table.decimal('amount', 15, 5);
table.string('currency_code', 3);
});
};
exports.down = (knex) => knex.schema.dropTableIfExists('account_balance');
exports.down = (knex) => knex.schema.dropTableIfExists('account_balances');

View File

@@ -3,6 +3,9 @@ exports.up = function (knex) {
return knex.schema.createTable('account_types', (table) => {
table.increments();
table.string('name');
table.string('normal');
table.boolean('balance_sheet');
table.boolean('income_sheet');
});
};

View File

@@ -3,11 +3,13 @@ exports.up = function (knex) {
return knex.schema.createTable('resource_fields', (table) => {
table.increments();
table.string('label_name');
table.string('slug');
table.string('data_type');
table.string('help_text');
table.string('default');
table.boolean('active');
table.boolean('predefined');
table.boolean('columnable');
table.json('options');
table.integer('resource_id').unsigned().references('id').inTable('resources');
});

View File

@@ -5,6 +5,7 @@ exports.up = function (knex) {
table.string('name');
table.boolean('predefined');
table.integer('resource_id').unsigned().references('id').inTable('resources');
table.string('roles_logic_expression');
});
};

View File

@@ -0,0 +1,20 @@
exports.up = function(knex) {
return knex.schema.createTable('accounts_transactions', (table) => {
table.increments();
table.decimal('credit');
table.decimal('debit');
table.string('transaction_type');
table.string('reference_type');
table.integer('reference_id');
table.integer('account_id').unsigned().references('id').inTable('accounts');
table.string('note');
table.integer('user_id').unsigned().references('id').inTable('users');
table.date('date');
table.timestamps();
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('accounts_transactions');
};

View File

@@ -0,0 +1,14 @@
exports.up = function(knex) {
return knex.schema.createTable('options', (table) => {
table.increments();
table.string('key');
table.string('value');
table.string('group');
table.string('type');
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('options');
};

View File

@@ -0,0 +1,20 @@
exports.up = function(knex) {
return knex.schema.createTable('expenses', (table) => {
table.increments();
table.decimal('amount');
table.string('currency_code');
table.decimal('exchange_rate');
table.text('description');
table.integer('expense_account_id').unsigned().references('id').inTable('accounts');
table.integer('payment_account_id').unsigned().references('id').inTable('accounts');
table.string('reference');
table.integer('user_id').unsigned().references('id').inTable('users');
table.date('date');
// table.timestamps();
})
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('expenses');
};

View File

@@ -0,0 +1,14 @@
exports.up = function(knex) {
return knex.schema.createTable('currency_adjustments', (table) => {
table.increments();
table.date('date');
table.string('currency_code');
table.decimal('exchange_rate');
table.string('note');
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('currency_adjustments');
};

View File

@@ -0,0 +1,17 @@
exports.up = function(knex) {
return knex.schema.createTable('manual_journals', (table) => {
table.increments();
table.string('reference');
table.string('transaction_type');
table.decimal('amount');
table.date('date');
table.string('note');
table.integer('user_id').unsigned().references('id').inTable('users');
table.timestamps();
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('manual_journals');
};

View File

@@ -0,0 +1,12 @@
exports.up = function(knex) {
return knex.schema.createTable('recurring_journals', (table) => {
table.increments();
table.string('template_name');
table.timestamps();
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('recurring_journals');
};

View File

@@ -0,0 +1,14 @@
exports.up = function(knex) {
return knex.schema.createTable('budgets', (table) => {
table.increments();
table.string('name');
table.string('fiscal_year');
table.string('period');
table.string('account_types');
});
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('budgets');
};

View File

@@ -0,0 +1,14 @@
exports.up = function(knex) {
return knex.schema.createTable('budget_entries', (table) => {
table.increments();
table.integer('budget_id').unsigned().references('id').inTable('budgets');
table.integer('account_id').unsigned().references('id').inTable('accounts');
table.decimal('amount', 15, 5);
table.integer('order');
})
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists('budget_entries');
};

View File

@@ -1,10 +1,10 @@
import express from 'express';
import { check, validationResult, oneOf } from 'express-validator';
import { difference } from 'lodash';
import knex from 'knex';
import asyncMiddleware from '../middleware/asyncMiddleware';
import Account from '@/models/Account';
import '@/models/AccountBalance';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry';
export default {
/**
@@ -36,7 +36,6 @@ export default {
],
async handler(req, res) {
const validationErrors = validationResult(req);
// const defaultCurrency = 'USD';
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
@@ -45,16 +44,13 @@ export default {
}
const { accounts } = req.body;
const accountsIds = accounts.map((account) => account.id);
const accountsCollection = await Account.query((query) => {
query.select(['id']);
query.whereIn('id', accountsIds);
}).fetchAll({
withRelated: ['balances'],
});
const accountsCollection = await Account.query()
.select(['id'])
.whereIn('id', accountsIds);
const accountsStoredIds = accountsCollection.map((account) => account.attributes.id);
// Get the stored accounts Ids and difference with submit accounts.
const accountsStoredIds = accountsCollection.map((account) => account.id);
const notFoundAccountsIds = difference(accountsIds, accountsStoredIds);
const errorReasons = [];
@@ -62,34 +58,35 @@ export default {
const ids = notFoundAccountsIds.map((a) => parseInt(a, 10));
errorReasons.push({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids });
}
if (errorReasons.length > 0) {
return res.boom.badData(null, { errors: errorReasons });
}
const storedAccountsBalances = accountsCollection.related('balances');
const sharedJournalDetails = new JournalEntry({
referenceType: 'OpeningBalance',
referenceId: 1,
});
const journalEntries = new JournalPoster(sharedJournalDetails);
const submitBalancesMap = new Map(accounts.map((account) => [account, account.id]));
const storedBalancesMap = new Map(storedAccountsBalances.map((balance) => [
balance.attributes, balance.attributes.id,
]));
accounts.forEach((account) => {
const entry = new JournalEntry({
account: account.id,
accountNormal: account.type.normal,
});
// const updatedStoredBalanced = [];
const notStoredBalances = [];
accountsIds.forEach((id) => {
if (!storedBalancesMap.get(id)) {
notStoredBalances.push(id);
if (account.credit) {
entry.credit = account.credit;
journalEntries.credit(entry);
} else if (account.debit) {
entry.debit = account.debit;
journalEntries.debit(entry);
}
});
await knex('accounts_balances').insert([
...notStoredBalances.map((id) => {
const account = submitBalancesMap.get(id);
return { ...account };
}),
await Promise.all([
journalEntries.saveEntries(),
journalEntries.saveBalance(),
]);
return res.status(200).send();
},
},

View File

@@ -0,0 +1,213 @@
import { check, query, validationResult } from 'express-validator';
import express from 'express';
import { difference } from 'lodash';
import Account from '@/models/Account';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import JWTAuth from '@/http/middleware/jwtAuth';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry';
import ManualJournal from '@/models/JournalEntry';
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.use(JWTAuth);
router.post('/make-journal-entries',
this.makeJournalEntries.validation,
asyncMiddleware(this.makeJournalEntries.handler));
router.post('/recurring-journal-entries',
this.recurringJournalEntries.validation,
asyncMiddleware(this.recurringJournalEntries.handler));
router.post('quick-journal-entries',
this.quickJournalEntries.validation,
asyncMiddleware(this.quickJournalEntries.handler));
return router;
},
/**
* Make journal entrires.
*/
makeJournalEntries: {
validation: [
check('date').isISO8601(),
check('reference').exists(),
check('entries').isArray({ min: 1 }),
check('entries.*.credit').isNumeric().toInt(),
check('entries.*.debit').isNumeric().toInt(),
check('entries.*.account_id').isNumeric().toInt(),
check('entries.*.note').optional(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const form = { ...req.body };
const errorReasons = [];
let totalCredit = 0;
let totalDebit = 0;
form.entries.forEach((entry) => {
if (entry.credit > 0) {
totalCredit += entry.credit;
}
if (entry.debit > 0) {
totalDebit += entry.debit;
}
});
if (totalCredit <= 0 || totalDebit <= 0) {
errorReasons.push({
type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
code: 400,
});
}
if (totalCredit !== totalDebit) {
errorReasons.push({ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 100 });
}
const accountsIds = form.entries.map((entry) => entry.account_id);
const accounts = await Account.query().whereIn('id', accountsIds)
.withGraphFetched('type');
const storedAccountsIds = accounts.map((account) => account.id);
if (difference(accountsIds, storedAccountsIds).length > 0) {
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
}
const journalReference = await ManualJournal.query().where('reference', form.reference);
if (journalReference.length > 0) {
errorReasons.push({ type: 'REFERENCE.ALREADY.EXISTS', code: 300 });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const journalPoster = new JournalPoster();
form.entries.forEach((entry) => {
const account = accounts.find((a) => a.id === entry.account_id);
const jouranlEntry = new JournalEntry({
debit: entry.debit,
credit: entry.credit,
account: account.id,
accountNormal: account.type.normal,
note: entry.note,
});
if (entry.debit) {
journalPoster.debit(jouranlEntry);
} else {
journalPoster.credit(jouranlEntry);
}
});
// Saves the journal entries and accounts balance changes.
await Promise.all([
journalPoster.saveEntries(),
journalPoster.saveBalance(),
]);
return res.status(200).send();
},
},
/**
* Saves recurring journal entries template.
*/
recurringJournalEntries: {
validation: [
check('template_name').exists(),
check('recurrence').exists(),
check('active').optional().isBoolean().toBoolean(),
check('entries').isArray({ min: 1 }),
check('entries.*.credit').isNumeric().toInt(),
check('entries.*.debit').isNumeric().toInt(),
check('entries.*.account_id').isNumeric().toInt(),
check('entries.*.note').optional(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
},
},
recurringJournalsList: {
validation: [
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('template_name').optional(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
},
},
quickJournalEntries: {
validation: [
check('date').exists().isISO8601(),
check('amount').exists().isNumeric().toFloat(),
check('credit_account_id').exists().isNumeric().toInt(),
check('debit_account_id').exists().isNumeric().toInt(),
check('transaction_type').exists(),
check('note').optional(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const errorReasons = [];
const form = { ...req.body };
const foundAccounts = await Account.query()
.where('id', form.credit_account_id)
.orWhere('id', form.debit_account_id);
const creditAccount = foundAccounts.find((a) => a.id === form.credit_account_id);
const debitAccount = foundAccounts.find((a) => a.id === form.debit_account_id);
if (!creditAccount) {
errorReasons.push({ type: 'CREDIT_ACCOUNT.NOT.EXIST', code: 100 });
}
if (!debitAccount) {
errorReasons.push({ type: 'DEBIT_ACCOUNT.NOT.EXIST', code: 200 });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
// const journalPoster = new JournalPoster();
// const journalCredit = new JournalEntry({
// debit:
// account: debitAccount.id,
// referenceId:
// })
return res.status(200).send();
},
},
};

View File

@@ -1,10 +1,13 @@
import express from 'express';
import { check, validationResult, param } from 'express-validator';
import asyncMiddleware from '../middleware/asyncMiddleware';
import { check, validationResult, param, query } from 'express-validator';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Account from '@/models/Account';
// import AccountBalance from '@/models/AccountBalance';
import AccountType from '@/models/AccountType';
// import JWTAuth from '@/http/middleware/jwtAuth';
import AccountTransaction from '@/models/AccountTransaction';
import JournalPoster from '@/services/Accounting/JournalPoster';
import AccountBalance from '@/models/AccountBalance';
import JWTAuth from '@/http/middleware/jwtAuth';
import NestedSet from '../../collection/NestedSet';
export default {
/**
@@ -13,7 +16,7 @@ export default {
router() {
const router = express.Router();
// router.use(JWTAuth);
router.use(JWTAuth);
router.post('/',
this.newAccount.validation,
asyncMiddleware(this.newAccount.handler));
@@ -23,12 +26,33 @@ export default {
asyncMiddleware(this.editAccount.handler));
router.get('/:id',
this.getAccount.validation,
asyncMiddleware(this.getAccount.handler));
router.get('/',
this.getAccountsList.validation,
asyncMiddleware(this.getAccountsList.handler));
router.delete('/:id',
this.deleteAccount.validation,
asyncMiddleware(this.deleteAccount.handler));
router.post('/:id/active',
this.activeAccount.validation,
asyncMiddleware(this.activeAccount.handler));
router.post('/:id/inactive',
this.inactiveAccount.validation,
asyncMiddleware(this.inactiveAccount.handler));
router.post('/:id/recalculate-balance',
this.recalcualteBalanace.validation,
asyncMiddleware(this.recalcualteBalanace.handler));
router.post('/:id/transfer_account/:toAccount',
this.transferToAnotherAccount.validation,
asyncMiddleware(this.transferToAnotherAccount.handler));
return router;
},
@@ -37,10 +61,10 @@ export default {
*/
newAccount: {
validation: [
check('name').isLength({ min: 3 }).trim().escape(),
check('code').isLength({ max: 10 }).trim().escape(),
check('account_type_id').isNumeric().toInt(),
check('description').trim().escape(),
check('name').exists().isLength({ min: 3 }).trim().escape(),
check('code').exists().isLength({ max: 10 }).trim().escape(),
check('account_type_id').exists().isNumeric().toInt(),
check('description').optional().trim().escape(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
@@ -50,34 +74,31 @@ export default {
code: 'validation_error', ...validationErrors,
});
}
const form = { ...req.body };
const { name, code, description } = req.body;
const { account_type_id: typeId } = req.body;
const foundAccountCodePromise = form.code
? Account.query().where('code', form.code) : null;
const foundAccountCodePromise = code ? Account.where('code', code).fetch() : null;
const foundAccountTypePromise = AccountType.where('id', typeId).fetch();
const foundAccountTypePromise = AccountType.query()
.findById(form.account_type_id);
const [foundAccountCode, foundAccountType] = await Promise.all([
foundAccountCodePromise,
foundAccountTypePromise,
foundAccountCodePromise, foundAccountTypePromise,
]);
if (!foundAccountCode && foundAccountCodePromise) {
if (foundAccountCodePromise && foundAccountCode.length > 0) {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
});
}
if (!foundAccountType) {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 110 }],
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 200 }],
});
}
const account = Account.forge({
name, code, account_type_id: typeId, description,
});
await Account.query().insert({ ...form });
await account.save();
return res.status(200).send({ item: { ...account.attributes } });
return res.status(200).send({ item: { } });
},
},
@@ -86,11 +107,11 @@ export default {
*/
editAccount: {
validation: [
param('id').toInt(),
check('name').isLength({ min: 3 }).trim().escape(),
check('code').isLength({ max: 10 }).trim().escape(),
check('account_type_id').isNumeric().toInt(),
check('description').trim().escape(),
param('id').exists().toInt(),
check('name').exists().isLength({ min: 3 }).trim().escape(),
check('code').exists().isLength({ max: 10 }).trim().escape(),
check('account_type_id').exists().isNumeric().toInt(),
check('description').optional().trim().escape(),
],
async handler(req, res) {
const { id } = req.params;
@@ -101,39 +122,33 @@ export default {
code: 'validation_error', ...validationErrors,
});
}
const account = await Account.where('id', id).fetch();
const form = { ...req.body };
const account = await Account.query().findById(id);
if (!account) {
return res.boom.notFound();
}
const { name, code, description } = req.body;
const { account_type_id: typeId } = req.body;
const foundAccountCodePromise = (form.code && form.code !== account.code)
? Account.query().where('code', form.code).whereNot('id', account.id) : null;
const foundAccountCodePromise = (code && code !== account.attributes.code)
? Account.query({ where: { code }, whereNot: { id } }).fetch() : null;
const foundAccountTypePromise = (typeId !== account.attributes.account_type_id)
? AccountType.where('id', typeId).fetch() : null;
const foundAccountTypePromise = (form.account_type_id !== account.account_type_id)
? AccountType.query().where('id', form.account_type_id) : null;
const [foundAccountCode, foundAccountType] = await Promise.all([
foundAccountCodePromise, foundAccountTypePromise,
]);
if (!foundAccountCode && foundAccountCodePromise) {
if (foundAccountCode.length > 0 && foundAccountCodePromise) {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
});
}
if (!foundAccountType && foundAccountTypePromise) {
if (foundAccountType.length <= 0 && foundAccountTypePromise) {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 110 }],
});
}
await account.patch({ ...form });
await account.save({
name, code, account_type_id: typeId, description,
});
return res.status(200).send();
},
},
@@ -142,7 +157,7 @@ export default {
* Get details of the given account.
*/
getAccount: {
valiation: [
validation: [
param('id').toInt(),
],
async handler(req, res) {
@@ -165,14 +180,163 @@ export default {
],
async handler(req, res) {
const { id } = req.params;
const account = await Account.where('id', id).fetch();
const account = await Account.query().findById(id);
if (!account) {
return res.boom.notFound();
}
await account.destroy();
const accountTransactions = await AccountTransaction.query()
.where('account_id', account.id);
return res.status(200).send({ id: account.previous('id') });
if (accountTransactions.length > 0) {
return res.boom.badRequest(null, {
errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 100 }],
});
}
await Account.query().deleteById(account.id);
return res.status(200).send();
},
},
/**
* Retrieve accounts list.
*/
getAccountsList: {
validation: [
query('account_types').optional().isArray(),
query('account_types.*').optional().isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const form = {
account_types: [],
...req.body,
};
const accounts = await Account.query()
.modify('filterAccountTypes', form.account_types);
const accountsNestedSet = new NestedSet(accounts, {
parentId: 'parentAccountId',
});
return res.status(200).send({
// ...accountsNestedSet.toArray(),
});
},
},
/**
* Re-calculates balance of the given account.
*/
recalcualteBalanace: {
validation: [
param('id').isNumeric().toInt(),
],
async handler(req, res) {
const { id } = req.params;
const account = await Account.findById(id);
if (!account) {
return res.status(400).send({
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
});
}
const accountTransactions = AccountTransaction.query()
.where('account_id', account.id);
const journalEntries = new JournalPoster();
journalEntries.loadFromCollection(accountTransactions);
// Delete the balance of the given account id.
await AccountBalance.query().where('account_id', account.id).delete();
// Save calcualted account balance.
await journalEntries.saveBalance();
return res.status(200).send();
},
},
/**
* Active the given account.
*/
activeAccount: {
validation: [
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) {
const { id } = req.params;
const account = await Account.findById(id);
if (!account) {
return res.status(400).send({
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
});
}
await account.patch({ active: true });
return res.status(200).send({ id: account.id });
},
},
/**
* Inactive the given account.
*/
inactiveAccount: {
validation: [
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) {
const { id } = req.params;
const account = await Account.findById(id);
if (!account) {
return res.status(400).send({
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
});
}
await account.patch({ active: false });
return res.status(200).send({ id: account.id });
},
},
/**
* Transfer all journal entries of the given account to another account.
*/
transferToAnotherAccount: {
validation: [
param('id').exists().isNumeric().toInt(),
param('toAccount').exists().isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
// const { id, toAccount: toAccountId } = req.params;
// const [fromAccount, toAccount] = await Promise.all([
// Account.query().findById(id),
// Account.query().findById(toAccountId),
// ]);
// const fromAccountTransactions = await AccountTransaction.query()
// .where('account_id', fromAccount);
// return res.status(200).send();
},
},
};

View File

@@ -5,8 +5,8 @@ import path from 'path';
import fs from 'fs';
import Mustache from 'mustache';
import jwt from 'jsonwebtoken';
import User from '@/models/User';
import asyncMiddleware from '../middleware/asyncMiddleware';
import User from '@/models/User';
import PasswordReset from '@/models/PasswordReset';
import mail from '@/services/mail';
import { hashPassword } from '@/utils';
@@ -52,10 +52,10 @@ export default {
const { crediential, password } = req.body;
const { JWT_SECRET_KEY } = process.env;
const user = await User.query({
where: { email: crediential },
orWhere: { phone_number: crediential },
}).fetch();
const user = await User.query()
.where('email', crediential)
.orWhere('phone_number', crediential)
.first();
if (!user) {
return res.boom.badRequest(null, {
@@ -67,15 +67,15 @@ export default {
errors: [{ type: 'INCORRECT_PASSWORD', code: 110 }],
});
}
if (!user.attributes.active) {
if (!user.active) {
return res.boom.badRequest(null, {
errors: [{ type: 'USER_INACTIVE', code: 120 }],
});
}
user.save({ last_login_at: new Date() });
// user.update({ last_login_at: new Date() });
const token = jwt.sign({
email: user.attributes.email,
email: user.email,
_id: user.id,
}, JWT_SECRET_KEY, {
expiresIn: '1d',
@@ -113,7 +113,6 @@ export default {
email,
token: '123123',
});
await passwordReset.save();
const filePath = path.join(__dirname, '../../views/mail/ResetPassword.html');
@@ -166,19 +165,18 @@ export default {
const { token } = req.params;
const { password } = req.body;
const tokenModel = await PasswordReset.query((query) => {
query.where({ token });
query.where('created_at', '>=', Date.now() - 3600000);
}).fetch();
const tokenModel = await PasswordReset.query()
.where('token', token)
.where('created_at', '>=', Date.now() - 3600000)
.first();
if (!tokenModel) {
return res.boom.badRequest(null, {
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
});
}
const user = await User.where({
email: tokenModel.attributes.email,
email: tokenModel.email,
});
if (!user) {
return res.boom.badRequest(null, {
@@ -187,7 +185,7 @@ export default {
}
const hashedPassword = await hashPassword(password);
user.set('password', hashedPassword);
user.password = hashedPassword;
await user.save();
await PasswordReset.where('email', user.get('email')).destroy({ require: false });

View File

@@ -0,0 +1,33 @@
import express from 'express';
export default {
router() {
const router = express.Router();
return router;
},
reconciliations: {
validation: [
],
async handler(req, res) {
},
},
reconciliation: {
validation: [
body('from_date'),
body('to_date'),
body('closing_balance'),
],
async handler(req, res) {
},
},
}

View File

@@ -0,0 +1,10 @@
import express from 'express';
export default {
router() {
const router = express.Router();
return router;
},
};

View File

@@ -0,0 +1,247 @@
import express from 'express';
import {
check,
query,
param,
validationResult,
} from 'express-validator';
import { pick, difference, groupBy } from 'lodash';
import asyncMiddleware from "@/http/middleware/asyncMiddleware";
import JWTAuth from '@/http/middleware/jwtAuth';
import Budget from '@/models/Budget';
import BudgetEntry from '@/models/BudgetEntry';
import Account from '@/models/Account';
import moment from '@/services/Moment';
import BudgetEntriesSet from '@/collection/BudgetEntriesSet';
import AccountType from '@/models/AccountType';
import NestedSet from '@/collection/NestedSet';
import { dateRangeFormat } from '@/utils';
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.use(JWTAuth);
router.post('/',
this.newBudget.validation,
asyncMiddleware(this.newBudget.handler));
router.get('/:id',
this.getBudget.validation,
asyncMiddleware(this.getBudget.handler));
router.get('/:id',
this.deleteBudget.validation,
asyncMiddleware(this.deleteBudget.handler));
router.get('/',
this.listBudgets.validation,
asyncMiddleware(this.listBudgets.handler));
return router;
},
/**
* Retrieve budget details of the given id.
*/
getBudget: {
validation: [
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { id } = req.params;
const budget = await Budget.query().findById(id);
if (!budget) {
return res.status(404).send({
errors: [{ type: 'budget.not.found', code: 100 }],
});
}
const accountTypes = await AccountType.query().where('balance_sheet', true);
const [budgetEntries, accounts] = await Promise.all([
BudgetEntry.query().where('budget_id', budget.id),
Account.query().whereIn('account_type_id', accountTypes.map((a) => a.id)),
]);
const accountsNestedSet = new NestedSet(accounts);
const columns = [];
const fromDate = moment(budget.year).startOf('year')
.add(budget.rangeOffset, budget.rangeBy).toDate();
const toDate = moment(budget.year).endOf('year').toDate();
const dateRange = moment.range(fromDate, toDate);
const dateRangeCollection = Array.from(dateRange.by(budget.rangeBy, {
step: budget.rangeIncrement, excludeEnd: false, excludeStart: false,
}));
dateRangeCollection.forEach((date) => {
columns.push(date.format(dateRangeFormat(budget.rangeBy)));
});
const budgetEntriesSet = BudgetEntriesSet.from(budgetEntries, {
orderSize: columns.length,
});
budgetEntriesSet.setZeroPlaceholder();
budgetEntriesSet.calcTotalSummary();
return res.status(200).send({
columns,
accounts: budgetEntriesSet.toArray(),
total: budgetEntriesSet.toArrayTotalSummary(),
});
},
},
/**
* Delete the given budget.
*/
deleteBudget: {
validation: [
param('id').exists(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { id } = req.params;
const budget = await Budget.query().findById(id);
if (!budget) {
return res.status(404).send({
errors: [{ type: 'budget.not.found', code: 100 }],
});
}
await BudgetEntry.query().where('budget_id', budget.id).delete();
await budget.delete();
return res.status(200).send();
},
},
/**
* Saves the new budget.
*/
newBudget: {
validation: [
check('name').exists(),
check('fiscal_year').exists(),
check('period').exists().isIn(['year', 'month', 'quarter', 'half-year']),
check('accounts_type').exists().isIn(['balance_sheet', 'profit_loss']),
check('accounts').isArray(),
check('accounts.*.account_id').exists().isNumeric().toInt(),
check('accounts.*.entries').exists().isArray(),
check('accounts.*.entries.*.amount').exists().isNumeric().toFloat(),
check('accounts.*.entries.*.order').exists().isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const form = { ...req.body };
const submitAccountsIds = form.accounts.map((a) => a.account_id);
const storedAccounts = await Account.query().whereIn('id', submitAccountsIds);
const storedAccountsIds = storedAccounts.map((a) => a.id);
const errorReasons = [];
const notFoundAccountsIds = difference(submitAccountsIds, storedAccountsIds);
if (notFoundAccountsIds.length > 0) {
errorReasons.push({
type: 'ACCOUNT.NOT.FOUND', code: 200, accounts: notFoundAccountsIds,
});
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
// validation entries order.
const budget = await Budget.query().insert({
...pick(form, ['name', 'fiscal_year', 'period', 'accounts_type']),
});
const promiseOpers = [];
form.accounts.forEach((account) => {
account.entries.forEach((entry) => {
const budgetEntry = BudgetEntry.query().insert({
account_id: account.account_id,
amount: entry.amount,
order: entry.order,
});
promiseOpers.push(budgetEntry);
});
});
await Promise.all(promiseOpers);
return res.status(200).send({ id: budget.id });
},
},
/**
* List of paginated budgets items.
*/
listBudgets: {
validation: [
query('year').optional(),
query('income_statement').optional().isBoolean().toBoolean(),
query('profit_loss').optional().isBoolean().toBoolean(),
query('page').optional().isNumeric().toInt(),
query('page_size').isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const filter = {
page_size: 10,
page: 1,
...req.query,
};
const budgets = await Budget.query().runBefore((result, q) => {
if (filter.profit_loss) {
q.modify('filterByYear', filter.year);
}
if (filter.income_statement) {
q.modify('filterByIncomeStatement', filter.income_statement);
}
if (filter.profit_loss) {
q.modify('filterByProfitLoss', filter.profit_loss);
}
q.page(filter.page, filter.page_size);
return result;
});
return res.status(200).send({
items: budgets.items,
})
},
},
};

View File

@@ -0,0 +1,122 @@
import express from 'express';
import { query, validationResult } from 'express-validator';
import moment from 'moment';
import jwtAuth from '@/http/middleware/jwtAuth';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Budget from '@/models/Budget';
import Account from '@/models/Account';
import AccountType from '@/models/AccountType';
import NestedSet from '@/collection/NestedSet';
import BudgetEntry from '@/models/BudgetEntry';
import { dateRangeFormat } from '@/utils';
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.use(jwtAuth);
router.get('/budget_verses_actual/:reportId',
this.budgetVersesActual.validation,
asyncMiddleware(this.budgetVersesActual.handler));
return router;
},
budgetVersesActual: {
validation: [
query('basis').optional().isIn(['cash', 'accural']),
query('period').optional(),
query('active_accounts').optional().toBoolean(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { reportId } = req.params;
const form = { ...req.body };
const errorReasons = [];
const budget = await Budget.query().findById(reportId);
if (!budget) {
errorReasons.push({ type: 'BUDGET_NOT_FOUND', code: 100 });
}
const budgetEntries = await BudgetEntry.query().where('budget_id', budget.id);
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const accountTypes = await AccountType.query()
.where('balance_sheet', budget.accountTypes === 'balance_sheet')
.where('income_sheet', budget.accountTypes === 'profit_losss');
const accounts = await Account.query().runBefore((result, q) => {
const accountTypesIds = accountTypes.map((t) => t.id);
if (accountTypesIds.length > 0) {
q.whereIn('account_type_id', accountTypesIds);
}
q.where('active', form.active_accounts === true);
q.withGraphFetched('transactions');
});
// const accountsNestedSet = NestedSet.from(accounts);
const fromDate = moment(budget.year).startOf('year')
.add(budget.rangeOffset, budget.rangeBy).toDate();
const toDate = moment(budget.year).endOf('year').toDate();
const dateRange = moment.range(fromDate, toDate);
const dateRangeCollection = Array.from(dateRange.by(budget.rangeBy, {
step: budget.rangeIncrement, excludeEnd: false, excludeStart: false,
}));
// // const accounts = {
// // assets: [
// // {
// // name: '',
// // code: '',
// // totalEntries: [
// // {
// // }
// // ],
// // children: [
// // {
// // name: '',
// // code: '',
// // entries: [
// // {
// // }
// // ]
// // }
// // ]
// // }
// // ]
// // }
return res.status(200).send({
columns: dateRangeCollection.map(d => d.format(dateRangeFormat(budget.rangeBy))),
// accounts: {
// asset: [],
// liabilities: [],
// equaity: [],
// income: [],
// expenses: [],
// }
});
},
},
}

View File

@@ -0,0 +1,17 @@
export default {
router() {
},
addExchangePrice: {
validation: {
},
async handler(req, res) {
},
},
}

View File

@@ -0,0 +1,10 @@
import express from 'express';
export default {
router() {
const router = express.Router();
return router;
},
};

View File

@@ -0,0 +1,367 @@
import express from 'express';
import {
check,
param,
query,
validationResult,
} from 'express-validator';
import moment from 'moment';
import { difference, chain } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Expense from '@/models/Expense';
import Account from '@/models/Account';
import JournalPoster from '@/services/Accounting/JournalPoster';
import JournalEntry from '@/services/Accounting/JournalEntry';
import JWTAuth from '@/http/middleware/jwtAuth';
import AccountTransaction from '@/models/AccountTransaction';
import View from '@/models/View';
import Resource from '../../models/Resource';
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.use(JWTAuth);
router.post('/',
this.newExpense.validation,
asyncMiddleware(this.newExpense.handler));
router.delete('/:id',
this.deleteExpense.validation,
asyncMiddleware(this.deleteExpense.handler));
router.post('/bulk',
this.bulkAddExpenses.validation,
asyncMiddleware(this.bulkAddExpenses.handler));
router.post('/:id',
this.updateExpense.validation,
asyncMiddleware(this.updateExpense.handler));
router.get('/',
this.listExpenses.validation,
asyncMiddleware(this.listExpenses.handler));
return router;
},
/**
* Saves a new expense.
*/
newExpense: {
validation: [
check('date').optional().isISO8601(),
check('payment_account_id').exists().isNumeric().toInt(),
check('expense_account_id').exists().isNumeric().toInt(),
check('description').optional(),
check('amount').exists().isNumeric().toFloat(),
check('currency_code').optional(),
check('exchange_rate').optional().isNumeric().toFloat(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const form = {
date: new Date(),
...req.body,
};
// Convert the date to the general format.
form.date = moment(form.date).format('YYYY-MM-DD');
const errorReasons = [];
const paymentAccount = await Account.query()
.findById(form.payment_account_id).first();
if (!paymentAccount) {
errorReasons.push({ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 100 });
}
const expenseAccount = await Account.query()
.findById(form.expense_account_id).first();
if (!expenseAccount) {
errorReasons.push({ type: 'EXPENSE.ACCOUNT.NOT.FOUND', code: 200 });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const expenseTransaction = await Expense.query().insert({ ...form });
const journalEntries = new JournalPoster();
const creditEntry = new JournalEntry({
credit: form.amount,
referenceId: expenseTransaction.id,
referenceType: Expense.referenceType,
date: form.date,
account: expenseAccount.id,
accountNormal: 'debit',
});
const debitEntry = new JournalEntry({
debit: form.amount,
referenceId: expenseTransaction.id,
referenceType: Expense.referenceType,
date: form.date,
account: paymentAccount.id,
accountNormal: 'debit',
});
journalEntries.credit(creditEntry);
journalEntries.debit(debitEntry);
await Promise.all([
journalEntries.saveEntries(),
journalEntries.saveBalance(),
]);
return res.status(200).send({ id: expenseTransaction.id });
},
},
/**
* Bulk add expneses to the given accounts.
*/
bulkAddExpenses: {
validation: [
check('expenses').exists().isArray({ min: 1 }),
check('expenses.*.date').optional().isISO8601(),
check('expenses.*.payment_account_id').exists().isNumeric().toInt(),
check('expenses.*.expense_account_id').exists().isNumeric().toInt(),
check('expenses.*.description').optional(),
check('expenses.*.amount').exists().isNumeric().toFloat(),
check('expenses.*.currency_code').optional(),
check('expenses.*.exchange_rate').optional().isNumeric().toFloat(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const form = { ...req.body };
const errorReasons = [];
const paymentAccountsIds = chain(form.expenses)
.map((e) => e.payment_account_id).uniq().value();
const expenseAccountsIds = chain(form.expenses)
.map((e) => e.expense_account_id).uniq().value();
const [expensesAccounts, paymentAccounts] = await Promise.all([
Account.query().whereIn('id', expenseAccountsIds),
Account.query().whereIn('id', paymentAccountsIds),
]);
const storedExpensesAccountsIds = expensesAccounts.map((a) => a.id);
const storedPaymentAccountsIds = paymentAccounts.map((a) => a.id);
const notFoundPaymentAccountsIds = difference(expenseAccountsIds, storedExpensesAccountsIds);
const notFoundExpenseAccountsIds = difference(paymentAccountsIds, storedPaymentAccountsIds);
if (notFoundPaymentAccountsIds.length > 0) {
errorReasons.push({
type: 'PAYMENY.ACCOUNTS.NOT.FOUND',
code: 100,
accounts: notFoundPaymentAccountsIds,
});
}
if (notFoundExpenseAccountsIds.length > 0) {
errorReasons.push({
type: 'EXPENSE.ACCOUNTS.NOT.FOUND',
code: 200,
accounts: notFoundExpenseAccountsIds,
});
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { reasons: errorReasons });
}
const expenseSaveOpers = [];
const journalPoster = new JournalPoster();
form.expenses.forEach(async (expense) => {
const expenseSaveOper = Expense.query().insert({ ...expense });
expenseSaveOpers.push(expenseSaveOper);
});
// Wait unit save all expense transactions.
const savedExpenseTransactions = await Promise.all(expenseSaveOpers);
savedExpenseTransactions.forEach((expense) => {
const date = moment(expense.date).format('YYYY-DD-MM');
const debit = new JournalEntry({
debit: expense.amount,
referenceId: expense.id,
referenceType: Expense.referenceType,
account: expense.payment_account_id,
accountNormal: 'debit',
date,
});
const credit = new JournalEntry({
credit: expense.amount,
referenceId: expense.id,
referenceType: Expense.referenceId,
account: expense.expense_account_id,
accountNormal: 'debit',
date,
});
journalPoster.credit(credit);
journalPoster.debit(debit);
});
// Save expense journal entries and balance change.
await Promise.all([
journalPoster.saveEntries(),
journalPoster.saveBalance(),
]);
return res.status(200).send();
},
},
/**
* Retrieve paginated expenses list.
*/
listExpenses: {
validation: [
query('expense_account_id').optional().isNumeric().toInt(),
query('payment_account_id').optional().isNumeric().toInt(),
query('note').optional(),
query('range_from').optional().isNumeric().toFloat(),
query('range_to').optional().isNumeric().toFloat(),
query('date_from').optional().isISO8601(),
query('date_to').optional().isISO8601(),
query('column_sort_order').optional().isIn(['created_at', 'date', 'amount']),
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('custom_view_id').optional().isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const filter = {
page_size: 10,
page: 1,
...req.query,
};
const errorReasons = [];
const expenseResource = await Resource.query().where('name', 'expenses').first();
if (!expenseResource) {
errorReasons.push({ type: 'EXPENSE_NOT_FOUND', code: 300 });
}
const view = await View.query().runBefore((result, q) => {
if (filter.customer_view_id) {
q.where('id', filter.customer_view_id);
} else {
q.where('favorite', true);
}
q.where('resource_id', expenseResource.id);
q.withGraphFetched('viewRoles');
q.withGraphFetched('columns');
q.first();
return result;
});
if (!view) {
errorReasons.push({ type: 'VIEW_NOT_FOUND', code: 100 });
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
const expenses = await Expense.query()
.modify('filterByAmountRange', filter.range_from, filter.to_range)
.modify('filterByDateRange', filter.date_from, filter.date_to)
.modify('filterByExpenseAccount', filter.expense_account_id)
.modify('filterByPaymentAccount', filter.payment_account_id)
.modify('orderBy', filter.column_sort_order, filter.sort_order)
.page(filter.page, filter.page_size);
return res.status(200).send({
columns: view.columns,
viewRoles: view.viewRoles,
});
},
},
/**
* Delete the given account.
*/
deleteExpense: {
validation: [
param('id').isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { id } = req.params;
const expenseTransaction = await Expense.query().findById(id);
if (!expenseTransaction) {
return res.status(404).send({
errors: [{ type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100 }],
});
}
const expenseEntries = await AccountTransaction.query()
.where('reference_type', 'Expense')
.where('reference_id', expenseTransaction.id);
const expenseEntriesCollect = new JournalPoster();
expenseEntriesCollect.loadEntries(expenseEntries);
expenseEntriesCollect.reverseEntries();
await Promise.all([
expenseTransaction.delete(),
expenseEntriesCollect.deleteEntries(),
expenseEntriesCollect.saveBalance(),
]);
return res.status(200).send();
},
},
/**
* Update details of the given account.
*/
updateExpense: {
validation: [
param('id').isNumeric().toInt(),
check('date').optional().isISO8601(),
check('payment_account_id').exists().isNumeric().toInt(),
check('expense_account_id').exists().isNumeric().toInt(),
check('description').optional(),
check('amount').exists().isNumeric().toFloat(),
check('currency_code').optional(),
check('exchange_rate').optional().isNumeric().toFloat(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { id } = req.params;
const expenseTransaction = await Expense.query().findById(id);
if (!expenseTransaction) {
return res.status(404).send({
errors: [{ type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100 }],
});
}
},
},
};

View File

@@ -0,0 +1,526 @@
import express from 'express';
import { query, validationResult } from 'express-validator';
import moment from 'moment';
import { pick } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import AccountTransaction from '@/models/AccountTransaction';
import jwtAuth from '@/http/middleware/jwtAuth';
import AccountType from '@/models/AccountType';
import Account from '@/models/Account';
import JournalPoster from '@/services/Accounting/JournalPoster';
import { dateRangeCollection } from '@/utils';
const formatNumberClosure = (filter) => (balance) => {
let formattedBalance = parseFloat(balance);
if (filter.no_cents) {
formattedBalance = parseInt(formattedBalance, 10);
}
if (filter.divide_1000) {
formattedBalance /= 1000;
}
return formattedBalance;
};
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.use(jwtAuth);
router.get('/ledger',
this.ledger.validation,
asyncMiddleware(this.ledger.handler));
router.get('/general_ledger',
this.generalLedger.validation,
asyncMiddleware(this.generalLedger.handler));
router.get('/balance_sheet',
this.balanceSheet.validation,
asyncMiddleware(this.balanceSheet.handler));
router.get('/trial_balance_sheet',
this.trialBalanceSheet.validation,
asyncMiddleware(this.trialBalanceSheet.handler));
router.get('/profit_loss_sheet',
this.profitLossSheet.validation,
asyncMiddleware(this.profitLossSheet.handler));
// router.get('/cash_flow_statement',
// this.cashFlowStatement.validation,
// asyncMiddleware(this.cashFlowStatement.handler));
// router.get('/badget_verses_actual',
// this.badgetVersesActuals.validation,
// asyncMiddleware(this.badgetVersesActuals.handler));
return router;
},
/**
* Retrieve the ledger report of the given account.
*/
ledger: {
validation: [
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
query('transaction_types').optional().isArray({ min: 1 }),
query('account_ids').optional().isArray({ min: 1 }),
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 filter = {
from_range: null,
to_range: null,
account_ids: [],
transaction_types: [],
number_format: {
no_cents: false,
divide_1000: false,
},
...req.query,
};
const accountsJournalEntries = await AccountTransaction.query()
.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');
const formatNumber = formatNumberClosure(filter.number_format);
return res.status(200).send({
meta: { ...filter },
items: accountsJournalEntries.map((entry) => ({
...entry,
credit: formatNumber(entry.credit),
debit: formatNumber(entry.debit),
})),
});
},
},
/**
* 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(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
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,
},
none_zero: false,
...req.query,
};
const accounts = await Account.query()
.orderBy('index', 'DESC')
.withGraphFetched('transactions')
.modifyGraph('transactions', (builder) => {
builder.modify('filterDateRange', filter.from_date, filter.to_date);
});
const openingBalanceTransactions = await AccountTransaction.query()
.modify('filterDateRange', null, filter.from_date)
.modify('sumationCreditDebit')
.withGraphFetched('account.type');
const closingBalanceTransactions = await AccountTransaction.query()
.modify('filterDateRange', null, filter.to_date)
.modify('sumationCreditDebit')
.withGraphFetched('account.type');
const opeingBalanceCollection = new JournalPoster();
const closingBalanceCollection = new JournalPoster();
opeingBalanceCollection.loadEntries(openingBalanceTransactions);
closingBalanceCollection.loadEntries(closingBalanceTransactions);
// Transaction amount formatter based on the given query.
const formatNumber = formatNumberClosure(filter.number_format);
const items = [
...accounts
.filter((account) => (
account.transactions.length > 0 || !filter.none_zero
))
.map((account) => ({
...pick(account, ['id', 'name', 'code', 'index']),
transactions: [
...account.transactions.map((transaction) => ({
...transaction,
credit: formatNumber(transaction.credit),
debit: formatNumber(transaction.debit),
})),
],
opening: {
date: filter.from_date,
balance: opeingBalanceCollection.getClosingBalance(account.id),
},
closing: {
date: filter.to_date,
balance: closingBalanceCollection.getClosingBalance(account.id),
},
})),
];
return res.status(200).send({
meta: { ...filter },
items,
});
},
},
/**
* Retrieve the balance sheet.
*/
balanceSheet: {
validation: [
query('accounting_method').optional().isIn(['cash', 'accural']),
query('from_date').optional(),
query('to_date').optional(),
query('display_columns_by').optional().isIn(['year', 'month', 'week', 'day', 'quarter']),
query('number_format.no_cents').optional().isBoolean().toBoolean(),
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
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 filter = {
display_columns_by: 'year',
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,
...req.query,
};
const balanceSheetTypes = await AccountType.query()
.where('balance_sheet', true);
// Fetch all balance sheet accounts.
const accounts = await Account.query()
.whereIn('account_type_id', balanceSheetTypes.map((a) => a.id))
.withGraphFetched('type')
.withGraphFetched('transactions')
.modifyGraph('transactions', (builder) => {
builder.modify('filterDateRange', null, filter.to_date);
});
const journalEntriesCollected = Account.collectJournalEntries(accounts);
const journalEntries = new JournalPoster();
journalEntries.loadEntries(journalEntriesCollected);
// Account balance formmatter based on the given query.
const balanceFormatter = formatNumberClosure(filter.number_format);
// Gets the date range set from start to end date.
const dateRangeSet = dateRangeCollection(
filter.from_date,
filter.to_date,
filter.display_columns_by,
);
// Retrieve the asset balance sheet.
const assets = [
...accounts
.filter((account) => (
account.type.normal === 'debit'
&& (account.transactions.length > 0 || !filter.none_zero)
))
.map((account) => ({
...pick(account, ['id', 'index', 'name', 'code']),
transactions: dateRangeSet.map((date) => {
const type = filter.display_columns_by;
const balance = journalEntries.getClosingBalance(account.id, date, type);
return { date, balance: balanceFormatter(balance) };
}),
})),
];
// Retrieve liabilities and equity balance sheet.
const liabilitiesEquity = [
...accounts
.filter((account) => (
account.type.normal === 'credit'
&& (account.transactions.length > 0 || !filter.none_zero)
))
.map((account) => ({
...pick(account, ['id', 'index', 'name', 'code']),
transactions: dateRangeSet.map((date) => {
const type = filter.display_columns_by;
const balance = journalEntries.getClosingBalance(account.id, date, type);
return { date, balance: balanceFormatter(balance) };
}),
})),
];
return res.status(200).send({
columns: { ...dateRangeSet },
balance_sheet: {
assets,
liabilities_equity: liabilitiesEquity,
},
});
},
},
/**
* 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(),
query('number_format.1000_divide').optional().isBoolean(),
query('basis').optional(),
query('none_zero').optional(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
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,
...req.query,
};
const accounts = await Account.query()
.withGraphFetched('type')
.withGraphFetched('transactions')
.modifyGraph('transactions', (builder) => {
builder.modify('sumationCreditDebit');
builder.modify('filterDateRange', filter.from_date, filter.to_date);
});
const journalEntriesCollect = Account.collectJournalEntries(accounts);
const journalEntries = new JournalPoster();
journalEntries.loadEntries(journalEntriesCollect);
// Account balance formmatter based on the given query.
const balanceFormatter = formatNumberClosure(filter.number_format);
const items = accounts
.filter((account) => (
account.transactions.length > 0 || !filter.none_zero
))
.map((account) => {
const trial = journalEntries.getTrialBalance(account.id);
return {
account_id: account.id,
code: account.code,
accountNormal: account.type.normal,
credit: balanceFormatter(trial.credit),
debit: balanceFormatter(trial.debit),
balance: balanceFormatter(trial.balance),
};
});
return res.status(200).send({
meta: { ...filter },
items: [...items],
});
},
},
/**
* 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(),
query('display_columns_by').optional().isIn(['year', 'month', 'week', 'day', 'quarter']),
query('accounts').optional().isArray(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
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_by: 'month',
...req.query,
};
const incomeStatementTypes = await AccountType.query().where('income_sheet', true);
const accounts = await Account.query()
.whereIn('account_type_id', incomeStatementTypes.map((t) => t.id))
.withGraphFetched('type')
.withGraphFetched('transactions');
const filteredAccounts = accounts.filter((account) => {
return account.transactions.length > 0 || !filter.none_zero;
});
const journalEntriesCollected = Account.collectJournalEntries(accounts);
const journalEntries = new JournalPoster();
journalEntries.loadEntries(journalEntriesCollected);
// Account balance formmatter based on the given query.
const numberFormatter = formatNumberClosure(filter.number_format);
// Gets the date range set from start to end date.
const dateRangeSet = dateRangeCollection(
filter.from_date,
filter.to_date,
filter.display_columns_by,
);
const accountsIncome = filteredAccounts
.filter((account) => account.type.normal === 'credit')
.map((account) => ({
...pick(account, ['id', 'index', 'name', 'code']),
dates: dateRangeSet.map((date) => {
const type = filter.display_columns_by;
const amount = journalEntries.getClosingBalance(account.id, date, type);
return { date, rawAmount: amount, amount: numberFormatter(amount) };
}),
}));
const accountsExpenses = filteredAccounts
.filter((account) => account.type.normal === 'debit')
.map((account) => ({
...pick(account, ['id', 'index', 'name', 'code']),
dates: dateRangeSet.map((date) => {
const type = filter.display_columns_by;
const amount = journalEntries.getClosingBalance(account.id, date, type);
return { date, rawAmount: amount, amount: numberFormatter(amount) };
}),
}));
// Calculates the total income of income accounts.
const totalAccountsIncome = dateRangeSet.reduce((acc, date, index) => {
let amount = 0;
accountsIncome.forEach((account) => {
const currentDate = account.dates[index];
amount += currentDate.rawAmount || 0;
});
acc[date] = { date, rawAmount: amount, amount: numberFormatter(amount) };
return acc;
}, {});
// Calculates the total expenses of expenses accounts.
const totalAccountsExpenses = dateRangeSet.reduce((acc, date, index) => {
let amount = 0;
accountsExpenses.forEach((account) => {
const currentDate = account.dates[index];
amount += currentDate.rawAmount || 0;
});
acc[date] = { date, rawAmount: amount, amount: numberFormatter(amount) };
return acc;
}, {});
// Total income(date) - Total expenses(date) = Net income(date)
const netIncome = dateRangeSet.map((date) => {
const totalIncome = totalAccountsIncome[date];
const totalExpenses = totalAccountsExpenses[date];
let amount = totalIncome.rawAmount || 0;
amount -= totalExpenses.rawAmount || 0;
return { date, rawAmount: amount, amount: numberFormatter(amount) };
});
return res.status(200).send({
meta: { ...filter },
income: {
entry_normal: 'credit',
accounts: accountsIncome,
},
expenses: {
entry_normal: 'debit',
accounts: accountsExpenses,
},
total_income: Object.values(totalAccountsIncome),
total_expenses: Object.values(totalAccountsExpenses),
total_net_income: netIncome,
});
},
},
cashFlowStatement: {
validation: [
query('date_from').optional(),
query('date_to').optional(),
],
async handler(req, res) {
return res.status(200).send();
},
},
badgetVersesActuals: {
validation: [
],
async handler(req, res) {
},
},
}

View File

View File

@@ -70,19 +70,17 @@ export default {
code: 'validation_error', ...validationErrors,
});
}
const { sell_account_id: sellAccountId, cost_account_id: costAccountId } = req.body;
const { category_id: categoryId, custom_fields: customFields } = req.body;
const form = { ...req.body };
const errorReasons = [];
const costAccountPromise = Account.where('id', costAccountId).fetch();
const sellAccountPromise = Account.where('id', sellAccountId).fetch();
const itemCategoryPromise = (categoryId)
? ItemCategory.where('id', categoryId).fetch() : null;
const costAccountPromise = Account.where('id', form.cost_account_id).fetch();
const sellAccountPromise = Account.where('id', form.sell_account_id).fetch();
const itemCategoryPromise = (form.category_id)
? ItemCategory.where('id', form.category_id).fetch() : null;
// Validate the custom fields key and value type.
if (customFields.length > 0) {
const customFieldsKeys = customFields.map((field) => field.key);
if (form.custom_fields.length > 0) {
const customFieldsKeys = form.custom_fields.map((field) => field.key);
// Get resource id than get all resource fields.
const resource = await Resource.where('name', 'items').fetch();
@@ -110,13 +108,12 @@ export default {
if (!sellAccount) {
errorReasons.push({ type: 'SELL_ACCOUNT_NOT_FOUND', code: 120 });
}
if (!itemCategory && categoryId) {
if (!itemCategory && form.category_id) {
errorReasons.push({ type: 'ITEM_CATEGORY_NOT_FOUND', code: 140 });
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
const item = Item.forge({
name: req.body.name,
type_id: 1,

View File

@@ -0,0 +1,76 @@
import express from 'express';
import { body, query, validationResult } from 'express-validator';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Option from '@/models/Option';
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.post('/',
this.saveOptions.validation,
asyncMiddleware(this.saveOptions.handler));
router.get('/',
this.getOptions.validation,
asyncMiddleware(this.getSettings));
return router;
},
/**
* Saves the given options to the storage.
*/
saveOptions: {
validation: [
body('options').isArray(),
body('options.*.key').exists(),
body('options.*.value').exists(),
body('options.*.group').exists(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'VALIDATION_ERROR', ...validationErrors,
});
}
const form = { ...req.body };
const optionsCollections = await Option.query();
form.options.forEach((option) => {
optionsCollections.setMeta(option.key, option.value, option.group);
});
await optionsCollections.saveMeta();
return res.status(200).send();
},
},
/**
* Retrieve the application options from the storage.
*/
getOptions: {
validation: [
query('key').optional(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'VALIDATION_ERROR', ...validationErrors,
});
}
const options = await Option.query();
return res.status(200).sends({
options: options.toArray(),
});
},
},
};

View File

@@ -18,42 +18,39 @@ const AccessControllSchema = [
},
];
// eslint-disable-next-line arrow-body-style
const getResourceSchema = (resource) => AccessControllSchema.find((schema) => {
return schema.resource === resource;
});
const getResourceSchema = (resource) => AccessControllSchema
.find((schema) => schema.resource === resource);
const getResourcePermissions = (resource) => {
const foundResource = getResourceSchema(resource);
return foundResource ? foundResource.permissions : [];
};
const findNotFoundResources = (resourcesSlugs) => {
const schemaResourcesSlugs = AccessControllSchema.map((s) => s.resource);
return difference(resourcesSlugs, schemaResourcesSlugs);
};
const findNotFoundPermissions = (permissions, resourceSlug) => {
const schemaPermissions = getResourcePermissions(resourceSlug);
return difference(permissions, schemaPermissions);
};
export default {
findNotFoundResources(resourcesSlugs) {
const schemaResourcesSlugs = AccessControllSchema.map((s) => s.resource);
return difference(resourcesSlugs, schemaResourcesSlugs);
},
findNotFoundPermissions(permissions, resourceSlug) {
const schemaPermissions = getResourcePermissions(resourceSlug);
return difference(permissions, schemaPermissions);
},
/**
* Router constructor method.
*/
router() {
const router = express.Router();
router.post('/',
this.newRole.validation,
asyncMiddleware(this.newRole.handler));
router.post('/:id',
this.editRole.validation,
asyncMiddleware(this.editRole.handler.bind(this)));
// router.post('/',
// this.newRole.validation,
// asyncMiddleware(this.newRole.handler));
router.delete('/:id',
this.deleteRole.validation,
asyncMiddleware(this.deleteRole.handler));
@@ -80,32 +77,30 @@ export default {
code: 'validation_error', ...validationErrors,
});
}
const { name, description, permissions } = req.body;
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
const permissionsSlugs = [];
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
const resourcesNotFound = this.findNotFoundResources(resourcesSlugs);
const errorReasons = [];
const notFoundPermissions = [];
if (resourcesNotFound.length > 0) {
errorReasons.push({
type: 'RESOURCE_SLUG_NOT_FOUND',
code: 100,
resources: resourcesNotFound,
type: 'RESOURCE_SLUG_NOT_FOUND', code: 100, resources: resourcesNotFound,
});
}
permissions.forEach((perm) => {
const abilities = perm.permissions.map((ability) => ability);
// Gets the not found permissions in the schema.
const notFoundAbilities = this.findNotFoundPermissions(abilities, perm.resource_slug);
const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug);
if (notFoundAbilities.length > 0) {
notFoundPermissions.push({
resource_slug: perm.resource_slug, permissions: notFoundAbilities,
resource_slug: perm.resource_slug,
permissions: notFoundAbilities,
});
} else {
const perms = perm.permissions || [];
@@ -217,7 +212,7 @@ export default {
const notFoundPermissions = [];
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
const resourcesNotFound = this.findNotFoundResources(resourcesSlugs);
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
if (resourcesNotFound.length > 0) {
errorReasons.push({
@@ -230,7 +225,7 @@ export default {
permissions.forEach((perm) => {
const abilities = perm.permissions.map((ability) => ability);
// Gets the not found permissions in the schema.
const notFoundAbilities = this.findNotFoundPermissions(abilities, perm.resource_slug);
const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug);
if (notFoundAbilities.length > 0) {
notFoundPermissions.push({

View File

@@ -0,0 +1,10 @@
import express from 'express';
export default {
router() {
const router = express.Router();
return router;
},
};

View File

@@ -1,6 +1,6 @@
import { difference } from 'lodash';
import express from 'express';
import { check, validationResult } from 'express-validator';
import { check, query, validationResult } from 'express-validator';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import Resource from '@/models/Resource';
import View from '../../models/View';
@@ -8,10 +8,13 @@ import View from '../../models/View';
export default {
resource: 'items',
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.post('/resource/:resource_id',
router.post('/',
this.createView.validation,
asyncMiddleware(this.createView.handler));
@@ -33,7 +36,9 @@ export default {
* List all views that associated with the given resource.
*/
listViews: {
validation: [],
validation: [
query('resource_name').optional().trim().escape(),
],
async handler(req, res) {
const { resource_id: resourceId } = req.params;
const views = await View.where('resource_id', resourceId).fetchAll();
@@ -54,7 +59,6 @@ export default {
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
});
}
return res.status(200).send({ ...view.toJSON() });
},
},
@@ -66,25 +70,23 @@ export default {
validation: [],
async handler(req, res) {
const { view_id: viewId } = req.params;
const view = await View.where('id', viewId).fetch({
withRelated: ['viewRoles', 'columns'],
});
const view = await View.query().findById(viewId);
if (!view) {
return res.boom.notFound(null, {
errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }],
});
}
if (view.attributes.predefined) {
if (view.predefined) {
return res.boom.badRequest(null, {
errors: [{ type: 'PREDEFINED_VIEW', code: 200 }],
});
}
// console.log(view);
await view.destroy();
// await view.columns().destroy({ require: false });
await Promise.all([
view.$relatedQuery('viewRoles').delete(),
view.$relatedQuery('columns').delete(),
]);
await view.delete();
return res.status(200).send({ id: view.get('id') });
},
@@ -95,16 +97,17 @@ export default {
*/
createView: {
validation: [
check('resource_name').exists().escape().trim(),
check('label').exists().escape().trim(),
check('columns').isArray({ min: 3 }),
check('columns').exists().isArray({ min: 1 }),
check('roles').isArray(),
check('roles.*.field').exists().escape().trim(),
check('roles.*.comparator').exists(),
check('roles.*.value').exists(),
check('roles.*.index').exists().isNumeric().toInt(),
check('columns.*').exists().escape().trim(),
],
async handler(req, res) {
const { resource_id: resourceId } = req.params;
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
@@ -113,7 +116,8 @@ export default {
});
}
const resource = await Resource.where('id', resourceId).fetch();
const form = { ...req.body };
const resource = await Resource.query().where('name', form.resource_name).first();
if (!resource) {
return res.boom.notFound(null, {
@@ -121,38 +125,34 @@ export default {
});
}
const errorReasons = [];
const { label, roles, columns } = req.body;
const fieldsSlugs = form.roles.map((role) => role.field);
const fieldsSlugs = roles.map((role) => role.field);
const resourceFields = await resource.$relatedQuery('fields');
const resourceFieldsKeys = resourceFields.map((f) => f.slug);
const resourceFields = await resource.fields().fetch();
const resourceFieldsKeys = resourceFields.map((f) => f.get('key'));
// The difference between the stored resource fields and submit fields keys.
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
if (notFoundFields.length > 0) {
errorReasons.push({ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields });
}
const notFoundColumns = difference(columns, resourceFieldsKeys);
// The difference between the stored resource fields and the submit columns keys.
const notFoundColumns = difference(form.columns, resourceFieldsKeys);
if (notFoundColumns.length > 0) {
errorReasons.push({ type: 'COLUMNS_NOT_EXIST', code: 200, fields: notFoundColumns });
errorReasons.push({ type: 'COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns });
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
const view = await View.forge({
name: label,
predefined: false,
});
// Save view details.
await view.save();
// Save view columns.
const view = await View.query().insert({
name: form.label,
predefined: false,
resource_id: resource.id,
});
// Save view roles.
@@ -160,7 +160,6 @@ export default {
},
},
editView: {
validation: [
check('label').exists().escape().trim(),

View File

@@ -8,6 +8,18 @@ import Accounts from '@/http/controllers/Accounts';
import AccountOpeningBalance from '@/http/controllers/AccountOpeningBalance';
import Views from '@/http/controllers/Views';
import CustomFields from '@/http/controllers/Fields';
import Accounting from '@/http/controllers/Accounting';
import FinancialStatements from '@/http/controllers/FinancialStatements';
import Expenses from '@/http/controllers/Expenses';
import Options from '@/http/controllers/Options';
import Budget from '@/http/controllers/Budget';
import BudgetReports from '@/http/controllers/BudgetReports';
import Customers from '@/http/controllers/Customers';
import Suppliers from '@/http/controllers/Suppliers';
import Bills from '@/http/controllers/Bills';
import CurrencyAdjustment from './controllers/CurrencyAdjustment';
// import SalesReports from '@/http/controllers/SalesReports';
// import PurchasesReports from '@/http/controllers/PurchasesReports';
export default (app) => {
// app.use('/api/oauth2', OAuth2.router());
@@ -15,9 +27,21 @@ export default (app) => {
app.use('/api/users', Users.router());
app.use('/api/roles', Roles.router());
app.use('/api/accounts', Accounts.router());
app.use('/api/accountOpeningBalance', AccountOpeningBalance.router());
app.use('/api/accounting', Accounting.router());
app.use('/api/accounts_opeing_balance', AccountOpeningBalance.router());
app.use('/api/views', Views.router());
app.use('/api/fields', CustomFields.router());
app.use('/api/items', Items.router());
app.use('/api/item_categories', ItemCategories.router());
app.use('/api/expenses', Expenses.router());
app.use('/api/financial_statements', FinancialStatements.router());
app.use('/api/options', Options.router());
app.use('/api/budget_reports', BudgetReports.router());
// app.use('/api/customers', Customers.router());
// app.use('/api/suppliers', Suppliers.router());
// app.use('/api/bills', Bills.router());
app.use('/api/budget', Budget.router());
// app.use('/api/currency_adjustment', CurrencyAdjustment.router());
// app.use('/api/reports/sales', SalesReports.router());
// app.use('/api/reports/purchases', PurchasesReports.router());
};

View File

@@ -25,7 +25,7 @@ const authMiddleware = (req, res, next) => {
reject(error);
} else {
// eslint-disable-next-line no-underscore-dangle
req.user = await User.where('id', decoded._id).fetch();
req.user = await User.query().findById(decoded._id);
// Auth.setAuthenticatedUser(req.user);
if (!req.user) {

View File

View File

@@ -0,0 +1,211 @@
export default class MetableCollection {
constructor() {
this.metadata = [];
this.KEY_COLUMN = 'key';
this.VALUE_COLUMN = 'value';
this.TYPE_COLUMN = 'type';
this.model = null;
}
/**
* Set model of this metadata collection.
* @param {Object} model -
*/
setModel(model) {
this.model = model;
}
/**
* Find the given metadata key.
* @param {String} key -
* @return {object} - Metadata object.
*/
findMeta(key) {
return this.allMetadata().find((meta) => meta.key === key);
}
/**
* Retrieve all metadata.
*/
allMetadata() {
return this.metadata.filter((meta) => !meta.markAsDeleted);
}
/**
* Retrieve metadata of the given key.
* @param {String} key -
* @param {Mixied} defaultValue -
*/
getMeta(key, defaultValue) {
const metadata = this.findMeta(key);
return metadata ? metadata.value : defaultValue || false;
}
/**
* Markes the metadata to should be deleted.
* @param {String} key -
*/
removeMeta(key) {
const metadata = this.findMeta(key);
if (metadata) {
metadata.markAsDeleted = true;
}
}
/**
* Remove all meta data of the given group.
* @param {*} group
*/
removeAllMeta(group = 'default') {
this.metadata.forEach(meta => {
meta.markAsDeleted = true;
});
}
/**
* Set the meta data to the stack.
* @param {String} key -
* @param {String} value -
*/
setMeta(key, value, payload) {
if (Array.isArray(key)) {
const metadata = key;
metadata.forEach((meta) => {
this.setMeta(meta.key, meta.value);
});
return;
}
const metadata = this.findMeta(key);
if (metadata) {
metadata.value = value;
metadata.markAsUpdated = true;
} else {
this.metadata.push({
value, key, ...payload, markAsInserted: true,
});
}
}
/**
* Saved the modified/deleted and inserted metadata.
*/
async saveMeta() {
const inserted = this.metadata.filter((m) => (m.markAsInserted === true));
const updated = this.metadata.filter((m) => (m.markAsUpdated === true));
const deleted = this.metadata.filter((m) => (m.markAsDeleted === true));
const opers = [];
if (deleted.length > 0) {
const deleteOper = this.model.query()
.whereIn('key', deleted.map((meta) => meta.key)).delete();
opers.push(deleteOper);
}
inserted.forEach((meta) => {
const insertOper = this.model.query().insert({
[this.KEY_COLUMN]: meta.key,
[this.VALUE_COLUMN]: meta.value,
});
opers.push(insertOper);
});
await Promise.all(opers);
}
/**
* Loads the metadata from the storage.
* @param {String|Array} key -
* @param {Boolean} force -
*/
async load() {
const metadata = await this.query();
const metadataArray = this.mapMetadataCollection(metadata);
metadataArray.forEach((meta) => {
this.metadata.push(meta);
});
}
/**
* Format the metadata before saving to the database.
* @param {String|Number|Boolean} value -
* @param {String} valueType -
* @return {String|Number|Boolean} -
*/
static formatMetaValue(value, valueType) {
let parsedValue;
switch (valueType) {
case 'number':
parsedValue = `${value}`;
break;
case 'boolean':
parsedValue = value ? '1' : '0';
break;
case 'json':
parsedValue = JSON.stringify(parsedValue);
break;
default:
parsedValue = value;
break;
}
return parsedValue;
}
/**
* Mapping and parse metadata to collection entries.
* @param {Meta} attr -
* @param {String} parseType -
*/
mapMetadata(attr, parseType = 'parse') {
return {
key: attr[this.KEY_COLUMN],
value: (parseType === 'parse')
? MetableCollection.parseMetaValue(
attr[this.VALUE_COLUMN],
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
)
: MetableCollection.formatMetaValue(
attr[this.VALUE_COLUMN],
this.TYPE_COLUMN ? attr[this.TYPE_COLUMN] : false,
),
...this.extraColumns.map((extraCol) => ({
[extraCol]: attr[extraCol] || null,
})),
};
}
/**
* Parse the metadata to the collection.
* @param {Array} collection -
*/
mapMetadataToCollection(metadata, parseType = 'parse') {
return metadata.map((model) => this.mapMetadataToCollection(model, parseType));
}
/**
* Load metadata to the metable collection.
* @param {Array} meta -
*/
from(meta) {
if (Array.isArray(meta)) {
meta.forEach((m) => { this.from(m); });
return;
}
this.metadata.push(meta);
}
/**
* Static method to load metadata to the collection.
* @param {Array} meta
*/
static from(meta) {
const collection = new MetableCollection();
collection.from(meta);
return collection;
}
}

View File

@@ -0,0 +1,12 @@
export default class Metable{
static get modifiers() {
return {
whereKey(builder, key) {
builder.where('key', key);
},
};
}
}

View File

@@ -1,34 +1,81 @@
import bookshelf from './bookshelf';
/* eslint-disable global-require */
import { Model } from 'objection';
import { flatten } from 'lodash';
import BaseModel from '@/models/Model';
const Account = bookshelf.Model.extend({
export default class Account extends BaseModel {
/**
* Table name
*/
tableName: 'accounts',
static get tableName() {
return 'accounts';
}
/**
* Timestamp columns.
* Model modifiers.
*/
hasTimestamps: ['created_at', 'updated_at'],
static get modifiers() {
return {
filterAccountTypes(query, typesIds) {
if (typesIds.length > 0) {
query.whereIn('accoun_type_id', typesIds);
}
},
};
}
/**
* Account model may belongs to account type.
* Relationship mapping.
*/
type() {
return this.belongsTo('AccountType', 'account_type_id');
},
static get relationMappings() {
const AccountType = require('@/models/AccountType');
const AccountBalance = require('@/models/AccountBalance');
const AccountTransaction = require('@/models/AccountTransaction');
/**
* Account model may has many balances accounts.
*/
balances() {
return this.hasMany('AccountBalance', 'account_id');
},
}, {
/**
* Cascade delete dependents.
*/
dependents: ['balances'],
});
return {
/**
* Account model may belongs to account type.
*/
type: {
relation: Model.BelongsToOneRelation,
modelClass: AccountType.default,
join: {
from: 'accounts.accountTypeId',
to: 'account_types.id',
},
},
export default bookshelf.model('Account', Account);
/**
* Account model may has many balances accounts.
*/
balance: {
relation: Model.HasOneRelation,
modelClass: AccountBalance.default,
join: {
from: 'accounts.id',
to: 'account_balances.accountId',
},
},
/**
* Account model may has many transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
join: {
from: 'accounts.id',
to: 'accounts_transactions.accountId',
},
},
};
}
static collectJournalEntries(accounts) {
return flatten(accounts.map((account) => account.transactions.map((transaction) => ({
accountId: account.id,
...transaction,
accountNormal: account.type.normal,
}))));
}
}

View File

@@ -1,21 +1,29 @@
import bookshelf from './bookshelf';
const AccountBalance = bookshelf.Model.extend({
import { Model } from 'objection';
import BaseModel from '@/models/Model';
export default class AccountBalance extends BaseModel {
/**
* Table name
*/
tableName: 'account_balance',
static get tableName() {
return 'account_balances';
}
/**
* Timestamp columns.
* Relationship mapping.
*/
hasTimestamps: false,
static get relationMappings() {
const Account = require('@/models/Account');
account() {
return this.belongsTo('Account', 'account_id');
},
});
export default bookshelf.model('AccountBalance', AccountBalance);
return {
account: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'account_balance.account_id',
to: 'accounts.id',
},
},
};
}
}

View File

@@ -0,0 +1,81 @@
import { Model } from 'objection';
import moment from 'moment';
import BaseModel from '@/models/Model';
export default class AccountTransaction extends BaseModel {
/**
* Table name
*/
static get tableName() {
return 'accounts_transactions';
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
filterAccounts(query, accountsIds) {
if (accountsIds.length > 0) {
query.whereIn('account_id', accountsIds);
}
},
filterTransactionTypes(query, types) {
if (Array.isArray(types) && types.length > 0) {
query.whereIn('reference_type', types);
} else if (typeof types === 'string') {
query.where('reference_type', types);
}
},
filterDateRange(query, startDate, endDate, type = 'day') {
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
const fromDate = moment(startDate).startOf(type).format(dateFormat);
const toDate = moment(endDate).endOf(type).format(dateFormat);
if (startDate) {
query.where('date', '>=', fromDate);
}
if (endDate) {
query.where('date', '<=', toDate);
}
},
filterAmountRange(query, fromAmount, toAmount) {
if (fromAmount) {
query.andWhere((q) => {
q.where('credit', '>=', fromAmount);
q.orWhere('debit', '>=', fromAmount);
});
}
if (toAmount) {
query.andWhere((q) => {
q.where('credit', '<=', toAmount);
q.orWhere('debit', '<=', toAmount);
});
}
},
sumationCreditDebit(query) {
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('account_id');
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Account = require('@/models/Account');
return {
account: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'accounts_transactions.accountId',
to: 'accounts.id',
},
},
};
}
}

View File

@@ -1,23 +1,33 @@
import bookshelf from './bookshelf';
const AccountType = bookshelf.Model.extend({
// import path from 'path';
import { Model } from 'objection';
import BaseModel from '@/models/Model';
export default class AccountType extends BaseModel {
/**
* Table name
*/
tableName: 'accounts',
static get tableName() {
return 'account_types';
}
/**
* Timestamp columns.
* Relationship mapping.
*/
hasTimestamps: false,
static get relationMappings() {
const Account = require('@/models/Account');
/**
* Account type may has many associated accounts.
*/
accounts() {
return this.hasMany('Account', 'account_type_id');
},
});
export default bookshelf.model('AccountType', AccountType);
return {
/**
* Account type may has many associated accounts.
*/
accounts: {
relation: Model.HasManyRelation,
modelClass: Account.default,
join: {
from: 'account_types.id',
to: 'accounts.accountTypeId',
},
},
};
}
}

View File

@@ -0,0 +1,60 @@
import BaseModel from '@/models/Model';
export default class Budget extends BaseModel {
/**
* Table name
*/
static get tableName() {
return 'budgets';
}
static get virtualAttributes() {
return ['rangeBy', 'rangeIncrement'];
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
filterByYear(query, year) {
query.where('year', year);
},
filterByIncomeStatement(query) {
query.where('account_types', 'income_statement');
},
filterByProfitLoss(query) {
query.where('accounts_types', 'profit_loss');
},
};
}
get rangeBy() {
switch (this.period) {
case 'half-year':
case 'quarter':
return 'month';
default:
return this.period;
}
}
get rangeIncrement() {
switch (this.period) {
case 'half-year':
return 6;
case 'quarter':
return 3;
default:
return 1;
}
}
get rangeOffset() {
switch (this.period) {
case 'half-year': return 5;
case 'quarter': return 2;
default: return 0;
}
}
}

View File

@@ -0,0 +1,10 @@
import BaseModel from '@/models/Model';
export default class Budget extends BaseModel {
/**
* Table name
*/
static get tableName() {
return 'budget_entries';
}
}

View File

@@ -0,0 +1,86 @@
import { Model } from 'objection';
import BaseModel from '@/models/Model';
export default class Expense extends BaseModel {
/**
* Table name
*/
static get tableName() {
return 'expenses';
}
static get referenceType() {
return 'Expense';
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
filterByDateRange(query, startDate, endDate) {
if (startDate) {
query.where('date', '>=', startDate);
}
if (endDate) {
query.where('date', '<=', endDate);
}
},
filterByAmountRange(query, from, to) {
if (from) {
query.where('amount', '>=', from);
}
if (to) {
query.where('amount', '<=', to);
}
},
filterByExpenseAccount(query, accountId) {
if (accountId) {
query.where('expense_account_id', accountId);
}
},
filterByPaymentAccount(query, accountId) {
if (accountId) {
query.where('payment_account_id', accountId);
}
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Account = require('@/models/Account');
const User = require('@/models/User');
return {
paymentAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'expenses.paymentAccountId',
to: 'accounts.id',
},
},
expenseAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: 'expenses.expenseAccountId',
to: 'accounts.id',
},
},
user: {
relation: Model.BelongsToOneRelation,
modelClass: User.default,
join: {
from: 'expenses.userId',
to: 'users.id',
},
},
};
}
}

View File

@@ -1,34 +1,43 @@
import bookshelf from './bookshelf';
import { Model } from 'objection';
import path from 'path';
import BaseModel from '@/models/Model';
const Item = bookshelf.Model.extend({
export default class Item extends BaseModel {
/**
* Table name
*/
tableName: 'items',
static get tableName() {
return 'items';
}
/**
* Timestamp columns.
* Relationship mapping.
*/
hasTimestamps: false,
static get relationMappings() {
return {
/**
* Item may has many meta data.
*/
metadata: {
relation: Model.HasManyRelation,
modelBase: path.join(__dirname, 'ItemMetadata'),
join: {
from: 'items.id',
to: 'items_metadata.item_id',
},
},
/**
* Item may has many meta data.
*/
metadata() {
return this.hasMany('ItemMetadata', 'item_id');
},
/**
* Item may belongs to the item category.
*/
category() {
return this.belongsTo('ItemCategory', 'category_id');
},
}, {
/**
* Cascade delete dependents.
*/
dependents: ['ItemMetadata'],
});
export default bookshelf.model('Item', Item);
/**
* Item may belongs to cateogory model.
*/
category: {
relation: Model.BelongsToOneRelation,
modelBase: path.join(__dirname, 'ItemCategory'),
join: {
from: 'items.categoryId',
to: 'items_categories.id',
},
},
};
}
}

View File

@@ -1,23 +1,31 @@
import bookshelf from './bookshelf';
import path from 'path';
import { Model } from 'objection';
import BaseModel from '@/models/Model';
const ItemCategory = bookshelf.Model.extend({
export default class ItemCategory extends BaseModel {
/**
* Table name.
*/
static get tableName() {
return 'items_categories';
}
/**
* Table name
* Relationship mapping.
*/
tableName: 'items_categories',
/**
* Timestamp columns.
*/
hasTimestamps: ['created_at', 'updated_at'],
/**
* Item category may has many items.
*/
items() {
return this.hasMany('Item', 'category_id');
},
});
export default bookshelf.model('ItemCategory', ItemCategory);
static get relationMappings() {
return {
/**
* Item category may has many items.
*/
items: {
relation: Model.HasManyRelation,
modelBase: path.join(__dirname, 'Item'),
join: {
from: 'items_categories.item_id',
to: 'items.id',
},
},
};
}
}

View File

@@ -1,23 +1,38 @@
import bookshelf from './bookshelf';
const ItemMetadata = bookshelf.Model.extend({
import path from 'path';
import { Model } from 'objection';
import BaseModel from '@/models/Model';
export default class ItemMetadata extends BaseModel {
/**
* Table name
*/
tableName: 'items_metadata',
static get tableName() {
return 'items_metadata';
}
/**
* Timestamp columns.
*/
hasTimestamps: ['created_at', 'updated_at'],
static get hasTimestamps() {
return ['created_at', 'updated_at'];
}
/**
* Item category may has many items.
* Relationship mapping.
*/
items() {
return this.belongsTo('Item', 'item_id');
},
});
export default bookshelf.model('ItemMetadata', ItemMetadata);
static get relationMappings() {
return {
/**
* Item category may has many items.
*/
items: {
relation: Model.BelongsToOneRelation,
modelBase: path.join(__dirname, 'Item'),
join: {
from: 'items_metadata.item_id',
to: 'items.id',
},
},
};
}
}

View File

@@ -0,0 +1,10 @@
import BaseModel from '@/models/Model';
export default class JournalEntry extends BaseModel {
/**
* Table name.
*/
static get tableName() {
return 'manual_journals';
}
}

View File

@@ -36,16 +36,7 @@ export default {
setExtraColumns(columns) {
this.extraColumns = columns;
},
/**
* Retrieve the cache namespace.
*/
getCacheNamespace() {
const { metadataCacheNamespace: cacheName } = this;
return typeof cacheName === 'function'
? cacheName() : cacheName;
},
/**
* Metadata database query.
* @param {Object} query -
@@ -126,7 +117,7 @@ export default {
metadata.markAsDeleted = true;
}
this.shouldReload = true;
},
/**
* Remove all meta data of the given group.

View File

@@ -0,0 +1,14 @@
import { Model } from 'objection';
export default class ModelBase extends Model {
static get collection() {
return Array;
}
static query(...args) {
return super.query(...args).runAfter((result) => {
return this.collection.from(result);
});
}
}

View File

@@ -0,0 +1,16 @@
import { mixin } from 'objection';
import BaseModel from '@/models/Model';
import MetableCollection from '@/lib/Metable/MetableCollection';
export default class Option extends mixin(BaseModel, [mixin]) {
/**
* Table name.
*/
static get tableName() {
return 'options';
}
static get collection() {
return MetableCollection;
}
}

View File

@@ -1,16 +1,10 @@
import bookshelf from './bookshelf';
const PasswordResets = bookshelf.Model.extend({
import Model from '@/models/Model';
export default class PasswordResets extends Model {
/**
* Table name
*/
tableName: 'password_resets',
/**
* Timestamp columns.
*/
hasTimestamps: false,
});
export default bookshelf.model('PasswordResets', PasswordResets);
static get tableName() {
return 'password_resets';
}
}

View File

@@ -1,25 +1,41 @@
import bookshelf from './bookshelf';
const Permission = bookshelf.Model.extend({
import { Model } from 'objection';
import path from 'path';
import BaseModel from '@/models/Model';
export default class Permission extends BaseModel {
/**
* Table name of Role model.
* @type {String}
*/
tableName: 'permissions',
static get tableName() {
return 'permissions';
}
/**
* Timestamp columns.
* Relationship mapping.
*/
hasTimestamps: false,
static get relationMappings() {
return {
/**
* Permission model may belongs to role model.
*/
role: {
relation: Model.BelongsToOneRelation,
modelBase: path.join(__dirname, 'Role'),
join: {
from: 'permissions.role_id',
to: 'roles.id',
},
},
role() {
return this.belongsTo('Role', 'role_id');
},
resource() {
return this.belongsTo('Resource', 'resource_id');
},
});
export default bookshelf.model('Permission', Permission);
// resource: {
// relation: Model.BelongsToOneRelation,
// modelBase: path.join(__dirname, 'Resource'),
// join: {
// from: 'permissions.',
// to: '',
// }
// }
};
}
}

View File

@@ -1,33 +1,70 @@
import bookshelf from './bookshelf';
import path from 'path';
import { Model } from 'objection';
import BaseModel from '@/models/Model';
const Resource = bookshelf.Model.extend({
export default class Resource extends BaseModel {
/**
* Table name.
*/
tableName: 'resources',
static get tableName() {
return 'resources';
}
/**
* Timestamp columns.
*/
hasTimestamps: false,
static get hasTimestamps() {
return false;
}
/**
* Resource model may has many views.
* Relationship mapping.
*/
views() {
return this.hasMany('View', 'resource_id');
},
static get relationMappings() {
const View = require('@/models/View');
const ResourceField = require('@/models/ResourceField');
const Permission = require('@/models/Permission');
/**
* Resource model may has many fields.
*/
fields() {
return this.hasMany('ResourceField', 'resource_id');
},
return {
/**
* Resource model may has many views.
*/
views: {
relation: Model.HasManyRelation,
modelClass: View.default,
join: {
from: 'resources.id',
to: 'views.resourceId',
},
},
permissions() {
return this.belongsToMany('Permission', 'role_has_permissions', 'resource_id', 'permission_id');
},
});
/**
* Resource model may has many fields.
*/
fields: {
relation: Model.HasManyRelation,
modelClass: ResourceField.default,
join: {
from: 'resources.id',
to: 'resource_fields.resourceId',
},
},
export default bookshelf.model('Resource', Resource);
/**
* Resource model may has many associated permissions.
*/
permissions: {
relation: Model.ManyToManyRelation,
modelClass: Permission.default,
join: {
from: 'resources.id',
through: {
from: 'role_has_permissions.resourceId',
to: 'role_has_permissions.permissionId',
},
to: 'permissions.id',
},
},
};
}
}

View File

@@ -1,37 +1,53 @@
import { snakeCase } from 'lodash';
import bookshelf from './bookshelf';
import { Model } from 'objection';
import path from 'path';
import BaseModel from '@/models/Model';
const ResourceField = bookshelf.Model.extend({
export default class ResourceField extends BaseModel {
/**
* Table name.
*/
tableName: 'resource_fields',
static get tableName() {
return 'resource_fields';
}
/**
* Timestamp columns.
*/
hasTimestamps: false,
virtuals: {
/**
* Resource field key.
*/
key() {
return snakeCase(this.attributes.label_name);
},
},
static get hasTimestamps() {
return false;
}
/**
* Resource field may belongs to resource model.
* Virtual attributes.
*/
resource() {
return this.belongsTo('Resource', 'resource_id');
},
}, {
/**
* JSON Columns.
*/
jsonColumns: ['options'],
});
static get virtualAttributes() {
return ['key'];
}
export default bookshelf.model('ResourceField', ResourceField);
/**
* Resource field key.
*/
key() {
return snakeCase(this.labelName);
}
/**
* Relationship mapping.
*/
static get relationMappings() {
return {
/**
* Resource field may belongs to resource model.
*/
resource: {
relation: Model.BelongsToOneRelation,
modelBase: path.join(__dirname, 'Resource'),
join: {
from: 'resource_fields.resource_id',
to: 'resources.id',
},
},
};
}
}

View File

@@ -1,38 +1,78 @@
import bookshelf from './bookshelf';
const Role = bookshelf.Model.extend({
import { Model } from 'objection';
import BaseModel from '@/models/Model';
export default class Role extends BaseModel {
/**
* Table name of Role model.
* @type {String}
*/
tableName: 'roles',
static get tableName() {
return 'roles';
}
/**
* Timestamp columns.
*/
hasTimestamps: false,
static get hasTimestamps() {
return false;
}
/**
* Role may has many permissions.
* Relationship mapping.
*/
permissions() {
return this.belongsToMany('Permission', 'role_has_permissions', 'role_id', 'permission_id');
},
static get relationMappings() {
const Permission = require('@/models/Permission');
const Resource = require('@/models/Resource');
const User = require('@/models/User');
/**
* Role may has many resources.
*/
resources() {
return this.belongsToMany('Resource', 'role_has_permissions', 'role_id', 'resource_id');
},
return {
/**
* Role may has many permissions.
*/
permissions: {
relation: Model.ManyToManyRelation,
modelClass: Permission.default,
join: {
from: 'roles.id',
through: {
from: 'role_has_permissions.roleId',
to: 'role_has_permissions.permissionId',
},
to: 'permissions.id',
},
},
/**
* Role model may has many users.
*/
users() {
return this.belongsToMany('User', 'user_has_roles');
},
});
/**
* Role may has many resources.
*/
resources: {
relation: Model.ManyToManyRelation,
modelClass: Resource.default,
join: {
from: 'roles.id',
through: {
from: 'role_has_permissions.roleId',
to: 'role_has_permissions.resourceId',
},
to: 'resources.id',
},
},
export default bookshelf.model('Role', Role);
/**
* Role may has many associated users.
*/
users: {
relation: Model.ManyToManyRelation,
modelClass: User.default,
join: {
from: 'roles.id',
through: {
from: 'user_has_roles.roleId',
to: 'user_has_roles.userId',
},
to: 'users.id',
},
},
};
}
}

View File

@@ -1,34 +1,28 @@
import bookshelf from './bookshelf';
import Metable from './Metable';
import BaseModel from '@/models/Model';
import Auth from './Auth';
const Setting = bookshelf.Model.extend({
export default class Setting extends BaseModel {
/**
* Table name
*/
tableName: 'settings',
static get tableName() {
return 'settings';
}
/**
* Timestamp columns.
*/
hasTimestamps: false,
static get hasTimestamps() {
return false;
}
/**
* Extra metadata query to query with the current authenticate user.
* @param {Object} query
*/
extraMetadataQuery(query) {
static extraMetadataQuery(query) {
if (Auth.isLogged()) {
query.where('user_id', Auth.userId());
}
},
}, {
/**
* Table name
*/
tableName: 'settings',
...Metable,
});
export default bookshelf.model('Setting', Setting);
}
}

View File

@@ -1,23 +1,39 @@
import bcrypt from 'bcryptjs';
import bookshelf from './bookshelf';
import PermissionsService from '@/services/PermissionsService';
import { Model } from 'objection';
import BaseModel from '@/models/Model';
// import PermissionsService from '@/services/PermissionsService';
const User = bookshelf.Model.extend({
...PermissionsService,
export default class User extends BaseModel {
// ...PermissionsService
/**
* Table name
*/
tableName: 'users',
static get tableName() {
return 'users';
}
/**
* Timestamp columns.
* Relationship mapping.
*/
hasTimestamps: ['created_at', 'updated_at'],
static get relationMappings() {
const Role = require('@/models/Role');
initialize() {
this.initializeCache();
},
return {
roles: {
relation: Model.ManyToManyRelation,
modelClass: Role.default,
join: {
from: 'users.id',
through: {
from: 'user_has_roles.userId',
to: 'user_has_roles.roleId',
},
to: 'roles.id',
},
},
};
}
/**
* Verify the password of the user.
@@ -25,15 +41,6 @@ const User = bookshelf.Model.extend({
* @return {Boolean}
*/
verifyPassword(password) {
return bcrypt.compareSync(password, this.get('password'));
},
/**
* User model may has many associated roles.
*/
roles() {
return this.belongsToMany('Role', 'user_has_roles', 'user_id', 'role_id');
},
});
export default bookshelf.model('User', User);
return bcrypt.compareSync(password, this.password);
}
}

View File

@@ -1,38 +1,67 @@
import bookshelf from './bookshelf';
import path from 'path';
import { Model } from 'objection';
import BaseModel from '@/models/Model';
const View = bookshelf.Model.extend({
export default class View extends BaseModel {
/**
* Table name.
*/
tableName: 'views',
static get tableName() {
return 'views';
}
/**
* Timestamp columns.
* Relationship mapping.
*/
hasTimestamps: false,
static get relationMappings() {
return {
/**
* View model belongs to resource model.
*/
resource: {
relation: Model.BelongsToOneRelation,
modelBase: path.join(__dirname, 'Resource'),
join: {
from: 'views.resource_id',
to: 'resources.id',
},
},
/**
* View model belongs to resource model.
*/
resource() {
return this.belongsTo('Resource', 'resource_id');
},
/**
* View model may has many columns.
*/
// columns: {
// relation: Model.ManyToManyRelation,
// modelBase: path.join(__dirname, 'ResourceField'),
// join: {
// from: 'id',
// through: {
// from: 'view_has_columns.view_id',
// to: 'view_has_columns.field_id',
// },
// to: 'resource_fields.view_id',
// }
// }
/**
* View model may has many columns.
*/
columns() {
return this.belongsToMany('ResourceField', 'view_has_columns', 'view_id', 'field_id');
},
/**
* View model may has many view roles.
*/
viewRoles: {
relation: Model.HasManyRelation,
modelBase: path.join(__dirname, 'ViewRole'),
join: {
from: 'views.id',
to: 'view_id',
},
},
};
}
/**
* View model may has many view roles.
*/
viewRoles() {
return this.hasMany('ViewRole', 'view_id');
},
}, {
dependents: ['columns', 'viewRoles'],
});
// columns() {
// return this.belongsToMany('ResourceField', 'view_has_columns', 'view_id', 'field_id');
// },
export default bookshelf.model('View', View);
// viewRoles() {
// return this.hasMany('ViewRole', 'view_id');
// },
}

View File

@@ -1,19 +1,17 @@
import bookshelf from './bookshelf';
import BaseModel from '@/models/Model';
const ViewColumn = bookshelf.Model.extend({
export default class ViewColumn extends BaseModel {
/**
* Table name.
*/
tableName: 'view_columns',
static get tableName() {
return 'view_columns';
}
/**
* Timestamp columns.
*/
hasTimestamps: false,
view() {
static get hasTimestamps() {
return false;
}
});
export default bookshelf.model('ViewColumn', ViewColumn);
}

View File

@@ -1,22 +1,38 @@
import bookshelf from './bookshelf';
import { Model } from 'objection';
import path from 'path';
import BaseModel from '@/models/Model';
const ViewRole = bookshelf.Model.extend({
export default class ViewRole extends BaseModel {
/**
* Table name.
*/
tableName: 'view_roles',
static get tableName() {
return 'view_roles';
}
/**
* Timestamp columns.
*/
hasTimestamps: false,
static get hasTimestamps() {
return false;
}
/**
* View role model may belongs to view model.
* Relationship mapping.
*/
view() {
return this.belongsTo('View', 'view_id');
},
});
export default bookshelf.model('ViewRole', ViewRole);
static get relationMappings() {
return {
/**
* View role model may belongs to view model.
*/
view: {
relation: Model.BelongsToOneRelation,
modelBase: path.join(__dirname, 'View'),
join: {
from: 'view_roles.view_id',
to: 'views.id',
},
},
};
}
}

View File

@@ -1,19 +0,0 @@
import Bookshelf from 'bookshelf';
import jsonColumns from 'bookshelf-json-columns';
import bookshelfParanoia from 'bookshelf-paranoia';
import bookshelfModelBase from 'bookshelf-modelbase';
import cascadeDelete from 'bookshelf-cascade-delete';
import knex from '../database/knex';
const bookshelf = Bookshelf(knex);
bookshelf.plugin('pagination');
bookshelf.plugin('visibility');
bookshelf.plugin('registry');
bookshelf.plugin('virtuals');
bookshelf.plugin(jsonColumns);
bookshelf.plugin(bookshelfParanoia);
bookshelf.plugin(bookshelfModelBase.pluggable);
bookshelf.plugin(cascadeDelete);
export default bookshelf;

View File

@@ -0,0 +1,7 @@
import { Model } from 'objection';
import knex from '@/database/knex';
// Bind all Models to a knex instance. If you only have one database in
// your server this is all you have to do. For multi database systems, see
// the Model.bindKnex() method.
Model.knex(knex);

View File

@@ -0,0 +1,10 @@
export default class JournalEntry {
constructor(entry) {
const defaults = {
credit: 0,
debit: 0,
};
this.entry = { ...defaults, ...entry };
}
}

View File

@@ -0,0 +1,241 @@
import { pick } from 'lodash';
import moment from 'moment';
import JournalEntry from '@/services/Accounting/JournalEntry';
import AccountTransaction from '@/models/AccountTransaction';
import AccountBalance from '@/models/AccountBalance';
export default class JournalPoster {
/**
* Journal poster constructor.
*/
constructor() {
this.entries = [];
this.balancesChange = {};
}
/**
* Writes the credit entry for the given account.
* @param {JournalEntry} entry -
*/
credit(entryModel) {
if (entryModel instanceof JournalEntry === false) {
throw new Error('The entry is not instance of JournalEntry.');
}
this.entries.push(entryModel.entry);
this.setAccountBalanceChange(entryModel.entry, 'credit');
}
/**
* Writes the debit entry for the given account.
* @param {JournalEntry} entry -
*/
debit(entryModel) {
if (entryModel instanceof JournalEntry === false) {
throw new Error('The entry is not instance of JournalEntry.');
}
this.entries.push(entryModel.entry);
this.setAccountBalanceChange(entryModel.entry, 'debit');
}
/**
* Sets account balance change.
* @param {JournalEntry} entry
* @param {String} type
*/
setAccountBalanceChange(entry, type) {
if (!this.balancesChange[entry.account]) {
this.balancesChange[entry.account] = 0;
}
let change = 0;
if (entry.accountNormal === 'credit') {
change = (type === 'credit') ? entry.credit : -1 * entry.debit;
} else if (entry.accountNormal === 'debit') {
change = (type === 'debit') ? entry.debit : -1 * entry.credit;
}
this.balancesChange[entry.account] += change;
}
/**
* Mapping the balance change to list.
*/
mapBalanceChangesToList() {
const mappedList = [];
Object.keys(this.balancesChange).forEach((accountId) => {
const balance = this.balancesChange[accountId];
mappedList.push({
account_id: accountId,
amount: balance,
});
});
return mappedList;
}
/**
* Saves the balance change of journal entries.
*/
async saveBalance() {
const balancesList = this.mapBalanceChangesToList();
const balanceUpdateOpers = [];
const balanceInsertOpers = [];
const balanceFindOneOpers = [];
let balanceAccounts = [];
balancesList.forEach((balance) => {
const oper = AccountBalance.query().findOne('account_id', balance.account_id);
balanceFindOneOpers.push(oper);
});
balanceAccounts = await Promise.all(balanceFindOneOpers);
balancesList.forEach((balance) => {
const method = balance.amount < 0 ? 'decrement' : 'increment';
// Detarmine if the account balance is already exists or not.
const foundAccBalance = balanceAccounts.some((account) => (
account && account.account_id === balance.account_id
));
if (foundAccBalance) {
const query = AccountBalance
.query()[method]('amount', Math.abs(balance.amount))
.where('account_id', balance.account_id);
balanceUpdateOpers.push(query);
} else {
const query = AccountBalance.query().insert({
account_id: balance.account_id,
amount: balance.amount,
currency_code: 'USD',
});
balanceInsertOpers.push(query);
}
});
await Promise.all([
...balanceUpdateOpers, ...balanceInsertOpers,
]);
}
/**
* Saves the stacked journal entries to the storage.
*/
async saveEntries() {
const saveOperations = [];
this.entries.forEach((entry) => {
const oper = AccountTransaction.query().insert({
accountId: entry.account,
...pick(entry, ['credit', 'debit', 'transactionType',
'referenceType', 'referenceId', 'note']),
});
saveOperations.push(oper);
});
await Promise.all(saveOperations);
}
/**
* Reverses the stacked journal entries.
*/
reverseEntries() {
const reverseEntries = [];
this.entries.forEach((entry) => {
const reverseEntry = { ...entry };
if (entry.credit) {
reverseEntry.debit = entry.credit;
}
if (entry.debit) {
reverseEntry.credit = entry.debit;
}
reverseEntries.push(reverseEntry);
});
this.entries = reverseEntries;
}
/**
* Delete the given or all stacked entries.
* @param {Array} ids -
*/
async deleteEntries(ids) {
const entriesIds = ids || this.entries.map((e) => e.id);
if (entriesIds.length > 0) {
await AccountTransaction.query().whereIn('id', entriesIds).delete();
}
}
/**
* Retrieve the closing balance for the given account and closing date.
* @param {Number} accountId -
* @param {Date} closingDate -
*/
getClosingBalance(accountId, closingDate, dateType = 'day') {
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 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.credit : -1 * entry.debit;
} else if (entry.accountNormal === 'debit') {
result.balance += (entry.debit) ? entry.debit : -1 * entry.credit;
}
});
return result;
}
/**
* Load fetched accounts journal entries.
* @param {Array} entries -
*/
loadEntries(entries) {
entries.forEach((entry) => {
this.entries.push({
...entry,
account: entry.account ? entry.account.id : entry.accountId,
accountNormal: (entry.account && entry.account.type)
? entry.account.type.normal : entry.accountNormal,
});
});
}
static loadAccounts() {
}
}

View File

@@ -0,0 +1,6 @@
import Moment from 'moment';
import { extendMoment } from 'moment-range';
const moment = extendMoment(Moment);
export default moment;

View File

@@ -3,12 +3,14 @@ import { difference } from 'lodash';
import Role from '@/models/Role';
export default {
cacheKey: 'ratteb.cache,',
cacheExpirationTime: null,
permissions: [],
cache: null,
/**
* Initialize the cache.
*/
initializeCache() {
if (!this.cache) {
this.cache = new cache.Cache();

View File

@@ -0,0 +1,13 @@
import SessionModel from '@/services/SessionModel';
export default class SessionQueryBuilder extends SessionModel.QueryBuilder {
/**
* Add a custom method that stores a session object to the query context.
* @param {*} session -
*/
session(session) {
return this.mergeContext({
session,
});
}
}

View File

@@ -0,0 +1,24 @@
import SessionQueryBuilder from '@/services/SessionModel/SessionQueryBuilder';
export default class SessionModel {
/**
* Constructor method.
* @param {Object} options -
*/
constructor(options) {
this.options = { ...options, ...SessionModel.defaultOptions };
}
static get defaultOptions() {
return {
setModifiedBy: true,
setModifiedAt: true,
setCreatedBy: true,
setCreatedAt: true,
};
}
static get QueryBuilder() {
return SessionQueryBuilder;
}
}

View File

@@ -1,14 +0,0 @@
import bcrypt from 'bcryptjs';
const hashPassword = (password) => new Promise((resolve) => {
bcrypt.genSalt(10, (error, salt) => {
bcrypt.hash(password, salt, (err, hash) => { resolve(hash); });
});
});
const origin = (request) => `${request.protocol}://${request.hostname}`;
export {
hashPassword,
origin,
};

51
server/src/utils/index.js Normal file
View File

@@ -0,0 +1,51 @@
import bcrypt from 'bcryptjs';
import moment from 'moment';
const hashPassword = (password) => new Promise((resolve) => {
bcrypt.genSalt(10, (error, salt) => {
bcrypt.hash(password, salt, (err, hash) => { resolve(hash); });
});
});
const origin = (request) => `${request.protocol}://${request.hostname}`;
const dateRangeCollection = (fromDate, toDate, addType = 'day', increment = 1) => {
const collection = [];
const momentFromDate = moment(fromDate);
let dateFormat = '';
switch (addType) {
case 'day':
default:
dateFormat = 'YYYY-MM-DD'; break;
case 'month':
case 'quarter':
dateFormat = 'YYYY-MM'; break;
case 'year':
dateFormat = 'YYYY'; break;
}
for (let i = momentFromDate;
(i.isBefore(toDate, addType) || i.isSame(toDate, addType));
i.add(increment, `${addType}s`)) {
collection.push(i.endOf(addType).format(dateFormat));
}
return collection;
};
const dateRangeFormat = (rangeType) => {
switch (rangeType) {
case 'year':
return 'YYYY';
case 'month':
case 'quarter':
default:
return 'YYYY-MM';
}
};
export {
hashPassword,
origin,
dateRangeCollection,
dateRangeFormat,
};