mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 14:41:53 +00:00
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:
@@ -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)
|
||||
Reference in New Issue
Block a user