From 0b43d518afc6eaf06ec71ed30984b129e19de729 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 00:16:20 +0200 Subject: [PATCH 01/10] refactor(Supplier): custom buttons call make methods (backport #49840) (#49841) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/buying/doctype/supplier/supplier.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 1a11b8b8f27..3f254c28317 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -89,21 +89,9 @@ frappe.ui.form.on("Supplier", { __("View") ); - frm.add_custom_button( - __("Bank Account"), - function () { - erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name); - }, - __("Create") - ); + frm.add_custom_button(__("Bank Account"), () => frm.make_methods["Bank Account"](), __("Create")); - frm.add_custom_button( - __("Pricing Rule"), - function () { - erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name); - }, - __("Create") - ); + frm.add_custom_button(__("Pricing Rule"), () => frm.make_methods["Pricing Rule"](), __("Create")); frm.add_custom_button( __("Get Supplier Group Details"), From 9b5183e0e5bd3077a2e36c746db6b602d3d08155 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 19:22:04 +0200 Subject: [PATCH 02/10] fix(Common Code): fetch canonical URI from Code List (backport #49882) (#49883) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> fix(Common Code): fetch canonical URI from Code List (#49882) --- erpnext/edi/doctype/common_code/common_code.json | 13 +++++++++++-- erpnext/edi/doctype/common_code/common_code.py | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/edi/doctype/common_code/common_code.json b/erpnext/edi/doctype/common_code/common_code.json index b2cb43fa575..f753bc94b6b 100644 --- a/erpnext/edi/doctype/common_code/common_code.json +++ b/erpnext/edi/doctype/common_code/common_code.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "code_list", + "canonical_uri", "title", "common_code", "description", @@ -71,10 +72,17 @@ "in_list_view": 1, "label": "Description", "max_height": "60px" + }, + { + "fetch_from": "code_list.canonical_uri", + "fieldname": "canonical_uri", + "fieldtype": "Data", + "label": "Canonical URI" } ], + "grid_page_length": 50, "links": [], - "modified": "2024-11-06 07:46:17.175687", + "modified": "2025-10-04 17:22:28.176155", "modified_by": "Administrator", "module": "EDI", "name": "Common Code", @@ -94,10 +102,11 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "common_code,description", "show_title_field_in_link": 1, "sort_field": "creation", "sort_order": "DESC", "states": [], "title_field": "title" -} \ No newline at end of file +} diff --git a/erpnext/edi/doctype/common_code/common_code.py b/erpnext/edi/doctype/common_code/common_code.py index d558b2d282f..d1fd88350be 100644 --- a/erpnext/edi/doctype/common_code/common_code.py +++ b/erpnext/edi/doctype/common_code/common_code.py @@ -22,6 +22,7 @@ class CommonCode(Document): additional_data: DF.Code | None applies_to: DF.Table[DynamicLink] + canonical_uri: DF.Data | None code_list: DF.Link common_code: DF.Data description: DF.SmallText | None From b7d76d8fad5e92a60a5e5f3f819f8d5ef314f63e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 6 Oct 2025 11:27:55 +0530 Subject: [PATCH 03/10] fix: Set paid amount automatically only if return entry validated and has negative grand total (#49829) (cherry picked from commit dcbcc596f2004643acde442128137e0ac778a4fe) # Conflicts: # erpnext/public/js/controllers/taxes_and_totals.js --- erpnext/controllers/accounts_controller.py | 10 +++++----- erpnext/public/js/controllers/taxes_and_totals.js | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 21567a60abe..756fa5f6d09 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -213,6 +213,11 @@ class AccountsController(TransactionBase): self.validate_date_with_fiscal_year() self.validate_party_accounts() + if self.doctype in ["Sales Invoice", "Purchase Invoice"]: + if self.is_return: + self.validate_qty() + else: + self.validate_deferred_start_and_end_date() self.validate_inter_company_reference() @@ -258,11 +263,6 @@ class AccountsController(TransactionBase): self.set_advance_gain_or_loss() - if self.is_return: - self.validate_qty() - else: - self.validate_deferred_start_and_end_date() - self.validate_deferred_income_expense_account() self.set_inter_company_account() diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index dca9f353457..8e39046c923 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -76,9 +76,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // Update paid amount on return/debit note creation if ( +<<<<<<< HEAD this.frm.doc.doctype === "Purchase Invoice" && this.frm.doc.is_return && (this.frm.doc.grand_total > this.frm.doc.paid_amount) +======= + this.frm.doc.doctype === "Purchase Invoice" && + this.frm.doc.is_return && + this.frm.doc.grand_total < 0 && + this.frm.doc.grand_total > this.frm.doc.paid_amount +>>>>>>> dcbcc596f2 (fix: Set paid amount automatically only if return entry validated and has negative grand total (#49829)) ) { this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total")); } From 2c383a69f94fa3f18f8aaf945f6ae0f79ff3bd2e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 6 Oct 2025 12:19:22 +0530 Subject: [PATCH 04/10] fix: resolved conflict --- erpnext/public/js/controllers/taxes_and_totals.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 8e39046c923..0d0b12fbf18 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -76,16 +76,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // Update paid amount on return/debit note creation if ( -<<<<<<< HEAD - this.frm.doc.doctype === "Purchase Invoice" - && this.frm.doc.is_return - && (this.frm.doc.grand_total > this.frm.doc.paid_amount) -======= this.frm.doc.doctype === "Purchase Invoice" && this.frm.doc.is_return && this.frm.doc.grand_total < 0 && this.frm.doc.grand_total > this.frm.doc.paid_amount ->>>>>>> dcbcc596f2 (fix: Set paid amount automatically only if return entry validated and has negative grand total (#49829)) ) { this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total")); } From 30a6fe55cae83c2836ed5270814e096c53cf25f8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 13:34:24 +0200 Subject: [PATCH 05/10] fix: linter; dont change doc after DB update (backport #49907) (#49909) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Co-authored-by: ruthra kumar fix: linter; dont change doc after DB update (#49907) --- .../selling/doctype/selling_settings/selling_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index d977807e7dc..490a2740117 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -37,15 +37,15 @@ class SellingSettings(Document): ) def toggle_hide_tax_id(self): - self.hide_tax_id = cint(self.hide_tax_id) + _hide_tax_id = cint(self.hide_tax_id) # Make property setters to hide tax_id fields for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"): make_property_setter( - doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False + doctype, "tax_id", "hidden", _hide_tax_id, "Check", validate_fields_for_doctype=False ) make_property_setter( - doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False + doctype, "tax_id", "print_hide", _hide_tax_id, "Check", validate_fields_for_doctype=False ) def toggle_editable_rate_for_bundle_items(self): From dcae024fd2de546364951e3979c34cc1df10c752 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 23 Sep 2025 19:10:23 +0530 Subject: [PATCH 06/10] fix: do not fetch disabled item tax template (cherry picked from commit b10cf4a928447dc768a1d90fb27fbc86d536ed14) # Conflicts: # erpnext/public/js/controllers/transaction.js # erpnext/stock/get_item_details.py --- erpnext/public/js/controllers/transaction.js | 9 +++++++++ erpnext/stock/get_item_details.py | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 7ab8533368f..c1f014fb971 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2200,10 +2200,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return doc.company ? {filters: {company: doc.company}} : {}; } else { let filters = { +<<<<<<< HEAD 'item_code': item.item_code, 'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], 'item_group': item.item_group, } +======= + item_code: item.item_code, + valid_from: ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], + item_group: item.item_group, + base_net_rate: item.base_net_rate, + disabled: 0, + }; +>>>>>>> b10cf4a928 (fix: do not fetch disabled item tax template) if (doc.tax_category) filters['tax_category'] = doc.tax_category; diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index bdc442e07a1..bf35785803a 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -635,8 +635,15 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity = [] for tax in taxes: +<<<<<<< HEAD tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, "company") if tax_company == args["company"]: +======= + disabled, tax_company = frappe.get_cached_value( + "Item Tax Template", tax.item_tax_template, ["disabled", "company"] + ) + if not disabled and tax_company == ctx["company"]: +>>>>>>> b10cf4a928 (fix: do not fetch disabled item tax template) if tax.valid_from or tax.maximum_net_rate: # In purchase Invoice first preference will be given to supplier invoice date # if supplier date is not present then posting date From aa20e224fbd3668b5aa79de593edc07dd9b976dd Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 7 Oct 2025 11:00:02 +0530 Subject: [PATCH 07/10] chore: fix conflicts --- erpnext/stock/get_item_details.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index bf35785803a..cdbc719df97 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -635,15 +635,8 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity = [] for tax in taxes: -<<<<<<< HEAD - tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, "company") - if tax_company == args["company"]: -======= - disabled, tax_company = frappe.get_cached_value( - "Item Tax Template", tax.item_tax_template, ["disabled", "company"] - ) - if not disabled and tax_company == ctx["company"]: ->>>>>>> b10cf4a928 (fix: do not fetch disabled item tax template) + disabled, tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, ["disabled", "company"]) + if not disabled and tax_company == args["company"]: if tax.valid_from or tax.maximum_net_rate: # In purchase Invoice first preference will be given to supplier invoice date # if supplier date is not present then posting date From 819667ab24c58acc5c728f526e4212e2dc16a414 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 7 Oct 2025 11:00:45 +0530 Subject: [PATCH 08/10] chore: fix conflicts --- erpnext/public/js/controllers/transaction.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index c1f014fb971..5fa63cb9c02 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2200,19 +2200,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return doc.company ? {filters: {company: doc.company}} : {}; } else { let filters = { -<<<<<<< HEAD 'item_code': item.item_code, 'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], 'item_group': item.item_group, + 'disabled': 0, } -======= - item_code: item.item_code, - valid_from: ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], - item_group: item.item_group, - base_net_rate: item.base_net_rate, - disabled: 0, - }; ->>>>>>> b10cf4a928 (fix: do not fetch disabled item tax template) if (doc.tax_category) filters['tax_category'] = doc.tax_category; From 014f5bff5c5b8cae3e0719f5d87ebbe1bfd31b8d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 7 Oct 2025 11:24:49 +0530 Subject: [PATCH 09/10] chore: fix linters issue --- erpnext/stock/get_item_details.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index cdbc719df97..bd263c6c6a1 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -635,7 +635,9 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False): taxes_with_no_validity = [] for tax in taxes: - disabled, tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, ["disabled", "company"]) + disabled, tax_company = frappe.get_cached_value( + "Item Tax Template", tax.item_tax_template, ["disabled", "company"] + ) if not disabled and tax_company == args["company"]: if tax.valid_from or tax.maximum_net_rate: # In purchase Invoice first preference will be given to supplier invoice date From d82106cb76122a1f3dedba88a30c0ff111a1ef0c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:10:46 +0530 Subject: [PATCH 10/10] feat: dynamic due date in payment terms when fetched from order (backport #48864) (#49937) * feat: dynamic due date in payment terms when fetched from order (#48864) * fix: dynamic due date when payment terms are fetched from order * fix(test): use change_settings decorator for settings enable and disable * fix(test): compare schedule for due_date dynamically * fix: save conditions for due date at invoice level * fix: make fields read only and on change of date unset the date condition fields * fix: remove fetch_form * fix: correct field assingment * fix: revert unwanted changes * refactor: streamline payment term field assignments and enhance discount date handling * refactor: remove payment_term from fields_to_copy and optimize currency handling in transaction callback * refactor: ensure default values for payment schedule and discount validity fields (cherry picked from commit 3c70cbbaf8929b358008c2bab6e0c799466ea8b7) # Conflicts: # erpnext/accounts/doctype/payment_schedule/payment_schedule.json # erpnext/accounts/doctype/payment_schedule/payment_schedule.py # erpnext/public/js/controllers/transaction.js # erpnext/selling/doctype/sales_order/test_sales_order.py # erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py * chore: resolve conflicts --------- Co-authored-by: Lakshit Jain --- .../payment_schedule/payment_schedule.json | 53 +++++++++++++++++-- .../payment_terms_template_detail.json | 2 +- .../purchase_invoice/test_purchase_invoice.py | 6 +-- .../purchase_order/test_purchase_order.py | 14 ++--- erpnext/controllers/accounts_controller.py | 46 ++++++++++++---- erpnext/public/js/controllers/transaction.js | 41 ++++++++++---- .../doctype/sales_order/test_sales_order.py | 19 +++---- .../delivery_note/test_delivery_note.py | 6 +-- .../purchase_receipt/test_purchase_receipt.py | 6 +-- 9 files changed, 129 insertions(+), 64 deletions(-) diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json index dde9980ce53..2572d4c8153 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json @@ -10,14 +10,19 @@ "description", "section_break_4", "due_date", + "invoice_portion", "mode_of_payment", "column_break_5", - "invoice_portion", + "due_date_based_on", + "credit_days", + "credit_months", "section_break_6", - "discount_type", "discount_date", - "column_break_9", "discount", + "discount_type", + "column_break_9", + "discount_validity_based_on", + "discount_validity", "section_break_9", "payment_amount", "outstanding", @@ -155,12 +160,50 @@ "fieldtype": "Currency", "label": "Payment Amount (Company Currency)", "options": "Company:company:default_currency" + }, + { + "fieldname": "due_date_based_on", + "fieldtype": "Select", + "label": "Due Date Based On", + "options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month", + "read_only": 1 + }, + { + "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)", + "fieldname": "credit_days", + "fieldtype": "Int", + "label": "Credit Days", + "non_negative": 1, + "read_only": 1 + }, + { + "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'", + "fieldname": "credit_months", + "fieldtype": "Int", + "label": "Credit Months", + "non_negative": 1, + "read_only": 1 + }, + { + "depends_on": "discount", + "fieldname": "discount_validity_based_on", + "fieldtype": "Select", + "label": "Discount Validity Based On", + "options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month", + "read_only": 1 + }, + { + "depends_on": "discount_validity_based_on", + "fieldname": "discount_validity", + "fieldtype": "Int", + "label": "Discount Validity", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-09-16 13:57:06.382859", + "modified": "2025-07-31 08:38:25.820701", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Schedule", @@ -171,4 +214,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json index 20b3dca6aae..fca9eaa53c2 100644 --- a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json +++ b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json @@ -161,4 +161,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 7d52fdcff25..fa01e452e90 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1816,19 +1816,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) self.assertAlmostEqual(rate, 500) + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_payment_allocation_for_payment_terms(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import ( create_pr_against_po, create_purchase_order, ) - from erpnext.selling.doctype.sales_order.test_sales_order import ( - automatically_fetch_payment_terms, - ) from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice as make_pi_from_pr, ) - automatically_fetch_payment_terms() frappe.db.set_value( "Payment Terms Template", "_Test Payment Term Template", @@ -1854,7 +1851,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): pi = make_pi_from_pr(pr.name) self.assertEqual(pi.payment_schedule[0].payment_amount, 1000) - automatically_fetch_payment_terms(enable=0) frappe.db.set_value( "Payment Terms Template", "_Test Payment Term Template", diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 38cb0fbf7ab..d54ad8d33c7 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -526,12 +526,8 @@ class TestPurchaseOrder(FrappeTestCase): self.assertRaises(frappe.ValidationError, pr.submit) self.assertRaises(frappe.ValidationError, pi.submit) + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_make_purchase_invoice_with_terms(self): - from erpnext.selling.doctype.sales_order.test_sales_order import ( - automatically_fetch_payment_terms, - ) - - automatically_fetch_payment_terms() po = create_purchase_order(do_not_save=True) self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name) @@ -555,7 +551,6 @@ class TestPurchaseOrder(FrappeTestCase): self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date)) self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0) self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30)) - automatically_fetch_payment_terms(enable=0) def test_warehouse_company_validation(self): from erpnext.stock.utils import InvalidWarehouseCompany @@ -703,6 +698,7 @@ class TestPurchaseOrder(FrappeTestCase): ) self.assertEqual(due_date, "2023-03-31") + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 0}) def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self): po = create_purchase_order(do_not_save=1) po.payment_terms_template = "_Test Payment Term Template" @@ -834,18 +830,16 @@ class TestPurchaseOrder(FrappeTestCase): bo.load_from_db() self.assertEqual(bo.items[0].ordered_qty, 5) + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, ) from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.selling.doctype.sales_order.test_sales_order import ( - automatically_fetch_payment_terms, compare_payment_schedules, ) - automatically_fetch_payment_terms() - po = create_purchase_order(qty=10, rate=100, do_not_save=1) create_payment_terms_template() po.payment_terms_template = "Test Receivable Template" @@ -859,8 +853,6 @@ class TestPurchaseOrder(FrappeTestCase): # self.assertEqual(po.payment_terms_template, pi.payment_terms_template) compare_payment_schedules(self, po, pi) - automatically_fetch_payment_terms(enable=0) - def test_internal_transfer_flow(self): from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( make_inter_company_purchase_invoice, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 756fa5f6d09..77d4db8504f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2239,6 +2239,7 @@ class AccountsController(TransactionBase): self.payment_schedule = [] self.payment_terms_template = po_or_so.payment_terms_template + posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date") for schedule in po_or_so.payment_schedule: payment_schedule = { @@ -2251,6 +2252,17 @@ class AccountsController(TransactionBase): } if automatically_fetch_payment_terms: + if schedule.due_date_based_on: + payment_schedule["due_date"] = get_due_date(schedule, posting_date) + payment_schedule["due_date_based_on"] = schedule.due_date_based_on + payment_schedule["credit_days"] = cint(schedule.credit_days) + payment_schedule["credit_months"] = cint(schedule.credit_months) + + if schedule.discount_validity_based_on: + payment_schedule["discount_date"] = get_discount_date(schedule, posting_date) + payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on + payment_schedule["discount_validity"] = cint(schedule.discount_validity) + payment_schedule["payment_amount"] = flt( grand_total * flt(payment_schedule["invoice_portion"]) / 100, schedule.precision("payment_amount"), @@ -2987,14 +2999,26 @@ def get_payment_term_details( term = frappe.get_doc("Payment Term", term) else: term_details.payment_term = term.payment_term - term_details.description = term.description - term_details.invoice_portion = term.invoice_portion + + fields_to_copy = [ + "description", + "invoice_portion", + "discount_type", + "discount", + "mode_of_payment", + "due_date_based_on", + "credit_days", + "credit_months", + "discount_validity_based_on", + "discount_validity", + ] + + for field in fields_to_copy: + term_details[field] = term.get(field) + term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100 term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100 - term_details.discount_type = term.discount_type - term_details.discount = term.discount term_details.outstanding = term_details.payment_amount - term_details.mode_of_payment = term.mode_of_payment if bill_date: term_details.due_date = get_due_date(term, bill_date) @@ -3013,11 +3037,11 @@ def get_due_date(term, posting_date=None, bill_date=None): due_date = None date = bill_date or posting_date if term.due_date_based_on == "Day(s) after invoice date": - due_date = add_days(date, term.credit_days) + due_date = add_days(date, cint(term.credit_days)) elif term.due_date_based_on == "Day(s) after the end of the invoice month": - due_date = add_days(get_last_day(date), term.credit_days) + due_date = add_days(get_last_day(date), cint(term.credit_days)) elif term.due_date_based_on == "Month(s) after the end of the invoice month": - due_date = get_last_day(add_months(date, term.credit_months)) + due_date = get_last_day(add_months(date, cint(term.credit_months))) return due_date @@ -3025,11 +3049,11 @@ def get_discount_date(term, posting_date=None, bill_date=None): discount_validity = None date = bill_date or posting_date if term.discount_validity_based_on == "Day(s) after invoice date": - discount_validity = add_days(date, term.discount_validity) + discount_validity = add_days(date, cint(term.discount_validity)) elif term.discount_validity_based_on == "Day(s) after the end of the invoice month": - discount_validity = add_days(get_last_day(date), term.discount_validity) + discount_validity = add_days(get_last_day(date), cint(term.discount_validity)) elif term.discount_validity_based_on == "Month(s) after the end of the invoice month": - discount_validity = get_last_day(add_months(date, term.discount_validity)) + discount_validity = get_last_day(add_months(date, cint(term.discount_validity))) return discount_validity diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5fa63cb9c02..a37b4048e10 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -878,6 +878,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + discount_date(doc, cdt, cdn) { + // Remove fields as discount_date is auto-managed by payment terms + const row = locals[cdt][cdn]; + ["discount_validity", "discount_validity_based_on"].forEach((field) => { + row[field] = ""; + }); + this.frm.refresh_field("payment_schedule"); + } + due_date() { // due_date is to be changed, payment terms template and/or payment schedule must // be removed as due_date is automatically changed based on payment terms @@ -2245,7 +2254,18 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe payment_term(doc, cdt, cdn) { const me = this; var row = locals[cdt][cdn]; - if(row.payment_term) { + // empty date condition fields + [ + "due_date_based_on", + "credit_days", + "credit_months", + "discount_validity", + "discount_validity_based_on", + ].forEach(function (field) { + row[field] = ""; + }); + + if (row.payment_term) { frappe.call({ method: "erpnext.controllers.accounts_controller.get_payment_term_details", args: { @@ -2255,16 +2275,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe grand_total: this.frm.doc.rounded_total || this.frm.doc.grand_total, base_grand_total: this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total }, - callback: function(r) { - if(r.message && !r.exc) { - for (var d in r.message) { - frappe.model.set_value(cdt, cdn, d, r.message[d]); - const company_currency = me.get_company_currency(); - me.update_payment_schedule_grid_labels(company_currency); + callback: function (r) { + if (r.message && !r.exc) { + const company_currency = me.get_company_currency(); + for (let d in r.message) { + row[d] = r.message[d]; } + me.update_payment_schedule_grid_labels(company_currency); + me.frm.refresh_field("payment_schedule"); } - } - }) + }, + }); + } else { + me.frm.refresh_field("payment_schedule"); } } diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 2f795cae5c4..5a71dac8200 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -from erpnext.controllers.accounts_controller import update_child_qty_rate +from erpnext.controllers.accounts_controller import get_due_date, update_child_qty_rate from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( make_maintenance_schedule, ) @@ -1759,14 +1759,13 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): so.load_from_db() self.assertRaises(frappe.LinkExistsError, so.cancel) + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, ) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice - automatically_fetch_payment_terms() - so = make_sales_order(uom="Nos", do_not_save=1) create_payment_terms_template() so.payment_terms_template = "Test Receivable Template" @@ -1780,8 +1779,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertEqual(so.payment_terms_template, si.payment_terms_template) compare_payment_schedules(self, so, si) - automatically_fetch_payment_terms(enable=0) - def test_zero_amount_sales_order_billing_status(self): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -2284,16 +2281,14 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertRaises(frappe.ValidationError, so1.update_status, "Draft") -def automatically_fetch_payment_terms(enable=1): - accounts_settings = frappe.get_doc("Accounts Settings") - accounts_settings.automatically_fetch_payment_terms = enable - accounts_settings.save() - - def compare_payment_schedules(doc, doc1, doc2): for index, schedule in enumerate(doc1.get("payment_schedule")): + posting_date = doc1.get("bill_date") or doc1.get("posting_date") or doc1.get("transaction_date") + due_date = schedule.due_date + if schedule.due_date_based_on: + due_date = get_due_date(schedule, posting_date=posting_date) doc.assertEqual(schedule.payment_term, doc2.payment_schedule[index].payment_term) - doc.assertEqual(getdate(schedule.due_date), doc2.payment_schedule[index].due_date) + doc.assertEqual(due_date, doc2.payment_schedule[index].due_date) doc.assertEqual(schedule.invoice_portion, doc2.payment_schedule[index].invoice_portion) doc.assertEqual(schedule.payment_amount, doc2.payment_schedule[index].payment_amount) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 1dfa4c0a10d..507fb78663e 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -13,7 +13,6 @@ from erpnext.accounts.utils import get_balance_on from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.test_sales_order import ( - automatically_fetch_payment_terms, compare_payment_schedules, create_dn_against_so, make_sales_order, @@ -1026,14 +1025,13 @@ class TestDeliveryNote(FrappeTestCase): self.assertTrue("TESTBATCH" in dn.packed_items[0].batch_no, "Batch number not added in packed item") + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, ) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice - automatically_fetch_payment_terms() - so = make_sales_order(uom="Nos", do_not_save=1) create_payment_terms_template() so.payment_terms_template = "Test Receivable Template" @@ -1053,8 +1051,6 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(so.payment_terms_template, si.payment_terms_template) compare_payment_schedules(self, so, si) - automatically_fetch_payment_terms(enable=0) - def test_returned_qty_in_return_dn(self): # SO ---> SI ---> DN # | diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 199a44b3734..97ce8c16d43 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1056,6 +1056,7 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount) + @change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1}) def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, @@ -1066,12 +1067,9 @@ class TestPurchaseReceipt(FrappeTestCase): make_pr_against_po, ) from erpnext.selling.doctype.sales_order.test_sales_order import ( - automatically_fetch_payment_terms, compare_payment_schedules, ) - automatically_fetch_payment_terms() - po = create_purchase_order(qty=10, rate=100, do_not_save=1) create_payment_terms_template() po.payment_terms_template = "Test Receivable Template" @@ -1089,8 +1087,6 @@ class TestPurchaseReceipt(FrappeTestCase): # self.assertEqual(po.payment_terms_template, pi.payment_terms_template) compare_payment_schedules(self, po, pi) - automatically_fetch_payment_terms(enable=0) - @change_settings("Stock Settings", {"allow_negative_stock": 1}) def test_neg_to_positive(self): from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry