WIP server side.

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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