diff --git a/client/src/containers/FinancialStatements/SalesByItems/SalesByItemProvider.js b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemProvider.js
new file mode 100644
index 000000000..86ce8b8a1
--- /dev/null
+++ b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemProvider.js
@@ -0,0 +1,38 @@
+import React, { createContext, useContext } from 'react';
+import FinancialReportPage from '../FinancialReportPage';
+import { useSalesByItems } from 'hooks/query';
+import { transformFilterFormToQuery } from '../common';
+
+const SalesByItemsContext = createContext();
+
+function SalesByItemProvider({ query, ...props }) {
+ const {
+ data: salesByItems,
+ isFetching,
+ isLoading,
+ refetch,
+ } = useSalesByItems(
+ {
+ ...transformFilterFormToQuery(query),
+ },
+ {
+ keepPreviousData: true,
+ },
+ );
+
+ const provider = {
+ salesByItems,
+ isFetching,
+ isLoading,
+ refetchSheet: refetch,
+ };
+ return (
+
+
+
+ );
+}
+
+const useSalesByItemsContext = () => useContext(SalesByItemsContext);
+
+export { SalesByItemProvider, useSalesByItemsContext };
diff --git a/client/src/containers/FinancialStatements/SalesByItems/SalesByItems.js b/client/src/containers/FinancialStatements/SalesByItems/SalesByItems.js
new file mode 100644
index 000000000..871315729
--- /dev/null
+++ b/client/src/containers/FinancialStatements/SalesByItems/SalesByItems.js
@@ -0,0 +1,89 @@
+import React, { useEffect, useState, useCallback } from 'react';
+import moment from 'moment';
+
+import 'style/pages/FinancialStatements/SalesByItems.scss';
+
+import { SalesByItemProvider } from './SalesByItemProvider';
+import SalesByItemsActionsBar from './SalesByItemsActionsBar';
+import SalesByItemsHeader from './SalesByItemsHeader';
+import SalesByItemsTable from './SalesByItemsTable';
+
+import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
+import { SalesByItemsLoadingBar } from './components';
+
+import withSalesByItemsActions from './withSalesByItemsActions';
+import withSettings from 'containers/Settings/withSettings';
+
+import { compose } from 'utils';
+
+/**
+ * Sales by items.
+ */
+function SalesByItems({
+ // #withPreferences
+ organizationName,
+
+ // #withSellsByItemsActions
+ toggleSalesByItemsFilterDrawer,
+}) {
+ 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(
+ () => () => {
+ toggleSalesByItemsFilterDrawer(false);
+ },
+ [toggleSalesByItemsFilterDrawer],
+ );
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withSalesByItemsActions,
+ withSettings(({ organizationSettings }) => ({
+ organizationName: organizationSettings.name,
+ })),
+)(SalesByItems);
diff --git a/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsActionsBar.js b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsActionsBar.js
new file mode 100644
index 000000000..e36574469
--- /dev/null
+++ b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsActionsBar.js
@@ -0,0 +1,127 @@
+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 withSalesByItems from './withSalesByItems';
+import withSalesByItemsActions from './withSalesByItemsActions';
+
+import { compose, saveInvoke } from 'utils';
+import { useSalesByItemsContext } from './SalesByItemProvider';
+
+function SalesByItemsActionsBar({
+ // #withSalesByItems
+ salesByItemsDrawerFilter,
+
+ // #withSalesByItemsActions
+ toggleSalesByItemsFilterDrawer,
+
+ // #ownProps
+ numberFormat,
+ onNumberFormatSubmit,
+}) {
+ const { refetchSheet, isLoading } = useSalesByItemsContext();
+
+ // Handle filter toggle click.
+ const handleFilterToggleClick = () => {
+ toggleSalesByItemsFilterDrawer();
+ };
+ // Handle re-calc button click.
+ const handleRecalculateReport = () => {
+ refetchSheet();
+ };
+
+ // Handle number format submit.
+ const handleNumberFormatSubmit = (values) => {
+ saveInvoke(onNumberFormatSubmit, values);
+ };
+
+ return (
+
+
+ }
+ />
+
+
+ }
+ text={
+ salesByItemsDrawerFilter ? (
+
+ ) : (
+
+ )
+ }
+ active={salesByItemsDrawerFilter}
+ onClick={handleFilterToggleClick}
+ />
+
+
+ }
+ minimal={true}
+ interactionKind={PopoverInteractionKind.CLICK}
+ position={Position.BOTTOM_LEFT}
+ >
+ }
+ icon={}
+ />
+
+
+
+ }
+ icon={}
+ />
+
+
+ }
+ text={}
+ />
+ }
+ text={}
+ />
+
+
+ );
+}
+
+export default compose(
+ withSalesByItems(({ salesByItemsDrawerFilter }) => ({
+ salesByItemsDrawerFilter,
+ })),
+ withSalesByItemsActions,
+)(SalesByItemsActionsBar);
diff --git a/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsHeader.js b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsHeader.js
new file mode 100644
index 000000000..29827eecd
--- /dev/null
+++ b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsHeader.js
@@ -0,0 +1,103 @@
+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 SalesByItemsHeaderGeneralPanel from './SalesByItemsHeaderGeneralPanel';
+
+import withSalesByItems from './withSalesByItems';
+import withSalesByItemsActions from './withSalesByItemsActions';
+
+import { compose } from 'utils';
+
+/**
+ * Sales by items header.
+ */
+function SalesByItemsHeader({
+ // #ownProps
+ pageFilter,
+ onSubmitFilter,
+
+ // #withSalesByItems
+ salesByItemsDrawerFilter,
+
+ // #withSalesByItemsActions
+ toggleSalesByItemsFilterDrawer,
+}) {
+ 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(),
+ };
+
+ const handleSubmit = (values, { setSubmitting }) => {
+ onSubmitFilter(values);
+ setSubmitting(false);
+ toggleSalesByItemsFilterDrawer(false);
+ };
+
+ // Handle drawer close action.
+ const handleDrawerClose = () => {
+ toggleSalesByItemsFilterDrawer(false);
+ };
+
+ // Handle cancel button click.
+ const handleCancelClick = () => {
+ toggleSalesByItemsFilterDrawer(false);
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withSalesByItems(({ salesByItemsDrawerFilter }) => ({
+ salesByItemsDrawerFilter,
+ })),
+ withSalesByItemsActions,
+)(SalesByItemsHeader);
diff --git a/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsHeaderGeneralPanel.js b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsHeaderGeneralPanel.js
new file mode 100644
index 000000000..a31f6cd7e
--- /dev/null
+++ b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsHeaderGeneralPanel.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
+
+/**
+ * sells by items - Drawer header - General panel.
+ */
+export default function SalesByItemsHeaderGeneralPanel() {
+ return (
+
+
+
+ );
+}
diff --git a/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsTable.js b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsTable.js
new file mode 100644
index 000000000..94611e888
--- /dev/null
+++ b/client/src/containers/FinancialStatements/SalesByItems/SalesByItemsTable.js
@@ -0,0 +1,59 @@
+import React from 'react';
+import { useIntl } from 'react-intl';
+
+import FinancialSheet from 'components/FinancialSheet';
+import { DataTable } from 'components';
+import { useSalesByItemsContext } from './SalesByItemProvider';
+import { useSalesByItemsTableColumns } from './components';
+
+/**
+ * Sales by items data table.
+ */
+export default function SalesByItemsTable({ companyName }) {
+ const { formatMessage } = useIntl();
+
+ // Sales by items context.
+ const {
+ salesByItems: { tableRows, query },
+ isLoading,
+ } = useSalesByItemsContext();
+
+ // Sales by items table columns.
+ const columns = useSalesByItemsTableColumns();
+
+ 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/SalesByItems/components.js b/client/src/containers/FinancialStatements/SalesByItems/components.js
new file mode 100644
index 000000000..bbf8deeee
--- /dev/null
+++ b/client/src/containers/FinancialStatements/SalesByItems/components.js
@@ -0,0 +1,75 @@
+import React, { useMemo } 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 { useSalesByItemsContext } from './SalesByItemProvider';
+import FinancialLoadingBar from '../FinancialLoadingBar';
+
+/**
+ * Retrieve sales by items table columns.
+ */
+export const useSalesByItemsTableColumns = () => {
+ const { formatMessage } = useIntl();
+
+ //sales by items context.
+ const {
+ salesByItems: { tableRows },
+ } = useSalesByItemsContext();
+
+ return 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: 'sold_quantity' }),
+ accessor: 'quantity_sold_formatted',
+ Cell: CellTextSpan,
+ className: 'quantity_sold',
+ width: getColumnWidth(tableRows, `quantity_sold_formatted`, {
+ minWidth: 150,
+ }),
+ textOverview: true,
+ },
+ {
+ Header: formatMessage({ id: 'sold_amount' }),
+ accessor: 'sold_cost_formatted',
+ Cell: CellTextSpan,
+ className: 'sold_cost',
+ width: getColumnWidth(tableRows, `sold_cost_formatted`, {
+ minWidth: 150,
+ }),
+ textOverview: true,
+ },
+ {
+ Header: formatMessage({ id: 'average_price' }),
+ accessor: 'average_sell_price_formatted',
+ Cell: CellTextSpan,
+ className: 'average_sell_price',
+ width: getColumnWidth(tableRows, `average_sell_price_formatted`, {
+ minWidth: 150,
+ }),
+ textOverview: true,
+ },
+ ],
+ [tableRows, formatMessage],
+ );
+};
+
+/**
+ * sales by items progress loading bar.
+ */
+export function SalesByItemsLoadingBar() {
+ const { isFetching } = useSalesByItemsContext();
+ return (
+
+
+
+ );
+}
diff --git a/client/src/containers/FinancialStatements/SalesByItems/withSalesByItems.js b/client/src/containers/FinancialStatements/SalesByItems/withSalesByItems.js
new file mode 100644
index 000000000..4318b2681
--- /dev/null
+++ b/client/src/containers/FinancialStatements/SalesByItems/withSalesByItems.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux';
+import { getSalesByItemsFilterDrawer } from 'store/financialStatement/financialStatements.selectors';
+
+export default (mapState) => {
+ const mapStateToProps = (state, props) => {
+ const mapped = {
+ salesByItemsDrawerFilter: getSalesByItemsFilterDrawer(state),
+ };
+ return mapState ? mapState(mapped, state, props) : mapped;
+ };
+ return connect(mapStateToProps);
+};
diff --git a/client/src/containers/FinancialStatements/SalesByItems/withSalesByItemsActions.js b/client/src/containers/FinancialStatements/SalesByItems/withSalesByItemsActions.js
new file mode 100644
index 000000000..f0bb9ca76
--- /dev/null
+++ b/client/src/containers/FinancialStatements/SalesByItems/withSalesByItemsActions.js
@@ -0,0 +1,9 @@
+import { connect } from 'react-redux';
+import { toggleSalesByItemsFilterDrawer } from 'store/financialStatement/financialStatements.actions';
+
+export const mapDispatchToProps = (dispatch) => ({
+ toggleSalesByItemsFilterDrawer: (toggle) =>
+ dispatch(toggleSalesByItemsFilterDrawer(toggle)),
+});
+
+export default connect(null, mapDispatchToProps);
diff --git a/client/src/style/pages/FinancialStatements/PurchasesByItems.scss b/client/src/style/pages/FinancialStatements/PurchasesByItems.scss
new file mode 100644
index 000000000..12ab84fcd
--- /dev/null
+++ b/client/src/style/pages/FinancialStatements/PurchasesByItems.scss
@@ -0,0 +1,27 @@
+.financial-sheet {
+ &--purchases-by-items {
+ min-width: 800px;
+
+ .financial-sheet__table {
+ .thead,
+ .tbody {
+ .tr .td:not(:first-child),
+ .tr .th:not(:first-child) {
+ text-align: right;
+ }
+ }
+ .tbody {
+ .tr .td {
+ border-bottom: 0;
+ padding-top: 0.4rem;
+ padding-bottom: 0.4rem;
+ }
+ .tr.row_type--total .td {
+ border-top: 1px solid #000;
+ font-weight: 500;
+ border-bottom: 3px double #000;
+ }
+ }
+ }
+ }
+}
diff --git a/client/src/style/pages/FinancialStatements/SalesByItems.scss b/client/src/style/pages/FinancialStatements/SalesByItems.scss
new file mode 100644
index 000000000..fda5840c8
--- /dev/null
+++ b/client/src/style/pages/FinancialStatements/SalesByItems.scss
@@ -0,0 +1,27 @@
+.financial-sheet {
+ &--sales-by-items {
+ min-width: 800px;
+
+ .financial-sheet__table {
+ .thead,
+ .tbody {
+ .tr .td:not(:first-child),
+ .tr .th:not(:first-child) {
+ text-align: right;
+ }
+ }
+ .tbody {
+ .tr .td {
+ border-bottom: 0;
+ padding-top: 0.4rem;
+ padding-bottom: 0.4rem;
+ }
+ .tr.row_type--total .td {
+ border-top: 1px solid #000;
+ font-weight: 500;
+ border-bottom: 3px double #000;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file