fix: remove unnecessary expense head warning for purchase invoices with update stock

When a Purchase Invoice is created with `update_stock = 1`, the system
automatically replaces the item's expense account with the correct
inventory account for perpetual inventory. This is expected behaviour,
but a `frappe.msgprint` warning was being shown to the user:

  "Expense Head changed to Stock In Hand because account Cost of Goods
   Sold is not linked to warehouse Stores or it is not the default
   inventory account."

The message is purely informational, provides no actionable guidance,
and confuses users who deliberately enable Update Stock. The underlying
account substitution logic is unchanged; only the popup is suppressed.

The two other `msgprint` calls (for the Purchase-Receipt-linked and
no-Purchase-Receipt flows) are intentionally preserved — those surface
a genuine change in behaviour that users may not expect.

Fixes: https://github.com/frappe/erpnext/issues/...
(cherry picked from commit 224dff32df)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/services/expense_account.py
This commit is contained in:
Dipen Gala
2026-06-12 12:57:58 +05:30
committed by Mergify
parent a7dfc55d02
commit 6949f23c7f

View File

@@ -0,0 +1,173 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
"""Expense account resolution for Purchase Invoice items."""
import frappe
from frappe import _, throw
from frappe.utils import get_link_to_form
import erpnext
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.controllers.accounts_controller import validate_account_head
class ExpenseAccountService:
def __init__(self, doc):
self.doc = doc
def set_expense_account(self, for_validate: bool = False) -> None:
doc = self.doc
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(doc.company)
if auto_accounting_for_stock:
stock_not_billed_account = doc.get_company_default("stock_received_but_not_billed")
stock_items = doc.get_stock_items()
doc.asset_received_but_not_billed = None
inventory_account_map = {}
if doc.update_stock:
doc.validate_item_code()
doc.validate_warehouse(for_validate)
if auto_accounting_for_stock:
inventory_account_map = doc.get_inventory_account_map()
for item in doc.get("items"):
# in case of auto inventory accounting,
# expense account is always "Stock Received But Not Billed" for a stock item
# except opening entry, drop-ship entry and fixed asset items
if (
auto_accounting_for_stock
and item.item_code in stock_items
and doc.is_opening == "No"
and not item.is_fixed_asset
and (
not item.po_detail
or not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")
)
):
if doc.update_stock and item.warehouse and (not item.from_warehouse):
_inv_dict = doc.get_inventory_account_dict(item, inventory_account_map)
item.expense_account = _inv_dict["account"]
else:
# check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not
if item.purchase_receipt:
negative_expense_booked_in_pr = frappe.db.sql(
"""select name from `tabGL Entry`
where voucher_type='Purchase Receipt' and voucher_no=%s and account = %s""",
(item.purchase_receipt, stock_not_billed_account),
)
if negative_expense_booked_in_pr:
if (
for_validate
and item.expense_account
and item.expense_account != stock_not_billed_account
):
msg = _(
"Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}"
).format(
item.idx,
frappe.bold(stock_not_billed_account),
frappe.bold(item.purchase_receipt),
)
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
else:
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
# This is done in cases when Purchase Invoice is created before Purchase Receipt
if (
for_validate
and item.expense_account
and item.expense_account != stock_not_billed_account
):
msg = _(
"Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}."
).format(
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code)
)
msg += "<br>"
msg += _(
"This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice"
)
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
elif item.is_fixed_asset:
account = None
if not item.pr_detail and item.po_detail:
receipt_item = frappe.get_cached_value(
"Purchase Receipt Item",
{
"purchase_order": item.purchase_order,
"purchase_order_item": item.po_detail,
"docstatus": 1,
},
["name", "parent"],
as_dict=1,
)
if receipt_item:
item.pr_detail = receipt_item.name
item.purchase_receipt = receipt_item.parent
if item.pr_detail:
if not doc.asset_received_but_not_billed:
doc.asset_received_but_not_billed = doc.get_company_default(
"asset_received_but_not_billed"
)
# check if 'Asset Received But Not Billed' account is credited in Purchase receipt or not
arbnb_booked_in_pr = frappe.db.get_value(
"GL Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": item.purchase_receipt,
"account": doc.asset_received_but_not_billed,
},
"name",
)
if arbnb_booked_in_pr:
account = doc.asset_received_but_not_billed
if not account:
account_type = (
"capital_work_in_progress_account"
if is_cwip_accounting_enabled(item.asset_category)
else "fixed_asset_account"
)
account = get_asset_category_account(
account_type, item=item.item_code, company=doc.company
)
if not account:
form_link = get_link_to_form("Asset Category", item.asset_category)
throw(
_("Please set Fixed Asset Account in {} against {}.").format(
form_link, doc.company
),
title=_("Missing Account"),
)
item.expense_account = account
elif not item.expense_account and for_validate:
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
def validate_expense_account(self) -> None:
for item in self.doc.get("items"):
validate_account_head(item.idx, item.expense_account, self.doc.company, _("Expense"))
def set_against_expense_account(self) -> None:
doc = self.doc
against_accounts = []
for item in doc.get("items"):
if item.expense_account and (item.expense_account not in against_accounts):
against_accounts.append(item.expense_account)
doc.against_expense_account = ",".join(against_accounts)
def force_set_against_expense_account(self) -> None:
doc = self.doc
self.set_against_expense_account()
frappe.db.set_value(doc.doctype, doc.name, "against_expense_account", doc.against_expense_account)