Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
blaggacao committed Apr 3, 2024
1 parent 121aa4f commit 2e79642
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 97 deletions.
20 changes: 16 additions & 4 deletions erpnext/accounts/doctype/payment_request/payment_request.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@
"message",
"message_examples",
"mute_email",
"payment_url",
"section_break_7",
"payment_gateway",
"payment_account",
"payment_channel",
"payment_order",
"amended_from"
"amended_from",
"column_break_pnyv",
"payment_url",
"integration_request"
],
"fields": [
{
Expand Down Expand Up @@ -399,13 +401,23 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_pnyv",
"fieldtype": "Column Break"
},
{
"fieldname": "integration_request",
"fieldtype": "Link",
"label": "Integration Request",
"options": "Integration Request"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:11.120742",
"modified": "2024-04-02 07:38:48.000436",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
Expand Down Expand Up @@ -443,4 +455,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}
121 changes: 35 additions & 86 deletions erpnext/accounts/doctype/payment_request/payment_request.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import json

import frappe
from frappe import _
from frappe.model.document import Document
Expand Down Expand Up @@ -51,6 +49,7 @@ class PaymentRequest(Document):
email_to: DF.Data | None
grand_total: DF.Currency
iban: DF.ReadOnly | None
integration_request: DF.Link | None
is_a_subscription: DF.Check
make_sales_invoice: DF.Check
message: DF.Text | None
Expand Down Expand Up @@ -164,53 +163,48 @@ def before_submit(self):
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 self.payment_request_type == "Inward" and self.payment_gateway:
controller = _get_payment_gateway_controller(self.payment_gateway)
tx_data = self.get_tx_data()
controller.on_refdoc_submission(tx_data) # preflight check
self.integration_request = controller.initiate_payment(tx_data)
# checkout page can also be the target of a gatway initialized flow
# gateways also may return None if appropriate
self.payment_url = controller.get_payment_url(self.integration_request)
if not controller.is_user_flow_initiation_delegated(self.integration_request):
if not (self.mute_email or self.flags.mute_email):
self.send_email()
self.make_communication_entry()

def request_phone_payment(self):
controller = _get_payment_gateway_controller(self.payment_gateway)
request_amount = self.get_request_amount()

payment_record = dict(
reference_doctype="Payment Request",
reference_docname=self.name,
payment_reference=self.reference_name,
request_amount=request_amount,
sender=self.email_to,
currency=self.currency,
payment_gateway=self.payment_gateway,
)

controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)
def on_cancel(self):
self.check_if_payment_entry_exists()
self.set_as_cancelled()

def get_request_amount(self):
data_of_completed_requests = frappe.get_all(
"Integration Request",
filters={
def get_tx_data(self):
party = frappe.get_doc(self.party_type, self.party)
if self.party_type == "Supplier" and party.supplier_primary_contact:
contact = frappe.get_doc("Contact", party.supplier_primary_contact).as_dict()
elif self.party_type == "Customer" and party.customer_primary_contact:
contact = frappe.get_doc("Contact", party.customer_primary_contact).as_dict()
else:
contact = {}
if self.party_type == "Supplier" and party.supplier_primary_address:
address = frappe.get_doc("address", party.supplier_primary_address).as_dict()
elif self.party_type == "Customer" and party.customer_primary_address:
address = frappe.get_doc("address", party.customer_primary_address).as_dict()
else:
address = {}
return frappe._dict(
{
"amount": self.grand_total,
"currency": self.currency,
"reference_doctype": self.doctype,
"reference_docname": self.name,
"status": "Completed",
},
pluck="data",
"payer_contact": contact,
"payer_address": address,
}
)

if not data_of_completed_requests:
return self.grand_total

request_amounts = sum(json.loads(d).get("request_amount") for d in data_of_completed_requests)
return request_amounts

def on_cancel(self):
self.check_if_payment_entry_exists()
self.set_as_cancelled()

def make_invoice(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice

Expand All @@ -219,51 +213,6 @@ def make_invoice(self):
si = si.insert(ignore_permissions=True)
si.submit()

def payment_gateway_validation(self):
try:
controller = _get_payment_gateway_controller(self.payment_gateway)
if hasattr(controller, "on_payment_request_submission"):
return controller.on_payment_request_submission(self)
else:
return True
except Exception:
return False

def set_payment_request_url(self):
if self.payment_account and self.payment_gateway and self.payment_gateway_validation():
self.payment_url = self.get_payment_url()

def get_payment_url(self):
if self.reference_doctype != "Fees":
data = frappe.db.get_value(
self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1
)
else:
data = frappe.db.get_value(
self.reference_doctype, self.reference_name, ["student_name"], as_dict=1
)
data.update({"company": frappe.defaults.get_defaults().company})

controller = _get_payment_gateway_controller(self.payment_gateway)
controller.validate_transaction_currency(self.currency)

if hasattr(controller, "validate_minimum_transaction_amount"):
controller.validate_minimum_transaction_amount(self.currency, self.grand_total)

return controller.get_payment_url(
**{
"amount": flt(self.grand_total, self.precision("grand_total")),
"title": data.company.encode("utf-8"),
"description": self.subject.encode("utf-8"),
"reference_doctype": "Payment Request",
"reference_docname": self.name,
"payer_email": self.email_to or frappe.session.user,
"payer_name": frappe.safe_encode(data.customer_name),
"order_id": self.name,
"currency": self.currency,
}
)

def set_as_paid(self):
if self.payment_channel == "Phone":
self.db_set("status", "Paid")
Expand Down Expand Up @@ -519,7 +468,7 @@ def make_payment_request(**args):
if args.order_type == "Shopping Cart":
frappe.db.commit()
frappe.local.response["type"] = "redirect"
frappe.local.response["location"] = pr.get_payment_url()
frappe.local.response["location"] = pr.payment_url

if args.return_doc:
return pr
Expand Down
40 changes: 33 additions & 7 deletions erpnext/accounts/doctype/payment_request/test_payment_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# See license.txt

import unittest
from unittest.mock import patch
from unittest.mock import DEFAULT, Mock, patch

import frappe

Expand All @@ -14,7 +14,7 @@

test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]

PAYMENT_URL = "https://example.com/payment"
PAYMENT_URL = "http://localhost/pay?ref=_Test Integration Request"

payment_gateways = [
{"doctype": "Payment Gateway", "gateway": "_Test Gateway"},
Expand Down Expand Up @@ -52,6 +52,28 @@
},
]

integration_request = {
"doctype": "Payment Gateway Account",
"payment_gateway": "_Test Gateway Phone",
"payment_account": "_Test Bank USD - _TC",
"payment_channel": "Phone",
"currency": "USD",
}


class GatewayControllerInterface:
def on_refdoc_submission(self, tx_data) -> None:
pass

def initiate_payment(self, tx_data, correlation_id=None, name=None) -> str:
return "_Test Integration Request"

def get_payment_url(self, integration_request_name: str) -> str:
return PAYMENT_URL

def is_user_flow_initiation_delegated(self, integration_request_name: str) -> bool:
return False


class TestPaymentRequest(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -82,6 +104,7 @@ def setUp(self):
self.addCleanup(get_payment_url.stop)
_get_payment_gateway_controller = patch(
"erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
return_value=Mock(wraps=GatewayControllerInterface()),
)
self._get_payment_gateway_controller = _get_payment_gateway_controller.start()
self.addCleanup(_get_payment_gateway_controller.stop)
Expand Down Expand Up @@ -131,7 +154,6 @@ def test_payment_channels(self):

self.assertEqual(pr.payment_url, PAYMENT_URL)
self.assertEqual(self.send_email.call_count, 0)
self.assertEqual(self._get_payment_gateway_controller.call_count, 1)
pr.cancel()

pr = make_payment_request(
Expand All @@ -149,9 +171,11 @@ def test_payment_channels(self):

self.assertEqual(pr.payment_url, PAYMENT_URL)
self.assertEqual(self.send_email.call_count, 0) # hence: no increment
self.assertEqual(self._get_payment_gateway_controller.call_count, 2)
pr.cancel()

# a phone controller presumable would override get_payment_url
self._get_payment_gateway_controller().get_payment_url.return_value = None

pr = make_payment_request(
dt="Sales Order",
dn=so.name,
Expand All @@ -166,9 +190,11 @@ def test_payment_channels(self):

self.assertIsNone(pr.payment_url)
self.assertEqual(self.send_email.call_count, 0) # no increment on phone channel
self.assertEqual(self._get_payment_gateway_controller.call_count, 3)
pr.cancel()

# restore
self._get_payment_gateway_controller().get_payment_url.return_value = DEFAULT

pr = make_payment_request(
dt="Sales Order",
dn=so.name,
Expand All @@ -183,7 +209,6 @@ def test_payment_channels(self):

self.assertEqual(pr.payment_url, PAYMENT_URL)
self.assertEqual(self.send_email.call_count, 1) # increment on normal email channel
self.assertEqual(self._get_payment_gateway_controller.call_count, 4)
pr.cancel()

so = make_sales_order(currency="USD", do_not_save=True)
Expand All @@ -206,9 +231,10 @@ def test_payment_channels(self):

self.assertEqual(pr.payment_url, PAYMENT_URL)
self.assertEqual(self.send_email.call_count, 1) # no increment on shopping cart
self.assertEqual(self._get_payment_gateway_controller.call_count, 5)
pr.cancel()

self.assertEqual(self._get_payment_gateway_controller().initiate_payment.call_count, 5)

def test_payment_entry_against_purchase_invoice(self):
si_usd = make_purchase_invoice(
customer="_Test Supplier USD",
Expand Down

0 comments on commit 2e79642

Please sign in to comment.