mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-17 02:26:33 +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)
|
tax_service = TaxService(doc)
|
||||||
valuation_tax = {}
|
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"):
|
for tax in doc.get("taxes"):
|
||||||
amount, base_amount = tax_service.get_tax_amounts(tax, None)
|
amount, base_amount = tax_service.get_tax_amounts(tax, None)
|
||||||
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
|
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
|
||||||
@@ -624,8 +628,7 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
|||||||
tax.idx, _(tax.category)
|
tax.idx, _(tax.category)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
valuation_tax.setdefault(tax.name, 0)
|
valuation_tax[tax.name] = capitalized_valuation_tax.get(tax.name, 0.0)
|
||||||
valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount)
|
|
||||||
|
|
||||||
if doc.is_opening == "No" and doc.negative_expense_to_be_booked and valuation_tax:
|
if doc.is_opening == "No" and doc.negative_expense_to_be_booked and valuation_tax:
|
||||||
total_valuation_amount = sum(valuation_tax.values())
|
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][1], gle.debit)
|
||||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
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(
|
@ERPNextTestSuite.change_settings(
|
||||||
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
|
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,14 +11,16 @@
|
|||||||
"add_deduct_tax",
|
"add_deduct_tax",
|
||||||
"charge_type",
|
"charge_type",
|
||||||
"row_id",
|
"row_id",
|
||||||
"allocate_full_amount_to_stock_items",
|
|
||||||
"included_in_print_rate",
|
|
||||||
"included_in_paid_amount",
|
|
||||||
"col_break1",
|
"col_break1",
|
||||||
"account_head",
|
"account_head",
|
||||||
"description",
|
"description",
|
||||||
"is_tax_withholding_account",
|
"section_break_uhfl",
|
||||||
"set_by_item_tax_template",
|
"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",
|
"section_break_10",
|
||||||
"rate",
|
"rate",
|
||||||
"accounting_dimensions_section",
|
"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.",
|
"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",
|
"fieldname": "allocate_full_amount_to_stock_items",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allocate Full Amount to Stock Items"
|
"label": "Allocate Full Amount to Stock Items",
|
||||||
|
"show_description_on_click": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -281,13 +284,21 @@
|
|||||||
"label": "Don't Recompute Tax",
|
"label": "Don't Recompute Tax",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_uhfl",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_zqtz",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-11-24 18:22:56.886010",
|
"modified": "2026-06-21 17:13:05.586544",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Taxes and Charges",
|
"name": "Purchase Taxes and Charges",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class PurchaseTaxesandCharges(Document):
|
|||||||
account_currency: DF.Link | None
|
account_currency: DF.Link | None
|
||||||
account_head: DF.Link
|
account_head: DF.Link
|
||||||
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
||||||
|
allocate_full_amount_to_stock_items: DF.Check
|
||||||
base_net_amount: DF.Currency
|
base_net_amount: DF.Currency
|
||||||
base_tax_amount: DF.Currency
|
base_tax_amount: DF.Currency
|
||||||
base_tax_amount_after_discount_amount: DF.Currency
|
base_tax_amount_after_discount_amount: DF.Currency
|
||||||
|
|||||||
Reference in New Issue
Block a user