Skip to content
Closed
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
61 changes: 56 additions & 5 deletions erpnext/accounts/doctype/payment_schedule/payment_schedule.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -172,12 +177,58 @@
"label": "Paid Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fetch_from": "payment_term.due_date_based_on",
"fetch_if_empty": 1,
"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)",
"fetch_if_empty": 1,
"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'",
"fetch_if_empty": 1,
"fieldname": "credit_months",
"fieldtype": "Int",
"label": "Credit Months",
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "discount",
"fetch_from": "payment_term.discount_validity_based_on",
"fetch_if_empty": 1,
"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",
"fetch_from": "payment_term.discount_validity",
"fetch_if_empty": 1,
"fieldname": "discount_validity",
"fieldtype": "Int",
"label": "Discount Validity",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-11 11:06:51.792982",
"modified": "2025-07-31 07:38:37.383470",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
Expand All @@ -189,4 +240,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}
15 changes: 15 additions & 0 deletions erpnext/accounts/doctype/payment_schedule/payment_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,27 @@ class PaymentSchedule(Document):
base_outstanding: DF.Currency
base_paid_amount: DF.Currency
base_payment_amount: DF.Currency
credit_days: DF.Int
credit_months: DF.Int
description: DF.SmallText | None
discount: DF.Float
discount_date: DF.Date | None
discount_type: DF.Literal["Percentage", "Amount"]
discount_validity: DF.Int
discount_validity_based_on: DF.Literal[
"",
"Day(s) after invoice date",
"Day(s) after the end of the invoice month",
"Month(s) after the end of the invoice month",
]
discounted_amount: DF.Currency
due_date: DF.Date
due_date_based_on: DF.Literal[
"",
"Day(s) after invoice date",
"Day(s) after the end of the invoice month",
"Month(s) after the end of the invoice month",
]
invoice_portion: DF.Percent
mode_of_payment: DF.Link | None
outstanding: DF.Currency
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,7 @@
"fetch_if_empty": 1,
"fieldname": "discount_validity",
"fieldtype": "Int",
"label": "Discount Validity",
"mandatory_depends_on": "discount"
"label": "Discount Validity"
},
{
"fieldname": "section_break_4",
Expand All @@ -151,15 +150,16 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:11.829680",
"modified": "2025-07-31 07:42:37.442017",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Terms Template Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2147,19 +2147,16 @@ def test_gl_entries_for_standalone_debit_note(self):
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
self.assertAlmostEqual(rate, 500)

@IntegrationTestCase.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",
Expand All @@ -2185,7 +2182,6 @@ def test_payment_allocation_for_payment_terms(self):
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",
Expand Down
14 changes: 3 additions & 11 deletions erpnext/buying/doctype/purchase_order/test_purchase_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,12 +540,8 @@ def test_purchase_order_on_hold(self):
self.assertRaises(frappe.ValidationError, pr.submit)
self.assertRaises(frappe.ValidationError, pi.submit)

@IntegrationTestCase.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)
Expand All @@ -569,7 +565,6 @@ def test_make_purchase_invoice_with_terms(self):
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
Expand Down Expand Up @@ -717,6 +712,7 @@ def test_default_payment_terms(self):
)
self.assertEqual(due_date, "2023-03-31")

@IntegrationTestCase.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"
Expand Down Expand Up @@ -910,18 +906,16 @@ def test_blanket_order_on_po_close_and_open(self):
bo.load_from_db()
self.assertEqual(bo.items[0].ordered_qty, 5)

@IntegrationTestCase.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"
Expand All @@ -935,8 +929,6 @@ def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
# 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.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
Expand Down
21 changes: 19 additions & 2 deletions erpnext/controllers/accounts_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -2594,6 +2594,7 @@ def fetch_payment_terms_from_order(

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 = {
Expand All @@ -2606,6 +2607,17 @@ def fetch_payment_terms_from_order(
}

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.credit_days
payment_schedule["credit_days"] = schedule.credit_days
payment_schedule["credit_months"] = 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"] = schedule.discount_validity

payment_schedule["payment_amount"] = flt(
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
schedule.precision("payment_amount"),
Expand Down Expand Up @@ -3471,6 +3483,11 @@ def get_payment_term_details(
term_details.discount = term.discount
term_details.outstanding = term_details.payment_amount
term_details.mode_of_payment = term.mode_of_payment
term_details.due_date_based_on = term.due_date_based_on
term_details.credit_days = term.credit_days
term_details.credit_months = term.credit_months
term_details.discount_validity_based_on = term.discount_validity_based_on
term_details.discount_validity = term.discount_validity

if bill_date:
term_details.due_date = get_due_date(term, bill_date)
Expand All @@ -3485,8 +3502,8 @@ def get_payment_term_details(
return term_details


def get_due_date(term, posting_date=None, bill_date=None):
due_date = None
def get_due_date(term, posting_date=None, bill_date=None, default_date=None):
due_date = default_date
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)
Expand Down
26 changes: 21 additions & 5 deletions erpnext/public/js/controllers/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -1103,12 +1103,22 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}

due_date(doc, cdt) {
discount_date(doc, cdt,cdn){
// Remove fields as due_date is auto-managed by payment terms
["discount_validity","discount_validity_based_on"].forEach(function(field) {
frappe.model.set_value(cdt, cdn, field,"")
});

}

due_date(doc, cdt,cdn) {
// 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
if (doc.doctype !== cdt) {
// triggered by change to the due_date field in payment schedule child table
// do nothing to avoid infinite clearing loop
// Remove fields as due_date is auto-managed by payment terms
["due_date_based_on", "credit_days", "credit_months"].forEach(function(field) {
frappe.model.set_value(cdt, cdn, field,"")
});
return;
}

Expand Down Expand Up @@ -2638,6 +2648,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
payment_term(doc, cdt, cdn) {
const me = this;
var row = locals[cdt][cdn];
// 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",
Expand All @@ -2650,15 +2665,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
callback: function(r) {
if(r.message && !r.exc) {
for (var d in r.message) {
frappe.model.set_value(cdt, cdn, d, r.message[d]);
for (let d in r.message) {
row[d] = r.message[d];
const company_currency = me.get_company_currency();
me.update_payment_schedule_grid_labels(company_currency);
}
}
}
})
}
me.frm.refresh_field("payment_schedule");
}

against_blanket_order(doc, cdt, cdn) {
Expand Down
19 changes: 7 additions & 12 deletions erpnext/selling/doctype/sales_order/test_sales_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from frappe.utils import add_days, flt, getdate, nowdate, today

from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
from erpnext.controllers.accounts_controller import InvalidQtyError, get_due_date, update_child_qty_rate
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
make_maintenance_schedule,
)
Expand Down Expand Up @@ -1678,14 +1678,13 @@ def test_so_cancellation_after_work_order_submission(self):
so.load_from_db()
self.assertRaises(frappe.LinkExistsError, so.cancel)

@IntegrationTestCase.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"
Expand All @@ -1699,8 +1698,6 @@ def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
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

Expand Down Expand Up @@ -2569,16 +2566,14 @@ def test_item_tax_transfer_from_sales_to_purchase(self):
self.assertEqual(po.taxes[0].tax_amount, 2)


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)

Expand Down
Loading
Loading