mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
WIP server side.
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult, oneOf } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import knex from 'knex';
|
||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||
import Account from '@/models/Account';
|
||||
import '@/models/AccountBalance';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
|
||||
export default {
|
||||
/**
|
||||
@@ -36,7 +36,6 @@ export default {
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
// const defaultCurrency = 'USD';
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
@@ -45,16 +44,13 @@ export default {
|
||||
}
|
||||
|
||||
const { accounts } = req.body;
|
||||
|
||||
const accountsIds = accounts.map((account) => account.id);
|
||||
const accountsCollection = await Account.query((query) => {
|
||||
query.select(['id']);
|
||||
query.whereIn('id', accountsIds);
|
||||
}).fetchAll({
|
||||
withRelated: ['balances'],
|
||||
});
|
||||
const accountsCollection = await Account.query()
|
||||
.select(['id'])
|
||||
.whereIn('id', accountsIds);
|
||||
|
||||
const accountsStoredIds = accountsCollection.map((account) => account.attributes.id);
|
||||
// Get the stored accounts Ids and difference with submit accounts.
|
||||
const accountsStoredIds = accountsCollection.map((account) => account.id);
|
||||
const notFoundAccountsIds = difference(accountsIds, accountsStoredIds);
|
||||
const errorReasons = [];
|
||||
|
||||
@@ -62,34 +58,35 @@ export default {
|
||||
const ids = notFoundAccountsIds.map((a) => parseInt(a, 10));
|
||||
errorReasons.push({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids });
|
||||
}
|
||||
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badData(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
const storedAccountsBalances = accountsCollection.related('balances');
|
||||
const sharedJournalDetails = new JournalEntry({
|
||||
referenceType: 'OpeningBalance',
|
||||
referenceId: 1,
|
||||
});
|
||||
const journalEntries = new JournalPoster(sharedJournalDetails);
|
||||
|
||||
const submitBalancesMap = new Map(accounts.map((account) => [account, account.id]));
|
||||
const storedBalancesMap = new Map(storedAccountsBalances.map((balance) => [
|
||||
balance.attributes, balance.attributes.id,
|
||||
]));
|
||||
accounts.forEach((account) => {
|
||||
const entry = new JournalEntry({
|
||||
account: account.id,
|
||||
accountNormal: account.type.normal,
|
||||
});
|
||||
|
||||
// const updatedStoredBalanced = [];
|
||||
const notStoredBalances = [];
|
||||
|
||||
accountsIds.forEach((id) => {
|
||||
if (!storedBalancesMap.get(id)) {
|
||||
notStoredBalances.push(id);
|
||||
if (account.credit) {
|
||||
entry.credit = account.credit;
|
||||
journalEntries.credit(entry);
|
||||
} else if (account.debit) {
|
||||
entry.debit = account.debit;
|
||||
journalEntries.debit(entry);
|
||||
}
|
||||
});
|
||||
|
||||
await knex('accounts_balances').insert([
|
||||
...notStoredBalances.map((id) => {
|
||||
const account = submitBalancesMap.get(id);
|
||||
return { ...account };
|
||||
}),
|
||||
await Promise.all([
|
||||
journalEntries.saveEntries(),
|
||||
journalEntries.saveBalance(),
|
||||
]);
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
213
server/src/http/controllers/Accounting.js
Normal file
213
server/src/http/controllers/Accounting.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import { check, query, validationResult } from 'express-validator';
|
||||
import express from 'express';
|
||||
import { difference } from 'lodash';
|
||||
import Account from '@/models/Account';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import ManualJournal from '@/models/JournalEntry';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
router.use(JWTAuth);
|
||||
|
||||
router.post('/make-journal-entries',
|
||||
this.makeJournalEntries.validation,
|
||||
asyncMiddleware(this.makeJournalEntries.handler));
|
||||
|
||||
router.post('/recurring-journal-entries',
|
||||
this.recurringJournalEntries.validation,
|
||||
asyncMiddleware(this.recurringJournalEntries.handler));
|
||||
|
||||
router.post('quick-journal-entries',
|
||||
this.quickJournalEntries.validation,
|
||||
asyncMiddleware(this.quickJournalEntries.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Make journal entrires.
|
||||
*/
|
||||
makeJournalEntries: {
|
||||
validation: [
|
||||
check('date').isISO8601(),
|
||||
check('reference').exists(),
|
||||
check('entries').isArray({ min: 1 }),
|
||||
check('entries.*.credit').isNumeric().toInt(),
|
||||
check('entries.*.debit').isNumeric().toInt(),
|
||||
check('entries.*.account_id').isNumeric().toInt(),
|
||||
check('entries.*.note').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
const errorReasons = [];
|
||||
let totalCredit = 0;
|
||||
let totalDebit = 0;
|
||||
|
||||
form.entries.forEach((entry) => {
|
||||
if (entry.credit > 0) {
|
||||
totalCredit += entry.credit;
|
||||
}
|
||||
if (entry.debit > 0) {
|
||||
totalDebit += entry.debit;
|
||||
}
|
||||
});
|
||||
if (totalCredit <= 0 || totalDebit <= 0) {
|
||||
errorReasons.push({
|
||||
type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
|
||||
code: 400,
|
||||
});
|
||||
}
|
||||
if (totalCredit !== totalDebit) {
|
||||
errorReasons.push({ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 100 });
|
||||
}
|
||||
const accountsIds = form.entries.map((entry) => entry.account_id);
|
||||
const accounts = await Account.query().whereIn('id', accountsIds)
|
||||
.withGraphFetched('type');
|
||||
|
||||
const storedAccountsIds = accounts.map((account) => account.id);
|
||||
|
||||
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
||||
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||
}
|
||||
|
||||
const journalReference = await ManualJournal.query().where('reference', form.reference);
|
||||
|
||||
if (journalReference.length > 0) {
|
||||
errorReasons.push({ type: 'REFERENCE.ALREADY.EXISTS', code: 300 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const journalPoster = new JournalPoster();
|
||||
|
||||
form.entries.forEach((entry) => {
|
||||
const account = accounts.find((a) => a.id === entry.account_id);
|
||||
|
||||
const jouranlEntry = new JournalEntry({
|
||||
debit: entry.debit,
|
||||
credit: entry.credit,
|
||||
account: account.id,
|
||||
accountNormal: account.type.normal,
|
||||
note: entry.note,
|
||||
});
|
||||
if (entry.debit) {
|
||||
journalPoster.debit(jouranlEntry);
|
||||
} else {
|
||||
journalPoster.credit(jouranlEntry);
|
||||
}
|
||||
});
|
||||
|
||||
// Saves the journal entries and accounts balance changes.
|
||||
await Promise.all([
|
||||
journalPoster.saveEntries(),
|
||||
journalPoster.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves recurring journal entries template.
|
||||
*/
|
||||
recurringJournalEntries: {
|
||||
validation: [
|
||||
check('template_name').exists(),
|
||||
check('recurrence').exists(),
|
||||
check('active').optional().isBoolean().toBoolean(),
|
||||
check('entries').isArray({ min: 1 }),
|
||||
check('entries.*.credit').isNumeric().toInt(),
|
||||
check('entries.*.debit').isNumeric().toInt(),
|
||||
check('entries.*.account_id').isNumeric().toInt(),
|
||||
check('entries.*.note').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
recurringJournalsList: {
|
||||
validation: [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('template_name').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
quickJournalEntries: {
|
||||
validation: [
|
||||
check('date').exists().isISO8601(),
|
||||
check('amount').exists().isNumeric().toFloat(),
|
||||
check('credit_account_id').exists().isNumeric().toInt(),
|
||||
check('debit_account_id').exists().isNumeric().toInt(),
|
||||
check('transaction_type').exists(),
|
||||
check('note').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const errorReasons = [];
|
||||
const form = { ...req.body };
|
||||
|
||||
const foundAccounts = await Account.query()
|
||||
.where('id', form.credit_account_id)
|
||||
.orWhere('id', form.debit_account_id);
|
||||
|
||||
const creditAccount = foundAccounts.find((a) => a.id === form.credit_account_id);
|
||||
const debitAccount = foundAccounts.find((a) => a.id === form.debit_account_id);
|
||||
|
||||
if (!creditAccount) {
|
||||
errorReasons.push({ type: 'CREDIT_ACCOUNT.NOT.EXIST', code: 100 });
|
||||
}
|
||||
if (!debitAccount) {
|
||||
errorReasons.push({ type: 'DEBIT_ACCOUNT.NOT.EXIST', code: 200 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
|
||||
// const journalPoster = new JournalPoster();
|
||||
// const journalCredit = new JournalEntry({
|
||||
// debit:
|
||||
// account: debitAccount.id,
|
||||
// referenceId:
|
||||
// })
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,10 +1,13 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult, param } from 'express-validator';
|
||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||
import { check, validationResult, param, query } from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Account from '@/models/Account';
|
||||
// import AccountBalance from '@/models/AccountBalance';
|
||||
import AccountType from '@/models/AccountType';
|
||||
// import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import AccountBalance from '@/models/AccountBalance';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import NestedSet from '../../collection/NestedSet';
|
||||
|
||||
export default {
|
||||
/**
|
||||
@@ -13,7 +16,7 @@ export default {
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
// router.use(JWTAuth);
|
||||
router.use(JWTAuth);
|
||||
router.post('/',
|
||||
this.newAccount.validation,
|
||||
asyncMiddleware(this.newAccount.handler));
|
||||
@@ -23,12 +26,33 @@ export default {
|
||||
asyncMiddleware(this.editAccount.handler));
|
||||
|
||||
router.get('/:id',
|
||||
this.getAccount.validation,
|
||||
asyncMiddleware(this.getAccount.handler));
|
||||
|
||||
router.get('/',
|
||||
this.getAccountsList.validation,
|
||||
asyncMiddleware(this.getAccountsList.handler));
|
||||
|
||||
router.delete('/:id',
|
||||
this.deleteAccount.validation,
|
||||
asyncMiddleware(this.deleteAccount.handler));
|
||||
|
||||
router.post('/:id/active',
|
||||
this.activeAccount.validation,
|
||||
asyncMiddleware(this.activeAccount.handler));
|
||||
|
||||
router.post('/:id/inactive',
|
||||
this.inactiveAccount.validation,
|
||||
asyncMiddleware(this.inactiveAccount.handler));
|
||||
|
||||
router.post('/:id/recalculate-balance',
|
||||
this.recalcualteBalanace.validation,
|
||||
asyncMiddleware(this.recalcualteBalanace.handler));
|
||||
|
||||
router.post('/:id/transfer_account/:toAccount',
|
||||
this.transferToAnotherAccount.validation,
|
||||
asyncMiddleware(this.transferToAnotherAccount.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
@@ -37,10 +61,10 @@ export default {
|
||||
*/
|
||||
newAccount: {
|
||||
validation: [
|
||||
check('name').isLength({ min: 3 }).trim().escape(),
|
||||
check('code').isLength({ max: 10 }).trim().escape(),
|
||||
check('account_type_id').isNumeric().toInt(),
|
||||
check('description').trim().escape(),
|
||||
check('name').exists().isLength({ min: 3 }).trim().escape(),
|
||||
check('code').exists().isLength({ max: 10 }).trim().escape(),
|
||||
check('account_type_id').exists().isNumeric().toInt(),
|
||||
check('description').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -50,34 +74,31 @@ export default {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
|
||||
const { name, code, description } = req.body;
|
||||
const { account_type_id: typeId } = req.body;
|
||||
const foundAccountCodePromise = form.code
|
||||
? Account.query().where('code', form.code) : null;
|
||||
|
||||
const foundAccountCodePromise = code ? Account.where('code', code).fetch() : null;
|
||||
const foundAccountTypePromise = AccountType.where('id', typeId).fetch();
|
||||
const foundAccountTypePromise = AccountType.query()
|
||||
.findById(form.account_type_id);
|
||||
|
||||
const [foundAccountCode, foundAccountType] = await Promise.all([
|
||||
foundAccountCodePromise,
|
||||
foundAccountTypePromise,
|
||||
foundAccountCodePromise, foundAccountTypePromise,
|
||||
]);
|
||||
|
||||
if (!foundAccountCode && foundAccountCodePromise) {
|
||||
if (foundAccountCodePromise && foundAccountCode.length > 0) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (!foundAccountType) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 110 }],
|
||||
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 200 }],
|
||||
});
|
||||
}
|
||||
const account = Account.forge({
|
||||
name, code, account_type_id: typeId, description,
|
||||
});
|
||||
await Account.query().insert({ ...form });
|
||||
|
||||
await account.save();
|
||||
return res.status(200).send({ item: { ...account.attributes } });
|
||||
return res.status(200).send({ item: { } });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -86,11 +107,11 @@ export default {
|
||||
*/
|
||||
editAccount: {
|
||||
validation: [
|
||||
param('id').toInt(),
|
||||
check('name').isLength({ min: 3 }).trim().escape(),
|
||||
check('code').isLength({ max: 10 }).trim().escape(),
|
||||
check('account_type_id').isNumeric().toInt(),
|
||||
check('description').trim().escape(),
|
||||
param('id').exists().toInt(),
|
||||
check('name').exists().isLength({ min: 3 }).trim().escape(),
|
||||
check('code').exists().isLength({ max: 10 }).trim().escape(),
|
||||
check('account_type_id').exists().isNumeric().toInt(),
|
||||
check('description').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
@@ -101,39 +122,33 @@ export default {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const account = await Account.where('id', id).fetch();
|
||||
const form = { ...req.body };
|
||||
const account = await Account.query().findById(id);
|
||||
|
||||
if (!account) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
const { name, code, description } = req.body;
|
||||
const { account_type_id: typeId } = req.body;
|
||||
const foundAccountCodePromise = (form.code && form.code !== account.code)
|
||||
? Account.query().where('code', form.code).whereNot('id', account.id) : null;
|
||||
|
||||
const foundAccountCodePromise = (code && code !== account.attributes.code)
|
||||
? Account.query({ where: { code }, whereNot: { id } }).fetch() : null;
|
||||
|
||||
const foundAccountTypePromise = (typeId !== account.attributes.account_type_id)
|
||||
? AccountType.where('id', typeId).fetch() : null;
|
||||
const foundAccountTypePromise = (form.account_type_id !== account.account_type_id)
|
||||
? AccountType.query().where('id', form.account_type_id) : null;
|
||||
|
||||
const [foundAccountCode, foundAccountType] = await Promise.all([
|
||||
foundAccountCodePromise, foundAccountTypePromise,
|
||||
]);
|
||||
|
||||
if (!foundAccountCode && foundAccountCodePromise) {
|
||||
if (foundAccountCode.length > 0 && foundAccountCodePromise) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (!foundAccountType && foundAccountTypePromise) {
|
||||
if (foundAccountType.length <= 0 && foundAccountTypePromise) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 110 }],
|
||||
});
|
||||
}
|
||||
await account.patch({ ...form });
|
||||
|
||||
await account.save({
|
||||
name, code, account_type_id: typeId, description,
|
||||
});
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
@@ -142,7 +157,7 @@ export default {
|
||||
* Get details of the given account.
|
||||
*/
|
||||
getAccount: {
|
||||
valiation: [
|
||||
validation: [
|
||||
param('id').toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
@@ -165,14 +180,163 @@ export default {
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const account = await Account.where('id', id).fetch();
|
||||
const account = await Account.query().findById(id);
|
||||
|
||||
if (!account) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
await account.destroy();
|
||||
const accountTransactions = await AccountTransaction.query()
|
||||
.where('account_id', account.id);
|
||||
|
||||
return res.status(200).send({ id: account.previous('id') });
|
||||
if (accountTransactions.length > 0) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 100 }],
|
||||
});
|
||||
}
|
||||
await Account.query().deleteById(account.id);
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve accounts list.
|
||||
*/
|
||||
getAccountsList: {
|
||||
validation: [
|
||||
query('account_types').optional().isArray(),
|
||||
query('account_types.*').optional().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const form = {
|
||||
account_types: [],
|
||||
...req.body,
|
||||
};
|
||||
const accounts = await Account.query()
|
||||
.modify('filterAccountTypes', form.account_types);
|
||||
|
||||
const accountsNestedSet = new NestedSet(accounts, {
|
||||
parentId: 'parentAccountId',
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
// ...accountsNestedSet.toArray(),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Re-calculates balance of the given account.
|
||||
*/
|
||||
recalcualteBalanace: {
|
||||
validation: [
|
||||
param('id').isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const account = await Account.findById(id);
|
||||
|
||||
if (!account) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
const accountTransactions = AccountTransaction.query()
|
||||
.where('account_id', account.id);
|
||||
|
||||
const journalEntries = new JournalPoster();
|
||||
journalEntries.loadFromCollection(accountTransactions);
|
||||
|
||||
// Delete the balance of the given account id.
|
||||
await AccountBalance.query().where('account_id', account.id).delete();
|
||||
|
||||
// Save calcualted account balance.
|
||||
await journalEntries.saveBalance();
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Active the given account.
|
||||
*/
|
||||
activeAccount: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const account = await Account.findById(id);
|
||||
|
||||
if (!account) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
await account.patch({ active: true });
|
||||
|
||||
return res.status(200).send({ id: account.id });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Inactive the given account.
|
||||
*/
|
||||
inactiveAccount: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const account = await Account.findById(id);
|
||||
|
||||
if (!account) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
await account.patch({ active: false });
|
||||
|
||||
return res.status(200).send({ id: account.id });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Transfer all journal entries of the given account to another account.
|
||||
*/
|
||||
transferToAnotherAccount: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
param('toAccount').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
// const { id, toAccount: toAccountId } = req.params;
|
||||
|
||||
// const [fromAccount, toAccount] = await Promise.all([
|
||||
// Account.query().findById(id),
|
||||
// Account.query().findById(toAccountId),
|
||||
// ]);
|
||||
|
||||
// const fromAccountTransactions = await AccountTransaction.query()
|
||||
// .where('account_id', fromAccount);
|
||||
|
||||
// return res.status(200).send();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import User from '@/models/User';
|
||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||
import User from '@/models/User';
|
||||
import PasswordReset from '@/models/PasswordReset';
|
||||
import mail from '@/services/mail';
|
||||
import { hashPassword } from '@/utils';
|
||||
@@ -52,10 +52,10 @@ export default {
|
||||
const { crediential, password } = req.body;
|
||||
const { JWT_SECRET_KEY } = process.env;
|
||||
|
||||
const user = await User.query({
|
||||
where: { email: crediential },
|
||||
orWhere: { phone_number: crediential },
|
||||
}).fetch();
|
||||
const user = await User.query()
|
||||
.where('email', crediential)
|
||||
.orWhere('phone_number', crediential)
|
||||
.first();
|
||||
|
||||
if (!user) {
|
||||
return res.boom.badRequest(null, {
|
||||
@@ -67,15 +67,15 @@ export default {
|
||||
errors: [{ type: 'INCORRECT_PASSWORD', code: 110 }],
|
||||
});
|
||||
}
|
||||
if (!user.attributes.active) {
|
||||
if (!user.active) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_INACTIVE', code: 120 }],
|
||||
});
|
||||
}
|
||||
user.save({ last_login_at: new Date() });
|
||||
// user.update({ last_login_at: new Date() });
|
||||
|
||||
const token = jwt.sign({
|
||||
email: user.attributes.email,
|
||||
email: user.email,
|
||||
_id: user.id,
|
||||
}, JWT_SECRET_KEY, {
|
||||
expiresIn: '1d',
|
||||
@@ -113,7 +113,6 @@ export default {
|
||||
email,
|
||||
token: '123123',
|
||||
});
|
||||
|
||||
await passwordReset.save();
|
||||
|
||||
const filePath = path.join(__dirname, '../../views/mail/ResetPassword.html');
|
||||
@@ -166,19 +165,18 @@ export default {
|
||||
const { token } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
const tokenModel = await PasswordReset.query((query) => {
|
||||
query.where({ token });
|
||||
query.where('created_at', '>=', Date.now() - 3600000);
|
||||
}).fetch();
|
||||
const tokenModel = await PasswordReset.query()
|
||||
.where('token', token)
|
||||
.where('created_at', '>=', Date.now() - 3600000)
|
||||
.first();
|
||||
|
||||
if (!tokenModel) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.where({
|
||||
email: tokenModel.attributes.email,
|
||||
email: tokenModel.email,
|
||||
});
|
||||
if (!user) {
|
||||
return res.boom.badRequest(null, {
|
||||
@@ -187,7 +185,7 @@ export default {
|
||||
}
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
user.set('password', hashedPassword);
|
||||
user.password = hashedPassword;
|
||||
await user.save();
|
||||
|
||||
await PasswordReset.where('email', user.get('email')).destroy({ require: false });
|
||||
|
||||
33
server/src/http/controllers/Banking.js
Normal file
33
server/src/http/controllers/Banking.js
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
import express from 'express';
|
||||
|
||||
export default {
|
||||
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
reconciliations: {
|
||||
validation: [
|
||||
|
||||
],
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
reconciliation: {
|
||||
validation: [
|
||||
body('from_date'),
|
||||
body('to_date'),
|
||||
body('closing_balance'),
|
||||
|
||||
],
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
10
server/src/http/controllers/Bills.js
Normal file
10
server/src/http/controllers/Bills.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import express from 'express';
|
||||
|
||||
export default {
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
return router;
|
||||
},
|
||||
};
|
||||
247
server/src/http/controllers/Budget.js
Normal file
247
server/src/http/controllers/Budget.js
Normal file
@@ -0,0 +1,247 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
check,
|
||||
query,
|
||||
param,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import { pick, difference, groupBy } from 'lodash';
|
||||
import asyncMiddleware from "@/http/middleware/asyncMiddleware";
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import Budget from '@/models/Budget';
|
||||
import BudgetEntry from '@/models/BudgetEntry';
|
||||
import Account from '@/models/Account';
|
||||
import moment from '@/services/Moment';
|
||||
import BudgetEntriesSet from '@/collection/BudgetEntriesSet';
|
||||
import AccountType from '@/models/AccountType';
|
||||
import NestedSet from '@/collection/NestedSet';
|
||||
import { dateRangeFormat } from '@/utils';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.use(JWTAuth);
|
||||
|
||||
router.post('/',
|
||||
this.newBudget.validation,
|
||||
asyncMiddleware(this.newBudget.handler));
|
||||
|
||||
router.get('/:id',
|
||||
this.getBudget.validation,
|
||||
asyncMiddleware(this.getBudget.handler));
|
||||
|
||||
router.get('/:id',
|
||||
this.deleteBudget.validation,
|
||||
asyncMiddleware(this.deleteBudget.handler));
|
||||
|
||||
router.get('/',
|
||||
this.listBudgets.validation,
|
||||
asyncMiddleware(this.listBudgets.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve budget details of the given id.
|
||||
*/
|
||||
getBudget: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
const budget = await Budget.query().findById(id);
|
||||
|
||||
if (!budget) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'budget.not.found', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
const accountTypes = await AccountType.query().where('balance_sheet', true);
|
||||
|
||||
const [budgetEntries, accounts] = await Promise.all([
|
||||
BudgetEntry.query().where('budget_id', budget.id),
|
||||
Account.query().whereIn('account_type_id', accountTypes.map((a) => a.id)),
|
||||
]);
|
||||
|
||||
const accountsNestedSet = new NestedSet(accounts);
|
||||
|
||||
const columns = [];
|
||||
const fromDate = moment(budget.year).startOf('year')
|
||||
.add(budget.rangeOffset, budget.rangeBy).toDate();
|
||||
|
||||
const toDate = moment(budget.year).endOf('year').toDate();
|
||||
|
||||
const dateRange = moment.range(fromDate, toDate);
|
||||
const dateRangeCollection = Array.from(dateRange.by(budget.rangeBy, {
|
||||
step: budget.rangeIncrement, excludeEnd: false, excludeStart: false,
|
||||
}));
|
||||
|
||||
dateRangeCollection.forEach((date) => {
|
||||
columns.push(date.format(dateRangeFormat(budget.rangeBy)));
|
||||
});
|
||||
const budgetEntriesSet = BudgetEntriesSet.from(budgetEntries, {
|
||||
orderSize: columns.length,
|
||||
});
|
||||
budgetEntriesSet.setZeroPlaceholder();
|
||||
budgetEntriesSet.calcTotalSummary();
|
||||
|
||||
return res.status(200).send({
|
||||
columns,
|
||||
accounts: budgetEntriesSet.toArray(),
|
||||
total: budgetEntriesSet.toArrayTotalSummary(),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the given budget.
|
||||
*/
|
||||
deleteBudget: {
|
||||
validation: [
|
||||
param('id').exists(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const budget = await Budget.query().findById(id);
|
||||
|
||||
if (!budget) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'budget.not.found', code: 100 }],
|
||||
});
|
||||
}
|
||||
await BudgetEntry.query().where('budget_id', budget.id).delete();
|
||||
await budget.delete();
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves the new budget.
|
||||
*/
|
||||
newBudget: {
|
||||
validation: [
|
||||
check('name').exists(),
|
||||
check('fiscal_year').exists(),
|
||||
check('period').exists().isIn(['year', 'month', 'quarter', 'half-year']),
|
||||
check('accounts_type').exists().isIn(['balance_sheet', 'profit_loss']),
|
||||
check('accounts').isArray(),
|
||||
check('accounts.*.account_id').exists().isNumeric().toInt(),
|
||||
check('accounts.*.entries').exists().isArray(),
|
||||
check('accounts.*.entries.*.amount').exists().isNumeric().toFloat(),
|
||||
check('accounts.*.entries.*.order').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const form = { ...req.body };
|
||||
const submitAccountsIds = form.accounts.map((a) => a.account_id);
|
||||
const storedAccounts = await Account.query().whereIn('id', submitAccountsIds);
|
||||
const storedAccountsIds = storedAccounts.map((a) => a.id);
|
||||
|
||||
const errorReasons = [];
|
||||
const notFoundAccountsIds = difference(submitAccountsIds, storedAccountsIds);
|
||||
|
||||
if (notFoundAccountsIds.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'ACCOUNT.NOT.FOUND', code: 200, accounts: notFoundAccountsIds,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// validation entries order.
|
||||
const budget = await Budget.query().insert({
|
||||
...pick(form, ['name', 'fiscal_year', 'period', 'accounts_type']),
|
||||
});
|
||||
|
||||
const promiseOpers = [];
|
||||
|
||||
form.accounts.forEach((account) => {
|
||||
account.entries.forEach((entry) => {
|
||||
const budgetEntry = BudgetEntry.query().insert({
|
||||
account_id: account.account_id,
|
||||
amount: entry.amount,
|
||||
order: entry.order,
|
||||
});
|
||||
promiseOpers.push(budgetEntry);
|
||||
});
|
||||
});
|
||||
await Promise.all(promiseOpers);
|
||||
|
||||
return res.status(200).send({ id: budget.id });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* List of paginated budgets items.
|
||||
*/
|
||||
listBudgets: {
|
||||
validation: [
|
||||
query('year').optional(),
|
||||
query('income_statement').optional().isBoolean().toBoolean(),
|
||||
query('profit_loss').optional().isBoolean().toBoolean(),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const filter = {
|
||||
page_size: 10,
|
||||
page: 1,
|
||||
...req.query,
|
||||
};
|
||||
const budgets = await Budget.query().runBefore((result, q) => {
|
||||
if (filter.profit_loss) {
|
||||
q.modify('filterByYear', filter.year);
|
||||
}
|
||||
if (filter.income_statement) {
|
||||
q.modify('filterByIncomeStatement', filter.income_statement);
|
||||
}
|
||||
if (filter.profit_loss) {
|
||||
q.modify('filterByProfitLoss', filter.profit_loss);
|
||||
}
|
||||
q.page(filter.page, filter.page_size);
|
||||
return result;
|
||||
});
|
||||
return res.status(200).send({
|
||||
items: budgets.items,
|
||||
})
|
||||
},
|
||||
},
|
||||
};
|
||||
122
server/src/http/controllers/BudgetReports.js
Normal file
122
server/src/http/controllers/BudgetReports.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import express from 'express';
|
||||
import { query, validationResult } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Budget from '@/models/Budget';
|
||||
import Account from '@/models/Account';
|
||||
import AccountType from '@/models/AccountType';
|
||||
import NestedSet from '@/collection/NestedSet';
|
||||
import BudgetEntry from '@/models/BudgetEntry';
|
||||
import { dateRangeFormat } from '@/utils';
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.use(jwtAuth);
|
||||
|
||||
router.get('/budget_verses_actual/:reportId',
|
||||
this.budgetVersesActual.validation,
|
||||
asyncMiddleware(this.budgetVersesActual.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
budgetVersesActual: {
|
||||
validation: [
|
||||
query('basis').optional().isIn(['cash', 'accural']),
|
||||
query('period').optional(),
|
||||
query('active_accounts').optional().toBoolean(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { reportId } = req.params;
|
||||
const form = { ...req.body };
|
||||
const errorReasons = [];
|
||||
|
||||
const budget = await Budget.query().findById(reportId);
|
||||
|
||||
if (!budget) {
|
||||
errorReasons.push({ type: 'BUDGET_NOT_FOUND', code: 100 });
|
||||
}
|
||||
const budgetEntries = await BudgetEntry.query().where('budget_id', budget.id);
|
||||
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const accountTypes = await AccountType.query()
|
||||
.where('balance_sheet', budget.accountTypes === 'balance_sheet')
|
||||
.where('income_sheet', budget.accountTypes === 'profit_losss');
|
||||
|
||||
const accounts = await Account.query().runBefore((result, q) => {
|
||||
const accountTypesIds = accountTypes.map((t) => t.id);
|
||||
|
||||
if (accountTypesIds.length > 0) {
|
||||
q.whereIn('account_type_id', accountTypesIds);
|
||||
}
|
||||
q.where('active', form.active_accounts === true);
|
||||
q.withGraphFetched('transactions');
|
||||
});
|
||||
|
||||
// const accountsNestedSet = NestedSet.from(accounts);
|
||||
|
||||
const fromDate = moment(budget.year).startOf('year')
|
||||
.add(budget.rangeOffset, budget.rangeBy).toDate();
|
||||
|
||||
const toDate = moment(budget.year).endOf('year').toDate();
|
||||
|
||||
const dateRange = moment.range(fromDate, toDate);
|
||||
const dateRangeCollection = Array.from(dateRange.by(budget.rangeBy, {
|
||||
step: budget.rangeIncrement, excludeEnd: false, excludeStart: false,
|
||||
}));
|
||||
|
||||
// // const accounts = {
|
||||
// // assets: [
|
||||
// // {
|
||||
// // name: '',
|
||||
// // code: '',
|
||||
// // totalEntries: [
|
||||
// // {
|
||||
|
||||
// // }
|
||||
// // ],
|
||||
// // children: [
|
||||
// // {
|
||||
// // name: '',
|
||||
// // code: '',
|
||||
// // entries: [
|
||||
// // {
|
||||
|
||||
// // }
|
||||
// // ]
|
||||
// // }
|
||||
// // ]
|
||||
// // }
|
||||
// // ]
|
||||
// // }
|
||||
|
||||
return res.status(200).send({
|
||||
columns: dateRangeCollection.map(d => d.format(dateRangeFormat(budget.rangeBy))),
|
||||
// accounts: {
|
||||
// asset: [],
|
||||
// liabilities: [],
|
||||
// equaity: [],
|
||||
|
||||
// income: [],
|
||||
// expenses: [],
|
||||
// }
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
17
server/src/http/controllers/CurrencyAdjustment.js
Normal file
17
server/src/http/controllers/CurrencyAdjustment.js
Normal file
@@ -0,0 +1,17 @@
|
||||
|
||||
export default {
|
||||
|
||||
|
||||
router() {
|
||||
|
||||
},
|
||||
|
||||
addExchangePrice: {
|
||||
validation: {
|
||||
|
||||
},
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
10
server/src/http/controllers/Customers.js
Normal file
10
server/src/http/controllers/Customers.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import express from 'express';
|
||||
|
||||
export default {
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
return router;
|
||||
},
|
||||
};
|
||||
367
server/src/http/controllers/Expenses.js
Normal file
367
server/src/http/controllers/Expenses.js
Normal file
@@ -0,0 +1,367 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
check,
|
||||
param,
|
||||
query,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { difference, chain } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Expense from '@/models/Expense';
|
||||
import Account from '@/models/Account';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
import View from '@/models/View';
|
||||
import Resource from '../../models/Resource';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
router.use(JWTAuth);
|
||||
|
||||
router.post('/',
|
||||
this.newExpense.validation,
|
||||
asyncMiddleware(this.newExpense.handler));
|
||||
|
||||
router.delete('/:id',
|
||||
this.deleteExpense.validation,
|
||||
asyncMiddleware(this.deleteExpense.handler));
|
||||
|
||||
router.post('/bulk',
|
||||
this.bulkAddExpenses.validation,
|
||||
asyncMiddleware(this.bulkAddExpenses.handler));
|
||||
|
||||
router.post('/:id',
|
||||
this.updateExpense.validation,
|
||||
asyncMiddleware(this.updateExpense.handler));
|
||||
|
||||
router.get('/',
|
||||
this.listExpenses.validation,
|
||||
asyncMiddleware(this.listExpenses.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves a new expense.
|
||||
*/
|
||||
newExpense: {
|
||||
validation: [
|
||||
check('date').optional().isISO8601(),
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('expense_account_id').exists().isNumeric().toInt(),
|
||||
check('description').optional(),
|
||||
check('amount').exists().isNumeric().toFloat(),
|
||||
check('currency_code').optional(),
|
||||
check('exchange_rate').optional().isNumeric().toFloat(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = {
|
||||
date: new Date(),
|
||||
...req.body,
|
||||
};
|
||||
// Convert the date to the general format.
|
||||
form.date = moment(form.date).format('YYYY-MM-DD');
|
||||
|
||||
const errorReasons = [];
|
||||
const paymentAccount = await Account.query()
|
||||
.findById(form.payment_account_id).first();
|
||||
|
||||
if (!paymentAccount) {
|
||||
errorReasons.push({ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 100 });
|
||||
}
|
||||
const expenseAccount = await Account.query()
|
||||
.findById(form.expense_account_id).first();
|
||||
|
||||
if (!expenseAccount) {
|
||||
errorReasons.push({ type: 'EXPENSE.ACCOUNT.NOT.FOUND', code: 200 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const expenseTransaction = await Expense.query().insert({ ...form });
|
||||
|
||||
const journalEntries = new JournalPoster();
|
||||
const creditEntry = new JournalEntry({
|
||||
credit: form.amount,
|
||||
referenceId: expenseTransaction.id,
|
||||
referenceType: Expense.referenceType,
|
||||
date: form.date,
|
||||
account: expenseAccount.id,
|
||||
accountNormal: 'debit',
|
||||
});
|
||||
const debitEntry = new JournalEntry({
|
||||
debit: form.amount,
|
||||
referenceId: expenseTransaction.id,
|
||||
referenceType: Expense.referenceType,
|
||||
date: form.date,
|
||||
account: paymentAccount.id,
|
||||
accountNormal: 'debit',
|
||||
});
|
||||
journalEntries.credit(creditEntry);
|
||||
journalEntries.debit(debitEntry);
|
||||
|
||||
await Promise.all([
|
||||
journalEntries.saveEntries(),
|
||||
journalEntries.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send({ id: expenseTransaction.id });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Bulk add expneses to the given accounts.
|
||||
*/
|
||||
bulkAddExpenses: {
|
||||
validation: [
|
||||
check('expenses').exists().isArray({ min: 1 }),
|
||||
check('expenses.*.date').optional().isISO8601(),
|
||||
check('expenses.*.payment_account_id').exists().isNumeric().toInt(),
|
||||
check('expenses.*.expense_account_id').exists().isNumeric().toInt(),
|
||||
check('expenses.*.description').optional(),
|
||||
check('expenses.*.amount').exists().isNumeric().toFloat(),
|
||||
check('expenses.*.currency_code').optional(),
|
||||
check('expenses.*.exchange_rate').optional().isNumeric().toFloat(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
const errorReasons = [];
|
||||
|
||||
const paymentAccountsIds = chain(form.expenses)
|
||||
.map((e) => e.payment_account_id).uniq().value();
|
||||
const expenseAccountsIds = chain(form.expenses)
|
||||
.map((e) => e.expense_account_id).uniq().value();
|
||||
|
||||
const [expensesAccounts, paymentAccounts] = await Promise.all([
|
||||
Account.query().whereIn('id', expenseAccountsIds),
|
||||
Account.query().whereIn('id', paymentAccountsIds),
|
||||
]);
|
||||
const storedExpensesAccountsIds = expensesAccounts.map((a) => a.id);
|
||||
const storedPaymentAccountsIds = paymentAccounts.map((a) => a.id);
|
||||
|
||||
const notFoundPaymentAccountsIds = difference(expenseAccountsIds, storedExpensesAccountsIds);
|
||||
const notFoundExpenseAccountsIds = difference(paymentAccountsIds, storedPaymentAccountsIds);
|
||||
|
||||
if (notFoundPaymentAccountsIds.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'PAYMENY.ACCOUNTS.NOT.FOUND',
|
||||
code: 100,
|
||||
accounts: notFoundPaymentAccountsIds,
|
||||
});
|
||||
}
|
||||
if (notFoundExpenseAccountsIds.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'EXPENSE.ACCOUNTS.NOT.FOUND',
|
||||
code: 200,
|
||||
accounts: notFoundExpenseAccountsIds,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { reasons: errorReasons });
|
||||
}
|
||||
const expenseSaveOpers = [];
|
||||
const journalPoster = new JournalPoster();
|
||||
|
||||
form.expenses.forEach(async (expense) => {
|
||||
const expenseSaveOper = Expense.query().insert({ ...expense });
|
||||
expenseSaveOpers.push(expenseSaveOper);
|
||||
});
|
||||
// Wait unit save all expense transactions.
|
||||
const savedExpenseTransactions = await Promise.all(expenseSaveOpers);
|
||||
|
||||
savedExpenseTransactions.forEach((expense) => {
|
||||
const date = moment(expense.date).format('YYYY-DD-MM');
|
||||
|
||||
const debit = new JournalEntry({
|
||||
debit: expense.amount,
|
||||
referenceId: expense.id,
|
||||
referenceType: Expense.referenceType,
|
||||
account: expense.payment_account_id,
|
||||
accountNormal: 'debit',
|
||||
date,
|
||||
});
|
||||
const credit = new JournalEntry({
|
||||
credit: expense.amount,
|
||||
referenceId: expense.id,
|
||||
referenceType: Expense.referenceId,
|
||||
account: expense.expense_account_id,
|
||||
accountNormal: 'debit',
|
||||
date,
|
||||
});
|
||||
journalPoster.credit(credit);
|
||||
journalPoster.debit(debit);
|
||||
});
|
||||
|
||||
// Save expense journal entries and balance change.
|
||||
await Promise.all([
|
||||
journalPoster.saveEntries(),
|
||||
journalPoster.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve paginated expenses list.
|
||||
*/
|
||||
listExpenses: {
|
||||
validation: [
|
||||
query('expense_account_id').optional().isNumeric().toInt(),
|
||||
query('payment_account_id').optional().isNumeric().toInt(),
|
||||
query('note').optional(),
|
||||
query('range_from').optional().isNumeric().toFloat(),
|
||||
query('range_to').optional().isNumeric().toFloat(),
|
||||
query('date_from').optional().isISO8601(),
|
||||
query('date_to').optional().isISO8601(),
|
||||
query('column_sort_order').optional().isIn(['created_at', 'date', 'amount']),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
page_size: 10,
|
||||
page: 1,
|
||||
...req.query,
|
||||
};
|
||||
const errorReasons = [];
|
||||
const expenseResource = await Resource.query().where('name', 'expenses').first();
|
||||
|
||||
if (!expenseResource) {
|
||||
errorReasons.push({ type: 'EXPENSE_NOT_FOUND', code: 300 });
|
||||
}
|
||||
const view = await View.query().runBefore((result, q) => {
|
||||
if (filter.customer_view_id) {
|
||||
q.where('id', filter.customer_view_id);
|
||||
} else {
|
||||
q.where('favorite', true);
|
||||
}
|
||||
q.where('resource_id', expenseResource.id);
|
||||
q.withGraphFetched('viewRoles');
|
||||
q.withGraphFetched('columns');
|
||||
q.first();
|
||||
return result;
|
||||
});
|
||||
|
||||
if (!view) {
|
||||
errorReasons.push({ type: 'VIEW_NOT_FOUND', code: 100 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
const expenses = await Expense.query()
|
||||
.modify('filterByAmountRange', filter.range_from, filter.to_range)
|
||||
.modify('filterByDateRange', filter.date_from, filter.date_to)
|
||||
.modify('filterByExpenseAccount', filter.expense_account_id)
|
||||
.modify('filterByPaymentAccount', filter.payment_account_id)
|
||||
.modify('orderBy', filter.column_sort_order, filter.sort_order)
|
||||
.page(filter.page, filter.page_size);
|
||||
|
||||
return res.status(200).send({
|
||||
columns: view.columns,
|
||||
viewRoles: view.viewRoles,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the given account.
|
||||
*/
|
||||
deleteExpense: {
|
||||
validation: [
|
||||
param('id').isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
const expenseTransaction = await Expense.query().findById(id);
|
||||
|
||||
if (!expenseTransaction) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
const expenseEntries = await AccountTransaction.query()
|
||||
.where('reference_type', 'Expense')
|
||||
.where('reference_id', expenseTransaction.id);
|
||||
|
||||
const expenseEntriesCollect = new JournalPoster();
|
||||
expenseEntriesCollect.loadEntries(expenseEntries);
|
||||
expenseEntriesCollect.reverseEntries();
|
||||
|
||||
await Promise.all([
|
||||
expenseTransaction.delete(),
|
||||
expenseEntriesCollect.deleteEntries(),
|
||||
expenseEntriesCollect.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Update details of the given account.
|
||||
*/
|
||||
updateExpense: {
|
||||
validation: [
|
||||
param('id').isNumeric().toInt(),
|
||||
check('date').optional().isISO8601(),
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('expense_account_id').exists().isNumeric().toInt(),
|
||||
check('description').optional(),
|
||||
check('amount').exists().isNumeric().toFloat(),
|
||||
check('currency_code').optional(),
|
||||
check('exchange_rate').optional().isNumeric().toFloat(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
const expenseTransaction = await Expense.query().findById(id);
|
||||
|
||||
if (!expenseTransaction) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
526
server/src/http/controllers/FinancialStatements.js
Normal file
526
server/src/http/controllers/FinancialStatements.js
Normal file
@@ -0,0 +1,526 @@
|
||||
import express from 'express';
|
||||
import { query, validationResult } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { pick } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import AccountType from '@/models/AccountType';
|
||||
import Account from '@/models/Account';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import { dateRangeCollection } from '@/utils';
|
||||
|
||||
const formatNumberClosure = (filter) => (balance) => {
|
||||
let formattedBalance = parseFloat(balance);
|
||||
|
||||
if (filter.no_cents) {
|
||||
formattedBalance = parseInt(formattedBalance, 10);
|
||||
}
|
||||
if (filter.divide_1000) {
|
||||
formattedBalance /= 1000;
|
||||
}
|
||||
return formattedBalance;
|
||||
};
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
router.use(jwtAuth);
|
||||
|
||||
router.get('/ledger',
|
||||
this.ledger.validation,
|
||||
asyncMiddleware(this.ledger.handler));
|
||||
|
||||
router.get('/general_ledger',
|
||||
this.generalLedger.validation,
|
||||
asyncMiddleware(this.generalLedger.handler));
|
||||
|
||||
router.get('/balance_sheet',
|
||||
this.balanceSheet.validation,
|
||||
asyncMiddleware(this.balanceSheet.handler));
|
||||
|
||||
router.get('/trial_balance_sheet',
|
||||
this.trialBalanceSheet.validation,
|
||||
asyncMiddleware(this.trialBalanceSheet.handler));
|
||||
|
||||
router.get('/profit_loss_sheet',
|
||||
this.profitLossSheet.validation,
|
||||
asyncMiddleware(this.profitLossSheet.handler));
|
||||
|
||||
// router.get('/cash_flow_statement',
|
||||
// this.cashFlowStatement.validation,
|
||||
// asyncMiddleware(this.cashFlowStatement.handler));
|
||||
|
||||
// router.get('/badget_verses_actual',
|
||||
// this.badgetVersesActuals.validation,
|
||||
// asyncMiddleware(this.badgetVersesActuals.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the ledger report of the given account.
|
||||
*/
|
||||
ledger: {
|
||||
validation: [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('transaction_types').optional().isArray({ min: 1 }),
|
||||
query('account_ids').optional().isArray({ min: 1 }),
|
||||
query('account_ids.*').optional().isNumeric().toInt(),
|
||||
query('from_range').optional().isNumeric().toInt(),
|
||||
query('to_range').optional().isNumeric().toInt(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
from_range: null,
|
||||
to_range: null,
|
||||
account_ids: [],
|
||||
transaction_types: [],
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
...req.query,
|
||||
};
|
||||
const accountsJournalEntries = await AccountTransaction.query()
|
||||
.modify('filterDateRange', filter.from_date, filter.to_date)
|
||||
.modify('filterAccounts', filter.account_ids)
|
||||
.modify('filterTransactionTypes', filter.transaction_types)
|
||||
.modify('filterAmountRange', filter.from_range, filter.to_range)
|
||||
.withGraphFetched('account');
|
||||
|
||||
const formatNumber = formatNumberClosure(filter.number_format);
|
||||
|
||||
return res.status(200).send({
|
||||
meta: { ...filter },
|
||||
items: accountsJournalEntries.map((entry) => ({
|
||||
...entry,
|
||||
credit: formatNumber(entry.credit),
|
||||
debit: formatNumber(entry.debit),
|
||||
})),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the general ledger financial statement.
|
||||
*/
|
||||
generalLedger: {
|
||||
validation: [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('basis').optional(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
none_zero: false,
|
||||
...req.query,
|
||||
};
|
||||
const accounts = await Account.query()
|
||||
.orderBy('index', 'DESC')
|
||||
.withGraphFetched('transactions')
|
||||
.modifyGraph('transactions', (builder) => {
|
||||
builder.modify('filterDateRange', filter.from_date, filter.to_date);
|
||||
});
|
||||
|
||||
const openingBalanceTransactions = await AccountTransaction.query()
|
||||
.modify('filterDateRange', null, filter.from_date)
|
||||
.modify('sumationCreditDebit')
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
const closingBalanceTransactions = await AccountTransaction.query()
|
||||
.modify('filterDateRange', null, filter.to_date)
|
||||
.modify('sumationCreditDebit')
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
const opeingBalanceCollection = new JournalPoster();
|
||||
const closingBalanceCollection = new JournalPoster();
|
||||
|
||||
opeingBalanceCollection.loadEntries(openingBalanceTransactions);
|
||||
closingBalanceCollection.loadEntries(closingBalanceTransactions);
|
||||
|
||||
// Transaction amount formatter based on the given query.
|
||||
const formatNumber = formatNumberClosure(filter.number_format);
|
||||
|
||||
const items = [
|
||||
...accounts
|
||||
.filter((account) => (
|
||||
account.transactions.length > 0 || !filter.none_zero
|
||||
))
|
||||
.map((account) => ({
|
||||
...pick(account, ['id', 'name', 'code', 'index']),
|
||||
transactions: [
|
||||
...account.transactions.map((transaction) => ({
|
||||
...transaction,
|
||||
credit: formatNumber(transaction.credit),
|
||||
debit: formatNumber(transaction.debit),
|
||||
})),
|
||||
],
|
||||
opening: {
|
||||
date: filter.from_date,
|
||||
balance: opeingBalanceCollection.getClosingBalance(account.id),
|
||||
},
|
||||
closing: {
|
||||
date: filter.to_date,
|
||||
balance: closingBalanceCollection.getClosingBalance(account.id),
|
||||
},
|
||||
})),
|
||||
];
|
||||
return res.status(200).send({
|
||||
meta: { ...filter },
|
||||
items,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet.
|
||||
*/
|
||||
balanceSheet: {
|
||||
validation: [
|
||||
query('accounting_method').optional().isIn(['cash', 'accural']),
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
query('display_columns_by').optional().isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
display_columns_by: 'year',
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
none_zero: false,
|
||||
...req.query,
|
||||
};
|
||||
|
||||
const balanceSheetTypes = await AccountType.query()
|
||||
.where('balance_sheet', true);
|
||||
|
||||
// Fetch all balance sheet accounts.
|
||||
const accounts = await Account.query()
|
||||
.whereIn('account_type_id', balanceSheetTypes.map((a) => a.id))
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions')
|
||||
.modifyGraph('transactions', (builder) => {
|
||||
builder.modify('filterDateRange', null, filter.to_date);
|
||||
});
|
||||
|
||||
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
||||
const journalEntries = new JournalPoster();
|
||||
journalEntries.loadEntries(journalEntriesCollected);
|
||||
|
||||
// Account balance formmatter based on the given query.
|
||||
const balanceFormatter = formatNumberClosure(filter.number_format);
|
||||
|
||||
// Gets the date range set from start to end date.
|
||||
const dateRangeSet = dateRangeCollection(
|
||||
filter.from_date,
|
||||
filter.to_date,
|
||||
filter.display_columns_by,
|
||||
);
|
||||
// Retrieve the asset balance sheet.
|
||||
const assets = [
|
||||
...accounts
|
||||
.filter((account) => (
|
||||
account.type.normal === 'debit'
|
||||
&& (account.transactions.length > 0 || !filter.none_zero)
|
||||
))
|
||||
.map((account) => ({
|
||||
...pick(account, ['id', 'index', 'name', 'code']),
|
||||
transactions: dateRangeSet.map((date) => {
|
||||
const type = filter.display_columns_by;
|
||||
const balance = journalEntries.getClosingBalance(account.id, date, type);
|
||||
return { date, balance: balanceFormatter(balance) };
|
||||
}),
|
||||
})),
|
||||
];
|
||||
// Retrieve liabilities and equity balance sheet.
|
||||
const liabilitiesEquity = [
|
||||
...accounts
|
||||
.filter((account) => (
|
||||
account.type.normal === 'credit'
|
||||
&& (account.transactions.length > 0 || !filter.none_zero)
|
||||
))
|
||||
.map((account) => ({
|
||||
...pick(account, ['id', 'index', 'name', 'code']),
|
||||
transactions: dateRangeSet.map((date) => {
|
||||
const type = filter.display_columns_by;
|
||||
const balance = journalEntries.getClosingBalance(account.id, date, type);
|
||||
return { date, balance: balanceFormatter(balance) };
|
||||
}),
|
||||
})),
|
||||
];
|
||||
return res.status(200).send({
|
||||
columns: { ...dateRangeSet },
|
||||
balance_sheet: {
|
||||
assets,
|
||||
liabilities_equity: liabilitiesEquity,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the trial balance sheet.
|
||||
*/
|
||||
trialBalanceSheet: {
|
||||
validation: [
|
||||
query('basis').optional(),
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('number_format.no_cents').optional().isBoolean(),
|
||||
query('number_format.1000_divide').optional().isBoolean(),
|
||||
query('basis').optional(),
|
||||
query('none_zero').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
basis: 'accural',
|
||||
none_zero: false,
|
||||
...req.query,
|
||||
};
|
||||
|
||||
const accounts = await Account.query()
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions')
|
||||
.modifyGraph('transactions', (builder) => {
|
||||
builder.modify('sumationCreditDebit');
|
||||
builder.modify('filterDateRange', filter.from_date, filter.to_date);
|
||||
});
|
||||
|
||||
const journalEntriesCollect = Account.collectJournalEntries(accounts);
|
||||
const journalEntries = new JournalPoster();
|
||||
journalEntries.loadEntries(journalEntriesCollect);
|
||||
|
||||
// Account balance formmatter based on the given query.
|
||||
const balanceFormatter = formatNumberClosure(filter.number_format);
|
||||
|
||||
const items = accounts
|
||||
.filter((account) => (
|
||||
account.transactions.length > 0 || !filter.none_zero
|
||||
))
|
||||
.map((account) => {
|
||||
const trial = journalEntries.getTrialBalance(account.id);
|
||||
return {
|
||||
account_id: account.id,
|
||||
code: account.code,
|
||||
accountNormal: account.type.normal,
|
||||
credit: balanceFormatter(trial.credit),
|
||||
debit: balanceFormatter(trial.debit),
|
||||
balance: balanceFormatter(trial.balance),
|
||||
};
|
||||
});
|
||||
return res.status(200).send({
|
||||
meta: { ...filter },
|
||||
items: [...items],
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve profit/loss financial statement.
|
||||
*/
|
||||
profitLossSheet: {
|
||||
validation: [
|
||||
query('basis').optional(),
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('number_format.no_cents').optional().isBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean(),
|
||||
query('basis').optional(),
|
||||
query('none_zero').optional(),
|
||||
query('display_columns_by').optional().isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
query('accounts').optional().isArray(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
basis: 'accural',
|
||||
none_zero: false,
|
||||
display_columns_by: 'month',
|
||||
...req.query,
|
||||
};
|
||||
const incomeStatementTypes = await AccountType.query().where('income_sheet', true);
|
||||
|
||||
const accounts = await Account.query()
|
||||
.whereIn('account_type_id', incomeStatementTypes.map((t) => t.id))
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions');
|
||||
|
||||
const filteredAccounts = accounts.filter((account) => {
|
||||
return account.transactions.length > 0 || !filter.none_zero;
|
||||
});
|
||||
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
||||
const journalEntries = new JournalPoster();
|
||||
journalEntries.loadEntries(journalEntriesCollected);
|
||||
|
||||
// Account balance formmatter based on the given query.
|
||||
const numberFormatter = formatNumberClosure(filter.number_format);
|
||||
|
||||
// Gets the date range set from start to end date.
|
||||
const dateRangeSet = dateRangeCollection(
|
||||
filter.from_date,
|
||||
filter.to_date,
|
||||
filter.display_columns_by,
|
||||
);
|
||||
const accountsIncome = filteredAccounts
|
||||
.filter((account) => account.type.normal === 'credit')
|
||||
.map((account) => ({
|
||||
...pick(account, ['id', 'index', 'name', 'code']),
|
||||
dates: dateRangeSet.map((date) => {
|
||||
const type = filter.display_columns_by;
|
||||
const amount = journalEntries.getClosingBalance(account.id, date, type);
|
||||
|
||||
return { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
}),
|
||||
}));
|
||||
|
||||
const accountsExpenses = filteredAccounts
|
||||
.filter((account) => account.type.normal === 'debit')
|
||||
.map((account) => ({
|
||||
...pick(account, ['id', 'index', 'name', 'code']),
|
||||
dates: dateRangeSet.map((date) => {
|
||||
const type = filter.display_columns_by;
|
||||
const amount = journalEntries.getClosingBalance(account.id, date, type);
|
||||
|
||||
return { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
}),
|
||||
}));
|
||||
|
||||
// Calculates the total income of income accounts.
|
||||
const totalAccountsIncome = dateRangeSet.reduce((acc, date, index) => {
|
||||
let amount = 0;
|
||||
accountsIncome.forEach((account) => {
|
||||
const currentDate = account.dates[index];
|
||||
amount += currentDate.rawAmount || 0;
|
||||
});
|
||||
acc[date] = { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Calculates the total expenses of expenses accounts.
|
||||
const totalAccountsExpenses = dateRangeSet.reduce((acc, date, index) => {
|
||||
let amount = 0;
|
||||
accountsExpenses.forEach((account) => {
|
||||
const currentDate = account.dates[index];
|
||||
amount += currentDate.rawAmount || 0;
|
||||
});
|
||||
acc[date] = { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Total income(date) - Total expenses(date) = Net income(date)
|
||||
const netIncome = dateRangeSet.map((date) => {
|
||||
const totalIncome = totalAccountsIncome[date];
|
||||
const totalExpenses = totalAccountsExpenses[date];
|
||||
|
||||
let amount = totalIncome.rawAmount || 0;
|
||||
amount -= totalExpenses.rawAmount || 0;
|
||||
return { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
meta: { ...filter },
|
||||
income: {
|
||||
entry_normal: 'credit',
|
||||
accounts: accountsIncome,
|
||||
},
|
||||
expenses: {
|
||||
entry_normal: 'debit',
|
||||
accounts: accountsExpenses,
|
||||
},
|
||||
total_income: Object.values(totalAccountsIncome),
|
||||
total_expenses: Object.values(totalAccountsExpenses),
|
||||
total_net_income: netIncome,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
cashFlowStatement: {
|
||||
validation: [
|
||||
query('date_from').optional(),
|
||||
query('date_to').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
badgetVersesActuals: {
|
||||
validation: [
|
||||
|
||||
],
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
0
server/src/http/controllers/Invoices.js
Normal file
0
server/src/http/controllers/Invoices.js
Normal file
@@ -70,19 +70,17 @@ export default {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { sell_account_id: sellAccountId, cost_account_id: costAccountId } = req.body;
|
||||
const { category_id: categoryId, custom_fields: customFields } = req.body;
|
||||
const form = { ...req.body };
|
||||
const errorReasons = [];
|
||||
|
||||
const costAccountPromise = Account.where('id', costAccountId).fetch();
|
||||
const sellAccountPromise = Account.where('id', sellAccountId).fetch();
|
||||
const itemCategoryPromise = (categoryId)
|
||||
? ItemCategory.where('id', categoryId).fetch() : null;
|
||||
const costAccountPromise = Account.where('id', form.cost_account_id).fetch();
|
||||
const sellAccountPromise = Account.where('id', form.sell_account_id).fetch();
|
||||
const itemCategoryPromise = (form.category_id)
|
||||
? ItemCategory.where('id', form.category_id).fetch() : null;
|
||||
|
||||
// Validate the custom fields key and value type.
|
||||
if (customFields.length > 0) {
|
||||
const customFieldsKeys = customFields.map((field) => field.key);
|
||||
if (form.custom_fields.length > 0) {
|
||||
const customFieldsKeys = form.custom_fields.map((field) => field.key);
|
||||
|
||||
// Get resource id than get all resource fields.
|
||||
const resource = await Resource.where('name', 'items').fetch();
|
||||
@@ -110,13 +108,12 @@ export default {
|
||||
if (!sellAccount) {
|
||||
errorReasons.push({ type: 'SELL_ACCOUNT_NOT_FOUND', code: 120 });
|
||||
}
|
||||
if (!itemCategory && categoryId) {
|
||||
if (!itemCategory && form.category_id) {
|
||||
errorReasons.push({ type: 'ITEM_CATEGORY_NOT_FOUND', code: 140 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
const item = Item.forge({
|
||||
name: req.body.name,
|
||||
type_id: 1,
|
||||
|
||||
76
server/src/http/controllers/Options.js
Normal file
76
server/src/http/controllers/Options.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import express from 'express';
|
||||
import { body, query, validationResult } from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Option from '@/models/Option';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/',
|
||||
this.saveOptions.validation,
|
||||
asyncMiddleware(this.saveOptions.handler));
|
||||
|
||||
router.get('/',
|
||||
this.getOptions.validation,
|
||||
asyncMiddleware(this.getSettings));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves the given options to the storage.
|
||||
*/
|
||||
saveOptions: {
|
||||
validation: [
|
||||
body('options').isArray(),
|
||||
body('options.*.key').exists(),
|
||||
body('options.*.value').exists(),
|
||||
body('options.*.group').exists(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'VALIDATION_ERROR', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
const optionsCollections = await Option.query();
|
||||
|
||||
form.options.forEach((option) => {
|
||||
optionsCollections.setMeta(option.key, option.value, option.group);
|
||||
});
|
||||
await optionsCollections.saveMeta();
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the application options from the storage.
|
||||
*/
|
||||
getOptions: {
|
||||
validation: [
|
||||
query('key').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'VALIDATION_ERROR', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const options = await Option.query();
|
||||
|
||||
return res.status(200).sends({
|
||||
options: options.toArray(),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
0
server/src/http/controllers/PurchasesReports.js
Normal file
0
server/src/http/controllers/PurchasesReports.js
Normal file
0
server/src/http/controllers/RecurringJournal.js
Normal file
0
server/src/http/controllers/RecurringJournal.js
Normal file
@@ -18,42 +18,39 @@ const AccessControllSchema = [
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line arrow-body-style
|
||||
const getResourceSchema = (resource) => AccessControllSchema.find((schema) => {
|
||||
return schema.resource === resource;
|
||||
});
|
||||
const getResourceSchema = (resource) => AccessControllSchema
|
||||
.find((schema) => schema.resource === resource);
|
||||
|
||||
const getResourcePermissions = (resource) => {
|
||||
const foundResource = getResourceSchema(resource);
|
||||
return foundResource ? foundResource.permissions : [];
|
||||
};
|
||||
|
||||
const findNotFoundResources = (resourcesSlugs) => {
|
||||
const schemaResourcesSlugs = AccessControllSchema.map((s) => s.resource);
|
||||
return difference(resourcesSlugs, schemaResourcesSlugs);
|
||||
};
|
||||
|
||||
const findNotFoundPermissions = (permissions, resourceSlug) => {
|
||||
const schemaPermissions = getResourcePermissions(resourceSlug);
|
||||
return difference(permissions, schemaPermissions);
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
findNotFoundResources(resourcesSlugs) {
|
||||
const schemaResourcesSlugs = AccessControllSchema.map((s) => s.resource);
|
||||
return difference(resourcesSlugs, schemaResourcesSlugs);
|
||||
},
|
||||
|
||||
findNotFoundPermissions(permissions, resourceSlug) {
|
||||
const schemaPermissions = getResourcePermissions(resourceSlug);
|
||||
return difference(permissions, schemaPermissions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/',
|
||||
this.newRole.validation,
|
||||
asyncMiddleware(this.newRole.handler));
|
||||
|
||||
router.post('/:id',
|
||||
this.editRole.validation,
|
||||
asyncMiddleware(this.editRole.handler.bind(this)));
|
||||
|
||||
// router.post('/',
|
||||
// this.newRole.validation,
|
||||
// asyncMiddleware(this.newRole.handler));
|
||||
|
||||
router.delete('/:id',
|
||||
this.deleteRole.validation,
|
||||
asyncMiddleware(this.deleteRole.handler));
|
||||
@@ -80,32 +77,30 @@ export default {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { name, description, permissions } = req.body;
|
||||
|
||||
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
|
||||
const permissionsSlugs = [];
|
||||
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
|
||||
|
||||
const resourcesNotFound = this.findNotFoundResources(resourcesSlugs);
|
||||
const errorReasons = [];
|
||||
const notFoundPermissions = [];
|
||||
|
||||
if (resourcesNotFound.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'RESOURCE_SLUG_NOT_FOUND',
|
||||
code: 100,
|
||||
resources: resourcesNotFound,
|
||||
type: 'RESOURCE_SLUG_NOT_FOUND', code: 100, resources: resourcesNotFound,
|
||||
});
|
||||
}
|
||||
permissions.forEach((perm) => {
|
||||
const abilities = perm.permissions.map((ability) => ability);
|
||||
|
||||
// Gets the not found permissions in the schema.
|
||||
const notFoundAbilities = this.findNotFoundPermissions(abilities, perm.resource_slug);
|
||||
|
||||
const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug);
|
||||
|
||||
if (notFoundAbilities.length > 0) {
|
||||
notFoundPermissions.push({
|
||||
resource_slug: perm.resource_slug, permissions: notFoundAbilities,
|
||||
resource_slug: perm.resource_slug,
|
||||
permissions: notFoundAbilities,
|
||||
});
|
||||
} else {
|
||||
const perms = perm.permissions || [];
|
||||
@@ -217,7 +212,7 @@ export default {
|
||||
const notFoundPermissions = [];
|
||||
|
||||
const resourcesSlugs = permissions.map((perm) => perm.resource_slug);
|
||||
const resourcesNotFound = this.findNotFoundResources(resourcesSlugs);
|
||||
const resourcesNotFound = findNotFoundResources(resourcesSlugs);
|
||||
|
||||
if (resourcesNotFound.length > 0) {
|
||||
errorReasons.push({
|
||||
@@ -230,7 +225,7 @@ export default {
|
||||
permissions.forEach((perm) => {
|
||||
const abilities = perm.permissions.map((ability) => ability);
|
||||
// Gets the not found permissions in the schema.
|
||||
const notFoundAbilities = this.findNotFoundPermissions(abilities, perm.resource_slug);
|
||||
const notFoundAbilities = findNotFoundPermissions(abilities, perm.resource_slug);
|
||||
|
||||
if (notFoundAbilities.length > 0) {
|
||||
notFoundPermissions.push({
|
||||
|
||||
0
server/src/http/controllers/SalesReports.js
Normal file
0
server/src/http/controllers/SalesReports.js
Normal file
10
server/src/http/controllers/Suppliers.js
Normal file
10
server/src/http/controllers/Suppliers.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import express from 'express';
|
||||
|
||||
export default {
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
return router;
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { difference } from 'lodash';
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import { check, query, validationResult } from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Resource from '@/models/Resource';
|
||||
import View from '../../models/View';
|
||||
@@ -8,10 +8,13 @@ import View from '../../models/View';
|
||||
export default {
|
||||
resource: 'items',
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/resource/:resource_id',
|
||||
router.post('/',
|
||||
this.createView.validation,
|
||||
asyncMiddleware(this.createView.handler));
|
||||
|
||||
@@ -33,7 +36,9 @@ export default {
|
||||
* List all views that associated with the given resource.
|
||||
*/
|
||||
listViews: {
|
||||
validation: [],
|
||||
validation: [
|
||||
query('resource_name').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { resource_id: resourceId } = req.params;
|
||||
const views = await View.where('resource_id', resourceId).fetchAll();
|
||||
@@ -54,7 +59,6 @@ export default {
|
||||
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({ ...view.toJSON() });
|
||||
},
|
||||
},
|
||||
@@ -66,25 +70,23 @@ export default {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const { view_id: viewId } = req.params;
|
||||
const view = await View.where('id', viewId).fetch({
|
||||
withRelated: ['viewRoles', 'columns'],
|
||||
});
|
||||
const view = await View.query().findById(viewId);
|
||||
|
||||
if (!view) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
if (view.attributes.predefined) {
|
||||
if (view.predefined) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'PREDEFINED_VIEW', code: 200 }],
|
||||
});
|
||||
}
|
||||
// console.log(view);
|
||||
await view.destroy();
|
||||
|
||||
// await view.columns().destroy({ require: false });
|
||||
await Promise.all([
|
||||
view.$relatedQuery('viewRoles').delete(),
|
||||
view.$relatedQuery('columns').delete(),
|
||||
]);
|
||||
await view.delete();
|
||||
|
||||
return res.status(200).send({ id: view.get('id') });
|
||||
},
|
||||
@@ -95,16 +97,17 @@ export default {
|
||||
*/
|
||||
createView: {
|
||||
validation: [
|
||||
check('resource_name').exists().escape().trim(),
|
||||
check('label').exists().escape().trim(),
|
||||
check('columns').isArray({ min: 3 }),
|
||||
check('columns').exists().isArray({ min: 1 }),
|
||||
check('roles').isArray(),
|
||||
check('roles.*.field').exists().escape().trim(),
|
||||
check('roles.*.comparator').exists(),
|
||||
check('roles.*.value').exists(),
|
||||
check('roles.*.index').exists().isNumeric().toInt(),
|
||||
check('columns.*').exists().escape().trim(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { resource_id: resourceId } = req.params;
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
@@ -113,7 +116,8 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
const resource = await Resource.where('id', resourceId).fetch();
|
||||
const form = { ...req.body };
|
||||
const resource = await Resource.query().where('name', form.resource_name).first();
|
||||
|
||||
if (!resource) {
|
||||
return res.boom.notFound(null, {
|
||||
@@ -121,38 +125,34 @@ export default {
|
||||
});
|
||||
}
|
||||
const errorReasons = [];
|
||||
const { label, roles, columns } = req.body;
|
||||
const fieldsSlugs = form.roles.map((role) => role.field);
|
||||
|
||||
const fieldsSlugs = roles.map((role) => role.field);
|
||||
const resourceFields = await resource.$relatedQuery('fields');
|
||||
const resourceFieldsKeys = resourceFields.map((f) => f.slug);
|
||||
|
||||
const resourceFields = await resource.fields().fetch();
|
||||
const resourceFieldsKeys = resourceFields.map((f) => f.get('key'));
|
||||
// The difference between the stored resource fields and submit fields keys.
|
||||
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
|
||||
|
||||
if (notFoundFields.length > 0) {
|
||||
errorReasons.push({ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields });
|
||||
}
|
||||
|
||||
const notFoundColumns = difference(columns, resourceFieldsKeys);
|
||||
// The difference between the stored resource fields and the submit columns keys.
|
||||
const notFoundColumns = difference(form.columns, resourceFieldsKeys);
|
||||
|
||||
if (notFoundColumns.length > 0) {
|
||||
errorReasons.push({ type: 'COLUMNS_NOT_EXIST', code: 200, fields: notFoundColumns });
|
||||
errorReasons.push({ type: 'COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns });
|
||||
}
|
||||
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
const view = await View.forge({
|
||||
name: label,
|
||||
predefined: false,
|
||||
});
|
||||
|
||||
// Save view details.
|
||||
await view.save();
|
||||
|
||||
// Save view columns.
|
||||
|
||||
const view = await View.query().insert({
|
||||
name: form.label,
|
||||
predefined: false,
|
||||
resource_id: resource.id,
|
||||
});
|
||||
|
||||
// Save view roles.
|
||||
|
||||
|
||||
@@ -160,7 +160,6 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
editView: {
|
||||
validation: [
|
||||
check('label').exists().escape().trim(),
|
||||
|
||||
@@ -8,6 +8,18 @@ import Accounts from '@/http/controllers/Accounts';
|
||||
import AccountOpeningBalance from '@/http/controllers/AccountOpeningBalance';
|
||||
import Views from '@/http/controllers/Views';
|
||||
import CustomFields from '@/http/controllers/Fields';
|
||||
import Accounting from '@/http/controllers/Accounting';
|
||||
import FinancialStatements from '@/http/controllers/FinancialStatements';
|
||||
import Expenses from '@/http/controllers/Expenses';
|
||||
import Options from '@/http/controllers/Options';
|
||||
import Budget from '@/http/controllers/Budget';
|
||||
import BudgetReports from '@/http/controllers/BudgetReports';
|
||||
import Customers from '@/http/controllers/Customers';
|
||||
import Suppliers from '@/http/controllers/Suppliers';
|
||||
import Bills from '@/http/controllers/Bills';
|
||||
import CurrencyAdjustment from './controllers/CurrencyAdjustment';
|
||||
// import SalesReports from '@/http/controllers/SalesReports';
|
||||
// import PurchasesReports from '@/http/controllers/PurchasesReports';
|
||||
|
||||
export default (app) => {
|
||||
// app.use('/api/oauth2', OAuth2.router());
|
||||
@@ -15,9 +27,21 @@ export default (app) => {
|
||||
app.use('/api/users', Users.router());
|
||||
app.use('/api/roles', Roles.router());
|
||||
app.use('/api/accounts', Accounts.router());
|
||||
app.use('/api/accountOpeningBalance', AccountOpeningBalance.router());
|
||||
app.use('/api/accounting', Accounting.router());
|
||||
app.use('/api/accounts_opeing_balance', AccountOpeningBalance.router());
|
||||
app.use('/api/views', Views.router());
|
||||
app.use('/api/fields', CustomFields.router());
|
||||
app.use('/api/items', Items.router());
|
||||
app.use('/api/item_categories', ItemCategories.router());
|
||||
app.use('/api/expenses', Expenses.router());
|
||||
app.use('/api/financial_statements', FinancialStatements.router());
|
||||
app.use('/api/options', Options.router());
|
||||
app.use('/api/budget_reports', BudgetReports.router());
|
||||
// app.use('/api/customers', Customers.router());
|
||||
// app.use('/api/suppliers', Suppliers.router());
|
||||
// app.use('/api/bills', Bills.router());
|
||||
app.use('/api/budget', Budget.router());
|
||||
// app.use('/api/currency_adjustment', CurrencyAdjustment.router());
|
||||
// app.use('/api/reports/sales', SalesReports.router());
|
||||
// app.use('/api/reports/purchases', PurchasesReports.router());
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ const authMiddleware = (req, res, next) => {
|
||||
reject(error);
|
||||
} else {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
req.user = await User.where('id', decoded._id).fetch();
|
||||
req.user = await User.query().findById(decoded._id);
|
||||
// Auth.setAuthenticatedUser(req.user);
|
||||
|
||||
if (!req.user) {
|
||||
|
||||
Reference in New Issue
Block a user