mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
WIP
This commit is contained in:
@@ -120,6 +120,10 @@ export default [
|
||||
text: 'Trial Balance Sheet',
|
||||
href: '/dashboard/accounting/trial-balance-sheet',
|
||||
},
|
||||
{
|
||||
text: 'Journal',
|
||||
href: '/dashboard/accounting/journal-sheet',
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
|
||||
19
client/src/connectors/Journal.connect.js
Normal file
19
client/src/connectors/Journal.connect.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
fetchJournalSheet
|
||||
} from 'store/financialStatement/financialStatements.actions';
|
||||
import {
|
||||
getFinancialSheetIndexByQuery,
|
||||
getFinancialSheet,
|
||||
} from 'store/financialStatement/financialStatements.selectors';
|
||||
|
||||
export const mapStateToProps = (state, props) => ({
|
||||
getJournalSheetIndex: (query) => getFinancialSheetIndexByQuery(state.financialStatements.journalSheets, query),
|
||||
getJournalSheet: (index) => getFinancialSheet(state.financialStatements.journalSheets, index),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
fetchJournalSheet: (query) => dispatch(fetchJournalSheet({ query })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps);
|
||||
@@ -78,16 +78,12 @@ function BalanceSheetTable({
|
||||
}
|
||||
},
|
||||
}))),
|
||||
|
||||
|
||||
|
||||
], [balanceSheetColumns]);
|
||||
], [balanceSheetColumns, balanceSheetQuery]);
|
||||
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!balanceSheet) { return; }
|
||||
|
||||
setData([
|
||||
{
|
||||
name: 'Assets',
|
||||
@@ -106,9 +102,6 @@ function BalanceSheetTable({
|
||||
])
|
||||
}, [])
|
||||
|
||||
// if (balanceSheets.length > 0) {
|
||||
// setData(balanceSheets[0].balance_sheet);
|
||||
// }
|
||||
return (
|
||||
<FinancialSheet
|
||||
companyTitle={'Facebook, Incopration'}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, {useState, useEffect, useMemo} from 'react';
|
||||
import {compose} from 'utils';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import JournalConnect from 'connectors/Journal.connect';
|
||||
import JournalHeader from 'containers/Dashboard/FinancialStatements/Journal/JournalHeader';
|
||||
import useAsync from 'hooks/async';
|
||||
import {useIntl} from 'react-intl';
|
||||
import moment from 'moment';
|
||||
import JournalTable from './JournalTable';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
|
||||
function Journal({
|
||||
fetchJournalSheet,
|
||||
getJournalSheet,
|
||||
getJournalSheetIndex,
|
||||
changePageTitle,
|
||||
}) {
|
||||
const [filter, setFilter] = useState({
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
});
|
||||
const [reload, setReload] = useState(false);
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
fetchJournalSheet(filter),
|
||||
]);
|
||||
setReload(false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
changePageTitle('Journal');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (reload) {
|
||||
fetchHook.execute();
|
||||
}
|
||||
}, [reload, fetchHook]);
|
||||
|
||||
const journalSheetIndex = useMemo(() => {
|
||||
return getJournalSheetIndex(filter);
|
||||
}, [filter, getJournalSheetIndex]);
|
||||
|
||||
const handleFilterSubmit = (filter) => {
|
||||
setFilter({
|
||||
...filter,
|
||||
from_date: moment(filter.from_date).format('YYYY-MM-DD'),
|
||||
to_date: moment(filter.to_date).format('YYYY-MM-DD'),
|
||||
});
|
||||
setReload(true);
|
||||
};
|
||||
return (
|
||||
<div class="financial-statement">
|
||||
<JournalHeader
|
||||
pageFilter={filter}
|
||||
onSubmitFilter={handleFilterSubmit} />
|
||||
|
||||
<div class="financial-statement__body">
|
||||
<LoadingIndicator loading={fetchHook.pending}>
|
||||
<JournalTable
|
||||
journalIndex={journalSheetIndex} />
|
||||
</LoadingIndicator>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default compose(
|
||||
JournalConnect,
|
||||
DashboardConnect,
|
||||
)(Journal);
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, {useState, useMemo, useEffect} from 'react';
|
||||
import FinancialStatementHeader from 'containers/Dashboard/FinancialStatements/FinancialStatementHeader';
|
||||
import {Row, Col} from 'react-grid-system';
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
Position,
|
||||
HTMLSelect,
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import {DateInput} from '@blueprintjs/datetime';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
momentFormatter,
|
||||
parseDateRangeQuery,
|
||||
} from 'utils';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
export default function JournalHeader({
|
||||
pageFilter,
|
||||
onSubmitFilter,
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
|
||||
const [filter, setFilter] = useState({
|
||||
...pageFilter,
|
||||
from_date: moment(pageFilter.from_date).toDate(),
|
||||
to_date: moment(pageFilter.to_date).toDate()
|
||||
});
|
||||
|
||||
const setFilterByKey = (name, value) => {
|
||||
setFilter({ ...filter, [name]: value });
|
||||
};
|
||||
const [reportDateRange, setReportDateRange] = useState('this_year');
|
||||
const dateRangeOptions = [
|
||||
{value: 'today', label: 'Today', },
|
||||
{value: 'this_week', label: 'This Week'},
|
||||
{value: 'this_month', label: 'This Month'},
|
||||
{value: 'this_quarter', label: 'This Quarter'},
|
||||
{value: 'this_year', label: 'This Year'},
|
||||
{value: 'custom', label: 'Custom Range'},
|
||||
];
|
||||
const handleDateChange = (name) => (date) => {
|
||||
setReportDateRange('custom');
|
||||
setFilterByKey(name, date);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (reportDateRange === 'custom') { return; }
|
||||
const dateRange = parseDateRangeQuery(reportDateRange);
|
||||
|
||||
if (dateRange) {
|
||||
setFilter((filter) => ({ ...filter, ...dateRange }));
|
||||
}
|
||||
}, [reportDateRange]);
|
||||
|
||||
const handleSubmitClick = () => { onSubmitFilter(filter); };
|
||||
|
||||
return (
|
||||
<FinancialStatementHeader>
|
||||
<Row>
|
||||
<Col sm={3}>
|
||||
<FormGroup
|
||||
label={intl.formatMessage({'id': 'report_date_range'})}
|
||||
minimal={true}
|
||||
fill={true}>
|
||||
|
||||
<HTMLSelect
|
||||
fill={true}
|
||||
options={dateRangeOptions}
|
||||
value={reportDateRange}
|
||||
onChange={(event) => setReportDateRange(event.target.value)} />
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col sm={3}>
|
||||
<FormGroup
|
||||
label={intl.formatMessage({'id': 'from_date'})}
|
||||
minimal={true}
|
||||
fill={true}>
|
||||
|
||||
<DateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
value={filter.from_date}
|
||||
onChange={handleDateChange('from_date')}
|
||||
popoverProps={{ position: Position.BOTTOM }}
|
||||
fill={true} />
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col sm={3}>
|
||||
<FormGroup
|
||||
label={intl.formatMessage({'id': 'to_date'})}
|
||||
minimal={true}
|
||||
fill={true}>
|
||||
|
||||
<DateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
value={filter.to_date}
|
||||
onChange={handleDateChange('to_date')}
|
||||
popoverProps={{ position: Position.BOTTOM }}
|
||||
fill={true} />
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col sm={3}>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
onClick={handleSubmitClick}>
|
||||
{ 'Run Report' }
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</FinancialStatementHeader>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, {useState, useEffect, useMemo} from 'react';
|
||||
import FinancialSheet from 'components/FinancialSheet';
|
||||
import DataTable from 'components/DataTable';
|
||||
import {compose} from 'utils';
|
||||
import moment from 'moment';
|
||||
import JournalConnect from 'connectors/Journal.connect';
|
||||
import {
|
||||
getFinancialSheet,
|
||||
} from 'store/financialStatement/financialStatements.selectors';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
function JournalSheetTable({
|
||||
journalIndex,
|
||||
journalTableData,
|
||||
}) {
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
Header: 'Date',
|
||||
accessor: r => moment(r.date).format('YYYY/MM/DD'),
|
||||
className: 'date',
|
||||
},
|
||||
{
|
||||
Header: 'Account Name',
|
||||
accessor: 'account.name',
|
||||
},
|
||||
{
|
||||
Header: 'Transaction Type',
|
||||
accessor: 'transaction_type',
|
||||
className: "transaction_type",
|
||||
},
|
||||
{
|
||||
Header: 'Num.',
|
||||
accessor: 'reference_id',
|
||||
className: 'reference_id',
|
||||
},
|
||||
{
|
||||
Header: 'Note',
|
||||
accessor: 'note',
|
||||
},
|
||||
{
|
||||
Header: 'Credit',
|
||||
accessor: 'credit',
|
||||
},
|
||||
{
|
||||
Header: 'Debit',
|
||||
accessor: 'debit',
|
||||
},
|
||||
], []);
|
||||
|
||||
return (
|
||||
<FinancialSheet
|
||||
companyTitle={'Facebook, Incopration'}
|
||||
sheetType={'Balance Sheet'}
|
||||
date={[]}>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={journalTableData} />
|
||||
|
||||
</FinancialSheet>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const journalTableData = [];
|
||||
const journalSheet = getFinancialSheet(state.financialStatements.journalSheets, props.journalIndex);
|
||||
|
||||
if (journalSheet && journalSheet.journal) {
|
||||
journalSheet.journal.forEach((journal) => {
|
||||
journal.entries.forEach((entry, index) => {
|
||||
journalTableData.push({ ...entry, index });
|
||||
});
|
||||
journalTableData.push({
|
||||
credit: journal.credit,
|
||||
debit: journal.debit,
|
||||
total: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
return {
|
||||
journalSheet,
|
||||
journalTableData,
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
JournalConnect,
|
||||
)(JournalSheetTable);
|
||||
@@ -95,5 +95,12 @@ export default [
|
||||
component: LazyLoader({
|
||||
loader: () => import('containers/Dashboard/FinancialStatements/ProfitLossSheet/ProfitLossSheet')
|
||||
}),
|
||||
}
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/accounting/journal-sheet`,
|
||||
name: 'dashboard.accounting.journal.sheet',
|
||||
component: LazyLoader({
|
||||
loader: () => import('containers/Dashboard/FinancialStatements/Journal/Journal')
|
||||
}),
|
||||
},
|
||||
];
|
||||
@@ -3,7 +3,7 @@ import t from 'store/types';
|
||||
|
||||
export const fetchGeneralLedger = ({ query }) => {
|
||||
return (dispatch) => new Promise((resolve, reject) => {
|
||||
ApiService.get('/financial_statements/general_ledger').then((response) => {
|
||||
ApiService.get('/financial_statements/general_ledger', { params: query }).then((response) => {
|
||||
dispatch({
|
||||
type: t.GENERAL_LEDGER_STATEMENT_SET,
|
||||
data: response.data,
|
||||
@@ -28,7 +28,7 @@ export const fetchBalanceSheet = ({ query }) => {
|
||||
|
||||
export const fetchTrialBalanceSheet = ({ query }) => {
|
||||
return (dispatch) => new Promise((resolve, reject) => {
|
||||
ApiService.get('/financial_statements/trial_balance_sheet').then((response) => {
|
||||
ApiService.get('/financial_statements/trial_balance_sheet', { params: query }).then((response) => {
|
||||
dispatch({
|
||||
type: t.TRAIL_BALANCE_STATEMENT_SET,
|
||||
data: response.data,
|
||||
@@ -40,7 +40,7 @@ export const fetchTrialBalanceSheet = ({ query }) => {
|
||||
|
||||
export const fetchProfitLossSheet = ({ query }) => {
|
||||
return (dispatch) => new Promise((resolve, reject) => {
|
||||
ApiService.get('/financial_statements/profit_loss_sheet').then((response) => {
|
||||
ApiService.get('/financial_statements/profit_loss_sheet', { params: query }).then((response) => {
|
||||
dispatch({
|
||||
type: t.PROFIT_LOSS_STATEMENT_SET,
|
||||
data: response.data,
|
||||
@@ -48,4 +48,17 @@ export const fetchProfitLossSheet = ({ query }) => {
|
||||
resolve(response.data);
|
||||
}).catch((error) => { reject(error); });
|
||||
})
|
||||
};
|
||||
|
||||
export const fetchJournalSheet = ({ query }) => {
|
||||
return (dispatch) => new Promise((resolve, reject) => {
|
||||
ApiService.get('/financial_statements/journal', { params: query }).then((response) => {
|
||||
dispatch({
|
||||
type: t.JOURNAL_SHEET_SET,
|
||||
data: response.data,
|
||||
query: response.data.query,
|
||||
});
|
||||
resolve(response.data);
|
||||
}).catch(error => { reject(error); });
|
||||
});
|
||||
};
|
||||
@@ -1,12 +1,16 @@
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import t from 'store/types';
|
||||
import {getBalanceSheetIndexByQuery, getTrialBalanceSheetIndex} from './financialStatements.selectors';
|
||||
import { actionComplete } from '@syncfusion/ej2-react-grids';
|
||||
import {
|
||||
getBalanceSheetIndexByQuery,
|
||||
getTrialBalanceSheetIndex,
|
||||
getFinancialSheetIndexByQuery,
|
||||
} from './financialStatements.selectors';
|
||||
|
||||
const initialState = {
|
||||
balanceSheets: [],
|
||||
trialBalanceSheets: [],
|
||||
generalLedger: [],
|
||||
journalSheets: [],
|
||||
};
|
||||
|
||||
export default createReducer(initialState, {
|
||||
@@ -37,5 +41,20 @@ export default createReducer(initialState, {
|
||||
} else {
|
||||
state.trailBalanceSheet.push(trailBalanceSheet);
|
||||
}
|
||||
},
|
||||
|
||||
[t.JOURNAL_SHEET_SET]: (state, action) => {
|
||||
const index = getFinancialSheetIndexByQuery(state.journalSheets, action.query);
|
||||
console.log(index, 'INDEX');
|
||||
|
||||
const journal = {
|
||||
query: action.data.query,
|
||||
journal: action.data.journal,
|
||||
};
|
||||
if (index !== -1) {
|
||||
state.journalSheets[index] = journal;
|
||||
} else {
|
||||
state.journalSheets.push(journal);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,49 @@
|
||||
import {getObjectDiff} from 'utils';
|
||||
|
||||
|
||||
// Financial Statements selectors.
|
||||
|
||||
/**
|
||||
* Retrieve financial statement sheet by the given query.
|
||||
* @param {array} sheets
|
||||
* @param {object} query
|
||||
*/
|
||||
export const getFinancialSheetIndexByQuery = (sheets, query) => {
|
||||
return sheets.findIndex(balanceSheet => (
|
||||
getObjectDiff(query, balanceSheet.query).length === 0
|
||||
));
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve financial statement sheet by the given sheet index.
|
||||
* @param {array} sheets
|
||||
* @param {number} index
|
||||
*/
|
||||
export const getFinancialSheet = (sheets, index) => {
|
||||
return (typeof sheets[index] !== 'undefined') ? sheets[index] : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve financial statement columns by the given sheet index.
|
||||
* @param {array} sheets
|
||||
* @param {number} index
|
||||
*/
|
||||
export const getFinancialSheetColumns = (sheets, index) => {
|
||||
const sheet = getFinancialSheet(sheets, index);
|
||||
return (sheet && sheet.columns) ? sheet.columns : [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve financial statement query by the given sheet index.
|
||||
* @param {array} sheets
|
||||
* @param {number} index
|
||||
*/
|
||||
export const getFinancialSheetsQuery = (sheets, index) => {
|
||||
const sheet = getFinancialSheet(sheets, index);
|
||||
return (sheet && sheet.query) ? sheet.columns : {};
|
||||
};
|
||||
|
||||
|
||||
// Balance Sheet.
|
||||
export const getBalanceSheetByQuery = (balanceSheets, query) => {
|
||||
return balanceSheets.find(balanceSheet => {
|
||||
|
||||
@@ -4,4 +4,5 @@ export default {
|
||||
GENERAL_LEDGER_STATEMENT_SET: 'GENERAL_LEDGER_STATEMENT_SET',
|
||||
BALANCE_SHEET_STATEMENT_SET: 'BALANCE_SHEET_STATEMENT_SET',
|
||||
TRAIL_BALANCE_STATEMENT_SET: 'TRAIL_BALANCE_STATEMENT_SET',
|
||||
JOURNAL_SHEET_SET: 'JOURNAL_SHEET_SET',
|
||||
}
|
||||
@@ -214,6 +214,12 @@ export default {
|
||||
query('account_types').optional().isArray(),
|
||||
query('account_types.*').optional().isNumeric().toInt(),
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
|
||||
query('roles').optional().isArray({ min: 1 }),
|
||||
query('roles.*.field_key').exists().escape().trim(),
|
||||
query('roles.*.comparator').exists(),
|
||||
query('roles.*.value').exists(),
|
||||
query('roles.*.index').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import express from 'express';
|
||||
import { query, validationResult } from 'express-validator';
|
||||
import { query, oneOf, validationResult } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { pick } from 'lodash';
|
||||
import { pick, difference, groupBy } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
@@ -30,9 +30,9 @@ export default {
|
||||
const router = express.Router();
|
||||
router.use(jwtAuth);
|
||||
|
||||
router.get('/ledger',
|
||||
this.ledger.validation,
|
||||
asyncMiddleware(this.ledger.handler));
|
||||
router.get('/journal',
|
||||
this.journal.validation,
|
||||
asyncMiddleware(this.journal.handler));
|
||||
|
||||
router.get('/general_ledger',
|
||||
this.generalLedger.validation,
|
||||
@@ -60,13 +60,22 @@ export default {
|
||||
/**
|
||||
* Retrieve the ledger report of the given account.
|
||||
*/
|
||||
ledger: {
|
||||
journal: {
|
||||
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(),
|
||||
oneOf([
|
||||
query('transaction_types').optional().isArray({ min: 1 }),
|
||||
query('transaction_types.*').optional().isNumeric().toInt(),
|
||||
], [
|
||||
query('transaction_types').optional().trim().escape(),
|
||||
]),
|
||||
oneOf([
|
||||
query('account_ids').optional().isArray({ min: 1 }),
|
||||
query('account_ids.*').optional().isNumeric().toInt(),
|
||||
], [
|
||||
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(),
|
||||
@@ -81,6 +90,8 @@ export default {
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
from_range: null,
|
||||
to_range: null,
|
||||
account_ids: [],
|
||||
@@ -91,22 +102,45 @@ export default {
|
||||
},
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.transaction_types)) {
|
||||
filter.transaction_types = [filter.transaction_types];
|
||||
}
|
||||
if (!Array.isArray(filter.account_ids)) {
|
||||
filter.account_ids = [filter.account_ids];
|
||||
}
|
||||
filter.account_ids = filter.account_ids.map((id) => parseInt(id, 10));
|
||||
|
||||
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');
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
const formatNumber = formatNumberClosure(filter.number_format);
|
||||
|
||||
const journalGrouped = groupBy(accountsJournalEntries, (entry) => {
|
||||
return `${entry.id}-${entry.referenceType}`;
|
||||
});
|
||||
const journal = Object.keys(journalGrouped).map((key) => {
|
||||
const transactionsGroup = journalGrouped[key];
|
||||
|
||||
const journalPoster = new JournalPoster();
|
||||
journalPoster.loadEntries(transactionsGroup);
|
||||
|
||||
const trialBalance = journalPoster.getTrialBalance();
|
||||
|
||||
return {
|
||||
id: key,
|
||||
entries: transactionsGroup,
|
||||
credit: formatNumber(trialBalance.credit),
|
||||
debit: formatNumber(trialBalance.debit),
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
meta: { ...filter },
|
||||
items: accountsJournalEntries.map((entry) => ({
|
||||
...entry,
|
||||
credit: formatNumber(entry.credit),
|
||||
debit: formatNumber(entry.debit),
|
||||
})),
|
||||
query: { ...filter },
|
||||
journal,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -122,7 +156,10 @@ 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(),
|
||||
query('accounts_ids').optional(),
|
||||
query('accounts_ids.*').isNumeric().toInt(),
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -144,12 +181,29 @@ export default {
|
||||
accounts_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
if (!Array.isArray(filter.accounts_ids)) {
|
||||
filter.accounts_ids = [filter.accounts_ids];
|
||||
}
|
||||
filter.accounts_ids = filter.accounts_ids.map((id) => parseInt(id, 10));
|
||||
|
||||
const errorReasons = [];
|
||||
|
||||
if (filter.accounts_ids.length > 0) {
|
||||
const accounts = await Account.query().whereIn('id', filter.accounts_ids);
|
||||
const accountsIds = accounts.map((a) => a.id);
|
||||
|
||||
if (difference(filter.accounts_ids, accountsIds).length > 0) {
|
||||
errorReasons.push({ type: 'FILTER.ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||
}
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ error: errorReasons });
|
||||
}
|
||||
const accounts = await Account.query()
|
||||
.orderBy('index', 'DESC')
|
||||
.modify('filterAccounts', filter.accounts_ids)
|
||||
.withGraphFetched('transactions')
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions')
|
||||
.modifyGraph('transactions', (builder) => {
|
||||
builder.modify('filterDateRange', filter.from_date, filter.to_date);
|
||||
});
|
||||
@@ -175,7 +229,7 @@ export default {
|
||||
|
||||
const items = accounts
|
||||
.filter((account) => (
|
||||
account.transactions.length > 0 || !filter.none_zero
|
||||
account.transactions.length > 0 || filter.none_zero
|
||||
))
|
||||
.map((account) => ({
|
||||
...pick(account, ['id', 'name', 'code', 'index']),
|
||||
@@ -184,12 +238,13 @@ export default {
|
||||
let amount = 0;
|
||||
|
||||
if (account.type.normal === 'credit') {
|
||||
amount += transaction.credit - transaction.credit;
|
||||
amount += transaction.credit - transaction.debit;
|
||||
} else if (account.type.normal === 'debit') {
|
||||
amount += transaction.debit - transaction.credit;
|
||||
}
|
||||
return {
|
||||
...transaction,
|
||||
...pick(transaction, ['id', 'note', 'transactionType', 'referenceType',
|
||||
'referenceId', 'date', 'createdAt']),
|
||||
amount: formatNumber(amount),
|
||||
};
|
||||
}),
|
||||
@@ -271,7 +326,6 @@ export default {
|
||||
filter.to_date,
|
||||
filterDateType,
|
||||
);
|
||||
|
||||
// Retrieve the asset balance sheet.
|
||||
const assets = accounts
|
||||
.filter((account) => (
|
||||
@@ -320,7 +374,6 @@ export default {
|
||||
...(type !== 'total') ? {
|
||||
periods_balance: dateRangeSet.map((date) => {
|
||||
const balance = journalEntries.getClosingBalance(account.id, date, filterDateType);
|
||||
|
||||
return {
|
||||
date,
|
||||
formatted_amount: balanceFormatter(balance),
|
||||
@@ -329,7 +382,7 @@ export default {
|
||||
}),
|
||||
} : {},
|
||||
balance: {
|
||||
formattedAmount: balanceFormatter(closingBalance),
|
||||
formatted_amount: balanceFormatter(closingBalance),
|
||||
amount: closingBalance,
|
||||
date: filter.to_date,
|
||||
},
|
||||
@@ -460,7 +513,7 @@ export default {
|
||||
basis: 'accural',
|
||||
none_zero: false,
|
||||
display_columns_type: 'total',
|
||||
display_columns_by: 'total',
|
||||
display_columns_by: 'month',
|
||||
...req.query,
|
||||
};
|
||||
const incomeStatementTypes = await AccountType.query().where('income_sheet', true);
|
||||
@@ -481,12 +534,14 @@ export default {
|
||||
|
||||
// Account balance formmatter based on the given query.
|
||||
const numberFormatter = formatNumberClosure(filter.number_format);
|
||||
const comparatorDateType = filter.display_columns_type === '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,
|
||||
comparatorDateType,
|
||||
);
|
||||
|
||||
const accountsMapper = (incomeExpenseAccounts) => (
|
||||
@@ -503,7 +558,7 @@ export default {
|
||||
// Date periods when display columns type `periods`.
|
||||
...(filter.display_columns_type === 'date_periods') && {
|
||||
periods: dateRangeSet.map((date) => {
|
||||
const type = filter.display_columns_by;
|
||||
const type = comparatorDateType;
|
||||
const amount = journalEntries.getClosingBalance(account.id, date, type);
|
||||
|
||||
return { date, amount, formatted_amount: numberFormatter(amount) };
|
||||
@@ -551,23 +606,22 @@ export default {
|
||||
// @return {Object}
|
||||
const netIncomeTotal = (totalIncome, totalExpenses) => {
|
||||
const netIncomeAmount = totalIncome.amount - totalExpenses.amount;
|
||||
return { amount: netIncomeAmount, formatted_amount: netIncomeAmount };
|
||||
return { amount: netIncomeAmount, formatted_amount: netIncomeAmount, date: filter.to_date };
|
||||
};
|
||||
|
||||
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 === 'total') && (() => {
|
||||
const totalIncomeAccounts = totalAccountsReducer(accountsIncome);
|
||||
return {
|
||||
total: {
|
||||
amount: totalIncomeAccounts,
|
||||
date: filter.to_date,
|
||||
formatted_amount: numberFormatter(totalIncomeAccounts),
|
||||
},
|
||||
};
|
||||
})(),
|
||||
...(filter.display_columns_type === 'date_periods') && {
|
||||
total_periods: [
|
||||
...totalPeriodsMapper(accountsIncome),
|
||||
@@ -577,14 +631,16 @@ export default {
|
||||
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 === 'total') && (() => {
|
||||
const totalExpensesAccounts = totalAccountsReducer(accountsExpenses);
|
||||
return {
|
||||
total: {
|
||||
amount: totalExpensesAccounts,
|
||||
date: filter.to_date,
|
||||
formatted_amount: numberFormatter(totalExpensesAccounts),
|
||||
},
|
||||
};
|
||||
})(),
|
||||
...(filter.display_columns_type === 'date_periods') && {
|
||||
total_periods: [
|
||||
...totalPeriodsMapper(accountsExpenses),
|
||||
|
||||
@@ -49,11 +49,14 @@ export default {
|
||||
newItem: {
|
||||
validation: [
|
||||
check('name').exists(),
|
||||
check('type').exists().trim().escape().isIn(['service', 'product']),
|
||||
check('type').exists().trim().escape()
|
||||
.isIn(['service', 'non-inventory', 'inventory']),
|
||||
check('sku').optional().trim().escape(),
|
||||
check('cost_price').exists().isNumeric(),
|
||||
check('sell_price').exists().isNumeric(),
|
||||
check('cost_account_id').exists().isInt().toInt(),
|
||||
check('sell_account_id').exists().isInt().toInt(),
|
||||
check('inventory_account_id').exists().isInt().toInt(),
|
||||
check('category_id').optional().isInt().toInt(),
|
||||
|
||||
check('custom_fields').optional().isArray({ min: 1 }),
|
||||
@@ -78,6 +81,9 @@ export default {
|
||||
|
||||
const costAccountPromise = Account.query().findById(form.cost_account_id);
|
||||
const sellAccountPromise = Account.query().findById(form.sell_account_id);
|
||||
const inventoryAccountPromise = (form.type === 'inventory') ?
|
||||
Account.query().findByid(form.inventory_account_id) : null;
|
||||
|
||||
const itemCategoryPromise = (form.category_id)
|
||||
? ItemCategory.query().findById(form.category_id) : null;
|
||||
|
||||
@@ -101,8 +107,14 @@ export default {
|
||||
errorReasons.push({ type: 'FIELD_KEY_NOT_FOUND', code: 150, fields: notFoundFields });
|
||||
}
|
||||
}
|
||||
const [costAccount, sellAccount, itemCategory] = await Promise.all([
|
||||
costAccountPromise, sellAccountPromise, itemCategoryPromise,
|
||||
const [
|
||||
costAccount,
|
||||
sellAccount,
|
||||
itemCategory,
|
||||
inventoryAccount,
|
||||
] = await Promise.all([
|
||||
costAccountPromise, sellAccountPromise,
|
||||
itemCategoryPromise, inventoryAccountPromise,
|
||||
]);
|
||||
if (!costAccount) {
|
||||
errorReasons.push({ type: 'COST_ACCOUNT_NOT_FOUND', code: 100 });
|
||||
@@ -113,6 +125,9 @@ export default {
|
||||
if (!itemCategory && form.category_id) {
|
||||
errorReasons.push({ type: 'ITEM_CATEGORY_NOT_FOUND', code: 140 });
|
||||
}
|
||||
if (!inventoryAccount && form.type === 'inventory') {
|
||||
errorReasons.push({ type: 'INVENTORY_ACCOUNT_NOT_FOUND', code: 150 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
@@ -300,6 +300,25 @@ describe('routes: /accounts/', () => {
|
||||
expect(res.body.accounts[2].id).equals(account3.id);
|
||||
expect(res.body.accounts[2].name).equals(`${account1.name} ― ${account2.name} ― ${account3.name}`);
|
||||
});
|
||||
|
||||
it('Should retrieve filtered accounts according to the given filter roles.', async () => {
|
||||
const account1 = await create('account', { name: 'ahmed' });
|
||||
const account2 = await create('account');
|
||||
const account3 = await create('account');
|
||||
|
||||
const res = await request()
|
||||
.get('/api/accounts')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
filter_roles: [{
|
||||
field_key: 'name',
|
||||
comparator: 'equals',
|
||||
value: 'ahmed',
|
||||
}],
|
||||
});
|
||||
|
||||
expect(res.body.accounts.length).equals(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE: `/accounts`', () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
expect,
|
||||
request,
|
||||
login,
|
||||
create,
|
||||
} from '~/testInit';
|
||||
import moment from 'moment';
|
||||
|
||||
let loginRes;
|
||||
let creditAccount;
|
||||
@@ -52,35 +52,36 @@ describe('routes: `/financial_statements`', () => {
|
||||
afterEach(() => {
|
||||
loginRes = null;
|
||||
});
|
||||
describe('routes: `/financial_statements/ledger`', () => {
|
||||
describe('routes: `/financial_statements/journal`', () => {
|
||||
it('Should response unauthorized in case the user was not authorized.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/ledger')
|
||||
.get('/api/financial_statements/journal')
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.status).equals(401);
|
||||
});
|
||||
|
||||
it('Should retrieve ledger transactions grouped by accounts.', async () => {
|
||||
it('Should retrieve ledger transactions grouped by reference type and id.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/ledger')
|
||||
.get('/api/financial_statements/journal')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(200);
|
||||
expect(res.body.items.length).to.be.at.least(1);
|
||||
expect(res.body.journal.length).to.be.at.least(1);
|
||||
|
||||
expect(res.body.items[0]).to.have.property('id');
|
||||
expect(res.body.items[0]).to.have.property('referenceType');
|
||||
expect(res.body.items[0]).to.have.property('referenceId');
|
||||
expect(res.body.items[0]).to.have.property('date');
|
||||
expect(res.body.items[0]).to.have.property('account');
|
||||
expect(res.body.items[0]).to.have.property('note');
|
||||
expect(res.body.journal[0].credit).to.be.a('number');
|
||||
expect(res.body.journal[0].debit).to.be.a('number');
|
||||
expect(res.body.journal[0].entries).to.be.a('array');
|
||||
expect(res.body.journal[0].id).to.be.a('string');
|
||||
|
||||
expect(res.body.journal[0].entries[0].credit).to.be.a('number');
|
||||
expect(res.body.journal[0].entries[0].debit).to.be.a('number');
|
||||
});
|
||||
|
||||
it('Should retrieve transactions between date range.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/ledger')
|
||||
.get('/api/financial_statements/journal')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2018-01-01',
|
||||
@@ -88,40 +89,47 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.items.length).equals(0);
|
||||
expect(res.body.journal.length).equals(0);
|
||||
});
|
||||
|
||||
it('Should retrieve transactions that associated to the queried accounts.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/ledger')
|
||||
.get('/api/financial_statements/journal')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
account_ids: [creditAccount.id, debitAccount.id],
|
||||
account_ids: [creditAccount.id],
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.items.length).equals(4);
|
||||
expect(res.body.journal.length).equals(2);
|
||||
});
|
||||
|
||||
it('Should retrieve tranasactions with the given types.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/ledger')
|
||||
.get('/api/financial_statements/journal')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
transaction_types: ['Expense'],
|
||||
});
|
||||
|
||||
expect(res.body.items.length).equals(1);
|
||||
expect(res.body.journal.length).equals(1);
|
||||
});
|
||||
|
||||
it('Should retrieve transactions with range amount.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/ledger')
|
||||
.get('/api/financial_statements/journal')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_range: 1000,
|
||||
from_range: 2000,
|
||||
to_range: 2000,
|
||||
});
|
||||
|
||||
expect(res.body.journal[0].credit).satisfy((credit) => {
|
||||
return credit === 0 || credit === 2000;
|
||||
});
|
||||
expect(res.body.journal[0].debit).satisfy((debit) => {
|
||||
return debit === 0 || debit === 2000;
|
||||
});
|
||||
});
|
||||
|
||||
it('Should format credit and debit to no cents of retrieved transactions.', async () => {
|
||||
@@ -130,7 +138,7 @@ describe('routes: `/financial_statements`', () => {
|
||||
|
||||
it('Should divide credit/debit amount on 1000', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/ledger')
|
||||
.get('/api/financial_statements/journal')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
number_format: {
|
||||
@@ -139,14 +147,14 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
res.body.items.forEach((item) => {
|
||||
expect(item.credit).to.be.at.most(100);
|
||||
expect(item.debit).to.be.at.most(100);
|
||||
});
|
||||
const journal = res.body.journal.find((j) => j.id === '1-Expense');
|
||||
|
||||
expect(journal.credit).equals(1);
|
||||
expect(journal.debit).equals(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe.only('routes: `/financial_statements/general_ledger`', () => {
|
||||
describe('routes: `/financial_statements/general_ledger`', () => {
|
||||
it('Should response unauthorized in case the user was not authorized.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
@@ -206,36 +214,152 @@ describe('routes: `/financial_statements`', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Should retrieve opening and closing balance between the given date range.', () => {
|
||||
it('Should retrieve opening and closing balance between the given date range.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-20',
|
||||
to_date: '2020-03-30',
|
||||
none_zero: true,
|
||||
})
|
||||
.send();
|
||||
|
||||
const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id);
|
||||
|
||||
expect(targetAccount).to.be.an('object');
|
||||
expect(targetAccount.opening).to.deep.equal({
|
||||
balance: 2000, date: '2020-01-20',
|
||||
});
|
||||
expect(targetAccount.closing).to.deep.equal({
|
||||
balance: 2000, date: '2020-03-30',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should retrieve transactions of accounts that has transactions between date range.', () => {
|
||||
it('Should retrieve accounts with associated transactions.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
none_zero: true,
|
||||
})
|
||||
.send();
|
||||
|
||||
const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id);
|
||||
|
||||
expect(targetAccount.transactions[0].amount).equals(1000);
|
||||
expect(targetAccount.transactions[1].amount).equals(1000);
|
||||
|
||||
expect(targetAccount.transactions[1].id).to.be.an('number');
|
||||
// expect(targetAccount.transactions[1].note).to.be.an('string');
|
||||
// expect(targetAccount.transactions[1].transactionType).to.be.an('string');
|
||||
// expect(targetAccount.transactions[1].referenceType).to.be.an('string');
|
||||
// expect(targetAccount.transactions[1].referenceId).to.be.an('number');
|
||||
expect(targetAccount.transactions[1].date).to.be.an('string');
|
||||
})
|
||||
|
||||
it('Should retrieve accounts transactions only that between date range.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-20',
|
||||
to_date: '2020-03-30',
|
||||
none_zero: true,
|
||||
})
|
||||
.send();
|
||||
|
||||
const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id);
|
||||
expect(targetAccount.transactions.length).equals(0);
|
||||
});
|
||||
|
||||
it('Should retrieve accounts transactions only that between date range.', () => {
|
||||
it('Should not retrieve all accounts that have no transactions in the given date range when `none_zero` is `false`.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-20',
|
||||
to_date: '2020-03-30',
|
||||
none_zero: false,
|
||||
})
|
||||
.send();
|
||||
|
||||
res.body.accounts.forEach((account) => {
|
||||
expect(account.transactions.length).not.equals(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not retrieve all accounts that have no transactions in the given date range when `none_zero` is `false`.', () => {
|
||||
it('Should retrieve all accounts even it have no transactions in the given date range when `none_zero` is `true`', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2020-03-30',
|
||||
none_zero: true,
|
||||
})
|
||||
.send();
|
||||
|
||||
const accountsNoTransactions = res.body.accounts.filter(a => a.transactions.length === 0);
|
||||
const accountsWithTransactions = res.body.accounts.filter(a => a.transactions.length > 0);
|
||||
|
||||
expect(accountsNoTransactions.length).not.equals(0);
|
||||
expect(accountsWithTransactions.length).not.equals(0);
|
||||
});
|
||||
|
||||
it('Should retrieve all accounts even it have no transactions in the given date range when `none_zero` is `true`', () => {
|
||||
|
||||
it('Should amount transactions divided on `1000` when `number_format.none_zero` is `true`.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2020-03-30',
|
||||
number_format: {
|
||||
divide_1000: true,
|
||||
},
|
||||
})
|
||||
.send();
|
||||
|
||||
const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id);
|
||||
expect(targetAccount.transactions[0].amount).equals(1);
|
||||
expect(targetAccount.transactions[1].amount).equals(1);
|
||||
});
|
||||
|
||||
it('Should amount transactions divided on 1000 when `number_format.none_zero` is `true`.', () => {
|
||||
it('Should amount transactions rounded with no decimals when `number_format.no_cents` is `true`.', async () => {
|
||||
await create('account_transaction', {
|
||||
debit: 0.25, credit: 0, account_id: debitAccount.id, date: '2020-1-10',
|
||||
});
|
||||
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2020-03-30',
|
||||
number_format: {
|
||||
divide_1000: true,
|
||||
no_cents: true,
|
||||
},
|
||||
accounts_ids: [debitAccount.id]
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.accounts[0].transactions[2].amount).equal(0);
|
||||
});
|
||||
|
||||
it('Should amount transactions rounded with no decimals when `number_format.no_cents` is `true`.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should retrieve only accounts that given in the query.', () => {
|
||||
it('Should retrieve only accounts that given in the query.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2020-03-30',
|
||||
none_zero: true,
|
||||
accounts_ids: [creditAccount.id],
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.accounts.length).equals(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,10 +418,10 @@ describe('routes: `/financial_statements`', () => {
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].balance).deep.equals({
|
||||
amount: 4000, formattedAmount: 4000, date: '2032-02-02',
|
||||
amount: 4000, formatted_amount: 4000, date: '2032-02-02',
|
||||
});
|
||||
expect(res.body.balance_sheet.liabilities_equity.accounts[0].balance).deep.equals({
|
||||
amount: 2000, formattedAmount: 2000, date: '2032-02-02',
|
||||
amount: 2000, formatted_amount: 2000, date: '2032-02-02',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -633,11 +757,11 @@ describe('routes: `/financial_statements`', () => {
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
display_columns_type: 'date_periods',
|
||||
display_columns_by: 'month',
|
||||
display_columns_by: 'quarter',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.columns.length).equals(12);
|
||||
expect(res.body.columns.length).equals(4);
|
||||
expect(res.body.columns).deep.equals([
|
||||
'2020-03', '2020-06', '2020-09', '2020-12',
|
||||
]);
|
||||
@@ -679,31 +803,33 @@ describe('routes: `/financial_statements`', () => {
|
||||
from_date: moment('2020-01-01').startOf('month').format('YYYY-MM-DD'),
|
||||
to_date: moment('2020-01-01').endOf('month').format('YYYY-MM-DD'),
|
||||
display_columns_type: 'total',
|
||||
display_columns_by: 'day',
|
||||
display_columns_by: 'month',
|
||||
})
|
||||
.send();
|
||||
|
||||
console.log(res.body);
|
||||
|
||||
// expect(res.body.income.accounts.length).equals(2);
|
||||
// expect(res.body.income.accounts[0].name).to.be.an('string');
|
||||
// expect(res.body.income.accounts[0].code).to.be.an('string');
|
||||
// expect(res.body.income.accounts[0].periods).to.be.an('array');
|
||||
// expect(res.body.income.accounts[0].periods.length).equals(31);
|
||||
const zeroAccount = res.body.income.accounts.filter((a) => a.total.amount === 0);
|
||||
expect(zeroAccount.length).not.equals(0);
|
||||
});
|
||||
|
||||
it('Should retrieve total of each income account when display columns by `total`.', async () => {
|
||||
const toDate = moment('2020-01-01').endOf('month').format('YYYY-MM-DD');
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/profit_loss_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: moment('2020-01-01').startOf('month').format('YYYY-MM-DD'),
|
||||
to_date: moment('2020-01-01').endOf('month').format('YYYY-MM-DD'),
|
||||
display_columns_by: 'day',
|
||||
to_date: toDate,
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.income).deep.equals();
|
||||
expect(res.body.income.accounts).to.be.an('array');
|
||||
expect(res.body.income.accounts.length).not.equals(0);
|
||||
expect(res.body.income.accounts[0].id).to.be.an('number');
|
||||
expect(res.body.income.accounts[0].name).to.be.an('string');
|
||||
expect(res.body.income.accounts[0].total).to.be.an('object');
|
||||
expect(res.body.income.accounts[0].total.amount).to.be.an('number');
|
||||
expect(res.body.income.accounts[0].total.formatted_amount).to.be.an('number');
|
||||
expect(res.body.income.accounts[0].total.date).equals(toDate);
|
||||
});
|
||||
|
||||
it('Should retrieve credit sumation of income accounts.', async () => {
|
||||
@@ -713,17 +839,13 @@ describe('routes: `/financial_statements`', () => {
|
||||
.query({
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2021-01-01',
|
||||
number_format: {
|
||||
divide_1000: true,
|
||||
},
|
||||
})
|
||||
.send();
|
||||
|
||||
console.log(res.body);
|
||||
|
||||
res.body.income.accounts[0].dates.forEach((item) => {
|
||||
expect(item.rawAmount).equals(2000);
|
||||
});
|
||||
expect(res.body.income.total).to.be.an('object');
|
||||
expect(res.body.income.total.amount).equals(2000);
|
||||
expect(res.body.income.total.formatted_amount).equals(2000);
|
||||
expect(res.body.income.total.date).equals('2021-01-01');
|
||||
});
|
||||
|
||||
it('Should retrieve debit sumation of expenses accounts.', async () => {
|
||||
@@ -733,91 +855,85 @@ describe('routes: `/financial_statements`', () => {
|
||||
.query({
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2021-01-01',
|
||||
number_format: {
|
||||
divide_1000: true,
|
||||
},
|
||||
})
|
||||
.send();
|
||||
|
||||
res.body.expenses.accounts[0].dates.forEach((item) => {
|
||||
expect(item.rawAmount).equals(4000);
|
||||
});
|
||||
expect(res.body.expenses.total).to.be.an('object');
|
||||
expect(res.body.expenses.total.amount).equals(6000);
|
||||
expect(res.body.expenses.total.formatted_amount).equals(6000);
|
||||
expect(res.body.expenses.total.date).equals('2021-01-01');
|
||||
});
|
||||
|
||||
it('Should retrieve credit sumation of income accounts between the given date range.', async () => {
|
||||
it('Should retrieve credit total of income accounts with `date_periods` columns between the given date range.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/profit_loss_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2021-01-01',
|
||||
from_date: '2019-12-01',
|
||||
to_date: '2020-12-01',
|
||||
display_columns_type: 'date_periods',
|
||||
display_columns_by: 'month',
|
||||
number_format: {
|
||||
divide_1000: true,
|
||||
},
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.income.accounts[0].dates.length).equals(12);
|
||||
expect(res.body.income.total_periods[0].amount).equals(0);
|
||||
expect(res.body.income.total_periods[1].amount).equals(2000);
|
||||
expect(res.body.income.total_periods[2].amount).equals(2000);
|
||||
});
|
||||
|
||||
it('Should retrieve debit sumation of expenses accounts between the given date range.', async () => {
|
||||
it('Should retrieve debit total of expenses accounts with `date_periods` columns between the given date range.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/profit_loss_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2021-01-01',
|
||||
from_date: '2019-12-01',
|
||||
to_date: '2020-12-01',
|
||||
display_columns_type: 'date_periods',
|
||||
display_columns_by: 'month',
|
||||
number_format: {
|
||||
divide_1000: true,
|
||||
},
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.expenses.accounts[0].dates.length).equals(12);
|
||||
expect(res.body.expenses.total_periods[0].amount).equals(0);
|
||||
expect(res.body.expenses.total_periods[1].amount).equals(6000);
|
||||
expect(res.body.expenses.total_periods[2].amount).equals(6000);
|
||||
});
|
||||
|
||||
it('Should retrieve total income of income accounts between the given date range.', async () => {
|
||||
it('Should retrieve total net income with `total column display between the given date range.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/profit_loss_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2021-01-01',
|
||||
display_columns_by: 'month',
|
||||
from_date: '2019-12-01',
|
||||
to_date: '2020-12-01',
|
||||
display_columns_type: 'total',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.total_income[0].rawAmount).equals(2000);
|
||||
expect(res.body.net_income.total.amount).equals(-4000);
|
||||
expect(res.body.net_income.total.formatted_amount).equals(-4000);
|
||||
expect(res.body.net_income.total.date).equals('2020-12-01');
|
||||
});
|
||||
|
||||
it('Should retrieve total expenses of expenses accounts between the given date range.', async () => {
|
||||
it('Should retrieve total net income with `date_periods` columns between the given date range.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/profit_loss_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2021-01-01',
|
||||
display_columns_by: 'month',
|
||||
from_date: '2019-12-01',
|
||||
to_date: '2020-12-01',
|
||||
display_columns_type: 'date_periods',
|
||||
display_columns_by: 'quarter',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.total_expenses[0].rawAmount).equals(6000);
|
||||
});
|
||||
expect(res.body.net_income.total_periods.length).equals(5);
|
||||
expect(res.body.net_income.total_periods[0].amount).equals(0);
|
||||
expect(res.body.net_income.total_periods[0].formatted_amount).equal(0);
|
||||
expect(res.body.net_income.total_periods[0].date).equals('2019-12');
|
||||
|
||||
it('Should retrieve total net income between the given date range.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/profit_loss_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2021-01-01',
|
||||
display_columns_by: 'month',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.total_net_income[0].rawAmount).equals(-4000);
|
||||
expect(res.body.net_income.total_periods[1].amount).equals(-4000);
|
||||
expect(res.body.net_income.total_periods[1].formatted_amount).equal(-4000);
|
||||
expect(res.body.net_income.total_periods[1].date).equals('2020-03');
|
||||
});
|
||||
|
||||
it('Should not retrieve income or expenses accounts that has no transactions between the given date range in case none_zero equals true.', async () => {
|
||||
@@ -828,7 +944,7 @@ describe('routes: `/financial_statements`', () => {
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2021-01-01',
|
||||
display_columns_by: 'month',
|
||||
none_zero: true
|
||||
none_zero: true,
|
||||
})
|
||||
.send();
|
||||
|
||||
|
||||
@@ -6,16 +6,29 @@ import knex from '@/database/knex';
|
||||
import '@/models';
|
||||
import app from '@/app';
|
||||
import factory from '@/database/factories';
|
||||
import knexConfig from '@/../knexfile';
|
||||
import dbManager from '@/database/manager';
|
||||
// import { hashPassword } from '@/utils';
|
||||
|
||||
const request = () => chai.request(app);
|
||||
const { expect } = chai;
|
||||
|
||||
const login = async (givenUser) => {
|
||||
const user = !givenUser ? await factory.create('user') : givenUser;
|
||||
|
||||
const response = request()
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
crediential: user.email,
|
||||
password: 'admin',
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
await dbManager.dropDb();
|
||||
await dbManager.createDb('ratteb');
|
||||
await dbManager.closeKnex();
|
||||
await dbManager.close();
|
||||
// await dbManager.dropDb();
|
||||
// await dbManager.createDb();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -23,25 +36,12 @@ beforeEach(async () => {
|
||||
await knex.migrate.latest();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
after(async () => {
|
||||
});
|
||||
|
||||
chai.use(chaiHttp);
|
||||
chai.use(chaiThings);
|
||||
|
||||
const login = async (givenUser) => {
|
||||
const user = !givenUser ? await factory.create('user') : givenUser;
|
||||
|
||||
const response = await request()
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
crediential: user.email,
|
||||
password: 'admin',
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const create = async (name, data) => factory.create(name, data);
|
||||
const make = async (name, data) => factory.build(name, data);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user