diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index d1f75903457..1e5314edf2e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -884,6 +884,7 @@ class PurchaseInvoice(BuyingController): self.make_write_off_gl_entry(gl_entries) self.make_gle_for_rounding_adjustment(gl_entries) self.set_transaction_currency_and_rate_in_gl_map(gl_entries) + self.set_gl_entry_for_purchase_expense(gl_entries) return gl_entries def check_asset_cwip_enabled(self): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 16cc900be2b..8d27668f7c0 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -17,7 +17,7 @@ from erpnext.accounts.party import get_party_details from erpnext.buying.utils import update_last_purchase_rate, validate_for_items from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.subcontracting_controller import SubcontractingController -from erpnext.stock.get_item_details import get_conversion_factor +from erpnext.stock.get_item_details import get_conversion_factor, get_item_defaults from erpnext.stock.utils import get_incoming_rate @@ -307,6 +307,60 @@ class BuyingController(SubcontractingController): address_display_field, render_address(self.get(address_field), check_permissions=False) ) + def set_gl_entry_for_purchase_expense(self, gl_entries): + 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) + ) + + amount = flt(row.valuation_rate * row.stock_qty, row.precision("base_amount")) + self.add_gl_entry( + gl_entries=gl_entries, + account=details.purchase_expense_account, + cost_center=row.cost_center, + debit=amount, + credit=0.0, + remarks=_("Purchase Expense for Item {0}").format(row.item_code), + against_account=details.purchase_expense_contra_account, + account_currency=frappe.get_cached_value( + "Account", details.purchase_expense_account, "account_currency" + ), + item=row, + ) + + self.add_gl_entry( + gl_entries=gl_entries, + account=details.purchase_expense_contra_account, + cost_center=row.cost_center, + debit=0.0, + credit=amount, + remarks=_("Purchase Expense for Item {0}").format(row.item_code), + against_account=details.purchase_expense_account, + account_currency=frappe.get_cached_value( + "Account", details.purchase_expense_contra_account, "account_currency" + ), + item=row, + ) + def set_total_in_words(self): from frappe.utils import money_in_words @@ -1171,3 +1225,33 @@ def validate_item_type(doc, fieldname, message): @erpnext.allow_regional def update_regional_item_valuation_rate(doc): pass + + +@frappe.request_cache +def get_purchase_expense_account(item_code, company): + defaults = get_item_defaults(item_code, company) + + details = frappe._dict( + { + "purchase_expense_account": defaults.get("purchase_expense_account"), + "purchase_expense_contra_account": defaults.get("purchase_expense_contra_account"), + } + ) + + if not details.purchase_expense_account: + details = frappe.db.get_value( + "Item Default", + {"parent": defaults.item_group, "company": company}, + ["purchase_expense_account", "purchase_expense_contra_account"], + as_dict=1, + ) or frappe._dict({}) + + if not details.purchase_expense_account: + details = frappe.db.get_value( + "Item Default", + {"parent": defaults.brand, "company": company}, + ["purchase_expense_account", "purchase_expense_contra_account"], + as_dict=1, + ) + + return details or frappe._dict({}) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 8b6329da031..1035fa3fb58 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -278,6 +278,8 @@ erpnext.company.setup_queries = function (frm) { ["depreciation_expense_account", { root_type: "Expense", account_type: "Depreciation" }], ["disposal_account", { report_type: "Profit and Loss" }], ["default_inventory_account", { account_type: "Stock" }], + ["purchase_expense_account", { root_type: "Expense" }], + ["purchase_expense_contra_account", { root_type: "Expense" }], ["cost_center", {}], ["round_off_cost_center", {}], ["depreciation_cost_center", {}], diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 54611d6479d..8d713600ba7 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -106,6 +106,10 @@ "default_warehouse_for_sales_return", "credit_limit", "transactions_annual_history", + "purchase_expense_section", + "purchase_expense_account", + "column_break_ereg", + "purchase_expense_contra_account", "stock_tab", "auto_accounting_for_stock_settings", "enable_perpetual_inventory", @@ -844,6 +848,27 @@ "options": "Currency", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "purchase_expense_section", + "fieldtype": "Section Break", + "label": "Purchase Expense" + }, + { + "fieldname": "column_break_ereg", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_expense_account", + "fieldtype": "Link", + "label": "Purchase Expense Account", + "options": "Account" + }, + { + "fieldname": "purchase_expense_contra_account", + "fieldtype": "Link", + "label": "Purchase Expense Contra Account", + "options": "Account" } ], "icon": "fa fa-building", @@ -851,7 +876,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2025-08-25 18:34:03.602046", + "modified": "2025-10-01 17:34:10.971627", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 160748b5e69..56f88c215ae 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -86,6 +86,8 @@ class Company(NestedSet): parent_company: DF.Link | None payment_terms: DF.Link | None phone_no: DF.Data | None + purchase_expense_account: DF.Link | None + purchase_expense_contra_account: DF.Link | None reconcile_on_advance_payment_date: DF.Check reconciliation_takes_effect_on: DF.Literal[ "Advance Payment Date", "Oldest Of Invoice Or Advance", "Reconciliation Date" diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 15ca9230d42..1e0e2d4bbd9 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -488,6 +488,21 @@ $.extend(erpnext.item, { }, }; }); + + let fields = ["purchase_expense_account", "purchase_expense_contra_account", "default_cogs_account"]; + + fields.forEach((field) => { + frm.set_query(field, "item_defaults", (doc, cdt, cdn) => { + let row = locals[cdt][cdn]; + return { + filters: { + company: row.company, + root_type: "Expense", + is_group: 0, + }, + }; + }); + }); }, make_dashboard: function (frm) { diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 5a17a7e399c..e9f49cfabca 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -16,20 +16,22 @@ "item_name", "item_group", "stock_uom", + "opening_stock", + "valuation_rate", + "standard_rate", "column_break0", "disabled", "allow_alternative_item", "is_stock_item", "has_variants", - "opening_stock", - "valuation_rate", - "standard_rate", "is_fixed_asset", "auto_create_assets", "is_grouped_asset", "asset_category", "asset_naming_series", + "section_break_znra", "over_delivery_receipt_allowance", + "column_break_wugd", "over_billing_allowance", "image", "section_break_11", @@ -63,6 +65,8 @@ "column_break_37", "has_serial_no", "serial_no_series", + "defaults_tab", + "item_defaults", "variants_section", "variant_of", "variant_based_on", @@ -74,8 +78,6 @@ "column_break_9s9o", "enable_deferred_revenue", "no_of_months", - "section_break_avcp", - "item_defaults", "purchasing_tab", "purchase_uom", "min_order_qty", @@ -887,10 +889,6 @@ "fieldname": "column_break_9s9o", "fieldtype": "Column Break" }, - { - "fieldname": "section_break_avcp", - "fieldtype": "Section Break" - }, { "collapsible": 1, "fieldname": "deferred_accounting_section", @@ -936,6 +934,19 @@ "fieldname": "production_capacity", "fieldtype": "Int", "label": "Production Capacity" + }, + { + "fieldname": "defaults_tab", + "fieldtype": "Tab Break", + "label": "Defaults" + }, + { + "fieldname": "section_break_znra", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_wugd", + "fieldtype": "Column Break" } ], "icon": "fa fa-tag", @@ -943,7 +954,7 @@ "image_field": "image", "links": [], "make_attachments_public": 1, - "modified": "2025-08-14 23:35:56.293048", + "modified": "2025-10-01 16:58:40.946604", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 51b38e30fc1..a4e9684062b 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -305,6 +305,7 @@ class TestItem(IntegrationTestCase): "company": "_Test Company", "default_warehouse": "_Test Warehouse 2 - _TC", # no override "expense_account": "_Test Account Stock Expenses - _TC", # override brand default + "default_cogs_account": "_Test Account Cost for Goods Sold - _TC", # override brand default "buying_cost_center": "_Test Write Off Cost Center - _TC", # override item group default } ], @@ -315,7 +316,7 @@ class TestItem(IntegrationTestCase): "item_code": "Test Item With Defaults", "warehouse": "_Test Warehouse 2 - _TC", # from item "income_account": "_Test Account Sales - _TC", # from brand - "expense_account": "_Test Account Stock Expenses - _TC", # from item + "expense_account": "_Test Account Cost for Goods Sold - _TC", # from item "cost_center": "_Test Cost Center 2 - _TC", # from item group } sales_item_details = get_item_details( diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json index bc452fd3848..67e511225f7 100644 --- a/erpnext/stock/doctype/item_default/item_default.json +++ b/erpnext/stock/doctype/item_default/item_default.json @@ -16,10 +16,15 @@ "column_break_8", "expense_account", "default_provisional_account", + "column_break_cpif", + "purchase_expense_account", + "purchase_expense_contra_account", "selling_defaults", "selling_cost_center", "column_break_12", "income_account", + "cost_of_good_sold_section", + "default_cogs_account", "deferred_accounting_defaults_section", "deferred_expense_account", "column_break_kwad", @@ -111,7 +116,7 @@ { "fieldname": "default_provisional_account", "fieldtype": "Link", - "label": "Default Provisional Account", + "label": "Default Provisional Account (Service)", "options": "Account" }, { @@ -136,19 +141,47 @@ { "fieldname": "column_break_kwad", "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_cpif", + "fieldtype": "Column Break" + }, + { + "fieldname": "cost_of_good_sold_section", + "fieldtype": "Section Break", + "label": "Cost of Goods Sold" + }, + { + "fieldname": "default_cogs_account", + "fieldtype": "Link", + "label": "Default COGS Account", + "options": "Account" + }, + { + "fieldname": "purchase_expense_account", + "fieldtype": "Link", + "label": "Purchase Expense Account", + "options": "Account" + }, + { + "fieldname": "purchase_expense_contra_account", + "fieldtype": "Link", + "label": "Purchase Expense Contra Account", + "options": "Account" } ], "istable": 1, "links": [], - "modified": "2025-03-17 13:46:09.719105", + "modified": "2025-10-01 19:17:33.687836", "modified_by": "Administrator", "module": "Stock", "name": "Item Default", "owner": "Administrator", "permissions": [], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/item_default/item_default.py b/erpnext/stock/doctype/item_default/item_default.py index 10dae2bc1c0..b24036da420 100644 --- a/erpnext/stock/doctype/item_default/item_default.py +++ b/erpnext/stock/doctype/item_default/item_default.py @@ -16,6 +16,7 @@ class ItemDefault(Document): buying_cost_center: DF.Link | None company: DF.Link + default_cogs_account: DF.Link | None default_discount_account: DF.Link | None default_price_list: DF.Link | None default_provisional_account: DF.Link | None @@ -28,6 +29,8 @@ class ItemDefault(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + purchase_expense_account: DF.Link | None + purchase_expense_contra_account: DF.Link | None selling_cost_center: DF.Link | None # end: auto-generated types diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 6400c826245..6765305b70c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -479,6 +479,7 @@ class PurchaseReceipt(BuyingController): self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account) 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) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d5a808171a6..539f538e775 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4415,6 +4415,69 @@ class TestPurchaseReceipt(IntegrationTestCase): self.assertEqual(srbnb_cost, 1000) + def test_purchase_expense_account(self): + item = "Test Item with Purchase Expense Account" + make_item(item, {"is_stock_item": 1}) + company = "_Test Company with perpetual inventory" + + expense_account = "_Test Account Purchase Expense - TCP1" + expense_contra_account = "_Test Account Purchase Contra Expense - TCP1" + if not frappe.db.exists("Account", expense_account): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "_Test Account Purchase Expense", + "parent_account": "Stock Expenses - TCP1", + "company": company, + "is_group": 0, + "root_type": "Expense", + } + ).insert() + + if not frappe.db.exists("Account", expense_contra_account): + frappe.get_doc( + { + "doctype": "Account", + "account_name": "_Test Account Purchase Contra Expense", + "parent_account": "Stock Expenses - TCP1", + "company": company, + "is_group": 0, + "root_type": "Expense", + } + ).insert() + + item_doc = frappe.get_doc("Item", item) + item_doc.append( + "item_defaults", + { + "company": company, + "default_warehouse": "Stores - TCP1", + "purchase_expense_account": expense_account, + "purchase_expense_contra_account": expense_contra_account, + }, + ) + + item_doc.save() + + pr = make_purchase_receipt( + item_code=item, + qty=10, + rate=100, + company=company, + warehouse="Stores - TCP1", + ) + + gl_entries = get_gl_entries(pr.doctype, pr.name) + accounts = [d.account for d in gl_entries] + self.assertTrue(expense_account in accounts) + self.assertTrue(expense_contra_account in accounts) + + for row in gl_entries: + if row.account == expense_account: + self.assertEqual(row.debit, 1000) + if row.account == expense_contra_account: + self.assertEqual(row.credit, 1000) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 985e51d11b8..0f1cdafc118 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -868,6 +868,19 @@ def get_default_income_account(ctx: ItemDetailsCtx, item, item_group, brand): def get_default_expense_account(ctx: ItemDetailsCtx, item, item_group, brand): + if ctx.get("doctype") in ["Sales Invoice", "Delivery Note"]: + expense_account = ( + item.get("default_cogs_account") + or item_group.get("default_cogs_account") + or brand.get("default_cogs_account") + ) + + if not expense_account: + expense_account = frappe.get_cached_value("Company", ctx.company, "default_expense_account") + + if expense_account: + return expense_account + return ( item.get("expense_account") or item_group.get("expense_account")