feat: enable item wise inventory account

This commit is contained in:
Rohit Waghchaure
2025-10-22 11:20:51 +05:30
parent 5b6979c700
commit 74192547ce
19 changed files with 831 additions and 98 deletions

View File

@@ -40,7 +40,6 @@ from erpnext.assets.doctype.asset_category.asset_category import get_asset_categ
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
update_billed_amount_based_on_po,
)
@@ -460,11 +459,12 @@ class PurchaseInvoice(BuyingController):
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:
warehouse_account = get_warehouse_account_map(self.company)
inventory_account_map = self.get_inventory_account_map()
for item in self.get("items"):
# in case of auto inventory accounting,
@@ -481,21 +481,19 @@ class PurchaseInvoice(BuyingController):
)
):
if self.update_stock and item.warehouse and (not item.from_warehouse):
if (
for_validate
and item.expense_account
and item.expense_account != warehouse_account[item.warehouse]["account"]
):
_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(warehouse_account[item.warehouse]["account"]),
frappe.bold(_inv_dict["account"]),
frappe.bold(item.expense_account),
frappe.bold(item.warehouse),
)
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = warehouse_account[item.warehouse]["account"]
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:
@@ -857,7 +855,7 @@ class PurchaseInvoice(BuyingController):
party=self.supplier,
)
def get_gl_entries(self, warehouse_account=None):
def get_gl_entries(self, inventory_account_map=None):
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if self.auto_accounting_for_stock:
@@ -947,7 +945,7 @@ class PurchaseInvoice(BuyingController):
# item gl entries
stock_items = self.get_stock_items()
if self.update_stock and self.auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
inventory_account_map = self.get_inventory_account_map()
landed_cost_entries = self.get_item_account_wise_lcv_entries()
@@ -997,18 +995,24 @@ class PurchaseInvoice(BuyingController):
)
if item.from_warehouse:
_inv_dict = self.get_inventory_account_dict(item, inventory_account_map)
_inv_dict_from_warehouse = self.get_inventory_account_dict(
item, inventory_account_map, "from_warehouse"
)
gl_entries.append(
self.get_gl_dict(
{
"account": warehouse_account[item.warehouse]["account"],
"against": warehouse_account[item.from_warehouse]["account"],
"account": _inv_dict["account"],
"against": _inv_dict_from_warehouse["account"],
"cost_center": item.cost_center,
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": warehouse_debit_amount,
"debit_in_transaction_currency": item.net_amount,
},
warehouse_account[item.warehouse]["account_currency"],
_inv_dict["account_currency"],
item=item,
)
)
@@ -1021,15 +1025,15 @@ class PurchaseInvoice(BuyingController):
gl_entries.append(
self.get_gl_dict(
{
"account": warehouse_account[item.from_warehouse]["account"],
"against": warehouse_account[item.warehouse]["account"],
"account": _inv_dict_from_warehouse["account"],
"against": _inv_dict["account"],
"cost_center": item.cost_center,
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
"debit_in_transaction_currency": item.net_amount,
},
warehouse_account[item.from_warehouse]["account_currency"],
_inv_dict_from_warehouse["account_currency"],
item=item,
)
)
@@ -1097,15 +1101,19 @@ class PurchaseInvoice(BuyingController):
# sub-contracting warehouse
if flt(item.rm_supp_cost):
supplier_warehouse_account = warehouse_account[self.supplier_warehouse]["account"]
if not supplier_warehouse_account:
supplier_wh_dict = self.get_inventory_account_dict(
item, inventory_account_map, "supplier_warehouse"
)
supplier_inventory_account = supplier_wh_dict["account"]
if not supplier_inventory_account:
frappe.throw(
_("Please set account in Warehouse {0}").format(self.supplier_warehouse)
)
gl_entries.append(
self.get_gl_dict(
{
"account": supplier_warehouse_account,
"account": supplier_inventory_account,
"against": item.expense_account,
"cost_center": item.cost_center,
"project": item.project or self.project,
@@ -1113,7 +1121,7 @@ class PurchaseInvoice(BuyingController):
"credit": flt(item.rm_supp_cost),
"credit_in_transaction_currency": item.net_amount,
},
warehouse_account[self.supplier_warehouse]["account_currency"],
supplier_wh_dict["account_currency"],
item=item,
)
)

View File

@@ -101,8 +101,8 @@ class RepostAccountingLedger(Document):
if doc.doctype in ["Payment Entry", "Journal Entry"]:
gle_map = doc.build_gl_map()
elif doc.doctype == "Purchase Receipt":
warehouse_account_map = get_warehouse_account_map(doc.company)
gle_map = doc.get_gl_entries(warehouse_account_map)
inventory_account_map = doc.get_inventory_account_map()
gle_map = doc.get_gl_entries(inventory_account_map)
else:
gle_map = doc.get_gl_entries()

View File

@@ -1551,7 +1551,7 @@ class SalesInvoice(SellingController):
elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock):
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
def get_gl_entries(self, warehouse_account=None):
def get_gl_entries(self, inventory_account_map=None):
from erpnext.accounts.general_ledger import merge_similar_entries
gl_entries = []

View File

@@ -1531,7 +1531,8 @@ def repost_gle_for_stock_vouchers(
voucher_obj = frappe.get_lazy_doc(voucher_type, voucher_no)
# Some transactions post credit as negative debit, this is handled while posting GLE
# but while comparing we need to make sure it's flipped so comparisons are accurate
expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account))
inventory_account_map = voucher_obj.get_inventory_account_map()
expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(inventory_account_map))
if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(
existing_gle, expected_gle, precision

View File

@@ -24,7 +24,6 @@ from erpnext.assets.doctype.asset_category.asset_category import get_asset_categ
from erpnext.controllers.stock_controller import StockController
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.get_item_details import (
ItemDetailsCtx,
@@ -412,13 +411,15 @@ class AssetCapitalization(StockController):
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None):
def get_gl_entries(
self, inventory_account_map=None, default_expense_account=None, default_cost_center=None
):
# Stock GL Entries
gl_entries = []
self.warehouse_account = warehouse_account
if not self.warehouse_account:
self.warehouse_account = get_warehouse_account_map(self.company)
self.inventory_account_map = inventory_account_map
if not self.inventory_account_map:
self.inventory_account_map = self.get_inventory_account_map()
precision = self.get_debit_field_precision()
self.sle_map = self.get_stock_ledger_details()
@@ -457,11 +458,12 @@ class AssetCapitalization(StockController):
for item_row in self.stock_items:
sle_list = self.sle_map.get(item_row.name)
if sle_list:
_inv_dict = self.get_inventory_account_dict(item_row, self.inventory_account_map)
for sle in sle_list:
stock_value_difference = flt(sle.stock_value_difference, precision)
if erpnext.is_perpetual_inventory_enabled(self.company):
account = self.warehouse_account[sle.warehouse]["account"]
account = _inv_dict["account"]
else:
account = self.get_company_default("default_expense_account")
@@ -476,7 +478,7 @@ class AssetCapitalization(StockController):
"remarks": self.get("remarks") or "Accounting Entry for Stock",
"credit": -1 * stock_value_difference,
},
self.warehouse_account[sle.warehouse]["account_currency"],
_inv_dict["account_currency"],
item=item_row,
)
)

View File

@@ -22,10 +22,13 @@ from erpnext.controllers.sales_and_purchase_return import (
filter_serial_batches,
make_serial_batch_bundle_for_return,
)
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
get_evaluated_inventory_dimension,
)
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
combine_datetime,
get_type_of_transaction,
@@ -152,6 +155,62 @@ class StockController(AccountsController):
)
)
def get_item_wise_inventory_account_map(self, company):
inventory_account_map = frappe._dict()
for table in ["items", "packed_items", "supplied_items"]:
if not self.get(table):
continue
_map = get_item_wise_inventory_account_map(self.get(table), self.company)
inventory_account_map.update(_map)
return inventory_account_map
@property
def use_item_inventory_account(self):
return frappe.get_cached_value("Company", self.company, "enable_item_wise_inventory_account")
def get_inventory_account_dict(self, row, inventory_account_map, warehouse_field=None):
account_dict = frappe._dict()
if isinstance(row, dict):
row = frappe._dict(row)
if self.use_item_inventory_account:
item_code = (
row.rm_item_code if hasattr(row, "rm_item_code") and row.rm_item_code else row.item_code
)
account_dict = inventory_account_map.get(item_code)
if not account_dict:
frappe.throw(
_(
"Please set default inventory account for item {0}, or their item group or brand."
).format(bold(item_code))
)
if account_dict:
return account_dict
if not warehouse_field:
warehouse_field = "warehouse"
warehouse = row.get(warehouse_field)
if not warehouse:
warehouse = self.get(warehouse_field)
if warehouse and warehouse in inventory_account_map:
account_dict = inventory_account_map[warehouse]
return account_dict
def get_inventory_account_map(self):
if self.use_item_inventory_account:
return self.get_item_wise_inventory_account_map(self.company)
return get_warehouse_account_map(self.company)
def make_gl_entries(self, gl_entries=None, from_repost=False, via_landed_cost_voucher=False):
if self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -169,14 +228,14 @@ class StockController(AccountsController):
or provisional_accounting_for_non_stock_items
or is_asset_pr
):
warehouse_account = get_warehouse_account_map(self.company)
inventory_account_map = self.get_inventory_account_map()
if self.docstatus == 1:
if not gl_entries:
gl_entries = (
self.get_gl_entries(warehouse_account, via_landed_cost_voucher)
self.get_gl_entries(inventory_account_map, via_landed_cost_voucher)
if self.doctype == "Purchase Receipt"
else self.get_gl_entries(warehouse_account)
else self.get_gl_entries(inventory_account_map)
)
make_gl_entries(gl_entries, from_repost=from_repost)
@@ -578,9 +637,11 @@ class StockController(AccountsController):
for row in self.items:
row.use_serial_batch_fields = 1
def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None):
if not warehouse_account:
warehouse_account = get_warehouse_account_map(self.company)
def get_gl_entries(
self, inventory_account_map=None, default_expense_account=None, default_cost_center=None
):
if not inventory_account_map:
inventory_account_map = self.get_inventory_account_map()
sle_map = self.get_stock_ledger_details()
voucher_details = self.get_voucher_details(default_expense_account, default_cost_center, sle_map)
@@ -593,7 +654,9 @@ class StockController(AccountsController):
sle_rounding_diff = 0.0
if sle_list:
for sle in sle_list:
if warehouse_account.get(sle.warehouse):
_inv_dict = self.get_inventory_account_dict(sle, inventory_account_map)
if _inv_dict.get("account"):
# from warehouse account
sle_rounding_diff += flt(sle.stock_value_difference)
@@ -602,15 +665,17 @@ class StockController(AccountsController):
# expense account/ target_warehouse / source_warehouse
if item_row.get("target_warehouse"):
warehouse = item_row.get("target_warehouse")
expense_account = warehouse_account[warehouse]["account"]
_target_wh_inv_dict = self.get_inventory_account_dict(
item_row, inventory_account_map, warehouse_field="target_warehouse"
)
expense_account = _target_wh_inv_dict["account"]
else:
expense_account = item_row.expense_account
gl_list.append(
self.get_gl_dict(
{
"account": warehouse_account[sle.warehouse]["account"],
"account": _inv_dict["account"],
"against": expense_account,
"cost_center": item_row.cost_center,
"project": sle.get("project") or item_row.project or self.get("project"),
@@ -620,7 +685,7 @@ class StockController(AccountsController):
or self.get("is_opening")
or "No",
},
warehouse_account[sle.warehouse]["account_currency"],
_inv_dict["account_currency"],
item=item_row,
)
)
@@ -629,7 +694,7 @@ class StockController(AccountsController):
self.get_gl_dict(
{
"account": expense_account,
"against": warehouse_account[sle.warehouse]["account"],
"against": _inv_dict["account"],
"cost_center": item_row.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": -1 * flt(sle.stock_value_difference, precision),
@@ -649,9 +714,15 @@ class StockController(AccountsController):
if abs(sle_rounding_diff) > (1.0 / (10**precision)) and self.is_internal_transfer():
warehouse_asset_account = ""
if self.get("is_internal_customer"):
warehouse_asset_account = warehouse_account[item_row.get("target_warehouse")]["account"]
_inv_dict = self.get_inventory_account_dict(
item_row, inventory_account_map, warehouse_field="target_warehouse"
)
warehouse_asset_account = _inv_dict["account"]
elif self.get("is_internal_supplier"):
warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"]
_inv_dict = self.get_inventory_account_dict(item_row, inventory_account_map)
warehouse_asset_account = _inv_dict["account"]
expense_account = frappe.get_cached_value("Company", self.company, "default_expense_account")
if not expense_account:
@@ -672,7 +743,7 @@ class StockController(AccountsController):
"debit": sle_rounding_diff,
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
},
warehouse_account[sle.warehouse]["account_currency"],
_inv_dict["account_currency"],
item=item_row,
)
)
@@ -2015,3 +2086,49 @@ def make_bundle_for_material_transfer(**kwargs):
bundle_doc.submit()
return bundle_doc.name
def get_item_wise_inventory_account_map(rows, company):
# returns dict of item_code and its inventory account details
# Example: {"ITEM-001": {"account": "Stock - ABC", "account_currency": "INR"}, ...}
inventory_map = frappe._dict()
for row in rows:
item_code = row.rm_item_code if hasattr(row, "rm_item_code") else row.item_code
if not item_code:
continue
if inventory_map.get(item_code):
continue
item_defaults = get_item_defaults(item_code, company)
if item_defaults.default_inventory_account:
inventory_map[item_code] = frappe._dict(
{
"account": item_defaults.default_inventory_account,
"account_currency": item_defaults.inventory_account_currency,
}
)
if not inventory_map.get(item_code):
item_group_defaults = get_item_group_defaults(item_code, company)
if item_group_defaults.default_inventory_account:
inventory_map[item_code] = frappe._dict(
{
"account": item_group_defaults.default_inventory_account,
"account_currency": item_group_defaults.inventory_account_currency,
}
)
if not inventory_map.get(item_code):
brand_defaults = get_brand_defaults(item_code, company)
if brand_defaults.default_inventory_account:
inventory_map[item_code] = frappe._dict(
{
"account": brand_defaults.default_inventory_account,
"account_currency": brand_defaults.inventory_account_currency,
}
)
return inventory_map

View File

@@ -0,0 +1,509 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import copy
from collections import defaultdict
import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import cint
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
class TestItemWiseInventoryAccount(IntegrationTestCase):
def setUp(self):
self.company = make_company()
self.company_abbr = frappe.db.get_value("Company", self.company, "abbr")
self.default_warehouse = frappe.db.get_value(
"Warehouse",
{"company": self.company, "is_group": 0, "warehouse_name": ("like", "%Stores%")},
)
def test_item_account_for_purchase_receipt_entry(self):
items = {
"Stock Item A": {"is_stock_item": 1},
"Stock Item B": {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SER-TT-.####"},
}
for item_name, item_data in items.items():
item = make_item(
item_name,
properties=item_data,
)
account = self.add_inventory_account(item)
items[item_name]["account"] = account
pr = make_purchase_receipt(
item_code="Stock Item A",
qty=5,
rate=100,
warehouse=self.default_warehouse,
company=self.company,
do_not_submit=True,
)
pr.append(
"items",
{
"item_code": "Stock Item B",
"qty": 2,
"rate": 200,
"warehouse": self.default_warehouse,
},
)
pr.submit()
for row in items:
item_code = row
account = items[item_code]["account"]
sle_value = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "item_code": item_code},
"stock_value_difference",
)
gl_value = frappe.db.get_value(
"GL Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"account": account,
},
"debit",
)
self.assertEqual(sle_value, gl_value, f"GL Entry not created for {item_code} correctly")
def test_item_account_for_delivery_note_entry(self):
items = {
"Stock Item A": {"is_stock_item": 1},
"Stock Item B": {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SER-TT-.####"},
}
for item_name, item_data in items.items():
item = make_item(
item_name,
properties=item_data,
)
account = self.add_inventory_account(item)
items[item_name]["account"] = account
pr = make_purchase_receipt(
item_code="Stock Item A",
qty=5,
rate=100,
warehouse=self.default_warehouse,
company=self.company,
do_not_submit=True,
)
pr.append(
"items",
{
"item_code": "Stock Item B",
"qty": 2,
"rate": 200,
"warehouse": self.default_warehouse,
},
)
pr.submit()
dn = create_delivery_note(
item_code="Stock Item A",
qty=5,
rate=200,
warehouse=self.default_warehouse,
company=self.company,
cost_center=frappe.db.get_value("Company", self.company, "cost_center"),
expense_account=frappe.db.get_value("Company", self.company, "default_expense_account"),
do_not_submit=True,
)
dn.append(
"items",
{
"item_code": "Stock Item B",
"qty": 2,
"rate": 300,
"warehouse": self.default_warehouse,
},
)
dn.submit()
for row in items:
item_code = row
account = items[item_code]["account"]
sle_value = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": item_code},
"stock_value_difference",
)
gl_value = (
frappe.db.get_value(
"GL Entry",
{
"voucher_type": "Delivery Note",
"voucher_no": dn.name,
"account": account,
},
"credit",
)
* -1
)
self.assertEqual(sle_value, gl_value, f"GL Entry not created for {item_code} correctly")
def test_item_group_account_for_purchase_receipt_entry(self):
items = {
"Stock Item C": {"is_stock_item": 1, "item_group": "Test Item Group C"},
"Stock Item C1": {"is_stock_item": 1, "item_group": "Test Item Group C", "qty": 3, "rate": 150},
"Stock Item D": {
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "SER-TT-.####",
"item_group": "Test Item Group D",
"qty": 2,
"rate": 250,
},
"Stock Item D1": {"is_stock_item": 1, "item_group": "Test Item Group D", "qty": 4, "rate": 300},
}
for row in items:
self.make_item_group(items[row]["item_group"])
inventory_account_dict = frappe._dict()
for item_name, item_data in items.items():
item_data = frappe._dict(item_data)
make_item(
item_name,
properties=item_data,
)
item_group = frappe.get_doc("Item Group", item_data.item_group)
account = self.add_inventory_account(item_group, "item_group_defaults")
inventory_account_dict[item_data.item_group] = account
pr = make_purchase_receipt(
item_code="Stock Item C",
qty=5,
rate=100,
warehouse=self.default_warehouse,
company=self.company,
do_not_submit=True,
)
for item_code, values in items.items():
if item_code == "Stock Item C":
continue
pr.append(
"items",
{
"item_code": item_code,
"qty": values.get("qty", 1),
"rate": values.get("rate", 200),
"warehouse": self.default_warehouse,
},
)
pr.submit()
for item_group, account in inventory_account_dict.items():
items = frappe.get_all(
"Item",
filters={"item_group": item_group},
pluck="name",
)
sle_value = frappe.get_all(
"Stock Ledger Entry",
filters={
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"item_code": ("in", items),
},
fields=["sum(stock_value_difference) as value"],
)
gl_value = frappe.db.get_value(
"GL Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": pr.name,
"account": account,
},
"debit",
)
self.assertEqual(sle_value[0].value, gl_value, f"GL Entry not created for {item_code} correctly")
def test_item_group_account_for_delivery_note_entry(self):
items = {
"Stock Item E": {"is_stock_item": 1, "item_group": "Test Item Group E"},
"Stock Item E1": {"is_stock_item": 1, "item_group": "Test Item Group E", "qty": 3, "rate": 150},
"Stock Item F": {
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "SER-TT-.####",
"item_group": "Test Item Group F",
"qty": 2,
"rate": 250,
},
"Stock Item F1": {"is_stock_item": 1, "item_group": "Test Item Group F", "qty": 4, "rate": 300},
}
for row in items:
self.make_item_group(items[row]["item_group"])
inventory_account_dict = frappe._dict()
for item_name, item_data in items.items():
item_data = frappe._dict(item_data)
make_item(
item_name,
properties=item_data,
)
item_group = frappe.get_doc("Item Group", item_data.item_group)
account = self.add_inventory_account(item_group, "item_group_defaults")
inventory_account_dict[item_data.item_group] = account
pr = make_purchase_receipt(
item_code="Stock Item E",
qty=5,
rate=100,
warehouse=self.default_warehouse,
company=self.company,
do_not_submit=True,
)
for item_code, values in items.items():
if item_code == "Stock Item E":
continue
pr.append(
"items",
{
"item_code": item_code,
"qty": values.get("qty", 1),
"rate": values.get("rate", 200),
"warehouse": self.default_warehouse,
},
)
pr.submit()
dn = create_delivery_note(
item_code="Stock Item E",
qty=5,
rate=200,
warehouse=self.default_warehouse,
company=self.company,
cost_center=frappe.db.get_value("Company", self.company, "cost_center"),
expense_account=frappe.db.get_value("Company", self.company, "default_expense_account"),
do_not_submit=True,
)
for item_code, values in items.items():
if item_code == "Stock Item E":
continue
dn.append(
"items",
{
"item_code": item_code,
"qty": values.get("qty", 1),
"rate": values.get("rate", 200),
"warehouse": self.default_warehouse,
},
)
dn.submit()
for item_group, account in inventory_account_dict.items():
items = frappe.get_all(
"Item",
filters={"item_group": item_group},
pluck="name",
)
sle_value = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_type": "Delivery Note", "voucher_no": dn.name, "item_code": ("in", items)},
fields=["sum(stock_value_difference) as value"],
)
gl_value = (
frappe.db.get_value(
"GL Entry",
{
"voucher_type": "Delivery Note",
"voucher_no": dn.name,
"account": account,
},
"credit",
)
* -1
)
self.assertEqual(sle_value[0].value, gl_value, f"GL Entry not created for {item_code} correctly")
def make_item_group(self, item_name):
if not frappe.db.exists("Item Group", item_name):
item_group = frappe.get_doc(
{
"doctype": "Item Group",
"item_group_name": item_name,
"is_group": 0,
}
)
item_group.insert()
return item_group
return frappe.get_doc("Item Group", item_name)
def add_inventory_account(self, item, table_name=None):
if not table_name:
table_name = "item_defaults"
account = item.name + " - " + self.company_abbr
if not frappe.db.exists("Account", account):
account_doc = frappe.get_doc(
{
"doctype": "Account",
"account_name": item.name,
"account_type": "Stock",
"company": self.company,
"is_group": 0,
"parent_account": "Stock Assets - " + self.company_abbr,
}
)
account_doc.insert()
if not frappe.db.get_value("Item Default", {"parent": item.name, "company": self.company}):
item.append(
table_name,
{
"company": self.company,
"default_inventory_account": account,
"default_warehouse": self.default_warehouse,
},
)
item.save()
return account
def test_item_account_for_manufacture_entry(self):
items = {
"Stock Item A1": {"is_stock_item": 1},
"Stock Item B1": {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SER-TT-.####"},
}
for item_name, item_data in items.items():
item = make_item(
item_name,
properties=item_data,
)
account = self.add_inventory_account(item)
items[item_name]["account"] = account
make_purchase_receipt(
item_code="Stock Item B1",
qty=5,
rate=100,
warehouse=self.default_warehouse,
company=self.company,
)
bom = make_bom(
item="Stock Item A1",
company=self.company,
source_warehouse=self.default_warehouse,
raw_materials=["Stock Item B1"],
)
wip_warehouse = frappe.db.get_value(
"Warehouse",
{"company": self.company, "is_group": 0, "warehouse_name": ("like", "%Work In Progress%")},
)
fg_warehouse = frappe.db.get_value(
"Warehouse",
{"company": self.company, "is_group": 0, "warehouse_name": ("like", "%Finished Goods%")},
)
wo_order = make_wo_order_test_record(
item="Stock Item A1",
qty=5,
company=self.company,
source_warehouse=self.default_warehouse,
bom=bom.name,
wip_warehouse=wip_warehouse,
fg_warehouse=fg_warehouse,
)
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 5))
stock_entry.submit()
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5))
stock_entry.submit()
for row in stock_entry.items:
item_code = row.item_code
account = items[item_code]["account"]
sle_value = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Stock Entry", "voucher_no": stock_entry.name, "item_code": item_code},
"stock_value_difference",
)
field = "debit" if row.t_warehouse == fg_warehouse else "credit"
gl_value = frappe.db.get_value(
"GL Entry",
{
"voucher_type": "Stock Entry",
"voucher_no": stock_entry.name,
"account": account,
},
field,
)
if row.s_warehouse:
gl_value = gl_value * -1
self.assertEqual(sle_value, gl_value, f"GL Entry not created for {item_code} correctly")
def make_company():
company = "_Test Company for Item Wise Inventory Account"
if frappe.db.exists("Company", company):
return company
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": "_Test Company for Item Wise Inventory Account",
"abbr": "_TCIWIA",
"default_currency": "INR",
"country": "India",
"enable_perpetual_inventory": 1,
"enable_item_wise_inventory_account": 1,
}
).insert()
return company.name

View File

@@ -114,10 +114,11 @@
"stock_tab",
"auto_accounting_for_stock_settings",
"enable_perpetual_inventory",
"enable_item_wise_inventory_account",
"enable_provisional_accounting_for_non_stock_items",
"default_inventory_account",
"stock_adjustment_account",
"column_break_32",
"stock_adjustment_account",
"stock_received_but_not_billed",
"default_provisional_account",
"default_in_transit_warehouse",
@@ -877,6 +878,13 @@
"fieldtype": "Link",
"label": "Service Expense Account",
"options": "Account"
},
{
"default": "0",
"description": "If enabled, the system will use the inventory account set in the Item Master or Item Group or Brand. Otherwise, it will use the inventory account set in the Warehouse.",
"fieldname": "enable_item_wise_inventory_account",
"fieldtype": "Check",
"label": "Enable Item-wise Inventory Account"
}
],
"icon": "fa fa-building",
@@ -884,7 +892,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2025-10-10 15:12:37.941251",
"modified": "2025-10-23 13:15:52.411984",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@@ -6,7 +6,7 @@ import json
import frappe
import frappe.defaults
from frappe import _
from frappe import _, bold
from frappe.cache_manager import clear_defaults_cache
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
@@ -73,6 +73,7 @@ class Company(NestedSet):
disposal_account: DF.Link | None
domain: DF.Data | None
email: DF.Data | None
enable_item_wise_inventory_account: DF.Check
enable_perpetual_inventory: DF.Check
enable_provisional_accounting_for_non_stock_items: DF.Check
exception_budget_approver_role: DF.Link | None
@@ -158,6 +159,24 @@ class Company(NestedSet):
self.set_chart_of_accounts()
self.validate_parent_company()
self.set_reporting_currency()
self.validate_inventory_account_settings()
def validate_inventory_account_settings(self):
doc_before_save = self.get_doc_before_save()
if not doc_before_save:
return
if (
doc_before_save.enable_item_wise_inventory_account != self.enable_item_wise_inventory_account
and frappe.db.get_value("Stock Ledger Entry", {"is_cancelled": 0, "company": self.name}, "name")
and doc_before_save.enable_perpetual_inventory
):
frappe.throw(
_(
"Cannot enable Item-wise Inventory Account, as there are existing Stock Ledger Entries for the company {0} with Warehouse-wise Inventory Account. Please cancel the stock transactions first and try again."
).format(bold(self.name)),
title=_("Cannot Change Inventory Account Setting"),
)
def validate_abbr(self):
if not self.abbr:
@@ -455,6 +474,22 @@ class Company(NestedSet):
_("Set default inventory account for perpetual inventory"), alert=True, indicator="orange"
)
doc_before_save = self.get_doc_before_save()
if not doc_before_save:
return
if (
doc_before_save.enable_perpetual_inventory
and not self.enable_perpetual_inventory
and doc_before_save.enable_item_wise_inventory_account != self.enable_item_wise_inventory_account
):
if frappe.db.get_value("Stock Ledger Entry", {"is_cancelled": 0, "company": self.name}, "name"):
frappe.throw(
_(
"Cannot disable perpetual inventory, as there are existing Stock Ledger Entries for the company {0}. Please cancel the stock transactions first and try again."
).format(bold(self.name))
)
def validate_provisional_account_for_non_stock_items(self):
if not self.get("__islocal"):
if (

View File

@@ -509,6 +509,17 @@ $.extend(erpnext.item, {
};
});
});
frm.set_query("default_inventory_account", "item_defaults", (doc, cdt, cdn) => {
let row = locals[cdt][cdn];
return {
filters: {
is_group: 0,
company: row.company,
account_type: "Stock",
},
};
});
},
make_dashboard: function (frm) {

View File

@@ -10,6 +10,8 @@
"column_break_3",
"default_price_list",
"default_discount_account",
"default_inventory_account",
"inventory_account_currency",
"purchase_defaults",
"buying_cost_center",
"default_supplier",
@@ -168,11 +170,25 @@
"fieldtype": "Link",
"label": "Purchase Expense Contra Account",
"options": "Account"
},
{
"fieldname": "default_inventory_account",
"fieldtype": "Link",
"label": "Default Inventory Account",
"options": "Account"
},
{
"fetch_from": "default_inventory_account.account_currency",
"fieldname": "inventory_account_currency",
"fieldtype": "Link",
"label": "Inventory Account Currency",
"options": "Currency",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2025-10-01 19:17:33.687836",
"modified": "2025-10-21 10:50:46.144721",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Default",

View File

@@ -18,6 +18,7 @@ class ItemDefault(Document):
company: DF.Link
default_cogs_account: DF.Link | None
default_discount_account: DF.Link | None
default_inventory_account: DF.Link | None
default_price_list: DF.Link | None
default_provisional_account: DF.Link | None
default_supplier: DF.Link | None
@@ -26,6 +27,7 @@ class ItemDefault(Document):
deferred_revenue_account: DF.Link | None
expense_account: DF.Link | None
income_account: DF.Link | None
inventory_account_currency: DF.Link | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -472,19 +472,19 @@ class PurchaseReceipt(BuyingController):
for item in self.items:
item.amount_difference_with_purchase_invoice = 0
def get_gl_entries(self, warehouse_account=None, via_landed_cost_voucher=False):
def get_gl_entries(self, inventory_account_map=None, via_landed_cost_voucher=False):
from erpnext.accounts.general_ledger import process_gl_map
gl_entries = []
self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account)
self.make_item_gl_entries(gl_entries, inventory_account_map=inventory_account_map)
self.make_tax_gl_entries(gl_entries, via_landed_cost_voucher)
self.set_gl_entry_for_purchase_expense(gl_entries)
update_regional_gl_entries(gl_entries, self)
return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
def make_item_gl_entries(self, gl_entries, warehouse_account=None):
def make_item_gl_entries(self, gl_entries, inventory_account_map=None):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
get_purchase_document_details,
)
@@ -526,9 +526,11 @@ class PurchaseReceipt(BuyingController):
):
return 0.0
account = (
warehouse_account[item.from_warehouse]["account"] if item.from_warehouse else stock_asset_rbnb
)
account = stock_asset_rbnb
if item.from_warehouse:
_inv_dict = self.get_inventory_account_dict(item, inventory_account_map, "from_warehouse")
account = _inv_dict["account"]
account_currency = get_account_currency(account)
# GL Entry for from warehouse or Stock Received but not billed
@@ -653,7 +655,7 @@ class PurchaseReceipt(BuyingController):
def make_sub_contracting_gl_entries(item):
# sub-contracting warehouse
if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
if flt(item.rm_supp_cost) and supplier_warehouse_account:
self.add_gl_entry(
gl_entries=gl_entries,
account=supplier_warehouse_account,
@@ -748,22 +750,25 @@ class PurchaseReceipt(BuyingController):
stock_value_diff = (
flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount)
)
elif warehouse_account.get(d.warehouse):
elif inventory_account := self.get_inventory_account_dict(d, inventory_account_map):
stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse)
stock_asset_account_name = warehouse_account[d.warehouse]["account"]
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get(
"account"
)
supplier_warehouse_account_currency = warehouse_account.get(
self.supplier_warehouse, {}
).get("account_currency")
stock_asset_account_name = inventory_account["account"]
supplier_warehouse_account = None
supplier_warehouse_account_currency = None
if self.supplier_warehouse:
if _inv_dict := self.get_inventory_account_dict(
d, inventory_account_map, "supplier_warehouse"
):
supplier_warehouse_account = _inv_dict["account"]
supplier_warehouse_account_currency = _inv_dict["account_currency"]
# If PR is sub-contracted and fg item rate is zero
# in that case if account for source and target warehouse are same,
# then GL entries should not be posted
if (
flt(stock_value_diff) == flt(d.rm_supp_cost)
and warehouse_account.get(self.supplier_warehouse)
and supplier_warehouse_account
and stock_asset_account_name == supplier_warehouse_account
):
continue
@@ -795,7 +800,9 @@ class PurchaseReceipt(BuyingController):
)
stock_value_diff = get_stock_value_difference(self.name, d.name, d.rejected_warehouse)
stock_asset_account_name = warehouse_account[d.rejected_warehouse]["account"]
_inv_dict = self.get_inventory_account_dict(d, inventory_account_map, "rejected_warehouse")
stock_asset_account_name = _inv_dict["account"]
make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
if not d.qty:

View File

@@ -1150,7 +1150,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-10-14 12:58:20.384056",
"modified": "2025-10-21 10:39:32.659933",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@@ -1644,8 +1644,8 @@ class StockEntry(StockController, SubcontractingInwardController):
sl_entries.append(sle)
def get_gl_entries(self, warehouse_account):
gl_entries = super().get_gl_entries(warehouse_account)
def get_gl_entries(self, inventory_account_map):
gl_entries = super().get_gl_entries(inventory_account_map)
if self.purpose in ("Repack", "Manufacture"):
total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
@@ -1720,11 +1720,11 @@ class StockEntry(StockController, SubcontractingInwardController):
)
)
self.set_gl_entries_for_landed_cost_voucher(gl_entries, warehouse_account)
self.set_gl_entries_for_landed_cost_voucher(gl_entries, inventory_account_map)
return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
def set_gl_entries_for_landed_cost_voucher(self, gl_entries, warehouse_account):
def set_gl_entries_for_landed_cost_voucher(self, gl_entries, inventory_account_map):
landed_cost_entries = self.get_item_account_wise_lcv_entries()
if not landed_cost_entries:
return
@@ -1742,11 +1742,12 @@ class StockEntry(StockController, SubcontractingInwardController):
else flt(amount["amount"])
)
_inv_dict = self.get_inventory_account_dict(item, inventory_account_map, "t_warehouse")
gl_entries.append(
self.get_gl_dict(
{
"account": account,
"against": warehouse_account.get(item.t_warehouse)["account"],
"against": _inv_dict["account"],
"cost_center": item.cost_center,
"debit": 0.0,
"credit": credit_amount,
@@ -1766,7 +1767,7 @@ class StockEntry(StockController, SubcontractingInwardController):
self.get_gl_dict(
{
"account": item.expense_account,
"against": warehouse_account.get(item.t_warehouse)["account"],
"against": _inv_dict["account"],
"cost_center": item.cost_center,
"debit": 0.0,
"credit": credit_amount * -1,

View File

@@ -944,11 +944,11 @@ class StockReconciliation(StockController):
new_sl_entries.extend(merge_similar_entries.values())
return new_sl_entries
def get_gl_entries(self, warehouse_account=None):
def get_gl_entries(self, inventory_account_map=None):
if not self.cost_center:
msgprint(_("Please enter Cost Center"), raise_exception=1)
return super().get_gl_entries(warehouse_account, self.expense_account, self.cost_center)
return super().get_gl_entries(inventory_account_map, self.expense_account, self.cost_center)
def validate_expense_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):

View File

@@ -545,7 +545,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-05-06 02:39:24.284587",
"modified": "2025-10-23 13:16:10.527190",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",

View File

@@ -872,6 +872,18 @@ def get_default_income_account(ctx: ItemDetailsCtx, item, item_group, brand):
)
def get_default_inventory_account(ctx: ItemDetailsCtx, item, item_group, brand):
if not frappe.get_cached_value("Company", ctx.company, "enable_item_wise_inventory_account"):
return None
return (
ctx.inventory_account
or item.get("default_inventory_account")
or item_group.get("default_inventory_account")
or brand.get("default_inventory_account")
)
def get_default_expense_account(ctx: ItemDetailsCtx, item, item_group, brand):
if ctx.get("doctype") in ["Sales Invoice", "Delivery Note"]:
expense_account = (

View File

@@ -596,19 +596,19 @@ class SubcontractingReceipt(SubcontractingController):
"Subcontracting Receipt", self.name, "status", status, update_modified=update_modified
)
def get_gl_entries(self, warehouse_account=None):
def get_gl_entries(self, inventory_account_map=None):
from erpnext.accounts.general_ledger import process_gl_map
if not erpnext.is_perpetual_inventory_enabled(self.company):
return []
gl_entries = []
self.make_item_gl_entries(gl_entries, warehouse_account)
self.make_item_gl_entries_for_lcv(gl_entries, warehouse_account)
self.make_item_gl_entries(gl_entries, inventory_account_map)
self.make_item_gl_entries_for_lcv(gl_entries, inventory_account_map)
return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
def make_item_gl_entries(self, gl_entries, warehouse_account=None):
def make_item_gl_entries(self, gl_entries, inventory_account_map=None):
warehouse_with_no_account = []
supplied_items_details = frappe._dict()
@@ -625,7 +625,9 @@ class SubcontractingReceipt(SubcontractingController):
for item in self.items:
if flt(item.rate) and flt(item.qty):
if warehouse_account.get(item.warehouse):
_inv_dict = self.get_inventory_account_dict(item, inventory_account_map)
if _inv_dict.get("account"):
stock_value_diff = frappe.db.get_value(
"Stock Ledger Entry",
{
@@ -638,22 +640,18 @@ class SubcontractingReceipt(SubcontractingController):
"stock_value_difference",
)
accepted_warehouse_account = warehouse_account[item.warehouse]["account"]
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get(
"account"
)
remarks = self.get("remarks") or _("Accounting Entry for Stock")
# Accepted Warehouse Account (Debit)
self.add_gl_entry(
gl_entries=gl_entries,
account=accepted_warehouse_account,
account=_inv_dict["account"],
cost_center=item.cost_center,
debit=stock_value_diff,
credit=0.0,
remarks=remarks,
against_account=item.expense_account,
account_currency=get_account_currency(accepted_warehouse_account),
account_currency=_inv_dict["account_currency"],
project=item.project,
item=item,
)
@@ -669,7 +667,7 @@ class SubcontractingReceipt(SubcontractingController):
debit=0.0,
credit=flt(stock_value_diff) - service_cost,
remarks=remarks,
against_account=accepted_warehouse_account,
against_account=_inv_dict["account"],
account_currency=get_account_currency(item.expense_account),
project=item.project,
item=item,
@@ -684,24 +682,28 @@ class SubcontractingReceipt(SubcontractingController):
debit=0.0,
credit=service_cost,
remarks=remarks,
against_account=accepted_warehouse_account,
against_account=_inv_dict["account"],
account_currency=get_account_currency(service_account),
project=item.project,
item=item,
)
if flt(item.rm_supp_cost) and supplier_warehouse_account:
if flt(item.rm_supp_cost):
for rm_item in supplied_items_details.get(item.name):
_inv_dict = self.get_inventory_account_dict(
rm_item, inventory_account_map, "supplier_warehouse"
)
# Supplier Warehouse Account (Credit)
self.add_gl_entry(
gl_entries=gl_entries,
account=supplier_warehouse_account,
account=_inv_dict.get("account"),
cost_center=rm_item.cost_center or item.cost_center,
debit=0.0,
credit=flt(rm_item.amount),
remarks=remarks,
against_account=rm_item.expense_account or item.expense_account,
account_currency=get_account_currency(supplier_warehouse_account),
account_currency=_inv_dict.get("account_currency"),
project=item.project,
item=item,
)
@@ -713,7 +715,7 @@ class SubcontractingReceipt(SubcontractingController):
debit=flt(rm_item.amount),
credit=0.0,
remarks=remarks,
against_account=supplier_warehouse_account,
against_account=_inv_dict.get("account"),
account_currency=get_account_currency(item.expense_account),
project=item.project,
item=item,
@@ -795,7 +797,7 @@ class SubcontractingReceipt(SubcontractingController):
+ "\n".join(warehouse_with_no_account)
)
def make_item_gl_entries_for_lcv(self, gl_entries, warehouse_account):
def make_item_gl_entries_for_lcv(self, gl_entries, inventory_account_map):
landed_cost_entries = self.get_item_account_wise_lcv_entries()
if not landed_cost_entries:
@@ -805,6 +807,8 @@ class SubcontractingReceipt(SubcontractingController):
if item.landed_cost_voucher_amount and landed_cost_entries:
remarks = _("Accounting Entry for Landed Cost Voucher for SCR {0}").format(self.name)
if (item.item_code, item.name) in landed_cost_entries:
_inv_dict = self.get_inventory_account_dict(item, inventory_account_map)
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
account_currency = get_account_currency(account)
credit_amount = (
@@ -820,7 +824,7 @@ class SubcontractingReceipt(SubcontractingController):
debit=0.0,
credit=credit_amount,
remarks=remarks,
against_account=warehouse_account.get(item.warehouse)["account"],
against_account=_inv_dict["account"],
credit_in_account_currency=flt(amount["amount"]),
account_currency=account_currency,
project=item.project,
@@ -837,7 +841,7 @@ class SubcontractingReceipt(SubcontractingController):
debit=0.0,
credit=credit_amount * -1,
remarks=remarks,
against_account=warehouse_account.get(item.warehouse)["account"],
against_account=_inv_dict["account"],
debit_in_account_currency=flt(amount["amount"]),
account_currency=account_currency,
project=item.project,