From 5fa408a1234f4aabb8c9443992ce07d925e336f1 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 22 Jul 2024 11:25:01 +0530 Subject: [PATCH 01/60] fix: multiple issues in Payment Request --- .../doctype/payment_entry/payment_entry.js | 36 +++- .../doctype/payment_entry/payment_entry.py | 132 ++++++++++++- .../payment_entry_reference.json | 11 +- .../payment_entry_reference.py | 1 + .../payment_request/payment_request.js | 4 +- .../payment_request/payment_request.json | 13 +- .../payment_request/payment_request.py | 174 ++++++++++++------ 7 files changed, 302 insertions(+), 69 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 6738743793db..c6c3651cb857 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,6 +7,8 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; erpnext.accounts.taxes.setup_tax_validations("Payment Entry"); erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges"); +const FAULTY_VALUES = ["", null, undefined, 0]; + frappe.ui.form.on("Payment Entry", { onload: function (frm) { frm.ignore_doctypes_on_cancel_all = [ @@ -165,6 +167,19 @@ frappe.ui.form.on("Payment Entry", { filters: filters, }; }); + + frm.set_query("payment_request", "references", function (doc, cdt, cdn) { + const row = locals[cdt][cdn]; + const filters = { + docstatus: 1, + status: ["!=", "Paid"], + reference_doctype: row.reference_doctype, + reference_name: row.reference_name, + }; + return { + filters: filters, + }; + }); }, refresh: function (frm) { @@ -995,6 +1010,8 @@ frappe.ui.form.on("Payment Entry", { total_negative_outstanding - total_positive_outstanding ); } + + frm.events.set_matched_payment_requests(frm); } frm.events.allocate_party_amount_against_ref_docs( @@ -1647,6 +1664,11 @@ frappe.ui.form.on("Payment Entry", { return current_tax_amount; }, + + set_matched_payment_requests: async function (frm) { + await frappe.after_ajax(); + frm.call("set_matched_payment_requests"); + }, }); frappe.ui.form.on("Payment Entry Reference", { @@ -1689,8 +1711,20 @@ frappe.ui.form.on("Payment Entry Reference", { } }, - allocated_amount: function (frm) { + allocated_amount: function (frm, cdt, cdn) { frm.events.set_total_allocated_amount(frm); + + const row = locals[cdt][cdn]; + + // if payment_request already set then return + if (row.payment_request) return; + + const references = [row.reference_name, row.reference_doctype, row.allocated_amount]; + + // if any of the reference fields are faulty, it returns + if (FAULTY_VALUES.some((el) => references.includes(el))) return; + + frm.call("set_matched_payment_request", { row_idx: row.idx }); }, references_remove: function (frm) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 3ad1f72af6e3..694c5fe78e67 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -8,7 +8,7 @@ import frappe from frappe import ValidationError, _, qb, scrub, throw from frappe.utils import cint, comma_or, flt, getdate, nowdate -from frappe.utils.data import comma_and, fmt_money +from frappe.utils.data import comma_and, fmt_money, get_link_to_form from pypika import Case from pypika.functions import Coalesce, Sum @@ -181,6 +181,9 @@ def validate(self): self.set_status() self.set_total_in_words() + def before_save(self): + self.check_payment_requests() + def on_submit(self): if self.difference_amount: frappe.throw(_("Difference Amount must be zero")) @@ -188,6 +191,7 @@ def on_submit(self): self.update_outstanding_amounts() self.update_advance_paid() self.update_payment_schedule() + self.set_payment_req_outstanding_amount() self.set_payment_req_status() self.set_status() @@ -263,9 +267,17 @@ def on_cancel(self): self.update_advance_paid() self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) + self.set_payment_req_outstanding_amount(cancel=True) self.set_payment_req_status() self.set_status() + def set_payment_req_outstanding_amount(self, cancel=False): + from erpnext.accounts.doctype.payment_request.payment_request import ( + update_payment_req_outstanding_amount, + ) + + update_payment_req_outstanding_amount(self, cancel=cancel) + def set_payment_req_status(self): from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status @@ -309,6 +321,8 @@ def validate_allocated_amount(self): if self.payment_type == "Internal Transfer": return + self.validate_allocated_amount_as_of_pr() + if self.party_type in ("Customer", "Supplier"): self.validate_allocated_amount_with_latest_data() else: @@ -321,6 +335,21 @@ def validate_allocated_amount(self): if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount): frappe.throw(fail_message.format(d.idx)) + def validate_allocated_amount_as_of_pr(self): + from erpnext.accounts.doctype.payment_request.payment_request import ( + get_outstanding_amount_of_payment_entry_references as get_outstanding_amounts, + ) + + outstanding_amounts = get_outstanding_amounts(self.references) + + for ref in self.references: + if ref.payment_request and ref.allocated_amount > outstanding_amounts[ref.payment_request]: + frappe.throw( + _("Allocated Amount cannot be greater than Outstanding Amount of {0}").format( + get_link_to_form("Payment Request", ref.payment_request) + ) + ) + def term_based_allocation_enabled_for_reference( self, reference_doctype: str, reference_name: str ) -> bool: @@ -1701,6 +1730,103 @@ def get_current_tax_fraction(self, tax): return current_tax_fraction + def check_payment_requests(self): + if not self.references: + return + + not_set_count = sum(1 for row in self.references if not row.payment_request) + + if not_set_count == 0: + return + elif not_set_count == 1: + msg = _("{0} {1} is not set in {2}").format( + not_set_count, + frappe.bold("Payment Request"), + frappe.bold("Payment References"), + ) + else: + msg = _("{0} {1} are not set in {2}").format( + not_set_count, + frappe.bold("Payment Request"), + frappe.bold("Payment References"), + ) + + frappe.msgprint(msg=msg, alert=True, indicator="orange") + + # todo: can be optimized + @frappe.whitelist() + def set_matched_payment_requests(self): + if not self.references: + return + + matched_count = 0 + + for row in self.references: + if row.payment_request or ( + not row.reference_doctype or not row.reference_name or not row.allocated_amount + ): + continue + + row.payment_request = get_matched_payment_request( + row.reference_doctype, row.reference_name, row.allocated_amount + ) + + if row.payment_request: + matched_count += 1 + + if matched_count == 0: + return + elif matched_count == 1: + msg = _("{0} matched {1} is set").format(matched_count, frappe.bold("Payment Request")) + else: + msg = _("{0} matched {1} are set").format(matched_count, frappe.bold("Payment Request")) + + frappe.msgprint( + msg=msg, + alert=True, + ) + + @frappe.whitelist() + def set_matched_payment_request(self, row_idx): + row = next((row for row in self.references if row.idx == row_idx), None) + + if not row: + frappe.throw(_("Row #{0} not found").format(row_idx), title=_("Row Not Found")) + + # if payment entry already set then do not set it again + if row.payment_request: + return + + row.payment_request = get_matched_payment_request( + row.reference_doctype, row.reference_name, row.allocated_amount + ) + + if row.payment_request: + frappe.msgprint( + msg=_("Matched {0} is set").format(frappe.bold("Payment Request")), + alert=True, + ) + + +# todo: can be optimized +def get_matched_payment_request(reference_doctype, reference_name, outstanding_amount): + payment_requests = frappe.get_all( + doctype="Payment Request", + filters={ + "reference_doctype": reference_doctype, + "reference_name": reference_name, + "outstanding_amount": outstanding_amount, + "status": ["!=", "Paid"], + "docstatus": 1, + }, + pluck="name", + ) + + if len(payment_requests) == 1: + return payment_requests[0] + + return None + def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): @@ -1732,6 +1858,7 @@ def _on_previous_row_error(row_range): frappe.throw(_("Valuation type charges can not be marked as Inclusive")) +# todo: modify its test @frappe.whitelist() def get_outstanding_reference_documents(args, validate=False): if isinstance(args, str): @@ -1889,6 +2016,9 @@ def get_outstanding_reference_documents(args, validate=False): ) ) + frappe.log("Data") + frappe.log(data) + return data diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 352ece24f06b..7fce86c99d9a 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -18,7 +18,8 @@ "allocated_amount", "exchange_rate", "exchange_gain_loss", - "account" + "account", + "payment_request" ], "fields": [ { @@ -120,12 +121,18 @@ "fieldname": "payment_type", "fieldtype": "Data", "label": "Payment Type" + }, + { + "fieldname": "payment_request", + "fieldtype": "Link", + "label": "Payment Request", + "options": "Payment Request" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-04-05 09:44:08.310593", + "modified": "2024-07-20 17:57:32.866780", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py index 4a027b4ee32b..68d819d08408 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -25,6 +25,7 @@ class PaymentEntryReference(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + payment_request: DF.Link | None payment_term: DF.Link | None payment_type: DF.Data | None reference_doctype: DF.Link diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index f12facfbf5a6..44313e5c0d2c 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -52,8 +52,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) { } if ( - (!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && - frm.doc.status == "Initiated" + frm.doc.payment_request_type == "Outward" && + ["Initiated", "Partially Paid"].includes(frm.doc.status) ) { frm.add_custom_button(__("Create Payment Entry"), function () { frappe.call({ diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 7674712374c8..46723df10610 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -21,6 +21,7 @@ "grand_total", "is_a_subscription", "column_break_18", + "outstanding_amount", "currency", "subscription_section", "subscription_plans", @@ -400,13 +401,21 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "eval:doc.docstatus==1", + "fieldname": "outstanding_amount", + "fieldtype": "Currency", + "label": "Outstanding Amount", + "non_negative": 1, + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-06-20 13:54:55.245774", + "modified": "2024-07-20 17:54:33.064658", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", @@ -444,4 +453,4 @@ "sort_field": "creation", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 8d20493706ce..51ebb7e2f8ea 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -49,6 +49,7 @@ class PaymentRequest(Document): cost_center: DF.Link | None currency: DF.Link | None email_to: DF.Data | None + failed_reason: DF.Data | None grand_total: DF.Currency iban: DF.ReadOnly | None is_a_subscription: DF.Check @@ -57,16 +58,17 @@ class PaymentRequest(Document): mode_of_payment: DF.Link | None mute_email: DF.Check naming_series: DF.Literal["ACC-PRQ-.YYYY.-"] + outstanding_amount: DF.Currency party: DF.DynamicLink | None party_type: DF.Link | None payment_account: DF.ReadOnly | None - payment_channel: DF.Literal["", "Email", "Phone"] + payment_channel: DF.Literal["", "Email", "Phone", "Other"] payment_gateway: DF.ReadOnly | None payment_gateway_account: DF.Link | None payment_order: DF.Link | None payment_request_type: DF.Literal["Outward", "Inward"] payment_url: DF.Data | None - print_format: DF.Literal + print_format: DF.Literal[None] project: DF.Link | None reference_doctype: DF.Link | None reference_name: DF.DynamicLink | None @@ -100,6 +102,9 @@ def validate_reference_document(self): frappe.throw(_("To create a Payment Request reference document is required")) def validate_payment_request_amount(self): + if self.grand_total == 0: + frappe.throw(_("Total Payment Request cannot be zero")) + existing_payment_request_amount = flt( get_existing_payment_request_amount(self.reference_doctype, self.reference_name) ) @@ -159,6 +164,8 @@ def on_change(self): ref_doc.set_advance_payment_status() def before_submit(self): + self.outstanding_amount = self.grand_total + if self.payment_request_type == "Outward": self.status = "Initiated" elif self.payment_request_type == "Inward": @@ -265,7 +272,9 @@ def get_payment_url(self): ) def set_as_paid(self): - if self.payment_channel == "Phone": + self.db_set("status", "Paid") + + if self.payment_channel == "Phone" and self.status != "Paid": self.db_set("status", "Paid") else: @@ -277,6 +286,8 @@ def set_as_paid(self): def create_payment_entry(self, submit=True): """create entry""" + if self.payment_channel == "Phone": + frappe.throw(_("Payment Entry cannot be created for Phone Payment")) frappe.flags.ignore_account_permission = True ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) @@ -290,11 +301,14 @@ def create_payment_entry(self, submit=True): party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account) - bank_amount = self.grand_total + bank_amount = self.outstanding_amount + if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: - party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") + total = ref_doc.get("rounded_total") or ref_doc.get("grand_total") + base_total = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") + party_amount = round(self.outstanding / total * base_total, self.precision("grand_total")) else: - party_amount = self.grand_total + party_amount = self.outstanding_amount payment_entry = get_payment_entry( self.reference_doctype, @@ -315,6 +329,9 @@ def create_payment_entry(self, submit=True): } ) + # Add reference of Payment Request + payment_entry.get("references")[0].payment_request = self.name + # Update dimensions payment_entry.update( { @@ -323,13 +340,8 @@ def create_payment_entry(self, submit=True): } ) - if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: - amount = payment_entry.base_paid_amount - else: - amount = self.grand_total - - payment_entry.received_amount = amount - payment_entry.get("references")[0].allocated_amount = amount + payment_entry.received_amount = self.outstanding_amount + payment_entry.get("references")[0].allocated_amount = self.outstanding_amount for dimension in get_accounting_dimensions(): payment_entry.update({dimension: self.get(dimension)}) @@ -529,7 +541,6 @@ def get_amount(ref_doc, payment_account=None): dt = ref_doc.doctype if dt in ["Sales Order", "Purchase Order"]: grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) - grand_total -= get_paid_amount_against_order(dt, ref_doc.name) elif dt in ["Sales Invoice", "Purchase Invoice"]: if not ref_doc.get("is_pos"): if ref_doc.party_account_currency == ref_doc.currency: @@ -554,24 +565,20 @@ def get_amount(ref_doc, payment_account=None): def get_existing_payment_request_amount(ref_dt, ref_dn): """ - Get the existing payment request which are unpaid or partially paid for payment channel other than Phone - and get the summation of existing paid payment request for Phone payment channel. + Get the total amount of `Paid` / `Partially Paid` payment requests against a document. """ - existing_payment_request_amount = frappe.db.sql( - """ - select sum(grand_total) - from `tabPayment Request` - where - reference_doctype = %s - and reference_name = %s - and docstatus = 1 - and (status != 'Paid' - or (payment_channel = 'Phone' - and status = 'Paid')) - """, - (ref_dt, ref_dn), + PR = frappe.qb.DocType("Payment Request") + + response = ( + frappe.qb.from_(PR) + .select(Sum(PR.grand_total - PR.outstanding_amount)) + .where(PR.reference_doctype == ref_dt) + .where(PR.reference_name == ref_dn) + .where(PR.docstatus == 1) + .run() ) - return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0 + + return response[0][0] or 0 def get_gateway_details(args): # nosemgrep @@ -613,41 +620,86 @@ def make_payment_entry(docname): return doc.create_payment_entry(submit=False).as_dict() -def update_payment_req_status(doc, method): - from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details +def update_payment_req_outstanding_amount(pe_doc, cancel=False): + outstanding_amounts = get_outstanding_amount_of_payment_entry_references(pe_doc.references) - for ref in doc.references: - payment_request_name = frappe.db.get_value( - "Payment Request", - { - "reference_doctype": ref.reference_doctype, - "reference_name": ref.reference_name, - "docstatus": 1, - }, + for ref in pe_doc.references: + if not ref.payment_request: + continue + + old_outstanding_amount = outstanding_amounts[ref.payment_request] + + new_outstanding_amount = ( + old_outstanding_amount + ref.allocated_amount + if cancel + else old_outstanding_amount - ref.allocated_amount ) - if payment_request_name: - ref_details = get_reference_details( - ref.reference_doctype, - ref.reference_name, - doc.party_account_currency, - doc.party_type, - doc.party, + if not cancel and new_outstanding_amount < 0: + frappe.throw( + _( + "The allocated amount is greater than the outstanding amount of Payment Request {0}" + ).format(ref.payment_request) ) - pay_req_doc = frappe.get_doc("Payment Request", payment_request_name) - status = pay_req_doc.status - - if status != "Paid" and not ref_details.outstanding_amount: - status = "Paid" - elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount: - status = "Partially Paid" - elif ref_details.outstanding_amount == ref_details.total_amount: - if pay_req_doc.payment_request_type == "Outward": - status = "Initiated" - elif pay_req_doc.payment_request_type == "Inward": - status = "Requested" - - pay_req_doc.db_set("status", status) + + frappe.db.set_value( + "Payment Request", + ref.payment_request, + "outstanding_amount", + new_outstanding_amount, + ) + + +def update_payment_req_status(pe_doc, method): + payment_requests = frappe.get_all( + "Payment Request", + filters={"name": ["in", get_referenced_payment_requests(pe_doc.references)]}, + fields=[ + "name", + "grand_total", + "outstanding_amount", + "payment_request_type", + ], + ) + + payment_requests = {pr.name: pr for pr in payment_requests} + + for ref in pe_doc.references: + if not ref.payment_request: + continue + + payment_request = payment_requests[ref.payment_request] + + if payment_request["outstanding_amount"] == payment_request["grand_total"]: + status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested" + elif payment_request["outstanding_amount"] == 0: + status = "Paid" + elif payment_request["outstanding_amount"] > 0: + status = "Partially Paid" + + frappe.db.set_value( + "Payment Request", + ref.payment_request, + "status", + status, + ) + + +def get_outstanding_amount_of_payment_entry_references(references: list) -> dict: + payment_requests = get_referenced_payment_requests(references) + + return dict( + frappe.get_all( + "Payment Request", + filters={"name": ["in", payment_requests]}, + fields=["name", "outstanding_amount"], + as_list=True, + ) + ) + + +def get_referenced_payment_requests(references: list) -> set: + return {row.payment_request for row in references if row.payment_request} def get_dummy_message(doc): From 469b805a97948dce202eeabcef957790eaea4911 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 22 Jul 2024 11:37:52 +0530 Subject: [PATCH 02/60] chore: minor changes --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 694c5fe78e67..5722a9137a85 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2016,8 +2016,6 @@ def get_outstanding_reference_documents(args, validate=False): ) ) - frappe.log("Data") - frappe.log(data) return data From 3718762fab0be651b7a975b95fedab48b0574860 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 22 Jul 2024 15:32:07 +0530 Subject: [PATCH 03/60] fix: remove bug --- .../doctype/payment_request/payment_request.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 51ebb7e2f8ea..85e1a6c10219 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -467,6 +467,7 @@ def make_payment_request(**args): {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0}, ) + # fetches existing payment request `grand_total` amount existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn) if existing_payment_request_amount: @@ -484,7 +485,6 @@ def make_payment_request(**args): args["payment_request_type"] = ( "Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward" ) - pr.update( { "payment_gateway_account": gateway_account.get("name"), @@ -563,15 +563,23 @@ def get_amount(ref_doc, payment_account=None): return grand_total -def get_existing_payment_request_amount(ref_dt, ref_dn): +def get_existing_payment_request_amount(ref_dt, ref_dn, only_paid=False): """ - Get the total amount of `Paid` / `Partially Paid` payment requests against a document. + Return the total amount of Payment Requests against a reference document. \n + If `only_paid` is True, it will return the total amount of paid Payment Requests. \n + Else, it will return the total amount of all Payment Requests. """ + PR = frappe.qb.DocType("Payment Request") + if only_paid: + select = Sum(PR.grand_total - PR.outstanding_amount) + else: + select = Sum(PR.grand_total) + response = ( frappe.qb.from_(PR) - .select(Sum(PR.grand_total - PR.outstanding_amount)) + .select(select) .where(PR.reference_doctype == ref_dt) .where(PR.reference_name == ref_dn) .where(PR.docstatus == 1) @@ -709,7 +717,7 @@ def get_dummy_message(doc): {%- else %}

Hello,

{% endif %}

{{ _("Requesting payment against {0} {1} for amount {2}").format(doc.doctype, - doc.name, doc.get_formatted("grand_total")) }}

+ doc.name, doc.get_formatted("grand_total")) }}

{{ _("Make Payment") }} From 9ed1d8145c4b201e4241003e0ace897ceeaaba14 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 22 Jul 2024 15:36:51 +0530 Subject: [PATCH 04/60] fix: replace `round` with `flt` --- erpnext/accounts/doctype/payment_request/payment_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 85e1a6c10219..4d269dcbbbe8 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -306,7 +306,7 @@ def create_payment_entry(self, submit=True): if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: total = ref_doc.get("rounded_total") or ref_doc.get("grand_total") base_total = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") - party_amount = round(self.outstanding / total * base_total, self.precision("grand_total")) + party_amount = flt(self.outstanding_amount / total * base_total, self.precision("grand_total")) else: party_amount = self.outstanding_amount From aeddb817b40aaedea0af2b1050cdafd75c5d9dde Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 22 Jul 2024 17:21:57 +0530 Subject: [PATCH 05/60] fix: update `set_advance_payment_status()` logic --- .../doctype/payment_entry/payment_entry.py | 2 - .../payment_request/payment_request.py | 9 ++-- erpnext/controllers/accounts_controller.py | 41 +++++++++---------- 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5722a9137a85..dc6d98d2b781 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1858,7 +1858,6 @@ def _on_previous_row_error(row_range): frappe.throw(_("Valuation type charges can not be marked as Inclusive")) -# todo: modify its test @frappe.whitelist() def get_outstanding_reference_documents(args, validate=False): if isinstance(args, str): @@ -2016,7 +2015,6 @@ def get_outstanding_reference_documents(args, validate=False): ) ) - return data diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 4d269dcbbbe8..f93d74fa1939 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -272,10 +272,8 @@ def get_payment_url(self): ) def set_as_paid(self): - self.db_set("status", "Paid") - if self.payment_channel == "Phone" and self.status != "Paid": - self.db_set("status", "Paid") + self.db_set({"status": "Paid", "outstanding_amount": 0}) else: payment_entry = self.create_payment_entry() @@ -304,8 +302,8 @@ def create_payment_entry(self, submit=True): bank_amount = self.outstanding_amount if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: - total = ref_doc.get("rounded_total") or ref_doc.get("grand_total") - base_total = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") + total = ref_doc.get("rounded_total") or ref_doc.grand_total + base_total = ref_doc.get("base_rounded_total") or ref_doc.base_grand_total party_amount = flt(self.outstanding_amount / total * base_total, self.precision("grand_total")) else: party_amount = self.outstanding_amount @@ -569,7 +567,6 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, only_paid=False): If `only_paid` is True, it will return the total amount of paid Payment Requests. \n Else, it will return the total amount of all Payment Requests. """ - PR = frappe.qb.DocType("Payment Request") if only_paid: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3a3de662d591..95adce8f2387 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1943,35 +1943,32 @@ def set_total_advance_paid(self): self.set_advance_payment_status() def set_advance_payment_status(self): - new_status = None + from erpnext.accounts.doctype.payment_request.payment_request import ( + get_existing_payment_request_amount as get_paid_amount, + ) - stati = frappe.get_all( + new_status = None + available_payment_requests = frappe.db.count( "Payment Request", - { - "reference_doctype": self.doctype, - "reference_name": self.name, - "docstatus": 1, - }, - pluck="status", + {"reference_doctype": self.doctype, "reference_name": self.name, "docstatus": 1}, ) - if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"): - if not stati: + total_amount = self.get("rounded_total") or self.grand_total + paid_amount = get_paid_amount(self.doctype, self.name, only_paid=True) + + if paid_amount == total_amount: + new_status = "Fully Paid" + elif paid_amount > 0 and paid_amount < total_amount: + new_status = "Partially Paid" + elif self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"): + if not available_payment_requests: new_status = "Not Requested" - elif "Requested" in stati or "Failed" in stati: + elif paid_amount == 0 or paid_amount == 0.0: new_status = "Requested" - elif "Partially Paid" in stati: - new_status = "Partially Paid" - elif "Paid" in stati: - new_status = "Fully Paid" - if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"): - if not stati: + elif self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"): + if not available_payment_requests: new_status = "Not Initiated" - elif "Initiated" in stati or "Failed" in stati or "Payment Ordered" in stati: + elif paid_amount == 0 or paid_amount == 0.0: new_status = "Initiated" - elif "Partially Paid" in stati: - new_status = "Partially Paid" - elif "Paid" in stati: - new_status = "Fully Paid" if new_status == self.advance_payment_status: return From 3c4b84dec3bd8874b397d326521f81e3408b0410 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 22 Jul 2024 18:52:00 +0530 Subject: [PATCH 06/60] fix: removed bug of `set_advance_payment_status` --- .../doctype/payment_entry/payment_entry.py | 18 +++++++++++++-- .../payment_request/payment_request.py | 22 +++++++++++-------- erpnext/controllers/accounts_controller.py | 2 ++ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index dc6d98d2b781..1854daab5864 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -193,6 +193,7 @@ def on_submit(self): self.update_payment_schedule() self.set_payment_req_outstanding_amount() self.set_payment_req_status() + self.set_reference_advance_payment_status() self.set_status() def set_liability_account(self): @@ -269,6 +270,7 @@ def on_cancel(self): self.update_payment_schedule(cancel=1) self.set_payment_req_outstanding_amount(cancel=True) self.set_payment_req_status() + self.set_reference_advance_payment_status() self.set_status() def set_payment_req_outstanding_amount(self, cancel=False): @@ -283,6 +285,18 @@ def set_payment_req_status(self): update_payment_req_status(self, None) + # todo: need to optimize + def set_reference_advance_payment_status(self): + advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( + "advance_payment_payable_doctypes" + ) + + for ref in self.get("references"): + ref_doc = frappe.get_doc(ref.reference_doctype, ref.reference_name) + if ref.reference_doctype in advance_payment_doctypes: + # set advance payment status + ref_doc.set_advance_payment_status() + def update_outstanding_amounts(self): self.set_missing_ref_details(force=True) @@ -1753,7 +1767,7 @@ def check_payment_requests(self): frappe.msgprint(msg=msg, alert=True, indicator="orange") - # todo: can be optimized + # todo: can be optimize @frappe.whitelist() def set_matched_payment_requests(self): if not self.references: @@ -1808,7 +1822,7 @@ def set_matched_payment_request(self, row_idx): ) -# todo: can be optimized +# todo: can be optimize def get_matched_payment_request(reference_doctype, reference_name, outstanding_amount): payment_requests = frappe.get_all( doctype="Payment Request", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index f93d74fa1939..f3adfd035117 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -154,15 +154,6 @@ def validate_subscription_details(self): ).format(self.grand_total, amount) ) - def on_change(self): - ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( - "advance_payment_payable_doctypes" - ) - if self.reference_doctype in advance_payment_doctypes: - # set advance payment status - ref_doc.set_advance_payment_status() - def before_submit(self): self.outstanding_amount = self.grand_total @@ -180,6 +171,9 @@ def before_submit(self): self.send_email() self.make_communication_entry() + def on_submit(self): + self.update_reference_advance_payment_status() + def request_phone_payment(self): controller = _get_payment_gateway_controller(self.payment_gateway) request_amount = self.get_request_amount() @@ -217,6 +211,7 @@ def get_request_amount(self): def on_cancel(self): self.check_if_payment_entry_exists() self.set_as_cancelled() + self.update_reference_advance_payment_status() def make_invoice(self): from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice @@ -424,6 +419,15 @@ def create_subscription(self, payment_provider, gateway_controller, data): return create_stripe_subscription(gateway_controller, data) + def update_reference_advance_payment_status(self): + ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) + advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( + "advance_payment_payable_doctypes" + ) + if self.reference_doctype in advance_payment_doctypes: + # set advance payment status + ref_doc.set_advance_payment_status() + @frappe.whitelist(allow_guest=True) def make_payment_request(**args): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 95adce8f2387..3c4599f30d98 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1942,6 +1942,8 @@ def set_total_advance_paid(self): self.set_advance_payment_status() + # todo: need to optimize + # todo: modularize def set_advance_payment_status(self): from erpnext.accounts.doctype.payment_request.payment_request import ( get_existing_payment_request_amount as get_paid_amount, From a26a75f83131e23f3e8accb8aa89af042d94eb5b Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 24 Jul 2024 16:54:20 +0530 Subject: [PATCH 07/60] fix: changes as per review --- .../doctype/payment_entry/payment_entry.js | 21 +-- .../doctype/payment_entry/payment_entry.py | 173 ++++++++++-------- .../payment_request/payment_request.json | 10 +- .../payment_request/payment_request.py | 105 +++++------ erpnext/controllers/accounts_controller.py | 44 ++--- 5 files changed, 170 insertions(+), 183 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index c6c3651cb857..1c54ee2b733b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,8 +7,6 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; erpnext.accounts.taxes.setup_tax_validations("Payment Entry"); erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges"); -const FAULTY_VALUES = ["", null, undefined, 0]; - frappe.ui.form.on("Payment Entry", { onload: function (frm) { frm.ignore_doctypes_on_cancel_all = [ @@ -1010,8 +1008,6 @@ frappe.ui.form.on("Payment Entry", { total_negative_outstanding - total_positive_outstanding ); } - - frm.events.set_matched_payment_requests(frm); } frm.events.allocate_party_amount_against_ref_docs( @@ -1117,6 +1113,7 @@ frappe.ui.form.on("Payment Entry", { }); frm.refresh_fields(); + if (frappe.flags.allocate_payment_amount) frm.call("set_matched_payment_requests"); frm.events.set_total_allocated_amount(frm); }, @@ -1664,11 +1661,6 @@ frappe.ui.form.on("Payment Entry", { return current_tax_amount; }, - - set_matched_payment_requests: async function (frm) { - await frappe.after_ajax(); - frm.call("set_matched_payment_requests"); - }, }); frappe.ui.form.on("Payment Entry Reference", { @@ -1714,15 +1706,10 @@ frappe.ui.form.on("Payment Entry Reference", { allocated_amount: function (frm, cdt, cdn) { frm.events.set_total_allocated_amount(frm); - const row = locals[cdt][cdn]; + const row = frappe.get_doc(cdt, cdn); - // if payment_request already set then return - if (row.payment_request) return; - - const references = [row.reference_name, row.reference_doctype, row.allocated_amount]; - - // if any of the reference fields are faulty, it returns - if (FAULTY_VALUES.some((el) => references.includes(el))) return; + if (row.payment_request || !row.reference_name || !row.reference_doctype || !row.allocated_amount) + return; frm.call("set_matched_payment_request", { row_idx: row.idx }); }, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 1854daab5864..9cc85243d7d7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -182,7 +182,7 @@ def validate(self): self.set_total_in_words() def before_save(self): - self.check_payment_requests() + self.check_references_for_unset_payment_request() def on_submit(self): if self.difference_amount: @@ -191,9 +191,8 @@ def on_submit(self): self.update_outstanding_amounts() self.update_advance_paid() self.update_payment_schedule() - self.set_payment_req_outstanding_amount() - self.set_payment_req_status() - self.set_reference_advance_payment_status() + self.update_payment_requests() + self.update_references_advance_payment_status() self.set_status() def set_liability_account(self): @@ -268,33 +267,25 @@ def on_cancel(self): self.update_advance_paid() self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) - self.set_payment_req_outstanding_amount(cancel=True) - self.set_payment_req_status() - self.set_reference_advance_payment_status() + self.update_payment_requests(cancel=True) + self.update_references_advance_payment_status() self.set_status() - def set_payment_req_outstanding_amount(self, cancel=False): + def update_payment_requests(self, cancel=False): from erpnext.accounts.doctype.payment_request.payment_request import ( - update_payment_req_outstanding_amount, + update_payment_requests_as_per_pe_references, ) - update_payment_req_outstanding_amount(self, cancel=cancel) + update_payment_requests_as_per_pe_references(self.references, cancel=cancel) - def set_payment_req_status(self): - from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status - - update_payment_req_status(self, None) - - # todo: need to optimize - def set_reference_advance_payment_status(self): + def update_references_advance_payment_status(self): advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( "advance_payment_payable_doctypes" ) for ref in self.get("references"): - ref_doc = frappe.get_doc(ref.reference_doctype, ref.reference_name) if ref.reference_doctype in advance_payment_doctypes: - # set advance payment status + ref_doc = frappe.get_doc(ref.reference_doctype, ref.reference_name) ref_doc.set_advance_payment_status() def update_outstanding_amounts(self): @@ -335,7 +326,7 @@ def validate_allocated_amount(self): if self.payment_type == "Internal Transfer": return - self.validate_allocated_amount_as_of_pr() + self.validate_allocated_amount_as_per_payment_request() if self.party_type in ("Customer", "Supplier"): self.validate_allocated_amount_with_latest_data() @@ -349,7 +340,7 @@ def validate_allocated_amount(self): if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount): frappe.throw(fail_message.format(d.idx)) - def validate_allocated_amount_as_of_pr(self): + def validate_allocated_amount_as_per_payment_request(self): from erpnext.accounts.doctype.payment_request.payment_request import ( get_outstanding_amount_of_payment_entry_references as get_outstanding_amounts, ) @@ -359,9 +350,10 @@ def validate_allocated_amount_as_of_pr(self): for ref in self.references: if ref.payment_request and ref.allocated_amount > outstanding_amounts[ref.payment_request]: frappe.throw( - _("Allocated Amount cannot be greater than Outstanding Amount of {0}").format( - get_link_to_form("Payment Request", ref.payment_request) - ) + msg=_( + "Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}" + ).format(ref.idx, get_link_to_form("Payment Request", ref.payment_request)), + title=_("Invalid Allocated Amount"), ) def term_based_allocation_enabled_for_reference( @@ -1744,59 +1736,69 @@ def get_current_tax_fraction(self, tax): return current_tax_fraction - def check_payment_requests(self): + def check_references_for_unset_payment_request(self): if not self.references: return - not_set_count = sum(1 for row in self.references if not row.payment_request) + matched_payment_requests = get_matched_payment_requests_of_references( + [row for row in self.references if not row.payment_request] + ) - if not_set_count == 0: - return - elif not_set_count == 1: - msg = _("{0} {1} is not set in {2}").format( - not_set_count, - frappe.bold("Payment Request"), - frappe.bold("Payment References"), - ) - else: - msg = _("{0} {1} are not set in {2}").format( - not_set_count, - frappe.bold("Payment Request"), - frappe.bold("Payment References"), + unset_pr_rows = {} + + for row in self.references: + if row.payment_request: + continue + + matched_pr = matched_payment_requests.get( + (row.reference_doctype, row.reference_name, row.allocated_amount) ) - frappe.msgprint(msg=msg, alert=True, indicator="orange") + if matched_pr: + unset_pr_rows[row.idx] = matched_pr + + if unset_pr_rows: + message = _("Matched Payment Requests found for references, but not set.

") + message += _("
View Details
    ") + for idx, pr in unset_pr_rows.items(): + message += _("
  • Row #{0}: {1}
  • ").format(idx, get_link_to_form("Payment Request", pr)) + message += _("
") + + frappe.msgprint( + msg=message, + indicator="yellow", + ) - # todo: can be optimize @frappe.whitelist() def set_matched_payment_requests(self): if not self.references: return + matched_payment_requests = get_matched_payment_requests_of_references(self.references) + matched_count = 0 for row in self.references: - if row.payment_request or ( - not row.reference_doctype or not row.reference_name or not row.allocated_amount + if ( + row.payment_request + or not row.reference_doctype + or not row.reference_name + or not row.allocated_amount ): continue - row.payment_request = get_matched_payment_request( - row.reference_doctype, row.reference_name, row.allocated_amount + row.payment_request = matched_payment_requests.get( + (row.reference_doctype, row.reference_name, row.allocated_amount) ) if row.payment_request: matched_count += 1 - if matched_count == 0: + if not matched_count: return - elif matched_count == 1: - msg = _("{0} matched {1} is set").format(matched_count, frappe.bold("Payment Request")) - else: - msg = _("{0} matched {1} are set").format(matched_count, frappe.bold("Payment Request")) frappe.msgprint( - msg=msg, + msg=_("Setting {0} matched Payment Request(s)").format(matched_count), alert=True, ) @@ -1808,38 +1810,61 @@ def set_matched_payment_request(self, row_idx): frappe.throw(_("Row #{0} not found").format(row_idx), title=_("Row Not Found")) # if payment entry already set then do not set it again - if row.payment_request: + if ( + row.payment_request + or not row.reference_doctype + or not row.reference_name + or not row.allocated_amount + ): + return + + matched_pr = get_matched_payment_requests_of_references([row]) + + if not matched_pr: return - row.payment_request = get_matched_payment_request( - row.reference_doctype, row.reference_name, row.allocated_amount + row.payment_request = matched_pr[(row.reference_doctype, row.reference_name, row.allocated_amount)] + + frappe.msgprint( + msg=_("Setting matched Payment Request"), + alert=True, ) - if row.payment_request: - frappe.msgprint( - msg=_("Matched {0} is set").format(frappe.bold("Payment Request")), - alert=True, - ) +# FIXME: can be optimize and use query builder +def get_matched_payment_requests_of_references(references=None): + if not references: + return -# todo: can be optimize -def get_matched_payment_request(reference_doctype, reference_name, outstanding_amount): - payment_requests = frappe.get_all( - doctype="Payment Request", - filters={ - "reference_doctype": reference_doctype, - "reference_name": reference_name, - "outstanding_amount": outstanding_amount, - "status": ["!=", "Paid"], - "docstatus": 1, - }, - pluck="name", + refs = [ + (row.reference_doctype, row.reference_name, row.allocated_amount) + for row in references + if row.reference_doctype and row.reference_name and row.allocated_amount + ] + + if not refs: + return + + all_matched_prs = frappe.db.sql( + """ + select name, reference_doctype, reference_name, outstanding_amount + from `tabPayment Request` + where (reference_doctype, reference_name, outstanding_amount) in %s + and status != 'Paid' and docstatus = 1 + """, + (refs,), + as_dict=True, ) - if len(payment_requests) == 1: - return payment_requests[0] + single_matched_prs = {} + for pr in all_matched_prs: + key = (pr.reference_doctype, pr.reference_name, pr.outstanding_amount) + if key in single_matched_prs: + single_matched_prs[key].append(pr.name) + else: + single_matched_prs[key] = [pr.name] - return None + return {key: names[0] for key, names in single_matched_prs.items() if len(names) == 1} def validate_inclusive_tax(tax, doc): diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 46723df10610..50cc12aa4ea2 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -134,7 +134,8 @@ "no_copy": 1, "options": "reference_doctype", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "transaction_details", @@ -147,7 +148,8 @@ "fieldtype": "Currency", "label": "Amount", "non_negative": 1, - "options": "currency" + "options": "currency", + "reqd": 1 }, { "default": "0", @@ -403,7 +405,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.docstatus==1", + "depends_on": "eval: doc.docstatus === 1", "fieldname": "outstanding_amount", "fieldtype": "Currency", "label": "Outstanding Amount", @@ -415,7 +417,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-07-20 17:54:33.064658", + "modified": "2024-07-23 19:02:07.754296", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index f3adfd035117..83c4cd966bd8 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -103,7 +103,10 @@ def validate_reference_document(self): def validate_payment_request_amount(self): if self.grand_total == 0: - frappe.throw(_("Total Payment Request cannot be zero")) + frappe.throw( + _("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")), + title=_("Invalid Amount"), + ) existing_payment_request_amount = flt( get_existing_payment_request_amount(self.reference_doctype, self.reference_name) @@ -267,7 +270,7 @@ def get_payment_url(self): ) def set_as_paid(self): - if self.payment_channel == "Phone" and self.status != "Paid": + if self.payment_channel == "Phone": self.db_set({"status": "Paid", "outstanding_amount": 0}) else: @@ -279,8 +282,6 @@ def set_as_paid(self): def create_payment_entry(self, submit=True): """create entry""" - if self.payment_channel == "Phone": - frappe.throw(_("Payment Entry cannot be created for Phone Payment")) frappe.flags.ignore_account_permission = True ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) @@ -297,8 +298,8 @@ def create_payment_entry(self, submit=True): bank_amount = self.outstanding_amount if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: - total = ref_doc.get("rounded_total") or ref_doc.grand_total - base_total = ref_doc.get("base_rounded_total") or ref_doc.base_grand_total + total = ref_doc.get("rounded_total") or ref_doc.get("grand_total") + base_total = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") party_amount = flt(self.outstanding_amount / total * base_total, self.precision("grand_total")) else: party_amount = self.outstanding_amount @@ -314,7 +315,6 @@ def create_payment_entry(self, submit=True): payment_entry.update( { "mode_of_payment": self.mode_of_payment, - "reference_no": self.name, "reference_date": nowdate(), "remarks": "Payment Entry against {} {} via Payment Request {}".format( self.reference_doctype, self.reference_name, self.name @@ -323,7 +323,7 @@ def create_payment_entry(self, submit=True): ) # Add reference of Payment Request - payment_entry.get("references")[0].payment_request = self.name + payment_entry.references[0].payment_request = self.name # Update dimensions payment_entry.update( @@ -333,9 +333,6 @@ def create_payment_entry(self, submit=True): } ) - payment_entry.received_amount = self.outstanding_amount - payment_entry.get("references")[0].allocated_amount = self.outstanding_amount - for dimension in get_accounting_dimensions(): payment_entry.update({dimension: self.get(dimension)}) @@ -420,12 +417,11 @@ def create_subscription(self, payment_provider, gateway_controller, data): return create_stripe_subscription(gateway_controller, data) def update_reference_advance_payment_status(self): - ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( "advance_payment_payable_doctypes" ) if self.reference_doctype in advance_payment_doctypes: - # set advance payment status + ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc.set_advance_payment_status() @@ -475,6 +471,9 @@ def make_payment_request(**args): if existing_payment_request_amount: grand_total -= existing_payment_request_amount + if not grand_total: + frappe.throw(_("Payment Request is already created")) + if draft_payment_request: frappe.db.set_value( "Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False @@ -565,29 +564,22 @@ def get_amount(ref_doc, payment_account=None): return grand_total -def get_existing_payment_request_amount(ref_dt, ref_dn, only_paid=False): +def get_existing_payment_request_amount(ref_dt, ref_dn): """ - Return the total amount of Payment Requests against a reference document. \n - If `only_paid` is True, it will return the total amount of paid Payment Requests. \n - Else, it will return the total amount of all Payment Requests. + Return the total amount of Payment Requests against a reference document. """ PR = frappe.qb.DocType("Payment Request") - if only_paid: - select = Sum(PR.grand_total - PR.outstanding_amount) - else: - select = Sum(PR.grand_total) - response = ( frappe.qb.from_(PR) - .select(select) + .select(Sum(PR.grand_total)) .where(PR.reference_doctype == ref_dt) .where(PR.reference_name == ref_dn) .where(PR.docstatus == 1) .run() ) - return response[0][0] or 0 + return response[0][0] if response else 0 def get_gateway_details(args): # nosemgrep @@ -629,40 +621,13 @@ def make_payment_entry(docname): return doc.create_payment_entry(submit=False).as_dict() -def update_payment_req_outstanding_amount(pe_doc, cancel=False): - outstanding_amounts = get_outstanding_amount_of_payment_entry_references(pe_doc.references) - - for ref in pe_doc.references: - if not ref.payment_request: - continue - - old_outstanding_amount = outstanding_amounts[ref.payment_request] - - new_outstanding_amount = ( - old_outstanding_amount + ref.allocated_amount - if cancel - else old_outstanding_amount - ref.allocated_amount - ) - - if not cancel and new_outstanding_amount < 0: - frappe.throw( - _( - "The allocated amount is greater than the outstanding amount of Payment Request {0}" - ).format(ref.payment_request) - ) - - frappe.db.set_value( - "Payment Request", - ref.payment_request, - "outstanding_amount", - new_outstanding_amount, - ) - +def update_payment_requests_as_per_pe_references(references=None, cancel=False): + if not references: + return -def update_payment_req_status(pe_doc, method): payment_requests = frappe.get_all( "Payment Request", - filters={"name": ["in", get_referenced_payment_requests(pe_doc.references)]}, + filters={"name": ["in", get_referenced_payment_requests(references)]}, fields=[ "name", "grand_total", @@ -673,24 +638,40 @@ def update_payment_req_status(pe_doc, method): payment_requests = {pr.name: pr for pr in payment_requests} - for ref in pe_doc.references: + for ref in references: if not ref.payment_request: continue payment_request = payment_requests[ref.payment_request] - if payment_request["outstanding_amount"] == payment_request["grand_total"]: + # update outstanding amount + new_outstanding_amount = ( + payment_request["outstanding_amount"] + ref.allocated_amount + if cancel + else payment_request["outstanding_amount"] - ref.allocated_amount + ) + + if not cancel and new_outstanding_amount < 0: + frappe.throw( + msg=_( + "The allocated amount is greater than the outstanding amount of Payment Request {0}" + ).format(ref.payment_request), + title=_("Invalid Allocated Amount"), + ) + + # update status + if new_outstanding_amount == payment_request["grand_total"]: status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested" - elif payment_request["outstanding_amount"] == 0: + elif new_outstanding_amount == 0: status = "Paid" - elif payment_request["outstanding_amount"] > 0: + elif new_outstanding_amount > 0: status = "Partially Paid" + # update database frappe.db.set_value( "Payment Request", ref.payment_request, - "status", - status, + {"outstanding_amount": new_outstanding_amount, "status": status}, ) @@ -718,7 +699,7 @@ def get_dummy_message(doc): {%- else %}

Hello,

{% endif %}

{{ _("Requesting payment against {0} {1} for amount {2}").format(doc.doctype, - doc.name, doc.get_formatted("grand_total")) }}

+ doc.name, doc.get_formatted("grand_total")) }}

{{ _("Make Payment") }} diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3c4599f30d98..1cb846cf5ce4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1942,35 +1942,27 @@ def set_total_advance_paid(self): self.set_advance_payment_status() - # todo: need to optimize - # todo: modularize def set_advance_payment_status(self): - from erpnext.accounts.doctype.payment_request.payment_request import ( - get_existing_payment_request_amount as get_paid_amount, - ) - new_status = None - available_payment_requests = frappe.db.count( - "Payment Request", - {"reference_doctype": self.doctype, "reference_name": self.name, "docstatus": 1}, + + paid_amount = frappe.get_value( + doctype="Payment Request", + filters={ + "reference_doctype": self.doctype, + "reference_name": self.name, + "docstatus": 1, + }, + fieldname="sum(grand_total - outstanding_amount)", ) - total_amount = self.get("rounded_total") or self.grand_total - paid_amount = get_paid_amount(self.doctype, self.name, only_paid=True) - - if paid_amount == total_amount: - new_status = "Fully Paid" - elif paid_amount > 0 and paid_amount < total_amount: - new_status = "Partially Paid" - elif self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"): - if not available_payment_requests: - new_status = "Not Requested" - elif paid_amount == 0 or paid_amount == 0.0: - new_status = "Requested" - elif self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"): - if not available_payment_requests: - new_status = "Not Initiated" - elif paid_amount == 0 or paid_amount == 0.0: - new_status = "Initiated" + + if not paid_amount: + if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"): + new_status = "Not Requested" if paid_amount is None else "Requested" + elif self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"): + new_status = "Not Initiated" if paid_amount is None else "Initiated" + else: + total_amount = self.get("rounded_total") or self.get("grand_total") + new_status = "Fully Paid" if paid_amount == total_amount else "Partially Paid" if new_status == self.advance_payment_status: return From e5a981bd730fc333181ee1de9aff91b2a1070fb0 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Thu, 25 Jul 2024 12:39:59 +0530 Subject: [PATCH 08/60] refactor: replace sql query of `matched_payment_requests` to query builder --- .../doctype/payment_entry/payment_entry.py | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 9cc85243d7d7..b08464f73f07 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -7,6 +7,8 @@ import frappe from frappe import ValidationError, _, qb, scrub, throw +from frappe.query_builder import Tuple +from frappe.query_builder.functions import Count from frappe.utils import cint, comma_or, flt, getdate, nowdate from frappe.utils.data import comma_and, fmt_money, get_link_to_form from pypika import Case @@ -1831,11 +1833,11 @@ def set_matched_payment_request(self, row_idx): ) -# FIXME: can be optimize and use query builder def get_matched_payment_requests_of_references(references=None): if not references: return + # to fetch matched rows refs = [ (row.reference_doctype, row.reference_name, row.allocated_amount) for row in references @@ -1845,26 +1847,27 @@ def get_matched_payment_requests_of_references(references=None): if not refs: return - all_matched_prs = frappe.db.sql( - """ - select name, reference_doctype, reference_name, outstanding_amount - from `tabPayment Request` - where (reference_doctype, reference_name, outstanding_amount) in %s - and status != 'Paid' and docstatus = 1 - """, - (refs,), - as_dict=True, + PR = frappe.qb.DocType("Payment Request") + + # query to group by reference_doctype, reference_name, outstanding_amount + subquery = ( + frappe.qb.from_(PR) + .select( + PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount, Count("*").as_("count") + ) + .where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs)) + .where(PR.status != "Paid") + .where(PR.docstatus == 1) + .groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount) ) - single_matched_prs = {} - for pr in all_matched_prs: - key = (pr.reference_doctype, pr.reference_name, pr.outstanding_amount) - if key in single_matched_prs: - single_matched_prs[key].append(pr.name) - else: - single_matched_prs[key] = [pr.name] + # query to fetch matched rows which are single + matched_prs = frappe.qb.from_(subquery).select("*").where(subquery.count == 1).run(as_dict=True) + + if not matched_prs: + return - return {key: names[0] for key, names in single_matched_prs.items() if len(names) == 1} + return {(pr.reference_doctype, pr.reference_name, pr.outstanding_amount): pr.name for pr in matched_prs} def validate_inclusive_tax(tax, doc): From 558d0079701c882cc763cccac61fede954111c67 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Thu, 25 Jul 2024 13:02:55 +0530 Subject: [PATCH 09/60] fix: replace `locals` with `get_doc` in set_query --- .../doctype/payment_entry/payment_entry.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 1c54ee2b733b..f8e8a6089ff8 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -167,15 +167,14 @@ frappe.ui.form.on("Payment Entry", { }); frm.set_query("payment_request", "references", function (doc, cdt, cdn) { - const row = locals[cdt][cdn]; - const filters = { - docstatus: 1, - status: ["!=", "Paid"], - reference_doctype: row.reference_doctype, - reference_name: row.reference_name, - }; + const row = frappe.get_doc(cdt, cdn); return { - filters: filters, + filters: { + docstatus: 1, + status: ["!=", "Paid"], + reference_doctype: row.reference_doctype, + reference_name: row.reference_name, + }, }; }); }, From 3c5ef059aeb7c1f4d3970ea077111758318ebe80 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Fri, 26 Jul 2024 16:49:31 +0530 Subject: [PATCH 10/60] fix: changes during review --- .../doctype/payment_entry/payment_entry.js | 12 ++++++++++-- .../doctype/payment_entry/payment_entry.py | 19 ++++++++++++------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index f8e8a6089ff8..23b8f873b5de 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1087,6 +1087,8 @@ frappe.ui.form.on("Payment Entry", { } } + let set_matched_payment_requests = false; + $.each(frm.doc.references || [], function (i, row) { if (frappe.flags.allocate_payment_amount == 0) { //If allocate payment amount checkbox is unchecked, set zero to allocate amount @@ -1095,6 +1097,8 @@ frappe.ui.form.on("Payment Entry", { frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change) ) { + let previous_allocated_amount = row.allocated_amount; + if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) { row.allocated_amount = row.outstanding_amount >= allocated_positive_outstanding @@ -1108,12 +1112,16 @@ frappe.ui.form.on("Payment Entry", { : row.outstanding_amount; allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); } + + if (!row.payment_request && row.allocated_amount > previous_allocated_amount) { + set_matched_payment_requests = true; + } } }); frm.refresh_fields(); - if (frappe.flags.allocate_payment_amount) frm.call("set_matched_payment_requests"); frm.events.set_total_allocated_amount(frm); + if (set_matched_payment_requests) frm.call("set_matched_payment_requests"); }, set_total_allocated_amount: function (frm) { @@ -1707,7 +1715,7 @@ frappe.ui.form.on("Payment Entry Reference", { const row = frappe.get_doc(cdt, cdn); - if (row.payment_request || !row.reference_name || !row.reference_doctype || !row.allocated_amount) + if (row.payment_request || !(row.reference_doctype && row.reference_name && row.allocated_amount)) return; frm.call("set_matched_payment_request", { row_idx: row.idx }); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b08464f73f07..ef5945561c26 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -294,15 +294,17 @@ def update_outstanding_amounts(self): self.set_missing_ref_details(force=True) def validate_duplicate_entry(self): - reference_names = [] + reference_names = {} for d in self.get("references"): - if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names: + key = (d.reference_doctype, d.reference_name, d.payment_term, d.payment_request) + if key in reference_names: frappe.throw( _("Row #{0}: Duplicate entry in References {1} {2}").format( d.idx, d.reference_doctype, d.reference_name ) ) - reference_names.append((d.reference_doctype, d.reference_name, d.payment_term)) + + reference_names.add(key) def set_bank_account_data(self): if self.bank_account: @@ -1742,10 +1744,13 @@ def check_references_for_unset_payment_request(self): if not self.references: return - matched_payment_requests = get_matched_payment_requests_of_references( + matched_payment_requests = get_matched_payment_requests( [row for row in self.references if not row.payment_request] ) + if not matched_payment_requests: + return + unset_pr_rows = {} for row in self.references: @@ -1776,7 +1781,7 @@ def set_matched_payment_requests(self): if not self.references: return - matched_payment_requests = get_matched_payment_requests_of_references(self.references) + matched_payment_requests = get_matched_payment_requests(self.references) matched_count = 0 @@ -1820,7 +1825,7 @@ def set_matched_payment_request(self, row_idx): ): return - matched_pr = get_matched_payment_requests_of_references([row]) + matched_pr = get_matched_payment_requests([row]) if not matched_pr: return @@ -1833,7 +1838,7 @@ def set_matched_payment_request(self, row_idx): ) -def get_matched_payment_requests_of_references(references=None): +def get_matched_payment_requests(references=None): if not references: return From 83b10d4e319813fa618eeadde5204745073733cd Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 12 Aug 2024 12:15:56 +0530 Subject: [PATCH 11/60] fix: minor review changes --- .../doctype/payment_entry/payment_entry.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7b0e2cb00473..2d9945b96cce 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -190,10 +190,9 @@ def on_submit(self): frappe.throw(_("Difference Amount must be zero")) self.make_gl_entries() self.update_outstanding_amounts() - self.update_advance_paid() self.update_payment_schedule() self.update_payment_requests() - self.update_references_advance_payment_status() + self.update_advance_paid() # advance_paid_status depends on the payment request amount self.set_status() def set_liability_account(self): @@ -265,11 +264,10 @@ def on_cancel(self): super().on_cancel() self.make_gl_entries(cancel=1) self.update_outstanding_amounts() - self.update_advance_paid() self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) self.update_payment_requests(cancel=True) - self.update_references_advance_payment_status() + self.update_advance_paid() # advance_paid_status depends on the payment request amount self.set_status() def update_payment_requests(self, cancel=False): @@ -279,21 +277,11 @@ def update_payment_requests(self, cancel=False): update_payment_requests_as_per_pe_references(self.references, cancel=cancel) - def update_references_advance_payment_status(self): - advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks( - "advance_payment_payable_doctypes" - ) - - for ref in self.get("references"): - if ref.reference_doctype in advance_payment_doctypes: - ref_doc = frappe.get_doc(ref.reference_doctype, ref.reference_name) - ref_doc.set_advance_payment_status() - def update_outstanding_amounts(self): self.set_missing_ref_details(force=True) def validate_duplicate_entry(self): - reference_names = {} + reference_names = set() for d in self.get("references"): key = (d.reference_doctype, d.reference_name, d.payment_term, d.payment_request) if key in reference_names: From dc757e1091c443975961ea3f405c8a96a908aff5 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 14 Aug 2024 11:25:58 +0530 Subject: [PATCH 12/60] fix: remove unnecessary code for setting payment entry received amount --- .../accounts/doctype/payment_request/payment_request.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index ebe35ba0bc9c..161b0bfdadd4 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -333,14 +333,6 @@ def create_payment_entry(self, submit=True): } ) - if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: - amount = payment_entry.base_paid_amount - else: - amount = self.grand_total - - payment_entry.received_amount = amount - payment_entry.get("references")[0].allocated_amount = amount - # Update 'Paid Amount' on Forex transactions if self.currency != ref_doc.company_currency: if ( From 3bfda7958017c59a45695536ac1562c069830c1f Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Thu, 15 Aug 2024 01:14:35 +0530 Subject: [PATCH 13/60] fix: logic for ser payment_request if PE made from transaction --- .../doctype/payment_entry/payment_entry.py | 125 ++++++++++++++++++ .../payment_request/payment_request.py | 7 +- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 2d9945b96cce..d6c8e322ca19 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2473,6 +2473,7 @@ def get_reference_details( return res +# todo-abdeali: add one more parameter that if it created from `payment_request` @frappe.whitelist() def get_payment_entry( dt, @@ -2633,9 +2634,133 @@ def get_payment_entry( pe.set_difference_amount() + # only if allocated_amount is set + set_open_payment_requests_to_references(pe.references) + return pe +def get_open_payment_requests_for_references(references=None): + if not references: + return + + refs = { + (row.reference_doctype, row.reference_name) + for row in references + if row.reference_doctype and row.reference_name and row.allocated_amount + } + + if not refs: + return + + PR = frappe.qb.DocType("Payment Request") + + response = ( + frappe.qb.from_(PR) + .select(PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount) + .where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs))) + .where(PR.status != "Paid") + .where(PR.docstatus == 1) + .orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc) + ).run(as_dict=True) + + if not response: + return + + reference_payment_requests = {} + + for row in response: + key = (row.reference_doctype, row.reference_name) + + if key not in reference_payment_requests: + reference_payment_requests[key] = {row.name: row.outstanding_amount} + else: + reference_payment_requests[key][row.name] = row.outstanding_amount + + return reference_payment_requests + + +# todo-abdeali: make it more efficient and less complex +def set_open_payment_requests_to_references(references=None): + if not references: + return + + reference_payment_requests = get_open_payment_requests_for_references(references) + + if not reference_payment_requests: + return + + row_idx = 0 + + while row_idx < len(references): + row = references[row_idx] + key = (row.reference_doctype, row.reference_name) + + # ? can make it efficient if only one transaction is there but have multiple row because of terms + payment_requests = reference_payment_requests.get(key) + + if not payment_requests: + row_idx += 1 + continue + + payment_request, outstanding_amount = next(iter(payment_requests.items())) + allocated_amount = row.allocated_amount + + if outstanding_amount == allocated_amount: + row.payment_request = payment_request + del reference_payment_requests[key][payment_request] + row_idx += 1 + elif outstanding_amount > allocated_amount: + row.payment_request = payment_request + + reference_payment_requests[key][payment_request] -= allocated_amount + row_idx += 1 + + elif outstanding_amount < allocated_amount: + row.payment_request = payment_request + row.allocated_amount = outstanding_amount + + del reference_payment_requests[key][payment_request] + allocated_amount -= outstanding_amount + + while allocated_amount: + payment_request, outstanding_amount = next(iter(payment_requests.items()), (None, None)) + + new_row = frappe.copy_doc(row) + new_row.allocated_amount = allocated_amount + references.insert(row_idx + 1, new_row) + + if not payment_request or not outstanding_amount: + new_row.allocated_amount = allocated_amount + new_row.payment_request = None + row_idx += 2 + break + else: + new_row.payment_request = payment_request + + if outstanding_amount == allocated_amount: + new_row.allocated_amount = allocated_amount + del reference_payment_requests[key][payment_request] + row_idx += 2 + + break + elif outstanding_amount > allocated_amount: + new_row.allocated_amount = allocated_amount + reference_payment_requests[key][payment_request] -= allocated_amount + row_idx += 2 + break + elif outstanding_amount < allocated_amount: + allocated_amount -= outstanding_amount + new_row.allocated_amount = outstanding_amount + + del reference_payment_requests[key][payment_request] + row_idx += 1 + + # set new idx to all refs + for idx, ref in enumerate(references, start=1): + ref.idx = idx + + def update_accounting_dimensions(pe, doc): """ Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 161b0bfdadd4..a858f8a56965 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -561,10 +561,13 @@ def get_amount(ref_doc, payment_account=None): grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) elif dt in ["Sales Invoice", "Purchase Invoice"]: if not ref_doc.get("is_pos"): + # use rounded totals to match with PE if ref_doc.party_account_currency == ref_doc.currency: - grand_total = flt(ref_doc.grand_total) + grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) else: - grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate + grand_total = ( + flt(ref_doc.base_rounded_total) or flt(ref_doc.base_grand_total) / ref_doc.conversion_rate + ) elif dt == "Sales Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: From 208a7e3533344a61ab3098614e1378593f4c2a2c Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 20 Aug 2024 10:11:42 +0530 Subject: [PATCH 14/60] fix: Use rounded total to make Payment Request from `Sales Invoice` or `Purchase Invoice` --- .../accounts/doctype/payment_request/payment_request.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index a858f8a56965..776dbe45e747 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -561,12 +561,11 @@ def get_amount(ref_doc, payment_account=None): grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) elif dt in ["Sales Invoice", "Purchase Invoice"]: if not ref_doc.get("is_pos"): - # use rounded totals to match with PE if ref_doc.party_account_currency == ref_doc.currency: - grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) + grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total) else: - grand_total = ( - flt(ref_doc.base_rounded_total) or flt(ref_doc.base_grand_total) / ref_doc.conversion_rate + grand_total = flt( + flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate ) elif dt == "Sales Invoice": for pay in ref_doc.payments: From c2973d2e5cf4969736ae691e8730f433eaf8b70b Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 20 Aug 2024 14:37:37 +0530 Subject: [PATCH 15/60] refactor: enhance logic of `set_open_payment_requests_to_references` --- .../doctype/payment_entry/payment_entry.py | 114 ++++++++++-------- 1 file changed, 61 insertions(+), 53 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index d6c8e322ca19..7ac550c37700 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2680,85 +2680,93 @@ def get_open_payment_requests_for_references(references=None): return reference_payment_requests -# todo-abdeali: make it more efficient and less complex def set_open_payment_requests_to_references(references=None): if not references: return - reference_payment_requests = get_open_payment_requests_for_references(references) + # get all unpaid payment requests for the references + all_references_payment_requests = get_open_payment_requests_for_references(references) - if not reference_payment_requests: + if not all_references_payment_requests: return - row_idx = 0 + # to manage new rows + row_number = 1 + MOVE_TO_NEXT_ROW = 1 + TO_SKIP_NEW_ROW = 2 - while row_idx < len(references): - row = references[row_idx] - key = (row.reference_doctype, row.reference_name) + while row_number <= len(references): + row = references[row_number - 1] + reference_key = (row.reference_doctype, row.reference_name) + + # update the idx to maintain the order + row.idx = row_number - # ? can make it efficient if only one transaction is there but have multiple row because of terms - payment_requests = reference_payment_requests.get(key) + # unpaid payment requests for the reference + reference_payment_requests = all_references_payment_requests.get(reference_key) - if not payment_requests: - row_idx += 1 + if not reference_payment_requests: + row_number += MOVE_TO_NEXT_ROW # to move to next reference row continue - payment_request, outstanding_amount = next(iter(payment_requests.items())) + # get the first payment request and its outstanding amount + payment_request, outstanding_amount = next(iter(reference_payment_requests.items())) allocated_amount = row.allocated_amount + # allocate the payment request to the reference + row.payment_request = payment_request + if outstanding_amount == allocated_amount: - row.payment_request = payment_request - del reference_payment_requests[key][payment_request] - row_idx += 1 - elif outstanding_amount > allocated_amount: - row.payment_request = payment_request + del reference_payment_requests[payment_request] + row_number += MOVE_TO_NEXT_ROW - reference_payment_requests[key][payment_request] -= allocated_amount - row_idx += 1 + elif outstanding_amount > allocated_amount: + # reduce the outstanding amount of the payment request + reference_payment_requests[payment_request] -= allocated_amount + row_number += MOVE_TO_NEXT_ROW - elif outstanding_amount < allocated_amount: - row.payment_request = payment_request + else: + # split the reference row to allocate the remaining amount + del reference_payment_requests[payment_request] row.allocated_amount = outstanding_amount - - del reference_payment_requests[key][payment_request] allocated_amount -= outstanding_amount + # set the remaining amount to the next row while allocated_amount: - payment_request, outstanding_amount = next(iter(payment_requests.items()), (None, None)) - + # create a new row for the remaining amount new_row = frappe.copy_doc(row) - new_row.allocated_amount = allocated_amount - references.insert(row_idx + 1, new_row) + references.insert(row_number, new_row) + + # get the next payment request and its outstanding amount + payment_request, outstanding_amount = next( + iter(reference_payment_requests.items()), (None, None) + ) + + # update new row + new_row.idx = row_number + 1 + new_row.payment_request = payment_request + new_row.allocated_amount = min( + outstanding_amount if outstanding_amount else allocated_amount, allocated_amount + ) if not payment_request or not outstanding_amount: - new_row.allocated_amount = allocated_amount - new_row.payment_request = None - row_idx += 2 + row_number += TO_SKIP_NEW_ROW + break + + elif outstanding_amount == allocated_amount: + del reference_payment_requests[payment_request] + row_number += TO_SKIP_NEW_ROW + break + + elif outstanding_amount > allocated_amount: + reference_payment_requests[payment_request] -= allocated_amount + row_number += TO_SKIP_NEW_ROW break + else: - new_row.payment_request = payment_request - - if outstanding_amount == allocated_amount: - new_row.allocated_amount = allocated_amount - del reference_payment_requests[key][payment_request] - row_idx += 2 - - break - elif outstanding_amount > allocated_amount: - new_row.allocated_amount = allocated_amount - reference_payment_requests[key][payment_request] -= allocated_amount - row_idx += 2 - break - elif outstanding_amount < allocated_amount: - allocated_amount -= outstanding_amount - new_row.allocated_amount = outstanding_amount - - del reference_payment_requests[key][payment_request] - row_idx += 1 - - # set new idx to all refs - for idx, ref in enumerate(references, start=1): - ref.idx = idx + allocated_amount -= outstanding_amount + del reference_payment_requests[payment_request] + row_number += MOVE_TO_NEXT_ROW def update_accounting_dimensions(pe, doc): From da36e3102c23133dd5775e5028ace063d8e60816 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 20 Aug 2024 15:41:12 +0530 Subject: [PATCH 16/60] fix: added one optional arg `created_from_payment_request` --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++--- erpnext/accounts/doctype/payment_request/payment_request.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7ac550c37700..c6b1b93a5fb1 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2473,7 +2473,6 @@ def get_reference_details( return res -# todo-abdeali: add one more parameter that if it created from `payment_request` @frappe.whitelist() def get_payment_entry( dt, @@ -2485,6 +2484,7 @@ def get_payment_entry( payment_type=None, reference_date=None, ignore_permissions=False, + created_from_payment_request=False, ): doc = frappe.get_doc(dt, dn) over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") @@ -2634,8 +2634,8 @@ def get_payment_entry( pe.set_difference_amount() - # only if allocated_amount is set - set_open_payment_requests_to_references(pe.references) + if not created_from_payment_request: + set_open_payment_requests_to_references(pe.references) return pe diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 776dbe45e747..49f965a7fd02 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -310,6 +310,7 @@ def create_payment_entry(self, submit=True): party_amount=party_amount, bank_account=self.payment_account, bank_amount=bank_amount, + created_from_payment_request=True, ) payment_entry.update( From baa11d1e9e531f1d7c1e6553eff7e5173d5b5b3a Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 20 Aug 2024 17:18:07 +0530 Subject: [PATCH 17/60] fix: handle multiple allocation of PR at PE's reference --- erpnext/accounts/doctype/payment_request/payment_request.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 49f965a7fd02..80cc2fa46879 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -671,6 +671,9 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False): else payment_request["outstanding_amount"] - ref.allocated_amount ) + # to handle same payment request for the multiple allocations + payment_request["outstanding_amount"] = new_outstanding_amount + if not cancel and new_outstanding_amount < 0: frappe.throw( msg=_( From 53f30ec4d8a007aeab8bc05652825305413f8e8b Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 20 Aug 2024 19:04:28 +0530 Subject: [PATCH 18/60] fix: logic for PR if outstanding docs fetch --- .../doctype/payment_entry/payment_entry.js | 19 +----- .../doctype/payment_entry/payment_entry.py | 62 +------------------ 2 files changed, 5 insertions(+), 76 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 616cd2e3e78d..761e6afc4a8d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1104,8 +1104,6 @@ frappe.ui.form.on("Payment Entry", { } } - let set_matched_payment_requests = false; - $.each(frm.doc.references || [], function (i, row) { if (frappe.flags.allocate_payment_amount == 0) { //If allocate payment amount checkbox is unchecked, set zero to allocate amount @@ -1114,8 +1112,6 @@ frappe.ui.form.on("Payment Entry", { frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change) ) { - let previous_allocated_amount = row.allocated_amount; - if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) { row.allocated_amount = row.outstanding_amount >= allocated_positive_outstanding @@ -1129,16 +1125,12 @@ frappe.ui.form.on("Payment Entry", { : row.outstanding_amount; allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); } - - if (!row.payment_request && row.allocated_amount > previous_allocated_amount) { - set_matched_payment_requests = true; - } } }); frm.refresh_fields(); frm.events.set_total_allocated_amount(frm); - if (set_matched_payment_requests) frm.call("set_matched_payment_requests"); + if (frappe.flags.allocate_payment_amount) frm.call("set_payment_requests_to_references"); }, set_total_allocated_amount: function (frm) { @@ -1727,15 +1719,8 @@ frappe.ui.form.on("Payment Entry Reference", { } }, - allocated_amount: function (frm, cdt, cdn) { + allocated_amount: function (frm) { frm.events.set_total_allocated_amount(frm); - - const row = frappe.get_doc(cdt, cdn); - - if (row.payment_request || !(row.reference_doctype && row.reference_name && row.allocated_amount)) - return; - - frm.call("set_matched_payment_request", { row_idx: row.idx }); }, references_remove: function (frm) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c6b1b93a5fb1..192c7ac58912 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1719,6 +1719,7 @@ def get_current_tax_fraction(self, tax): return current_tax_fraction + # todo-abdeali: needs changes if PR already use for ref and one ref row have no PR then do not show this!! def check_references_for_unset_payment_request(self): if not self.references: return @@ -1756,65 +1757,8 @@ def check_references_for_unset_payment_request(self): ) @frappe.whitelist() - def set_matched_payment_requests(self): - if not self.references: - return - - matched_payment_requests = get_matched_payment_requests(self.references) - - matched_count = 0 - - for row in self.references: - if ( - row.payment_request - or not row.reference_doctype - or not row.reference_name - or not row.allocated_amount - ): - continue - - row.payment_request = matched_payment_requests.get( - (row.reference_doctype, row.reference_name, row.allocated_amount) - ) - - if row.payment_request: - matched_count += 1 - - if not matched_count: - return - - frappe.msgprint( - msg=_("Setting {0} matched Payment Request(s)").format(matched_count), - alert=True, - ) - - @frappe.whitelist() - def set_matched_payment_request(self, row_idx): - row = next((row for row in self.references if row.idx == row_idx), None) - - if not row: - frappe.throw(_("Row #{0} not found").format(row_idx), title=_("Row Not Found")) - - # if payment entry already set then do not set it again - if ( - row.payment_request - or not row.reference_doctype - or not row.reference_name - or not row.allocated_amount - ): - return - - matched_pr = get_matched_payment_requests([row]) - - if not matched_pr: - return - - row.payment_request = matched_pr[(row.reference_doctype, row.reference_name, row.allocated_amount)] - - frappe.msgprint( - msg=_("Setting matched Payment Request"), - alert=True, - ) + def set_payment_requests_to_references(self): + set_open_payment_requests_to_references(self.references) def get_matched_payment_requests(references=None): From 77ba62a6c3cf7c681802b0104d9a47a988828c37 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 21 Aug 2024 15:57:49 +0530 Subject: [PATCH 19/60] fix: formatted Link field for `Payment Request` for PE's references --- .../doctype/payment_entry/payment_entry.js | 3 +- .../doctype/payment_entry/payment_entry.py | 3 ++ .../payment_request/payment_request.py | 42 +++++++++++++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 761e6afc4a8d..4df93ba0b77f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -169,9 +169,8 @@ frappe.ui.form.on("Payment Entry", { frm.set_query("payment_request", "references", function (doc, cdt, cdn) { const row = frappe.get_doc(cdt, cdn); return { + query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests", filters: { - docstatus: 1, - status: ["!=", "Paid"], reference_doctype: row.reference_doctype, reference_name: row.reference_name, }, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 192c7ac58912..2d93353a3425 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -336,6 +336,9 @@ def validate_allocated_amount_as_per_payment_request(self): get_outstanding_amount_of_payment_entry_references as get_outstanding_amounts, ) + if not self.references: + return + outstanding_amounts = get_outstanding_amounts(self.references) for ref in self.references: diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 80cc2fa46879..b85761927233 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -698,7 +698,10 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False): ) -def get_outstanding_amount_of_payment_entry_references(references: list) -> dict: +def get_outstanding_amount_of_payment_entry_references(references): + if not references: + return {} + payment_requests = get_referenced_payment_requests(references) return dict( @@ -711,8 +714,11 @@ def get_outstanding_amount_of_payment_entry_references(references: list) -> dict ) -def get_referenced_payment_requests(references: list) -> set: - return {row.payment_request for row in references if row.payment_request} +def get_referenced_payment_requests(references): + if not references: + return () + + return {row["payment_request"] for row in references if row["payment_request"]} def get_dummy_message(doc): @@ -823,3 +829,33 @@ def get_paid_amount_against_order(dt, dn): ) ) ).run()[0][0] or 0 + + +@frappe.whitelist() +def get_open_payment_requests(doctype, txt, searchfield, start, page_len, filters): + reference_doctype = filters.get("reference_doctype") + reference_name = filters.get("reference_doctype") + + if not reference_doctype or not reference_name: + return [] + + open_payment_requests = frappe.get_all( + "Payment Request", + filters={ + "reference_doctype": filters["reference_doctype"], + "reference_name": filters["reference_name"], + "status": ["!=", "Paid"], + "docstatus": 1, + }, + fields=["name", "grand_total", "outstanding_amount"], + order_by="transaction_date ASC,creation ASC", + ) + + return [ + ( + pr.name, + _("Grand Total: {0}").format(pr.grand_total), + _("Outstanding Amount: {0}").format(pr.outstanding_amount), + ) + for pr in open_payment_requests + ] From bb02ffbaacb5ea59d8e1aaec31b022f768d111b7 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 21 Aug 2024 16:33:14 +0530 Subject: [PATCH 20/60] fix: replace `get_all()` with `get_list()` for getting Payment Request for Link field --- erpnext/accounts/doctype/payment_request/payment_request.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index b85761927233..b301a80aec47 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -833,13 +833,14 @@ def get_paid_amount_against_order(dt, dn): @frappe.whitelist() def get_open_payment_requests(doctype, txt, searchfield, start, page_len, filters): + # permission checks in `get_list()` reference_doctype = filters.get("reference_doctype") reference_name = filters.get("reference_doctype") if not reference_doctype or not reference_name: return [] - open_payment_requests = frappe.get_all( + open_payment_requests = frappe.get_list( "Payment Request", filters={ "reference_doctype": filters["reference_doctype"], From f024d6670c434eb51e5d88d0719f285c0303c6a9 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 21 Aug 2024 16:33:44 +0530 Subject: [PATCH 21/60] fix: replace `get_all()` with `get_list()` for getting Payment Request for Link field --- erpnext/accounts/doctype/payment_request/payment_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index b301a80aec47..a194b657563e 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -833,7 +833,7 @@ def get_paid_amount_against_order(dt, dn): @frappe.whitelist() def get_open_payment_requests(doctype, txt, searchfield, start, page_len, filters): - # permission checks in `get_list()` + # permission checks in `get_list()` reference_doctype = filters.get("reference_doctype") reference_name = filters.get("reference_doctype") From 70ec3f6aba4b5171fa483fbcb16304cd7ef84ab9 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 21 Aug 2024 16:45:45 +0530 Subject: [PATCH 22/60] chore: format `payment_entry.js` file --- .../doctype/payment_entry/payment_entry.js | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 4df93ba0b77f..03aeab606aab 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -195,6 +195,7 @@ frappe.ui.form.on("Payment Entry", { }; }); }, + refresh: function (frm) { erpnext.hide_company(frm); frm.events.hide_unhide_fields(frm); @@ -1676,6 +1677,37 @@ frappe.ui.form.on("Payment Entry", { return current_tax_amount; }, + + cost_center: function (frm) { + if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) { + return frappe.call({ + method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance", + args: { + company: frm.doc.company, + date: frm.doc.posting_date, + paid_from: frm.doc.paid_from, + paid_to: frm.doc.paid_to, + ptype: frm.doc.party_type, + pty: frm.doc.party, + cost_center: frm.doc.cost_center, + }, + callback: function (r, rt) { + if (r.message) { + frappe.run_serially([ + () => { + frm.set_value( + "paid_from_account_balance", + r.message.paid_from_account_balance + ); + frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance); + frm.set_value("party_balance", r.message.party_balance); + }, + ]); + } + }, + }); + } + }, }); frappe.ui.form.on("Payment Entry Reference", { @@ -1768,35 +1800,3 @@ frappe.ui.form.on("Payment Entry Deduction", { frm.events.set_unallocated_amount(frm); }, }); -frappe.ui.form.on("Payment Entry", { - cost_center: function (frm) { - if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) { - return frappe.call({ - method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance", - args: { - company: frm.doc.company, - date: frm.doc.posting_date, - paid_from: frm.doc.paid_from, - paid_to: frm.doc.paid_to, - ptype: frm.doc.party_type, - pty: frm.doc.party, - cost_center: frm.doc.cost_center, - }, - callback: function (r, rt) { - if (r.message) { - frappe.run_serially([ - () => { - frm.set_value( - "paid_from_account_balance", - r.message.paid_from_account_balance - ); - frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance); - frm.set_value("party_balance", r.message.party_balance); - }, - ]); - } - }, - }); - } - }, -}); From cc5db67362b398480503ac753769ce0d3a29fd74 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 21 Aug 2024 17:40:06 +0530 Subject: [PATCH 23/60] style: Show preview popup of `Payment Request` --- .../accounts/doctype/payment_request/payment_request.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 50cc12aa4ea2..e08a0c8ffb7b 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -70,6 +70,7 @@ { "fieldname": "transaction_date", "fieldtype": "Date", + "in_preview": 1, "label": "Transaction Date" }, { @@ -408,6 +409,7 @@ "depends_on": "eval: doc.docstatus === 1", "fieldname": "outstanding_amount", "fieldtype": "Currency", + "in_preview": 1, "label": "Outstanding Amount", "non_negative": 1, "read_only": 1 @@ -417,7 +419,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-07-23 19:02:07.754296", + "modified": "2024-08-21 17:17:16.584404", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", @@ -452,6 +454,7 @@ "write": 1 } ], + "show_preview_popup": 1, "sort_field": "creation", "sort_order": "DESC", "states": [] From 3270991d3e10eb210e147bfbb20480522661592d Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Thu, 22 Aug 2024 12:32:10 +0530 Subject: [PATCH 24/60] fix: remove minor bug --- erpnext/accounts/doctype/payment_request/payment_request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index a194b657563e..c4d291cf91cf 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -665,7 +665,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False): payment_request = payment_requests[ref.payment_request] # update outstanding amount - new_outstanding_amount = ( + new_outstanding_amount = flt( payment_request["outstanding_amount"] + ref.allocated_amount if cancel else payment_request["outstanding_amount"] - ref.allocated_amount @@ -718,7 +718,7 @@ def get_referenced_payment_requests(references): if not references: return () - return {row["payment_request"] for row in references if row["payment_request"]} + return {row.payment_request for row in references if row.payment_request} def get_dummy_message(doc): From dfefdffa9b2a80003d9376370c283e797b344ddc Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Thu, 22 Aug 2024 18:28:13 +0530 Subject: [PATCH 25/60] fix: add virtual field for Payment Term and Request `outstanding_amount` in PE's reference --- .../doctype/payment_entry/payment_entry.py | 4 +++- .../payment_entry_reference.json | 22 +++++++++++++++++-- .../payment_entry_reference.py | 2 ++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 2d93353a3425..16c1c1962a40 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2660,8 +2660,9 @@ def set_open_payment_requests_to_references(references=None): payment_request, outstanding_amount = next(iter(reference_payment_requests.items())) allocated_amount = row.allocated_amount - # allocate the payment request to the reference + # allocate the payment request to the reference and PR's outstanding amount row.payment_request = payment_request + row.payment_request_outstanding = outstanding_amount if outstanding_amount == allocated_amount: del reference_payment_requests[payment_request] @@ -2692,6 +2693,7 @@ def set_open_payment_requests_to_references(references=None): # update new row new_row.idx = row_number + 1 new_row.payment_request = payment_request + new_row.payment_request_outstanding = outstanding_amount new_row.allocated_amount = min( outstanding_amount if outstanding_amount else allocated_amount, allocated_amount ) diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 7fce86c99d9a..75d5a79b97b7 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -10,6 +10,7 @@ "due_date", "bill_no", "payment_term", + "payment_term_outstanding", "account_type", "payment_type", "column_break_4", @@ -19,7 +20,8 @@ "exchange_rate", "exchange_gain_loss", "account", - "payment_request" + "payment_request", + "payment_request_outstanding" ], "fields": [ { @@ -127,12 +129,28 @@ "fieldtype": "Link", "label": "Payment Request", "options": "Payment Request" + }, + { + "depends_on": "eval: doc.payment_term", + "fieldname": "payment_term_outstanding", + "fieldtype": "Float", + "is_virtual": 1, + "label": "Payment Term Outstanding", + "read_only": 1 + }, + { + "depends_on": "eval: doc.payment_request", + "fieldname": "payment_request_outstanding", + "fieldtype": "Float", + "is_virtual": 1, + "label": "Payment Request Outstanding", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-07-20 17:57:32.866780", + "modified": "2024-08-22 18:16:42.138982", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py index 68d819d08408..fbd5571531b2 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -26,7 +26,9 @@ class PaymentEntryReference(Document): parentfield: DF.Data parenttype: DF.Data payment_request: DF.Link | None + payment_request_outstanding: DF.Float payment_term: DF.Link | None + payment_term_outstanding: DF.Float payment_type: DF.Data | None reference_doctype: DF.Link reference_name: DF.DynamicLink From d4b36015c076a711daae8e0aed87f72914d71e6b Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 23 Aug 2024 12:46:57 +0530 Subject: [PATCH 26/60] fix: get outstanding amount in PE's reference on realtime --- .../doctype/payment_entry/payment_entry.js | 8 +++++++ .../payment_entry_reference.json | 2 +- .../payment_entry_reference.py | 24 +++++++++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 03aeab606aab..1ceeccbcb78f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -194,6 +194,14 @@ frappe.ui.form.on("Payment Entry", { }, }; }); + + // todo: fetch payment term outstanding amount also + frm.add_fetch( + "payment_request", + "outstanding_amount", + "payment_request_outstanding", + "Payment Entry Reference" + ); }, refresh: function (frm) { diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 75d5a79b97b7..0506517a70c0 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -150,7 +150,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-08-22 18:16:42.138982", + "modified": "2024-08-23 12:35:40.525380", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py index fbd5571531b2..3afff4c8d25b 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -1,7 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - +import frappe from frappe.model.document import Document @@ -35,4 +35,24 @@ class PaymentEntryReference(Document): total_amount: DF.Float # end: auto-generated types - pass + @property + def payment_term_outstanding(self): + if not self.payment_term: + return 0 + + return frappe.db.get_value( + "Payment Schedule", + { + "payment_term": self.payment_term, + "parenttype": self.reference_doctype, + "parent": self.reference_name, + }, + "outstanding", + ) + + @property + def payment_request_outstanding(self): + if not self.payment_request: + return 0 + + return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount") From 9e092bb62234397ac70ba08d0fc2f3f6856a61ee Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Sat, 24 Aug 2024 17:23:53 +0530 Subject: [PATCH 27/60] fix: move allocation of allocated_amount to server side (no change) --- .../doctype/payment_entry/payment_entry.js | 94 ++------------ .../doctype/payment_entry/payment_entry.py | 119 +++++++++++++++++- 2 files changed, 122 insertions(+), 91 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 1ceeccbcb78f..96c72bf820ec 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -228,6 +228,7 @@ frappe.ui.form.on("Payment Entry", { ); } erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm); + frappe.flags.allocate_payment_amount = true; }, validate_company: (frm) => { @@ -1050,95 +1051,16 @@ frappe.ui.form.on("Payment Entry", { return ["Sales Invoice", "Purchase Invoice"]; }, - allocate_party_amount_against_ref_docs: function (frm, paid_amount, paid_amount_change) { - var total_positive_outstanding_including_order = 0; - var total_negative_outstanding = 0; - var total_deductions = frappe.utils.sum( - $.map(frm.doc.deductions || [], function (d) { - return flt(d.amount); - }) - ); - - paid_amount -= total_deductions; - - $.each(frm.doc.references || [], function (i, row) { - if (flt(row.outstanding_amount) > 0) - total_positive_outstanding_including_order += flt(row.outstanding_amount); - else total_negative_outstanding += Math.abs(flt(row.outstanding_amount)); - }); - - var allocated_negative_outstanding = 0; - if ( - (frm.doc.payment_type == "Receive" && frm.doc.party_type == "Customer") || - (frm.doc.payment_type == "Pay" && frm.doc.party_type == "Supplier") || - (frm.doc.payment_type == "Pay" && frm.doc.party_type == "Employee") - ) { - if (total_positive_outstanding_including_order > paid_amount) { - var remaining_outstanding = total_positive_outstanding_including_order - paid_amount; - allocated_negative_outstanding = - total_negative_outstanding < remaining_outstanding - ? total_negative_outstanding - : remaining_outstanding; - } - - var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding; - } else if (["Customer", "Supplier"].includes(frm.doc.party_type)) { - total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount")); - if (paid_amount > total_negative_outstanding) { - if (total_negative_outstanding == 0) { - frappe.msgprint( - __("Cannot {0} {1} {2} without any negative outstanding invoice", [ - frm.doc.payment_type, - frm.doc.party_type == "Customer" ? "to" : "from", - frm.doc.party_type, - ]) - ); - return false; - } else { - frappe.msgprint( - __("Paid Amount cannot be greater than total negative outstanding amount {0}", [ - total_negative_outstanding, - ]) - ); - return false; - } - } else { - allocated_positive_outstanding = total_negative_outstanding - paid_amount; - allocated_negative_outstanding = - paid_amount + - (total_positive_outstanding_including_order < allocated_positive_outstanding - ? total_positive_outstanding_including_order - : allocated_positive_outstanding); - } - } - - $.each(frm.doc.references || [], function (i, row) { - if (frappe.flags.allocate_payment_amount == 0) { - //If allocate payment amount checkbox is unchecked, set zero to allocate amount - row.allocated_amount = 0; - } else if ( - frappe.flags.allocate_payment_amount != 0 && - (!row.allocated_amount || paid_amount_change) - ) { - if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) { - row.allocated_amount = - row.outstanding_amount >= allocated_positive_outstanding - ? allocated_positive_outstanding - : row.outstanding_amount; - allocated_positive_outstanding -= flt(row.allocated_amount); - } else if (row.outstanding_amount < 0 && allocated_negative_outstanding) { - row.allocated_amount = - Math.abs(row.outstanding_amount) >= allocated_negative_outstanding - ? -1 * allocated_negative_outstanding - : row.outstanding_amount; - allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); - } - } + allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { + await frm.call("allocate_party_amount_against_ref_docs", { + paid_amount: paid_amount, + paid_amount_change: paid_amount_change ? paid_amount_change : 0, + allocate_payment_amount: frappe.flags.allocate_payment_amount + ? frappe.flags.allocate_payment_amount + : 0, }); - frm.refresh_fields(); frm.events.set_total_allocated_amount(frm); - if (frappe.flags.allocate_payment_amount) frm.call("set_payment_requests_to_references"); }, set_total_allocated_amount: function (frm) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 16c1c1962a40..7c58dd86a23c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -182,8 +182,10 @@ def validate(self): self.set_status() self.set_total_in_words() + # todo-abdeali: move to front end def before_save(self): - self.check_references_for_unset_payment_request() + pass + # self.check_references_for_unset_payment_request() def on_submit(self): if self.difference_amount: @@ -1727,7 +1729,7 @@ def check_references_for_unset_payment_request(self): if not self.references: return - matched_payment_requests = get_matched_payment_requests( + matched_payment_requests = get_matched_payment_request_of_references( [row for row in self.references if not row.payment_request] ) @@ -1760,11 +1762,91 @@ def check_references_for_unset_payment_request(self): ) @frappe.whitelist() - def set_payment_requests_to_references(self): - set_open_payment_requests_to_references(self.references) + def allocate_party_amount_against_ref_docs( + self, paid_amount, paid_amount_change, allocate_payment_amount + ): + if not self.references: + return + + # if `allocate_payment_amount` is False, then do not allocate amount + if not allocate_payment_amount: + for ref in self.references: + ref.allocated_amount = 0 + return + # todo-abdeali: here will update table (reference) priority given to those which have PR inside it ... + # todo: store old data also, never should allow more amount than paid amount + + # variables which will be used to calculate the allocated amount + total_positive_outstanding_including_order = 0 + total_negative_outstanding = 0 + paid_amount -= sum(flt(d.amount, self.precision("paid_amount")) for d in self.deductions) + + # count total positive outstanding and total negative outstanding + for ref in self.references: + outstanding_amount = flt(ref.outstanding_amount, self.precision("paid_amount")) + abs_outstanding_amount = abs(outstanding_amount) + + if outstanding_amount > 0: + total_positive_outstanding_including_order += abs_outstanding_amount + else: + total_negative_outstanding += abs_outstanding_amount + + # allocation variables counting + allocated_negative_outstanding = 0 + allocated_positive_outstanding = 0 + + if (self.payment_type == "Receive" and self.party_type == "Customer") or ( + self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee") + ): + if total_positive_outstanding_including_order > paid_amount: + remaining_outstanding = total_positive_outstanding_including_order - paid_amount + allocated_negative_outstanding = min(remaining_outstanding, total_negative_outstanding) + allocated_positive_outstanding = paid_amount + allocated_negative_outstanding + + elif self.party_type in ("Supplier", "Employee"): + if paid_amount > total_negative_outstanding: + if total_negative_outstanding == 0: + frappe.msgprint( + _("Cannot {0} from {2} without any negative outstanding invoice").format( + self.payment_type, + self.party_type, + ) + ) + else: + frappe.msgprint( + _("Paid Amount cannot be greater than total negative outstanding amount {0}").format( + total_negative_outstanding + ) + ) + + return + + else: + allocated_positive_outstanding = total_negative_outstanding - paid_amount + allocated_negative_outstanding = paid_amount + min( + total_positive_outstanding_including_order, allocated_positive_outstanding + ) -def get_matched_payment_requests(references=None): + # correct data, because it it is possible PT's outstanding amount is not updated + payment_term_outstanding = get_payment_term_outstanding_of_references(self.references) + + if not payment_term_outstanding: + payment_term_outstanding = {} + + # ! may possible that PR is there or not and same for the PT + # todo: allocate amount (based on outstanding_amount + PT outstanding amount + PR outstanding amount) + + for ref in self.references: + if ref.outstanding_amount > 0 and allocated_positive_outstanding > 0: + ref.allocated_amount = min(allocated_positive_outstanding, ref.outstanding_amount) + allocated_positive_outstanding -= flt(ref.allocated_amount) + elif ref.outstanding_amount < 0 and allocated_negative_outstanding > 0: + ref.allocated_amount = min(allocated_negative_outstanding, abs(ref.outstanding_amount)) * -1 + allocated_negative_outstanding -= abs(flt(ref.allocated_amount)) + + +def get_matched_payment_request_of_references(references=None): if not references: return @@ -1801,6 +1883,33 @@ def get_matched_payment_requests(references=None): return {(pr.reference_doctype, pr.reference_name, pr.outstanding_amount): pr.name for pr in matched_prs} +def get_payment_term_outstanding_of_references(references=None): + if not references: + return + + refs = { + (row.reference_doctype, row.reference_name, row.payment_term) + for row in references + if row.reference_doctype and row.reference_name and row.payment_term + } + + if not refs: + return + + PS = frappe.qb.DocType("Payment Schedule") + + response = ( + frappe.qb.from_(PS) + .select(PS.parenttype, PS.parent, PS.payment_term, PS.outstanding) + .where(Tuple(PS.parenttype, PS.parent, PS.payment_term).isin(refs)) + ).run(as_dict=True) + + if not response: + return + + return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response} + + def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): throw( From c40419d1a3126d72f76b35641ad8ef98905b80e8 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Sat, 24 Aug 2024 18:41:12 +0530 Subject: [PATCH 28/60] fix: some minor changes to allocation --- .../doctype/payment_entry/payment_entry.js | 20 +++++++++++++------ .../doctype/payment_entry/payment_entry.py | 15 +++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 96c72bf820ec..eef3569c594d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -796,7 +796,7 @@ frappe.ui.form.on("Payment Entry", { ); if (frm.doc.payment_type == "Pay") - frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1); + frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1, false); else frm.events.set_unallocated_amount(frm); frm.set_paid_amount_based_on_received_amount = false; @@ -817,7 +817,7 @@ frappe.ui.form.on("Payment Entry", { } if (frm.doc.payment_type == "Receive") - frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1); + frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1, false); else frm.events.set_unallocated_amount(frm); }, @@ -1037,7 +1037,9 @@ frappe.ui.form.on("Payment Entry", { frm.events.allocate_party_amount_against_ref_docs( frm, - frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount + frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount, + 0, + true ); }, }); @@ -1051,13 +1053,19 @@ frappe.ui.form.on("Payment Entry", { return ["Sales Invoice", "Purchase Invoice"]; }, - allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { + allocate_party_amount_against_ref_docs: async function ( + frm, + paid_amount, + paid_amount_change, + allocate_payment_request + ) { await frm.call("allocate_party_amount_against_ref_docs", { paid_amount: paid_amount, - paid_amount_change: paid_amount_change ? paid_amount_change : 0, + paid_amount_change: paid_amount_change, allocate_payment_amount: frappe.flags.allocate_payment_amount ? frappe.flags.allocate_payment_amount - : 0, + : false, + allocate_payment_request: allocate_payment_request, }); frm.events.set_total_allocated_amount(frm); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7c58dd86a23c..73f75d58d2ee 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1763,7 +1763,7 @@ def check_references_for_unset_payment_request(self): @frappe.whitelist() def allocate_party_amount_against_ref_docs( - self, paid_amount, paid_amount_change, allocate_payment_amount + self, paid_amount, paid_amount_change, allocate_payment_amount, allocate_payment_request ): if not self.references: return @@ -1828,11 +1828,11 @@ def allocate_party_amount_against_ref_docs( total_positive_outstanding_including_order, allocated_positive_outstanding ) - # correct data, because it it is possible PT's outstanding amount is not updated - payment_term_outstanding = get_payment_term_outstanding_of_references(self.references) + payment_term_outstanding = {} - if not payment_term_outstanding: - payment_term_outstanding = {} + if not allocate_payment_request: + # correct data, because it it is possible PT's outstanding amount is not updated + payment_term_outstanding = get_payment_term_outstanding_of_references(self.references) # ! may possible that PR is there or not and same for the PT # todo: allocate amount (based on outstanding_amount + PT outstanding amount + PR outstanding amount) @@ -1845,6 +1845,9 @@ def allocate_party_amount_against_ref_docs( ref.allocated_amount = min(allocated_negative_outstanding, abs(ref.outstanding_amount)) * -1 allocated_negative_outstanding -= abs(flt(ref.allocated_amount)) + if allocate_payment_request: + set_open_payment_requests_to_references(self.references) + def get_matched_payment_request_of_references(references=None): if not references: @@ -2771,7 +2774,6 @@ def set_open_payment_requests_to_references(references=None): # allocate the payment request to the reference and PR's outstanding amount row.payment_request = payment_request - row.payment_request_outstanding = outstanding_amount if outstanding_amount == allocated_amount: del reference_payment_requests[payment_request] @@ -2802,7 +2804,6 @@ def set_open_payment_requests_to_references(references=None): # update new row new_row.idx = row_number + 1 new_row.payment_request = payment_request - new_row.payment_request_outstanding = outstanding_amount new_row.allocated_amount = min( outstanding_amount if outstanding_amount else allocated_amount, allocated_amount ) From a7aaff133445a464ef76c2397e7baa4950982c6d Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 26 Aug 2024 11:46:11 +0530 Subject: [PATCH 29/60] fix: Split `Payment Request` if PE is created from PR and there are `Payment Terms` --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 5 +---- erpnext/accounts/doctype/payment_request/payment_request.py | 5 +++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 73f75d58d2ee..3ccf086f4256 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1768,7 +1768,6 @@ def allocate_party_amount_against_ref_docs( if not self.references: return - # if `allocate_payment_amount` is False, then do not allocate amount if not allocate_payment_amount: for ref in self.references: ref.allocated_amount = 0 @@ -1776,12 +1775,10 @@ def allocate_party_amount_against_ref_docs( # todo-abdeali: here will update table (reference) priority given to those which have PR inside it ... # todo: store old data also, never should allow more amount than paid amount - # variables which will be used to calculate the allocated amount total_positive_outstanding_including_order = 0 total_negative_outstanding = 0 paid_amount -= sum(flt(d.amount, self.precision("paid_amount")) for d in self.deductions) - # count total positive outstanding and total negative outstanding for ref in self.references: outstanding_amount = flt(ref.outstanding_amount, self.precision("paid_amount")) abs_outstanding_amount = abs(outstanding_amount) @@ -1832,7 +1829,7 @@ def allocate_party_amount_against_ref_docs( if not allocate_payment_request: # correct data, because it it is possible PT's outstanding amount is not updated - payment_term_outstanding = get_payment_term_outstanding_of_references(self.references) + payment_term_outstanding = get_payment_term_outstanding_of_references(self.references) or {} # ! may possible that PR is there or not and same for the PT # todo: allocate amount (based on outstanding_amount + PT outstanding amount + PR outstanding amount) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index c4d291cf91cf..c9c4bcce3065 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -323,8 +323,9 @@ def create_payment_entry(self, submit=True): } ) - # Add reference of Payment Request - payment_entry.references[0].payment_request = self.name + # Update payment_request for each reference in payment_entry (Payment Term can splits the row) + for row in payment_entry.references: + row.payment_request = self.name # Update dimensions payment_entry.update( From 568adefa32a25ddeb5b85036df060ad6de26b0c9 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 26 Aug 2024 12:14:11 +0530 Subject: [PATCH 30/60] fix: minor logic changes --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 3ccf086f4256..fa5c6222569f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1835,10 +1835,10 @@ def allocate_party_amount_against_ref_docs( # todo: allocate amount (based on outstanding_amount + PT outstanding amount + PR outstanding amount) for ref in self.references: - if ref.outstanding_amount > 0 and allocated_positive_outstanding > 0: + if ref.outstanding_amount > 0 and allocated_positive_outstanding >= 0: ref.allocated_amount = min(allocated_positive_outstanding, ref.outstanding_amount) allocated_positive_outstanding -= flt(ref.allocated_amount) - elif ref.outstanding_amount < 0 and allocated_negative_outstanding > 0: + elif ref.outstanding_amount < 0 and allocated_negative_outstanding: ref.allocated_amount = min(allocated_negative_outstanding, abs(ref.outstanding_amount)) * -1 allocated_negative_outstanding -= abs(flt(ref.allocated_amount)) From fc340c4b51cb0a8bdbc134af2648381d6fca7cd2 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 26 Aug 2024 17:20:24 +0530 Subject: [PATCH 31/60] fix: Allocation of allocated_amount if `paid_amount` is changes --- .../doctype/payment_entry/payment_entry.js | 15 +-- .../doctype/payment_entry/payment_entry.py | 109 +++++++++++++++--- 2 files changed, 94 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index eef3569c594d..00c8ac6790e8 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -796,7 +796,7 @@ frappe.ui.form.on("Payment Entry", { ); if (frm.doc.payment_type == "Pay") - frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1, false); + frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true); else frm.events.set_unallocated_amount(frm); frm.set_paid_amount_based_on_received_amount = false; @@ -817,7 +817,7 @@ frappe.ui.form.on("Payment Entry", { } if (frm.doc.payment_type == "Receive") - frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1, false); + frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true); else frm.events.set_unallocated_amount(frm); }, @@ -1038,8 +1038,7 @@ frappe.ui.form.on("Payment Entry", { frm.events.allocate_party_amount_against_ref_docs( frm, frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount, - 0, - true + false ); }, }); @@ -1053,19 +1052,13 @@ frappe.ui.form.on("Payment Entry", { return ["Sales Invoice", "Purchase Invoice"]; }, - allocate_party_amount_against_ref_docs: async function ( - frm, - paid_amount, - paid_amount_change, - allocate_payment_request - ) { + allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { await frm.call("allocate_party_amount_against_ref_docs", { paid_amount: paid_amount, paid_amount_change: paid_amount_change, allocate_payment_amount: frappe.flags.allocate_payment_amount ? frappe.flags.allocate_payment_amount : false, - allocate_payment_request: allocate_payment_request, }); frm.events.set_total_allocated_amount(frm); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index fa5c6222569f..b4f2ae3d299f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1763,7 +1763,7 @@ def check_references_for_unset_payment_request(self): @frappe.whitelist() def allocate_party_amount_against_ref_docs( - self, paid_amount, paid_amount_change, allocate_payment_amount, allocate_payment_request + self, paid_amount, paid_amount_change, allocate_payment_amount ): if not self.references: return @@ -1772,8 +1772,6 @@ def allocate_party_amount_against_ref_docs( for ref in self.references: ref.allocated_amount = 0 return - # todo-abdeali: here will update table (reference) priority given to those which have PR inside it ... - # todo: store old data also, never should allow more amount than paid amount total_positive_outstanding_including_order = 0 total_negative_outstanding = 0 @@ -1788,7 +1786,6 @@ def allocate_party_amount_against_ref_docs( else: total_negative_outstanding += abs_outstanding_amount - # allocation variables counting allocated_negative_outstanding = 0 allocated_positive_outstanding = 0 @@ -1825,24 +1822,61 @@ def allocate_party_amount_against_ref_docs( total_positive_outstanding_including_order, allocated_positive_outstanding ) - payment_term_outstanding = {} + if paid_amount_change: + # correct data, because it is possible that PT's outstanding amount is not updated + payment_request_outstanding = get_payment_request_outstanding_of_references(self.references) or {} + references_outstanding = get_payment_term_outstanding_of_references(self.references) or {} + references_outstanding.update(get_no_payment_terms_references_outstanding(self.references)) + not_allocated_amounts = references_outstanding.copy() - if not allocate_payment_request: - # correct data, because it it is possible PT's outstanding amount is not updated - payment_term_outstanding = get_payment_term_outstanding_of_references(self.references) or {} + for ref in self.references: + if not ref.payment_request: + continue - # ! may possible that PR is there or not and same for the PT - # todo: allocate amount (based on outstanding_amount + PT outstanding amount + PR outstanding amount) + # fetch outstanding_amount + key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term")) + outstanding_amount = references_outstanding[key] + pr_outstanding_amount = payment_request_outstanding[ref.payment_request] + + if outstanding_amount > 0 and allocated_positive_outstanding >= 0: + ref.allocated_amount = min( + allocated_positive_outstanding, outstanding_amount, pr_outstanding_amount + ) + allocated_positive_outstanding -= flt(ref.allocated_amount) + not_allocated_amounts[key] -= flt(ref.allocated_amount) + payment_request_outstanding[ref.payment_request] -= flt(ref.allocated_amount) + elif outstanding_amount < 0 and allocated_negative_outstanding: + ref.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1 + allocated_negative_outstanding -= abs(flt(ref.allocated_amount)) + not_allocated_amounts[key] -= abs(flt(ref.allocated_amount)) + payment_request_outstanding[ref.payment_request] -= abs(flt(ref.allocated_amount)) + + for ref in self.references: + if ref.payment_request: + continue + + key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term")) + outstanding_amount = not_allocated_amounts[key] + + if outstanding_amount > 0 and allocated_positive_outstanding >= 0: + ref.allocated_amount = min(allocated_positive_outstanding, outstanding_amount) + allocated_positive_outstanding -= flt(ref.allocated_amount) + elif outstanding_amount < 0 and allocated_negative_outstanding: + ref.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1 + allocated_negative_outstanding -= abs(flt(ref.allocated_amount)) + + else: + # todo: make more efficient using same variable + for ref in self.references: + if ref.outstanding_amount > 0 and allocated_positive_outstanding >= 0: + ref.allocated_amount = min(allocated_positive_outstanding, ref.outstanding_amount) + allocated_positive_outstanding -= flt(ref.allocated_amount) + elif ref.outstanding_amount < 0 and allocated_negative_outstanding: + ref.allocated_amount = ( + min(allocated_negative_outstanding, abs(ref.outstanding_amount)) * -1 + ) + allocated_negative_outstanding -= abs(flt(ref.allocated_amount)) - for ref in self.references: - if ref.outstanding_amount > 0 and allocated_positive_outstanding >= 0: - ref.allocated_amount = min(allocated_positive_outstanding, ref.outstanding_amount) - allocated_positive_outstanding -= flt(ref.allocated_amount) - elif ref.outstanding_amount < 0 and allocated_negative_outstanding: - ref.allocated_amount = min(allocated_negative_outstanding, abs(ref.outstanding_amount)) * -1 - allocated_negative_outstanding -= abs(flt(ref.allocated_amount)) - - if allocate_payment_request: set_open_payment_requests_to_references(self.references) @@ -1910,6 +1944,43 @@ def get_payment_term_outstanding_of_references(references=None): return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response} +def get_payment_request_outstanding_of_references(references=None): + if not references: + return + + prs = {row.payment_request for row in references if row.payment_request} + + if not prs: + return + + PR = frappe.qb.DocType("Payment Request") + + response = (frappe.qb.from_(PR).select(PR.name, PR.outstanding_amount).where(PR.name.isin(prs))).run() + + if not response: + return + + return dict(response) + + +def get_no_payment_terms_references_outstanding(references): + if not references: + return + + outstanding_amounts = {} + + for ref in references: + if ref.payment_term: + continue + + key = (ref.reference_doctype, ref.reference_name, None) + + if key not in outstanding_amounts: + outstanding_amounts[key] = ref.outstanding_amount + + return outstanding_amounts + + def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): throw( From ac51bdff06e653601897ea87978a5377f79c5584 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 27 Aug 2024 00:29:51 +0530 Subject: [PATCH 32/60] fix: improve logic of allocation --- .../doctype/payment_entry/payment_entry.py | 168 +++++++++++------- 1 file changed, 106 insertions(+), 62 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b4f2ae3d299f..c4a97be08479 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1773,22 +1773,25 @@ def allocate_party_amount_against_ref_docs( ref.allocated_amount = 0 return + # calculating outstanding amounts total_positive_outstanding_including_order = 0 total_negative_outstanding = 0 paid_amount -= sum(flt(d.amount, self.precision("paid_amount")) for d in self.deductions) for ref in self.references: - outstanding_amount = flt(ref.outstanding_amount, self.precision("paid_amount")) - abs_outstanding_amount = abs(outstanding_amount) + reference_outstanding_amount = flt(ref.outstanding_amount, self.precision("paid_amount")) + abs_outstanding_amount = abs(reference_outstanding_amount) - if outstanding_amount > 0: + if reference_outstanding_amount > 0: total_positive_outstanding_including_order += abs_outstanding_amount else: total_negative_outstanding += abs_outstanding_amount + # calculating allocated outstanding amounts allocated_negative_outstanding = 0 allocated_positive_outstanding = 0 + # checking party type and payment type if (self.payment_type == "Receive" and self.party_type == "Customer") or ( self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee") ): @@ -1822,62 +1825,93 @@ def allocate_party_amount_against_ref_docs( total_positive_outstanding_including_order, allocated_positive_outstanding ) - if paid_amount_change: - # correct data, because it is possible that PT's outstanding amount is not updated - payment_request_outstanding = get_payment_request_outstanding_of_references(self.references) or {} - references_outstanding = get_payment_term_outstanding_of_references(self.references) or {} - references_outstanding.update(get_no_payment_terms_references_outstanding(self.references)) - not_allocated_amounts = references_outstanding.copy() + # inner function to set `allocated_amount` to those row which have no PR + def _allocation_to_unset_pr_row( + row, outstanding_amount, allocated_positive_outstanding, allocated_negative_outstanding + ): + if outstanding_amount > 0 and allocated_positive_outstanding >= 0: + row.allocated_amount = min(allocated_positive_outstanding, outstanding_amount) + allocated_positive_outstanding -= flt(row.allocated_amount) + elif outstanding_amount < 0 and allocated_negative_outstanding: + row.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1 + allocated_negative_outstanding -= abs(flt(row.allocated_amount)) + return allocated_positive_outstanding, allocated_negative_outstanding + + # allocate amount based on `paid_amount` is changed or not + if not paid_amount_change: + for ref in self.references: + allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row( + ref, + ref.outstanding_amount, + allocated_positive_outstanding, + allocated_negative_outstanding, + ) + + set_open_payment_requests_to_references(self.references) + + else: + payment_request_outstanding_amounts = ( + get_payment_request_outstanding_of_references(self.references) or {} + ) + references_outstanding_amounts = get_reference_outstanding_amounts(self.references) or {} + remaining_references_allocated_amounts = references_outstanding_amounts.copy() + # Re allocate amount to those references which have PR set (Higher priority) for ref in self.references: if not ref.payment_request: continue - # fetch outstanding_amount + # fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term")) - outstanding_amount = references_outstanding[key] - pr_outstanding_amount = payment_request_outstanding[ref.payment_request] + reference_outstanding_amount = references_outstanding_amounts[key] + pr_outstanding_amount = payment_request_outstanding_amounts[ref.payment_request] + + if reference_outstanding_amount > 0 and allocated_positive_outstanding >= 0: + # allocate amount according to outstanding amounts + outstanding_amounts = ( + allocated_positive_outstanding, + reference_outstanding_amount, + pr_outstanding_amount, + ) + + ref.allocated_amount = min(outstanding_amounts) - if outstanding_amount > 0 and allocated_positive_outstanding >= 0: - ref.allocated_amount = min( - allocated_positive_outstanding, outstanding_amount, pr_outstanding_amount + # update amounts to track allocation + allocated_amount = flt(ref.allocated_amount) + allocated_positive_outstanding -= allocated_amount + remaining_references_allocated_amounts[key] -= allocated_amount + payment_request_outstanding_amounts[ref.payment_request] -= allocated_amount + + elif reference_outstanding_amount < 0 and allocated_negative_outstanding: + # allocate amount according to outstanding amounts + outstanding_amounts = ( + allocated_negative_outstanding, + abs(reference_outstanding_amount), + pr_outstanding_amount, ) - allocated_positive_outstanding -= flt(ref.allocated_amount) - not_allocated_amounts[key] -= flt(ref.allocated_amount) - payment_request_outstanding[ref.payment_request] -= flt(ref.allocated_amount) - elif outstanding_amount < 0 and allocated_negative_outstanding: - ref.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1 - allocated_negative_outstanding -= abs(flt(ref.allocated_amount)) - not_allocated_amounts[key] -= abs(flt(ref.allocated_amount)) - payment_request_outstanding[ref.payment_request] -= abs(flt(ref.allocated_amount)) + ref.allocated_amount = min(outstanding_amounts) * -1 + + # update amounts to track allocation + allocated_amount = abs(flt(ref.allocated_amount)) + allocated_negative_outstanding -= allocated_amount + remaining_references_allocated_amounts[key] += allocated_amount # negative amount + payment_request_outstanding_amounts[ref.payment_request] -= allocated_amount + + # Re allocate amount to those references which have no PR (Lower priority) for ref in self.references: if ref.payment_request: continue key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term")) - outstanding_amount = not_allocated_amounts[key] - - if outstanding_amount > 0 and allocated_positive_outstanding >= 0: - ref.allocated_amount = min(allocated_positive_outstanding, outstanding_amount) - allocated_positive_outstanding -= flt(ref.allocated_amount) - elif outstanding_amount < 0 and allocated_negative_outstanding: - ref.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1 - allocated_negative_outstanding -= abs(flt(ref.allocated_amount)) + reference_outstanding_amount = remaining_references_allocated_amounts[key] - else: - # todo: make more efficient using same variable - for ref in self.references: - if ref.outstanding_amount > 0 and allocated_positive_outstanding >= 0: - ref.allocated_amount = min(allocated_positive_outstanding, ref.outstanding_amount) - allocated_positive_outstanding -= flt(ref.allocated_amount) - elif ref.outstanding_amount < 0 and allocated_negative_outstanding: - ref.allocated_amount = ( - min(allocated_negative_outstanding, abs(ref.outstanding_amount)) * -1 - ) - allocated_negative_outstanding -= abs(flt(ref.allocated_amount)) - - set_open_payment_requests_to_references(self.references) + allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row( + ref, + reference_outstanding_amount, + allocated_positive_outstanding, + allocated_negative_outstanding, + ) def get_matched_payment_request_of_references(references=None): @@ -1917,6 +1951,16 @@ def get_matched_payment_request_of_references(references=None): return {(pr.reference_doctype, pr.reference_name, pr.outstanding_amount): pr.name for pr in matched_prs} +def get_reference_outstanding_amounts(references=None): + if not references: + return + + refs_with_payment_term = get_payment_term_outstanding_of_references(references) or {} + refs_without_payment_term = get_no_payment_terms_references_outstanding(references) or {} + + return {**refs_with_payment_term, **refs_without_payment_term} + + def get_payment_term_outstanding_of_references(references=None): if not references: return @@ -1944,6 +1988,24 @@ def get_payment_term_outstanding_of_references(references=None): return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response} +def get_no_payment_terms_references_outstanding(references): + if not references: + return + + outstanding_amounts = {} + + for ref in references: + if ref.payment_term: + continue + + key = (ref.reference_doctype, ref.reference_name, None) + + if key not in outstanding_amounts: + outstanding_amounts[key] = ref.outstanding_amount + + return outstanding_amounts + + def get_payment_request_outstanding_of_references(references=None): if not references: return @@ -1963,24 +2025,6 @@ def get_payment_request_outstanding_of_references(references=None): return dict(response) -def get_no_payment_terms_references_outstanding(references): - if not references: - return - - outstanding_amounts = {} - - for ref in references: - if ref.payment_term: - continue - - key = (ref.reference_doctype, ref.reference_name, None) - - if key not in outstanding_amounts: - outstanding_amounts[key] = ref.outstanding_amount - - return outstanding_amounts - - def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): throw( From b860eeaaa4c22cf611dcfd6631cbf17bf49912e8 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 27 Aug 2024 17:12:45 +0530 Subject: [PATCH 33/60] fix: set matched payment request if unset --- .../doctype/payment_entry/payment_entry.js | 24 ++++++ .../doctype/payment_entry/payment_entry.py | 78 +++++++++++-------- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 00c8ac6790e8..f54ab356f6b7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1639,6 +1639,30 @@ frappe.ui.form.on("Payment Entry", { }); } }, + + after_save: function (frm) { + const { matched_payment_requests } = frappe.last_response; + if (!matched_payment_requests) return; + + const COLUMN_LABEL = [ + [__("Reference DocType"), __("Reference Name"), __("Allocated Amount"), __("Payment Request")], + ]; + + frappe.msgprint({ + title: __("Matched Payment Request"), + message: COLUMN_LABEL.concat(matched_payment_requests), + as_table: true, + primary_action: { + label: __("Allocate Payment Request"), + action() { + frappe.hide_msgprint(); + frm.call("set_matched_payment_requests", { matched_payment_requests }, () => { + frm.dirty(); + }); + }, + }, + }); + }, }); frappe.ui.form.on("Payment Entry Reference", { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c4a97be08479..9cdc1ae1c224 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -182,10 +182,8 @@ def validate(self): self.set_status() self.set_total_in_words() - # todo-abdeali: move to front end def before_save(self): - pass - # self.check_references_for_unset_payment_request() + self.set_matched_unset_payment_requests_to_response() def on_submit(self): if self.difference_amount: @@ -1724,8 +1722,7 @@ def get_current_tax_fraction(self, tax): return current_tax_fraction - # todo-abdeali: needs changes if PR already use for ref and one ref row have no PR then do not show this!! - def check_references_for_unset_payment_request(self): + def set_matched_unset_payment_requests_to_response(self): if not self.references: return @@ -1736,30 +1733,7 @@ def check_references_for_unset_payment_request(self): if not matched_payment_requests: return - unset_pr_rows = {} - - for row in self.references: - if row.payment_request: - continue - - matched_pr = matched_payment_requests.get( - (row.reference_doctype, row.reference_name, row.allocated_amount) - ) - - if matched_pr: - unset_pr_rows[row.idx] = matched_pr - - if unset_pr_rows: - message = _("Matched Payment Requests found for references, but not set.

") - message += _("
View Details
    ") - for idx, pr in unset_pr_rows.items(): - message += _("
  • Row #{0}: {1}
  • ").format(idx, get_link_to_form("Payment Request", pr)) - message += _("
") - - frappe.msgprint( - msg=message, - indicator="yellow", - ) + frappe.response["matched_payment_requests"] = matched_payment_requests @frappe.whitelist() def allocate_party_amount_against_ref_docs( @@ -1913,17 +1887,39 @@ def _allocation_to_unset_pr_row( allocated_negative_outstanding, ) + @frappe.whitelist() + def set_matched_payment_requests(self, matched_payment_requests): + if not self.references: + return + + # modify matched_payment_requests + payment_requests = {} + + for row in matched_payment_requests: + key = tuple(row[:3]) + payment_requests[key] = row[3] + + for ref in self.references: + if ref.payment_request: + continue + + key = (ref.reference_doctype, ref.reference_name, ref.allocated_amount) + + if key in payment_requests: + ref.payment_request = payment_requests[key] + del payment_requests[key] + def get_matched_payment_request_of_references(references=None): if not references: return # to fetch matched rows - refs = [ + refs = { (row.reference_doctype, row.reference_name, row.allocated_amount) for row in references if row.reference_doctype and row.reference_name and row.allocated_amount - ] + } if not refs: return @@ -1934,7 +1930,11 @@ def get_matched_payment_request_of_references(references=None): subquery = ( frappe.qb.from_(PR) .select( - PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount, Count("*").as_("count") + PR.reference_doctype, + PR.reference_name, + PR.outstanding_amount.as_("allocated_amount"), + PR.name.as_("payment_request"), + Count("*").as_("count"), ) .where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs)) .where(PR.status != "Paid") @@ -1943,12 +1943,22 @@ def get_matched_payment_request_of_references(references=None): ) # query to fetch matched rows which are single - matched_prs = frappe.qb.from_(subquery).select("*").where(subquery.count == 1).run(as_dict=True) + matched_prs = ( + frappe.qb.from_(subquery) + .select( + subquery.reference_doctype, + subquery.reference_name, + subquery.allocated_amount, + subquery.payment_request, + ) + .where(subquery.count == 1) + .run() + ) if not matched_prs: return - return {(pr.reference_doctype, pr.reference_name, pr.outstanding_amount): pr.name for pr in matched_prs} + return matched_prs def get_reference_outstanding_amounts(references=None): From 451bdd55ed59743684caf6ac728422a3ca0e7db5 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 11 Sep 2024 11:12:29 +0530 Subject: [PATCH 34/60] fix: minor changes --- erpnext/accounts/doctype/payment_request/payment_request.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 0e63ed522df9..d905f9adc788 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -608,8 +608,7 @@ def get_existing_payment_request_amount(ref_dt, ref_dn): .run() ) - return response[0][0] if response else 0 - + return response[0][0] if response[0] else 0 def get_gateway_details(args): # nosemgrep """ From 9e89da43ced64318f10c5396e9b5965c980172de Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 11 Sep 2024 14:02:54 +0530 Subject: [PATCH 35/60] fix: Allocate single Payment Request if PE created from PR --- .../doctype/payment_entry/payment_entry.py | 6 +-- .../payment_request/payment_request.py | 53 +++++++++++++++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index f5befc503abe..bdd21cb0f8db 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1821,7 +1821,7 @@ def _allocation_to_unset_pr_row( allocated_negative_outstanding, ) - set_open_payment_requests_to_references(self.references) + allocate_open_payment_requests_to_references(self.references) else: payment_request_outstanding_amounts = ( @@ -2816,7 +2816,7 @@ def get_payment_entry( pe.set_difference_amount() if not created_from_payment_request: - set_open_payment_requests_to_references(pe.references) + allocate_open_payment_requests_to_references(pe.references) return pe @@ -2861,7 +2861,7 @@ def get_open_payment_requests_for_references(references=None): return reference_payment_requests -def set_open_payment_requests_to_references(references=None): +def allocate_open_payment_requests_to_references(references=None): if not references: return diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index d905f9adc788..08479cf4c752 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -324,9 +324,8 @@ def create_payment_entry(self, submit=True): } ) - # Update payment_request for each reference in payment_entry (Payment Term can splits the row) - for row in payment_entry.references: - row.payment_request = self.name + # Allocate payment_request for each reference in payment_entry (Payment Term can splits the row) + self._allocate_payment_request_to_pe_references(references=payment_entry.references) # Update dimensions payment_entry.update( @@ -438,6 +437,53 @@ def update_reference_advance_payment_status(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc.set_advance_payment_status() + def _allocate_payment_request_to_pe_references(self, references): + if len(references) == 1: + references[0].payment_request = self.name + return + + outstanding_amount = self.outstanding_amount + + # to manage rows + row_number = 1 + MOVE_TO_NEXT_ROW = 1 + TO_SKIP_NEW_ROW = 2 + NEW_ROW_ADDED = False + + while row_number <= len(references): + row = references[row_number - 1] + + # update the idx to maintain the order + row.idx = row_number + + if outstanding_amount == 0: + if not NEW_ROW_ADDED: + break + continue + + # allocate the payment request to the row + row.payment_request = self.name + + if row.allocated_amount <= outstanding_amount: + outstanding_amount -= row.allocated_amount + row_number += MOVE_TO_NEXT_ROW + else: + remaining_allocated_amount = row.allocated_amount - outstanding_amount + row.allocated_amount = outstanding_amount + outstanding_amount = 0 + + # create a new row without PR for remaining unallocated amount + new_row = frappe.copy_doc(row) + references.insert(row_number, new_row) + + # update new row + new_row.idx = row_number + 1 + new_row.payment_request = None + new_row.allocated_amount = remaining_allocated_amount + + NEW_ROW_ADDED = True + row_number += TO_SKIP_NEW_ROW + @frappe.whitelist(allow_guest=True) def make_payment_request(**args): @@ -610,6 +656,7 @@ def get_existing_payment_request_amount(ref_dt, ref_dn): return response[0][0] if response[0] else 0 + def get_gateway_details(args): # nosemgrep """ Return gateway and payment account of default payment gateway From 871806ebce6d3560451b271c61b207ba2f6aa86e Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Thu, 12 Sep 2024 13:32:49 +0530 Subject: [PATCH 36/60] fix: improve code logic --- .../doctype/payment_entry/payment_entry.js | 6 +- .../doctype/payment_entry/payment_entry.py | 158 ++++++++++++++---- .../payment_entry_reference.py | 6 +- .../payment_request/payment_request.py | 16 +- 4 files changed, 139 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index faee53b121aa..699fdb3cf5eb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1067,12 +1067,10 @@ frappe.ui.form.on("Payment Entry", { }, allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { - await frm.call("allocate_party_amount_against_ref_docs", { + await frm.call("allocate_party_amount_and_payment_request_against_ref_docs", { paid_amount: paid_amount, paid_amount_change: paid_amount_change, - allocate_payment_amount: frappe.flags.allocate_payment_amount - ? frappe.flags.allocate_payment_amount - : false, + allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false, }); frm.events.set_total_allocated_amount(frm); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index bdd21cb0f8db..e91bebefff83 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -332,6 +332,9 @@ def validate_allocated_amount(self): frappe.throw(fail_message.format(d.idx)) def validate_allocated_amount_as_per_payment_request(self): + """ + Allocated amount should not be greater than the outstanding amount of the Payment Request.f + """ from erpnext.accounts.doctype.payment_request.payment_request import ( get_outstanding_amount_of_payment_entry_references as get_outstanding_amounts, ) @@ -1723,6 +1726,10 @@ def get_current_tax_fraction(self, tax): return current_tax_fraction def set_matched_unset_payment_requests_to_response(self): + """ + Find matched Payment Requests for those references which have no Payment Request set.\n + And set to `frappe.response` to show in the frontend for allocation. + """ if not self.references: return @@ -1736,9 +1743,15 @@ def set_matched_unset_payment_requests_to_response(self): frappe.response["matched_payment_requests"] = matched_payment_requests @frappe.whitelist() - def allocate_party_amount_against_ref_docs( + def allocate_party_amount_and_payment_request_against_ref_docs( self, paid_amount, paid_amount_change, allocate_payment_amount ): + """ + Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n + :param paid_amount: Paid Amount / Received Amount. + :param paid_amount_change: Flag to check if `Paid Amount` is changed or not. + :param allocate_payment_amount: Flag to allocate amount or not. + """ if not self.references: return @@ -1825,9 +1838,9 @@ def _allocation_to_unset_pr_row( else: payment_request_outstanding_amounts = ( - get_payment_request_outstanding_of_references(self.references) or {} + get_payment_request_outstanding_set_in_references(self.references) or {} ) - references_outstanding_amounts = get_reference_outstanding_amounts(self.references) or {} + references_outstanding_amounts = get_references_outstanding_amount(self.references) or {} remaining_references_allocated_amounts = references_outstanding_amounts.copy() # Re allocate amount to those references which have PR set (Higher priority) @@ -1889,10 +1902,21 @@ def _allocation_to_unset_pr_row( @frappe.whitelist() def set_matched_payment_requests(self, matched_payment_requests): - if not self.references: + """ + Set `Payment Request` against `Reference` based on `matched_payment_requests`.\n + :param matched_payment_requests: List of tuple of matched Payment Requests. + + --- + Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...] + """ + if not self.references or not matched_payment_requests: return + if isinstance(matched_payment_requests, str): + matched_payment_requests = json.loads(matched_payment_requests) + # modify matched_payment_requests + # like (reference_doctype, reference_name, allocated_amount): payment_request payment_requests = {} for row in matched_payment_requests: @@ -1907,10 +1931,17 @@ def set_matched_payment_requests(self, matched_payment_requests): if key in payment_requests: ref.payment_request = payment_requests[key] - del payment_requests[key] + del payment_requests[key] # to avoid duplicate allocation def get_matched_payment_request_of_references(references=None): + """ + Get those `Payment Requests` which are matched with `References`.\n + - Amount must be same. + - Only single `Payment Request` available for this amount. + + Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...] + """ if not references: return @@ -1955,23 +1986,31 @@ def get_matched_payment_request_of_references(references=None): .run() ) - if not matched_prs: - return + return matched_prs if matched_prs else None - return matched_prs +def get_references_outstanding_amount(references=None): + """ + Fetch accurate outstanding amount of `References`.\n + - If `Payment Term` is set, then fetch outstanding amount from `Payment Schedule`. + - If `Payment Term` is not set, then fetch outstanding amount from `References` it self. -def get_reference_outstanding_amounts(references=None): + Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...} + """ if not references: return - refs_with_payment_term = get_payment_term_outstanding_of_references(references) or {} - refs_without_payment_term = get_no_payment_terms_references_outstanding(references) or {} + refs_with_payment_term = get_outstanding_of_references_with_payment_term(references) or {} + refs_without_payment_term = get_outstanding_of_references_with_no_payment_term(references) or {} return {**refs_with_payment_term, **refs_without_payment_term} -def get_payment_term_outstanding_of_references(references=None): +def get_outstanding_of_references_with_payment_term(references=None): + """ + Fetch outstanding amount of `References` which have `Payment Term` set.\n + Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...} + """ if not references: return @@ -1998,7 +2037,14 @@ def get_payment_term_outstanding_of_references(references=None): return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response} -def get_no_payment_terms_references_outstanding(references): +def get_outstanding_of_references_with_no_payment_term(references): + """ + Fetch outstanding amount of `References` which have no `Payment Term` set.\n + - Fetch outstanding amount from `References` it self. + + Note: `None` is used for allocation of `Payment Request` + Example: {(reference_doctype, reference_name, None): outstanding_amount, ...} + """ if not references: return @@ -2016,23 +2062,28 @@ def get_no_payment_terms_references_outstanding(references): return outstanding_amounts -def get_payment_request_outstanding_of_references(references=None): +def get_payment_request_outstanding_set_in_references(references=None): + """ + Fetch outstanding amount of `Payment Request` which are set in `References`.\n + Example: {payment_request: outstanding_amount, ...} + """ if not references: return - prs = {row.payment_request for row in references if row.payment_request} + referenced_payment_requests = {row.payment_request for row in references if row.payment_request} - if not prs: + if not referenced_payment_requests: return PR = frappe.qb.DocType("Payment Request") - response = (frappe.qb.from_(PR).select(PR.name, PR.outstanding_amount).where(PR.name.isin(prs))).run() - - if not response: - return + response = ( + frappe.qb.from_(PR) + .select(PR.name, PR.outstanding_amount) + .where(PR.name.isin(referenced_payment_requests)) + ).run() - return dict(response) + return dict(response) if response else None def validate_inclusive_tax(tax, doc): @@ -2815,6 +2866,7 @@ def get_payment_entry( pe.set_difference_amount() + # If PE is created from PR directly, then no need to find open PRs for the references if not created_from_payment_request: allocate_open_payment_requests_to_references(pe.references) @@ -2822,6 +2874,12 @@ def get_payment_entry( def get_open_payment_requests_for_references(references=None): + """ + Fetch all unpaid Payment Requests for the references. \n + - Each reference can have multiple Payment Requests. \n + + Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}} + """ if not references: return @@ -2862,13 +2920,41 @@ def get_open_payment_requests_for_references(references=None): def allocate_open_payment_requests_to_references(references=None): + """ + Allocate unpaid Payment Requests to the references. \n + --- + - Allocation based on below factors + - Reference Allocated Amount + - Reference Outstanding Amount (With Payment Terms or without Payment Terms) + - Reference Payment Request's outstanding amount + --- + - Allocation based on below scenarios + - Reference's Allocated Amount == Payment Request's Outstanding Amount + - Allocate the Payment Request to the reference + - This PR will not be allocated further + - Reference's Allocated Amount < Payment Request's Outstanding Amount + - Allocate the Payment Request to the reference + - Reduce the PR's outstanding amount by the allocated amount + - This PR can be allocated further + - Reference's Allocated Amount > Payment Request's Outstanding Amount + - Allocate the Payment Request to the reference + - Reduce Allocated Amount of the reference by the PR's outstanding amount + - Create a new row for the remaining amount until the Allocated Amount is 0 + - Allocate PR if available + --- + - Note: + - Priority is given to the first Payment Request of respective references. + - Single Reference can have multiple rows. + - With Payment Terms or without Payment Terms + - With Payment Request or without Payment Request + """ if not references: return # get all unpaid payment requests for the references - all_references_payment_requests = get_open_payment_requests_for_references(references) + references_open_payment_requests = get_open_payment_requests_for_references(references) - if not all_references_payment_requests: + if not references_open_payment_requests: return # to manage new rows @@ -2884,24 +2970,24 @@ def allocate_open_payment_requests_to_references(references=None): row.idx = row_number # unpaid payment requests for the reference - reference_payment_requests = all_references_payment_requests.get(reference_key) + reference_payment_requests = references_open_payment_requests.get(reference_key) if not reference_payment_requests: row_number += MOVE_TO_NEXT_ROW # to move to next reference row continue # get the first payment request and its outstanding amount - payment_request, outstanding_amount = next(iter(reference_payment_requests.items())) + payment_request, pr_outstanding_amount = next(iter(reference_payment_requests.items())) allocated_amount = row.allocated_amount # allocate the payment request to the reference and PR's outstanding amount row.payment_request = payment_request - if outstanding_amount == allocated_amount: + if pr_outstanding_amount == allocated_amount: del reference_payment_requests[payment_request] row_number += MOVE_TO_NEXT_ROW - elif outstanding_amount > allocated_amount: + elif pr_outstanding_amount > allocated_amount: # reduce the outstanding amount of the payment request reference_payment_requests[payment_request] -= allocated_amount row_number += MOVE_TO_NEXT_ROW @@ -2909,8 +2995,8 @@ def allocate_open_payment_requests_to_references(references=None): else: # split the reference row to allocate the remaining amount del reference_payment_requests[payment_request] - row.allocated_amount = outstanding_amount - allocated_amount -= outstanding_amount + row.allocated_amount = pr_outstanding_amount + allocated_amount -= pr_outstanding_amount # set the remaining amount to the next row while allocated_amount: @@ -2918,8 +3004,8 @@ def allocate_open_payment_requests_to_references(references=None): new_row = frappe.copy_doc(row) references.insert(row_number, new_row) - # get the next payment request and its outstanding amount - payment_request, outstanding_amount = next( + # get the first payment request and its outstanding amount + payment_request, pr_outstanding_amount = next( iter(reference_payment_requests.items()), (None, None) ) @@ -2927,25 +3013,25 @@ def allocate_open_payment_requests_to_references(references=None): new_row.idx = row_number + 1 new_row.payment_request = payment_request new_row.allocated_amount = min( - outstanding_amount if outstanding_amount else allocated_amount, allocated_amount + pr_outstanding_amount if pr_outstanding_amount else allocated_amount, allocated_amount ) - if not payment_request or not outstanding_amount: + if not payment_request or not pr_outstanding_amount: row_number += TO_SKIP_NEW_ROW break - elif outstanding_amount == allocated_amount: + elif pr_outstanding_amount == allocated_amount: del reference_payment_requests[payment_request] row_number += TO_SKIP_NEW_ROW break - elif outstanding_amount > allocated_amount: + elif pr_outstanding_amount > allocated_amount: reference_payment_requests[payment_request] -= allocated_amount row_number += TO_SKIP_NEW_ROW break else: - allocated_amount -= outstanding_amount + allocated_amount -= pr_outstanding_amount del reference_payment_requests[payment_request] row_number += MOVE_TO_NEXT_ROW diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py index 3afff4c8d25b..c7d9909950d8 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -37,8 +37,8 @@ class PaymentEntryReference(Document): @property def payment_term_outstanding(self): - if not self.payment_term: - return 0 + if not self.payment_term or not self.reference_doctype or not self.reference_name: + return return frappe.db.get_value( "Payment Schedule", @@ -53,6 +53,6 @@ def payment_term_outstanding(self): @property def payment_request_outstanding(self): if not self.payment_request: - return 0 + return return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount") diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 08479cf4c752..e870c4214c13 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -438,6 +438,12 @@ def update_reference_advance_payment_status(self): ref_doc.set_advance_payment_status() def _allocate_payment_request_to_pe_references(self, references): + """ + Allocate the Payment Request to the Payment Entry references based on\n + - Allocated Amount. + - Outstanding Amount of Payment Request.\n + Payment Request is doc itself and references are the rows of Payment Entry. + """ if len(references) == 1: references[0].payment_request = self.name return @@ -459,6 +465,8 @@ def _allocate_payment_request_to_pe_references(self, references): if outstanding_amount == 0: if not NEW_ROW_ADDED: break + + row_number += MOVE_TO_NEXT_ROW continue # allocate the payment request to the row @@ -700,7 +708,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False): if not references: return - payment_requests = frappe.get_all( + referenced_payment_requests = frappe.get_all( "Payment Request", filters={"name": ["in", get_referenced_payment_requests(references)]}, fields=[ @@ -711,13 +719,13 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False): ], ) - payment_requests = {pr.name: pr for pr in payment_requests} + referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests} for ref in references: if not ref.payment_request: continue - payment_request = payment_requests[ref.payment_request] + payment_request = referenced_payment_requests[ref.payment_request] # update outstanding amount new_outstanding_amount = flt( @@ -783,7 +791,7 @@ def get_dummy_message(doc): {%- else %}

Hello,

{% endif %}

{{ _("Requesting payment against {0} {1} for amount {2}").format(doc.doctype, - doc.name, doc.get_formatted("grand_total")) }}

+ doc.name, doc.get_formatted("grand_total")) }}

{{ _("Make Payment") }} From 3ee0a0e473fcface1b296a6d65f63484bd9e7531 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Thu, 12 Sep 2024 16:52:54 +0530 Subject: [PATCH 37/60] fix: Removed duplication code --- .../doctype/payment_entry/payment_entry.js | 2 +- .../doctype/payment_entry/payment_entry.py | 11 +++--- .../payment_request/payment_request.py | 37 +++++-------------- 3 files changed, 15 insertions(+), 35 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 699fdb3cf5eb..4591ca809f2e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -177,7 +177,7 @@ frappe.ui.form.on("Payment Entry", { frm.set_query("payment_request", "references", function (doc, cdt, cdn) { const row = frappe.get_doc(cdt, cdn); return { - query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests", + query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests_query", filters: { reference_doctype: row.reference_doctype, reference_name: row.reference_name, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index e91bebefff83..cf99082b5131 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -335,17 +335,16 @@ def validate_allocated_amount_as_per_payment_request(self): """ Allocated amount should not be greater than the outstanding amount of the Payment Request.f """ - from erpnext.accounts.doctype.payment_request.payment_request import ( - get_outstanding_amount_of_payment_entry_references as get_outstanding_amounts, - ) - if not self.references: return - outstanding_amounts = get_outstanding_amounts(self.references) + pr_outstanding_amounts = get_payment_request_outstanding_set_in_references(self.references) + + if not pr_outstanding_amounts: + return for ref in self.references: - if ref.payment_request and ref.allocated_amount > outstanding_amounts[ref.payment_request]: + if ref.payment_request and ref.allocated_amount > pr_outstanding_amounts[ref.payment_request]: frappe.throw( msg=_( "Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}" diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index e870c4214c13..7e6ab9d7a438 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -705,12 +705,15 @@ def make_payment_entry(docname): def update_payment_requests_as_per_pe_references(references=None, cancel=False): + """ + Update Payment Request's `Status` and `Outstanding Amount` based on Payment Entry Reference's `Allocated Amount`. + """ if not references: return referenced_payment_requests = frappe.get_all( "Payment Request", - filters={"name": ["in", get_referenced_payment_requests(references)]}, + filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]}, fields=[ "name", "grand_total", @@ -761,29 +764,6 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False): ) -def get_outstanding_amount_of_payment_entry_references(references): - if not references: - return {} - - payment_requests = get_referenced_payment_requests(references) - - return dict( - frappe.get_all( - "Payment Request", - filters={"name": ["in", payment_requests]}, - fields=["name", "outstanding_amount"], - as_list=True, - ) - ) - - -def get_referenced_payment_requests(references): - if not references: - return () - - return {row.payment_request for row in references if row.payment_request} - - def get_dummy_message(doc): return frappe.render_template( """{% if doc.contact_person -%} @@ -791,7 +771,7 @@ def get_dummy_message(doc): {%- else %}

Hello,

{% endif %}

{{ _("Requesting payment against {0} {1} for amount {2}").format(doc.doctype, - doc.name, doc.get_formatted("grand_total")) }}

+ doc.name, doc.get_formatted("grand_total")) }}

{{ _("Make Payment") }} @@ -895,7 +875,7 @@ def get_paid_amount_against_order(dt, dn): @frappe.whitelist() -def get_open_payment_requests(doctype, txt, searchfield, start, page_len, filters): +def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters): # permission checks in `get_list()` reference_doctype = filters.get("reference_doctype") reference_name = filters.get("reference_doctype") @@ -909,6 +889,7 @@ def get_open_payment_requests(doctype, txt, searchfield, start, page_len, filter "reference_doctype": filters["reference_doctype"], "reference_name": filters["reference_name"], "status": ["!=", "Paid"], + "outstanding_amount": ["!=", 0], # for compatibility with old data "docstatus": 1, }, fields=["name", "grand_total", "outstanding_amount"], @@ -918,8 +899,8 @@ def get_open_payment_requests(doctype, txt, searchfield, start, page_len, filter return [ ( pr.name, - _("Grand Total: {0}").format(pr.grand_total), - _("Outstanding Amount: {0}").format(pr.outstanding_amount), + _("Grand Total: {0}").format(pr.grand_total), + _("Outstanding Amount: {0}").format(pr.outstanding_amount), ) for pr in open_payment_requests ] From 2182e0c621a3479a01cebd63119d268b1b156edf Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Thu, 12 Sep 2024 17:27:36 +0530 Subject: [PATCH 38/60] fix: proper message title --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 4591ca809f2e..4c8723e62d19 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1661,7 +1661,7 @@ frappe.ui.form.on("Payment Entry", { ]; frappe.msgprint({ - title: __("Matched Payment Request"), + title: __("Unset Matched Payment Request"), message: COLUMN_LABEL.concat(matched_payment_requests), as_table: true, primary_action: { From c5b5057cd88d3cba5d5e8c2c8c8e3b1b9dc2606f Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 13 Sep 2024 14:47:32 +0530 Subject: [PATCH 39/60] refactor: Rename method of Allocation Amount to References --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 2 +- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 4c8723e62d19..56fbc18f12a8 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1067,7 +1067,7 @@ frappe.ui.form.on("Payment Entry", { }, allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { - await frm.call("allocate_party_amount_and_payment_request_against_ref_docs", { + await frm.call("allocate_amount_to_references", { paid_amount: paid_amount, paid_amount_change: paid_amount_change, allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index cf99082b5131..f4864d059dfa 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1742,14 +1742,12 @@ def set_matched_unset_payment_requests_to_response(self): frappe.response["matched_payment_requests"] = matched_payment_requests @frappe.whitelist() - def allocate_party_amount_and_payment_request_against_ref_docs( - self, paid_amount, paid_amount_change, allocate_payment_amount - ): + def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocate_payment_amount): """ Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n :param paid_amount: Paid Amount / Received Amount. :param paid_amount_change: Flag to check if `Paid Amount` is changed or not. - :param allocate_payment_amount: Flag to allocate amount or not. + :param allocate_payment_amount: Flag to allocate amount or not. (Payment Request is also dependent on this flag) """ if not self.references: return From 22e41e49588ace54ec3adba665491cb4fc07ab2c Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 13 Sep 2024 15:58:33 +0530 Subject: [PATCH 40/60] refactor: Changing `grand_total` description based on `party_type` --- .../doctype/payment_request/payment_request.js | 11 +++++++++++ .../doctype/payment_request/payment_request.json | 3 +-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index 44313e5c0d2c..dd959251f168 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -25,6 +25,8 @@ frappe.ui.form.on("Payment Request", "onload", function (frm, dt, dn) { }); frappe.ui.form.on("Payment Request", "refresh", function (frm) { + set_grand_total_field_description(frm); + if (frm.doc.status == "Failed") { frm.set_intro(__("Failure: {0}", [frm.doc.failed_reason]), "red"); } @@ -97,3 +99,12 @@ frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) { }); } }); + +frappe.ui.form.on("Payment Request", "party_type", function (frm) { + set_grand_total_field_description(frm); +}); + +function set_grand_total_field_description(frm) { + const party_type = (frm.doc.party_type || "party").toLowerCase(); + frm.get_field("grand_total").set_description(`Amount in ${party_type}'s currency`); +} diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index d9baeee28539..8ebdf53bbde0 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -146,7 +146,6 @@ "label": "Transaction Details" }, { - "description": "Amount in customer's currency", "fieldname": "grand_total", "fieldtype": "Currency", "label": "Amount", @@ -432,7 +431,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-08-21 17:17:16.584404", + "modified": "2024-09-13 15:36:08.198722", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", From 3a95e183a4231c42b1ad7402984f41ca8b0365af Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 13 Sep 2024 19:48:34 +0530 Subject: [PATCH 41/60] refactor: update Payment Request --- .../payment_request/payment_request.js | 11 ------ .../payment_request/payment_request.json | 7 ++-- .../payment_request/payment_request.py | 36 +++++-------------- 3 files changed, 13 insertions(+), 41 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index dd959251f168..44313e5c0d2c 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -25,8 +25,6 @@ frappe.ui.form.on("Payment Request", "onload", function (frm, dt, dn) { }); frappe.ui.form.on("Payment Request", "refresh", function (frm) { - set_grand_total_field_description(frm); - if (frm.doc.status == "Failed") { frm.set_intro(__("Failure: {0}", [frm.doc.failed_reason]), "red"); } @@ -99,12 +97,3 @@ frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) { }); } }); - -frappe.ui.form.on("Payment Request", "party_type", function (frm) { - set_grand_total_field_description(frm); -}); - -function set_grand_total_field_description(frm) { - const party_type = (frm.doc.party_type || "party").toLowerCase(); - frm.get_field("grand_total").set_description(`Amount in ${party_type}'s currency`); -} diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 8ebdf53bbde0..5b1b9e4a14f5 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -146,8 +146,10 @@ "label": "Transaction Details" }, { + "description": "Amount in party's account currency", "fieldname": "grand_total", "fieldtype": "Currency", + "in_preview": 1, "label": "Amount", "non_negative": 1, "options": "currency", @@ -166,7 +168,7 @@ { "fieldname": "currency", "fieldtype": "Link", - "label": "Transaction Currency", + "label": "Party Account Currency", "options": "Currency", "read_only": 1 }, @@ -413,6 +415,7 @@ "in_preview": 1, "label": "Outstanding Amount", "non_negative": 1, + "options": "currency", "read_only": 1 }, { @@ -431,7 +434,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-09-13 15:36:08.198722", + "modified": "2024-09-13 19:17:28.734384", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 7e6ab9d7a438..056b59e9c6e0 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -46,6 +46,7 @@ class PaymentRequest(Document): bank_account: DF.Link | None bank_account_no: DF.ReadOnly | None branch_code: DF.ReadOnly | None + company: DF.Link | None cost_center: DF.Link | None currency: DF.Link | None email_to: DF.Data | None @@ -87,7 +88,6 @@ class PaymentRequest(Document): subscription_plans: DF.Table[SubscriptionPlanDetail] swift_number: DF.ReadOnly | None transaction_date: DF.Date | None - company: DF.Link | None # end: auto-generated types def validate(self): @@ -287,36 +287,20 @@ def create_payment_entry(self, submit=True): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - if self.reference_doctype in ["Sales Invoice", "POS Invoice"]: - party_account = ref_doc.debit_to - elif self.reference_doctype == "Purchase Invoice": - party_account = ref_doc.credit_to - else: - party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company) - - party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account) - - bank_amount = self.outstanding_amount - - if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: - total = ref_doc.get("rounded_total") or ref_doc.get("grand_total") - base_total = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") - party_amount = flt(self.outstanding_amount / total * base_total, self.precision("grand_total")) - else: - party_amount = self.outstanding_amount - + # outstanding amount is already in Part's account currency payment_entry = get_payment_entry( self.reference_doctype, self.reference_name, - party_amount=party_amount, + party_amount=self.outstanding_amount, bank_account=self.payment_account, - bank_amount=bank_amount, + bank_amount=self.outstanding_amount, created_from_payment_request=True, ) payment_entry.update( { "mode_of_payment": self.mode_of_payment, + "reference_no": self.name, # to prevent validation error "reference_date": nowdate(), "remarks": "Payment Entry against {} {} via Payment Request {}".format( self.reference_doctype, self.reference_name, self.name @@ -562,7 +546,7 @@ def make_payment_request(**args): "payment_account": gateway_account.get("payment_account"), "payment_channel": gateway_account.get("payment_channel"), "payment_request_type": args.get("payment_request_type"), - "currency": ref_doc.currency, + "currency": ref_doc.party_account_currency, # no need of conversion using this "grand_total": grand_total, "mode_of_payment": args.mode_of_payment, "email_to": args.recipient_id or ref_doc.owner, @@ -622,12 +606,8 @@ def get_amount(ref_doc, payment_account=None): grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) elif dt in ["Sales Invoice", "Purchase Invoice"]: if not ref_doc.get("is_pos"): - if ref_doc.party_account_currency == ref_doc.currency: - grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total) - else: - grand_total = flt( - flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate - ) + # always get base amount to create a payment request (to match with PE) + grand_total = flt(ref_doc.base_rounded_total) or flt(ref_doc.base_grand_total) elif dt == "Sales Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: From b4aee31d85f03716dbf83a48a43eb1af2955d91c Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 16 Sep 2024 13:48:38 +0530 Subject: [PATCH 42/60] fix: Remove virtual property of payment_term_oustanding from references --- .../payment_entry_reference.json | 3 +-- .../payment_entry_reference.py | 15 --------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 0506517a70c0..365058c0bca7 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -134,7 +134,6 @@ "depends_on": "eval: doc.payment_term", "fieldname": "payment_term_outstanding", "fieldtype": "Float", - "is_virtual": 1, "label": "Payment Term Outstanding", "read_only": 1 }, @@ -150,7 +149,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-08-23 12:35:40.525380", + "modified": "2024-09-16 13:44:14.289408", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py index c7d9909950d8..2ac92ba4a841 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -35,21 +35,6 @@ class PaymentEntryReference(Document): total_amount: DF.Float # end: auto-generated types - @property - def payment_term_outstanding(self): - if not self.payment_term or not self.reference_doctype or not self.reference_name: - return - - return frappe.db.get_value( - "Payment Schedule", - { - "payment_term": self.payment_term, - "parenttype": self.reference_doctype, - "parent": self.reference_name, - }, - "outstanding", - ) - @property def payment_request_outstanding(self): if not self.payment_request: From 7ceeb2c59afc63ab5408b392b2ecc44bbb86dd95 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 16 Sep 2024 15:38:54 +0530 Subject: [PATCH 43/60] fix: fetch party account currency for creating payment request --- .../doctype/payment_request/payment_request.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 056b59e9c6e0..da838a1e803f 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -539,6 +539,14 @@ def make_payment_request(**args): args["payment_request_type"] = ( "Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward" ) + + party_type = args.get("party_type") or "Customer" + party_account_currency = ref_doc.party_account_currency + + if not party_account_currency: + party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company) + party_account_currency = get_account_currency(party_account) + pr.update( { "payment_gateway_account": gateway_account.get("name"), @@ -546,7 +554,7 @@ def make_payment_request(**args): "payment_account": gateway_account.get("payment_account"), "payment_channel": gateway_account.get("payment_channel"), "payment_request_type": args.get("payment_request_type"), - "currency": ref_doc.party_account_currency, # no need of conversion using this + "currency": party_account_currency, # consistent with PE "grand_total": grand_total, "mode_of_payment": args.mode_of_payment, "email_to": args.recipient_id or ref_doc.owner, @@ -555,7 +563,7 @@ def make_payment_request(**args): "reference_doctype": ref_doc.doctype, "reference_name": ref_doc.name, "company": ref_doc.get("company"), - "party_type": args.get("party_type") or "Customer", + "party_type": party_type, "party": args.get("party") or ref_doc.get("customer"), "bank_account": bank_account, "make_sales_invoice": ( From ca0c1535e695e94ff7f353d5f5c295e8ce71b2a6 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 16 Sep 2024 16:41:08 +0530 Subject: [PATCH 44/60] fix: use transaction currency as base in payment request --- .../payment_request/payment_request.json | 18 +++++-- .../payment_request/payment_request.py | 54 ++++++++++++++++--- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 5b1b9e4a14f5..5a1652f6db27 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -20,10 +20,11 @@ "reference_name", "transaction_details", "grand_total", + "outstanding_amount", "is_a_subscription", "column_break_18", - "outstanding_amount", "currency", + "party_account_currency", "subscription_section", "subscription_plans", "bank_account_details", @@ -146,7 +147,7 @@ "label": "Transaction Details" }, { - "description": "Amount in party's account currency", + "description": "Amount in transaction currency", "fieldname": "grand_total", "fieldtype": "Currency", "in_preview": 1, @@ -168,7 +169,7 @@ { "fieldname": "currency", "fieldtype": "Link", - "label": "Party Account Currency", + "label": "Transaction Currency", "options": "Currency", "read_only": 1 }, @@ -415,7 +416,7 @@ "in_preview": 1, "label": "Outstanding Amount", "non_negative": 1, - "options": "currency", + "options": "party_account_currency", "read_only": 1 }, { @@ -428,13 +429,20 @@ { "fieldname": "column_break_pnyv", "fieldtype": "Column Break" + }, + { + "fieldname": "party_account_currency", + "fieldtype": "Link", + "label": "Party Account Currency", + "options": "Currency", + "read_only": 1 } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-09-13 19:17:28.734384", + "modified": "2024-09-16 06:34:36.517077", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index da838a1e803f..969071e95f02 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -7,6 +7,7 @@ from frappe.utils import flt, nowdate from frappe.utils.background_jobs import enqueue +from erpnext import get_company_currency from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) @@ -61,6 +62,7 @@ class PaymentRequest(Document): naming_series: DF.Literal["ACC-PRQ-.YYYY.-"] outstanding_amount: DF.Currency party: DF.DynamicLink | None + party_account_currency: DF.Link | None party_type: DF.Link | None payment_account: DF.ReadOnly | None payment_channel: DF.Literal["", "Email", "Phone", "Other"] @@ -159,7 +161,20 @@ def validate_subscription_details(self): ) def before_submit(self): - self.outstanding_amount = self.grand_total + if ( + self.currency != self.party_account_currency + and self.party_account_currency == get_company_currency(self.company) + ): + invoice = frappe.get_value( + self.reference_doctype, self.reference_name, ["grand_total", "base_grand_total"] + ) + self.outstanding_amount = flt( + self.grand_total / invoice.grand_total * invoice.base_grand_total, + self.precision("outstanding_amount"), + ) + + else: + self.outstanding_amount = self.grand_total if self.payment_request_type == "Outward": self.status = "Initiated" @@ -287,13 +302,35 @@ def create_payment_entry(self, submit=True): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) + if self.reference_doctype in ["Sales Invoice", "POS Invoice"]: + party_account = ref_doc.debit_to + elif self.reference_doctype == "Purchase Invoice": + party_account = ref_doc.credit_to + else: + party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company) + + party_account_currency = ( + self.get("party_account_currency") + or ref_doc.get("party_account_currency") + or get_account_currency(party_account) + ) + + bank_amount = self.outstanding_amount + + if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: + total = ref_doc.get("rounded_total") or ref_doc.get("grand_total") + base_total = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") + party_amount = flt(self.outstanding_amount / total * base_total, self.precision("grand_total")) + else: + party_amount = self.outstanding_amount + # outstanding amount is already in Part's account currency payment_entry = get_payment_entry( self.reference_doctype, self.reference_name, - party_amount=self.outstanding_amount, + party_amount=party_amount, bank_account=self.payment_account, - bank_amount=self.outstanding_amount, + bank_amount=bank_amount, created_from_payment_request=True, ) @@ -554,7 +591,8 @@ def make_payment_request(**args): "payment_account": gateway_account.get("payment_account"), "payment_channel": gateway_account.get("payment_channel"), "payment_request_type": args.get("payment_request_type"), - "currency": party_account_currency, # consistent with PE + "currency": ref_doc.currency, + "party_account_currency": party_account_currency, "grand_total": grand_total, "mode_of_payment": args.mode_of_payment, "email_to": args.recipient_id or ref_doc.owner, @@ -614,8 +652,12 @@ def get_amount(ref_doc, payment_account=None): grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) elif dt in ["Sales Invoice", "Purchase Invoice"]: if not ref_doc.get("is_pos"): - # always get base amount to create a payment request (to match with PE) - grand_total = flt(ref_doc.base_rounded_total) or flt(ref_doc.base_grand_total) + if ref_doc.party_account_currency == ref_doc.currency: + grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total) + else: + grand_total = flt( + flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate + ) elif dt == "Sales Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: From 79840a167499435e185db347738b4dd7a0442d29 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 16 Sep 2024 16:54:50 +0530 Subject: [PATCH 45/60] fix: party amount for creating payment entry --- .../doctype/payment_request/payment_request.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 969071e95f02..62cc85e33e4e 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -165,11 +165,17 @@ def before_submit(self): self.currency != self.party_account_currency and self.party_account_currency == get_company_currency(self.company) ): + # set outstanding amount in party account currency invoice = frappe.get_value( - self.reference_doctype, self.reference_name, ["grand_total", "base_grand_total"] + self.reference_doctype, + self.reference_name, + ["rounded_total", "grand_total", "base_rounded_total", "base_grand_total"], + as_dict=1, ) + grand_total = invoice.get("rounded_total") or invoice.get("grand_total") + base_grand_total = invoice.get("base_rounded_total") or invoice.get("base_grand_total") self.outstanding_amount = flt( - self.grand_total / invoice.grand_total * invoice.base_grand_total, + self.grand_total / grand_total * base_grand_total, self.precision("outstanding_amount"), ) @@ -315,14 +321,12 @@ def create_payment_entry(self, submit=True): or get_account_currency(party_account) ) - bank_amount = self.outstanding_amount + bank_amount = self.grand_total if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: - total = ref_doc.get("rounded_total") or ref_doc.get("grand_total") - base_total = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") - party_amount = flt(self.outstanding_amount / total * base_total, self.precision("grand_total")) - else: party_amount = self.outstanding_amount + else: + party_amount = self.grand_total # outstanding amount is already in Part's account currency payment_entry = get_payment_entry( From 3cec6f0cf36dfddcf0b9202f910df74cd3f4945c Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 16 Sep 2024 17:06:17 +0530 Subject: [PATCH 46/60] fix: allow for proportional amount paid by bank --- .../accounts/doctype/payment_request/payment_request.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 62cc85e33e4e..dbdd77c39fbb 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -321,12 +321,11 @@ def create_payment_entry(self, submit=True): or get_account_currency(party_account) ) - bank_amount = self.grand_total + party_amount = bank_amount = self.outstanding_amount if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency: - party_amount = self.outstanding_amount - else: - party_amount = self.grand_total + exchange_rate = ref_doc.get("conversion_rate") + bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total")) # outstanding amount is already in Part's account currency payment_entry = get_payment_entry( From 4416fb72247d1855dcec2e1d141f3887c9b0082c Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 16 Sep 2024 18:03:54 +0530 Subject: [PATCH 47/60] fix: Changed field order in Payment Request --- .../payment_request/payment_request.json | 7 ++++--- .../doctype/payment_request/payment_request.py | 17 ++--------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 5a1652f6db27..36ef7a59ca8f 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -20,10 +20,10 @@ "reference_name", "transaction_details", "grand_total", - "outstanding_amount", + "currency", "is_a_subscription", "column_break_18", - "currency", + "outstanding_amount", "party_account_currency", "subscription_section", "subscription_plans", @@ -411,6 +411,7 @@ }, { "depends_on": "eval: doc.docstatus === 1", + "description": "Amount in party's bank account currency", "fieldname": "outstanding_amount", "fieldtype": "Currency", "in_preview": 1, @@ -442,7 +443,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-09-16 06:34:36.517077", + "modified": "2024-09-16 17:50:54.440090", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index dbdd77c39fbb..77fdd37fec74 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -35,12 +35,9 @@ class PaymentRequest(Document): from typing import TYPE_CHECKING if TYPE_CHECKING: + from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import SubscriptionPlanDetail from frappe.types import DF - from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import ( - SubscriptionPlanDetail, - ) - account: DF.ReadOnly | None amended_from: DF.Link | None bank: DF.Link | None @@ -75,17 +72,7 @@ class PaymentRequest(Document): project: DF.Link | None reference_doctype: DF.Link | None reference_name: DF.DynamicLink | None - status: DF.Literal[ - "", - "Draft", - "Requested", - "Initiated", - "Partially Paid", - "Payment Ordered", - "Paid", - "Failed", - "Cancelled", - ] + status: DF.Literal["", "Draft", "Requested", "Initiated", "Partially Paid", "Payment Ordered", "Paid", "Failed", "Cancelled"] subject: DF.Data | None subscription_plans: DF.Table[SubscriptionPlanDetail] swift_number: DF.ReadOnly | None From aa93c8bc2f347d8abd5f5ee4a1b804e4c9fb827e Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 16 Sep 2024 18:57:05 +0530 Subject: [PATCH 48/60] fix: Minor refactor in Payment Entry Reference table data --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 1 + .../payment_entry_reference/payment_entry_reference.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 86924a869403..3e5d643e775e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1010,6 +1010,7 @@ frappe.ui.form.on("Payment Entry", { c.outstanding_amount = d.outstanding_amount; c.bill_no = d.bill_no; c.payment_term = d.payment_term; + c.payment_term_outstanding = d.payment_term_outstanding; c.allocated_amount = d.allocated_amount; c.account = d.account; diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 365058c0bca7..f5d39c134b50 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -138,7 +138,7 @@ "read_only": 1 }, { - "depends_on": "eval: doc.payment_request", + "depends_on": "eval: doc.payment_request && doc.payment_request_outstanding", "fieldname": "payment_request_outstanding", "fieldtype": "Float", "is_virtual": 1, @@ -149,7 +149,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-09-16 13:44:14.289408", + "modified": "2024-09-16 18:11:50.019343", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", From e92f44ccea8f37aedf4ff147850132921a7e0aa9 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 16 Sep 2024 20:10:49 +0530 Subject: [PATCH 49/60] test: Added test cases for allow Payment at `Partially Paid` status for PR --- .../payment_request/test_payment_request.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 8aa169fa3a24..67f048f304ff 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -415,3 +415,30 @@ def test_conversion_on_foreign_currency_accounts(self): self.assertEqual(pe.paid_amount, 800) self.assertEqual(pe.base_received_amount, 800) self.assertEqual(pe.received_amount, 10) + + def test_multiple_payment_if_partially_paid(self): + so = make_sales_order(currency="INR", qty=1, rate=1000) + + pr = make_payment_request( + dt="Sales Order", + dn=so.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + pe = pr.create_payment_entry(submit=False) + pe.paid_amount = 200 + pe.references[0].allocated_amount = 200 + pe.submit() + pr.load_from_db() + + self.assertEqual(pr.status, "Partially Paid") + + pe = pr.create_payment_entry(submit=False) + pe.paid_amount = 800 + pe.references[0].allocated_amount = 800 + pe.submit() + pr.load_from_db() + + self.assertEqual(pr.status, "Paid") From 3cb9cbc20797658c768554bcf9c422a11a0ff6d5 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 16 Sep 2024 20:28:30 +0530 Subject: [PATCH 50/60] test: Update partial paid status test case --- .../payment_request/test_payment_request.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 67f048f304ff..ef616b7c3ee9 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -416,7 +416,7 @@ def test_conversion_on_foreign_currency_accounts(self): self.assertEqual(pe.base_received_amount, 800) self.assertEqual(pe.received_amount, 10) - def test_multiple_payment_if_partially_paid(self): + def test_multiple_payment_if_partially_paid_for_same_currency(self): so = make_sales_order(currency="INR", qty=1, rate=1000) pr = make_payment_request( @@ -442,3 +442,31 @@ def test_multiple_payment_if_partially_paid(self): pr.load_from_db() self.assertEqual(pr.status, "Paid") + + def test_multiple_payment_if_partially_paid_for_multi_currency(self): + si = create_sales_invoice(currency="USD", conversion_rate=50, qty=1, rate=100) + + pr = make_payment_request( + dt="Sales Invoice", + dn=si.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + # 50 USD -> 5000 INR + pe = pr.create_payment_entry(submit=False) + pe.paid_amount = 2000 + pe.references[0].allocated_amount = 2000 + pe.submit() + pr.load_from_db() + + self.assertEqual(pr.status, "Partially Paid") + + pe = pr.create_payment_entry(submit=False) + pe.paid_amount = 3000 + pe.references[0].allocated_amount = 3000 + pe.submit() + pr.load_from_db() + + self.assertEqual(pr.status, "Paid") From f13cc62977688990895176ca613e3ee3f2f2bbf5 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 18 Sep 2024 10:06:55 +0530 Subject: [PATCH 51/60] test: Update test case for same currency PR --- .../payment_request/payment_request.py | 17 +++++- .../payment_request/test_payment_request.py | 54 +++++++++++++++---- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 77fdd37fec74..dbdd77c39fbb 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -35,9 +35,12 @@ class PaymentRequest(Document): from typing import TYPE_CHECKING if TYPE_CHECKING: - from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import SubscriptionPlanDetail from frappe.types import DF + from erpnext.accounts.doctype.subscription_plan_detail.subscription_plan_detail import ( + SubscriptionPlanDetail, + ) + account: DF.ReadOnly | None amended_from: DF.Link | None bank: DF.Link | None @@ -72,7 +75,17 @@ class PaymentRequest(Document): project: DF.Link | None reference_doctype: DF.Link | None reference_name: DF.DynamicLink | None - status: DF.Literal["", "Draft", "Requested", "Initiated", "Partially Paid", "Payment Ordered", "Paid", "Failed", "Cancelled"] + status: DF.Literal[ + "", + "Draft", + "Requested", + "Initiated", + "Partially Paid", + "Payment Ordered", + "Paid", + "Failed", + "Cancelled", + ] subject: DF.Data | None subscription_plans: DF.Table[SubscriptionPlanDetail] swift_number: DF.ReadOnly | None diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index ef616b7c3ee9..a571bd9d2c5a 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import re import unittest from unittest.mock import patch @@ -336,8 +337,8 @@ def test_payment_entry(self): gl_entries = frappe.db.sql( """select account, debit, credit, against_voucher - from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s - order by account asc""", + from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s + order by account asc""", pe.name, as_dict=1, ) @@ -427,21 +428,46 @@ def test_multiple_payment_if_partially_paid_for_same_currency(self): return_doc=1, ) + self.assertEqual(pr.grand_total, 1000) + self.assertEqual(pr.outstanding_amount, pr.grand_total) + self.assertEqual(pr.party_account_currency, pr.currency) # INR + self.assertEqual(pr.status, "Requested") + + # to make partial payment pe = pr.create_payment_entry(submit=False) pe.paid_amount = 200 pe.references[0].allocated_amount = 200 pe.submit() - pr.load_from_db() - self.assertEqual(pr.status, "Partially Paid") + self.assertEqual(pe.references[0].payment_request, pr.name) - pe = pr.create_payment_entry(submit=False) - pe.paid_amount = 800 - pe.references[0].allocated_amount = 800 - pe.submit() pr.load_from_db() + self.assertEqual(pr.status, "Partially Paid") + self.assertEqual(pr.outstanding_amount, 800) + self.assertEqual(pr.grand_total, 1000) + pe = pr.create_payment_entry() + self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount + self.assertEqual(pe.references[0].allocated_amount, 800) + self.assertEqual(pe.references[0].outstanding_amount, 800) + self.assertEqual(pe.references[0].payment_request, pr.name) + + pr.load_from_db() self.assertEqual(pr.status, "Paid") + self.assertEqual(pr.outstanding_amount, 0) + self.assertEqual(pr.grand_total, 1000) + + # creating a more payment Request must not allowed + self.assertRaisesRegex( + frappe.exceptions.ValidationError, + re.compile(r"Payment Request is already created"), + make_payment_request, + dt="Sales Order", + dn=so.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) def test_multiple_payment_if_partially_paid_for_multi_currency(self): si = create_sales_invoice(currency="USD", conversion_rate=50, qty=1, rate=100) @@ -454,7 +480,13 @@ def test_multiple_payment_if_partially_paid_for_multi_currency(self): return_doc=1, ) - # 50 USD -> 5000 INR + # 100 USD -> 5000 INR + self.assertEqual(pr.grand_total, 100) + self.assertEqual(pr.outstanding_amount, 5000) + self.assertEqual(pr.currency, "USD") + self.assertEqual(pr.party_account_currency, "INR") + self.assertEqual(pr.status, "Requested") + pe = pr.create_payment_entry(submit=False) pe.paid_amount = 2000 pe.references[0].allocated_amount = 2000 @@ -462,6 +494,8 @@ def test_multiple_payment_if_partially_paid_for_multi_currency(self): pr.load_from_db() self.assertEqual(pr.status, "Partially Paid") + self.assertEqual(pr.outstanding_amount, 3000) + self.assertEqual(pr.grand_total, 100) pe = pr.create_payment_entry(submit=False) pe.paid_amount = 3000 @@ -470,3 +504,5 @@ def test_multiple_payment_if_partially_paid_for_multi_currency(self): pr.load_from_db() self.assertEqual(pr.status, "Paid") + self.assertEqual(pr.outstanding_amount, 0) + self.assertEqual(pr.grand_total, 100) From f809548927457df0557e34b50c4feeba5ec54594 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 20 Sep 2024 11:27:55 +0530 Subject: [PATCH 52/60] refactor: Wider the `msgprint` dialog for after save PE --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 3e5d643e775e..b7ff852dcdee 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1673,6 +1673,7 @@ frappe.ui.form.on("Payment Entry", { title: __("Unset Matched Payment Request"), message: COLUMN_LABEL.concat(matched_payment_requests), as_table: true, + wide:true, primary_action: { label: __("Allocate Payment Request"), action() { From 133791dd01ad3fd0ca81ef811763608b3ab5f770 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 20 Sep 2024 11:32:52 +0530 Subject: [PATCH 53/60] test: Update PR test cases --- .../payment_request/test_payment_request.py | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index a571bd9d2c5a..9b2b51699e7d 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -446,10 +446,14 @@ def test_multiple_payment_if_partially_paid_for_same_currency(self): self.assertEqual(pr.outstanding_amount, 800) self.assertEqual(pr.grand_total, 1000) + # complete payment pe = pr.create_payment_entry() + self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount self.assertEqual(pe.references[0].allocated_amount, 800) - self.assertEqual(pe.references[0].outstanding_amount, 800) + self.assertEqual( + pe.references[0].outstanding_amount, 800 + ) # for orders it should be same as allocated amount self.assertEqual(pe.references[0].payment_request, pr.name) pr.load_from_db() @@ -487,22 +491,39 @@ def test_multiple_payment_if_partially_paid_for_multi_currency(self): self.assertEqual(pr.party_account_currency, "INR") self.assertEqual(pr.status, "Requested") + # to make partial payment pe = pr.create_payment_entry(submit=False) pe.paid_amount = 2000 pe.references[0].allocated_amount = 2000 pe.submit() - pr.load_from_db() + self.assertEqual(pe.references[0].payment_request, pr.name) + + pr.load_from_db() self.assertEqual(pr.status, "Partially Paid") self.assertEqual(pr.outstanding_amount, 3000) self.assertEqual(pr.grand_total, 100) - pe = pr.create_payment_entry(submit=False) - pe.paid_amount = 3000 - pe.references[0].allocated_amount = 3000 - pe.submit() - pr.load_from_db() + # complete payment + pe = pr.create_payment_entry() + self.assertEqual(pe.paid_amount, 3000) # paid amount set from pr's outstanding amount + self.assertEqual(pe.references[0].allocated_amount, 3000) + self.assertEqual(pe.references[0].outstanding_amount, 0) # for Invoices it will zero + self.assertEqual(pe.references[0].payment_request, pr.name) + pr.load_from_db() self.assertEqual(pr.status, "Paid") self.assertEqual(pr.outstanding_amount, 0) self.assertEqual(pr.grand_total, 100) + + # creating a more payment Request must not allowed + self.assertRaisesRegex( + frappe.exceptions.ValidationError, + re.compile(r"Payment Request is already created"), + make_payment_request, + dt="Sales Invoice", + dn=si.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) From d048123c0ed906b212d2b44418fe97780a708572 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 20 Sep 2024 11:48:32 +0530 Subject: [PATCH 54/60] chore: Remove dirty lines --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 3 +-- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index b7ff852dcdee..c4f14d71036d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -203,7 +203,6 @@ frappe.ui.form.on("Payment Entry", { }; }); - // todo: fetch payment term outstanding amount also frm.add_fetch( "payment_request", "outstanding_amount", @@ -1673,7 +1672,7 @@ frappe.ui.form.on("Payment Entry", { title: __("Unset Matched Payment Request"), message: COLUMN_LABEL.concat(matched_payment_requests), as_table: true, - wide:true, + wide: true, primary_action: { label: __("Allocate Payment Request"), action() { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index f4864d059dfa..572d39db4b09 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -333,7 +333,7 @@ def validate_allocated_amount(self): def validate_allocated_amount_as_per_payment_request(self): """ - Allocated amount should not be greater than the outstanding amount of the Payment Request.f + Allocated amount should not be greater than the outstanding amount of the Payment Request. """ if not self.references: return From c370e725c3fd853ccdb5330b84e957d60b7b3eca Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 20 Sep 2024 12:19:16 +0530 Subject: [PATCH 55/60] test: Checking `Advance Payment Status` --- .../payment_request/test_payment_request.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 9b2b51699e7d..f88d9e3dc30d 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -420,6 +420,8 @@ def test_conversion_on_foreign_currency_accounts(self): def test_multiple_payment_if_partially_paid_for_same_currency(self): so = make_sales_order(currency="INR", qty=1, rate=1000) + self.assertEqual(so.advance_payment_status, "Not Requested") + pr = make_payment_request( dt="Sales Order", dn=so.name, @@ -433,6 +435,9 @@ def test_multiple_payment_if_partially_paid_for_same_currency(self): self.assertEqual(pr.party_account_currency, pr.currency) # INR self.assertEqual(pr.status, "Requested") + so.load_from_db() + self.assertEqual(so.advance_payment_status, "Requested") + # to make partial payment pe = pr.create_payment_entry(submit=False) pe.paid_amount = 200 @@ -441,6 +446,9 @@ def test_multiple_payment_if_partially_paid_for_same_currency(self): self.assertEqual(pe.references[0].payment_request, pr.name) + so.load_from_db() + self.assertEqual(so.advance_payment_status, "Partially Paid") + pr.load_from_db() self.assertEqual(pr.status, "Partially Paid") self.assertEqual(pr.outstanding_amount, 800) @@ -451,11 +459,12 @@ def test_multiple_payment_if_partially_paid_for_same_currency(self): self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount self.assertEqual(pe.references[0].allocated_amount, 800) - self.assertEqual( - pe.references[0].outstanding_amount, 800 - ) # for orders it should be same as allocated amount + self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero self.assertEqual(pe.references[0].payment_request, pr.name) + so.load_from_db() + self.assertEqual(so.advance_payment_status, "Fully Paid") + pr.load_from_db() self.assertEqual(pr.status, "Paid") self.assertEqual(pr.outstanding_amount, 0) @@ -474,11 +483,11 @@ def test_multiple_payment_if_partially_paid_for_same_currency(self): ) def test_multiple_payment_if_partially_paid_for_multi_currency(self): - si = create_sales_invoice(currency="USD", conversion_rate=50, qty=1, rate=100) + pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100) pr = make_payment_request( - dt="Sales Invoice", - dn=si.name, + dt="Purchase Invoice", + dn=pi.name, mute_email=1, submit_doc=1, return_doc=1, @@ -489,7 +498,7 @@ def test_multiple_payment_if_partially_paid_for_multi_currency(self): self.assertEqual(pr.outstanding_amount, 5000) self.assertEqual(pr.currency, "USD") self.assertEqual(pr.party_account_currency, "INR") - self.assertEqual(pr.status, "Requested") + self.assertEqual(pr.status, "Initiated") # to make partial payment pe = pr.create_payment_entry(submit=False) @@ -521,8 +530,8 @@ def test_multiple_payment_if_partially_paid_for_multi_currency(self): frappe.exceptions.ValidationError, re.compile(r"Payment Request is already created"), make_payment_request, - dt="Sales Invoice", - dn=si.name, + dt="Purchase Invoice", + dn=pi.name, mute_email=1, submit_doc=1, return_doc=1, From 9fa760fc700944ef5817293bdf7931bdc6847d0d Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 20 Sep 2024 13:06:40 +0530 Subject: [PATCH 56/60] fix: formatting update --- .../accounts/doctype/payment_request/test_payment_request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index f88d9e3dc30d..0f64d64357e7 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -337,8 +337,8 @@ def test_payment_entry(self): gl_entries = frappe.db.sql( """select account, debit, credit, against_voucher - from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s - order by account asc""", + from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s + order by account asc""", pe.name, as_dict=1, ) From b83931989bae71ba77d125f7a0487ba0bad911ef Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 20 Sep 2024 16:20:46 +0530 Subject: [PATCH 57/60] fix: Use `flt` where doing subtraction --- .../doctype/payment_entry/payment_entry.py | 53 +++++++++++++------ .../payment_request/payment_request.py | 13 +++-- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 572d39db4b09..8a7115ebb805 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1758,12 +1758,13 @@ def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocat return # calculating outstanding amounts + precision = self.precision("paid_amount") total_positive_outstanding_including_order = 0 total_negative_outstanding = 0 - paid_amount -= sum(flt(d.amount, self.precision("paid_amount")) for d in self.deductions) + paid_amount -= sum(flt(d.amount, precision) for d in self.deductions) for ref in self.references: - reference_outstanding_amount = flt(ref.outstanding_amount, self.precision("paid_amount")) + reference_outstanding_amount = flt(ref.outstanding_amount, precision) abs_outstanding_amount = abs(reference_outstanding_amount) if reference_outstanding_amount > 0: @@ -1780,7 +1781,9 @@ def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocat self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee") ): if total_positive_outstanding_including_order > paid_amount: - remaining_outstanding = total_positive_outstanding_including_order - paid_amount + remaining_outstanding = flt( + total_positive_outstanding_including_order - paid_amount, precision + ) allocated_negative_outstanding = min(remaining_outstanding, total_negative_outstanding) allocated_positive_outstanding = paid_amount + allocated_negative_outstanding @@ -1804,7 +1807,7 @@ def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocat return else: - allocated_positive_outstanding = total_negative_outstanding - paid_amount + allocated_positive_outstanding = flt(total_negative_outstanding - paid_amount, precision) allocated_negative_outstanding = paid_amount + min( total_positive_outstanding_including_order, allocated_positive_outstanding ) @@ -1815,10 +1818,14 @@ def _allocation_to_unset_pr_row( ): if outstanding_amount > 0 and allocated_positive_outstanding >= 0: row.allocated_amount = min(allocated_positive_outstanding, outstanding_amount) - allocated_positive_outstanding -= flt(row.allocated_amount) + allocated_positive_outstanding = flt( + allocated_positive_outstanding - row.allocated_amount, precision + ) elif outstanding_amount < 0 and allocated_negative_outstanding: row.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1 - allocated_negative_outstanding -= abs(flt(row.allocated_amount)) + allocated_negative_outstanding = flt( + allocated_negative_outstanding - abs(row.allocated_amount), precision + ) return allocated_positive_outstanding, allocated_negative_outstanding # allocate amount based on `paid_amount` is changed or not @@ -1831,7 +1838,7 @@ def _allocation_to_unset_pr_row( allocated_negative_outstanding, ) - allocate_open_payment_requests_to_references(self.references) + allocate_open_payment_requests_to_references(self.references, self.precision("paid_amount")) else: payment_request_outstanding_amounts = ( @@ -1862,9 +1869,15 @@ def _allocation_to_unset_pr_row( # update amounts to track allocation allocated_amount = flt(ref.allocated_amount) - allocated_positive_outstanding -= allocated_amount - remaining_references_allocated_amounts[key] -= allocated_amount - payment_request_outstanding_amounts[ref.payment_request] -= allocated_amount + allocated_positive_outstanding = flt( + allocated_positive_outstanding - allocated_amount, precision + ) + remaining_references_allocated_amounts[key] = flt( + remaining_references_allocated_amounts[key] - allocated_amount, precision + ) + payment_request_outstanding_amounts[ref.payment_request] = flt( + payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision + ) elif reference_outstanding_amount < 0 and allocated_negative_outstanding: # allocate amount according to outstanding amounts @@ -1878,10 +1891,13 @@ def _allocation_to_unset_pr_row( # update amounts to track allocation allocated_amount = abs(flt(ref.allocated_amount)) - allocated_negative_outstanding -= allocated_amount + allocated_negative_outstanding = flt( + allocated_negative_outstanding - allocated_amount, precision + ) remaining_references_allocated_amounts[key] += allocated_amount # negative amount - payment_request_outstanding_amounts[ref.payment_request] -= allocated_amount - + payment_request_outstanding_amounts[ref.payment_request] = flt( + payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision + ) # Re allocate amount to those references which have no PR (Lower priority) for ref in self.references: if ref.payment_request: @@ -2865,7 +2881,7 @@ def get_payment_entry( # If PE is created from PR directly, then no need to find open PRs for the references if not created_from_payment_request: - allocate_open_payment_requests_to_references(pe.references) + allocate_open_payment_requests_to_references(pe.references, pe.precision("paid_amount")) return pe @@ -2916,7 +2932,7 @@ def get_open_payment_requests_for_references(references=None): return reference_payment_requests -def allocate_open_payment_requests_to_references(references=None): +def allocate_open_payment_requests_to_references(references=None, precision=None): """ Allocate unpaid Payment Requests to the references. \n --- @@ -2954,6 +2970,9 @@ def allocate_open_payment_requests_to_references(references=None): if not references_open_payment_requests: return + if not precision: + precision = references[0].precision("allocated_amount") + # to manage new rows row_number = 1 MOVE_TO_NEXT_ROW = 1 @@ -2993,7 +3012,7 @@ def allocate_open_payment_requests_to_references(references=None): # split the reference row to allocate the remaining amount del reference_payment_requests[payment_request] row.allocated_amount = pr_outstanding_amount - allocated_amount -= pr_outstanding_amount + allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision) # set the remaining amount to the next row while allocated_amount: @@ -3028,7 +3047,7 @@ def allocate_open_payment_requests_to_references(references=None): break else: - allocated_amount -= pr_outstanding_amount + allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision) del reference_payment_requests[payment_request] row_number += MOVE_TO_NEXT_ROW diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index dbdd77c39fbb..87cd23c25ba5 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -472,6 +472,7 @@ def _allocate_payment_request_to_pe_references(self, references): references[0].payment_request = self.name return + precision = references[0].precision("allocated_amount") outstanding_amount = self.outstanding_amount # to manage rows @@ -497,10 +498,10 @@ def _allocate_payment_request_to_pe_references(self, references): row.payment_request = self.name if row.allocated_amount <= outstanding_amount: - outstanding_amount -= row.allocated_amount + outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision) row_number += MOVE_TO_NEXT_ROW else: - remaining_allocated_amount = row.allocated_amount - outstanding_amount + remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision) row.allocated_amount = outstanding_amount outstanding_amount = 0 @@ -744,6 +745,8 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False): if not references: return + precision = references[0].precision("allocated_amount") + referenced_payment_requests = frappe.get_all( "Payment Request", filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]}, @@ -762,12 +765,12 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False): continue payment_request = referenced_payment_requests[ref.payment_request] + pr_outstanding = payment_request["outstanding_amount"] # update outstanding amount new_outstanding_amount = flt( - payment_request["outstanding_amount"] + ref.allocated_amount - if cancel - else payment_request["outstanding_amount"] - ref.allocated_amount + pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount, + precision, ) # to handle same payment request for the multiple allocations From b7cb361a717bc559ca6c6111670034deb0da063c Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 20 Sep 2024 16:23:13 +0530 Subject: [PATCH 58/60] test: PR test case with Payment Term for same currency --- .../payment_request/test_payment_request.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 0f64d64357e7..96a024253c6b 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -8,6 +8,7 @@ import frappe from frappe.tests.utils import FrappeTestCase +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -536,3 +537,50 @@ def test_multiple_payment_if_partially_paid_for_multi_currency(self): submit_doc=1, return_doc=1, ) + + def test_single_payment_with_payment_term_for_same_currency(self): + create_payment_terms_template() + + po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=20000) + po.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254 + po.save() + po.submit() + + self.assertEqual(po.advance_payment_status, "Not Initiated") + + pr = make_payment_request( + dt="Purchase Order", + dn=po.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + self.assertEqual(pr.grand_total, 20000) + self.assertEqual(pr.outstanding_amount, pr.grand_total) + self.assertEqual(pr.party_account_currency, pr.currency) # INR + self.assertEqual(pr.status, "Initiated") + + po.load_from_db() + self.assertEqual(po.advance_payment_status, "Initiated") + + pe = pr.create_payment_entry() + + self.assertEqual(len(pe.references), 2) + self.assertEqual(pe.paid_amount, 20000) + + # check 1st payment term + self.assertEqual(pe.references[0].allocated_amount, 16949.2) + self.assertEqual(pe.references[0].payment_request, pr.name) + + # check 2nd payment term + self.assertEqual(pe.references[1].allocated_amount, 3050.8) + self.assertEqual(pe.references[1].payment_request, pr.name) + + po.load_from_db() + self.assertEqual(po.advance_payment_status, "Fully Paid") + + pr.load_from_db() + self.assertEqual(pr.status, "Paid") + self.assertEqual(pr.outstanding_amount, 0) + self.assertEqual(pr.grand_total, 20000) From 943e1b7b988bb10653e64bbf5864e1cf365884ef Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 20 Sep 2024 18:07:59 +0530 Subject: [PATCH 59/60] fix: remove redundant `flt` --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 8a7115ebb805..d688b1fa3e07 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1764,7 +1764,7 @@ def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocat paid_amount -= sum(flt(d.amount, precision) for d in self.deductions) for ref in self.references: - reference_outstanding_amount = flt(ref.outstanding_amount, precision) + reference_outstanding_amount = ref.outstanding_amount abs_outstanding_amount = abs(reference_outstanding_amount) if reference_outstanding_amount > 0: @@ -1868,7 +1868,7 @@ def _allocation_to_unset_pr_row( ref.allocated_amount = min(outstanding_amounts) # update amounts to track allocation - allocated_amount = flt(ref.allocated_amount) + allocated_amount = ref.allocated_amount allocated_positive_outstanding = flt( allocated_positive_outstanding - allocated_amount, precision ) @@ -1890,7 +1890,7 @@ def _allocation_to_unset_pr_row( ref.allocated_amount = min(outstanding_amounts) * -1 # update amounts to track allocation - allocated_amount = abs(flt(ref.allocated_amount)) + allocated_amount = abs(ref.allocated_amount) allocated_negative_outstanding = flt( allocated_negative_outstanding - allocated_amount, precision ) From f1d89460000e43451419b0cca7a3bb8f6b786a60 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 20 Sep 2024 18:20:02 +0530 Subject: [PATCH 60/60] test: Add test cases for PR --- .../payment_request/test_payment_request.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 96a024253c6b..0b2cdef8b541 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -584,3 +584,89 @@ def test_single_payment_with_payment_term_for_same_currency(self): self.assertEqual(pr.status, "Paid") self.assertEqual(pr.outstanding_amount, 0) self.assertEqual(pr.grand_total, 20000) + + def test_single_payment_with_payment_term_for_multi_currency(self): + create_payment_terms_template() + + si = create_sales_invoice(do_not_save=1, currency="USD", qty=1, rate=200, conversion_rate=50) + si.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254 + si.save() + si.submit() + + pr = make_payment_request( + dt="Sales Invoice", + dn=si.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + # 200 USD -> 10000 INR + self.assertEqual(pr.grand_total, 200) + self.assertEqual(pr.outstanding_amount, 10000) + self.assertEqual(pr.currency, "USD") + self.assertEqual(pr.party_account_currency, "INR") + self.assertEqual(pr.status, "Requested") + + pe = pr.create_payment_entry() + self.assertEqual(len(pe.references), 2) + self.assertEqual(pe.paid_amount, 10000) + + # check 1st payment term + # convert it via dollar and conversion_rate + self.assertEqual(pe.references[0].allocated_amount, 8474.5) # multi currency conversion + self.assertEqual(pe.references[0].payment_request, pr.name) + + # check 2nd payment term + self.assertEqual(pe.references[1].allocated_amount, 1525.5) # multi currency conversion + self.assertEqual(pe.references[1].payment_request, pr.name) + + pr.load_from_db() + self.assertEqual(pr.status, "Paid") + self.assertEqual(pr.outstanding_amount, 0) + self.assertEqual(pr.grand_total, 200) + + def test_payment_cancel_process(self): + so = make_sales_order(currency="INR", qty=1, rate=1000) + self.assertEqual(so.advance_payment_status, "Not Requested") + + pr = make_payment_request( + dt="Sales Order", + dn=so.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + self.assertEqual(pr.status, "Requested") + self.assertEqual(pr.grand_total, 1000) + self.assertEqual(pr.outstanding_amount, pr.grand_total) + + so.load_from_db() + self.assertEqual(so.advance_payment_status, "Requested") + + pe = pr.create_payment_entry(submit=False) + pe.paid_amount = 800 + pe.references[0].allocated_amount = 800 + pe.submit() + + self.assertEqual(pe.references[0].payment_request, pr.name) + + so.load_from_db() + self.assertEqual(so.advance_payment_status, "Partially Paid") + + pr.load_from_db() + self.assertEqual(pr.status, "Partially Paid") + self.assertEqual(pr.outstanding_amount, 200) + self.assertEqual(pr.grand_total, 1000) + + # cancelling PE + pe.cancel() + + pr.load_from_db() + self.assertEqual(pr.status, "Requested") + self.assertEqual(pr.outstanding_amount, 1000) + self.assertEqual(pr.grand_total, 1000) + + so.load_from_db() + self.assertEqual(so.advance_payment_status, "Requested")