2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 19:09:01 +00:00

incr: use flags in invoiceitem discounting

- update grand total calculation
This commit is contained in:
18alantom 2022-07-14 14:58:26 +05:30
parent 7f928ca712
commit bab18dc8f7
6 changed files with 184 additions and 168 deletions

View File

@ -133,25 +133,29 @@ export abstract class Invoice extends Transactional {
}
> = {};
for (const row of this.items as Doc[]) {
if (!row.tax) {
type TaxDetail = { account: string; rate: number };
for (const item of this.items ?? []) {
if (!item.tax) {
continue;
}
const tax = await this.getTax(row.tax as string);
for (const d of tax.details as Doc[]) {
const account = d.account as string;
const rate = d.rate as number;
taxes[account] = taxes[account] || {
const tax = await this.getTax(item.tax!);
for (const { account, rate } of tax.details as TaxDetail[]) {
taxes[account] ??= {
account,
rate,
amount: this.fyo.pesa(0),
baseAmount: this.fyo.pesa(0),
};
const amount = (row.amount as Money).mul(rate).div(100);
taxes[account].amount = taxes[account].amount.add(amount);
let amount = item.amount!;
if (this.enableDiscounting && !this.discountAfterTax) {
amount = item.itemDiscountedTotal!;
}
const taxAmount = amount.mul(rate / 100);
taxes[account].amount = taxes[account].amount.add(taxAmount);
}
}
@ -188,10 +192,28 @@ export abstract class Invoice extends Transactional {
return this.fyo.pesa(0);
}
return this.items.reduce(
(acc, i) => acc.add(i.itemDiscountAmount ?? this.fyo.pesa(0)),
this.fyo.pesa(0)
);
let discountAmount = this.fyo.pesa(0);
for (const item of this.items) {
if (item.setItemDiscountAmount) {
discountAmount = discountAmount.add(
item.itemDiscountAmount ?? this.fyo.pesa(0)
);
} else if (!this.discountAfterTax) {
discountAmount = discountAmount.add(
(item.amount ?? this.fyo.pesa(0)).mul(
(item.itemDiscountPercent ?? 0) / 100
)
);
} else if (this.discountAfterTax) {
discountAmount = discountAmount.add(
(item.itemTaxedTotal ?? this.fyo.pesa(0)).mul(
(item.itemDiscountPercent ?? 0) / 100
)
);
}
}
return discountAmount;
}
formulas: FormulaMap = {

View File

@ -4,7 +4,7 @@ import {
FiltersMap,
FormulaMap,
HiddenMap,
ValidationMap,
ValidationMap
} from 'fyo/model/types';
import { ValidationError } from 'fyo/utils/errors';
import { ModelNameEnum } from 'models/types';
@ -63,11 +63,12 @@ export abstract class InvoiceItem extends Doc {
},
rate: {
formula: async (fieldname) => {
let rate = (await this.fyo.getValue(
const rate = (await this.fyo.getValue(
'Item',
this.item as string,
'rate'
)) as undefined | Money;
if (
fieldname !== 'itemTaxedTotal' &&
fieldname !== 'itemDiscountedTotal'
@ -76,49 +77,36 @@ export abstract class InvoiceItem extends Doc {
}
const quantity = this.quantity ?? 0;
const taxedTotal = this.itemTaxedTotal ?? this.fyo.pesa(0);
const discountedTotal = this.itemDiscountedTotal ?? this.fyo.pesa(0);
const itemDiscountPercent = this.itemDiscountPercent ?? 0;
const itemDiscountAmount = this.itemDiscountAmount ?? this.fyo.pesa(0);
const totalTaxRate = await this.getTotalTaxRate();
const discountAmount = this.itemDiscountAmount ?? this.fyo.pesa(0);
const itemTaxedTotal = this.itemTaxedTotal ?? this.fyo.pesa(0);
const itemDiscountedTotal =
this.itemDiscountedTotal ?? this.fyo.pesa(0);
const isItemTaxedTotal = fieldname === 'itemTaxedTotal';
const discountAfterTax = this.discountAfterTax;
const setItemDiscountAmount = !!this.setItemDiscountAmount;
if (fieldname === 'itemTaxedTotal' && this.discountAfterTax) {
rate = getRateFromTaxedTotalWhenDiscountingAfterTaxation(
quantity,
taxedTotal,
totalTaxRate
);
} else if (
fieldname === 'itemDiscountedTotal' &&
this.discountAfterTax
) {
rate = getRateFromDiscountedTotalWhenDiscountingAfterTaxation(
quantity,
discountAmount,
discountedTotal,
totalTaxRate
);
} else if (fieldname === 'itemTaxedTotal' && !this.discountAfterTax) {
rate = getRateFromTaxedTotalWhenDiscountingBeforeTaxation(
quantity,
discountAmount,
taxedTotal,
totalTaxRate
);
} else if (
fieldname === 'itemDiscountedTotal' &&
!this.discountAfterTax
) {
rate = getRateFromDiscountedTotalWhenDiscountingBeforeTaxation(
quantity,
discountAmount,
discountedTotal
);
}
const rateFromTotals = getRate(
quantity,
itemDiscountPercent,
itemDiscountAmount,
totalTaxRate,
itemTaxedTotal,
itemDiscountedTotal,
isItemTaxedTotal,
discountAfterTax,
setItemDiscountAmount
);
console.log(rate?.float, fieldname, this.discountAfterTax);
return rate ?? this.fyo.pesa(0);
return rateFromTotals ?? rate ?? this.fyo.pesa(0);
},
dependsOn: ['item', 'itemTaxedTotal', 'itemDiscountedTotal'],
dependsOn: [
'item',
'itemTaxedTotal',
'itemDiscountedTotal',
'setItemDiscountAmount',
],
},
baseRate: {
formula: () =>
@ -179,59 +167,19 @@ export abstract class InvoiceItem extends Doc {
await this.fyo.getValue('Item', this.item as string, 'hsnCode'),
dependsOn: ['item'],
},
itemDiscountAmount: {
formula: async (fieldname) => {
if (fieldname === 'itemDiscountPercent') {
return this.amount!.percent(this.itemDiscountPercent ?? 0);
}
return this.fyo.pesa(0);
},
dependsOn: ['itemDiscountPercent'],
},
itemDiscountPercent: {
formula: async (fieldname) => {
const itemDiscountAmount = this.itemDiscountAmount ?? this.fyo.pesa(0);
if (!this.discountAfterTax) {
return itemDiscountAmount.div(this.amount ?? 0).mul(100).float;
}
const totalTaxRate = await this.getTotalTaxRate();
const rate = this.rate ?? this.fyo.pesa(0);
const quantity = this.quantity ?? 1;
let itemTaxedTotal = this.itemTaxedTotal;
if (fieldname !== 'itemTaxedTotal' || !itemTaxedTotal) {
itemTaxedTotal = getTaxedTotalBeforeDiscounting(
totalTaxRate,
rate,
quantity
);
}
return itemDiscountAmount.div(itemTaxedTotal).mul(100).float;
},
dependsOn: [
'itemDiscountAmount',
'item',
'rate',
'quantity',
'itemTaxedTotal',
'itemDiscountedTotal',
],
},
itemDiscountedTotal: {
formula: async (fieldname) => {
formula: async () => {
const totalTaxRate = await this.getTotalTaxRate();
const rate = this.rate ?? this.fyo.pesa(0);
const quantity = this.quantity ?? 1;
const itemDiscountAmount = this.itemDiscountAmount ?? this.fyo.pesa(0);
const itemDiscountPercent = this.itemDiscountPercent ?? 0;
if (
this.itemDiscountAmount?.isZero() ||
this.itemDiscountPercent === 0
) {
if (this.setItemDiscountAmount && this.itemDiscountAmount?.isZero()) {
return rate.mul(quantity);
}
if (!this.setItemDiscountAmount && this.itemDiscountPercent === 0) {
return rate.mul(quantity);
}
@ -241,7 +189,7 @@ export abstract class InvoiceItem extends Doc {
quantity,
itemDiscountAmount,
itemDiscountPercent,
fieldname
!!this.setItemDiscountAmount
);
}
@ -251,13 +199,14 @@ export abstract class InvoiceItem extends Doc {
quantity,
itemDiscountAmount,
itemDiscountPercent,
fieldname
!!this.setItemDiscountAmount
);
},
dependsOn: [
'itemDiscountAmount',
'itemDiscountPercent',
'itemTaxedTotal',
'setItemDiscountAmount',
'tax',
'rate',
'quantity',
@ -279,7 +228,7 @@ export abstract class InvoiceItem extends Doc {
quantity,
itemDiscountAmount,
itemDiscountPercent,
fieldname
!!this.setItemDiscountAmount
);
}
@ -289,6 +238,7 @@ export abstract class InvoiceItem extends Doc {
'itemDiscountAmount',
'itemDiscountPercent',
'itemDiscountedTotal',
'setItemDiscountAmount',
'tax',
'rate',
'quantity',
@ -310,14 +260,49 @@ export abstract class InvoiceItem extends Doc {
)}) cannot be less zero.`
);
},
itemDiscountAmount: async (value: DocValue) => {
if ((value as Money).lte(this.amount!)) {
return;
}
throw new ValidationError(
this.fyo.t`Discount Amount (${this.fyo.format(
value,
'Currency'
)}) cannot be greated than Amount (${this.fyo.format(
this.amount!,
'Currency'
)}).`
);
},
itemDiscountPercent: async (value: DocValue) => {
if ((value as number) < 100) {
return;
}
throw new ValidationError(
this.fyo.t`Discount Percent (${
value as number
}) cannot be greater than 100.`
);
},
};
hidden: HiddenMap = {
itemDiscountedTotal: () => {
return (
!this.discountAfterTax &&
(this.itemDiscountAmount?.isZero() || this.itemDiscountPercent === 0)
);
if (!this.enableDiscounting) {
return true;
}
if (!!this.setItemDiscountAmount && this.itemDiscountAmount?.isZero()) {
return true;
}
if (!this.setItemDiscountAmount && this.itemDiscountPercent === 0) {
return true;
}
return false;
},
setItemDiscountAmount: () => !this.enableDiscounting,
itemDiscountAmount: () =>
@ -360,7 +345,7 @@ function getDiscountedTotalBeforeTaxation(
quantity: number,
itemDiscountAmount: Money,
itemDiscountPercent: number,
fieldname?: string
setDiscountAmount: boolean
) {
/**
* If Discount is applied before taxation
@ -368,8 +353,9 @@ function getDiscountedTotalBeforeTaxation(
* - if amount : Quantity * Rate - DiscountAmount
* - if percent: Quantity * Rate (1 - DiscountPercent / 100)
*/
const amount = rate.mul(quantity);
if (fieldname === 'itemDiscountAmount') {
if (setDiscountAmount) {
return amount.sub(itemDiscountAmount);
}
@ -382,7 +368,7 @@ function getTaxedTotalAfterDiscounting(
quantity: number,
itemDiscountAmount: Money,
itemDiscountPercent: number,
fieldname?: string
setItemDiscountAmount: boolean
) {
/**
* If Discount is applied before taxation
@ -394,7 +380,7 @@ function getTaxedTotalAfterDiscounting(
quantity,
itemDiscountAmount,
itemDiscountPercent,
fieldname
setItemDiscountAmount
);
return discountedTotal.mul(1 + totalTaxRate / 100);
@ -406,7 +392,7 @@ function getDiscountedTotalAfterTaxation(
quantity: number,
itemDiscountAmount: Money,
itemDiscountPercent: number,
fieldname?: string
setItemDiscountAmount: boolean
) {
/**
* If Discount is applied after taxation
@ -420,7 +406,7 @@ function getDiscountedTotalAfterTaxation(
quantity
);
if (fieldname === 'itemDiscountAmount') {
if (setItemDiscountAmount) {
return taxedTotal.sub(itemDiscountAmount);
}
@ -440,45 +426,62 @@ function getTaxedTotalBeforeDiscounting(
return rate.mul(quantity).mul(1 + totalTaxRate / 100);
}
/**
* Calculate Rate if any of the final amounts is set
*/
function getRateFromDiscountedTotalWhenDiscountingBeforeTaxation(
function getRate(
quantity: number,
discountAmount: Money,
discountedTotal: Money
itemDiscountPercent: number,
itemDiscountAmount: Money,
totalTaxRate: number,
itemTaxedTotal: Money,
itemDiscountedTotal: Money,
isItemTaxedTotal: boolean,
discountAfterTax: boolean,
setItemDiscountAmount: boolean
) {
return discountedTotal.add(discountAmount).div(quantity);
}
const isItemDiscountedTotal = !isItemTaxedTotal;
const discountBeforeTax = !discountAfterTax;
function getRateFromTaxedTotalWhenDiscountingBeforeTaxation(
quantity: number,
discountAmount: Money,
taxedTotal: Money,
totalTaxRatio: number
) {
return taxedTotal
.div(1 + totalTaxRatio / 100)
.add(discountAmount)
.div(quantity);
}
/**
* Rate calculated from itemDiscountedTotal
*/
if (isItemDiscountedTotal && discountBeforeTax && setItemDiscountAmount) {
return itemDiscountedTotal.add(itemDiscountAmount).div(quantity);
}
function getRateFromDiscountedTotalWhenDiscountingAfterTaxation(
quantity: number,
discountAmount: Money,
discountedTotal: Money,
totalTaxRatio: number
) {
return discountedTotal
.add(discountAmount)
.div(1 + totalTaxRatio / 100)
.div(quantity);
}
if (isItemDiscountedTotal && discountBeforeTax && !setItemDiscountAmount) {
return itemDiscountedTotal.div(quantity * (1 - itemDiscountPercent / 100));
}
function getRateFromTaxedTotalWhenDiscountingAfterTaxation(
quantity: number,
taxedTotal: Money,
totalTaxRatio: number
) {
return taxedTotal.div(1 + totalTaxRatio / 100).div(quantity);
if (isItemDiscountedTotal && discountAfterTax && setItemDiscountAmount) {
return itemDiscountedTotal
.add(itemDiscountAmount)
.div(quantity * (1 + totalTaxRate / 100));
}
if (isItemDiscountedTotal && discountAfterTax && !setItemDiscountAmount) {
return itemDiscountedTotal.div(
(quantity * (100 - itemDiscountPercent) * (100 + totalTaxRate)) / 100
);
}
/**
* Rate calculated from itemTaxedTotal
*/
if (isItemTaxedTotal && discountAfterTax) {
return itemTaxedTotal.div(quantity * (1 + totalTaxRate / 100));
}
if (isItemTaxedTotal && discountBeforeTax && setItemDiscountAmount) {
return itemTaxedTotal
.div(1 + totalTaxRate / 100)
.add(itemDiscountAmount)
.div(quantity);
}
if (isItemTaxedTotal && discountBeforeTax && !setItemDiscountAmount) {
return itemTaxedTotal.div(
quantity * (1 - itemDiscountPercent / 100) * (1 + totalTaxRate / 100)
);
}
return null;
}

View File

@ -84,14 +84,14 @@
},
{
"fieldname": "itemDiscountedTotal",
"label": "Discounted Total",
"label": "Discounted Amount",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "itemTaxedTotal",
"label": "Taxed Total",
"label": "Taxed Amount",
"fieldtype": "Currency",
"readOnly": false,
"computed": true

View File

@ -81,14 +81,14 @@
},
{
"fieldname": "itemDiscountedTotal",
"label": "Discounted Total",
"label": "Discounted Amount",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "itemTaxedTotal",
"label": "Taxed Total",
"label": "Taxed Amount",
"fieldtype": "Currency",
"readOnly": false,
"computed": true

View File

@ -62,10 +62,8 @@
height: getFieldHeight(df),
}"
>
<div class="py-2 pl-4 flex text-gray-600">
<div class="py-1">
{{ df.label }}
</div>
<div class="pl-4 flex text-gray-600">
{{ df.label }}
</div>
<div

View File

@ -12,7 +12,7 @@
</Button>
<Button
:icon="true"
v-if="!doc?.isSubmitted && !quickEditDoc"
v-if="!doc?.isSubmitted && !quickEditDoc && doc.enableDiscounting"
@click="toggleInvoiceSettings"
>
<feather-icon name="settings" class="w-4 h-4" />
@ -334,10 +334,8 @@ export default {
},
discountNote() {
const zeroInvoiceDiscount = this.doc?.discountAmount?.isZero();
const zeroItemDiscount = this.itemDiscountAmount?.isZero();
const zeroItemDiscount = (this.doc?.items ?? []).every(
(i) => i?.itemDiscountAmount?.isZero() && i?.itemDiscountPercent === 0
);
if (zeroInvoiceDiscount && zeroItemDiscount) {
return '';
}
@ -355,12 +353,7 @@ export default {
},
totalDiscount() {
const discountAmount = this.doc?.discountAmount ?? fyo.pesa(0);
const itemDiscount = (this.doc?.items ?? []).reduce(
(acc, i) => acc.add(i.itemDiscountAmount),
fyo.pesa(0)
);
return discountAmount.add(itemDiscount);
return discountAmount.add(this.itemDiscountAmount);
},
discountAmount() {
return this.doc?.discountAmount ?? fyo.pesa(0);