mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 07:10:33 +00:00
feat: Receivable and payable aging summary financial statement.
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import {
|
import { Route, Switch } from 'react-router-dom';
|
||||||
Route,
|
|
||||||
Switch,
|
|
||||||
} from 'react-router-dom';
|
|
||||||
import { Alert, Intent } from '@blueprintjs/core';
|
import { Alert, Intent } from '@blueprintjs/core';
|
||||||
import { useQuery } from 'react-query'
|
import { useQuery } from 'react-query';
|
||||||
import { FormattedMessage as T, FormattedHTMLMessage, useIntl } from 'react-intl';
|
import {
|
||||||
|
FormattedMessage as T,
|
||||||
|
FormattedHTMLMessage,
|
||||||
|
useIntl,
|
||||||
|
} from 'react-intl';
|
||||||
|
|
||||||
import AppToaster from 'components/AppToaster';
|
import AppToaster from 'components/AppToaster';
|
||||||
|
|
||||||
@@ -24,7 +25,6 @@ import withAccounts from 'containers/Accounts/withAccounts';
|
|||||||
|
|
||||||
import { compose } from 'utils';
|
import { compose } from 'utils';
|
||||||
|
|
||||||
|
|
||||||
function AccountsChart({
|
function AccountsChart({
|
||||||
// #withDashboardActions
|
// #withDashboardActions
|
||||||
changePageTitle,
|
changePageTitle,
|
||||||
@@ -57,8 +57,8 @@ function AccountsChart({
|
|||||||
const [activateAccount, setActivateAccount] = useState(false);
|
const [activateAccount, setActivateAccount] = useState(false);
|
||||||
const [bulkDelete, setBulkDelete] = useState(false);
|
const [bulkDelete, setBulkDelete] = useState(false);
|
||||||
const [selectedRows, setSelectedRows] = useState([]);
|
const [selectedRows, setSelectedRows] = useState([]);
|
||||||
const [bulkActivate,setBulkActivate] =useState(false);
|
const [bulkActivate, setBulkActivate] = useState(false);
|
||||||
const [bulkInactiveAccounts,setBulkInactiveAccounts] =useState(false)
|
const [bulkInactiveAccounts, setBulkInactiveAccounts] = useState(false);
|
||||||
const [tableLoading, setTableLoading] = useState(false);
|
const [tableLoading, setTableLoading] = useState(false);
|
||||||
|
|
||||||
// Fetch accounts resource views and fields.
|
// Fetch accounts resource views and fields.
|
||||||
@@ -77,7 +77,7 @@ function AccountsChart({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
changePageTitle(formatMessage({ id: 'chart_of_accounts' }));
|
changePageTitle(formatMessage({ id: 'chart_of_accounts' }));
|
||||||
}, [changePageTitle,formatMessage]);
|
}, [changePageTitle, formatMessage]);
|
||||||
|
|
||||||
// Handle click and cancel/confirm account delete
|
// Handle click and cancel/confirm account delete
|
||||||
const handleDeleteAccount = (account) => {
|
const handleDeleteAccount = (account) => {
|
||||||
@@ -85,7 +85,9 @@ function AccountsChart({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// handle cancel delete account alert.
|
// handle cancel delete account alert.
|
||||||
const handleCancelAccountDelete = useCallback(() => { setDeleteAccount(false); }, []);
|
const handleCancelAccountDelete = useCallback(() => {
|
||||||
|
setDeleteAccount(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDeleteErrors = (errors) => {
|
const handleDeleteErrors = (errors) => {
|
||||||
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) {
|
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) {
|
||||||
@@ -98,24 +100,30 @@ function AccountsChart({
|
|||||||
}
|
}
|
||||||
if (errors.find((e) => e.type === 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS')) {
|
if (errors.find((e) => e.type === 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS')) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: formatMessage({id:'cannot_delete_account_has_associated_transactions'})
|
message: formatMessage({
|
||||||
|
id: 'cannot_delete_account_has_associated_transactions',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Handle confirm account delete
|
// Handle confirm account delete
|
||||||
const handleConfirmAccountDelete = useCallback(() => {
|
const handleConfirmAccountDelete = useCallback(() => {
|
||||||
requestDeleteAccount(deleteAccount.id).then(() => {
|
requestDeleteAccount(deleteAccount.id)
|
||||||
setDeleteAccount(false);
|
.then(() => {
|
||||||
AppToaster.show({
|
setDeleteAccount(false);
|
||||||
message: formatMessage({ id: 'the_account_has_been_successfully_deleted' }),
|
AppToaster.show({
|
||||||
intent: Intent.SUCCESS,
|
message: formatMessage({
|
||||||
|
id: 'the_account_has_been_successfully_deleted',
|
||||||
|
}),
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((errors) => {
|
||||||
|
setDeleteAccount(false);
|
||||||
|
handleDeleteErrors(errors);
|
||||||
});
|
});
|
||||||
}).catch(errors => {
|
}, [deleteAccount, requestDeleteAccount, formatMessage]);
|
||||||
setDeleteAccount(false);
|
|
||||||
handleDeleteErrors(errors);
|
|
||||||
});
|
|
||||||
}, [deleteAccount, requestDeleteAccount,formatMessage]);
|
|
||||||
|
|
||||||
// Handle cancel/confirm account inactive.
|
// Handle cancel/confirm account inactive.
|
||||||
const handleInactiveAccount = useCallback((account) => {
|
const handleInactiveAccount = useCallback((account) => {
|
||||||
@@ -138,7 +146,7 @@ function AccountsChart({
|
|||||||
intent: Intent.SUCCESS,
|
intent: Intent.SUCCESS,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [inactiveAccount, requestInactiveAccount,formatMessage]);
|
}, [inactiveAccount, requestInactiveAccount, formatMessage]);
|
||||||
|
|
||||||
// Handle activate account click.
|
// Handle activate account click.
|
||||||
const handleActivateAccount = useCallback((account) => {
|
const handleActivateAccount = useCallback((account) => {
|
||||||
@@ -166,23 +174,30 @@ function AccountsChart({
|
|||||||
const handleRestoreAccount = (account) => {};
|
const handleRestoreAccount = (account) => {};
|
||||||
|
|
||||||
// Handle accounts bulk delete button click.,
|
// Handle accounts bulk delete button click.,
|
||||||
const handleBulkDelete = useCallback((accountsIds) => {
|
const handleBulkDelete = useCallback(
|
||||||
setBulkDelete(accountsIds);
|
(accountsIds) => {
|
||||||
}, [setBulkDelete]);
|
setBulkDelete(accountsIds);
|
||||||
|
},
|
||||||
|
[setBulkDelete],
|
||||||
|
);
|
||||||
|
|
||||||
// Handle confirm accounts bulk delete.
|
// Handle confirm accounts bulk delete.
|
||||||
const handleConfirmBulkDelete = useCallback(() => {
|
const handleConfirmBulkDelete = useCallback(() => {
|
||||||
requestDeleteBulkAccounts(bulkDelete).then(() => {
|
requestDeleteBulkAccounts(bulkDelete)
|
||||||
setBulkDelete(false);
|
.then(() => {
|
||||||
AppToaster.show({
|
setBulkDelete(false);
|
||||||
message: formatMessage({ id: 'the_accounts_has_been_successfully_deleted' }),
|
AppToaster.show({
|
||||||
intent: Intent.SUCCESS,
|
message: formatMessage({
|
||||||
|
id: 'the_accounts_has_been_successfully_deleted',
|
||||||
|
}),
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((errors) => {
|
||||||
|
setBulkDelete(false);
|
||||||
|
handleDeleteErrors(errors);
|
||||||
});
|
});
|
||||||
}).catch((errors) => {
|
}, [requestDeleteBulkAccounts, bulkDelete, formatMessage]);
|
||||||
setBulkDelete(false);
|
|
||||||
handleDeleteErrors(errors);
|
|
||||||
});
|
|
||||||
}, [requestDeleteBulkAccounts, bulkDelete,formatMessage]);
|
|
||||||
|
|
||||||
// Handle cancel accounts bulk delete.
|
// Handle cancel accounts bulk delete.
|
||||||
const handleCancelBulkDelete = useCallback(() => {
|
const handleCancelBulkDelete = useCallback(() => {
|
||||||
@@ -191,16 +206,14 @@ function AccountsChart({
|
|||||||
|
|
||||||
const handleBulkArchive = useCallback((accounts) => {}, []);
|
const handleBulkArchive = useCallback((accounts) => {}, []);
|
||||||
|
|
||||||
const handleEditAccount = useCallback(() => {
|
const handleEditAccount = useCallback(() => {}, []);
|
||||||
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle selected rows change.
|
// Handle selected rows change.
|
||||||
const handleSelectedRowsChange = useCallback(
|
const handleSelectedRowsChange = useCallback(
|
||||||
(accounts) => {
|
(accounts) => {
|
||||||
setSelectedRows(accounts);
|
setSelectedRows(accounts);
|
||||||
},
|
},
|
||||||
[setSelectedRows]
|
[setSelectedRows],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Refetches accounts data table when current custom view changed.
|
// Refetches accounts data table when current custom view changed.
|
||||||
@@ -232,70 +245,73 @@ function AccountsChart({
|
|||||||
});
|
});
|
||||||
fetchAccountsHook.refetch();
|
fetchAccountsHook.refetch();
|
||||||
},
|
},
|
||||||
[fetchAccountsHook, addAccountsTableQueries]
|
[fetchAccountsHook, addAccountsTableQueries],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculates the data table selected rows count.
|
// Calculates the data table selected rows count.
|
||||||
const selectedRowsCount = useMemo(() => Object.values(selectedRows).length, [selectedRows]);
|
const selectedRowsCount = useMemo(() => Object.values(selectedRows).length, [
|
||||||
|
selectedRows,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Handle bulk Activate accounts button click.,
|
||||||
|
const handleBulkActivate = useCallback(
|
||||||
|
(bulkActivateIds) => {
|
||||||
|
setBulkActivate(bulkActivateIds);
|
||||||
|
},
|
||||||
|
[setBulkActivate],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle cancel Bulk Activate accounts bulk delete.
|
||||||
// Handle bulk Activate accounts button click.,
|
|
||||||
const handleBulkActivate = useCallback((bulkActivateIds) => {
|
|
||||||
setBulkActivate(bulkActivateIds);
|
|
||||||
}, [setBulkActivate]);
|
|
||||||
|
|
||||||
|
|
||||||
// Handle cancel Bulk Activate accounts bulk delete.
|
|
||||||
const handleCancelBulkActivate = useCallback(() => {
|
const handleCancelBulkActivate = useCallback(() => {
|
||||||
setBulkActivate(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle Bulk activate account confirm.
|
|
||||||
const handleConfirmBulkActivate = useCallback(() => {
|
|
||||||
requestBulkActivateAccounts(bulkActivate).then(() => {
|
|
||||||
setBulkActivate(false);
|
|
||||||
AppToaster.show({
|
|
||||||
message: formatMessage({ id: 'the_accounts_has_been_successfully_activated' }),
|
|
||||||
intent: Intent.SUCCESS,
|
|
||||||
});
|
|
||||||
}).catch((errors) => {
|
|
||||||
setBulkActivate(false);
|
setBulkActivate(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
});
|
// Handle Bulk activate account confirm.
|
||||||
}, [requestBulkActivateAccounts, bulkActivate,formatMessage]);
|
const handleConfirmBulkActivate = useCallback(() => {
|
||||||
|
requestBulkActivateAccounts(bulkActivate)
|
||||||
|
.then(() => {
|
||||||
|
setBulkActivate(false);
|
||||||
// Handle bulk Inactive accounts button click.,
|
AppToaster.show({
|
||||||
const handleBulkInactive = useCallback((bulkInactiveIds) => {
|
message: formatMessage({
|
||||||
setBulkInactiveAccounts(bulkInactiveIds);
|
id: 'the_accounts_has_been_successfully_activated',
|
||||||
}, [setBulkInactiveAccounts]);
|
}),
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((errors) => {
|
||||||
|
setBulkActivate(false);
|
||||||
|
});
|
||||||
|
}, [requestBulkActivateAccounts, bulkActivate, formatMessage]);
|
||||||
|
|
||||||
|
// Handle bulk Inactive accounts button click.,
|
||||||
|
const handleBulkInactive = useCallback(
|
||||||
|
(bulkInactiveIds) => {
|
||||||
|
setBulkInactiveAccounts(bulkInactiveIds);
|
||||||
|
},
|
||||||
|
[setBulkInactiveAccounts],
|
||||||
|
);
|
||||||
|
|
||||||
// Handle cancel Bulk Inactive accounts bulk delete.
|
// Handle cancel Bulk Inactive accounts bulk delete.
|
||||||
const handleCancelBulkInactive = useCallback(() => {
|
const handleCancelBulkInactive = useCallback(() => {
|
||||||
setBulkInactiveAccounts(false);
|
setBulkInactiveAccounts(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Handle Bulk Inactive accounts confirm.
|
// Handle Bulk Inactive accounts confirm.
|
||||||
const handleConfirmBulkInactive = useCallback(() => {
|
const handleConfirmBulkInactive = useCallback(() => {
|
||||||
requestBulkInactiveAccounts(bulkInactiveAccounts).then(() => {
|
requestBulkInactiveAccounts(bulkInactiveAccounts)
|
||||||
setBulkInactiveAccounts(false);
|
.then(() => {
|
||||||
AppToaster.show({
|
setBulkInactiveAccounts(false);
|
||||||
message: formatMessage({ id: 'the_accounts_has_been_successfully_inactivated' }),
|
AppToaster.show({
|
||||||
intent: Intent.SUCCESS,
|
message: formatMessage({
|
||||||
});
|
id: 'the_accounts_has_been_successfully_inactivated',
|
||||||
}).catch((errors) => {
|
}),
|
||||||
setBulkInactiveAccounts(false);
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
});
|
})
|
||||||
}, [requestBulkInactiveAccounts, bulkInactiveAccounts]);
|
.catch((errors) => {
|
||||||
|
setBulkInactiveAccounts(false);
|
||||||
|
});
|
||||||
|
}, [requestBulkInactiveAccounts, bulkInactiveAccounts]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardInsider loading={fetchHook.isFetching} name={'accounts-chart'}>
|
<DashboardInsider loading={fetchHook.isFetching} name={'accounts-chart'}>
|
||||||
@@ -312,10 +328,7 @@ const handleConfirmBulkActivate = useCallback(() => {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
exact={true}
|
exact={true}
|
||||||
path={[
|
path={['/accounts/:custom_view_id/custom_view', '/accounts']}
|
||||||
'/accounts/:custom_view_id/custom_view',
|
|
||||||
'/accounts',
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<AccountsViewsTabs onViewChanged={handleViewChanged} />
|
<AccountsViewsTabs onViewChanged={handleViewChanged} />
|
||||||
|
|
||||||
@@ -343,7 +356,8 @@ const handleConfirmBulkActivate = useCallback(() => {
|
|||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<FormattedHTMLMessage
|
<FormattedHTMLMessage
|
||||||
id={'once_delete_this_account_you_will_able_to_restore_it'} />
|
id={'once_delete_this_account_you_will_able_to_restore_it'}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
@@ -366,15 +380,18 @@ const handleConfirmBulkActivate = useCallback(() => {
|
|||||||
intent={Intent.WARNING}
|
intent={Intent.WARNING}
|
||||||
isOpen={activateAccount}
|
isOpen={activateAccount}
|
||||||
onCancel={handleCancelActivateAccount}
|
onCancel={handleCancelActivateAccount}
|
||||||
onConfirm={handleConfirmAccountActivate}>
|
onConfirm={handleConfirmAccountActivate}
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
<T id={'are_sure_to_activate_this_account'} />
|
<T id={'are_sure_to_activate_this_account'} />
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Alert
|
<Alert
|
||||||
cancelButtonText={<T id={'cancel'}/>}
|
cancelButtonText={<T id={'cancel'} />}
|
||||||
confirmButtonText={`${formatMessage({id:'delete'})} (${selectedRowsCount})`}
|
confirmButtonText={`${formatMessage({
|
||||||
|
id: 'delete',
|
||||||
|
})} (${selectedRowsCount})`}
|
||||||
icon="trash"
|
icon="trash"
|
||||||
intent={Intent.DANGER}
|
intent={Intent.DANGER}
|
||||||
isOpen={bulkDelete}
|
isOpen={bulkDelete}
|
||||||
@@ -382,28 +399,36 @@ const handleConfirmBulkActivate = useCallback(() => {
|
|||||||
onConfirm={handleConfirmBulkDelete}
|
onConfirm={handleConfirmBulkDelete}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<T id={'once_delete_these_accounts_you_will_not_able_restore_them'} />
|
<T
|
||||||
|
id={'once_delete_these_accounts_you_will_not_able_restore_them'}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Alert
|
<Alert
|
||||||
cancelButtonText={<T id={'cancel'} />}
|
cancelButtonText={<T id={'cancel'} />}
|
||||||
confirmButtonText={`${formatMessage({id:'activate'})} (${selectedRowsCount})`}
|
confirmButtonText={`${formatMessage({
|
||||||
|
id: 'activate',
|
||||||
|
})} (${selectedRowsCount})`}
|
||||||
intent={Intent.WARNING}
|
intent={Intent.WARNING}
|
||||||
isOpen={bulkActivate}
|
isOpen={bulkActivate}
|
||||||
onCancel={handleCancelBulkActivate}
|
onCancel={handleCancelBulkActivate}
|
||||||
onConfirm={handleConfirmBulkActivate}>
|
onConfirm={handleConfirmBulkActivate}
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
<T id={'are_sure_to_activate_this_accounts'} />
|
<T id={'are_sure_to_activate_this_accounts'} />
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
<Alert
|
<Alert
|
||||||
cancelButtonText={<T id={'cancel'} />}
|
cancelButtonText={<T id={'cancel'} />}
|
||||||
confirmButtonText={`${formatMessage({id:'inactivate'})} (${selectedRowsCount})`}
|
confirmButtonText={`${formatMessage({
|
||||||
|
id: 'inactivate',
|
||||||
|
})} (${selectedRowsCount})`}
|
||||||
intent={Intent.WARNING}
|
intent={Intent.WARNING}
|
||||||
isOpen={bulkInactiveAccounts}
|
isOpen={bulkInactiveAccounts}
|
||||||
onCancel={handleCancelBulkInactive}
|
onCancel={handleCancelBulkInactive}
|
||||||
onConfirm={handleConfirmBulkInactive}>
|
onConfirm={handleConfirmBulkInactive}
|
||||||
|
>
|
||||||
<p>
|
<p>
|
||||||
<T id={'are_sure_to_inactive_this_accounts'} />
|
<T id={'are_sure_to_inactive_this_accounts'} />
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ exports.up = (knex) => {
|
|||||||
return knex.schema.createTable('account_types', (table) => {
|
return knex.schema.createTable('account_types', (table) => {
|
||||||
table.increments();
|
table.increments();
|
||||||
table.string('name');
|
table.string('name');
|
||||||
|
table.string('key');
|
||||||
table.string('normal');
|
table.string('normal');
|
||||||
table.string('root_type');
|
table.string('root_type');
|
||||||
table.boolean('balance_sheet');
|
table.boolean('balance_sheet');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ exports.up = function (knex) {
|
|||||||
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
table.integer('resource_id').unsigned().references('id').inTable('resources');
|
||||||
table.boolean('favourite');
|
table.boolean('favourite');
|
||||||
table.string('roles_logic_expression');
|
table.string('roles_logic_expression');
|
||||||
|
table.timestamps();
|
||||||
}).raw('ALTER TABLE `VIEWS` AUTO_INCREMENT = 1000').then(() => {
|
}).raw('ALTER TABLE `VIEWS` AUTO_INCREMENT = 1000').then(() => {
|
||||||
return knex.seed.run({
|
return knex.seed.run({
|
||||||
specific: 'seed_views.js',
|
specific: 'seed_views.js',
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ exports.up = function(knex) {
|
|||||||
table.string('reference_type');
|
table.string('reference_type');
|
||||||
table.integer('reference_id');
|
table.integer('reference_id');
|
||||||
table.integer('account_id').unsigned();
|
table.integer('account_id').unsigned();
|
||||||
|
table.string('contact_type').nullable();
|
||||||
|
table.integer('contact_id').unsigned().nullable();
|
||||||
table.string('note');
|
table.string('note');
|
||||||
table.boolean('draft').defaultTo(false);
|
table.boolean('draft').defaultTo(false);
|
||||||
table.integer('user_id').unsigned();
|
table.integer('user_id').unsigned();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ exports.up = function(knex) {
|
|||||||
table.increments();
|
table.increments();
|
||||||
table.string('currency_name');
|
table.string('currency_name');
|
||||||
table.string('currency_code', 4);
|
table.string('currency_code', 4);
|
||||||
|
table.timestamps();
|
||||||
}).raw('ALTER TABLE `CURRENCIES` AUTO_INCREMENT = 1000');
|
}).raw('ALTER TABLE `CURRENCIES` AUTO_INCREMENT = 1000');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ exports.up = function(knex) {
|
|||||||
table.string('currency_code', 4);
|
table.string('currency_code', 4);
|
||||||
table.decimal('exchange_rate');
|
table.decimal('exchange_rate');
|
||||||
table.date('date');
|
table.date('date');
|
||||||
|
table.timestamps();
|
||||||
}).raw('ALTER TABLE `EXCHANGE_RATES` AUTO_INCREMENT = 1000');
|
}).raw('ALTER TABLE `EXCHANGE_RATES` AUTO_INCREMENT = 1000');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ exports.up = function(knex) {
|
|||||||
|
|
||||||
table.text('note');
|
table.text('note');
|
||||||
table.boolean('active').defaultTo(true);
|
table.boolean('active').defaultTo(true);
|
||||||
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ exports.up = function(knex) {
|
|||||||
|
|
||||||
table.text('note');
|
table.text('note');
|
||||||
table.boolean('active').defaultTo(true);
|
table.boolean('active').defaultTo(true);
|
||||||
|
|
||||||
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ exports.seed = (knex) => {
|
|||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Fixed Asset',
|
name: 'Fixed Asset',
|
||||||
|
key: 'fixed_asset',
|
||||||
normal: 'debit',
|
normal: 'debit',
|
||||||
root_type: 'asset',
|
root_type: 'asset',
|
||||||
balance_sheet: true,
|
balance_sheet: true,
|
||||||
@@ -16,6 +17,7 @@ exports.seed = (knex) => {
|
|||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Current Asset',
|
name: 'Current Asset',
|
||||||
|
key: 'current_asset',
|
||||||
normal: 'debit',
|
normal: 'debit',
|
||||||
root_type: 'asset',
|
root_type: 'asset',
|
||||||
balance_sheet: true,
|
balance_sheet: true,
|
||||||
@@ -24,6 +26,7 @@ exports.seed = (knex) => {
|
|||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
name: 'Long Term Liability',
|
name: 'Long Term Liability',
|
||||||
|
key: 'long_term_liability',
|
||||||
normal: 'credit',
|
normal: 'credit',
|
||||||
root_type: 'liability',
|
root_type: 'liability',
|
||||||
balance_sheet: false,
|
balance_sheet: false,
|
||||||
@@ -32,6 +35,7 @@ exports.seed = (knex) => {
|
|||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
name: 'Current Liability',
|
name: 'Current Liability',
|
||||||
|
key: 'current_liability',
|
||||||
normal: 'credit',
|
normal: 'credit',
|
||||||
root_type: 'liability',
|
root_type: 'liability',
|
||||||
balance_sheet: false,
|
balance_sheet: false,
|
||||||
@@ -40,6 +44,7 @@ exports.seed = (knex) => {
|
|||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
name: 'Equity',
|
name: 'Equity',
|
||||||
|
key: 'equity',
|
||||||
normal: 'credit',
|
normal: 'credit',
|
||||||
root_type: 'equity',
|
root_type: 'equity',
|
||||||
balance_sheet: true,
|
balance_sheet: true,
|
||||||
@@ -48,6 +53,7 @@ exports.seed = (knex) => {
|
|||||||
{
|
{
|
||||||
id: 6,
|
id: 6,
|
||||||
name: 'Expense',
|
name: 'Expense',
|
||||||
|
key: 'expense',
|
||||||
normal: 'debit',
|
normal: 'debit',
|
||||||
root_type: 'expense',
|
root_type: 'expense',
|
||||||
balance_sheet: false,
|
balance_sheet: false,
|
||||||
@@ -56,6 +62,7 @@ exports.seed = (knex) => {
|
|||||||
{
|
{
|
||||||
id: 7,
|
id: 7,
|
||||||
name: 'Income',
|
name: 'Income',
|
||||||
|
key: 'income',
|
||||||
normal: 'credit',
|
normal: 'credit',
|
||||||
root_type: 'income',
|
root_type: 'income',
|
||||||
balance_sheet: false,
|
balance_sheet: false,
|
||||||
@@ -64,6 +71,7 @@ exports.seed = (knex) => {
|
|||||||
{
|
{
|
||||||
id: 8,
|
id: 8,
|
||||||
name: 'Accounts Receivable',
|
name: 'Accounts Receivable',
|
||||||
|
key: 'accounts_receivable',
|
||||||
normal: 'debit',
|
normal: 'debit',
|
||||||
root_type: 'asset',
|
root_type: 'asset',
|
||||||
balance_sheet: true,
|
balance_sheet: true,
|
||||||
@@ -72,6 +80,7 @@ exports.seed = (knex) => {
|
|||||||
{
|
{
|
||||||
id: 9,
|
id: 9,
|
||||||
name: 'Accounts Payable',
|
name: 'Accounts Payable',
|
||||||
|
key: 'accounts_payable',
|
||||||
normal: 'credit',
|
normal: 'credit',
|
||||||
root_type: 'liability',
|
root_type: 'liability',
|
||||||
balance_sheet: true,
|
balance_sheet: true,
|
||||||
|
|||||||
@@ -103,7 +103,29 @@ exports.seed = (knex) => {
|
|||||||
active: 1,
|
active: 1,
|
||||||
index: 1,
|
index: 1,
|
||||||
predefined: 1,
|
predefined: 1,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
name: 'Accounts Receivable',
|
||||||
|
account_type_id: 8,
|
||||||
|
parent_account_id: null,
|
||||||
|
code: '1000',
|
||||||
|
description: '',
|
||||||
|
active: 1,
|
||||||
|
index: 1,
|
||||||
|
predefined: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
name: 'Accounts Payable',
|
||||||
|
account_type_id: 9,
|
||||||
|
parent_account_id: null,
|
||||||
|
code: '1000',
|
||||||
|
description: '',
|
||||||
|
active: 1,
|
||||||
|
index: 1,
|
||||||
|
predefined: 1,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export default {
|
|||||||
asyncMiddleware(this.manualJournals.handler));
|
asyncMiddleware(this.manualJournals.handler));
|
||||||
|
|
||||||
router.post('/make-journal-entries',
|
router.post('/make-journal-entries',
|
||||||
|
this.validateMediaIds,
|
||||||
|
this.validateContactEntries,
|
||||||
this.makeJournalEntries.validation,
|
this.makeJournalEntries.validation,
|
||||||
asyncMiddleware(this.makeJournalEntries.handler));
|
asyncMiddleware(this.makeJournalEntries.handler));
|
||||||
|
|
||||||
@@ -41,6 +43,8 @@ export default {
|
|||||||
asyncMiddleware(this.publishManualJournal.handler));
|
asyncMiddleware(this.publishManualJournal.handler));
|
||||||
|
|
||||||
router.post('/manual-journals/:id',
|
router.post('/manual-journals/:id',
|
||||||
|
this.validateMediaIds,
|
||||||
|
this.validateContactEntries,
|
||||||
this.editManualJournal.validation,
|
this.editManualJournal.validation,
|
||||||
asyncMiddleware(this.editManualJournal.handler));
|
asyncMiddleware(this.editManualJournal.handler));
|
||||||
|
|
||||||
@@ -168,6 +172,114 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate media ids.
|
||||||
|
* @param {Request} req -
|
||||||
|
* @param {Response} res -
|
||||||
|
* @param {Function} next -
|
||||||
|
*/
|
||||||
|
async validateMediaIds(req, res, next) {
|
||||||
|
const form = { media_ids: [], ...req.body };
|
||||||
|
const { Media } = req.models;
|
||||||
|
const errorReasons = [];
|
||||||
|
|
||||||
|
// Validate if media ids was not already exists on the storage.
|
||||||
|
if (form.media_ids.length > 0) {
|
||||||
|
const storedMedia = await Media.query().whereIn('id', form.media_ids);
|
||||||
|
const notFoundMedia = difference(form.media_ids, storedMedia.map((m) => m.id));
|
||||||
|
|
||||||
|
if (notFoundMedia.length > 0) {
|
||||||
|
errorReasons.push({ type: 'MEDIA.IDS.NOT.FOUND', code: 400, ids: notFoundMedia });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.errorReasons = Array.isArray(req.errorReasons) && req.errorReasons.length
|
||||||
|
? req.errorReasons.push(...errorReasons) : errorReasons;
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate form entries with contact customers and vendors.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {Function} next
|
||||||
|
*/
|
||||||
|
async validateContactEntries(req, res, next) {
|
||||||
|
const form = { entries: [], ...req.body };
|
||||||
|
const { AccountType, Vendor, Customer } = req.models;
|
||||||
|
const errorReasons = [];
|
||||||
|
|
||||||
|
// Validate the entries contact type and ids.
|
||||||
|
const customersContacts = form.entries.filter(e => e.contact_type === 'customer');
|
||||||
|
const vendorsContacts = form.entries.filter(e => e.contact_type === 'vendor');
|
||||||
|
|
||||||
|
const accountsTypes = await AccountType.query();
|
||||||
|
|
||||||
|
const payableAccountsType = accountsTypes.find(t => t.key === 'accounts_payable');;
|
||||||
|
const receivableAccountsType = accountsTypes.find(t => t.key === 'accounts_receivable');
|
||||||
|
|
||||||
|
// Validate customers contacts.
|
||||||
|
if (customersContacts.length > 0) {
|
||||||
|
const customersContactsIds = customersContacts.map(c => c.contact_id);
|
||||||
|
const storedContacts = await Customer.query().whereIn('id', customersContactsIds);
|
||||||
|
|
||||||
|
const storedContactsIds = storedContacts.map(c => c.id);
|
||||||
|
const formEntriesCustomersIds = form.entries.filter(e => e.contact_type === 'customer');
|
||||||
|
|
||||||
|
const notFoundContactsIds = difference(
|
||||||
|
formEntriesCustomersIds.map(c => c.contact_id),
|
||||||
|
storedContactsIds,
|
||||||
|
);
|
||||||
|
if (notFoundContactsIds.length > 0) {
|
||||||
|
errorReasons.push({ type: 'CUSTOMERS.CONTACTS.NOT.FOUND', code: 500, ids: notFoundContactsIds });
|
||||||
|
}
|
||||||
|
|
||||||
|
const notReceivableAccounts = formEntriesCustomersIds.filter(
|
||||||
|
c => receivableAccountsType && c.contact_id !== receivableAccountsType.id);
|
||||||
|
|
||||||
|
if (notReceivableAccounts.length > 0) {
|
||||||
|
errorReasons.push({
|
||||||
|
type: 'CUSTOMERS.ACCOUNTS.NOT.RECEIVABLE.TYPE',
|
||||||
|
code: 700,
|
||||||
|
indexes: notReceivableAccounts.map(a => a.index),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate vendors contacts.
|
||||||
|
if (vendorsContacts.length > 0) {
|
||||||
|
const vendorsContactsIds = vendorsContacts.map(c => c.contact_id);
|
||||||
|
const storedContacts = await Vendor.query().where('id', vendorsContactsIds);
|
||||||
|
|
||||||
|
const storedContactsIds = storedContacts.map(c => c.id);
|
||||||
|
const formEntriesVendorsIds = form.entries.filter(e => e.contact_type === 'vendor');
|
||||||
|
|
||||||
|
const notFoundContactsIds = difference(
|
||||||
|
formEntriesVendorsIds.map(v => v.contact_id),
|
||||||
|
storedContactsIds,
|
||||||
|
);
|
||||||
|
if (notFoundContactsIds.length > 0) {
|
||||||
|
errorReasons.push({
|
||||||
|
type: 'VENDORS.CONTACTS.NOT.FOUND', code: 600, ids: notFoundContactsIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const notPayableAccounts = formEntriesVendorsIds.filter(
|
||||||
|
v => payableAccountsType && v.contact_id === payableAccountsType.id
|
||||||
|
);
|
||||||
|
if (notPayableAccounts.length > 0) {
|
||||||
|
errorReasons.push({
|
||||||
|
type: 'VENDORS.ACCOUNTS.NOT.PAYABLE.TYPE',
|
||||||
|
code: 800,
|
||||||
|
indexes: notPayableAccounts.map(a => a.index),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.errorReasons = Array.isArray(req.errorReasons) && req.errorReasons.length
|
||||||
|
? req.errorReasons.push(...errorReasons) : errorReasons;
|
||||||
|
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make journal entrires.
|
* Make journal entrires.
|
||||||
*/
|
*/
|
||||||
@@ -180,10 +292,13 @@ export default {
|
|||||||
check('description').optional().trim().escape(),
|
check('description').optional().trim().escape(),
|
||||||
check('status').optional().isBoolean().toBoolean(),
|
check('status').optional().isBoolean().toBoolean(),
|
||||||
check('entries').isArray({ min: 2 }),
|
check('entries').isArray({ min: 2 }),
|
||||||
|
check('entries.*.index').exists().isNumeric().toInt(),
|
||||||
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
|
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('entries.*.account_id').isNumeric().toInt(),
|
check('entries.*.account_id').isNumeric().toInt(),
|
||||||
check('entries.*.note').optional(),
|
check('entries.*.note').optional(),
|
||||||
|
check('entries.*.contact_id').optional().isNumeric().toInt(),
|
||||||
|
check('entries.*.contact_type').optional().isIn(['vendor', 'customer']),
|
||||||
check('media_ids').optional().isArray(),
|
check('media_ids').optional().isArray(),
|
||||||
check('media_ids.*').exists().isNumeric().toInt(),
|
check('media_ids.*').exists().isNumeric().toInt(),
|
||||||
],
|
],
|
||||||
@@ -202,13 +317,17 @@ export default {
|
|||||||
media_ids: [],
|
media_ids: [],
|
||||||
...req.body,
|
...req.body,
|
||||||
};
|
};
|
||||||
const { ManualJournal, Account, Media, MediaLink } = req.models;
|
const {
|
||||||
|
ManualJournal,
|
||||||
|
Account,
|
||||||
|
MediaLink,
|
||||||
|
} = req.models;
|
||||||
|
|
||||||
let totalCredit = 0;
|
let totalCredit = 0;
|
||||||
let totalDebit = 0;
|
let totalDebit = 0;
|
||||||
|
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
const errorReasons = [];
|
const errorReasons = [...(req.errorReasons || [])];
|
||||||
const entries = form.entries.filter((entry) => (entry.credit || entry.debit));
|
const entries = form.entries.filter((entry) => (entry.credit || entry.debit));
|
||||||
const formattedDate = moment(form.date).format('YYYY-MM-DD');
|
const formattedDate = moment(form.date).format('YYYY-MM-DD');
|
||||||
|
|
||||||
@@ -229,23 +348,18 @@ export default {
|
|||||||
if (totalCredit !== totalDebit) {
|
if (totalCredit !== totalDebit) {
|
||||||
errorReasons.push({ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 100 });
|
errorReasons.push({ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 100 });
|
||||||
}
|
}
|
||||||
const accountsIds = entries.map((entry) => entry.account_id);
|
const formEntriesAccountsIds = entries.map((entry) => entry.account_id);
|
||||||
|
const formEntriesContactsIds = entries.map((entry) => entry.contact_id);
|
||||||
|
|
||||||
const accounts = await Account.query()
|
const accounts = await Account.query()
|
||||||
.whereIn('id', accountsIds)
|
.whereIn('id', formEntriesAccountsIds)
|
||||||
.withGraphFetched('type')
|
.withGraphFetched('type')
|
||||||
.remember();
|
.remember();
|
||||||
|
|
||||||
const storedAccountsIds = accounts.map((account) => account.id);
|
const storedAccountsIds = accounts.map((account) => account.id);
|
||||||
|
|
||||||
if (form.media_ids.length > 0) {
|
|
||||||
const storedMedia = await Media.query().whereIn('id', form.media_ids);
|
|
||||||
const notFoundMedia = difference(form.media_ids, storedMedia.map((m) => m.id));
|
|
||||||
|
|
||||||
if (notFoundMedia.length > 0) {
|
if (difference(formEntriesAccountsIds, storedAccountsIds).length > 0) {
|
||||||
errorReasons.push({ type: 'MEDIA.IDS.NOT.FOUND', code: 400, ids: notFoundMedia });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
|
||||||
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,10 +369,11 @@ export default {
|
|||||||
if (journalNumber.length > 0) {
|
if (journalNumber.length > 0) {
|
||||||
errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
|
errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorReasons.length > 0) {
|
if (errorReasons.length > 0) {
|
||||||
return res.status(400).send({ errors: errorReasons });
|
return res.status(400).send({ errors: errorReasons });
|
||||||
}
|
}
|
||||||
// Save manual journal transaction.
|
// Save manual journal tansaction.
|
||||||
const manualJournal = await ManualJournal.query().insert({
|
const manualJournal = await ManualJournal.query().insert({
|
||||||
reference: form.reference,
|
reference: form.reference,
|
||||||
transaction_type: 'Journal',
|
transaction_type: 'Journal',
|
||||||
@@ -282,6 +397,8 @@ export default {
|
|||||||
referenceType: 'Journal',
|
referenceType: 'Journal',
|
||||||
referenceId: manualJournal.id,
|
referenceId: manualJournal.id,
|
||||||
accountNormal: account.type.normal,
|
accountNormal: account.type.normal,
|
||||||
|
contactType: entry.contact_type,
|
||||||
|
contactId: entry.contact_id,
|
||||||
note: entry.note,
|
note: entry.note,
|
||||||
date: formattedDate,
|
date: formattedDate,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@@ -356,6 +473,8 @@ export default {
|
|||||||
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
|
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
||||||
check('entries.*.account_id').isNumeric().toInt(),
|
check('entries.*.account_id').isNumeric().toInt(),
|
||||||
|
check('entries.*.contact_id').optional().isNumeric().toInt(),
|
||||||
|
check('entries.*.contact_type').optional().isIn(['vendor', 'customer']).isNumeric().toInt(),
|
||||||
check('entries.*.note').optional(),
|
check('entries.*.note').optional(),
|
||||||
check('media_ids').optional().isArray(),
|
check('media_ids').optional().isArray(),
|
||||||
check('media_ids.*').isNumeric().toInt(),
|
check('media_ids.*').isNumeric().toInt(),
|
||||||
@@ -393,7 +512,7 @@ export default {
|
|||||||
let totalDebit = 0;
|
let totalDebit = 0;
|
||||||
|
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
const errorReasons = [];
|
const errorReasons = [...(req.errorReasons || [])];
|
||||||
const entries = form.entries.filter((entry) => (entry.credit || entry.debit));
|
const entries = form.entries.filter((entry) => (entry.credit || entry.debit));
|
||||||
const formattedDate = moment(form.date).format('YYYY-MM-DD');
|
const formattedDate = moment(form.date).format('YYYY-MM-DD');
|
||||||
|
|
||||||
@@ -431,16 +550,6 @@ export default {
|
|||||||
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
||||||
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate if media ids was not already exists on the storage.
|
|
||||||
if (form.media_ids.length > 0) {
|
|
||||||
const storedMedia = await Media.query().whereIn('id', form.media_ids);
|
|
||||||
const notFoundMedia = difference(form.media_ids, storedMedia.map((m) => m.id));
|
|
||||||
|
|
||||||
if (notFoundMedia.length > 0) {
|
|
||||||
errorReasons.push({ type: 'MEDIA.IDS.NOT.FOUND', code: 400, ids: notFoundMedia });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (errorReasons.length > 0) {
|
if (errorReasons.length > 0) {
|
||||||
return res.status(400).send({ errors: errorReasons });
|
return res.status(400).send({ errors: errorReasons });
|
||||||
}
|
}
|
||||||
|
|||||||
6
server/src/http/controllers/BaseController.js
Normal file
6
server/src/http/controllers/BaseController.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default class BaseController {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import TrialBalanceSheetController from './FinancialStatements/TrialBalanceSheet
|
|||||||
import GeneralLedgerController from './FinancialStatements/generalLedger';
|
import GeneralLedgerController from './FinancialStatements/generalLedger';
|
||||||
import JournalSheetController from './FinancialStatements/JournalSheet';
|
import JournalSheetController from './FinancialStatements/JournalSheet';
|
||||||
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
||||||
|
import ReceivableAgingSummary from './FinancialStatements/ReceivableAgingSummary';
|
||||||
|
import PayableAgingSummary from './FinancialStatements/PayableAgingSummary';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -18,6 +20,8 @@ export default {
|
|||||||
router.use('/general_ledger', GeneralLedgerController.router());
|
router.use('/general_ledger', GeneralLedgerController.router());
|
||||||
router.use('/trial_balance_sheet', TrialBalanceSheetController.router());
|
router.use('/trial_balance_sheet', TrialBalanceSheetController.router());
|
||||||
router.use('/journal', JournalSheetController.router());
|
router.use('/journal', JournalSheetController.router());
|
||||||
|
router.use('/receivable_aging_summary', ReceivableAgingSummary.router());
|
||||||
|
router.use('/payable_aging_summary', PayableAgingSummary.router());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
},
|
},
|
||||||
|
|||||||
106
server/src/http/controllers/FinancialStatements/AgingReport.js
Normal file
106
server/src/http/controllers/FinancialStatements/AgingReport.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import { validationResult } from 'express-validator';
|
||||||
|
import { omit, reverse } from 'lodash';
|
||||||
|
import BaseController from '@/http/controllers/BaseController';
|
||||||
|
|
||||||
|
export default class AgingReport extends BaseController{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express validator middleware.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {Function} next
|
||||||
|
*/
|
||||||
|
static validateResults(req, res, next) {
|
||||||
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
|
if (!validationErrors.isEmpty()) {
|
||||||
|
return res.boom.badData(null, {
|
||||||
|
code: 'validation_error', ...validationErrors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Array} agingPeriods
|
||||||
|
* @param {Numeric} customerBalance
|
||||||
|
*/
|
||||||
|
static contactAgingBalance(agingPeriods, receivableTotalCredit) {
|
||||||
|
let prevAging = 0;
|
||||||
|
let receivableCredit = receivableTotalCredit;
|
||||||
|
let diff = receivableCredit;
|
||||||
|
|
||||||
|
const periods = reverse(agingPeriods).map((agingPeriod) => {
|
||||||
|
const agingAmount = (agingPeriod.closingBalance - prevAging);
|
||||||
|
const subtract = Math.min(diff, agingAmount);
|
||||||
|
diff -= Math.min(agingAmount, diff);
|
||||||
|
|
||||||
|
const total = Math.max(agingAmount - subtract, 0);
|
||||||
|
|
||||||
|
const output = {
|
||||||
|
...omit(agingPeriod, ['closingBalance']),
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
prevAging = agingPeriod.closingBalance;
|
||||||
|
return output;
|
||||||
|
});
|
||||||
|
return reverse(periods);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} asDay
|
||||||
|
* @param {*} agingDaysBefore
|
||||||
|
* @param {*} agingPeriodsFreq
|
||||||
|
*/
|
||||||
|
static agingRangePeriods(asDay, agingDaysBefore, agingPeriodsFreq) {
|
||||||
|
const totalAgingDays = agingDaysBefore * agingPeriodsFreq;
|
||||||
|
const startAging = moment(asDay).startOf('day');
|
||||||
|
const endAging = startAging.clone().subtract('days', totalAgingDays).endOf('day');
|
||||||
|
|
||||||
|
const agingPeriods = [];
|
||||||
|
const startingAging = startAging.clone();
|
||||||
|
|
||||||
|
let beforeDays = 1;
|
||||||
|
let toDays = 0;
|
||||||
|
|
||||||
|
while (startingAging > endAging) {
|
||||||
|
const currentAging = startingAging.clone();
|
||||||
|
startingAging.subtract('days', agingDaysBefore).endOf('day');
|
||||||
|
toDays += agingDaysBefore;
|
||||||
|
|
||||||
|
agingPeriods.push({
|
||||||
|
from_period: moment(currentAging).toDate(),
|
||||||
|
to_period: moment(startingAging).toDate(),
|
||||||
|
before_days: beforeDays === 1 ? 0 : beforeDays,
|
||||||
|
to_days: toDays,
|
||||||
|
...(startingAging.valueOf() === endAging.valueOf()) ? {
|
||||||
|
to_period: null,
|
||||||
|
to_days: null,
|
||||||
|
} : {},
|
||||||
|
});
|
||||||
|
beforeDays += agingDaysBefore;
|
||||||
|
}
|
||||||
|
return agingPeriods;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} filter
|
||||||
|
*/
|
||||||
|
static formatNumberClosure(filter) {
|
||||||
|
return (balance) => {
|
||||||
|
let formattedBalance = parseFloat(balance);
|
||||||
|
|
||||||
|
if (filter.no_cents) {
|
||||||
|
formattedBalance = parseInt(formattedBalance, 10);
|
||||||
|
}
|
||||||
|
if (filter.divide_1000) {
|
||||||
|
formattedBalance /= 1000;
|
||||||
|
}
|
||||||
|
return formattedBalance;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { query } from 'express-validator';
|
||||||
|
import { difference } from 'lodash';
|
||||||
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||||
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
|
import AgingReport from '@/http/controllers/FinancialStatements/AgingReport';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export default class PayableAgingSummary extends AgingReport {
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
static router() {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
this.payableAgingSummaryRoles(),
|
||||||
|
this.validateResults,
|
||||||
|
asyncMiddleware(this.validateVendorsIds.bind(this)),
|
||||||
|
asyncMiddleware(this.payableAgingSummary.bind(this))
|
||||||
|
);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the report vendors ids query.
|
||||||
|
*/
|
||||||
|
static async validateVendorsIds(req, res, next) {
|
||||||
|
const { Vendor } = req.models;
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
vendors_ids: [],
|
||||||
|
...req.query,
|
||||||
|
};
|
||||||
|
if (!Array.isArray(filter.vendors_ids)) {
|
||||||
|
filter.vendors_ids = [filter.vendors_ids];
|
||||||
|
}
|
||||||
|
if (filter.vendors_ids.length > 0) {
|
||||||
|
const storedCustomers = await Vendor.query().whereIn(
|
||||||
|
'id',
|
||||||
|
filter.vendors_ids
|
||||||
|
);
|
||||||
|
const storedCustomersIds = storedCustomers.map((c) => c.id);
|
||||||
|
const notStoredCustomersIds = difference(
|
||||||
|
storedCustomersIds,
|
||||||
|
filter,
|
||||||
|
vendors_ids
|
||||||
|
);
|
||||||
|
|
||||||
|
if (notStoredCustomersIds.length) {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [{ type: 'VENDORS.IDS.NOT.FOUND', code: 300 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receivable aging summary validation roles.
|
||||||
|
*/
|
||||||
|
static payableAgingSummaryRoles() {
|
||||||
|
return [
|
||||||
|
query('as_date').optional().isISO8601(),
|
||||||
|
query('aging_days_before').optional().isNumeric().toInt(),
|
||||||
|
query('aging_periods').optional().isNumeric().toInt(),
|
||||||
|
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||||
|
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||||
|
query('vendors_ids.*').isNumeric().toInt(),
|
||||||
|
query('none_zero').optional().isBoolean().toBoolean(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve payable aging summary report.
|
||||||
|
*/
|
||||||
|
static async payableAgingSummary(req, res) {
|
||||||
|
const { Customer, Account, AccountTransaction, AccountType } = req.models;
|
||||||
|
const storedVendors = await Customer.query();
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
as_date: moment().format('YYYY-MM-DD'),
|
||||||
|
aging_days_before: 30,
|
||||||
|
aging_periods: 3,
|
||||||
|
number_format: {
|
||||||
|
no_cents: false,
|
||||||
|
divide_1000: false,
|
||||||
|
},
|
||||||
|
...req.query,
|
||||||
|
};
|
||||||
|
const accountsReceivableType = await AccountType.query()
|
||||||
|
.where('key', 'accounts_payable')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const accountsReceivable = await Account.query()
|
||||||
|
.where('account_type_id', accountsReceivableType.id)
|
||||||
|
.remember()
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const transactions = await AccountTransaction.query()
|
||||||
|
.modify('filterDateRange', null, filter.as_date)
|
||||||
|
.where('account_id', accountsReceivable.id)
|
||||||
|
.remember();
|
||||||
|
|
||||||
|
const journalPoster = new JournalPoster();
|
||||||
|
journalPoster.loadEntries(transactions);
|
||||||
|
|
||||||
|
const agingPeriods = this.agingRangePeriods(
|
||||||
|
filter.as_date,
|
||||||
|
filter.aging_days_before,
|
||||||
|
filter.aging_periods
|
||||||
|
);
|
||||||
|
// Total amount formmatter based on the given query.
|
||||||
|
const totalFormatter = formatNumberClosure(filter.number_format);
|
||||||
|
|
||||||
|
const vendors = storedVendors.map((vendor) => {
|
||||||
|
// Calculate the trial balance total of the given vendor.
|
||||||
|
const vendorBalance = journalPoster.getContactTrialBalance(
|
||||||
|
accountsReceivable.id,
|
||||||
|
vendor.id,
|
||||||
|
'vendor'
|
||||||
|
);
|
||||||
|
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||||
|
// Calculate the trial balance between the given date period.
|
||||||
|
const agingTrialBalance = journalPoster.getContactTrialBalance(
|
||||||
|
accountsReceivable.id,
|
||||||
|
vendor.id,
|
||||||
|
'vendor',
|
||||||
|
agingPeriod.from_period
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...agingPeriod,
|
||||||
|
closingBalance: agingTrialBalance.debit,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const aging = this.contactAgingBalance(
|
||||||
|
agingClosingBalance,
|
||||||
|
vendorBalance.credit
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
vendor_name: vendor.displayName,
|
||||||
|
aging: aging.map((item) => ({
|
||||||
|
...item,
|
||||||
|
formatted_total: totalFormatter(item.total),
|
||||||
|
})),
|
||||||
|
total: vendorBalance.balance,
|
||||||
|
formatted_total: totalFormatted(vendorBalance.balance),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||||
|
const closingTrialBalance = journalPoster.getContactTrialBalance(
|
||||||
|
accountsReceivable.id,
|
||||||
|
null,
|
||||||
|
'vendor',
|
||||||
|
agingPeriod.from_period
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...agingPeriod,
|
||||||
|
closingBalance: closingTrialBalance.balance,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalClosingBalance = journalPoster.getContactTrialBalance(
|
||||||
|
accountsReceivable.id,
|
||||||
|
null,
|
||||||
|
'vendor'
|
||||||
|
);
|
||||||
|
const agingTotal = this.contactAgingBalance(
|
||||||
|
agingClosingBalance,
|
||||||
|
totalClosingBalance.credit
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
columns: [ ...agingPeriods ],
|
||||||
|
aging: {
|
||||||
|
vendors,
|
||||||
|
total: [
|
||||||
|
...agingTotal.map((item) => ({
|
||||||
|
...item,
|
||||||
|
formatted_total: totalFormatter(item.total),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { query, oneOf } from 'express-validator';
|
||||||
|
import { difference } from 'lodash';
|
||||||
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||||
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
|
import AgingReport from '@/http/controllers/FinancialStatements/AgingReport';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export default class ReceivableAgingSummary extends AgingReport {
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
static router() {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
this.receivableAgingSummaryRoles,
|
||||||
|
this.validateResults,
|
||||||
|
asyncMiddleware(this.validateCustomersIds.bind(this)),
|
||||||
|
asyncMiddleware(this.receivableAgingSummary.bind(this))
|
||||||
|
);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the report customers ids query.
|
||||||
|
*/
|
||||||
|
static async validateCustomersIds(req, res, next) {
|
||||||
|
const { Customer } = req.models;
|
||||||
|
|
||||||
|
console.log(req.query);
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
customer_ids: [],
|
||||||
|
...req.query,
|
||||||
|
};
|
||||||
|
if (!Array.isArray(filter.customer_ids)) {
|
||||||
|
filter.customer_ids = [filter.customer_ids];
|
||||||
|
}
|
||||||
|
if (filter.customer_ids.length > 0) {
|
||||||
|
const storedCustomers = await Customer.query().whereIn(
|
||||||
|
'id',
|
||||||
|
filter.customer_ids
|
||||||
|
);
|
||||||
|
const storedCustomersIds = storedCustomers.map((c) => parseInt(c.id, 10));
|
||||||
|
const notStoredCustomersIds = difference(
|
||||||
|
filter.customer_ids.map(a => parseInt(a, 10)),
|
||||||
|
storedCustomersIds
|
||||||
|
);
|
||||||
|
|
||||||
|
if (notStoredCustomersIds.length) {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'CUSTOMERS.IDS.NOT.FOUND',
|
||||||
|
code: 300,
|
||||||
|
ids: notStoredCustomersIds,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receivable aging summary validation roles.
|
||||||
|
*/
|
||||||
|
static get receivableAgingSummaryRoles() {
|
||||||
|
return [
|
||||||
|
query('as_date').optional().isISO8601(),
|
||||||
|
query('aging_days_before').optional().isNumeric().toInt(),
|
||||||
|
query('aging_periods').optional().isNumeric().toInt(),
|
||||||
|
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||||
|
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||||
|
oneOf(
|
||||||
|
[
|
||||||
|
query('customer_ids').optional().isArray({ min: 1 }),
|
||||||
|
query('customer_ids.*').isNumeric().toInt(),
|
||||||
|
],
|
||||||
|
[query('customer_ids').optional().isNumeric().toInt()]
|
||||||
|
),
|
||||||
|
query('none_zero').optional().isBoolean().toBoolean(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve receivable aging summary report.
|
||||||
|
*/
|
||||||
|
static async receivableAgingSummary(req, res) {
|
||||||
|
const { Customer, Account, AccountTransaction, AccountType } = req.models;
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
as_date: moment().format('YYYY-MM-DD'),
|
||||||
|
aging_days_before: 30,
|
||||||
|
aging_periods: 3,
|
||||||
|
number_format: {
|
||||||
|
no_cents: false,
|
||||||
|
divide_1000: false,
|
||||||
|
},
|
||||||
|
customer_ids: [],
|
||||||
|
...req.query,
|
||||||
|
};
|
||||||
|
if (!Array.isArray(filter.customer_ids)) {
|
||||||
|
filter.customer_ids = [filter.customer_ids];
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedCustomers = await Customer.query().onBuild((builder) => {
|
||||||
|
if (filter.customer_ids) {
|
||||||
|
builder.modify('filterCustomerIds', filter.customer_ids);
|
||||||
|
}
|
||||||
|
return builder;
|
||||||
|
});
|
||||||
|
const accountsReceivableType = await AccountType.query()
|
||||||
|
.where('key', 'accounts_receivable')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const accountsReceivable = await Account.query()
|
||||||
|
.where('account_type_id', accountsReceivableType.id)
|
||||||
|
.remember()
|
||||||
|
.first();
|
||||||
|
|
||||||
|
const transactions = await AccountTransaction.query().onBuild((query) => {
|
||||||
|
query.modify('filterDateRange', null, filter.as_date)
|
||||||
|
query.where('account_id', accountsReceivable.id)
|
||||||
|
query.modify('filterContactType', 'customer');
|
||||||
|
|
||||||
|
if (filter.customer_ids.length> 0) {
|
||||||
|
query.modify('filterContactIds', filter.customer_ids)
|
||||||
|
}
|
||||||
|
query.remember();
|
||||||
|
return query;
|
||||||
|
});
|
||||||
|
|
||||||
|
const journalPoster = new JournalPoster();
|
||||||
|
journalPoster.loadEntries(transactions);
|
||||||
|
|
||||||
|
const agingPeriods = this.agingRangePeriods(
|
||||||
|
filter.as_date,
|
||||||
|
filter.aging_days_before,
|
||||||
|
filter.aging_periods
|
||||||
|
);
|
||||||
|
// Total amount formmatter based on the given query.
|
||||||
|
const totalFormatter = this.formatNumberClosure(filter.number_format);
|
||||||
|
|
||||||
|
const customers = storedCustomers.map((customer) => {
|
||||||
|
// Calculate the trial balance total of the given customer.
|
||||||
|
const customerBalance = journalPoster.getContactTrialBalance(
|
||||||
|
accountsReceivable.id,
|
||||||
|
customer.id,
|
||||||
|
'customer'
|
||||||
|
);
|
||||||
|
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||||
|
// Calculate the trial balance between the given date period.
|
||||||
|
const agingTrialBalance = journalPoster.getContactTrialBalance(
|
||||||
|
accountsReceivable.id,
|
||||||
|
customer.id,
|
||||||
|
'customer',
|
||||||
|
agingPeriod.from_period
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...agingPeriod,
|
||||||
|
closingBalance: agingTrialBalance.debit,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const aging = this.contactAgingBalance(
|
||||||
|
agingClosingBalance,
|
||||||
|
customerBalance.credit
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
customer_name: customer.displayName,
|
||||||
|
aging: aging.map((item) => ({
|
||||||
|
...item,
|
||||||
|
formatted_total: totalFormatter(item.total),
|
||||||
|
})),
|
||||||
|
total: customerBalance.balance,
|
||||||
|
formatted_total: totalFormatter(customerBalance.balance),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const agingClosingBalance = agingPeriods.map((agingPeriod) => {
|
||||||
|
const closingTrialBalance = journalPoster.getContactTrialBalance(
|
||||||
|
accountsReceivable.id,
|
||||||
|
null,
|
||||||
|
'customer',
|
||||||
|
agingPeriod.from_period
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...agingPeriod,
|
||||||
|
closingBalance: closingTrialBalance.balance,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalClosingBalance = journalPoster.getContactTrialBalance(
|
||||||
|
accountsReceivable.id,
|
||||||
|
null,
|
||||||
|
'customer'
|
||||||
|
);
|
||||||
|
const agingTotal = this.contactAgingBalance(
|
||||||
|
agingClosingBalance,
|
||||||
|
totalClosingBalance.credit
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
columns: [...agingPeriods],
|
||||||
|
aging: {
|
||||||
|
customers,
|
||||||
|
total: [
|
||||||
|
...agingTotal.map((item) => ({
|
||||||
|
...item,
|
||||||
|
formatted_total: totalFormatter(item.total),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,10 @@ import {
|
|||||||
} from '@/lib/ViewRolesBuilder';
|
} from '@/lib/ViewRolesBuilder';
|
||||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||||
import DateSession from '@/models/DateSession';
|
|
||||||
import { flatToNestedArray } from '@/utils';
|
import { flatToNestedArray } from '@/utils';
|
||||||
import DependencyGraph from '@/lib/DependencyGraph';
|
import DependencyGraph from '@/lib/DependencyGraph';
|
||||||
|
|
||||||
export default class Account extends mixin(TenantModel, [CachableModel, DateSession]) {
|
export default class Account extends mixin(TenantModel, [CachableModel]) {
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
@@ -20,6 +19,13 @@ export default class Account extends mixin(TenantModel, [CachableModel, DateSess
|
|||||||
return 'accounts';
|
return 'accounts';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend query builder model.
|
* Extend query builder model.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import moment from 'moment';
|
|||||||
import TenantModel from '@/models/TenantModel';
|
import TenantModel from '@/models/TenantModel';
|
||||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||||
import DateSession from '@/models/DateSession';
|
|
||||||
|
|
||||||
|
|
||||||
export default class AccountTransaction extends mixin(TenantModel, [CachableModel, DateSession]) {
|
export default class AccountTransaction extends mixin(TenantModel, [CachableModel]) {
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
@@ -14,6 +13,13 @@ export default class AccountTransaction extends mixin(TenantModel, [CachableMode
|
|||||||
return 'accounts_transactions';
|
return 'accounts_transactions';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend query builder model.
|
* Extend query builder model.
|
||||||
*/
|
*/
|
||||||
@@ -69,6 +75,12 @@ export default class AccountTransaction extends mixin(TenantModel, [CachableMode
|
|||||||
query.sum('debit as debit');
|
query.sum('debit as debit');
|
||||||
query.groupBy('account_id');
|
query.groupBy('account_id');
|
||||||
},
|
},
|
||||||
|
filterContactType(query, contactType) {
|
||||||
|
query.where('contact_type', contactType);
|
||||||
|
},
|
||||||
|
filterContactIds(query, contactIds) {
|
||||||
|
query.whereIn('contact_id', contactIds);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// import path from 'path';
|
// import path from 'path';
|
||||||
import { Model } from 'objection';
|
import { Model, mixin } from 'objection';
|
||||||
import TenantModel from '@/models/TenantModel';
|
import TenantModel from '@/models/TenantModel';
|
||||||
|
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||||
|
|
||||||
export default class AccountType extends TenantModel {
|
export default class AccountType extends mixin(TenantModel, [CachableModel]) {
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,4 +7,11 @@ export default class Currency extends TenantModel {
|
|||||||
static get tableName() {
|
static get tableName() {
|
||||||
return 'currencies';
|
return 'currencies';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,22 @@ export default class Customer extends TenantModel {
|
|||||||
static get tableName() {
|
static get tableName() {
|
||||||
return 'customers';
|
return 'customers';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model timestamps.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model modifiers.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
filterCustomerIds(query, customerIds) {
|
||||||
|
query.whereIn('id', customerIds);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,11 @@ export default class ExchangeRate extends TenantModel {
|
|||||||
static get tableName() {
|
static get tableName() {
|
||||||
return 'exchange_rates';
|
return 'exchange_rates';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,10 +10,20 @@ export default class Expense extends TenantModel {
|
|||||||
return 'expenses_transactions';
|
return 'expenses_transactions';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account transaction reference type.
|
||||||
|
*/
|
||||||
static get referenceType() {
|
static get referenceType() {
|
||||||
return 'Expense';
|
return 'Expense';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model timestamps.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model modifiers.
|
* Model modifiers.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ export default class Item extends TenantModel {
|
|||||||
return 'items';
|
return 'items';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model timestamps.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model modifiers.
|
* Model modifiers.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ export default class ManualJournal extends TenantModel {
|
|||||||
return 'manual_journals';
|
return 'manual_journals';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model timestamps.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { Model } from 'objection';
|
import { Model, mixin } from 'objection';
|
||||||
import { snakeCase } from 'lodash';
|
import { snakeCase } from 'lodash';
|
||||||
import { mapKeysDeep } from '@/utils';
|
import { mapKeysDeep } from '@/utils';
|
||||||
import PaginationQueryBuilder from '@/models/Pagination';
|
import PaginationQueryBuilder from '@/models/Pagination';
|
||||||
|
import DateSession from '@/models/DateSession';
|
||||||
|
|
||||||
export default class ModelBase extends Model {
|
export default class ModelBase extends mixin(Model, [DateSession]) {
|
||||||
|
|
||||||
|
static get timestamps() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
static get knexBinded() {
|
static get knexBinded() {
|
||||||
return this.knexBindInstance;
|
return this.knexBindInstance;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { Model, mixin } from 'objection';
|
import { Model } from 'objection';
|
||||||
import TenantModel from '@/models/TenantModel';
|
import TenantModel from '@/models/TenantModel';
|
||||||
import DateSession from '@/models/DateSession';
|
|
||||||
// import PermissionsService from '@/services/PermissionsService';
|
// import PermissionsService from '@/services/PermissionsService';
|
||||||
|
|
||||||
export default class TenantUser extends mixin(TenantModel, [DateSession]) {
|
export default class TenantUser extends TenantModel {
|
||||||
/**
|
/**
|
||||||
* Virtual attributes.
|
* Virtual attributes.
|
||||||
*/
|
*/
|
||||||
@@ -19,6 +18,13 @@ export default class TenantUser extends mixin(TenantModel, [DateSession]) {
|
|||||||
return 'users';
|
return 'users';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,4 +8,11 @@ export default class Vendor extends TenantModel {
|
|||||||
static get tableName() {
|
static get tableName() {
|
||||||
return 'vendors';
|
return 'vendors';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model timestamps.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ export default class View extends mixin(TenantModel, [CachableModel]) {
|
|||||||
return 'views';
|
return 'views';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model timestamps.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend query builder model.
|
* Extend query builder model.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import moment from 'moment';
|
|||||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||||
import AccountTransaction from '@/models/AccountTransaction';
|
import AccountTransaction from '@/models/AccountTransaction';
|
||||||
import AccountBalance from '@/models/AccountBalance';
|
import AccountBalance from '@/models/AccountBalance';
|
||||||
import {promiseSerial} from '@/utils';
|
import { promiseSerial } from '@/utils';
|
||||||
import Account from '@/models/Account';
|
import Account from '@/models/Account';
|
||||||
import NestedSet from '../../collection/NestedSet';
|
import NestedSet from '../../collection/NestedSet';
|
||||||
|
|
||||||
|
|
||||||
export default class JournalPoster {
|
export default class JournalPoster {
|
||||||
/**
|
/**
|
||||||
* Journal poster constructor.
|
* Journal poster constructor.
|
||||||
@@ -34,7 +33,7 @@ export default class JournalPoster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the debit entr y for the given account.
|
* Writes the debit entry for the given account.
|
||||||
* @param {JournalEntry} entry -
|
* @param {JournalEntry} entry -
|
||||||
*/
|
*/
|
||||||
debit(entryModel) {
|
debit(entryModel) {
|
||||||
@@ -78,7 +77,11 @@ export default class JournalPoster {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_setAccountBalanceChange({
|
_setAccountBalanceChange({
|
||||||
accountId, accountNormal, debit, credit, entryType
|
accountId,
|
||||||
|
accountNormal,
|
||||||
|
debit,
|
||||||
|
credit,
|
||||||
|
entryType,
|
||||||
}) {
|
}) {
|
||||||
if (!this.balancesChange[accountId]) {
|
if (!this.balancesChange[accountId]) {
|
||||||
this.balancesChange[accountId] = 0;
|
this.balancesChange[accountId] = 0;
|
||||||
@@ -86,9 +89,9 @@ export default class JournalPoster {
|
|||||||
let change = 0;
|
let change = 0;
|
||||||
|
|
||||||
if (accountNormal === 'credit') {
|
if (accountNormal === 'credit') {
|
||||||
change = (entryType === 'credit') ? credit : -1 * debit;
|
change = entryType === 'credit' ? credit : -1 * debit;
|
||||||
} else if (accountNormal === 'debit') {
|
} else if (accountNormal === 'debit') {
|
||||||
change = (entryType === 'debit') ? debit : -1 * credit;
|
change = entryType === 'debit' ? debit : -1 * credit;
|
||||||
}
|
}
|
||||||
this.balancesChange[accountId] += change;
|
this.balancesChange[accountId] += change;
|
||||||
}
|
}
|
||||||
@@ -132,9 +135,9 @@ export default class JournalPoster {
|
|||||||
const method = balance.amount < 0 ? 'decrement' : 'increment';
|
const method = balance.amount < 0 ? 'decrement' : 'increment';
|
||||||
|
|
||||||
// Detarmine if the account balance is already exists or not.
|
// Detarmine if the account balance is already exists or not.
|
||||||
const foundAccBalance = balanceAccounts.some((account) => (
|
const foundAccBalance = balanceAccounts.some(
|
||||||
account && account.account_id === balance.account_id
|
(account) => account && account.account_id === balance.account_id
|
||||||
));
|
);
|
||||||
|
|
||||||
if (foundAccBalance) {
|
if (foundAccBalance) {
|
||||||
const query = AccountBalance.tenant()
|
const query = AccountBalance.tenant()
|
||||||
@@ -152,9 +155,7 @@ export default class JournalPoster {
|
|||||||
balanceInsertOpers.push(query);
|
balanceInsertOpers.push(query);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await Promise.all([
|
await Promise.all([...balanceUpdateOpers, ...balanceInsertOpers]);
|
||||||
...balanceUpdateOpers, ...balanceInsertOpers,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,11 +165,23 @@ export default class JournalPoster {
|
|||||||
const saveOperations = [];
|
const saveOperations = [];
|
||||||
|
|
||||||
this.entries.forEach((entry) => {
|
this.entries.forEach((entry) => {
|
||||||
const oper = AccountTransaction.tenant().query().insert({
|
const oper = AccountTransaction.tenant()
|
||||||
accountId: entry.account,
|
.query()
|
||||||
...pick(entry, ['credit', 'debit', 'transactionType', 'date', 'userId',
|
.insert({
|
||||||
'referenceType', 'referenceId', 'note']),
|
accountId: entry.account,
|
||||||
});
|
...pick(entry, [
|
||||||
|
'credit',
|
||||||
|
'debit',
|
||||||
|
'transactionType',
|
||||||
|
'date',
|
||||||
|
'userId',
|
||||||
|
'referenceType',
|
||||||
|
'referenceId',
|
||||||
|
'note',
|
||||||
|
'contactId',
|
||||||
|
'contactType',
|
||||||
|
]),
|
||||||
|
});
|
||||||
saveOperations.push(() => oper);
|
saveOperations.push(() => oper);
|
||||||
});
|
});
|
||||||
await promiseSerial(saveOperations);
|
await promiseSerial(saveOperations);
|
||||||
@@ -199,11 +212,12 @@ export default class JournalPoster {
|
|||||||
* @param {Array} ids -
|
* @param {Array} ids -
|
||||||
*/
|
*/
|
||||||
removeEntries(ids = []) {
|
removeEntries(ids = []) {
|
||||||
const targetIds = (ids.length <= 0) ? this.entries.map(e => e.id) : ids;
|
const targetIds = ids.length <= 0 ? this.entries.map((e) => e.id) : ids;
|
||||||
const removeEntries = this.entries.filter((e) => targetIds.indexOf(e.id) !== -1);
|
const removeEntries = this.entries.filter(
|
||||||
|
(e) => targetIds.indexOf(e.id) !== -1
|
||||||
|
);
|
||||||
|
|
||||||
this.entries = this.entries
|
this.entries = this.entries.filter((e) => targetIds.indexOf(e.id) === -1);
|
||||||
.filter(e => targetIds.indexOf(e.id) === -1)
|
|
||||||
|
|
||||||
removeEntries.forEach((entry) => {
|
removeEntries.forEach((entry) => {
|
||||||
entry.credit = -1 * entry.credit;
|
entry.credit = -1 * entry.credit;
|
||||||
@@ -211,9 +225,7 @@ export default class JournalPoster {
|
|||||||
|
|
||||||
this.setAccountBalanceChange(entry, entry.accountNormal);
|
this.setAccountBalanceChange(entry, entry.accountNormal);
|
||||||
});
|
});
|
||||||
this.deletedEntriesIds.push(
|
this.deletedEntriesIds.push(...removeEntries.map((entry) => entry.id));
|
||||||
...removeEntries.map(entry => entry.id),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,7 +233,8 @@ export default class JournalPoster {
|
|||||||
*/
|
*/
|
||||||
async deleteEntries() {
|
async deleteEntries() {
|
||||||
if (this.deletedEntriesIds.length > 0) {
|
if (this.deletedEntriesIds.length > 0) {
|
||||||
await AccountTransaction.tenant().query()
|
await AccountTransaction.tenant()
|
||||||
|
.query()
|
||||||
.whereIn('id', this.deletedEntriesIds)
|
.whereIn('id', this.deletedEntriesIds)
|
||||||
.delete();
|
.delete();
|
||||||
}
|
}
|
||||||
@@ -238,15 +251,17 @@ export default class JournalPoster {
|
|||||||
|
|
||||||
this.entries.forEach((entry) => {
|
this.entries.forEach((entry) => {
|
||||||
// Can not continue if not before or event same closing date.
|
// Can not continue if not before or event same closing date.
|
||||||
if ((!momentClosingDate.isAfter(entry.date, dateType)
|
if (
|
||||||
&& !momentClosingDate.isSame(entry.date, dateType))
|
(!momentClosingDate.isAfter(entry.date, dateType) &&
|
||||||
|| (entry.account !== accountId && accountId)) {
|
!momentClosingDate.isSame(entry.date, dateType)) ||
|
||||||
|
(entry.account !== accountId && accountId)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (entry.accountNormal === 'credit') {
|
if (entry.accountNormal === 'credit') {
|
||||||
closingBalance += (entry.credit) ? entry.credit : -1 * entry.debit;
|
closingBalance += entry.credit ? entry.credit : -1 * entry.debit;
|
||||||
} else if (entry.accountNormal === 'debit') {
|
} else if (entry.accountNormal === 'debit') {
|
||||||
closingBalance += (entry.debit) ? entry.debit : -1 * entry.credit;
|
closingBalance += entry.debit ? entry.debit : -1 * entry.credit;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return closingBalance;
|
return closingBalance;
|
||||||
@@ -262,13 +277,19 @@ export default class JournalPoster {
|
|||||||
getAccountBalance(accountId, closingDate, dateType) {
|
getAccountBalance(accountId, closingDate, dateType) {
|
||||||
const accountNode = this.accountsGraph.getNodeData(accountId);
|
const accountNode = this.accountsGraph.getNodeData(accountId);
|
||||||
const depAccountsIds = this.accountsGraph.dependenciesOf(accountId);
|
const depAccountsIds = this.accountsGraph.dependenciesOf(accountId);
|
||||||
const depAccounts = depAccountsIds.map((id) => this.accountsGraph.getNodeData(id));
|
const depAccounts = depAccountsIds.map((id) =>
|
||||||
|
this.accountsGraph.getNodeData(id)
|
||||||
|
);
|
||||||
let balance = 0;
|
let balance = 0;
|
||||||
|
|
||||||
[...depAccounts, accountNode].forEach((account) => {
|
[...depAccounts, accountNode].forEach((account) => {
|
||||||
// if (!this.accountsBalanceTable[account.id]) {
|
// if (!this.accountsBalanceTable[account.id]) {
|
||||||
const closingBalance = this.getClosingBalance(account.id, closingDate, dateType);
|
const closingBalance = this.getClosingBalance(
|
||||||
this.accountsBalanceTable[account.id] = closingBalance;
|
account.id,
|
||||||
|
closingDate,
|
||||||
|
dateType
|
||||||
|
);
|
||||||
|
this.accountsBalanceTable[account.id] = closingBalance;
|
||||||
// }
|
// }
|
||||||
balance += this.accountsBalanceTable[account.id];
|
balance += this.accountsBalanceTable[account.id];
|
||||||
});
|
});
|
||||||
@@ -288,9 +309,11 @@ export default class JournalPoster {
|
|||||||
balance: 0,
|
balance: 0,
|
||||||
};
|
};
|
||||||
this.entries.forEach((entry) => {
|
this.entries.forEach((entry) => {
|
||||||
if ((!momentClosingDate.isAfter(entry.date, dateType)
|
if (
|
||||||
&& !momentClosingDate.isSame(entry.date, dateType))
|
(!momentClosingDate.isAfter(entry.date, dateType) &&
|
||||||
|| (entry.account !== accountId && accountId)) {
|
!momentClosingDate.isSame(entry.date, dateType)) ||
|
||||||
|
(entry.account !== accountId && accountId)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
result.credit += entry.credit;
|
result.credit += entry.credit;
|
||||||
@@ -312,15 +335,22 @@ export default class JournalPoster {
|
|||||||
* @param {String} dateType
|
* @param {String} dateType
|
||||||
* @return {Number}
|
* @return {Number}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getTrialBalanceWithDepands(accountId, closingDate, dateType) {
|
getTrialBalanceWithDepands(accountId, closingDate, dateType) {
|
||||||
const accountNode = this.accountsGraph.getNodeData(accountId);
|
const accountNode = this.accountsGraph.getNodeData(accountId);
|
||||||
const depAccountsIds = this.accountsGraph.dependenciesOf(accountId);
|
const depAccountsIds = this.accountsGraph.dependenciesOf(accountId);
|
||||||
const depAccounts = depAccountsIds.map((id) => this.accountsGraph.getNodeData(id));
|
const depAccounts = depAccountsIds.map((id) =>
|
||||||
|
this.accountsGraph.getNodeData(id)
|
||||||
|
);
|
||||||
|
|
||||||
const trialBalance = { credit: 0, debit: 0, balance: 0 };
|
const trialBalance = { credit: 0, debit: 0, balance: 0 };
|
||||||
|
|
||||||
[...depAccounts, accountNode].forEach((account) => {
|
[...depAccounts, accountNode].forEach((account) => {
|
||||||
const _trialBalance = this.getTrialBalance(account.id, closingDate, dateType);
|
const _trialBalance = this.getTrialBalance(
|
||||||
|
account.id,
|
||||||
|
closingDate,
|
||||||
|
dateType
|
||||||
|
);
|
||||||
|
|
||||||
trialBalance.credit += _trialBalance.credit;
|
trialBalance.credit += _trialBalance.credit;
|
||||||
trialBalance.debit += _trialBalance.debit;
|
trialBalance.debit += _trialBalance.debit;
|
||||||
@@ -329,6 +359,85 @@ export default class JournalPoster {
|
|||||||
return trialBalance;
|
return trialBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getContactTrialBalance(
|
||||||
|
accountId,
|
||||||
|
contactId,
|
||||||
|
contactType,
|
||||||
|
closingDate,
|
||||||
|
openingDate
|
||||||
|
) {
|
||||||
|
const momentClosingDate = moment(closingDate);
|
||||||
|
const momentOpeningDate = moment(openingDate);
|
||||||
|
const trial = {
|
||||||
|
credit: 0,
|
||||||
|
debit: 0,
|
||||||
|
balance: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.entries.forEach((entry) => {
|
||||||
|
if (
|
||||||
|
(closingDate &&
|
||||||
|
!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||||
|
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||||
|
(openingDate &&
|
||||||
|
!momentOpeningDate.isBefore(entry.date, 'day') &&
|
||||||
|
!momentOpeningDate.isSame(entry.date)) ||
|
||||||
|
(accountId && entry.account !== accountId) ||
|
||||||
|
(contactId && entry.contactId !== contactId) ||
|
||||||
|
entry.contactType !== contactType
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entry.credit) {
|
||||||
|
trial.balance -= entry.credit;
|
||||||
|
trial.credit += entry.credit;
|
||||||
|
}
|
||||||
|
if (entry.debit) {
|
||||||
|
trial.balance += entry.debit;
|
||||||
|
trial.debit += entry.debit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return trial;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve total balnace of the given customer/vendor contact.
|
||||||
|
* @param {Number} accountId
|
||||||
|
* @param {Number} contactId
|
||||||
|
* @param {String} contactType
|
||||||
|
* @param {Date} closingDate
|
||||||
|
*/
|
||||||
|
getContactBalance(
|
||||||
|
accountId,
|
||||||
|
contactId,
|
||||||
|
contactType,
|
||||||
|
closingDate,
|
||||||
|
openingDate
|
||||||
|
) {
|
||||||
|
const momentClosingDate = moment(closingDate);
|
||||||
|
let balance = 0;
|
||||||
|
|
||||||
|
this.entries.forEach((entry) => {
|
||||||
|
if (
|
||||||
|
(closingDate &&
|
||||||
|
!momentClosingDate.isAfter(entry.date, 'day') &&
|
||||||
|
!momentClosingDate.isSame(entry.date, 'day')) ||
|
||||||
|
(entry.account !== accountId && accountId) ||
|
||||||
|
(contactId && entry.contactId !== contactId) ||
|
||||||
|
entry.contactType !== contactType
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entry.credit) {
|
||||||
|
balance -= entry.credit;
|
||||||
|
}
|
||||||
|
if (entry.debit) {
|
||||||
|
balance += entry.debit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return balance;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load fetched accounts journal entries.
|
* Load fetched accounts journal entries.
|
||||||
* @param {Array} entries -
|
* @param {Array} entries -
|
||||||
@@ -338,8 +447,10 @@ export default class JournalPoster {
|
|||||||
this.entries.push({
|
this.entries.push({
|
||||||
...entry,
|
...entry,
|
||||||
account: entry.account ? entry.account.id : entry.accountId,
|
account: entry.account ? entry.account.id : entry.accountId,
|
||||||
accountNormal: (entry.account && entry.account.type)
|
accountNormal:
|
||||||
? entry.account.type.normal : entry.accountNormal,
|
entry.account && entry.account.type
|
||||||
|
? entry.account.type.normal
|
||||||
|
: entry.accountNormal,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Model, mixin } from 'objection';
|
import { Model, mixin } from 'objection';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import SystemModel from '@/system/models/SystemModel';
|
import SystemModel from '@/system/models/SystemModel';
|
||||||
import DateSession from '@/models/DateSession';
|
|
||||||
import UserSubscription from '@/services/Subscription/UserSubscription';
|
import UserSubscription from '@/services/Subscription/UserSubscription';
|
||||||
|
|
||||||
|
|
||||||
export default class SystemUser extends mixin(SystemModel, [DateSession, UserSubscription]) {
|
export default class SystemUser extends mixin(SystemModel, [UserSubscription]) {
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
@@ -13,6 +12,13 @@ export default class SystemUser extends mixin(SystemModel, [DateSession, UserSub
|
|||||||
return 'users';
|
return 'users';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Relationship mapping.
|
* Relationship mapping.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -27,11 +27,13 @@ describe('routes: `/accounting`', () => {
|
|||||||
reference: 'ASC',
|
reference: 'ASC',
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
|
index: 1,
|
||||||
credit: 0,
|
credit: 0,
|
||||||
debit: 0,
|
debit: 0,
|
||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
index: 2,
|
||||||
credit: 0,
|
credit: 0,
|
||||||
debit: 0,
|
debit: 0,
|
||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
@@ -56,11 +58,13 @@ describe('routes: `/accounting`', () => {
|
|||||||
journal_number: '123',
|
journal_number: '123',
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
|
index: 1,
|
||||||
credit: 1000,
|
credit: 1000,
|
||||||
debit: 0,
|
debit: 0,
|
||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
index: 2,
|
||||||
credit: 0,
|
credit: 0,
|
||||||
debit: 500,
|
debit: 500,
|
||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
@@ -88,11 +92,13 @@ describe('routes: `/accounting`', () => {
|
|||||||
journal_number: manualJournal.journalNumber,
|
journal_number: manualJournal.journalNumber,
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
|
index: 1,
|
||||||
credit: 1000,
|
credit: 1000,
|
||||||
debit: 0,
|
debit: 0,
|
||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
index: 2,
|
||||||
credit: 0,
|
credit: 0,
|
||||||
debit: 1000,
|
debit: 1000,
|
||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
@@ -117,11 +123,13 @@ describe('routes: `/accounting`', () => {
|
|||||||
journal_number: '123',
|
journal_number: '123',
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
|
index: 1,
|
||||||
credit: 1000,
|
credit: 1000,
|
||||||
debit: 0,
|
debit: 0,
|
||||||
account_id: 12,
|
account_id: 12,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
index: 2,
|
||||||
credit: 0,
|
credit: 0,
|
||||||
debit: 1000,
|
debit: 1000,
|
||||||
account_id: 12,
|
account_id: 12,
|
||||||
@@ -149,11 +157,13 @@ describe('routes: `/accounting`', () => {
|
|||||||
journal_number: '1000',
|
journal_number: '1000',
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
|
index: 1,
|
||||||
credit: null,
|
credit: null,
|
||||||
debit: 0,
|
debit: 0,
|
||||||
account_id: account1.id,
|
account_id: account1.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
index: 2,
|
||||||
credit: null,
|
credit: null,
|
||||||
debit: 0,
|
debit: 0,
|
||||||
account_id: account2.id,
|
account_id: account2.id,
|
||||||
@@ -164,10 +174,88 @@ describe('routes: `/accounting`', () => {
|
|||||||
expect(res.status).equals(400);
|
expect(res.status).equals(400);
|
||||||
expect(res.body.errors).include.something.that.deep.equal({
|
expect(res.body.errors).include.something.that.deep.equal({
|
||||||
type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
|
type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
|
||||||
|
|
||||||
code: 400,
|
code: 400,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should validate the customers and vendors contact if were not found on the storage.', async () => {
|
||||||
|
const account1 = await tenantFactory.create('account');
|
||||||
|
const account2 = await tenantFactory.create('account');
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.post('/api/accounting/make-journal-entries')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.send({
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
journal_number: '1000',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
credit: null,
|
||||||
|
debit: 1000,
|
||||||
|
account_id: account1.id,
|
||||||
|
contact_type: 'customer',
|
||||||
|
contact_id: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 2,
|
||||||
|
credit: 1000,
|
||||||
|
debit: 0,
|
||||||
|
account_id: account1.id,
|
||||||
|
contact_type: 'vendor',
|
||||||
|
contact_id: 300,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.body.errors).include.something.deep.equals({
|
||||||
|
type: 'CUSTOMERS.CONTACTS.NOT.FOUND', code: 500, ids: [100],
|
||||||
|
});
|
||||||
|
expect(res.body.errors).include.something.deep.equals({
|
||||||
|
type: 'VENDORS.CONTACTS.NOT.FOUND', code: 600, ids: [300],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should customer contact_type with receivable accounts type.', async () => {
|
||||||
|
const account1 = await tenantFactory.create('account');
|
||||||
|
const account2 = await tenantFactory.create('account');
|
||||||
|
const customer = await tenantFactory.create('customer');
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.post('/api/accounting/make-journal-entries')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.send({
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
journal_number: '1000',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
credit: null,
|
||||||
|
debit: 1000,
|
||||||
|
account_id: account1.id,
|
||||||
|
contact_type: 'customer',
|
||||||
|
contact_id: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: 2,
|
||||||
|
credit: 1000,
|
||||||
|
debit: 0,
|
||||||
|
account_id: account1.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.status).equals(400);
|
||||||
|
expect(res.body.errors).include.something.deep.equals({
|
||||||
|
type: 'CUSTOMERS.ACCOUNTS.NOT.RECEIVABLE.TYPE',
|
||||||
|
code: 700,
|
||||||
|
indexes: [1]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('Should store manual journal transaction to the storage.', async () => {
|
it('Should store manual journal transaction to the storage.', async () => {
|
||||||
const account1 = await tenantFactory.create('account');
|
const account1 = await tenantFactory.create('account');
|
||||||
const account2 = await tenantFactory.create('account');
|
const account2 = await tenantFactory.create('account');
|
||||||
@@ -183,10 +271,12 @@ describe('routes: `/accounting`', () => {
|
|||||||
description: 'Description here.',
|
description: 'Description here.',
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
|
index: 1,
|
||||||
credit: 1000,
|
credit: 1000,
|
||||||
account_id: account1.id,
|
account_id: account1.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
index: 2,
|
||||||
debit: 1000,
|
debit: 1000,
|
||||||
account_id: account2.id,
|
account_id: account2.id,
|
||||||
},
|
},
|
||||||
@@ -194,7 +284,6 @@ describe('routes: `/accounting`', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const foundManualJournal = await ManualJournal.tenant().query();
|
const foundManualJournal = await ManualJournal.tenant().query();
|
||||||
|
|
||||||
expect(foundManualJournal.length).equals(1);
|
expect(foundManualJournal.length).equals(1);
|
||||||
|
|
||||||
expect(foundManualJournal[0].reference).equals('2000');
|
expect(foundManualJournal[0].reference).equals('2000');
|
||||||
@@ -221,11 +310,13 @@ describe('routes: `/accounting`', () => {
|
|||||||
memo: 'Description here.',
|
memo: 'Description here.',
|
||||||
entries: [
|
entries: [
|
||||||
{
|
{
|
||||||
|
index: 1,
|
||||||
credit: 1000,
|
credit: 1000,
|
||||||
account_id: account1.id,
|
account_id: account1.id,
|
||||||
note: 'First note',
|
note: 'First note',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
index: 2,
|
||||||
debit: 1000,
|
debit: 1000,
|
||||||
account_id: account2.id,
|
account_id: account2.id,
|
||||||
note: 'Second note',
|
note: 'Second note',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '~/dbInit';
|
} from '~/dbInit';
|
||||||
|
|
||||||
|
|
||||||
describe('routes: /accounts/', () => {
|
describe.only('routes: /accounts/', () => {
|
||||||
describe('POST `/accounts`', () => {
|
describe('POST `/accounts`', () => {
|
||||||
it('Should `name` be required.', async () => {
|
it('Should `name` be required.', async () => {
|
||||||
const res = await request()
|
const res = await request()
|
||||||
@@ -190,7 +190,7 @@ describe('routes: /accounts/', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should response success with correct data form.', async () => {
|
it.only('Should response success with correct data form.', async () => {
|
||||||
const account = await tenantFactory.create('account');
|
const account = await tenantFactory.create('account');
|
||||||
const res = await request()
|
const res = await request()
|
||||||
.post('/api/accounts')
|
.post('/api/accounts')
|
||||||
@@ -204,6 +204,8 @@ describe('routes: /accounts/', () => {
|
|||||||
code: '123',
|
code: '123',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(res.body);
|
||||||
|
|
||||||
expect(res.status).equals(200);
|
expect(res.status).equals(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
0
server/tests/routes/payable_aging.test.js
Normal file
0
server/tests/routes/payable_aging.test.js
Normal file
234
server/tests/routes/receivable_aging.test.js
Normal file
234
server/tests/routes/receivable_aging.test.js
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import {
|
||||||
|
request,
|
||||||
|
expect,
|
||||||
|
} from '~/testInit';
|
||||||
|
import Item from '@/models/Item';
|
||||||
|
import {
|
||||||
|
tenantWebsite,
|
||||||
|
tenantFactory,
|
||||||
|
loginRes
|
||||||
|
} from '~/dbInit';
|
||||||
|
|
||||||
|
|
||||||
|
describe('routes: `/financial_statements/receivable_aging_summary`', () => {
|
||||||
|
|
||||||
|
it('Should retrieve customers list.', async () => {
|
||||||
|
const customer1 = await tenantFactory.create('customer', { display_name: 'Ahmed' });
|
||||||
|
const customer2 = await tenantFactory.create('customer', { display_name: 'Mohamed' });
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/receivable_aging_summary')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.status).equals(200);
|
||||||
|
|
||||||
|
expect(res.body.aging.customers).is.an('array');
|
||||||
|
expect(res.body.aging.customers.length).equals(2);
|
||||||
|
|
||||||
|
expect(res.body.aging.customers[0].customer_name).equals('Ahmed');
|
||||||
|
expect(res.body.aging.customers[1].customer_name).equals('Mohamed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should respon se the customers ids not found.', async () => {
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/receivable_aging_summary')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
customer_ids: [3213, 3322],
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.status).equals(400);
|
||||||
|
expect(res.body.errors).include.something.deep.equals({
|
||||||
|
type: 'CUSTOMERS.IDS.NOT.FOUND', code: 300, ids: [3213, 3322]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should retrieve aging report columns.', async () => {
|
||||||
|
const customer1 = await tenantFactory.create('customer', { display_name: 'Ahmed' });
|
||||||
|
const customer2 = await tenantFactory.create('customer', { display_name: 'Mohamed' });
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/receivable_aging_summary')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
as_date: '2020-06-01',
|
||||||
|
aging_days_before: 30,
|
||||||
|
aging_periods: 6,
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.body.columns).length(6);
|
||||||
|
expect(res.body.columns[0].before_days).equals(0);
|
||||||
|
expect(res.body.columns[0].to_days).equals(30);
|
||||||
|
|
||||||
|
expect(res.body.columns[1].before_days).equals(31);
|
||||||
|
expect(res.body.columns[1].to_days).equals(60);
|
||||||
|
|
||||||
|
expect(res.body.columns[2].before_days).equals(61);
|
||||||
|
expect(res.body.columns[2].to_days).equals(90);
|
||||||
|
|
||||||
|
expect(res.body.columns[3].before_days).equals(91);
|
||||||
|
expect(res.body.columns[3].to_days).equals(120);
|
||||||
|
|
||||||
|
expect(res.body.columns[4].before_days).equals(121);
|
||||||
|
expect(res.body.columns[4].to_days).equals(150);
|
||||||
|
|
||||||
|
expect(res.body.columns[5].before_days).equals(151);
|
||||||
|
expect(res.body.columns[5].to_days).equals(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should retrieve receivable total of the customers.', async () => {
|
||||||
|
const customer1 = await tenantFactory.create('customer', { display_name: 'Ahmed' });
|
||||||
|
const customer2 = await tenantFactory.create('customer', { display_name: 'Mohamed' });
|
||||||
|
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
contact_id: customer1.id,
|
||||||
|
contact_type: 'customer',
|
||||||
|
debit: 10000,
|
||||||
|
credit: 0,
|
||||||
|
account_id: 10,
|
||||||
|
date: '2020-01-01',
|
||||||
|
});
|
||||||
|
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
contact_id: customer1.id,
|
||||||
|
contact_type: 'customer',
|
||||||
|
debit: 1000,
|
||||||
|
credit: 0,
|
||||||
|
account_id: 10,
|
||||||
|
date: '2020-03-15',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receive
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
contact_id: customer1.id,
|
||||||
|
contact_type: 'customer',
|
||||||
|
debit: 0,
|
||||||
|
credit: 8000,
|
||||||
|
account_id: 10,
|
||||||
|
date: '2020-06-01',
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/receivable_aging_summary')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
as_date: '2020-06-01',
|
||||||
|
aging_days_before: 30,
|
||||||
|
aging_periods: 6,
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.body.aging.total[0].total).equals(0);
|
||||||
|
expect(res.body.aging.total[1].total).equals(0);
|
||||||
|
expect(res.body.aging.total[2].total).equals(1000);
|
||||||
|
expect(res.body.aging.total[3].total).equals(0);
|
||||||
|
expect(res.body.aging.total[4].total).equals(0);
|
||||||
|
expect(res.body.aging.total[5].total).equals(2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('Should retrieve customer aging.', async () => {
|
||||||
|
const customer1 = await tenantFactory.create('customer', { display_name: 'Ahmed' });
|
||||||
|
const customer2 = await tenantFactory.create('customer', { display_name: 'Mohamed' });
|
||||||
|
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
contact_id: customer1.id,
|
||||||
|
contact_type: 'customer',
|
||||||
|
debit: 10000,
|
||||||
|
credit: 0,
|
||||||
|
account_id: 10,
|
||||||
|
date: '2020-01-14',
|
||||||
|
});
|
||||||
|
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
contact_id: customer1.id,
|
||||||
|
contact_type: 'customer',
|
||||||
|
debit: 1000,
|
||||||
|
credit: 0,
|
||||||
|
account_id: 10,
|
||||||
|
date: '2020-03-15',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receive
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
contact_id: customer1.id,
|
||||||
|
contact_type: 'customer',
|
||||||
|
debit: 0,
|
||||||
|
credit: 8000,
|
||||||
|
account_id: 10,
|
||||||
|
date: '2020-06-01',
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/receivable_aging_summary')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
as_date: '2020-06-01',
|
||||||
|
aging_days_before: 30,
|
||||||
|
aging_periods: 6,
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.body.aging.customers[0].aging[0].total).equals(0);
|
||||||
|
expect(res.body.aging.customers[0].aging[1].total).equals(0);
|
||||||
|
expect(res.body.aging.customers[0].aging[2].total).equals(1000);
|
||||||
|
expect(res.body.aging.customers[0].aging[3].total).equals(0);
|
||||||
|
expect(res.body.aging.customers[0].aging[4].total).equals(2000);
|
||||||
|
expect(res.body.aging.customers[0].aging[5].total).equals(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should retrieve the queried customers ids only.', async () => {
|
||||||
|
const customer1 = await tenantFactory.create('customer', { display_name: 'Ahmed' });
|
||||||
|
const customer2 = await tenantFactory.create('customer', { display_name: 'Mohamed' });
|
||||||
|
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
contact_id: customer1.id,
|
||||||
|
contact_type: 'customer',
|
||||||
|
debit: 10000,
|
||||||
|
credit: 0,
|
||||||
|
account_id: 10,
|
||||||
|
date: '2020-01-14',
|
||||||
|
});
|
||||||
|
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
contact_id: customer1.id,
|
||||||
|
contact_type: 'customer',
|
||||||
|
debit: 1000,
|
||||||
|
credit: 0,
|
||||||
|
account_id: 10,
|
||||||
|
date: '2020-03-15',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Receive
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
contact_id: customer1.id,
|
||||||
|
contact_type: 'customer',
|
||||||
|
debit: 0,
|
||||||
|
credit: 8000,
|
||||||
|
account_id: 10,
|
||||||
|
date: '2020-06-01',
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/receivable_aging_summary')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
as_date: '2020-06-01',
|
||||||
|
aging_days_before: 30,
|
||||||
|
aging_periods: 6,
|
||||||
|
customer_ids: [customer1.id],
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.body.aging.customers.length).equals(1);
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '~/dbInit';
|
} from '~/dbInit';
|
||||||
import Vendor from '@/models/Vendor';
|
import Vendor from '@/models/Vendor';
|
||||||
|
|
||||||
describe.only('route: `/vendors`', () => {
|
describe('route: `/vendors`', () => {
|
||||||
describe('POST: `/vendors`', () => {
|
describe('POST: `/vendors`', () => {
|
||||||
it('Should response unauthorized in case the user was not logged in.', async () => {
|
it('Should response unauthorized in case the user was not logged in.', async () => {
|
||||||
const res = await request()
|
const res = await request()
|
||||||
|
|||||||
Reference in New Issue
Block a user