Merge branch 'develop' of https://github.com/frappe/erpnext into develop-ritvik-pos-dark
This commit is contained in:
commit
5389dabe19
@ -122,13 +122,10 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_query('payment_term', 'references', function(frm, cdt, cdn) {
|
||||
const child = locals[cdt][cdn];
|
||||
if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) {
|
||||
let payment_term_list = frappe.get_list('Payment Schedule', {'parent': child.reference_name});
|
||||
|
||||
payment_term_list = payment_term_list.map(pt => pt.payment_term);
|
||||
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_payment_terms_for_references",
|
||||
filters: {
|
||||
'name': ['in', payment_term_list]
|
||||
'reference': child.reference_name
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1463,4 +1460,4 @@ frappe.ui.form.on('Payment Entry', {
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -207,6 +207,16 @@ class PaymentEntry(AccountsController):
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def term_based_allocation_enabled_for_reference(
|
||||
self, reference_doctype: str, reference_name: str
|
||||
) -> bool:
|
||||
if reference_doctype and reference_name:
|
||||
if template := frappe.db.get_value(reference_doctype, reference_name, "payment_terms_template"):
|
||||
return frappe.db.get_value(
|
||||
"Payment Terms Template", template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
return False
|
||||
|
||||
def validate_allocated_amount_with_latest_data(self):
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
@ -228,10 +238,23 @@ class PaymentEntry(AccountsController):
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
|
||||
|
||||
for d in self.get("references"):
|
||||
latest = (latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()).get(
|
||||
d.payment_term
|
||||
)
|
||||
for idx, d in enumerate(self.get("references"), start=1):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()
|
||||
|
||||
# If term based allocation is enabled, throw
|
||||
if (
|
||||
d.payment_term is None or d.payment_term == ""
|
||||
) and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||
).format(frappe.bold(d.reference_name), frappe.bold(idx))
|
||||
)
|
||||
|
||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||
latest = latest.get(d.payment_term) or latest.get(None)
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
@ -1633,6 +1656,9 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
"invoice_amount": flt(d.invoice_amount),
|
||||
"outstanding_amount": flt(d.outstanding_amount),
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"allocated_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else d.outstanding_amount,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
"account": d.account,
|
||||
|
@ -1061,6 +1061,101 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
}
|
||||
self.assertDictEqual(ref_details, expected_response)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"unlink_payment_on_cancellation_of_invoice": 1,
|
||||
"delete_linked_ledger_entries": 1,
|
||||
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||
},
|
||||
)
|
||||
def test_overallocation_validation_on_payment_terms(self):
|
||||
"""
|
||||
Validate Allocation on Payment Entry based on Payment Schedule. Upon overallocation, validation error must be thrown.
|
||||
|
||||
"""
|
||||
customer = create_customer()
|
||||
create_payment_terms_template()
|
||||
|
||||
# Validate allocation on base/company currency
|
||||
si1 = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||
si1.payment_terms_template = "Test Receivable Template"
|
||||
si1.save().submit()
|
||||
|
||||
si1.reload()
|
||||
pe = get_payment_entry(si1.doctype, si1.name).save()
|
||||
# Allocated amount should be according to the payment schedule
|
||||
for idx, schedule in enumerate(si1.payment_schedule):
|
||||
with self.subTest(idx=idx):
|
||||
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
|
||||
pe.save()
|
||||
|
||||
# Overallocation validation should trigger
|
||||
pe.paid_amount = 400
|
||||
pe.references[0].allocated_amount = 200
|
||||
pe.references[1].allocated_amount = 200
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
pe.delete()
|
||||
si1.cancel()
|
||||
si1.delete()
|
||||
|
||||
# Validate allocation on foreign currency
|
||||
si2 = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=80,
|
||||
do_not_save=1,
|
||||
)
|
||||
si2.payment_terms_template = "Test Receivable Template"
|
||||
si2.save().submit()
|
||||
|
||||
si2.reload()
|
||||
pe = get_payment_entry(si2.doctype, si2.name).save()
|
||||
# Allocated amount should be according to the payment schedule
|
||||
for idx, schedule in enumerate(si2.payment_schedule):
|
||||
with self.subTest(idx=idx):
|
||||
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
|
||||
pe.save()
|
||||
|
||||
# Overallocation validation should trigger
|
||||
pe.paid_amount = 200
|
||||
pe.references[0].allocated_amount = 100
|
||||
pe.references[1].allocated_amount = 100
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
pe.delete()
|
||||
si2.cancel()
|
||||
si2.delete()
|
||||
|
||||
# Validate allocation in base/company currency on a foreign currency document
|
||||
# when invoice is made is foreign currency, but posted to base/company currency debtors account
|
||||
si3 = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=80,
|
||||
do_not_save=1,
|
||||
)
|
||||
si3.payment_terms_template = "Test Receivable Template"
|
||||
si3.save().submit()
|
||||
|
||||
si3.reload()
|
||||
pe = get_payment_entry(si3.doctype, si3.name).save()
|
||||
# Allocated amount should be equal to payment term outstanding
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
for idx, ref in enumerate(pe.references):
|
||||
with self.subTest(idx=idx):
|
||||
self.assertEqual(ref.payment_term_outstanding, ref.allocated_amount)
|
||||
pe.save()
|
||||
|
||||
# Overallocation validation should trigger
|
||||
pe.paid_amount = 16000
|
||||
pe.references[0].allocated_amount = 8000
|
||||
pe.references[1].allocated_amount = 8000
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
pe.delete()
|
||||
si3.cancel()
|
||||
si3.delete()
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
@ -1150,3 +1245,17 @@ def create_payment_terms_template_with_discount(
|
||||
def create_payment_term(name):
|
||||
if not frappe.db.exists("Payment Term", name):
|
||||
frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert()
|
||||
|
||||
|
||||
def create_customer(name="_Test Customer 2 USD", currency="USD"):
|
||||
customer = None
|
||||
if frappe.db.exists("Customer", name):
|
||||
customer = name
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = name
|
||||
customer.default_currency = currency
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
customer = customer.name
|
||||
return customer
|
||||
|
@ -1112,7 +1112,8 @@ def get_autoname_with_number(number_value, doc_title, company):
|
||||
|
||||
def parse_naming_series_variable(doc, variable):
|
||||
if variable == "FY":
|
||||
return get_fiscal_year(date=doc.get("posting_date"), company=doc.get("company"))[0]
|
||||
date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
|
||||
return get_fiscal_year(date=date, company=doc.get("company"))[0]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -874,3 +874,18 @@ def get_fields(doctype, fields=None):
|
||||
fields.insert(1, meta.title_field.strip())
|
||||
|
||||
return unique(fields)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len, filters) -> list:
|
||||
terms = []
|
||||
if filters:
|
||||
terms = frappe.db.get_all(
|
||||
"Payment Schedule",
|
||||
filters={"parent": filters.get("reference")},
|
||||
fields=["payment_term"],
|
||||
limit=page_len,
|
||||
as_list=1,
|
||||
)
|
||||
return terms
|
||||
|
@ -7,7 +7,16 @@ def execute():
|
||||
frappe.reload_doc("accounts", "doctype", "overdue_payment")
|
||||
frappe.reload_doc("accounts", "doctype", "dunning")
|
||||
|
||||
all_dunnings = frappe.get_all("Dunning", filters={"docstatus": ("!=", 2)}, pluck="name")
|
||||
# Migrate schema of all uncancelled dunnings
|
||||
filters = {"docstatus": ("!=", 2)}
|
||||
|
||||
can_edit_accounts_after = get_accounts_closing_date()
|
||||
if can_edit_accounts_after:
|
||||
# Get dunnings after the date when accounts were frozen/closed
|
||||
filters["posting_date"] = (">", can_edit_accounts_after)
|
||||
|
||||
all_dunnings = frappe.get_all("Dunning", filters=filters, pluck="name")
|
||||
|
||||
for dunning_name in all_dunnings:
|
||||
dunning = frappe.get_doc("Dunning", dunning_name)
|
||||
if not dunning.sales_invoice:
|
||||
@ -41,9 +50,29 @@ def execute():
|
||||
dunning.flags.ignore_validate_update_after_submit = True
|
||||
dunning.save()
|
||||
|
||||
if dunning.status != "Resolved":
|
||||
# Reverse entries only if dunning is submitted and not resolved
|
||||
if dunning.docstatus == 1 and dunning.status != "Resolved":
|
||||
# With the new logic, dunning amount gets recorded as additional income
|
||||
# at time of payment. We don't want to record the dunning amount twice,
|
||||
# so we reverse previous GL Entries that recorded the dunning amount at
|
||||
# time of submission of the Dunning.
|
||||
make_reverse_gl_entries(voucher_type="Dunning", voucher_no=dunning.name)
|
||||
|
||||
|
||||
def get_accounts_closing_date():
|
||||
"""Get the date when accounts were frozen/closed"""
|
||||
accounts_frozen_till = frappe.db.get_single_value(
|
||||
"Accounts Settings", "acc_frozen_upto"
|
||||
) # always returns datetime.date
|
||||
|
||||
period_closing_date = frappe.db.get_value(
|
||||
"Period Closing Voucher", {"docstatus": 1}, "posting_date", order_by="posting_date desc"
|
||||
)
|
||||
|
||||
# Set most recent frozen/closing date as filter
|
||||
if accounts_frozen_till and period_closing_date:
|
||||
can_edit_accounts_after = max(accounts_frozen_till, period_closing_date)
|
||||
else:
|
||||
can_edit_accounts_after = accounts_frozen_till or period_closing_date
|
||||
|
||||
return can_edit_accounts_after
|
||||
|
Loading…
x
Reference in New Issue
Block a user