test: add unit test cases for Stock Delivered But Not Billed

This commit is contained in:
kavin-114
2026-03-22 22:30:19 +05:30
parent 05877140d1
commit 6ee7dc0b49

View File

@@ -10,6 +10,7 @@ from frappe.utils import add_days, cstr, flt, getdate, nowdate, nowtime, today
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.utils import get_balance_on
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
@@ -45,8 +46,35 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestDeliveryNote(ERPNextTestSuite):
SDBNB_COMPANY_NAME = "_Test SDBNB Company"
SDBNB_COMPANY_ABBR = "_TSDBNB"
def setUp(self):
self.load_test_records("Stock Entry")
self.setup_sdbnb_company()
def setup_sdbnb_company(self):
if frappe.db.exists("Company", self.SDBNB_COMPANY_NAME):
company = frappe.get_doc("Company", self.SDBNB_COMPANY_NAME)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": self.SDBNB_COMPANY_NAME,
"abbr": self.SDBNB_COMPANY_ABBR,
"country": "India",
"default_currency": "INR",
"enable_perpetual_inventory": 1,
}
).insert()
self.sdbnb_company = company.name
self.sdbnb_account = company.stock_delivered_but_not_billed
self.sdbnb_cost_center = company.cost_center
self.sdbnb_warehouse = f"Stores - {self.SDBNB_COMPANY_ABBR}"
self.sdbnb_expense_account = f"Cost of Goods Sold - {self.SDBNB_COMPANY_ABBR}"
self.sdbnb_income_account = f"Sales - {self.SDBNB_COMPANY_ABBR}"
self.sdbnb_debit_to = f"Debtors - {self.SDBNB_COMPANY_ABBR}"
def test_delivery_note_qty(self):
dn = create_delivery_note(qty=0, do_not_save=True)
@@ -2865,6 +2893,478 @@ class TestDeliveryNote(ERPNextTestSuite):
for entry in sabb.entries:
self.assertEqual(entry.incoming_rate, 200)
def test_sdbnb_gl_entry_on_delivery_note(self):
"""Test that DN GL entries use SDBNB account when configured on the company."""
item_code = make_item("SDBNB Test Item", properties={"is_stock_item": 1}).name
make_stock_entry(
item_code=item_code,
target=self.sdbnb_warehouse,
qty=10,
basic_rate=100,
company=self.sdbnb_company,
)
dn = create_delivery_note(
item_code=item_code,
qty=5,
rate=150,
company=self.sdbnb_company,
warehouse=self.sdbnb_warehouse,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
)
# DN expense_account should be overridden to SDBNB
dn.reload()
self.assertEqual(dn.items[0].expense_account, self.sdbnb_account)
# Verify DN GL entries use SDBNB account (not COGS)
gl_entries = get_gl_entries("Delivery Note", dn.name)
self.assertTrue(gl_entries)
stock_in_hand_account = get_inventory_account(self.sdbnb_company)
expected_values = {
self.sdbnb_account: {"debit": True},
stock_in_hand_account: {"credit": True},
}
for gle in gl_entries:
self.assertIn(gle.account, expected_values)
if expected_values[gle.account].get("debit"):
self.assertGreater(gle.debit, 0)
if expected_values[gle.account].get("credit"):
self.assertGreater(gle.credit, 0)
def test_sdbnb_reversal_on_sales_invoice(self):
"""Test that SI created from DN reverses SDBNB entries (credits SDBNB, debits COGS)."""
item_code = make_item("SDBNB Reversal Test Item", properties={"is_stock_item": 1}).name
make_stock_entry(
item_code=item_code,
target=self.sdbnb_warehouse,
qty=10,
basic_rate=100,
company=self.sdbnb_company,
)
dn = create_delivery_note(
item_code=item_code,
qty=5,
rate=150,
company=self.sdbnb_company,
warehouse=self.sdbnb_warehouse,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
)
si = make_sales_invoice(dn.name)
si.submit()
# Get the stock value difference from the DN's SLE
sle = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Delivery Note",
"voucher_no": dn.name,
"item_code": item_code,
"is_cancelled": 0,
},
["stock_value_difference", "actual_qty"],
as_dict=True,
)
valuation_rate = abs(flt(sle.stock_value_difference) / flt(sle.actual_qty))
expected_amount = flt(valuation_rate * 5) # SI qty = 5
# SI GL entries should have SDBNB reversal
si_gl_entries = get_gl_entries("Sales Invoice", si.name)
self.assertTrue(si_gl_entries)
self.assertGreater(
sum(gle.debit for gle in si_gl_entries if gle.account == self.sdbnb_expense_account), 0
)
sdbnb_credit = sum(gle.credit for gle in si_gl_entries if gle.account == self.sdbnb_account)
cogs_debit = sum(gle.debit for gle in si_gl_entries if gle.account == self.sdbnb_expense_account)
self.assertEqual(flt(sdbnb_credit, 2), flt(expected_amount, 2))
self.assertEqual(flt(cogs_debit, 2), flt(expected_amount, 2))
def test_sdbnb_partial_billing(self):
"""Test SDBNB reversal for partial invoicing - only billed qty should be reversed."""
item_code = make_item("SDBNB Partial Bill Item", properties={"is_stock_item": 1}).name
make_stock_entry(
item_code=item_code,
target=self.sdbnb_warehouse,
qty=10,
basic_rate=100,
company=self.sdbnb_company,
)
dn = create_delivery_note(
item_code=item_code,
qty=10,
rate=150,
company=self.sdbnb_company,
warehouse=self.sdbnb_warehouse,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
)
# Create SI from DN and reduce qty to 4 (partial billing)
si = make_sales_invoice(dn.name)
si.items[0].qty = 4
si.save()
si.submit()
# Get valuation rate from DN's SLE
sle = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Delivery Note",
"voucher_no": dn.name,
"item_code": item_code,
"is_cancelled": 0,
},
["stock_value_difference", "actual_qty"],
as_dict=True,
)
valuation_rate = abs(flt(sle.stock_value_difference) / flt(sle.actual_qty))
expected_amount = flt(valuation_rate * 4) # Only 4 out of 10
si_gl_entries = get_gl_entries("Sales Invoice", si.name)
sdbnb_credit = sum(gle.credit for gle in si_gl_entries if gle.account == self.sdbnb_account)
self.assertEqual(flt(sdbnb_credit, 2), flt(expected_amount, 2))
def test_sdbnb_disabled_for_sales_return(self):
"""Test that sales return DN uses default expense account when disable_sdbnb_in_sr is enabled."""
frappe.db.set_value("Company", self.sdbnb_company, "disable_sdbnb_in_sr", 1)
try:
item_code = make_item("SDBNB Return Disable Item", properties={"is_stock_item": 1}).name
make_stock_entry(
item_code=item_code,
target=self.sdbnb_warehouse,
qty=10,
basic_rate=100,
company=self.sdbnb_company,
)
dn = create_delivery_note(
item_code=item_code,
qty=5,
rate=150,
company=self.sdbnb_company,
warehouse=self.sdbnb_warehouse,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
)
# Original DN should use SDBNB
dn.reload()
self.assertEqual(dn.items[0].expense_account, self.sdbnb_account)
return_dn = create_delivery_note(
item_code=item_code,
qty=-3,
rate=150,
company=self.sdbnb_company,
warehouse=self.sdbnb_warehouse,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
is_return=1,
return_against=dn.name,
)
# Return DN should not use SDBNB (disable_sdbnb_in_sr is on)
return_dn.reload()
self.assertNotEqual(return_dn.items[0].expense_account, self.sdbnb_account)
finally:
frappe.db.set_value("Company", self.sdbnb_company, "disable_sdbnb_in_sr", 0)
def test_sdbnb_enabled_for_sales_return(self):
"""Test that sales return DN uses SDBNB account when disable_sdbnb_in_sr is off."""
item_code = make_item("SDBNB Return Enable Item", properties={"is_stock_item": 1}).name
make_stock_entry(
item_code=item_code,
target=self.sdbnb_warehouse,
qty=10,
basic_rate=100,
company=self.sdbnb_company,
)
dn = create_delivery_note(
item_code=item_code,
qty=5,
rate=150,
company=self.sdbnb_company,
warehouse=self.sdbnb_warehouse,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
)
return_dn = create_delivery_note(
item_code=item_code,
qty=-3,
rate=150,
company=self.sdbnb_company,
warehouse=self.sdbnb_warehouse,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
is_return=1,
return_against=dn.name,
)
# Return DN should also use SDBNB since disable flag is off by default
return_dn.reload()
self.assertEqual(return_dn.items[0].expense_account, self.sdbnb_account)
def test_sdbnb_no_reversal_with_update_stock(self):
"""Test that SI with update_stock=1 (standalone, no DN link) does NOT create SDBNB GL entries."""
item_code = make_item("SDBNB Update Stock Item", properties={"is_stock_item": 1}).name
make_stock_entry(
item_code=item_code,
target=self.sdbnb_warehouse,
qty=10,
basic_rate=100,
company=self.sdbnb_company,
)
# Create standalone SI with update_stock=1 (no DN link)
si = create_sales_invoice(
company=self.sdbnb_company,
currency="INR",
debit_to=self.sdbnb_debit_to,
income_account=self.sdbnb_income_account,
update_stock=1,
item_code=item_code,
qty=5,
rate=150,
warehouse=self.sdbnb_warehouse,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
)
# SI GL entries should not have SDBNB account
si_gl_entries = get_gl_entries("Sales Invoice", si.name)
sdbnb_entries = [gle for gle in si_gl_entries if gle.account == self.sdbnb_account]
self.assertEqual(len(sdbnb_entries), 0)
def test_sdbnb_skip_for_dn_against_sales_invoice(self):
"""Test that DN items with against_sales_invoice reference skips SDBNB account assignment."""
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
make_delivery_note as make_dn_from_si,
)
item_code = make_item("SDBNB Against SI Item", properties={"is_stock_item": 1}).name
make_stock_entry(
item_code=item_code,
target=self.sdbnb_warehouse,
qty=10,
basic_rate=100,
company=self.sdbnb_company,
)
si = create_sales_invoice(
company=self.sdbnb_company,
currency="INR",
debit_to=self.sdbnb_debit_to,
income_account=self.sdbnb_income_account,
update_stock=0,
item_code=item_code,
qty=5,
rate=150,
warehouse=self.sdbnb_warehouse,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
)
dn = make_dn_from_si(si.name)
self.assertEqual(dn.items[0].expense_account, self.sdbnb_expense_account)
dn.submit()
# DN items created from SI have against_sales_invoice set,
# so SDBNB should be skipped
dn.reload()
self.assertEqual(dn.items[0].expense_account, self.sdbnb_expense_account)
def test_sdbnb_non_stock_item_skipped(self):
"""Test that non-stock items are not assigned SDBNB account."""
non_stock_item = make_item(
"SDBNB Non Stock Item",
properties={"is_stock_item": 0},
).name
dn = create_delivery_note(
company=self.sdbnb_company,
item_code=non_stock_item,
warehouse=self.sdbnb_warehouse,
qty=5,
rate=150,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
do_not_submit=True,
)
# Non-stock item should retain original expense_account, not SDBNB
self.assertNotEqual(dn.items[0].expense_account, self.sdbnb_account)
self.assertEqual(dn.items[0].expense_account, self.sdbnb_expense_account)
def test_sdbnb_reposting_with_fifo(self):
"""Test that backdated inward entry triggers reposting and updates SDBNB GL entries (FIFO)."""
item_code = make_item(
"SDBNB Repost FIFO Item", properties={"is_stock_item": 1, "valuation_method": "FIFO"}
).name
posting_date = add_days(nowdate(), -1)
# Inward 10 qty @ 100
make_stock_entry(
item_code=item_code,
target=self.sdbnb_warehouse,
qty=10,
basic_rate=100,
company=self.sdbnb_company,
posting_date=posting_date,
)
# DN 5 qty → FIFO consumes 5 @ 100 → stock_value_diff = -500
dn = create_delivery_note(
item_code=item_code,
qty=5,
rate=150,
company=self.sdbnb_company,
warehouse=self.sdbnb_warehouse,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
posting_date=posting_date,
)
# Verify initial DN GL: SDBNB Dr 500, Stock In Hand Cr 500
dn_gl = get_gl_entries("Delivery Note", dn.name)
sdbnb_debit = sum(gle.debit for gle in dn_gl if gle.account == self.sdbnb_account)
self.assertEqual(flt(sdbnb_debit, 2), 500.0)
# SI from DN
si = make_sales_invoice(dn.name)
si.set_posting_time = 1
si.posting_date = posting_date
si.submit()
# Verify initial SI GL: SDBNB Cr 500, COGS Dr 500
si_gl = get_gl_entries("Sales Invoice", si.name)
sdbnb_credit = sum(gle.credit for gle in si_gl if gle.account == self.sdbnb_account)
cogs_debit = sum(gle.debit for gle in si_gl if gle.account == self.sdbnb_expense_account)
self.assertEqual(flt(sdbnb_credit, 2), 500.0)
self.assertEqual(flt(cogs_debit, 2), 500.0)
# Backdated inward: 5 qty @ 50 → FIFO queue becomes [[5,50],[10,100]]
# DN now consumes 5@50 from front → stock_value_diff = -250
make_stock_entry(
item_code=item_code,
target=self.sdbnb_warehouse,
qty=5,
basic_rate=50,
company=self.sdbnb_company,
posting_date=add_days(posting_date, -1),
)
# After repost: DN GL should reflect new valuation (250 instead of 500)
dn_gl = get_gl_entries("Delivery Note", dn.name)
sdbnb_debit = sum(gle.debit for gle in dn_gl if gle.account == self.sdbnb_account)
self.assertEqual(flt(sdbnb_debit, 2), 250.0)
# After repost: SI GL should also reflect new valuation
si_gl = get_gl_entries("Sales Invoice", si.name)
sdbnb_credit = sum(gle.credit for gle in si_gl if gle.account == self.sdbnb_account)
cogs_debit = sum(gle.debit for gle in si_gl if gle.account == self.sdbnb_expense_account)
self.assertEqual(flt(sdbnb_credit, 2), 250.0)
self.assertEqual(flt(cogs_debit, 2), 250.0)
def test_sdbnb_reposting_with_moving_average(self):
"""Test that backdated inward entry triggers reposting and updates SDBNB GL entries (Moving Average)."""
item_code = make_item(
"SDBNB Repost MA Item", properties={"is_stock_item": 1, "valuation_method": "Moving Average"}
).name
posting_date = add_days(nowdate(), -1)
# Inward 10 qty @ 100 → avg = 100
make_stock_entry(
item_code=item_code,
target=self.sdbnb_warehouse,
qty=10,
basic_rate=100,
company=self.sdbnb_company,
posting_date=posting_date,
)
# DN 5 qty → avg = 100 → stock_value_diff = -500
dn = create_delivery_note(
item_code=item_code,
qty=5,
rate=150,
company=self.sdbnb_company,
warehouse=self.sdbnb_warehouse,
cost_center=self.sdbnb_cost_center,
expense_account=self.sdbnb_expense_account,
posting_date=posting_date,
)
# Verify initial DN GL: SDBNB Dr 500, Stock In Hand Cr 500
dn_gl = get_gl_entries("Delivery Note", dn.name)
sdbnb_debit = sum(gle.debit for gle in dn_gl if gle.account == self.sdbnb_account)
self.assertEqual(flt(sdbnb_debit, 2), 500.0)
# SI from DN
si = make_sales_invoice(dn.name)
si.set_posting_time = 1
si.posting_date = posting_date
si.submit()
# Verify initial SI GL: SDBNB Cr 500, COGS Dr 500
si_gl = get_gl_entries("Sales Invoice", si.name)
sdbnb_credit = sum(gle.credit for gle in si_gl if gle.account == self.sdbnb_account)
cogs_debit = sum(gle.debit for gle in si_gl if gle.account == self.sdbnb_expense_account)
self.assertEqual(flt(sdbnb_credit, 2), 500.0)
self.assertEqual(flt(cogs_debit, 2), 500.0)
# Backdated inward: 5 qty @ 50
# Moving avg becomes: (5*50 + 10*100) / 15 = 1250/15 ≈ 83.33
# DN 5 qty → reposted stock_value_diff ≈ -416.67
make_stock_entry(
item_code=item_code,
target=self.sdbnb_warehouse,
qty=5,
basic_rate=50,
company=self.sdbnb_company,
posting_date=add_days(posting_date, -1),
)
# Read actual stock_value_difference from reposted SLE
sle = frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Delivery Note",
"voucher_no": dn.name,
"item_code": item_code,
"is_cancelled": 0,
},
"stock_value_difference",
)
expected_amount = abs(flt(sle, 2))
# DN GL should reflect new moving average valuation
dn_gl = get_gl_entries("Delivery Note", dn.name)
sdbnb_debit = sum(gle.debit for gle in dn_gl if gle.account == self.sdbnb_account)
self.assertEqual(flt(sdbnb_debit, 2), expected_amount)
self.assertLess(expected_amount, 500.0)
# SI GL should also reflect new valuation
si_gl = get_gl_entries("Sales Invoice", si.name)
sdbnb_credit = sum(gle.credit for gle in si_gl if gle.account == self.sdbnb_account)
cogs_debit = sum(gle.debit for gle in si_gl if gle.account == self.sdbnb_expense_account)
self.assertEqual(flt(sdbnb_credit, 2), expected_amount)
self.assertEqual(flt(cogs_debit, 2), expected_amount)
@ERPNextTestSuite.change_settings("Selling Settings", {"validate_selling_price": 1})
def test_validate_selling_price(self):
item_code = make_item("VSP Item", properties={"is_stock_item": 1}).name