Compare commits

...

9 Commits

Author SHA1 Message Date
Abdeali Chharchhoda
a5f0dfacb4 fix: replace locals with get_doc in set_query 2024-08-12 10:50:38 +05:30
Abdeali Chharchhoda
2af7ed84cd refactor: replace sql query of matched_payment_requests to query builder 2024-08-12 10:50:38 +05:30
Abdeali Chharchhoda
527c3e0b24 fix: changes as per review 2024-08-12 10:50:38 +05:30
Abdeali Chharchhoda
b36fb8b218 fix: removed bug of set_advance_payment_status 2024-08-12 10:50:38 +05:30
Abdeali Chharchhoda
cbed1d7826 fix: update set_advance_payment_status() logic 2024-08-12 10:50:38 +05:30
Abdeali Chharchhoda
4f0541dc16 fix: replace round with flt 2024-08-12 10:50:38 +05:30
Abdeali Chharchhoda
290dc7d2b2 fix: remove bug 2024-08-12 10:50:38 +05:30
Abdeali Chharchhoda
4d1cb318dd chore: minor changes 2024-08-12 10:50:38 +05:30
Abdeali Chharchhoda
552c46db98 fix: multiple issues in Payment Request 2024-08-12 10:50:38 +05:30
8 changed files with 348 additions and 108 deletions

View File

@@ -166,6 +166,18 @@ frappe.ui.form.on("Payment Entry", {
};
});
frm.set_query("payment_request", "references", function (doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
return {
filters: {
docstatus: 1,
status: ["!=", "Paid"],
reference_doctype: row.reference_doctype,
reference_name: row.reference_name,
},
};
});
frm.set_query("sales_taxes_and_charges_template", function () {
return {
filters: {
@@ -1117,6 +1129,7 @@ frappe.ui.form.on("Payment Entry", {
});
frm.refresh_fields();
if (frappe.flags.allocate_payment_amount) frm.call("set_matched_payment_requests");
frm.events.set_total_allocated_amount(frm);
},
@@ -1706,8 +1719,15 @@ frappe.ui.form.on("Payment Entry Reference", {
}
},
allocated_amount: function (frm) {
allocated_amount: function (frm, cdt, cdn) {
frm.events.set_total_allocated_amount(frm);
const row = frappe.get_doc(cdt, cdn);
if (row.payment_request || !row.reference_name || !row.reference_doctype || !row.allocated_amount)
return;
frm.call("set_matched_payment_request", { row_idx: row.idx });
},
references_remove: function (frm) {

View File

@@ -7,8 +7,10 @@ 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
from frappe.utils.data import comma_and, fmt_money
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
from pypika import Case
from pypika.functions import Coalesce, Sum
@@ -180,6 +182,9 @@ class PaymentEntry(AccountsController):
self.set_status()
self.set_total_in_words()
def before_save(self):
self.check_references_for_unset_payment_request()
def on_submit(self):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
@@ -187,7 +192,8 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_payment_schedule()
self.set_payment_req_status()
self.update_payment_requests()
self.update_references_advance_payment_status()
self.set_status()
def set_liability_account(self):
@@ -262,13 +268,26 @@ class PaymentEntry(AccountsController):
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_references_advance_payment_status()
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_references_advance_payment_status(self):
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
for ref in self.get("references"):
if ref.reference_doctype in advance_payment_doctypes:
ref_doc = frappe.get_doc(ref.reference_doctype, ref.reference_name)
ref_doc.set_advance_payment_status()
def update_outstanding_amounts(self):
self.set_missing_ref_details(force=True)
@@ -308,6 +327,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:
@@ -320,6 +341,22 @@ 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):
from erpnext.accounts.doctype.payment_request.payment_request import (
get_outstanding_amount_of_payment_entry_references as get_outstanding_amounts,
)
outstanding_amounts = get_outstanding_amounts(self.references)
for ref in self.references:
if ref.payment_request and ref.allocated_amount > outstanding_amounts[ref.payment_request]:
frappe.throw(
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:
@@ -1692,6 +1729,137 @@ class PaymentEntry(AccountsController):
return current_tax_fraction
def check_references_for_unset_payment_request(self):
if not self.references:
return
matched_payment_requests = get_matched_payment_requests_of_references(
[row for row in self.references if not row.payment_request]
)
unset_pr_rows = {}
for row in self.references:
if row.payment_request:
continue
matched_pr = matched_payment_requests.get(
(row.reference_doctype, row.reference_name, row.allocated_amount)
)
if matched_pr:
unset_pr_rows[row.idx] = matched_pr
if unset_pr_rows:
message = _("Matched Payment Requests found for references, but not set. <br><br>")
message += _("<details><summary><strong>View Details</strong></summary><ul>")
for idx, pr in unset_pr_rows.items():
message += _("<li>Row #{0}: {1}</li>").format(idx, get_link_to_form("Payment Request", pr))
message += _("</ul></details>")
frappe.msgprint(
msg=message,
indicator="yellow",
)
@frappe.whitelist()
def set_matched_payment_requests(self):
if not self.references:
return
matched_payment_requests = get_matched_payment_requests_of_references(self.references)
matched_count = 0
for row in self.references:
if (
row.payment_request
or not row.reference_doctype
or not row.reference_name
or not row.allocated_amount
):
continue
row.payment_request = matched_payment_requests.get(
(row.reference_doctype, row.reference_name, row.allocated_amount)
)
if row.payment_request:
matched_count += 1
if not matched_count:
return
frappe.msgprint(
msg=_("Setting {0} matched Payment Request(s)").format(matched_count),
alert=True,
)
@frappe.whitelist()
def set_matched_payment_request(self, row_idx):
row = next((row for row in self.references if row.idx == row_idx), None)
if not row:
frappe.throw(_("Row #{0} not found").format(row_idx), title=_("Row Not Found"))
# if payment entry already set then do not set it again
if (
row.payment_request
or not row.reference_doctype
or not row.reference_name
or not row.allocated_amount
):
return
matched_pr = get_matched_payment_requests_of_references([row])
if not matched_pr:
return
row.payment_request = matched_pr[(row.reference_doctype, row.reference_name, row.allocated_amount)]
frappe.msgprint(
msg=_("Setting matched Payment Request"),
alert=True,
)
def get_matched_payment_requests_of_references(references=None):
if not references:
return
# to fetch matched rows
refs = [
(row.reference_doctype, row.reference_name, row.allocated_amount)
for row in references
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.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount, Count("*").as_("count")
)
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
.where(PR.status != "Paid")
.where(PR.docstatus == 1)
.groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
)
# query to fetch matched rows which are single
matched_prs = frappe.qb.from_(subquery).select("*").where(subquery.count == 1).run(as_dict=True)
if not matched_prs:
return
return {(pr.reference_doctype, pr.reference_name, pr.outstanding_amount): pr.name for pr in matched_prs}
def validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range):

View File

@@ -18,7 +18,8 @@
"allocated_amount",
"exchange_rate",
"exchange_gain_loss",
"account"
"account",
"payment_request"
],
"fields": [
{
@@ -120,12 +121,18 @@
"fieldname": "payment_type",
"fieldtype": "Data",
"label": "Payment Type"
},
{
"fieldname": "payment_request",
"fieldtype": "Link",
"label": "Payment Request",
"options": "Payment Request"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-04-05 09:44:08.310593",
"modified": "2024-07-20 17:57:32.866780",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@@ -25,6 +25,7 @@ class PaymentEntryReference(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
payment_request: DF.Link | None
payment_term: DF.Link | None
payment_type: DF.Data | None
reference_doctype: DF.Link

View File

@@ -52,8 +52,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
}
if (
(!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") &&
frm.doc.status == "Initiated"
frm.doc.payment_request_type == "Outward" &&
["Initiated", "Partially Paid"].includes(frm.doc.status)
) {
frm.add_custom_button(__("Create Payment Entry"), function () {
frappe.call({

View File

@@ -21,6 +21,7 @@
"grand_total",
"is_a_subscription",
"column_break_18",
"outstanding_amount",
"currency",
"subscription_section",
"subscription_plans",
@@ -133,7 +134,8 @@
"no_copy": 1,
"options": "reference_doctype",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "transaction_details",
@@ -146,7 +148,8 @@
"fieldtype": "Currency",
"label": "Amount",
"non_negative": 1,
"options": "currency"
"options": "currency",
"reqd": 1
},
{
"default": "0",
@@ -400,13 +403,21 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "eval: doc.docstatus === 1",
"fieldname": "outstanding_amount",
"fieldtype": "Currency",
"label": "Outstanding Amount",
"non_negative": 1,
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-06-20 13:54:55.245774",
"modified": "2024-07-23 19:02:07.754296",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
@@ -444,4 +455,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -49,6 +49,7 @@ class PaymentRequest(Document):
cost_center: DF.Link | None
currency: DF.Link | None
email_to: DF.Data | None
failed_reason: DF.Data | None
grand_total: DF.Currency
iban: DF.ReadOnly | None
is_a_subscription: DF.Check
@@ -57,16 +58,17 @@ class PaymentRequest(Document):
mode_of_payment: DF.Link | None
mute_email: DF.Check
naming_series: DF.Literal["ACC-PRQ-.YYYY.-"]
outstanding_amount: DF.Currency
party: DF.DynamicLink | None
party_type: DF.Link | None
payment_account: DF.ReadOnly | None
payment_channel: DF.Literal["", "Email", "Phone"]
payment_channel: DF.Literal["", "Email", "Phone", "Other"]
payment_gateway: DF.ReadOnly | None
payment_gateway_account: DF.Link | None
payment_order: DF.Link | None
payment_request_type: DF.Literal["Outward", "Inward"]
payment_url: DF.Data | None
print_format: DF.Literal
print_format: DF.Literal[None]
project: DF.Link | None
reference_doctype: DF.Link | None
reference_name: DF.DynamicLink | None
@@ -100,6 +102,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)
)
@@ -149,16 +157,9 @@ class PaymentRequest(Document):
).format(self.grand_total, amount)
)
def on_change(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
advance_payment_doctypes = frappe.get_hooks("advance_payment_receivable_doctypes") + frappe.get_hooks(
"advance_payment_payable_doctypes"
)
if self.reference_doctype in advance_payment_doctypes:
# set advance payment status
ref_doc.set_advance_payment_status()
def before_submit(self):
self.outstanding_amount = self.grand_total
if self.payment_request_type == "Outward":
self.status = "Initiated"
elif self.payment_request_type == "Inward":
@@ -173,6 +174,9 @@ 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()
@@ -210,6 +214,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):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
@@ -266,7 +271,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()
@@ -290,11 +295,14 @@ class PaymentRequest(Document):
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
bank_amount = self.grand_total
bank_amount = self.outstanding_amount
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
total = ref_doc.get("rounded_total") or ref_doc.get("grand_total")
base_total = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
party_amount = flt(self.outstanding_amount / total * base_total, self.precision("grand_total"))
else:
party_amount = self.grand_total
party_amount = self.outstanding_amount
payment_entry = get_payment_entry(
self.reference_doctype,
@@ -307,7 +315,6 @@ class PaymentRequest(Document):
payment_entry.update(
{
"mode_of_payment": self.mode_of_payment,
"reference_no": self.name,
"reference_date": nowdate(),
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
self.reference_doctype, self.reference_name, self.name
@@ -315,6 +322,9 @@ class PaymentRequest(Document):
}
)
# Add reference of Payment Request
payment_entry.references[0].payment_request = self.name
# Update dimensions
payment_entry.update(
{
@@ -323,14 +333,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
for dimension in get_accounting_dimensions():
payment_entry.update({dimension: self.get(dimension)})
@@ -414,6 +416,14 @@ class PaymentRequest(Document):
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()
@frappe.whitelist(allow_guest=True)
def make_payment_request(**args):
@@ -455,11 +465,15 @@ def make_payment_request(**args):
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
)
# fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
if existing_payment_request_amount:
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
@@ -472,7 +486,6 @@ def make_payment_request(**args):
args["payment_request_type"] = (
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
)
pr.update(
{
"payment_gateway_account": gateway_account.get("name"),
@@ -535,7 +548,6 @@ def get_amount(ref_doc, payment_account=None):
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
grand_total -= get_paid_amount_against_order(dt, ref_doc.name)
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if not ref_doc.get("is_pos"):
if ref_doc.party_account_currency == ref_doc.currency:
@@ -560,24 +572,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 else 0
def get_gateway_details(args): # nosemgrep
@@ -619,41 +627,75 @@ 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):
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,
},
payment_requests = frappe.get_all(
"Payment Request",
filters={"name": ["in", get_referenced_payment_requests(references)]},
fields=[
"name",
"grand_total",
"outstanding_amount",
"payment_request_type",
],
)
payment_requests = {pr.name: pr for pr in payment_requests}
for ref in references:
if not ref.payment_request:
continue
payment_request = payment_requests[ref.payment_request]
# update outstanding amount
new_outstanding_amount = (
payment_request["outstanding_amount"] + ref.allocated_amount
if cancel
else payment_request["outstanding_amount"] - ref.allocated_amount
)
if payment_request_name:
ref_details = get_reference_details(
ref.reference_doctype,
ref.reference_name,
doc.party_account_currency,
doc.party_type,
doc.party,
if not cancel and new_outstanding_amount < 0:
frappe.throw(
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_outstanding_amount_of_payment_entry_references(references: list) -> dict:
payment_requests = get_referenced_payment_requests(references)
return dict(
frappe.get_all(
"Payment Request",
filters={"name": ["in", payment_requests]},
fields=["name", "outstanding_amount"],
as_list=True,
)
)
def get_referenced_payment_requests(references: list) -> set:
return {row.payment_request for row in references if row.payment_request}
def get_dummy_message(doc):

View File

@@ -1946,33 +1946,24 @@ class AccountsController(TransactionBase):
def set_advance_payment_status(self):
new_status = None
stati = frappe.get_all(
"Payment Request",
{
paid_amount = frappe.get_value(
doctype="Payment Request",
filters={
"reference_doctype": self.doctype,
"reference_name": self.name,
"docstatus": 1,
},
pluck="status",
fieldname="sum(grand_total - outstanding_amount)",
)
if self.doctype in frappe.get_hooks("advance_payment_receivable_doctypes"):
if not stati:
new_status = "Not Requested"
elif "Requested" in stati or "Failed" in stati:
new_status = "Requested"
elif "Partially Paid" in stati:
new_status = "Partially Paid"
elif "Paid" in stati:
new_status = "Fully Paid"
if self.doctype in frappe.get_hooks("advance_payment_payable_doctypes"):
if not stati:
new_status = "Not Initiated"
elif "Initiated" in stati or "Failed" in stati or "Payment Ordered" in stati:
new_status = "Initiated"
elif "Partially Paid" in stati:
new_status = "Partially Paid"
elif "Paid" in stati:
new_status = "Fully Paid"
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