diff --git a/src/components/DialogsContainer.js b/src/components/DialogsContainer.js
index 833016beb..8ddff3496 100644
--- a/src/components/DialogsContainer.js
+++ b/src/components/DialogsContainer.js
@@ -38,6 +38,8 @@ import WarehouseFormDialog from '../containers/Dialogs/WarehouseFormDialog';
import BranchFormDialog from '../containers/Dialogs/BranchFormDialog';
import BranchActivateDialog from '../containers/Dialogs/BranchActivateDialog';
import WarehouseActivateDialog from '../containers/Dialogs/WarehouseActivateDialog';
+import CustomerOpeningBalanceDialog from '../containers/Dialogs/CustomerOpeningBalanceDialog';
+import VendorOpeningBalanceDialog from '../containers/Dialogs/VendorOpeningBalanceDialog';
/**
* Dialogs container.
@@ -86,6 +88,8 @@ export default function DialogsContainer() {
+
+
);
}
diff --git a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.js b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.js
index 2e9715b26..3a296fe3d 100644
--- a/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.js
+++ b/src/containers/Dialogs/CustomerOpeningBalanceDialog/CustomerOpeningBalanceForm.js
@@ -3,6 +3,7 @@ import moment from 'moment';
import intl from 'react-intl-universal';
import { Formik } from 'formik';
import { Intent } from '@blueprintjs/core';
+import { defaultTo } from 'lodash';
import { AppToaster } from 'components';
import { CreateCustomerOpeningBalanceFormSchema } from './CustomerOpeningBalanceForm.schema';
@@ -35,6 +36,7 @@ function CustomerOpeningBalanceForm({
const initialValues = {
...defaultInitialValues,
...customer,
+ opening_balance: defaultTo(customer.opening_balance, ''),
};
// Handles the form submit.
diff --git a/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceDialogContent.js b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceDialogContent.js
new file mode 100644
index 000000000..19ae1055e
--- /dev/null
+++ b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceDialogContent.js
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import 'style/pages/VendorOpeningBalance/VendorOpeningBalance.scss';
+
+import VendorOpeningBalanceForm from './VendorOpeningBalanceForm';
+import { VendorOpeningBalanceFormProvider } from './VendorOpeningBalanceFormProvider';
+
+/**
+ * Vendor Opening balance dialog content.
+ * @returns
+ */
+export default function VendorOpeningBalanceDialogContent({
+ // #ownProps
+ dialogName,
+ vendorId,
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceForm.js b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceForm.js
new file mode 100644
index 000000000..4b732b79e
--- /dev/null
+++ b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceForm.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import moment from 'moment';
+import intl from 'react-intl-universal';
+import { Formik } from 'formik';
+import { Intent } from '@blueprintjs/core';
+import { defaultTo } from 'lodash';
+
+import { AppToaster } from 'components';
+import { CreateVendorOpeningBalanceFormSchema } from './VendorOpeningBalanceForm.schema';
+import { useVendorOpeningBalanceContext } from './VendorOpeningBalanceFormProvider';
+
+import VendorOpeningBalanceFormContent from './VendorOpeningBalanceFormContent';
+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'),
+};
+
+/**
+ * Vendor Opening balance form.
+ * @returns
+ */
+function VendorOpeningBalanceForm({
+ // #withDialogActions
+ closeDialog,
+}) {
+ const { dialogName, vendor, editVendorOpeningBalanceMutate } =
+ useVendorOpeningBalanceContext();
+
+ // Initial form values
+ const initialValues = {
+ ...defaultInitialValues,
+ ...vendor,
+ opening_balance: defaultTo(vendor.opening_balance, ''),
+
+ };
+
+ // Handles the form submit.
+ const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
+ const formValues = {
+ ...values,
+ };
+
+ // Handle request response success.
+ const onSuccess = (response) => {
+ AppToaster.show({
+ message: intl.get('vendor_opening_balance.success_message'),
+ intent: Intent.SUCCESS,
+ });
+ closeDialog(dialogName);
+ };
+
+ // Handle request response errors.
+ const onError = ({
+ response: {
+ data: { errors },
+ },
+ }) => {
+ if (errors) {
+ }
+ setSubmitting(false);
+ };
+
+ editVendorOpeningBalanceMutate([vendor.id, formValues])
+ .then(onSuccess)
+ .catch(onError);
+ };
+
+ return (
+
+ );
+}
+export default compose(withDialogActions)(VendorOpeningBalanceForm);
diff --git a/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceForm.schema.js b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceForm.schema.js
new file mode 100644
index 000000000..5435aa0b7
--- /dev/null
+++ b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceForm.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 CreateVendorOpeningBalanceFormSchema = Schema;
diff --git a/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormContent.js b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormContent.js
new file mode 100644
index 000000000..4cdca9151
--- /dev/null
+++ b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormContent.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Form } from 'formik';
+
+import VendorOpeningBalanceFormFields from './VendorOpeningBalanceFormFields';
+import VendorOpeningBalanceFormFloatingActions from './VendorOpeningBalanceFormFloatingActions';
+
+/**
+ * Vendor Opening balance form content.
+ * @returns
+ */
+function VendorOpeningBalanceFormContent() {
+ return (
+
+ );
+}
+
+export default VendorOpeningBalanceFormContent;
diff --git a/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormFields.js b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormFields.js
new file mode 100644
index 000000000..7f885366c
--- /dev/null
+++ b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormFields.js
@@ -0,0 +1,116 @@
+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 { useVendorOpeningBalanceContext } from './VendorOpeningBalanceFormProvider';
+import { useSetPrimaryBranchToForm } from './utils';
+
+import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
+import { compose } from 'utils';
+
+/**
+ * Vendor Opening balance form fields.
+ * @returns
+ */
+function VendorOpeningBalanceFormFields({
+ // #withCurrentOrganization
+ organization: { base_currency },
+}) {
+ // Formik context.
+ const { values } = useFormikContext();
+
+ const { branches, vendor } = useVendorOpeningBalanceContext();
+
+ // 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())(
+ VendorOpeningBalanceFormFields,
+);
diff --git a/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormFloatingActions.js b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormFloatingActions.js
new file mode 100644
index 000000000..cfbf2812a
--- /dev/null
+++ b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormFloatingActions.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import { Intent, Button, Classes } from '@blueprintjs/core';
+import { useFormikContext } from 'formik';
+import { FormattedMessage as T } from 'components';
+
+import { useVendorOpeningBalanceContext } from './VendorOpeningBalanceFormProvider';
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import { compose } from 'utils';
+
+/**
+ * Vendor Opening balance floating actions.
+ * @returns
+ */
+function VendorOpeningBalanceFormFloatingActions({
+ // #withDialogActions
+ closeDialog,
+}) {
+ // dialog context.
+ const { dialogName } = useVendorOpeningBalanceContext();
+
+ // Formik context.
+ const { isSubmitting } = useFormikContext();
+
+ // Handle close button click.
+ const handleCancelBtnClick = () => {
+ closeDialog(dialogName);
+ };
+
+ return (
+
+ );
+}
+export default compose(withDialogActions)(
+ VendorOpeningBalanceFormFloatingActions,
+);
diff --git a/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormProvider.js b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormProvider.js
new file mode 100644
index 000000000..4f0aa7b4b
--- /dev/null
+++ b/src/containers/Dialogs/VendorOpeningBalanceDialog/VendorOpeningBalanceFormProvider.js
@@ -0,0 +1,71 @@
+import React from 'react';
+import { DialogContent } from 'components';
+import {
+ useBranches,
+ useVendor,
+ useEditVendorOpeningBalance,
+} from 'hooks/query';
+import { useFeatureCan } from 'hooks/state';
+import { Features } from 'common';
+import { pick, defaultTo } from 'lodash';
+
+const VendorOpeningBalanceContext = React.createContext();
+
+/**
+ * Vendor Opening balance provider.
+ * @returns
+ */
+function VendorOpeningBalanceFormProvider({
+ query,
+ vendorId,
+ dialogName,
+ ...props
+}) {
+ // Features guard.
+ const { featureCan } = useFeatureCan();
+ const isBranchFeatureCan = featureCan(Features.Branches);
+
+ const { mutateAsync: editVendorOpeningBalanceMutate } =
+ useEditVendorOpeningBalance();
+
+ // Fetches the branches list.
+ const {
+ data: branches,
+ isLoading: isBranchesLoading,
+ isSuccess: isBranchesSuccess,
+ } = useBranches(query, { enabled: isBranchFeatureCan });
+
+ // Handle fetch vendor details.
+ const { data: vendor, isLoading: isVendorLoading } = useVendor(vendorId, {
+ enabled: !!vendorId,
+ });
+
+ // State provider.
+ const provider = {
+ branches,
+ vendor: {
+ ...pick(vendor, [
+ 'id',
+ 'opening_balance',
+ 'opening_balance_exchange_rate',
+ 'currency_code',
+ ]),
+ },
+
+ isBranchesSuccess,
+ isBranchesLoading,
+ dialogName,
+ editVendorOpeningBalanceMutate,
+ };
+
+ return (
+
+
+
+ );
+}
+
+const useVendorOpeningBalanceContext = () =>
+ React.useContext(VendorOpeningBalanceContext);
+
+export { VendorOpeningBalanceFormProvider, useVendorOpeningBalanceContext };
diff --git a/src/containers/Dialogs/VendorOpeningBalanceDialog/index.js b/src/containers/Dialogs/VendorOpeningBalanceDialog/index.js
new file mode 100644
index 000000000..08fe52fac
--- /dev/null
+++ b/src/containers/Dialogs/VendorOpeningBalanceDialog/index.js
@@ -0,0 +1,39 @@
+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 VendorOpeningBalanceDialogContent = React.lazy(() =>
+ import('./VendorOpeningBalanceDialogContent'),
+);
+
+/**
+ * Vendor Opening balance dialog.
+ * @returns
+ */
+function VendorOpeningBalanceDialog({
+ dialogName,
+ payload: { vendorId },
+ isOpen,
+}) {
+ return (
+ }
+ isOpen={isOpen}
+ canEscapeJeyClose={true}
+ autoFocus={true}
+ className={'dialog--vendor-opening-balance'}
+ >
+
+
+
+
+ );
+}
+export default compose(withDialogRedux())(VendorOpeningBalanceDialog);
diff --git a/src/containers/Dialogs/VendorOpeningBalanceDialog/utils.js b/src/containers/Dialogs/VendorOpeningBalanceDialog/utils.js
new file mode 100644
index 000000000..8b534e87e
--- /dev/null
+++ b/src/containers/Dialogs/VendorOpeningBalanceDialog/utils.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import { useFormikContext } from 'formik';
+import { first } from 'lodash';
+
+import { useVendorOpeningBalanceContext } from './VendorOpeningBalanceFormProvider';
+
+export const useSetPrimaryBranchToForm = () => {
+ const { setFieldValue } = useFormikContext();
+ const { branches, isBranchesSuccess } = useVendorOpeningBalanceContext();
+
+ 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/VendorDetailsDrawer/VendorDetailsActionsBar.js b/src/containers/Drawers/VendorDetailsDrawer/VendorDetailsActionsBar.js
index 86854bda2..82d1c2967 100644
--- a/src/containers/Drawers/VendorDetailsDrawer/VendorDetailsActionsBar.js
+++ b/src/containers/Drawers/VendorDetailsDrawer/VendorDetailsActionsBar.js
@@ -19,8 +19,10 @@ import { useVendorDetailsDrawerContext } from './VendorDetailsDrawerProvider';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
+import withDialogActions from 'containers/Dialog/withDialogActions';
import { Can, Icon, FormattedMessage as T } from 'components';
+import { VendorMoreMenuItem } from './utils';
import {
AbilitySubject,
SaleInvoiceAction,
@@ -33,13 +35,16 @@ import { safeCallback, compose } from 'utils';
* Vendor details actions bar.
*/
function VendorDetailsActionsBar({
+ // #withDialogActions
+ openDialog,
+
// #withAlertsActions
openAlert,
// #withDrawerActions
closeDrawer,
}) {
- const { vendor, vendorId } = useVendorDetailsDrawerContext();
+ const { vendorId } = useVendorDetailsDrawerContext();
const history = useHistory();
// Handle edit vendor.
@@ -63,6 +68,10 @@ function VendorDetailsActionsBar({
closeDrawer('vendor-details-drawer');
};
+ const handleEditOpeningBalance = () => {
+ openDialog('vendor-opening-balance', { vendorId });
+ };
+
return (
@@ -112,6 +121,12 @@ function VendorDetailsActionsBar({
onClick={safeCallback(onDeleteContact)}
/>
+
+
);
@@ -120,4 +135,5 @@ function VendorDetailsActionsBar({
export default compose(
withDrawerActions,
withAlertsActions,
+ withDialogActions,
)(VendorDetailsActionsBar);
diff --git a/src/containers/Drawers/VendorDetailsDrawer/utils.js b/src/containers/Drawers/VendorDetailsDrawer/utils.js
new file mode 100644
index 000000000..0849640c0
--- /dev/null
+++ b/src/containers/Drawers/VendorDetailsDrawer/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';
+
+/**
+ * Vendor more actions menu items.
+ * @param {*} param0
+ */
+export function VendorMoreMenuItem({ payload: { onEditOpeningBalance } }) {
+ return (
+
+ }
+ onClick={onEditOpeningBalance}
+ />
+
+ }
+ >
+ } minimal={true} />
+
+ );
+}
diff --git a/src/hooks/query/vendors.js b/src/hooks/query/vendors.js
index c9bc4c783..c8c54060d 100644
--- a/src/hooks/query/vendors.js
+++ b/src/hooks/query/vendors.js
@@ -115,6 +115,25 @@ export function useVendor(id, props) {
);
}
+export function useEditVendorOpeningBalance(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation(
+ ([id, values]) => apiRequest.post(`vendors/${id}/opening_balance`, values),
+ {
+ onSuccess: (res, [id, values]) => {
+ // Invalidate specific vendor.
+ queryClient.invalidateQueries([t.VENDOR, id]);
+
+ // Common invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ },
+ );
+}
+
export function useRefreshVendors() {
const queryClient = useQueryClient();
diff --git a/src/lang/en/index.json b/src/lang/en/index.json
index 603e82507..471f9d4c0 100644
--- a/src/lang/en/index.json
+++ b/src/lang/en/index.json
@@ -1298,6 +1298,7 @@
"customer.drawer.action.new_payment": "New payment",
"customer.drawer.action.new_receipt": "New receipt",
"customer.drawer.action.new_transaction": "New transaction",
+ "customer.drawer.action.edit_opending_balance": "Edit Opending Balance",
"customer.drawer.action.edit": "Edit",
"customer.drawer.label.outstanding_receivable": "Outstanding receivable",
"customer.drawer.label.customer_name": "Customer name",
@@ -1327,6 +1328,7 @@
"vendor.drawer.action.new_payment": "New payment",
"vendor.drawer.action.new_invoice": "New purchase invoice",
"vendor.drawer.action.edit": "Edit",
+ "vendor.drawer.action.edit_opending_balance": "Edit Opending Balance",
"manual_journals.empty_status.description": "Manual journals can be used to record financial transactions manually, used by accountants to work with the ledger.",
"manual_journals.empty_status.title": "Create your first journal entries on accounts chart.",
"expenses.empty_status.description": "Create and manage expeses that are part of your organization's operating costs.",
@@ -1844,7 +1846,7 @@
"branches.column.code": "Code",
"select_branch": "Select branch",
"branch": "Branch",
- "warehouse":"Warehouse",
+ "warehouse": "Warehouse",
"branch.dialog.label_new_branch": "New Branch",
"branch.dialog.label_edit_branch": "New Branch",
"branch.dialog.label.branch_name": "Branch Name",
@@ -1885,5 +1887,13 @@
"warehouse_transfer.alert.are_you_sure_you_want_to_initate": "Are you sure you want to initiate this warehouse transfer?",
"warehouse_transfer.alert.transferred_warehouse": "The given warehouse transfer has been delivered",
"warehouse_transfer.alert.are_you_sure_you_want_to_deliver": "Are you sure you want to deliver this warehouse transfer?",
- "accounts.error.account_currency_not_same_parent_account": "You could not create an account in a currency different from the parent account currency."
+ "accounts.error.account_currency_not_same_parent_account": "You could not create an account in a currency different from the parent account currency.",
+ "customer_opening_balance.success_message": "The opening balance of the given customer has been changed successfully.",
+ "customer_opening_balance.label": "Edit Customer Opening Balance",
+ "customer_opening_balance.label.opening_balance": "Opening balance",
+ "customer_opening_balance.label.opening_balance_at": "Opening balance at",
+ "vendor_opening_balance.success_message": "The opening balance of the given vendor has been changed successfully.",
+ "vendor_opening_balance.label": "Edit Vendor Opening Balance",
+ "vendor_opening_balance.label.opening_balance": "Opening balance",
+ "vendor_opening_balance.label.opening_balance_at": "Opening balance at"
}
\ No newline at end of file
diff --git a/src/style/pages/VendorOpeningBalance/VendorOpeningBalance.scss b/src/style/pages/VendorOpeningBalance/VendorOpeningBalance.scss
new file mode 100644
index 000000000..30c5b7124
--- /dev/null
+++ b/src/style/pages/VendorOpeningBalance/VendorOpeningBalance.scss
@@ -0,0 +1,19 @@
+.dialog--vendor-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;
+ }
+}