mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
WIP
This commit is contained in:
@@ -1,12 +1,27 @@
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import boom from 'express-boom';
|
||||
import i18n from 'i18n';
|
||||
import '../config';
|
||||
import routes from '@/http';
|
||||
import '@/models';
|
||||
|
||||
const app = express();
|
||||
|
||||
// i18n.configure({
|
||||
// // setup some locales - other locales default to en silently
|
||||
// locales: ['en'],
|
||||
|
||||
// // sets a custom cookie name to parse locale settings from.
|
||||
// cookie: 'yourcookiename',
|
||||
|
||||
// // where to store json files - defaults to './locales'
|
||||
// directory: `${__dirname}/resources/locale`,
|
||||
// });
|
||||
|
||||
// i18n init parses req for language headers, cookies, etc.
|
||||
// app.use(i18n.init);
|
||||
|
||||
// Express configuration
|
||||
app.set('port', process.env.PORT || 3000);
|
||||
|
||||
|
||||
17
server/src/database/manager.js
Normal file
17
server/src/database/manager.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import knexManager from 'knex-db-manager';
|
||||
import knexfile from '@/../knexfile';
|
||||
|
||||
const config = knexfile[process.env.NODE_ENV];
|
||||
|
||||
const dbManager = knexManager.databaseManagerFactory({
|
||||
knex: config,
|
||||
dbManager: {
|
||||
// db manager related configuration
|
||||
collate: [],
|
||||
superUser: 'root',
|
||||
superPassword: 'root',
|
||||
// populatePathPattern: 'data/**/*.js', // glob format for searching seeds
|
||||
},
|
||||
});
|
||||
|
||||
export default dbManager;
|
||||
@@ -2,8 +2,8 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('user_has_roles', (table) => {
|
||||
table.increments();
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('role_id').unsigned().references('id').inTable('roles');
|
||||
table.integer('user_id').unsigned();
|
||||
table.integer('role_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ exports.up = function(knex) {
|
||||
table.integer('client_id').unsigned();
|
||||
table.string('refresh_token');
|
||||
table.date('refresh_token_expires_on');
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('user_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('settings', (table) => {
|
||||
table.increments();
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('user_id').unsigned();
|
||||
table.string('group');
|
||||
table.string('type');
|
||||
table.string('key');
|
||||
|
||||
@@ -13,7 +13,7 @@ exports.up = function (knex) {
|
||||
table.boolean('columnable');
|
||||
table.integer('index');
|
||||
table.json('options');
|
||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||
table.integer('resource_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('role_has_accounts', (table) => {
|
||||
table.increments();
|
||||
table.integer('role_id').unsigned().references('id').inTable('roles');
|
||||
table.integer('account_id').unsigned().references('id').inTable('accounts');
|
||||
table.integer('role_id').unsigned();
|
||||
table.integer('account_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('role_has_permissions', (table) => {
|
||||
table.increments();
|
||||
table.integer('role_id').unsigned().references('id').inTable('roles');
|
||||
table.integer('permission_id').unsigned().references('id').inTable('permissions');
|
||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||
table.integer('role_id').unsigned();
|
||||
table.integer('permission_id').unsigned();
|
||||
table.integer('resource_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ exports.up = function (knex) {
|
||||
return knex.schema.createTable('view_roles', (table) => {
|
||||
table.increments();
|
||||
table.integer('index');
|
||||
table.integer('field_id').unsigned().references('id').inTable('resource_fields');
|
||||
table.integer('field_id').unsigned();
|
||||
table.string('comparator');
|
||||
table.string('value');
|
||||
table.integer('view_id').unsigned();
|
||||
|
||||
@@ -7,10 +7,10 @@ exports.up = function(knex) {
|
||||
table.string('transaction_type');
|
||||
table.string('reference_type');
|
||||
table.integer('reference_id');
|
||||
table.integer('account_id').unsigned().references('id').inTable('accounts');
|
||||
table.integer('account_id').unsigned();
|
||||
table.string('note');
|
||||
table.boolean('draft').defaultTo(false);
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('user_id').unsigned();
|
||||
table.date('date');
|
||||
table.timestamps();
|
||||
});
|
||||
|
||||
@@ -6,11 +6,11 @@ exports.up = function(knex) {
|
||||
table.string('currency_code');
|
||||
table.decimal('exchange_rate');
|
||||
table.text('description');
|
||||
table.integer('expense_account_id').unsigned().references('id').inTable('accounts');
|
||||
table.integer('payment_account_id').unsigned().references('id').inTable('accounts');
|
||||
table.integer('expense_account_id').unsigned();
|
||||
table.integer('payment_account_id').unsigned();
|
||||
table.string('reference');
|
||||
table.boolean('published').defaultTo(false);
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('user_id').unsigned();
|
||||
table.date('date');
|
||||
// table.timestamps();
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ exports.up = function(knex) {
|
||||
table.decimal('amount');
|
||||
table.date('date');
|
||||
table.string('note');
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('user_id').unsigned();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('budget_entries', (table) => {
|
||||
table.increments();
|
||||
table.integer('budget_id').unsigned().references('id').inTable('budgets');
|
||||
table.integer('account_id').unsigned().references('id').inTable('accounts');
|
||||
table.integer('budget_id').unsigned();
|
||||
table.integer('account_id').unsigned();
|
||||
table.decimal('amount', 15, 5);
|
||||
table.integer('order');
|
||||
})
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('resource_custom_fields_metadata', (table) => {
|
||||
table.increments();
|
||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||
table.integer('resource_id').unsigned();
|
||||
table.integer('resource_item_id').unsigned();
|
||||
table.string('key');
|
||||
table.string('value');
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
|
||||
@@ -8,54 +8,63 @@ exports.seed = (knex) => {
|
||||
{
|
||||
id: 1,
|
||||
name: 'Fixed Asset',
|
||||
normal: 'debit',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Current Asset',
|
||||
normal: 'debit',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Long Term Liability',
|
||||
normal: 'credit',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Current Liability',
|
||||
normal: 'credit',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Equity',
|
||||
normal: 'credit',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Expense',
|
||||
normal: 'debit',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Income',
|
||||
normal: 'credit',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Accounts Receivable',
|
||||
normal: 'debit',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Accounts Payable',
|
||||
normal: 'credit',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { check, query, validationResult } from 'express-validator';
|
||||
import { check, query, oneOf, validationResult } from 'express-validator';
|
||||
import express from 'express';
|
||||
import { difference } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import Account from '@/models/Account';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
@@ -38,9 +39,10 @@ export default {
|
||||
validation: [
|
||||
check('date').isISO8601(),
|
||||
check('reference').exists(),
|
||||
check('memo').optional().trim().escape(),
|
||||
check('entries').isArray({ min: 1 }),
|
||||
check('entries.*.credit').isNumeric().toInt(),
|
||||
check('entries.*.debit').isNumeric().toInt(),
|
||||
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.account_id').isNumeric().toInt(),
|
||||
check('entries.*.note').optional(),
|
||||
],
|
||||
@@ -56,11 +58,16 @@ export default {
|
||||
date: new Date(),
|
||||
...req.body,
|
||||
};
|
||||
const errorReasons = [];
|
||||
|
||||
let totalCredit = 0;
|
||||
let totalDebit = 0;
|
||||
|
||||
form.entries.forEach((entry) => {
|
||||
const { user } = req;
|
||||
const errorReasons = [];
|
||||
const entries = form.entries.filter((entry) => (entry.credit || entry.debit));
|
||||
const formattedDate = moment(form.date).format('YYYY-MM-DD');
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (entry.credit > 0) {
|
||||
totalCredit += entry.credit;
|
||||
}
|
||||
@@ -68,6 +75,7 @@ export default {
|
||||
totalDebit += entry.debit;
|
||||
}
|
||||
});
|
||||
|
||||
if (totalCredit <= 0 || totalDebit <= 0) {
|
||||
errorReasons.push({
|
||||
type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
|
||||
@@ -77,7 +85,7 @@ export default {
|
||||
if (totalCredit !== totalDebit) {
|
||||
errorReasons.push({ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 100 });
|
||||
}
|
||||
const accountsIds = form.entries.map((entry) => entry.account_id);
|
||||
const accountsIds = entries.map((entry) => entry.account_id);
|
||||
const accounts = await Account.query().whereIn('id', accountsIds)
|
||||
.withGraphFetched('type');
|
||||
|
||||
@@ -95,18 +103,30 @@ export default {
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
|
||||
// Save manual journal transaction.
|
||||
const manualJournal = await ManualJournal.query().insert({
|
||||
reference: form.reference,
|
||||
transaction_type: 'Journal',
|
||||
amount: totalCredit,
|
||||
date: formattedDate,
|
||||
note: form.memo,
|
||||
user_id: user.id,
|
||||
});
|
||||
const journalPoster = new JournalPoster();
|
||||
|
||||
form.entries.forEach((entry) => {
|
||||
entries.forEach((entry) => {
|
||||
const account = accounts.find((a) => a.id === entry.account_id);
|
||||
|
||||
const jouranlEntry = new JournalEntry({
|
||||
date: entry.date,
|
||||
debit: entry.debit,
|
||||
credit: entry.credit,
|
||||
account: account.id,
|
||||
transactionType: 'Journal',
|
||||
accountNormal: account.type.normal,
|
||||
note: entry.note,
|
||||
date: formattedDate,
|
||||
userId: user.id,
|
||||
});
|
||||
if (entry.debit) {
|
||||
journalPoster.debit(jouranlEntry);
|
||||
@@ -120,7 +140,7 @@ export default {
|
||||
journalPoster.saveEntries(),
|
||||
journalPoster.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send();
|
||||
return res.status(200).send({ id: manualJournal.id });
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -1,10 +1,71 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
check,
|
||||
param,
|
||||
query,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
|
||||
export default {
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/',
|
||||
this.newCustomer.validation,
|
||||
asyncMiddleware(this.newCustomer.handler));
|
||||
|
||||
router.post('/:id',
|
||||
this.editCustomer.validation,
|
||||
asyncMiddleware(this.editCustomer.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
newCustomer: {
|
||||
validation: [
|
||||
check('custom_type').exists().trim().escape(),
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name'),
|
||||
check('company_name'),
|
||||
check('email'),
|
||||
check('work_phone'),
|
||||
check('personal_phone'),
|
||||
|
||||
check('billing_address.country'),
|
||||
check('billing_address.address'),
|
||||
check('billing_address.city'),
|
||||
check('billing_address.phone'),
|
||||
check('billing_address.zip_code'),
|
||||
|
||||
check('shiping_address.country'),
|
||||
check('shiping_address.address'),
|
||||
check('shiping_address.city'),
|
||||
check('shiping_address.phone'),
|
||||
check('shiping_address.zip_code'),
|
||||
|
||||
check('contact.additional_phone'),
|
||||
check('contact.additional_email'),
|
||||
|
||||
check('custom_fields').optional().isArray({ min: 1 }),
|
||||
check('custom_fields.*.key').exists().trim().escape(),
|
||||
check('custom_fields.*.value').exists(),
|
||||
|
||||
check('inactive').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
editCustomer: {
|
||||
validation: [
|
||||
|
||||
],
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -317,6 +317,12 @@ export default {
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
|
||||
query('filter_roles').optional().isArray(),
|
||||
query('filter_roles.*.field_key').exists().escape().trim(),
|
||||
query('filter_roles.*.value').exists().escape().trim(),
|
||||
query('filter_roles.*.comparator').exists().escape().trim(),
|
||||
query('filter_roles.*.index').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
@@ -122,6 +122,7 @@ export default {
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('accounts_ids').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -134,16 +135,21 @@ export default {
|
||||
const filter = {
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
basis: 'cash',
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
none_zero: false,
|
||||
accounts_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
|
||||
const accounts = await Account.query()
|
||||
.orderBy('index', 'DESC')
|
||||
.modify('filterAccounts', filter.accounts_ids)
|
||||
.withGraphFetched('transactions')
|
||||
.withGraphFetched('type')
|
||||
.modifyGraph('transactions', (builder) => {
|
||||
builder.modify('filterDateRange', filter.from_date, filter.to_date);
|
||||
});
|
||||
@@ -167,33 +173,40 @@ export default {
|
||||
// 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) => ({
|
||||
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) => {
|
||||
let amount = 0;
|
||||
|
||||
if (account.type.normal === 'credit') {
|
||||
amount += transaction.credit - transaction.credit;
|
||||
} else if (account.type.normal === 'debit') {
|
||||
amount += transaction.debit - transaction.credit;
|
||||
}
|
||||
return {
|
||||
...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),
|
||||
},
|
||||
})),
|
||||
];
|
||||
amount: formatNumber(amount),
|
||||
};
|
||||
}),
|
||||
],
|
||||
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,
|
||||
query: { ...filter },
|
||||
accounts: items,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -206,7 +219,7 @@ export default {
|
||||
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('display_columns_by').optional().isIn(['total', '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(),
|
||||
@@ -220,7 +233,7 @@ export default {
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
display_columns_by: 'year',
|
||||
display_columns_by: 'total',
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
number_format: {
|
||||
@@ -228,11 +241,11 @@ export default {
|
||||
divide_1000: false,
|
||||
},
|
||||
none_zero: false,
|
||||
basis: 'cash',
|
||||
...req.query,
|
||||
};
|
||||
|
||||
const balanceSheetTypes = await AccountType.query()
|
||||
.where('balance_sheet', true);
|
||||
const balanceSheetTypes = await AccountType.query().where('balance_sheet', true);
|
||||
|
||||
// Fetch all balance sheet accounts.
|
||||
const accounts = await Account.query()
|
||||
@@ -249,51 +262,92 @@ export default {
|
||||
|
||||
// Account balance formmatter based on the given query.
|
||||
const balanceFormatter = formatNumberClosure(filter.number_format);
|
||||
const filterDateType = filter.display_columns_by === 'total'
|
||||
? 'day' : filter.display_columns_by;
|
||||
|
||||
// Gets the date range set from start to end date.
|
||||
const dateRangeSet = dateRangeCollection(
|
||||
filter.from_date,
|
||||
filter.to_date,
|
||||
filter.display_columns_by,
|
||||
filterDateType,
|
||||
);
|
||||
|
||||
// Retrieve the asset balance sheet.
|
||||
const assets = [
|
||||
...accounts
|
||||
.filter((account) => (
|
||||
account.type.normal === 'debit'
|
||||
const assets = accounts
|
||||
.filter((account) => (
|
||||
account.type.normal === 'debit'
|
||||
&& (account.transactions.length > 0 || !filter.none_zero)
|
||||
))
|
||||
.map((account) => ({
|
||||
))
|
||||
.map((account) => {
|
||||
// Calculates the closing balance to the given date.
|
||||
const closingBalance = journalEntries.getClosingBalance(account.id, filter.to_date);
|
||||
const type = filter.display_columns_by;
|
||||
|
||||
return {
|
||||
...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) };
|
||||
}),
|
||||
})),
|
||||
];
|
||||
...(type !== 'total') ? {
|
||||
periods_balance: dateRangeSet.map((date) => {
|
||||
const balance = journalEntries.getClosingBalance(account.id, date, filterDateType);
|
||||
|
||||
return {
|
||||
date,
|
||||
formatted_amount: balanceFormatter(balance),
|
||||
amount: balance,
|
||||
};
|
||||
}),
|
||||
} : {},
|
||||
balance: {
|
||||
formatted_amount: balanceFormatter(closingBalance),
|
||||
amount: closingBalance,
|
||||
date: filter.to_date,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Retrieve liabilities and equity balance sheet.
|
||||
const liabilitiesEquity = [
|
||||
...accounts
|
||||
.filter((account) => (
|
||||
account.type.normal === 'credit'
|
||||
const liabilitiesEquity = accounts
|
||||
.filter((account) => (
|
||||
account.type.normal === 'credit'
|
||||
&& (account.transactions.length > 0 || !filter.none_zero)
|
||||
))
|
||||
.map((account) => ({
|
||||
))
|
||||
.map((account) => {
|
||||
// Calculates the closing balance to the given date.
|
||||
const closingBalance = journalEntries.getClosingBalance(account.id, filter.to_date);
|
||||
const type = filter.display_columns_by;
|
||||
|
||||
return {
|
||||
...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) };
|
||||
}),
|
||||
})),
|
||||
];
|
||||
...(type !== 'total') ? {
|
||||
periods_balance: dateRangeSet.map((date) => {
|
||||
const balance = journalEntries.getClosingBalance(account.id, date, filterDateType);
|
||||
|
||||
return {
|
||||
date,
|
||||
formatted_amount: balanceFormatter(balance),
|
||||
amount: balance,
|
||||
};
|
||||
}),
|
||||
} : {},
|
||||
balance: {
|
||||
formattedAmount: balanceFormatter(closingBalance),
|
||||
amount: closingBalance,
|
||||
date: filter.to_date,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
query: { ...filter },
|
||||
columns: { ...dateRangeSet },
|
||||
balance_sheet: {
|
||||
assets,
|
||||
liabilities_equity: liabilitiesEquity,
|
||||
assets: {
|
||||
title: 'Assets',
|
||||
accounts: [...assets],
|
||||
},
|
||||
liabilities_equity: {
|
||||
title: 'Liabilities & Equity',
|
||||
accounts: [...liabilitiesEquity],
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -363,7 +417,7 @@ export default {
|
||||
};
|
||||
});
|
||||
return res.status(200).send({
|
||||
meta: { ...filter },
|
||||
query: { ...filter },
|
||||
items: [...items],
|
||||
});
|
||||
},
|
||||
@@ -381,8 +435,12 @@ export default {
|
||||
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(),
|
||||
query('display_columns_type').optional().isIn([
|
||||
'total', 'date_periods',
|
||||
]),
|
||||
query('display_columns_by').optional().isIn([
|
||||
'year', 'month', 'week', 'day', 'quarter',
|
||||
]),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -401,19 +459,22 @@ export default {
|
||||
},
|
||||
basis: 'accural',
|
||||
none_zero: false,
|
||||
display_columns_by: 'month',
|
||||
display_columns_type: 'total',
|
||||
display_columns_by: 'total',
|
||||
...req.query,
|
||||
};
|
||||
const incomeStatementTypes = await AccountType.query().where('income_sheet', true);
|
||||
|
||||
// Fetch all income accounts from storage.
|
||||
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;
|
||||
});
|
||||
// Filter all none zero accounts if it was enabled.
|
||||
const filteredAccounts = accounts.filter((account) => (
|
||||
account.transactions.length > 0 || !filter.none_zero
|
||||
));
|
||||
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
||||
const journalEntries = new JournalPoster();
|
||||
journalEntries.loadEntries(journalEntriesCollected);
|
||||
@@ -427,75 +488,130 @@ export default {
|
||||
filter.to_date,
|
||||
filter.display_columns_by,
|
||||
);
|
||||
const accountsIncome = filteredAccounts
|
||||
.filter((account) => account.type.normal === 'credit')
|
||||
.map((account) => ({
|
||||
|
||||
const accountsMapper = (incomeExpenseAccounts) => (
|
||||
incomeExpenseAccounts.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) };
|
||||
}),
|
||||
}));
|
||||
// Total closing balance of the account.
|
||||
...(filter.display_columns_type === 'total') && {
|
||||
total: (() => {
|
||||
const amount = journalEntries.getClosingBalance(account.id, filter.to_date);
|
||||
return { amount, date: filter.to_date, formatted_amount: numberFormatter(amount) };
|
||||
})(),
|
||||
},
|
||||
// Date periods when display columns type `periods`.
|
||||
...(filter.display_columns_type === 'date_periods') && {
|
||||
periods: dateRangeSet.map((date) => {
|
||||
const type = filter.display_columns_by;
|
||||
const amount = journalEntries.getClosingBalance(account.id, date, type);
|
||||
|
||||
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, amount, formatted_amount: numberFormatter(amount) };
|
||||
}),
|
||||
},
|
||||
})));
|
||||
|
||||
return { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
}),
|
||||
}));
|
||||
const totalAccountsReducer = (incomeExpenseAccounts) => (
|
||||
incomeExpenseAccounts.reduce((acc, account) => {
|
||||
const amount = (account) ? account.total.amount : 0;
|
||||
return amount + acc;
|
||||
}, 0));
|
||||
|
||||
// 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;
|
||||
}, {});
|
||||
const accountsIncome = accountsMapper(filteredAccounts
|
||||
.filter((account) => account.type.normal === 'credit'));
|
||||
|
||||
// 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;
|
||||
}, {});
|
||||
const accountsExpenses = accountsMapper(filteredAccounts
|
||||
.filter((account) => account.type.normal === 'debit'));
|
||||
|
||||
// @return {Array}
|
||||
const totalPeriodsMapper = (incomeExpenseAccounts) => (
|
||||
Object.values(dateRangeSet.reduce((acc, date, index) => {
|
||||
let amount = 0;
|
||||
|
||||
incomeExpenseAccounts.forEach((account) => {
|
||||
const currentDate = account.periods[index];
|
||||
amount += currentDate.amount || 0;
|
||||
});
|
||||
acc[date] = { date, amount, formatted_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];
|
||||
// @return {Array}
|
||||
const netIncomePeriodsMapper = (totalIncomeAcocunts, totalExpenseAccounts) => (
|
||||
dateRangeSet.map((date, index) => {
|
||||
const totalIncome = totalIncomeAcocunts[index];
|
||||
const totalExpenses = totalExpenseAccounts[index];
|
||||
|
||||
let amount = totalIncome.rawAmount || 0;
|
||||
amount -= totalExpenses.rawAmount || 0;
|
||||
return { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
});
|
||||
let amount = totalIncome.amount || 0;
|
||||
amount -= totalExpenses.amount || 0;
|
||||
return { date, amount, formatted_amount: numberFormatter(amount) };
|
||||
}));
|
||||
|
||||
// @return {Object}
|
||||
const netIncomeTotal = (totalIncome, totalExpenses) => {
|
||||
const netIncomeAmount = totalIncome.amount - totalExpenses.amount;
|
||||
return { amount: netIncomeAmount, formatted_amount: netIncomeAmount };
|
||||
};
|
||||
|
||||
const totalIncomeAccounts = totalAccountsReducer(accountsIncome);
|
||||
const totalExpensesAccounts = totalAccountsReducer(accountsExpenses);
|
||||
|
||||
const incomeResponse = {
|
||||
entry_normal: 'credit',
|
||||
accounts: accountsIncome,
|
||||
|
||||
...(filter.display_columns_type === 'total') && {
|
||||
total: {
|
||||
amount: totalIncomeAccounts,
|
||||
date: filter.to_date,
|
||||
formatted_amount: numberFormatter(totalIncomeAccounts),
|
||||
},
|
||||
},
|
||||
...(filter.display_columns_type === 'date_periods') && {
|
||||
total_periods: [
|
||||
...totalPeriodsMapper(accountsIncome),
|
||||
],
|
||||
},
|
||||
};
|
||||
const expenseResponse = {
|
||||
entry_normal: 'debit',
|
||||
accounts: accountsExpenses,
|
||||
|
||||
...(filter.display_columns_type === 'total') && {
|
||||
total: {
|
||||
amount: totalExpensesAccounts,
|
||||
date: filter.to_date,
|
||||
formatted_amount: numberFormatter(totalExpensesAccounts),
|
||||
},
|
||||
},
|
||||
...(filter.display_columns_type === 'date_periods') && {
|
||||
total_periods: [
|
||||
...totalPeriodsMapper(accountsExpenses),
|
||||
],
|
||||
},
|
||||
};
|
||||
const netIncomeResponse = {
|
||||
...(filter.display_columns_type === 'total') && {
|
||||
total: {
|
||||
...netIncomeTotal(incomeResponse.total, expenseResponse.total),
|
||||
},
|
||||
},
|
||||
...(filter.display_columns_type === 'date_periods') && {
|
||||
total_periods: [
|
||||
...netIncomePeriodsMapper(
|
||||
incomeResponse.total_periods,
|
||||
expenseResponse.total_periods,
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
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,
|
||||
query: { ...filter },
|
||||
columns: [...dateRangeSet],
|
||||
income: incomeResponse,
|
||||
expenses: expenseResponse,
|
||||
net_income: netIncomeResponse,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,6 +17,11 @@ export default class Account extends BaseModel {
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
filterAccounts(query, accountIds) {
|
||||
if (accountIds.length > 0) {
|
||||
query.whereIn('id', accountIds);
|
||||
}
|
||||
},
|
||||
filterAccountTypes(query, typesIds) {
|
||||
if (typesIds.length > 0) {
|
||||
query.whereIn('accoun_type_id', typesIds);
|
||||
|
||||
@@ -3,6 +3,7 @@ import moment from 'moment';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
import AccountBalance from '@/models/AccountBalance';
|
||||
import {promiseSerial} from '@/utils';
|
||||
|
||||
export default class JournalPoster {
|
||||
/**
|
||||
@@ -125,12 +126,12 @@ export default class JournalPoster {
|
||||
this.entries.forEach((entry) => {
|
||||
const oper = AccountTransaction.query().insert({
|
||||
accountId: entry.account,
|
||||
...pick(entry, ['credit', 'debit', 'transactionType',
|
||||
...pick(entry, ['credit', 'debit', 'transactionType', 'date', 'userId',
|
||||
'referenceType', 'referenceId', 'note']),
|
||||
});
|
||||
saveOperations.push(oper);
|
||||
saveOperations.push(() => oper);
|
||||
});
|
||||
await Promise.all(saveOperations);
|
||||
await promiseSerial(saveOperations);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,6 +66,12 @@ const mapValuesDeep = (v, callback) => (
|
||||
? _.mapValues(v, v => mapValuesDeep(v, callback))
|
||||
: callback(v));
|
||||
|
||||
|
||||
const promiseSerial = (funcs) => {
|
||||
return funcs.reduce((promise, func) => promise.then((result) => func().then(Array.prototype.concat.bind(result))),
|
||||
Promise.resolve([]));
|
||||
}
|
||||
|
||||
export {
|
||||
hashPassword,
|
||||
origin,
|
||||
@@ -73,4 +79,5 @@ export {
|
||||
dateRangeFormat,
|
||||
mapValuesDeep,
|
||||
mapKeysDeep,
|
||||
promiseSerial,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user