From ef52be2f1771081bc326818acbefb85ea88dbb0a Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com> Date: Sat, 21 Sep 2024 07:04:46 +0530 Subject: [PATCH 01/14] fix: multiple issues in Payment Request (#42427) * fix: multiple issues in Payment Request * chore: minor changes * fix: remove bug * fix: replace `round` with `flt` * fix: update `set_advance_payment_status()` logic * fix: removed bug of `set_advance_payment_status` * fix: changes as per review * refactor: replace sql query of `matched_payment_requests` to query builder * fix: replace `locals` with `get_doc` in set_query * fix: changes during review * fix: minor review changes * fix: remove unnecessary code for setting payment entry received amount * fix: logic for ser payment_request if PE made from transaction * fix: Use rounded total to make Payment Request from `Sales Invoice` or `Purchase Invoice` * refactor: enhance logic of `set_open_payment_requests_to_references` * fix: added one optional arg `created_from_payment_request` * fix: handle multiple allocation of PR at PE's reference * fix: logic for PR if outstanding docs fetch * fix: formatted Link field for `Payment Request` for PE's references * fix: replace `get_all()` with `get_list()` for getting Payment Request for Link field * fix: replace `get_all()` with `get_list()` for getting Payment Request for Link field * chore: format `payment_entry.js` file * style: Show preview popup of `Payment Request` * fix: remove minor bug * fix: add virtual field for Payment Term and Request `outstanding_amount` in PE's reference * fix: get outstanding amount in PE's reference on realtime * fix: move allocation of allocated_amount to server side (no change) * fix: some minor changes to allocation * fix: Split `Payment Request` if PE is created from PR and there are `Payment Terms` * fix: minor logic changes * fix: Allocation of allocated_amount if `paid_amount` is changes * fix: improve logic of allocation * fix: set matched payment request if unset * fix: minor changes * fix: Allocate single Payment Request if PE created from PR * fix: improve code logic * fix: Removed duplication code * fix: proper message title * refactor: Rename method of Allocation Amount to References * refactor: Changing `grand_total` description based on `party_type` * refactor: update Payment Request * fix: Remove virtual property of payment_term_oustanding from references * fix: fetch party account currency for creating payment request * fix: use transaction currency as base in payment request * fix: party amount for creating payment entry * fix: allow for proportional amount paid by bank * fix: Changed field order in Payment Request * fix: Minor refactor in Payment Entry Reference table data * test: Added test cases for allow Payment at `Partially Paid` status for PR * test: Update partial paid status test case * test: Update test case for same currency PR * refactor: Wider the `msgprint` dialog for after save PE * test: Update PR test cases * chore: Remove dirty lines * test: Checking `Advance Payment Status` * fix: formatting update * fix: Use `flt` where doing subtraction * test: PR test case with Payment Term for same currency * fix: remove redundant `flt` * test: Add test cases for PR --------- Co-authored-by: Sagar Vora (cherry picked from commit ea69ba7cd8967af626d8efee1c4575d980533923) # Conflicts: # erpnext/accounts/doctype/payment_entry/payment_entry.js # erpnext/accounts/doctype/payment_entry/payment_entry.py # erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json # erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py # erpnext/accounts/doctype/payment_request/payment_request.json # erpnext/accounts/doctype/payment_request/payment_request.py --- .../doctype/payment_entry/payment_entry.js | 113 ++++ .../doctype/payment_entry/payment_entry.py | 603 +++++++++++++++++- .../payment_entry_reference.json | 57 ++ .../payment_entry_reference.py | 40 +- .../payment_request/payment_request.js | 4 +- .../payment_request/payment_request.json | 61 +- .../payment_request/payment_request.py | 423 +++++++++--- .../payment_request/test_payment_request.py | 255 ++++++++ erpnext/controllers/accounts_controller.py | 33 +- 9 files changed, 1491 insertions(+), 98 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 2f9d5be07e7..b2eb8d27c6f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -154,6 +154,17 @@ 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", + filters: { + reference_doctype: row.reference_doctype, + reference_name: row.reference_name, + }, + }; + }); + frm.set_query("sales_taxes_and_charges_template", function () { return { filters: { @@ -171,7 +182,15 @@ frappe.ui.form.on('Payment Entry', { }, }; }); + + frm.add_fetch( + "payment_request", + "outstanding_amount", + "payment_request_outstanding", + "Payment Entry Reference" + ); }, + refresh: function (frm) { erpnext.hide_company(frm); frm.events.hide_unhide_fields(frm); @@ -184,6 +203,7 @@ frappe.ui.form.on('Payment Entry', { } erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm); + frappe.flags.allocate_payment_amount = true; }, validate_company: (frm) => { @@ -633,10 +653,16 @@ frappe.ui.form.on('Payment Entry', { frm.set_value("base_received_amount", flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)); +<<<<<<< HEAD if(frm.doc.payment_type == "Pay") frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1); else frm.events.set_unallocated_amount(frm); +======= + if (frm.doc.payment_type == "Pay") + frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true); + else frm.events.set_unallocated_amount(frm); +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) frm.set_paid_amount_based_on_received_amount = false; frm.events.hide_unhide_fields(frm); @@ -654,10 +680,16 @@ frappe.ui.form.on('Payment Entry', { frm.set_value("base_received_amount", frm.doc.base_paid_amount); } +<<<<<<< HEAD if(frm.doc.payment_type == "Receive") frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1); else frm.events.set_unallocated_amount(frm); +======= + if (frm.doc.payment_type == "Receive") + frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true); + else frm.events.set_unallocated_amount(frm); +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) }, get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) { @@ -801,6 +833,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; if(!in_list(frm.events.get_order_doctypes(frm), d.voucher_type)) { @@ -842,10 +875,19 @@ frappe.ui.form.on('Payment Entry', { } } +<<<<<<< HEAD frm.events.allocate_party_amount_against_ref_docs(frm, (frm.doc.payment_type=="Receive" ? frm.doc.paid_amount : frm.doc.received_amount)); } +======= + frm.events.allocate_party_amount_against_ref_docs( + frm, + frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount, + false + ); + }, +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) }); }, @@ -857,6 +899,7 @@ frappe.ui.form.on('Payment Entry', { return ["Sales Invoice", "Purchase Invoice"]; }, +<<<<<<< HEAD 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; @@ -927,6 +970,15 @@ frappe.ui.form.on('Payment Entry', { }) frm.refresh_fields() +======= + allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { + 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, + }); + +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) frm.events.set_total_allocated_amount(frm); }, @@ -1409,6 +1461,62 @@ 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); + }, + ]); + } + }, + }); + } + }, + + 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: __("Unset Matched Payment Request"), + message: COLUMN_LABEL.concat(matched_payment_requests), + as_table: true, + wide: true, + primary_action: { + label: __("Allocate Payment Request"), + action() { + frappe.hide_msgprint(); + frm.call("set_matched_payment_requests", { matched_payment_requests }, () => { + frm.dirty(); + }); + }, + }, + }); + }, }); @@ -1495,6 +1603,7 @@ frappe.ui.form.on('Payment Entry Deduction', { deductions_remove: function(frm) { frm.events.set_unallocated_amount(frm); +<<<<<<< HEAD } }) frappe.ui.form.on('Payment Entry', { @@ -1527,3 +1636,7 @@ frappe.ui.form.on('Payment Entry', { } }, }) +======= + }, +}); +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 6d9565fb21c..caff50d249a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -7,9 +7,17 @@ from functools import reduce 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 +<<<<<<< HEAD from frappe.utils.data import comma_and, fmt_money from pypika.functions import Sum +======= +from frappe.utils.data import comma_and, fmt_money, get_link_to_form +from pypika import Case +from pypika.functions import Coalesce, Sum +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions @@ -91,13 +99,17 @@ class PaymentEntry(AccountsController): self.set_tax_withholding() self.set_status() + def before_save(self): + self.set_matched_unset_payment_requests_to_response() + def on_submit(self): if self.difference_amount: 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_advance_paid() # advance_paid_status depends on the payment request amount self.set_status() def on_cancel(self): @@ -115,30 +127,34 @@ class PaymentEntry(AccountsController): 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.set_payment_req_status() + self.update_payment_requests(cancel=True) + self.update_advance_paid() # advance_paid_status depends on the payment request amount self.set_status() - def set_payment_req_status(self): - from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status + def update_payment_requests(self, cancel=False): + from erpnext.accounts.doctype.payment_request.payment_request import ( + update_payment_requests_as_per_pe_references, + ) - update_payment_req_status(self, None) + update_payment_requests_as_per_pe_references(self.references, cancel=cancel) 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"): - 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: @@ -164,6 +180,8 @@ class PaymentEntry(AccountsController): if self.payment_type == "Internal Transfer": return + self.validate_allocated_amount_as_per_payment_request() + if self.party_type in ("Customer", "Supplier"): self.validate_allocated_amount_with_latest_data() else: @@ -176,6 +194,27 @@ class PaymentEntry(AccountsController): if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount): frappe.throw(fail_message.format(d.idx)) + def validate_allocated_amount_as_per_payment_request(self): + """ + Allocated amount should not be greater than the outstanding amount of the Payment Request. + """ + if not self.references: + return + + 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 > pr_outstanding_amounts[ref.payment_request]: + frappe.throw( + 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( self, reference_doctype: str, reference_name: str ) -> bool: @@ -1422,6 +1461,380 @@ class PaymentEntry(AccountsController): 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 + + matched_payment_requests = get_matched_payment_request_of_references( + [row for row in self.references if not row.payment_request] + ) + + if not matched_payment_requests: + return + + frappe.response["matched_payment_requests"] = matched_payment_requests + + @frappe.whitelist() + 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. (Payment Request is also dependent on this flag) + """ + if not self.references: + return + + if not allocate_payment_amount: + for ref in self.references: + ref.allocated_amount = 0 + 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, precision) for d in self.deductions) + + for ref in self.references: + reference_outstanding_amount = ref.outstanding_amount + abs_outstanding_amount = abs(reference_outstanding_amount) + + 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") + ): + if 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 + + 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 = flt(total_negative_outstanding - paid_amount, precision) + allocated_negative_outstanding = paid_amount + min( + total_positive_outstanding_including_order, allocated_positive_outstanding + ) + + # 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( + 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 = 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 + 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, + ) + + allocate_open_payment_requests_to_references(self.references, self.precision("paid_amount")) + + else: + payment_request_outstanding_amounts = ( + get_payment_request_outstanding_set_in_references(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) + for ref in self.references: + if not ref.payment_request: + continue + + # 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")) + 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) + + # update amounts to track allocation + allocated_amount = ref.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 + outstanding_amounts = ( + allocated_negative_outstanding, + abs(reference_outstanding_amount), + pr_outstanding_amount, + ) + + ref.allocated_amount = min(outstanding_amounts) * -1 + + # update amounts to track allocation + allocated_amount = abs(ref.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] = 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: + continue + + key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term")) + reference_outstanding_amount = remaining_references_allocated_amounts[key] + + allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row( + ref, + reference_outstanding_amount, + allocated_positive_outstanding, + allocated_negative_outstanding, + ) + + @frappe.whitelist() + def set_matched_payment_requests(self, matched_payment_requests): + """ + 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: + 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] # 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 + + # to fetch matched rows + 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 + + PR = frappe.qb.DocType("Payment Request") + + # query to group by reference_doctype, reference_name, outstanding_amount + subquery = ( + frappe.qb.from_(PR) + .select( + 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") + .where(PR.docstatus == 1) + .groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount) + ) + + # query to fetch matched rows which are single + matched_prs = ( + frappe.qb.from_(subquery) + .select( + subquery.reference_doctype, + subquery.reference_name, + subquery.allocated_amount, + subquery.payment_request, + ) + .where(subquery.count == 1) + .run() + ) + + return matched_prs if matched_prs else None + + +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. + + Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...} + """ + if not references: + return + + 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_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 + + 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 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 + + 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_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 + + referenced_payment_requests = {row.payment_request for row in references if row.payment_request} + + 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(referenced_payment_requests)) + ).run() + + return dict(response) if response else None + def validate_inclusive_tax(tax, doc): def _on_previous_row_error(row_range): @@ -2010,6 +2423,8 @@ def get_payment_entry( party_type=None, 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") @@ -2160,9 +2575,179 @@ 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, pe.precision("paid_amount")) + return pe +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 + + 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 + + +def allocate_open_payment_requests_to_references(references=None, precision=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 + references_open_payment_requests = get_open_payment_requests_for_references(references) + + 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 + TO_SKIP_NEW_ROW = 2 + + 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 + + # unpaid payment requests for the reference + 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, 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 pr_outstanding_amount == allocated_amount: + del reference_payment_requests[payment_request] + row_number += MOVE_TO_NEXT_ROW + + 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 + + else: + # split the reference row to allocate the remaining amount + del reference_payment_requests[payment_request] + row.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: + # create a new row for the remaining amount + new_row = frappe.copy_doc(row) + references.insert(row_number, new_row) + + # get the first payment request and its outstanding amount + payment_request, pr_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( + pr_outstanding_amount if pr_outstanding_amount else allocated_amount, allocated_amount + ) + + if not payment_request or not pr_outstanding_amount: + row_number += TO_SKIP_NEW_ROW + break + + elif pr_outstanding_amount == allocated_amount: + del reference_payment_requests[payment_request] + row_number += TO_SKIP_NEW_ROW + break + + elif pr_outstanding_amount > allocated_amount: + reference_payment_requests[payment_request] -= allocated_amount + row_number += TO_SKIP_NEW_ROW + break + + else: + allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision) + del reference_payment_requests[payment_request] + row_number += MOVE_TO_NEXT_ROW + + 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_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 3003c68196e..1a64f5b49a7 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -10,12 +10,25 @@ "due_date", "bill_no", "payment_term", +<<<<<<< HEAD +======= + "payment_term_outstanding", + "account_type", + "payment_type", +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) "column_break_4", "total_amount", "outstanding_amount", "allocated_amount", "exchange_rate", +<<<<<<< HEAD "exchange_gain_loss" +======= + "exchange_gain_loss", + "account", + "payment_request", + "payment_request_outstanding" +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) ], "fields": [ { @@ -101,12 +114,56 @@ "label": "Exchange Gain/Loss", "options": "Company:company:default_currency", "read_only": 1 +<<<<<<< HEAD +======= + }, + { + "fieldname": "account", + "fieldtype": "Link", + "label": "Account", + "options": "Account" + }, + { + "fieldname": "account_type", + "fieldtype": "Data", + "label": "Account Type" + }, + { + "fieldname": "payment_type", + "fieldtype": "Data", + "label": "Payment Type" + }, + { + "fieldname": "payment_request", + "fieldtype": "Link", + "label": "Payment Request", + "options": "Payment Request" + }, + { + "depends_on": "eval: doc.payment_term", + "fieldname": "payment_term_outstanding", + "fieldtype": "Float", + "label": "Payment Term Outstanding", + "read_only": 1 + }, + { + "depends_on": "eval: doc.payment_request && doc.payment_request_outstanding", + "fieldname": "payment_request_outstanding", + "fieldtype": "Float", + "is_virtual": 1, + "label": "Payment Request Outstanding", + "read_only": 1 +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2022-12-12 12:31:44.919895", +======= + "modified": "2024-09-16 18:11:50.019343", +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) "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 fc1cad9ad67..b036898643c 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -1,9 +1,47 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - +import frappe from frappe.model.document import Document class PaymentEntryReference(Document): +<<<<<<< HEAD pass +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + account: DF.Link | None + account_type: DF.Data | None + allocated_amount: DF.Float + bill_no: DF.Data | None + due_date: DF.Date | None + exchange_gain_loss: DF.Currency + exchange_rate: DF.Float + outstanding_amount: DF.Float + parent: DF.Data + 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 + total_amount: DF.Float + # end: auto-generated types + + @property + def payment_request_outstanding(self): + if not self.payment_request: + return + + return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount") +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js index e45aa512fe8..50f96a4e2b6 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.js +++ b/erpnext/accounts/doctype/payment_request/payment_request.js @@ -48,8 +48,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 381f3fb531a..af1524de114 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -18,9 +18,11 @@ "reference_name", "transaction_details", "grand_total", + "currency", "is_a_subscription", "column_break_18", - "currency", + "outstanding_amount", + "party_account_currency", "subscription_section", "subscription_plans", "bank_account_details", @@ -68,6 +70,7 @@ { "fieldname": "transaction_date", "fieldtype": "Date", + "in_preview": 1, "label": "Transaction Date" }, { @@ -132,7 +135,8 @@ "no_copy": 1, "options": "reference_doctype", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "transaction_details", @@ -140,11 +144,18 @@ "label": "Transaction Details" }, { - "description": "Amount in customer's currency", + "description": "Amount in transaction currency", "fieldname": "grand_total", "fieldtype": "Currency", + "in_preview": 1, "label": "Amount", +<<<<<<< HEAD "options": "currency" +======= + "non_negative": 1, + "options": "currency", + "reqd": 1 +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) }, { "default": "0", @@ -360,6 +371,7 @@ "read_only": 1 }, { +<<<<<<< HEAD "fetch_from": "payment_gateway_account.payment_channel", "fieldname": "payment_channel", "fieldtype": "Select", @@ -388,13 +400,55 @@ "fieldtype": "Link", "label": "Project", "options": "Project" +======= + "fieldname": "failed_reason", + "fieldtype": "Data", + "hidden": 1, + "label": "Reason for Failure", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval: doc.docstatus === 1", + "description": "Amount in party's bank account currency", + "fieldname": "outstanding_amount", + "fieldtype": "Currency", + "in_preview": 1, + "label": "Outstanding Amount", + "non_negative": 1, + "options": "party_account_currency", + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "read_only": 1 + }, + { + "fieldname": "column_break_pnyv", + "fieldtype": "Column Break" + }, + { + "fieldname": "party_account_currency", + "fieldtype": "Link", + "label": "Party Account Currency", + "options": "Currency", + "read_only": 1 +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2022-12-21 16:56:40.115737", +======= + "modified": "2024-09-16 17:50:54.440090", +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", @@ -429,6 +483,7 @@ "write": 1 } ], + "show_preview_popup": 1, "sort_field": "modified", "sort_order": "DESC", "states": [] diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 78567c37cb5..8c6c8918c76 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -7,9 +7,15 @@ import json import frappe from frappe import _ from frappe.model.document import Document +<<<<<<< HEAD from frappe.utils import flt, get_url, nowdate +======= +from frappe.query_builder.functions import Sum +from frappe.utils import flt, nowdate +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) 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, ) @@ -32,6 +38,72 @@ def _get_payment_gateway_controller(*args, **kwargs): class PaymentRequest(Document): +<<<<<<< HEAD +======= + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + 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 + 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 + failed_reason: DF.Data | None + grand_total: DF.Currency + iban: DF.ReadOnly | None + is_a_subscription: DF.Check + make_sales_invoice: DF.Check + message: DF.Text | None + 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_account_currency: DF.Link | None + party_type: DF.Link | None + payment_account: DF.ReadOnly | None + 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[None] + 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", + ] + subject: DF.Data | None + subscription_plans: DF.Table[SubscriptionPlanDetail] + swift_number: DF.ReadOnly | None + transaction_date: DF.Date | None + # end: auto-generated types + +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) def validate(self): if self.get("__islocal"): self.status = "Draft" @@ -45,6 +117,12 @@ class PaymentRequest(Document): frappe.throw(_("To create a Payment Request reference document is required")) def validate_payment_request_amount(self): + if self.grand_total == 0: + 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) ) @@ -92,28 +170,44 @@ class PaymentRequest(Document): ).format(self.grand_total, amount) ) - def on_submit(self): - if self.payment_request_type == "Outward": - self.db_set("status", "Initiated") - return - elif self.payment_request_type == "Inward": - self.db_set("status", "Requested") - - send_mail = self.payment_gateway_validation() if self.payment_gateway else None - ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) - + def before_submit(self): if ( - hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart" - ) or self.flags.mute_email: - send_mail = False + 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, + ["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 / grand_total * base_grand_total, + self.precision("outstanding_amount"), + ) - if send_mail and self.payment_channel != "Phone": - self.set_payment_request_url() - self.send_email() - self.make_communication_entry() + else: + self.outstanding_amount = self.grand_total - elif self.payment_channel == "Phone": - self.request_phone_payment() + if self.payment_request_type == "Outward": + self.status = "Initiated" + elif self.payment_request_type == "Inward": + self.status = "Requested" + + if self.payment_request_type == "Inward": + if self.payment_channel == "Phone": + self.request_phone_payment() + else: + self.set_payment_request_url() + if not (self.mute_email or self.flags.mute_email): + 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) @@ -152,6 +246,7 @@ class PaymentRequest(Document): def on_cancel(self): self.check_if_payment_entry_exists() self.set_as_cancelled() + self.update_reference_advance_payment_status() def make_invoice(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) @@ -220,7 +315,7 @@ class PaymentRequest(Document): def set_as_paid(self): if self.payment_channel == "Phone": - self.db_set("status", "Paid") + self.db_set({"status": "Paid", "outstanding_amount": 0}) else: payment_entry = self.create_payment_entry() @@ -241,26 +336,32 @@ class PaymentRequest(Document): 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) + party_account_currency = ( + self.get("party_account_currency") + or ref_doc.get("party_account_currency") + or get_account_currency(party_account) + ) + + party_amount = bank_amount = self.outstanding_amount - bank_amount = self.grand_total 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") - 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( self.reference_doctype, self.reference_name, party_amount=party_amount, bank_account=self.payment_account, bank_amount=bank_amount, + created_from_payment_request=True, ) payment_entry.update( { "mode_of_payment": self.mode_of_payment, - "reference_no": self.name, + "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 @@ -268,6 +369,9 @@ class PaymentRequest(Document): } ) + # 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( { @@ -276,14 +380,6 @@ class PaymentRequest(Document): } ) - 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 ( @@ -397,6 +493,70 @@ class PaymentRequest(Document): if payment_provider == "stripe": return create_stripe_subscription(gateway_controller, data) + def update_reference_advance_payment_status(self): + 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: + 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): + """ + 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 + + precision = references[0].precision("allocated_amount") + 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 + + row_number += MOVE_TO_NEXT_ROW + continue + + # allocate the payment request to the row + row.payment_request = self.name + + if row.allocated_amount <= outstanding_amount: + outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision) + row_number += MOVE_TO_NEXT_ROW + else: + remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision) + 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): @@ -427,11 +587,15 @@ def make_payment_request(**args): {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0}, ) - existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn) + # fetches existing payment request `grand_total` amount + existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name) 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 @@ -445,6 +609,13 @@ def make_payment_request(**args): "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"), @@ -453,6 +624,7 @@ def make_payment_request(**args): "payment_channel": gateway_account.get("payment_channel"), "payment_request_type": args.get("payment_request_type"), "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, @@ -460,7 +632,12 @@ def make_payment_request(**args): "message": gateway_account.get("message") or get_dummy_message(ref_doc), "reference_doctype": args.dt, "reference_name": args.dn, +<<<<<<< HEAD "party_type": args.get("party_type") or "Customer", +======= + "company": ref_doc.get("company"), + "party_type": party_type, +>>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) "party": args.get("party") or ref_doc.get("customer"), "bank_account": bank_account, } @@ -503,9 +680,11 @@ def get_amount(ref_doc, payment_account=None): 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.grand_total) + grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total) else: - grand_total = 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: if pay.type == "Phone" and pay.account == payment_account: @@ -527,24 +706,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. + Return the total amount of Payment Requests against a reference 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)) + .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] if response[0] else 0 def get_gateway_details(args): # nosemgrep @@ -592,41 +767,66 @@ 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_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 - 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, - }, + 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}]}, + fields=[ + "name", + "grand_total", + "outstanding_amount", + "payment_request_type", + ], + ) + + referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests} + + for ref in references: + if not ref.payment_request: + continue + + payment_request = referenced_payment_requests[ref.payment_request] + pr_outstanding = payment_request["outstanding_amount"] + + # update outstanding amount + new_outstanding_amount = flt( + pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount, + precision, ) - if payment_request_name: - ref_details = get_reference_details( - ref.reference_doctype, - ref.reference_name, - doc.party_account_currency, - doc.party_type, - doc.party, + # 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=_( + "The allocated amount is greater than the outstanding amount of Payment Request {0}" + ).format(ref.payment_request), + title=_("Invalid Allocated Amount"), ) - 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" + # update status + if new_outstanding_amount == payment_request["grand_total"]: + status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested" + elif new_outstanding_amount == 0: + status = "Paid" + elif new_outstanding_amount > 0: + status = "Partially Paid" - pay_req_doc.db_set("status", status) + # update database + frappe.db.set_value( + "Payment Request", + ref.payment_request, + {"outstanding_amount": new_outstanding_amount, "status": status}, + ) def get_dummy_message(doc): @@ -710,3 +910,62 @@ def validate_payment(doc, method=None): doc.reference_docname ) ) + + +def get_paid_amount_against_order(dt, dn): + pe_ref = frappe.qb.DocType("Payment Entry Reference") + if dt == "Sales Order": + inv_dt, inv_field = "Sales Invoice Item", "sales_order" + else: + inv_dt, inv_field = "Purchase Invoice Item", "purchase_order" + inv_item = frappe.qb.DocType(inv_dt) + return ( + frappe.qb.from_(pe_ref) + .select( + Sum(pe_ref.allocated_amount), + ) + .where( + (pe_ref.docstatus == 1) + & ( + (pe_ref.reference_name == dn) + | pe_ref.reference_name.isin( + frappe.qb.from_(inv_item) + .select(inv_item.parent) + .where(inv_item[inv_field] == dn) + .distinct() + ) + ) + ) + ).run()[0][0] or 0 + + +@frappe.whitelist() +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") + + if not reference_doctype or not reference_name: + return [] + + open_payment_requests = frappe.get_list( + "Payment Request", + filters={ + "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"], + 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 + ] diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 6d15f84d7cf..4caffdb431a 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -1,11 +1,13 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +import re import unittest 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 @@ -278,3 +280,256 @@ class TestPaymentRequest(FrappeTestCase): 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_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, + mute_email=1, + submit_doc=1, + 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") + + 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 + pe.references[0].allocated_amount = 200 + 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, 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) # 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) + 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): + pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100) + + pr = make_payment_request( + dt="Purchase Invoice", + dn=pi.name, + mute_email=1, + submit_doc=1, + return_doc=1, + ) + + # 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, "Initiated") + + # to make partial payment + pe = pr.create_payment_entry(submit=False) + pe.paid_amount = 2000 + pe.references[0].allocated_amount = 2000 + pe.submit() + + 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) + + # 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="Purchase Invoice", + dn=pi.name, + mute_email=1, + 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) + + 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") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 54984f17d0c..ff41b3038cc 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1844,7 +1844,38 @@ class AccountsController(TransactionBase): ).format(formatted_advance_paid, self.name, formatted_order_total) ) - frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid) + self.db_set("advance_paid", advance_paid) + + self.set_advance_payment_status() + + def set_advance_payment_status(self): + new_status = None + + 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)", + ) + + 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 + + self.db_set("advance_payment_status", new_status, update_modified=False) + self.set_status(update=True) + self.notify_update() @property def company_abbr(self): From 36b4f68566082e5c41b5b0cbbdf78e11fd6e6b72 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Sat, 28 Sep 2024 12:09:33 +0530 Subject: [PATCH 02/14] fix: Remove `advance_payment_status` uses (cherry picked from commit 907e3af1b0f5a08617f637573f3abc3e3eb56253) --- .../payment_request/payment_request.py | 11 ------- .../payment_request/test_payment_request.py | 13 -------- erpnext/controllers/accounts_controller.py | 31 ------------------- erpnext/hooks.py | 1 - 4 files changed, 56 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 8c6c8918c76..f1a88d0adc4 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -206,9 +206,6 @@ class PaymentRequest(Document): 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() @@ -493,14 +490,6 @@ class PaymentRequest(Document): if payment_provider == "stripe": return create_stripe_subscription(gateway_controller, data) - def update_reference_advance_payment_status(self): - 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: - 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): """ Allocate the Payment Request to the Payment Entry references based on\n diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 4caffdb431a..053863babdc 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -284,8 +284,6 @@ class TestPaymentRequest(FrappeTestCase): 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, @@ -300,7 +298,6 @@ class TestPaymentRequest(FrappeTestCase): 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) @@ -311,7 +308,6 @@ class TestPaymentRequest(FrappeTestCase): 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") @@ -327,7 +323,6 @@ class TestPaymentRequest(FrappeTestCase): 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") @@ -409,8 +404,6 @@ class TestPaymentRequest(FrappeTestCase): po.save() po.submit() - self.assertEqual(po.advance_payment_status, "Not Initiated") - pr = make_payment_request( dt="Purchase Order", dn=po.name, @@ -425,7 +418,6 @@ class TestPaymentRequest(FrappeTestCase): self.assertEqual(pr.status, "Initiated") po.load_from_db() - self.assertEqual(po.advance_payment_status, "Initiated") pe = pr.create_payment_entry() @@ -441,7 +433,6 @@ class TestPaymentRequest(FrappeTestCase): 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") @@ -491,7 +482,6 @@ class TestPaymentRequest(FrappeTestCase): 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", @@ -506,7 +496,6 @@ class TestPaymentRequest(FrappeTestCase): 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 @@ -516,7 +505,6 @@ class TestPaymentRequest(FrappeTestCase): 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") @@ -532,4 +520,3 @@ class TestPaymentRequest(FrappeTestCase): self.assertEqual(pr.grand_total, 1000) so.load_from_db() - self.assertEqual(so.advance_payment_status, "Requested") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ff41b3038cc..da222d5f58e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1846,37 +1846,6 @@ class AccountsController(TransactionBase): self.db_set("advance_paid", advance_paid) - self.set_advance_payment_status() - - def set_advance_payment_status(self): - new_status = None - - 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)", - ) - - 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 - - self.db_set("advance_payment_status", new_status, update_modified=False) - self.set_status(update=True) - self.notify_update() - @property def company_abbr(self): if not hasattr(self, "_abbr"): diff --git a/erpnext/hooks.py b/erpnext/hooks.py index ae6a15bfd8d..3fde454e23e 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -354,7 +354,6 @@ doc_events = { "Payment Entry": { "on_submit": [ "erpnext.regional.create_transaction_log", - "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning", ], "on_trash": "erpnext.regional.check_deletion_permission", From d4421dade1d89a4b2ead8b2c673e06b2ce05e242 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Sat, 28 Sep 2024 12:29:34 +0530 Subject: [PATCH 03/14] fix: Remove unreference method (cherry picked from commit 770bc1c293b106c1bb3ec5077ce73c068973d179) --- erpnext/accounts/doctype/payment_request/payment_request.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index f1a88d0adc4..e4540de11bf 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -243,7 +243,6 @@ class PaymentRequest(Document): def on_cancel(self): self.check_if_payment_entry_exists() self.set_as_cancelled() - self.update_reference_advance_payment_status() def make_invoice(self): ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) From 8f5c19113661bab89babdb95039964408a0c650e Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 30 Sep 2024 17:20:42 +0530 Subject: [PATCH 04/14] fix: Add removed test code `https://github.com/frappe/erpnext/commit/b41f10c1b98b01a181a6f9dbdf2531b108dc3bae` (cherry picked from commit 30fd11f138ce468f5c01f3c5a265eca071293b30) --- .../payment_request/test_payment_request.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 053863babdc..6644254ae7e 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -3,6 +3,7 @@ import re import unittest +from unittest.mock import patch import frappe from frappe.tests.utils import FrappeTestCase @@ -17,6 +18,8 @@ from erpnext.setup.utils import get_exchange_rate test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"] +PAYMENT_URL = "https://example.com/payment" + payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"} payment_method = [ @@ -49,6 +52,28 @@ class TestPaymentRequest(FrappeTestCase): ): frappe.get_doc(method).insert(ignore_permissions=True) + send_email = patch( + "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email", + return_value=None, + ) + self.send_email = send_email.start() + self.addCleanup(send_email.stop) + get_payment_url = patch( + # this also shadows one (1) call to _get_payment_gateway_controller + "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url", + return_value=PAYMENT_URL, + ) + self.get_payment_url = get_payment_url.start() + self.addCleanup(get_payment_url.stop) + _get_payment_gateway_controller = patch( + "erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller", + ) + self._get_payment_gateway_controller = _get_payment_gateway_controller.start() + self.addCleanup(_get_payment_gateway_controller.stop) + + def tearDown(self): + frappe.db.rollback() + def test_payment_request_linkings(self): so_inr = make_sales_order(currency="INR", do_not_save=True) so_inr.disable_rounded_total = 1 From 7d0a094515ce0d5950e34bf3ae211265a300214e Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 30 Sep 2024 18:21:59 +0530 Subject: [PATCH 05/14] test: Removed initial PR status `assertion` (cherry picked from commit 67bd540135c246a658e0f081db62e01eb4eacc7f) --- .../accounts/doctype/payment_request/test_payment_request.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 6644254ae7e..e9f8f25ff2d 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -320,7 +320,6 @@ class TestPaymentRequest(FrappeTestCase): 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") so.load_from_db() @@ -485,7 +484,6 @@ class TestPaymentRequest(FrappeTestCase): 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) @@ -516,7 +514,6 @@ class TestPaymentRequest(FrappeTestCase): return_doc=1, ) - self.assertEqual(pr.status, "Requested") self.assertEqual(pr.grand_total, 1000) self.assertEqual(pr.outstanding_amount, pr.grand_total) From 3a51cf4e1a0261ad9b43766389ac35bdad55a61e Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 7 Oct 2024 12:08:02 +0530 Subject: [PATCH 06/14] fix: Remove unused field (cherry picked from commit e785928c0fcb5e2549302ea77217fe88f9797717) # Conflicts: # erpnext/accounts/doctype/payment_request/payment_request.json --- .../accounts/doctype/payment_request/payment_request.json | 5 ++++- erpnext/accounts/doctype/payment_request/payment_request.py | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index af1524de114..85354d6ca1c 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -371,6 +371,7 @@ "read_only": 1 }, { +<<<<<<< HEAD <<<<<<< HEAD "fetch_from": "payment_gateway_account.payment_channel", "fieldname": "payment_channel", @@ -410,6 +411,8 @@ "read_only": 1 }, { +======= +>>>>>>> e785928c0f (fix: Remove unused field) "depends_on": "eval: doc.docstatus === 1", "description": "Amount in party's bank account currency", "fieldname": "outstanding_amount", @@ -487,4 +490,4 @@ "sort_field": "modified", "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 e4540de11bf..c81df6f99c8 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -62,7 +62,6 @@ 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 From 677aadde7c7fbcea65e50800e5d75c31e35fa3a6 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 7 Oct 2024 12:10:51 +0530 Subject: [PATCH 07/14] fix: Remove unused function `get_paid_amount_against_order` (cherry picked from commit 75916629c857465a1a6efadd2fd0c77624c93cf5) --- .../payment_request/payment_request.py | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index c81df6f99c8..0411a311735 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -899,33 +899,6 @@ def validate_payment(doc, method=None): ) -def get_paid_amount_against_order(dt, dn): - pe_ref = frappe.qb.DocType("Payment Entry Reference") - if dt == "Sales Order": - inv_dt, inv_field = "Sales Invoice Item", "sales_order" - else: - inv_dt, inv_field = "Purchase Invoice Item", "purchase_order" - inv_item = frappe.qb.DocType(inv_dt) - return ( - frappe.qb.from_(pe_ref) - .select( - Sum(pe_ref.allocated_amount), - ) - .where( - (pe_ref.docstatus == 1) - & ( - (pe_ref.reference_name == dn) - | pe_ref.reference_name.isin( - frappe.qb.from_(inv_item) - .select(inv_item.parent) - .where(inv_item[inv_field] == dn) - .distinct() - ) - ) - ) - ).run()[0][0] or 0 - - @frappe.whitelist() def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters): # permission checks in `get_list()` From d828ea6a1a986a07f82c117405940b164cbf2e7c Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 7 Oct 2024 12:33:42 +0530 Subject: [PATCH 08/14] fix: Separate `on_submit` and `before_submit` of PR (cherry picked from commit dbd7b83204d1e36790d838a5c96c55d4d93464bd) --- .../payment_request/payment_request.py | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 0411a311735..dc74392cba9 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -191,19 +191,28 @@ class PaymentRequest(Document): else: self.outstanding_amount = self.grand_total + def on_submit(self): if self.payment_request_type == "Outward": - self.status = "Initiated" + self.db_set("status", "Initiated") + return elif self.payment_request_type == "Inward": - self.status = "Requested" + self.db_set("status", "Requested") - if self.payment_request_type == "Inward": - if self.payment_channel == "Phone": - self.request_phone_payment() - else: - self.set_payment_request_url() - if not (self.mute_email or self.flags.mute_email): - self.send_email() - self.make_communication_entry() + send_mail = self.payment_gateway_validation() if self.payment_gateway else None + ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) + + if ( + hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart" + ) or self.flags.mute_email: + send_mail = False + + if send_mail and self.payment_channel != "Phone": + self.set_payment_request_url() + self.send_email() + self.make_communication_entry() + + elif self.payment_channel == "Phone": + self.request_phone_payment() def request_phone_payment(self): controller = _get_payment_gateway_controller(self.payment_gateway) From 4d00d2b87b04e95329d077b520f6b3cd3967cb12 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 7 Oct 2024 12:42:59 +0530 Subject: [PATCH 09/14] test: Remove `Payment Gateway` settings from test (cherry picked from commit 3d9d56ab504ef2fd321fe4cbdd38414a1700ccfa) --- .../payment_request/test_payment_request.py | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index e9f8f25ff2d..34b3b7bb5fa 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -3,7 +3,6 @@ import re import unittest -from unittest.mock import patch import frappe from frappe.tests.utils import FrappeTestCase @@ -18,7 +17,6 @@ from erpnext.setup.utils import get_exchange_rate test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"] -PAYMENT_URL = "https://example.com/payment" payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"} @@ -52,28 +50,6 @@ class TestPaymentRequest(FrappeTestCase): ): frappe.get_doc(method).insert(ignore_permissions=True) - send_email = patch( - "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email", - return_value=None, - ) - self.send_email = send_email.start() - self.addCleanup(send_email.stop) - get_payment_url = patch( - # this also shadows one (1) call to _get_payment_gateway_controller - "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url", - return_value=PAYMENT_URL, - ) - self.get_payment_url = get_payment_url.start() - self.addCleanup(get_payment_url.stop) - _get_payment_gateway_controller = patch( - "erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller", - ) - self._get_payment_gateway_controller = _get_payment_gateway_controller.start() - self.addCleanup(_get_payment_gateway_controller.stop) - - def tearDown(self): - frappe.db.rollback() - def test_payment_request_linkings(self): so_inr = make_sales_order(currency="INR", do_not_save=True) so_inr.disable_rounded_total = 1 From 54608bdff9d7c4e79fa2fdd3989315eecf5ac621 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 7 Oct 2024 16:00:06 +0530 Subject: [PATCH 10/14] test: Change `Accounts Settings` for multi currency (https://github.com/frappe/erpnext/pull/42427#discussion_r1789859737) (cherry picked from commit 62cc86114b3f2bca31d66975fb4182e165ce2c90) --- .../doctype/payment_request/test_payment_request.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 34b3b7bb5fa..b0c3dbf4d5b 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -5,7 +5,7 @@ import re import unittest import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings 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 @@ -341,8 +341,11 @@ class TestPaymentRequest(FrappeTestCase): return_doc=1, ) + @change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}) def test_multiple_payment_if_partially_paid_for_multi_currency(self): - pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100) + pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100, do_not_save=1) + pi.credit_to = "Creditors - _TC" + pi.submit() pr = make_payment_request( dt="Purchase Invoice", @@ -439,10 +442,13 @@ class TestPaymentRequest(FrappeTestCase): self.assertEqual(pr.outstanding_amount, 0) self.assertEqual(pr.grand_total, 20000) + @change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}) 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 = create_sales_invoice( + do_not_save=1, currency="USD", debit_to="Debtors - _TC", qty=1, rate=200, conversion_rate=50 + ) si.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254 si.save() si.submit() From d4258fff9d8d6fa37209e6a29f18c5e585954a6d Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 7 Oct 2024 20:36:53 +0530 Subject: [PATCH 11/14] chore: remove unused filed (cherry picked from commit 0c599c2b6d171161de4c697ae7e6c691e70cc2f4) --- erpnext/accounts/doctype/payment_request/payment_request.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 85354d6ca1c..4d3457d38de 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -430,10 +430,6 @@ "options": "Company", "read_only": 1 }, - { - "fieldname": "column_break_pnyv", - "fieldtype": "Column Break" - }, { "fieldname": "party_account_currency", "fieldtype": "Link", From 3f5f37fa73ef7542ce16792dfb37ee09eb0033c0 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 7 Oct 2024 22:15:26 +0530 Subject: [PATCH 12/14] chore: resolve conflicts visible prima-facie --- .../doctype/payment_entry/payment_entry.js | 129 ------------------ .../payment_entry_reference.json | 14 -- .../payment_entry_reference.py | 33 ----- .../payment_request/payment_request.json | 16 +-- .../payment_request/payment_request.py | 76 +---------- 5 files changed, 3 insertions(+), 265 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index b2eb8d27c6f..383b8100780 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -653,16 +653,9 @@ frappe.ui.form.on('Payment Entry', { frm.set_value("base_received_amount", flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)); -<<<<<<< HEAD - if(frm.doc.payment_type == "Pay") - frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1); - else - frm.events.set_unallocated_amount(frm); -======= if (frm.doc.payment_type == "Pay") frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true); else frm.events.set_unallocated_amount(frm); ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) frm.set_paid_amount_based_on_received_amount = false; frm.events.hide_unhide_fields(frm); @@ -680,16 +673,9 @@ frappe.ui.form.on('Payment Entry', { frm.set_value("base_received_amount", frm.doc.base_paid_amount); } -<<<<<<< HEAD - if(frm.doc.payment_type == "Receive") - frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1); - else - frm.events.set_unallocated_amount(frm); -======= if (frm.doc.payment_type == "Receive") frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true); else frm.events.set_unallocated_amount(frm); ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) }, get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) { @@ -875,19 +861,12 @@ frappe.ui.form.on('Payment Entry', { } } -<<<<<<< HEAD - frm.events.allocate_party_amount_against_ref_docs(frm, - (frm.doc.payment_type=="Receive" ? frm.doc.paid_amount : frm.doc.received_amount)); - - } -======= frm.events.allocate_party_amount_against_ref_docs( frm, frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount, false ); }, ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) }); }, @@ -899,78 +878,6 @@ frappe.ui.form.on('Payment Entry', { return ["Sales Invoice", "Purchase Invoice"]; }, -<<<<<<< HEAD - 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)); - } - } - }) - - frm.refresh_fields() -======= allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) { await frm.call("allocate_amount_to_references", { paid_amount: paid_amount, @@ -978,7 +885,6 @@ frappe.ui.form.on('Payment Entry', { allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false, }); ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) frm.events.set_total_allocated_amount(frm); }, @@ -1603,40 +1509,5 @@ frappe.ui.form.on('Payment Entry Deduction', { deductions_remove: function(frm) { frm.events.set_unallocated_amount(frm); -<<<<<<< HEAD - } -}) -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); - } - ]); - - } - } - }); - } - }, -}) -======= }, }); ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) 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 1a64f5b49a7..361f516b830 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -10,25 +10,18 @@ "due_date", "bill_no", "payment_term", -<<<<<<< HEAD -======= "payment_term_outstanding", "account_type", "payment_type", ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) "column_break_4", "total_amount", "outstanding_amount", "allocated_amount", "exchange_rate", -<<<<<<< HEAD - "exchange_gain_loss" -======= "exchange_gain_loss", "account", "payment_request", "payment_request_outstanding" ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) ], "fields": [ { @@ -114,8 +107,6 @@ "label": "Exchange Gain/Loss", "options": "Company:company:default_currency", "read_only": 1 -<<<<<<< HEAD -======= }, { "fieldname": "account", @@ -153,17 +144,12 @@ "is_virtual": 1, "label": "Payment Request Outstanding", "read_only": 1 ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2022-12-12 12:31:44.919895", -======= "modified": "2024-09-16 18:11:50.019343", ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) "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 b036898643c..9d73583337c 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -6,42 +6,9 @@ from frappe.model.document import Document class PaymentEntryReference(Document): -<<<<<<< HEAD - pass -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - account: DF.Link | None - account_type: DF.Data | None - allocated_amount: DF.Float - bill_no: DF.Data | None - due_date: DF.Date | None - exchange_gain_loss: DF.Currency - exchange_rate: DF.Float - outstanding_amount: DF.Float - parent: DF.Data - 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 - total_amount: DF.Float - # end: auto-generated types - @property def payment_request_outstanding(self): if not self.payment_request: return return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount") ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 4d3457d38de..010efc475b4 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -149,13 +149,9 @@ "fieldtype": "Currency", "in_preview": 1, "label": "Amount", -<<<<<<< HEAD - "options": "currency" -======= "non_negative": 1, "options": "currency", "reqd": 1 ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) }, { "default": "0", @@ -371,8 +367,6 @@ "read_only": 1 }, { -<<<<<<< HEAD -<<<<<<< HEAD "fetch_from": "payment_gateway_account.payment_channel", "fieldname": "payment_channel", "fieldtype": "Select", @@ -401,7 +395,8 @@ "fieldtype": "Link", "label": "Project", "options": "Project" -======= + }, + { "fieldname": "failed_reason", "fieldtype": "Data", "hidden": 1, @@ -411,8 +406,6 @@ "read_only": 1 }, { -======= ->>>>>>> e785928c0f (fix: Remove unused field) "depends_on": "eval: doc.docstatus === 1", "description": "Amount in party's bank account currency", "fieldname": "outstanding_amount", @@ -436,18 +429,13 @@ "label": "Party Account Currency", "options": "Currency", "read_only": 1 ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2022-12-21 16:56:40.115737", -======= "modified": "2024-09-16 17:50:54.440090", ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) "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 dc74392cba9..dc21f0b88a2 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -7,12 +7,8 @@ import json import frappe from frappe import _ from frappe.model.document import Document -<<<<<<< HEAD -from frappe.utils import flt, get_url, nowdate -======= from frappe.query_builder.functions import Sum -from frappe.utils import flt, nowdate ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) +from frappe.utils import flt, get_url, nowdate from frappe.utils.background_jobs import enqueue from erpnext import get_company_currency @@ -38,71 +34,6 @@ def _get_payment_gateway_controller(*args, **kwargs): class PaymentRequest(Document): -<<<<<<< HEAD -======= - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - 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 - 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 - grand_total: DF.Currency - iban: DF.ReadOnly | None - is_a_subscription: DF.Check - make_sales_invoice: DF.Check - message: DF.Text | None - 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_account_currency: DF.Link | None - party_type: DF.Link | None - payment_account: DF.ReadOnly | None - 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[None] - 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", - ] - subject: DF.Data | None - subscription_plans: DF.Table[SubscriptionPlanDetail] - swift_number: DF.ReadOnly | None - transaction_date: DF.Date | None - # end: auto-generated types - ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) def validate(self): if self.get("__islocal"): self.status = "Draft" @@ -628,12 +559,7 @@ def make_payment_request(**args): "message": gateway_account.get("message") or get_dummy_message(ref_doc), "reference_doctype": args.dt, "reference_name": args.dn, -<<<<<<< HEAD - "party_type": args.get("party_type") or "Customer", -======= - "company": ref_doc.get("company"), "party_type": party_type, ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) "party": args.get("party") or ref_doc.get("customer"), "bank_account": bank_account, } From f6b91969b2d1f86b69ed86f611885f92ba36c077 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 7 Oct 2024 22:31:49 +0530 Subject: [PATCH 13/14] chore: resolve conflicts, remove additional new fields --- .../doctype/payment_entry/payment_entry.py | 6 ------ .../payment_entry_reference.json | 19 ------------------- .../payment_request/payment_request.json | 16 ---------------- 3 files changed, 41 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index caff50d249a..c8f53b25a55 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -10,14 +10,8 @@ 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 -<<<<<<< HEAD -from frappe.utils.data import comma_and, fmt_money -from pypika.functions import Sum -======= from frappe.utils.data import comma_and, fmt_money, get_link_to_form -from pypika import Case from pypika.functions import Coalesce, Sum ->>>>>>> ea69ba7cd8 (fix: multiple issues in Payment Request (#42427)) import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions 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 361f516b830..69a5db2556c 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -11,15 +11,12 @@ "bill_no", "payment_term", "payment_term_outstanding", - "account_type", - "payment_type", "column_break_4", "total_amount", "outstanding_amount", "allocated_amount", "exchange_rate", "exchange_gain_loss", - "account", "payment_request", "payment_request_outstanding" ], @@ -108,22 +105,6 @@ "options": "Company:company:default_currency", "read_only": 1 }, - { - "fieldname": "account", - "fieldtype": "Link", - "label": "Account", - "options": "Account" - }, - { - "fieldname": "account_type", - "fieldtype": "Data", - "label": "Account Type" - }, - { - "fieldname": "payment_type", - "fieldtype": "Data", - "label": "Payment Type" - }, { "fieldname": "payment_request", "fieldtype": "Link", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 010efc475b4..9a93d5755c7 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -396,15 +396,6 @@ "label": "Project", "options": "Project" }, - { - "fieldname": "failed_reason", - "fieldtype": "Data", - "hidden": 1, - "label": "Reason for Failure", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, { "depends_on": "eval: doc.docstatus === 1", "description": "Amount in party's bank account currency", @@ -416,13 +407,6 @@ "options": "party_account_currency", "read_only": 1 }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "options": "Company", - "read_only": 1 - }, { "fieldname": "party_account_currency", "fieldtype": "Link", From 078eb46eb80d2c367828fbe69b25a21d40896a31 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 7 Oct 2024 22:55:58 +0530 Subject: [PATCH 14/14] fix: get company from ref doc as it's not available in payment request --- 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 dc21f0b88a2..f24b2891723 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -101,9 +101,10 @@ class PaymentRequest(Document): ) def before_submit(self): + company = frappe.get_value(self.reference_doctype, self.reference_name, "company") if ( self.currency != self.party_account_currency - and self.party_account_currency == get_company_currency(self.company) + and self.party_account_currency == get_company_currency(company) ): # set outstanding amount in party account currency invoice = frappe.get_value(