mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-17 02:26:33 +00:00
refactor(accounts): extract Purchase Invoice ExpenseAccountService
Move expense-account resolution into accounts/doctype/purchase_invoice/services/expense_account.py: - set_expense_account stays as a controller delegator (dispatched from accounts_controller.py) and force_set_against_expense_account stays (called by repost_accounting_ledger) - validate_expense_account and set_against_expense_account move into the service; validate() repointed (the unused force kwarg on set_against_expense_account is dropped with the method) Pre-existing raw SQL (SRBNB-booked-in-PR check) moved verbatim. No behaviour change.
This commit is contained in:
@@ -10,6 +10,7 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate,
|
|||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||||
|
from erpnext.accounts.doctype.purchase_invoice.services.expense_account import ExpenseAccountService
|
||||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||||
validate_docs_for_deferred_accounting,
|
validate_docs_for_deferred_accounting,
|
||||||
validate_docs_for_voucher_types,
|
validate_docs_for_voucher_types,
|
||||||
@@ -32,8 +33,6 @@ from erpnext.accounts.general_ledger import (
|
|||||||
from erpnext.accounts.party import get_due_date, get_party_account
|
from erpnext.accounts.party import get_due_date, get_party_account
|
||||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
|
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
|
||||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
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
|
|
||||||
from erpnext.controllers.buying_controller import BuyingController
|
from erpnext.controllers.buying_controller import BuyingController
|
||||||
from erpnext.stock.doctype.purchase_receipt.services.billing_status import (
|
from erpnext.stock.doctype.purchase_receipt.services.billing_status import (
|
||||||
update_billed_amount_based_on_po,
|
update_billed_amount_based_on_po,
|
||||||
@@ -284,9 +283,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.validate_with_previous_doc()
|
self.validate_with_previous_doc()
|
||||||
self.validate_uom_is_integer("uom", "qty")
|
self.validate_uom_is_integer("uom", "qty")
|
||||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||||
self.set_expense_account(for_validate=True)
|
expense_account_service = ExpenseAccountService(self)
|
||||||
self.validate_expense_account()
|
expense_account_service.set_expense_account(for_validate=True)
|
||||||
self.set_against_expense_account()
|
expense_account_service.validate_expense_account()
|
||||||
|
expense_account_service.set_against_expense_account()
|
||||||
self.validate_write_off_account()
|
self.validate_write_off_account()
|
||||||
self.validate_write_off_cost_center()
|
self.validate_write_off_cost_center()
|
||||||
|
|
||||||
@@ -446,166 +446,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
frappe.msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True)
|
frappe.msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True)
|
||||||
|
|
||||||
def set_expense_account(self, for_validate=False):
|
def set_expense_account(self, for_validate=False):
|
||||||
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
ExpenseAccountService(self).set_expense_account(for_validate)
|
||||||
|
|
||||||
if auto_accounting_for_stock:
|
|
||||||
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
|
||||||
stock_items = self.get_stock_items()
|
|
||||||
|
|
||||||
self.asset_received_but_not_billed = None
|
|
||||||
|
|
||||||
inventory_account_map = {}
|
|
||||||
if self.update_stock:
|
|
||||||
self.validate_item_code()
|
|
||||||
self.validate_warehouse(for_validate)
|
|
||||||
if auto_accounting_for_stock:
|
|
||||||
inventory_account_map = self.get_inventory_account_map()
|
|
||||||
|
|
||||||
for item in self.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 self.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 self.update_stock and item.warehouse and (not item.from_warehouse):
|
|
||||||
_inv_dict = self.get_inventory_account_dict(item, inventory_account_map)
|
|
||||||
|
|
||||||
if for_validate and item.expense_account and item.expense_account != _inv_dict["account"]:
|
|
||||||
msg = _(
|
|
||||||
"Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account"
|
|
||||||
).format(
|
|
||||||
item.idx,
|
|
||||||
frappe.bold(_inv_dict["account"]),
|
|
||||||
frappe.bold(item.expense_account),
|
|
||||||
frappe.bold(item.warehouse),
|
|
||||||
)
|
|
||||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
|
||||||
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 self.asset_received_but_not_billed:
|
|
||||||
self.asset_received_but_not_billed = self.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": self.asset_received_but_not_billed,
|
|
||||||
},
|
|
||||||
"name",
|
|
||||||
)
|
|
||||||
if arbnb_booked_in_pr:
|
|
||||||
account = self.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=self.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, self.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):
|
|
||||||
for item in self.get("items"):
|
|
||||||
validate_account_head(item.idx, item.expense_account, self.company, _("Expense"))
|
|
||||||
|
|
||||||
def set_against_expense_account(self, force=False):
|
|
||||||
against_accounts = []
|
|
||||||
for item in self.get("items"):
|
|
||||||
if item.expense_account and (item.expense_account not in against_accounts):
|
|
||||||
against_accounts.append(item.expense_account)
|
|
||||||
|
|
||||||
self.against_expense_account = ",".join(against_accounts)
|
|
||||||
|
|
||||||
def force_set_against_expense_account(self):
|
def force_set_against_expense_account(self):
|
||||||
self.set_against_expense_account()
|
ExpenseAccountService(self).force_set_against_expense_account()
|
||||||
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
|
|
||||||
|
|
||||||
def po_required(self):
|
def po_required(self):
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
if for_validate and item.expense_account and item.expense_account != _inv_dict["account"]:
|
||||||
|
msg = _(
|
||||||
|
"Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account"
|
||||||
|
).format(
|
||||||
|
item.idx,
|
||||||
|
frappe.bold(_inv_dict["account"]),
|
||||||
|
frappe.bold(item.expense_account),
|
||||||
|
frappe.bold(item.warehouse),
|
||||||
|
)
|
||||||
|
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||||
|
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