diff --git a/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItems.js b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItems.js
new file mode 100644
index 000000000..b1fa012bf
--- /dev/null
+++ b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItems.js
@@ -0,0 +1,88 @@
+import React, { useEffect, useState, useCallback } from 'react';
+import moment from 'moment';
+
+import 'style/pages/FinancialStatements/PurchasesByItems.scss';
+
+import { PurchasesByItemsProvider } from './PurchasesByItemsProvider';
+import PurchasesByItemsActionsBar from './PurchasesByItemsActionsBar';
+import PurchasesByItemsHeader from './PurchasesByItemsHeader';
+import PurchasesByItemsTable from './PurchasesByItemsTable';
+import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
+import { PurchasesByItemsLoadingBar } from './components';
+
+import withPurchasesByItemsActions from './withPurchasesByItemsActions';
+import withSettings from 'containers/Settings/withSettings';
+import { compose } from 'utils';
+
+/**
+ * Purchases by items.
+ */
+function PurchasesByItems({
+ // #withPreferences
+ organizationName,
+
+ // #withPurchasesByItemsActions
+ togglePurchasesByItemsFilterDrawer,
+}) {
+ const [filter, setFilter] = useState({
+ fromDate: moment().startOf('year').format('YYYY-MM-DD'),
+ toDate: moment().endOf('year').format('YYYY-MM-DD'),
+ });
+
+ // Handle filter form submit.
+ const handleFilterSubmit = useCallback(
+ (filter) => {
+ const parsedFilter = {
+ ...filter,
+ fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
+ toDate: moment(filter.toDate).format('YYYY-MM-DD'),
+ };
+ setFilter(parsedFilter);
+ },
+ [setFilter],
+ );
+
+ // Handle number format form submit.
+ const handleNumberFormatSubmit = (numberFormat) => {
+ setFilter({
+ ...filter,
+ numberFormat,
+ });
+ };
+
+ // Hide the filter drawer once the page unmount.
+ useEffect(
+ () => () => {
+ togglePurchasesByItemsFilterDrawer(false);
+ },
+ [togglePurchasesByItemsFilterDrawer],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withPurchasesByItemsActions,
+ withSettings(({ organizationSettings }) => ({
+ organizationName: organizationSettings.name,
+ })),
+)(PurchasesByItems);
diff --git a/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsActionsBar.js b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsActionsBar.js
new file mode 100644
index 000000000..6bbd3d542
--- /dev/null
+++ b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsActionsBar.js
@@ -0,0 +1,125 @@
+import React from 'react';
+import {
+ NavbarGroup,
+ Button,
+ Classes,
+ NavbarDivider,
+ Popover,
+ PopoverInteractionKind,
+ Position,
+} from '@blueprintjs/core';
+import classNames from 'classnames';
+import { FormattedMessage as T } from 'react-intl';
+
+import { Icon } from 'components';
+import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
+import NumberFormatDropdown from 'components/NumberFormatDropdown';
+
+import withPurchasesByItems from './withPurchasesByItems';
+import withPurchasesByItemsActions from './withPurchasesByItemsActions';
+import { compose, saveInvoke } from 'utils';
+import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
+
+function PurchasesByItemsActionsBar({
+ // #withPurchasesByItems
+ purchasesByItemsDrawerFilter,
+
+ // #withPurchasesByItemsActions
+ togglePurchasesByItemsFilterDrawer,
+
+ // #ownProps
+ numberFormat,
+ onNumberFormatSubmit,
+}) {
+ const { refetchSheet, isLoading } = usePurchaseByItemsContext();
+
+ // Handle re-calc button click.
+ const handleRecalculateReport = () => {
+ refetchSheet();
+ };
+
+ // Handle filter toggle click.
+ const handleFilterToggleClick = () => {
+ togglePurchasesByItemsFilterDrawer();
+ };
+
+ // Handle number format submit.
+ const handleNumberFormatSubmit = (values) => {
+ saveInvoke(onNumberFormatSubmit, values);
+ };
+
+ return (
+
+
+ }
+ />
+
+ }
+ text={
+ purchasesByItemsDrawerFilter ? (
+
+ ) : (
+
+ )
+ }
+ active={purchasesByItemsDrawerFilter}
+ onClick={handleFilterToggleClick}
+ />
+
+
+ }
+ minimal={true}
+ interactionKind={PopoverInteractionKind.CLICK}
+ position={Position.BOTTOM_LEFT}
+ >
+ }
+ icon={}
+ />
+
+
+ }
+ icon={}
+ />
+
+
+ }
+ text={}
+ />
+ }
+ text={}
+ />
+
+
+ );
+}
+
+export default compose(
+ withPurchasesByItems(({ purchasesByItemsDrawerFilter }) => ({
+ purchasesByItemsDrawerFilter,
+ })),
+ withPurchasesByItemsActions,
+)(PurchasesByItemsActionsBar);
diff --git a/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsGeneralPanel.js b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsGeneralPanel.js
new file mode 100644
index 000000000..49d24ed87
--- /dev/null
+++ b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsGeneralPanel.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
+
+/**
+ * Purchases by items - Drawer header - General panel.
+ */
+export default function PurchasesByItemsGeneralPanel() {
+ return (
+
+
+
+ );
+}
diff --git a/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsHeader.js b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsHeader.js
new file mode 100644
index 000000000..fc1843dce
--- /dev/null
+++ b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsHeader.js
@@ -0,0 +1,99 @@
+import React from 'react';
+import * as Yup from 'yup';
+import moment from 'moment';
+import { FormattedMessage as T, useIntl } from 'react-intl';
+import { Formik, Form } from 'formik';
+import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
+
+import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
+import PurchasesByItemsGeneralPanel from './PurchasesByItemsGeneralPanel';
+
+import withPurchasesByItems from './withPurchasesByItems';
+import withPurchasesByItemsActions from './withPurchasesByItemsActions';
+
+import { compose } from 'utils';
+
+/**
+ * Purchases by items header.
+ */
+function PurchasesByItemsHeader({
+ // #ownProps
+ pageFilter,
+ onSubmitFilter,
+
+ // #withPurchasesByItems
+ purchasesByItemsDrawerFilter,
+
+ // #withPurchasesByItems
+ togglePurchasesByItemsFilterDrawer,
+}) {
+ const { formatMessage } = useIntl();
+
+ // Form validation schema.
+ const validationSchema = Yup.object().shape({
+ fromDate: Yup.date()
+ .required()
+ .label(formatMessage({ id: 'from_date' })),
+ toDate: Yup.date()
+ .min(Yup.ref('fromDate'))
+ .required()
+ .label(formatMessage({ id: 'to_date' })),
+ });
+
+ // Initial values.
+ const initialValues = {
+ ...pageFilter,
+ fromDate: moment(pageFilter.fromDate).toDate(),
+ toDate: moment(pageFilter.toDate).toDate(),
+ };
+
+ // Handle form submit.
+ const handleSubmit = (values, { setSubmitting }) => {
+ onSubmitFilter(values);
+ setSubmitting(false);
+ togglePurchasesByItemsFilterDrawer(false);
+ };
+
+ // Handle drawer close & cancel action.
+ const handleDrawerClose = () => {
+ togglePurchasesByItemsFilterDrawer(false);
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withPurchasesByItems(({ purchasesByItemsDrawerFilter }) => ({
+ purchasesByItemsDrawerFilter,
+ })),
+ withPurchasesByItemsActions,
+)(PurchasesByItemsHeader);
diff --git a/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsProvider.js b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsProvider.js
new file mode 100644
index 000000000..9de8c6c47
--- /dev/null
+++ b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsProvider.js
@@ -0,0 +1,39 @@
+import React, { createContext, useContext } from 'react';
+import FinancialReportPage from '../FinancialReportPage';
+import { usePurchasesByItems } from 'hooks/query';
+import { transformFilterFormToQuery } from '../common';
+
+
+const PurchasesByItemsContext = createContext();
+
+function PurchasesByItemsProvider({ query, ...props }) {
+ const {
+ data: purchaseByItems,
+ isFetching,
+ isLoading,
+ refetch,
+ } = usePurchasesByItems(
+ {
+ ...transformFilterFormToQuery(query),
+ },
+ {
+ keepPreviousData: true,
+ },
+ );
+
+ const provider = {
+ purchaseByItems,
+ isFetching,
+ isLoading,
+ refetchSheet: refetch,
+ };
+ return (
+
+
+
+ );
+}
+
+const usePurchaseByItemsContext = () => useContext(PurchasesByItemsContext);
+
+export { PurchasesByItemsProvider, usePurchaseByItemsContext };
diff --git a/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsTable.js b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsTable.js
new file mode 100644
index 000000000..140bd28ae
--- /dev/null
+++ b/client/src/containers/FinancialStatements/PurchasesByItems/PurchasesByItemsTable.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import { useIntl } from 'react-intl';
+
+import FinancialSheet from 'components/FinancialSheet';
+import { DataTable } from 'components';
+
+import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
+
+import { usePurchasesByItemsTableColumns } from './components';
+
+/**
+ * purchases by items data table.
+ */
+export default function PurchasesByItemsTable({ companyName }) {
+ const { formatMessage } = useIntl();
+
+ // Purchases by items context.
+ const {
+ purchaseByItems: { tableRows, query },
+ isLoading,
+ } = usePurchaseByItemsContext();
+
+ // Purchases by items table columns.
+ const columns = usePurchasesByItemsTableColumns();
+
+ const rowClassNames = (row) => {
+ const { original } = row;
+ const rowTypes = Array.isArray(original.rowType)
+ ? original.rowType
+ : [original.rowType];
+
+ return {
+ ...rowTypes.reduce((acc, rowType) => {
+ acc[`row_type--${rowType}`] = rowType;
+ return acc;
+ }, {}),
+ };
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/client/src/containers/FinancialStatements/PurchasesByItems/components.js b/client/src/containers/FinancialStatements/PurchasesByItems/components.js
new file mode 100644
index 000000000..16b23c203
--- /dev/null
+++ b/client/src/containers/FinancialStatements/PurchasesByItems/components.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import { useIntl } from 'react-intl';
+import { Button } from '@blueprintjs/core';
+import { getColumnWidth } from 'utils';
+import { If, Icon } from 'components';
+import { CellTextSpan } from 'components/Datatable/Cells';
+import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
+import FinancialLoadingBar from '../FinancialLoadingBar';
+
+/**
+ * Retrieve purchases by items table columns.
+ */
+export const usePurchasesByItemsTableColumns = () => {
+ const { formatMessage } = useIntl();
+
+ // purchases by items context.
+ const {
+ purchaseByItems: { tableRows },
+ } = usePurchaseByItemsContext();
+
+ return React.useMemo(
+ () => [
+ {
+ Header: formatMessage({ id: 'item_name' }),
+ accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
+ className: 'name',
+ width: 180,
+ textOverview: true,
+ },
+ {
+ Header: formatMessage({ id: 'quantity_purchased' }),
+ accessor: 'quantity_purchased_formatted',
+ Cell: CellTextSpan,
+ className: 'quantity_purchased_formatted',
+ width: getColumnWidth(tableRows, `quantity_purchased_formatted`, {
+ minWidth: 150,
+ }),
+ textOverview: true,
+ },
+ {
+ Header: formatMessage({ id: 'purchase_amount' }),
+ accessor: 'purchase_cost_formatted',
+ Cell: CellTextSpan,
+ className: 'purchase_cost_formatted',
+ width: getColumnWidth(tableRows, `purchase_cost_formatted`, {
+ minWidth: 150,
+ }),
+ textOverview: true,
+ },
+ {
+ Header: formatMessage({ id: 'average_price' }),
+ accessor: 'average_cost_price_formatted',
+ Cell: CellTextSpan,
+ className: 'average_cost_price_formatted',
+ width: getColumnWidth(tableRows, `average_cost_price_formatted`, {
+ minWidth: 180,
+ }),
+ textOverview: true,
+ },
+ ],
+ [tableRows,formatMessage],
+ );
+};
+
+/**
+ * Purchases by items progress loading bar.
+ */
+export function PurchasesByItemsLoadingBar() {
+ const { isFetching } = usePurchaseByItemsContext();
+
+ return (
+
+
+
+ );
+}
diff --git a/client/src/containers/FinancialStatements/PurchasesByItems/withPurchasesByItems.js b/client/src/containers/FinancialStatements/PurchasesByItems/withPurchasesByItems.js
new file mode 100644
index 000000000..1eaf2b3fe
--- /dev/null
+++ b/client/src/containers/FinancialStatements/PurchasesByItems/withPurchasesByItems.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux';
+import { getPurchasesByItemsFilterDrawer } from 'store/financialStatement/financialStatements.selectors';
+
+export default (mapState) => {
+ const mapStateToProps = (state, props) => {
+ const mapped = {
+ purchasesByItemsDrawerFilter: getPurchasesByItemsFilterDrawer(state),
+ };
+ return mapState ? mapState(mapped, state, props) : mapped;
+ };
+ return connect(mapStateToProps);
+};
diff --git a/client/src/containers/FinancialStatements/PurchasesByItems/withPurchasesByItemsActions.js b/client/src/containers/FinancialStatements/PurchasesByItems/withPurchasesByItemsActions.js
new file mode 100644
index 000000000..63744c190
--- /dev/null
+++ b/client/src/containers/FinancialStatements/PurchasesByItems/withPurchasesByItemsActions.js
@@ -0,0 +1,9 @@
+import { connect } from 'react-redux';
+import { togglePurchasesByItemsFilterDrawer } from 'store/financialStatement/financialStatements.actions';
+
+export const mapDispatchToProps = (dispatch) => ({
+ togglePurchasesByItemsFilterDrawer: (toggle) =>
+ dispatch(togglePurchasesByItemsFilterDrawer(toggle)),
+});
+
+export default connect(null, mapDispatchToProps);