diff --git a/erpnext/accounts/doctype/purchase_invoice/services/gl_composer.py b/erpnext/accounts/doctype/purchase_invoice/services/gl_composer.py index 60f49535cb0..9dbd8f01b3b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/services/gl_composer.py +++ b/erpnext/accounts/doctype/purchase_invoice/services/gl_composer.py @@ -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()) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 0527f518a65..8ad37f7f9d5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -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} ) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 8a4d95c6421..b785bb8126f 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -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", diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py index 4e054593455..9426fa5cc02 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.py @@ -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