From 314881645162a19efb03c4d4fd035bd3e299e5bd Mon Sep 17 00:00:00 2001 From: Jatin3128 Date: Fri, 6 Mar 2026 15:08:35 +0530 Subject: [PATCH] fix: correct payment terms fetching and recalculation logic --- .../accounts_settings/accounts_settings.json | 8 +-- erpnext/controllers/accounts_controller.py | 31 +++++---- erpnext/public/js/controllers/transaction.js | 1 + .../selling/doctype/quotation/quotation.py | 11 +++- .../doctype/quotation/test_quotation.py | 65 ++++++++++++++++++- .../doctype/sales_order/sales_order.json | 16 ++++- .../doctype/sales_order/sales_order.py | 26 ++------ 7 files changed, 113 insertions(+), 45 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 15badf105f8..25e62e20f3e 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -216,7 +216,7 @@ "description": "Payment Terms from orders will be fetched into the invoices as is", "fieldname": "automatically_fetch_payment_terms", "fieldtype": "Check", - "label": "Automatically Fetch Payment Terms from Order" + "label": "Automatically Fetch Payment Terms from Order/Quotation" }, { "description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ", @@ -307,7 +307,7 @@ }, { "default": "0", - "description": "Learn about Common Party", + "description": "Learn about Common Party", "fieldname": "enable_common_party_accounting", "fieldtype": "Check", "label": "Enable Common Party Accounting" @@ -671,7 +671,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-12-26 19:46:55.093717", + "modified": "2026-03-06 14:49:11.467716", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", @@ -701,4 +701,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 008402eeb53..e53299ac2b7 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2502,13 +2502,14 @@ class AccountsController(TransactionBase): grand_total = self.get("rounded_total") or self.grand_total automatically_fetch_payment_terms = 0 - if self.doctype in ("Sales Invoice", "Purchase Invoice"): - base_grand_total = base_grand_total - flt(self.base_write_off_amount) - grand_total = grand_total - flt(self.write_off_amount) + if self.doctype in ("Sales Invoice", "Purchase Invoice", "Sales Order"): po_or_so, doctype, fieldname = self.get_order_details() automatically_fetch_payment_terms = cint( frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") ) + if self.doctype != "Sales Order": + base_grand_total = base_grand_total - flt(self.base_write_off_amount) + grand_total = grand_total - flt(self.write_off_amount) if self.get("total_advance"): if party_account_currency == self.company_currency: @@ -2524,7 +2525,7 @@ class AccountsController(TransactionBase): if not self.get("payment_schedule"): if ( - self.doctype in ["Sales Invoice", "Purchase Invoice"] + self.doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order"] and automatically_fetch_payment_terms and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype) ): @@ -2579,17 +2580,21 @@ class AccountsController(TransactionBase): self.ignore_default_payment_terms_template = 1 def get_order_details(self): + if not self.get("items"): + return None, None, None if self.doctype == "Sales Invoice": - po_or_so = self.get("items") and self.get("items")[0].get("sales_order") - po_or_so_doctype = "Sales Order" - po_or_so_doctype_name = "sales_order" - + prev_doc = self.get("items")[0].get("sales_order") + prev_doctype = "Sales Order" + prev_doctype_name = "sales_order" + elif self.doctype == "Purchase Invoice": + prev_doc = self.get("items")[0].get("purchase_order") + prev_doctype = "Purchase Order" + prev_doctype_name = "purchase_order" else: - po_or_so = self.get("items") and self.get("items")[0].get("purchase_order") - po_or_so_doctype = "Purchase Order" - po_or_so_doctype_name = "purchase_order" - - return po_or_so, po_or_so_doctype, po_or_so_doctype_name + prev_doc = self.get("items")[0].get("prevdoc_docname") + prev_doctype = "Quotation" + prev_doctype_name = "prevdoc_docname" + return prev_doc, prev_doctype, prev_doctype_name def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype): if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index ef727eec8d8..b0b1281aeff 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1052,6 +1052,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if (this.frm.doc.transaction_date) { this.frm.transaction_date = this.frm.doc.transaction_date; frappe.ui.form.trigger(this.frm.doc.doctype, "currency"); + this.recalculate_terms(); } } diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 7a31854d259..593a80d92bc 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -7,7 +7,7 @@ import json import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc -from frappe.utils import flt, getdate, nowdate +from frappe.utils import cint, flt, getdate, nowdate from erpnext.controllers.selling_controller import SellingController @@ -441,6 +441,11 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar filtered_items = args.get("filtered_children", []) child_filter = d.name in filtered_items if filtered_items else True return child_filter + + automatically_fetch_payment_terms = cint( + frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms") + ) + doclist = get_mapped_doc( "Quotation", @@ -449,6 +454,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar "Quotation": { "doctype": "Sales Order", "validation": {"docstatus": ["=", 1]}, + "field_no_map": ["payment_terms_template"], }, "Quotation Item": { "doctype": "Sales Order Item", @@ -465,6 +471,9 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar ignore_permissions=ignore_permissions, ) + if automatically_fetch_payment_terms: + doclist.set_payment_schedule() + return doclist diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 2d1da049653..6f33a753c4e 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -174,6 +174,11 @@ class TestQuotation(FrappeTestCase): quotation.insert() self.assertTrue(quotation.payment_schedule) + + @change_settings( + "Accounts Settings", + {"automatically_fetch_payment_terms": 1}, + ) def test_make_sales_order_terms_copied(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order @@ -317,7 +322,7 @@ class TestQuotation(FrappeTestCase): @change_settings( "Accounts Settings", - {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0}, + {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0,"automatically_fetch_payment_terms": 1,}, ) def test_make_sales_order_with_terms(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order @@ -355,10 +360,13 @@ class TestQuotation(FrappeTestCase): sales_order.save() self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00) - self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date)) + self.assertEqual( + getdate(sales_order.payment_schedule[0].due_date), getdate(quotation.transaction_date) + ) self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00) self.assertEqual( - sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30)) + getdate(sales_order.payment_schedule[1].due_date), + getdate(add_days(quotation.transaction_date, 30)), ) def test_valid_till_before_transaction_date(self): @@ -1057,6 +1065,57 @@ class TestQuotation(FrappeTestCase): quotation.reload() self.assertEqual(quotation.status, "Open") + + @change_settings( + "Accounts Settings", + {"automatically_fetch_payment_terms": 1}, + ) + def test_make_sales_order_with_payment_terms(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order + + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test Payment Terms Template for Quotation", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 0, + }, + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 10, + }, + ], + } + ).save() + + quotation = make_quotation(qty=10, rate=1000, do_not_submit=1) + quotation.transaction_date = add_days(nowdate(), -2) + quotation.valid_till = add_days(nowdate(), 10) + quotation.update({"payment_terms_template": template.name, "payment_schedule": []}) + quotation.save() + quotation.submit() + + self.assertEqual(quotation.payment_schedule[0].payment_amount, 5000) + self.assertEqual(quotation.payment_schedule[1].payment_amount, 5000) + self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date) + self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 10)) + + sales_order = make_sales_order(quotation.name) + sales_order.transaction_date = nowdate() + sales_order.delivery_date = nowdate() + sales_order.save() + + self.assertEqual(sales_order.payment_schedule[0].due_date, sales_order.transaction_date) + self.assertEqual(sales_order.payment_schedule[1].due_date, add_days(sales_order.transaction_date, 10)) + self.assertEqual(sales_order.payment_schedule[0].payment_amount, 5000) + self.assertEqual(sales_order.payment_schedule[1].payment_amount, 5000) + test_records = frappe.get_test_records("Quotation") diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 4bbdb20d311..27e2f72bc4f 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -160,6 +160,7 @@ "language", "additional_info_section", "is_internal_customer", + "ignore_default_payment_terms_template", "represents_company", "column_break_152", "source", @@ -1484,9 +1485,9 @@ }, { "default": "0", - "depends_on": "eval:doc.order_type == 'Maintenance';", "fieldname": "skip_delivery_note", "fieldtype": "Check", + "hidden": 1, "hide_days": 1, "hide_seconds": 1, "label": "Skip Delivery Note", @@ -1665,13 +1666,22 @@ "fieldtype": "Data", "is_virtual": 1, "label": "Last Scanned Warehouse" + }, + { + "default": "0", + "fetch_from": "customer.is_internal_customer", + "fieldname": "ignore_default_payment_terms_template", + "fieldtype": "Check", + "hidden": 1, + "label": "Ignore Default Payment Terms Template", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2026-02-06 11:06:16.092658", + "modified": "2026-03-06 15:03:35.717402", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1750,4 +1760,4 @@ "title_field": "customer_name", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 7ceba32232f..268219e3c2e 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -52,21 +52,17 @@ class SalesOrder(SellingController): from typing import TYPE_CHECKING if TYPE_CHECKING: - from frappe.types import DF - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( - SalesTaxesandCharges, - ) + from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import SalesTaxesandCharges from erpnext.selling.doctype.sales_order_item.sales_order_item import SalesOrderItem from erpnext.selling.doctype.sales_team.sales_team import SalesTeam from erpnext.stock.doctype.packed_item.packed_item import PackedItem + from frappe.types import DF additional_discount_percentage: DF.Float address_display: DF.SmallText | None advance_paid: DF.Currency - advance_payment_status: DF.Literal["Not Requested", "Requested", "Partially Paid", "Fully Paid"] amended_from: DF.Link | None amount_eligible_for_commission: DF.Currency apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] @@ -100,9 +96,7 @@ class SalesOrder(SellingController): customer_group: DF.Link | None customer_name: DF.Data | None delivery_date: DF.Date | None - delivery_status: DF.Literal[ - "Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable" - ] + delivery_status: DF.Literal["Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable"] disable_rounded_total: DF.Check discount_amount: DF.Currency dispatch_address: DF.SmallText | None @@ -111,6 +105,7 @@ class SalesOrder(SellingController): grand_total: DF.Currency group_same_items: DF.Check has_unit_price_items: DF.Check + ignore_default_payment_terms_template: DF.Check ignore_pricing_rule: DF.Check in_words: DF.Data | None incoterm: DF.Link | None @@ -154,18 +149,7 @@ class SalesOrder(SellingController): shipping_rule: DF.Link | None skip_delivery_note: DF.Check source: DF.Link | None - status: DF.Literal[ - "", - "Draft", - "On Hold", - "To Pay", - "To Deliver and Bill", - "To Bill", - "To Deliver", - "Completed", - "Cancelled", - "Closed", - ] + status: DF.Literal["", "Draft", "On Hold", "To Deliver and Bill", "To Bill", "To Deliver", "Completed", "Cancelled", "Closed"] tax_category: DF.Link | None tax_id: DF.Data | None taxes: DF.Table[SalesTaxesandCharges]