diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 27806857fd6..3bc183151c0 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -21,6 +21,8 @@ "enable_common_party_accounting", "allow_multi_currency_invoices_against_single_party_account", "confirm_before_resetting_posting_date", + "stock_expense_section", + "book_stock_expense_gl_entries", "analytics_section", "enable_discounts_and_margin", "enable_accounting_dimensions", @@ -766,6 +768,18 @@ "description": "Changing the account in any transaction of the DocTypes listed below will trigger a repost. To prevent reposting, remove the relevant DocType from the list.", "fieldname": "column_break_mfor", "fieldtype": "Column Break" + }, + { + "fieldname": "stock_expense_section", + "fieldtype": "Section Break", + "label": "Stock Expense Accounting" + }, + { + "default": "0", + "description": "Books Purchase Expense and Expenses Added To Stock account pairs against stock value. On enabling this, the accounts become mandatory in Company or Item Defaults for Purchase Receipt, Purchase Invoice, Stock Entry, Stock Reconciliation and Landed Cost Voucher", + "fieldname": "book_stock_expense_gl_entries", + "fieldtype": "Check", + "label": "Book Stock Expense GL Entries" } ], "grid_page_length": 50, @@ -774,7 +788,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-06-24 12:59:41.868865", + "modified": "2026-07-27 12:00:00.000000", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index f1b9aaa2f87..0497429e6d2 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -62,6 +62,7 @@ class AccountsSettings(Document): book_asset_depreciation_entry_automatically: DF.Check book_deferred_entries_based_on: DF.Literal["Days", "Months"] book_deferred_entries_via_journal_entry: DF.Check + book_stock_expense_gl_entries: DF.Check book_tax_discount_loss: DF.Check calculate_depr_using_total_days: DF.Check check_supplier_invoice_uniqueness: DF.Check diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index cad62820d03..01967042673 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -331,32 +331,40 @@ class BuyingController(SubcontractingController): address_display_field, render_address(self.get(address_field), check_permissions=False) ) + def get_validated_purchase_expense_details(self, item_code): + fields = ("purchase_expense_account", "purchase_expense_contra_account") + details = get_purchase_expense_account(item_code, self.company) + + for field in fields: + if not details.get(field): + details[field] = frappe.get_cached_value("Company", self.company, field) + + for field in fields: + if not details.get(field): + frappe.throw( + _("Please set {0} in Company {1} or in the Item Defaults of Item {2}").format( + frappe.bold(_(frappe.unscrub(field))), self.company, item_code + ) + ) + + return details + def set_gl_entry_for_purchase_expense(self, gl_entries): + if not cint(frappe.db.get_single_value("Accounts Settings", "book_stock_expense_gl_entries")): + return + if self.doctype == "Purchase Invoice" and not self.update_stock: return for row in self.items: - details = get_purchase_expense_account(row.item_code, self.company) - - if not details.purchase_expense_account: - details.purchase_expense_account = frappe.get_cached_value( - "Company", self.company, "purchase_expense_account" - ) - - if not details.purchase_expense_account: - return - - if not details.purchase_expense_contra_account: - details.purchase_expense_contra_account = frappe.get_cached_value( - "Company", self.company, "purchase_expense_contra_account" - ) - - if not details.purchase_expense_contra_account: - frappe.throw( - _("Please set Purchase Expense Contra Account in Company {0}").format(self.company) - ) + details = self.get_validated_purchase_expense_details(row.item_code) + if not details: + continue amount = flt(row.valuation_rate * row.stock_qty, row.precision("base_amount")) + if row.landed_cost_voucher_amount: + amount -= flt(row.landed_cost_voucher_amount, row.precision("base_amount")) + self.add_gl_entry( gl_entries=gl_entries, account=details.purchase_expense_account, diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 3e9803888f0..c6435795443 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -84,6 +84,11 @@ def stock_entry_row_requires_inspection(purpose, row): class StockController(AccountsController): + #: Vouchers whose stock value change should also be booked to the Expenses Added To Stock + #: account pair (Stock Entry, Stock Reconciliation). Purchase Receipt books its own, against + #: the landed cost amount rather than the stock value difference. + book_expenses_added_to_stock = False + def validate(self): super().validate() @@ -858,10 +863,84 @@ class StockController(AccountsController): ).format(wh, self.company) ) + if self.book_expenses_added_to_stock: + self.append_expenses_added_to_stock_entries(gl_list, voucher_details, sle_map) + return process_gl_map( gl_list, precision=precision, from_repost=frappe.flags.through_repost_item_valuation ) + def book_stock_expense_enabled(self): + if not hasattr(self, "_book_stock_expense_enabled"): + self._book_stock_expense_enabled = cint( + frappe.db.get_single_value("Accounts Settings", "book_stock_expense_gl_entries") + ) + + return self._book_stock_expense_enabled + + def append_expenses_added_to_stock_entries(self, gl_list, voucher_details, sle_map): + if not self.book_stock_expense_enabled(): + return + + precision = self.get_debit_field_precision() + + for item_row in voucher_details: + sle_list = sle_map.get(item_row.name) + if not sle_list: + continue + + amount = flt(sum(flt(sle.stock_value_difference) for sle in sle_list), precision) + if not amount: + continue + + item_code = item_row.get("item_code") or sle_list[0].item_code + self.append_expenses_added_to_stock_pair(gl_list, item_code, amount, item_row) + + def append_expenses_added_to_stock_pair(self, gl_list, item_code, amount, item_row): + fields = ("expenses_added_to_stock_account", "expenses_added_to_stock_contra_account") + details = get_expenses_added_to_stock_accounts(item_code, self.company) + + for field in fields: + if not details.get(field): + frappe.throw( + _("Please set {0} in Company {1} or in the Item Defaults of Item {2}").format( + frappe.bold(_(frappe.unscrub(field))), self.company, item_code + ) + ) + + cost_center = item_row.get("cost_center") or frappe.get_cached_value( + "Company", self.company, "cost_center" + ) + remarks = _("Expenses Added To Stock for Item {0}").format(item_code) + common_args = { + "cost_center": cost_center, + "project": item_row.get("project") or self.get("project"), + "remarks": remarks, + } + + gl_list.append( + self.get_gl_dict( + { + "account": details.expenses_added_to_stock_account, + "against": details.expenses_added_to_stock_contra_account, + "debit": amount, + **common_args, + }, + item=item_row, + ) + ) + gl_list.append( + self.get_gl_dict( + { + "account": details.expenses_added_to_stock_contra_account, + "against": details.expenses_added_to_stock_account, + "debit": -1 * amount, + **common_args, + }, + item=item_row, + ) + ) + def get_debit_field_precision(self): if not frappe.flags.debit_field_precision: frappe.flags.debit_field_precision = frappe.get_precision("GL Entry", "debit_in_account_currency") @@ -2568,3 +2647,31 @@ def get_item_wise_inventory_account_map(rows, company): ) return inventory_map + + +@frappe.request_cache +def get_expenses_added_to_stock_accounts(item_code, company): + """Resolves the Expenses Added To Stock account pair for an item, falling back through + Item Defaults -> Item Group -> Brand -> Company.""" + from erpnext.stock.doctype.item.item import get_item_defaults + + fields = ["expenses_added_to_stock_account", "expenses_added_to_stock_contra_account"] + defaults = get_item_defaults(item_code, company) + + details = frappe._dict({field: defaults.get(field) for field in fields}) + + if not details.expenses_added_to_stock_account: + details = frappe.db.get_value( + "Item Default", {"parent": defaults.item_group, "company": company}, fields, as_dict=1 + ) or frappe._dict({}) + + if not details.expenses_added_to_stock_account and defaults.get("brand"): + details = frappe.db.get_value( + "Item Default", {"parent": defaults.brand, "company": company}, fields, as_dict=1 + ) or frappe._dict({}) + + for field in fields: + if not details.get(field): + details[field] = frappe.get_cached_value("Company", company, field) + + return details diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d012b859b7c..7e2567e26e3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -491,4 +491,5 @@ erpnext.patches.v15_0.backfill_sla_link_filters_on_docfield erpnext.patches.v16_0.crm_settings_handle_allowed_users_for_frappe_crm erpnext.patches.v16_0.backfill_pick_list_transferred_qty erpnext.patches.v16_0.access_control_for_project_users +erpnext.patches.v16_0.enable_book_stock_expense_gl_entries erpnext.patches.v16_0.rename_ar_ap_ageing_filter diff --git a/erpnext/patches/v16_0/enable_book_stock_expense_gl_entries.py b/erpnext/patches/v16_0/enable_book_stock_expense_gl_entries.py new file mode 100644 index 00000000000..c21add4e073 --- /dev/null +++ b/erpnext/patches/v16_0/enable_book_stock_expense_gl_entries.py @@ -0,0 +1,10 @@ +import frappe + + +def execute(): + has_expense_accounts = frappe.db.exists( + "Company", {"purchase_expense_account": ("is", "set")} + ) or frappe.db.exists("Item Default", {"purchase_expense_account": ("is", "set")}) + + if has_expense_accounts: + frappe.db.set_single_value("Accounts Settings", "book_stock_expense_gl_entries", 1) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 417d83acb1b..1ac0cf1e34f 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -307,6 +307,8 @@ erpnext.company.setup_queries = function (frm) { ["default_advance_received_account", { root_type: "Liability", account_type: "Receivable" }], ["default_advance_paid_account", { root_type: "Asset", account_type: "Payable" }], ["service_expense_account", { root_type: "Expense" }], + ["expenses_added_to_stock_account", { root_type: "Expense" }], + ["expenses_added_to_stock_contra_account", { root_type: "Expense" }], ], function (i, v) { erpnext.company.set_custom_query(frm, v); diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 353cd799415..74e0c78ecc2 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -117,6 +117,10 @@ "service_expense_account", "column_break_ereg", "purchase_expense_contra_account", + "stock_expense_section", + "expenses_added_to_stock_account", + "column_break_gthb", + "expenses_added_to_stock_contra_account", "stock_tab", "auto_accounting_for_stock_settings", "enable_perpetual_inventory", @@ -824,6 +828,27 @@ "fieldtype": "Tab Break", "label": "Buying and Selling" }, + { + "fieldname": "stock_expense_section", + "fieldtype": "Section Break", + "label": "Stock Expense" + }, + { + "fieldname": "expenses_added_to_stock_account", + "fieldtype": "Link", + "label": "Expenses Added To Stock Account", + "options": "Account" + }, + { + "fieldname": "column_break_gthb", + "fieldtype": "Column Break" + }, + { + "fieldname": "expenses_added_to_stock_contra_account", + "fieldtype": "Link", + "label": "Expenses Added To Stock Contra Account", + "options": "Account" + }, { "fieldname": "stock_tab", "fieldtype": "Tab Break", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 7ac095a6fdd..20c7142b9a9 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -98,6 +98,8 @@ class Company(NestedSet): exception_budget_approver_role: DF.Link | None exchange_gain_loss_account: DF.Link | None existing_company: DF.Link | None + expenses_added_to_stock_account: DF.Link | None + expenses_added_to_stock_contra_account: DF.Link | None fax: DF.Data | None is_group: DF.Check lft: DF.Int diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index c37e35d2114..d7fde5f6961 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -620,7 +620,13 @@ $.extend(erpnext.item, { }; }); - let fields = ["purchase_expense_account", "purchase_expense_contra_account", "default_cogs_account"]; + let fields = [ + "purchase_expense_account", + "purchase_expense_contra_account", + "expenses_added_to_stock_account", + "expenses_added_to_stock_contra_account", + "default_cogs_account", + ]; fields.forEach((field) => { frm.set_query(field, "item_defaults", (doc, cdt, cdn) => { diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json index b00644d51f2..b9709275be0 100644 --- a/erpnext/stock/doctype/item_default/item_default.json +++ b/erpnext/stock/doctype/item_default/item_default.json @@ -21,6 +21,8 @@ "column_break_cpif", "purchase_expense_account", "purchase_expense_contra_account", + "expenses_added_to_stock_account", + "expenses_added_to_stock_contra_account", "selling_defaults", "selling_cost_center", "column_break_12", @@ -192,6 +194,22 @@ "label": "Purchase Expense Contra Account", "options": "Account" }, + { + "description": "Account to track value added to stock via Stock Entry, Stock Reconciliation or Landed Cost Voucher", + "fieldname": "expenses_added_to_stock_account", + "fieldtype": "Link", + "label": "Expenses Added To Stock Account", + "options": "Account", + "show_description_on_click": 1 + }, + { + "description": "Used to balance the books when recording expenses added to stock", + "fieldname": "expenses_added_to_stock_contra_account", + "fieldtype": "Link", + "label": "Expenses Added To Stock Contra Account", + "options": "Account", + "show_description_on_click": 1 + }, { "description": "Stock account where inventory value for this item will be tracked", "fieldname": "default_inventory_account", @@ -211,7 +229,7 @@ ], "istable": 1, "links": [], - "modified": "2026-04-27 01:49:01.396845", + "modified": "2026-07-27 12:00:00.000000", "modified_by": "Administrator", "module": "Stock", "name": "Item Default", diff --git a/erpnext/stock/doctype/item_default/item_default.py b/erpnext/stock/doctype/item_default/item_default.py index 10651d260c6..c2937dbc950 100644 --- a/erpnext/stock/doctype/item_default/item_default.py +++ b/erpnext/stock/doctype/item_default/item_default.py @@ -26,6 +26,8 @@ class ItemDefault(Document): deferred_expense_account: DF.Link | None deferred_revenue_account: DF.Link | None expense_account: DF.Link | None + expenses_added_to_stock_account: DF.Link | None + expenses_added_to_stock_contra_account: DF.Link | None income_account: DF.Link | None inventory_account_currency: DF.Link | None parent: DF.Data diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 90d4dfab63c..c40631a6d82 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -652,6 +652,14 @@ class PurchaseReceipt(BuyingController): item=item, ) + def make_expenses_added_to_stock_entries(item): + if not self.book_stock_expense_enabled(): + return + + amount = flt(item.landed_cost_voucher_amount, item.precision("base_net_amount")) + if amount and not item.is_fixed_asset: + self.append_expenses_added_to_stock_pair(gl_entries, item.item_code, amount, item) + def make_amount_difference_entry(item): if item.amount_difference_with_purchase_invoice and stock_asset_rbnb: account_currency = get_account_currency(stock_asset_rbnb) @@ -796,6 +804,7 @@ class PurchaseReceipt(BuyingController): make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name) outgoing_amount = make_stock_received_but_not_billed_entry(d) make_landed_cost_gl_entries(d) + make_expenses_added_to_stock_entries(d) make_amount_difference_entry(d) make_sub_contracting_gl_entries(d) make_divisional_loss_gl_entry(d, outgoing_amount) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index f5aedd15de2..eb867ff1f96 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -5125,6 +5125,14 @@ class TestPurchaseReceipt(ERPNextTestSuite): self.assertEqual(srbnb_cost, 1000) def test_purchase_expense_account(self): + # Single, so it outlives this test - every later Purchase Receipt / Invoice would otherwise + # be forced to resolve the expense account pair and throw for unconfigured companies. + previous = frappe.db.get_single_value("Accounts Settings", "book_stock_expense_gl_entries") + self.addCleanup( + frappe.db.set_single_value, "Accounts Settings", "book_stock_expense_gl_entries", previous + ) + frappe.db.set_single_value("Accounts Settings", "book_stock_expense_gl_entries", 1) + item = "Test Item with Purchase Expense Account" make_item(item, {"is_stock_item": 1}) company = "_Test Company with perpetual inventory" diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e050429eb98..50ce29b29f4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -176,6 +176,8 @@ class StockEntry(StockController, SubcontractingInwardController): work_order: DF.Link | None # end: auto-generated types + book_expenses_added_to_stock = True + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.status_updater = [ diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index aa94b808988..bd789e9785e 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -57,6 +57,8 @@ class StockReconciliation(StockController): set_warehouse: DF.Link | None # end: auto-generated types + book_expenses_added_to_stock = True + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"] diff --git a/erpnext/stock/tests/test_expenses_added_to_stock.py b/erpnext/stock/tests/test_expenses_added_to_stock.py new file mode 100644 index 00000000000..3a464cb968a --- /dev/null +++ b/erpnext/stock/tests/test_expenses_added_to_stock.py @@ -0,0 +1,176 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.tests.utils import ERPNextTestSuite + +COMPANY = "_Test Company with perpetual inventory" +WAREHOUSE = "Stores - TCP1" + + +class TestExpensesAddedToStock(ERPNextTestSuite): + def setUp(self): + self.restore_stock_expense_settings() + self.eats_account = create_account( + account_name="Expenses Added To Stock", + parent_account="Expenses - TCP1", + company=COMPANY, + ) + self.eats_contra_account = create_account( + account_name="Expenses Added To Stock Contra", + parent_account="Expenses - TCP1", + company=COMPANY, + ) + self.purchase_expense_account = create_account( + account_name="Test Purchase Expense EATS", + parent_account="Expenses - TCP1", + company=COMPANY, + ) + self.purchase_expense_contra_account = create_account( + account_name="Test Purchase Expense Contra EATS", + parent_account="Expenses - TCP1", + company=COMPANY, + ) + frappe.db.set_value( + "Company", + COMPANY, + { + "expenses_added_to_stock_account": self.eats_account, + "expenses_added_to_stock_contra_account": self.eats_contra_account, + "purchase_expense_account": self.purchase_expense_account, + "purchase_expense_contra_account": self.purchase_expense_contra_account, + }, + ) + frappe.db.set_single_value("Accounts Settings", "book_stock_expense_gl_entries", 1) + self.item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name + + def restore_stock_expense_settings(self): + """These are a Single and Company fields, so they outlive the test. Left set, every later + Purchase Receipt / Invoice in the run has to resolve the expense account pair and throws + for any company that has none configured.""" + account_fields = [ + "expenses_added_to_stock_account", + "expenses_added_to_stock_contra_account", + "purchase_expense_account", + "purchase_expense_contra_account", + ] + previous_accounts = frappe.db.get_value("Company", COMPANY, account_fields, as_dict=True) + previous_flag = frappe.db.get_single_value("Accounts Settings", "book_stock_expense_gl_entries") + + self.addCleanup(frappe.db.set_value, "Company", COMPANY, dict(previous_accounts)) + self.addCleanup( + frappe.db.set_single_value, + "Accounts Settings", + "book_stock_expense_gl_entries", + previous_flag, + ) + + def get_gl_balances(self, voucher_type, voucher_no): + entries = frappe.get_all( + "GL Entry", + filters={ + "voucher_type": voucher_type, + "voucher_no": voucher_no, + "is_cancelled": 0, + "account": ("in", [self.eats_account, self.eats_contra_account]), + }, + fields=["account", "debit", "credit"], + ) + + balances = frappe._dict({self.eats_account: 0.0, self.eats_contra_account: 0.0}) + debits = frappe._dict({self.eats_account: 0.0, self.eats_contra_account: 0.0}) + credits = frappe._dict({self.eats_account: 0.0, self.eats_contra_account: 0.0}) + for entry in entries: + balances[entry.account] += entry.debit - entry.credit + debits[entry.account] += entry.debit + credits[entry.account] += entry.credit + + return balances, debits, credits + + def test_material_receipt_books_expenses_added_to_stock(self): + se = make_stock_entry(item_code=self.item, to_warehouse=WAREHOUSE, qty=10, rate=100, company=COMPANY) + + _balances, debits, credits = self.get_gl_balances("Stock Entry", se.name) + self.assertEqual(debits[self.eats_account], 1000) + self.assertEqual(credits[self.eats_contra_account], 1000) + + def test_material_issue_books_reverse_pair(self): + make_stock_entry(item_code=self.item, to_warehouse=WAREHOUSE, qty=10, rate=100, company=COMPANY) + se = make_stock_entry(item_code=self.item, from_warehouse=WAREHOUSE, qty=5, company=COMPANY) + + _balances, debits, credits = self.get_gl_balances("Stock Entry", se.name) + self.assertEqual(credits[self.eats_account], 500) + self.assertEqual(debits[self.eats_contra_account], 500) + + def test_material_transfer_books_nothing(self): + make_stock_entry(item_code=self.item, to_warehouse=WAREHOUSE, qty=10, rate=100, company=COMPANY) + se = make_stock_entry( + item_code=self.item, + from_warehouse=WAREHOUSE, + to_warehouse="Finished Goods - TCP1", + qty=5, + company=COMPANY, + ) + + _balances, debits, credits = self.get_gl_balances("Stock Entry", se.name) + self.assertEqual(debits[self.eats_account], 0) + self.assertEqual(credits[self.eats_account], 0) + + def test_stock_reconciliation_books_pair(self): + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + + make_stock_entry(item_code=self.item, to_warehouse=WAREHOUSE, qty=10, rate=100, company=COMPANY) + sr = create_stock_reconciliation( + item_code=self.item, warehouse=WAREHOUSE, qty=15, rate=100, company=COMPANY + ) + + _balances, debits, credits = self.get_gl_balances("Stock Reconciliation", sr.name) + self.assertEqual(debits[self.eats_account], 500) + self.assertEqual(credits[self.eats_contra_account], 500) + + def test_landed_cost_voucher_books_pair(self): + from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( + create_landed_cost_voucher, + ) + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + pr = make_purchase_receipt( + company=COMPANY, warehouse=WAREHOUSE, item_code=self.item, qty=10, rate=100 + ) + + _balances, debits, credits = self.get_gl_balances("Purchase Receipt", pr.name) + self.assertEqual(debits[self.eats_account], 0) + + create_landed_cost_voucher("Purchase Receipt", pr.name, COMPANY, charges=200) + + _balances, debits, credits = self.get_gl_balances("Purchase Receipt", pr.name) + self.assertEqual(debits[self.eats_account], 200) + self.assertEqual(credits[self.eats_contra_account], 200) + + def test_no_entries_when_feature_disabled(self): + frappe.db.set_single_value("Accounts Settings", "book_stock_expense_gl_entries", 0) + + se = make_stock_entry(item_code=self.item, to_warehouse=WAREHOUSE, qty=10, rate=100, company=COMPANY) + + _balances, debits, credits = self.get_gl_balances("Stock Entry", se.name) + self.assertEqual(debits[self.eats_account], 0) + self.assertEqual(credits[self.eats_contra_account], 0) + + def test_missing_contra_account_raises_when_feature_enabled(self): + frappe.db.set_value("Company", COMPANY, "expenses_added_to_stock_contra_account", None) + + self.assertRaises( + frappe.ValidationError, + make_stock_entry, + item_code=self.item, + to_warehouse=WAREHOUSE, + qty=10, + rate=100, + company=COMPANY, + )