diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceDialogContent.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceDialogContent.js
new file mode 100644
index 000000000..6109cc66e
--- /dev/null
+++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceDialogContent.js
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import 'style/pages/CustomerOpeningBalance/CustomerOpeningBalance.scss';
+
+import CustomerOpeningBalanceForm from './CustomerOpeningBalanceForm';
+import { CustomerOpeningBalanceFormProvider } from './CustomerOpeningBalanceFormProvider';
+
+/**
+ * Customer opening balance dialog content.
+ * @returns
+ */
+export default function CustomerOpeningBalanceDialogContent({
+ // #ownProps
+ dialogName,
+ customerId,
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFields.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFields.js
new file mode 100644
index 000000000..e79bd4d37
--- /dev/null
+++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFields.js
@@ -0,0 +1,115 @@
+import React from 'react';
+import { Classes, Position, FormGroup, ControlGroup } from '@blueprintjs/core';
+import { DateInput } from '@blueprintjs/datetime';
+import { isEqual } from 'lodash';
+import { FastField, useFormikContext } from 'formik';
+import { momentFormatter, tansformDateValue, handleDateChange } from 'utils';
+import { Features } from 'common';
+import classNames from 'classnames';
+
+import {
+ If,
+ Icon,
+ FormattedMessage as T,
+ ExchangeRateMutedField,
+ BranchSelect,
+ BranchSelectButton,
+ FeatureCan,
+ InputPrependText,
+} from 'components';
+import { FMoneyInputGroup, FFormGroup } from '../../../components/Forms';
+
+import { useCustomerOpeningBalanceContext } from './CustomerOpeningBalanceFormProvider';
+import { useSetPrimaryBranchToForm } from './utils';
+
+import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
+import { compose } from 'utils';
+
+/**
+ * Customer Opening balance fields.
+ * @returns
+ */
+function CustomerOpeningBalanceFields({
+ // #withCurrentOrganization
+ organization: { base_currency },
+}) {
+ // Formik context.
+ const { values } = useFormikContext();
+
+ const { branches, customer } = useCustomerOpeningBalanceContext();
+
+ // Sets the primary branch to form.
+ useSetPrimaryBranchToForm();
+
+ return (
+
+ {/*------------ Opening balance -----------*/}
+ }
+ >
+
+
+
+
+
+
+ {/*------------ Opening balance at -----------*/}
+
+ {({ form, field: { value } }) => (
+
+ }
+ className={Classes.FILL}
+ >
+ {
+ form.setFieldValue('opening_balance_at', formattedDate);
+ })}
+ value={tansformDateValue(value)}
+ popoverProps={{ position: Position.BOTTOM, minimal: true }}
+ inputProps={{
+ leftIcon: ,
+ }}
+ />
+
+ )}
+
+
+
+ {/*------------ Opening balance exchange rate -----------*/}
+
+
+
+ {/*------------ Opening balance branch id -----------*/}
+
+ }
+ name={'opening_balance_branch_id'}
+ className={classNames('form-group--select-list', Classes.FILL)}
+ >
+
+
+
+
+ );
+}
+export default compose(withCurrentOrganization())(CustomerOpeningBalanceFields);
diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.js
new file mode 100644
index 000000000..2e9715b26
--- /dev/null
+++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.js
@@ -0,0 +1,81 @@
+import React from 'react';
+import moment from 'moment';
+import intl from 'react-intl-universal';
+import { Formik } from 'formik';
+import { Intent } from '@blueprintjs/core';
+
+import { AppToaster } from 'components';
+import { CreateCustomerOpeningBalanceFormSchema } from './CustomerOpeningBalanceForm.schema';
+import { useCustomerOpeningBalanceContext } from './CustomerOpeningBalanceFormProvider';
+
+import CustomerOpeningBalanceFormContent from './CustomerOpeningBalanceFormContent';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+
+import { compose } from 'utils';
+
+const defaultInitialValues = {
+ opening_balance: '0',
+ opening_balance_branch_id: '',
+ opening_balance_exchange_rate: 1,
+ opening_balance_at: moment(new Date()).format('YYYY-MM-DD'),
+};
+
+/**
+ * Customer Opening balance form.
+ * @returns
+ */
+function CustomerOpeningBalanceForm({
+ // #withDialogActions
+ closeDialog,
+}) {
+ const { dialogName, customer, editCustomerOpeningBalanceMutate } =
+ useCustomerOpeningBalanceContext();
+
+ // Initial form values
+ const initialValues = {
+ ...defaultInitialValues,
+ ...customer,
+ };
+
+ // Handles the form submit.
+ const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
+ const formValues = {
+ ...values,
+ };
+
+ // Handle request response success.
+ const onSuccess = (response) => {
+ AppToaster.show({
+ message: intl.get('customer_opening_balance.success_message'),
+ intent: Intent.SUCCESS,
+ });
+ closeDialog(dialogName);
+ };
+
+ // Handle request response errors.
+ const onError = ({
+ response: {
+ data: { errors },
+ },
+ }) => {
+ if (errors) {
+ }
+ setSubmitting(false);
+ };
+
+ editCustomerOpeningBalanceMutate([customer.id, formValues])
+ .then(onSuccess)
+ .catch(onError);
+ };
+
+ return (
+
+ );
+}
+
+export default compose(withDialogActions)(CustomerOpeningBalanceForm);
diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.schema.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.schema.js
new file mode 100644
index 000000000..a2f608eb0
--- /dev/null
+++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.schema.js
@@ -0,0 +1,10 @@
+import * as Yup from 'yup';
+
+const Schema = Yup.object().shape({
+ opening_balance_branch_id: Yup.string(),
+ opening_balance: Yup.number().nullable(),
+ opening_balance_at: Yup.date(),
+ opening_balance_exchange_rate: Yup.number(),
+});
+
+export const CreateCustomerOpeningBalanceFormSchema = Schema;
diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormContent.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormContent.js
new file mode 100644
index 000000000..fb1318af6
--- /dev/null
+++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormContent.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Form } from 'formik';
+
+import CustomerOpeningBalanceFields from './CustomerOpeningBalanceFields';
+import CustomerOpeningBalanceFormFloatingActions from './CustomerOpeningBalanceFormFloatingActions';
+
+/**
+ * Customer Opening balance form content.
+ * @returns
+ */
+export default function CustomerOpeningBalanceFormContent() {
+ return (
+
+ );
+}
diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormFloatingActions.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormFloatingActions.js
new file mode 100644
index 000000000..12514d7b6
--- /dev/null
+++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormFloatingActions.js
@@ -0,0 +1,50 @@
+import React from 'react';
+import { Intent, Button, Classes } from '@blueprintjs/core';
+import { useFormikContext } from 'formik';
+import { FormattedMessage as T } from 'components';
+
+import { useCustomerOpeningBalanceContext } from './CustomerOpeningBalanceFormProvider';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import { compose } from 'utils';
+
+
+/**
+ * Customer Opening balance floating actions.
+ * @returns
+ */
+function CustomerOpeningBalanceFormFloatingActions({
+ // #withDialogActions
+ closeDialog,
+}) {
+ // dialog context.
+ const { dialogName } = useCustomerOpeningBalanceContext();
+
+ // Formik context.
+ const { isSubmitting } = useFormikContext();
+
+ // Handle close button click.
+ const handleCancelBtnClick = () => {
+ closeDialog(dialogName);
+ };
+
+ return (
+
+ );
+}
+export default compose(withDialogActions)(
+ CustomerOpeningBalanceFormFloatingActions,
+);
diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormProvider.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormProvider.js
new file mode 100644
index 000000000..62d070245
--- /dev/null
+++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceFormProvider.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import { DialogContent } from 'components';
+import {
+ useBranches,
+ useCustomer,
+ useEditCustomerOpeningBalance,
+} from 'hooks/query';
+import { useFeatureCan } from 'hooks/state';
+import { Features } from 'common';
+import { pick } from 'lodash';
+
+const CustomerOpeningBalanceContext = React.createContext();
+
+/**
+ * Customer opening balance provider.
+ * @returns
+ */
+function CustomerOpeningBalanceFormProvider({
+ query,
+ customerId,
+ dialogName,
+ ...props
+}) {
+ // Features guard.
+ const { featureCan } = useFeatureCan();
+ const isBranchFeatureCan = featureCan(Features.Branches);
+
+ const { mutateAsync: editCustomerOpeningBalanceMutate } =
+ useEditCustomerOpeningBalance();
+
+ // Fetches the branches list.
+ const {
+ data: branches,
+ isLoading: isBranchesLoading,
+ isSuccess: isBranchesSuccess,
+ } = useBranches(query, { enabled: isBranchFeatureCan });
+
+ // Handle fetch customer details.
+ const { data: customer, isLoading: isCustomerLoading } = useCustomer(
+ customerId,
+ { enabled: !!customerId },
+ );
+
+ // State provider.
+ const provider = {
+ branches,
+ customer: {
+ ...pick(customer, [
+ 'id',
+ 'opening_balance',
+ 'opening_balance_exchange_rate',
+ 'currency_code',
+ ]),
+ // opening_balance_at: customer.formatted_opening_balance_at,
+ },
+
+ isBranchesSuccess,
+ isBranchesLoading,
+ dialogName,
+ editCustomerOpeningBalanceMutate,
+ };
+
+ return (
+
+
+
+ );
+}
+
+const useCustomerOpeningBalanceContext = () =>
+ React.useContext(CustomerOpeningBalanceContext);
+
+export { CustomerOpeningBalanceFormProvider, useCustomerOpeningBalanceContext };
diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/index.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/index.js
new file mode 100644
index 000000000..fd68d49c2
--- /dev/null
+++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/index.js
@@ -0,0 +1,40 @@
+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 CustomerOpeningBalanceDialogContent = React.lazy(() =>
+ import('./CustomerOpeningBalanceDialogContent'),
+);
+
+/**
+ * Customer opening balance dialog.
+ * @returns
+ */
+function CustomerOpeningBalanceDialog({
+ dialogName,
+ payload: { customerId },
+ isOpen,
+}) {
+ return (
+ }
+ isOpen={isOpen}
+ canEscapeJeyClose={true}
+ autoFocus={true}
+ className={'dialog--customer-opening-balance'}
+ >
+
+
+
+
+ );
+}
+
+export default compose(withDialogRedux())(CustomerOpeningBalanceDialog);
diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/utils.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/utils.js
new file mode 100644
index 000000000..206545395
--- /dev/null
+++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/utils.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import { useFormikContext } from 'formik';
+import { first } from 'lodash';
+
+import { useCustomerOpeningBalanceContext } from './CustomerOpeningBalanceFormProvider';
+
+export const useSetPrimaryBranchToForm = () => {
+ const { setFieldValue } = useFormikContext();
+ const { branches, isBranchesSuccess } = useCustomerOpeningBalanceContext();
+
+ React.useEffect(() => {
+ if (isBranchesSuccess) {
+ const primaryBranch = branches.find((b) => b.primary) || first(branches);
+
+ if (primaryBranch) {
+ setFieldValue('opening_balance_branch_id', primaryBranch.id);
+ }
+ }
+ }, [isBranchesSuccess, setFieldValue, branches]);
+};
diff --git a/src/containers/Drawers/CustomerDetailsDrawer/CustomerDetailsActionsBar.js b/src/containers/Drawers/CustomerDetailsDrawer/CustomerDetailsActionsBar.js
index 48f4ac7ff..226927dba 100644
--- a/src/containers/Drawers/CustomerDetailsDrawer/CustomerDetailsActionsBar.js
+++ b/src/containers/Drawers/CustomerDetailsDrawer/CustomerDetailsActionsBar.js
@@ -19,9 +19,11 @@ import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { useCustomerDetailsDrawerContext } from './CustomerDetailsDrawerProvider';
import withAlertsActions from 'containers/Alert/withAlertActions';
+import withDialogActions from 'containers/Dialog/withDialogActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { Can, Icon, FormattedMessage as T } from 'components';
+import { CustomerMoreMenuItem } from './utils';
import {
AbilitySubject,
SaleInvoiceAction,
@@ -36,6 +38,9 @@ import { compose } from 'utils';
* Customer details actions bar.
*/
function CustomerDetailsActionsBar({
+ // #withDialogActions
+ openDialog,
+
// #withAlertsActions
openAlert,
@@ -75,6 +80,10 @@ function CustomerDetailsActionsBar({
closeDrawer('customer-details-drawer');
};
+ const handleEditOpeningBalance = () => {
+ openDialog('customer-opening-balance', { customerId });
+ };
+
return (
@@ -141,6 +150,12 @@ function CustomerDetailsActionsBar({
onClick={handleDeleteCustomer}
/>
+
+
);
@@ -149,4 +164,5 @@ function CustomerDetailsActionsBar({
export default compose(
withDrawerActions,
withAlertsActions,
+ withDialogActions,
)(CustomerDetailsActionsBar);
diff --git a/src/containers/Drawers/CustomerDetailsDrawer/utils.js b/src/containers/Drawers/CustomerDetailsDrawer/utils.js
new file mode 100644
index 000000000..6fa7b0d59
--- /dev/null
+++ b/src/containers/Drawers/CustomerDetailsDrawer/utils.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import {
+ Button,
+ Popover,
+ PopoverInteractionKind,
+ Position,
+ MenuItem,
+ Menu,
+} from '@blueprintjs/core';
+import { Icon, FormattedMessage as T } from 'components';
+
+/**
+ * Customer more actions menu items.
+ * @returns
+ */
+export function CustomerMoreMenuItem({ payload: { onEditOpeningBalance } }) {
+ return (
+
+ }
+ onClick={onEditOpeningBalance}
+ />
+
+ }
+ >
+ } minimal={true} />
+
+ );
+}
diff --git a/src/hooks/query/customers.js b/src/hooks/query/customers.js
index 75a6d6d3b..38f2f1641 100644
--- a/src/hooks/query/customers.js
+++ b/src/hooks/query/customers.js
@@ -127,6 +127,26 @@ export function useCustomer(id, props) {
);
}
+export function useEditCustomerOpeningBalance(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation(
+ ([id, values]) =>
+ apiRequest.post(`customers/${id}/opening_balance`, values),
+ {
+ onSuccess: (res, [id, values]) => {
+ // Invalidate specific customer.
+ queryClient.invalidateQueries([t.CUSTOMER, id]);
+
+ // Common invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ },
+ );
+}
+
export function useRefreshCustomers() {
const queryClient = useQueryClient();
diff --git a/src/style/pages/CustomerOpeningBalance/CustomerOpeningBalance.scss b/src/style/pages/CustomerOpeningBalance/CustomerOpeningBalance.scss
new file mode 100644
index 000000000..1aeb28e83
--- /dev/null
+++ b/src/style/pages/CustomerOpeningBalance/CustomerOpeningBalance.scss
@@ -0,0 +1,19 @@
+.dialog--customer-opening-balance {
+ max-width: 400px;
+
+ .bp3-dialog-body {
+ .bp3-form-group {
+ margin-bottom: 15px;
+ margin-top: 15px;
+
+ label.bp3-label {
+ margin-bottom: px;
+ font-size: 13px;
+ }
+ }
+ }
+
+ .bp3-dialog-footer {
+ padding-top: 10px;
+ }
+}