fix: items entries total calculation.

This commit is contained in:
a.bouhuolia
2021-08-03 16:04:31 +02:00
parent 0783fb26f2
commit b2c892b649
15 changed files with 222 additions and 175 deletions

View File

@@ -75,7 +75,7 @@ export function QuantityTotalFooterCell({ rows }) {
* Total footer cell. * Total footer cell.
*/ */
export function TotalFooterCell({ payload: { currencyCode }, rows }) { export function TotalFooterCell({ payload: { currencyCode }, rows }) {
const total = safeSumBy(rows, 'original.total'); const total = safeSumBy(rows, 'original.amount');
return <span>{formattedAmount(total, currencyCode)}</span>; return <span>{formattedAmount(total, currencyCode)}</span>;
} }
@@ -168,7 +168,7 @@ export function useEditableItemsEntriesColumns({ landedCost }) {
{ {
Header: intl.get('total'), Header: intl.get('total'),
Footer: TotalFooterCell, Footer: TotalFooterCell,
accessor: 'total', accessor: 'amount',
Cell: TotalCell, Cell: TotalCell,
disableSortBy: true, disableSortBy: true,
width: 100, width: 100,

View File

@@ -1,3 +1,4 @@
import { sumBy } from 'lodash';
import { toSafeNumber } from 'utils'; import { toSafeNumber } from 'utils';
/** /**
@@ -21,7 +22,7 @@ export const calcItemEntryTotal = (discount, quantity, rate) => {
export function updateItemsEntriesTotal(rows) { export function updateItemsEntriesTotal(rows) {
return rows.map((row) => ({ return rows.map((row) => ({
...row, ...row,
total: calcItemEntryTotal(row.discount, row.quantity, row.rate), amount: calcItemEntryTotal(row.discount, row.quantity, row.rate),
})); }));
} }
@@ -29,3 +30,10 @@ export const ITEM_TYPE = {
SELLABLE: 'SELLABLE', SELLABLE: 'SELLABLE',
PURCHASABLE: 'PURCHASABLE', PURCHASABLE: 'PURCHASABLE',
}; };
/**
* Retrieve total of the given items entries.
*/
export function getEntriesTotal(entries) {
return sumBy(entries, 'amount');
}

View File

@@ -21,7 +21,7 @@ function BillFormHeader({
const { values } = useFormikContext(); const { values } = useFormikContext();
// Calculate the total due amount of bill entries. // Calculate the total due amount of bill entries.
const totalDueAmount = useMemo(() => sumBy(values.entries, 'total'), [ const totalDueAmount = useMemo(() => sumBy(values.entries, 'amount'), [
values.entries, values.entries,
]); ]);

View File

@@ -17,6 +17,7 @@ export const defaultBillEntry = {
discount: '', discount: '',
quantity: '', quantity: '',
description: '', description: '',
amount: '',
landed_cost: false, landed_cost: false,
}; };

View File

@@ -89,6 +89,7 @@ function EstimateForm({
); );
const totalQuantity = sumBy(entries, (entry) => parseInt(entry.quantity)); const totalQuantity = sumBy(entries, (entry) => parseInt(entry.quantity));
// Validate the entries quantity should be bigger than zero.
if (totalQuantity === 0) { if (totalQuantity === 0) {
AppToaster.show({ AppToaster.show({
message: intl.get('quantity_cannot_be_zero_or_empty'), message: intl.get('quantity_cannot_be_zero_or_empty'),

View File

@@ -1,14 +1,15 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { sumBy } from 'lodash';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import EstimateFormHeaderFields from './EstimateFormHeaderFields'; import EstimateFormHeaderFields from './EstimateFormHeaderFields';
import { PageFormBigNumber } from 'components';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import { getEntriesTotal } from 'containers/Entries/utils';
import { PageFormBigNumber } from 'components';
import { compose } from 'utils'; import { compose } from 'utils';
// Estimate form top header. // Estimate form top header.
@@ -20,7 +21,7 @@ function EstimateFormHeader({
// Calculate the total due amount of bill entries. // Calculate the total due amount of bill entries.
const totalDueAmount = useMemo( const totalDueAmount = useMemo(
() => sumBy(values.entries, 'total'), () => getEntriesTotal(values.entries),
[values.entries], [values.entries],
); );

View File

@@ -8,6 +8,7 @@ import {
useCreateEstimate, useCreateEstimate,
useEditEstimate, useEditEstimate,
} from 'hooks/query'; } from 'hooks/query';
import { ITEMS_FILTER_ROLES } from './utils';
const EstimateFormContext = createContext(); const EstimateFormContext = createContext();
@@ -18,31 +19,24 @@ function EstimateFormProvider({ estimateId, ...props }) {
const { const {
data: estimate, data: estimate,
isFetching: isEstimateFetching, isFetching: isEstimateFetching,
isLoading: isEstimateLoading,
} = useEstimate(estimateId, { enabled: !!estimateId }); } = useEstimate(estimateId, { enabled: !!estimateId });
// Filter all sellable items only.
const stringifiedFilterRoles = React.useMemo(
() =>
JSON.stringify([
{ index: 1, fieldKey: 'sellable', value: true, condition: '&&', comparator: 'equals', },
{ index: 2, fieldKey: 'active', value: true, condition: '&&', comparator: 'equals' },
]),
[],
);
// Handle fetch Items data table or list // Handle fetch Items data table or list
const { const {
data: { items }, data: { items },
isFetching: isItemsFetching, isFetching: isItemsFetching,
isLoading: isItemsLoading,
} = useItems({ } = useItems({
page_size: 10000, page_size: 10000,
stringified_filter_roles: stringifiedFilterRoles, stringified_filter_roles: ITEMS_FILTER_ROLES,
}); });
// Handle fetch customers data table or list // Handle fetch customers data table or list
const { const {
data: { customers }, data: { customers },
isFetch: isCustomersFetching, isFetch: isCustomersFetching,
isLoading: isCustomersLoading,
} = useCustomers({ page_size: 10000 }); } = useCustomers({ page_size: 10000 });
// Handle fetch settings. // Handle fetch settings.
@@ -68,6 +62,10 @@ function EstimateFormProvider({ estimateId, ...props }) {
isItemsFetching, isItemsFetching,
isEstimateFetching, isEstimateFetching,
isCustomersLoading,
isItemsLoading,
isEstimateLoading,
submitPayload, submitPayload,
setSubmitPayload, setSubmitPayload,
@@ -77,7 +75,7 @@ function EstimateFormProvider({ estimateId, ...props }) {
return ( return (
<DashboardInsider <DashboardInsider
loading={isCustomersFetching || isItemsFetching || isEstimateFetching} loading={isCustomersLoading || isItemsLoading || isEstimateLoading}
name={'estimate-form'} name={'estimate-form'}
> >
<EstimateFormContext.Provider value={provider} {...props} /> <EstimateFormContext.Provider value={provider} {...props} />

View File

@@ -17,6 +17,7 @@ export const defaultEstimateEntry = {
discount: '', discount: '',
quantity: '', quantity: '',
description: '', description: '',
amount: '',
}; };
export const defaultEstimate = { export const defaultEstimate = {
@@ -55,7 +56,7 @@ export const useObserveEstimateNoSettings = (prefix, nextNumber) => {
setFieldValue('estimate_number', estimateNo); setFieldValue('estimate_number', estimateNo);
}, [setFieldValue, prefix, nextNumber]); }, [setFieldValue, prefix, nextNumber]);
}; };
/** /**
* Detarmines customers fast field when update. * Detarmines customers fast field when update.
*/ */
@@ -75,3 +76,20 @@ export const entriesFieldShouldUpdate = (newProps, oldProps) => {
defaultFastFieldShouldUpdate(newProps, oldProps) defaultFastFieldShouldUpdate(newProps, oldProps)
); );
}; };
export const ITEMS_FILTER_ROLES = JSON.stringify([
{
index: 1,
fieldKey: 'sellable',
value: true,
condition: '&&',
comparator: 'equals',
},
{
index: 2,
fieldKey: 'active',
value: true,
condition: '&&',
comparator: 'equals',
},
]);

View File

@@ -1,13 +1,14 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { sumBy } from 'lodash';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import InvoiceFormHeaderFields from './InvoiceFormHeaderFields'; import InvoiceFormHeaderFields from './InvoiceFormHeaderFields';
import { getEntriesTotal } from 'containers/Entries/utils';
import { PageFormBigNumber } from 'components'; import { PageFormBigNumber } from 'components';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import { compose } from 'redux'; import { compose } from 'redux';
@@ -23,7 +24,7 @@ function InvoiceFormHeader({
// Calculate the total due amount of invoice entries. // Calculate the total due amount of invoice entries.
const totalDueAmount = useMemo( const totalDueAmount = useMemo(
() => sumBy(values.entries, 'total'), () => getEntriesTotal(values.entries),
[values.entries], [values.entries],
); );

View File

@@ -2,7 +2,7 @@ import React, { createContext, useState } from 'react';
import { isEmpty, pick } from 'lodash'; import { isEmpty, pick } from 'lodash';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import DashboardInsider from 'components/Dashboard/DashboardInsider'; import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { transformToEditForm } from './utils'; import { transformToEditForm, ITEMS_FILTER_ROLES_QUERY } from './utils';
import { import {
useInvoice, useInvoice,
useItems, useItems,
@@ -27,10 +27,10 @@ function InvoiceFormProvider({ invoiceId, ...props }) {
}); });
// Fetches the estimate by the given id. // Fetches the estimate by the given id.
const { const { data: estimate, isLoading: isEstimateLoading } = useEstimate(
data: estimate, estimateId,
isLoading: isEstimateLoading, { enabled: !!estimateId },
} = useEstimate(estimateId, { enabled: !!estimateId }); );
const newInvoice = !isEmpty(estimate) const newInvoice = !isEmpty(estimate)
? transformToEditForm({ ? transformToEditForm({
@@ -38,23 +38,13 @@ function InvoiceFormProvider({ invoiceId, ...props }) {
}) })
: []; : [];
// Filter all sellable items only.
const stringifiedFilterRoles = React.useMemo(
() =>
JSON.stringify([
{ index: 1, fieldKey: 'sellable', value: true, condition: '&&', comparator: 'equals', },
{ index: 2, fieldKey: 'active', value: true, condition: '&&', comparator: 'equals' },
]),
[],
);
// Handle fetching the items table based on the given query. // Handle fetching the items table based on the given query.
const { const {
data: { items }, data: { items },
isLoading: isItemsLoading, isLoading: isItemsLoading,
} = useItems({ } = useItems({
page_size: 10000, page_size: 10000,
stringified_filter_roles: stringifiedFilterRoles, stringified_filter_roles: ITEMS_FILTER_ROLES_QUERY,
}); });
// Handle fetch customers data table or list // Handle fetch customers data table or list

View File

@@ -26,7 +26,7 @@ export const defaultInvoiceEntry = {
discount: '', discount: '',
quantity: '', quantity: '',
description: '', description: '',
total: 0, amount: '',
}; };
// Default invoice object. // Default invoice object.
@@ -63,6 +63,9 @@ export function transformToEditForm(invoice) {
}; };
} }
/**
* Transformes the response errors types.
*/
export const transformErrors = (errors, { setErrors }) => { export const transformErrors = (errors, { setErrors }) => {
if (errors.some((e) => e.type === ERROR.SALE_INVOICE_NUMBER_IS_EXISTS)) { if (errors.some((e) => e.type === ERROR.SALE_INVOICE_NUMBER_IS_EXISTS)) {
setErrors({ setErrors({
@@ -102,6 +105,9 @@ export const useObserveInvoiceNoSettings = (prefix, nextNumber) => {
}, [setFieldValue, prefix, nextNumber]); }, [setFieldValue, prefix, nextNumber]);
}; };
/**
* Detarmines customer name field when should update.
*/
export const customerNameFieldShouldUpdate = (newProps, oldProps) => { export const customerNameFieldShouldUpdate = (newProps, oldProps) => {
return ( return (
newProps.customers !== oldProps.customers || newProps.customers !== oldProps.customers ||
@@ -109,9 +115,29 @@ export const customerNameFieldShouldUpdate = (newProps, oldProps) => {
); );
}; };
/**
* Detarmines invoice entries field when should update.
*/
export const entriesFieldShouldUpdate = (newProps, oldProps) => { export const entriesFieldShouldUpdate = (newProps, oldProps) => {
return ( return (
newProps.items !== oldProps.items || newProps.items !== oldProps.items ||
defaultFastFieldShouldUpdate(newProps, oldProps) defaultFastFieldShouldUpdate(newProps, oldProps)
); );
}; };
export const ITEMS_FILTER_ROLES_QUERY = JSON.stringify([
{
index: 1,
fieldKey: 'sellable',
value: true,
condition: '&&',
comparator: 'equals',
},
{
index: 2,
fieldKey: 'active',
value: true,
condition: '&&',
comparator: 'equals',
},
]);

View File

@@ -1,15 +1,15 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { sumBy } from 'lodash';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { CLASSES } from 'common/classes';
import ReceiptFormHeaderFields from './ReceiptFormHeaderFields';
import { PageFormBigNumber } from 'components';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { CLASSES } from 'common/classes';
import { PageFormBigNumber } from 'components';
import ReceiptFormHeaderFields from './ReceiptFormHeaderFields';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import { getEntriesTotal } from 'containers/Entries/utils';
import { compose } from 'redux'; import { compose } from 'redux';
/** /**
@@ -25,7 +25,7 @@ function ReceiptFormHeader({
// Calculate the total due amount of bill entries. // Calculate the total due amount of bill entries.
const totalDueAmount = useMemo( const totalDueAmount = useMemo(
() => sumBy(values.entries, 'total'), () => getEntriesTotal(values.entries),
[values.entries], [values.entries],
); );

View File

@@ -17,6 +17,7 @@ export const defaultReceiptEntry = {
discount: '', discount: '',
quantity: '', quantity: '',
description: '', description: '',
amount: '',
}; };
export const defaultReceipt = { export const defaultReceipt = {

View File

@@ -26,6 +26,7 @@
@import 'components/PageForm'; @import 'components/PageForm';
@import 'components/Tooltip'; @import 'components/Tooltip';
@import 'components/Postbox'; @import 'components/Postbox';
@import 'components/SidebarOverlay';
// Pages // Pages
@import 'pages/view-form'; @import 'pages/view-form';
@@ -201,134 +202,6 @@ html[lang^="ar"] {
} }
} }
.sidebar-overlay {
background: #fff;
height: 100%;
width: 225px;
outline: 0;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
&__scroll-wrapper {
height: 100%
}
.ScrollbarsCustom-Track {
&.ScrollbarsCustom-TrackY,
&.ScrollbarsCustom-TrackX {
background: rgba(0, 0, 0, 0);
}
}
.ScrollbarsCustom-Thumb {
&.ScrollbarsCustom-ThumbX,
&.ScrollbarsCustom-ThumbY {
background: rgba(0, 0, 0, 0);
}
}
.ScrollbarsCustom-Content {
display: flex;
flex-direction: column;
height: 100%;
}
&:hover {
.ScrollbarsCustom-Thumb {
&.ScrollbarsCustom-ThumbX,
&.ScrollbarsCustom-ThumbY {
background: rgba(0, 0, 0, 0.5);
}
}
}
&__menu {
margin: 16px 0;
}
&__item {
font-size: 15px;
color: #00102b;
a {
color: inherit;
display: block;
padding: 10px 22px;
text-decoration: none;
&:hover,
&:focus{
background: #f3f3f3;
}
}
}
&__divider {
height: 1px;
margin: 6px 0;
background: #e2e5ec;
}
&__label{
text-transform: uppercase;
font-size: 12px;
padding: 14px 20px 10px;
letter-spacing: 1px;
color: #707a85;
html[lang^="ar"] & {
font-size: 13px;
letter-spacing: 0;
font-weight: 500;
}
}
&__label + .sidebar-overlay__divider{
margin-top: 0;
}
}
.sidebar-overlay-transition {
transform: translateX(-100%);
&.bp3-overlay{
&-appear,
&-enter {
transform: translateX(-100%)
}
&-appear-active,
&-enter-active {
transform: translateX(0) !important;
transition: all 100ms ease-in-out;
}
&-appear-done,
&-enter-done {
transform: translateX(0) !important;
}
&-exit {
transform: translateX(0) !important;
}
&-exit-active {
transform: translateX(-100%) !important;
transition: all 100ms ease-in-out;
}
&-exit-done{
transform: translateX(-100%) !important;
}
}
}
.sidebar-overlay-backdrop{
background-color: rgba(0, 10, 30, 0.15);
}
.bp3-popover2{ .bp3-popover2{
box-shadow: 0 0 0; box-shadow: 0 0 0;
} }

View File

@@ -0,0 +1,129 @@
.sidebar-overlay {
background: #fff;
height: 100%;
width: 225px;
outline: 0;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.1);
&__scroll-wrapper {
height: 100%
}
.ScrollbarsCustom-Track {
&.ScrollbarsCustom-TrackY,
&.ScrollbarsCustom-TrackX {
background: rgba(0, 0, 0, 0);
}
}
.ScrollbarsCustom-Thumb {
&.ScrollbarsCustom-ThumbX,
&.ScrollbarsCustom-ThumbY {
background: rgba(0, 0, 0, 0);
}
}
.ScrollbarsCustom-Content {
display: flex;
flex-direction: column;
height: 100%;
}
&:hover {
.ScrollbarsCustom-Thumb {
&.ScrollbarsCustom-ThumbX,
&.ScrollbarsCustom-ThumbY {
background: rgba(0, 0, 0, 0.5);
}
}
}
&__menu {
margin: 16px 0;
}
&__item {
font-size: 15px;
color: #00102b;
a {
color: inherit;
display: block;
padding: 10px 22px;
text-decoration: none;
&:hover,
&:focus{
background: #f3f3f3;
}
}
}
&__divider {
height: 1px;
margin: 6px 0;
background: #e2e5ec;
}
&__label{
text-transform: uppercase;
font-size: 12px;
padding: 14px 20px 10px;
letter-spacing: 1px;
color: #707a85;
html[lang^="ar"] & {
font-size: 13px;
letter-spacing: 0;
font-weight: 500;
}
}
&__label + .sidebar-overlay__divider{
margin-top: 0;
}
}
.sidebar-overlay-transition {
transform: translateX(-100%);
&.bp3-overlay{
&-appear,
&-enter {
transform: translateX(-100%)
}
&-appear-active,
&-enter-active {
transform: translateX(0) !important;
transition: all 100ms ease-in-out;
}
&-appear-done,
&-enter-done {
transform: translateX(0) !important;
}
&-exit {
transform: translateX(0) !important;
}
&-exit-active {
transform: translateX(-100%) !important;
transition: all 100ms ease-in-out;
}
&-exit-done{
transform: translateX(-100%) !important;
}
}
}
.sidebar-overlay-backdrop{
background-color: rgba(0, 10, 30, 0.15);
}