diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index c7febc5b889..3605d60a0fc 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -355,7 +355,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 @@ -364,6 +371,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/stock/services/base_stock_gl_composer.py b/erpnext/stock/services/base_stock_gl_composer.py index 944b9e70a8e..1d5a80ef10d 100644 --- a/erpnext/stock/services/base_stock_gl_composer.py +++ b/erpnext/stock/services/base_stock_gl_composer.py @@ -196,6 +196,12 @@ class BaseStockGLComposer(BaseGLComposer): 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 + doc = self.doc fields = ("expenses_added_to_stock_account", "expenses_added_to_stock_contra_account") details = get_expenses_added_to_stock_accounts(item_code, doc.company) diff --git a/erpnext/stock/tests/test_expenses_added_to_stock.py b/erpnext/stock/tests/test_expenses_added_to_stock.py index 7ddc0ec4bc8..c1ca84a4e69 100644 --- a/erpnext/stock/tests/test_expenses_added_to_stock.py +++ b/erpnext/stock/tests/test_expenses_added_to_stock.py @@ -152,3 +152,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)