1
0
This commit is contained in:
Ahmed Bouhuolia
2020-03-19 17:02:37 +02:00
parent 73711384f6
commit ab81f4be40
57 changed files with 3734 additions and 515 deletions
+1 -1
View File
@@ -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
+1402
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -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 (
+4
View File
@@ -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);
@@ -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]);
@@ -57,30 +66,55 @@ export default function BalanceSheetHeader({
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,7 +216,7 @@ export default function BalanceSheetHeader({
</RadioGroup>
</Col>
<Col sm={4}>
<Col sm={3}>
<Button intent={Intent.PRIMARY} type="submit" onClick={handleSubmitClick}>
{ 'Calculate Report' }
</Button>
@@ -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",
},
...(balanceSheetQuery &&
balanceSheetQuery.display_columns_by === 'total') ? [
{
Header: 'Total',
accessor: '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'}
@@ -39,3 +122,7 @@ export default function BalanceSheetTable({
</FinancialSheet>
)
}
export default compose(
FinancialStatementConnect,
)(BalanceSheetTable);
@@ -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);
@@ -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>
)
}
@@ -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);
@@ -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>
);
}
@@ -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>
);
}
+15 -8
View File
@@ -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',
}
+35
View File
@@ -1,4 +1,5 @@
import moment from 'moment';
import _ from 'lodash';
export function removeEmptyFromObject(obj) {
obj = Object.assign({}, obj);
@@ -77,3 +78,37 @@ export const objectKeysTransform = (obj, transform) => {
export const compose = (...funcs) =>
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
View File
@@ -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
View File
@@ -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
+231 -71
View File
File diff suppressed because one or more lines are too long
+98 -19
View File
@@ -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",
+2
View File
@@ -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",
+15
View File
@@ -1,12 +1,27 @@
import express from 'express';
import helmet from 'helmet';
import boom from 'express-boom';
import i18n from 'i18n';
import '../config';
import routes from '@/http';
import '@/models';
const app = express();
// i18n.configure({
// // setup some locales - other locales default to en silently
// locales: ['en'],
// // sets a custom cookie name to parse locale settings from.
// cookie: 'yourcookiename',
// // where to store json files - defaults to './locales'
// directory: `${__dirname}/resources/locale`,
// });
// i18n init parses req for language headers, cookies, etc.
// app.use(i18n.init);
// Express configuration
app.set('port', process.env.PORT || 3000);
+17
View File
@@ -0,0 +1,17 @@
import knexManager from 'knex-db-manager';
import knexfile from '@/../knexfile';
const config = knexfile[process.env.NODE_ENV];
const dbManager = knexManager.databaseManagerFactory({
knex: config,
dbManager: {
// db manager related configuration
collate: [],
superUser: 'root',
superPassword: 'root',
// populatePathPattern: 'data/**/*.js', // glob format for searching seeds
},
});
export default dbManager;
@@ -2,8 +2,8 @@
exports.up = function (knex) {
return knex.schema.createTable('user_has_roles', (table) => {
table.increments();
table.integer('user_id').unsigned().references('id').inTable('users');
table.integer('role_id').unsigned().references('id').inTable('roles');
table.integer('user_id').unsigned();
table.integer('role_id').unsigned();
});
};
@@ -7,7 +7,7 @@ exports.up = function(knex) {
table.integer('client_id').unsigned();
table.string('refresh_token');
table.date('refresh_token_expires_on');
table.integer('user_id').unsigned().references('id').inTable('users');
table.integer('user_id').unsigned();
});
};
@@ -2,7 +2,7 @@
exports.up = function (knex) {
return knex.schema.createTable('settings', (table) => {
table.increments();
table.integer('user_id').unsigned().references('id').inTable('users');
table.integer('user_id').unsigned();
table.string('group');
table.string('type');
table.string('key');
@@ -13,7 +13,7 @@ exports.up = function (knex) {
table.boolean('columnable');
table.integer('index');
table.json('options');
table.integer('resource_id').unsigned().references('id').inTable('resources');
table.integer('resource_id').unsigned();
});
};
@@ -2,8 +2,8 @@
exports.up = function (knex) {
return knex.schema.createTable('role_has_accounts', (table) => {
table.increments();
table.integer('role_id').unsigned().references('id').inTable('roles');
table.integer('account_id').unsigned().references('id').inTable('accounts');
table.integer('role_id').unsigned();
table.integer('account_id').unsigned();
});
};
@@ -2,9 +2,9 @@
exports.up = function (knex) {
return knex.schema.createTable('role_has_permissions', (table) => {
table.increments();
table.integer('role_id').unsigned().references('id').inTable('roles');
table.integer('permission_id').unsigned().references('id').inTable('permissions');
table.integer('resource_id').unsigned().references('id').inTable('resources');
table.integer('role_id').unsigned();
table.integer('permission_id').unsigned();
table.integer('resource_id').unsigned();
});
};
@@ -3,7 +3,7 @@ exports.up = function (knex) {
return knex.schema.createTable('view_roles', (table) => {
table.increments();
table.integer('index');
table.integer('field_id').unsigned().references('id').inTable('resource_fields');
table.integer('field_id').unsigned();
table.string('comparator');
table.string('value');
table.integer('view_id').unsigned();
@@ -7,10 +7,10 @@ exports.up = function(knex) {
table.string('transaction_type');
table.string('reference_type');
table.integer('reference_id');
table.integer('account_id').unsigned().references('id').inTable('accounts');
table.integer('account_id').unsigned();
table.string('note');
table.boolean('draft').defaultTo(false);
table.integer('user_id').unsigned().references('id').inTable('users');
table.integer('user_id').unsigned();
table.date('date');
table.timestamps();
});
@@ -6,11 +6,11 @@ exports.up = function(knex) {
table.string('currency_code');
table.decimal('exchange_rate');
table.text('description');
table.integer('expense_account_id').unsigned().references('id').inTable('accounts');
table.integer('payment_account_id').unsigned().references('id').inTable('accounts');
table.integer('expense_account_id').unsigned();
table.integer('payment_account_id').unsigned();
table.string('reference');
table.boolean('published').defaultTo(false);
table.integer('user_id').unsigned().references('id').inTable('users');
table.integer('user_id').unsigned();
table.date('date');
// table.timestamps();
})
@@ -7,7 +7,7 @@ exports.up = function(knex) {
table.decimal('amount');
table.date('date');
table.string('note');
table.integer('user_id').unsigned().references('id').inTable('users');
table.integer('user_id').unsigned();
table.timestamps();
});
};
@@ -2,8 +2,8 @@
exports.up = function(knex) {
return knex.schema.createTable('budget_entries', (table) => {
table.increments();
table.integer('budget_id').unsigned().references('id').inTable('budgets');
table.integer('account_id').unsigned().references('id').inTable('accounts');
table.integer('budget_id').unsigned();
table.integer('account_id').unsigned();
table.decimal('amount', 15, 5);
table.integer('order');
})
@@ -2,11 +2,11 @@
exports.up = function(knex) {
return knex.schema.createTable('resource_custom_fields_metadata', (table) => {
table.increments();
table.integer('resource_id').unsigned().references('id').inTable('resources');
table.integer('resource_id').unsigned();
table.integer('resource_item_id').unsigned();
table.string('key');
table.string('value');
})
});
};
exports.down = function(knex) {
@@ -8,54 +8,63 @@ exports.seed = (knex) => {
{
id: 1,
name: 'Fixed Asset',
normal: 'debit',
balance_sheet: true,
income_sheet: false,
},
{
id: 2,
name: 'Current Asset',
normal: 'debit',
balance_sheet: true,
income_sheet: false,
},
{
id: 3,
name: 'Long Term Liability',
normal: 'credit',
balance_sheet: false,
income_sheet: true,
},
{
id: 4,
name: 'Current Liability',
normal: 'credit',
balance_sheet: false,
income_sheet: true,
},
{
id: 5,
name: 'Equity',
normal: 'credit',
balance_sheet: false,
income_sheet: true,
},
{
id: 6,
name: 'Expense',
normal: 'debit',
balance_sheet: false,
income_sheet: true,
},
{
id: 7,
name: 'Income',
normal: 'credit',
balance_sheet: false,
income_sheet: true,
},
{
id: 8,
name: 'Accounts Receivable',
normal: 'debit',
balance_sheet: true,
income_sheet: false,
},
{
id: 9,
name: 'Accounts Payable',
normal: 'credit',
balance_sheet: true,
income_sheet: false,
},
+29 -9
View File
@@ -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 });
},
},
+61
View File
@@ -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) {
},
},
};
+6
View File
@@ -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,19 +173,26 @@ export default {
// Transaction amount formatter based on the given query.
const formatNumber = formatNumberClosure(filter.number_format);
const items = [
...accounts
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) => ({
...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),
})),
amount: formatNumber(amount),
};
}),
],
opening: {
date: filter.from_date,
@@ -189,11 +202,11 @@ export default {
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
const assets = accounts
.filter((account) => (
account.type.normal === 'debit'
&& (account.transactions.length > 0 || !filter.none_zero)
))
.map((account) => ({
...pick(account, ['id', 'index', 'name', 'code']),
transactions: dateRangeSet.map((date) => {
.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;
const balance = journalEntries.getClosingBalance(account.id, date, type);
return { date, balance: balanceFormatter(balance) };
return {
...pick(account, ['id', 'index', 'name', 'code']),
...(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
const liabilitiesEquity = accounts
.filter((account) => (
account.type.normal === 'credit'
&& (account.transactions.length > 0 || !filter.none_zero)
))
.map((account) => ({
...pick(account, ['id', 'index', 'name', 'code']),
transactions: dateRangeSet.map((date) => {
.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;
const balance = journalEntries.getClosingBalance(account.id, date, type);
return { date, balance: balanceFormatter(balance) };
return {
...pick(account, ['id', 'index', 'name', 'code']),
...(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) => {
// 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);
return { date, rawAmount: amount, amount: numberFormatter(amount) };
return { date, amount, formatted_amount: numberFormatter(amount) };
}),
}));
},
})));
const accountsExpenses = filteredAccounts
.filter((account) => account.type.normal === 'debit')
.map((account) => ({
...pick(account, ['id', 'index', 'name', 'code']),
dates: dateRangeSet.map((date) => {
const type = filter.display_columns_by;
const amount = journalEntries.getClosingBalance(account.id, date, type);
const totalAccountsReducer = (incomeExpenseAccounts) => (
incomeExpenseAccounts.reduce((acc, account) => {
const amount = (account) ? account.total.amount : 0;
return amount + acc;
}, 0));
return { date, rawAmount: amount, amount: numberFormatter(amount) };
}),
}));
const accountsIncome = accountsMapper(filteredAccounts
.filter((account) => account.type.normal === 'credit'));
// Calculates the total income of income accounts.
const totalAccountsIncome = dateRangeSet.reduce((acc, date, index) => {
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;
accountsIncome.forEach((account) => {
const currentDate = account.dates[index];
amount += currentDate.rawAmount || 0;
});
acc[date] = { date, rawAmount: amount, amount: numberFormatter(amount) };
return acc;
}, {});
// Calculates the total expenses of expenses accounts.
const totalAccountsExpenses = dateRangeSet.reduce((acc, date, index) => {
let amount = 0;
accountsExpenses.forEach((account) => {
const currentDate = account.dates[index];
amount += currentDate.rawAmount || 0;
incomeExpenseAccounts.forEach((account) => {
const currentDate = account.periods[index];
amount += currentDate.amount || 0;
});
acc[date] = { date, rawAmount: amount, amount: numberFormatter(amount) };
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 res.status(200).send({
meta: { ...filter },
income: {
// @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),
},
expenses: {
},
...(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),
},
total_income: Object.values(totalAccountsIncome),
total_expenses: Object.values(totalAccountsExpenses),
total_net_income: netIncome,
},
...(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({
query: { ...filter },
columns: [...dateRangeSet],
income: incomeResponse,
expenses: expenseResponse,
net_income: netIncomeResponse,
});
},
},
+5
View File
@@ -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);
}
/**
+7
View File
@@ -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,
};
+116 -3
View File
@@ -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);
});
});
+325 -135
View File
@@ -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(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',
},
]);
});
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);
});
});
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(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 },
]);
});
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);
});
});
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(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 },
]);
});
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);
});
});
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(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 },
]);
});
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);
});
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();
it('Should retrieve the balance sheet amounts without cents.', () => {
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);
});
+1 -1
View File
@@ -11,7 +11,7 @@ import ViewColumn from '../../src/models/ViewColumn';
let loginRes;
describe.only('routes: `/views`', () => {
describe('routes: `/views`', () => {
beforeEach(async () => {
loginRes = await login();
});
+8 -1
View File
@@ -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);