feat: capitalize full actual charge on stock items only for Purchase Invoice (#56223)

* feat: capitalize full actual charge on stock items only for Purchase Invoice

Extends #56102 (Purchase Receipt) to the Purchase Invoice GL: an actual
valuation charge (e.g. Freight) flagged 'Allocate Full Amount to Stock Items'
is fully capitalized onto stock/asset items only; when unchecked, only the
stock items' share of a spread-across-all-items charge is capitalized.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: aggregate GL rows per account in PI freight test

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rohitwaghchaure
2026-06-22 15:39:10 +05:30
committed by GitHub
parent 23bbcca97e
commit 9b8c363bed
4 changed files with 100 additions and 8 deletions

View File

@@ -590,6 +590,10 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
tax_service = TaxService(doc)
valuation_tax = {}
# Amount of each valuation charge actually capitalized into stock/asset valuation, keyed by
# tax row name - a non-stock item's share of a spread-across-all-items charge is excluded.
capitalized_valuation_tax = doc.get_capitalized_valuation_tax()
for tax in doc.get("taxes"):
amount, base_amount = tax_service.get_tax_amounts(tax, None)
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
@@ -624,8 +628,7 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
tax.idx, _(tax.category)
)
)
valuation_tax.setdefault(tax.name, 0)
valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount)
valuation_tax[tax.name] = capitalized_valuation_tax.get(tax.name, 0.0)
if doc.is_opening == "No" and doc.negative_expense_to_be_booked and valuation_tax:
total_valuation_amount = sum(valuation_tax.values())

View File

@@ -341,6 +341,83 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_full_actual_charge_capitalized_on_stock_items_only(self):
"""On a stock-updating Purchase Invoice, an actual valuation charge (e.g. Freight) with
"Allocate Full Amount to Stock Items" checked is fully capitalized onto stock/asset items
only. For 2 stock items + 1 service item (each net 100) and a 30 freight charge, the charge
is distributed over the 200 stock net only (15 per stock item) and the entire 30 is
capitalized; nothing is lost to the non-stock item."""
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
company = "_Test Company with perpetual inventory"
warehouse = "Stores - TCP1"
stock_item1 = make_item(properties={"is_stock_item": 1}).name
stock_item2 = make_item(properties={"is_stock_item": 1}).name
service_item = make_item(properties={"is_stock_item": 0}).name
pi = frappe.new_doc("Purchase Invoice")
pi.company = company
pi.supplier = "_Test Supplier"
pi.currency = "INR"
pi.update_stock = 1
pi.credit_to = "Creditors - TCP1"
# Order matters: stock, service, stock (service item in the middle)
for code in (stock_item1, service_item, stock_item2):
pi.append(
"items",
{
"item_code": code,
"qty": 1,
"rate": 100,
"warehouse": warehouse,
"cost_center": "Main - TCP1",
"expense_account": "Cost of Goods Sold - TCP1",
},
)
pi.append(
"taxes",
{
"charge_type": "Actual",
"account_head": "_Test Account Shipping Charges - TCP1",
"category": "Valuation and Total",
"cost_center": "Main - TCP1",
"description": "Freight",
"tax_amount": 30,
# Default behavior: allocate the full amount to stock/asset items only
"allocate_full_amount_to_stock_items": 1,
},
)
pi.insert()
# 30 freight / 200 stock net = 15 per stock item. The service item carries nothing.
self.assertAlmostEqual(pi.items[0].item_tax_amount, 15.0, places=2)
self.assertAlmostEqual(pi.items[1].item_tax_amount, 0.0, places=2)
self.assertAlmostEqual(pi.items[2].item_tax_amount, 15.0, places=2)
pi.submit()
gl_entries = get_gl_entries("Purchase Invoice", pi.name, skip_cancelled=True, as_dict=True)
# Sum per account - the same account can appear in multiple GL rows (e.g. the stock account
# is debited once per item), so aggregate rather than keeping only the last row.
gl_map = {}
for row in gl_entries:
acc = gl_map.setdefault(row.account, {"debit": 0.0, "credit": 0.0})
acc["debit"] += row.debit
acc["credit"] += row.credit
warehouse_account = get_warehouse_account_map(company)
stock_account = warehouse_account[warehouse]["account"]
# Stock asset = 200 (goods) + 30 (the entire freight charge)
self.assertAlmostEqual(gl_map[stock_account]["debit"], 230.0, places=2)
# The whole freight charge (30) is capitalized
self.assertAlmostEqual(gl_map["_Test Account Shipping Charges - TCP1"]["credit"], 30.0, places=2)
@ERPNextTestSuite.change_settings(
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
)

View File

@@ -11,14 +11,16 @@
"add_deduct_tax",
"charge_type",
"row_id",
"allocate_full_amount_to_stock_items",
"included_in_print_rate",
"included_in_paid_amount",
"col_break1",
"account_head",
"description",
"is_tax_withholding_account",
"section_break_uhfl",
"set_by_item_tax_template",
"is_tax_withholding_account",
"allocate_full_amount_to_stock_items",
"column_break_zqtz",
"included_in_print_rate",
"included_in_paid_amount",
"section_break_10",
"rate",
"accounting_dimensions_section",
@@ -85,7 +87,8 @@
"description": "If checked, the entire amount (e.g. Freight) is allocated to the valuation of stock & asset items only. If unchecked, the amount is distributed across all items and the portion belonging to non-stock items is not added to valuation.",
"fieldname": "allocate_full_amount_to_stock_items",
"fieldtype": "Check",
"label": "Allocate Full Amount to Stock Items"
"label": "Allocate Full Amount to Stock Items",
"show_description_on_click": 1
},
{
"default": "0",
@@ -281,13 +284,21 @@
"label": "Don't Recompute Tax",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_uhfl",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_zqtz",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-11-24 18:22:56.886010",
"modified": "2026-06-21 17:13:05.586544",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",

View File

@@ -17,6 +17,7 @@ class PurchaseTaxesandCharges(Document):
account_currency: DF.Link | None
account_head: DF.Link
add_deduct_tax: DF.Literal["Add", "Deduct"]
allocate_full_amount_to_stock_items: DF.Check
base_net_amount: DF.Currency
base_tax_amount: DF.Currency
base_tax_amount_after_discount_amount: DF.Currency