This commit is contained in:
elforjani3
2021-01-21 15:19:46 +02:00
62 changed files with 1598 additions and 794 deletions

View File

@@ -2,6 +2,8 @@ import React from 'react';
import { Switch, Route } from 'react-router';
import { useQuery } from 'react-query';
import 'style/pages/Dashboard/Dashboard.scss';
import DashboardLoadingIndicator from './DashboardLoadingIndicator';
import Sidebar from 'components/Sidebar/Sidebar';
@@ -15,8 +17,6 @@ import withSettingsActions from 'containers/Settings/withSettingsActions';
import { compose } from 'utils';
import 'style/pages/Dashboard/Dashboard.scss';
/**
* Dashboard page.
*/

View File

@@ -191,7 +191,7 @@ export default function DataTable({
// Renders table cell.
const RenderCell = useCallback(
({ row, cell, index }) => (
({ row, cell, column, index }) => (
<ConditionalWrapper
condition={expandToggleColumn === index && expandable}
wrapper={(children) => (
@@ -199,6 +199,7 @@ export default function DataTable({
style={{
'padding-left': `${row.depth * expandColumnSpace}rem`,
}}
className={'expend-padding'}
>
{children}
</div>
@@ -224,7 +225,14 @@ export default function DataTable({
/>
</span>
</If>
{cell.render('Cell')}
<ConditionalWrapper
condition={cell.column.textOverview}
wrapper={(children) => (
<span class="text-overview">{ children }</span>
)}>
{cell.render('Cell')}
</ConditionalWrapper>
</ConditionalWrapper>
),
[expandable, expandToggleColumn, expandColumnSpace],
@@ -276,7 +284,13 @@ export default function DataTable({
return (
<div
{...cell.getCellProps({
className: classnames(cell.column.className || '', 'td'),
className: classnames(
cell.column.className,
'td',
{
'is-text-overview': cell.column.textOverview,
}
),
})}
onContextMenu={handleRowContextMenu(cell, row)}
>

View File

@@ -0,0 +1,5 @@
import React from 'react';
export function CellTextSpan({ cell: { value } }) {
return (<span class="cell-text">{ value }</span>)
}

View File

@@ -3,9 +3,10 @@ import moment from 'moment';
import classnames from 'classnames';
import { FormattedMessage as T, useIntl } from 'react-intl';
import 'style/pages/FinancialStatements/FinancialSheet.scss';
import { If, LoadingIndicator, MODIFIER } from 'components';
import 'style/pages/FinancialStatements/FinancialSheet.scss';
export default function FinancialSheet({
companyName,
@@ -20,7 +21,8 @@ export default function FinancialSheet({
className,
basis,
minimal = false,
fullWidth = false
fullWidth = false,
currentDate = true,
}) {
const { formatMessage } = useIntl();
const format = 'DD MMMM YYYY';
@@ -84,11 +86,19 @@ export default function FinancialSheet({
<div class="financial-sheet__table">{children}</div>
<div class="financial-sheet__accounting-basis">{accountingBasis}</div>
{basisLabel && (
<div class="financial-sheet__basis">
<T id={'accounting_basis'} /> {basisLabel}
</div>
)}
<div class="financial-sheet__footer">
<If condition={basisLabel}>
<span class="financial-sheet__basis">
<T id={'accounting_basis'} /> {basisLabel}
</span>
</If>
<If condition={currentDate}>
<span class="financial-sheet__current-date">
{moment().format('YYYY MMM DD HH:MM')}
</span>
</If>
</div>
</div>
</div>
);

View File

@@ -27,8 +27,6 @@ import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withCurrentView from 'containers/Views/withCurrentView';
import { accountNameAccessor } from './utils';
function AccountsDataTable({
// #withDashboardActions
accountsTable,
@@ -136,7 +134,7 @@ function AccountsDataTable({
{
id: 'name',
Header: formatMessage({ id: 'account_name' }),
accessor: accountNameAccessor,
accessor: 'name',
className: 'account_name',
width: 220,
},
@@ -145,7 +143,7 @@ function AccountsDataTable({
Header: formatMessage({ id: 'code' }),
accessor: 'code',
className: 'code',
width: 125,
width: 70,
},
{
id: 'type',
@@ -160,13 +158,13 @@ function AccountsDataTable({
Cell: NormalCell,
accessor: 'type.normal',
className: 'normal',
width: 115,
width: 65,
},
{
id: 'currency',
Header: formatMessage({ id: 'currency' }),
accessor: (row) => 'USD',
width: 100,
width: 75,
},
{
id: 'balance',

View File

@@ -38,6 +38,7 @@ function ReceivableAgingSummaryTable({
className: 'customer_name',
sticky: 'left',
width: 240,
textOverview: true,
},
{
Header: <T id={'current'} />,

View File

@@ -4,33 +4,12 @@ import classNames from 'classnames';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import { CellTextSpan } from 'components/Datatable/Cells';
import withBalanceSheetDetail from './withBalanceSheetDetail';
import { compose, defaultExpanderReducer, getColumnWidth } from 'utils';
// Total cell.
function TotalCell({ cell }) {
const row = cell.row.original;
if (row.total) {
return row.total.formatted_amount;
}
return '';
}
// Total period cell.
const TotalPeriodCell = (index) => ({ cell }) => {
const { original } = cell.row;
if (original.total_periods && original.total_periods[index]) {
const amount = original.total_periods[index].formatted_amount;
return amount;
}
return '';
};
/**
* Balance sheet table.
*/
@@ -52,14 +31,15 @@ function BalanceSheetTable({
Header: formatMessage({ id: 'account_name' }),
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'account_name',
textOverview: true,
width: 240,
},
...(balanceSheetQuery.display_columns_type === 'total'
? [
{
Header: formatMessage({ id: 'total' }),
accessor: 'balance.formatted_amount',
Cell: TotalCell,
accessor: 'total.formatted_amount',
Cell: CellTextSpan,
className: 'total',
width: 140,
},
@@ -69,8 +49,8 @@ function BalanceSheetTable({
? balanceSheetColumns.map((column, index) => ({
id: `date_period_${index}`,
Header: column,
accessor: `total_periods[${index}]`,
Cell: TotalPeriodCell(index),
Cell: CellTextSpan,
accessor: `total_periods[${index}].formatted_amount`,
className: classNames('total-period', `total-periods-${index}`),
width: getColumnWidth(
balanceSheetTableRows,
@@ -93,7 +73,7 @@ function BalanceSheetTable({
const { original } = row;
const rowTypes = Array.isArray(original.row_types)
? original.row_types
: [];
: [original.row_types];
return {
...rowTypes.reduce((acc, rowType) => {

View File

@@ -14,6 +14,8 @@ import { CLASSES } from 'common/classes';
import { Col, Row, ListSelect, MODIFIER } from 'components';
import { filterAccountsOptions } from './common';
export default function FinancialAccountsFilter({ ...restProps }) {
const SUBMENU_POPOVER_MODIFIERS = {
flip: { boundariesElement: 'viewport', padding: 20 },

View File

@@ -49,6 +49,7 @@ function GeneralLedger({
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'accural',
accountsFilter: 'with-transactions',
});
// Change page title of the dashboard.

View File

@@ -1,16 +1,21 @@
import React from 'react';
import { FormGroup, Classes } from '@blueprintjs/core';
import {
FormGroup,
Classes,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { compose } from 'redux';
import { AccountsMultiSelect, Row, Col } from 'components';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import withAccounts from 'containers/Accounts/withAccounts';
import { compose } from 'redux';
import { filterAccountsOptions } from './common';
/**
* General ledger (GL) - Header - General panel.
@@ -22,7 +27,10 @@ function GeneralLedgerHeaderGeneralPane({
return (
<div>
<FinancialStatementDateRange />
<FinancialAccountsFilter
items={filterAccountsOptions}
initialSelectedItem={'all-accounts'}
/>
<Row>
<Col xs={4}>
<FormGroup

View File

@@ -8,6 +8,7 @@ import DataTable from 'components/DataTable';
import Money from 'components/Money';
import withGeneralLedger from './withGeneralLedger';
import { getForceWidth, getColumnWidth } from 'utils';
const ROW_TYPE = {
CLOSING_BALANCE: 'closing_balance',
@@ -25,91 +26,89 @@ function GeneralLedgerTable({
}) {
const { formatMessage } = useIntl();
// Account name column accessor.
const accountNameAccessor = (row) => {
switch (row.rowType) {
case ROW_TYPE.OPENING_BALANCE:
return 'Opening Balance';
case ROW_TYPE.CLOSING_BALANCE:
return 'Closing Balance';
default:
return row.name;
}
};
// Date accessor.
const dateAccessor = (row) => {
const TYPES = [
ROW_TYPE.OPENING_BALANCE,
ROW_TYPE.CLOSING_BALANCE,
ROW_TYPE.TRANSACTION,
];
return TYPES.indexOf(row.rowType) !== -1
? moment(row.date).format('DD MMM YYYY')
: '';
};
// Amount cell
const amountCell = useCallback(({ cell }) => {
const transaction = cell.row.original;
if (transaction.rowType === ROW_TYPE.ACCOUNT) {
return !cell.row.isExpanded ? (
<Money amount={transaction.closing.amount} currency={'USD'} />
) : (
''
);
}
return <Money amount={transaction.amount} currency={'USD'} />;
}, []);
const columns = useMemo(
() => [
{
Header: formatMessage({ id: 'account_name' }),
accessor: accountNameAccessor,
className: 'name',
width: 225,
Header: formatMessage({ id: 'date' }),
accessor: (row) => {
if (row.rowType === 'ACCOUNT_ROW') {
return (
<span
className={'force-width'}
style={{ minWidth: getForceWidth(row.date) }}
>
{row.date}
</span>
);
}
return row.date;
},
className: 'date',
width: 120,
},
{
Header: formatMessage({ id: 'date' }),
accessor: dateAccessor,
className: 'date',
width: 115,
Header: formatMessage({ id: 'account_name' }),
accessor: 'name',
className: 'name',
textOverview: true,
// width: 200,
},
{
Header: formatMessage({ id: 'transaction_type' }),
accessor: 'referenceType',
accessor: 'reference_type_formatted',
className: 'transaction_type',
width: 145,
width: 125 ,
},
{
Header: formatMessage({ id: 'trans_num' }),
Header: formatMessage({ id: 'transaction_number' }),
accessor: 'reference_id',
className: 'transaction_number',
width: 110,
width: 100,
},
{
Header: formatMessage({ id: 'description' }),
accessor: 'note',
className: 'description',
width: 145,
// width: 145,
},
{
Header: formatMessage({ id: 'credit' }),
accessor: 'formatted_credit',
className: 'credit',
width: getColumnWidth(generalLedgerTableRows, 'formatted_credit', {
minWidth: 100,
magicSpacing: 10,
}),
},
{
Header: formatMessage({ id: 'debit' }),
accessor: 'formatted_debit',
className: 'debit',
width: getColumnWidth(generalLedgerTableRows, 'formatted_debit', {
minWidth: 100,
magicSpacing: 10,
}),
},
{
Header: formatMessage({ id: 'amount' }),
Cell: amountCell,
accessor: 'formatted_amount',
className: 'amount',
width: 150,
width: getColumnWidth(generalLedgerTableRows, 'formatted_amount', {
minWidth: 100,
magicSpacing: 10,
}),
},
{
Header: formatMessage({ id: 'balance' }),
Cell: amountCell,
className: 'balance',
width: 150,
Header: formatMessage({ id: 'running_balance' }),
accessor: 'formatted_running_balance',
className: 'running_balance',
width: getColumnWidth(generalLedgerTableRows, 'formatted_running_balance', {
minWidth: 100,
magicSpacing: 10,
}),
},
],
[],
[formatMessage, generalLedgerTableRows],
);
// Default expanded rows of general ledger table.
@@ -140,7 +139,7 @@ function GeneralLedgerTable({
rowClassNames={rowClassNames}
expanded={expandedRows}
virtualizedRows={true}
fixedItemSize={37}
fixedItemSize={30}
fixedSizeHeight={1000}
expandable={true}
expandToggleColumn={1}

View File

@@ -0,0 +1,16 @@
import { formatMessage } from 'services/intl';
export const filterAccountsOptions = [
{
key: 'all-accounts',
name: formatMessage({ id: 'all_accounts' }),
hint: formatMessage({ id: 'all_accounts_including_with_zero_balance' }),
},
{
key: 'with-transactions',
name: formatMessage({ id: 'accounts_with_transactions' }),
hint: formatMessage({
id: 'include_accounts_once_has_transactions_on_given_date_period',
}),
},
];

View File

@@ -8,7 +8,7 @@ import Money from 'components/Money';
import withJournal from './withJournal';
import { compose, defaultExpanderReducer } from 'utils';
import { compose, defaultExpanderReducer, getForceWidth } from 'utils';
function JournalSheetTable({
// #withJournal
@@ -22,70 +22,52 @@ function JournalSheetTable({
}) {
const { formatMessage } = useIntl();
const rowTypeFilter = (rowType, value, types) => {
return types.indexOf(rowType) === -1 ? '' : value;
};
const exceptRowTypes = (rowType, value, types) => {
return types.indexOf(rowType) !== -1 ? '' : value;
};
const columns = useMemo(
() => [
{
Header: formatMessage({ id: 'date' }),
accessor: (r) =>
rowTypeFilter(r.rowType, moment(r.date).format('YYYY MMM DD'), [
'first_entry',
]),
accessor: row => row.date ? moment(row.date).format('YYYY MMM DD') : '',
className: 'date',
width: 85,
width: 100,
},
{
Header: formatMessage({ id: 'transaction_type' }),
accessor: (r) =>
rowTypeFilter(r.rowType, r.transaction_type, ['first_entry']),
accessor: 'reference_type_formatted',
className: 'reference_type_formatted',
width: 145,
width: 120,
},
{
Header: formatMessage({ id: 'num' }),
accessor: (r) =>
rowTypeFilter(r.rowType, r.reference_id, ['first_entry']),
accessor: 'reference_id',
className: 'reference_id',
width: 70,
},
{
Header: formatMessage({ id: 'description' }),
accessor: 'note',
className: 'note'
},
{
Header: formatMessage({ id: 'acc_code' }),
accessor: 'account.code',
accessor: 'account_code',
width: 95,
className: 'account_code',
},
{
Header: formatMessage({ id: 'account' }),
accessor: 'account.name',
accessor: 'account_name',
className: 'account_name',
textOverview: true,
},
{
Header: formatMessage({ id: 'credit' }),
accessor: (r) =>
exceptRowTypes(
r.rowType,
<Money amount={r.credit} currency={'USD'} />,
['space_entry'],
),
accessor: 'formatted_credit',
className: 'credit'
},
{
Header: formatMessage({ id: 'debit' }),
accessor: (r) =>
exceptRowTypes(
r.rowType,
<Money amount={r.debit} currency={'USD'} />,
['space_entry'],
),
accessor: 'formatted_debit',
className: 'debit'
},
],
[formatMessage],
@@ -101,6 +83,20 @@ function JournalSheetTable({
// Default expanded rows of general journal table.
const expandedRows = useMemo(() => defaultExpanderReducer([], 1), []);
const rowClassNames = useCallback((row) => {
const { original } = row;
const rowTypes = Array.isArray(original.rowType)
? original.rowType
: [original.rowType];
return {
...rowTypes.reduce((acc, rowType) => {
acc[`row_type--${rowType}`] = rowType;
return acc;
}, {}),
};
}, []);
return (
<FinancialSheet
companyName={companyName}
@@ -111,11 +107,12 @@ function JournalSheetTable({
loading={journalSheetLoading}
// minimal={true}
fullWidth={true}
>
>
<DataTable
className="bigcapital-datatable--financial-report"
columns={columns}
data={journalSheetTableRows}
rowClassNames={rowClassNames}
onFetchData={handleFetchData}
noResults={formatMessage({
id: 'this_report_does_not_contain_any_data_between_date_period',

View File

@@ -3,7 +3,7 @@ import { FormattedMessage as T, useIntl } from 'react-intl';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import Money from 'components/Money';
import { CellTextSpan } from 'components/Datatable/Cells';
import { compose, defaultExpanderReducer, getColumnWidth } from 'utils';
import withProfitLossDetail from './withProfitLoss';
@@ -26,12 +26,14 @@ function ProfitLossSheetTable({
Header: formatMessage({ id: 'account' }),
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'name',
textOverview: true,
width: 240,
},
...(profitLossQuery.display_columns_type === 'total'
? [
{
Header: formatMessage({ id: 'total' }),
Cell: CellTextSpan,
accessor: 'total.formatted_amount',
className: 'total',
width: 140,
@@ -42,6 +44,7 @@ function ProfitLossSheetTable({
? profitLossColumns.map((column, index) => ({
id: `date_period_${index}`,
Header: column,
Cell: CellTextSpan,
accessor: `total_periods[${index}].formatted_amount`,
width: getColumnWidth(
profitLossTableRows,

View File

@@ -3,7 +3,7 @@ import { useIntl } from 'react-intl';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import Money from 'components/Money';
import { CellTextSpan } from 'components/Datatable/Cells';
import withTrialBalance from './withTrialBalance';
@@ -28,9 +28,11 @@ function TrialBalanceSheetTable({
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'name',
width: 160,
textOverview: true,
},
{
Header: formatMessage({ id: 'credit' }),
Cell: CellTextSpan,
accessor: 'formatted_credit',
className: 'credit',
width: getColumnWidth(trialBalanceTableRows, `credit`, {
@@ -39,11 +41,13 @@ function TrialBalanceSheetTable({
},
{
Header: formatMessage({ id: 'debit' }),
Cell: CellTextSpan,
accessor: 'formatted_debit',
width: getColumnWidth(trialBalanceTableRows, `debit`, { minWidth: 95 }),
},
{
Header: formatMessage({ id: 'balance' }),
Cell: CellTextSpan,
accessor: 'formatted_balance',
className: 'balance',
width: getColumnWidth(trialBalanceTableRows, `balance`, {
@@ -56,7 +60,7 @@ function TrialBalanceSheetTable({
const rowClassNames = (row) => {
const { original } = row;
const rowTypes = Array.isArray(original.rowTypes) ? original.rowTypes : [];
const rowTypes = Array.isArray(original.rowType) ? original.rowType : [original.rowType];
return {
...rowTypes.reduce((acc, rowType) => {

View File

@@ -1,4 +1,4 @@
import { mapKeys, omit, snakeCase } from 'lodash';
import { omit } from 'lodash';
import { transformToCamelCase, flatObject } from 'utils';
import { formatMessage } from 'services/intl';

View File

@@ -974,4 +974,6 @@ export default {
specific_customers: 'Specific Customers',
all_customers: 'All Customers',
selected_customers: '{count} Selected Customers',
transaction_number: 'Transaction #',
running_balance: 'Running balance'
};

View File

@@ -1,14 +1,8 @@
import { omit } from 'lodash';
import { omit, chain } from 'lodash';
import moment from 'moment';
export const mapBalanceSheetToTableRows = (accounts) => {
return accounts.map((account) => {
const PRIMARY_SECTIONS = ['assets', 'liability', 'equity'];
const rowTypes = [
'total_row',
...(PRIMARY_SECTIONS.indexOf(account.section_type) !== -1
? ['total_assets']
: []),
];
return {
...account,
children: mapBalanceSheetToTableRows([
@@ -31,51 +25,79 @@ export const mapBalanceSheetToTableRows = (accounts) => {
};
export const journalToTableRowsMapper = (journal) => {
return journal.reduce((rows, journal) => {
journal.entries.forEach((entry, index) => {
rows.push({
...entry,
rowType: index === 0 ? 'first_entry' : 'entry',
});
});
rows.push({
credit: journal.credit,
debit: journal.debit,
rowType: 'entries_total',
});
rows.push({
rowType: 'space_entry',
});
return rows;
}, []);
const TYPES = {
ENTRY: 'ENTRY',
TOTAL_ENTRIES: 'TOTAL_ENTRIES',
EMPTY_ROW: 'EMPTY_ROW',
};
const entriesMapper = (transaction) => {
return transaction.entries.map((entry, index) => ({
...(index === 0
? {
date: transaction.date,
reference_type: transaction.reference_type,
reference_id: transaction.reference_id,
reference_type_formatted: transaction.reference_type_formatted,
}
: {}),
rowType: TYPES.ENTRY,
...entry,
}));
};
return chain(journal)
.map((transaction) => {
const entries = entriesMapper(transaction);
return [
...entries,
{
rowType: TYPES.TOTAL_ENTRIES,
currency_code: transaction.currency_code,
credit: transaction.credit,
debit: transaction.debit,
formatted_credit: transaction.formatted_credit,
formatted_debit: transaction.formatted_debit,
},
{
rowType: TYPES.EMPTY_ROW,
},
];
})
.flatten()
.value();
};
export const generalLedgerToTableRows = (accounts) => {
return accounts.reduce((tableRows, account) => {
const children = [];
children.push({
...account.opening,
rowType: 'opening_balance',
});
account.transactions.map((transaction) => {
children.push({
...transaction,
...omit(account, ['transactions']),
rowType: 'transaction',
});
});
children.push({
...account.closing,
rowType: 'closing_balance',
});
tableRows.push({
...omit(account, ['transactions']),
children,
rowType: 'account_name',
});
return tableRows;
}, []);
return chain(accounts)
.map((account) => {
return {
name: '',
code: account.code,
rowType: 'ACCOUNT_ROW',
date: account.name,
children: [
{
...account.opening_balance,
name: 'Opening balance',
rowType: 'OPENING_BALANCE',
},
...account.transactions.map((transaction) => ({
...transaction,
name: account.name,
code: account.code,
date: moment(transaction.date).format('DD MMM YYYY'),
})),
{
...account.closing_balance,
name: 'Closing balance',
rowType: 'CLOSING_BALANCE',
},
],
};
})
.value();
};
export const ARAgingSummaryTableRowsMapper = (sheet, total) => {
@@ -109,25 +131,32 @@ export const ARAgingSummaryTableRowsMapper = (sheet, total) => {
current: sheet.total.current.formatted_amount,
...mapAging(sheet.total.aging),
total: sheet.total.total.formatted_amount,
}
];
};
export const mapTrialBalanceSheetToRows = (sheet) => {
return [
...sheet.accounts,
{
name: 'Total',
rowTypes: ['total'],
...sheet.total,
},
];
};
export const profitLossToTableRowsMapper = (profitLoss) => {
export const mapTrialBalanceSheetToRows = (sheet) => {
const results = [];
return [
{
if (sheet.accounts) {
sheet.accounts.forEach((account) => {
results.push(account);
});
}
if (sheet.total) {
results.push({
rowType: 'total',
...sheet.total,
});
}
return results;
};
export const profitLossToTableRowsMapper = (profitLoss) => {
const results = [];
if (profitLoss.income) {
results.push({
name: 'Income',
total: profitLoss.income.total,
children: [
@@ -140,8 +169,10 @@ export const profitLossToTableRowsMapper = (profitLoss) => {
},
],
total_periods: profitLoss.income.total_periods,
},
{
});
}
if (profitLoss.cost_of_sales) {
results.push({
name: 'Cost of sales',
total: profitLoss.cost_of_sales.total,
children: [
@@ -153,15 +184,19 @@ export const profitLossToTableRowsMapper = (profitLoss) => {
rowTypes: ['cogs_total', 'section_total', 'total'],
},
],
total_periods: profitLoss.cost_of_sales.total_periods
},
{
total_periods: profitLoss.cost_of_sales.total_periods,
});
}
if (profitLoss.gross_profit) {
results.push({
name: 'Gross profit',
total: profitLoss.gross_profit.total,
total_periods: profitLoss.gross_profit.total_periods,
rowTypes: ['gross_total', 'section_total', 'total'],
},
{
})
}
if (profitLoss.expenses) {
results.push({
name: 'Expenses',
total: profitLoss.expenses.total,
children: [
@@ -174,14 +209,34 @@ export const profitLossToTableRowsMapper = (profitLoss) => {
},
],
total_periods: profitLoss.expenses.total_periods,
},
{
})
}
if (profitLoss.operating_profit) {
results.push({
name: 'Net Operating income',
total: profitLoss.operating_profit.total,
total_periods: profitLoss.income.total_periods,
rowTypes: ['net_operating_total', 'section_total', 'total'],
},
{
})
}
if (profitLoss.other_income) {
results.push({
name: 'Other Income',
total: profitLoss.other_income.total,
total_periods: profitLoss.other_income.total_periods,
children: [
...profitLoss.other_income.accounts,
{
name: 'Total other income',
total: profitLoss.other_income.total,
total_periods: profitLoss.other_income.total_periods,
rowTypes: ['expenses_total', 'section_total', 'total'],
},
],
});
}
if (profitLoss.other_expenses) {
results.push({
name: 'Other expenses',
total: profitLoss.other_expenses.total,
total_periods: profitLoss.other_expenses.total_periods,
@@ -194,12 +249,15 @@ export const profitLossToTableRowsMapper = (profitLoss) => {
rowTypes: ['expenses_total', 'section_total', 'total'],
},
],
},
{
});
}
if (profitLoss.net_income) {
results.push({
name: 'Net Income',
total: profitLoss.net_income.total,
total_periods: profitLoss.net_income.total_periods,
rowTypes: ['net_income_total', 'section_total', 'total'],
},
];
};
})
};
return results;
};

View File

@@ -27,6 +27,12 @@
color: #58667b;
font-weight: 500;
border-bottom: 1px solid rgb(224, 224, 224);
> div{
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.sort-icon {
width: 0;
@@ -141,10 +147,23 @@
.placeholder {
color: #a0a0a0;
}
.text-overview{
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.bp3-form-group {
width: 100%;
}
&.is-text-overview {
.expend-padding{
display: flex;
width: 100%;
}
}
}
.tr:hover .td {
background: #f3f7fc;

View File

@@ -60,12 +60,17 @@
display: none;
}
}
&__basis {
&__footer {
color: #888;
text-align: center;
margin-top: auto;
padding-top: 18px;
font-size: 13px;
> span + span{
padding-left: 10px;
}
}
.dashboard__loading-indicator {
margin: auto;

View File

@@ -13,20 +13,22 @@
}
}
.tbody{
.tr .td{
border-bottom: 0;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
.tr:not(:first-child) .td{
border-top: 1px solid transparent;
}
.tr.row-type--total{
font-weight: 500;
.tr:not(.no-results) {
.td{
border-top: 1px solid #333;
border-bottom: 3px double #333;
border-bottom: 0;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
&.row-type--total{
font-weight: 500;
.td{
border-top: 1px solid #333;
border-bottom: 3px double #333;
}
}
&:not(:first-child) .td{
border-top: 1px solid transparent;
}
}
}

View File

@@ -36,7 +36,7 @@
.tr.is-expanded{
.td.total,
.td.total-period{
> span{
> span.cell-text{
display: none;
}
}

View File

@@ -3,9 +3,17 @@
&--financial-report{
.table {
.tbody{
.tr.no-results {
.td{
border-bottom: 1px solid #DDD;
}
}
}
.thead{
.tr .th{
background: transparent;
background-color: #fff;
border-top: 1px solid #666;
border-bottom: 1px solid #666;

View File

@@ -2,25 +2,64 @@
.financial-sheet{
&--general-ledger{
.financial-sheet__table{
.tbody,
.thead{
.tr .td,
.tr .th{
&.credit,
&.debit,
&.running_balance,
&.amount{
justify-content: flex-end;
}
}
}
.tbody{
.tr .td{
padding-top: 0.2rem;
padding-bottom: 0.2rem;
border-top-color: transparent;
border-bottom-color: transparent;
&.date{
> div{
display: flex;
}
span.force-width{
position: relative;
}
}
}
.tr:not(.no-results) .td{
border-left: 1px solid #ececec;
}
.tr.row-type{
&--opening_balance,
&--closing_balance{
&--ACCOUNT_ROW{
.td{
border-top: 1px solid #333;
&.date{
font-weight: 500;
}
&.name{
border-left-color: transparent;
}
}
.name,
.amount,
.balance{
&:not(:first-child).is-expanded .td{
border-top: 1px solid #DDD;
}
}
&--OPENING_BALANCE,
&--CLOSING_BALANCE{
.amount{
font-weight: 500;
}
}
&--closing_balance .td{
border-bottom-color: #666;
}
&--account_name .td.name{
font-weight: 500;
&--CLOSING_BALANCE{
.name{
font-weight: 500;
}
}
}
}

View File

@@ -3,19 +3,39 @@
&--journal{
.financial-sheet__table{
.tr .td.credit,
.tr .th.credit,
.tr .td.debit,
.tr .th.debit{
justify-content: flex-end;
}
.tbody{
.tr:not(.no-results) .td{
padding: 0.4rem;
padding: 0.3rem 0.4rem;
color: #000;
border-bottom-color: transparent;
min-height: 32px;
min-height: 28px;
border-left: 1px solid #ececec;
&:first-of-type{
border-left: 0;
}
&.account_name,
&.reference_type_formatted{
white-space: nowrap;
overflow: hidden;
text-overflow:ellipsis;
}
}
.tr:not(.no-results):last-child{
.td{
border-bottom: 1px solid #dbdbdb;
}
}
.tr.row_type--TOTAL_ENTRIES{
font-weight: 600;
}
}
}
}

View File

@@ -389,6 +389,16 @@ export const getColumnWidth = (
return result;
};
export const getForceWidth = (
text,
magicSpacing = 14,
) => {
const textLength = text.length;
const result = textLength * magicSpacing
return result;
}
export const toSafeNumber = (number) => {
return _.toNumber(_.defaultTo(number, 0));
};

View File

@@ -16,7 +16,8 @@ export default class JournalSheetController extends BaseFinancialReportControlle
router() {
const router = Router();
router.get('/',
router.get(
'/',
this.journalValidationSchema,
this.validationResult,
this.asyncMiddleware(this.journal.bind(this))
@@ -31,18 +32,20 @@ export default class JournalSheetController extends BaseFinancialReportControlle
return [
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
oneOf([
query('transaction_types').optional().isArray({ min: 1 }),
query('transaction_types.*').optional().isNumeric().toInt(),
], [
query('transaction_types').optional().trim().escape(),
]),
oneOf([
query('account_ids').optional().isArray({ min: 1 }),
query('account_ids.*').optional().isNumeric().toInt(),
], [
query('account_ids').optional().isNumeric().toInt(),
]),
oneOf(
[
query('transaction_types').optional().isArray({ min: 1 }),
query('transaction_types.*').optional().isNumeric().toInt(),
],
[query('transaction_types').optional().trim().escape()]
),
oneOf(
[
query('account_ids').optional().isArray({ min: 1 }),
query('account_ids.*').optional().isNumeric().toInt(),
],
[query('account_ids').optional().isNumeric().toInt()]
),
query('from_range').optional().isNumeric().toInt(),
query('to_range').optional().isNumeric().toInt(),
query('number_format.no_cents').optional().isBoolean().toBoolean(),
@@ -52,7 +55,7 @@ export default class JournalSheetController extends BaseFinancialReportControlle
/**
* Retrieve the ledger report of the given account.
* @param {Request} req -
* @param {Request} req -
* @param {Response} res -
*/
async journal(req: Request, res: Response, next: NextFunction) {
@@ -63,11 +66,20 @@ export default class JournalSheetController extends BaseFinancialReportControlle
...filter,
accountsIds: castArray(filter.accountsIds),
};
const organizationName = settings.get({ group: 'organization', key: 'name' });
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
const organizationName = settings.get({
group: 'organization',
key: 'name',
});
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
try {
const { data, query } = await this.journalService.journalSheet(tenantId, filter);
const { data, query } = await this.journalService.journalSheet(
tenantId,
filter
);
return res.status(200).send({
organization_name: organizationName,
@@ -79,4 +91,4 @@ export default class JournalSheetController extends BaseFinancialReportControlle
next(error);
}
}
}
}

View File

@@ -1,6 +1,7 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response } from 'express';
import { check, body, param } from 'express-validator';
import { IInviteUserInput } from 'interfaces';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import InviteUserService from 'services/InviteUsers';
import { ServiceErrors, ServiceError } from 'exceptions';
@@ -22,7 +23,7 @@ export default class InviteUsersController extends BaseController {
[body('email').exists().trim().escape()],
this.validationResult,
asyncMiddleware(this.sendInvite.bind(this)),
this.handleServicesError,
this.handleServicesError
);
return router;
}
@@ -38,14 +39,14 @@ export default class InviteUsersController extends BaseController {
[...this.inviteUserDTO],
this.validationResult,
asyncMiddleware(this.accept.bind(this)),
this.handleServicesError,
this.handleServicesError
);
router.get(
'/invited/:token',
[param('token').exists().trim().escape()],
this.validationResult,
asyncMiddleware(this.invited.bind(this)),
this.handleServicesError,
this.handleServicesError
);
return router;
@@ -76,8 +77,11 @@ export default class InviteUsersController extends BaseController {
const { user } = req;
try {
await this.inviteUsersService.sendInvite(tenantId, email, user);
const { invite } = await this.inviteUsersService.sendInvite(
tenantId,
email,
user
);
return res.status(200).send({
type: 'success',
code: 'INVITE.SENT.SUCCESSFULLY',
@@ -104,6 +108,7 @@ export default class InviteUsersController extends BaseController {
try {
await this.inviteUsersService.acceptInvite(token, inviteUserInput);
return res.status(200).send({
type: 'success',
code: 'USER.INVITE.ACCEPTED',
@@ -144,19 +149,40 @@ export default class InviteUsersController extends BaseController {
*/
handleServicesError(error, req: Request, res: Response, next: Function) {
if (error instanceof ServiceError) {
if (error.errorType === 'EMAIL_EXISTS') {
return res.status(400).send({
errors: [{
type: 'EMAIL.ALREADY.EXISTS',
code: 100,
message: 'Email already exists in the users.'
}],
});
}
if (error.errorType === 'EMAIL_ALREADY_INVITED') {
return res.status(400).send({
errors: [{ type: 'EMAIL.ALREADY.INVITED' }],
errors: [{
type: 'EMAIL.ALREADY.INVITED',
code: 200,
message: 'Email already invited.',
}],
});
}
if (error.errorType === 'INVITE_TOKEN_INVALID') {
return res.status(400).send({
errors: [{ type: 'INVITE.TOKEN.INVALID' }],
errors: [{
type: 'INVITE.TOKEN.INVALID',
code: 300,
message: 'Invite token is invalid, please try another one.',
}],
});
}
if (error.errorType === 'PHONE_NUMBER_EXISTS') {
return res.status(400).send({
errors: [{ type: 'PHONE_NUMBER.EXISTS' }],
errors: [{
type: 'PHONE_NUMBER.EXISTS',
code: 400,
message: 'Phone number is already invited, please try another unique one.'
}],
});
}
}

View File

@@ -2,7 +2,7 @@ import { Router, Request, Response } from 'express';
export default class Ping {
/**
* Router constur
* Router constructor.
*/
router() {
const router = Router();

View File

@@ -126,6 +126,7 @@ export default class UsersController extends BaseController{
try {
await this.usersService.deleteUser(tenantId, id);
return res.status(200).send({
id,
message: 'The user has been deleted successfully.'
@@ -225,10 +226,10 @@ export default class UsersController extends BaseController{
if (error instanceof ServiceErrors) {
const errorReasons = [];
if (error.errorType === 'email_already_exists') {
if (error.errorType === 'EMAIL_ALREADY_EXISTS') {
errorReasons.push({ type: 'EMAIL_ALREADY_EXIST', code: 100 });
}
if (error.errorType === 'phone_number_already_exist') {
if (error.errorType === 'PHONE_NUMBER_ALREADY_EXIST') {
errorReasons.push({ type: 'PHONE_NUMBER_ALREADY_EXIST', code: 200 });
}
if (errorReasons.length > 0) {
@@ -236,30 +237,36 @@ export default class UsersController extends BaseController{
}
}
if (error instanceof ServiceError) {
if (error.errorType === 'user_not_found') {
if (error.errorType === 'USER_NOT_FOUND') {
return res.boom.badRequest(
'User not found.',
{ errors: [{ type: 'USER.NOT.FOUND', code: 100 }] }
);
}
if (error.errorType === 'user_already_active') {
if (error.errorType === 'USER_ALREADY_ACTIVE') {
return res.boom.badRequest(
'User is already active.',
{ errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }] },
);
}
if (error.errorType === 'user_already_inactive') {
if (error.errorType === 'USER_ALREADY_INACTIVE') {
return res.boom.badRequest(
'User is already inactive.',
{ errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }] },
);
}
if (error.errorType === 'user_same_the_authorized_user') {
if (error.errorType === 'USER_SAME_THE_AUTHORIZED_USER') {
return res.boom.badRequest(
'You could not activate/inactivate the same authorized user.',
{ errors: [{ type: 'CANNOT.TOGGLE.ACTIVATE.AUTHORIZED.USER', code: 300 }] },
)
}
if (error.errorType === 'CANNOT_DELETE_LAST_USER') {
return res.boom.badRequest(
'Cannot delete last user in the organization.',
{ errors: [{ type: 'CANNOT_DELETE_LAST_USER', code: 400 }] },
);
}
}
next(error);
}

View File

@@ -14,7 +14,6 @@ const attachCurrentUser = async (req: Request, res: Response, next: Function) =>
try {
Logger.info('[attach_user_middleware] finding system user by id.');
const user = await systemUserRepository.findOneById(req.token.id);
console.log(user);
if (!user) {
Logger.info('[attach_user_middleware] the system user not found.');

View File

@@ -9,11 +9,13 @@ export default (req: Request, res: Response, next: Function) => {
throw new Error('Should load this middleware after `TenancyMiddleware`.');
}
if (!req.tenant.seededAt) {
Logger.info('[ensure_tenant_initialized_middleware] tenant databae not seeded.');
Logger.info(
'[ensure_tenant_initialized_middleware] tenant databae not seeded.'
);
return res.boom.badRequest(
'Tenant database is not seeded with initial data yet.',
{ errors: [{ type: 'TENANT.DATABASE.NOT.SEED' }] },
{ errors: [{ type: 'TENANT.DATABASE.NOT.SEED' }] }
);
}
next();
};
};

View File

@@ -1,7 +1,11 @@
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
export default (subscriptionSlug = 'main') => async (req: Request, res: Response, next: NextFunction) => {
export default (subscriptionSlug = 'main') => async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenant, tenantId } = req;
const Logger = Container.get('logger');
const { subscriptionRepository } = Container.get('repositories');
@@ -10,22 +14,28 @@ export default (subscriptionSlug = 'main') => async (req: Request, res: Response
throw new Error('Should load `TenancyMiddlware` before this middleware.');
}
Logger.info('[subscription_middleware] trying get tenant main subscription.');
const subscription = await subscriptionRepository.getBySlugInTenant(subscriptionSlug, tenantId);
const subscription = await subscriptionRepository.getBySlugInTenant(
subscriptionSlug,
tenantId
);
// Validate in case there is no any already subscription.
if (!subscription) {
Logger.info('[subscription_middleware] tenant has no subscription.', { tenantId });
return res.boom.badRequest(
'Tenant has no subscription.',
{ errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }] }
);
Logger.info('[subscription_middleware] tenant has no subscription.', {
tenantId,
});
return res.boom.badRequest('Tenant has no subscription.', {
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
});
}
// Validate in case the subscription is inactive.
else if (subscription.inactive()) {
Logger.info('[subscription_middleware] tenant main subscription is expired.', { tenantId });
Logger.info(
'[subscription_middleware] tenant main subscription is expired.',
{ tenantId }
);
return res.boom.badRequest(null, {
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
});
}
next();
};
};

View File

@@ -1,5 +1,3 @@
import logger from "src/loaders/logger";
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';

View File

@@ -0,0 +1,73 @@
import moment from 'moment';
import { Model } from 'objection';
const options = {
columnName: 'deleted_at',
deletedValue: moment().format('YYYY-MM-DD HH:mm:ss'),
notDeletedValue: null,
};
export default class SoftDeleteQueryBuilder extends Model.QueryBuilder {
constructor(...args) {
super(...args);
this.onBuild((builder) => {
if (builder.isFind() || builder.isDelete() || builder.isUpdate()) {
builder.whereNotDeleted();
}
});
}
/**
* override the normal delete function with one that patches the row's "deleted" column
*/
delete() {
this.context({
softDelete: true,
});
const patch = {};
patch[options.columnName] = options.deletedValue;
return this.patch(patch);
}
/**
* Provide a way to actually delete the row if necessary
*/
hardDelete() {
return super.delete();
}
/**
* Provide a way to undo the delete
*/
undelete() {
this.context({
undelete: true,
});
const patch = {};
patch[options.columnName] = options.notDeletedValue;
return this.patch(patch);
}
/**
* Provide a way to filter to ONLY deleted records without having to remember the column name
*/
whereDeleted() {
const prefix = this.modelClass().tableName;
// this if is for backwards compatibility, to protect those that used a nullable `deleted` field
if (options.deletedValue === true) {
return this.where(`${prefix}.${options.columnName}`, options.deletedValue);
}
// qualify the column name
return this.whereNot(`${prefix}.${options.columnName}`, options.notDeletedValue);
}
// provide a way to filter out deleted records without having to remember the column name
whereNotDeleted() {
const prefix = this.modelClass().tableName;
// qualify the column name
return this.where(`${prefix}.${options.columnName}`, options.notDeletedValue);
}
}

View File

@@ -8,19 +8,42 @@ const balanceSheetStructure: IBalanceSheetStructureSection[] = [
children: [
{
name: 'Current Asset',
type: 'accounts_section',
accountsTypesRelated: ['current_asset'],
sectionType: 'assets',
type: 'section',
children: [
{
name: 'Cash and cash equivalents',
type: 'accounts_section',
accountsTypes: ['cash', 'bank'],
},
{
name: 'Accounts Receivable',
type: 'accounts_section',
accountsTypes: ['accounts_receivable'],
},
{
name: 'Inventories',
type: 'accounts_section',
accountsTypes: ['inventory'],
},
{
name: 'Other current assets',
type: 'accounts_section',
accountsTypes: ['other_current_asset'],
},
],
alwaysShow: true,
},
{
name: 'Fixed Asset',
type: 'accounts_section',
accountsTypesRelated: ['fixed_asset'],
accountsTypes: ['fixed_asset'],
},
{
name: 'Other Asset',
name: 'Non-Current Assets',
type: 'accounts_section',
accountsTypesRelated: ['other_asset'],
},
accountsTypes: ['non_current_asset'],
}
],
alwaysShow: true,
},
@@ -35,27 +58,32 @@ const balanceSheetStructure: IBalanceSheetStructureSection[] = [
type: 'section',
children: [
{
name: 'Current Liability',
name: 'Current Liabilties',
type: 'accounts_section',
accountsTypesRelated: ['current_liability'],
accountsTypes: [
'accounts_payable',
'tax_payable',
'credit_card',
'other_current_liability'
],
},
{
name: 'Long Term Liability',
name: 'Long-Term Liabilities',
type: 'accounts_section',
accountsTypesRelated: ['long_term_liability'],
accountsTypes: ['long_term_liability'],
},
{
name: 'Other Liability',
name: 'Non-Current Liabilities',
type: 'accounts_section',
accountsTypesRelated: ['other_liability'],
},
accountsTypes: ['non_current_liability'],
}
],
},
{
name: 'Equity',
sectionType: 'equity',
type: 'accounts_section',
accountsTypesRelated: ['equity'],
accountsTypes: ['equity'],
},
],
alwaysShow: true,

View File

@@ -1,141 +1,14 @@
import Container from 'typedi';
import TenancyService from 'services/Tenancy/TenancyService'
import I18nMiddleware from 'api/middleware/I18nMiddleware';
import AccountsTypesData from '../data/accounts_types';
exports.up = function (knex) {
const tenancyService = Container.get(TenancyService);
const i18n = tenancyService.i18n(knex.userParams.tenantId);
return knex('account_types').insert([
{
id: 1,
key: 'fixed_asset',
normal: 'debit',
root_type: 'asset',
child_type: 'fixed_asset',
balance_sheet: true,
income_sheet: false,
},
{
id: 2,
key: 'current_asset',
normal: 'debit',
root_type: 'asset',
child_type: 'current_asset',
balance_sheet: true,
income_sheet: false,
},
{
id: 14,
key: 'other_asset',
normal: 'debit',
root_type: 'asset',
child_type: 'other_asset',
balance_sheet: true,
income_sheet: false,
},
{
id: 3,
key: 'long_term_liability',
normal: 'credit',
root_type: 'liability',
child_type: 'long_term_liability',
balance_sheet: false,
income_sheet: true,
},
{
id: 4,
key: 'current_liability',
normal: 'credit',
root_type: 'liability',
child_type: 'current_liability',
balance_sheet: false,
income_sheet: true,
},
{
id: 13,
key: 'other_liability',
normal: 'credit',
root_type: 'liability',
child_type: 'other_liability',
balance_sheet: false,
income_sheet: true,
},
{
id: 5,
key: 'equity',
normal: 'credit',
root_type: 'equity',
child_type: 'equity',
balance_sheet: true,
income_sheet: false,
},
{
id: 6,
key: 'expense',
normal: 'debit',
root_type: 'expense',
child_type: 'expense',
balance_sheet: false,
income_sheet: true,
},
{
id: 10,
key: 'other_expense',
normal: 'debit',
root_type: 'expense',
balance_sheet: false,
income_sheet: true,
},
{
id: 7,
key: 'income',
normal: 'credit',
root_type: 'income',
child_type: 'income',
balance_sheet: false,
income_sheet: true,
},
{
id: 11,
key: 'other_income',
normal: 'credit',
root_type: 'income',
child_type: 'other_income',
balance_sheet: false,
income_sheet: true,
},
{
id: 12,
key: 'cost_of_goods_sold',
normal: 'debit',
root_type: 'expenses',
child_type: 'expenses',
balance_sheet: false,
income_sheet: true,
},
{
id: 8,
key: 'accounts_receivable',
normal: 'debit',
root_type: 'asset',
child_type: 'current_asset',
balance_sheet: true,
income_sheet: false,
},
{
id: 9,
key: 'accounts_payable',
normal: 'credit',
root_type: 'liability',
child_type: 'current_liability',
balance_sheet: true,
income_sheet: false,
},
...AccountsTypesData
]);
};
exports.down = function(knex) {
}

View File

@@ -1,179 +1,32 @@
import Container from 'typedi';
import { get } from 'lodash';
import TenancyService from 'services/Tenancy/TenancyService'
import AccountsData from '../data/accounts';
exports.up = function (knex) {
const tenancyService = Container.get(TenancyService);
const i18n = tenancyService.i18n(knex.userParams.tenantId);
return knex('accounts').then(() => {
// Inserts seed entries
return knex('accounts').insert([
{
id: 1,
name: i18n.__('Petty Cash'),
slug: 'petty-cash',
account_type_id: 2,
parent_account_id: null,
code: '1000',
description: '',
const accountMapper = (account) => {
return knex('account_types').where('key', account.account_type).first()
.then((accountType) => ({
name: i18n.__(account.name),
slug: account.slug,
account_type_id: get(accountType, 'id', null),
code: account.code,
description: i18n.__(account.description),
active: 1,
index: 1,
predefined: 1,
},
{
id: 2,
name: i18n.__('Bank'),
slug: 'bank',
account_type_id: 2,
parent_account_id: null,
code: '2000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 3,
name: i18n.__('Other Income'),
slug: 'other-income',
account_type_id: 7,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 4,
name: i18n.__('Interest Income'),
slug: 'interest-income',
account_type_id: 7,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 5,
name: i18n.__('Opening Balance'),
slug: 'opening-balance',
account_type_id: 5,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 6,
name: i18n.__('Depreciation Expense'),
slug: 'depreciation-expense',
account_type_id: 6,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 7,
name: i18n.__('Interest Expense'),
slug: 'interest-expense',
account_type_id: 6,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 8,
name: i18n.__('Payroll Expenses'),
slug: 'payroll-expenses',
account_type_id: 6,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 9,
name: i18n.__('Other Expenses'),
slug: 'other-expenses',
account_type_id: 6,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 10,
name: i18n.__('Accounts Receivable'),
slug: 'accounts-receivable',
account_type_id: 8,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 11,
name: i18n.__('Accounts Payable'),
slug: 'accounts-payable',
account_type_id: 9,
parent_account_id: null,
code: '1000',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
id: 12,
name: i18n.__('Cost of Goods Sold (COGS)'),
slug: 'cost-of-goods-sold',
account_type_id: 12,
predefined: 1,
parent_account_id: null,
index: 1,
active: 1,
description: 1,
},
{
id: 13,
name: i18n.__('Inventory Asset'),
slug: 'inventory-asset',
account_type_id: 14,
predefined: 1,
parent_account_id: null,
index: 1,
active: 1,
description: '',
},
{
id: 14,
name: i18n.__('Sales of Product Income'),
slug: 'sales-of-product-income',
account_type_id: 7,
predefined: 1,
parent_account_id: null,
index: 1,
active: 1,
description: '',
}
]);
predefined: account.predefined,
}));
};
return knex('accounts').then(async () => {
const accountsPromises = AccountsData.map(accountMapper);
const accounts = await Promise.all(accountsPromises);
// Inserts seed entries.
return knex('accounts').insert([ ...accounts ]);
});
};

View File

@@ -0,0 +1,318 @@
export default [
{
name:'Bank Account',
slug: 'bank-account',
account_type: 'bank',
code: '10001',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name:'Saving Bank Account',
slug: 'saving-bank-account',
account_type: 'bank',
code: '10002',
description: '',
active: 1,
index: 1,
predefined: 0,
},
{
name:'Undeposited Funds',
slug: 'undeposited-funds',
account_type: 'cash',
code: '10003',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name:'Petty Cash',
slug: 'petty-cash',
account_type: 'cash',
code: '10004',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name:'Computer Equipment',
slug: 'computer-equipment',
code: '10005',
account_type: 'fixed_asset',
predefined: 0,
parent_account_id: null,
index: 1,
active: 1,
description: '',
},
{
name:'Office Equipment',
slug: 'office-equipment',
code: '10006',
account_type: 'fixed_asset',
predefined: 0,
parent_account_id: null,
index: 1,
active: 1,
description: '',
},
{
name:'Accounts Receivable (A/R)',
slug: 'accounts-receivable',
account_type: 'accounts_receivable',
code: '10007',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name:'Inventory Asset',
slug: 'inventory-asset',
code: '10008',
account_type: 'inventory',
predefined: 1,
parent_account_id: null,
index: 1,
active: 1,
description:'An account that holds valuation of products or goods that availiable for sale.',
},
// Libilities
{
name:'Accounts Payable (A/P)',
slug: 'accounts-payable',
account_type: 'accounts_payable',
parent_account_id: null,
code: '20001',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name:'Owner A Drawings',
slug: 'owner-drawings',
account_type: 'other_current_liability',
parent_account_id: null,
code: '20002',
description:'Withdrawals by the owners.',
active: 1,
index: 1,
predefined: 0,
},
{
name:'Loan',
slug: 'owner-drawings',
account_type: 'other_current_liability',
code: '20003',
description:'Money that has been borrowed from a creditor.',
active: 1,
index: 1,
predefined: 0,
},
{
name:'Opening Balance Adjustments',
slug: 'opening-balance-adjustments',
account_type: 'other_current_liability',
code: '20004',
description:'This account will hold the difference in the debits and credits entered during the opening balance..',
active: 1,
index: 1,
predefined: 0,
},
{
name:'Revenue Received in Advance',
slug: 'Revenue-received-in-advance',
account_type: 'other_current_liability',
parent_account_id: null,
code: '20005',
description: 'When customers pay in advance for products/services.',
active: 1,
index: 1,
predefined: 0,
},
{
name:'Sales Tax Payable',
slug: 'owner-drawings',
account_type: 'tax_payable',
code: '20006',
description: '',
active: 1,
index: 1,
predefined: 1,
},
// Equity
{
name:'Retained Earnings',
slug: 'retained-earnings',
account_type: 'equity',
code: '30001',
description:'Retained earnings tracks net income from previous fiscal years.',
active: 1,
index: 1,
predefined: 1,
},
{
name:'Opening Balance Equity',
slug: 'opening-balance-equity',
account_type: 'equity',
code: '30002',
description:'When you enter opening balances to the accounts, the amounts enter in Opening balance equity. This ensures that you have a correct trial balance sheet for your company, without even specific the second credit or debit entry.',
active: 1,
index: 1,
predefined: 1,
},
{
name: "Owner's Equity",
slug: 'owner-equity',
account_type: 'equity',
code: '30003',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name:`Drawings`,
slug: 'drawings',
account_type: 'equity',
code: '30003',
description:'Goods purchased with the intention of selling these to customers',
active: 1,
index: 1,
predefined: 1,
},
// Expenses
{
name:'Uncategorized Expenses',
slug: 'uncategorized-expense',
account_type: 'expense',
parent_account_id: null,
code: '40001',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name:'Cost of Goods Sold',
slug: 'cost-of-goods-sold',
account_type: 'cost_of_goods_sold',
parent_account_id: null,
code: '40002',
description:'Tracks the direct cost of the goods sold.',
active: 1,
index: 1,
predefined: 1,
},
{
name:'Office expenses',
slug: 'office-expenses',
account_type: 'expense',
parent_account_id: null,
code: '40003',
description: '',
active: 1,
index: 1,
predefined: 0,
},
{
name:'Rent',
slug: 'rent',
account_type: 'expense',
parent_account_id: null,
code: '40004',
description: '',
active: 1,
index: 1,
predefined: 0,
},
{
name:'Exchange Gain or Loss',
slug: 'exchange-grain-loss',
account_type: 'other_expense',
parent_account_id: null,
code: '40005',
description:'Tracks the gain and losses of the exchange differences.',
active: 1,
index: 1,
predefined: 1,
},
{
name:'Bank Fees and Charges',
slug: 'bank-fees-and-charges',
account_type: 'expense',
parent_account_id: null,
code: '40006',
description: 'Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.',
active: 1,
index: 1,
predefined: 0,
},
{
name:'Depreciation Expense',
slug: 'depreciation-expense',
account_type: 'expense',
parent_account_id: null,
code: '40007',
description: '',
active: 1,
index: 1,
predefined: 0,
},
// Income
{
name:'Sales of Product Income',
slug: 'sales-of-product-income',
account_type: 'income',
predefined: 1,
parent_account_id: null,
code: '50001',
index: 1,
active: 1,
description: '',
},
{
name:'Sales of Service Income',
slug: 'sales-of-service-income',
account_type: 'income',
predefined: 0,
parent_account_id: null,
code: '50002',
index: 1,
active: 1,
description: '',
},
{
name:'Uncategorized Income',
slug: 'uncategorized-income',
account_type: 'income',
parent_account_id: null,
code: '50003',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name:'Other Income',
slug: 'other-income',
account_type: 'other_income',
parent_account_id: null,
code: '50004',
description:'The income activities are not associated to the core business.',
active: 1,
index: 1,
predefined: 0,
}
];

View File

@@ -0,0 +1,155 @@
export default [
{
key: 'cash',
normal: 'debit',
child_type: 'current_asset',
root_type: 'asset',
balance_sheet: true,
income_sheet: false,
},
{
key: 'bank',
normal: 'debit',
child_type: 'current_asset',
root_type: 'asset',
balance_sheet: true,
income_sheet: false,
},
{
key: 'accounts_receivable',
normal: 'debit',
root_type: 'asset',
child_type: 'current_asset',
balance_sheet: true,
income_sheet: false,
},
{
key: 'inventory',
normal: 'debit',
root_type: 'asset',
child_type: 'current_asset',
balance_sheet: true,
income_sheet: false,
},
{
key: 'other_current_asset',
normal: 'debit',
root_type: 'asset',
child_type: 'current_asset',
balance_sheet: true,
income_sheet: false,
},
{
key: 'fixed_asset',
normal: 'debit',
root_type: 'asset',
child_type: 'fixed_asset',
balance_sheet: true,
income_sheet: false,
},
{
key: 'non_current_asset',
normal: 'debit',
root_type: 'asset',
child_type: 'fixed_asset',
balance_sheet: true,
income_sheet: false,
},
{
key: 'accounts_payable',
normal: 'credit',
root_type: 'liability',
child_type: 'current_liability',
balance_sheet: true,
income_sheet: false,
},
{
key: 'credit_card',
normal: 'credit',
root_type: 'liability',
child_type: 'current_liability',
balance_sheet: true,
income_sheet: false,
},
{
key: 'tax_payable',
normal: 'credit',
root_type: 'liability',
child_type: 'current_liability',
balance_sheet: true,
income_sheet: false,
},
{
key: 'other_current_liability',
normal: 'credit',
root_type: 'liability',
child_type: 'current_liability',
balance_sheet: false,
income_sheet: true,
},
{
key: 'non_current_liability',
normal: 'credit',
root_type: 'liability',
child_type: 'current_liability',
balance_sheet: false,
income_sheet: true,
},
{
key: 'long_term_liability',
normal: 'credit',
root_type: 'liability',
child_type: 'long_term_liability',
balance_sheet: false,
income_sheet: true,
},
{
key: 'equity',
normal: 'credit',
root_type: 'equity',
child_type: 'equity',
balance_sheet: true,
income_sheet: false,
},
{
key: 'income',
normal: 'credit',
root_type: 'income',
child_type: 'income',
balance_sheet: false,
income_sheet: true,
},
{
key: 'other_income',
normal: 'credit',
root_type: 'income',
child_type: 'other_income',
balance_sheet: false,
income_sheet: true,
},
{
key: 'cost_of_goods_sold',
normal: 'debit',
root_type: 'expenses',
child_type: 'expenses',
balance_sheet: false,
income_sheet: true,
},
{
key: 'expense',
normal: 'debit',
root_type: 'expense',
child_type: 'expense',
balance_sheet: false,
income_sheet: true,
},
{
key: 'other_expense',
normal: 'debit',
root_type: 'expense',
balance_sheet: false,
income_sheet: true,
},
];

View File

@@ -42,7 +42,7 @@ export interface IBalanceSheetStructureSection {
sectionType?: string;
type: 'section' | 'accounts_section';
children?: IBalanceSheetStructureSection[];
accountsTypesRelated?: string[];
accountsTypes?: string[];
alwaysShow?: boolean;
}
@@ -74,6 +74,6 @@ export interface IBalanceSheetSection {
total: IBalanceSheetAccountTotal;
totalPeriods?: IBalanceSheetAccountTotal[];
accountsTypesRelated?: string[];
accountsTypes?: string[];
_forceShow?: boolean;
}

View File

@@ -14,13 +14,26 @@ export interface IGeneralLedgerSheetQuery {
export interface IGeneralLedgerSheetAccountTransaction {
id: number,
amount: number,
runningBalance: number,
credit: number,
debit: number,
formattedAmount: string,
formattedCredit: string,
formattedDebit: string,
formattedRunningBalance: string,
currencyCode: string,
note?: string,
transactionType?: string,
transactionNumber: string,
referenceId?: number,
referenceType?: string,
date: Date|string,
};
@@ -38,8 +51,8 @@ export interface IGeneralLedgerSheetAccount {
index: number,
parentAccountId: number,
transactions: IGeneralLedgerSheetAccountTransaction[],
opening: IGeneralLedgerSheetAccountBalance,
closing: IGeneralLedgerSheetAccountBalance,
openingBalance: IGeneralLedgerSheetAccountBalance,
closingBalance: IGeneralLedgerSheetAccountBalance,
}
export interface IAccountTransaction {

View File

@@ -50,7 +50,7 @@ export interface IProfitLossSheetStatement {
costOfSales: IProfitLossSheetAccountsSection,
expenses: IProfitLossSheetAccountsSection,
otherExpenses: IProfitLossSheetAccountsSection,
otherIncome: IProfitLossSheetAccountsSection,
netIncome: IProfitLossSheetTotalSection;
operatingProfit: IProfitLossSheetTotalSection;
grossProfit: IProfitLossSheetTotalSection;

View File

@@ -1,6 +1,6 @@
import { Model } from 'objection';
export interface ISystemUser {
export interface ISystemUser extends Model {
id: number,
firstName: string,
lastName: string,
@@ -34,4 +34,12 @@ export interface IInviteUserInput {
lastName: string,
phoneNumber: string,
password: string,
};
export interface IUserInvite {
id: number,
email: string,
token: string,
tenantId: number,
createdAt?: Date,
}

View File

@@ -57,20 +57,24 @@ export default class AccountType extends TenantModel {
*/
static get labels() {
return {
fixed_asset: 'Fixed asset',
current_asset: "Current asset",
long_term_liability: "Long term liability",
current_liability: "Current liability",
inventory: 'Inventory',
other_current_asset: 'Other Current Asset',
bank: 'Bank Account',
cash: 'Cash',
fixed_asset: 'Fixed Asset',
non_current_asset: 'Non-Current Asset',
accounts_payable: 'Accounts Payable (A/P)',
accounts_receivable: 'Accounts Receivable (A/R)',
credit_card: 'Credit Card',
long_term_liability: 'Long Term Liability',
other_current_liability: 'Other Current Liability',
other_liability: 'Other Liability',
equity: "Equity",
expense: "Expense",
income: "Income",
accounts_receivable: "Accounts receivable",
accounts_payable: "Accounts payable",
other_expense: "Other expense",
other_income: "Other income",
cost_of_goods_sold: "Cost of goods sold (COGS)",
other_liability: "Other liability",
other_asset: 'Other asset',
other_income: "Other Income",
other_expense: "Other Expense",
cost_of_goods_sold: "Cost of Goods Sold (COGS)",
};
}
}

View File

@@ -1,4 +1,4 @@
import { cloneDeep, cloneDeepWith, forOwn, isString } from 'lodash';
import { cloneDeep, forOwn, isString } from 'lodash';
import ModelEntityNotFound from 'exceptions/ModelEntityNotFound';
export default class EntityRepository {
@@ -38,8 +38,7 @@ export default class EntityRepository {
* @returns {Promise<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
find(attributeValues = {}, withRelations?) {
return this.model
.query()
return this.model.query()
.where(attributeValues)
.withGraphFetched(withRelations);
}

View File

@@ -71,6 +71,13 @@ export default class JournalPoster implements IJournalPoster {
}
}
/**
*
*/
public isEmpty() {
return this.entries.length === 0;
}
/**
* Writes the credit entry for the given account.
* @param {IJournalEntry} entry -

View File

@@ -23,7 +23,6 @@ import AuthenticationMailMessages from 'services/Authentication/AuthenticationMa
import AuthenticationSMSMessages from 'services/Authentication/AuthenticationSMSMessages';
import TenantsManager from 'services/Tenancy/TenantsManager';
const ERRORS = {
INVALID_DETAILS: 'INVALID_DETAILS',
USER_INACTIVE: 'USER_INACTIVE',
@@ -32,7 +31,7 @@ const ERRORS = {
USER_NOT_FOUND: 'USER_NOT_FOUND',
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
EMAIL_EXISTS: 'EMAIL_EXISTS'
EMAIL_EXISTS: 'EMAIL_EXISTS',
};
@Service()
export default class AuthenticationService implements IAuthenticationService {
@@ -136,6 +135,7 @@ export default class AuthenticationService implements IAuthenticationService {
*/
private async validateEmailAndPhoneUniqiness(registerDTO: IRegisterDTO) {
const { systemUserRepository } = this.sysRepositories;
const isEmailExists = await systemUserRepository.findOneByEmail(
registerDTO.email
);
@@ -279,7 +279,10 @@ export default class AuthenticationService implements IAuthenticationService {
const hashedPassword = await hashPassword(password);
this.logger.info('[reset_password] saving a new hashed password.');
await systemUserRepository.update({ password: hashedPassword }, { id: user.id });
await systemUserRepository.update(
{ password: hashedPassword },
{ id: user.id }
);
// Deletes the used token.
await this.deletePasswordResetToken(tokenModel.email);

View File

@@ -184,7 +184,7 @@ export default class BalanceSheetStatement extends FinancialSheet {
const filteredAccounts = accounts
// Filter accounts that associated to the section accounts types.
.filter(
(account) => sectionAccountsTypes.indexOf(account.type.childType) !== -1
(account) => sectionAccountsTypes.indexOf(account.type.key) !== -1
)
.map((account) => this.balanceSheetAccountMapper(account))
// Filter accounts that have no transaction when `noneTransactions` is on.
@@ -258,7 +258,7 @@ export default class BalanceSheetStatement extends FinancialSheet {
type: structure.type,
...(structure.type === 'accounts_section'
? this.structureRelatedAccountsMapper(
structure.accountsTypesRelated,
structure.accountsTypes,
accounts
)
: this.structureSectionMapper(structure, accounts)),

View File

@@ -1,4 +1,4 @@
import { pick } from 'lodash';
import { pick, get, last } from 'lodash';
import {
IGeneralLedgerSheetQuery,
IGeneralLedgerSheetAccount,
@@ -8,9 +8,13 @@ import {
IJournalPoster,
IAccountType,
IJournalEntry,
IContact,
} from 'interfaces';
import FinancialSheet from '../FinancialSheet';
/**
* General ledger sheet.
*/
export default class GeneralLedgerSheet extends FinancialSheet {
tenantId: number;
accounts: IAccount[];
@@ -18,6 +22,7 @@ export default class GeneralLedgerSheet extends FinancialSheet {
openingBalancesJournal: IJournalPoster;
closingBalancesJournal: IJournalPoster;
transactions: IJournalPoster;
contactsMap: Map<number, IContact>;
baseCurrency: string;
/**
@@ -32,6 +37,7 @@ export default class GeneralLedgerSheet extends FinancialSheet {
tenantId: number,
query: IGeneralLedgerSheetQuery,
accounts: IAccount[],
contactsByIdMap: Map<number, IContact>,
transactions: IJournalPoster,
openingBalancesJournal: IJournalPoster,
closingBalancesJournal: IJournalPoster,
@@ -43,48 +49,100 @@ export default class GeneralLedgerSheet extends FinancialSheet {
this.query = query;
this.numberFormat = this.query.numberFormat;
this.accounts = accounts;
this.contactsMap = contactsByIdMap;
this.transactions = transactions;
this.openingBalancesJournal = openingBalancesJournal;
this.closingBalancesJournal = closingBalancesJournal;
this.baseCurrency = baseCurrency;
}
/**
* Retrieve the transaction amount.
* @param {number} credit - Credit amount.
* @param {number} debit - Debit amount.
* @param {string} normal - Credit or debit.
*/
getAmount(credit: number, debit: number, normal: string) {
return normal === 'credit' ? credit - debit : debit - credit;
}
/**
* Entry mapper.
* @param {IJournalEntry} entry -
* @return {IGeneralLedgerSheetAccountTransaction}
*/
entryReducer(
entries: IGeneralLedgerSheetAccountTransaction[],
entry: IJournalEntry,
index: number
): IGeneralLedgerSheetAccountTransaction[] {
const lastEntry = last(entries);
const openingBalance = 0;
const contact = this.contactsMap.get(entry.contactId);
const amount = this.getAmount(
entry.credit,
entry.debit,
entry.accountNormal
);
const runningBalance =
(entries.length === 0
? openingBalance
: lastEntry
? lastEntry.runningBalance
: 0) + amount;
const newEntry = {
date: entry.date,
entryId: entry.id,
referenceType: entry.referenceType,
referenceId: entry.referenceId,
referenceTypeFormatted: entry.referenceTypeFormatted,
contactName: get(contact, 'displayName'),
contactType: get(contact, 'contactService'),
transactionType: entry.transactionType,
index: entry.index,
note: entry.note,
credit: entry.credit,
debit: entry.debit,
amount,
runningBalance,
formattedAmount: this.formatNumber(amount),
formattedCredit: this.formatNumber(entry.credit),
formattedDebit: this.formatNumber(entry.debit),
formattedRunningBalance: this.formatNumber(runningBalance),
currencyCode: this.baseCurrency,
};
entries.push(newEntry);
return entries;
}
/**
* Mapping the account transactions to general ledger transactions of the given account.
* @param {IAccount} account
* @return {IGeneralLedgerSheetAccountTransaction[]}
*/
private accountTransactionsMapper(
account: IAccount & { type: IAccountType }
account: IAccount & { type: IAccountType },
openingBalance: number
): IGeneralLedgerSheetAccountTransaction[] {
const entries = this.transactions.getAccountEntries(account.id);
return entries.map(
(transaction: IJournalEntry): IGeneralLedgerSheetAccountTransaction => {
let amount = 0;
if (account.type.normal === 'credit') {
amount += transaction.credit - transaction.debit;
} else if (account.type.normal === 'debit') {
amount += transaction.debit - transaction.credit;
}
const formattedAmount = this.formatNumber(amount);
return {
...pick(transaction, [
'id',
'note',
'transactionType',
'referenceType',
'referenceId',
'referenceTypeFormatted',
'date',
]),
amount,
formattedAmount,
currencyCode: this.baseCurrency,
};
}
return entries.reduce(
(
entries: IGeneralLedgerSheetAccountTransaction[],
entry: IJournalEntry
) => {
return this.entryReducer(entries, entry, openingBalance);
},
[]
);
}
@@ -128,11 +186,21 @@ export default class GeneralLedgerSheet extends FinancialSheet {
private accountMapper(
account: IAccount & { type: IAccountType }
): IGeneralLedgerSheetAccount {
const openingBalance = this.accountOpeningBalance(account);
const closingBalance = this.accountClosingBalance(account);
return {
...pick(account, ['id', 'name', 'code', 'index', 'parentAccountId']),
opening: this.accountOpeningBalance(account),
transactions: this.accountTransactionsMapper(account),
closing: this.accountClosingBalance(account),
id: account.id,
name: account.name,
code: account.code,
index: account.index,
parentAccountId: account.parentAccountId,
openingBalance,
transactions: this.accountTransactionsMapper(
account,
openingBalance.amount
),
closingBalance,
};
}
@@ -149,7 +217,8 @@ export default class GeneralLedgerSheet extends FinancialSheet {
.map((account: IAccount & { type: IAccountType }) =>
this.accountMapper(account)
)
// Filter general ledger accounts that have no transactions when `noneTransactions` is on.
// Filter general ledger accounts that have no transactions
// when`noneTransactions` is on.
.filter(
(generalLedgerAccount: IGeneralLedgerSheetAccount) =>
!(

View File

@@ -7,6 +7,8 @@ import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster';
import GeneralLedgerSheet from 'services/FinancialStatements/GeneralLedger/GeneralLedger';
import { transformToMap } from 'utils';
const ERRORS = {
ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND',
};
@@ -70,6 +72,7 @@ export default class GeneralLedgerService {
const {
accountRepository,
transactionsRepository,
contactRepository
} = this.tenancy.repositories(tenantId);
const settings = this.tenancy.settings(tenantId);
@@ -89,6 +92,10 @@ export default class GeneralLedgerService {
const accounts = await accountRepository.all('type');
const accountsGraph = await accountRepository.getDependencyGraph();
// Retrieve all contacts on the storage.
const contacts = await contactRepository.all();
const contactsByIdMap = transformToMap(contacts, 'id');
// Retreive journal transactions from/to the given date.
const transactions = await transactionsRepository.journal({
fromDate: filter.fromDate,
@@ -127,6 +134,7 @@ export default class GeneralLedgerService {
tenantId,
filter,
accounts,
contactsByIdMap,
transactionsJournal,
openingTransJournal,
closingTransJournal,

View File

@@ -1,19 +1,20 @@
import { sumBy, chain, omit } from 'lodash';
import { sumBy, chain, get, head } from 'lodash';
import {
IJournalEntry,
IJournalPoster,
IJournalReportEntriesGroup,
IJournalReportQuery,
IJournalReport,
IContact,
} from 'interfaces';
import FinancialSheet from '../FinancialSheet';
import { AccountTransaction } from 'models';
export default class JournalSheet extends FinancialSheet {
tenantId: number;
journal: IJournalPoster;
query: IJournalReportQuery;
baseCurrency: string;
readonly contactsById: Map<number | string, IContact>;
/**
* Constructor method.
@@ -24,6 +25,8 @@ export default class JournalSheet extends FinancialSheet {
tenantId: number,
query: IJournalReportQuery,
journal: IJournalPoster,
accountsGraph: any,
contactsById: Map<number | string, IContact>,
baseCurrency: string
) {
super();
@@ -32,22 +35,48 @@ export default class JournalSheet extends FinancialSheet {
this.journal = journal;
this.query = query;
this.numberFormat = this.query.numberFormat;
this.accountsGraph = accountsGraph;
this.contactsById = contactsById;
this.baseCurrency = baseCurrency;
}
/**
* Mappes the journal entries.
* @param {IJournalEntry[]} entries -
* Entry mapper.
* @param {IJournalEntry} entry
*/
entriesMapper(
entries: IJournalEntry[],
) {
return entries.map((entry: IJournalEntry) => {
return {
...omit(entry, 'account'),
currencyCode: this.baseCurrency,
};
})
entryMapper(entry: IJournalEntry) {
const account = this.accountsGraph.getNodeData(entry.accountId);
const contact = this.contactsById.get(entry.contactId);
return {
entryId: entry.id,
index: entry.index,
note: entry.note,
contactName: get(contact, 'displayName'),
contactType: get(contact, 'contactService'),
accountName: account.name,
accountCode: account.code,
transactionNumber: entry.transactionNumber,
currencyCode: this.baseCurrency,
formattedCredit: this.formatNumber(entry.credit),
formattedDebit: this.formatNumber(entry.debit),
credit: entry.credit,
debit: entry.debit,
createdAt: entry.createdAt,
};
}
/**
* Mappes the journal entries.
* @param {IJournalEntry[]} entries -
*/
entriesMapper(entries: IJournalEntry[]) {
return entries.map(this.entryMapper.bind(this));
}
/**
@@ -58,13 +87,17 @@ export default class JournalSheet extends FinancialSheet {
*/
entriesGroupsMapper(
entriesGroup: IJournalEntry[],
key: string
groupEntry: IJournalEntry
): IJournalReportEntriesGroup {
const totalCredit = sumBy(entriesGroup, 'credit');
const totalDebit = sumBy(entriesGroup, 'debit');
return {
id: key,
date: groupEntry.date,
referenceType: groupEntry.referenceType,
referenceId: groupEntry.referenceId,
referenceTypeFormatted: groupEntry.referenceTypeFormatted,
entries: this.entriesMapper(entriesGroup),
currencyCode: this.baseCurrency,
@@ -72,8 +105,8 @@ export default class JournalSheet extends FinancialSheet {
credit: totalCredit,
debit: totalDebit,
formattedCredit: this.formatNumber(totalCredit),
formattedDebit: this.formatNumber(totalDebit),
formattedCredit: this.formatTotalNumber(totalCredit),
formattedDebit: this.formatTotalNumber(totalDebit),
};
}
@@ -85,9 +118,10 @@ export default class JournalSheet extends FinancialSheet {
entriesWalker(entries: IJournalEntry[]): IJournalReportEntriesGroup[] {
return chain(entries)
.groupBy((entry) => `${entry.referenceId}-${entry.referenceType}`)
.map((entriesGroup: IJournalEntry[], key: string) =>
this.entriesGroupsMapper(entriesGroup, key)
)
.map((entriesGroup: IJournalEntry[], key: string) => {
const headEntry = head(entriesGroup);
return this.entriesGroupsMapper(entriesGroup, headEntry);
})
.value();
}

View File

@@ -1,10 +1,13 @@
import { Service, Inject } from 'typedi';
import { IJournalReportQuery } from 'interfaces';
import moment from 'moment';
import JournalSheet from './JournalSheet';
import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster';
import { transformToMap } from 'utils';
@Service()
export default class JournalSheetService {
@Inject()
@@ -40,6 +43,7 @@ export default class JournalSheetService {
const {
accountRepository,
transactionsRepository,
contactRepository,
} = this.tenancy.repositories(tenantId);
const filter = {
@@ -50,7 +54,6 @@ export default class JournalSheetService {
tenantId,
filter,
});
// Settings service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({
@@ -60,6 +63,10 @@ export default class JournalSheetService {
// Retrieve all accounts on the storage.
const accountsGraph = await accountRepository.getDependencyGraph();
// Retrieve all contacts on the storage.
const contacts = await contactRepository.all();
const contactsByIdMap = transformToMap(contacts, 'id');
// Retrieve all journal transactions based on the given query.
const transactions = await transactionsRepository.journal({
fromDate: filter.fromDate,
@@ -79,6 +86,8 @@ export default class JournalSheetService {
tenantId,
filter,
transactionsJournal,
accountsGraph,
contactsByIdMap,
baseCurrency
);
// Retrieve journal report columns.

View File

@@ -50,6 +50,10 @@ export default class ProfitLossSheet extends FinancialSheet {
this.initDateRangeCollection();
}
get otherIncomeAccounts() {
return this.accounts.filter((a) => a.type.key === 'other_income');
}
/**
* Filtering income accounts.
* @return {IAccount & { type: IAccountType }[]}
@@ -235,6 +239,14 @@ export default class ProfitLossSheet extends FinancialSheet {
};
}
private get otherIncomeSection(): any {
return {
name: 'Other Income',
entryNormal: 'credit',
...this.sectionMapper(this.otherIncomeAccounts)
}
}
/**
* Retreive expenses section.
* @return {IProfitLossSheetLossSection}
@@ -343,10 +355,14 @@ export default class ProfitLossSheet extends FinancialSheet {
* @return {IProfitLossSheetStatement}
*/
public reportData(): IProfitLossSheetStatement {
if (this.journal.isEmpty()) {
return null;
}
const income = this.incomeSection;
const costOfSales = this.costOfSalesSection;
const expenses = this.expensesSection;
const otherExpenses = this.otherExpensesSection;
const otherIncome = this.otherIncomeSection;
// - Gross profit = Total income - COGS.
const grossProfit = this.getSummarySection(income, costOfSales);
@@ -356,7 +372,6 @@ export default class ProfitLossSheet extends FinancialSheet {
expenses,
costOfSales,
]);
// - Net income = Operating profit - Other expenses.
const netIncome = this.getSummarySection(operatingProfit, otherExpenses);
@@ -365,6 +380,7 @@ export default class ProfitLossSheet extends FinancialSheet {
costOfSales,
grossProfit,
expenses,
otherIncome,
otherExpenses,
netIncome,
operatingProfit,

View File

@@ -4,6 +4,7 @@ import {
ITrialBalanceAccount,
IAccount,
ITrialBalanceTotal,
ITrialBalanceSheetData,
IAccountType,
} from 'interfaces';
import FinancialSheet from '../FinancialSheet';
@@ -49,6 +50,7 @@ export default class TrialBalanceSheet extends FinancialSheet {
/**
* Account mapper.
* @param {IAccount} account
* @return {ITrialBalanceAccount}
*/
private accountMapper(
account: IAccount & { type: IAccountType }
@@ -80,6 +82,7 @@ export default class TrialBalanceSheet extends FinancialSheet {
/**
* Accounts walker.
* @param {IAccount[]} accounts
* @return {ITrialBalanceAccount[]}
*/
private accountsWalker(
accounts: IAccount & { type: IAccountType }[]
@@ -136,8 +139,15 @@ export default class TrialBalanceSheet extends FinancialSheet {
/**
* Retrieve trial balance sheet statement data.
* Note: Retruns null in case there is no transactions between the given date periods.
*
* @return {ITrialBalanceSheetData}
*/
public reportData() {
public reportData(): ITrialBalanceSheetData {
// Don't return noting if the journal has no transactions.
if (this.journalFinancial.isEmpty()) {
return null;
}
const accounts = this.accountsWalker(this.accounts);
const total = this.tatalSection(accounts);

View File

@@ -12,13 +12,14 @@ import { hashPassword } from 'utils';
import TenancyService from 'services/Tenancy/TenancyService';
import InviteUsersMailMessages from 'services/InviteUsers/InviteUsersMailMessages';
import events from 'subscribers/events';
import { ISystemUser, IInviteUserInput } from 'interfaces';
import { ISystemUser, IInviteUserInput, IUserInvite } from 'interfaces';
import TenantsManagerService from 'services/Tenancy/TenantsManager';
const ERRORS = {
EMAIL_ALREADY_INVITED: 'EMAIL_ALREADY_INVITED',
INVITE_TOKEN_INVALID: 'INVITE_TOKEN_INVALID',
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS'
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
EMAIL_EXISTS: 'EMAIL_EXISTS'
};
@Service()
export default class InviteUserService {
@@ -66,12 +67,16 @@ export default class InviteUserService {
const user = await systemUserRepository.findOneByEmail(inviteToken.email);
// Sets the invited user details after invite accepting.
const updateUserOper = systemUserRepository.update({
...inviteUserInput,
active: 1,
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
password: hashedPassword,
}, { id: user.id });
const systemUserOper = systemUserRepository.create(
{
...inviteUserInput,
email: inviteToken.email,
tenantId: inviteToken.tenantId,
active: 1,
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
password: hashedPassword,
},
);
this.logger.info('[accept_invite] trying to delete the given token.');
const deleteInviteTokenOper = Invite.query()
@@ -79,14 +84,14 @@ export default class InviteUserService {
.delete();
// Await all async operations.
const [updatedUser] = await Promise.all([
updateUserOper,
const [systemUser] = await Promise.all([
systemUserOper,
deleteInviteTokenOper,
]);
// Triggers `onUserAcceptInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.acceptInvite, {
inviteToken,
user: updatedUser,
user: systemUser,
});
}
@@ -96,21 +101,21 @@ export default class InviteUserService {
* @param {string} email -
* @param {IUser} authorizedUser -
*
* @return {Promise<IInvite>}
* @return {Promise<IUserInvite>}
*/
public async sendInvite(
tenantId: number,
email: string,
authorizedUser: ISystemUser
): Promise<{
invite: IInvite,
user: ISystemUser
invite: IUserInvite;
}> {
const { systemUserRepository } = this.sysRepositories;
// Throw error in case user email exists.
await this.throwErrorIfUserEmailExists(email);
// Throws service error in case the user already invited.
await this.throwErrorIfUserInvited(email);
this.logger.info('[send_invite] trying to store invite token.');
const invite = await Invite.query().insert({
email,
@@ -121,16 +126,13 @@ export default class InviteUserService {
this.logger.info(
'[send_invite] trying to store user with email and tenant.'
);
const user = await systemUserRepository.create({
email,
tenant_id: authorizedUser.tenantId,
active: 1,
});
// Triggers `onUserSendInvite` event.
this.eventDispatcher.dispatch(events.inviteUser.sendInvite, {
invite, authorizedUser, tenantId
invite,
authorizedUser,
tenantId,
});
return { invite, user };
return { invite };
}
/**
@@ -140,7 +142,7 @@ export default class InviteUserService {
*/
public async checkInvite(
token: string
): Promise<{ inviteToken: string; orgName: object }> {
): Promise<{ inviteToken: IUserInvite; orgName: object }> {
const inviteToken = await this.getInviteOrThrowError(token);
// Find the tenant that associated to the given token.
@@ -170,14 +172,27 @@ export default class InviteUserService {
*/
private async throwErrorIfUserEmailExists(
email: string
): Promise<ISystemUser> {
): Promise<void> {
const { systemUserRepository } = this.sysRepositories;
const foundUser = await systemUserRepository.findOneByEmail(email);
if (foundUser) {
throw new ServiceError(ERRORS.EMAIL_EXISTS);
}
}
/**
* Throws service error if the user already invited.
* @param {string} email -
*/
private async throwErrorIfUserInvited(
email: string,
): Promise<void> {
const inviteToken = await Invite.query().findOne('email', email);
if (inviteToken) {
throw new ServiceError(ERRORS.EMAIL_ALREADY_INVITED);
}
return foundUser;
}
/**
@@ -186,7 +201,7 @@ export default class InviteUserService {
* @throws {ServiceError}
* @returns {Invite}
*/
private async getInviteOrThrowError(token: string) {
private async getInviteOrThrowError(token: string): Promise<IUserInvite> {
const inviteToken = await Invite.query().findOne('token', token);
if (!inviteToken) {

View File

@@ -1,9 +1,17 @@
import { Inject, Service } from 'typedi';
import TenancyService from 'services/Tenancy/TenancyService';
import { SystemUser } from 'system/models';
import { Inject, Service } from 'typedi';
import { ServiceError, ServiceErrors } from 'exceptions';
import { ISystemUser, ISystemUserDTO } from 'interfaces';
import systemRepositories from 'loaders/systemRepositories';
const ERRORS = {
CANNOT_DELETE_LAST_USER: 'CANNOT_DELETE_LAST_USER',
USER_ALREADY_ACTIVE: 'USER_ALREADY_ACTIVE',
USER_ALREADY_INACTIVE: 'USER_ALREADY_INACTIVE',
EMAIL_ALREADY_EXISTS: 'EMAIL_ALREADY_EXISTS',
PHONE_NUMBER_ALREADY_EXIST: 'PHONE_NUMBER_ALREADY_EXIST',
USER_NOT_FOUND: 'USER_NOT_FOUND',
USER_SAME_THE_AUTHORIZED_USER: 'USER_SAME_THE_AUTHORIZED_USER',
};
@Service()
export default class UsersService {
@@ -23,7 +31,7 @@ export default class UsersService {
* @param {IUserDTO} userDTO
* @return {Promise<ISystemUser>}
*/
async editUser(
public async editUser(
tenantId: number,
userId: number,
userDTO: ISystemUserDTO
@@ -36,49 +44,24 @@ export default class UsersService {
});
const userByPhoneNumber = await systemUserRepository.findOne({
phoneNumber: userDTO.phoneNumber,
id: userId
id: userId,
});
const serviceErrors: ServiceError[] = [];
if (userByEmail) {
serviceErrors.push(new ServiceError('email_already_exists'));
serviceErrors.push(new ServiceError(ERRORS.EMAIL_ALREADY_EXISTS));
}
if (userByPhoneNumber) {
serviceErrors.push(new ServiceError('phone_number_already_exist'));
serviceErrors.push(new ServiceError(ERRORS.PHONE_NUMBER_ALREADY_EXIST));
}
if (serviceErrors.length > 0) {
throw new ServiceErrors(serviceErrors);
}
const updateSystemUser = await systemUserRepository
.update({ ...userDTO, }, { id: userId });
return updateSystemUser;
}
/**
* Validate user existance throw error in case user was not found.,
* @param {number} tenantId -
* @param {number} userId -
* @returns {ISystemUser}
*/
async getUserOrThrowError(
tenantId: number,
userId: number
): Promise<ISystemUser> {
const { systemUserRepository } = this.repositories;
const user = await systemUserRepository.findOneByIdAndTenant(
userId,
tenantId
const updateSystemUser = await systemUserRepository.update(
{ ...userDTO },
{ id: userId }
);
if (!user) {
this.logger.info('[users] the given user not found.', {
tenantId,
userId,
});
throw new ServiceError('user_not_found');
}
return user;
return updateSystemUser;
}
/**
@@ -86,14 +69,20 @@ export default class UsersService {
* @param {number} tenantId
* @param {number} userId
*/
async deleteUser(tenantId: number, userId: number): Promise<void> {
public async deleteUser(tenantId: number, userId: number): Promise<void> {
const { systemUserRepository } = this.repositories;
await this.getUserOrThrowError(tenantId, userId);
// Retrieve user details or throw not found service error.
const oldUser = await this.getUserOrThrowError(tenantId, userId);
this.logger.info('[users] trying to delete the given user.', {
tenantId,
userId,
});
// Validate the delete user should not be the last user.
await this.validateNotLastUserDelete(tenantId);
// Delete user from the storage.
await systemUserRepository.deleteById(userId);
this.logger.info('[users] the given user deleted successfully.', {
@@ -104,18 +93,24 @@ export default class UsersService {
/**
* Activate the given user id.
* @param {number} tenantId
* @param {number} userId
* @param {number} tenantId - Tenant id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
async activateUser(
public async activateUser(
tenantId: number,
userId: number,
authorizedUser: ISystemUser
): Promise<void> {
this.throwErrorIfUserIdSameAuthorizedUser(userId, authorizedUser);
const { systemUserRepository } = this.repositories;
// Throw service error if the given user is equals the authorized user.
this.throwErrorIfUserSameAuthorizedUser(userId, authorizedUser);
// Retrieve the user or throw not found service error.
const user = await this.getUserOrThrowError(tenantId, userId);
// Throw serivce error if the user is already activated.
this.throwErrorIfUserActive(user);
await systemUserRepository.activateUser(userId);
@@ -127,15 +122,20 @@ export default class UsersService {
* @param {number} userId
* @return {Promise<void>}
*/
async inactivateUser(
public async inactivateUser(
tenantId: number,
userId: number,
authorizedUser: ISystemUser
): Promise<void> {
this.throwErrorIfUserIdSameAuthorizedUser(userId, authorizedUser);
const { systemUserRepository } = this.repositories;
// Throw service error if the given user is equals the authorized user.
this.throwErrorIfUserSameAuthorizedUser(userId, authorizedUser);
// Retrieve the user or throw not found service error.
const user = await this.getUserOrThrowError(tenantId, userId);
// Throw serivce error if the user is already inactivated.
this.throwErrorIfUserInactive(user);
await systemUserRepository.inactivateById(userId);
@@ -146,10 +146,10 @@ export default class UsersService {
* @param {number} tenantId
* @param {object} filter
*/
async getList(tenantId: number) {
const users = await SystemUser.query()
.whereNotDeleted()
.where('tenant_id', tenantId);
public async getList(tenantId: number) {
const { systemUserRepository } = this.repositories;
const users = await systemUserRepository.find({ tenantId });
return users;
}
@@ -159,18 +159,58 @@ export default class UsersService {
* @param {number} tenantId - Tenant id.
* @param {number} userId - User id.
*/
async getUser(tenantId: number, userId: number) {
public async getUser(tenantId: number, userId: number) {
return this.getUserOrThrowError(tenantId, userId);
}
/**
* Validate user existance throw error in case user was not found.,
* @param {number} tenantId -
* @param {number} userId -
* @returns {ISystemUser}
*/
async getUserOrThrowError(
tenantId: number,
userId: number
): Promise<ISystemUser> {
const { systemUserRepository } = this.repositories;
const user = await systemUserRepository.findOneByIdAndTenant(
userId,
tenantId
);
if (!user) {
this.logger.info('[users] the given user not found.', {
tenantId,
userId,
});
throw new ServiceError(ERRORS.USER_NOT_FOUND);
}
return user;
}
/**
* Validate the delete user should not be the last user.
* @param {number} tenantId
*/
private async validateNotLastUserDelete(tenantId: number) {
const { systemUserRepository } = this.repositories;
const usersFound = await systemUserRepository.find({ tenantId });
if (usersFound.length === 1) {
throw new ServiceError(ERRORS.CANNOT_DELETE_LAST_USER);
}
}
/**
* Throws service error in case the user was already active.
* @param {ISystemUser} user
* @throws {ServiceError}
*/
throwErrorIfUserActive(user: ISystemUser) {
private throwErrorIfUserActive(user: ISystemUser) {
if (user.active) {
throw new ServiceError('user_already_active');
throw new ServiceError(ERRORS.USER_ALREADY_ACTIVE);
}
}
@@ -179,9 +219,9 @@ export default class UsersService {
* @param {ISystemUser} user
* @throws {ServiceError}
*/
throwErrorIfUserInactive(user: ISystemUser) {
private throwErrorIfUserInactive(user: ISystemUser) {
if (!user.active) {
throw new ServiceError('user_already_inactive');
throw new ServiceError(ERRORS.USER_ALREADY_INACTIVE);
}
}
@@ -190,12 +230,12 @@ export default class UsersService {
* @param {number} userId
* @param {ISystemUser} authorizedUser
*/
throwErrorIfUserIdSameAuthorizedUser(
private throwErrorIfUserSameAuthorizedUser(
userId: number,
authorizedUser: ISystemUser
) {
if (userId === authorizedUser.id) {
throw new ServiceError('user_same_the_authorized_user');
throw new ServiceError(ERRORS.USER_SAME_THE_AUTHORIZED_USER);
}
}
}

View File

@@ -59,7 +59,6 @@ export default class SaleInvoiceSubscriber {
);
}
/**
* Handles customer balance decrement once sale invoice deleted.
*/

View File

@@ -4,8 +4,8 @@ exports.up = function (knex) {
table.increments();
table.string('first_name');
table.string('last_name');
table.string('email').unique().index();
table.string('phone_number').unique().index();
table.string('email').index();
table.string('phone_number').index();
table.string('password');
table.boolean('active').index();
table.string('language');

View File

@@ -1,14 +1,9 @@
import { Model, mixin } from 'objection';
import { Model } from 'objection';
import bcrypt from 'bcryptjs';
import SoftDelete from 'objection-soft-delete';
import SystemModel from 'system/models/SystemModel';
import moment from 'moment';
import SoftDeleteQueryBuilder from 'collection/SoftDeleteQueryBuilder';
export default class SystemUser extends mixin(SystemModel, [SoftDelete({
columnName: 'deleted_at',
deletedValue: moment().format('YYYY-MM-DD HH:mm:ss'),
notDeletedValue: null,
})]) {
export default class SystemUser extends SystemModel {
/**
* Table name.
*/
@@ -16,6 +11,13 @@ export default class SystemUser extends mixin(SystemModel, [SoftDelete({
return 'users';
}
/**
* Soft delete query builder.
*/
static get QueryBuilder() {
return SoftDeleteQueryBuilder;
}
/**
* Timestamps columns.
*/
@@ -23,10 +25,16 @@ export default class SystemUser extends mixin(SystemModel, [SoftDelete({
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['fullName'];
}
/**
* Full name attribute.
*/
get fullName() {
return (this.firstName + ' ' + this.lastName).trim();
}

View File

@@ -22,7 +22,6 @@ export default class SystemUserRepository extends SystemRepository {
return this.cache.get(cacheKey, () => {
return this.model.query()
.whereNotDeleted()
.findOne('email', crediential)
.orWhere('phone_number', crediential);
});
@@ -39,7 +38,6 @@ export default class SystemUserRepository extends SystemRepository {
return this.cache.get(cacheKey, () => {
return this.model.query()
.whereNotDeleted()
.findOne({ id: userId, tenant_id: tenantId });
});
}
@@ -53,7 +51,7 @@ export default class SystemUserRepository extends SystemRepository {
const cacheKey = this.getCacheKey('findOneByEmail', email);
return this.cache.get(cacheKey, () => {
return this.model.query().whereNotDeleted().findOne('email', email);
return this.model.query().findOne('email', email);
});
}
@@ -67,7 +65,6 @@ export default class SystemUserRepository extends SystemRepository {
return this.cache.get(cacheKey, () => {
return this.model.query()
.whereNotDeleted()
.findOne('phoneNumber', phoneNumber);
});
}

View File

@@ -278,6 +278,15 @@ function defaultToTransform(value, defaultOrTransformedValue, defaultValue) {
: _transfromedValue;
}
const transformToMap = (objects, key) => {
const map = new Map();
objects.forEach(object => {
map.set(object[key], object);
});
return map;
}
export {
hashPassword,
origin,
@@ -299,4 +308,5 @@ export {
formatNumber,
isBlank,
defaultToTransform,
transformToMap
};