WIP server side.
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
MAIL_HOST=
|
||||||
|
MAIL_USERNAME=
|
||||||
|
MAIL_PASSWORD=
|
||||||
|
MAIL_PORT=
|
||||||
|
MAIL_SECURE=false
|
||||||
|
|
||||||
|
MAIL_FROM_ADDRESS=
|
||||||
|
MAIL_FROM_NAME=
|
||||||
|
|
||||||
|
DB_CLIENT=mysql
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=root
|
||||||
|
DB_NAME=ratteb
|
||||||
@@ -33,7 +33,7 @@ const actions = {
|
|||||||
return ApiService.post('auth/send_reset_password', { email });
|
return ApiService.post('auth/send_reset_password', { email });
|
||||||
},
|
},
|
||||||
|
|
||||||
newPassword(null, { form }) {
|
newPassword({}, { form }) {
|
||||||
return ApiService.post('auth/new_password', form);
|
return ApiService.post('auth/new_password', form);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,3 +36,5 @@ export const handleClipboard = (text, event) => {
|
|||||||
// })
|
// })
|
||||||
// clipboard.onClick(event)
|
// clipboard.onClick(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Generated
+67
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"requires": true,
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": {
|
||||||
|
"version": "6.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
|
||||||
|
"integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^2.0.1",
|
||||||
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
|
"json-schema-traverse": "^0.4.1",
|
||||||
|
"uri-js": "^4.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"db-errors": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/db-errors/-/db-errors-0.2.3.tgz",
|
||||||
|
"integrity": "sha512-OOgqgDuCavHXjYSJoV2yGhv6SeG8nk42aoCSoyXLZUH7VwFG27rxbavU1z+VrZbZjphw5UkDQwUlD21MwZpUng==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"fast-deep-equal": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"fast-json-stable-stringify": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"json-schema-traverse": {
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"objection": {
|
||||||
|
"version": "2.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/objection/-/objection-2.0.10.tgz",
|
||||||
|
"integrity": "sha512-mgt79CgmAJMmRr+fql60n1LRWJuHXjE1Wy3zZt0f+MJc8VB1lNZKHGqFFVgAJ61yjCL3WKta6HncYgl7Z17mKA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ajv": "^6.10.2",
|
||||||
|
"db-errors": "^0.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"punycode": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"uri-js": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"punycode": "^2.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-4
@@ -1,6 +1,9 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"presets": ["@babel/preset-env"],
|
"presets": ["@babel/preset-env"],
|
||||||
"retainLines": true,
|
"retainLines": true,
|
||||||
"plugins": ["@babel/plugin-transform-runtime"]
|
"plugins": [
|
||||||
}
|
"@babel/plugin-transform-runtime",
|
||||||
|
"@babel/plugin-syntax-dynamic-import"
|
||||||
|
]
|
||||||
|
}
|
||||||
+2
-2
@@ -8,7 +8,7 @@ MAIL_FROM_ADDRESS=
|
|||||||
MAIL_FROM_NAME=
|
MAIL_FROM_NAME=
|
||||||
|
|
||||||
DB_CLIENT=mysql
|
DB_CLIENT=mysql
|
||||||
DB_HOST=127.0.0.1
|
DB_HOST=localhost
|
||||||
DB_USER=root
|
DB_USER=root
|
||||||
DB_PASSWORD=root
|
DB_PASSWORD=root
|
||||||
DB_NAME=moosher
|
DB_NAME=ratteb
|
||||||
|
|||||||
+1
-1
@@ -11,6 +11,6 @@ DB_CLIENT=mysql
|
|||||||
DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
DB_USER=root
|
DB_USER=root
|
||||||
DB_PASSWORD=root
|
DB_PASSWORD=root
|
||||||
DB_NAME=moosher
|
DB_NAME=ratteb
|
||||||
|
|
||||||
JWT_SECRET_KEY=ahmedmohamked
|
JWT_SECRET_KEY=ahmedmohamked
|
||||||
Vendored
+6549
-30
File diff suppressed because one or more lines are too long
+7
-6
@@ -1,6 +1,7 @@
|
|||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
const MIGRATIONS_DIR = `./${__dirname}/src/database/migrations`;
|
const MIGRATIONS_DIR = './src/database/migrations';
|
||||||
const SEEDS_DIR = `./${__dirname}/src/database/seeds`;
|
const SEEDS_DIR = './src/database/seeds';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
test: {
|
test: {
|
||||||
@@ -9,10 +10,10 @@ module.exports = {
|
|||||||
directory: MIGRATIONS_DIR,
|
directory: MIGRATIONS_DIR,
|
||||||
},
|
},
|
||||||
connection: {
|
connection: {
|
||||||
host: '172.17.0.2',
|
host: process.env.DB_HOST,
|
||||||
user: 'root',
|
user: process.env.DB_USER,
|
||||||
password: 'root',
|
password: process.env.DB_PASSWORD,
|
||||||
database: 'moosher',
|
database: process.env.DB_NAME,
|
||||||
charset: 'utf8',
|
charset: 'utf8',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+1405
File diff suppressed because one or more lines are too long
Generated
+597
-63
File diff suppressed because it is too large
Load Diff
+7
-3
@@ -30,18 +30,22 @@
|
|||||||
"express-validator": "^6.2.0",
|
"express-validator": "^6.2.0",
|
||||||
"helmet": "^3.21.0",
|
"helmet": "^3.21.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"knex": "^0.19.2",
|
"knex": "^0.20.3",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
|
"moment-range": "^4.0.2",
|
||||||
"mustache": "^3.0.3",
|
"mustache": "^3.0.3",
|
||||||
|
"mysql": "^2.17.1",
|
||||||
"mysql2": "^1.6.5",
|
"mysql2": "^1.6.5",
|
||||||
"node-cache": "^4.2.1",
|
"node-cache": "^4.2.1",
|
||||||
"nodemailer": "^6.3.0",
|
"nodemailer": "^6.3.0",
|
||||||
"nodemon": "^1.19.1"
|
"nodemon": "^1.19.1",
|
||||||
|
"objection": "^2.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.5.5",
|
"@babel/core": "^7.5.5",
|
||||||
|
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||||
"@babel/plugin-transform-runtime": "^7.5.5",
|
"@babel/plugin-transform-runtime": "^7.5.5",
|
||||||
"@babel/polyfill": "^7.4.4",
|
"@babel/polyfill": "^7.4.4",
|
||||||
"@babel/preset-env": "^7.5.5",
|
"@babel/preset-env": "^7.5.5",
|
||||||
@@ -56,7 +60,7 @@
|
|||||||
"eslint-friendly-formatter": "^4.0.1",
|
"eslint-friendly-formatter": "^4.0.1",
|
||||||
"eslint-import-resolver-webpack": "^0.11.1",
|
"eslint-import-resolver-webpack": "^0.11.1",
|
||||||
"eslint-loader": "^2.2.1",
|
"eslint-loader": "^2.2.1",
|
||||||
"eslint-plugin-import": "^2.18.2",
|
"eslint-plugin-import": "^2.19.1",
|
||||||
"faker": "^4.1.0",
|
"faker": "^4.1.0",
|
||||||
"knex-factory": "0.0.6",
|
"knex-factory": "0.0.6",
|
||||||
"mocha": "^5.2.0",
|
"mocha": "^5.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MYSQL_USER="moosher"
|
MYSQL_USER="ratteb"
|
||||||
MYSQL_DATABASE="moosher"
|
MYSQL_DATABASE="ratteb"
|
||||||
MYSQL_CONTAINER_NAME="moosher_test"
|
MYSQL_CONTAINER_NAME="ratteb_test"
|
||||||
|
|
||||||
MYSQL_ROOT_PASSWORD="root"
|
MYSQL_ROOT_PASSWORD="root"
|
||||||
MYSQL_PASSWORD="root"
|
MYSQL_PASSWORD="root"
|
||||||
@@ -28,4 +28,4 @@ done
|
|||||||
|
|
||||||
echo "Database '${MYSQL_DATABASE}' running."
|
echo "Database '${MYSQL_DATABASE}' running."
|
||||||
echo " Username: ${MYSQL_USER}"
|
echo " Username: ${MYSQL_USER}"
|
||||||
echo " Password: ${MYSQL_PASSWORD}"
|
echo " Password: ${MYSQL_PASSWORD}"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import helmet from 'helmet';
|
|||||||
import boom from 'express-boom';
|
import boom from 'express-boom';
|
||||||
import '../config';
|
import '../config';
|
||||||
import routes from '@/http';
|
import routes from '@/http';
|
||||||
|
import '@/models';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
|||||||
@@ -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,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));
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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 () => ({
|
factory.define('account_type', 'account_types', async () => ({
|
||||||
name: faker.lorem.words(2),
|
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 () => {
|
factory.define('account', 'accounts', async () => {
|
||||||
const accountType = await factory.create('account_type');
|
const accountType = await factory.create('account_type');
|
||||||
return {
|
return {
|
||||||
name: faker.lorem.word(),
|
name: faker.lorem.word(),
|
||||||
|
code: faker.random.number(),
|
||||||
account_type_id: accountType.id,
|
account_type_id: accountType.id,
|
||||||
description: faker.lorem.paragraph(),
|
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', () => ({
|
factory.define('item_category', 'items_categories', () => ({
|
||||||
label: faker.name.firstName(),
|
label: faker.name.firstName(),
|
||||||
description: faker.lorem.text(),
|
description: faker.lorem.text(),
|
||||||
@@ -135,11 +170,13 @@ factory.define('resource_field', 'resource_fields', async () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
label_name: faker.lorem.words(),
|
label_name: faker.lorem.words(),
|
||||||
|
slug: faker.lorem.slug(),
|
||||||
data_type: dataTypes[Math.floor(Math.random() * dataTypes.length)],
|
data_type: dataTypes[Math.floor(Math.random() * dataTypes.length)],
|
||||||
help_text: faker.lorem.words(),
|
help_text: faker.lorem.words(),
|
||||||
default: faker.lorem.word(),
|
default: faker.lorem.word(),
|
||||||
resource_id: resource.id,
|
resource_id: resource.id,
|
||||||
active: true,
|
active: true,
|
||||||
|
columnable: true,
|
||||||
predefined: false,
|
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;
|
export default factory;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import Knex from 'knex';
|
import Knex from 'knex';
|
||||||
|
import { knexSnakeCaseMappers } from 'objection';
|
||||||
import knexfile from '@/../knexfile';
|
import knexfile from '@/../knexfile';
|
||||||
|
|
||||||
const config = knexfile[process.env.NODE_ENV];
|
const config = knexfile[process.env.NODE_ENV];
|
||||||
const knex = Knex(config);
|
const knex = Knex({
|
||||||
|
...config,
|
||||||
|
...knexSnakeCaseMappers({ upperCase: true }),
|
||||||
|
});
|
||||||
|
|
||||||
export default knex;
|
export default knex;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ exports.up = function (knex) {
|
|||||||
table.integer('parent_account_id');
|
table.integer('parent_account_id');
|
||||||
table.string('code', 10);
|
table.string('code', 10);
|
||||||
table.text('description');
|
table.text('description');
|
||||||
|
table.boolean('active').defaultTo(true);
|
||||||
|
table.integer('index').unsigned();
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
exports.up = function (knex) {
|
exports.up = function (knex) {
|
||||||
return knex.schema.createTable('account_balance', (table) => {
|
return knex.schema.createTable('account_balances', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.integer('account_id');
|
table.integer('account_id');
|
||||||
table.decimal('amount');
|
table.decimal('amount', 15, 5);
|
||||||
table.string('currency_code', 3);
|
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) => {
|
return knex.schema.createTable('account_types', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.string('name');
|
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) => {
|
return knex.schema.createTable('resource_fields', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.string('label_name');
|
table.string('label_name');
|
||||||
|
table.string('slug');
|
||||||
table.string('data_type');
|
table.string('data_type');
|
||||||
table.string('help_text');
|
table.string('help_text');
|
||||||
table.string('default');
|
table.string('default');
|
||||||
table.boolean('active');
|
table.boolean('active');
|
||||||
table.boolean('predefined');
|
table.boolean('predefined');
|
||||||
|
table.boolean('columnable');
|
||||||
table.json('options');
|
table.json('options');
|
||||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ exports.up = function (knex) {
|
|||||||
table.string('name');
|
table.string('name');
|
||||||
table.boolean('predefined');
|
table.boolean('predefined');
|
||||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
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 express from 'express';
|
||||||
import { check, validationResult, oneOf } from 'express-validator';
|
import { check, validationResult, oneOf } from 'express-validator';
|
||||||
import { difference } from 'lodash';
|
import { difference } from 'lodash';
|
||||||
import knex from 'knex';
|
|
||||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||||
import Account from '@/models/Account';
|
import Account from '@/models/Account';
|
||||||
import '@/models/AccountBalance';
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||||
|
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -36,7 +36,6 @@ export default {
|
|||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const validationErrors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
// const defaultCurrency = 'USD';
|
|
||||||
|
|
||||||
if (!validationErrors.isEmpty()) {
|
if (!validationErrors.isEmpty()) {
|
||||||
return res.boom.badData(null, {
|
return res.boom.badData(null, {
|
||||||
@@ -45,16 +44,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { accounts } = req.body;
|
const { accounts } = req.body;
|
||||||
|
|
||||||
const accountsIds = accounts.map((account) => account.id);
|
const accountsIds = accounts.map((account) => account.id);
|
||||||
const accountsCollection = await Account.query((query) => {
|
const accountsCollection = await Account.query()
|
||||||
query.select(['id']);
|
.select(['id'])
|
||||||
query.whereIn('id', accountsIds);
|
.whereIn('id', accountsIds);
|
||||||
}).fetchAll({
|
|
||||||
withRelated: ['balances'],
|
|
||||||
});
|
|
||||||
|
|
||||||
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 notFoundAccountsIds = difference(accountsIds, accountsStoredIds);
|
||||||
const errorReasons = [];
|
const errorReasons = [];
|
||||||
|
|
||||||
@@ -62,34 +58,35 @@ export default {
|
|||||||
const ids = notFoundAccountsIds.map((a) => parseInt(a, 10));
|
const ids = notFoundAccountsIds.map((a) => parseInt(a, 10));
|
||||||
errorReasons.push({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids });
|
errorReasons.push({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorReasons.length > 0) {
|
if (errorReasons.length > 0) {
|
||||||
return res.boom.badData(null, { errors: errorReasons });
|
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]));
|
accounts.forEach((account) => {
|
||||||
const storedBalancesMap = new Map(storedAccountsBalances.map((balance) => [
|
const entry = new JournalEntry({
|
||||||
balance.attributes, balance.attributes.id,
|
account: account.id,
|
||||||
]));
|
accountNormal: account.type.normal,
|
||||||
|
});
|
||||||
|
|
||||||
// const updatedStoredBalanced = [];
|
if (account.credit) {
|
||||||
const notStoredBalances = [];
|
entry.credit = account.credit;
|
||||||
|
journalEntries.credit(entry);
|
||||||
accountsIds.forEach((id) => {
|
} else if (account.debit) {
|
||||||
if (!storedBalancesMap.get(id)) {
|
entry.debit = account.debit;
|
||||||
notStoredBalances.push(id);
|
journalEntries.debit(entry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await knex('accounts_balances').insert([
|
await Promise.all([
|
||||||
...notStoredBalances.map((id) => {
|
journalEntries.saveEntries(),
|
||||||
const account = submitBalancesMap.get(id);
|
journalEntries.saveBalance(),
|
||||||
return { ...account };
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return res.status(200).send();
|
return res.status(200).send();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 express from 'express';
|
||||||
import { check, validationResult, param } from 'express-validator';
|
import { check, validationResult, param, query } from 'express-validator';
|
||||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
import Account from '@/models/Account';
|
import Account from '@/models/Account';
|
||||||
// import AccountBalance from '@/models/AccountBalance';
|
|
||||||
import AccountType from '@/models/AccountType';
|
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 {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -13,7 +16,7 @@ export default {
|
|||||||
router() {
|
router() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// router.use(JWTAuth);
|
router.use(JWTAuth);
|
||||||
router.post('/',
|
router.post('/',
|
||||||
this.newAccount.validation,
|
this.newAccount.validation,
|
||||||
asyncMiddleware(this.newAccount.handler));
|
asyncMiddleware(this.newAccount.handler));
|
||||||
@@ -23,12 +26,33 @@ export default {
|
|||||||
asyncMiddleware(this.editAccount.handler));
|
asyncMiddleware(this.editAccount.handler));
|
||||||
|
|
||||||
router.get('/:id',
|
router.get('/:id',
|
||||||
|
this.getAccount.validation,
|
||||||
asyncMiddleware(this.getAccount.handler));
|
asyncMiddleware(this.getAccount.handler));
|
||||||
|
|
||||||
|
router.get('/',
|
||||||
|
this.getAccountsList.validation,
|
||||||
|
asyncMiddleware(this.getAccountsList.handler));
|
||||||
|
|
||||||
router.delete('/:id',
|
router.delete('/:id',
|
||||||
this.deleteAccount.validation,
|
this.deleteAccount.validation,
|
||||||
asyncMiddleware(this.deleteAccount.handler));
|
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;
|
return router;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -37,10 +61,10 @@ export default {
|
|||||||
*/
|
*/
|
||||||
newAccount: {
|
newAccount: {
|
||||||
validation: [
|
validation: [
|
||||||
check('name').isLength({ min: 3 }).trim().escape(),
|
check('name').exists().isLength({ min: 3 }).trim().escape(),
|
||||||
check('code').isLength({ max: 10 }).trim().escape(),
|
check('code').exists().isLength({ max: 10 }).trim().escape(),
|
||||||
check('account_type_id').isNumeric().toInt(),
|
check('account_type_id').exists().isNumeric().toInt(),
|
||||||
check('description').trim().escape(),
|
check('description').optional().trim().escape(),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const validationErrors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
@@ -50,34 +74,31 @@ export default {
|
|||||||
code: 'validation_error', ...validationErrors,
|
code: 'validation_error', ...validationErrors,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const form = { ...req.body };
|
||||||
|
|
||||||
const { name, code, description } = req.body;
|
const foundAccountCodePromise = form.code
|
||||||
const { account_type_id: typeId } = req.body;
|
? Account.query().where('code', form.code) : null;
|
||||||
|
|
||||||
const foundAccountCodePromise = code ? Account.where('code', code).fetch() : null;
|
const foundAccountTypePromise = AccountType.query()
|
||||||
const foundAccountTypePromise = AccountType.where('id', typeId).fetch();
|
.findById(form.account_type_id);
|
||||||
|
|
||||||
const [foundAccountCode, foundAccountType] = await Promise.all([
|
const [foundAccountCode, foundAccountType] = await Promise.all([
|
||||||
foundAccountCodePromise,
|
foundAccountCodePromise, foundAccountTypePromise,
|
||||||
foundAccountTypePromise,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!foundAccountCode && foundAccountCodePromise) {
|
if (foundAccountCodePromise && foundAccountCode.length > 0) {
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
|
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!foundAccountType) {
|
if (!foundAccountType) {
|
||||||
return res.boom.badRequest(null, {
|
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({
|
await Account.query().insert({ ...form });
|
||||||
name, code, account_type_id: typeId, description,
|
|
||||||
});
|
|
||||||
|
|
||||||
await account.save();
|
return res.status(200).send({ item: { } });
|
||||||
return res.status(200).send({ item: { ...account.attributes } });
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -86,11 +107,11 @@ export default {
|
|||||||
*/
|
*/
|
||||||
editAccount: {
|
editAccount: {
|
||||||
validation: [
|
validation: [
|
||||||
param('id').toInt(),
|
param('id').exists().toInt(),
|
||||||
check('name').isLength({ min: 3 }).trim().escape(),
|
check('name').exists().isLength({ min: 3 }).trim().escape(),
|
||||||
check('code').isLength({ max: 10 }).trim().escape(),
|
check('code').exists().isLength({ max: 10 }).trim().escape(),
|
||||||
check('account_type_id').isNumeric().toInt(),
|
check('account_type_id').exists().isNumeric().toInt(),
|
||||||
check('description').trim().escape(),
|
check('description').optional().trim().escape(),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@@ -101,39 +122,33 @@ export default {
|
|||||||
code: 'validation_error', ...validationErrors,
|
code: 'validation_error', ...validationErrors,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const form = { ...req.body };
|
||||||
const account = await Account.where('id', id).fetch();
|
const account = await Account.query().findById(id);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return res.boom.notFound();
|
return res.boom.notFound();
|
||||||
}
|
}
|
||||||
const { name, code, description } = req.body;
|
const foundAccountCodePromise = (form.code && form.code !== account.code)
|
||||||
const { account_type_id: typeId } = req.body;
|
? Account.query().where('code', form.code).whereNot('id', account.id) : null;
|
||||||
|
|
||||||
const foundAccountCodePromise = (code && code !== account.attributes.code)
|
const foundAccountTypePromise = (form.account_type_id !== account.account_type_id)
|
||||||
? Account.query({ where: { code }, whereNot: { id } }).fetch() : null;
|
? AccountType.query().where('id', form.account_type_id) : null;
|
||||||
|
|
||||||
const foundAccountTypePromise = (typeId !== account.attributes.account_type_id)
|
|
||||||
? AccountType.where('id', typeId).fetch() : null;
|
|
||||||
|
|
||||||
const [foundAccountCode, foundAccountType] = await Promise.all([
|
const [foundAccountCode, foundAccountType] = await Promise.all([
|
||||||
foundAccountCodePromise, foundAccountTypePromise,
|
foundAccountCodePromise, foundAccountTypePromise,
|
||||||
]);
|
]);
|
||||||
|
if (foundAccountCode.length > 0 && foundAccountCodePromise) {
|
||||||
if (!foundAccountCode && foundAccountCodePromise) {
|
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
|
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!foundAccountType && foundAccountTypePromise) {
|
if (foundAccountType.length <= 0 && foundAccountTypePromise) {
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 110 }],
|
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();
|
return res.status(200).send();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -142,7 +157,7 @@ export default {
|
|||||||
* Get details of the given account.
|
* Get details of the given account.
|
||||||
*/
|
*/
|
||||||
getAccount: {
|
getAccount: {
|
||||||
valiation: [
|
validation: [
|
||||||
param('id').toInt(),
|
param('id').toInt(),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
@@ -165,14 +180,163 @@ export default {
|
|||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const account = await Account.where('id', id).fetch();
|
const account = await Account.query().findById(id);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return res.boom.notFound();
|
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 fs from 'fs';
|
||||||
import Mustache from 'mustache';
|
import Mustache from 'mustache';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import User from '@/models/User';
|
|
||||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||||
|
import User from '@/models/User';
|
||||||
import PasswordReset from '@/models/PasswordReset';
|
import PasswordReset from '@/models/PasswordReset';
|
||||||
import mail from '@/services/mail';
|
import mail from '@/services/mail';
|
||||||
import { hashPassword } from '@/utils';
|
import { hashPassword } from '@/utils';
|
||||||
@@ -52,10 +52,10 @@ export default {
|
|||||||
const { crediential, password } = req.body;
|
const { crediential, password } = req.body;
|
||||||
const { JWT_SECRET_KEY } = process.env;
|
const { JWT_SECRET_KEY } = process.env;
|
||||||
|
|
||||||
const user = await User.query({
|
const user = await User.query()
|
||||||
where: { email: crediential },
|
.where('email', crediential)
|
||||||
orWhere: { phone_number: crediential },
|
.orWhere('phone_number', crediential)
|
||||||
}).fetch();
|
.first();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
@@ -67,15 +67,15 @@ export default {
|
|||||||
errors: [{ type: 'INCORRECT_PASSWORD', code: 110 }],
|
errors: [{ type: 'INCORRECT_PASSWORD', code: 110 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!user.attributes.active) {
|
if (!user.active) {
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
errors: [{ type: 'USER_INACTIVE', code: 120 }],
|
errors: [{ type: 'USER_INACTIVE', code: 120 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
user.save({ last_login_at: new Date() });
|
// user.update({ last_login_at: new Date() });
|
||||||
|
|
||||||
const token = jwt.sign({
|
const token = jwt.sign({
|
||||||
email: user.attributes.email,
|
email: user.email,
|
||||||
_id: user.id,
|
_id: user.id,
|
||||||
}, JWT_SECRET_KEY, {
|
}, JWT_SECRET_KEY, {
|
||||||
expiresIn: '1d',
|
expiresIn: '1d',
|
||||||
@@ -113,7 +113,6 @@ export default {
|
|||||||
email,
|
email,
|
||||||
token: '123123',
|
token: '123123',
|
||||||
});
|
});
|
||||||
|
|
||||||
await passwordReset.save();
|
await passwordReset.save();
|
||||||
|
|
||||||
const filePath = path.join(__dirname, '../../views/mail/ResetPassword.html');
|
const filePath = path.join(__dirname, '../../views/mail/ResetPassword.html');
|
||||||
@@ -166,19 +165,18 @@ export default {
|
|||||||
const { token } = req.params;
|
const { token } = req.params;
|
||||||
const { password } = req.body;
|
const { password } = req.body;
|
||||||
|
|
||||||
const tokenModel = await PasswordReset.query((query) => {
|
const tokenModel = await PasswordReset.query()
|
||||||
query.where({ token });
|
.where('token', token)
|
||||||
query.where('created_at', '>=', Date.now() - 3600000);
|
.where('created_at', '>=', Date.now() - 3600000)
|
||||||
}).fetch();
|
.first();
|
||||||
|
|
||||||
if (!tokenModel) {
|
if (!tokenModel) {
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
|
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.where({
|
const user = await User.where({
|
||||||
email: tokenModel.attributes.email,
|
email: tokenModel.email,
|
||||||
});
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
@@ -187,7 +185,7 @@ export default {
|
|||||||
}
|
}
|
||||||
const hashedPassword = await hashPassword(password);
|
const hashedPassword = await hashPassword(password);
|
||||||
|
|
||||||
user.set('password', hashedPassword);
|
user.password = hashedPassword;
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
await PasswordReset.where('email', user.get('email')).destroy({ require: false });
|
await PasswordReset.where('email', user.get('email')).destroy({ require: false });
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
router() {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
return router;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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: [],
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
|
||||||
|
router() {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
addExchangePrice: {
|
||||||
|
validation: {
|
||||||
|
|
||||||
|
},
|
||||||
|
async handler(req, res) {
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
router() {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
return router;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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) {
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -70,19 +70,17 @@ export default {
|
|||||||
code: 'validation_error', ...validationErrors,
|
code: 'validation_error', ...validationErrors,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const form = { ...req.body };
|
||||||
const { sell_account_id: sellAccountId, cost_account_id: costAccountId } = req.body;
|
|
||||||
const { category_id: categoryId, custom_fields: customFields } = req.body;
|
|
||||||
const errorReasons = [];
|
const errorReasons = [];
|
||||||
|
|
||||||
const costAccountPromise = Account.where('id', costAccountId).fetch();
|
const costAccountPromise = Account.where('id', form.cost_account_id).fetch();
|
||||||
const sellAccountPromise = Account.where('id', sellAccountId).fetch();
|
const sellAccountPromise = Account.where('id', form.sell_account_id).fetch();
|
||||||
const itemCategoryPromise = (categoryId)
|
const itemCategoryPromise = (form.category_id)
|
||||||
? ItemCategory.where('id', categoryId).fetch() : null;
|
? ItemCategory.where('id', form.category_id).fetch() : null;
|
||||||
|
|
||||||
// Validate the custom fields key and value type.
|
// Validate the custom fields key and value type.
|
||||||
if (customFields.length > 0) {
|
if (form.custom_fields.length > 0) {
|
||||||
const customFieldsKeys = customFields.map((field) => field.key);
|
const customFieldsKeys = form.custom_fields.map((field) => field.key);
|
||||||
|
|
||||||
// Get resource id than get all resource fields.
|
// Get resource id than get all resource fields.
|
||||||
const resource = await Resource.where('name', 'items').fetch();
|
const resource = await Resource.where('name', 'items').fetch();
|
||||||
@@ -110,13 +108,12 @@ export default {
|
|||||||
if (!sellAccount) {
|
if (!sellAccount) {
|
||||||
errorReasons.push({ type: 'SELL_ACCOUNT_NOT_FOUND', code: 120 });
|
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 });
|
errorReasons.push({ type: 'ITEM_CATEGORY_NOT_FOUND', code: 140 });
|
||||||
}
|
}
|
||||||
if (errorReasons.length > 0) {
|
if (errorReasons.length > 0) {
|
||||||
return res.boom.badRequest(null, { errors: errorReasons });
|
return res.boom.badRequest(null, { errors: errorReasons });
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = Item.forge({
|
const item = Item.forge({
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
type_id: 1,
|
type_id: 1,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -18,42 +18,39 @@ const AccessControllSchema = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// eslint-disable-next-line arrow-body-style
|
const getResourceSchema = (resource) => AccessControllSchema
|
||||||
const getResourceSchema = (resource) => AccessControllSchema.find((schema) => {
|
.find((schema) => schema.resource === resource);
|
||||||
return schema.resource === resource;
|
|
||||||
});
|
|
||||||
|
|
||||||
const getResourcePermissions = (resource) => {
|
const getResourcePermissions = (resource) => {
|
||||||
const foundResource = getResourceSchema(resource);
|
const foundResource = getResourceSchema(resource);
|
||||||
return foundResource ? foundResource.permissions : [];
|
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 {
|
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 constructor method.
|
||||||
*/
|
*/
|
||||||
router() {
|
router() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/',
|
||||||
|
this.newRole.validation,
|
||||||
|
asyncMiddleware(this.newRole.handler));
|
||||||
|
|
||||||
router.post('/:id',
|
router.post('/:id',
|
||||||
this.editRole.validation,
|
this.editRole.validation,
|
||||||
asyncMiddleware(this.editRole.handler.bind(this)));
|
asyncMiddleware(this.editRole.handler.bind(this)));
|
||||||
|
|
||||||
// router.post('/',
|
|
||||||
// this.newRole.validation,
|
|
||||||
// asyncMiddleware(this.newRole.handler));
|
|
||||||
|
|
||||||
router.delete('/:id',
|
router.delete('/:id',
|
||||||
this.deleteRole.validation,
|
this.deleteRole.validation,
|
||||||
asyncMiddleware(this.deleteRole.handler));
|
asyncMiddleware(this.deleteRole.handler));
|
||||||
@@ -80,32 +77,30 @@ export default {
|
|||||||
code: 'validation_error', ...validationErrors,
|
code: 'validation_error', ...validationErrors,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, description, permissions } = req.body;
|
const { name, description, permissions } = req.body;
|
||||||
|
|
||||||
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
|
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
|
||||||
const permissionsSlugs = [];
|
const permissionsSlugs = [];
|
||||||
|
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
|
||||||
|
|
||||||
const resourcesNotFound = this.findNotFoundResources(resourcesSlugs);
|
|
||||||
const errorReasons = [];
|
const errorReasons = [];
|
||||||
const notFoundPermissions = [];
|
const notFoundPermissions = [];
|
||||||
|
|
||||||
if (resourcesNotFound.length > 0) {
|
if (resourcesNotFound.length > 0) {
|
||||||
errorReasons.push({
|
errorReasons.push({
|
||||||
type: 'RESOURCE_SLUG_NOT_FOUND',
|
type: 'RESOURCE_SLUG_NOT_FOUND', code: 100, resources: resourcesNotFound,
|
||||||
code: 100,
|
|
||||||
resources: resourcesNotFound,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
permissions.forEach((perm) => {
|
permissions.forEach((perm) => {
|
||||||
const abilities = perm.permissions.map((ability) => ability);
|
const abilities = perm.permissions.map((ability) => ability);
|
||||||
|
|
||||||
// Gets the not found permissions in the schema.
|
// 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) {
|
if (notFoundAbilities.length > 0) {
|
||||||
notFoundPermissions.push({
|
notFoundPermissions.push({
|
||||||
resource_slug: perm.resource_slug, permissions: notFoundAbilities,
|
resource_slug: perm.resource_slug,
|
||||||
|
permissions: notFoundAbilities,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const perms = perm.permissions || [];
|
const perms = perm.permissions || [];
|
||||||
@@ -217,7 +212,7 @@ export default {
|
|||||||
const notFoundPermissions = [];
|
const notFoundPermissions = [];
|
||||||
|
|
||||||
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
|
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
|
||||||
const resourcesNotFound = this.findNotFoundResources(resourcesSlugs);
|
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
|
||||||
|
|
||||||
if (resourcesNotFound.length > 0) {
|
if (resourcesNotFound.length > 0) {
|
||||||
errorReasons.push({
|
errorReasons.push({
|
||||||
@@ -230,7 +225,7 @@ export default {
|
|||||||
permissions.forEach((perm) => {
|
permissions.forEach((perm) => {
|
||||||
const abilities = perm.permissions.map((ability) => ability);
|
const abilities = perm.permissions.map((ability) => ability);
|
||||||
// Gets the not found permissions in the schema.
|
// 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) {
|
if (notFoundAbilities.length > 0) {
|
||||||
notFoundPermissions.push({
|
notFoundPermissions.push({
|
||||||
|
|||||||
@@ -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 { difference } from 'lodash';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { check, validationResult } from 'express-validator';
|
import { check, query, validationResult } from 'express-validator';
|
||||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
import Resource from '@/models/Resource';
|
import Resource from '@/models/Resource';
|
||||||
import View from '../../models/View';
|
import View from '../../models/View';
|
||||||
@@ -8,10 +8,13 @@ import View from '../../models/View';
|
|||||||
export default {
|
export default {
|
||||||
resource: 'items',
|
resource: 'items',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
router() {
|
router() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post('/resource/:resource_id',
|
router.post('/',
|
||||||
this.createView.validation,
|
this.createView.validation,
|
||||||
asyncMiddleware(this.createView.handler));
|
asyncMiddleware(this.createView.handler));
|
||||||
|
|
||||||
@@ -33,7 +36,9 @@ export default {
|
|||||||
* List all views that associated with the given resource.
|
* List all views that associated with the given resource.
|
||||||
*/
|
*/
|
||||||
listViews: {
|
listViews: {
|
||||||
validation: [],
|
validation: [
|
||||||
|
query('resource_name').optional().trim().escape(),
|
||||||
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const { resource_id: resourceId } = req.params;
|
const { resource_id: resourceId } = req.params;
|
||||||
const views = await View.where('resource_id', resourceId).fetchAll();
|
const views = await View.where('resource_id', resourceId).fetchAll();
|
||||||
@@ -54,7 +59,6 @@ export default {
|
|||||||
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
|
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({ ...view.toJSON() });
|
return res.status(200).send({ ...view.toJSON() });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -66,25 +70,23 @@ export default {
|
|||||||
validation: [],
|
validation: [],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const { view_id: viewId } = req.params;
|
const { view_id: viewId } = req.params;
|
||||||
const view = await View.where('id', viewId).fetch({
|
const view = await View.query().findById(viewId);
|
||||||
withRelated: ['viewRoles', 'columns'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!view) {
|
if (!view) {
|
||||||
return res.boom.notFound(null, {
|
return res.boom.notFound(null, {
|
||||||
errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }],
|
errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (view.predefined) {
|
||||||
if (view.attributes.predefined) {
|
|
||||||
return res.boom.badRequest(null, {
|
return res.boom.badRequest(null, {
|
||||||
errors: [{ type: 'PREDEFINED_VIEW', code: 200 }],
|
errors: [{ type: 'PREDEFINED_VIEW', code: 200 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// console.log(view);
|
await Promise.all([
|
||||||
await view.destroy();
|
view.$relatedQuery('viewRoles').delete(),
|
||||||
|
view.$relatedQuery('columns').delete(),
|
||||||
// await view.columns().destroy({ require: false });
|
]);
|
||||||
|
await view.delete();
|
||||||
|
|
||||||
return res.status(200).send({ id: view.get('id') });
|
return res.status(200).send({ id: view.get('id') });
|
||||||
},
|
},
|
||||||
@@ -95,16 +97,17 @@ export default {
|
|||||||
*/
|
*/
|
||||||
createView: {
|
createView: {
|
||||||
validation: [
|
validation: [
|
||||||
|
check('resource_name').exists().escape().trim(),
|
||||||
check('label').exists().escape().trim(),
|
check('label').exists().escape().trim(),
|
||||||
check('columns').isArray({ min: 3 }),
|
check('columns').exists().isArray({ min: 1 }),
|
||||||
check('roles').isArray(),
|
check('roles').isArray(),
|
||||||
check('roles.*.field').exists().escape().trim(),
|
check('roles.*.field').exists().escape().trim(),
|
||||||
check('roles.*.comparator').exists(),
|
check('roles.*.comparator').exists(),
|
||||||
check('roles.*.value').exists(),
|
check('roles.*.value').exists(),
|
||||||
check('roles.*.index').exists().isNumeric().toInt(),
|
check('roles.*.index').exists().isNumeric().toInt(),
|
||||||
|
check('columns.*').exists().escape().trim(),
|
||||||
],
|
],
|
||||||
async handler(req, res) {
|
async handler(req, res) {
|
||||||
const { resource_id: resourceId } = req.params;
|
|
||||||
const validationErrors = validationResult(req);
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
if (!validationErrors.isEmpty()) {
|
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) {
|
if (!resource) {
|
||||||
return res.boom.notFound(null, {
|
return res.boom.notFound(null, {
|
||||||
@@ -121,38 +125,34 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const errorReasons = [];
|
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();
|
// The difference between the stored resource fields and submit fields keys.
|
||||||
const resourceFieldsKeys = resourceFields.map((f) => f.get('key'));
|
|
||||||
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
|
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
|
||||||
|
|
||||||
if (notFoundFields.length > 0) {
|
if (notFoundFields.length > 0) {
|
||||||
errorReasons.push({ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields });
|
errorReasons.push({ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields });
|
||||||
}
|
}
|
||||||
|
// The difference between the stored resource fields and the submit columns keys.
|
||||||
const notFoundColumns = difference(columns, resourceFieldsKeys);
|
const notFoundColumns = difference(form.columns, resourceFieldsKeys);
|
||||||
|
|
||||||
if (notFoundColumns.length > 0) {
|
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) {
|
if (errorReasons.length > 0) {
|
||||||
return res.boom.badRequest(null, { errors: errorReasons });
|
return res.boom.badRequest(null, { errors: errorReasons });
|
||||||
}
|
}
|
||||||
|
|
||||||
const view = await View.forge({
|
|
||||||
name: label,
|
|
||||||
predefined: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save view details.
|
// Save view details.
|
||||||
await view.save();
|
const view = await View.query().insert({
|
||||||
|
name: form.label,
|
||||||
// Save view columns.
|
predefined: false,
|
||||||
|
resource_id: resource.id,
|
||||||
|
});
|
||||||
|
|
||||||
// Save view roles.
|
// Save view roles.
|
||||||
|
|
||||||
|
|
||||||
@@ -160,7 +160,6 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
editView: {
|
editView: {
|
||||||
validation: [
|
validation: [
|
||||||
check('label').exists().escape().trim(),
|
check('label').exists().escape().trim(),
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ import Accounts from '@/http/controllers/Accounts';
|
|||||||
import AccountOpeningBalance from '@/http/controllers/AccountOpeningBalance';
|
import AccountOpeningBalance from '@/http/controllers/AccountOpeningBalance';
|
||||||
import Views from '@/http/controllers/Views';
|
import Views from '@/http/controllers/Views';
|
||||||
import CustomFields from '@/http/controllers/Fields';
|
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) => {
|
export default (app) => {
|
||||||
// app.use('/api/oauth2', OAuth2.router());
|
// app.use('/api/oauth2', OAuth2.router());
|
||||||
@@ -15,9 +27,21 @@ export default (app) => {
|
|||||||
app.use('/api/users', Users.router());
|
app.use('/api/users', Users.router());
|
||||||
app.use('/api/roles', Roles.router());
|
app.use('/api/roles', Roles.router());
|
||||||
app.use('/api/accounts', Accounts.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/views', Views.router());
|
||||||
app.use('/api/fields', CustomFields.router());
|
app.use('/api/fields', CustomFields.router());
|
||||||
app.use('/api/items', Items.router());
|
app.use('/api/items', Items.router());
|
||||||
app.use('/api/item_categories', ItemCategories.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);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
// 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);
|
// Auth.setAuthenticatedUser(req.user);
|
||||||
|
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
* 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() {
|
static get relationMappings() {
|
||||||
return this.belongsTo('AccountType', 'account_type_id');
|
const AccountType = require('@/models/AccountType');
|
||||||
},
|
const AccountBalance = require('@/models/AccountBalance');
|
||||||
|
const AccountTransaction = require('@/models/AccountTransaction');
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* Account model may has many balances accounts.
|
/**
|
||||||
*/
|
* Account model may belongs to account type.
|
||||||
balances() {
|
*/
|
||||||
return this.hasMany('AccountBalance', 'account_id');
|
type: {
|
||||||
},
|
relation: Model.BelongsToOneRelation,
|
||||||
}, {
|
modelClass: AccountType.default,
|
||||||
/**
|
join: {
|
||||||
* Cascade delete dependents.
|
from: 'accounts.accountTypeId',
|
||||||
*/
|
to: 'account_types.id',
|
||||||
dependents: ['balances'],
|
},
|
||||||
});
|
},
|
||||||
|
|
||||||
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';
|
import { Model } from 'objection';
|
||||||
|
import BaseModel from '@/models/Model';
|
||||||
const AccountBalance = bookshelf.Model.extend({
|
|
||||||
|
|
||||||
|
export default class AccountBalance extends BaseModel {
|
||||||
/**
|
/**
|
||||||
* Table name
|
* 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');
|
||||||
|
|
||||||
|
return {
|
||||||
account() {
|
account: {
|
||||||
return this.belongsTo('Account', 'account_id');
|
relation: Model.BelongsToOneRelation,
|
||||||
},
|
modelClass: Account.default,
|
||||||
});
|
join: {
|
||||||
|
from: 'account_balance.account_id',
|
||||||
export default bookshelf.model('AccountBalance', AccountBalance);
|
to: 'accounts.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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';
|
// import path from 'path';
|
||||||
|
import { Model } from 'objection';
|
||||||
const AccountType = bookshelf.Model.extend({
|
import BaseModel from '@/models/Model';
|
||||||
|
|
||||||
|
export default class AccountType extends BaseModel {
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
tableName: 'accounts',
|
static get tableName() {
|
||||||
|
return 'account_types';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp columns.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
hasTimestamps: false,
|
static get relationMappings() {
|
||||||
|
const Account = require('@/models/Account');
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* Account type may has many associated accounts.
|
/**
|
||||||
*/
|
* Account type may has many associated accounts.
|
||||||
accounts() {
|
*/
|
||||||
return this.hasMany('Account', 'account_type_id');
|
accounts: {
|
||||||
},
|
relation: Model.HasManyRelation,
|
||||||
});
|
modelClass: Account.default,
|
||||||
|
join: {
|
||||||
export default bookshelf.model('AccountType', AccountType);
|
from: 'account_types.id',
|
||||||
|
to: 'accounts.accountTypeId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import BaseModel from '@/models/Model';
|
||||||
|
|
||||||
|
export default class Budget extends BaseModel {
|
||||||
|
/**
|
||||||
|
* Table name
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'budget_entries';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-26
@@ -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
|
* 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.
|
* Item may belongs to cateogory model.
|
||||||
*/
|
*/
|
||||||
metadata() {
|
category: {
|
||||||
return this.hasMany('ItemMetadata', 'item_id');
|
relation: Model.BelongsToOneRelation,
|
||||||
},
|
modelBase: path.join(__dirname, 'ItemCategory'),
|
||||||
|
join: {
|
||||||
/**
|
from: 'items.categoryId',
|
||||||
* Item may belongs to the item category.
|
to: 'items_categories.id',
|
||||||
*/
|
},
|
||||||
category() {
|
},
|
||||||
return this.belongsTo('ItemCategory', 'category_id');
|
};
|
||||||
},
|
}
|
||||||
}, {
|
}
|
||||||
/**
|
|
||||||
* Cascade delete dependents.
|
|
||||||
*/
|
|
||||||
dependents: ['ItemMetadata'],
|
|
||||||
});
|
|
||||||
|
|
||||||
export default bookshelf.model('Item', Item);
|
|
||||||
|
|||||||
@@ -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',
|
static get relationMappings() {
|
||||||
|
return {
|
||||||
/**
|
/**
|
||||||
* Timestamp columns.
|
* Item category may has many items.
|
||||||
*/
|
*/
|
||||||
hasTimestamps: ['created_at', 'updated_at'],
|
items: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
/**
|
modelBase: path.join(__dirname, 'Item'),
|
||||||
* Item category may has many items.
|
join: {
|
||||||
*/
|
from: 'items_categories.item_id',
|
||||||
items() {
|
to: 'items.id',
|
||||||
return this.hasMany('Item', 'category_id');
|
},
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
}
|
||||||
export default bookshelf.model('ItemCategory', ItemCategory);
|
}
|
||||||
|
|||||||
@@ -1,23 +1,38 @@
|
|||||||
import bookshelf from './bookshelf';
|
import path from 'path';
|
||||||
|
import { Model } from 'objection';
|
||||||
const ItemMetadata = bookshelf.Model.extend({
|
import BaseModel from '@/models/Model';
|
||||||
|
|
||||||
|
export default class ItemMetadata extends BaseModel {
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
tableName: 'items_metadata',
|
static get tableName() {
|
||||||
|
return 'items_metadata';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp columns.
|
* Timestamp columns.
|
||||||
*/
|
*/
|
||||||
hasTimestamps: ['created_at', 'updated_at'],
|
static get hasTimestamps() {
|
||||||
|
return ['created_at', 'updated_at'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Item category may has many items.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
items() {
|
static get relationMappings() {
|
||||||
return this.belongsTo('Item', 'item_id');
|
return {
|
||||||
},
|
/**
|
||||||
});
|
* Item category may has many items.
|
||||||
|
*/
|
||||||
export default bookshelf.model('ItemMetadata', ItemMetadata);
|
items: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelBase: path.join(__dirname, 'Item'),
|
||||||
|
join: {
|
||||||
|
from: 'items_metadata.item_id',
|
||||||
|
to: 'items.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
setExtraColumns(columns) {
|
||||||
this.extraColumns = columns;
|
this.extraColumns = columns;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the cache namespace.
|
|
||||||
*/
|
|
||||||
getCacheNamespace() {
|
|
||||||
const { metadataCacheNamespace: cacheName } = this;
|
|
||||||
return typeof cacheName === 'function'
|
|
||||||
? cacheName() : cacheName;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata database query.
|
* Metadata database query.
|
||||||
* @param {Object} query -
|
* @param {Object} query -
|
||||||
@@ -126,7 +117,7 @@ export default {
|
|||||||
metadata.markAsDeleted = true;
|
metadata.markAsDeleted = true;
|
||||||
}
|
}
|
||||||
this.shouldReload = true;
|
this.shouldReload = true;
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove all meta data of the given group.
|
* Remove all meta data of the given group.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
import Model from '@/models/Model';
|
||||||
|
|
||||||
const PasswordResets = bookshelf.Model.extend({
|
|
||||||
|
|
||||||
|
export default class PasswordResets extends Model {
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
tableName: 'password_resets',
|
static get tableName() {
|
||||||
|
return 'password_resets';
|
||||||
/**
|
}
|
||||||
* Timestamp columns.
|
}
|
||||||
*/
|
|
||||||
hasTimestamps: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default bookshelf.model('PasswordResets', PasswordResets);
|
|
||||||
|
|||||||
@@ -1,25 +1,41 @@
|
|||||||
import bookshelf from './bookshelf';
|
import { Model } from 'objection';
|
||||||
|
import path from 'path';
|
||||||
const Permission = bookshelf.Model.extend({
|
import BaseModel from '@/models/Model';
|
||||||
|
|
||||||
|
export default class Permission extends BaseModel {
|
||||||
/**
|
/**
|
||||||
* Table name of Role model.
|
* Table name of Role model.
|
||||||
* @type {String}
|
* @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() {
|
// resource: {
|
||||||
return this.belongsTo('Role', 'role_id');
|
// relation: Model.BelongsToOneRelation,
|
||||||
},
|
// modelBase: path.join(__dirname, 'Resource'),
|
||||||
|
// join: {
|
||||||
resource() {
|
// from: 'permissions.',
|
||||||
return this.belongsTo('Resource', 'resource_id');
|
// to: '',
|
||||||
},
|
// }
|
||||||
});
|
// }
|
||||||
|
};
|
||||||
export default bookshelf.model('Permission', Permission);
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
tableName: 'resources',
|
static get tableName() {
|
||||||
|
return 'resources';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp columns.
|
* Timestamp columns.
|
||||||
*/
|
*/
|
||||||
hasTimestamps: false,
|
static get hasTimestamps() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resource model may has many views.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
views() {
|
static get relationMappings() {
|
||||||
return this.hasMany('View', 'resource_id');
|
const View = require('@/models/View');
|
||||||
},
|
const ResourceField = require('@/models/ResourceField');
|
||||||
|
const Permission = require('@/models/Permission');
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* Resource model may has many fields.
|
/**
|
||||||
*/
|
* Resource model may has many views.
|
||||||
fields() {
|
*/
|
||||||
return this.hasMany('ResourceField', 'resource_id');
|
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 { 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.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
tableName: 'resource_fields',
|
static get tableName() {
|
||||||
|
return 'resource_fields';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp columns.
|
* Timestamp columns.
|
||||||
*/
|
*/
|
||||||
hasTimestamps: false,
|
static get hasTimestamps() {
|
||||||
|
return false;
|
||||||
virtuals: {
|
}
|
||||||
/**
|
|
||||||
* Resource field key.
|
|
||||||
*/
|
|
||||||
key() {
|
|
||||||
return snakeCase(this.attributes.label_name);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resource field may belongs to resource model.
|
* Virtual attributes.
|
||||||
*/
|
*/
|
||||||
resource() {
|
static get virtualAttributes() {
|
||||||
return this.belongsTo('Resource', 'resource_id');
|
return ['key'];
|
||||||
},
|
}
|
||||||
}, {
|
|
||||||
/**
|
|
||||||
* JSON Columns.
|
|
||||||
*/
|
|
||||||
jsonColumns: ['options'],
|
|
||||||
});
|
|
||||||
|
|
||||||
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+63
-23
@@ -1,38 +1,78 @@
|
|||||||
import bookshelf from './bookshelf';
|
import { Model } from 'objection';
|
||||||
|
import BaseModel from '@/models/Model';
|
||||||
const Role = bookshelf.Model.extend({
|
|
||||||
|
|
||||||
|
export default class Role extends BaseModel {
|
||||||
/**
|
/**
|
||||||
* Table name of Role model.
|
* Table name of Role model.
|
||||||
* @type {String}
|
* @type {String}
|
||||||
*/
|
*/
|
||||||
tableName: 'roles',
|
static get tableName() {
|
||||||
|
return 'roles';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp columns.
|
* Timestamp columns.
|
||||||
*/
|
*/
|
||||||
hasTimestamps: false,
|
static get hasTimestamps() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Role may has many permissions.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
permissions() {
|
static get relationMappings() {
|
||||||
return this.belongsToMany('Permission', 'role_has_permissions', 'role_id', 'permission_id');
|
const Permission = require('@/models/Permission');
|
||||||
},
|
const Resource = require('@/models/Resource');
|
||||||
|
const User = require('@/models/User');
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* Role may has many resources.
|
/**
|
||||||
*/
|
* Role may has many permissions.
|
||||||
resources() {
|
*/
|
||||||
return this.belongsToMany('Resource', 'role_has_permissions', 'role_id', 'resource_id');
|
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.
|
* Role may has many resources.
|
||||||
*/
|
*/
|
||||||
users() {
|
resources: {
|
||||||
return this.belongsToMany('User', 'user_has_roles');
|
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 BaseModel from '@/models/Model';
|
||||||
import Metable from './Metable';
|
|
||||||
import Auth from './Auth';
|
import Auth from './Auth';
|
||||||
|
|
||||||
const Setting = bookshelf.Model.extend({
|
export default class Setting extends BaseModel {
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
tableName: 'settings',
|
static get tableName() {
|
||||||
|
return 'settings';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp columns.
|
* Timestamp columns.
|
||||||
*/
|
*/
|
||||||
hasTimestamps: false,
|
static get hasTimestamps() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extra metadata query to query with the current authenticate user.
|
* Extra metadata query to query with the current authenticate user.
|
||||||
* @param {Object} query
|
* @param {Object} query
|
||||||
*/
|
*/
|
||||||
extraMetadataQuery(query) {
|
static extraMetadataQuery(query) {
|
||||||
if (Auth.isLogged()) {
|
if (Auth.isLogged()) {
|
||||||
query.where('user_id', Auth.userId());
|
query.where('user_id', Auth.userId());
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}, {
|
}
|
||||||
/**
|
|
||||||
* Table name
|
|
||||||
*/
|
|
||||||
tableName: 'settings',
|
|
||||||
|
|
||||||
...Metable,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default bookshelf.model('Setting', Setting);
|
|
||||||
|
|||||||
+29
-22
@@ -1,23 +1,39 @@
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import bookshelf from './bookshelf';
|
import { Model } from 'objection';
|
||||||
import PermissionsService from '@/services/PermissionsService';
|
import BaseModel from '@/models/Model';
|
||||||
|
// import PermissionsService from '@/services/PermissionsService';
|
||||||
|
|
||||||
const User = bookshelf.Model.extend({
|
export default class User extends BaseModel {
|
||||||
...PermissionsService,
|
// ...PermissionsService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* 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() {
|
return {
|
||||||
this.initializeCache();
|
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.
|
* Verify the password of the user.
|
||||||
@@ -25,15 +41,6 @@ const User = bookshelf.Model.extend({
|
|||||||
* @return {Boolean}
|
* @return {Boolean}
|
||||||
*/
|
*/
|
||||||
verifyPassword(password) {
|
verifyPassword(password) {
|
||||||
return bcrypt.compareSync(password, this.get('password'));
|
return bcrypt.compareSync(password, this.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);
|
|
||||||
|
|||||||
+56
-27
@@ -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.
|
* 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.
|
* View model may has many columns.
|
||||||
*/
|
*/
|
||||||
resource() {
|
// columns: {
|
||||||
return this.belongsTo('Resource', 'resource_id');
|
// 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.
|
* View model may has many view roles.
|
||||||
*/
|
*/
|
||||||
columns() {
|
viewRoles: {
|
||||||
return this.belongsToMany('ResourceField', 'view_has_columns', 'view_id', 'field_id');
|
relation: Model.HasManyRelation,
|
||||||
},
|
modelBase: path.join(__dirname, 'ViewRole'),
|
||||||
|
join: {
|
||||||
|
from: 'views.id',
|
||||||
|
to: 'view_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
// columns() {
|
||||||
* View model may has many view roles.
|
// return this.belongsToMany('ResourceField', 'view_has_columns', 'view_id', 'field_id');
|
||||||
*/
|
// },
|
||||||
viewRoles() {
|
|
||||||
return this.hasMany('ViewRole', 'view_id');
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
dependents: ['columns', 'viewRoles'],
|
|
||||||
});
|
|
||||||
|
|
||||||
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.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
tableName: 'view_columns',
|
static get tableName() {
|
||||||
|
return 'view_columns';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp columns.
|
* Timestamp columns.
|
||||||
*/
|
*/
|
||||||
hasTimestamps: false,
|
static get hasTimestamps() {
|
||||||
|
return false;
|
||||||
view() {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
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.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
tableName: 'view_roles',
|
static get tableName() {
|
||||||
|
return 'view_roles';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp columns.
|
* Timestamp columns.
|
||||||
*/
|
*/
|
||||||
hasTimestamps: false,
|
static get hasTimestamps() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View role model may belongs to view model.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
view() {
|
static get relationMappings() {
|
||||||
return this.belongsTo('View', 'view_id');
|
return {
|
||||||
},
|
/**
|
||||||
});
|
* View role model may belongs to view model.
|
||||||
|
*/
|
||||||
export default bookshelf.model('ViewRole', ViewRole);
|
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;
|
|
||||||
@@ -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,0 +1,10 @@
|
|||||||
|
|
||||||
|
export default class JournalEntry {
|
||||||
|
constructor(entry) {
|
||||||
|
const defaults = {
|
||||||
|
credit: 0,
|
||||||
|
debit: 0,
|
||||||
|
};
|
||||||
|
this.entry = { ...defaults, ...entry };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import Moment from 'moment';
|
||||||
|
import { extendMoment } from 'moment-range';
|
||||||
|
|
||||||
|
const moment = extendMoment(Moment);
|
||||||
|
|
||||||
|
export default moment;
|
||||||
+3
-1
@@ -3,12 +3,14 @@ import { difference } from 'lodash';
|
|||||||
import Role from '@/models/Role';
|
import Role from '@/models/Role';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
cacheKey: 'ratteb.cache,',
|
cacheKey: 'ratteb.cache,',
|
||||||
cacheExpirationTime: null,
|
cacheExpirationTime: null,
|
||||||
permissions: [],
|
permissions: [],
|
||||||
cache: null,
|
cache: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the cache.
|
||||||
|
*/
|
||||||
initializeCache() {
|
initializeCache() {
|
||||||
if (!this.cache) {
|
if (!this.cache) {
|
||||||
this.cache = new cache.Cache();
|
this.cache = new cache.Cache();
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { expect } from '~/testInit';
|
||||||
|
import NestedSet from '@/collection/NestedSet';
|
||||||
|
|
||||||
|
describe('NestedSet', () => {
|
||||||
|
it('Should link parent and children nodes.', () => {
|
||||||
|
const flattenArray = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
parent_id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
parent_id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
parent_id: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const collection = new NestedSet(flattenArray);
|
||||||
|
|
||||||
|
expect(collection[0].id).equals(1);
|
||||||
|
expect(collection[0].children[0].id).equals(2);
|
||||||
|
expect(collection[0].children[1].id).equals(3);
|
||||||
|
expect(collection[0].children[1].children[0].id).equals(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import Option from '@/models/Option';
|
||||||
|
import MetadataCollection from '@/lib/Metable/MetableCollection';
|
||||||
|
import { create, expect } from '~/testInit';
|
||||||
|
|
||||||
|
describe('MetableCollection', () => {
|
||||||
|
describe('findMeta', () => {
|
||||||
|
it('Should retrieve the found meta object.', async () => {
|
||||||
|
const option = await create('option');
|
||||||
|
const options = await Option.query();
|
||||||
|
const metadataCollection = MetadataCollection.from(options);
|
||||||
|
|
||||||
|
const foundMeta = metadataCollection.findMeta(option.key);
|
||||||
|
expect(foundMeta).to.be.an('object');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('allMetadata', () => {
|
||||||
|
it('Should retrieve all exists metadata entries.', async () => {
|
||||||
|
const option = await create('option');
|
||||||
|
const options = await Option.query();
|
||||||
|
const metadataCollection = MetadataCollection.from(options);
|
||||||
|
|
||||||
|
const foundMetadata = metadataCollection.allMetadata();
|
||||||
|
|
||||||
|
expect(foundMetadata.length).equals(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMeta', () => {
|
||||||
|
it('Should retrieve the found meta value.', async () => {
|
||||||
|
const option = await create('option');
|
||||||
|
const options = await Option.query();
|
||||||
|
const metadataCollection = MetadataCollection.from(options);
|
||||||
|
|
||||||
|
const foundMeta = metadataCollection.getMeta(option.key);
|
||||||
|
|
||||||
|
expect(foundMeta).equals(option.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should retrieve the default meta value in case the meta key was not exist.', async () => {
|
||||||
|
const option = await create('option');
|
||||||
|
const options = await Option.query();
|
||||||
|
const metadataCollection = MetadataCollection.from(options);
|
||||||
|
|
||||||
|
const foundMeta = metadataCollection.getMeta('not-found', true);
|
||||||
|
|
||||||
|
expect(foundMeta).equals(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setMeta', () => {
|
||||||
|
it('Should sets the meta value to the stack.', async () => {
|
||||||
|
const metadataCollection = new MetadataCollection();
|
||||||
|
metadataCollection.setMeta('key', 'value');
|
||||||
|
|
||||||
|
expect(metadataCollection.metadata.length).equals(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeAllMeta()', () => {
|
||||||
|
it('Should remove all metadata from the stack.', async () => {
|
||||||
|
const metadataCollection = new MetadataCollection();
|
||||||
|
metadataCollection.setMeta('key', 'value');
|
||||||
|
metadataCollection.setMeta('key2', 'value2');
|
||||||
|
|
||||||
|
metadataCollection.removeAllMeta();
|
||||||
|
|
||||||
|
expect(metadataCollection.metadata.length).equals(2);
|
||||||
|
expect(metadataCollection.allMetadata().length).equals(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('saveMeta', () => {
|
||||||
|
it('Should save inserted new metadata.', async () => {
|
||||||
|
const metadataCollection = new MetadataCollection();
|
||||||
|
metadataCollection.setMeta('key', 'value');
|
||||||
|
metadataCollection.setModel(Option);
|
||||||
|
|
||||||
|
await metadataCollection.saveMeta();
|
||||||
|
|
||||||
|
const storedMetadata = await Option.query();
|
||||||
|
expect(storedMetadata.length).equals(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should save updated the exist metadata.', async () => {
|
||||||
|
const option = await create('option');
|
||||||
|
const metadataCollection = new MetadataCollection();
|
||||||
|
metadataCollection.setMeta(option.key, 'value');
|
||||||
|
metadataCollection.setModel(Option);
|
||||||
|
|
||||||
|
await metadataCollection.saveMeta();
|
||||||
|
|
||||||
|
const storedMetadata = Option.query().where('key', option.key).first();
|
||||||
|
expect(storedMetadata.value).equals('value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should delete the removed metadata from storage.', async () => {
|
||||||
|
const option = await create('option');
|
||||||
|
|
||||||
|
const options = await Option.query();
|
||||||
|
const metadataCollection = MetadataCollection.from(options);
|
||||||
|
metadataCollection.setModel(Option);
|
||||||
|
metadataCollection.removeMeta(option.key);
|
||||||
|
|
||||||
|
expect(metadataCollection.metadata.length).equals(1);
|
||||||
|
await metadataCollection.saveMeta();
|
||||||
|
|
||||||
|
const storedMetadata = await Option.query();
|
||||||
|
expect(storedMetadata.length).equals(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
|
describe('MetableModel', () => {
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { create, expect } from '~/testInit';
|
import { create, expect } from '~/testInit';
|
||||||
import Account from '@/models/Account';
|
import Account from '@/models/Account';
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
import AccountType from '@/models/AccountType';
|
import AccountType from '@/models/AccountType';
|
||||||
|
|
||||||
describe('Model: Account', () => {
|
describe('Model: Account', () => {
|
||||||
@@ -8,9 +7,32 @@ describe('Model: Account', () => {
|
|||||||
const accountType = await create('account_type');
|
const accountType = await create('account_type');
|
||||||
const account = await create('account', { account_type_id: accountType.id });
|
const account = await create('account', { account_type_id: accountType.id });
|
||||||
|
|
||||||
const accountModel = await Account.where('id', account.id).fetch();
|
const accountModel = await Account.query()
|
||||||
const accountTypeModel = await accountModel.type().fetch();
|
.where('id', account.id)
|
||||||
|
.withGraphFetched('type')
|
||||||
|
.first();
|
||||||
|
|
||||||
expect(accountTypeModel.attributes.id).equals(account.id);
|
expect(accountModel.type.id).equals(accountType.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should account model has one balance model that associated to the account model.', async () => {
|
||||||
|
const accountBalance = await create('account_balance');
|
||||||
|
|
||||||
|
const accountModel = await Account.query()
|
||||||
|
.where('id', accountBalance.accountId)
|
||||||
|
.withGraphFetched('balance')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
expect(accountModel.balance.amount).equals(accountBalance.amount);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should account model has many transactions models that associated to the account model.', async () => {
|
||||||
|
const account = await create('account');
|
||||||
|
const accountTransaction = await create('account_transaction', { account_id: account.id });
|
||||||
|
|
||||||
|
const accountModel = await Account.query().where('id', account.id).first();
|
||||||
|
const transactionsModels = await accountModel.$relatedQuery('transactions');
|
||||||
|
|
||||||
|
expect(transactionsModels.length).equals(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user