diff --git a/src/common/moreVertOptions.js b/src/common/moreVertOptions.js
new file mode 100644
index 000000000..62a15a755
--- /dev/null
+++ b/src/common/moreVertOptions.js
@@ -0,0 +1,12 @@
+import intl from 'react-intl-universal';
+
+export const moreVertOptions = [
+ {
+ name: intl.get('badDebt.label'),
+ value: 'bad debt',
+ },
+ {
+ name: intl.get('badDebt.label_cancel_bad_debt'),
+ value: 'cancel bad debt',
+ },
+];
diff --git a/src/components/DialogsContainer.js b/src/components/DialogsContainer.js
index 07c00ca6f..f1e803fff 100644
--- a/src/components/DialogsContainer.js
+++ b/src/components/DialogsContainer.js
@@ -19,6 +19,7 @@ import EstimatePdfPreviewDialog from 'containers/Dialogs/EstimatePdfPreviewDialo
import ReceiptPdfPreviewDialog from '../containers/Dialogs/ReceiptPdfPreviewDialog';
import MoneyInDialog from '../containers/Dialogs/MoneyInDialog';
import MoneyOutDialog from '../containers/Dialogs/MoneyOutDialog';
+import BadDebtDialog from '../containers/Dialogs/BadDebtDialog';
/**
* Dialogs container.
@@ -44,6 +45,7 @@ export default function DialogsContainer() {
+
);
}
diff --git a/src/components/MoreVertMenutItems.js b/src/components/MoreVertMenutItems.js
new file mode 100644
index 000000000..9282f33c1
--- /dev/null
+++ b/src/components/MoreVertMenutItems.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import {
+ Button,
+ PopoverInteractionKind,
+ MenuItem,
+ Position,
+} from '@blueprintjs/core';
+
+import { Select } from '@blueprintjs/select';
+import { Icon } from 'components';
+
+function MoreVertMenutItems({ text, items, onItemSelect, buttonProps }) {
+ // Menu items renderer.
+ const itemsRenderer = (item, { handleClick, modifiers, query }) => (
+
+ );
+ const handleMenuSelect = (type) => {
+ onItemSelect && onItemSelect(type);
+ };
+
+ return (
+
+ );
+}
+
+export default MoreVertMenutItems;
diff --git a/src/components/index.js b/src/components/index.js
index 6d78330e3..36ad2797e 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -61,6 +61,7 @@ import Card from './Card';
import AvaterCell from './AvaterCell';
import { ItemsMultiSelect } from './Items';
+import MoreVertMenutItems from './MoreVertMenutItems';
export * from './Menu';
export * from './AdvancedFilter/AdvancedFilterDropdown';
@@ -82,7 +83,7 @@ export * from './MultiSelectTaggable';
export * from './Utils/FormatNumber';
export * from './Utils/FormatDate';
export * from './BankAccounts';
-export * from './IntersectionObserver'
+export * from './IntersectionObserver';
const Hint = FieldHint;
@@ -154,4 +155,5 @@ export {
ItemsMultiSelect,
Card,
AvaterCell,
+ MoreVertMenutItems,
};
diff --git a/src/containers/Alerts/Invoices/BadDebtAlert.js b/src/containers/Alerts/Invoices/BadDebtAlert.js
new file mode 100644
index 000000000..b3d05c083
--- /dev/null
+++ b/src/containers/Alerts/Invoices/BadDebtAlert.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import { FormattedMessage as T } from 'components';
+import intl from 'react-intl-universal';
+import { Intent, Alert } from '@blueprintjs/core';
+import { AppToaster } from 'components';
+import { useCancelBadDebt } from 'hooks/query';
+
+import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
+import withAlertActions from 'containers/Alert/withAlertActions';
+
+import { compose } from 'utils';
+
+/**
+ * bad debt alert.
+ */
+function BadDebtAlert({
+ name,
+
+ // #withAlertStoreConnect
+ isOpen,
+ payload: { invoiceId },
+
+ // #withAlertActions
+ closeAlert,
+}) {
+ // handle cancel alert.
+ const handleCancel = () => {
+ closeAlert(name);
+ };
+
+ const { mutateAsync: cancelBadDebtMutate, isLoading } = useCancelBadDebt();
+
+ // handleConfirm alert.
+ const handleConfirm = () => {
+ cancelBadDebtMutate(invoiceId)
+ .then(() => {
+ AppToaster.show({
+ message: intl.get('the_invoice_has_been_deleted_successfully'),
+ intent: Intent.SUCCESS,
+ });
+ })
+ .catch(() => {})
+ .finally(() => {
+ closeAlert(name);
+ });
+ };
+
+ return (
+ }
+ confirmButtonText={}
+ intent={Intent.WARNING}
+ isOpen={isOpen}
+ onCancel={handleCancel}
+ onConfirm={handleConfirm}
+ loading={isLoading}
+ >
+
+
+
+
+ );
+}
+
+export default compose(withAlertStoreConnect(), withAlertActions)(BadDebtAlert);
diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtDialogContent.js b/src/containers/Dialogs/BadDebtDialog/BadDebtDialogContent.js
new file mode 100644
index 000000000..1e4b205b0
--- /dev/null
+++ b/src/containers/Dialogs/BadDebtDialog/BadDebtDialogContent.js
@@ -0,0 +1,20 @@
+import React from 'react';
+
+import 'style/pages/BadDebt/BadDebtDialog.scss';
+import { BadDebtFormProvider } from './BadDebtFormProvider';
+import BadDebtForm from './BadDebtForm';
+
+/**
+ * Bad debt dialog content.
+ */
+export default function BadDebtDialogContent({
+ // #ownProps
+ dialogName,
+ invoice,
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtForm.js b/src/containers/Dialogs/BadDebtDialog/BadDebtForm.js
new file mode 100644
index 000000000..5b579db28
--- /dev/null
+++ b/src/containers/Dialogs/BadDebtDialog/BadDebtForm.js
@@ -0,0 +1,86 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+
+import { Formik } from 'formik';
+import { Intent } from '@blueprintjs/core';
+import { omit } from 'lodash';
+
+import { AppToaster } from 'components';
+import { CreateBadDebtFormSchema } from './BadDebtForm.schema';
+import { transformErrors } from './utils';
+
+import BadDebtFormContent from './BadDebtFormContent';
+
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
+
+import { useBadDebtContext } from './BadDebtFormProvider';
+
+import { compose } from 'utils';
+
+const defaultInitialValues = {
+ expense_account_id: '',
+ reason: '',
+ amount: '',
+};
+
+function BadDebtForm({
+ // #withDialogActions
+ closeDialog,
+
+ // #withCurrentOrganization
+ organization: { base_currency },
+}) {
+ const { invoice, dialogName, createBadDebtMutate, cancelBadDebtMutate } =
+ useBadDebtContext();
+
+ // Initial form values
+ const initialValues = {
+ ...defaultInitialValues,
+ currency_code: base_currency,
+ amount: invoice.due_amount,
+ };
+
+ // Handles the form submit.
+ const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
+ const form = {
+ ...omit(values, ['currency_code']),
+ };
+
+ // Handle request response success.
+ const onSuccess = (response) => {
+ AppToaster.show({
+ message: intl.get('badDebt_success_message'),
+ intent: Intent.SUCCESS,
+ });
+ closeDialog(dialogName);
+ };
+
+ // Handle request response errors.
+ const onError = ({
+ response: {
+ data: { errors },
+ },
+ }) => {
+ if (errors) {
+ transformErrors(errors, { setErrors });
+ }
+ setSubmitting(false);
+ };
+ createBadDebtMutate([invoice.id, form]).then(onSuccess).catch(onError);
+ };
+
+ return (
+
+ );
+}
+
+export default compose(
+ withDialogActions,
+ withCurrentOrganization(),
+)(BadDebtForm);
diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtForm.schema.js b/src/containers/Dialogs/BadDebtDialog/BadDebtForm.schema.js
new file mode 100644
index 000000000..dc3d00f61
--- /dev/null
+++ b/src/containers/Dialogs/BadDebtDialog/BadDebtForm.schema.js
@@ -0,0 +1,17 @@
+import * as Yup from 'yup';
+import intl from 'react-intl-universal';
+import { DATATYPES_LENGTH } from 'common/dataTypes';
+
+const Schema = Yup.object().shape({
+ expense_account_id: Yup.number()
+ .required()
+ .label(intl.get('expense_account_id')),
+ amount: Yup.number().required().label(intl.get('amount')),
+ reason: Yup.string()
+ .required()
+ .min(3)
+ .max(DATATYPES_LENGTH.TEXT)
+ .label(intl.get('reason')),
+});
+
+export const CreateBadDebtFormSchema = Schema;
diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtFormContent.js b/src/containers/Dialogs/BadDebtDialog/BadDebtFormContent.js
new file mode 100644
index 000000000..568c04a8c
--- /dev/null
+++ b/src/containers/Dialogs/BadDebtDialog/BadDebtFormContent.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Form } from 'formik';
+
+import BadDebtFormFields from './BadDebtFormFields';
+import BadDebtFormFloatingActions from './BadDebtFormFloatingActions';
+
+/**
+ * Bad debt form content.
+ */
+export default function BadDebtFormContent() {
+ return (
+
+ );
+}
diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtFormFields.js b/src/containers/Dialogs/BadDebtDialog/BadDebtFormFields.js
new file mode 100644
index 000000000..2d52b4a3c
--- /dev/null
+++ b/src/containers/Dialogs/BadDebtDialog/BadDebtFormFields.js
@@ -0,0 +1,121 @@
+import React from 'react';
+import { FastField, ErrorMessage } from 'formik';
+import { FormattedMessage as T } from 'components';
+
+import { useAutofocus } from 'hooks';
+import {
+ Classes,
+ FormGroup,
+ TextArea,
+ ControlGroup,
+ Callout,
+ Intent,
+} from '@blueprintjs/core';
+import classNames from 'classnames';
+import { CLASSES } from 'common/classes';
+import { ACCOUNT_TYPE } from 'common/accountTypes';
+import { inputIntent } from 'utils';
+import {
+ AccountsSuggestField,
+ InputPrependText,
+ MoneyInputGroup,
+ FieldRequiredHint,
+} from 'components';
+
+import { useBadDebtContext } from './BadDebtFormProvider';
+
+/**
+ * Bad debt form fields.
+ */
+function BadDebtFormFields() {
+ const amountfieldRef = useAutofocus();
+
+ const { accounts } = useBadDebtContext();
+
+ return (
+
+
+
+
+
+
+
+ {/*------------ Written-off amount -----------*/}
+
+ {({
+ form: { values, setFieldValue },
+ field: { value },
+ meta: { error, touched },
+ }) => (
+ }
+ labelInfo={}
+ className={classNames('form-group--amount', CLASSES.FILL)}
+ intent={inputIntent({ error, touched })}
+ helperText={}
+ >
+
+
+
+ {
+ setFieldValue('amount', amount);
+ }}
+ intent={inputIntent({ error, touched })}
+ disabled={amountfieldRef}
+ />
+
+
+ )}
+
+ {/*------------ Expense account -----------*/}
+
+ {({ form, field: { value }, meta: { error, touched } }) => (
+ }
+ className={classNames(
+ 'form-group--expense_account_id',
+ 'form-group--select-list',
+ CLASSES.FILL,
+ )}
+ labelInfo={}
+ intent={inputIntent({ error, touched })}
+ helperText={}
+ >
+
+ form.setFieldValue('expense_account_id', id)
+ }
+ filterByTypes={[ACCOUNT_TYPE.EXPENSE]}
+ />
+
+ )}
+
+ {/*------------ reason -----------*/}
+
+ {({ field, meta: { error, touched } }) => (
+ }
+ labelInfo={}
+ className={'form-group--reason'}
+ intent={inputIntent({ error, touched })}
+ helperText={}
+ >
+
+
+ )}
+
+
+ );
+}
+
+export default BadDebtFormFields;
diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtFormFloatingActions.js b/src/containers/Dialogs/BadDebtDialog/BadDebtFormFloatingActions.js
new file mode 100644
index 000000000..fe50fda63
--- /dev/null
+++ b/src/containers/Dialogs/BadDebtDialog/BadDebtFormFloatingActions.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import { Intent, Button, Classes } from '@blueprintjs/core';
+import { useFormikContext } from 'formik';
+import { FormattedMessage as T } from 'components';
+
+import { useBadDebtContext } from './BadDebtFormProvider';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import { compose } from 'utils';
+
+
+/**
+ * Bad bebt form floating actions.
+ */
+function BadDebtFormFloatingActions({
+ // #withDialogActions
+ closeDialog,
+}) {
+ // bad debt invoice dialog context.
+ const { dialogName } = useBadDebtContext();
+
+ // Formik context.
+ const { isSubmitting } = useFormikContext();
+
+ // Handle close button click.
+ const handleCancelBtnClick = () => {
+ closeDialog(dialogName);
+ };
+
+ return (
+
+ );
+}
+
+export default compose(withDialogActions)(BadDebtFormFloatingActions);
diff --git a/src/containers/Dialogs/BadDebtDialog/BadDebtFormProvider.js b/src/containers/Dialogs/BadDebtDialog/BadDebtFormProvider.js
new file mode 100644
index 000000000..acc7f36b8
--- /dev/null
+++ b/src/containers/Dialogs/BadDebtDialog/BadDebtFormProvider.js
@@ -0,0 +1,46 @@
+import React from 'react';
+
+import { DialogContent } from 'components';
+import {
+ useAccounts,
+ useInvoice,
+ useCreateBadDebt,
+ useCancelBadDebt,
+} from 'hooks/query';
+
+const BadDebtContext = React.createContext();
+
+/**
+ * Bad debt provider.
+ */
+function BadDebtFormProvider({ invoiceId, dialogName, ...props }) {
+ // Handle fetch accounts data.
+ const { data: accounts, isLoading: isAccountsLoading } = useAccounts();
+
+ // Handle fetch invoice data.
+ const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, {
+ enabled: !!invoiceId,
+ });
+
+ // Create and cancel bad debt mutations.
+ const { mutateAsync: createBadDebtMutate } = useCreateBadDebt();
+
+ // State provider.
+ const provider = {
+ accounts,
+ invoice,
+ invoiceId,
+ dialogName,
+ createBadDebtMutate,
+ };
+
+ return (
+
+
+
+ );
+}
+
+const useBadDebtContext = () => React.useContext(BadDebtContext);
+
+export { BadDebtFormProvider, useBadDebtContext };
diff --git a/src/containers/Dialogs/BadDebtDialog/index.js b/src/containers/Dialogs/BadDebtDialog/index.js
new file mode 100644
index 000000000..7641bfb03
--- /dev/null
+++ b/src/containers/Dialogs/BadDebtDialog/index.js
@@ -0,0 +1,29 @@
+import React from 'react';
+
+import { FormattedMessage as T } from 'components';
+import { Dialog, DialogSuspense } from 'components';
+import withDialogRedux from 'components/DialogReduxConnect';
+import { compose } from 'redux';
+
+const BadDebtDialogContent = React.lazy(() => import('./BadDebtDialogContent'));
+
+/**
+ * Bad debt dialog.
+ */
+function BadDebtDialog({ dialogName, payload: { invoiceId = null }, isOpen }) {
+ return (
+ }
+ isOpen={isOpen}
+ canEscapeJeyClose={true}
+ autoFocus={true}
+ className={'dialog--bad-debt'}
+ >
+
+
+
+
+ );
+}
+export default compose(withDialogRedux())(BadDebtDialog);
diff --git a/src/containers/Dialogs/BadDebtDialog/utils.js b/src/containers/Dialogs/BadDebtDialog/utils.js
new file mode 100644
index 000000000..c2845c87f
--- /dev/null
+++ b/src/containers/Dialogs/BadDebtDialog/utils.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Intent } from '@blueprintjs/core';
+
+import { AppToaster } from 'components';
+
+/**
+ * Transformes the response errors types.
+ */
+export const transformErrors = (errors, { setErrors }) => {
+ if (errors.some(({ type }) => type === 'SALE_INVOICE_ALREADY_WRITTEN_OFF')) {
+ AppToaster.show({
+ message: 'SALE_INVOICE_ALREADY_WRITTEN_OFF',
+ // message: intl.get(''),
+ intent: Intent.DANGER,
+ });
+ }
+};
diff --git a/src/containers/Drawers/BillDrawer/BillDrawerDetails.js b/src/containers/Drawers/BillDrawer/BillDrawerDetails.js
index 60df54da1..4b72212da 100644
--- a/src/containers/Drawers/BillDrawer/BillDrawerDetails.js
+++ b/src/containers/Drawers/BillDrawer/BillDrawerDetails.js
@@ -38,6 +38,11 @@ export default function BillDrawerDetails() {
id={'landed_cost'}
panel={}
/>
+
);
diff --git a/src/containers/Drawers/InvoiceDetailDrawer/BadDebtMenuItem.js b/src/containers/Drawers/InvoiceDetailDrawer/BadDebtMenuItem.js
new file mode 100644
index 000000000..ea86bd2f7
--- /dev/null
+++ b/src/containers/Drawers/InvoiceDetailDrawer/BadDebtMenuItem.js
@@ -0,0 +1,70 @@
+import React from 'react';
+import {
+ Button,
+ NavbarGroup,
+ Classes,
+ NavbarDivider,
+ Popover,
+ PopoverInteractionKind,
+ Position,
+ Intent,
+ MenuItem,
+ Menu,
+} from '@blueprintjs/core';
+import { If, Icon, FormattedMessage as T } from 'components';
+import { useInvoiceDetailDrawerContext } from './InvoiceDetailDrawerProvider';
+
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import withAlertsActions from 'containers/Alert/withAlertActions';
+
+import { compose } from 'utils';
+
+function BadDebtMenuItem({
+ // #withDialogActions
+ openDialog,
+
+ // #withAlertsActions
+ openAlert,
+}) {
+ // Invoice detail drawer context.
+ const { invoiceId, invoice } = useInvoiceDetailDrawerContext();
+
+ const handleBadDebtInvoiceDialog = () => {
+ openDialog('invoice-bad-debt', { invoiceId });
+ };
+
+ const handleBadDebtInvoiceAlert = () => {
+ openAlert('bad-debt', { invoiceId });
+ };
+
+ return (
+
+
+ }
+ onClick={handleBadDebtInvoiceAlert}
+ />
+
+
+ }
+ />
+
+ }
+ minimal={true}
+ interactionKind={PopoverInteractionKind.CLICK}
+ position={Position.BOTTOM_LEFT}
+ canOutsideClickClose={false}
+ usePortal={false}
+ modifiers={{
+ offset: { offset: '0, 4' },
+ }}
+ >
+ } minimal={true} />
+
+ );
+}
+export default compose(withDialogActions, withAlertsActions)(BadDebtMenuItem);
diff --git a/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetail.js b/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetail.js
index e80b8b9bf..6318900b0 100644
--- a/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetail.js
+++ b/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetail.js
@@ -30,6 +30,11 @@ export default function InvoiceDetail() {
id={'journal_entries'}
panel={}
/>
+
);
diff --git a/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailActionsBar.js b/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailActionsBar.js
index 32e9b709b..dc821f2d3 100644
--- a/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailActionsBar.js
+++ b/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailActionsBar.js
@@ -6,20 +6,33 @@ import {
NavbarGroup,
Classes,
NavbarDivider,
+ Popover,
+ PopoverInteractionKind,
+ Position,
Intent,
+ MenuItem,
+ Menu,
} from '@blueprintjs/core';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { useInvoiceDetailDrawerContext } from './InvoiceDetailDrawerProvider';
+import { moreVertOptions } from '../../../common/moreVertOptions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
-import { If, Icon, FormattedMessage as T } from 'components';
+import {
+ If,
+ Icon,
+ FormattedMessage as T,
+ // MoreVertMenutItems,
+} from 'components';
import { compose } from 'utils';
+import BadDebtMenuItem from './BadDebtMenuItem';
+
/**
* Invoice details action bar.
*/
@@ -58,6 +71,13 @@ function InvoiceDetailActionsBar({
const handleQuickPaymentInvoice = () => {
openDialog('quick-payment-receive', { invoiceId });
};
+ const handleBadDebtInvoiceDialog = () => () => {
+ openDialog('invoice-bad-debt', { invoiceId });
+ };
+
+ const handleBadDebtInvoiceAlert = () => {
+ openAlert('bad-debt', { invoiceId });
+ };
return (
@@ -91,6 +111,8 @@ function InvoiceDetailActionsBar({
intent={Intent.DANGER}
onClick={handleDeleteInvoice}
/>
+
+
);
diff --git a/src/containers/Sales/Invoices/InvoicesAlerts.js b/src/containers/Sales/Invoices/InvoicesAlerts.js
index 7325876da..cae3ac2e5 100644
--- a/src/containers/Sales/Invoices/InvoicesAlerts.js
+++ b/src/containers/Sales/Invoices/InvoicesAlerts.js
@@ -7,10 +7,15 @@ const InvoiceDeliverAlert = React.lazy(() =>
import('../../Alerts/Invoices/InvoiceDeliverAlert'),
);
+const BadDebtAlert = React.lazy(() =>
+ import('../../Alerts/Invoices/BadDebtAlert'),
+);
+
/**
* Invoices alert.
*/
export default [
{ name: 'invoice-delete', component: InvoiceDeleteAlert },
{ name: 'invoice-deliver', component: InvoiceDeliverAlert },
+ { name: 'bad-debt', component: BadDebtAlert },
];
diff --git a/src/hooks/query/invoices.js b/src/hooks/query/invoices.js
index 58580dc83..22ad30b7d 100644
--- a/src/hooks/query/invoices.js
+++ b/src/hooks/query/invoices.js
@@ -194,3 +194,38 @@ export function useRefreshInvoices() {
},
};
}
+
+export function useCreateBadDebt(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation(
+ ([id, values]) => apiRequest.post(`sales/invoices/${id}/writeoff`, values),
+ {
+ onSuccess: (res, [id, values]) => {
+ // Invalidate
+ queryClient.invalidateQueries([t.BAD_DEBT, id]);
+
+ // Common invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ },
+ );
+}
+
+export function useCancelBadDebt(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation((id) => apiRequest.post(`sales/invoices/${id}/writeoff/cancel`), {
+ onSuccess: (res, id) => {
+ // Invalidate
+ queryClient.invalidateQueries([t.CANCEL_BAD_DEBT, id]);
+
+ // Common invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ });
+}
diff --git a/src/hooks/query/types.js b/src/hooks/query/types.js
index 9b3637f27..54952a47c 100644
--- a/src/hooks/query/types.js
+++ b/src/hooks/query/types.js
@@ -85,6 +85,8 @@ const SALE_INVOICES = {
SALE_INVOICES: 'SALE_INVOICES',
SALE_INVOICE: 'SALE_INVOICE',
SALE_INVOICES_DUE: 'SALE_INVOICES_DUE',
+ BAD_DEBT: 'BAD_DEBT',
+ CANCEL_BAD_DEBT: 'CANCEL_BAD_DEBT',
};
const USERS = {
@@ -137,7 +139,8 @@ const CASH_FLOW_ACCOUNTS = {
CASH_FLOW_ACCOUNTS: 'CASH_FLOW_ACCOUNTS',
CASH_FLOW_TRANSACTIONS: 'CASH_FLOW_TRANSACTIONS',
CASH_FLOW_TRANSACTION: 'CASH_FLOW_TRANSACTION',
- CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY: 'CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY'
+ CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY:
+ 'CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY',
};
export default {
diff --git a/src/lang/ar/index.json b/src/lang/ar/index.json
index f6b14d9c0..6fea45707 100644
--- a/src/lang/ar/index.json
+++ b/src/lang/ar/index.json
@@ -1012,6 +1012,8 @@
"jump_to_the_bills": "انتقل إلي فواتير الشراء.",
"jump_to_the_manual_journals": "انتقل إلي القيود اليدوية.",
"jump_to_the_items": "انتقل إلي المنتجات.",
+ "jump_to_the_add_money_in": "انتقل إلي إيداع إلي الحساب.",
+ "jump_to_the_add_money_out": "انتقل إلي سحب من الحساب.",
"jump_to_the_balance_sheet": "انتقل إلي تقرير الميزانية العمومية.",
"jump_to_the_profit_loss_sheet": "انتقل إلي تقرير قائمة الدخل.",
"jump_to_the_journal_sheet": "انتقل إلي دفتر اليومية العامة.",
@@ -1371,7 +1373,6 @@
"payment_made.empty_status.title": "المنشأة لم تدفع اي اموال إلي الموردين ، إلي حد الأن!.",
"estimate.delete.error.estimate_converted_to_invoice": "لا يمكن حذف عملية عرض اسعار الذي تم تحويلها إلي فاتورة بيع.",
"landed_cost.action.delete.success_message": "The landed cost has been deleted successfully.",
-
"items.option.only_active": "Only active",
"items.option_all_items.hint": "جميع الاصناف ، بما في ذلك تلك الاصناف لديها رصيد صفر.",
"items.option_with_transactions": "الاصناف مع معاملات",
@@ -1379,22 +1380,18 @@
"items.option_without_zero_balance": "الاصناف ذات رصيد صفر",
"items.option_without_zero_balance.hint": "قم بتضمين الاصناف واستبعاد تلك التي لديها رصيد صفري.",
"items.label_filter_items": "تصفية الاصناف",
-
"customers.option_all_customers.hint": "All customers, including that ones have zero-balance.",
"customers.option_without_zero_balance": "Customers without zero-balance",
"customers.option_without_zero_balance.hint": "Include customers and exclude that ones have zero-balance.",
"customers.option_with_transactions": "Customers with transactions",
"customers.option_with_transactions.hint": "Include customers that onces have transactions on the given date period only.",
"customers.label_filter_customers": "Filter customers",
-
-
"vendors.option_all_vendors.hint": "All vendors, including that ones have zero-balance.",
"vendors.label_filter_vendors": "Filter Vendors",
"vendors.option_without_zero_balance": "Vendors without zero-balance",
"vendors.option_without_zero_balance.hint": "Include vendors and exclude that ones have zero-balance.",
"vendors.option_with_transactions": "Vendors with transactions",
"vendors.option_with_transactions.hint": "Include vendors that onces have transactions on the given date period only.",
-
"siebar.cashflow": "التدفق النقدي",
"siebar.cashflow.label_cash_and_bank_accounts": "حسابات نقدية والمصارف ",
"cash_flow.label_account_transcations": "معاملات الحساب",
@@ -1405,7 +1402,6 @@
"cash_flow.label.add_bank_account": "اضافة حساب مصرف",
"cash_flow.label.add_money_in": "إيداع إلي الحساب",
"cash_flow.label.add_money_out": "سحب من الحساب",
-
"cash_flow.owner_contribution": "زيادة رأس المال",
"cash_flow.other_income": "إيراد أخر",
"cash_flow.owner_drawings": "سحب رأس المال",
@@ -1436,5 +1432,8 @@
"cash_flow_money_in": "إيداع",
"cash_flow_money_out": "سحب",
"cash_flow_transaction.switch_item": " {value} معاملة علي حساب",
- "cash_flow_transaction.balance_in_bigcapital": "الرصيد في Bigcapital"
+ "cash_flow_transaction.balance_in_bigcapital": "الرصيد في Bigcapital",
+ "badDebt.label_written_off_amount": "المبلغ المشطوب",
+ "badDebt.label": "الديون المعدومة",
+ "badDebt_the_seller_can_charge_the_amount_of_an_invoice": "يمكن للبائع تحميل مبلغ الفاتورة على حساب مصروفات الديون المعدومة عندما يكون من المؤكد أن الفاتورة لن يتم دفعها."
}
\ No newline at end of file
diff --git a/src/lang/en/index.json b/src/lang/en/index.json
index 968aa8644..61228c973 100644
--- a/src/lang/en/index.json
+++ b/src/lang/en/index.json
@@ -1420,5 +1420,15 @@
"AP_aging_summary.filter_options.label": "Filter vendors",
"item.error.type_cannot_change_with_item_has_transactions": "Cannot change item type to inventory with item has associated transactions.",
"item.error.cannot_change_inventory_account": "Cannot change item inventory account while the item has transactions.",
- "customer.link.customer_details": "Customer details ({amount})"
+ "customer.link.customer_details": "Customer details ({amount})",
+ "badDebt.label_written_off_amount": "Written-off amount",
+ "badDebt.label": "Bad debt",
+ "badDebt.label_cancel_bad_debt": "Cancel bad debt",
+ "badDebt_the_seller_can_charge_the_amount_of_an_invoice": "The seller can charge the amount of an invoice to the bad debt expense account when it is certain that the invoice will not be paid.",
+ "badDebt_success_message":"The given sale invoice has been writte-off successfully.",
+ "are_sure_to_cancel_this_invoice": "Are you sure you want to cancel this invoice?",
+
+ "payment_transactions": "Payment transactions"
+
+
}
\ No newline at end of file
diff --git a/src/static/json/icons.js b/src/static/json/icons.js
index 28516dab0..42a191624 100644
--- a/src/static/json/icons.js
+++ b/src/static/json/icons.js
@@ -502,5 +502,11 @@ export default {
'arrow-upward': {
path: ['M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z'],
viewBox: '0 0 24 24',
- }
+ },
+ 'more-vert': {
+ path: [
+ 'M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z',
+ ],
+ viewBox: '0 0 24 24',
+ },
};
diff --git a/src/style/pages/BadDebt/BadDebtDialog.scss b/src/style/pages/BadDebt/BadDebtDialog.scss
new file mode 100644
index 000000000..a83fcc41b
--- /dev/null
+++ b/src/style/pages/BadDebt/BadDebtDialog.scss
@@ -0,0 +1,34 @@
+.dialog--bad-debt {
+ max-width: 400px;
+
+ .bp3-dialog-body {
+ .bp3-form-group {
+ margin-bottom: 15px;
+ margin-top: 15px;
+
+ label.bp3-label {
+ margin-bottom: 3px;
+ font-size: 13px;
+ }
+ }
+
+ .bp3-callout {
+ font-size: 13px;
+ }
+ .form-group {
+ &--reason {
+ .bp3-form-content {
+ textarea {
+ width: 100%;
+ min-width: 100%;
+ font-size: 14px;
+ }
+ }
+ }
+ }
+ }
+
+ .bp3-dialog-footer {
+ padding-top: 10px;
+ }
+}