feat(stock): add Stock Delivered But Not Billed GL entries on Delivery Note and Sales Invoice

This commit is contained in:
Pugazhendhi Velu
2026-03-21 07:30:23 +00:00
committed by kavin-114
parent 8596d98ac4
commit 3364ee9274
4 changed files with 140 additions and 0 deletions

View File

@@ -1586,6 +1586,12 @@ class SalesInvoice(SellingController):
self.make_internal_transfer_gl_entries(gl_entries)
self.make_item_gl_entries(gl_entries)
disable_sdbnb_in_sr = frappe.get_cached_value("Company", self.company, "disable_sdbnb_in_sr")
if not (self.is_return and disable_sdbnb_in_sr):
self.stock_delivered_but_not_billed_gl_entries(gl_entries)
self.make_precision_loss_gl_entry(gl_entries)
self.make_discount_gl_entries(gl_entries)
@@ -1603,6 +1609,81 @@ class SalesInvoice(SellingController):
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
return gl_entries
def stock_delivered_but_not_billed_gl_entries(self, gl_entries):
if self.update_stock or not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
return
for item in self.get("items"):
if not item.delivery_note and not item.dn_detail:
continue
if not frappe.get_cached_value("Item", item.item_code, "is_stock_item"):
continue
dn_expense_account = frappe.get_cached_value(
"Delivery Note Item", item.dn_detail, "expense_account"
)
if (
not dn_expense_account
or frappe.get_cached_value("Account", dn_expense_account, "account_type")
!= "Stock Delivered But Not Billed"
or not item.expense_account
or dn_expense_account == item.expense_account
):
continue
delivery_note = item.delivery_note or frappe.get_cached_value(
"Delivery Note Item", item.dn_detail, "parent"
)
if not delivery_note:
continue
item_g = frappe.get_cached_value(
"Stock Ledger Entry",
{
"voucher_no": delivery_note,
"voucher_detail_no": item.dn_detail,
"item_code": item.item_code,
"is_cancelled": 0,
},
["stock_value_difference", "actual_qty"],
as_dict=True,
)
if not item_g or not flt(item_g.actual_qty):
continue
valuation_rate = flt(item_g.stock_value_difference) / flt(item_g.actual_qty)
valuation_amount = valuation_rate * item.stock_qty
dn_account_currency = get_account_currency(dn_expense_account)
item_account_currency = get_account_currency(item.expense_account)
gl_entries.append(
self.get_gl_dict(
{
"account": dn_expense_account,
"against": item.expense_account,
"credit": flt(valuation_amount),
"credit_in_account_currency": flt(valuation_amount),
"cost_center": item.cost_center,
},
dn_account_currency,
item=item,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": item.expense_account,
"against": dn_expense_account,
"debit": flt(valuation_amount),
"debit_in_account_currency": flt(valuation_amount),
"cost_center": item.cost_center,
},
item_account_currency,
item=item,
)
)
def make_customer_gl_entry(self, gl_entries):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introduction of posting GLE based on rounded total

View File

@@ -940,6 +940,7 @@ class StockController(AccountsController):
"Stock Reconciliation",
"Stock Entry",
"Subcontracting Receipt",
"Delivery Note",
)
and not is_expense_account
):

View File

@@ -289,6 +289,7 @@ class DeliveryNote(SellingController):
self.validate_posting_time()
super().validate()
self.validate_references()
self.validate_expense_account()
self.set_status()
self.so_required()
self.validate_proj_cust()
@@ -461,6 +462,42 @@ class DeliveryNote(SellingController):
d.actual_qty = flt(bin_qty.actual_qty)
d.projected_qty = flt(bin_qty.projected_qty)
def validate_expense_account(self):
company_values = frappe.get_cached_value(
"Company",
self.company,
[
"stock_delivered_but_not_billed",
"disable_sdbnb_in_sr",
"default_expense_account",
],
as_dict=True,
)
sdbnb_account = company_values.stock_delivered_but_not_billed
disable_sdbnb_in_sr = company_values.disable_sdbnb_in_sr
default_expense_account = company_values.default_expense_account
for item in self.items:
if item.get("against_sales_invoice"):
continue
is_stock_item = frappe.get_cached_value("Item", item.item_code, "is_stock_item")
# Only stock items
if not is_stock_item or item.get("is_fixed_asset") or item.get("is_subcontracted"):
continue
# Sales Return handling
if self.is_return and disable_sdbnb_in_sr:
if default_expense_account and (
not item.expense_account or item.expense_account == sdbnb_account
):
item.expense_account = default_expense_account
continue
if sdbnb_account:
item.expense_account = sdbnb_account
elif not item.expense_account and default_expense_account:
item.expense_account = default_expense_account
def on_submit(self):
self.validate_packed_qty()
self.update_pick_list_status()

View File

@@ -436,6 +436,27 @@ def get_basic_details(ctx: ItemDetailsCtx, item, overwrite_warehouse=True) -> It
fieldname="fixed_asset_account", item=ctx.item_code, company=ctx.company
)
company_values = frappe.get_cached_value(
"Company",
ctx.company,
[
"stock_delivered_but_not_billed",
"disable_sdbnb_in_sr",
],
as_dict=True,
)
if (
ctx.doctype == "Delivery Note"
and ctx.is_stock_item
and company_values
and company_values.stock_delivered_but_not_billed
and not ctx.get("is_fixed_asset")
and not ctx.get("is_subcontracted")
):
if not (ctx.get("is_return") and company_values.disable_sdbnb_in_sr):
expense_account = company_values.stock_delivered_but_not_billed
# Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master
if not ctx.uom:
if ctx.doctype in sales_doctypes: