feat: add discount fields in sale and purchase forms

This commit is contained in:
Ahmed Bouhuolia
2024-11-30 18:02:50 +02:00
parent 73ab92e693
commit ffb06f5194
11 changed files with 352 additions and 36 deletions

View File

@@ -5,10 +5,14 @@ import {
TotalLine, TotalLine,
TotalLineBorderStyle, TotalLineBorderStyle,
TotalLineTextStyle, TotalLineTextStyle,
FFormGroup,
FInputGroup,
FSelect,
} from '@/components'; } from '@/components';
import { useBillAggregatedTaxRates, useBillTotals } from './utils'; import { useBillAggregatedTaxRates, useBillTotals } from './utils';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { TaxType } from '@/interfaces/TaxRates'; import { TaxType } from '@/interfaces/TaxRates';
import { Button } from '@blueprintjs/core';
export function BillFormFooterRight() { export function BillFormFooterRight() {
const { const {
@@ -37,6 +41,34 @@ export function BillFormFooterRight() {
value={formattedSubtotal} value={formattedSubtotal}
borderStyle={TotalLineBorderStyle.None} borderStyle={TotalLineBorderStyle.None}
/> />
{/* ----------- Discount ----------- */}
<FFormGroup name={'discount'} label={'Discount'} inline>
<FInputGroup
name={'discount'}
rightElement={
<FSelect
name={'discount_type'}
items={[
{ text: 'USD', value: 'amount' },
{ text: '%', value: 'percentage' },
]}
input={({ text }) => (
<Button small minimal>
{text}
</Button>
)}
filterable={false}
/>
}
/>
</FFormGroup>
{/* ----------- Adjustment ----------- */}
<FFormGroup name={'adjustment'} label={'Adjustment'} inline>
<FInputGroup name={'adjustment'} />
</FFormGroup>
{taxEntries.map((tax, index) => ( {taxEntries.map((tax, index) => (
<TotalLine <TotalLine
key={index} key={index}

View File

@@ -13,6 +13,7 @@ import {
repeatValue, repeatValue,
orderingLinesIndexes, orderingLinesIndexes,
formattedAmount, formattedAmount,
toSafeNumber,
} from '@/utils'; } from '@/utils';
import { import {
updateItemsEntriesTotal, updateItemsEntriesTotal,
@@ -364,7 +365,27 @@ export const useBillSubtotal = () => {
}; };
/** /**
* Retreives the bill total tax amount. * Retrieves the bill discount amount.
* @returns {number}
*/
export const useBillDiscountAmount = () => {
const { values } = useFormikContext();
return toSafeNumber(values.discount);
};
/**
* Retrieves the bill adjustment amount.
* @returns {number}
*/
export const useBillAdjustmentAmount = () => {
const { values } = useFormikContext();
return toSafeNumber(values.adjustment);
};
/**
* Retrieves the bill total tax amount.
* @returns {number} * @returns {number}
*/ */
export const useBillTotalTaxAmount = () => { export const useBillTotalTaxAmount = () => {
@@ -389,15 +410,19 @@ export const useIsBillTaxExclusive = () => {
}; };
/** /**
* Retreives the bill total. * Retrieves the bill total.
* @returns {number} * @returns {number}
*/ */
export const useBillTotal = () => { export const useBillTotal = () => {
const subtotal = useBillSubtotal(); const subtotal = useBillSubtotal();
const totalTaxAmount = useBillTotalTaxAmount(); const totalTaxAmount = useBillTotalTaxAmount();
const isExclusiveTax = useIsBillTaxExclusive(); const isExclusiveTax = useIsBillTaxExclusive();
const discountAmount = useBillDiscountAmount();
const adjustmentAmount = useBillAdjustmentAmount();
return R.compose(R.when(R.always(isExclusiveTax), R.add(totalTaxAmount)))( return R.compose(
subtotal, R.when(R.always(isExclusiveTax), R.add(totalTaxAmount)),
); R.subtract(R.__, discountAmount),
R.subtract(R.__, adjustmentAmount),
)(subtotal);
}; };

View File

@@ -53,6 +53,9 @@ export const defaultVendorsCreditNote = {
currency_code: '', currency_code: '',
entries: [...repeatValue(defaultCreditNoteEntry, MIN_LINES_NUMBER)], entries: [...repeatValue(defaultCreditNoteEntry, MIN_LINES_NUMBER)],
attachments: [], attachments: [],
discount: '',
discount_type: 'amount',
adjustment: '',
}; };
/** /**
@@ -207,6 +210,75 @@ export const useVendorCrditNoteTotals = () => {
}; };
}; };
/**
* Retrieves the vendor credit subtotal.
* @returns {number}
*/
export const useVendorCreditSubtotal = () => {
const {
values: { entries },
} = useFormikContext();
// Retrieves the invoice entries total.
const total = React.useMemo(() => getEntriesTotal(entries), [entries]);
return total;
};
/**
* Retrieves the vendor credit discount amount.
* @returns {number}
*/
export const useVendorCreditDiscountAmount = () => {
const { values } = useFormikContext();
return toSafeNumber(values.discount);
};
/**
* Retrieves the vendor credit adjustment amount.
* @returns {number}
*/
export const useVendorCreditAdjustment = () => {
const { values } = useFormikContext();
return toSafeNumber(values.adjustment);
};
/**
* Retrieves the vendor credit total.
* @returns {number}
*/
export const useVendorCreditTotal = () => {
const subtotal = useVendorCreditSubtotal();
const discountAmount = useVendorCreditDiscountAmount();
const adjustment = useVendorCreditAdjustment();
return subtotal - discountAmount - adjustment;
};
/**
* Retrieves the vendor credit formatted total.
* @returns {string}
*/
export const useVendorCreditFormattedTotal = () => {
const total = useVendorCreditTotal();
const currencyCode = useCurrentOrganizationCurrencyCode();
return formattedAmount(total, currencyCode);
};
/**
* Retrieves the vendor credit formatted subtotal.
* @returns {string}
*/
export const useVendorCreditFormattedSubtotal = () => {
const subtotal = useVendorCreditSubtotal();
const currencyCode = useCurrentOrganizationCurrencyCode();
return formattedAmount(subtotal, currencyCode);
};
/** /**
* Detarmines whether the vendor note has foreign customer. * Detarmines whether the vendor note has foreign customer.
* @returns {boolean} * @returns {boolean}

View File

@@ -1,28 +1,61 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { Button } from '@blueprintjs/core';
import { import {
T, T,
TotalLines, TotalLines,
TotalLine, TotalLine,
TotalLineBorderStyle, TotalLineBorderStyle,
TotalLineTextStyle, TotalLineTextStyle,
FInputGroup,
FFormGroup,
FSelect,
} from '@/components'; } from '@/components';
import { useCreditNoteTotals } from './utils'; import {
useCreditNoteSubtotalFormatted,
useCreditNoteTotalFormatted,
} from './utils';
export function CreditNoteFormFooterRight() { export function CreditNoteFormFooterRight() {
const { formattedSubtotal, formattedTotal } = useCreditNoteTotals(); const subtotalFormatted = useCreditNoteSubtotalFormatted();
const totalFormatted = useCreditNoteTotalFormatted();
return ( return (
<CreditNoteTotalLines labelColWidth={'180px'} amountColWidth={'180px'}> <CreditNoteTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
<TotalLine <TotalLine
title={<T id={'credit_note.label_subtotal'} />} title={<T id={'credit_note.label_subtotal'} />}
value={formattedSubtotal} value={subtotalFormatted}
borderStyle={TotalLineBorderStyle.None} borderStyle={TotalLineBorderStyle.None}
/> />
<FFormGroup name={'discount'} label={'Discount'} inline>
<FInputGroup
name={'discount'}
rightElement={
<FSelect
name={'discount_type'}
items={[
{ text: 'USD', value: 'amount' },
{ text: '%', value: 'percentage' },
]}
input={({ text }) => (
<Button small minimal>
{text}
</Button>
)}
filterable={false}
/>
}
/>
</FFormGroup>
<FFormGroup name={'adjustment'} label={'Adjustment'} inline>
<FInputGroup name={'adjustment'} />
</FFormGroup>
<TotalLine <TotalLine
title={<T id={'credit_note.label_total'} />} title={<T id={'credit_note.label_total'} />}
value={formattedTotal} value={totalFormatted}
textStyle={TotalLineTextStyle.Bold} textStyle={TotalLineTextStyle.Bold}
/> />
</CreditNoteTotalLines> </CreditNoteTotalLines>

View File

@@ -4,7 +4,7 @@ import { useFormikContext } from 'formik';
import * as R from 'ramda'; import * as R from 'ramda';
import { ExchangeRateInputGroup } from '@/components'; import { ExchangeRateInputGroup } from '@/components';
import { useCurrentOrganization } from '@/hooks/state'; import { useCurrentOrganization } from '@/hooks/state';
import { useCreditNoteIsForeignCustomer, useCreditNoteTotals } from './utils'; import { useCreditNoteIsForeignCustomer, useCreditNoteSubtotal } from './utils';
import withSettings from '@/containers/Settings/withSettings'; import withSettings from '@/containers/Settings/withSettings';
import { transactionNumber } from '@/utils'; import { transactionNumber } from '@/utils';
import { import {
@@ -78,13 +78,13 @@ export const CreditNoteSyncIncrementSettingsToForm = R.compose(
*/ */
export const CreditNoteExchangeRateSync = R.compose(withDialogActions)( export const CreditNoteExchangeRateSync = R.compose(withDialogActions)(
({ openDialog }) => { ({ openDialog }) => {
const { total } = useCreditNoteTotals(); const subtotal = useCreditNoteSubtotal();
const timeout = useRef(); const timeout = useRef();
useSyncExRateToForm({ useSyncExRateToForm({
onSynced: () => { onSynced: () => {
// If the total bigger then zero show alert to the user after adjusting entries. // If the total bigger then zero show alert to the user after adjusting entries.
if (total > 0) { if (subtotal > 0) {
clearTimeout(timeout.current); clearTimeout(timeout.current);
timeout.current = setTimeout(() => { timeout.current = setTimeout(() => {
openDialog(DialogsName.InvoiceExchangeRateChangeNotice); openDialog(DialogsName.InvoiceExchangeRateChangeNotice);

View File

@@ -10,6 +10,7 @@ import {
repeatValue, repeatValue,
formattedAmount, formattedAmount,
orderingLinesIndexes, orderingLinesIndexes,
toSafeNumber,
} from '@/utils'; } from '@/utils';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { useCreditNoteFormContext } from './CreditNoteFormProvider'; import { useCreditNoteFormContext } from './CreditNoteFormProvider';
@@ -57,6 +58,9 @@ export const defaultCreditNote = {
entries: [...repeatValue(defaultCreditNoteEntry, MIN_LINES_NUMBER)], entries: [...repeatValue(defaultCreditNoteEntry, MIN_LINES_NUMBER)],
attachments: [], attachments: [],
pdf_template_id: '', pdf_template_id: '',
discount: '',
discount_type: 'amount',
adjustment: '',
}; };
/** /**
@@ -174,32 +178,74 @@ export const useSetPrimaryWarehouseToForm = () => {
}; };
/** /**
* Retreives the credit note totals. * Retrieves the credit note subtotal.
* @returns {number}
*/ */
export const useCreditNoteTotals = () => { export const useCreditNoteSubtotal = () => {
const { const {
values: { entries, currency_code: currencyCode }, values: { entries },
} = useFormikContext(); } = useFormikContext();
// Retrieves the invoice entries total.
const total = React.useMemo(() => getEntriesTotal(entries), [entries]); const total = React.useMemo(() => getEntriesTotal(entries), [entries]);
// Retrieves the formatted total money. return total;
const formattedTotal = React.useMemo( };
() => formattedAmount(total, currencyCode),
[total, currencyCode],
);
// Retrieves the formatted subtotal.
const formattedSubtotal = React.useMemo(
() => formattedAmount(total, currencyCode, { money: false }),
[total, currencyCode],
);
return { /**
total, * Retrieves the credit note subtotal formatted.
formattedTotal, * @returns {string}
formattedSubtotal, */
}; export const useCreditNoteSubtotalFormatted = () => {
const subtotal = useCreditNoteSubtotal();
const { currency_code: currencyCode } = useFormikContext();
return formattedAmount(subtotal, currencyCode, { money: false });
};
/**
* Retrieves the credit note discount amount.
* @returns {number}
*/
export const useCreditNoteDiscountAmount = () => {
const { values } = useFormikContext();
const subtotal = useCreditNoteSubtotal();
const discount = toSafeNumber(values.discount);
return values?.discount_type === 'percentage'
? (discount * subtotal) / 100
: discount;
};
/**
* Retrieves the credit note adjustment amount.
* @returns {number}
*/
export const useCreditNoteAdjustmentAmount = () => {
const { values } = useFormikContext();
return toSafeNumber(values.adjustment);
};
/**
* Retrieves the credit note total.
* @returns {number}
*/
export const useCreditNoteTotal = () => {
const subtotal = useCreditNoteSubtotal();
const discountAmount = useCreditNoteDiscountAmount();
const adjustmentAmount = useCreditNoteAdjustmentAmount();
return subtotal - discountAmount - adjustmentAmount;
};
/**
* Retrieves the credit note total formatted.
* @returns {string}
*/
export const useCreditNoteTotalFormatted = () => {
const total = useCreditNoteTotal();
const { currency_code: currencyCode } = useFormikContext();
return formattedAmount(total, currencyCode);
}; };
/** /**
@@ -217,7 +263,6 @@ export const useCreditNoteIsForeignCustomer = () => {
return isForeignCustomer; return isForeignCustomer;
}; };
export const useCreditNoteFormBrandingTemplatesOptions = () => { export const useCreditNoteFormBrandingTemplatesOptions = () => {
const { brandingTemplates } = useCreditNoteFormContext(); const { brandingTemplates } = useCreditNoteFormContext();

View File

@@ -7,8 +7,12 @@ import {
TotalLine, TotalLine,
TotalLineBorderStyle, TotalLineBorderStyle,
TotalLineTextStyle, TotalLineTextStyle,
FInputGroup,
FFormGroup,
FSelect,
} from '@/components'; } from '@/components';
import { useEstimateTotals } from './utils'; import { useEstimateTotals } from './utils';
import { Button } from '@blueprintjs/core';
export function EstimateFormFooterRight() { export function EstimateFormFooterRight() {
const { formattedSubtotal, formattedTotal } = useEstimateTotals(); const { formattedSubtotal, formattedTotal } = useEstimateTotals();
@@ -20,6 +24,32 @@ export function EstimateFormFooterRight() {
value={formattedSubtotal} value={formattedSubtotal}
borderStyle={TotalLineBorderStyle.None} borderStyle={TotalLineBorderStyle.None}
/> />
<FFormGroup name={'discount'} label={'Discount'} inline>
<FInputGroup
name={'discount'}
rightElement={
<FSelect
name={'discount_type'}
items={[
{ text: 'USD', value: 'amount' },
{ text: '%', value: 'percentage' },
]}
input={({ text }) => (
<Button small minimal>
{text}
</Button>
)}
filterable={false}
/>
}
/>
</FFormGroup>
<FFormGroup name={'adjustment'} label={'Adjustment'} inline>
<FInputGroup name={'adjustment'} />
</FFormGroup>
<TotalLine <TotalLine
title={<T id={'estimate_form.label.total'} />} title={<T id={'estimate_form.label.total'} />}
value={formattedTotal} value={formattedTotal}

View File

@@ -63,6 +63,8 @@ export const defaultEstimate = {
entries: [...repeatValue(defaultEstimateEntry, MIN_LINES_NUMBER)], entries: [...repeatValue(defaultEstimateEntry, MIN_LINES_NUMBER)],
attachments: [], attachments: [],
pdf_template_id: '', pdf_template_id: '',
discount: '',
discount_type: 'amount'
}; };
const ERRORS = { const ERRORS = {

View File

@@ -9,6 +9,9 @@ import {
TotalLine, TotalLine,
TotalLineBorderStyle, TotalLineBorderStyle,
TotalLineTextStyle, TotalLineTextStyle,
FFormGroup,
FInputGroup,
FSelect,
} from '@/components'; } from '@/components';
import { useInvoiceAggregatedTaxRates } from './utils'; import { useInvoiceAggregatedTaxRates } from './utils';
import { TaxType } from '@/interfaces/TaxRates'; import { TaxType } from '@/interfaces/TaxRates';
@@ -18,6 +21,7 @@ import {
InvoiceSubTotalFormatted, InvoiceSubTotalFormatted,
InvoiceTotalFormatted, InvoiceTotalFormatted,
} from './components'; } from './components';
import { Button } from '@blueprintjs/core';
export function InvoiceFormFooterRight() { export function InvoiceFormFooterRight() {
const { const {
@@ -38,6 +42,31 @@ export function InvoiceFormFooterRight() {
} }
value={<InvoiceSubTotalFormatted />} value={<InvoiceSubTotalFormatted />}
/> />
<FFormGroup name={'discount'} label={'Discount'} inline>
<FInputGroup
name={'discount'}
rightElement={
<FSelect
name={'discount_type'}
items={[
{ text: 'USD', value: 'amount' },
{ text: '%', value: 'percentage' },
]}
input={({ text }) => (
<Button small minimal>
{text}
</Button>
)}
filterable={false}
/>
}
/>
</FFormGroup>
<FFormGroup name={'adjustment'} label={'Adjustment'} inline>
<FInputGroup name={'adjustment'} />
</FFormGroup>
{taxEntries.map((tax, index) => ( {taxEntries.map((tax, index) => (
<TotalLine <TotalLine
key={index} key={index}

View File

@@ -70,6 +70,9 @@ export const defaultInvoice = {
entries: [...repeatValue(defaultInvoiceEntry, MIN_LINES_NUMBER)], entries: [...repeatValue(defaultInvoiceEntry, MIN_LINES_NUMBER)],
attachments: [], attachments: [],
payment_methods: {}, payment_methods: {},
discount: '',
discount_type: 'amount',
adjustment: '',
}; };
// Invoice entry request schema. // Invoice entry request schema.
@@ -301,6 +304,20 @@ export const useInvoiceSubtotal = () => {
return React.useMemo(() => getEntriesTotal(entries), [entries]); return React.useMemo(() => getEntriesTotal(entries), [entries]);
}; };
/**
* Retrieves the invoice discount amount.
* @returns {number}
*/
export const useInvoiceDiscountAmount = () => {
const { values } = useFormikContext();
const subtotal = useInvoiceSubtotal();
const discount = parseFloat(values.discount);
return values?.discount_type === 'percentage'
? (subtotal * discount) / 100
: discount;
};
/** /**
* Detarmines whether the invoice has foreign customer. * Detarmines whether the invoice has foreign customer.
* @returns {boolean} * @returns {boolean}
@@ -382,10 +399,12 @@ export const useInvoiceTotal = () => {
const subtotal = useInvoiceSubtotal(); const subtotal = useInvoiceSubtotal();
const totalTaxAmount = useInvoiceTotalTaxAmount(); const totalTaxAmount = useInvoiceTotalTaxAmount();
const isExclusiveTax = useIsInvoiceTaxExclusive(); const isExclusiveTax = useIsInvoiceTaxExclusive();
const discountAmount = useInvoiceDiscountAmount();
return R.compose(R.when(R.always(isExclusiveTax), R.add(totalTaxAmount)))( return R.compose(
subtotal, R.when(R.always(isExclusiveTax), R.add(totalTaxAmount)),
); R.subtract(R.__, discountAmount),
)(subtotal);
}; };
/** /**

View File

@@ -8,8 +8,12 @@ import {
TotalLine, TotalLine,
TotalLineBorderStyle, TotalLineBorderStyle,
TotalLineTextStyle, TotalLineTextStyle,
FInputGroup,
FFormGroup,
FSelect,
} from '@/components'; } from '@/components';
import { useReceiptTotals } from './utils'; import { useReceiptTotals } from './utils';
import { Button } from '@blueprintjs/core';
export function ReceiptFormFooterRight() { export function ReceiptFormFooterRight() {
const { const {
@@ -26,6 +30,31 @@ export function ReceiptFormFooterRight() {
value={formattedSubtotal} value={formattedSubtotal}
borderStyle={TotalLineBorderStyle.None} borderStyle={TotalLineBorderStyle.None}
/> />
<FFormGroup name={'discount'} label={'Discount'} inline>
<FInputGroup
name={'discount'}
rightElement={
<FSelect
name={'discount_type'}
items={[
{ text: 'USD', value: 'amount' },
{ text: '%', value: 'percentage' },
]}
input={({ text }) => (
<Button small minimal>
{text}
</Button>
)}
filterable={false}
/>
}
/>
</FFormGroup>
<FFormGroup name={'adjustment'} label={'Adjustment'} inline>
<FInputGroup name={'adjustment'} />
</FFormGroup>
<TotalLine <TotalLine
title={<T id={'receipt_form.label.total'} />} title={<T id={'receipt_form.label.total'} />}
value={formattedTotal} value={formattedTotal}