WIP
This commit is contained in:
@@ -10,5 +10,5 @@ MAIL_FROM_NAME=
|
||||
DB_CLIENT=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_USER=root
|
||||
DB_PASSWORD=123123123
|
||||
DB_PASSWORD=root
|
||||
DB_NAME=ratteb
|
||||
|
||||
Generated
+1402
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTable, usePagination } from 'react-table'
|
||||
import { useTable, useExpanded, usePagination } from 'react-table'
|
||||
|
||||
export default function DataTable({
|
||||
columns,
|
||||
@@ -32,8 +32,10 @@ export default function DataTable({
|
||||
// This means we'll also have to provide our own
|
||||
// pageCount.
|
||||
// pageCount: controlledPageCount,
|
||||
getSubRows: row => row.children,
|
||||
},
|
||||
usePagination
|
||||
useExpanded,
|
||||
usePagination,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -116,6 +116,10 @@ export default [
|
||||
text: 'Balance Sheet',
|
||||
href: '/dashboard/accounting/balance-sheet',
|
||||
},
|
||||
{
|
||||
text: 'Trial Balance Sheet',
|
||||
href: '/dashboard/accounting/trial-balance-sheet',
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
|
||||
@@ -3,10 +3,29 @@ import {
|
||||
fetchGeneralLedger,
|
||||
fetchBalanceSheet,
|
||||
} from 'store/financialStatement/financialStatements.actions';
|
||||
import t from 'store/types';
|
||||
import {
|
||||
getBalanceSheetByQuery,
|
||||
getBalanceSheetColumns,
|
||||
getBalanceSheetIndexByQuery,
|
||||
getBalanceSheetByIndex,
|
||||
getBalanceSheetAssetsAccounts,
|
||||
getBalanceSheetLiabilitiesAccounts,
|
||||
getBalanceSheetQuery,
|
||||
} from 'store/financialStatement/financialStatements.selectors';
|
||||
|
||||
export const mapStateToProps = (state, props) => ({
|
||||
generalLedeger: state.financialStatements.generalLedger,
|
||||
balanceSheets: state.financialStatements.balanceSheets,
|
||||
|
||||
getBalanceSheetByQuery: (query) => getBalanceSheetByQuery(state.financialStatements.balanceSheets, query),
|
||||
getBalanceSheetColumns: (sheetIndex) => getBalanceSheetColumns(state.financialStatements.balanceSheets, sheetIndex),
|
||||
getBalanceSheetIndexByQuery: (query) => getBalanceSheetIndexByQuery(state.financialStatements.balanceSheets, query),
|
||||
getBalanceSheetByIndex: (sheetIndex) => getBalanceSheetByIndex(state.financialStatements.balanceSheets, sheetIndex),
|
||||
|
||||
getBalanceSheetAssetsAccounts: (sheetIndex) => getBalanceSheetAssetsAccounts(state.financialStatements.balanceSheets, sheetIndex),
|
||||
getBalanceSheetLiabilitiesAccounts: (sheetIndex) => getBalanceSheetLiabilitiesAccounts(state.financialStatements.balanceSheets, sheetIndex),
|
||||
|
||||
getBalanceSheetQuery: (sheetIndex) => getBalanceSheetQuery(state.financialStatements.balanceSheets, sheetIndex),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
fetchProfitLossSheet,
|
||||
} from 'store/financialStatement/financialStatements.actions';
|
||||
import {
|
||||
getProfitLossSheetIndex,
|
||||
getProfitLossSheet,
|
||||
getProfitLossSheetColumns,
|
||||
getProfitLossSheetAccounts,
|
||||
} from 'store/financialStatement/financialStatements.selectors';
|
||||
|
||||
export const mapStateToProps = (state, props) => ({
|
||||
getProfitLossSheetIndex: (query) => getProfitLossSheetIndex(state.financialStatements.profitLossSheets, query),
|
||||
getProfitLossSheet: (index) => getProfitLossSheet(state.financialStatements.profitLossSheets, index),
|
||||
getProfitLossSheetColumns: (index) => getProfitLossSheetColumns(state.financialStatements.profitLossSheets, index),
|
||||
getProfitLossSheetAccounts: (index) => getProfitLossSheetAccounts(state.financialStatements.profitLossSheets, index),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
fetchProfitLossSheet: (query = {}) => dispatch(fetchProfitLossSheet({ query })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps);
|
||||
@@ -0,0 +1,21 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
fetchTrialBalanceSheet
|
||||
} from 'store/financialStatement/financialStatements.actions';
|
||||
import {
|
||||
getTrialBalanceSheetIndex,
|
||||
getTrialBalanceAccounts,
|
||||
getTrialBalanceQuery,
|
||||
} from 'store/financialStatement/financialStatements.selectors';
|
||||
|
||||
export const mapStateToProps = (state, props) => ({
|
||||
getTrialBalanceSheetIndex: (query) => getTrialBalanceSheetIndex(state.financialStatements.trialBalanceSheets, query),
|
||||
getTrialBalanceAccounts: (sheetIndex) => getTrialBalanceAccounts(state.financialStatements.trialBalanceSheets, sheetIndex),
|
||||
getTrialBalanceQuery: (sheetIndex) => getTrialBalanceQuery(state.financialStatements.trialBalanceSheets, sheetIndex),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
fetchTrialBalanceSheet: (query = {}) => dispatch(fetchTrialBalanceSheet({ query })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps);
|
||||
@@ -1,48 +0,0 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import {compose} from 'utils';
|
||||
import useAsync from 'hooks/async';
|
||||
import FinancialStatementConnect from 'connectors/FinancialStatements.connector';
|
||||
import {useIntl} from 'react-intl';
|
||||
import BalanceSheetHeader from './BalanceSheet/BalanceSheetHeader';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import BalanceSheetTable from './BalanceSheet/BalanceSheetTable';
|
||||
|
||||
function BalanceSheet({
|
||||
fetchBalanceSheet,
|
||||
changePageTitle,
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const handleDateChange = () => {};
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
fetchBalanceSheet({}),
|
||||
]);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
changePageTitle('Balance Sheet');
|
||||
}, []);
|
||||
|
||||
const handleFilterSubmit = (filter) => {
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="financial-statement">
|
||||
<BalanceSheetHeader onSubmitFilter={handleFilterSubmit} />
|
||||
|
||||
<div class="financial-statement__body">
|
||||
<LoadingIndicator loading={fetchHook.pending}>
|
||||
<BalanceSheetTable />
|
||||
</LoadingIndicator>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
DashboardConnect,
|
||||
FinancialStatementConnect,
|
||||
)(BalanceSheet);
|
||||
@@ -0,0 +1,86 @@
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import {compose} from 'utils';
|
||||
import useAsync from 'hooks/async';
|
||||
import FinancialStatementConnect from 'connectors/FinancialStatements.connector';
|
||||
import {useIntl} from 'react-intl';
|
||||
import BalanceSheetHeader from './BalanceSheetHeader';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import BalanceSheetTable from './BalanceSheetTable';
|
||||
import moment from 'moment';
|
||||
|
||||
function BalanceSheet({
|
||||
fetchBalanceSheet,
|
||||
changePageTitle,
|
||||
getBalanceSheetByQuery,
|
||||
getBalanceSheetIndexByQuery,
|
||||
getBalanceSheetByIndex,
|
||||
balanceSheets
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const [filter, setFilter] = useState({
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
basis: 'cash',
|
||||
display_columns_by: 'total',
|
||||
none_zero: false,
|
||||
});
|
||||
|
||||
const [reload, setReload] = useState(false);
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
fetchBalanceSheet(filter),
|
||||
]);
|
||||
setReload(false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!reload) { return; }
|
||||
fetchHook.execute();
|
||||
}, [reload]);
|
||||
|
||||
useEffect(() => {
|
||||
changePageTitle('Balance Sheet');
|
||||
}, []);
|
||||
|
||||
// Retrieve balance sheet index by the given filter query.
|
||||
const balanceSheetIndex = useMemo(() => {
|
||||
return getBalanceSheetIndexByQuery(filter);
|
||||
}, [filter, balanceSheets]);
|
||||
|
||||
// Retreive balance sheet by the given sheet index.
|
||||
const balanceSheet = useMemo(() => {
|
||||
return getBalanceSheetByIndex(balanceSheetIndex);
|
||||
}, [balanceSheetIndex, balanceSheets]);
|
||||
|
||||
// Handle re-fetch balance sheet after filter change.
|
||||
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">
|
||||
<BalanceSheetHeader
|
||||
pageFilter={filter}
|
||||
onSubmitFilter={handleFilterSubmit} />
|
||||
|
||||
<div class="financial-statement__body">
|
||||
<LoadingIndicator loading={fetchHook.pending}>
|
||||
<BalanceSheetTable
|
||||
balanceSheet={balanceSheet}
|
||||
balanceSheetIndex={balanceSheetIndex} />
|
||||
</LoadingIndicator>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
DashboardConnect,
|
||||
FinancialStatementConnect,
|
||||
)(BalanceSheet);
|
||||
+95
-39
@@ -1,4 +1,4 @@
|
||||
import React, {useState, useMemo} from 'react';
|
||||
import React, {useState, useMemo, useEffect} from 'react';
|
||||
import FinancialStatementHeader from 'containers/Dashboard/FinancialStatements/FinancialStatementHeader';
|
||||
import {Row, Col} from 'react-grid-system';
|
||||
import {
|
||||
@@ -10,44 +10,53 @@ import {
|
||||
Radio,
|
||||
HTMLSelect,
|
||||
Intent,
|
||||
Popover,
|
||||
} from "@blueprintjs/core";
|
||||
import {Select} from '@blueprintjs/select';
|
||||
import {DateInput} from '@blueprintjs/datetime';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {momentFormatter, handleStringChange} from 'utils';
|
||||
import {
|
||||
momentFormatter,
|
||||
handleStringChange,
|
||||
parseDateRangeQuery,
|
||||
} from 'utils';
|
||||
import moment from 'moment';
|
||||
|
||||
export default function BalanceSheetHeader({
|
||||
onSubmitFilter,
|
||||
pageFilter,
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const [filter, setFilter] = useState({
|
||||
from_date: null,
|
||||
to_date: null,
|
||||
accounting_basis: 'cash',
|
||||
display_columns_by: 'total',
|
||||
});
|
||||
|
||||
const setFilterByName = (name, value) => {
|
||||
setFilter({
|
||||
...filter,
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFieldChange = (event) => {
|
||||
setFilterByName(event.target.name, event.target.value);
|
||||
};
|
||||
|
||||
const displayColumnsByOptions = [
|
||||
{key: 'total', name: 'Total'},
|
||||
{key: 'year', name: 'Year'},
|
||||
{key: 'month', name: 'Month'},
|
||||
{key: 'week', name: 'Week'},
|
||||
{key: 'day', name: 'Day'},
|
||||
{key: 'quarter', name: 'Quarter'}
|
||||
{key: 'quarter', name: 'Quarter'},
|
||||
];
|
||||
|
||||
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');
|
||||
|
||||
useEffect(() => {
|
||||
if (reportDateRange === 'custom') { return; }
|
||||
const dateRange = parseDateRangeQuery(reportDateRange);
|
||||
|
||||
if (dateRange) {
|
||||
setFilter((filter) => ({ ...filter, ...dateRange, }));
|
||||
}
|
||||
}, [reportDateRange])
|
||||
|
||||
const selectedDisplayColumnOpt = useMemo(() => {
|
||||
return displayColumnsByOptions.find(o => o.key === filter.display_columns_by);
|
||||
}, [filter.display_columns_by, displayColumnsByOptions]);
|
||||
@@ -56,31 +65,56 @@ export default function BalanceSheetHeader({
|
||||
const accountTypeItem = (item, { handleClick, modifiers, query }) => {
|
||||
return (<MenuItem text={item.name} key={item.id} onClick={handleClick} />);
|
||||
};
|
||||
|
||||
|
||||
// Handle item select of `display columns by` field.
|
||||
const onItemSelectDisplayColumns = (item) => {
|
||||
setFilterByName('display_columns_by', item.key);
|
||||
setFilterByKey('display_columns_by', item.key);
|
||||
};
|
||||
|
||||
// Handle any date change.
|
||||
const handleDateChange = (name) => (date) => {
|
||||
setFilterByName(name, moment(date).format('YYYY-MM-DD'));
|
||||
setReportDateRange('custom');
|
||||
setFilterByKey(name, date);
|
||||
};
|
||||
|
||||
// handle submit filter submit button.
|
||||
const handleSubmitClick = () => {
|
||||
onSubmitFilter(filter);
|
||||
};
|
||||
|
||||
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 [activeRowsColumns, setActiveRowsColumns] = useState(false);
|
||||
|
||||
const onClickActiveRowsColumnsBtn = () => {
|
||||
setActiveRowsColumns(!activeRowsColumns);
|
||||
};
|
||||
|
||||
const activeRowsColumnsPopover = (
|
||||
<div>
|
||||
<h5>Columns</h5>
|
||||
<RadioGroup
|
||||
name="none_zero"
|
||||
selectedValue={filter.none_zero}
|
||||
onChange={handleStringChange((value) => {
|
||||
setFilterByKey('none_zero', value);
|
||||
})}
|
||||
>
|
||||
<Radio label="All" value="0" />
|
||||
<Radio label="Non-Zero" value="1" />
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<FinancialStatementHeader>
|
||||
<Row>
|
||||
<Col sm={4}>
|
||||
<Col sm={3}>
|
||||
<FormGroup
|
||||
label={intl.formatMessage({'id': 'report_date_range'})}
|
||||
minimal={true}
|
||||
@@ -88,11 +122,13 @@ export default function BalanceSheetHeader({
|
||||
|
||||
<HTMLSelect
|
||||
fill={true}
|
||||
options={dateRangeOptions} />
|
||||
options={dateRangeOptions}
|
||||
value={reportDateRange}
|
||||
onChange={(event) => setReportDateRange(event.target.value)} />
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col sm={4}>
|
||||
<Col sm={3}>
|
||||
<FormGroup
|
||||
label={intl.formatMessage({'id': 'from_date'})}
|
||||
minimal={true}
|
||||
@@ -100,14 +136,14 @@ export default function BalanceSheetHeader({
|
||||
|
||||
<DateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
defaultValue={new Date()}
|
||||
value={filter.from_date}
|
||||
onChange={handleDateChange('from_date')}
|
||||
popoverProps={{ position: Position.BOTTOM }}
|
||||
fill={true} />
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col sm={4}>
|
||||
<Col sm={3}>
|
||||
<FormGroup
|
||||
label={intl.formatMessage({'id': 'to_date'})}
|
||||
minimal={true}
|
||||
@@ -115,7 +151,7 @@ export default function BalanceSheetHeader({
|
||||
|
||||
<DateInput
|
||||
{...momentFormatter('YYYY/MM/DD')}
|
||||
defaultValue={new Date()}
|
||||
value={filter.to_date}
|
||||
onChange={handleDateChange('to_date')}
|
||||
popoverProps={{ position: Position.BOTTOM }}
|
||||
fill={true} />
|
||||
@@ -124,11 +160,11 @@ export default function BalanceSheetHeader({
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col sm={4}>
|
||||
<Col sm={3}>
|
||||
<FormGroup
|
||||
label={'Display report columns'}
|
||||
className="{'form-group-display-columns-by'}"
|
||||
inline={true}>
|
||||
inline={false}>
|
||||
|
||||
<Select
|
||||
items={displayColumnsByOptions}
|
||||
@@ -145,14 +181,34 @@ export default function BalanceSheetHeader({
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col sm={4}>
|
||||
<Col sm={3}>
|
||||
<FormGroup
|
||||
label={'Show non-zero or active only'}
|
||||
inline={false}>
|
||||
|
||||
<Popover
|
||||
isOpen={activeRowsColumns}
|
||||
content={activeRowsColumnsPopover}
|
||||
minimal={true}
|
||||
position={Position.BOTTOM}>
|
||||
|
||||
<Button
|
||||
rightIcon="caret-down"
|
||||
fill={true}
|
||||
text="Active rows/Columns Active"
|
||||
onClick={onClickActiveRowsColumnsBtn} />
|
||||
</Popover>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col sm={3}>
|
||||
<RadioGroup
|
||||
inline={true}
|
||||
label={intl.formatMessage({'id': 'accounting_basis'})}
|
||||
name="accounting_bahandleRadioChangesis"
|
||||
selectedValue={filter.accounting_basis}
|
||||
onChange={handleStringChange((value) => {
|
||||
setFilterByName('accounting_basis', value);
|
||||
setFilterByKey('accounting_basis', value);
|
||||
})}
|
||||
>
|
||||
<Radio label="Cash" value="cash" />
|
||||
@@ -160,10 +216,10 @@ export default function BalanceSheetHeader({
|
||||
</RadioGroup>
|
||||
</Col>
|
||||
|
||||
<Col sm={4}>
|
||||
<Button intent={Intent.PRIMARY} type="submit" onClick={handleSubmitClick}>
|
||||
{ 'Calculate Report' }
|
||||
</Button>
|
||||
<Col sm={3}>
|
||||
<Button intent={Intent.PRIMARY} type="submit" onClick={handleSubmitClick}>
|
||||
{ 'Calculate Report' }
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</FinancialStatementHeader>
|
||||
|
||||
+99
-12
@@ -1,31 +1,114 @@
|
||||
import React, {useMemo, useState} from 'react';
|
||||
import React, {useMemo, useState, useEffect} from 'react';
|
||||
import FinancialSheet from 'components/FinancialSheet';
|
||||
import DataTable from 'components/DataTable';
|
||||
import FinancialStatementConnect from 'connectors/FinancialStatements.connector';
|
||||
import {compose} from 'utils';
|
||||
import moment from 'moment';
|
||||
|
||||
export default function BalanceSheetTable({
|
||||
function BalanceSheetTable({
|
||||
balanceSheet,
|
||||
balanceSheetIndex,
|
||||
getBalanceSheetColumns,
|
||||
|
||||
getBalanceSheetAssetsAccounts,
|
||||
getBalanceSheetLiabilitiesAccounts,
|
||||
|
||||
getBalanceSheetQuery,
|
||||
}) {
|
||||
const balanceSheetColumns = useMemo(() => {
|
||||
return getBalanceSheetColumns(balanceSheetIndex);
|
||||
}, [getBalanceSheetColumns]);
|
||||
|
||||
const balanceSheetQuery = useMemo(() => {
|
||||
return getBalanceSheetQuery(balanceSheetIndex);
|
||||
}, [getBalanceSheetQuery])
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
// Build our expander column
|
||||
id: 'expander', // Make sure it has an ID
|
||||
Header: ({
|
||||
getToggleAllRowsExpandedProps,
|
||||
isAllRowsExpanded
|
||||
}) => (
|
||||
<span {...getToggleAllRowsExpandedProps()}>
|
||||
{isAllRowsExpanded ? '👇' : '👉'}
|
||||
</span>
|
||||
),
|
||||
Cell: ({ row }) =>
|
||||
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter
|
||||
// to build the toggle for expanding a row
|
||||
row.canExpand ? (
|
||||
<span
|
||||
{...row.getToggleRowExpandedProps({
|
||||
style: {
|
||||
// We can even use the row.depth property
|
||||
// and paddingLeft to indicate the depth
|
||||
// of the row
|
||||
paddingLeft: `${row.depth * 2}rem`,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{row.isExpanded ? '👇' : '👉'}
|
||||
</span>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
Header: 'Account Name',
|
||||
accessor: 'index',
|
||||
accessor: 'name',
|
||||
className: "actions",
|
||||
},
|
||||
{
|
||||
Header: 'Code',
|
||||
accessor: 'note',
|
||||
accessor: 'code',
|
||||
className: "note",
|
||||
},
|
||||
{
|
||||
Header: 'Total',
|
||||
accessor: 'total',
|
||||
className: "credit",
|
||||
},
|
||||
]);
|
||||
|
||||
...(balanceSheetQuery &&
|
||||
balanceSheetQuery.display_columns_by === 'total') ? [
|
||||
{
|
||||
Header: 'Total',
|
||||
accessor: 'balance.formatted_amount',
|
||||
className: "credit",
|
||||
}
|
||||
]: (balanceSheetColumns.map((column, index) => ({
|
||||
Header: column,
|
||||
accessor: (row) => {
|
||||
if (row.periods_balance && row.periods_balance[index]) {
|
||||
return row.periods_balance[index].formatted_amount;
|
||||
}
|
||||
},
|
||||
}))),
|
||||
|
||||
|
||||
|
||||
], [balanceSheetColumns]);
|
||||
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!balanceSheet) { return; }
|
||||
|
||||
setData([
|
||||
{
|
||||
name: 'Assets',
|
||||
code: '',
|
||||
children: [
|
||||
...getBalanceSheetAssetsAccounts(balanceSheetIndex),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Liabilies & Equity',
|
||||
code: '',
|
||||
children: [
|
||||
...getBalanceSheetLiabilitiesAccounts(balanceSheetIndex),
|
||||
]
|
||||
}
|
||||
])
|
||||
}, [])
|
||||
|
||||
// if (balanceSheets.length > 0) {
|
||||
// setData(balanceSheets[0].balance_sheet);
|
||||
// }
|
||||
return (
|
||||
<FinancialSheet
|
||||
companyTitle={'Facebook, Incopration'}
|
||||
@@ -38,4 +121,8 @@ export default function BalanceSheetTable({
|
||||
|
||||
</FinancialSheet>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
FinancialStatementConnect,
|
||||
)(BalanceSheetTable);
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import React, {useState, useEffect, useMemo} from 'react';
|
||||
import ProfitLossSheetHeader from './ProfitLossSheetHeader';
|
||||
import ProfitLossSheetTable from './ProfitLossSheetTable';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import { useAsync } from 'react-use';
|
||||
import moment from 'moment';
|
||||
import {compose} from 'utils';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import ProfitLossSheetConnect from 'connectors/ProfitLossSheet.connect';
|
||||
|
||||
function ProfitLossSheet({
|
||||
changePageTitle,
|
||||
|
||||
fetchProfitLossSheet,
|
||||
|
||||
getProfitLossSheetIndex,
|
||||
getProfitLossSheet,
|
||||
|
||||
getProfitLossSheetAccounts,
|
||||
getProfitLossSheetColumns,
|
||||
}) {
|
||||
const [filter, setFilter] = useState({});
|
||||
const [reload, setReload] = useState(false);
|
||||
|
||||
// Change page title of the dashboard.
|
||||
useEffect(() => {
|
||||
changePageTitle('Trial Balance Sheet');
|
||||
}, []);
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
fetchProfitLossSheet(filter),
|
||||
]);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const profitLossSheetIndex = useMemo(() => {
|
||||
return getProfitLossSheetIndex(filter);
|
||||
}, [filter, getProfitLossSheetIndex]);
|
||||
|
||||
const profitLossSheetAccounts = useMemo(() => {
|
||||
return getProfitLossSheetAccounts(profitLossSheetIndex);
|
||||
}, [profitLossSheetIndex, getProfitLossSheet]);
|
||||
|
||||
const profitLossSheetColumns = useMemo(() => {
|
||||
return getProfitLossSheetColumns(profitLossSheetIndex);
|
||||
}, [profitLossSheetIndex])
|
||||
|
||||
return (
|
||||
<div class="financial-statement">
|
||||
<ProfitLossSheetHeader
|
||||
pageFilter={filter}
|
||||
onSubmitFilter={handleFilterSubmit} />
|
||||
|
||||
<div class="financial-statement__body">
|
||||
<LoadingIndicator loading={fetchHook.pending}>
|
||||
|
||||
<ProfitLossSheetTable
|
||||
accounts={profitLossSheetAccounts}
|
||||
columns={profitLossSheetColumns} />
|
||||
</LoadingIndicator>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
DashboardConnect,
|
||||
ProfitLossSheetConnect
|
||||
)(ProfitLossSheet);
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
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,
|
||||
MenuItem,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
HTMLSelect,
|
||||
Intent,
|
||||
Popover,
|
||||
} from "@blueprintjs/core";
|
||||
import {Select} from '@blueprintjs/select';
|
||||
import {DateInput} from '@blueprintjs/datetime';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
momentFormatter,
|
||||
handleStringChange,
|
||||
parseDateRangeQuery,
|
||||
} from 'utils';
|
||||
import moment from 'moment';
|
||||
|
||||
export default function ProfitLossSheetHeader({
|
||||
onSubmitFilter,
|
||||
pageFilter,
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const displayColumnsByOptions = [
|
||||
{key: 'total', name: 'Total'},
|
||||
{key: 'year', name: 'Year'},
|
||||
{key: 'month', name: 'Month'},
|
||||
{key: 'week', name: 'Week'},
|
||||
{key: 'day', name: 'Day'},
|
||||
{key: 'quarter', name: 'Quarter'},
|
||||
];
|
||||
|
||||
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');
|
||||
|
||||
useEffect(() => {
|
||||
if (reportDateRange === 'custom') { return; }
|
||||
const dateRange = parseDateRangeQuery(reportDateRange);
|
||||
|
||||
if (dateRange) {
|
||||
setFilter((filter) => ({ ...filter, ...dateRange, }));
|
||||
}
|
||||
}, [reportDateRange])
|
||||
|
||||
const selectedDisplayColumnOpt = useMemo(() => {
|
||||
return displayColumnsByOptions.find(o => o.key === filter.display_columns_by);
|
||||
}, [filter.display_columns_by, displayColumnsByOptions]);
|
||||
|
||||
// Account type item of select filed.
|
||||
const accountTypeItem = (item, { handleClick, modifiers, query }) => {
|
||||
return (<MenuItem text={item.name} key={item.id} onClick={handleClick} />);
|
||||
};
|
||||
|
||||
// Handle item select of `display columns by` field.
|
||||
const onItemSelectDisplayColumns = (item) => {
|
||||
setFilterByKey('display_columns_by', item.key);
|
||||
};
|
||||
|
||||
// Handle any date change.
|
||||
const handleDateChange = (name) => (date) => {
|
||||
setReportDateRange('custom');
|
||||
setFilterByKey(name, date);
|
||||
};
|
||||
|
||||
// handle submit filter submit button.
|
||||
const handleSubmitClick = () => {
|
||||
onSubmitFilter(filter);
|
||||
};
|
||||
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 [activeRowsColumns, setActiveRowsColumns] = useState(false);
|
||||
|
||||
const onClickActiveRowsColumnsBtn = () => {
|
||||
setActiveRowsColumns(!activeRowsColumns);
|
||||
};
|
||||
|
||||
const activeRowsColumnsPopover = (
|
||||
<div>
|
||||
<h5>Columns</h5>
|
||||
<RadioGroup
|
||||
name="none_zero"
|
||||
selectedValue={filter.none_zero}
|
||||
onChange={handleStringChange((value) => {
|
||||
setFilterByKey('none_zero', value);
|
||||
})}
|
||||
>
|
||||
<Radio label="All" value="0" />
|
||||
<Radio label="Non-Zero" value="1" />
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
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}>
|
||||
<FormGroup
|
||||
label={'Display report columns'}
|
||||
className="{'form-group-display-columns-by'}"
|
||||
inline={false}>
|
||||
|
||||
<Select
|
||||
items={displayColumnsByOptions}
|
||||
noResults={<MenuItem disabled={true} text="No results." />}
|
||||
filterable={false}
|
||||
itemRenderer={accountTypeItem}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onItemSelectDisplayColumns}>
|
||||
<Button
|
||||
rightIcon="caret-down"
|
||||
fill={true}
|
||||
text={selectedDisplayColumnOpt ? selectedDisplayColumnOpt.name : 'Select'} />
|
||||
</Select>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col sm={3}>
|
||||
<FormGroup
|
||||
label={'Show non-zero or active only'}
|
||||
inline={false}>
|
||||
|
||||
<Popover
|
||||
isOpen={activeRowsColumns}
|
||||
content={activeRowsColumnsPopover}
|
||||
minimal={true}
|
||||
position={Position.BOTTOM}>
|
||||
|
||||
<Button
|
||||
rightIcon="caret-down"
|
||||
fill={true}
|
||||
text="Active rows/Columns Active"
|
||||
onClick={onClickActiveRowsColumnsBtn} />
|
||||
</Popover>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col sm={3}>
|
||||
<RadioGroup
|
||||
inline={true}
|
||||
label={intl.formatMessage({'id': 'accounting_basis'})}
|
||||
name="accounting_bahandleRadioChangesis"
|
||||
selectedValue={filter.accounting_basis}
|
||||
onChange={handleStringChange((value) => {
|
||||
setFilterByKey('accounting_basis', value);
|
||||
})}
|
||||
>
|
||||
<Radio label="Cash" value="cash" />
|
||||
<Radio label="Accural" value="accural" />
|
||||
</RadioGroup>
|
||||
</Col>
|
||||
|
||||
<Col sm={3}>
|
||||
<Button intent={Intent.PRIMARY} type="submit" onClick={handleSubmitClick}>
|
||||
{ 'Calculate Report' }
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</FinancialStatementHeader>
|
||||
)
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import TrialBalanceSheetHeader from "./TrialBalanceSheetHeader";
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import TrialBalanceSheetTable from './TrialBalanceSheetTable';
|
||||
import { useAsync } from 'react-use';
|
||||
import moment from 'moment';
|
||||
import {compose} from 'utils';
|
||||
import TrialBalanceSheetConnect from 'connectors/TrialBalanceSheet.connect';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
|
||||
function TrialBalanceSheet({
|
||||
changePageTitle,
|
||||
fetchTrialBalanceSheet,
|
||||
getTrialBalanceSheetIndex,
|
||||
getTrialBalanceAccounts,
|
||||
}) {
|
||||
const [filter, setFilter] = useState({
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
basis: 'cash',
|
||||
none_zero: false,
|
||||
});
|
||||
const [reload, setReload] = useState(false);
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
fetchTrialBalanceSheet(),
|
||||
]);
|
||||
});
|
||||
|
||||
// Retrieve balance sheet index by the given filter query.
|
||||
const trialBalanceSheetIndex = useMemo(() => {
|
||||
return getTrialBalanceSheetIndex(filter);
|
||||
}, [getTrialBalanceSheetIndex, filter]);
|
||||
|
||||
// Retrieve balance sheet accounts bu the given sheet index.
|
||||
const trialBalanceAccounts = useMemo(() => {
|
||||
return getTrialBalanceAccounts(trialBalanceSheetIndex);
|
||||
}, [trialBalanceSheetIndex]);
|
||||
|
||||
// Change page title of the dashboard.
|
||||
useEffect(() => {
|
||||
changePageTitle('Trial Balance Sheet');
|
||||
}, []);
|
||||
|
||||
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">
|
||||
<TrialBalanceSheetHeader
|
||||
pageFilter={filter}
|
||||
onSubmitFilter={handleFilterSubmit} />
|
||||
|
||||
<div class="financial-statement__body">
|
||||
<LoadingIndicator loading={fetchHook.pending}>
|
||||
<TrialBalanceSheetTable
|
||||
trialBalanceSheetAccounts={trialBalanceAccounts}
|
||||
trialBalanceSheetIndex={trialBalanceSheetIndex} />
|
||||
</LoadingIndicator>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default compose(
|
||||
DashboardConnect,
|
||||
TrialBalanceSheetConnect,
|
||||
)(TrialBalanceSheet);
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import React, {useState} from 'react';
|
||||
import FinancialStatementHeader from 'containers/Dashboard/FinancialStatements/FinancialStatementHeader';
|
||||
import {Row, Col} from 'react-grid-system';
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
Position,
|
||||
MenuItem,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
HTMLSelect,
|
||||
Intent,
|
||||
Popover,
|
||||
} from "@blueprintjs/core";
|
||||
import {DateInput} from '@blueprintjs/datetime';
|
||||
import moment from 'moment';
|
||||
import {momentFormatter} from 'utils';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
export default function TrialBalanceSheetHeader({
|
||||
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);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import React, {useEffect, useState, useMemo} from 'react';
|
||||
import FinancialSheet from 'components/FinancialSheet';
|
||||
import DataTable from 'components/DataTable';
|
||||
|
||||
|
||||
export default function TrialBalanceSheetTable({
|
||||
trialBalanceSheetAccounts,
|
||||
trialBalanceSheetIndex,
|
||||
}) {
|
||||
const [data, setData] = useState([]);
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
// Build our expander column
|
||||
id: 'expander', // Make sure it has an ID
|
||||
Header: ({
|
||||
getToggleAllRowsExpandedProps,
|
||||
isAllRowsExpanded
|
||||
}) => (
|
||||
<span {...getToggleAllRowsExpandedProps()}>
|
||||
{isAllRowsExpanded ? '👇' : '👉'}
|
||||
</span>
|
||||
),
|
||||
Cell: ({ row }) =>
|
||||
// Use the row.canExpand and row.getToggleRowExpandedProps prop getter
|
||||
// to build the toggle for expanding a row
|
||||
row.canExpand ? (
|
||||
<span
|
||||
{...row.getToggleRowExpandedProps({
|
||||
style: {
|
||||
// We can even use the row.depth property
|
||||
// and paddingLeft to indicate the depth
|
||||
// of the row
|
||||
paddingLeft: `${row.depth * 2}rem`,
|
||||
},
|
||||
})}
|
||||
>
|
||||
{row.isExpanded ? '👇' : '👉'}
|
||||
</span>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
Header: 'Account Name',
|
||||
accessor: 'name',
|
||||
className: "actions",
|
||||
},
|
||||
{
|
||||
Header: 'Code',
|
||||
accessor: 'code',
|
||||
className: "note",
|
||||
},
|
||||
{
|
||||
Header: 'Credit',
|
||||
accessor: 'credit',
|
||||
className: 'credit',
|
||||
},
|
||||
{
|
||||
Header: 'debit',
|
||||
accessor: 'debit',
|
||||
className: 'debit',
|
||||
},
|
||||
{
|
||||
Header: 'Balance',
|
||||
accessor: 'balance',
|
||||
className: 'balance',
|
||||
}
|
||||
], []);
|
||||
|
||||
return (
|
||||
<FinancialSheet
|
||||
companyTitle={'Facebook, Incopration'}
|
||||
sheetType={'Trial Balance Sheet'}
|
||||
date={''}>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={trialBalanceSheetAccounts} />
|
||||
</FinancialSheet>
|
||||
);
|
||||
}
|
||||
@@ -75,18 +75,25 @@ export default [
|
||||
loader: () => import('containers/Dashboard/FinancialStatements/LedgerSheet')
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/accounting/trial-balance`,
|
||||
name: 'dashboard.accounting.trial.balance',
|
||||
component: LazyLoader({
|
||||
loader: () => import('containers/Dashboard/FinancialStatements/TrialBalanceSheet')
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/accounting/balance-sheet`,
|
||||
name: 'dashboard.accounting.balance.sheet',
|
||||
component: LazyLoader({
|
||||
loader: () => import('containers/Dashboard/FinancialStatements/BalanceSheet')
|
||||
loader: () => import('containers/Dashboard/FinancialStatements/BalanceSheet/BalanceSheet')
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/accounting/trial-balance-sheet`,
|
||||
name: 'dashboard.accounting.trial.balance',
|
||||
component: LazyLoader({
|
||||
loader: () => import('containers/Dashboard/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet')
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/accounting/profit-loss-sheet`,
|
||||
name: 'dashboard.accounting.profit.loss.sheet',
|
||||
component: LazyLoader({
|
||||
loader: () => import('containers/Dashboard/FinancialStatements/ProfitLossSheet/ProfitLossSheet')
|
||||
}),
|
||||
}
|
||||
];
|
||||
@@ -19,8 +19,33 @@ export const fetchBalanceSheet = ({ query }) => {
|
||||
dispatch({
|
||||
type: t.BALANCE_SHEET_STATEMENT_SET,
|
||||
data: response.data,
|
||||
query: query,
|
||||
});
|
||||
resolve(response);
|
||||
}).catch((error) => { reject(error); });
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchTrialBalanceSheet = ({ query }) => {
|
||||
return (dispatch) => new Promise((resolve, reject) => {
|
||||
ApiService.get('/financial_statements/trial_balance_sheet').then((response) => {
|
||||
dispatch({
|
||||
type: t.TRAIL_BALANCE_STATEMENT_SET,
|
||||
data: response.data,
|
||||
});
|
||||
resolve(response.data);
|
||||
}).catch((error) => { reject(error); })
|
||||
})
|
||||
};
|
||||
|
||||
export const fetchProfitLossSheet = ({ query }) => {
|
||||
return (dispatch) => new Promise((resolve, reject) => {
|
||||
ApiService.get('/financial_statements/profit_loss_sheet').then((response) => {
|
||||
dispatch({
|
||||
type: t.PROFIT_LOSS_STATEMENT_SET,
|
||||
data: response.data,
|
||||
});
|
||||
resolve(response.data);
|
||||
}).catch((error) => { reject(error); });
|
||||
})
|
||||
};
|
||||
@@ -1,14 +1,41 @@
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import t from 'store/types';
|
||||
import {getBalanceSheetIndexByQuery, getTrialBalanceSheetIndex} from './financialStatements.selectors';
|
||||
import { actionComplete } from '@syncfusion/ej2-react-grids';
|
||||
|
||||
const initialState = {
|
||||
balanceSheet: [],
|
||||
balanceSheets: [],
|
||||
trialBalanceSheets: [],
|
||||
generalLedger: [],
|
||||
trialBalance: [],
|
||||
};
|
||||
|
||||
export default createReducer(initialState, {
|
||||
[t.BALANCE_SHEET_STATEMENT_SET]: (state, action) => {
|
||||
state.balanceSheet.push(action.data);
|
||||
const index = getBalanceSheetIndexByQuery(state.balanceSheets, action.query);
|
||||
|
||||
const balanceSheet = {
|
||||
balances: action.data.balance_sheet,
|
||||
columns: Object.values(action.data.columns),
|
||||
query: action.data.query,
|
||||
};
|
||||
if (index !== -1) {
|
||||
state.balanceSheets[index] = balanceSheet;
|
||||
} else {
|
||||
state.balanceSheets.push(balanceSheet);
|
||||
}
|
||||
},
|
||||
|
||||
[t.TRAIL_BALANCE_STATEMENT_SET]: (state, action) => {
|
||||
const index = getTrialBalanceSheetIndex(state.trialBalanceSheets, action.query);
|
||||
|
||||
const trailBalanceSheet = {
|
||||
accounts: action.data.accounts,
|
||||
query: action.data.query,
|
||||
};
|
||||
if (index !== -1) {
|
||||
state.trialBalanceSheets[index] = trailBalanceSheet;
|
||||
} else {
|
||||
state.trailBalanceSheet.push(trailBalanceSheet);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import {getObjectDiff} from 'utils';
|
||||
|
||||
|
||||
// Balance Sheet.
|
||||
export const getBalanceSheetByQuery = (balanceSheets, query) => {
|
||||
return balanceSheets.find(balanceSheet => {
|
||||
return getObjectDiff(query, balanceSheet.query).length === 0;
|
||||
});
|
||||
};
|
||||
|
||||
export const getBalanceSheetIndexByQuery = (balanceSheets, query) => {
|
||||
return balanceSheets.findIndex((balanceSheet) => {
|
||||
return getObjectDiff(query, balanceSheet.query).length === 0;
|
||||
});
|
||||
};
|
||||
|
||||
export const getBalanceSheetByIndex = (balanceSheets, sheetIndex) => {
|
||||
return balanceSheets[sheetIndex];
|
||||
};
|
||||
|
||||
export const getBalanceSheetQuery = (balanceSheets, sheetIndex) => {
|
||||
if (typeof balanceSheets[sheetIndex] === 'object') {
|
||||
return balanceSheets[sheetIndex].query || {};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const getBalanceSheetAssetsAccounts = (balanceSheets, sheetIndex) => {
|
||||
if (typeof balanceSheets[sheetIndex] === 'object') {
|
||||
return balanceSheets[sheetIndex].balances.assets.accounts || [];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getBalanceSheetLiabilitiesAccounts = (balanceSheets, sheetIndex) => {
|
||||
if (typeof balanceSheets[sheetIndex] === 'object') {
|
||||
return balanceSheets[sheetIndex].balances.liabilities_equity.accounts || [];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getBalanceSheetColumns = (balanceSheets, sheetIndex) => {
|
||||
if (typeof balanceSheets[sheetIndex] === 'object') {
|
||||
return balanceSheets[sheetIndex].columns;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
|
||||
// Trial Balance Sheet.
|
||||
export const getTrialBalanceSheetIndex = (trialBalanceSheets, query) => {
|
||||
return trialBalanceSheets.find((trialBalanceSheet) => {
|
||||
return getObjectDiff(query, trialBalanceSheet.query).length === 0;
|
||||
});
|
||||
};
|
||||
|
||||
export const getTrialBalanceAccounts = (trialBalanceSheets, sheetIndex) => {
|
||||
if (typeof trialBalanceSheets[sheetIndex] === 'object') {
|
||||
return trialBalanceSheets[sheetIndex].accounts;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getTrialBalanceQuery = (trialBalanceSheets, sheetIndex) => {
|
||||
if (typeof trialBalanceSheets[sheetIndex] === 'object') {
|
||||
return trialBalanceSheets[sheetIndex].query;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// Profit/Loss Sheet selectors.
|
||||
export const getProfitLossSheetIndex = (profitLossSheets, query) => {
|
||||
return profitLossSheets.find((profitLossSheet) => {
|
||||
return getObjectDiff(query, profitLossSheet.query).length === 0;
|
||||
});
|
||||
}
|
||||
|
||||
export const getProfitLossSheet = (profitLossSheets, index) => {
|
||||
return (typeof profitLossSheets[index] !== 'undefined') ?
|
||||
profitLossSheets[index] : null;
|
||||
};
|
||||
|
||||
export const getProfitLossSheetColumns = (profitLossSheets, index) => {
|
||||
const sheet = getProfitLossSheet(profitLossSheets, index);
|
||||
return (sheet) ? sheet.columns : [];
|
||||
};
|
||||
|
||||
export const getProfitLossSheetAccounts = (profitLossSheets, index) => {
|
||||
const sheet = getProfitLossSheet(profitLossSheets, index);
|
||||
return (sheet) ? sheet.accounts : [];
|
||||
};
|
||||
@@ -3,4 +3,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',
|
||||
}
|
||||
+36
-1
@@ -1,4 +1,5 @@
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
|
||||
export function removeEmptyFromObject(obj) {
|
||||
obj = Object.assign({}, obj);
|
||||
@@ -76,4 +77,38 @@ export const objectKeysTransform = (obj, transform) => {
|
||||
};
|
||||
|
||||
export const compose = (...funcs) =>
|
||||
funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg);
|
||||
funcs.reduce((a, b) => (...args) => a(b(...args)), arg => arg);
|
||||
|
||||
export const getObjectDiff = (a, b) => {
|
||||
return _.reduce(a, (result, value, key) => {
|
||||
return _.isEqual(value, b[key]) ?
|
||||
result : result.concat(key);
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const parseDateRangeQuery = (keyword) => {
|
||||
const queries = {
|
||||
'today': {
|
||||
range: 'day',
|
||||
},
|
||||
'this_year': {
|
||||
range: 'year',
|
||||
},
|
||||
'this_month': {
|
||||
range: 'month'
|
||||
},
|
||||
'this_week': {
|
||||
range: 'week'
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof queries[keyword] === 'undefined') {
|
||||
throw new Error(`The date range query ${keyword} is not defined.`);
|
||||
}
|
||||
const query = queries[keyword];
|
||||
|
||||
return {
|
||||
from_date: moment().startOf(query.range).toDate(),
|
||||
to_date: moment().endOf(query.range).toDate(),
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -10,5 +10,5 @@ MAIL_FROM_NAME=
|
||||
DB_CLIENT=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_USER=root
|
||||
DB_PASSWORD=123123123
|
||||
DB_PASSWORD=root
|
||||
DB_NAME=ratteb
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ MAIL_FROM_NAME=
|
||||
DB_CLIENT=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_USER=root
|
||||
DB_PASSWORD=123123123
|
||||
DB_PASSWORD=root
|
||||
DB_NAME=ratteb
|
||||
|
||||
JWT_SECRET_KEY=ahmedmohamked
|
||||
|
||||
Vendored
+241
-81
File diff suppressed because one or more lines are too long
Generated
+98
-19
@@ -2228,14 +2228,6 @@
|
||||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"collections": {
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/collections/-/collections-5.1.9.tgz",
|
||||
"integrity": "sha512-omsKk7VkxuYKsxKG9jdyNtqTVJXZuwLnK53lc7M7AW4cXKcEyK3F+4cfQFVOy4ivT+UWhJlvClrJI7qF2Ts6iA==",
|
||||
"requires": {
|
||||
"weak-map": "~1.0.x"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@@ -3915,8 +3907,7 @@
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "1.2.9",
|
||||
@@ -4788,6 +4779,26 @@
|
||||
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
|
||||
"dev": true
|
||||
},
|
||||
"i18n": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/i18n/-/i18n-0.8.5.tgz",
|
||||
"integrity": "sha512-6UgLbhJGgn4XFeuZc/dDdrrri0ij24EK4hxv4Pbi5hloYAZ1B2+0eQchEryBFezLKYOHhVGV/5+H4i0oxng94w==",
|
||||
"requires": {
|
||||
"debug": "*",
|
||||
"make-plural": "^6.0.1",
|
||||
"math-interval-parser": "^2.0.1",
|
||||
"messageformat": "^2.3.0",
|
||||
"mustache": "*",
|
||||
"sprintf-js": "^1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@@ -4869,7 +4880,6 @@
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
@@ -5570,6 +5580,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"knex-db-manager": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/knex-db-manager/-/knex-db-manager-0.6.1.tgz",
|
||||
"integrity": "sha512-H1IBb/zUwFXYyok8n/WmtRNV/DcZ77lV9qXqjpoF4otFPgf3W85/l79Dq/8gOE+0WnawLsYKXD8Hdz5VsaHIzQ==",
|
||||
"requires": {
|
||||
"bluebird": "^3.7.2",
|
||||
"glob": "^7.1.6",
|
||||
"lodash": "^4.17.15"
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"knex-factory": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/knex-factory/-/knex-factory-0.0.6.tgz",
|
||||
@@ -5847,6 +5887,11 @@
|
||||
"kind-of": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"make-plural": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-plural/-/make-plural-6.1.0.tgz",
|
||||
"integrity": "sha512-0ekbPHqxcdRcmjZ43TkRuejK5rXgMF1OjG4FVnVHgCvOcjrexaSX7a0dfAvqhOm1qWPgjYnXtmz3cHpHW5ZewA=="
|
||||
},
|
||||
"mamacro": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz",
|
||||
@@ -5875,6 +5920,11 @@
|
||||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"math-interval-parser": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-2.0.1.tgz",
|
||||
"integrity": "sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA=="
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
|
||||
@@ -5939,6 +5989,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"messageformat": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/messageformat/-/messageformat-2.3.0.tgz",
|
||||
"integrity": "sha512-uTzvsv0lTeQxYI2y1NPa1lItL5VRI8Gb93Y2K2ue5gBPyrbJxfDi/EYWxh2PKv5yO42AJeeqblS9MJSh/IEk4w==",
|
||||
"requires": {
|
||||
"make-plural": "^4.3.0",
|
||||
"messageformat-formatters": "^2.0.1",
|
||||
"messageformat-parser": "^4.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"make-plural": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz",
|
||||
"integrity": "sha512-xTYd4JVHpSCW+aqDof6w/MebaMVNTVYBZhbB/vi513xXdiPT92JMVCo0Jq8W2UZnzYRFeVbQiQ+I25l13JuKvA==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"messageformat-formatters": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/messageformat-formatters/-/messageformat-formatters-2.0.1.tgz",
|
||||
"integrity": "sha512-E/lQRXhtHwGuiQjI7qxkLp8AHbMD5r2217XNe/SREbBlSawe0lOqsFb7rflZJmlQFSULNLIqlcjjsCPlB3m3Mg=="
|
||||
},
|
||||
"messageformat-parser": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-4.1.2.tgz",
|
||||
"integrity": "sha512-7dWuifeyldz7vhEuL96Kwq1fhZXBW+TUfbnHN4UCrCxoXQTYjHnR78eI66Gk9LaLLsAvzPNVJBaa66DRfFNaiA=="
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
@@ -6944,7 +7030,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -9100,11 +9185,6 @@
|
||||
"neo-async": "^2.5.0"
|
||||
}
|
||||
},
|
||||
"weak-map": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.5.tgz",
|
||||
"integrity": "sha1-eWkVhNmGB/UHC9O3CkDmuyLkAes="
|
||||
},
|
||||
"webpack": {
|
||||
"version": "4.39.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.39.2.tgz",
|
||||
@@ -9556,8 +9636,7 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"write": {
|
||||
"version": "1.0.3",
|
||||
|
||||
@@ -29,8 +29,10 @@
|
||||
"express-oauth-server": "^2.0.0",
|
||||
"express-validator": "^6.2.0",
|
||||
"helmet": "^3.21.0",
|
||||
"i18n": "^0.8.5",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"knex": "^0.20.3",
|
||||
"knex-db-manager": "^0.6.1",
|
||||
"lodash": "^4.17.15",
|
||||
"memory-cache": "^0.2.0",
|
||||
"moment": "^2.24.0",
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import boom from 'express-boom';
|
||||
import i18n from 'i18n';
|
||||
import '../config';
|
||||
import routes from '@/http';
|
||||
import '@/models';
|
||||
|
||||
const app = express();
|
||||
|
||||
// i18n.configure({
|
||||
// // setup some locales - other locales default to en silently
|
||||
// locales: ['en'],
|
||||
|
||||
// // sets a custom cookie name to parse locale settings from.
|
||||
// cookie: 'yourcookiename',
|
||||
|
||||
// // where to store json files - defaults to './locales'
|
||||
// directory: `${__dirname}/resources/locale`,
|
||||
// });
|
||||
|
||||
// i18n init parses req for language headers, cookies, etc.
|
||||
// app.use(i18n.init);
|
||||
|
||||
// Express configuration
|
||||
app.set('port', process.env.PORT || 3000);
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import knexManager from 'knex-db-manager';
|
||||
import knexfile from '@/../knexfile';
|
||||
|
||||
const config = knexfile[process.env.NODE_ENV];
|
||||
|
||||
const dbManager = knexManager.databaseManagerFactory({
|
||||
knex: config,
|
||||
dbManager: {
|
||||
// db manager related configuration
|
||||
collate: [],
|
||||
superUser: 'root',
|
||||
superPassword: 'root',
|
||||
// populatePathPattern: 'data/**/*.js', // glob format for searching seeds
|
||||
},
|
||||
});
|
||||
|
||||
export default dbManager;
|
||||
@@ -2,8 +2,8 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('user_has_roles', (table) => {
|
||||
table.increments();
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('role_id').unsigned().references('id').inTable('roles');
|
||||
table.integer('user_id').unsigned();
|
||||
table.integer('role_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ exports.up = function(knex) {
|
||||
table.integer('client_id').unsigned();
|
||||
table.string('refresh_token');
|
||||
table.date('refresh_token_expires_on');
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('user_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('settings', (table) => {
|
||||
table.increments();
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('user_id').unsigned();
|
||||
table.string('group');
|
||||
table.string('type');
|
||||
table.string('key');
|
||||
|
||||
@@ -13,7 +13,7 @@ exports.up = function (knex) {
|
||||
table.boolean('columnable');
|
||||
table.integer('index');
|
||||
table.json('options');
|
||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||
table.integer('resource_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('role_has_accounts', (table) => {
|
||||
table.increments();
|
||||
table.integer('role_id').unsigned().references('id').inTable('roles');
|
||||
table.integer('account_id').unsigned().references('id').inTable('accounts');
|
||||
table.integer('role_id').unsigned();
|
||||
table.integer('account_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('role_has_permissions', (table) => {
|
||||
table.increments();
|
||||
table.integer('role_id').unsigned().references('id').inTable('roles');
|
||||
table.integer('permission_id').unsigned().references('id').inTable('permissions');
|
||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||
table.integer('role_id').unsigned();
|
||||
table.integer('permission_id').unsigned();
|
||||
table.integer('resource_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ exports.up = function (knex) {
|
||||
return knex.schema.createTable('view_roles', (table) => {
|
||||
table.increments();
|
||||
table.integer('index');
|
||||
table.integer('field_id').unsigned().references('id').inTable('resource_fields');
|
||||
table.integer('field_id').unsigned();
|
||||
table.string('comparator');
|
||||
table.string('value');
|
||||
table.integer('view_id').unsigned();
|
||||
|
||||
@@ -7,10 +7,10 @@ exports.up = function(knex) {
|
||||
table.string('transaction_type');
|
||||
table.string('reference_type');
|
||||
table.integer('reference_id');
|
||||
table.integer('account_id').unsigned().references('id').inTable('accounts');
|
||||
table.integer('account_id').unsigned();
|
||||
table.string('note');
|
||||
table.boolean('draft').defaultTo(false);
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('user_id').unsigned();
|
||||
table.date('date');
|
||||
table.timestamps();
|
||||
});
|
||||
|
||||
@@ -6,11 +6,11 @@ exports.up = function(knex) {
|
||||
table.string('currency_code');
|
||||
table.decimal('exchange_rate');
|
||||
table.text('description');
|
||||
table.integer('expense_account_id').unsigned().references('id').inTable('accounts');
|
||||
table.integer('payment_account_id').unsigned().references('id').inTable('accounts');
|
||||
table.integer('expense_account_id').unsigned();
|
||||
table.integer('payment_account_id').unsigned();
|
||||
table.string('reference');
|
||||
table.boolean('published').defaultTo(false);
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('user_id').unsigned();
|
||||
table.date('date');
|
||||
// table.timestamps();
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ exports.up = function(knex) {
|
||||
table.decimal('amount');
|
||||
table.date('date');
|
||||
table.string('note');
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.integer('user_id').unsigned();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('budget_entries', (table) => {
|
||||
table.increments();
|
||||
table.integer('budget_id').unsigned().references('id').inTable('budgets');
|
||||
table.integer('account_id').unsigned().references('id').inTable('accounts');
|
||||
table.integer('budget_id').unsigned();
|
||||
table.integer('account_id').unsigned();
|
||||
table.decimal('amount', 15, 5);
|
||||
table.integer('order');
|
||||
})
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('resource_custom_fields_metadata', (table) => {
|
||||
table.increments();
|
||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||
table.integer('resource_id').unsigned();
|
||||
table.integer('resource_item_id').unsigned();
|
||||
table.string('key');
|
||||
table.string('value');
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
|
||||
@@ -8,54 +8,63 @@ exports.seed = (knex) => {
|
||||
{
|
||||
id: 1,
|
||||
name: 'Fixed Asset',
|
||||
normal: 'debit',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Current Asset',
|
||||
normal: 'debit',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Long Term Liability',
|
||||
normal: 'credit',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Current Liability',
|
||||
normal: 'credit',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Equity',
|
||||
normal: 'credit',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Expense',
|
||||
normal: 'debit',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: 'Income',
|
||||
normal: 'credit',
|
||||
balance_sheet: false,
|
||||
income_sheet: true,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: 'Accounts Receivable',
|
||||
normal: 'debit',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: 'Accounts Payable',
|
||||
normal: 'credit',
|
||||
balance_sheet: true,
|
||||
income_sheet: false,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { check, query, validationResult } from 'express-validator';
|
||||
import { check, query, oneOf, validationResult } from 'express-validator';
|
||||
import express from 'express';
|
||||
import { difference } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import Account from '@/models/Account';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
@@ -38,9 +39,10 @@ export default {
|
||||
validation: [
|
||||
check('date').isISO8601(),
|
||||
check('reference').exists(),
|
||||
check('memo').optional().trim().escape(),
|
||||
check('entries').isArray({ min: 1 }),
|
||||
check('entries.*.credit').isNumeric().toInt(),
|
||||
check('entries.*.debit').isNumeric().toInt(),
|
||||
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.account_id').isNumeric().toInt(),
|
||||
check('entries.*.note').optional(),
|
||||
],
|
||||
@@ -56,11 +58,16 @@ export default {
|
||||
date: new Date(),
|
||||
...req.body,
|
||||
};
|
||||
const errorReasons = [];
|
||||
|
||||
let totalCredit = 0;
|
||||
let totalDebit = 0;
|
||||
|
||||
form.entries.forEach((entry) => {
|
||||
const { user } = req;
|
||||
const errorReasons = [];
|
||||
const entries = form.entries.filter((entry) => (entry.credit || entry.debit));
|
||||
const formattedDate = moment(form.date).format('YYYY-MM-DD');
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (entry.credit > 0) {
|
||||
totalCredit += entry.credit;
|
||||
}
|
||||
@@ -68,6 +75,7 @@ export default {
|
||||
totalDebit += entry.debit;
|
||||
}
|
||||
});
|
||||
|
||||
if (totalCredit <= 0 || totalDebit <= 0) {
|
||||
errorReasons.push({
|
||||
type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
|
||||
@@ -77,7 +85,7 @@ export default {
|
||||
if (totalCredit !== totalDebit) {
|
||||
errorReasons.push({ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 100 });
|
||||
}
|
||||
const accountsIds = form.entries.map((entry) => entry.account_id);
|
||||
const accountsIds = entries.map((entry) => entry.account_id);
|
||||
const accounts = await Account.query().whereIn('id', accountsIds)
|
||||
.withGraphFetched('type');
|
||||
|
||||
@@ -95,18 +103,30 @@ export default {
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
|
||||
// Save manual journal transaction.
|
||||
const manualJournal = await ManualJournal.query().insert({
|
||||
reference: form.reference,
|
||||
transaction_type: 'Journal',
|
||||
amount: totalCredit,
|
||||
date: formattedDate,
|
||||
note: form.memo,
|
||||
user_id: user.id,
|
||||
});
|
||||
const journalPoster = new JournalPoster();
|
||||
|
||||
form.entries.forEach((entry) => {
|
||||
entries.forEach((entry) => {
|
||||
const account = accounts.find((a) => a.id === entry.account_id);
|
||||
|
||||
const jouranlEntry = new JournalEntry({
|
||||
date: entry.date,
|
||||
debit: entry.debit,
|
||||
credit: entry.credit,
|
||||
account: account.id,
|
||||
transactionType: 'Journal',
|
||||
accountNormal: account.type.normal,
|
||||
note: entry.note,
|
||||
date: formattedDate,
|
||||
userId: user.id,
|
||||
});
|
||||
if (entry.debit) {
|
||||
journalPoster.debit(jouranlEntry);
|
||||
@@ -120,7 +140,7 @@ export default {
|
||||
journalPoster.saveEntries(),
|
||||
journalPoster.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send();
|
||||
return res.status(200).send({ id: manualJournal.id });
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -1,10 +1,71 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
check,
|
||||
param,
|
||||
query,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
|
||||
export default {
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/',
|
||||
this.newCustomer.validation,
|
||||
asyncMiddleware(this.newCustomer.handler));
|
||||
|
||||
router.post('/:id',
|
||||
this.editCustomer.validation,
|
||||
asyncMiddleware(this.editCustomer.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
newCustomer: {
|
||||
validation: [
|
||||
check('custom_type').exists().trim().escape(),
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name'),
|
||||
check('company_name'),
|
||||
check('email'),
|
||||
check('work_phone'),
|
||||
check('personal_phone'),
|
||||
|
||||
check('billing_address.country'),
|
||||
check('billing_address.address'),
|
||||
check('billing_address.city'),
|
||||
check('billing_address.phone'),
|
||||
check('billing_address.zip_code'),
|
||||
|
||||
check('shiping_address.country'),
|
||||
check('shiping_address.address'),
|
||||
check('shiping_address.city'),
|
||||
check('shiping_address.phone'),
|
||||
check('shiping_address.zip_code'),
|
||||
|
||||
check('contact.additional_phone'),
|
||||
check('contact.additional_email'),
|
||||
|
||||
check('custom_fields').optional().isArray({ min: 1 }),
|
||||
check('custom_fields.*.key').exists().trim().escape(),
|
||||
check('custom_fields.*.value').exists(),
|
||||
|
||||
check('inactive').optional().isBoolean().toBoolean(),
|
||||
],
|
||||
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
editCustomer: {
|
||||
validation: [
|
||||
|
||||
],
|
||||
async handler(req, res) {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -317,6 +317,12 @@ export default {
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
|
||||
query('filter_roles').optional().isArray(),
|
||||
query('filter_roles.*.field_key').exists().escape().trim(),
|
||||
query('filter_roles.*.value').exists().escape().trim(),
|
||||
query('filter_roles.*.comparator').exists().escape().trim(),
|
||||
query('filter_roles.*.index').exists().isNumeric().toInt(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
@@ -122,6 +122,7 @@ export default {
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('accounts_ids').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -134,16 +135,21 @@ export default {
|
||||
const filter = {
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
basis: 'cash',
|
||||
number_format: {
|
||||
no_cents: false,
|
||||
divide_1000: false,
|
||||
},
|
||||
none_zero: false,
|
||||
accounts_ids: [],
|
||||
...req.query,
|
||||
};
|
||||
|
||||
const accounts = await Account.query()
|
||||
.orderBy('index', 'DESC')
|
||||
.modify('filterAccounts', filter.accounts_ids)
|
||||
.withGraphFetched('transactions')
|
||||
.withGraphFetched('type')
|
||||
.modifyGraph('transactions', (builder) => {
|
||||
builder.modify('filterDateRange', filter.from_date, filter.to_date);
|
||||
});
|
||||
@@ -167,33 +173,40 @@ export default {
|
||||
// Transaction amount formatter based on the given query.
|
||||
const formatNumber = formatNumberClosure(filter.number_format);
|
||||
|
||||
const items = [
|
||||
...accounts
|
||||
.filter((account) => (
|
||||
account.transactions.length > 0 || !filter.none_zero
|
||||
))
|
||||
.map((account) => ({
|
||||
...pick(account, ['id', 'name', 'code', 'index']),
|
||||
transactions: [
|
||||
...account.transactions.map((transaction) => ({
|
||||
const items = accounts
|
||||
.filter((account) => (
|
||||
account.transactions.length > 0 || !filter.none_zero
|
||||
))
|
||||
.map((account) => ({
|
||||
...pick(account, ['id', 'name', 'code', 'index']),
|
||||
transactions: [
|
||||
...account.transactions.map((transaction) => {
|
||||
let amount = 0;
|
||||
|
||||
if (account.type.normal === 'credit') {
|
||||
amount += transaction.credit - transaction.credit;
|
||||
} else if (account.type.normal === 'debit') {
|
||||
amount += transaction.debit - transaction.credit;
|
||||
}
|
||||
return {
|
||||
...transaction,
|
||||
credit: formatNumber(transaction.credit),
|
||||
debit: formatNumber(transaction.debit),
|
||||
})),
|
||||
],
|
||||
opening: {
|
||||
date: filter.from_date,
|
||||
balance: opeingBalanceCollection.getClosingBalance(account.id),
|
||||
},
|
||||
closing: {
|
||||
date: filter.to_date,
|
||||
balance: closingBalanceCollection.getClosingBalance(account.id),
|
||||
},
|
||||
})),
|
||||
];
|
||||
amount: formatNumber(amount),
|
||||
};
|
||||
}),
|
||||
],
|
||||
opening: {
|
||||
date: filter.from_date,
|
||||
balance: opeingBalanceCollection.getClosingBalance(account.id),
|
||||
},
|
||||
closing: {
|
||||
date: filter.to_date,
|
||||
balance: closingBalanceCollection.getClosingBalance(account.id),
|
||||
},
|
||||
}));
|
||||
|
||||
return res.status(200).send({
|
||||
meta: { ...filter },
|
||||
items,
|
||||
query: { ...filter },
|
||||
accounts: items,
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -206,7 +219,7 @@ export default {
|
||||
query('accounting_method').optional().isIn(['cash', 'accural']),
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
query('display_columns_by').optional().isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
query('display_columns_by').optional().isIn(['total', 'year', 'month', 'week', 'day', 'quarter']),
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
@@ -220,7 +233,7 @@ export default {
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
display_columns_by: 'year',
|
||||
display_columns_by: 'total',
|
||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
number_format: {
|
||||
@@ -228,11 +241,11 @@ export default {
|
||||
divide_1000: false,
|
||||
},
|
||||
none_zero: false,
|
||||
basis: 'cash',
|
||||
...req.query,
|
||||
};
|
||||
|
||||
const balanceSheetTypes = await AccountType.query()
|
||||
.where('balance_sheet', true);
|
||||
const balanceSheetTypes = await AccountType.query().where('balance_sheet', true);
|
||||
|
||||
// Fetch all balance sheet accounts.
|
||||
const accounts = await Account.query()
|
||||
@@ -249,51 +262,92 @@ export default {
|
||||
|
||||
// Account balance formmatter based on the given query.
|
||||
const balanceFormatter = formatNumberClosure(filter.number_format);
|
||||
const filterDateType = filter.display_columns_by === 'total'
|
||||
? 'day' : filter.display_columns_by;
|
||||
|
||||
// Gets the date range set from start to end date.
|
||||
const dateRangeSet = dateRangeCollection(
|
||||
filter.from_date,
|
||||
filter.to_date,
|
||||
filter.display_columns_by,
|
||||
filterDateType,
|
||||
);
|
||||
|
||||
// Retrieve the asset balance sheet.
|
||||
const assets = [
|
||||
...accounts
|
||||
.filter((account) => (
|
||||
account.type.normal === 'debit'
|
||||
const assets = accounts
|
||||
.filter((account) => (
|
||||
account.type.normal === 'debit'
|
||||
&& (account.transactions.length > 0 || !filter.none_zero)
|
||||
))
|
||||
.map((account) => ({
|
||||
))
|
||||
.map((account) => {
|
||||
// Calculates the closing balance to the given date.
|
||||
const closingBalance = journalEntries.getClosingBalance(account.id, filter.to_date);
|
||||
const type = filter.display_columns_by;
|
||||
|
||||
return {
|
||||
...pick(account, ['id', 'index', 'name', 'code']),
|
||||
transactions: dateRangeSet.map((date) => {
|
||||
const type = filter.display_columns_by;
|
||||
const balance = journalEntries.getClosingBalance(account.id, date, type);
|
||||
return { date, balance: balanceFormatter(balance) };
|
||||
}),
|
||||
})),
|
||||
];
|
||||
...(type !== 'total') ? {
|
||||
periods_balance: dateRangeSet.map((date) => {
|
||||
const balance = journalEntries.getClosingBalance(account.id, date, filterDateType);
|
||||
|
||||
return {
|
||||
date,
|
||||
formatted_amount: balanceFormatter(balance),
|
||||
amount: balance,
|
||||
};
|
||||
}),
|
||||
} : {},
|
||||
balance: {
|
||||
formatted_amount: balanceFormatter(closingBalance),
|
||||
amount: closingBalance,
|
||||
date: filter.to_date,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Retrieve liabilities and equity balance sheet.
|
||||
const liabilitiesEquity = [
|
||||
...accounts
|
||||
.filter((account) => (
|
||||
account.type.normal === 'credit'
|
||||
const liabilitiesEquity = accounts
|
||||
.filter((account) => (
|
||||
account.type.normal === 'credit'
|
||||
&& (account.transactions.length > 0 || !filter.none_zero)
|
||||
))
|
||||
.map((account) => ({
|
||||
))
|
||||
.map((account) => {
|
||||
// Calculates the closing balance to the given date.
|
||||
const closingBalance = journalEntries.getClosingBalance(account.id, filter.to_date);
|
||||
const type = filter.display_columns_by;
|
||||
|
||||
return {
|
||||
...pick(account, ['id', 'index', 'name', 'code']),
|
||||
transactions: dateRangeSet.map((date) => {
|
||||
const type = filter.display_columns_by;
|
||||
const balance = journalEntries.getClosingBalance(account.id, date, type);
|
||||
return { date, balance: balanceFormatter(balance) };
|
||||
}),
|
||||
})),
|
||||
];
|
||||
...(type !== 'total') ? {
|
||||
periods_balance: dateRangeSet.map((date) => {
|
||||
const balance = journalEntries.getClosingBalance(account.id, date, filterDateType);
|
||||
|
||||
return {
|
||||
date,
|
||||
formatted_amount: balanceFormatter(balance),
|
||||
amount: balance,
|
||||
};
|
||||
}),
|
||||
} : {},
|
||||
balance: {
|
||||
formattedAmount: balanceFormatter(closingBalance),
|
||||
amount: closingBalance,
|
||||
date: filter.to_date,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
query: { ...filter },
|
||||
columns: { ...dateRangeSet },
|
||||
balance_sheet: {
|
||||
assets,
|
||||
liabilities_equity: liabilitiesEquity,
|
||||
assets: {
|
||||
title: 'Assets',
|
||||
accounts: [...assets],
|
||||
},
|
||||
liabilities_equity: {
|
||||
title: 'Liabilities & Equity',
|
||||
accounts: [...liabilitiesEquity],
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -363,7 +417,7 @@ export default {
|
||||
};
|
||||
});
|
||||
return res.status(200).send({
|
||||
meta: { ...filter },
|
||||
query: { ...filter },
|
||||
items: [...items],
|
||||
});
|
||||
},
|
||||
@@ -381,8 +435,12 @@ export default {
|
||||
query('number_format.divide_1000').optional().isBoolean(),
|
||||
query('basis').optional(),
|
||||
query('none_zero').optional(),
|
||||
query('display_columns_by').optional().isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
query('accounts').optional().isArray(),
|
||||
query('display_columns_type').optional().isIn([
|
||||
'total', 'date_periods',
|
||||
]),
|
||||
query('display_columns_by').optional().isIn([
|
||||
'year', 'month', 'week', 'day', 'quarter',
|
||||
]),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -401,19 +459,22 @@ export default {
|
||||
},
|
||||
basis: 'accural',
|
||||
none_zero: false,
|
||||
display_columns_by: 'month',
|
||||
display_columns_type: 'total',
|
||||
display_columns_by: 'total',
|
||||
...req.query,
|
||||
};
|
||||
const incomeStatementTypes = await AccountType.query().where('income_sheet', true);
|
||||
|
||||
// Fetch all income accounts from storage.
|
||||
const accounts = await Account.query()
|
||||
.whereIn('account_type_id', incomeStatementTypes.map((t) => t.id))
|
||||
.withGraphFetched('type')
|
||||
.withGraphFetched('transactions');
|
||||
|
||||
const filteredAccounts = accounts.filter((account) => {
|
||||
return account.transactions.length > 0 || !filter.none_zero;
|
||||
});
|
||||
// Filter all none zero accounts if it was enabled.
|
||||
const filteredAccounts = accounts.filter((account) => (
|
||||
account.transactions.length > 0 || !filter.none_zero
|
||||
));
|
||||
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
||||
const journalEntries = new JournalPoster();
|
||||
journalEntries.loadEntries(journalEntriesCollected);
|
||||
@@ -427,75 +488,130 @@ export default {
|
||||
filter.to_date,
|
||||
filter.display_columns_by,
|
||||
);
|
||||
const accountsIncome = filteredAccounts
|
||||
.filter((account) => account.type.normal === 'credit')
|
||||
.map((account) => ({
|
||||
|
||||
const accountsMapper = (incomeExpenseAccounts) => (
|
||||
incomeExpenseAccounts.map((account) => ({
|
||||
...pick(account, ['id', 'index', 'name', 'code']),
|
||||
dates: dateRangeSet.map((date) => {
|
||||
const type = filter.display_columns_by;
|
||||
const amount = journalEntries.getClosingBalance(account.id, date, type);
|
||||
|
||||
return { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
}),
|
||||
}));
|
||||
// Total closing balance of the account.
|
||||
...(filter.display_columns_type === 'total') && {
|
||||
total: (() => {
|
||||
const amount = journalEntries.getClosingBalance(account.id, filter.to_date);
|
||||
return { amount, date: filter.to_date, formatted_amount: numberFormatter(amount) };
|
||||
})(),
|
||||
},
|
||||
// Date periods when display columns type `periods`.
|
||||
...(filter.display_columns_type === 'date_periods') && {
|
||||
periods: dateRangeSet.map((date) => {
|
||||
const type = filter.display_columns_by;
|
||||
const amount = journalEntries.getClosingBalance(account.id, date, type);
|
||||
|
||||
const accountsExpenses = filteredAccounts
|
||||
.filter((account) => account.type.normal === 'debit')
|
||||
.map((account) => ({
|
||||
...pick(account, ['id', 'index', 'name', 'code']),
|
||||
dates: dateRangeSet.map((date) => {
|
||||
const type = filter.display_columns_by;
|
||||
const amount = journalEntries.getClosingBalance(account.id, date, type);
|
||||
return { date, amount, formatted_amount: numberFormatter(amount) };
|
||||
}),
|
||||
},
|
||||
})));
|
||||
|
||||
return { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
}),
|
||||
}));
|
||||
const totalAccountsReducer = (incomeExpenseAccounts) => (
|
||||
incomeExpenseAccounts.reduce((acc, account) => {
|
||||
const amount = (account) ? account.total.amount : 0;
|
||||
return amount + acc;
|
||||
}, 0));
|
||||
|
||||
// Calculates the total income of income accounts.
|
||||
const totalAccountsIncome = dateRangeSet.reduce((acc, date, index) => {
|
||||
let amount = 0;
|
||||
accountsIncome.forEach((account) => {
|
||||
const currentDate = account.dates[index];
|
||||
amount += currentDate.rawAmount || 0;
|
||||
});
|
||||
acc[date] = { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
return acc;
|
||||
}, {});
|
||||
const accountsIncome = accountsMapper(filteredAccounts
|
||||
.filter((account) => account.type.normal === 'credit'));
|
||||
|
||||
// Calculates the total expenses of expenses accounts.
|
||||
const totalAccountsExpenses = dateRangeSet.reduce((acc, date, index) => {
|
||||
let amount = 0;
|
||||
accountsExpenses.forEach((account) => {
|
||||
const currentDate = account.dates[index];
|
||||
amount += currentDate.rawAmount || 0;
|
||||
});
|
||||
acc[date] = { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
return acc;
|
||||
}, {});
|
||||
const accountsExpenses = accountsMapper(filteredAccounts
|
||||
.filter((account) => account.type.normal === 'debit'));
|
||||
|
||||
// @return {Array}
|
||||
const totalPeriodsMapper = (incomeExpenseAccounts) => (
|
||||
Object.values(dateRangeSet.reduce((acc, date, index) => {
|
||||
let amount = 0;
|
||||
|
||||
incomeExpenseAccounts.forEach((account) => {
|
||||
const currentDate = account.periods[index];
|
||||
amount += currentDate.amount || 0;
|
||||
});
|
||||
acc[date] = { date, amount, formatted_amount: numberFormatter(amount) };
|
||||
return acc;
|
||||
}, {})));
|
||||
|
||||
// Total income(date) - Total expenses(date) = Net income(date)
|
||||
const netIncome = dateRangeSet.map((date) => {
|
||||
const totalIncome = totalAccountsIncome[date];
|
||||
const totalExpenses = totalAccountsExpenses[date];
|
||||
// @return {Array}
|
||||
const netIncomePeriodsMapper = (totalIncomeAcocunts, totalExpenseAccounts) => (
|
||||
dateRangeSet.map((date, index) => {
|
||||
const totalIncome = totalIncomeAcocunts[index];
|
||||
const totalExpenses = totalExpenseAccounts[index];
|
||||
|
||||
let amount = totalIncome.rawAmount || 0;
|
||||
amount -= totalExpenses.rawAmount || 0;
|
||||
return { date, rawAmount: amount, amount: numberFormatter(amount) };
|
||||
});
|
||||
let amount = totalIncome.amount || 0;
|
||||
amount -= totalExpenses.amount || 0;
|
||||
return { date, amount, formatted_amount: numberFormatter(amount) };
|
||||
}));
|
||||
|
||||
// @return {Object}
|
||||
const netIncomeTotal = (totalIncome, totalExpenses) => {
|
||||
const netIncomeAmount = totalIncome.amount - totalExpenses.amount;
|
||||
return { amount: netIncomeAmount, formatted_amount: netIncomeAmount };
|
||||
};
|
||||
|
||||
const totalIncomeAccounts = totalAccountsReducer(accountsIncome);
|
||||
const totalExpensesAccounts = totalAccountsReducer(accountsExpenses);
|
||||
|
||||
const incomeResponse = {
|
||||
entry_normal: 'credit',
|
||||
accounts: accountsIncome,
|
||||
|
||||
...(filter.display_columns_type === 'total') && {
|
||||
total: {
|
||||
amount: totalIncomeAccounts,
|
||||
date: filter.to_date,
|
||||
formatted_amount: numberFormatter(totalIncomeAccounts),
|
||||
},
|
||||
},
|
||||
...(filter.display_columns_type === 'date_periods') && {
|
||||
total_periods: [
|
||||
...totalPeriodsMapper(accountsIncome),
|
||||
],
|
||||
},
|
||||
};
|
||||
const expenseResponse = {
|
||||
entry_normal: 'debit',
|
||||
accounts: accountsExpenses,
|
||||
|
||||
...(filter.display_columns_type === 'total') && {
|
||||
total: {
|
||||
amount: totalExpensesAccounts,
|
||||
date: filter.to_date,
|
||||
formatted_amount: numberFormatter(totalExpensesAccounts),
|
||||
},
|
||||
},
|
||||
...(filter.display_columns_type === 'date_periods') && {
|
||||
total_periods: [
|
||||
...totalPeriodsMapper(accountsExpenses),
|
||||
],
|
||||
},
|
||||
};
|
||||
const netIncomeResponse = {
|
||||
...(filter.display_columns_type === 'total') && {
|
||||
total: {
|
||||
...netIncomeTotal(incomeResponse.total, expenseResponse.total),
|
||||
},
|
||||
},
|
||||
...(filter.display_columns_type === 'date_periods') && {
|
||||
total_periods: [
|
||||
...netIncomePeriodsMapper(
|
||||
incomeResponse.total_periods,
|
||||
expenseResponse.total_periods,
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
return res.status(200).send({
|
||||
meta: { ...filter },
|
||||
income: {
|
||||
entry_normal: 'credit',
|
||||
accounts: accountsIncome,
|
||||
},
|
||||
expenses: {
|
||||
entry_normal: 'debit',
|
||||
accounts: accountsExpenses,
|
||||
},
|
||||
total_income: Object.values(totalAccountsIncome),
|
||||
total_expenses: Object.values(totalAccountsExpenses),
|
||||
total_net_income: netIncome,
|
||||
query: { ...filter },
|
||||
columns: [...dateRangeSet],
|
||||
income: incomeResponse,
|
||||
expenses: expenseResponse,
|
||||
net_income: netIncomeResponse,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,6 +17,11 @@ export default class Account extends BaseModel {
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
filterAccounts(query, accountIds) {
|
||||
if (accountIds.length > 0) {
|
||||
query.whereIn('id', accountIds);
|
||||
}
|
||||
},
|
||||
filterAccountTypes(query, typesIds) {
|
||||
if (typesIds.length > 0) {
|
||||
query.whereIn('accoun_type_id', typesIds);
|
||||
|
||||
@@ -3,6 +3,7 @@ import moment from 'moment';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
import AccountBalance from '@/models/AccountBalance';
|
||||
import {promiseSerial} from '@/utils';
|
||||
|
||||
export default class JournalPoster {
|
||||
/**
|
||||
@@ -125,12 +126,12 @@ export default class JournalPoster {
|
||||
this.entries.forEach((entry) => {
|
||||
const oper = AccountTransaction.query().insert({
|
||||
accountId: entry.account,
|
||||
...pick(entry, ['credit', 'debit', 'transactionType',
|
||||
...pick(entry, ['credit', 'debit', 'transactionType', 'date', 'userId',
|
||||
'referenceType', 'referenceId', 'note']),
|
||||
});
|
||||
saveOperations.push(oper);
|
||||
saveOperations.push(() => oper);
|
||||
});
|
||||
await Promise.all(saveOperations);
|
||||
await promiseSerial(saveOperations);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,6 +66,12 @@ const mapValuesDeep = (v, callback) => (
|
||||
? _.mapValues(v, v => mapValuesDeep(v, callback))
|
||||
: callback(v));
|
||||
|
||||
|
||||
const promiseSerial = (funcs) => {
|
||||
return funcs.reduce((promise, func) => promise.then((result) => func().then(Array.prototype.concat.bind(result))),
|
||||
Promise.resolve([]));
|
||||
}
|
||||
|
||||
export {
|
||||
hashPassword,
|
||||
origin,
|
||||
@@ -73,4 +79,5 @@ export {
|
||||
dateRangeFormat,
|
||||
mapValuesDeep,
|
||||
mapKeysDeep,
|
||||
promiseSerial,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { request, expect, create, login } from '~/testInit';
|
||||
import {
|
||||
request,
|
||||
expect,
|
||||
create,
|
||||
login,
|
||||
} from '~/testInit';
|
||||
import moment from 'moment';
|
||||
import ManualJournal from '@/models/ManualJournal';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
|
||||
let loginRes;
|
||||
|
||||
describe('routes: /accounting', () => {
|
||||
describe('routes: `/accounting`', () => {
|
||||
beforeEach(async () => {
|
||||
loginRes = await login();
|
||||
});
|
||||
@@ -127,8 +135,113 @@ describe('routes: /accounting', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('Should store all journal entries to the storage.', async () => {
|
||||
it('Should discard journal entries that has null credit and debit amount.', async () => {
|
||||
const account1 = await create('account');
|
||||
const account2 = await create('account');
|
||||
|
||||
const res = await request()
|
||||
.post('/api/accounting/make-journal-entries')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send({
|
||||
date: new Date().toISOString(),
|
||||
reference: '1000',
|
||||
entries: [
|
||||
{
|
||||
credit: null,
|
||||
debit: 0,
|
||||
account_id: account1.id,
|
||||
},
|
||||
{
|
||||
credit: null,
|
||||
debit: 0,
|
||||
account_id: account2.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.that.deep.equal({
|
||||
type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
|
||||
code: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should store manual journal transaction to the storage.', async () => {
|
||||
const account1 = await create('account');
|
||||
const account2 = await create('account');
|
||||
|
||||
const res = await request()
|
||||
.post('/api/accounting/make-journal-entries')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send({
|
||||
date: new Date('2020-2-2').toISOString(),
|
||||
reference: '1000',
|
||||
memo: 'Description here.',
|
||||
entries: [
|
||||
{
|
||||
credit: 1000,
|
||||
account_id: account1.id,
|
||||
},
|
||||
{
|
||||
debit: 1000,
|
||||
account_id: account2.id,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const foundManualJournal = await ManualJournal.query();
|
||||
|
||||
expect(foundManualJournal.length).equals(1);
|
||||
expect(foundManualJournal[0].reference).equals('1000');
|
||||
expect(foundManualJournal[0].transactionType).equals('Journal');
|
||||
expect(foundManualJournal[0].amount).equals(1000);
|
||||
expect(moment(foundManualJournal[0].date).format('YYYY-MM-DD')).equals('2020-02-02');
|
||||
expect(foundManualJournal[0].note).equals('Description here.');
|
||||
expect(foundManualJournal[0].userId).equals(1);
|
||||
});
|
||||
|
||||
it('Should store journal transactions to the storage.', async () => {
|
||||
const account1 = await create('account');
|
||||
const account2 = await create('account');
|
||||
|
||||
const res = await request()
|
||||
.post('/api/accounting/make-journal-entries')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send({
|
||||
date: new Date('2020-1-1').toISOString(),
|
||||
reference: '1000',
|
||||
memo: 'Description here.',
|
||||
entries: [
|
||||
{
|
||||
credit: 1000,
|
||||
account_id: account1.id,
|
||||
note: 'First note',
|
||||
},
|
||||
{
|
||||
debit: 1000,
|
||||
account_id: account2.id,
|
||||
note: 'Second note',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const foundAccountsTransactions = await AccountTransaction.query();
|
||||
|
||||
expect(foundAccountsTransactions.length).equals(2);
|
||||
|
||||
expect(foundAccountsTransactions[0].credit).equals(1000);
|
||||
expect(foundAccountsTransactions[0].debit).equals(null);
|
||||
expect(foundAccountsTransactions[0].accountId).equals(account1.id);
|
||||
expect(foundAccountsTransactions[0].note).equals('First note');
|
||||
expect(foundAccountsTransactions[0].transactionType).equals('Journal');
|
||||
expect(foundAccountsTransactions[0].userId).equals(1);
|
||||
|
||||
expect(foundAccountsTransactions[1].credit).equals(null);
|
||||
expect(foundAccountsTransactions[1].debit).equals(1000);
|
||||
expect(foundAccountsTransactions[1].accountId).equals(account2.id);
|
||||
expect(foundAccountsTransactions[1].note).equals('Second note');
|
||||
expect(foundAccountsTransactions[1].transactionType).equals('Journal');
|
||||
expect(foundAccountsTransactions[1].userId).equals(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
login,
|
||||
create,
|
||||
} from '~/testInit';
|
||||
import moment from 'moment';
|
||||
|
||||
let loginRes;
|
||||
let creditAccount;
|
||||
@@ -145,56 +146,95 @@ describe('routes: `/financial_statements`', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('routes: `/financial_statements/general_ledger`', () => {
|
||||
describe.only('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')
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.status).equals(401);
|
||||
});
|
||||
|
||||
it('Should retrieve the genereal ledger transactions.', async () => {
|
||||
it('Should retrieve request query meta on response schema.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send();
|
||||
|
||||
expect(res.body.items).to.be.a('array');
|
||||
expect(res.body.items.length).equals(4);
|
||||
expect(res.body.query.from_date).equals(moment().startOf('year').format('YYYY-MM-DD'));
|
||||
expect(res.body.query.to_date).equals(moment().endOf('year').format('YYYY-MM-DD'));
|
||||
expect(res.body.query.basis).equals('cash');
|
||||
expect(res.body.query.number_format.no_cents).equals(false);
|
||||
expect(res.body.query.number_format.divide_1000).equals(false);
|
||||
expect(res.body.query.none_zero).equals(false);
|
||||
expect(res.body.query.accounts_ids).to.be.an('array');
|
||||
});
|
||||
|
||||
it('Should retrieve opeing and closing balance in each account.', async () => {
|
||||
it('Should retrieve the general ledger accounts with associated transactions and opening/closing balance.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send();
|
||||
|
||||
const foundCreditAccount = res.body.items.find((a) => a.id === creditAccount.id);
|
||||
|
||||
expect(foundCreditAccount.closing.balance).equals(2000);
|
||||
expect(foundCreditAccount.opening.balance).equals(0);
|
||||
expect(res.body.accounts).is.an('array');
|
||||
expect(res.body.accounts[0].id).to.be.an('number');
|
||||
expect(res.body.accounts[0].name).to.be.a('string');
|
||||
expect(res.body.accounts[0].code).to.be.a('string');
|
||||
expect(res.body.accounts[0].transactions).to.be.a('array');
|
||||
expect(res.body.accounts[0].opening).to.be.a('object');
|
||||
expect(res.body.accounts[0].opening.balance).to.be.a('number');
|
||||
expect(res.body.accounts[0].opening.date).to.be.a('string');
|
||||
expect(res.body.accounts[0].closing).to.be.a('object');
|
||||
expect(res.body.accounts[0].closing.balance).to.be.a('number');
|
||||
expect(res.body.accounts[0].closing.date).to.be.a('string');
|
||||
});
|
||||
|
||||
it('Should retrieve the general ledger transactions between date range.', async () => {
|
||||
it('Should retrieve opening and closing balance.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/general_ledger')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
from_date: '2020-04-04',
|
||||
to_date: '2020-05-05',
|
||||
})
|
||||
.send();
|
||||
|
||||
const foundCreditAccount = res.body.items.find((a) => a.id === creditAccount.id);
|
||||
expect(foundCreditAccount.transactions.length).equals(0);
|
||||
|
||||
const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id);
|
||||
|
||||
expect(targetAccount).to.be.an('object');
|
||||
expect(targetAccount.opening).to.deep.equal({
|
||||
balance: 0, date: '2020-01-01',
|
||||
});
|
||||
expect(targetAccount.closing).to.deep.equal({
|
||||
balance: 2000, date: '2020-12-31',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should retrieve the general ledger transactions with no cents numbers.', () => {
|
||||
it('Should retrieve opening and closing balance between the given date range.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should retrieve the transacvtions divided on 1000.', () => {
|
||||
it('Should retrieve transactions of accounts that has transactions between date range.', () => {
|
||||
|
||||
});
|
||||
|
||||
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`.', () => {
|
||||
|
||||
});
|
||||
|
||||
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`.', () => {
|
||||
|
||||
});
|
||||
|
||||
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.', () => {
|
||||
|
||||
});
|
||||
});
|
||||
@@ -208,6 +248,27 @@ describe('routes: `/financial_statements`', () => {
|
||||
expect(res.status).equals(401);
|
||||
});
|
||||
|
||||
it('Should retrieve query of the balance sheet with default values.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/balance_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
display_columns_by: 'year',
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2020-02-01',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.query.display_columns_by).equals('year');
|
||||
expect(res.body.query.from_date).equals('2020-01-01');
|
||||
expect(res.body.query.to_date).equals('2020-02-01');
|
||||
|
||||
expect(res.body.query.number_format.no_cents).equals(false);
|
||||
expect(res.body.query.number_format.divide_1000).equals(false);
|
||||
|
||||
expect(res.body.query.none_zero).equals(false);
|
||||
});
|
||||
|
||||
it('Should retrieve the asset accounts balance.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/balance_sheet')
|
||||
@@ -217,11 +278,30 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets).to.be.a('array');
|
||||
expect(res.body.balance_sheet.liabilities_equity).to.be.a('array');
|
||||
expect(res.body.balance_sheet.assets.accounts).to.be.a('array');
|
||||
expect(res.body.balance_sheet.liabilities_equity.accounts).to.be.a('array');
|
||||
});
|
||||
|
||||
it('Should retrieve asset/liabilities balance sheet between the given date range.', async () => {
|
||||
it('Should retrieve assets/liabilities total balance between the given date range.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/balance_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
display_columns_by: 'total',
|
||||
from_date: '2012-01-01',
|
||||
to_date: '2032-02-02',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].balance).deep.equals({
|
||||
amount: 4000, formattedAmount: 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',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should retrieve asset/liabilities balance sheet with display columns by `year`.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/balance_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
@@ -232,121 +312,142 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
const { balance_sheet: balanceSheet } = res.body;
|
||||
const foundCreditAccount = balanceSheet.assets.find((account) => {
|
||||
return account.id === debitAccount.id;
|
||||
});
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(7);
|
||||
expect(res.body.balance_sheet.liabilities_equity.accounts[0].periods_balance.length).equals(7);
|
||||
|
||||
expect(foundCreditAccount.transactions.length).equals(6);
|
||||
foundCreditAccount.transactions.forEach((transaction) => {
|
||||
expect(transaction.balance).equals(0);
|
||||
});
|
||||
|
||||
const foundDebitAccount = balanceSheet.liabilities_equity.find((account) => {
|
||||
return account.id === creditAccount.id;
|
||||
});
|
||||
|
||||
expect(foundDebitAccount.transactions.length).equals(6);
|
||||
foundDebitAccount.transactions.forEach((transaction) => {
|
||||
expect(transaction.balance).equals(0);
|
||||
});
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
{
|
||||
amount: 0,
|
||||
formatted_amount: 0,
|
||||
date: '2012',
|
||||
},
|
||||
{
|
||||
amount: 0,
|
||||
formatted_amount: 0,
|
||||
date: '2013',
|
||||
},
|
||||
{
|
||||
amount: 0,
|
||||
formatted_amount: 0,
|
||||
date: '2014',
|
||||
},
|
||||
{
|
||||
amount: 0,
|
||||
formatted_amount: 0,
|
||||
date: '2015',
|
||||
},
|
||||
{
|
||||
amount: 0,
|
||||
formatted_amount: 0,
|
||||
date: '2016',
|
||||
},
|
||||
{
|
||||
amount: 0,
|
||||
formatted_amount: 0,
|
||||
date: '2017',
|
||||
},
|
||||
{
|
||||
amount: 0,
|
||||
formatted_amount: 0,
|
||||
date: '2018',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should retrieve balance sheet with display columns day.', async () => {
|
||||
it('Should retrieve balance sheet with display columns by `day`.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/balance_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
display_columns_by: 'day',
|
||||
from_date: '2020-03-01',
|
||||
to_date: '2020-04-01',
|
||||
from_date: '2020-01-08',
|
||||
to_date: '2020-01-12',
|
||||
})
|
||||
.send();
|
||||
|
||||
const { balance_sheet: balanceSheet } = res.body;
|
||||
|
||||
const foundDebitAccount = balanceSheet.assets.find((account) => {
|
||||
return account.id === debitAccount.id;
|
||||
});
|
||||
const foundCreditAccount = balanceSheet.liabilities_equity.find((account) => {
|
||||
return account.id === creditAccount.id;
|
||||
});
|
||||
|
||||
expect(foundDebitAccount.transactions.length).equals(31);
|
||||
expect(foundCreditAccount.transactions.length).equals(31);
|
||||
|
||||
foundDebitAccount.transactions.forEach((transaction) => {
|
||||
expect(transaction.balance).equals(4000);
|
||||
});
|
||||
foundCreditAccount.transactions.forEach((transaction) => {
|
||||
expect(transaction.balance).equals(2000);
|
||||
});
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2020-01-10', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-01-11', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-01-12', formatted_amount: 4000, amount: 4000 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should retrieve the balance sheet with display columns month.', async () => {
|
||||
it('Should retrieve the balance sheet with display columns by `month`.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/balance_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
display_columns_by: 'month',
|
||||
from_date: '2020',
|
||||
to_date: '2021',
|
||||
from_date: '2019-07-01',
|
||||
to_date: '2020-06-30',
|
||||
})
|
||||
.send();
|
||||
|
||||
const { balance_sheet: balanceSheet } = res.body;
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(12);
|
||||
expect(res.body.balance_sheet.liabilities_equity.accounts[0].periods_balance.length).equals(12);
|
||||
|
||||
const foundDebitAccount = balanceSheet.assets.find((account) => {
|
||||
return account.id === debitAccount.id;
|
||||
});
|
||||
const foundCreditAccount = balanceSheet.liabilities_equity.find((account) => {
|
||||
return account.id === creditAccount.id;
|
||||
});
|
||||
|
||||
expect(foundDebitAccount.transactions.length).equals(12);
|
||||
expect(foundCreditAccount.transactions.length).equals(12);
|
||||
|
||||
foundDebitAccount.transactions.forEach((transaction) => {
|
||||
expect(transaction.balance).equals(4000);
|
||||
});
|
||||
foundCreditAccount.transactions.forEach((transaction) => {
|
||||
expect(transaction.balance).equals(2000);
|
||||
});
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
{ date: '2019-07', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-08', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-09', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-10', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-11', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2019-12', formatted_amount: 0, amount: 0 },
|
||||
{ date: '2020-01', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-02', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-03', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-04', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-05', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-06', formatted_amount: 4000, amount: 4000 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should retrieve the balance sheet with display columns quarter.', async () => {
|
||||
it('Should retrieve the balance sheet with display columns `quarter`.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/balance_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
display_columns_by: 'quarter',
|
||||
from_date: '2020',
|
||||
to_date: '2021',
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2020-12-31',
|
||||
})
|
||||
.send();
|
||||
|
||||
const { balance_sheet: balanceSheet } = res.body;
|
||||
|
||||
const foundDebitAccount = balanceSheet.assets.find((account) => {
|
||||
return account.id === debitAccount.id;
|
||||
});
|
||||
const foundCreditAccount = balanceSheet.liabilities_equity.find((account) => {
|
||||
return account.id === creditAccount.id;
|
||||
});
|
||||
|
||||
expect(foundDebitAccount.transactions.length).equals(4);
|
||||
expect(foundCreditAccount.transactions.length).equals(4);
|
||||
|
||||
foundDebitAccount.transactions.forEach((transaction) => {
|
||||
expect(transaction.balance).equals(4000);
|
||||
});
|
||||
foundCreditAccount.transactions.forEach((transaction) => {
|
||||
expect(transaction.balance).equals(2000);
|
||||
});
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(4);
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
{ date: '2020-03', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-06', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-09', formatted_amount: 4000, amount: 4000 },
|
||||
{ date: '2020-12', formatted_amount: 4000, amount: 4000 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should retrieve the balance sheet amounts without cents.', () => {
|
||||
|
||||
it('Should retrieve the balance sheet amounts without cents.', 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/balance_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
display_columns_by: 'quarter',
|
||||
from_date: '2020-01-01',
|
||||
to_date: '2020-12-31',
|
||||
number_format: {
|
||||
no_cents: true,
|
||||
},
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(4);
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
{ date: '2020-03', formatted_amount: 4000, amount: 4000.25 },
|
||||
{ date: '2020-06', formatted_amount: 4000, amount: 4000.25 },
|
||||
{ date: '2020-09', formatted_amount: 4000, amount: 4000.25 },
|
||||
{ date: '2020-12', formatted_amount: 4000, amount: 4000.25 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should retrieve the balance sheet amounts divided on 1000.', async () => {
|
||||
@@ -363,23 +464,13 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
const { balance_sheet: balanceSheet } = res.body;
|
||||
const foundDebitAccount = balanceSheet.assets.find((account) => {
|
||||
return account.id === debitAccount.id;
|
||||
});
|
||||
const foundCreditAccount = balanceSheet.liabilities_equity.find((account) => {
|
||||
return account.id === creditAccount.id;
|
||||
});
|
||||
|
||||
expect(foundDebitAccount.transactions.length).equals(4);
|
||||
expect(foundCreditAccount.transactions.length).equals(4);
|
||||
|
||||
foundDebitAccount.transactions.forEach((transaction) => {
|
||||
expect(transaction.balance).equals(4);
|
||||
});
|
||||
foundCreditAccount.transactions.forEach((transaction) => {
|
||||
expect(transaction.balance).equals(2);
|
||||
});
|
||||
expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([
|
||||
{ date: '2020-03', formatted_amount: 4, amount: 4000 },
|
||||
{ date: '2020-06', formatted_amount: 4, amount: 4000 },
|
||||
{ date: '2020-09', formatted_amount: 4, amount: 4000 },
|
||||
{ date: '2020-12', formatted_amount: 4, amount: 4000 },
|
||||
{ date: '2021-03', formatted_amount: 4, amount: 4000 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should not retrieve accounts has no transactions between the given date range in case query none_zero is true.', async () => {
|
||||
@@ -397,16 +488,8 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
const { balance_sheet: balanceSheet } = res.body;
|
||||
const foundDebitAccount = balanceSheet.assets.find((account) => {
|
||||
return account.id === debitAccount.id;
|
||||
});
|
||||
|
||||
const foundCreditAccount = balanceSheet.liabilities_equity.find((account) => {
|
||||
return account.id === creditAccount.id;
|
||||
});
|
||||
expect(foundDebitAccount).equals(undefined);
|
||||
expect(foundCreditAccount).equals(undefined);
|
||||
expect(res.body.balance_sheet.assets.accounts.length).equals(0);
|
||||
expect(res.body.balance_sheet.liabilities_equity.accounts.length).equals(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -441,14 +524,11 @@ describe('routes: `/financial_statements`', () => {
|
||||
// There is no transactions between these dates.
|
||||
from_date: '2002-01-01',
|
||||
to_date: '2003-01-01',
|
||||
none_zero: true,
|
||||
})
|
||||
.send();
|
||||
|
||||
res.body.items.forEach((item) => {
|
||||
expect(item.credit).equals(0);
|
||||
expect(item.debit).equals(0);
|
||||
expect(item.balance).equals(0);
|
||||
});
|
||||
expect(res.body.items.length).equals(0);
|
||||
});
|
||||
|
||||
it('Should retrieve trial balance of accounts between the given date range.', async () => {
|
||||
@@ -506,6 +586,10 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
});
|
||||
|
||||
it('Should retrieve associated account details in accounts list.', async () => {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('routes: `/api/financial_statements/profit_loss_sheet`', () => {
|
||||
@@ -515,7 +599,111 @@ describe('routes: `/financial_statements`', () => {
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(401);
|
||||
expect(res.body.message).equals('unauthorzied');
|
||||
expect(res.body.message).equals('unauthorized');
|
||||
});
|
||||
|
||||
it('Should retrieve columns when display type `date_periods` and columns by `month` between 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: '2020-12-12',
|
||||
display_columns_type: 'date_periods',
|
||||
display_columns_by: 'month',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.columns.length).equals(12);
|
||||
expect(res.body.columns).deep.equals([
|
||||
'2020-01', '2020-02',
|
||||
'2020-03', '2020-04',
|
||||
'2020-05', '2020-06',
|
||||
'2020-07', '2020-08',
|
||||
'2020-09', '2020-10',
|
||||
'2020-11', '2020-12',
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should retrieve columns when display type `date_periods` and columns by `quarter`.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/financial_statements/profit_loss_sheet')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.query({
|
||||
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',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.columns.length).equals(12);
|
||||
expect(res.body.columns).deep.equals([
|
||||
'2020-03', '2020-06', '2020-09', '2020-12',
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should retrieve columns when display type `date_periods` and columns by `day` between date range.', async () => {
|
||||
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_type: 'date_periods',
|
||||
display_columns_by: 'day',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.columns.length).equals(31);
|
||||
expect(res.body.columns).deep.equals([
|
||||
'2020-01-01', '2020-01-02', '2020-01-03',
|
||||
'2020-01-04', '2020-01-05', '2020-01-06',
|
||||
'2020-01-07', '2020-01-08', '2020-01-09',
|
||||
'2020-01-10', '2020-01-11', '2020-01-12',
|
||||
'2020-01-13', '2020-01-14', '2020-01-15',
|
||||
'2020-01-16', '2020-01-17', '2020-01-18',
|
||||
'2020-01-19', '2020-01-20', '2020-01-21',
|
||||
'2020-01-22', '2020-01-23', '2020-01-24',
|
||||
'2020-01-25', '2020-01-26', '2020-01-27',
|
||||
'2020-01-28', '2020-01-29', '2020-01-30',
|
||||
'2020-01-31',
|
||||
]);
|
||||
});
|
||||
|
||||
it('Should retrieve all income accounts even it has no transactions.', async () => {
|
||||
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_type: 'total',
|
||||
display_columns_by: 'day',
|
||||
})
|
||||
.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);
|
||||
});
|
||||
|
||||
it('Should retrieve total of each income account when display columns by `total`.', async () => {
|
||||
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',
|
||||
})
|
||||
.send();
|
||||
|
||||
expect(res.body.income).deep.equals();
|
||||
});
|
||||
|
||||
it('Should retrieve credit sumation of income accounts.', async () => {
|
||||
@@ -531,6 +719,8 @@ describe('routes: `/financial_statements`', () => {
|
||||
})
|
||||
.send();
|
||||
|
||||
console.log(res.body);
|
||||
|
||||
res.body.income.accounts[0].dates.forEach((item) => {
|
||||
expect(item.rawAmount).equals(2000);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import ViewColumn from '../../src/models/ViewColumn';
|
||||
|
||||
let loginRes;
|
||||
|
||||
describe.only('routes: `/views`', () => {
|
||||
describe('routes: `/views`', () => {
|
||||
beforeEach(async () => {
|
||||
loginRes = await login();
|
||||
});
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import chai from 'chai';
|
||||
import chaiHttp from 'chai-http';
|
||||
import chaiThings from 'chai-things';
|
||||
|
||||
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;
|
||||
|
||||
before(async () => {
|
||||
await dbManager.dropDb();
|
||||
await dbManager.createDb('ratteb');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await knex.migrate.rollback();
|
||||
await knex.migrate.latest();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await knex.migrate.rollback();
|
||||
});
|
||||
|
||||
chai.use(chaiHttp);
|
||||
|
||||
Reference in New Issue
Block a user