mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
WIP server side.
This commit is contained in:
@@ -3,6 +3,7 @@ import helmet from 'helmet';
|
||||
import boom from 'express-boom';
|
||||
import '../config';
|
||||
import routes from '@/http';
|
||||
import '@/models';
|
||||
|
||||
const app = express();
|
||||
|
||||
|
||||
76
server/src/collection/BudgetEntriesSet.js
Normal file
76
server/src/collection/BudgetEntriesSet.js
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
0
server/src/collection/Cachable.js
Normal file
0
server/src/collection/Cachable.js
Normal file
279
server/src/collection/Metable.js
Normal file
279
server/src/collection/Metable.js
Normal 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));
|
||||
},
|
||||
};
|
||||
73
server/src/collection/NestedSet/index.js
Normal file
73
server/src/collection/NestedSet/index.js
Normal 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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
|
||||
213
server/src/http/controllers/Accounting.js
Normal file
213
server/src/http/controllers/Accounting.js
Normal 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();
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
|
||||
33
server/src/http/controllers/Banking.js
Normal file
33
server/src/http/controllers/Banking.js
Normal 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) {
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
10
server/src/http/controllers/Bills.js
Normal file
10
server/src/http/controllers/Bills.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import express from 'express';
|
||||
|
||||
export default {
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
return router;
|
||||
},
|
||||
};
|
||||
247
server/src/http/controllers/Budget.js
Normal file
247
server/src/http/controllers/Budget.js
Normal 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,
|
||||
})
|
||||
},
|
||||
},
|
||||
};
|
||||
122
server/src/http/controllers/BudgetReports.js
Normal file
122
server/src/http/controllers/BudgetReports.js
Normal 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: [],
|
||||
// }
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
17
server/src/http/controllers/CurrencyAdjustment.js
Normal file
17
server/src/http/controllers/CurrencyAdjustment.js
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
export default {
|
||||
|
||||
|
||||
router() {
|
||||
|
||||
},
|
||||
|
||||
addExchangePrice: {
|
||||
validation: {
|
||||
|
||||
},
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
10
server/src/http/controllers/Customers.js
Normal file
10
server/src/http/controllers/Customers.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import express from 'express';
|
||||
|
||||
export default {
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
return router;
|
||||
},
|
||||
};
|
||||
367
server/src/http/controllers/Expenses.js
Normal file
367
server/src/http/controllers/Expenses.js
Normal 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 }],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
526
server/src/http/controllers/FinancialStatements.js
Normal file
526
server/src/http/controllers/FinancialStatements.js
Normal 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) {
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
0
server/src/http/controllers/Invoices.js
Normal file
0
server/src/http/controllers/Invoices.js
Normal 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,
|
||||
|
||||
76
server/src/http/controllers/Options.js
Normal file
76
server/src/http/controllers/Options.js
Normal 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(),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
0
server/src/http/controllers/PurchasesReports.js
Normal file
0
server/src/http/controllers/PurchasesReports.js
Normal file
0
server/src/http/controllers/RecurringJournal.js
Normal file
0
server/src/http/controllers/RecurringJournal.js
Normal 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({
|
||||
|
||||
0
server/src/http/controllers/SalesReports.js
Normal file
0
server/src/http/controllers/SalesReports.js
Normal file
10
server/src/http/controllers/Suppliers.js
Normal file
10
server/src/http/controllers/Suppliers.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import express from 'express';
|
||||
|
||||
export default {
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
return router;
|
||||
},
|
||||
};
|
||||
@@ -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(),
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
0
server/src/lib/Cachable/CachableModel.js
Normal file
0
server/src/lib/Cachable/CachableModel.js
Normal file
211
server/src/lib/Metable/MetableCollection.js
Normal file
211
server/src/lib/Metable/MetableCollection.js
Normal 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;
|
||||
}
|
||||
}
|
||||
12
server/src/lib/Metable/MetableModel.js
Normal file
12
server/src/lib/Metable/MetableModel.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
export default class Metable{
|
||||
|
||||
static get modifiers() {
|
||||
return {
|
||||
whereKey(builder, key) {
|
||||
builder.where('key', key);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
81
server/src/models/AccountTransaction.js
Normal file
81
server/src/models/AccountTransaction.js
Normal 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
60
server/src/models/Budget.js
Normal file
60
server/src/models/Budget.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
server/src/models/BudgetEntry.js
Normal file
10
server/src/models/BudgetEntry.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import BaseModel from '@/models/Model';
|
||||
|
||||
export default class Budget extends BaseModel {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'budget_entries';
|
||||
}
|
||||
}
|
||||
86
server/src/models/Expense.js
Normal file
86
server/src/models/Expense.js
Normal 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
10
server/src/models/JournalEntry.js
Normal file
10
server/src/models/JournalEntry.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import BaseModel from '@/models/Model';
|
||||
|
||||
export default class JournalEntry extends BaseModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'manual_journals';
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
14
server/src/models/Model.js
Normal file
14
server/src/models/Model.js
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
16
server/src/models/Option.js
Normal file
16
server/src/models/Option.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '',
|
||||
// }
|
||||
// }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
// },
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
7
server/src/models/index.js
Normal file
7
server/src/models/index.js
Normal 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);
|
||||
0
server/src/services/Accounting/Accounting.js
Normal file
0
server/src/services/Accounting/Accounting.js
Normal file
10
server/src/services/Accounting/JournalEntry.js
Normal file
10
server/src/services/Accounting/JournalEntry.js
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
export default class JournalEntry {
|
||||
constructor(entry) {
|
||||
const defaults = {
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
};
|
||||
this.entry = { ...defaults, ...entry };
|
||||
}
|
||||
}
|
||||
241
server/src/services/Accounting/JournalPoster.js
Normal file
241
server/src/services/Accounting/JournalPoster.js
Normal 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() {
|
||||
|
||||
}
|
||||
}
|
||||
6
server/src/services/Moment/index.js
Normal file
6
server/src/services/Moment/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import Moment from 'moment';
|
||||
import { extendMoment } from 'moment-range';
|
||||
|
||||
const moment = extendMoment(Moment);
|
||||
|
||||
export default moment;
|
||||
@@ -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();
|
||||
13
server/src/services/SessionModel/SessionQueryBuilder.js
Normal file
13
server/src/services/SessionModel/SessionQueryBuilder.js
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
24
server/src/services/SessionModel/index.js
Normal file
24
server/src/services/SessionModel/index.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
51
server/src/utils/index.js
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user