diff --git a/src/components/DialogsContainer.js b/src/components/DialogsContainer.js
index 2a32b532d..8ea35efde 100644
--- a/src/components/DialogsContainer.js
+++ b/src/components/DialogsContainer.js
@@ -28,6 +28,7 @@ import SMSMessageDialog from '../containers/Dialogs/SMSMessageDialog';
import TransactionsLockingDialog from '../containers/Dialogs/TransactionsLockingDialog';
import RefundCreditNoteDialog from '../containers/Dialogs/RefundCreditNoteDialog';
import RefundVendorCreditDialog from '../containers/Dialogs/RefundVendorCreditDialog';
+import ReconcileCreditNoteDialog from '../containers/Dialogs/ReconcileCreditNoteDialog';
/**
* Dialogs container.
@@ -64,6 +65,7 @@ export default function DialogsContainer() {
+
);
}
diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteDialogContent.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteDialogContent.js
new file mode 100644
index 000000000..62b18d718
--- /dev/null
+++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteDialogContent.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import { ReconcileCreditNoteFormProvider } from './ReconcileCreditNoteFormProvider';
+import ReconcileCreditNoteForm from './ReconcileCreditNoteForm';
+
+/**
+ * Reconcile credit note dialog content.
+ */
+export default function ReconcileCreditNoteDialogContent({
+ // #ownProps
+ dialogName,
+ creditNoteId,
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteEntriesTable.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteEntriesTable.js
new file mode 100644
index 000000000..c268c7e00
--- /dev/null
+++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteEntriesTable.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+import { MoneyFieldCell, DataTableEditable, FormatDateCell } from 'components';
+import { compose, updateTableCell } from 'utils';
+
+/**
+ * Reconcile credit note entries table.
+ */
+export default function ReconcileCreditNoteEntriesTable({
+ onUpdateData,
+ entries,
+ errors,
+}) {
+ const columns = React.useMemo(
+ () => [
+ {
+ Header: intl.get('invoice_date'),
+ accessor: 'formatted_invoice_date',
+ Cell: FormatDateCell,
+ disableSortBy: true,
+ width: '120',
+ },
+ {
+ Header: intl.get('invoice_no'),
+ accessor: 'invoice_no',
+ disableSortBy: true,
+ width: '100',
+ },
+ {
+ Header: intl.get('amount'),
+ accessor: 'formatted_amount',
+ disableSortBy: true,
+ align: 'right',
+ width: '100',
+ },
+ {
+ Header: intl.get('reconcile_credit_note.column.remaining_amount'),
+ accessor: 'formatted_due_amount',
+ disableSortBy: true,
+ align: 'right',
+ width: '150',
+ },
+ {
+ Header: intl.get('reconcile_credit_note.column.amount_to_credit'),
+ accessor: 'amount',
+ Cell: MoneyFieldCell,
+ disableSortBy: true,
+ width: '150',
+ },
+ ],
+ [],
+ );
+
+ // Handle update data.
+ const handleUpdateData = React.useCallback(
+ (rowIndex, columnId, value) => {
+ const newRows = compose(updateTableCell(rowIndex, columnId, value))(
+ entries,
+ );
+ onUpdateData(newRows);
+ },
+ [onUpdateData, entries],
+ );
+
+ return (
+
+ );
+}
diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.js
new file mode 100644
index 000000000..a238911a3
--- /dev/null
+++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.js
@@ -0,0 +1,99 @@
+import React from 'react';
+import { Formik } from 'formik';
+import { Intent } from '@blueprintjs/core';
+import intl from 'react-intl-universal';
+
+import '../../../style/pages/ReconcileCreditNote/ReconcileCreditNoteForm.scss';
+import { AppToaster } from 'components';
+import { CreateReconcileCreditNoteFormSchema } from './ReconcileCreditNoteForm.schema';
+import { useReconcileCreditNoteContext } from './ReconcileCreditNoteFormProvider';
+import ReconcileCreditNoteFormContent from './ReconcileCreditNoteFormContent';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import { compose, transformToForm } from 'utils';
+import { transformErrors } from './utils';
+
+// Default form initial values.
+const defaultInitialValues = {
+ entries: [
+ {
+ invoice_id: '',
+ amount: '',
+ },
+ ],
+};
+
+/**
+ * Reconcile credit note form.
+ */
+function ReconcileCreditNoteForm({
+ // #withDialogActions
+ closeDialog,
+}) {
+ const {
+ dialogName,
+ creditNoteId,
+ reconcileCreditNotes,
+ createReconcileCreditNoteMutate,
+ } = useReconcileCreditNoteContext();
+
+ // Initial form values.
+ const initialValues = {
+ entries: reconcileCreditNotes.map((entry) => ({
+ ...entry,
+ invoice_id: entry.id,
+ amount: '',
+ })),
+ };
+
+ // Handle form submit.
+ const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
+ setSubmitting(false);
+
+ // Filters the entries.
+ const entries = values.entries
+ .filter((entry) => entry.id && entry.amount)
+ .map((entry) => transformToForm(entry, defaultInitialValues.entries[0]));
+
+ const form = {
+ ...values,
+ entries: entries,
+ };
+
+ // Handle the request success.
+ const onSuccess = (response) => {
+ AppToaster.show({
+ message: intl.get('reconcile_credit_note.success_message'),
+ intent: Intent.SUCCESS,
+ });
+ setSubmitting(false);
+ closeDialog(dialogName);
+ };
+
+ // Handle the request error.
+ const onError = ({
+ response: {
+ data: { errors },
+ },
+ }) => {
+ if (errors) {
+ transformErrors(errors, { setErrors });
+ }
+ setSubmitting(false);
+ };
+
+ createReconcileCreditNoteMutate([creditNoteId, form])
+ .then(onSuccess)
+ .catch(onError);
+ };
+
+ return (
+
+ );
+}
+
+export default compose(withDialogActions)(ReconcileCreditNoteForm);
diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.schema.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.schema.js
new file mode 100644
index 000000000..a6cd9dc09
--- /dev/null
+++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteForm.schema.js
@@ -0,0 +1,12 @@
+import * as Yup from 'yup';
+
+const Schema = Yup.object().shape({
+ entries: Yup.array().of(
+ Yup.object().shape({
+ invoice_id: Yup.number().required(),
+ amount: Yup.number().nullable(),
+ }),
+ ),
+});
+
+export const CreateReconcileCreditNoteFormSchema = Schema;
diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormContent.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormContent.js
new file mode 100644
index 000000000..1c1e12d21
--- /dev/null
+++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormContent.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import { Form } from 'formik';
+import { Choose } from 'components';
+
+import ReconcileCreditNoteFormFields from './ReconcileCreditNoteFormFields';
+import ReconcileCreditNoteFormFloatingActions from './ReconcileCreditNoteFormFloatingActions';
+import { EmptyStatuCallout } from './utils';
+import { useReconcileCreditNoteContext } from './ReconcileCreditNoteFormProvider';
+
+/**
+ * Reconcile credit note form content.
+ */
+export default function ReconcileCreditNoteFormContent() {
+ const { isEmptyStatus } = useReconcileCreditNoteContext();
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFields.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFields.js
new file mode 100644
index 000000000..a035fd95d
--- /dev/null
+++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFields.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import { FastField, useFormikContext } from 'formik';
+import { Classes } from '@blueprintjs/core';
+import { T, TotalLines, TotalLine } from 'components';
+import { getEntriesTotal } from 'containers/Entries/utils';
+import ReconcileCreditNoteEntriesTable from './ReconcileCreditNoteEntriesTable';
+import { useReconcileCreditNoteContext } from './ReconcileCreditNoteFormProvider';
+import { formattedAmount } from 'utils';
+
+/**
+ * Reconcile credit note form fields.
+ */
+export default function ReconcileCreditNoteFormFields() {
+ const {
+ creditNote: { formatted_credits_remaining, currency_code },
+ } = useReconcileCreditNoteContext();
+
+ const { values } = useFormikContext();
+
+ // Calculate the total amount.
+ const totalAmount = React.useMemo(
+ () => getEntriesTotal(values.entries),
+ [values.entries],
+ );
+
+ return (
+
+ {/*------------ Reconcile credit entries table -----------*/}
+
+ {({
+ form: { setFieldValue, values },
+ field: { value },
+ meta: { error, touched },
+ }) => (
+ {
+ setFieldValue('entries', newEntries);
+ }}
+ />
+ )}
+
+
+
+
+ }
+ value={formattedAmount(totalAmount, currency_code)}
+ />
+ }
+ value={formatted_credits_remaining}
+ />
+
+
+
+ );
+}
diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFloatingActions.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFloatingActions.js
new file mode 100644
index 000000000..e885d0325
--- /dev/null
+++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormFloatingActions.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import { useFormikContext } from 'formik';
+import { Intent, Button, Classes } from '@blueprintjs/core';
+import { FormattedMessage as T } from 'components';
+
+import { useReconcileCreditNoteContext } from './ReconcileCreditNoteFormProvider';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import { compose } from 'utils';
+
+/**
+ * Reconcile credit note floating actions.
+ */
+function ReconcileCreditNoteFormFloatingActions({
+ // #withDialogActions
+ closeDialog,
+}) {
+ // Formik context.
+ const { isSubmitting } = useFormikContext();
+
+ const { dialogName } = useReconcileCreditNoteContext();
+
+ // Handle cancel button click.
+ const handleCancelBtnClick = (event) => {
+ closeDialog(dialogName);
+ };
+
+ return (
+
+ );
+}
+export default compose(withDialogActions)(
+ ReconcileCreditNoteFormFloatingActions,
+);
diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormProvider.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormProvider.js
new file mode 100644
index 000000000..92cc90bd1
--- /dev/null
+++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/ReconcileCreditNoteFormProvider.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import { DialogContent } from 'components';
+import {
+ useCreditNote,
+ useReconcileCreditNote,
+ useCreateReconcileCreditNote,
+} from 'hooks/query';
+import { isEmpty } from 'lodash';
+
+const ReconcileCreditNoteDialogContext = React.createContext();
+
+/**
+ * Reconcile credit note provider.
+ */
+function ReconcileCreditNoteFormProvider({
+ creditNoteId,
+ dialogName,
+ ...props
+}) {
+ // Handle fetch reconcile credit note details.
+ const { isLoading: isReconcileCreditLoading, data: reconcileCreditNotes } =
+ useReconcileCreditNote(creditNoteId, {
+ enabled: !!creditNoteId,
+ });
+
+ // Handle fetch vendor credit details.
+ const { data: creditNote, isLoading: isCreditNoteLoading } = useCreditNote(
+ creditNoteId,
+ {
+ enabled: !!creditNoteId,
+ },
+ );
+
+ // Create reconcile credit note mutations.
+ const { mutateAsync: createReconcileCreditNoteMutate } =
+ useCreateReconcileCreditNote();
+
+ // Detarmines the datatable empty status.
+ const isEmptyStatus = isEmpty(reconcileCreditNotes);
+
+ // provider payload.
+ const provider = {
+ dialogName,
+ reconcileCreditNotes,
+ createReconcileCreditNoteMutate,
+ isEmptyStatus,
+ creditNote,
+ creditNoteId,
+ };
+
+ return (
+
+
+
+ );
+}
+
+const useReconcileCreditNoteContext = () =>
+ React.useContext(ReconcileCreditNoteDialogContext);
+
+export { ReconcileCreditNoteFormProvider, useReconcileCreditNoteContext };
diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/index.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/index.js
new file mode 100644
index 000000000..217d93b32
--- /dev/null
+++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/index.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import { FormattedMessage as T, Dialog, DialogSuspense } from 'components';
+import withDialogRedux from 'components/DialogReduxConnect';
+import { compose } from 'utils';
+
+const ReconcileCreditNoteDialogContent = React.lazy(() =>
+ import('./ReconcileCreditNoteDialogContent'),
+);
+
+/**
+ * Reconcile credit note dialog.
+ */
+function ReconcileCreditNoteDialog({
+ dialogName,
+ payload: { creditNoteId },
+ isOpen,
+}) {
+ return (
+ }
+ canEscapeKeyClose={true}
+ isOpen={isOpen}
+ className="dialog--reconcile-credit-form"
+ >
+
+
+
+
+ );
+}
+
+export default compose(withDialogRedux())(ReconcileCreditNoteDialog);
diff --git a/src/containers/Dialogs/ReconcileCreditNoteDialog/utils.js b/src/containers/Dialogs/ReconcileCreditNoteDialog/utils.js
new file mode 100644
index 000000000..4f9b6a8f1
--- /dev/null
+++ b/src/containers/Dialogs/ReconcileCreditNoteDialog/utils.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+import { Callout, Intent, Classes } from '@blueprintjs/core';
+
+import { AppToaster, T } from 'components';
+
+export const transformErrors = (errors, { setErrors }) => {
+ if (errors.some((e) => e.type === 'INVOICES_HAS_NO_REMAINING_AMOUNT')) {
+ AppToaster.show({
+ message: 'INVOICES_HAS_NO_REMAINING_AMOUNT',
+ intent: Intent.DANGER,
+ });
+ }
+
+ if (
+ errors.find((error) => error.type === 'CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT')
+ ) {
+ AppToaster.show({
+ message: 'CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT',
+ intent: Intent.DANGER,
+ });
+ }
+};
+
+export function EmptyStatuCallout() {
+ return (
+
+ );
+}
diff --git a/src/containers/Purchases/CreditNotes/CreditNoteForm/VendorCreditNoteFloatingActions.js b/src/containers/Purchases/CreditNotes/CreditNoteForm/VendorCreditNoteFloatingActions.js
index 0d2c7a72b..ca41694fb 100644
--- a/src/containers/Purchases/CreditNotes/CreditNoteForm/VendorCreditNoteFloatingActions.js
+++ b/src/containers/Purchases/CreditNotes/CreditNoteForm/VendorCreditNoteFloatingActions.js
@@ -27,6 +27,7 @@ export default function VendorCreditNoteFloatingActions() {
// Credit note form context.
const { setSubmitPayload, vendorCredit } = useVendorCreditNoteFormContext();
+
// Handle submit as open button click.
const handleSubmitOpenBtnClick = (event) => {
setSubmitPayload({ redirect: true, open: true });
@@ -70,7 +71,6 @@ export default function VendorCreditNoteFloatingActions() {
const handleClearBtnClick = (event) => {
resetForm();
};
-
return (
{/* ----------- Save And Open ----------- */}
diff --git a/src/containers/Purchases/CreditNotes/CreditNoteForm/VendorCreditNoteForm.js b/src/containers/Purchases/CreditNotes/CreditNoteForm/VendorCreditNoteForm.js
index dff4f9571..0a5639ba5 100644
--- a/src/containers/Purchases/CreditNotes/CreditNoteForm/VendorCreditNoteForm.js
+++ b/src/containers/Purchases/CreditNotes/CreditNoteForm/VendorCreditNoteForm.js
@@ -1,9 +1,9 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { Formik, Form } from 'formik';
-import { Button, Intent } from '@blueprintjs/core';
+import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
-import { sumBy, omit, isEmpty } from 'lodash';
+import { isEmpty } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
diff --git a/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteForm.js b/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteForm.js
index 5e0aca734..25a636aa0 100644
--- a/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteForm.js
+++ b/src/containers/Sales/CreditNotes/CreditNoteForm/CreditNoteForm.js
@@ -3,7 +3,7 @@ import { useHistory } from 'react-router-dom';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
-import { sumBy, omit, isEmpty } from 'lodash';
+import { isEmpty } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
diff --git a/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesDataTable.js b/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesDataTable.js
index a1721a8bb..e3c8a903e 100644
--- a/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesDataTable.js
+++ b/src/containers/Sales/CreditNotes/CreditNotesLanding/CreditNotesDataTable.js
@@ -105,6 +105,11 @@ function CreditNotesDataTable({
openAlert('credit-note-open', { creditNoteId: id });
};
+ // Handle reconcile credit note.
+ const handleReconcileCreditNote = ({ id }) => {
+ openDialog('reconcile-credit-note', { creditNoteId: id });
+ };
+
return (
diff --git a/src/containers/Sales/CreditNotes/CreditNotesLanding/components.js b/src/containers/Sales/CreditNotes/CreditNotesLanding/components.js
index b1ea5a384..81adb4906 100644
--- a/src/containers/Sales/CreditNotes/CreditNotesLanding/components.js
+++ b/src/containers/Sales/CreditNotes/CreditNotesLanding/components.js
@@ -14,7 +14,7 @@ import {
import { safeCallback } from 'utils';
export function ActionsMenu({
- payload: { onEdit, onDelete, onRefund, onOpen, onViewDetails },
+ payload: { onEdit, onDelete, onRefund, onOpen, onReconcile, onViewDetails },
row: { original },
}) {
return (
@@ -44,7 +44,12 @@ export function ActionsMenu({
onClick={safeCallback(onOpen, original)}
/>
-
+ }
+ // text={intl.get('credit_note.action.refund_credit_note')}
+ onClick={safeCallback(onReconcile, original)}
+ />