Skip to content

feat: add overall discount to sales order and delivery note #47857

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 13 additions & 15 deletions erpnext/accounts/doctype/sales_invoice/sales_invoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -556,21 +556,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}

is_cash_or_non_trade_discount() {
this.frm.set_df_property(
"additional_discount_account",
"hidden",
1 - this.frm.doc.is_cash_or_non_trade_discount
);
this.frm.set_df_property(
"additional_discount_account",
"reqd",
this.frm.doc.is_cash_or_non_trade_discount
);

if (!this.frm.doc.is_cash_or_non_trade_discount) {
this.frm.set_value("additional_discount_account", "");
}

toggle_additional_discount_account(this.frm);
this.calculate_taxes_and_totals();
}
};
Expand Down Expand Up @@ -784,6 +770,9 @@ frappe.ui.form.on("Sales Invoice", {
},
onload: function (frm) {
frm.redemption_conversion_factor = null;
if (frm.doc.is_cash_or_non_trade_discount) {
toggle_additional_discount_account(frm);
}
},

update_stock: function (frm, dt, dn) {
Expand Down Expand Up @@ -1113,6 +1102,15 @@ var set_timesheet_detail_rate = function (cdt, cdn, currency, timelog) {
});
};

function toggle_additional_discount_account(frm) {
frm.set_df_property("additional_discount_account", "hidden", !frm.doc.is_cash_or_non_trade_discount);
frm.set_df_property("additional_discount_account", "reqd", frm.doc.is_cash_or_non_trade_discount);

if (!frm.doc.is_cash_or_non_trade_discount) {
frm.set_value("additional_discount_account", "");
}
}

var select_loyalty_program = function (frm, loyalty_programs) {
var dialog = new frappe.ui.Dialog({
title: __("Select Loyalty Program"),
Expand Down
8 changes: 8 additions & 0 deletions erpnext/selling/doctype/sales_order/sales_order.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ frappe.ui.form.on("Sales Order", {
frm.set_df_property("packed_items", "cannot_delete_rows", true);
},

is_cash_or_non_trade_discount: function (frm) {
if (!frm.doc.is_cash_or_non_trade_discount) {
frm.set_value("additional_discount_account", "");
}

frm.cscript.calculate_taxes_and_totals();
},

refresh: function (frm) {
if (frm.doc.docstatus === 1) {
if (
Expand Down
22 changes: 20 additions & 2 deletions erpnext/selling/doctype/sales_order/sales_order.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@
"apply_discount_on",
"base_discount_amount",
"coupon_code",
"is_cash_or_non_trade_discount",
"additional_discount_account",
"column_break_50",
"additional_discount_percentage",
"discount_amount",
Expand Down Expand Up @@ -1681,13 +1683,29 @@
"hidden": 1,
"label": "Has Unit Price Items",
"no_copy": 1
},
{
"default": "0",
"depends_on": "eval: doc.apply_discount_on == \"Grand Total\"",
"fieldname": "is_cash_or_non_trade_discount",
"fieldtype": "Check",
"label": "Is Cash or Non Trade Discount"
},
{
"allow_on_submit": 1,
"depends_on": "eval: doc.is_cash_or_non_trade_discount",
"fieldname": "additional_discount_account",
"fieldtype": "Link",
"label": "Discount Account",
"mandatory_depends_on": "eval: doc.is_cash_or_non_trade_discount",
"options": "Account"
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2025-03-03 16:49:00.676927",
"modified": "2025-06-02 15:05:23.067656",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
Expand Down Expand Up @@ -1766,4 +1784,4 @@
"title_field": "customer_name",
"track_changes": 1,
"track_seen": 1
}
}
2 changes: 2 additions & 0 deletions erpnext/selling/doctype/sales_order/sales_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class SalesOrder(SellingController):
from erpnext.selling.doctype.sales_team.sales_team import SalesTeam
from erpnext.stock.doctype.packed_item.packed_item import PackedItem

additional_discount_account: DF.Link | None
additional_discount_percentage: DF.Float
address_display: DF.TextEditor | None
advance_paid: DF.Currency
Expand Down Expand Up @@ -119,6 +120,7 @@ class SalesOrder(SellingController):
in_words: DF.Data | None
incoterm: DF.Link | None
inter_company_order_reference: DF.Link | None
is_cash_or_non_trade_discount: DF.Check
is_internal_customer: DF.Check
items: DF.Table[SalesOrderItem]
language: DF.Link | None
Expand Down
66 changes: 66 additions & 0 deletions erpnext/selling/doctype/sales_order/test_sales_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from frappe.tests import IntegrationTestCase, change_settings
from frappe.utils import add_days, flt, getdate, nowdate, today

from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
Expand Down Expand Up @@ -161,6 +162,71 @@ def test_make_sales_invoice(self):
si1 = make_sales_invoice(so.name)
self.assertEqual(len(si1.get("items")), 0)

def test_overall_discount_calculation(self):
so = make_sales_order(do_not_submit=True)

discount_percent = 5
grand_total = so.grand_total
net_total = so.net_total
additional_discount_account = frappe.db.get_value(
"Account", {"account_name": "Test Discount Account", "company": "_Test Company"}, "name"
)

if not additional_discount_account:
additional_discount_account = create_account(
account_name="Test Discount Account",
parent_account="Indirect Expenses - _TC",
company="_Test Company",
)

discount_amount = grand_total * (discount_percent / 100)

so.apply_discount_on = "Grand Total"
so.additional_discount_percentage = discount_percent
so.is_cash_or_non_trade_discount = 1
so.additional_discount_account = additional_discount_account

so.save()

self.assertEqual(so.grand_total, (grand_total - discount_amount))
self.assertEqual(so.net_total, net_total)

so.submit()

def test_sales_invoice_creation_from_sales_order_with_cash_discount_fields(self):
so = make_sales_order(do_not_submit=True)

additional_discount_account = frappe.db.get_value(
"Account", {"account_name": "Test Discount Account", "company": "_Test Company"}, "name"
)

if not additional_discount_account:
additional_discount_account = create_account(
account_name="Test Discount Account",
parent_account="Indirect Expenses - _TC",
company="_Test Company",
)
discount_percent = 5

so.apply_discount_on = "Grand Total"
so.additional_discount_percentage = discount_percent
so.is_cash_or_non_trade_discount = 1
so.additional_discount_account = additional_discount_account

so.submit()

si = make_sales_invoice(so.name)

self.assertEqual(so.is_cash_or_non_trade_discount, si.is_cash_or_non_trade_discount)
self.assertEqual(so.additional_discount_account, si.additional_discount_account)
self.assertEqual(so.apply_discount_on, si.apply_discount_on)
self.assertEqual(so.additional_discount_percentage, si.additional_discount_percentage)
self.assertEqual(so.grand_total, si.grand_total)
self.assertEqual(so.net_total, si.net_total)

si.save()
si.submit()

def test_so_billed_amount_against_return_entry(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return

Expand Down
7 changes: 7 additions & 0 deletions erpnext/stock/doctype/delivery_note/delivery_note.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ frappe.ui.form.on("Delivery Note", {
frm.set_df_property("packed_items", "cannot_delete_rows", true);
},

is_cash_or_non_trade_discount: function (frm) {
if (!frm.doc.is_cash_or_non_trade_discount) {
frm.set_value("additional_discount_account", "");
}

frm.cscript.calculate_taxes_and_totals();
},
print_without_amount: function (frm) {
erpnext.stock.delivery_note.set_print_hide(frm.doc);
},
Expand Down
23 changes: 21 additions & 2 deletions erpnext/stock/doctype/delivery_note/delivery_note.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
"section_break_49",
"apply_discount_on",
"base_discount_amount",
"is_cash_or_non_trade_discount",
"additional_discount_account",
"column_break_51",
"additional_discount_percentage",
"discount_amount",
Expand Down Expand Up @@ -1419,13 +1421,29 @@
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"default": "0",
"depends_on": "eval: doc.apply_discount_on == \"Grand Total\"",
"fieldname": "is_cash_or_non_trade_discount",
"fieldtype": "Check",
"label": "Is Cash or Non Trade Discount"
},
{
"allow_on_submit": 1,
"depends_on": "eval: doc.is_cash_or_non_trade_discount",
"fieldname": "additional_discount_account",
"fieldtype": "Link",
"label": "Discount Account",
"mandatory_depends_on": "eval: doc.is_cash_or_non_trade_discount",
"options": "Account"
}
],
"icon": "fa fa-truck",
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2025-01-06 15:02:30.558756",
"modified": "2025-06-02 15:14:28.590001",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
Expand Down Expand Up @@ -1516,6 +1534,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "status,customer,customer_name, territory,base_grand_total",
"show_name_in_global_search": 1,
"sort_field": "creation",
Expand All @@ -1525,4 +1544,4 @@
"title_field": "customer_name",
"track_changes": 1,
"track_seen": 1
}
}
2 changes: 2 additions & 0 deletions erpnext/stock/doctype/delivery_note/delivery_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class DeliveryNote(SellingController):
from erpnext.stock.doctype.delivery_note_item.delivery_note_item import DeliveryNoteItem
from erpnext.stock.doctype.packed_item.packed_item import PackedItem

additional_discount_account: DF.Link | None
additional_discount_percentage: DF.Float
address_display: DF.TextEditor | None
amended_from: DF.Link | None
Expand Down Expand Up @@ -79,6 +80,7 @@ class DeliveryNote(SellingController):
installation_status: DF.Literal[None]
instructions: DF.Text | None
inter_company_reference: DF.Link | None
is_cash_or_non_trade_discount: DF.Check
is_internal_customer: DF.Check
is_return: DF.Check
issue_credit_note: DF.Check
Expand Down
65 changes: 64 additions & 1 deletion erpnext/stock/doctype/delivery_note/test_delivery_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from frappe.tests import IntegrationTestCase, UnitTestCase
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.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.utils import get_balance_on
from erpnext.controllers.accounts_controller import InvalidQtyError
Expand Down Expand Up @@ -1226,6 +1226,69 @@ def test_make_sales_invoice_from_dn_for_returned_qty(self):
si = make_sales_invoice(dn.name)
self.assertEqual(si.items[0].qty, 1)

def test_overall_discount_calculatin(self):
dn = create_delivery_note(do_not_submit=True)

discount_percent = 5
grand_total = dn.grand_total
net_total = dn.net_total
additional_discount_account = frappe.db.get_value(
"Account", {"account_name": "Test Discount Account", "company": "_Test Company"}, "name"
)

if not additional_discount_account:
additional_discount_account = create_account(
account_name="Test Discount Account",
parent_account="Indirect Expenses - _TC",
company="_Test Company",
)
discount_amount = grand_total * (discount_percent / 100)

dn.apply_discount_on = "Grand Total"
dn.additional_discount_percentage = discount_percent
dn.is_cash_or_non_trade_discount = 1
dn.additional_discount_account = additional_discount_account

dn.save()

self.assertEqual(dn.grand_total, (grand_total - discount_amount))
self.assertEqual(dn.net_total, net_total)

dn.submit()

def test_sales_invoice_creation_from_delivery_note_with_cash_discount_fields(self):
dn = create_delivery_note(do_not_submit=True)

additional_discount_account = frappe.db.get_value(
"Account", {"account_name": "Test Discount Account", "company": "_Test Company"}, "name"
)

if not additional_discount_account:
additional_discount_account = create_account(
account_name="Test Discount Account",
parent_account="Indirect Expenses - _TC",
company="_Test Company",
)

dn.apply_discount_on = "Grand Total"
dn.additional_discount_percentage = 5
dn.is_cash_or_non_trade_discount = 1
dn.additional_discount_account = additional_discount_account

dn.submit()

si = make_sales_invoice(dn.name)

self.assertEqual(dn.is_cash_or_non_trade_discount, si.is_cash_or_non_trade_discount)
self.assertEqual(dn.additional_discount_account, si.additional_discount_account)
self.assertEqual(dn.apply_discount_on, si.apply_discount_on)
self.assertEqual(dn.additional_discount_percentage, si.additional_discount_percentage)
self.assertEqual(dn.grand_total, si.grand_total)
self.assertEqual(dn.net_total, si.net_total)

si.save()
si.submit()

def test_make_sales_invoice_from_dn_with_returned_qty_duplicate_items(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice

Expand Down
Loading