diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 01967042673..88bb4b29a84 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -356,7 +356,14 @@ class BuyingController(SubcontractingController): if self.doctype == "Purchase Invoice" and not self.update_stock: return + stock_items = self.get_stock_items() + for row in self.items: + # A service item holds no stock value, so there is nothing to book against it - and it + # must not make the expense accounts mandatory either. + if row.item_code not in stock_items: + continue + details = self.get_validated_purchase_expense_details(row.item_code) if not details: continue @@ -365,6 +372,10 @@ class BuyingController(SubcontractingController): if row.landed_cost_voucher_amount: amount -= flt(row.landed_cost_voucher_amount, row.precision("base_amount")) + if not amount: + # GL Entry rejects a row with neither a debit nor a credit. + continue + 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 c6435795443..7a292b078b6 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -897,6 +897,12 @@ class StockController(AccountsController): 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): + # A service item holds no stock value, so there is nothing to book against it - and it must + # not make the expense accounts mandatory either. A zero pair would be rejected by GL Entry + # anyway, which needs a debit or a credit on every row. + if not amount or not frappe.get_cached_value("Item", item_code, "is_stock_item"): + return + fields = ("expenses_added_to_stock_account", "expenses_added_to_stock_contra_account") details = get_expenses_added_to_stock_accounts(item_code, self.company) diff --git a/erpnext/stock/tests/test_expenses_added_to_stock.py b/erpnext/stock/tests/test_expenses_added_to_stock.py index 3a464cb968a..75170494875 100644 --- a/erpnext/stock/tests/test_expenses_added_to_stock.py +++ b/erpnext/stock/tests/test_expenses_added_to_stock.py @@ -174,3 +174,38 @@ class TestExpensesAddedToStock(ERPNextTestSuite): rate=100, company=COMPANY, ) + + def test_service_item_books_nothing_on_purchase_invoice_with_update_stock(self): + """A service item carries no stock value, so booking it produced a GL row with neither a + debit nor a credit, which GL Entry rejects outright.""" + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + + service_item = make_item(properties={"is_stock_item": 0}).name + + pi = make_purchase_invoice( + company=COMPANY, + warehouse=WAREHOUSE, + item_code=service_item, + qty=1, + rate=500, + update_stock=1, + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) + + self.assertEqual(pi.docstatus, 1) + + _balances, debits, credits = self.get_gl_balances("Purchase Invoice", pi.name) + self.assertEqual(debits[self.eats_account], 0) + self.assertEqual(credits[self.eats_contra_account], 0) + + booked = frappe.get_all( + "GL Entry", + filters={ + "voucher_type": "Purchase Invoice", + "voucher_no": pi.name, + "is_cancelled": 0, + "account": self.purchase_expense_account, + }, + ) + self.assertFalse(booked)