mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-27 05:45:19 +00:00
feat: taxable-base resolver hook for custom charge types (#56175)
(cherry picked from commit 986cea2331)
This commit is contained in:
@@ -314,33 +314,32 @@ class calculate_taxes_and_totals:
|
|||||||
for item in self.doc.items:
|
for item in self.doc.items:
|
||||||
item._unrounded_net_amount = None
|
item._unrounded_net_amount = None
|
||||||
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
||||||
cumulated_tax_fraction = 0
|
total_tax_slope = 0
|
||||||
total_inclusive_tax_amount_per_qty = 0
|
total_tax_intercept = 0
|
||||||
for i, tax in enumerate(self.doc.get("taxes")):
|
for i, tax in enumerate(self.doc.get("taxes")):
|
||||||
(
|
(
|
||||||
tax.tax_fraction_for_current_item,
|
tax.tax_fraction_for_current_item,
|
||||||
inclusive_tax_amount_per_qty,
|
tax_intercept_per_qty,
|
||||||
) = self.get_current_tax_fraction(tax, item_tax_map)
|
) = self.get_current_tax_fraction(tax, item_tax_map, item)
|
||||||
|
tax.inclusive_amount_per_qty = tax_intercept_per_qty
|
||||||
|
|
||||||
if i == 0:
|
if i == 0:
|
||||||
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item
|
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item
|
||||||
|
tax.grand_total_amount_per_qty = tax_intercept_per_qty
|
||||||
else:
|
else:
|
||||||
|
prev = self.doc.get("taxes")[i - 1]
|
||||||
tax.grand_total_fraction_for_current_item = (
|
tax.grand_total_fraction_for_current_item = (
|
||||||
self.doc.get("taxes")[i - 1].grand_total_fraction_for_current_item
|
prev.grand_total_fraction_for_current_item + tax.tax_fraction_for_current_item
|
||||||
+ tax.tax_fraction_for_current_item
|
|
||||||
)
|
)
|
||||||
|
tax.grand_total_amount_per_qty = prev.grand_total_amount_per_qty + tax_intercept_per_qty
|
||||||
|
|
||||||
cumulated_tax_fraction += tax.tax_fraction_for_current_item
|
total_tax_slope += tax.tax_fraction_for_current_item
|
||||||
total_inclusive_tax_amount_per_qty += inclusive_tax_amount_per_qty * flt(item.qty)
|
total_tax_intercept += tax_intercept_per_qty * flt(item.qty)
|
||||||
|
|
||||||
if (
|
if not self.discount_amount_applied and item.qty and (total_tax_slope or total_tax_intercept):
|
||||||
not self.discount_amount_applied
|
amount = flt(item.amount) - total_tax_intercept
|
||||||
and item.qty
|
|
||||||
and (cumulated_tax_fraction or total_inclusive_tax_amount_per_qty)
|
|
||||||
):
|
|
||||||
amount = flt(item.amount) - total_inclusive_tax_amount_per_qty
|
|
||||||
|
|
||||||
item._unrounded_net_amount = amount / (1 + cumulated_tax_fraction)
|
item._unrounded_net_amount = amount / (1 + total_tax_slope)
|
||||||
item.net_amount = flt(item._unrounded_net_amount, item.precision("net_amount"))
|
item.net_amount = flt(item._unrounded_net_amount, item.precision("net_amount"))
|
||||||
item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate"))
|
item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate"))
|
||||||
item.discount_percentage = flt(
|
item.discount_percentage = flt(
|
||||||
@@ -352,41 +351,48 @@ class calculate_taxes_and_totals:
|
|||||||
def _load_item_tax_rate(self, item_tax_rate):
|
def _load_item_tax_rate(self, item_tax_rate):
|
||||||
return json.loads(item_tax_rate) if item_tax_rate else {}
|
return json.loads(item_tax_rate) if item_tax_rate else {}
|
||||||
|
|
||||||
def get_current_tax_fraction(self, tax, item_tax_map):
|
def get_current_tax_fraction(self, tax, item_tax_map, item):
|
||||||
"""
|
"""
|
||||||
Get tax fraction for calculating tax exclusive amount
|
tax = slope * net + intercept.
|
||||||
from tax inclusive amount
|
Returns (slope, intercept_per_qty)
|
||||||
"""
|
"""
|
||||||
current_tax_fraction = 0
|
tax_slope = 0
|
||||||
inclusive_tax_amount_per_qty = 0
|
tax_intercept = 0
|
||||||
|
|
||||||
if cint(tax.included_in_print_rate):
|
if cint(tax.included_in_print_rate):
|
||||||
tax_rate = self._get_tax_rate(tax, item_tax_map)
|
tax_rate = self._get_tax_rate(tax, item_tax_map)
|
||||||
|
|
||||||
if tax_rate == NOT_APPLICABLE_TAX:
|
if tax_rate == NOT_APPLICABLE_TAX:
|
||||||
return current_tax_fraction, inclusive_tax_amount_per_qty
|
return tax_slope, tax_intercept
|
||||||
|
|
||||||
if tax.charge_type == "On Net Total":
|
if tax.charge_type == "On Net Total":
|
||||||
current_tax_fraction = tax_rate / 100.0
|
tax_slope = tax_rate / 100.0
|
||||||
|
|
||||||
elif tax.charge_type == "On Previous Row Amount":
|
elif tax.charge_type == "On Previous Row Amount":
|
||||||
current_tax_fraction = (tax_rate / 100.0) * self.doc.get("taxes")[
|
row = self.doc.get("taxes")[cint(tax.row_id) - 1]
|
||||||
cint(tax.row_id) - 1
|
tax_slope = (tax_rate / 100.0) * row.tax_fraction_for_current_item
|
||||||
].tax_fraction_for_current_item
|
tax_intercept = (tax_rate / 100.0) * flt(getattr(row, "inclusive_amount_per_qty", 0))
|
||||||
|
|
||||||
elif tax.charge_type == "On Previous Row Total":
|
elif tax.charge_type == "On Previous Row Total":
|
||||||
current_tax_fraction = (tax_rate / 100.0) * self.doc.get("taxes")[
|
row = self.doc.get("taxes")[cint(tax.row_id) - 1]
|
||||||
cint(tax.row_id) - 1
|
tax_slope = (tax_rate / 100.0) * row.grand_total_fraction_for_current_item
|
||||||
].grand_total_fraction_for_current_item
|
tax_intercept = (tax_rate / 100.0) * flt(getattr(row, "grand_total_amount_per_qty", 0))
|
||||||
|
|
||||||
elif tax.charge_type == "On Item Quantity":
|
elif tax.charge_type == "On Item Quantity":
|
||||||
inclusive_tax_amount_per_qty = flt(tax_rate)
|
tax_intercept = flt(tax_rate)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Custom charge_type: the rate applies to a resolved (fixed) base,
|
||||||
|
# e.g. a tax on MRP included in the printed price.
|
||||||
|
qty = flt(item.qty) or 1
|
||||||
|
base = self.get_item_taxable_base(item, tax)
|
||||||
|
tax_intercept = (tax_rate / 100.0) * base / qty
|
||||||
|
|
||||||
if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct":
|
if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct":
|
||||||
current_tax_fraction *= -1.0
|
tax_slope *= -1.0
|
||||||
inclusive_tax_amount_per_qty *= -1.0
|
tax_intercept *= -1.0
|
||||||
|
|
||||||
return current_tax_fraction, inclusive_tax_amount_per_qty
|
return tax_slope, tax_intercept
|
||||||
|
|
||||||
def _get_tax_rate(self, tax, item_tax_map):
|
def _get_tax_rate(self, tax, item_tax_map):
|
||||||
if tax.account_head in item_tax_map:
|
if tax.account_head in item_tax_map:
|
||||||
@@ -612,7 +618,6 @@ class calculate_taxes_and_totals:
|
|||||||
elif tax.charge_type == "On Net Total":
|
elif tax.charge_type == "On Net Total":
|
||||||
if tax.account_head in item_tax_map:
|
if tax.account_head in item_tax_map:
|
||||||
current_net_amount = item.net_amount
|
current_net_amount = item.net_amount
|
||||||
|
|
||||||
# Use unrounded net for inclusive taxes to avoid double rounding
|
# Use unrounded net for inclusive taxes to avoid double rounding
|
||||||
if (
|
if (
|
||||||
cint(tax.included_in_print_rate)
|
cint(tax.included_in_print_rate)
|
||||||
@@ -631,12 +636,46 @@ class calculate_taxes_and_totals:
|
|||||||
elif tax.charge_type == "On Item Quantity":
|
elif tax.charge_type == "On Item Quantity":
|
||||||
# don't sum current net amount due to the field being a currency field
|
# don't sum current net amount due to the field being a currency field
|
||||||
current_tax_amount = tax_rate * item.qty
|
current_tax_amount = tax_rate * item.qty
|
||||||
|
else:
|
||||||
|
# Custom charge_type: rate applies to the resolver-provided base.
|
||||||
|
base = self.get_item_taxable_base(item, tax)
|
||||||
|
current_net_amount = base
|
||||||
|
current_tax_amount = (tax_rate / 100.0) * base
|
||||||
|
|
||||||
if not tax.get("dont_recompute_tax"):
|
if not tax.get("dont_recompute_tax"):
|
||||||
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount, current_net_amount)
|
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount, current_net_amount)
|
||||||
|
|
||||||
return current_net_amount, current_tax_amount
|
return current_net_amount, current_tax_amount
|
||||||
|
|
||||||
|
def get_item_taxable_base(self, item, tax):
|
||||||
|
"""Per-item base a custom charge_type's rate is applied to.
|
||||||
|
|
||||||
|
Override the base (gross, MRP, net of other taxes, …) via the
|
||||||
|
`erpnext_taxable_base_resolvers` hook
|
||||||
|
|
||||||
|
Register a resolver in `hooks.py`, keyed by charge_type:
|
||||||
|
|
||||||
|
erpnext_taxable_base_resolvers = {"On Gross Amount": "my_app.taxes.gross_base"}
|
||||||
|
|
||||||
|
It receives (calc, item, tax) — calc is this instance, calc.doc the parent —
|
||||||
|
and returns the base (flt-coerced by the caller):
|
||||||
|
|
||||||
|
def gross_base(calc, item, tax):
|
||||||
|
return item.custom_field_mrp * item.qty
|
||||||
|
|
||||||
|
A resolver may stamp transient attributes on `item`; it can be called more than once
|
||||||
|
per item, so such stamping must be idempotent.
|
||||||
|
"""
|
||||||
|
resolvers = frappe.get_hooks("erpnext_taxable_base_resolvers") or {}
|
||||||
|
path = resolvers.get(tax.charge_type)
|
||||||
|
|
||||||
|
if path:
|
||||||
|
method = path[-1] if isinstance(path, list | tuple) else path
|
||||||
|
return flt(frappe.get_attr(method)(self, item, tax))
|
||||||
|
|
||||||
|
# fallback
|
||||||
|
return flt(item.net_amount)
|
||||||
|
|
||||||
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount, current_net_amount):
|
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount, current_net_amount):
|
||||||
# store tax breakup for each item
|
# store tax breakup for each item
|
||||||
multiplier = -1 if tax.get("add_deduct_tax") == "Deduct" else 1
|
multiplier = -1 if tax.get("add_deduct_tax") == "Deduct" else 1
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
|
from unittest import mock
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
|
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_on_gross(calc, item, tax):
|
||||||
|
# base = gross printed line amount
|
||||||
|
return flt(item.amount)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_on_mrp(calc, item, tax):
|
||||||
|
# base = MRP, not net
|
||||||
|
return flt(item.price_list_rate) * flt(item.qty)
|
||||||
|
|
||||||
|
|
||||||
class TestTaxesAndTotals(ERPNextTestSuite):
|
class TestTaxesAndTotals(ERPNextTestSuite):
|
||||||
def test_regional_round_off_accounts(self):
|
def test_regional_round_off_accounts(self):
|
||||||
"""
|
"""
|
||||||
@@ -30,6 +42,93 @@ class TestTaxesAndTotals(ERPNextTestSuite):
|
|||||||
|
|
||||||
self.assertIn(test_account, frappe.flags.round_off_applicable_accounts)
|
self.assertIn(test_account, frappe.flags.round_off_applicable_accounts)
|
||||||
|
|
||||||
|
def test_exclusive_custom_charge_on_resolved_base(self):
|
||||||
|
"""Added (exclusive) custom charge_type whose base is resolved by the
|
||||||
|
`erpnext_taxable_base_resolvers` hook. IPI 10% on the gross product value 1000
|
||||||
|
-> tax 100, net 1000, grand 1100."""
|
||||||
|
so = make_sales_order(do_not_save=True)
|
||||||
|
so.items = []
|
||||||
|
so.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 1000,
|
||||||
|
"price_list_rate": 1000,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
so.set("taxes", [])
|
||||||
|
so.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Gross Value",
|
||||||
|
"account_head": "_Test Account Excise Duty - _TC",
|
||||||
|
"description": "IPI 10% on gross product value",
|
||||||
|
"rate": 10,
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
real_get_hooks = frappe.get_hooks
|
||||||
|
|
||||||
|
def fake_get_hooks(hook=None, *args, **kwargs):
|
||||||
|
if hook == "erpnext_taxable_base_resolvers":
|
||||||
|
return {
|
||||||
|
"On Gross Value": ["erpnext.controllers.tests.test_taxes_and_totals.resolve_on_gross"]
|
||||||
|
}
|
||||||
|
return real_get_hooks(hook, *args, **kwargs)
|
||||||
|
|
||||||
|
with mock.patch("frappe.get_hooks", side_effect=fake_get_hooks):
|
||||||
|
calculate_taxes_and_totals(so)
|
||||||
|
|
||||||
|
self.assertEqual(so.net_total, 1000.0)
|
||||||
|
self.assertEqual(so.taxes[0].tax_amount, 100.0)
|
||||||
|
self.assertEqual(so.grand_total, 1100.0)
|
||||||
|
|
||||||
|
def test_inclusive_custom_charge_on_resolved_base(self):
|
||||||
|
"""Inclusive custom charge on a resolved base backs out non-compounding
|
||||||
|
(tax = rate x resolved base) — a resolved base is fixed, so it never
|
||||||
|
compounds. MRP 1200, printed 1000, rate 10%: tax 120, net 880."""
|
||||||
|
so = make_sales_order(do_not_save=True)
|
||||||
|
so.items = []
|
||||||
|
so.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 1000,
|
||||||
|
"price_list_rate": 1200,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
so.set("taxes", [])
|
||||||
|
so.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On MRP",
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"description": "Tax 10% on MRP, inclusive",
|
||||||
|
"rate": 10,
|
||||||
|
"included_in_print_rate": 1,
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
real_get_hooks = frappe.get_hooks
|
||||||
|
|
||||||
|
def fake_get_hooks(hook=None, *args, **kwargs):
|
||||||
|
if hook == "erpnext_taxable_base_resolvers":
|
||||||
|
return {"On MRP": ["erpnext.controllers.tests.test_taxes_and_totals.resolve_on_mrp"]}
|
||||||
|
return real_get_hooks(hook, *args, **kwargs)
|
||||||
|
|
||||||
|
with mock.patch("frappe.get_hooks", side_effect=fake_get_hooks):
|
||||||
|
calculate_taxes_and_totals(so)
|
||||||
|
|
||||||
|
self.assertEqual(so.taxes[0].tax_amount, 120.0)
|
||||||
|
self.assertEqual(so.net_total, 880.0)
|
||||||
|
self.assertEqual(so.grand_total, 1000.0)
|
||||||
|
|
||||||
def test_disabling_rounded_total_resets_base_fields(self):
|
def test_disabling_rounded_total_resets_base_fields(self):
|
||||||
"""Disabling rounded total should also clear base rounded values."""
|
"""Disabling rounded total should also clear base rounded values."""
|
||||||
so = make_sales_order(do_not_save=True)
|
so = make_sales_order(do_not_save=True)
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
|
|
||||||
const NOT_APPLICABLE_TAX = "N/A";
|
const NOT_APPLICABLE_TAX = "N/A";
|
||||||
|
|
||||||
|
// Per-charge_type base resolvers, mirror of the `erpnext_taxable_base_resolvers`
|
||||||
|
// server hook. A localization registers `fn(calc, item, tax)` returning the per-item
|
||||||
|
// base, so the client preview matches the server for custom charge types.
|
||||||
|
erpnext.taxable_base_resolvers = erpnext.taxable_base_resolvers || {};
|
||||||
|
|
||||||
erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||||
setup() {
|
setup() {
|
||||||
this.fetch_round_off_accounts();
|
this.fetch_round_off_accounts();
|
||||||
@@ -278,32 +283,32 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
$.each(this.frm.doc.items || [], function (n, item) {
|
$.each(this.frm.doc.items || [], function (n, item) {
|
||||||
item._unrounded_net_amount = null;
|
item._unrounded_net_amount = null;
|
||||||
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
|
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
|
||||||
var cumulated_tax_fraction = 0.0;
|
var total_tax_slope = 0.0;
|
||||||
var total_inclusive_tax_amount_per_qty = 0;
|
var total_tax_intercept = 0;
|
||||||
$.each(me.frm.doc["taxes"] || [], function (i, tax) {
|
$.each(me.frm.doc["taxes"] || [], function (i, tax) {
|
||||||
var current_tax_fraction = me.get_current_tax_fraction(tax, item_tax_map);
|
var tax_contribution = me.get_current_tax_fraction(tax, item_tax_map, item);
|
||||||
tax.tax_fraction_for_current_item = current_tax_fraction[0];
|
tax.tax_fraction_for_current_item = tax_contribution[0];
|
||||||
var inclusive_tax_amount_per_qty = current_tax_fraction[1];
|
var tax_intercept_per_qty = tax_contribution[1];
|
||||||
|
tax.inclusive_amount_per_qty = tax_intercept_per_qty;
|
||||||
|
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item;
|
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item;
|
||||||
|
tax.grand_total_amount_per_qty = tax_intercept_per_qty;
|
||||||
} else {
|
} else {
|
||||||
|
var prev = me.frm.doc["taxes"][i - 1];
|
||||||
tax.grand_total_fraction_for_current_item =
|
tax.grand_total_fraction_for_current_item =
|
||||||
me.frm.doc["taxes"][i - 1].grand_total_fraction_for_current_item +
|
prev.grand_total_fraction_for_current_item + tax.tax_fraction_for_current_item;
|
||||||
tax.tax_fraction_for_current_item;
|
tax.grand_total_amount_per_qty =
|
||||||
|
flt(prev.grand_total_amount_per_qty) + tax_intercept_per_qty;
|
||||||
}
|
}
|
||||||
|
|
||||||
cumulated_tax_fraction += tax.tax_fraction_for_current_item;
|
total_tax_slope += tax.tax_fraction_for_current_item;
|
||||||
total_inclusive_tax_amount_per_qty += inclusive_tax_amount_per_qty * flt(item.qty);
|
total_tax_intercept += tax_intercept_per_qty * flt(item.qty);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (!me.discount_amount_applied && item.qty && (total_tax_intercept || total_tax_slope)) {
|
||||||
!me.discount_amount_applied &&
|
var amount = flt(item.amount) - total_tax_intercept;
|
||||||
item.qty &&
|
item._unrounded_net_amount = amount / (1 + total_tax_slope);
|
||||||
(total_inclusive_tax_amount_per_qty || cumulated_tax_fraction)
|
|
||||||
) {
|
|
||||||
var amount = flt(item.amount) - total_inclusive_tax_amount_per_qty;
|
|
||||||
item._unrounded_net_amount = amount / (1 + cumulated_tax_fraction);
|
|
||||||
item.net_amount = flt(item._unrounded_net_amount, precision("net_amount", item));
|
item.net_amount = flt(item._unrounded_net_amount, precision("net_amount", item));
|
||||||
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0;
|
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0;
|
||||||
|
|
||||||
@@ -312,39 +317,53 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get_current_tax_fraction(tax, item_tax_map) {
|
get_current_tax_fraction(tax, item_tax_map, item) {
|
||||||
// Get tax fraction for calculating tax exclusive amount
|
// tax = slope * net + intercept.
|
||||||
// from tax inclusive amount
|
// Returns [slope, intercept_per_qty]
|
||||||
var current_tax_fraction = 0.0;
|
var tax_slope = 0.0;
|
||||||
var inclusive_tax_amount_per_qty = 0;
|
var tax_intercept = 0;
|
||||||
|
|
||||||
if (cint(tax.included_in_print_rate)) {
|
if (cint(tax.included_in_print_rate)) {
|
||||||
var tax_rate = this._get_tax_rate(tax, item_tax_map);
|
var tax_rate = this._get_tax_rate(tax, item_tax_map);
|
||||||
|
|
||||||
if (tax_rate === NOT_APPLICABLE_TAX) {
|
if (tax_rate === NOT_APPLICABLE_TAX) {
|
||||||
return [current_tax_fraction, inclusive_tax_amount_per_qty];
|
return [tax_slope, tax_intercept];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tax.charge_type == "On Net Total") {
|
if (tax.charge_type == "On Net Total") {
|
||||||
current_tax_fraction = tax_rate / 100.0;
|
tax_slope = tax_rate / 100.0;
|
||||||
} else if (tax.charge_type == "On Previous Row Amount") {
|
} else if (tax.charge_type == "On Previous Row Amount") {
|
||||||
current_tax_fraction =
|
const row = this.frm.doc["taxes"][cint(tax.row_id) - 1];
|
||||||
(tax_rate / 100.0) *
|
tax_slope = (tax_rate / 100.0) * row.tax_fraction_for_current_item;
|
||||||
this.frm.doc["taxes"][cint(tax.row_id) - 1].tax_fraction_for_current_item;
|
tax_intercept = (tax_rate / 100.0) * flt(row.inclusive_amount_per_qty);
|
||||||
} else if (tax.charge_type == "On Previous Row Total") {
|
} else if (tax.charge_type == "On Previous Row Total") {
|
||||||
current_tax_fraction =
|
const row = this.frm.doc["taxes"][cint(tax.row_id) - 1];
|
||||||
(tax_rate / 100.0) *
|
tax_slope = (tax_rate / 100.0) * row.grand_total_fraction_for_current_item;
|
||||||
this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_fraction_for_current_item;
|
tax_intercept = (tax_rate / 100.0) * flt(row.grand_total_amount_per_qty);
|
||||||
} else if (tax.charge_type == "On Item Quantity") {
|
} else if (tax.charge_type == "On Item Quantity") {
|
||||||
inclusive_tax_amount_per_qty = flt(tax_rate);
|
tax_intercept = flt(tax_rate);
|
||||||
|
} else {
|
||||||
|
// Custom charge_type: the rate applies to a resolved (fixed) base,
|
||||||
|
// e.g. a tax on MRP included in the printed price.
|
||||||
|
const qty = flt(item.qty) || 1;
|
||||||
|
const base = this.get_item_taxable_base(item, tax);
|
||||||
|
tax_intercept = ((tax_rate / 100.0) * base) / qty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tax.add_deduct_tax && tax.add_deduct_tax == "Deduct") {
|
if (tax.add_deduct_tax && tax.add_deduct_tax == "Deduct") {
|
||||||
current_tax_fraction *= -1;
|
tax_slope *= -1;
|
||||||
inclusive_tax_amount_per_qty *= -1;
|
tax_intercept *= -1;
|
||||||
}
|
}
|
||||||
return [current_tax_fraction, inclusive_tax_amount_per_qty];
|
return [tax_slope, tax_intercept];
|
||||||
|
}
|
||||||
|
|
||||||
|
get_item_taxable_base(item, tax) {
|
||||||
|
// Mirror of the server get_item_taxable_base: a custom charge_type's resolver
|
||||||
|
// overrides the base value; otherwise the net amount.
|
||||||
|
const resolver = erpnext.taxable_base_resolvers[tax.charge_type];
|
||||||
|
if (resolver) return flt(resolver(this, item, tax));
|
||||||
|
return flt(item.net_amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
_get_tax_rate(tax, item_tax_map) {
|
_get_tax_rate(tax, item_tax_map) {
|
||||||
@@ -606,6 +625,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
} else if (tax.charge_type == "On Item Quantity") {
|
} else if (tax.charge_type == "On Item Quantity") {
|
||||||
// don't sum current net amount due to the field being a currency field
|
// don't sum current net amount due to the field being a currency field
|
||||||
current_tax_amount = tax_rate * item.qty;
|
current_tax_amount = tax_rate * item.qty;
|
||||||
|
} else {
|
||||||
|
// Custom charge_type: rate applies to the resolver-provided base.
|
||||||
|
var resolved_base = this.get_item_taxable_base(item, tax);
|
||||||
|
current_net_amount = resolved_base;
|
||||||
|
current_tax_amount = (tax_rate / 100.0) * resolved_base;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [current_net_amount, current_tax_amount];
|
return [current_net_amount, current_tax_amount];
|
||||||
|
|||||||
Reference in New Issue
Block a user