From 6b664e63e9f0d5a97cfca9a96a2da43d8ac2a8e3 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Sun, 28 Sep 2025 11:15:49 +0200 Subject: [PATCH 1/5] feat: Auto add Down Payment Invoice as print heading for down payments --- .../doctype/sales_invoice/sales_invoice.py | 7 ++++++- erpnext/patches.txt | 1 + .../v4_0/add_down_payment_invoice_heading.py | 8 ++++++++ .../setup_wizard/operations/install_fixtures.py | 17 +++++++++-------- 4 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 erpnext/patches/dokos/v4_0/add_down_payment_invoice_heading.py diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 72bb04feb0..59ff912e5d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -298,7 +298,8 @@ class SalesInvoice(SellingController): self.clear_unallocated_advances("Sales Invoice Advance", "advances") self.validate_fixed_asset() self.set_income_account_for_fixed_assets() - self.set_income_account_for_down_payments() + self.set_income_account_for_down_payments() # @dokos + self.set_print_heading() # @dokos self.validate_item_cost_centers() self.check_conversion_rate() self.validate_accounts() @@ -1225,6 +1226,10 @@ class SalesInvoice(SellingController): for d in self.get("items"): d.income_account = debit_to + def set_print_heading(self): + if self.get("is_down_payment_invoice"): + self.select_print_heading = frappe.get_cached_value("Print Heading", _("Down Payment Invoice")) + def check_prev_docstatus(self): for d in self.get("items"): if ( diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1d3aa0b3d3..5ddb141a36 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -393,6 +393,7 @@ erpnext.patches.dokos.v4_0.check_enable_pappers erpnext.patches.dokos.v4_0.suspense_accounts_for_vat_on_payments erpnext.patches.dokos.v4_0.move_bundle_price_determination_to_bundle execute:frappe.delete_doc_if_exists("Page", "booking-credits") +erpnext.patches.dokos.v4_0.add_down_payment_invoice_heading # @dokos diff --git a/erpnext/patches/dokos/v4_0/add_down_payment_invoice_heading.py b/erpnext/patches/dokos/v4_0/add_down_payment_invoice_heading.py new file mode 100644 index 0000000000..d4ba1f3b5c --- /dev/null +++ b/erpnext/patches/dokos/v4_0/add_down_payment_invoice_heading.py @@ -0,0 +1,8 @@ +import frappe +from frappe.printing.doctype.print_heading.print_heading import PrintHeading + + +def execute(): + print_heading: PrintHeading = frappe.new_doc("Print Heading") # type: ignore + print_heading.print_heading = frappe._("Down Payment Invoice") + print_heading.insert() diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 0442cb8cd1..76ca460c87 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -288,14 +288,15 @@ def install(country=None): {"doctype": "Party Type", "party_type": "Supplier", "account_type": "Payable"}, {"doctype": "Party Type", "party_type": "Employee", "account_type": "Payable"}, {"doctype": "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, - {"doctype": "Opportunity Type", "name": _("Sales")}, - {"doctype": "Opportunity Type", "name": _("Support")}, - {"doctype": "Opportunity Type", "name": _("Maintenance")}, - {"doctype": "Project Type", "project_type": "Internal"}, - {"doctype": "Project Type", "project_type": "External"}, - {"doctype": "Project Type", "project_type": "Other"}, - {"doctype": "Print Heading", "print_heading": _("Credit Note")}, - {"doctype": "Print Heading", "print_heading": _("Debit Note")}, + {"doctype": "Opportunity Type", "name": frappe._("Sales")}, + {"doctype": "Opportunity Type", "name": frappe._("Support")}, + {"doctype": "Opportunity Type", "name": frappe._("Maintenance")}, + {"doctype": "Project Type", "project_type": _("Internal")}, + {"doctype": "Project Type", "project_type": _("External")}, + {"doctype": "Project Type", "project_type": _("Other")}, + {"doctype": "Print Heading", "print_heading": frappe._("Credit Note")}, + {"doctype": "Print Heading", "print_heading": frappe._("Debit Note")}, + {"doctype": "Print Heading", "print_heading": frappe._("Down Payment Invoice")}, # Share Management {"doctype": "Share Type", "title": _("Equity")}, {"doctype": "Share Type", "title": _("Preference")}, -- GitLab From 819f44529d81e61c5c4aeeb89cea450bf9b9d8cf Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Sun, 28 Sep 2025 11:18:06 +0200 Subject: [PATCH 2/5] fix: fetch lang for creation in patch --- .../patches/dokos/v4_0/add_down_payment_invoice_heading.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/dokos/v4_0/add_down_payment_invoice_heading.py b/erpnext/patches/dokos/v4_0/add_down_payment_invoice_heading.py index d4ba1f3b5c..09c1127fea 100644 --- a/erpnext/patches/dokos/v4_0/add_down_payment_invoice_heading.py +++ b/erpnext/patches/dokos/v4_0/add_down_payment_invoice_heading.py @@ -3,6 +3,7 @@ from frappe.printing.doctype.print_heading.print_heading import PrintHeading def execute(): + lang = frappe.get_system_settings("language") print_heading: PrintHeading = frappe.new_doc("Print Heading") # type: ignore - print_heading.print_heading = frappe._("Down Payment Invoice") - print_heading.insert() + print_heading.print_heading = frappe._("Down Payment Invoice", lang=lang) + print_heading.insert(ignore_if_duplicate=True) -- GitLab From fe47a280c1ec649b9b5b6175beab9f9e33cba715 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Sun, 28 Sep 2025 11:54:00 +0200 Subject: [PATCH 3/5] fix: Auto adjust discount amount on down payment creation --- .../doctype/sales_invoice/sales_invoice.js | 56 +++++++++++++------ .../doctype/sales_invoice/sales_invoice.json | 6 +- .../doctype/sales_order/sales_order.py | 31 +++++++++- 3 files changed, 73 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 4dfb5b18c4..aec947b168 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -948,23 +948,22 @@ frappe.ui.form.on('Sales Invoice', { frm.trigger('reset_posting_time'); }, - redeem_loyalty_points: function(frm) { - frm.events.get_loyalty_details(frm); - }, + frappe.db + .get_list("Item", { + filters: { is_down_payment_item: 1 }, + order_by: "down_payment_percentage DESC", + fields: ["name", "down_payment_percentage"], + }) + .then((r) => { + if (!r.exc && r.length) { + frm.doc.items.forEach((i) => { + frappe.model.set_value(i.doctype, i.name, "item_code", r[0].name).then(() => { + const line = locals[i.doctype][i.name]; + calculate_down_payment(line); + }); + }); - loyalty_points: function(frm) { - if (frm.redemption_conversion_factor) { - frm.events.set_loyalty_points(frm); - } else { - frappe.call({ - method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_redeemption_factor", - args: { - "loyalty_program": frm.doc.loyalty_program - }, - callback: function(r) { - if (r) { - frm.redemption_conversion_factor = r.message; - frm.events.set_loyalty_points(frm); + adjust_discount_amount(frm, r[0].down_payment_percentage); } } }); @@ -1082,7 +1081,30 @@ const calculate_down_payment = line => { } } -var set_timesheet_detail_rate = function(cdt, cdn, currency, timelog) { +const adjust_discount_amount = (frm, down_payment_percentage) => { + if (frm.doc.is_down_payment_invoice) { + if (frm.doc.additional_discount_percentage === 0.0 && frm.doc.discount_amount > 0.0) { + frm.set_value( + "discount_amount", + (flt(down_payment_percentage) / 100.0) * flt(frm.doc.discount_amount) + ); + } + } +}; + +frappe.ui.form.on("Sales Invoice Payment", { + mode_of_payment: function (frm) { + frappe.call({ + doc: frm.doc, + method: "set_account_for_mode_of_payment", + callback: function (r) { + refresh_field("payments"); + }, + }); + }, +}); + +var set_timesheet_detail_rate = function (cdt, cdn, currency, timelog) { frappe.call({ method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_detail_rate", args: { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index ef20364d7a..5a8f29b15f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1640,7 +1640,9 @@ "fieldtype": "Check", "label": "Is Down Payment Invoice", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "read_only": 1, + "set_only_once": 1 }, { "default": "0", @@ -1926,7 +1928,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2025-02-06 15:59:54.636202", + "modified": "2025-09-28 11:28:35.262629", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e27507488c..709360f8ef 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -3,7 +3,7 @@ import json -from typing import Literal +from typing import TYPE_CHECKING, Literal import frappe import frappe.utils @@ -42,6 +42,9 @@ from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty form_grid_templates = {"items": "templates/form_grid/item_grid.html"} +if TYPE_CHECKING: + from erpnext.stock.doctype.item.item import Item + class WarehouseRequired(frappe.ValidationError): pass @@ -1197,6 +1200,32 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): def postprocess(source, target): + if "is_down_payment_invoice" in (frappe.flags.args or {}): + target.is_down_payment_invoice = True + down_payment_item: Item = frappe.db.get_value( + "Item", + filters={"is_down_payment_item": 1}, + order_by="down_payment_percentage DESC", + as_dict=True, + fieldname=["name", "down_payment_percentage"], + ) # type: ignore + target.items = [] + target.append( + "items", + { + "sales_order": source.name, + "item_code": down_payment_item.name, + "price_list_rate": down_payment_item.down_payment_percentage / 100 * source.total, + "base_rate": down_payment_item.down_payment_percentage / 100 * source.base_total, + "rate": down_payment_item.down_payment_percentage / 100 * source.total, + }, + ) + + if not target.additional_discount_percentage and target.discount_amount: + target.discount_amount = ( + target.discount_amount * flt(down_payment_item.down_payment_percentage) / 100.0 + ) + set_missing_values(source, target) # Get the advance paid Journal Entries in Sales Invoice Advance if target.get("allocate_advances_automatically"): -- GitLab From 34575294ead86d80bbdede2a2f786f069ca3e435 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Sun, 28 Sep 2025 12:10:24 +0200 Subject: [PATCH 4/5] fix: filter for down payment items --- erpnext/public/js/controllers/buying.js | 20 ++++++++++++++------ erpnext/public/js/utils/sales_common.js | 7 ++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 0164e8da3c..33b35843a4 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -99,14 +99,22 @@ erpnext.buying = { return{ query: "erpnext.controllers.queries.item_query", - filters: filters + filters: filters, + }; + } else { + const filters = { + supplier: me.frm.doc.supplier, + is_purchase_item: 1, + has_variants: 0, + }; // @dokos + if (me.frm.doc.is_down_payment_invoice) { + // @dokos + filters["is_down_payment_item"] = 1; } - } - else { - return{ + return { query: "erpnext.controllers.queries.item_query", - filters: { 'supplier': me.frm.doc.supplier, 'is_purchase_item': 1, 'has_variants': 0} - } + filters: filters, // @dokos + }; } }); diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index b72083c860..e6446b8f80 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -75,9 +75,14 @@ erpnext.sales_common = { if (me.frm.doc.doctype == "Quotation" && me.frm.doc.quotation_to == "Customer") { customer = me.frm.doc.party_name; } + const filters = { is_sales_item: 1, customer: customer, has_variants: 0 }; // @dokos + if (me.frm.doc.is_down_payment_invoice) { + // @dokos + filters["is_down_payment_item"] = 1; + } return { query: "erpnext.controllers.queries.item_query", - filters: { is_sales_item: 1, customer: customer, has_variants: 0 }, + filters: filters, // @dokos }; }); } -- GitLab From 88edb807aaa7f22df0debec9891bd24dc4afdb56 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Sun, 28 Sep 2025 16:26:41 +0200 Subject: [PATCH 5/5] fix: merge conflicts --- .../doctype/sales_invoice/sales_invoice.js | 48 ++++++++----------- .../doctype/sales_invoice/sales_invoice.json | 1 - 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index aec947b168..62165a0dce 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -948,22 +948,23 @@ frappe.ui.form.on('Sales Invoice', { frm.trigger('reset_posting_time'); }, - frappe.db - .get_list("Item", { - filters: { is_down_payment_item: 1 }, - order_by: "down_payment_percentage DESC", - fields: ["name", "down_payment_percentage"], - }) - .then((r) => { - if (!r.exc && r.length) { - frm.doc.items.forEach((i) => { - frappe.model.set_value(i.doctype, i.name, "item_code", r[0].name).then(() => { - const line = locals[i.doctype][i.name]; - calculate_down_payment(line); - }); - }); + redeem_loyalty_points: function(frm) { + frm.events.get_loyalty_details(frm); + }, - adjust_discount_amount(frm, r[0].down_payment_percentage); + loyalty_points: function(frm) { + if (frm.redemption_conversion_factor) { + frm.events.set_loyalty_points(frm); + } else { + frappe.call({ + method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_redeemption_factor", + args: { + "loyalty_program": frm.doc.loyalty_program + }, + callback: function(r) { + if (r) { + frm.redemption_conversion_factor = r.message; + frm.events.set_loyalty_points(frm); } } }); @@ -1042,7 +1043,10 @@ frappe.ui.form.on('Sales Invoice', { frappe.db.get_list("Item", {filters: {is_down_payment_item: 1}, order_by: "down_payment_percentage DESC"}) .then(r => { if (!r.exc && r.length) { - frappe.model.set_value(frm.doc.items[0].doctype, frm.doc.items[0].name, "item_code", r[0].name) + frappe.model.set_value(frm.doc.items[0].doctype, frm.doc.items[0].name, "item_code", r[0].name); + const line = locals[frm.doc.items[0].doctype][frm.doc.items[0].name]; + calculate_down_payment(line); + adjust_discount_amount(frm, r[0].down_payment_percentage); } }) } @@ -1092,18 +1096,6 @@ const adjust_discount_amount = (frm, down_payment_percentage) => { } }; -frappe.ui.form.on("Sales Invoice Payment", { - mode_of_payment: function (frm) { - frappe.call({ - doc: frm.doc, - method: "set_account_for_mode_of_payment", - callback: function (r) { - refresh_field("payments"); - }, - }); - }, -}); - var set_timesheet_detail_rate = function (cdt, cdn, currency, timelog) { frappe.call({ method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_detail_rate", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 5a8f29b15f..a17cc5392f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1641,7 +1641,6 @@ "label": "Is Down Payment Invoice", "no_copy": 1, "print_hide": 1, - "read_only": 1, "set_only_once": 1 }, { -- GitLab