mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
feat: Receivable and payable aging summary financial statement.
This commit is contained in:
@@ -3,6 +3,7 @@ exports.up = (knex) => {
|
||||
return knex.schema.createTable('account_types', (table) => {
|
||||
table.increments();
|
||||
table.string('name');
|
||||
table.string('key');
|
||||
table.string('normal');
|
||||
table.string('root_type');
|
||||
table.boolean('balance_sheet');
|
||||
|
||||
@@ -7,6 +7,7 @@ exports.up = function (knex) {
|
||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||
table.boolean('favourite');
|
||||
table.string('roles_logic_expression');
|
||||
table.timestamps();
|
||||
}).raw('ALTER TABLE `VIEWS` AUTO_INCREMENT = 1000').then(() => {
|
||||
return knex.seed.run({
|
||||
specific: 'seed_views.js',
|
||||
|
||||
@@ -8,6 +8,8 @@ exports.up = function(knex) {
|
||||
table.string('reference_type');
|
||||
table.integer('reference_id');
|
||||
table.integer('account_id').unsigned();
|
||||
table.string('contact_type').nullable();
|
||||
table.integer('contact_id').unsigned().nullable();
|
||||
table.string('note');
|
||||
table.boolean('draft').defaultTo(false);
|
||||
table.integer('user_id').unsigned();
|
||||
|
||||
@@ -4,6 +4,7 @@ exports.up = function(knex) {
|
||||
table.increments();
|
||||
table.string('currency_name');
|
||||
table.string('currency_code', 4);
|
||||
table.timestamps();
|
||||
}).raw('ALTER TABLE `CURRENCIES` AUTO_INCREMENT = 1000');
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ exports.up = function(knex) {
|
||||
table.string('currency_code', 4);
|
||||
table.decimal('exchange_rate');
|
||||
table.date('date');
|
||||
table.timestamps();
|
||||
}).raw('ALTER TABLE `EXCHANGE_RATES` AUTO_INCREMENT = 1000');
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ exports.up = function(knex) {
|
||||
|
||||
table.text('note');
|
||||
table.boolean('active').defaultTo(true);
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ exports.up = function(knex) {
|
||||
|
||||
table.text('note');
|
||||
table.boolean('active').defaultTo(true);
|
||||
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ exports.seed = (knex) => {
|
||||
{
|
||||
id: 1,
|
||||
name: 'Fixed Asset',
|
||||
key: 'fixed_asset',
|
||||
normal: 'debit',
|
||||
root_type: 'asset',
|
||||
balance_sheet: true,
|
||||
@@ -16,6 +17,7 @@ exports.seed = (knex) => {
|
||||
{
|
||||
id: 2,
|
||||
name: 'Current Asset',
|
||||
key: 'current_asset',
|
||||
normal: 'debit',
|
||||
root_type: 'asset',
|
||||
balance_sheet: true,
|
||||
@@ -24,6 +26,7 @@ exports.seed = (knex) => {
|
||||
{
|
||||
id: 3,
|
||||
name: 'Long Term Liability',
|
||||
key: 'long_term_liability',
|
||||
normal: 'credit',
|
||||
root_type: 'liability',
|
||||
balance_sheet: false,
|
||||
@@ -32,6 +35,7 @@ exports.seed = (knex) => {
|
||||
{
|
||||
id: 4,
|
||||
name: 'Current Liability',
|
||||
key: 'current_liability',
|
||||
normal: 'credit',
|
||||
root_type: 'liability',
|
||||
balance_sheet: false,
|
||||
@@ -40,6 +44,7 @@ exports.seed = (knex) => {
|
||||
{
|
||||
id: 5,
|
||||
name: 'Equity',
|
||||
key: 'equity',
|
||||
normal: 'credit',
|
||||
root_type: 'equity',
|
||||
balance_sheet: true,
|
||||
@@ -48,6 +53,7 @@ exports.seed = (knex) => {
|
||||
{
|
||||
id: 6,
|
||||
name: 'Expense',
|
||||
key: 'expense',
|
||||
normal: 'debit',
|
||||
root_type: 'expense',
|
||||
balance_sheet: false,
|
||||
@@ -56,6 +62,7 @@ exports.seed = (knex) => {
|
||||
{
|
||||
id: 7,
|
||||
name: 'Income',
|
||||
key: 'income',
|
||||
normal: 'credit',
|
||||
root_type: 'income',
|
||||
balance_sheet: false,
|
||||
@@ -64,6 +71,7 @@ exports.seed = (knex) => {
|
||||
{
|
||||
id: 8,
|
||||
name: 'Accounts Receivable',
|
||||
key: 'accounts_receivable',
|
||||
normal: 'debit',
|
||||
root_type: 'asset',
|
||||
balance_sheet: true,
|
||||
@@ -72,6 +80,7 @@ exports.seed = (knex) => {
|
||||
{
|
||||
id: 9,
|
||||
name: 'Accounts Payable',
|
||||
key: 'accounts_payable',
|
||||
normal: 'credit',
|
||||
root_type: 'liability',
|
||||
balance_sheet: true,
|
||||
|
||||
@@ -103,7 +103,29 @@ exports.seed = (knex) => {
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Accounts Receivable',
|
||||
account_type_id: 8,
|
||||
parent_account_id: null,
|
||||
code: '1000',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: 'Accounts Payable',
|
||||
account_type_id: 9,
|
||||
parent_account_id: null,
|
||||
code: '1000',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -33,6 +33,8 @@ export default {
|
||||
asyncMiddleware(this.manualJournals.handler));
|
||||
|
||||
router.post('/make-journal-entries',
|
||||
this.validateMediaIds,
|
||||
this.validateContactEntries,
|
||||
this.makeJournalEntries.validation,
|
||||
asyncMiddleware(this.makeJournalEntries.handler));
|
||||
|
||||
@@ -41,6 +43,8 @@ export default {
|
||||
asyncMiddleware(this.publishManualJournal.handler));
|
||||
|
||||
router.post('/manual-journals/:id',
|
||||
this.validateMediaIds,
|
||||
this.validateContactEntries,
|
||||
this.editManualJournal.validation,
|
||||
asyncMiddleware(this.editManualJournal.handler));
|
||||
|
||||
@@ -168,6 +172,114 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate media ids.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {Function} next -
|
||||
*/
|
||||
async validateMediaIds(req, res, next) {
|
||||
const form = { media_ids: [], ...req.body };
|
||||
const { Media } = req.models;
|
||||
const errorReasons = [];
|
||||
|
||||
// Validate if media ids was not already exists on the storage.
|
||||
if (form.media_ids.length > 0) {
|
||||
const storedMedia = await Media.query().whereIn('id', form.media_ids);
|
||||
const notFoundMedia = difference(form.media_ids, storedMedia.map((m) => m.id));
|
||||
|
||||
if (notFoundMedia.length > 0) {
|
||||
errorReasons.push({ type: 'MEDIA.IDS.NOT.FOUND', code: 400, ids: notFoundMedia });
|
||||
}
|
||||
}
|
||||
req.errorReasons = Array.isArray(req.errorReasons) && req.errorReasons.length
|
||||
? req.errorReasons.push(...errorReasons) : errorReasons;
|
||||
next();
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate form entries with contact customers and vendors.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateContactEntries(req, res, next) {
|
||||
const form = { entries: [], ...req.body };
|
||||
const { AccountType, Vendor, Customer } = req.models;
|
||||
const errorReasons = [];
|
||||
|
||||
// Validate the entries contact type and ids.
|
||||
const customersContacts = form.entries.filter(e => e.contact_type === 'customer');
|
||||
const vendorsContacts = form.entries.filter(e => e.contact_type === 'vendor');
|
||||
|
||||
const accountsTypes = await AccountType.query();
|
||||
|
||||
const payableAccountsType = accountsTypes.find(t => t.key === 'accounts_payable');;
|
||||
const receivableAccountsType = accountsTypes.find(t => t.key === 'accounts_receivable');
|
||||
|
||||
// Validate customers contacts.
|
||||
if (customersContacts.length > 0) {
|
||||
const customersContactsIds = customersContacts.map(c => c.contact_id);
|
||||
const storedContacts = await Customer.query().whereIn('id', customersContactsIds);
|
||||
|
||||
const storedContactsIds = storedContacts.map(c => c.id);
|
||||
const formEntriesCustomersIds = form.entries.filter(e => e.contact_type === 'customer');
|
||||
|
||||
const notFoundContactsIds = difference(
|
||||
formEntriesCustomersIds.map(c => c.contact_id),
|
||||
storedContactsIds,
|
||||
);
|
||||
if (notFoundContactsIds.length > 0) {
|
||||
errorReasons.push({ type: 'CUSTOMERS.CONTACTS.NOT.FOUND', code: 500, ids: notFoundContactsIds });
|
||||
}
|
||||
|
||||
const notReceivableAccounts = formEntriesCustomersIds.filter(
|
||||
c => receivableAccountsType && c.contact_id !== receivableAccountsType.id);
|
||||
|
||||
if (notReceivableAccounts.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'CUSTOMERS.ACCOUNTS.NOT.RECEIVABLE.TYPE',
|
||||
code: 700,
|
||||
indexes: notReceivableAccounts.map(a => a.index),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validate vendors contacts.
|
||||
if (vendorsContacts.length > 0) {
|
||||
const vendorsContactsIds = vendorsContacts.map(c => c.contact_id);
|
||||
const storedContacts = await Vendor.query().where('id', vendorsContactsIds);
|
||||
|
||||
const storedContactsIds = storedContacts.map(c => c.id);
|
||||
const formEntriesVendorsIds = form.entries.filter(e => e.contact_type === 'vendor');
|
||||
|
||||
const notFoundContactsIds = difference(
|
||||
formEntriesVendorsIds.map(v => v.contact_id),
|
||||
storedContactsIds,
|
||||
);
|
||||
if (notFoundContactsIds.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'VENDORS.CONTACTS.NOT.FOUND', code: 600, ids: notFoundContactsIds,
|
||||
});
|
||||
}
|
||||
const notPayableAccounts = formEntriesVendorsIds.filter(
|
||||
v => payableAccountsType && v.contact_id === payableAccountsType.id
|
||||
);
|
||||
if (notPayableAccounts.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'VENDORS.ACCOUNTS.NOT.PAYABLE.TYPE',
|
||||
code: 800,
|
||||
indexes: notPayableAccounts.map(a => a.index),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
req.errorReasons = Array.isArray(req.errorReasons) && req.errorReasons.length
|
||||
? req.errorReasons.push(...errorReasons) : errorReasons;
|
||||
|
||||
next();
|
||||
},
|
||||
|
||||
/**
|
||||
* Make journal entrires.
|
||||
*/
|
||||
@@ -180,10 +292,13 @@ export default {
|
||||
check('description').optional().trim().escape(),
|
||||
check('status').optional().isBoolean().toBoolean(),
|
||||
check('entries').isArray({ min: 2 }),
|
||||
check('entries.*.index').exists().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(),
|
||||
check('entries.*.contact_id').optional().isNumeric().toInt(),
|
||||
check('entries.*.contact_type').optional().isIn(['vendor', 'customer']),
|
||||
check('media_ids').optional().isArray(),
|
||||
check('media_ids.*').exists().isNumeric().toInt(),
|
||||
],
|
||||
@@ -202,13 +317,17 @@ export default {
|
||||
media_ids: [],
|
||||
...req.body,
|
||||
};
|
||||
const { ManualJournal, Account, Media, MediaLink } = req.models;
|
||||
const {
|
||||
ManualJournal,
|
||||
Account,
|
||||
MediaLink,
|
||||
} = req.models;
|
||||
|
||||
let totalCredit = 0;
|
||||
let totalDebit = 0;
|
||||
|
||||
const { user } = req;
|
||||
const errorReasons = [];
|
||||
const errorReasons = [...(req.errorReasons || [])];
|
||||
const entries = form.entries.filter((entry) => (entry.credit || entry.debit));
|
||||
const formattedDate = moment(form.date).format('YYYY-MM-DD');
|
||||
|
||||
@@ -229,23 +348,18 @@ export default {
|
||||
if (totalCredit !== totalDebit) {
|
||||
errorReasons.push({ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 100 });
|
||||
}
|
||||
const accountsIds = entries.map((entry) => entry.account_id);
|
||||
const formEntriesAccountsIds = entries.map((entry) => entry.account_id);
|
||||
const formEntriesContactsIds = entries.map((entry) => entry.contact_id);
|
||||
|
||||
const accounts = await Account.query()
|
||||
.whereIn('id', accountsIds)
|
||||
.whereIn('id', formEntriesAccountsIds)
|
||||
.withGraphFetched('type')
|
||||
.remember();
|
||||
|
||||
const storedAccountsIds = accounts.map((account) => account.id);
|
||||
|
||||
if (form.media_ids.length > 0) {
|
||||
const storedMedia = await Media.query().whereIn('id', form.media_ids);
|
||||
const notFoundMedia = difference(form.media_ids, storedMedia.map((m) => m.id));
|
||||
|
||||
if (notFoundMedia.length > 0) {
|
||||
errorReasons.push({ type: 'MEDIA.IDS.NOT.FOUND', code: 400, ids: notFoundMedia });
|
||||
}
|
||||
}
|
||||
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
||||
|
||||
if (difference(formEntriesAccountsIds, storedAccountsIds).length > 0) {
|
||||
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||
}
|
||||
|
||||
@@ -255,10 +369,11 @@ export default {
|
||||
if (journalNumber.length > 0) {
|
||||
errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
|
||||
}
|
||||
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// Save manual journal transaction.
|
||||
// Save manual journal tansaction.
|
||||
const manualJournal = await ManualJournal.query().insert({
|
||||
reference: form.reference,
|
||||
transaction_type: 'Journal',
|
||||
@@ -282,6 +397,8 @@ export default {
|
||||
referenceType: 'Journal',
|
||||
referenceId: manualJournal.id,
|
||||
accountNormal: account.type.normal,
|
||||
contactType: entry.contact_type,
|
||||
contactId: entry.contact_id,
|
||||
note: entry.note,
|
||||
date: formattedDate,
|
||||
userId: user.id,
|
||||
@@ -356,6 +473,8 @@ export default {
|
||||
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.account_id').isNumeric().toInt(),
|
||||
check('entries.*.contact_id').optional().isNumeric().toInt(),
|
||||
check('entries.*.contact_type').optional().isIn(['vendor', 'customer']).isNumeric().toInt(),
|
||||
check('entries.*.note').optional(),
|
||||
check('media_ids').optional().isArray(),
|
||||
check('media_ids.*').isNumeric().toInt(),
|
||||
@@ -393,7 +512,7 @@ export default {
|
||||
let totalDebit = 0;
|
||||
|
||||
const { user } = req;
|
||||
const errorReasons = [];
|
||||
const errorReasons = [...(req.errorReasons || [])];
|
||||
const entries = form.entries.filter((entry) => (entry.credit || entry.debit));
|
||||
const formattedDate = moment(form.date).format('YYYY-MM-DD');
|
||||
|
||||
@@ -431,16 +550,6 @@ export default {
|
||||
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
||||
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||
}
|
||||
|
||||
// Validate if media ids was not already exists on the storage.
|
||||
if (form.media_ids.length > 0) {
|
||||
const storedMedia = await Media.query().whereIn('id', form.media_ids);
|
||||
const notFoundMedia = difference(form.media_ids, storedMedia.map((m) => m.id));
|
||||
|
||||
if (notFoundMedia.length > 0) {
|
||||
errorReasons.push({ type: 'MEDIA.IDS.NOT.FOUND', code: 400, ids: notFoundMedia });
|
||||
}
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
|
||||
6
server/src/http/controllers/BaseController.js
Normal file
6
server/src/http/controllers/BaseController.js
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
|
||||
export default class BaseController {
|
||||
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import TrialBalanceSheetController from './FinancialStatements/TrialBalanceSheet
|
||||
import GeneralLedgerController from './FinancialStatements/generalLedger';
|
||||
import JournalSheetController from './FinancialStatements/JournalSheet';
|
||||
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
||||
import ReceivableAgingSummary from './FinancialStatements/ReceivableAgingSummary';
|
||||
import PayableAgingSummary from './FinancialStatements/PayableAgingSummary';
|
||||
|
||||
export default {
|
||||
/**
|
||||
@@ -18,6 +20,8 @@ export default {
|
||||
router.use('/general_ledger', GeneralLedgerController.router());
|
||||
router.use('/trial_balance_sheet', TrialBalanceSheetController.router());
|
||||
router.use('/journal', JournalSheetController.router());
|
||||
router.use('/receivable_aging_summary', ReceivableAgingSummary.router());
|
||||
router.use('/payable_aging_summary', PayableAgingSummary.router());
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
106
server/src/http/controllers/FinancialStatements/AgingReport.js
Normal file
106
server/src/http/controllers/FinancialStatements/AgingReport.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import moment from 'moment';
|
||||
import { validationResult } from 'express-validator';
|
||||
import { omit, reverse } from 'lodash';
|
||||
import BaseController from '@/http/controllers/BaseController';
|
||||
|
||||
export default class AgingReport extends BaseController{
|
||||
|
||||
/**
|
||||
* Express validator middleware.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
static validateResults(req, res, next) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Array} agingPeriods
|
||||
* @param {Numeric} customerBalance
|
||||
*/
|
||||
static contactAgingBalance(agingPeriods, receivableTotalCredit) {
|
||||
let prevAging = 0;
|
||||
let receivableCredit = receivableTotalCredit;
|
||||
let diff = receivableCredit;
|
||||
|
||||
const periods = reverse(agingPeriods).map((agingPeriod) => {
|
||||
const agingAmount = (agingPeriod.closingBalance - prevAging);
|
||||
const subtract = Math.min(diff, agingAmount);
|
||||
diff -= Math.min(agingAmount, diff);
|
||||
|
||||
const total = Math.max(agingAmount - subtract, 0);
|
||||
|
||||
const output = {
|
||||
...omit(agingPeriod, ['closingBalance']),
|
||||
total,
|
||||
};
|
||||
prevAging = agingPeriod.closingBalance;
|
||||
return output;
|
||||
});
|
||||
return reverse(periods);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} asDay
|
||||
* @param {*} agingDaysBefore
|
||||
* @param {*} agingPeriodsFreq
|
||||
*/
|
||||
static agingRangePeriods(asDay, agingDaysBefore, agingPeriodsFreq) {
|
||||
const totalAgingDays = agingDaysBefore * agingPeriodsFreq;
|
||||
const startAging = moment(asDay).startOf('day');
|
||||
const endAging = startAging.clone().subtract('days', totalAgingDays).endOf('day');
|
||||
|
||||
const agingPeriods = [];
|
||||
const startingAging = startAging.clone();
|
||||
|
||||
let beforeDays = 1;
|
||||
let toDays = 0;
|
||||
|
||||
while (startingAging > endAging) {
|
||||
const currentAging = startingAging.clone();
|
||||
startingAging.subtract('days', agingDaysBefore).endOf('day');
|
||||
toDays += agingDaysBefore;
|
||||
|
||||
agingPeriods.push({
|
||||
from_period: moment(currentAging).toDate(),
|
||||
to_period: moment(startingAging).toDate(),
|
||||
before_days: beforeDays === 1 ? 0 : beforeDays,
|
||||
to_days: toDays,
|
||||
...(startingAging.valueOf() === endAging.valueOf()) ? {
|
||||
to_period: null,
|
||||
to_days: null,
|
||||
} : {},
|
||||
});
|
||||
beforeDays += agingDaysBefore;
|
||||
}
|
||||
return agingPeriods;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} filter
|
||||
*/
|
||||
static formatNumberClosure(filter) {
|
||||
return (balance) => {
|
||||
let formattedBalance = parseFloat(balance);
|
||||
|
||||
if (filter.no_cents) {
|
||||
formattedBalance = parseInt(formattedBalance, 10);
|
||||
}
|
||||
if (filter.divide_1000) {
|
||||
formattedBalance /= 1000;
|
||||
}
|
||||
return formattedBalance;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import express from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import AgingReport from '@/http/controllers/FinancialStatements/AgingReport';
|
||||
import moment from 'moment';
|
||||
|
||||
export default class PayableAgingSummary extends AgingReport {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
static router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.payableAgingSummaryRoles(),
|
||||
this.validateResults,
|
||||
asyncMiddleware(this.validateVendorsIds.bind(this)),
|
||||
asyncMiddleware(this.payableAgingSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the report vendors ids query.
|
||||
*/
|
||||
static async validateVendorsIds(req, res, next) {
|
||||
const { Vendor } = req.models;
|
||||
|
||||
const filter = {
|
||||
vendors_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.vendors_ids)) {
|
||||
filter.vendors_ids = [filter.vendors_ids];
|
||||
}
|
||||
if (filter.vendors_ids.length > 0) {
|
||||
const storedCustomers = await Vendor.query().whereIn(
|
||||
'id',
|
||||
filter.vendors_ids
|
||||
);
|
||||
const storedCustomersIds = storedCustomers.map((c) => c.id);
|
||||
const notStoredCustomersIds = difference(
|
||||
storedCustomersIds,
|
||||
filter,
|
||||
vendors_ids
|
||||
);
|
||||
|
||||
if (notStoredCustomersIds.length) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'VENDORS.IDS.NOT.FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receivable aging summary validation roles.
|
||||
*/
|
||||
static payableAgingSummaryRoles() {
|
||||
return [
|
||||
query('as_date').optional().isISO8601(),
|
||||
query('aging_days_before').optional().isNumeric().toInt(),
|
||||
query('aging_periods').optional().isNumeric().toInt(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||
query('vendors_ids.*').isNumeric().toInt(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
*/
|
||||
static async payableAgingSummary(req, res) {
|
||||
const { Customer, Account, AccountTransaction, AccountType } = req.models;
|
||||
const storedVendors = await Customer.query();
|
||||
|
||||
const filter = {
|
||||
as_date: moment().format('YYYY-MM-DD'),
|
||||
aging_days_before: 30,
|
||||
aging_periods: 3,
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
...req.query,
|
||||
};
|
||||
const accountsReceivableType = await AccountType.query()
|
||||
.where('key', 'accounts_payable')
|
||||
.first();
|
||||
|
||||
const accountsReceivable = await Account.query()
|
||||
.where('account_type_id', accountsReceivableType.id)
|
||||
.remember()
|
||||
.first();
|
||||
|
||||
const transactions = await AccountTransaction.query()
|
||||
.modify('filterDateRange', null, filter.as_date)
|
||||
.where('account_id', accountsReceivable.id)
|
||||
.remember();
|
||||
|
||||
const journalPoster = new JournalPoster();
|
||||
journalPoster.loadEntries(transactions);
|
||||
|
||||
const agingPeriods = this.agingRangePeriods(
|
||||
filter.as_date,
|
||||
filter.aging_days_before,
|
||||
filter.aging_periods
|
||||
);
|
||||
// Total amount formmatter based on the given query.
|
||||
const totalFormatter = formatNumberClosure(filter.number_format);
|
||||
|
||||
const vendors = storedVendors.map((vendor) => {
|
||||
// Calculate the trial balance total of the given vendor.
|
||||
const vendorBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
vendor.id,
|
||||
'vendor'
|
||||
);
|
||||
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||
// Calculate the trial balance between the given date period.
|
||||
const agingTrialBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
vendor.id,
|
||||
'vendor',
|
||||
agingPeriod.from_period
|
||||
);
|
||||
return {
|
||||
...agingPeriod,
|
||||
closingBalance: agingTrialBalance.debit,
|
||||
};
|
||||
});
|
||||
const aging = this.contactAgingBalance(
|
||||
agingClosingBalance,
|
||||
vendorBalance.credit
|
||||
);
|
||||
return {
|
||||
vendor_name: vendor.displayName,
|
||||
aging: aging.map((item) => ({
|
||||
...item,
|
||||
formatted_total: totalFormatter(item.total),
|
||||
})),
|
||||
total: vendorBalance.balance,
|
||||
formatted_total: totalFormatted(vendorBalance.balance),
|
||||
};
|
||||
});
|
||||
|
||||
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||
const closingTrialBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
null,
|
||||
'vendor',
|
||||
agingPeriod.from_period
|
||||
);
|
||||
return {
|
||||
...agingPeriod,
|
||||
closingBalance: closingTrialBalance.balance,
|
||||
};
|
||||
});
|
||||
|
||||
const totalClosingBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
null,
|
||||
'vendor'
|
||||
);
|
||||
const agingTotal = this.contactAgingBalance(
|
||||
agingClosingBalance,
|
||||
totalClosingBalance.credit
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
columns: [ ...agingPeriods ],
|
||||
aging: {
|
||||
vendors,
|
||||
total: [
|
||||
...agingTotal.map((item) => ({
|
||||
...item,
|
||||
formatted_total: totalFormatter(item.total),
|
||||
})),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import express from 'express';
|
||||
import { query, oneOf } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import AgingReport from '@/http/controllers/FinancialStatements/AgingReport';
|
||||
import moment from 'moment';
|
||||
|
||||
export default class ReceivableAgingSummary extends AgingReport {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
static router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.receivableAgingSummaryRoles,
|
||||
this.validateResults,
|
||||
asyncMiddleware(this.validateCustomersIds.bind(this)),
|
||||
asyncMiddleware(this.receivableAgingSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the report customers ids query.
|
||||
*/
|
||||
static async validateCustomersIds(req, res, next) {
|
||||
const { Customer } = req.models;
|
||||
|
||||
console.log(req.query);
|
||||
|
||||
const filter = {
|
||||
customer_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.customer_ids)) {
|
||||
filter.customer_ids = [filter.customer_ids];
|
||||
}
|
||||
if (filter.customer_ids.length > 0) {
|
||||
const storedCustomers = await Customer.query().whereIn(
|
||||
'id',
|
||||
filter.customer_ids
|
||||
);
|
||||
const storedCustomersIds = storedCustomers.map((c) => parseInt(c.id, 10));
|
||||
const notStoredCustomersIds = difference(
|
||||
filter.customer_ids.map(a => parseInt(a, 10)),
|
||||
storedCustomersIds
|
||||
);
|
||||
|
||||
if (notStoredCustomersIds.length) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'CUSTOMERS.IDS.NOT.FOUND',
|
||||
code: 300,
|
||||
ids: notStoredCustomersIds,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receivable aging summary validation roles.
|
||||
*/
|
||||
static get receivableAgingSummaryRoles() {
|
||||
return [
|
||||
query('as_date').optional().isISO8601(),
|
||||
query('aging_days_before').optional().isNumeric().toInt(),
|
||||
query('aging_periods').optional().isNumeric().toInt(),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||
oneOf(
|
||||
[
|
||||
query('customer_ids').optional().isArray({ min: 1 }),
|
||||
query('customer_ids.*').isNumeric().toInt(),
|
||||
],
|
||||
[query('customer_ids').optional().isNumeric().toInt()]
|
||||
),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve receivable aging summary report.
|
||||
*/
|
||||
static async receivableAgingSummary(req, res) {
|
||||
const { Customer, Account, AccountTransaction, AccountType } = req.models;
|
||||
|
||||
const filter = {
|
||||
as_date: moment().format('YYYY-MM-DD'),
|
||||
aging_days_before: 30,
|
||||
aging_periods: 3,
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
customer_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.customer_ids)) {
|
||||
filter.customer_ids = [filter.customer_ids];
|
||||
}
|
||||
|
||||
const storedCustomers = await Customer.query().onBuild((builder) => {
|
||||
if (filter.customer_ids) {
|
||||
builder.modify('filterCustomerIds', filter.customer_ids);
|
||||
}
|
||||
return builder;
|
||||
});
|
||||
const accountsReceivableType = await AccountType.query()
|
||||
.where('key', 'accounts_receivable')
|
||||
.first();
|
||||
|
||||
const accountsReceivable = await Account.query()
|
||||
.where('account_type_id', accountsReceivableType.id)
|
||||
.remember()
|
||||
.first();
|
||||
|
||||
const transactions = await AccountTransaction.query().onBuild((query) => {
|
||||
query.modify('filterDateRange', null, filter.as_date)
|
||||
query.where('account_id', accountsReceivable.id)
|
||||
query.modify('filterContactType', 'customer');
|
||||
|
||||
if (filter.customer_ids.length> 0) {
|
||||
query.modify('filterContactIds', filter.customer_ids)
|
||||
}
|
||||
query.remember();
|
||||
return query;
|
||||
});
|
||||
|
||||
const journalPoster = new JournalPoster();
|
||||
journalPoster.loadEntries(transactions);
|
||||
|
||||
const agingPeriods = this.agingRangePeriods(
|
||||
filter.as_date,
|
||||
filter.aging_days_before,
|
||||
filter.aging_periods
|
||||
);
|
||||
// Total amount formmatter based on the given query.
|
||||
const totalFormatter = this.formatNumberClosure(filter.number_format);
|
||||
|
||||
const customers = storedCustomers.map((customer) => {
|
||||
// Calculate the trial balance total of the given customer.
|
||||
const customerBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
customer.id,
|
||||
'customer'
|
||||
);
|
||||
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||
// Calculate the trial balance between the given date period.
|
||||
const agingTrialBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
customer.id,
|
||||
'customer',
|
||||
agingPeriod.from_period
|
||||
);
|
||||
return {
|
||||
...agingPeriod,
|
||||
closingBalance: agingTrialBalance.debit,
|
||||
};
|
||||
});
|
||||
const aging = this.contactAgingBalance(
|
||||
agingClosingBalance,
|
||||
customerBalance.credit
|
||||
);
|
||||
return {
|
||||
customer_name: customer.displayName,
|
||||
aging: aging.map((item) => ({
|
||||
...item,
|
||||
formatted_total: totalFormatter(item.total),
|
||||
})),
|
||||
total: customerBalance.balance,
|
||||
formatted_total: totalFormatter(customerBalance.balance),
|
||||
};
|
||||
});
|
||||
|
||||
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||
const closingTrialBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
null,
|
||||
'customer',
|
||||
agingPeriod.from_period
|
||||
);
|
||||
return {
|
||||
...agingPeriod,
|
||||
closingBalance: closingTrialBalance.balance,
|
||||
};
|
||||
});
|
||||
|
||||
const totalClosingBalance = journalPoster.getContactTrialBalance(
|
||||
accountsReceivable.id,
|
||||
null,
|
||||
'customer'
|
||||
);
|
||||
const agingTotal = this.contactAgingBalance(
|
||||
agingClosingBalance,
|
||||
totalClosingBalance.credit
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
columns: [...agingPeriods],
|
||||
aging: {
|
||||
customers,
|
||||
total: [
|
||||
...agingTotal.map((item) => ({
|
||||
...item,
|
||||
formatted_total: totalFormatter(item.total),
|
||||
})),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,10 @@ import {
|
||||
} from '@/lib/ViewRolesBuilder';
|
||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||
import DateSession from '@/models/DateSession';
|
||||
import { flatToNestedArray } from '@/utils';
|
||||
import DependencyGraph from '@/lib/DependencyGraph';
|
||||
|
||||
export default class Account extends mixin(TenantModel, [CachableModel, DateSession]) {
|
||||
export default class Account extends mixin(TenantModel, [CachableModel]) {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -20,6 +19,13 @@ export default class Account extends mixin(TenantModel, [CachableModel, DateSess
|
||||
return 'accounts';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
*/
|
||||
|
||||
@@ -3,10 +3,9 @@ import moment from 'moment';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||
import DateSession from '@/models/DateSession';
|
||||
|
||||
|
||||
export default class AccountTransaction extends mixin(TenantModel, [CachableModel, DateSession]) {
|
||||
export default class AccountTransaction extends mixin(TenantModel, [CachableModel]) {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -14,6 +13,13 @@ export default class AccountTransaction extends mixin(TenantModel, [CachableMode
|
||||
return 'accounts_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
*/
|
||||
@@ -69,6 +75,12 @@ export default class AccountTransaction extends mixin(TenantModel, [CachableMode
|
||||
query.sum('debit as debit');
|
||||
query.groupBy('account_id');
|
||||
},
|
||||
filterContactType(query, contactType) {
|
||||
query.where('contact_type', contactType);
|
||||
},
|
||||
filterContactIds(query, contactIds) {
|
||||
query.whereIn('contact_id', contactIds);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// import path from 'path';
|
||||
import { Model } from 'objection';
|
||||
import { Model, mixin } from 'objection';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||
|
||||
export default class AccountType extends TenantModel {
|
||||
export default class AccountType extends mixin(TenantModel, [CachableModel]) {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
|
||||
@@ -7,4 +7,11 @@ export default class Currency extends TenantModel {
|
||||
static get tableName() {
|
||||
return 'currencies';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,22 @@ export default class Customer extends TenantModel {
|
||||
static get tableName() {
|
||||
return 'customers';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
filterCustomerIds(query, customerIds) {
|
||||
query.whereIn('id', customerIds);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,11 @@ export default class ExchangeRate extends TenantModel {
|
||||
static get tableName() {
|
||||
return 'exchange_rates';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,20 @@ export default class Expense extends TenantModel {
|
||||
return 'expenses_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Account transaction reference type.
|
||||
*/
|
||||
static get referenceType() {
|
||||
return 'Expense';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,13 @@ export default class Item extends TenantModel {
|
||||
return 'items';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,13 @@ export default class ManualJournal extends TenantModel {
|
||||
return 'manual_journals';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Model } from 'objection';
|
||||
import { Model, mixin } from 'objection';
|
||||
import { snakeCase } from 'lodash';
|
||||
import { mapKeysDeep } from '@/utils';
|
||||
import PaginationQueryBuilder from '@/models/Pagination';
|
||||
import DateSession from '@/models/DateSession';
|
||||
|
||||
export default class ModelBase extends Model {
|
||||
export default class ModelBase extends mixin(Model, [DateSession]) {
|
||||
|
||||
static get timestamps() {
|
||||
return [];
|
||||
}
|
||||
|
||||
static get knexBinded() {
|
||||
return this.knexBindInstance;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { Model, mixin } from 'objection';
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import DateSession from '@/models/DateSession';
|
||||
// import PermissionsService from '@/services/PermissionsService';
|
||||
|
||||
export default class TenantUser extends mixin(TenantModel, [DateSession]) {
|
||||
export default class TenantUser extends TenantModel {
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
@@ -19,6 +18,13 @@ export default class TenantUser extends mixin(TenantModel, [DateSession]) {
|
||||
return 'users';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
|
||||
@@ -8,4 +8,11 @@ export default class Vendor extends TenantModel {
|
||||
static get tableName() {
|
||||
return 'vendors';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ export default class View extends mixin(TenantModel, [CachableModel]) {
|
||||
return 'views';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend query builder model.
|
||||
*/
|
||||
|
||||
@@ -3,11 +3,10 @@ import moment from 'moment';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
import AccountBalance from '@/models/AccountBalance';
|
||||
import {promiseSerial} from '@/utils';
|
||||
import { promiseSerial } from '@/utils';
|
||||
import Account from '@/models/Account';
|
||||
import NestedSet from '../../collection/NestedSet';
|
||||
|
||||
|
||||
export default class JournalPoster {
|
||||
/**
|
||||
* Journal poster constructor.
|
||||
@@ -34,7 +33,7 @@ export default class JournalPoster {
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the debit entr y for the given account.
|
||||
* Writes the debit entry for the given account.
|
||||
* @param {JournalEntry} entry -
|
||||
*/
|
||||
debit(entryModel) {
|
||||
@@ -78,7 +77,11 @@ export default class JournalPoster {
|
||||
* @private
|
||||
*/
|
||||
_setAccountBalanceChange({
|
||||
accountId, accountNormal, debit, credit, entryType
|
||||
accountId,
|
||||
accountNormal,
|
||||
debit,
|
||||
credit,
|
||||
entryType,
|
||||
}) {
|
||||
if (!this.balancesChange[accountId]) {
|
||||
this.balancesChange[accountId] = 0;
|
||||
@@ -86,9 +89,9 @@ export default class JournalPoster {
|
||||
let change = 0;
|
||||
|
||||
if (accountNormal === 'credit') {
|
||||
change = (entryType === 'credit') ? credit : -1 * debit;
|
||||
change = entryType === 'credit' ? credit : -1 * debit;
|
||||
} else if (accountNormal === 'debit') {
|
||||
change = (entryType === 'debit') ? debit : -1 * credit;
|
||||
change = entryType === 'debit' ? debit : -1 * credit;
|
||||
}
|
||||
this.balancesChange[accountId] += change;
|
||||
}
|
||||
@@ -132,9 +135,9 @@ export default class JournalPoster {
|
||||
const method = balance.amount < 0 ? 'decrement' : 'increment';
|
||||
|
||||
// Detarmine if the account balance is already exists or not.
|
||||
const foundAccBalance = balanceAccounts.some((account) => (
|
||||
account && account.account_id === balance.account_id
|
||||
));
|
||||
const foundAccBalance = balanceAccounts.some(
|
||||
(account) => account && account.account_id === balance.account_id
|
||||
);
|
||||
|
||||
if (foundAccBalance) {
|
||||
const query = AccountBalance.tenant()
|
||||
@@ -152,9 +155,7 @@ export default class JournalPoster {
|
||||
balanceInsertOpers.push(query);
|
||||
}
|
||||
});
|
||||
await Promise.all([
|
||||
...balanceUpdateOpers, ...balanceInsertOpers,
|
||||
]);
|
||||
await Promise.all([...balanceUpdateOpers, ...balanceInsertOpers]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,11 +165,23 @@ export default class JournalPoster {
|
||||
const saveOperations = [];
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
const oper = AccountTransaction.tenant().query().insert({
|
||||
accountId: entry.account,
|
||||
...pick(entry, ['credit', 'debit', 'transactionType', 'date', 'userId',
|
||||
'referenceType', 'referenceId', 'note']),
|
||||
});
|
||||
const oper = AccountTransaction.tenant()
|
||||
.query()
|
||||
.insert({
|
||||
accountId: entry.account,
|
||||
...pick(entry, [
|
||||
'credit',
|
||||
'debit',
|
||||
'transactionType',
|
||||
'date',
|
||||
'userId',
|
||||
'referenceType',
|
||||
'referenceId',
|
||||
'note',
|
||||
'contactId',
|
||||
'contactType',
|
||||
]),
|
||||
});
|
||||
saveOperations.push(() => oper);
|
||||
});
|
||||
await promiseSerial(saveOperations);
|
||||
@@ -195,15 +208,16 @@ export default class JournalPoster {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {Array} ids -
|
||||
*/
|
||||
removeEntries(ids = []) {
|
||||
const targetIds = (ids.length <= 0) ? this.entries.map(e => e.id) : ids;
|
||||
const removeEntries = this.entries.filter((e) => targetIds.indexOf(e.id) !== -1);
|
||||
const targetIds = ids.length <= 0 ? this.entries.map((e) => e.id) : ids;
|
||||
const removeEntries = this.entries.filter(
|
||||
(e) => targetIds.indexOf(e.id) !== -1
|
||||
);
|
||||
|
||||
this.entries = this.entries
|
||||
.filter(e => targetIds.indexOf(e.id) === -1)
|
||||
this.entries = this.entries.filter((e) => targetIds.indexOf(e.id) === -1);
|
||||
|
||||
removeEntries.forEach((entry) => {
|
||||
entry.credit = -1 * entry.credit;
|
||||
@@ -211,9 +225,7 @@ export default class JournalPoster {
|
||||
|
||||
this.setAccountBalanceChange(entry, entry.accountNormal);
|
||||
});
|
||||
this.deletedEntriesIds.push(
|
||||
...removeEntries.map(entry => entry.id),
|
||||
);
|
||||
this.deletedEntriesIds.push(...removeEntries.map((entry) => entry.id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,7 +233,8 @@ export default class JournalPoster {
|
||||
*/
|
||||
async deleteEntries() {
|
||||
if (this.deletedEntriesIds.length > 0) {
|
||||
await AccountTransaction.tenant().query()
|
||||
await AccountTransaction.tenant()
|
||||
.query()
|
||||
.whereIn('id', this.deletedEntriesIds)
|
||||
.delete();
|
||||
}
|
||||
@@ -238,15 +251,17 @@ export default class JournalPoster {
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
// Can not continue if not before or event same closing date.
|
||||
if ((!momentClosingDate.isAfter(entry.date, dateType)
|
||||
&& !momentClosingDate.isSame(entry.date, dateType))
|
||||
|| (entry.account !== accountId && accountId)) {
|
||||
if (
|
||||
(!momentClosingDate.isAfter(entry.date, dateType) &&
|
||||
!momentClosingDate.isSame(entry.date, dateType)) ||
|
||||
(entry.account !== accountId && accountId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (entry.accountNormal === 'credit') {
|
||||
closingBalance += (entry.credit) ? entry.credit : -1 * entry.debit;
|
||||
closingBalance += entry.credit ? entry.credit : -1 * entry.debit;
|
||||
} else if (entry.accountNormal === 'debit') {
|
||||
closingBalance += (entry.debit) ? entry.debit : -1 * entry.credit;
|
||||
closingBalance += entry.debit ? entry.debit : -1 * entry.credit;
|
||||
}
|
||||
});
|
||||
return closingBalance;
|
||||
@@ -254,21 +269,27 @@ export default class JournalPoster {
|
||||
|
||||
/**
|
||||
* Retrieve the given account balance with dependencies accounts.
|
||||
* @param {Number} accountId
|
||||
* @param {Date} closingDate
|
||||
* @param {String} dateType
|
||||
* @param {Number} accountId
|
||||
* @param {Date} closingDate
|
||||
* @param {String} dateType
|
||||
* @return {Number}
|
||||
*/
|
||||
getAccountBalance(accountId, closingDate, dateType) {
|
||||
const accountNode = this.accountsGraph.getNodeData(accountId);
|
||||
const depAccountsIds = this.accountsGraph.dependenciesOf(accountId);
|
||||
const depAccounts = depAccountsIds.map((id) => this.accountsGraph.getNodeData(id));
|
||||
const depAccounts = depAccountsIds.map((id) =>
|
||||
this.accountsGraph.getNodeData(id)
|
||||
);
|
||||
let balance = 0;
|
||||
|
||||
[...depAccounts, accountNode].forEach((account) => {
|
||||
// if (!this.accountsBalanceTable[account.id]) {
|
||||
const closingBalance = this.getClosingBalance(account.id, closingDate, dateType);
|
||||
this.accountsBalanceTable[account.id] = closingBalance;
|
||||
const closingBalance = this.getClosingBalance(
|
||||
account.id,
|
||||
closingDate,
|
||||
dateType
|
||||
);
|
||||
this.accountsBalanceTable[account.id] = closingBalance;
|
||||
// }
|
||||
balance += this.accountsBalanceTable[account.id];
|
||||
});
|
||||
@@ -288,9 +309,11 @@ export default class JournalPoster {
|
||||
balance: 0,
|
||||
};
|
||||
this.entries.forEach((entry) => {
|
||||
if ((!momentClosingDate.isAfter(entry.date, dateType)
|
||||
&& !momentClosingDate.isSame(entry.date, dateType))
|
||||
|| (entry.account !== accountId && accountId)) {
|
||||
if (
|
||||
(!momentClosingDate.isAfter(entry.date, dateType) &&
|
||||
!momentClosingDate.isSame(entry.date, dateType)) ||
|
||||
(entry.account !== accountId && accountId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
result.credit += entry.credit;
|
||||
@@ -307,20 +330,27 @@ export default class JournalPoster {
|
||||
|
||||
/**
|
||||
* Retrieve trial balance of the given account with depends.
|
||||
* @param {Number} accountId
|
||||
* @param {Date} closingDate
|
||||
* @param {String} dateType
|
||||
* @param {Number} accountId
|
||||
* @param {Date} closingDate
|
||||
* @param {String} dateType
|
||||
* @return {Number}
|
||||
*/
|
||||
*/
|
||||
|
||||
getTrialBalanceWithDepands(accountId, closingDate, dateType) {
|
||||
const accountNode = this.accountsGraph.getNodeData(accountId);
|
||||
const depAccountsIds = this.accountsGraph.dependenciesOf(accountId);
|
||||
const depAccounts = depAccountsIds.map((id) => this.accountsGraph.getNodeData(id));
|
||||
const depAccounts = depAccountsIds.map((id) =>
|
||||
this.accountsGraph.getNodeData(id)
|
||||
);
|
||||
|
||||
const trialBalance = { credit: 0, debit: 0, balance: 0 };
|
||||
|
||||
[...depAccounts, accountNode].forEach((account) => {
|
||||
const _trialBalance = this.getTrialBalance(account.id, closingDate, dateType);
|
||||
const _trialBalance = this.getTrialBalance(
|
||||
account.id,
|
||||
closingDate,
|
||||
dateType
|
||||
);
|
||||
|
||||
trialBalance.credit += _trialBalance.credit;
|
||||
trialBalance.debit += _trialBalance.debit;
|
||||
@@ -329,6 +359,85 @@ export default class JournalPoster {
|
||||
return trialBalance;
|
||||
}
|
||||
|
||||
getContactTrialBalance(
|
||||
accountId,
|
||||
contactId,
|
||||
contactType,
|
||||
closingDate,
|
||||
openingDate
|
||||
) {
|
||||
const momentClosingDate = moment(closingDate);
|
||||
const momentOpeningDate = moment(openingDate);
|
||||
const trial = {
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
balance: 0,
|
||||
};
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
if (
|
||||
(closingDate &&
|
||||
!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||
(openingDate &&
|
||||
!momentOpeningDate.isBefore(entry.date, 'day') &&
|
||||
!momentOpeningDate.isSame(entry.date)) ||
|
||||
(accountId && entry.account !== accountId) ||
|
||||
(contactId && entry.contactId !== contactId) ||
|
||||
entry.contactType !== contactType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (entry.credit) {
|
||||
trial.balance -= entry.credit;
|
||||
trial.credit += entry.credit;
|
||||
}
|
||||
if (entry.debit) {
|
||||
trial.balance += entry.debit;
|
||||
trial.debit += entry.debit;
|
||||
}
|
||||
});
|
||||
return trial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve total balnace of the given customer/vendor contact.
|
||||
* @param {Number} accountId
|
||||
* @param {Number} contactId
|
||||
* @param {String} contactType
|
||||
* @param {Date} closingDate
|
||||
*/
|
||||
getContactBalance(
|
||||
accountId,
|
||||
contactId,
|
||||
contactType,
|
||||
closingDate,
|
||||
openingDate
|
||||
) {
|
||||
const momentClosingDate = moment(closingDate);
|
||||
let balance = 0;
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
if (
|
||||
(closingDate &&
|
||||
!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||
(entry.account !== accountId && accountId) ||
|
||||
(contactId && entry.contactId !== contactId) ||
|
||||
entry.contactType !== contactType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (entry.credit) {
|
||||
balance -= entry.credit;
|
||||
}
|
||||
if (entry.debit) {
|
||||
balance += entry.debit;
|
||||
}
|
||||
});
|
||||
return balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load fetched accounts journal entries.
|
||||
* @param {Array} entries -
|
||||
@@ -338,8 +447,10 @@ export default class JournalPoster {
|
||||
this.entries.push({
|
||||
...entry,
|
||||
account: entry.account ? entry.account.id : entry.accountId,
|
||||
accountNormal: (entry.account && entry.account.type)
|
||||
? entry.account.type.normal : entry.accountNormal,
|
||||
accountNormal:
|
||||
entry.account && entry.account.type
|
||||
? entry.account.type.normal
|
||||
: entry.accountNormal,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Model, mixin } from 'objection';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
import DateSession from '@/models/DateSession';
|
||||
import UserSubscription from '@/services/Subscription/UserSubscription';
|
||||
|
||||
|
||||
export default class SystemUser extends mixin(SystemModel, [DateSession, UserSubscription]) {
|
||||
export default class SystemUser extends mixin(SystemModel, [UserSubscription]) {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
@@ -13,6 +12,13 @@ export default class SystemUser extends mixin(SystemModel, [DateSession, UserSub
|
||||
return 'users';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
static get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user