Merge branch 'feature/viewDetail'

This commit is contained in:
elforjani3
2021-08-01 17:42:32 +02:00
125 changed files with 3987 additions and 1896 deletions

View File

@@ -11,7 +11,7 @@ const CheckboxEditableCell = ({
const [value, setValue] = React.useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
setValue(e.target.checked);
};
const onBlur = () => {
payload.updateData(index, id, value);
@@ -24,12 +24,13 @@ const CheckboxEditableCell = ({
return (
<FormGroup
// intent={error ? Intent.DANGER : null}
intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<Checkbox
value={value}
onChange={onChange}
checked={initialValue}
onBlur={onBlur}
minimal={true}
className="ml2"

View File

@@ -7,6 +7,10 @@ import AccountDrawer from 'containers/Drawers/AccountDrawer';
import ManualJournalDrawer from 'containers/Drawers/ManualJournalDrawer';
import ExpenseDrawer from 'containers/Drawers/ExpenseDrawer';
import BillDrawer from 'containers/Drawers/BillDrawer';
import InvoiceDetailDrawer from 'containers/Drawers/InvoiceDetailDrawer';
import ReceiptDetailDrawer from 'containers/Drawers/ReceiptDetailDrawer';
import PaymentReceiveDetailDrawer from 'containers/Drawers/PaymentReceiveDetailDrawer';
import PaymentMadeDetailDrawer from 'containers/Drawers/PaymentMadeDetailDrawer';
export default function DrawersContainer() {
return (
@@ -19,6 +23,10 @@ export default function DrawersContainer() {
<ManualJournalDrawer name={'journal-drawer'} />
<ExpenseDrawer name={'expense-drawer'} />
<BillDrawer name={'bill-drawer'} />
<InvoiceDetailDrawer name={'invoice-detail-drawer'} />
<ReceiptDetailDrawer name={'receipt-detail-drawer'} />
<PaymentReceiveDetailDrawer name={'payment-receive-detail-drawer'} />
<PaymentMadeDetailDrawer name={'payment-made-detail-drawer'} />
</div>
);
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import 'style/components/Drawers/BillDrawer.scss';
import 'style/components/Drawers/ViewDetail/ViewDetail.scss';
import { BillDrawerProvider } from './BillDrawerProvider';
import BillDrawerDetails from './BillDrawerDetails';

View File

@@ -3,15 +3,24 @@ import { Tabs, Tab } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import LocatedLandedCostTable from './LocatedLandedCostTable';
import JournalEntriesTable from '../../JournalEntriesTable/JournalEntriesTable';
import { useBillDrawerContext } from './BillDrawerProvider';
/**
* Bill view details.
*/
export default function BillDrawerDetails() {
const { data } = useBillDrawerContext();
return (
<div className="bill-drawer">
<Tabs animate={true} large={true} selectedTabId="landed_cost">
<div className="view-detail-drawer">
<Tabs animate={true} large={true} defaultSelectedTabId="landed_cost">
<Tab title={intl.get('details')} id={'details'} disabled={true} />
<Tab
title={intl.get('journal_entries')}
id={'journal_entries'}
panel={<JournalEntriesTable transactions={data} />}
/>
<Tab
title={intl.get('located_landed_cost')}
id={'landed_cost'}

View File

@@ -2,6 +2,7 @@ import React from 'react';
import intl from 'react-intl-universal';
import { DrawerHeaderContent, DashboardInsider } from 'components';
import { useBillLocatedLandedCost } from 'hooks/query';
import { useTransactionsByReference } from 'hooks/query';
const BillDrawerContext = React.createContext();
@@ -9,6 +10,16 @@ const BillDrawerContext = React.createContext();
* Bill drawer provider.
*/
function BillDrawerProvider({ billId, ...props }) {
// Handle fetch transaction by reference.
const { data, isLoading: isTransactionLoading } = useTransactionsByReference(
{
reference_id: billId,
reference_type: 'Bill',
},
{ enabled: !!billId },
);
// Handle fetch bill located landed cost transaction.
const { isLoading: isLandedCostLoading, data: transactions } =
useBillLocatedLandedCost(billId, {
@@ -19,10 +30,11 @@ function BillDrawerProvider({ billId, ...props }) {
const provider = {
transactions,
billId,
data,
};
return (
<DashboardInsider loading={isLandedCostLoading}>
<DashboardInsider loading={isLandedCostLoading || isTransactionLoading}>
<DrawerHeaderContent
name="bill-drawer"
title={intl.get('bill_details')}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { Tabs, Tab } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import JournalEntriesTable from '../../JournalEntriesTable/JournalEntriesTable';
import { useInvoiceDetailDrawerContext } from './InvoiceDetailDrawerProvider';
/**
* Invoice view detail.
*/
export default function InvoiceDetail() {
const { data } = useInvoiceDetailDrawerContext();
return (
<div className="view-detail-drawer">
<Tabs
animate={true}
large={true}
defaultSelectedTabId="journal_entries"
renderActiveTabPanelOnly={false}
>
<Tab title={intl.get('details')} disabled={true} />
<Tab
title={intl.get('journal_entries')}
id={'journal_entries'}
panel={<JournalEntriesTable transactions={data} />}
/>
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
import 'style/components/Drawers/ViewDetail/ViewDetail.scss';
import InvoiceDetail from './InvoiceDetail';
import { InvoiceDetailDrawerProvider } from './InvoiceDetailDrawerProvider';
/**
* Invoice detail drawer content.
*/
export default function InvoiceDetailDrawerContent({
// #ownProp
invoice,
}) {
return (
<InvoiceDetailDrawerProvider invoiceId={invoice}>
<InvoiceDetail />
</InvoiceDetailDrawerProvider>
);
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import intl from 'react-intl-universal';
import { DrawerHeaderContent, DashboardInsider } from 'components';
import { useTransactionsByReference } from 'hooks/query';
const InvoiceDetailDrawerContext = React.createContext();
/**
* Invoice detail provider.
*/
function InvoiceDetailDrawerProvider({ invoiceId, ...props }) {
// Handle fetch transaction by reference.
const { data, isLoading: isTransactionLoading } = useTransactionsByReference(
{
reference_id: invoiceId,
reference_type: 'SaleInvoice',
},
{ enabled: !!invoiceId },
);
//provider.
const provider = {
data,
};
return (
<DashboardInsider loading={isTransactionLoading}>
<DrawerHeaderContent
name="invoice-detail-drawer"
title={intl.get('invoice_details')}
/>
<InvoiceDetailDrawerContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useInvoiceDetailDrawerContext = () =>
React.useContext(InvoiceDetailDrawerContext);
export { InvoiceDetailDrawerProvider, useInvoiceDetailDrawerContext };

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { Drawer, DrawerSuspense } from 'components';
import withDrawers from 'containers/Drawer/withDrawers';
import { compose } from 'utils';
const InvoiceDetailDrawerContent = React.lazy(() =>
import('./InvoiceDetailDrawerContent'),
);
/**
* Invoice Detail drawer.
*/
function InvoiceDetailDrawer({
name,
// #withDrawer
isOpen,
payload: { invoiceId },
}) {
return (
<Drawer isOpen={isOpen} name={name} size={'750px'}>
<DrawerSuspense>
<InvoiceDetailDrawerContent invoice={invoiceId} />
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(InvoiceDetailDrawer);

View File

@@ -0,0 +1,20 @@
import React from 'react';
import 'style/components/Drawers/ViewDetail/ViewDetail.scss';
import PaymentMadeDetails from './PaymentMadeDetails';
import { PaymentMadeDetailProvider } from './PaymentMadeDetailProvider';
/**
* Payment made detail content.
*/
export default function PaymentMadeDetailContent({
// #ownProp
paymentMade,
}) {
return (
<PaymentMadeDetailProvider paymentMadeId={paymentMade}>
<PaymentMadeDetails />
</PaymentMadeDetailProvider>
);
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import intl from 'react-intl-universal';
import { DrawerHeaderContent, DashboardInsider } from 'components';
import { useTransactionsByReference } from 'hooks/query';
const PaymentMadeDetailContext = React.createContext();
/**
* Payment made detail provider.
*/
function PaymentMadeDetailProvider({ paymentMadeId, ...props }) {
// Handle fetch transaction by reference.
const { data, isLoading: isTransactionLoading } = useTransactionsByReference(
{
reference_id: paymentMadeId,
reference_type: 'paymentMade',
},
{ enabled: !!paymentMadeId },
);
//provider.
const provider = {
data,
};
return (
<DashboardInsider loading={isTransactionLoading}>
<DrawerHeaderContent
name="payment-made-detail-drawer"
title={intl.get('payment_made_details')}
/>
<PaymentMadeDetailContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const usePaymentMadeDetailContext = () =>
React.useContext(PaymentMadeDetailContext);
export { PaymentMadeDetailProvider, usePaymentMadeDetailContext };

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { Tabs, Tab } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import JournalEntriesTable from '../../JournalEntriesTable/JournalEntriesTable';
/**
* payment made view detail.
*/
export default function PaymentMadeDetails() {
return (
<div className="view-detail-drawer">
<Tabs animate={true} large={true} defaultSelectedTabId="journal_entries">
<Tab title={intl.get('details')} id={'details'} disabled={true} />
<Tab
title={intl.get('journal_entries')}
id={'journal_entries'}
panel={<JournalEntriesTable journal={[]} />}
/>
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { Drawer, DrawerSuspense } from 'components';
import withDrawers from 'containers/Drawer/withDrawers';
import { compose } from 'utils';
const PaymentMadeDetailContent = React.lazy(() =>
import('./PaymentMadeDetailContent'),
);
/**
* Payment made detail drawer.
*/
function PaymentMadeDetailDrawer({
name,
// #withDrawer
isOpen,
payload: { paymentMadeId },
}) {
return (
<Drawer isOpen={isOpen} name={name} size={'750px'}>
<DrawerSuspense>
<PaymentMadeDetailContent paymentMade={paymentMadeId} />
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(PaymentMadeDetailDrawer);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { Tabs, Tab } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import JournalEntriesTable from '../../JournalEntriesTable/JournalEntriesTable';
import { usePaymentReceiveDetailContext } from './PaymentReceiveDetailProvider';
/**
* payment receive view detail.
*/
export default function PaymentReceiveDetail() {
const { data } = usePaymentReceiveDetailContext();
return (
<div className="view-detail-drawer">
<Tabs animate={true} large={true} defaultSelectedTabId="journal_entries">
<Tab title={intl.get('details')} id={'details'} disabled={true} />
<Tab
title={intl.get('journal_entries')}
id={'journal_entries'}
panel={<JournalEntriesTable transactions={data} />}
/>
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,21 @@
import React from 'react';
import 'style/components/Drawers/ViewDetail/ViewDetail.scss';
import PaymentReceiveDetail from './PaymentReceiveDetail';
import { PaymentReceiveDetailProvider } from './PaymentReceiveDetailProvider';
/**
* Payment receive detail content.
*/
export default function PaymentReceiveDetailContent({
// #ownProp
paymentReceive,
}) {
return (
<PaymentReceiveDetailProvider paymentReceiveId={paymentReceive}>
<PaymentReceiveDetail />
</PaymentReceiveDetailProvider>
);
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import intl from 'react-intl-universal';
import { DrawerHeaderContent, DashboardInsider } from 'components';
import { useTransactionsByReference } from 'hooks/query';
const PaymentReceiveDetailContext = React.createContext();
/**
* Payment receive detail provider.
*/
function PaymentReceiveDetailProvider({ paymentReceiveId, ...props }) {
// Handle fetch transaction by reference.
const { data, isLoading: isTransactionLoading } = useTransactionsByReference(
{
reference_id: paymentReceiveId,
reference_type: 'paymentReceive',
},
{ enabled: !!paymentReceiveId },
);
//provider.
const provider = { data };
return (
<DashboardInsider loading={isTransactionLoading}>
<DrawerHeaderContent
name="payment-receive-detail-drawer"
title={intl.get('payment_receive_details')}
/>
<PaymentReceiveDetailContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const usePaymentReceiveDetailContext = () =>
React.useContext(PaymentReceiveDetailContext);
export { PaymentReceiveDetailProvider, usePaymentReceiveDetailContext };

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { Drawer, DrawerSuspense } from 'components';
import withDrawers from 'containers/Drawer/withDrawers';
import { compose } from 'utils';
const PaymentReceiveDetailContent = React.lazy(() =>
import('./PaymentReceiveDetailContent'),
);
/**
* Payment receive detail drawer
*/
function PaymentReceiveDetailDrawer({
name,
// #withDrawer
isOpen,
payload: { paymentReceiveId },
}) {
return (
<Drawer isOpen={isOpen} name={name} size={'750px'}>
<DrawerSuspense>
<PaymentReceiveDetailContent paymentReceive={paymentReceiveId} />
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(PaymentReceiveDetailDrawer);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { Tabs, Tab } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import JournalEntriesTable from '../../JournalEntriesTable/JournalEntriesTable';
import { useReceiptDetailDrawerContext } from './ReceiptDetailDrawerProvider';
/**
* Receipt view detail.
*/
export default function ReceiptDetail() {
const { data } = useReceiptDetailDrawerContext();
return (
<div className="view-detail-drawer">
<Tabs animate={true} large={true} defaultSelectedTabId="journal_entries">
<Tab title={intl.get('details')} id={'details'} disabled={true} />
<Tab
title={intl.get('journal_entries')}
id={'journal_entries'}
panel={<JournalEntriesTable transactions={data} />}
/>
</Tabs>
</div>
);
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
import 'style/components/Drawers/ViewDetail/ViewDetail.scss';
import ReceiptDetail from './ReceiptDetail';
import { ReceiptDetailDrawerProvider } from './ReceiptDetailDrawerProvider';
/**
* Receipt detail drawer content.
*/
export default function ReceiptDetailDrawerContent({
// #ownProp
receipt,
}) {
return (
<ReceiptDetailDrawerProvider receiptId={receipt}>
<ReceiptDetail />
</ReceiptDetailDrawerProvider>
);
}

View File

@@ -0,0 +1,40 @@
import React from 'react';
import intl from 'react-intl-universal';
import { DrawerHeaderContent, DashboardInsider } from 'components';
import { useTransactionsByReference } from 'hooks/query';
// useTransactionsByReference
const ReceiptDetailDrawerContext = React.createContext();
/**
* Receipt detail provider.
*/
function ReceiptDetailDrawerProvider({ receiptId, ...props }) {
// Handle fetch transaction by reference.
const { data, isLoading: isTransactionLoading } = useTransactionsByReference(
{
reference_id: receiptId,
reference_type: 'SaleReceipt',
},
{ enabled: !!receiptId },
);
//provider.
const provider = {
data,
};
return (
<DashboardInsider loading={isTransactionLoading}>
<DrawerHeaderContent
name="receipt-detail-drawer"
title={intl.get('receipt_details')}
/>
<ReceiptDetailDrawerContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useReceiptDetailDrawerContext = () =>
React.useContext(ReceiptDetailDrawerContext);
export { ReceiptDetailDrawerProvider, useReceiptDetailDrawerContext };

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { Drawer, DrawerSuspense } from 'components';
import withDrawers from 'containers/Drawer/withDrawers';
import { compose } from 'utils';
const ReceiptDetailDrawerContent = React.lazy(() =>
import('./ReceiptDetailDrawerContent'),
);
/**
* Receipt Detail drawer.
*/
function ReceiptDetailDrawer({
name,
// #withDrawer
isOpen,
payload: { receiptId },
}) {
return (
<Drawer isOpen={isOpen} name={name} size={'750px'}>
<DrawerSuspense>
<ReceiptDetailDrawerContent receipt={receiptId} />
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(ReceiptDetailDrawer);

View File

@@ -79,10 +79,7 @@ function ExpenseForm({
}
const categories = values.categories.filter(
(category) =>
category.amount &&
category.index &&
category.expense_account_id &&
category.landed_cost,
category.amount && category.index && category.expense_account_id,
);
const form = {

View File

@@ -31,7 +31,7 @@ export const defaultExpenseEntry = {
amount: '',
expense_account_id: '',
description: '',
landed_cost: false,
landed_cost: 0,
};
export const defaultExpense = {

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { DataTable, Card } from 'components';
import intl from 'react-intl-universal';
import moment from 'moment';
/**
* Journal entries table.
*/
export default function JournalEntriesTable({ transactions }) {
const columns = React.useMemo(
() => [
{
Header: intl.get('date'),
accessor: 'date',
// accessor: (r) => moment(new Date()).format('YYYY MMM DD'),
width: 150,
},
{
Header: intl.get('account_name'),
accessor: 'accountName',
width: 150,
},
{
Header: intl.get('contact'),
accessor: 'contactTypeFormatted',
width: 150,
},
{
Header: intl.get('credit'),
accessor: ({ credit }) => credit.formattedAmount,
width: 100,
},
{
Header: intl.get('debit'),
accessor: ({ debit }) => debit.formattedAmount,
width: 100,
},
],
[],
);
return (
<Card>
<DataTable
columns={columns}
data={transactions}
className={'datatable--journal-entries'}
/>
</Card>
);
}

View File

@@ -12,6 +12,7 @@ import withPaymentMadeActions from './withPaymentMadeActions';
import withPaymentMade from './withPaymentMade';
import withSettings from 'containers/Settings/withSettings';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { usePaymentMadesTableColumns, ActionsMenu } from './components';
import { usePaymentMadesListContext } from './PaymentMadesListProvider';
@@ -27,6 +28,9 @@ function PaymentMadesTable({
// #withAlerts
openAlert,
// #withDrawerActions
openDrawer,
}) {
// Payment mades table columns.
const columns = usePaymentMadesTableColumns();
@@ -53,6 +57,11 @@ function PaymentMadesTable({
openAlert('payment-made-delete', { paymentMadeId: paymentMade.id });
};
// Handle view detail payment made.
const handleViewDetailPaymentMade = ({ id }) => {
openDrawer('payment-receive-detail-drawer', { paymentMadeId: id });
};
// Handle datatable fetch data once the table state change.
const handleDataTableFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
@@ -89,6 +98,7 @@ function PaymentMadesTable({
payload={{
onEdit: handleEditPaymentMade,
onDelete: handleDeletePaymentMade,
onViewDetails: handleViewDetailPaymentMade,
}}
/>
);
@@ -98,6 +108,7 @@ export default compose(
withPaymentMadeActions,
withPaymentMade(({ paymentMadesTableState }) => ({ paymentMadesTableState })),
withAlertsActions,
withDrawerActions,
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),

View File

@@ -26,15 +26,14 @@ export function AmountAccessor(row) {
*/
export function ActionsMenu({
row: { original },
payload: { onEdit, onDelete },
payload: { onEdit, onDelete, onViewDetails },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, original)}
/>
<MenuDivider />
<MenuItem
@@ -70,8 +69,6 @@ export function ActionsCell(props) {
* Retrieve payment mades table columns.
*/
export function usePaymentMadesTableColumns() {
return React.useMemo(
() => [
{

View File

@@ -79,6 +79,12 @@ function InvoicesDataTable({
const handleQuickPaymentReceive = ({ id }) => {
openDialog('quick-payment-receive', { invoiceId: id });
};
// Handle view detail invoice.
const handleViewDetailInvoice = ({ id }) => {
openDrawer('invoice-detail-drawer', { invoiceId: id });
};
// Handles fetch data once the table state change.
const handleDataTableFetchData = useCallback(
({ pageSize, pageIndex, sortBy }) => {
@@ -123,6 +129,7 @@ function InvoicesDataTable({
onEdit: handleEditInvoice,
onDrawer: handleDrawerInvoice,
onQuick: handleQuickPaymentReceive,
onViewDetails: handleViewDetailInvoice,
baseCurrency,
}}
/>

View File

@@ -95,7 +95,7 @@ export const handleDeleteErrors = (errors) => {
};
export function ActionsMenu({
payload: { onEdit, onDeliver, onDelete, onDrawer, onQuick },
payload: { onEdit, onDeliver, onDelete, onDrawer, onQuick, onViewDetails },
row: { original },
}) {
return (
@@ -103,6 +103,7 @@ export function ActionsMenu({
<MenuItem
icon={<Icon icon="reader-18" />}
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, original)}
/>
<MenuDivider />
<MenuItem

View File

@@ -62,6 +62,11 @@ function PaymentReceivesDataTable({
openDrawer('payment-receive-drawer', { paymentReceiveId: id });
};
// Handle view detail payment receive..
const handleViewDetailPaymentReceive = ({ id }) => {
openDrawer('payment-receive-detail-drawer', { paymentReceiveId: id });
};
// Handle datatable fetch once the table's state changing.
const handleDataTableFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
@@ -103,6 +108,7 @@ function PaymentReceivesDataTable({
onDelete: handleDeletePaymentReceive,
onEdit: handleEditPaymentReceive,
onDrawer: handleDrawerPaymentReceive,
onViewDetails: handleViewDetailPaymentReceive,
}}
/>
);

View File

@@ -19,15 +19,14 @@ import { safeCallback } from 'utils';
*/
export function ActionsMenu({
row: { original: paymentReceive },
payload: { onEdit, onDelete, onDrawer },
payload: { onEdit, onDelete, onDrawer, onViewDetails },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, paymentReceive)}
/>
<MenuDivider />
<MenuItem
@@ -82,8 +81,6 @@ export function ActionsCell(props) {
* Retrieve payment receives columns.
*/
export function usePaymentReceivesColumns() {
return React.useMemo(
() => [
{
@@ -128,7 +125,7 @@ export function usePaymentReceivesColumns() {
accessor: 'reference_no',
width: 140,
className: 'reference_no',
}
},
],
[],
);

View File

@@ -60,14 +60,19 @@ function ReceiptsDataTable({
openAlert('receipt-delete', { receiptId: receipt.id });
};
// Handle drawer receipts.
const handleDrawerReceipt = ({ id }) => {
openDrawer('receipt-drawer', { receiptId: id });
};
// Handles receipt close action.
const handleCloseReceipt = (receipt) => {
openAlert('receipt-close', { receiptId: receipt.id });
};
// Handle drawer receipts.
const handleDrawerReceipt = ({ id }) => {
openDrawer('receipt-drawer', { receiptId: id });
// Handle view detail receipt.
const handleViewDetailReceipt = ({ id }) => {
openDrawer('receipt-detail-drawer', { receiptId: id });
};
// Handles the datable fetch data once the state changing.
@@ -112,6 +117,7 @@ function ReceiptsDataTable({
onDelete: handleDeleteReceipt,
onClose: handleCloseReceipt,
onDrawer: handleDrawerReceipt,
onViewDetails: handleViewDetailReceipt,
baseCurrency,
}}
/>

View File

@@ -16,16 +16,15 @@ import { Choose, Money, Icon, If } from 'components';
import moment from 'moment';
export function ActionsMenu({
payload: { onEdit, onDelete, onClose, onDrawer },
payload: { onEdit, onDelete, onClose, onDrawer, onViewDetails },
row: { original: receipt },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, receipt)}
/>
<MenuDivider />
<MenuItem
@@ -94,8 +93,6 @@ export function StatusAccessor(receipt) {
* Retrieve receipts table columns.
*/
export function useReceiptsTableColumns() {
return React.useMemo(
() => [
{

View File

@@ -466,3 +466,22 @@ export function useInventoryItemDetailsReport(query, props) {
},
);
}
/**
* Retrieve transactions by reference report.
*/
export function useTransactionsByReference(query, props) {
return useRequestQuery(
[t.TRANSACTIONS_BY_REFERENCE, query],
{
method: 'get',
url: `/financial_statements/transactions-by-reference`,
params: query,
},
{
select: (res) => res.data.data,
defaultData: {},
...props,
},
);
}

View File

@@ -23,6 +23,7 @@ const FINANCIAL_REPORTS = {
INVENTORY_VALUATION: 'INVENTORY_VALUATION',
CASH_FLOW_STATEMENT: 'CASH_FLOW_STATEMENT',
INVENTORY_ITEM_DETAILS: 'INVENTORY_ITEM_DETAILS',
TRANSACTIONS_BY_REFERENCE: 'TRANSACTIONS_BY_REFERENCE',
};
const BILLS = {

View File

@@ -1166,5 +1166,11 @@
"From transaction": "From transaction",
"Landed": "Landed",
"This options allows you to be able to add additional cost eg. freight then allocate cost to the items in your bills.": "This options allows you to be able to add additional cost eg. freight then allocate cost to the items in your bills.",
"Once your delete this located landed cost, you won't be able to restore it later, Are your sure you want to delete this transaction?": "Once your delete this located landed cost, you won't be able to restore it later, Are your sure you want to delete this transaction?"
"Once your delete this located landed cost, you won't be able to restore it later, Are your sure you want to delete this transaction?": "Once your delete this located landed cost, you won't be able to restore it later, Are your sure you want to delete this transaction?",
"journal_entries":"Journal entries",
"contact":"Contact",
"invoice_details":"Invoice details",
"receipt_details":"Receipt details",
"payment_receive_details":"Payment receive details",
"payment_made_details":"Payment made details"
}

View File

@@ -6,7 +6,8 @@ import t from 'store/types';
const getSubscriptionPlans = () => [
{
name: intl.get('Starter'),
slug: 'starter',
slug: 'free',
// slug: 'starter',
description: [
intl.get('Sale and purchase invoices.'),
intl.get('Customers/vendors accounts.'),

View File

@@ -1,11 +1,11 @@
@import '../../Base.scss';
@import '../../../Base.scss';
.bill-drawer {
.view-detail-drawer {
.bp3-tabs {
.bp3-tab-list {
position: relative;
background-color: #FFF;
background-color: #fff;
&:before {
content: '';
position: absolute;
@@ -31,10 +31,10 @@
}
}
.bp3-tab-panel{
.bp3-tab-panel {
margin-top: 0;
.card{
.card {
margin: 15px;
}
}
@@ -42,7 +42,6 @@
.datatable--landed-cost-transactions {
.table {
.tbody,
.tbody-inner {
height: auto;
@@ -55,11 +54,11 @@
.tr .td {
padding: 0.6rem;
&.amount{
&.amount {
font-weight: 600;
}
}
}
}
}
}
}

View File

@@ -29,11 +29,9 @@ export default class AccountsController extends BaseController {
router.get(
'/transactions',
[
query('account_id').optional().isInt().toInt(),
],
[query('account_id').optional().isInt().toInt()],
this.asyncMiddleware(this.accountTransactions.bind(this)),
this.catchServiceErrors,
this.catchServiceErrors
);
router.post(
'/:id/activate',
@@ -136,6 +134,8 @@ export default class AccountsController extends BaseController {
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('inactive_mode').optional().isBoolean().toBoolean(),
];
}
@@ -213,7 +213,9 @@ export default class AccountsController extends BaseController {
tenantId,
accountId
);
return res.status(200).send({ account: this.transfromToResponse(account) });
return res
.status(200)
.send({ account: this.transfromToResponse(account) });
} catch (error) {
next(error);
}
@@ -256,7 +258,7 @@ export default class AccountsController extends BaseController {
return res.status(200).send({
id: accountId,
message: 'The account has been activated successfully.'
message: 'The account has been activated successfully.',
});
} catch (error) {
next(error);
@@ -291,22 +293,24 @@ export default class AccountsController extends BaseController {
* @param {Response} res
* @param {Response}
*/
async getAccountsList(req: Request, res: Response, next: NextFunction) {
public async getAccountsList(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter: IAccountsFilter = {
filterRoles: [],
// Filter query.
const filter = {
sortOrder: 'asc',
columnSortBy: 'name',
inactiveMode: false,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const {
accounts,
filterMeta,
} = await this.accountsService.getAccountsList(tenantId, filter);
const { accounts, filterMeta } =
await this.accountsService.getAccountsList(tenantId, filter);
return res.status(200).send({
accounts: this.transfromToResponse(accounts, 'accountTypeLabel', req),
@@ -343,9 +347,9 @@ export default class AccountsController extends BaseController {
/**
* Retrieve accounts transactions list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
async accountTransactions(req: Request, res: Response, next: NextFunction) {
@@ -353,10 +357,11 @@ export default class AccountsController extends BaseController {
const transactionsFilter = this.matchedQueryData(req);
try {
const { transactions } = await this.accountsService.getAccountsTransactions(
tenantId,
transactionsFilter
);
const { transactions } =
await this.accountsService.getAccountsTransactions(
tenantId,
transactionsFilter
);
return res.status(200).send({
transactions: this.transfromToResponse(transactions),
});
@@ -372,7 +377,12 @@ export default class AccountsController extends BaseController {
* @param {Response} res
* @param {ServiceError} error
*/
catchServiceErrors(error, req: Request, res: Response, next: NextFunction) {
private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'account_not_found') {
return res.boom.notFound('The given account not found.', {

View File

@@ -120,6 +120,8 @@ export default class CustomersController extends ContactsController {
query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(),
query('inactive_mode').optional().isBoolean().toBoolean(),
];
}
@@ -264,17 +266,15 @@ export default class CustomersController extends ContactsController {
*/
async getCustomersList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
inactiveMode: false,
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const {

View File

@@ -100,6 +100,8 @@ export default class VendorsController extends ContactsController {
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('inactive_mode').optional().isBoolean().toBoolean(),
];
}
@@ -227,8 +229,13 @@ export default class VendorsController extends ContactsController {
*/
async getVendorsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const vendorsFilter: IVendorsFilter = {
filterRoles: [],
inactiveMode: false,
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};

View File

@@ -290,16 +290,12 @@ export default class ExpensesController extends BaseController {
async getExpensesList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const { expenses, pagination, filterMeta } =

View File

@@ -17,6 +17,7 @@ import TransactionsByCustomers from './FinancialStatements/TransactionsByCustome
import TransactionsByVendors from './FinancialStatements/TransactionsByVendors';
import CashFlowStatementController from './FinancialStatements/CashFlow/CashFlow';
import InventoryDetailsController from './FinancialStatements/InventoryDetails';
import TransactionsByReferenceController from './FinancialStatements/TransactionsByReference';
@Service()
export default class FinancialStatementsService {
@@ -87,6 +88,10 @@ export default class FinancialStatementsService {
'/inventory-item-details',
Container.get(InventoryDetailsController).router(),
);
router.use(
'/transactions-by-reference',
Container.get(TransactionsByReferenceController).router(),
)
return router;
}
}

View File

@@ -32,13 +32,8 @@ export default class JournalSheetController extends BaseFinancialReportControlle
return [
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
oneOf(
[
query('transaction_types').optional().isArray({ min: 1 }),
query('transaction_types.*').optional().isNumeric().toInt(),
],
[query('transaction_types').optional().trim().escape()]
),
query('transaction_type').optional().trim().escape(),
query('transaction_id').optional().isInt().toInt(),
oneOf(
[
query('account_ids').optional().isArray({ min: 1 }),

View File

@@ -0,0 +1,87 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import BaseController from 'api/controllers/BaseController';
import TransactionsByReferenceService from 'services/FinancialStatements/TransactionsByReference';
@Service()
export default class TransactionsByReferenceController extends BaseController {
@Inject()
private transactionsByReferenceService: TransactionsByReferenceService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
this.validationSchema,
this.validationResult,
this.asyncMiddleware(this.transactionsByReference.bind(this))
);
return router;
}
/**
* Validation schema.
*/
get validationSchema(): ValidationChain[] {
return [
query('reference_id').exists().isInt(),
query('reference_type').exists().isString(),
query('number_format.precision')
.optional()
.isInt({ min: 0, max: 5 })
.toInt(),
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
query('number_format.negative_format')
.optional()
.isIn(['parentheses', 'mines'])
.trim()
.escape(),
];
}
/**
* Retrieve transactions by the given reference type and id.
* @param {Request} req - Request object.
* @param {Response} res - Response.
* @param {NextFunction} next
* @returns
*/
public async transactionsByReference(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const transactions =
await this.transactionsByReferenceService.getTransactionsByReference(
tenantId,
filter
);
const accept = this.accepts(req);
const acceptType = accept.types(['json']);
switch (acceptType) {
case 'json':
default:
return res
.status(200)
.send(this.transformToJsonResponse(transactions));
}
} catch (error) {
next(error);
}
}
private transformToJsonResponse(transactions) {
return transactions;
}
}

View File

@@ -62,6 +62,8 @@ export default class InventoryAdjustmentsController extends BaseController {
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(),
];
}

View File

@@ -199,8 +199,8 @@ export default class ItemsCategoriesController extends BaseController {
*/
async getList(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
const itemCategoriesFilter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
...this.matchedQueryData(req),

View File

@@ -185,6 +185,8 @@ export default class ItemsController extends BaseController {
query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(),
query('inactive_mode').optional().isBoolean().toBoolean(),
];
}
@@ -339,17 +341,16 @@ export default class ItemsController extends BaseController {
*/
async getItemsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
inactiveMode: false,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const {
items,

View File

@@ -288,14 +288,10 @@ export default class ManualJournalsController extends BaseController {
const filter = {
sortOrder: 'asc',
columnSortBy: 'created_at',
filterRoles: [],
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const {
manualJournals,

View File

@@ -1,5 +1,6 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import * as R from 'ramda';
import { Service, Inject } from 'typedi';
import { IBillDTO, IBillEditDTO } from 'interfaces';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
@@ -300,14 +301,11 @@ export default class BillsController extends BaseController {
const filter = {
page: 1,
pageSize: 12,
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const { bills, pagination, filterMeta } =
await this.billsService.getBills(tenantId, filter);

View File

@@ -1,16 +1,13 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import {
param,
query,
} from 'express-validator';
import { param, query } from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import BaseController from './BaseController';
import { ServiceError } from 'exceptions';
import ResourceService from 'services/Resource/ResourceService';
@Service()
export default class ResourceController extends BaseController{
export default class ResourceController extends BaseController {
@Inject()
resourcesService: ResourceService;
@@ -21,42 +18,76 @@ export default class ResourceController extends BaseController{
const router = Router();
router.get(
'/:resource_model/fields', [
...this.resourceModelParamSchema,
],
'/:resource_model/meta',
[...this.resourceModelParamSchema],
this.asyncMiddleware(this.resourceMeta.bind(this)),
this.handleServiceErrors
);
router.get(
'/:resource_model/fields',
[...this.resourceModelParamSchema],
this.validationResult,
asyncMiddleware(this.resourceFields.bind(this)),
this.handleServiceErrors
);
router.get(
'/:resource_model/data', [
...this.resourceModelParamSchema,
],
'/:resource_model/data',
[...this.resourceModelParamSchema],
this.validationResult,
asyncMiddleware(this.resourceData.bind(this)),
this.handleServiceErrors,
)
this.handleServiceErrors
);
return router;
}
get resourceModelParamSchema() {
return [
param('resource_model').exists().trim().escape(),
];
return [param('resource_model').exists().trim().escape()];
}
/**
* Retrieve resource model meta.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
* @returns {Response}
*/
private resourceMeta = (
req: Request,
res: Response,
next: NextFunction
): Response => {
const { tenantId } = req;
const { resource_model: resourceModel } = req.params;
try {
const resourceMeta = this.resourcesService.getResourceMeta(
tenantId,
resourceModel
);
return res
.status(200)
.send({ resource_meta: this.transfromToResponse(resourceMeta) });
} catch (error) {
next(error);
}
};
/**
* Retrieve resource fields of the given resource.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
resourceFields(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { resource_model: resourceModel } = req.params;
try {
const resourceFields = this.resourcesService.getResourceFields(tenantId, resourceModel);
const resourceFields = this.resourcesService.getResourceFields(
tenantId,
resourceModel
);
return res.status(200).send({
resource_fields: this.transfromToResponse(resourceFields),
@@ -68,9 +99,9 @@ export default class ResourceController extends BaseController{
/**
* Retrieve resource data of the give resource based on the given query.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async resourceData(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
@@ -78,7 +109,11 @@ export default class ResourceController extends BaseController{
const filter = req.query;
try {
const resourceData = await this.resourcesService.getResourceData(tenantId, resourceModel, filter);
const resourceData = await this.resourcesService.getResourceData(
tenantId,
resourceModel,
filter
);
return res.status(200).send({
resource_data: this.transfromToResponse(resourceData),
@@ -90,12 +125,17 @@ export default class ResourceController extends BaseController{
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) {
private handleServiceErrors(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') {
return res.status(400).send({
@@ -105,4 +145,4 @@ export default class ResourceController extends BaseController{
}
next(error);
}
};
}

View File

@@ -261,16 +261,12 @@ export default class PaymentReceivesController extends BaseController {
async getPaymentReceiveList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const {

View File

@@ -2,11 +2,11 @@ import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, matchedData } from 'express-validator';
import { Inject, Service } from 'typedi';
import { ISaleEstimateDTO } from 'interfaces';
import BaseController from 'api/controllers/BaseController'
import BaseController from 'api/controllers/BaseController';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from "exceptions";
import { ServiceError } from 'exceptions';
@Service()
export default class SalesEstimatesController extends BaseController {
@@ -23,63 +23,56 @@ export default class SalesEstimatesController extends BaseController {
const router = Router();
router.post(
'/', [
...this.estimateValidationSchema,
],
'/',
[...this.estimateValidationSchema],
this.validationResult,
asyncMiddleware(this.newEstimate.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.post(
'/:id/deliver',
[
...this.validateSpecificEstimateSchema,
],
[...this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.deliverSaleEstimate.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.post(
'/:id/approve',
[
this.validateSpecificEstimateSchema,
],
[this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.approveSaleEstimate.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id/reject',
[
this.validateSpecificEstimateSchema,
],
[this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.rejectSaleEstimate.bind(this)),
this.handleServiceErrors,
)
this.handleServiceErrors
);
router.post(
'/:id', [
'/:id',
[
...this.validateSpecificEstimateSchema,
...this.estimateValidationSchema,
],
this.validationResult,
asyncMiddleware(this.editEstimate.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.delete(
'/:id', [
this.validateSpecificEstimateSchema,
],
'/:id',
[this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.deleteEstimate.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.get(
'/:id',
this.validateSpecificEstimateSchema,
this.validationResult,
asyncMiddleware(this.getEstimate.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.get(
'/',
@@ -87,7 +80,7 @@ export default class SalesEstimatesController extends BaseController {
this.validationResult,
asyncMiddleware(this.getEstimates.bind(this)),
this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse,
this.dynamicListService.handlerErrorsToResponse
);
return router;
}
@@ -109,8 +102,14 @@ export default class SalesEstimatesController extends BaseController {
check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.quantity').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.description').optional({ nullable: true }).trim().escape(),
check('entries.*.discount').optional({ nullable: true }).isNumeric().toFloat(),
check('entries.*.description')
.optional({ nullable: true })
.trim()
.escape(),
check('entries.*.discount')
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('note').optional().trim().escape(),
check('terms_conditions').optional().trim().escape(),
@@ -122,9 +121,7 @@ export default class SalesEstimatesController extends BaseController {
* Specific sale estimate validation schema.
*/
get validateSpecificEstimateSchema() {
return [
param('id').exists().isNumeric().toInt(),
];
return [param('id').exists().isNumeric().toInt()];
}
/**
@@ -137,8 +134,8 @@ export default class SalesEstimatesController extends BaseController {
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
]
query('page_size').optional().isNumeric().toInt(),
];
}
/**
@@ -152,7 +149,10 @@ export default class SalesEstimatesController extends BaseController {
const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req);
try {
const storedEstimate = await this.saleEstimateService.createEstimate(tenantId, estimateDTO);
const storedEstimate = await this.saleEstimateService.createEstimate(
tenantId,
estimateDTO
);
return res.status(200).send({
id: storedEstimate.id,
@@ -165,8 +165,8 @@ export default class SalesEstimatesController extends BaseController {
/**
* Handle update estimate details with associated entries.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async editEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
@@ -175,12 +175,16 @@ export default class SalesEstimatesController extends BaseController {
try {
// Update estimate with associated estimate entries.
await this.saleEstimateService.editEstimate(tenantId, estimateId, estimateDTO);
await this.saleEstimateService.editEstimate(
tenantId,
estimateId,
estimateDTO
);
return res.status(200).send({
id: estimateId,
message: 'The sale estimate has been created successfully.',
});
});
} catch (error) {
next(error);
}
@@ -188,8 +192,8 @@ export default class SalesEstimatesController extends BaseController {
/**
* Deletes the given estimate with associated entries.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async deleteEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
@@ -200,7 +204,7 @@ export default class SalesEstimatesController extends BaseController {
return res.status(200).send({
id: estimateId,
message: 'The sale estimate has been deleted successfully.'
message: 'The sale estimate has been deleted successfully.',
});
} catch (error) {
next(error);
@@ -209,8 +213,8 @@ export default class SalesEstimatesController extends BaseController {
/**
* Deliver the given sale estimate.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async deliverSaleEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
@@ -230,9 +234,9 @@ export default class SalesEstimatesController extends BaseController {
/**
* Marks the sale estimate as approved.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async approveSaleEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
@@ -252,9 +256,9 @@ export default class SalesEstimatesController extends BaseController {
/**
* Marks the sale estimate as rejected.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async rejectSaleEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
@@ -274,16 +278,19 @@ export default class SalesEstimatesController extends BaseController {
/**
* Retrieve the given estimate with associated entries.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
const { tenantId } = req;
try {
const estimate = await this.saleEstimateService.getEstimate(tenantId, estimateId);
const estimate = await this.saleEstimateService.getEstimate(
tenantId,
estimateId
);
return res.status(200).send({ estimate });
} catch (error) {
@@ -293,35 +300,28 @@ export default class SalesEstimatesController extends BaseController {
/**
* Retrieve estimates with pagination metadata.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async getEstimates(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const {
salesEstimates,
pagination,
filterMeta
} = await this.saleEstimateService.estimatesList(tenantId, filter);
const { salesEstimates, pagination, filterMeta } =
await this.saleEstimateService.estimatesList(tenantId, filter);
return res.status(200).send({
sales_estimates: this.transfromToResponse(salesEstimates),
pagination,
filter_meta: this.transfromToResponse(filterMeta),
})
});
} catch (error) {
next(error);
}
@@ -329,12 +329,17 @@ export default class SalesEstimatesController extends BaseController {
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) {
private handleServiceErrors(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'ITEMS_NOT_FOUND') {
return res.boom.badRequest(null, {
@@ -409,4 +414,4 @@ export default class SalesEstimatesController extends BaseController {
}
next(error);
}
};
}

View File

@@ -284,23 +284,15 @@ export default class SaleInvoicesController extends BaseController {
) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const {
salesInvoices,
filterMeta,
pagination,
} = await this.saleInvoiceService.salesInvoicesList(tenantId, filter);
const { salesInvoices, filterMeta, pagination } =
await this.saleInvoiceService.salesInvoicesList(tenantId, filter);
return res.status(200).send({
sales_invoices: salesInvoices,

View File

@@ -230,16 +230,12 @@ export default class SalesReceiptsController extends BaseController {
async getSalesReceipts(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const {

View File

@@ -47,6 +47,7 @@ export interface IAccountResponse extends IAccount {
export interface IAccountsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string,
onlyInactive: boolean;
};
export interface IAccountType {

View File

@@ -62,6 +62,8 @@ export interface IBill {
export interface IBillsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string;
page: number;
pageSize: number;
}
export interface IBillsService {

View File

@@ -1,6 +1,9 @@
import { IModel, ISortOrder } from "./Model";
export interface IDynamicFilter {
setTableName(tableName: string): void;
setModel(model: IModel): void;
buildQuery(): void;
getResponseMeta();
}
export interface IFilterRole {
@@ -10,19 +13,19 @@ export interface IFilterRole {
index?: number;
comparator?: string;
}
export interface IDynamicListFilterDTO {
export interface IDynamicListFilter {
customViewId?: number;
filterRoles?: IFilterRole[];
columnSortBy: string;
columnSortBy: ISortOrder;
sortOrder: string;
stringifiedFilterRoles: string;
}
export interface IDynamicListService {
dynamicList(
tenantId: number,
model: any,
filter: IDynamicListFilterDTO
filter: IDynamicListFilter
): Promise<any>;
handlerErrorsToResponse(error, req, res, next): void;
}

View File

@@ -72,6 +72,7 @@ export interface IItemsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string,
page: number,
pageSize: number,
inactiveMode: boolean,
};
export interface IItemsAutoCompleteFilter {

View File

@@ -7,7 +7,9 @@ export interface IJournalReportQuery {
noCents: boolean,
divideOn1000: boolean,
},
transactionTypes: string | string[],
transactionType: string,
transactionId: string,
accountsIds: number | number[],
fromRange: number,
toRange: number,

View File

@@ -1,17 +1,81 @@
export interface IModel {
name: string,
tableName: string,
fields: { [key: string]: any, },
};
name: string;
tableName: string;
fields: { [key: string]: any };
}
export interface IFilterMeta {
sortOrder: string,
sortBy: string,
};
sortOrder: string;
sortBy: string;
}
export interface IPaginationMeta {
pageSize: number,
page: number,
};
pageSize: number;
page: number;
}
export interface IModelMetaDefaultSort {
sortOrder: ISortOrder;
sortField: string;
}
export type IModelColumnType =
| 'text'
| 'number'
| 'enumeration'
| 'boolean'
| 'relation';
export type ISortOrder = 'DESC' | 'ASC';
export interface IModelMetaFieldCommon {
name: string;
column: string;
columnable?: boolean;
fieldType: IModelColumnType;
customQuery?: Function;
}
export interface IModelMetaFieldNumber {
fieldType: 'number';
minLength?: number;
maxLength?: number;
}
export interface IModelMetaFieldOther {
fieldType: 'text' | 'boolean';
}
export type IModelMetaField = IModelMetaFieldCommon &
(IModelMetaFieldOther | IModelMetaEnumerationField | IModelMetaRelationField);
export interface IModelMetaEnumerationOption {
key: string;
label: string;
}
export interface IModelMetaEnumerationField {
fieldType: 'enumeration';
options: IModelMetaEnumerationOption[];
}
export interface IModelMetaRelationFieldCommon {
fieldType: 'relation';
}
export interface IModelMetaRelationEnumerationField {
relationType: 'enumeration';
relationKey: string;
relationEntityLabel: string;
relationEntityKey: string;
}
export type IModelMetaRelationField = IModelMetaRelationFieldCommon & (
IModelMetaRelationEnumerationField
);
export interface IModelMeta {
defaultFilterField: string;
defaultSort: IModelMetaDefaultSort;
fields: { [key: string]: IModelMetaField };
}

View File

@@ -0,0 +1,31 @@
export interface ITransactionsByReferenceQuery {
referenceType: string;
referenceId: string;
}
export interface ITransactionsByReferenceAmount {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface ITransactionsByReferenceTransaction{
credit: ITransactionsByReferenceAmount;
debit: ITransactionsByReferenceAmount;
contactType: string;
formattedContactType: string;
contactId: number;
referenceType: string;
formattedReferenceType: string;
referenceId: number;
accountName: string;
accountCode: string;
accountId: number;
}

View File

@@ -55,6 +55,7 @@ export * from './CashFlow';
export * from './InventoryDetails';
export * from './LandedCost';
export * from './Entry';
export * from './TransactionsByReference';
export interface I18nService {
__: (input: string) => string;

View File

@@ -1,65 +1,85 @@
import { forEach, uniqBy } from 'lodash';
import { buildFilterRolesJoins } from 'lib/ViewRolesBuilder';
import { IModel } from 'interfaces';
import DynamicFilterAbstructor from './DynamicFilterAbstructor';
import { IDynamicFilter, IFilterRole, IModel } from 'interfaces';
export default class DynamicFilter {
model: IModel;
tableName: string;
export default class DynamicFilter extends DynamicFilterAbstructor{
private model: IModel;
private tableName: string;
private dynamicFilters: IDynamicFilter[];
/**
* Constructor.
* @param {String} tableName -
*/
constructor(model) {
super();
this.model = model;
this.tableName = model.tableName;
this.filters = [];
this.dynamicFilters = [];
}
/**
* Set filter.
* @param {*} filterRole - Filter role.
* Registers the given dynamic filter.
* @param {IDynamicFilter} filterRole - Filter role.
*/
setFilter(filterRole) {
filterRole.setModel(this.model);
this.filters.push(filterRole);
public setFilter = (dynamicFilter: IDynamicFilter) => {
dynamicFilter.setModel(this.model);
dynamicFilter.onInitialize();
this.dynamicFilters.push(dynamicFilter);
}
/**
* Retrieve dynamic filter build queries.
* @returns
*/
private dynamicFiltersBuildQuery = () => {
return this.dynamicFilters.map((filter) => {
return filter.buildQuery()
});
}
/**
* Retrieve dynamic filter roles.
* @returns {IFilterRole[]}
*/
private dynamicFilterTableColumns = (): IFilterRole[] => {
const localFilterRoles = [];
this.dynamicFilters.forEach((dynamicFilter) => {
const { filterRoles } = dynamicFilter;
localFilterRoles.push(
...(Array.isArray(filterRoles) ? filterRoles : [filterRoles])
);
});
return localFilterRoles;
}
/**
* Builds queries of filter roles.
*/
buildQuery() {
const buildersCallbacks = [];
const tableColumns = [];
this.filters.forEach((filter) => {
const { filterRoles } = filter;
buildersCallbacks.push(filter.buildQuery());
tableColumns.push(
...(Array.isArray(filterRoles) ? filterRoles : [filterRoles])
);
});
public buildQuery = () => {
const buildersCallbacks = this.dynamicFiltersBuildQuery();
const tableColumns = this.dynamicFilterTableColumns();
return (builder) => {
buildersCallbacks.forEach((builderCallback) => {
builderCallback(builder);
});
buildFilterRolesJoins(
this.model,
uniqBy(tableColumns, 'columnKey')
)(builder);
this.buildFilterRolesJoins(builder);
};
}
/**
* Retrieve response metadata from all filters adapters.
*/
getResponseMeta() {
public getResponseMeta = () => {
const responseMeta = {};
this.filters.forEach((filter) => {
this.dynamicFilters.forEach((filter) => {
const { responseMeta: filterMeta } = filter;
forEach(filterMeta, (value, key) => {

View File

@@ -0,0 +1,40 @@
import { IModel, IFilterRole } from 'interfaces';
import { FIELD_TYPE } from './constants';
export default class DynamicFilterAbstructor {
/**
* Extract relation table name from relation.
* @param {String} column -
* @return {String} - join relation table.
*/
protected getTableFromRelationColumn = (column: string) => {
const splitedColumn = column.split('.');
return splitedColumn.length > 0 ? splitedColumn[0] : '';
};
/**
* Builds view roles join queries.
* @param {String} tableName - Table name.
* @param {Array} roles - Roles.
*/
protected buildFilterRolesJoins = (builder) => {
this.dynamicFilters.forEach((dynamicFilter) => {
const relationsFields = dynamicFilter.relationFields;
this.buildFieldsJoinQueries(builder, relationsFields);
});
};
private buildFieldsJoinQueries = (builder, fieldsRelations: string[]) => {
fieldsRelations.forEach((fieldRelation) => {
const relation = this.model.relationMappings[fieldRelation];
if (relation) {
const splitToRelation = relation.join.to.split('.');
const relationTable = splitToRelation[0] || '';
builder.join(relationTable, relation.join.from, '=', relation.join.to);
}
});
};
}

View File

@@ -1,10 +1,8 @@
import { difference } from 'lodash';
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import { buildFilterQuery } from 'lib/ViewRolesBuilder';
import DynamicFilterRoleAbstructor from './DynamicFilterRoleAbstructor';
import { IFilterRole } from 'interfaces';
export default class FilterRoles extends DynamicFilterRoleAbstructor {
filterRoles: IFilterRole[];
private filterRoles: IFilterRole[];
/**
* Constructor method.
@@ -13,18 +11,23 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
*/
constructor(filterRoles: IFilterRole[]) {
super();
this.filterRoles = filterRoles;
this.setResponseMeta();
}
public onInitialize() {
this.setFilterRolesRelations();
}
/**
* Builds filter roles logic expression.
* @return {string}
*/
private buildLogicExpression(): string {
let expression = '';
this.filterRoles.forEach((role, index) => {
expression +=
index === 0 ? `${role.index} ` : `${role.condition} ${role.index} `;
@@ -35,19 +38,33 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
/**
* Builds database query of view roles.
*/
buildQuery() {
protected buildQuery() {
const logicExpression = this.buildLogicExpression();
return (builder) => {
const logicExpression = this.buildLogicExpression();
buildFilterQuery(this.model, this.filterRoles, logicExpression)(builder);
this.buildFilterQuery(
this.model,
this.filterRoles,
logicExpression
)(builder);
};
}
/**
* Sets response meta.
*/
setResponseMeta() {
private setResponseMeta() {
this.responseMeta = {
filterRoles: this.filterRoles,
};
}
/**
* Sets filter roles relations if field was relation type.
*/
private setFilterRolesRelations() {
this.filterRoles.forEach((relationRole) => {
this.setRelationIfRelationField(relationRole.fieldKey);
});
}
}

View File

@@ -0,0 +1,61 @@
import { OPERATION } from 'lib/LogicEvaluation/Parser';
export default class QueryParser {
constructor(tree, queries) {
this.tree = tree;
this.queries = queries;
this.query = null;
}
setQuery(query) {
this.query = query.clone();
}
parse() {
return this.parseNode(this.tree);
}
parseNode(node) {
if (typeof node === 'string') {
const nodeQuery = this.getQuery(node);
return (query) => { nodeQuery(query); };
}
if (OPERATION[node.operation] === undefined) {
throw new Error(`unknow expression ${node.operation}`);
}
const leftQuery = this.getQuery(node.left);
const rightQuery = this.getQuery(node.right);
switch (node.operation) {
case '&&':
case 'AND':
default:
return (nodeQuery) => nodeQuery.where((query) => {
query.where((q) => { leftQuery(q); });
query.andWhere((q) => { rightQuery(q); });
});
case '||':
case 'OR':
return (nodeQuery) => nodeQuery.where((query) => {
query.where((q) => { leftQuery(q); });
query.orWhere((q) => { rightQuery(q); });
});
}
}
getQuery(node) {
if (typeof node !== 'string' && node !== null) {
return this.parseNode(node);
}
const value = parseFloat(node);
if (!isNaN(value)) {
if (typeof this.queries[node] === 'undefined') {
throw new Error(`unknow query under index ${node}`);
}
return this.queries[node];
}
return null;
}
}

View File

@@ -1,13 +1,349 @@
import { IFilterRole, IDynamicFilter, IModel } from "interfaces";
import moment from 'moment';
import { IFilterRole, IDynamicFilter, IModel } from 'interfaces';
import { Lexer } from 'lib/LogicEvaluation/Lexer';
import Parser from 'lib/LogicEvaluation/Parser';
import DynamicFilterQueryParser from './DynamicFilterQueryParser';
import { COMPARATOR_TYPE, FIELD_TYPE } from './constants';
export default class DynamicFilterAbstructor implements IDynamicFilter {
filterRoles: IFilterRole[] = [];
tableName: string;
model: IModel;
responseMeta: { [key: string]: any } = {};
export default abstract class DynamicFilterAbstructor
implements IDynamicFilter
{
protected filterRoles: IFilterRole[] = [];
protected tableName: string;
protected model: IModel;
protected responseMeta: { [key: string]: any } = {};
public relationFields = [];
setModel(model: IModel) {
/**
* Sets model the dynamic filter service.
* @param {IModel} model
*/
public setModel(model: IModel) {
this.model = model;
this.tableName = model.tableName;
}
}
/**
* Transformes filter roles to map by index.
* @param {IModel} model
* @param {IFilterRole[]} roles
* @returns
*/
protected convertRolesMapByIndex = (model, roles) => {
const rolesIndexSet = {};
roles.forEach((role) => {
rolesIndexSet[role.index] = this.buildRoleQuery(model, role);
});
return rolesIndexSet;
};
/**
* Builds database query from stored view roles.
* @param {Array} roles -
* @return {Function}
*/
protected buildFilterRolesQuery = (
model: IModel,
roles: IFilterRole[],
logicExpression: string = ''
) => {
const rolesIndexSet = this.convertRolesMapByIndex(model, roles);
// Lexer for logic expression.
const lexer = new Lexer(logicExpression);
const tokens = lexer.getTokens();
// Parse the logic expression.
const parser = new Parser(tokens);
const parsedTree = parser.parse();
const queryParser = new DynamicFilterQueryParser(parsedTree, rolesIndexSet);
return queryParser.parse();
};
/**
* Builds filter query for query builder.
* @param {String} tableName - Table name.
* @param {Array} roles - Filter roles.
* @param {String} logicExpression - Logic expression.
*/
protected buildFilterQuery = (
model: IModel,
roles: IFilterRole[],
logicExpression: string
) => {
return (builder) => {
this.buildFilterRolesQuery(model, roles, logicExpression)(builder);
};
};
/**
* Retrieve relation column of comparator fieldز
*/
private getFieldComparatorRelationColumn(field) {
const relation = this.model.relationMappings[field.relationKey];
if (relation) {
const relationModel = relation.modelClass;
const relationColumn =
field.relationEntityKey === 'id'
? 'id'
: relationModel.getField(field.relationEntityKey, 'column');
return `${relationModel.tableName}.${relationColumn}`;
}
}
/**
* Retrieve the comparator field column.
* @param {IModel} model -
* @param {} -
*/
private getFieldComparatorColumn = (field) => {
return field.fieldType === FIELD_TYPE.RELATION
? this.getFieldComparatorRelationColumn(field)
: `${this.tableName}.${field.column}`;
};
/**
* Builds roles queries.
* @param {IModel} model -
* @param {Object} role -
*/
protected buildRoleQuery = (model: IModel, role: IFilterRole) => {
const field = model.getField(role.fieldKey);
const comparatorColumn = this.getFieldComparatorColumn(field);
// Field relation custom query.
if (typeof field.filterCustomQuery !== 'undefined') {
return (builder) => {
field.filterCustomQuery(builder, role);
};
}
switch (field.fieldType) {
case FIELD_TYPE.BOOLEAN:
case FIELD_TYPE.ENUMERATION:
return this.booleanRoleQueryBuilder(role, comparatorColumn);
case FIELD_TYPE.NUMBER:
return this.numberRoleQueryBuilder(role, comparatorColumn);
case FIELD_TYPE.DATE:
return this.dateQueryBuilder(role, comparatorColumn);
case FIELD_TYPE.TEXT:
default:
return this.textRoleQueryBuilder(role, comparatorColumn);
}
};
/**
* Boolean column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns
*/
protected booleanRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUALS:
case COMPARATOR_TYPE.EQUAL:
case COMPARATOR_TYPE.IS:
default:
return (builder) => {
builder.where(comparatorColumn, '=', role.value);
};
case COMPARATOR_TYPE.NOT_EQUAL:
case COMPARATOR_TYPE.NOT_EQUALS:
case COMPARATOR_TYPE.IS_NOT:
return (builder) => {
builder.where(comparatorColumn, '<>', role.value);
};
}
};
/**
* Numeric column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns
*/
protected numberRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUALS:
case COMPARATOR_TYPE.EQUAL:
default:
return (builder) => {
builder.where(comparatorColumn, '=', role.value);
};
case COMPARATOR_TYPE.NOT_EQUAL:
case COMPARATOR_TYPE.NOT_EQUALS:
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};
case COMPARATOR_TYPE.BIGGER_THAN:
case COMPARATOR_TYPE.BIGGER:
return (builder) => {
builder.where(comparatorColumn, '>', role.value);
};
case COMPARATOR_TYPE.BIGGER_OR_EQUALS:
return (builder) => {
builder.where(comparatorColumn, '>=', role.value);
};
case COMPARATOR_TYPE.SMALLER_THAN:
case COMPARATOR_TYPE.SMALLER:
return (builder) => {
builder.where(comparatorColumn, '<', role.value);
};
case COMPARATOR_TYPE.SMALLER_OR_EQUALS:
return (builder) => {
builder.where(comparatorColumn, '<=', role.value);
};
}
};
/**
* Text column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns {Function}
*/
protected textRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUAL:
case COMPARATOR_TYPE.EQUALS:
case COMPARATOR_TYPE.IS:
default:
return (builder) => {
builder.where(comparatorColumn, role.value);
};
case COMPARATOR_TYPE.NOT_EQUALS:
case COMPARATOR_TYPE.NOT_EQUAL:
case COMPARATOR_TYPE.IS_NOT:
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};
case COMPARATOR_TYPE.CONTAIN:
case COMPARATOR_TYPE.CONTAINS:
return (builder) => {
builder.where(comparatorColumn, 'LIKE', `%${role.value}%`);
};
case COMPARATOR_TYPE.NOT_CONTAIN:
case COMPARATOR_TYPE.NOT_CONTAINS:
return (builder) => {
builder.whereNot(comparatorColumn, 'LIKE', `%${role.value}%`);
};
}
};
/**
* Date column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns {Function}
*/
protected dateQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.AFTER:
case COMPARATOR_TYPE.BEFORE:
return (builder) => {
this.dateQueryAfterBeforeComparator(role, comparatorColumn, builder);
};
case COMPARATOR_TYPE.IN:
return (builder) => {
this.dateQueryInComparator(role, comparatorColumn, builder);
};
}
};
/**
* Date query 'IN' comparator type.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @param builder
*/
protected dateQueryInComparator = (
role: IFilterRole,
comparatorColumn: string,
builder
) => {
const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
).isValid();
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (hasTimeFormat) {
const targetDateTime = moment(role.value).format(dateFormat);
builder.where(comparatorColumn, '=', targetDateTime);
} else {
const startDate = moment(role.value).startOf('day');
const endDate = moment(role.value).endOf('day');
builder.where(comparatorColumn, '>=', startDate.format(dateFormat));
builder.where(comparatorColumn, '<=', endDate.format(dateFormat));
}
};
/**
* Date query after/before comparator type.
* @param {IFilterRole} role
* @param {string} comparatorColumn - Column.
* @param builder
*/
protected dateQueryAfterBeforeComparator = (
role: IFilterRole,
comparatorColumn: string,
builder
) => {
const comparator = role.comparator === COMPARATOR_TYPE.BEFORE ? '<' : '>';
const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
).isValid();
const targetDate = moment(role.value);
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (!hasTimeFormat) {
if (role.comparator === COMPARATOR_TYPE.BEFORE) {
targetDate.startOf('day');
} else {
targetDate.endOf('day');
}
}
const comparatorValue = targetDate.format(dateFormat);
builder.where(comparatorColumn, comparator, comparatorValue);
};
/**
* Registers relation field if the given field was relation type
* and not registered.
* @param {string} fieldKey - Field key.
*/
protected setRelationIfRelationField = (fieldKey: string): void => {
const field = this.model.getField(fieldKey);
const isAlreadyRegistered = this.relationFields.some(
(field) => field === fieldKey
);
if (
!isAlreadyRegistered &&
field &&
field.fieldType === FIELD_TYPE.RELATION
) {
this.relationFields.push(field.relationKey);
}
};
}

View File

@@ -1,12 +1,13 @@
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import {
getRoleFieldColumn,
validateFieldKeyExistance,
getTableFromRelationColumn,
} from 'lib/ViewRolesBuilder';
import { FIELD_TYPE } from './constants';
interface ISortRole {
fieldKey: string;
order: string;
}
export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
sortRole: { fieldKey: string; order: string } = {};
private sortRole: ISortRole = {};
/**
* Constructor method.
@@ -24,58 +25,65 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
}
/**
* Validate the given field key with the model.
* On initialize the dyanmic sort by.
*/
validate() {
validateFieldKeyExistance(this.model, this.sortRole.fieldKey);
public onInitialize() {
this.setRelationIfRelationField(this.sortRole.fieldKey);
}
/**
* Retrieve field comparator relatin column.
* @param field
* @returns {string}
*/
private getFieldComparatorRelationColumn = (field): string => {
const relation = this.model.relationMappings[field.relationKey];
if (relation) {
const relationModel = relation.modelClass;
const relationField = relationModel.getField(field.relationEntityLabel);
return `${relationModel.tableName}.${relationField.column}`;
}
return '';
};
/**
* Retrieve the comparator field column.
* @param {IModel} field
* @returns {string}
*/
private getFieldComparatorColumn = (field): string => {
return field.fieldType === FIELD_TYPE.RELATION
? this.getFieldComparatorRelationColumn(field)
: `${this.tableName}.${field.column}`;
};
/**
* Builds database query of sort by column on the given direction.
*/
buildQuery() {
const fieldRelation = getRoleFieldColumn(
this.model,
this.sortRole.fieldKey
);
const comparatorColumn =
fieldRelation.relationColumn ||
`${this.tableName}.${fieldRelation.column}`;
public buildQuery = () => {
const field = this.model.getField(this.sortRole.fieldKey);
const comparatorColumn = this.getFieldComparatorColumn(field);
if (typeof fieldRelation.sortQuery !== 'undefined') {
// Sort custom query.
if (typeof field.sortCustomQuery !== 'undefined') {
return (builder) => {
fieldRelation.sortQuery(builder, this.sortRole);
field.sortCustomQuery(builder, this.sortRole);
};
}
return (builder) => {
if (this.sortRole.fieldKey) {
builder.orderBy(`${comparatorColumn}`, this.sortRole.order);
}
this.joinBuildQuery()(builder);
};
}
joinBuildQuery() {
const fieldColumn = getRoleFieldColumn(this.model, this.sortRole.fieldKey);
return (builder) => {
if (fieldColumn.relation) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation);
builder.join(
joinTable,
`${this.model.tableName}.${fieldColumn.column}`,
'=',
fieldColumn.relation
);
}
};
}
};
/**
* Sets response meta.
*/
setResponseMeta() {
public setResponseMeta() {
this.responseMeta = {
sortOrder: this.sortRole.fieldKey,
sortBy: this.sortRole.order,

View File

@@ -0,0 +1,37 @@
export const COMPARATOR_TYPE = {
EQUAL: 'equal',
EQUALS: 'equals',
NOT_EQUAL: 'not_equal',
NOT_EQUALS: 'not_equals',
BIGGER_THAN: 'bigger_than',
BIGGER: 'bigger',
BIGGER_OR_EQUALS: 'bigger_or_equals',
SMALLER_THAN: 'smaller_than',
SMALLER: 'smaller',
SMALLER_OR_EQUALS: 'smaller_or_equals',
IS: 'is',
IS_NOT: 'is_not',
CONTAINS: 'contains',
CONTAIN: 'contain',
NOT_CONTAINS: 'contains',
NOT_CONTAIN: 'contain',
AFTER: 'after',
BEFORE: 'before',
IN: 'in',
};
export const FIELD_TYPE = {
TEXT: 'text',
NUMBER: 'number',
ENUMERATION: 'enumeration',
BOOLEAN: 'boolean',
RELATION: 'relation',
DATE: 'date',
COMPUTED: 'computed'
};

View File

@@ -1,121 +1,7 @@
import { difference } from 'lodash';
import moment from 'moment';
import { Lexer } from 'lib/LogicEvaluation/Lexer';
import Parser from 'lib/LogicEvaluation/Parser';
import QueryParser from 'lib/LogicEvaluation/QueryParser';
import { IFilterRole, IModel } from 'interfaces';
const numberRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case 'equals':
case 'equal':
default:
return (builder) => {
builder.where(comparatorColumn, '=', role.value);
};
case 'not_equals':
case 'not_equal':
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};
case 'bigger_than':
case 'bigger':
return (builder) => {
builder.where(comparatorColumn, '>', role.value);
};
case 'bigger_or_equals':
return (builder) => {
builder.where(comparatorColumn, '>=', role.value);
};
case 'smaller_than':
case 'smaller':
return (builder) => {
builder.where(comparatorColumn, '<', role.value);
};
case 'smaller_or_equals':
return (builder) => {
builder.where(comparatorColumn, '<=', role.value);
};
}
};
const textRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
switch (role.comparator) {
case 'equals':
case 'is':
default:
return (builder) => {
builder.where(comparatorColumn, role.value);
};
case 'not_equal':
case 'not_equals':
case 'is_not':
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};
case 'contain':
case 'contains':
return (builder) => {
builder.where(comparatorColumn, 'LIKE', `%${role.value}%`);
};
case 'not_contain':
case 'not_contains':
return (builder) => {
builder.whereNot(comparatorColumn, 'LIKE', `%${role.value}%`);
};
}
};
const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
switch (role.comparator) {
case 'after':
case 'before':
return (builder) => {
const comparator = role.comparator === 'before' ? '<' : '>';
const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
).isValid();
const targetDate = moment(role.value);
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (!hasTimeFormat) {
if (role.comparator === 'before') {
targetDate.startOf('day');
} else {
targetDate.endOf('day');
}
}
const comparatorValue = targetDate.format(dateFormat);
builder.where(comparatorColumn, comparator, comparatorValue);
};
case 'in':
return (builder) => {
const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
).isValid();
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (hasTimeFormat) {
const targetDateTime = moment(role.value).format(dateFormat);
builder.where(comparatorColumn, '=', targetDateTime);
} else {
const startDate = moment(role.value).startOf('day');
const endDate = moment(role.value).endOf('day');
builder.where(comparatorColumn, '>=', startDate.format(dateFormat));
builder.where(comparatorColumn, '<=', endDate.format(dateFormat));
}
};
}
};
/**
* Get field column metadata and its relation with other tables.
* @param {String} tableName - Table name of target column.
@@ -126,68 +12,6 @@ export function getRoleFieldColumn(model: IModel, fieldKey: string) {
return tableFields[fieldKey] ? tableFields[fieldKey] : null;
}
/**
* Builds roles queries.
* @param {IModel} model -
* @param {Object} role -
*/
export function buildRoleQuery(model: IModel, role: IFilterRole) {
const fieldRelation = getRoleFieldColumn(model, role.fieldKey);
const comparatorColumn =
fieldRelation.relationColumn ||
`${model.tableName}.${fieldRelation.column}`;
if (typeof fieldRelation.query !== 'undefined') {
return (builder) => {
fieldRelation.query(builder, role);
};
}
switch (fieldRelation.columnType) {
case 'number':
return numberRoleQueryBuilder(role, comparatorColumn);
case 'date':
return dateQueryBuilder(role, comparatorColumn);
case 'text':
case 'varchar':
default:
return textRoleQueryBuilder(role, comparatorColumn);
}
}
/**
* Extract relation table name from relation.
* @param {String} column -
* @return {String} - join relation table.
*/
export const getTableFromRelationColumn = (column: string) => {
const splitedColumn = column.split('.');
return splitedColumn.length > 0 ? splitedColumn[0] : '';
};
/**
* Builds view roles join queries.
* @param {String} tableName - Table name.
* @param {Array} roles - Roles.
*/
export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) {
return (builder) => {
roles.forEach((role) => {
const fieldColumn = getRoleFieldColumn(model, role.fieldKey);
if (fieldColumn.relation) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation);
builder.join(
joinTable,
`${model.tableName}.${fieldColumn.column}`,
'=',
fieldColumn.relation
);
}
});
};
}
export function buildSortColumnJoin(model: IModel, sortColumnKey: string) {
return (builder) => {
const fieldColumn = getRoleFieldColumn(model, sortColumnKey);
@@ -204,50 +28,6 @@ export function buildSortColumnJoin(model: IModel, sortColumnKey: string) {
};
}
/**
* Builds database query from stored view roles.
*
* @param {Array} roles -
* @return {Function}
*/
export function buildFilterRolesQuery(
model: IModel,
roles: IFilterRole[],
logicExpression: string = ''
) {
const rolesIndexSet = {};
roles.forEach((role) => {
rolesIndexSet[role.index] = buildRoleQuery(model, role);
});
// Lexer for logic expression.
const lexer = new Lexer(logicExpression);
const tokens = lexer.getTokens();
// Parse the logic expression.
const parser = new Parser(tokens);
const parsedTree = parser.parse();
const queryParser = new QueryParser(parsedTree, rolesIndexSet);
return queryParser.parse();
}
/**
* Builds filter query for query builder.
* @param {String} tableName -
* @param {Array} roles -
* @param {String} logicExpression -
*/
export const buildFilterQuery = (
model: IModel,
roles: IFilterRole[],
logicExpression: string
) => {
return (builder) => {
buildFilterRolesQuery(model, roles, logicExpression)(builder);
};
};
/**
* Mapes the view roles to view conditionals.
* @param {Array} viewRoles -
@@ -316,14 +96,6 @@ export function validateFieldKeyExistance(model: any, fieldKey: string) {
return model?.fields?.[fieldKey] || false;
}
export function validateFilterRolesFieldsExistance(
model,
filterRoles: IFilterRole[]
) {
return filterRoles.filter((filterRole: IFilterRole) => {
return !validateFieldKeyExistance(model, filterRole.fieldKey);
});
}
/**
* Retrieve model fields keys.

View File

@@ -0,0 +1,100 @@
import { IModelMeta } from 'interfaces';
import { ACCOUNT_TYPES } from 'data/AccountTypes';
export default {
defaultFilterField: 'name',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
name: {
name: 'Account name',
column: 'name',
fieldType: 'text',
},
description: {
name: 'Description',
column: 'description',
fieldType: 'text',
},
slug: {
name: 'Account slug',
column: 'slug',
fieldType: 'text',
columnable: false,
},
code: {
name: 'Account code',
column: 'code',
fieldType: 'text',
},
root_type: {
name: 'Root type',
fieldType: 'enumeration',
options: [
{ key: 'asset', label: 'Asset' },
{ key: 'liability', label: 'Liability' },
{ key: 'equity', label: 'Equity' },
{ key: 'Income', label: 'Income' },
{ key: 'expense', label: 'Expense' },
],
filterCustomQuery: RootTypeFieldFilterQuery,
sortable: false,
},
normal: {
name: 'Account normal',
fieldType: 'enumeration',
options: [
{ key: 'debit', label: 'Debit' },
{ key: 'credit', label: 'Credit' },
],
filterCustomQuery: NormalTypeFieldFilterQuery,
sortable: false,
},
type: {
name: 'Type',
column: 'account_type',
fieldType: 'enumeration',
options: ACCOUNT_TYPES.map((accountType) => ({
label: accountType.label,
key: accountType.key
})),
},
active: {
name: 'Active',
column: 'active',
fieldType: 'boolean',
filterable: false,
},
balance: {
name: 'Account balance',
column: 'amount',
fieldType: 'number',
},
currency: {
name: 'Currency',
column: 'currency_code',
fieldType: 'text',
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};
/**
* Filter query of root type field .
*/
function RootTypeFieldFilterQuery(query, role) {
query.modify('filterByRootType', role.value);
}
/**
* Filter query of normal field .
*/
function NormalTypeFieldFilterQuery(query, role) {
query.modify('filterByAccountNormal', role.value);
}

View File

@@ -1,16 +1,16 @@
/* eslint-disable global-require */
import { Model } from 'objection';
import { flatten, castArray } from 'lodash';
import { mixin, Model } from 'objection';
import { castArray } from 'lodash';
import TenantModel from 'models/TenantModel';
import {
buildFilterQuery,
buildSortColumnQuery,
} from 'lib/ViewRolesBuilder';
import { buildFilterQuery, buildSortColumnQuery } from 'lib/ViewRolesBuilder';
import { flatToNestedArray } from 'utils';
import DependencyGraph from 'lib/DependencyGraph';
import AccountTypesUtils from 'lib/AccountTypes'
import AccountTypesUtils from 'lib/AccountTypes';
import AccountSettings from './Account.Settings';
import ModelSettings from './ModelSetting';
import { ACCOUNT_TYPES } from 'data/AccountTypes';
export default class Account extends TenantModel {
export default class Account extends mixin(TenantModel, [ModelSettings]) {
/**
* Table name.
*/
@@ -21,7 +21,7 @@ export default class Account extends TenantModel {
/**
* Timestamps columns.
*/
get timestamps() {
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
@@ -35,7 +35,7 @@ export default class Account extends TenantModel {
'accountRootType',
'accountNormal',
'isBalanceSheetAccount',
'isPLSheet'
'isPLSheet',
];
}
@@ -95,6 +95,13 @@ export default class Account extends TenantModel {
const TABLE_NAME = Account.tableName;
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('accounts.active', !active);
},
filterAccounts(query, accountIds) {
if (accountIds.length > 0) {
query.whereIn(`${TABLE_NAME}.id`, accountIds);
@@ -111,6 +118,28 @@ export default class Account extends TenantModel {
sortColumnBuilder(query, columnKey, direction) {
buildSortColumnQuery(Account.tableName, columnKey, direction)(query);
},
/**
* Filter by root type.
*/
filterByRootType(query, rootType) {
const filterTypes = ACCOUNT_TYPES.filter(
(accountType) => accountType.rootType === rootType
).map((accountType) => accountType.key);
query.whereIn('account_type', filterTypes);
},
/**
* Filter by account normal
*/
filterByAccountNormal(query, accountNormal) {
const filterTypes = ACCOUNT_TYPES.filter(
(accountType) => accountType.normal === accountNormal,
).map((accountType) => accountType.key);
query.whereIn('account_type', filterTypes);
},
};
}
@@ -134,10 +163,10 @@ export default class Account extends TenantModel {
},
};
}
/**
* Detarmines whether the given type equals the account type.
* @param {string} accountType
* @param {string} accountType
* @return {boolean}
*/
isAccountType(accountType) {
@@ -147,7 +176,7 @@ export default class Account extends TenantModel {
/**
* Detarmines whether the given root type equals the account type.
* @param {string} rootType
* @param {string} rootType
* @return {boolean}
*/
isRootType(rootType) {
@@ -156,11 +185,14 @@ export default class Account extends TenantModel {
/**
* Detarmine whether the given parent type equals the account type.
* @param {string} parentType
* @param {string} parentType
* @return {boolean}
*/
isParentType(parentType) {
return AccountTypesUtils.isParentTypeEqualsKey(this.accountType, parentType);
return AccountTypesUtils.isParentTypeEqualsKey(
this.accountType,
parentType
);
}
/**
@@ -188,105 +220,32 @@ export default class Account extends TenantModel {
}
/**
* Converts flatten accounts list to nested array.
* @param {Array} accounts
* @param {Object} options
* Converts flatten accounts list to nested array.
* @param {Array} accounts
* @param {Object} options
*/
static toNestedArray(accounts, options = { children: 'children' }) {
return flatToNestedArray(accounts, { id: 'id', parentId: 'parentAccountId' })
return flatToNestedArray(accounts, {
id: 'id',
parentId: 'parentAccountId',
});
}
/**
* Transformes the accounts list to depenedency graph structure.
* @param {IAccount[]} accounts
*/
* @param {IAccount[]} accounts
*/
static toDependencyGraph(accounts) {
return DependencyGraph.fromArray(
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
);
return DependencyGraph.fromArray(accounts, {
itemId: 'id',
parentItemId: 'parentAccountId',
});
}
/**
* Model defined fields.
* Model settings.
*/
static get fields() {
return {
name: {
label: 'Account name',
column: 'name',
columnType: 'string',
fieldType: 'text',
},
type: {
label: 'Account type',
column: 'account_type',
},
description: {
label: 'Description',
column: 'description',
columnType: 'string',
fieldType: 'text',
},
code: {
label: 'Account code',
column: 'code',
columnType: 'string',
fieldType: 'text',
},
root_type: {
label: 'Root type',
options: [
{ key: 'asset', label: 'Asset', },
{ key: 'liability', label: 'Liability' },
{ key: 'equity', label: 'Equity' },
{ key: 'Income', label: 'Income' },
{ key: 'expense', label: 'Expense' },
],
query: (query, role) => {
const accountsTypes = AccountTypesUtils.getTypesByRootType(role.value);
const accountsTypesKeys = accountsTypes.map(type => type.key);
query.whereIn('account_type', accountsTypesKeys);
},
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
active: {
label: 'Active',
column: 'active',
columnType: 'boolean',
fieldType: 'checkbox',
},
balance: {
label: 'Balance',
column: 'amount',
columnType: 'number',
fieldType: 'number',
},
currency: {
label: 'Currency',
column: 'currency_code',
fieldType: 'options',
optionsResource: 'currency',
optionsKey: 'currency_code',
optionsLabel: 'currency_name',
},
normal: {
label: 'Account normal',
column: 'account_type_id',
fieldType: 'options',
relation: 'account_types.id',
relationColumn: 'account_types.normal',
options: [
{ key: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' },
],
},
};
static get meta() {
return AccountSettings;
}
}

View File

@@ -0,0 +1,96 @@
import { IModelMeta } from 'interfaces';
import Bill from './Bill';
export default {
defaultFilterField: 'vendor',
defaultSort: {
sortOrder: 'DESC',
sortField: 'bill_date',
},
fields: {
vendor: {
name: 'Vendor',
column: 'vendor_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'vendor',
relationEntityLabel: 'name',
relationEntityKey: 'id',
},
bill_number: {
name: 'Bill number',
column: 'bill_number',
columnable: true,
fieldType: 'text',
},
bill_date: {
name: 'Bill date',
column: 'bill_date',
columnable: true,
fieldType: 'date',
},
due_date: {
name: 'Due date',
column: 'due_date',
columnable: true,
fieldType: 'date',
},
reference_no: {
name: 'Reference No.',
column: 'reference_no',
columnable: true,
fieldType: 'text',
},
status: {
name: 'Status',
fieldType: 'enumeration',
columnable: true,
options: [
{ name: 'Paid', key: 'paid' },
{ name: 'Partially paid', key: 'partially-paid' },
{ name: 'Overdue', key: 'overdue' },
{ name: 'Unpaid', key: 'unpaid' },
{ name: 'Opened', key: 'opened' },
{ name: 'Draft', key: 'draft' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
amount: {
name: 'Amount',
column: 'amount',
fieldType: 'number',
},
payment_amount: {
name: 'Payment amount',
column: 'payment_amount',
fieldType: 'number',
},
note: {
name: 'Note',
column: 'note',
fieldType: 'text',
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};
/**
* Status field filter custom query.
*/
function StatusFieldFilterQuery(query, role) {
query.modify('statusFilter', role.value);
}
/**
* Status field sort custom query.
*/
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -1,10 +1,11 @@
import { Model, raw } from 'objection';
import { Model, raw, mixin } from 'objection';
import moment from 'moment';
import { difference } from 'lodash';
import TenantModel from 'models/TenantModel';
import { query } from 'winston';
import BillSettings from './Bill.Settings';
import ModelSetting from './ModelSetting';
export default class Bill extends TenantModel {
export default class Bill extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name
*/
@@ -12,10 +13,9 @@ export default class Bill extends TenantModel {
return 'bills';
}
static get resourceable() {
return true;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
@@ -71,15 +71,42 @@ export default class Bill extends TenantModel {
* Filters the bills from the given date.
*/
fromDate(query, fromDate) {
query.where('bill_date', '<=', fromDate)
query.where('bill_date', '<=', fromDate);
},
/**
* Sort the bills by full-payment bills.
*/
sortByStatus(query, order) {
sortByStatus(query, order) {
query.orderByRaw(`PAYMENT_AMOUNT = AMOUNT ${order}`);
},
/**
* Status filter.
*/
statusFilter(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
default:
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
break;
}
},
};
}
@@ -103,7 +130,7 @@ export default class Bill extends TenantModel {
'remainingDays',
'overdueDays',
'isOverdue',
'unallocatedCostAmount'
'unallocatedCostAmount',
];
}
@@ -198,6 +225,13 @@ export default class Bill extends TenantModel {
return Math.max(date.diff(dueDate, 'days'), 0);
}
/**
* Bill model settings.
*/
static get meta() {
return BillSettings;
}
/**
* Relationship mapping.
*/
@@ -269,88 +303,4 @@ export default class Bill extends TenantModel {
.where('id', billId)
[changeMethod]('payment_amount', Math.abs(amount));
}
static get fields() {
return {
vendor: {
label: 'Vendor',
column: 'vendor_id',
relation: 'contacts.id',
relationColumn: 'contacts.display_name',
},
bill_number: {
label: 'Bill number',
column: 'bill_number',
columnType: 'string',
fieldType: 'text',
},
bill_date: {
label: 'Bill date',
column: 'bill_date',
columnType: 'date',
fieldType: 'date',
},
due_date: {
label: 'Due date',
column: 'due_date',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
columnType: 'string',
fieldType: 'text',
},
status: {
label: 'Status',
options: [],
query: (query, role) => {
switch (role.value) {
case 'draft':
query.modify('draft');
break;
case 'opened':
query.modify('opened');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
break;
}
},
sortQuery(query, role) {
query.modify('sortByStatus', role.order);
},
},
amount: {
label: 'Amount',
column: 'amount',
columnType: 'number',
fieldType: 'number',
},
payment_amount: {
label: 'Payment amount',
column: 'payment_amount',
columnType: 'number',
fieldType: 'number',
},
note: {
label: 'Note',
column: 'note',
},
user: {},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
};
}
}

View File

@@ -0,0 +1,67 @@
export default {
defaultFilterField: 'vendor',
defaultSort: {
sortOrder: 'DESC',
sortField: 'bill_date',
},
fields: {
'vendor': {
name: 'Vendor name',
column: 'vendor_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'vendor',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
'amount': {
name: 'Amount',
column: 'amount',
fieldType: 'number',
},
'due_amount': {
name: 'Due amount',
column: 'due_amount',
fieldType: 'number',
},
'payment_account': {
name: 'Payment account',
column: 'payment_account_id',
fieldType: 'relation',
fieldRelation: 'paymentAccount',
fieldRelationType: 'enumeration',
relationLabelField: 'name',
relationKeyField: 'slug',
},
'payment_number': {
name: 'Payment number',
column: 'payment_number',
fieldType: 'number',
},
'payment_date': {
name: 'Payment date',
column: 'payment_date',
fieldType: 'date',
},
'reference_no': {
name: 'Reference No.',
column: 'reference',
fieldType: 'text',
},
'description': {
name: 'Description',
column: 'description',
fieldType: 'text',
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};

View File

@@ -1,7 +1,9 @@
import { Model } from "objection";
import { Model, mixin } from "objection";
import TenantModel from "models/TenantModel";
import ModelSetting from "./ModelSetting";
import BillPaymentSettings from "./BillPayment.Settings";
export default class BillPayment extends TenantModel {
export default class BillPayment extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name
*/
@@ -16,8 +18,11 @@ export default class BillPayment extends TenantModel {
return ["createdAt", "updatedAt"];
}
static get resourceable() {
return true;
/**
* Model settings.
*/
static get meta() {
return BillPaymentSettings;
}
/**
@@ -26,7 +31,7 @@ export default class BillPayment extends TenantModel {
static get relationMappings() {
const BillPaymentEntry = require("models/BillPaymentEntry");
const AccountTransaction = require("models/AccountTransaction");
const Contact = require("models/Contact");
const Vendor = require("models/Vendor");
const Account = require("models/Account");
return {
@@ -41,7 +46,7 @@ export default class BillPayment extends TenantModel {
vendor: {
relation: Model.BelongsToOneRelation,
modelClass: Contact.default,
modelClass: Vendor.default,
join: {
from: "bills_payments.vendorId",
to: "contacts.id",
@@ -73,70 +78,4 @@ export default class BillPayment extends TenantModel {
},
};
}
/**
* Resource fields.
*/
static get fields() {
return {
vendor: {
label: "Vendor name",
column: "vendor_id",
relation: "contacts.id",
relationColumn: "contacts.display_name",
},
amount: {
label: "Amount",
column: "amount",
columnType: "number",
fieldType: "number",
},
due_amount: {
label: "Due amount",
column: "due_amount",
columnType: "number",
fieldType: "number",
},
payment_account: {
label: "Payment account",
column: "payment_account_id",
relation: "accounts.id",
relationColumn: "accounts.name",
fieldType: "options",
optionsResource: "Account",
optionsKey: "id",
optionsLabel: "name",
},
payment_number: {
label: "Payment number",
column: "payment_number",
columnType: "string",
fieldType: "text",
},
payment_date: {
label: "Payment date",
column: "payment_date",
columnType: "date",
fieldType: "date",
},
reference_no: {
label: "Reference No.",
column: "reference",
columnType: "string",
fieldType: "text",
},
description: {
label: "Description",
column: "description",
columnType: "string",
fieldType: "text",
},
created_at: {
label: "Created at",
column: "created_at",
columnType: "date",
},
};
}
}

View File

@@ -0,0 +1,98 @@
export default {
fields: {
display_name: {
name: 'Display name',
column: 'display_name',
fieldType: 'text',
columnable: true,
},
email: {
name: 'Email',
column: 'email',
fieldType: 'text',
columnable: true,
},
work_phone: {
name: 'Work phone',
column: 'work_phone',
fieldType: 'text',
columnable: true,
},
personal_phone: {
name: 'Personal phone',
column: 'personal_phone',
fieldType: 'text',
columnable: true,
},
company_name: {
name: 'Company name',
column: 'company_name',
fieldType: 'text',
columnable: true,
},
website: {
name: 'Website',
column: 'website',
fieldType: 'text',
columnable: true,
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
columnable: true,
},
balance: {
name: 'Balance',
column: 'balance',
fieldType: 'number',
columnable: true,
},
opening_balance: {
name: 'Opening balance',
column: 'opening_balance',
fieldType: 'number',
columnable: true,
},
opening_balance_at: {
name: 'Opening balance at',
column: 'opening_balance_at',
filterable: false,
fieldType: 'date',
columnable: true,
},
currency_code: {
column: 'currency_code',
columnable: true,
fieldType: 'text',
},
status: {
label: 'Status',
options: [
{ key: 'active', label: 'Active' },
{ key: 'inactive', label: 'Inactive' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'unpaid', label: 'Unpaid' },
],
columnable: true,
filterQuery: statusFieldFilterQuery,
},
},
};
function statusFieldFilterQuery(query, role) {
switch (role.value) {
case 'active':
query.modify('active');
break;
case 'inactive':
query.modify('inactive');
break;
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
}

View File

@@ -1,7 +1,9 @@
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import PaginationQueryBuilder from './Pagination';
import QueryParser from 'lib/LogicEvaluation/QueryParser';
import ModelSetting from './ModelSetting';
import CustomerSettings from './Customer.Settings';
class CustomerQueryBuilder extends PaginationQueryBuilder {
constructor(...args) {
@@ -15,7 +17,7 @@ class CustomerQueryBuilder extends PaginationQueryBuilder {
}
}
export default class Customer extends TenantModel {
export default class Customer extends mixin(TenantModel, [ModelSetting]) {
/**
* Query builder.
*/
@@ -63,6 +65,13 @@ export default class Customer extends TenantModel {
*/
static get modifiers() {
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('active', !active);
},
/**
* Filters the active customers.
*/
@@ -81,10 +90,9 @@ export default class Customer extends TenantModel {
overdue(query) {
query.select(
'*',
Customer
.relatedQuery('overDueInvoices', query.knex())
Customer.relatedQuery('overDueInvoices', query.knex())
.count()
.as('countOverdue'),
.as('countOverdue')
);
query.having('countOverdue', '>', 0);
},
@@ -93,7 +101,7 @@ export default class Customer extends TenantModel {
*/
unpaid(query) {
query.whereRaw('`BALANCE` + `OPENING_BALANCE` <> 0');
}
},
};
}
@@ -122,77 +130,12 @@ export default class Customer extends TenantModel {
},
filter: (query) => {
query.modify('overdue');
}
}
},
},
};
}
static get fields() {
return {
contact_service: {
column: 'contact_service',
},
display_name: {
column: 'display_name',
},
email: {
column: 'email',
},
work_phone: {
column: 'work_phone',
},
personal_phone: {
column: 'personal_phone',
},
company_name: {
column: 'company_name',
},
website: {
column: 'website'
},
created_at: {
column: 'created_at',
},
balance: {
column: 'balance',
},
opening_balance: {
column: 'opening_balance',
},
opening_balance_at: {
column: 'opening_balance_at',
},
currency_code: {
column: 'currency_code',
},
status: {
label: 'Status',
options: [
{ key: 'active', label: 'Active' },
{ key: 'inactive', label: 'Inactive' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'unpaid', label: 'Unpaid' },
],
query: (query, role) => {
switch(role.value) {
case 'active':
query.modify('active');
break;
case 'inactive':
query.modify('inactive');
break;
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
},
},
created_at: {
column: 'created_at',
}
};
static get meta() {
return CustomerSettings;
}
}

View File

@@ -0,0 +1,71 @@
/**
* Expense - Settings.
*/
export default {
defaultFilterField: 'description',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
'payment_date': {
name: 'Payment date',
column: 'payment_date',
fieldType: 'date',
},
'payment_account': {
name: 'Payment account',
column: 'payment_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'paymentAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'amount': {
name: 'Amount',
column: 'total_amount',
fieldType: 'number',
},
'reference_no': {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
},
'description': {
name: 'Description',
column: 'description',
fieldType: 'text',
},
'published': {
name: 'Published',
column: 'published_at',
fieldType: 'date',
},
'status': {
name: 'Status',
fieldType: 'enumeration',
options: [
{ key: 'draft', name: 'Draft' },
{ key: 'published', name: 'Published' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -1,8 +1,10 @@
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import { viewRolesBuilder } from 'lib/ViewRolesBuilder';
import ModelSetting from './ModelSetting';
import ExpenseSettings from './Expense.Settings';
export default class Expense extends TenantModel {
export default class Expense extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name
*/
@@ -25,19 +27,8 @@ export default class Expense extends TenantModel {
}
/**
* Allows to mark model as resourceable to viewable and filterable.
* Virtual attributes.
*/
static get resourceable() {
return true;
}
/**
*
*/
static get media() {
return true;
}
static get virtualAttributes() {
return ['isPublished', 'unallocatedCostAmount'];
}
@@ -96,6 +87,18 @@ export default class Expense extends TenantModel {
filterByPublished(query) {
query.whereNot('published_at', null);
},
filterByStatus(query, status) {
switch (status) {
case 'draft':
query.modify('filterByDraft');
break;
case 'published':
default:
query.modify('filterByPublished');
break;
}
},
};
}
@@ -142,71 +145,7 @@ export default class Expense extends TenantModel {
};
}
/**
* Model defined fields.
*/
static get fields() {
return {
payment_date: {
label: 'Payment date',
column: 'payment_date',
columnType: 'date',
},
payment_account: {
label: 'Payment account',
column: 'payment_account_id',
relation: 'accounts.id',
optionsResource: 'account',
},
amount: {
label: 'Amount',
column: 'total_amount',
columnType: 'number',
},
currency_code: {
label: 'Currency',
column: 'currency_code',
optionsResource: 'currency',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
columnType: 'string',
},
description: {
label: 'Description',
column: 'description',
columnType: 'string',
},
published: {
label: 'Published',
column: 'published_at',
},
status: {
label: 'Status',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'published', label: 'Published' },
],
query: (query, role) => {
switch (role.value) {
case 'draft':
query.modify('filterByDraft');
break;
case 'published':
query.modify('filterByPublished');
break;
}
},
sortQuery(query, role) {
query.modify('sortByStatus', role.order);
},
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
};
static get meta() {
return ExpenseSettings;
}
}

View File

@@ -0,0 +1,59 @@
export default {
defaultFilterField: 'date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'date',
},
fields: {
'date': {
name: 'Date',
column: 'date',
fieldType: 'date',
},
'type': {
name: 'Adjustment type',
column: 'type',
fieldType: 'enumeration',
options: [
{ key: 'increment', name: 'Increment' },
{ key: 'decrement', name: 'Decrement' },
],
},
'adjustment_account': {
name: 'Adjustment account',
column: 'adjustment_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'adjustmentAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'reason': {
name: 'Reason',
column: 'reason',
fieldType: 'text',
},
'reference_no': {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
},
'description': {
name: 'Description',
column: 'description',
fieldType: 'text',
},
'published_at': {
name: 'Published at',
column: 'published_at',
fieldType: 'date',
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};

View File

@@ -1,7 +1,9 @@
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import InventoryAdjustmentSettings from './InventoryAdjustment.Settings';
import ModelSetting from './ModelSetting';
export default class InventoryAdjustment extends TenantModel {
export default class InventoryAdjustment extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name
*/
@@ -40,8 +42,8 @@ export default class InventoryAdjustment extends TenantModel {
static getInventoryDirection(type) {
const directions = {
'increment': 'IN',
'decrement': 'OUT',
increment: 'IN',
decrement: 'OUT',
};
return directions[type] || '';
}
@@ -81,52 +83,9 @@ export default class InventoryAdjustment extends TenantModel {
}
/**
* Model defined fields.
* Model settings.
*/
static get fields() {
return {
date: {
label: 'Date',
column: 'date',
columnType: 'date',
},
type: {
label: 'Adjustment type',
column: 'type',
options: [
{ key: 'increment', label: 'Increment', },
{ key: 'decrement', label: 'Decrement' },
],
},
adjustment_account: {
column: 'adjustment_account_id',
},
reason: {
label: 'Reason',
column: 'reason',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
},
description: {
label: 'Description',
column: 'description',
},
user: {
label: 'User',
column: 'user_id',
},
published_at: {
label: 'Published at',
column: 'published_at'
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
};
static get meta() {
return InventoryAdjustmentSettings;
}
}

View File

@@ -0,0 +1,123 @@
export default {
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
sortOrder: 'DESC',
},
fields: {
'type': {
name: 'Item type',
column: 'type',
fieldType: 'enumeration',
options: [
{ key: 'inventory', label: 'Inventory', },
{ key: 'service', label: 'Service' },
{ key: 'non-inventory', label: 'Non Inventory', },
],
},
'name': {
name: 'Name',
column: 'name',
fieldType: 'text',
},
'code': {
name: 'Code',
column: 'code',
fieldType: 'text',
},
'sellable': {
name: 'Sellable',
column: 'sellable',
fieldType: 'boolean',
},
'purchasable': {
name: 'Purchasable',
column: 'purchasable',
fieldType: 'boolean',
},
'sell_price': {
name: 'Sell price',
column: 'sell_price',
fieldType: 'number',
},
'cost_price': {
name: 'Cost price',
column: 'cost_price',
fieldType: 'number',
},
'cost_account': {
name: 'Cost account',
column: 'cost_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'costAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'sell_account': {
name: 'Sell account',
column: 'sell_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'sellAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'inventory_account': {
name: 'Inventory account',
column: 'inventory_account_id',
relationType: 'enumeration',
relationKey: 'inventoryAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'sell_description': {
name: 'Sell description',
column: 'sell_description',
fieldType: 'text',
},
'purchase_description': {
name: 'Purchase description',
column: 'purchase_description',
fieldType: 'text',
},
'quantity_on_hand': {
name: 'Quantity on hand',
column: 'quantity_on_hand',
fieldType: 'number',
},
'note': {
name: 'Note',
column: 'note',
fieldType: 'text',
},
'category': {
name: 'Category',
column: 'category_id',
relationType: 'enumeration',
relationKey: 'category',
relationEntityLabel: 'name',
relationEntityKey: 'id',
},
'active': {
name: 'Active',
column: 'active',
fieldType: 'boolean',
filterable: false,
},
'created_at': {
name: 'Created at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
},
};

View File

@@ -1,20 +1,22 @@
import { Model } from "objection";
import TenantModel from "models/TenantModel";
import { buildFilterQuery } from "lib/ViewRolesBuilder";
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import { buildFilterQuery } from 'lib/ViewRolesBuilder';
import ItemSettings from './Item.Settings';
import ModelSetting from './ModelSetting';
export default class Item extends TenantModel {
export default class Item extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name
*/
static get tableName() {
return "items";
return 'items';
}
/**
* Model timestamps.
*/
get timestamps() {
return ["createdAt", "updatedAt"];
return ['createdAt', 'updatedAt'];
}
/**
@@ -35,6 +37,13 @@ export default class Item extends TenantModel {
viewRolesBuilder(query, conditions, logicExpression) {
buildFilterQuery(Item.tableName, conditions, logicExpression)(query);
},
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('items.active', !active);
},
};
}
@@ -42,9 +51,9 @@ export default class Item extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
const Media = require("models/Media");
const Account = require("models/Account");
const ItemCategory = require("models/ItemCategory");
const Media = require('models/Media');
const Account = require('models/Account');
const ItemCategory = require('models/ItemCategory');
return {
/**
@@ -54,8 +63,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation,
modelClass: ItemCategory.default,
join: {
from: "items.categoryId",
to: "items_categories.id",
from: 'items.categoryId',
to: 'items_categories.id',
},
},
@@ -63,8 +72,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: "items.costAccountId",
to: "accounts.id",
from: 'items.costAccountId',
to: 'accounts.id',
},
},
@@ -72,8 +81,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: "items.sellAccountId",
to: "accounts.id",
from: 'items.sellAccountId',
to: 'accounts.id',
},
},
@@ -81,8 +90,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: "items.inventoryAccountId",
to: "accounts.id",
from: 'items.inventoryAccountId',
to: 'accounts.id',
},
},
@@ -90,110 +99,21 @@ export default class Item extends TenantModel {
relation: Model.ManyToManyRelation,
modelClass: Media.default,
join: {
from: "items.id",
from: 'items.id',
through: {
from: "media_links.model_id",
to: "media_links.media_id",
from: 'media_links.model_id',
to: 'media_links.media_id',
},
to: "media.id",
to: 'media.id',
},
},
};
}
/**
* Item fields.
* Model settings.
*/
static get fields() {
return {
type: {
label: "Type",
column: "type",
},
name: {
label: "Name",
column: "name",
},
code: {
label: "Code",
column: "code",
},
sellable: {
label: "Sellable",
column: "sellable",
},
purchasable: {
label: "Purchasable",
column: "purchasable",
},
sell_price: {
label: "Sell price",
column: "sell_price",
},
cost_price: {
label: "Cost price",
column: "cost_price",
},
currency_code: {
label: "Currency",
column: "currency_code",
},
cost_account: {
label: "Cost account",
column: "cost_account_id",
relation: "accounts.id",
relationColumn: "accounts.name",
},
sell_account: {
label: "Sell account",
column: "sell_account_id",
relation: "accounts.id",
relationColumn: "accounts.name",
},
inventory_account: {
label: "Inventory account",
column: "inventory_account_id",
relation: "accounts.id",
relationColumn: "accounts.name",
},
sell_description: {
label: "Sell description",
column: "sell_description",
},
purchase_description: {
label: "Purchase description",
column: "purchase_description",
},
quantity_on_hand: {
label: "Quantity on hand",
column: "quantity_on_hand",
},
note: {
label: "Note",
column: "note",
},
category: {
label: "Category",
column: "category_id",
relation: "items_categories.id",
relationColumn: "items_categories.name",
},
active: {
label: "Active",
column: "active",
},
// user: {
// label: 'User',
// column: 'user_id',
// relation: 'users.id',
// relationColumn: 'users.',
// },
created_at: {
label: "Created at",
column: "created_at",
columnType: "date",
fieldType: "date",
},
};
static get meta() {
return ItemSettings;
}
}

View File

@@ -0,0 +1,30 @@
export default {
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
sortOrder: 'DESC',
},
fields: {
name: {
label: 'Name',
column: 'name',
fieldType: 'text',
},
description: {
label: 'Description',
column: 'description',
fieldType: 'text',
},
count: {
label: 'Count',
column: 'count',
fieldType: 'number',
virtualColumn: true,
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
},
};

View File

@@ -1,8 +1,9 @@
import path from 'path';
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import ItemCategorySettings from './ItemCategory.Settings';
export default class ItemCategory extends TenantModel {
export default class ItemCategory extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name.
*/
@@ -10,10 +11,6 @@ export default class ItemCategory extends TenantModel {
return 'items_categories';
}
static get resourceable() {
return true;
}
/**
* Timestamps columns.
*/
@@ -43,68 +40,23 @@ export default class ItemCategory extends TenantModel {
}
/**
* Item category fields.
* Model modifiers.
*/
static get fields() {
static get modifiers() {
return {
name: {
label: 'Name',
column: 'name',
columnType: 'string'
},
description: {
label: 'Description',
column: 'description',
columnType: 'string'
},
user: {
label: 'User',
column: 'user_id',
relation: 'users.id',
relationColumn: 'users.id',
},
cost_account: {
label: 'Cost account',
column: 'cost_account_id',
relation: 'accounts.id',
optionsResource: 'account'
},
sell_account: {
label: 'Sell account',
column: 'sell_account_id',
relation: 'accounts.id',
optionsResource: 'account'
},
inventory_account: {
label: 'Inventory account',
column: 'inventory_account_id',
relation: 'accounts.id',
optionsResource: 'account'
},
cost_method: {
label: 'Cost method',
column: 'cost_method',
options: [{
key: 'FIFO', label: 'First-in first-out (FIFO)',
key: 'LIFO', label: 'Last-in first-out (LIFO)',
key: 'average', label: 'Average rate',
}],
columnType: 'string',
},
count: {
label: 'Count',
column: 'count',
sortQuery: this.sortCountQuery
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
/**
* Inactive/Active mode.
*/
sortByCount(query, order = 'asc') {
query.orderBy('count', order);
},
};
}
static sortCountQuery(query, role) {
query.orderBy('count', role.order);
}
/**
* Model meta.
*/
static get meta() {
return ItemCategorySettings;
}
}

View File

@@ -0,0 +1,54 @@
export default {
defaultFilterField: 'date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
'date': {
label: 'Date',
column: 'date',
fieldType: 'date',
},
'journal_number': {
label: 'Journal number',
column: 'journal_number',
fieldType: 'text',
},
'reference': {
label: 'Reference No.',
column: 'reference',
fieldType: 'text',
},
'journal_type': {
label: 'Journal type',
column: 'journal_type',
fieldType: 'text',
},
'amount': {
label: 'Amount',
column: 'amount',
columnType: 'number',
},
'description': {
label: 'Description',
column: 'description',
fieldType: 'text',
},
'status': {
label: 'Status',
column: 'status',
fieldType: 'enumeration',
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
label: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};
function StatusFieldSortQuery(query, role) {
return query.modify('sortByStatus', role.order);
}

View File

@@ -1,8 +1,10 @@
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import { formatNumber } from 'utils';
import ModelSetting from './ModelSetting';
import ManualJournalSettings from './ManualJournal.Settings';
export default class ManualJournal extends TenantModel {
export default class ManualJournal extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name.
*/
@@ -99,52 +101,7 @@ export default class ManualJournal extends TenantModel {
};
}
/**
* Model defined fields.
*/
static get fields() {
return {
date: {
label: 'Date',
column: 'date',
columnType: 'date',
},
journal_number: {
label: 'Journal number',
column: 'journal_number',
columnType: 'string',
},
reference: {
label: 'Reference No.',
column: 'reference',
columnType: 'string',
},
journal_type: {
label: 'Journal type',
column: 'journal_type',
columnType: 'string',
},
amount: {
label: 'Amount',
column: 'amount',
columnType: 'number',
},
description: {
label: 'Description',
column: 'description',
columnType: 'string',
},
status: {
label: 'Status',
column: 'status',
sortQuery(query, role) {
query.modify('sortByStatus', role.order);
},
},
created_at: {
label: 'Created at',
column: 'created_at',
},
};
static get meta() {
return ManualJournalSettings;
}
}

View File

@@ -0,0 +1,56 @@
import { get } from 'lodash';
import { IModelMeta, IModelMetaField, IModelMetaDefaultSort } from 'interfaces';
export default (Model) =>
class ModelSettings extends Model {
/**
*
*/
static get meta(): IModelMeta {
throw new Error('');
}
/**
* Retrieve specific model field meta of the given field key.
* @param {string} key
* @returns {IModelMetaField}
*/
public static getField(key: string, attribute?:string): IModelMetaField {
const field = get(this.meta.fields, key);
return attribute ? get(field, attribute) : field;
}
/**
* Retrieve the specific model meta.
* @param {string} key
* @returns
*/
public static getMeta(key: string) {
return get(this.meta, key);
}
/**
* Retrieve the model meta fields.
* @return {{ [key: string]: IModelMetaField }}
*/
public static get fields(): { [key: string]: IModelMetaField } {
return this.getMeta('fields');
}
/**
* Retrieve the model default sort settings.
* @return {IModelMetaDefaultSort}
*/
public static get defaultSort(): IModelMetaDefaultSort {
return this.getMeta('defaultSort');
}
/**
* Retrieve the default filter field key.
* @return {string}
*/
public static get defaultFilterField(): string {
return this.getMeta('defaultFilterField');
}
};

View File

@@ -0,0 +1,57 @@
export default {
fields: {
customer: {
name: 'Customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'name',
relationEntityKey: 'id',
},
payment_date: {
name: 'Payment date',
column: 'payment_date',
fieldType: 'date',
},
amount: {
name: 'Amount',
column: 'amount',
fieldType: 'number',
},
reference_no: {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
},
deposit_account: {
name: 'Deposit account',
column: 'deposit_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'depositAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
payment_receive_no: {
name: 'Payment receive No.',
column: 'payment_receive_no',
fieldType: 'text',
},
statement: {
name: 'Statement',
column: 'statement',
fieldType: 'text',
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldDate: 'date',
},
},
};

View File

@@ -1,7 +1,9 @@
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import PaymentReceiveSettings from './PaymentReceive.Settings';
export default class PaymentReceive extends TenantModel {
export default class PaymentReceive extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name.
*/
@@ -75,63 +77,9 @@ export default class PaymentReceive extends TenantModel {
}
/**
* Model defined fields.
*
*/
static get fields() {
return {
customer: {
label: 'Customer',
column: 'customer_id',
relation: 'contacts.id',
relationColumn: 'contacts.displayName',
fieldType: 'options',
optionsResource: 'customers',
optionsKey: 'id',
optionsLable: 'displayName',
},
payment_date: {
label: 'Payment date',
column: 'payment_date',
columnType: 'date',
fieldType: 'date',
},
amount: {
label: 'Amount',
column: 'amount',
columnType: 'number',
fieldType: 'number',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
columnType: 'string',
fieldType: 'text',
},
deposit_account: {
column: 'deposit_account_id',
lable: 'Deposit account',
relation: "accounts.id",
relationColumn: 'accounts.name',
optionsResource: "account",
},
payment_receive_no: {
label: 'Payment receive No.',
column: 'payment_receive_no',
columnType: 'string',
fieldType: 'text',
},
description: {
label: 'description',
column: 'description',
columnType: 'string',
fieldType: 'text',
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
};
static get meta() {
return PaymentReceiveSettings;
}
}

View File

@@ -0,0 +1,81 @@
export default {
defaultFilterField: 'estimate_date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'estimate_date',
},
fields: {
'amount': {
name: 'Amount',
column: 'amount',
fieldType: 'number',
},
'estimate_number': {
name: 'Estimate number',
column: 'estimate_number',
fieldType: 'text',
},
'customer': {
name: 'Customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
'estimate_date': {
name: 'Estimate date',
column: 'estimate_date',
fieldType: 'date',
},
'expiration_date': {
name: 'Expiration date',
column: 'expiration_date',
fieldType: 'date',
},
'reference_no': {
name: 'Reference No.',
column: 'reference',
fieldType: 'text',
},
'note': {
name: 'Note',
column: 'note',
fieldType: 'text',
},
'terms_conditions': {
name: 'Terms & conditions',
column: 'terms_conditions',
fieldType: 'text',
},
'status': {
name: 'Status',
fieldType: 'enumeration',
options: [
{ name: 'Delivered', key: 'delivered' },
{ name: 'Rejected', key: 'rejected' },
{ name: 'Approved', key: 'approved' },
{ name: 'Delivered', key: 'delivered' },
{ name: 'Draft', key: 'draft' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
name: 'Created at',
column: 'created_at',
columnType: 'date',
},
},
};
function StatusFieldSortQuery(query, role) {
query.modify('orderByStatus', role.order);
}
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}

View File

@@ -1,11 +1,11 @@
import moment from 'moment';
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import { defaultToTransform } from 'utils';
import HasItemEntries from 'services/Sales/HasItemsEntries';
import { query } from 'winston';
import SaleEstimateSettings from './SaleEstimate.Settings';
import ModelSetting from './ModelSetting';
export default class SaleEstimate extends TenantModel {
export default class SaleEstimate extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name
*/
@@ -29,9 +29,9 @@ export default class SaleEstimate extends TenantModel {
'isExpired',
'isConvertedToInvoice',
'isApproved',
'isRejected'
'isRejected',
];
}
}
/**
* Detarmines whether the sale estimate converted to sale invoice.
@@ -57,7 +57,7 @@ export default class SaleEstimate extends TenantModel {
return defaultToTransform(
this.expirationDate,
moment().isAfter(this.expirationDate, 'day'),
false,
false
);
}
@@ -123,13 +123,38 @@ export default class SaleEstimate extends TenantModel {
* Filters the approved estimates transactions.
*/
approved(query) {
query.whereNot('approved_at', null)
query.whereNot('approved_at', null);
},
/**
* Sorting the estimates orders by delivery status.
*/
orderByDraft(query, order) {
query.orderByRaw(`delivered_at is null ${order}`)
orderByStatus(query, order) {
query.orderByRaw(`delivered_at is null ${order}`);
},
/**
* Filtering the estimates oreders by status field.
*/
filterByStatus(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'approved':
query.modify('approved');
break;
case 'rejected':
query.modify('rejected');
break;
case 'invoiced':
query.modify('invoiced');
break;
case 'expired':
query.modify('expired');
break;
}
}
};
}
@@ -151,7 +176,7 @@ export default class SaleEstimate extends TenantModel {
},
filter(query) {
query.where('contact_service', 'customer');
}
},
},
entries: {
relation: Model.HasManyRelation,
@@ -168,91 +193,9 @@ export default class SaleEstimate extends TenantModel {
}
/**
* Model defined fields.
* Model settings.
*/
static get fields() {
return {
amount: {
label: 'Amount',
column: 'amount',
columnType: 'number',
fieldType: 'number',
},
estimate_number: {
label: 'Estimate number',
column: 'estimate_number',
columnType: 'text',
fieldType: 'text',
},
customer: {
label: 'Customer',
column: 'customer_id',
relation: 'contacts.id',
relationColumn: 'contacts.displayName',
fieldType: 'options',
optionsResource: 'customers',
optionsKey: 'id',
optionsLable: 'displayName',
},
estimate_date: {
label: 'Estimate date',
column: 'estimate_date',
columnType: 'date',
fieldType: 'date',
},
expiration_date: {
label: 'Expiration date',
column: 'expiration_date',
columnType: 'date',
fieldType: 'date',
},
reference_no: {
label: "Reference No.",
column: "reference",
columnType: "number",
fieldType: "number",
},
note: {
label: 'Note',
column: 'note',
columnType: 'text',
fieldType: 'text',
},
terms_conditions: {
label: 'Terms & conditions',
column: 'terms_conditions',
columnType: 'text',
fieldType: 'text',
},
status: {
label: 'Status',
query: (query, role) => {
switch(role.value) {
case 'draft':
query.modify('draft'); break;
case 'delivered':
query.modify('delivered'); break;
case 'approved':
query.modify('approved'); break;
case 'rejected':
query.modify('rejected'); break;
case 'invoiced':
query.modify('invoiced');
break;
case 'expired':
query.modify('expired'); break;
}
},
sortQuery: (query, role) => {
query.modify('orderByDraft', role.order);
}
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
};
static get meta() {
return SaleEstimateSettings;
}
}

View File

@@ -0,0 +1,100 @@
export default {
defaultFilterField: 'customer',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
fields: {
customer: {
name: 'Customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
invoice_date: {
name: 'Invoice date',
column: 'invoice_date',
fieldType: 'date',
},
due_date: {
name: 'Due date',
column: 'due_date',
fieldType: 'date',
},
invoice_no: {
name: 'Invoice No.',
column: 'invoice_no',
fieldType: 'text',
},
reference_no: {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
},
invoice_message: {
name: 'Invoice message',
column: 'invoice_message',
fieldType: 'text',
},
terms_conditions: {
name: 'Terms & conditions',
column: 'terms_conditions',
fieldType: 'text',
},
amount: {
name: 'Invoice amount',
column: 'balance',
fieldType: 'number',
},
payment_amount: {
name: 'Payment amount',
column: 'payment_amount',
fieldType: 'number',
},
due_amount: { // calculated.
name: 'Due amount',
column: 'due_amount',
fieldType: 'number',
virtualColumn: true,
},
status: {
name: 'Status',
columnable: true,
fieldType: 'enumeration',
options: [
{ key: 'draft', name: 'Draft' },
{ key: 'delivered', name: 'Delivered' },
{ key: 'unpaid', name: 'Unpaid' },
{ key: 'overdue', name: 'Overdue' },
{ key: 'partially-paid', name: 'Partially paid' },
{ key: 'paid', name: 'Paid' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};
/**
* Status field filter custom query.
*/
function StatusFieldFilterQuery(query, role) {
query.modify('statusFilter', role.value);
}
/**
* Status field sort custom query.
*/
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -1,8 +1,10 @@
import { Model, raw } from 'objection';
import { mixin, Model, raw } from 'objection';
import moment from 'moment';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import SaleInvoiceMeta from './SaleInvoice.Settings';
export default class SaleInvoice extends TenantModel {
export default class SaleInvoice extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name
*/
@@ -106,10 +108,6 @@ export default class SaleInvoice extends TenantModel {
return this.getOverdueDays();
}
static get resourceable() {
return true;
}
/**
*
* @param {*} asDate
@@ -229,6 +227,33 @@ export default class SaleInvoice extends TenantModel {
byPrefixAndNumber(query, prefix, number) {
query.where('invoice_no', `${prefix}${number}`);
},
/**
* Status filter.
*/
statusFilter(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
default:
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
break;
}
},
};
}
@@ -238,11 +263,14 @@ export default class SaleInvoice extends TenantModel {
static get relationMappings() {
const AccountTransaction = require('models/AccountTransaction');
const ItemEntry = require('models/ItemEntry');
const Contact = require('models/Contact');
const Customer = require('models/Customer');
const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
return {
/**
* Sale invoice associated entries.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry.default,
@@ -255,9 +283,12 @@ export default class SaleInvoice extends TenantModel {
},
},
/**
* Belongs to customer model.
*/
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Contact.default,
modelClass: Customer.default,
join: {
from: 'sales_invoices.customerId',
to: 'contacts.id',
@@ -267,6 +298,9 @@ export default class SaleInvoice extends TenantModel {
},
},
/**
* Invoice has associated account transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
@@ -316,125 +350,13 @@ export default class SaleInvoice extends TenantModel {
}
/**
* Model defined fields.
* Sale invoice meta.
*/
static get fields() {
return {
customer: {
label: 'Customer',
column: 'customer_id',
relation: 'contacts.id',
relationColumn: 'contacts.displayName',
static get meta() {
return SaleInvoiceMeta;
}
fieldType: 'options',
optionsResource: 'customers',
optionsKey: 'id',
optionsLable: 'displayName',
},
invoice_date: {
label: 'Invoice date',
column: 'invoice_date',
columnType: 'date',
fieldType: 'date',
},
due_date: {
label: 'Due date',
column: 'due_date',
columnType: 'date',
fieldType: 'date',
},
invoice_no: {
label: 'Invoice No.',
column: 'invoice_no',
columnType: 'number',
fieldType: 'number',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
columnType: 'number',
fieldType: 'number',
},
invoice_message: {
label: 'Invoice message',
column: 'invoice_message',
columnType: 'text',
fieldType: 'text',
},
terms_conditions: {
label: 'Terms & conditions',
column: 'terms_conditions',
columnType: 'text',
fieldType: 'text',
},
invoice_amount: {
label: 'Invoice amount',
column: 'invoice_amount',
columnType: 'number',
fieldType: 'number',
},
payment_amount: {
label: 'Payment amount',
column: 'payment_amount',
columnType: 'number',
fieldType: 'number',
},
balance: {
label: 'Balance',
column: 'balance',
columnType: 'number',
fieldType: 'number',
},
due_amount: {
label: 'Due amount',
column: 'due_amount',
columnType: 'number',
fieldType: 'number',
sortQuery(query, role) {
query.modify('sortByDueAmount', role.order);
},
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
status: {
label: 'Status',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'delivered', label: 'Delivered' },
{ key: 'unpaid', label: 'Unpaid' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'partially-paid', label: 'Partially paid' },
{ key: 'paid', label: 'Paid' },
],
query: (query, role) => {
switch (role.value) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
break;
}
},
sortQuery(query, role) {
query.modify('sortByStatus', role.order);
},
},
};
static dueAmountFieldSortQuery(query, role) {
query.modify('sortByDueAmount', role.order);
}
}

View File

@@ -0,0 +1,85 @@
export default {
defaultFilterField: 'receipt_date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
fields: {
'amount': {
name: 'Amount',
column: 'amount',
fieldType: 'number',
},
'deposit_account': {
column: 'deposit_account_id',
name: 'Deposit account',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'depositAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'customer': {
name: 'Customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
'receipt_date': {
name: 'Receipt date',
column: 'receipt_date',
fieldType: 'date',
},
'receipt_number': {
name: 'Receipt No.',
column: 'receipt_number',
fieldType: 'text',
},
'reference_no': {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
},
'receipt_message': {
name: 'Receipt message',
column: 'receipt_message',
fieldType: 'text',
},
'statement': {
name: 'Statement',
column: 'statement',
fieldType: 'text',
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
'status': {
name: 'Status',
fieldType: 'enumeration',
options: [
{ key: 'draft', name: 'Draft' },
{ key: 'closed', name: 'Closed' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
},
};
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -1,7 +1,9 @@
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import SaleReceiptSettings from './SaleReceipt.Settings';
export default class SaleReceipt extends TenantModel {
export default class SaleReceipt extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name
*/
@@ -16,14 +18,11 @@ export default class SaleReceipt extends TenantModel {
return ['created_at', 'updated_at'];
}
/**
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'isClosed',
'isDraft',
];
return ['isClosed', 'isDraft'];
}
/**
@@ -66,6 +65,21 @@ export default class SaleReceipt extends TenantModel {
*/
sortByStatus(query, order) {
query.orderByRaw(`CLOSED_AT IS NULL ${order}`);
},
/**
* Filtering the receipts orders by status.
*/
filterByStatus(query, status) {
switch (status) {
case 'draft':
query.modify('draft');
break;
case 'closed':
default:
query.modify('closed');
break;
}
}
};
}
@@ -74,7 +88,7 @@ export default class SaleReceipt extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
const Contact = require('models/Contact');
const Customer = require('models/Customer');
const Account = require('models/Account');
const AccountTransaction = require('models/AccountTransaction');
const ItemEntry = require('models/ItemEntry');
@@ -82,14 +96,14 @@ export default class SaleReceipt extends TenantModel {
return {
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Contact.default,
modelClass: Customer.default,
join: {
from: 'sales_receipts.customerId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'customer');
}
},
},
depositAccount: {
@@ -118,95 +132,19 @@ export default class SaleReceipt extends TenantModel {
modelClass: AccountTransaction.default,
join: {
from: 'sales_receipts.id',
to: 'accounts_transactions.referenceId'
to: 'accounts_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleReceipt');
},
}
},
};
}
/**
* Model defined fields.
* Sale invoice meta.
*/
static get fields() {
return {
amount: {
label: 'Amount',
column: 'amount',
columnType: 'number',
fieldType: 'number',
},
deposit_account: {
column: 'deposit_account_id',
label: 'Deposit account',
relation: "accounts.id",
optionsResource: "account",
},
customer: {
label: 'Customer',
column: 'customer_id',
fieldType: 'options',
optionsResource: 'customers',
optionsKey: 'id',
optionsLable: 'displayName',
},
receipt_date: {
label: 'Receipt date',
column: 'receipt_date',
columnType: 'date',
fieldType: 'date',
},
receipt_number: {
label: 'Receipt No.',
column: 'receipt_number',
columnType: 'string',
fieldType: 'text',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
columnType: 'text',
fieldType: 'text',
},
receipt_message: {
label: 'Receipt message',
column: 'receipt_message',
columnType: 'text',
fieldType: 'text',
},
statement: {
label: 'Statement',
column: 'statement',
columnType: 'text',
fieldType: 'text',
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
status: {
label: 'Status',
options: [
{ key: 'draft', label: 'Draft', },
{ key: 'closed', label: 'Closed' },
],
query: (query, role) => {
switch(role.value) {
case 'draft':
query.modify('draft');
break;
case 'closed':
query.modify('closed');
break;
}
},
sortQuery(query, role) {
query.modify('sortByStatus', role.order);
}
}
};
static get meta() {
return SaleReceiptSettings;
}
}

View File

@@ -0,0 +1,89 @@
export default {
defaultFilterField: 'display_name',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
fields: {
'display_name': {
name: 'Display name',
column: 'display_name',
fieldType: 'text',
},
'email': {
name: 'Email',
column: 'email',
fieldType: 'text',
},
'work_phone': {
name: 'Work phone',
column: 'work_phone',
fieldType: 'text',
},
'personal_phone': {
name: 'Personal phone',
column: 'personal_phone',
fieldType: 'text',
},
'company_name': {
name: 'Company name',
column: 'company_name',
fieldType: 'text',
},
'website': {
name: 'Website',
column: 'website',
fieldType: 'text',
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
'balance': {
name: 'Balance',
column: 'balance',
fieldType: 'number',
},
'opening_balance': {
name: 'Opening balance',
column: 'opening_balance',
fieldType: 'number',
},
'opening_balance_at': {
name: 'Opening balance at',
column: 'opening_balance_at',
fieldType: 'date',
},
'currency_code': {
name: 'Currency code',
column: 'currency_code',
fieldType: 'text',
},
'status': {
label: 'Status',
options: [
{ key: 'active', label: 'Active' },
{ key: 'inactive', label: 'Inactive' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'unpaid', label: 'Unpaid' },
],
query: (query, role) => {
switch (role.value) {
case 'active':
query.modify('active');
break;
case 'inactive':
query.modify('inactive');
break;
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
},
},
},
};

View File

@@ -1,6 +1,8 @@
import { Model, QueryBuilder } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import PaginationQueryBuilder from './Pagination';
import ModelSetting from './ModelSetting';
import VendorSettings from './Vendor.Settings';
class VendorQueryBuilder extends PaginationQueryBuilder {
constructor(...args) {
@@ -14,7 +16,7 @@ class VendorQueryBuilder extends PaginationQueryBuilder {
}
}
export default class Vendor extends TenantModel {
export default class Vendor extends mixin(TenantModel, [ModelSetting]) {
/**
* Query builder.
*/
@@ -62,6 +64,13 @@ export default class Vendor extends TenantModel {
*/
static get modifiers() {
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('active', !active);
},
/**
* Filters the active customers.
*/
@@ -125,72 +134,7 @@ export default class Vendor extends TenantModel {
};
}
static get fields() {
return {
contact_service: {
column: 'contact_service',
},
display_name: {
column: 'display_name',
},
email: {
column: 'email',
},
work_phone: {
column: 'work_phone',
},
personal_phone: {
column: 'personal_phone',
},
company_name: {
column: 'company_name',
},
website: {
column: 'website'
},
created_at: {
column: 'created_at',
},
balance: {
column: 'balance',
},
opening_balance: {
column: 'opening_balance',
},
opening_balance_at: {
column: 'opening_balance_at',
},
currency_code: {
column: 'currency_code',
},
status: {
label: 'Status',
options: [
{ key: 'active', label: 'Active' },
{ key: 'inactive', label: 'Inactive' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'unpaid', label: 'Unpaid' },
],
query: (query, role) => {
switch(role.value) {
case 'active':
query.modify('active');
break;
case 'inactive':
query.modify('inactive');
break;
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
},
},
created_at: {
column: 'created_at',
}
};
static get meta() {
return VendorSettings;
}
}

View File

@@ -1,6 +1,7 @@
import { Inject, Service } from 'typedi';
import { difference, chain, uniq } from 'lodash';
import { kebabCase } from 'lodash';
import R from 'ramda';
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
import {
@@ -606,6 +607,17 @@ export default class AccountsService {
this.eventDispatcher.dispatch(events.accounts.onActivated);
}
/**
* Parsees accounts list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter
)(filterDTO);
}
/**
* Retrieve accounts datatable list.
* @param {number} tenantId
@@ -613,21 +625,26 @@ export default class AccountsService {
*/
public async getAccountsList(
tenantId: number,
filter: IAccountsFilter
filterDTO: IAccountsFilter
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> {
const { Account } = this.tenancy.models(tenantId);
// Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
Account,
filter
);
this.logger.info('[accounts] trying to get accounts datatable list.', {
tenantId,
filter,
});
const accounts = await Account.query().onBuild((builder) => {
dynamicList.buildQuery()(builder);
builder.modify('inactiveMode', filter.inactiveMode);
});
return {
@@ -727,10 +744,11 @@ export default class AccountsService {
}));
return flatToNestedArray(
this.i18nService.i18nMapper(_accounts, ['account_type_label'], tenantId),
{
id: 'id',
parentId: 'parent_account_id',
});
{
id: 'id',
parentId: 'parent_account_id',
}
);
}
/**

Some files were not shown because too many files have changed in this diff Show More