feat(stock): add configurable Stock Delivered But Not Billed (SDBNB) support (#56070)

* feat: add company setting to enable Stock Delivered But Not Billed accounting

* test: add tests for Stock Delivered But Not Billed account config

* fix(company): skip outstanding SDBNB validation when no previous config exists

* test: add dedicated company fixture for SDBNB tests

* test: use SDBNB company for Sales Invoice SDBNB test

---------

Co-authored-by: Pugazhendhi Velu <pugazhendhi720@gmail.com>
Co-authored-by: Pugazhendhi Velu <126157273+PugazhendhiVelu@users.noreply.github.com>
This commit is contained in:
Kavin
2026-07-02 13:34:25 +05:30
committed by GitHub
parent c98ca6d2cc
commit 7248053c6a
12 changed files with 198 additions and 21 deletions

View File

@@ -339,7 +339,7 @@ erpnext.company.setup_queries = function (frm) {
],
[
"stock_delivered_but_not_billed",
{ root_type: "Liability", account_type: "Stock Delivered But Not Billed" },
{ root_type: "Asset", account_type: "Stock Delivered But Not Billed" },
],
[
"service_received_but_not_billed",

View File

@@ -132,6 +132,7 @@
"default_purchase_price_variance_account",
"default_manufacturing_variance_account",
"stock_received_but_not_billed",
"enable_stock_delivered_but_not_billed",
"stock_delivered_but_not_billed",
"disable_sdbnb_in_sr",
"default_provisional_account",
@@ -1048,18 +1049,28 @@
},
{
"default": "0",
"depends_on": "enable_stock_delivered_but_not_billed",
"fieldname": "disable_sdbnb_in_sr",
"fieldtype": "Check",
"label": "Disable Stock Delivered But Not Billed in Sales Return",
"no_copy": 1
},
{
"depends_on": "enable_stock_delivered_but_not_billed",
"fieldname": "stock_delivered_but_not_billed",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Stock Delivered But Not Billed",
"mandatory_depends_on": "enable_stock_delivered_but_not_billed",
"no_copy": 1,
"options": "Account"
},
{
"default": "0",
"description": "If enabled, the value of goods delivered before invoicing will be recorded in the Stock Delivered But Not Billed account.",
"fieldname": "enable_stock_delivered_but_not_billed",
"fieldtype": "Check",
"label": "Enable Stock Delivered But Not Billed"
}
],
"grid_page_length": 50,

View File

@@ -100,6 +100,7 @@ class Company(NestedSet):
enable_item_wise_inventory_account: DF.Check
enable_perpetual_inventory: DF.Check
enable_provisional_accounting_for_non_stock_items: DF.Check
enable_stock_delivered_but_not_billed: DF.Check
exception_budget_approver_role: DF.Link | None
exchange_gain_loss_account: DF.Link | None
existing_company: DF.Link | None
@@ -186,6 +187,64 @@ class Company(NestedSet):
self.validate_inventory_account_settings()
self.cant_change_valuation_method()
self.validate_pending_reposts(old_doc)
self.validate_sdbnb_configuration()
def validate_outstanding_sdbnb_transactions(self, account):
GLEntry = frappe.qb.DocType("GL Entry")
DeliveryNote = frappe.qb.DocType("Delivery Note")
delivery_notes = (
frappe.qb.from_(GLEntry)
.join(DeliveryNote)
.on((GLEntry.voucher_type == "Delivery Note") & (GLEntry.voucher_no == DeliveryNote.name))
.select(DeliveryNote.name)
.where(
(GLEntry.is_cancelled == 0)
& (GLEntry.company == self.name)
& (GLEntry.account == account)
& (DeliveryNote.per_billed < 100)
& (DeliveryNote.docstatus == 1)
& (DeliveryNote.status.isin(["To Bill", "Partially Billed"]))
)
.distinct()
.run(pluck=True)
)
if delivery_notes:
dn_links = ", ".join(get_link_to_form("Delivery Note", dn) for dn in delivery_notes[:10])
frappe.throw(
_(
"Stock Delivered But Not Billed Account cannot be changed or disabled since account {0} contains outstanding Delivery Notes: {1}"
).format(
bold(account),
dn_links,
)
)
def validate_sdbnb_configuration(self):
if self.get("__islocal"):
return
if self.enable_stock_delivered_but_not_billed and not self.stock_delivered_but_not_billed:
frappe.throw(_("Please select Stock Delivered But Not Billed Account"))
doc_before_save = self.get_doc_before_save()
if not (doc_before_save and doc_before_save.stock_delivered_but_not_billed):
return
account_changed = (
self.stock_delivered_but_not_billed != doc_before_save.stock_delivered_but_not_billed
)
feature_disabled = (
doc_before_save.enable_stock_delivered_but_not_billed
and not self.enable_stock_delivered_but_not_billed
)
if account_changed or feature_disabled:
self.validate_outstanding_sdbnb_transactions(doc_before_save.stock_delivered_but_not_billed)
def cant_change_valuation_method(self):
doc_before_save = self.get_doc_before_save()

View File

@@ -10,7 +10,11 @@ from frappe.utils import random_string
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import (
get_charts_for_country,
)
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.setup.doctype.company.company import get_default_company_address
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.stock_entry.test_stock_entry import make_stock_entry
from erpnext.tests.utils import ERPNextTestSuite
@@ -234,6 +238,44 @@ class TestCompany(ERPNextTestSuite):
after = get_all_transactions_annual_history(company).get(key, 0)
self.assertEqual(after - before, 2)
def test_sdbnb_validation_requires_account_when_enabled(self):
company = get_test_company()
company.enable_stock_delivered_but_not_billed = 1
company.stock_delivered_but_not_billed = None
with self.assertRaises(frappe.ValidationError):
company.save()
def test_disable_sdbnb_with_outstanding_delivery_note_fails(self):
company = get_test_company()
item_code = create_stock_item_with_inventory()
create_outstanding_delivery_note(item_code)
company.enable_stock_delivered_but_not_billed = 0
with self.assertRaises(frappe.ValidationError):
company.save()
def test_cannot_change_sdbnb_account_with_outstanding_delivery_note(self):
company = get_test_company()
item_code = create_stock_item_with_inventory()
create_outstanding_delivery_note(item_code)
new_account = create_account(
account_name="Stock Delivered But Not Billed - New",
account_type="Stock Delivered But Not Billed",
parent_account="Stock Assets - _TSDBNB",
company=company.name,
)
company.stock_delivered_but_not_billed = new_account
with self.assertRaises(frappe.ValidationError):
company.save()
def test_demo_data(self):
from erpnext.setup.demo import clear_demo_data, setup_demo_data
@@ -297,3 +339,49 @@ def create_test_lead_in_company(company):
lead.company = company
lead.save()
return lead.name
def get_test_company():
if frappe.db.exists("Company", "_Test SDBNB Company"):
return frappe.get_doc("Company", "_Test SDBNB Company")
return frappe.get_doc(
{
"doctype": "Company",
"company_name": "_Test SDBNB Company",
"abbr": "_TSDBNB",
"country": "India",
"default_currency": "INR",
"enable_perpetual_inventory": 1,
"enable_stock_delivered_but_not_billed": 1,
}
).insert()
def create_stock_item_with_inventory():
item_code = make_item(
"SDBNB Test Item",
properties={"is_stock_item": 1},
).name
make_stock_entry(
item_code=item_code,
target="Stores - _TSDBNB",
qty=10,
basic_rate=100,
company="_Test SDBNB Company",
)
return item_code
def create_outstanding_delivery_note(item_code):
return create_delivery_note(
item_code=item_code,
qty=5,
rate=150,
company="_Test SDBNB Company",
warehouse="Stores - _TSDBNB",
cost_center="Main - _TSDBNB",
expense_account="Stock Delivered But Not Billed - _TSDBNB",
)

View File

@@ -223,5 +223,17 @@
"doctype": "Company",
"chart_of_accounts": "Standard",
"create_chart_of_accounts_based_on": "Standard Template"
},
{
"abbr": "_TSDBNB",
"company_name": "_Test SDBNB Company",
"country": "India",
"default_currency": "INR",
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 1,
"enable_stock_delivered_but_not_billed": 1
}
]
]