From 3364ee92741b0aee5c656e60b5aecf5caa82cff3 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Velu Date: Sat, 21 Mar 2026 07:30:23 +0000 Subject: [PATCH] feat(stock): add Stock Delivered But Not Billed GL entries on Delivery Note and Sales Invoice --- .../doctype/sales_invoice/sales_invoice.py | 81 +++++++++++++++++++ erpnext/controllers/stock_controller.py | 1 + .../doctype/delivery_note/delivery_note.py | 37 +++++++++ erpnext/stock/get_item_details.py | 21 +++++ 4 files changed, 140 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ebef1a8040d..86f84be0973 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -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 diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 4951ec346be..9fb9dfe58ab 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -940,6 +940,7 @@ class StockController(AccountsController): "Stock Reconciliation", "Stock Entry", "Subcontracting Receipt", + "Delivery Note", ) and not is_expense_account ): diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index fe03e3218b7..3d827cb84b2 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -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() diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index d40511d5e2c..38cb8b2eb1b 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -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: