mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 06:31:48 +00:00
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:
@@ -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())
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user