Merge branch 'develop' of https://github.com/frappe/erpnext into rounded-row-wise-tax
This commit is contained in:
commit
621da178e1
@ -37,6 +37,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
cle.flags.ignore_permissions = True
|
cle.flags.ignore_permissions = True
|
||||||
|
cle.flags.ignore_links = True
|
||||||
cle.submit()
|
cle.submit()
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
|||||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||||
from erpnext.accounts.utils import get_account_currency
|
from erpnext.accounts.utils import get_account_currency
|
||||||
from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscription
|
|
||||||
from erpnext.utilities import payment_app_import_guard
|
from erpnext.utilities import payment_app_import_guard
|
||||||
|
|
||||||
|
|
||||||
@ -393,6 +392,9 @@ class PaymentRequest(Document):
|
|||||||
|
|
||||||
def create_subscription(self, payment_provider, gateway_controller, data):
|
def create_subscription(self, payment_provider, gateway_controller, data):
|
||||||
if payment_provider == "stripe":
|
if payment_provider == "stripe":
|
||||||
|
with payment_app_import_guard():
|
||||||
|
from payments.payment_gateways.stripe_integration import create_stripe_subscription
|
||||||
|
|
||||||
return create_stripe_subscription(gateway_controller, data)
|
return create_stripe_subscription(gateway_controller, data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -539,8 +539,9 @@ class PurchaseInvoice(BuyingController):
|
|||||||
]
|
]
|
||||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||||
self.validate_for_repost()
|
if self.needs_repost:
|
||||||
self.db_set("repost_required", self.needs_repost)
|
self.validate_for_repost()
|
||||||
|
self.db_set("repost_required", self.needs_repost)
|
||||||
|
|
||||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||||
if not gl_entries:
|
if not gl_entries:
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, cint, flt, getdate, nowdate, today
|
from frappe.utils import add_days, cint, flt, getdate, nowdate, today
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@ -38,7 +38,7 @@ test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Templ
|
|||||||
test_ignore = ["Serial No"]
|
test_ignore = ["Serial No"]
|
||||||
|
|
||||||
|
|
||||||
class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
unlink_payment_on_cancel_of_invoice()
|
unlink_payment_on_cancel_of_invoice()
|
||||||
@ -48,6 +48,9 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
def tearDownClass(self):
|
def tearDownClass(self):
|
||||||
unlink_payment_on_cancel_of_invoice(0)
|
unlink_payment_on_cancel_of_invoice(0)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_purchase_invoice_received_qty(self):
|
def test_purchase_invoice_received_qty(self):
|
||||||
"""
|
"""
|
||||||
1. Test if received qty is validated against accepted + rejected
|
1. Test if received qty is validated against accepted + rejected
|
||||||
@ -422,6 +425,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
self.assertEqual(tax.tax_amount, expected_values[i][1])
|
self.assertEqual(tax.tax_amount, expected_values[i][1])
|
||||||
self.assertEqual(tax.total, expected_values[i][2])
|
self.assertEqual(tax.total, expected_values[i][2])
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_purchase_invoice_with_advance(self):
|
def test_purchase_invoice_with_advance(self):
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||||
test_records as jv_test_records,
|
test_records as jv_test_records,
|
||||||
@ -476,6 +480,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_invoice_with_advance_and_multi_payment_terms(self):
|
def test_invoice_with_advance_and_multi_payment_terms(self):
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||||
test_records as jv_test_records,
|
test_records as jv_test_records,
|
||||||
@ -1220,6 +1225,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
||||||
acc_settings.save()
|
acc_settings.save()
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_gain_loss_with_advance_entry(self):
|
def test_gain_loss_with_advance_entry(self):
|
||||||
unlink_enabled = frappe.db.get_value(
|
unlink_enabled = frappe.db.get_value(
|
||||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
|
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
|
||||||
@ -1420,6 +1426,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
)
|
)
|
||||||
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
|
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_purchase_invoice_advance_taxes(self):
|
def test_purchase_invoice_advance_taxes(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
|
|
||||||
|
@ -536,8 +536,9 @@ class SalesInvoice(SellingController):
|
|||||||
"taxes": ("account_head",),
|
"taxes": ("account_head",),
|
||||||
}
|
}
|
||||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||||
self.validate_for_repost()
|
if self.needs_repost:
|
||||||
self.db_set("repost_required", self.needs_repost)
|
self.validate_for_repost()
|
||||||
|
self.db_set("repost_required", self.needs_repost)
|
||||||
|
|
||||||
def set_paid_amount(self):
|
def set_paid_amount(self):
|
||||||
paid_amount = 0.0
|
paid_amount = 0.0
|
||||||
|
@ -6,7 +6,7 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||||
from frappe.tests.utils import change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@ -45,13 +45,17 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
|||||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||||
|
|
||||||
|
|
||||||
class TestSalesInvoice(unittest.TestCase):
|
class TestSalesInvoice(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
|
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
|
||||||
|
|
||||||
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
|
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
|
||||||
create_internal_parties()
|
create_internal_parties()
|
||||||
setup_accounts()
|
setup_accounts()
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def make(self):
|
def make(self):
|
||||||
w = frappe.copy_doc(test_records[0])
|
w = frappe.copy_doc(test_records[0])
|
||||||
@ -179,6 +183,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertRaises(frappe.LinkExistsError, si.cancel)
|
self.assertRaises(frappe.LinkExistsError, si.cancel)
|
||||||
unlink_payment_on_cancel_of_invoice()
|
unlink_payment_on_cancel_of_invoice()
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_payment_entry_unlink_against_standalone_credit_note(self):
|
def test_payment_entry_unlink_against_standalone_credit_note(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
@ -1300,6 +1305,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
dn.submit()
|
dn.submit()
|
||||||
return dn
|
return dn
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_sales_invoice_with_advance(self):
|
def test_sales_invoice_with_advance(self):
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||||
test_records as jv_test_records,
|
test_records as jv_test_records,
|
||||||
@ -2775,6 +2781,13 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
company="_Test Company",
|
company="_Test Company",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tds_payable_account = create_account(
|
||||||
|
account_name="TDS Payable",
|
||||||
|
account_type="Tax",
|
||||||
|
parent_account="Duties and Taxes - _TC",
|
||||||
|
company="_Test Company",
|
||||||
|
)
|
||||||
|
|
||||||
si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1)
|
si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1)
|
||||||
si.apply_discount_on = "Grand Total"
|
si.apply_discount_on = "Grand Total"
|
||||||
si.additional_discount_account = additional_discount_account
|
si.additional_discount_account = additional_discount_account
|
||||||
@ -3073,8 +3086,8 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
si.commission_rate = commission_rate
|
si.commission_rate = commission_rate
|
||||||
self.assertRaises(frappe.ValidationError, si.save)
|
self.assertRaises(frappe.ValidationError, si.save)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)})
|
||||||
def test_sales_invoice_submission_post_account_freezing_date(self):
|
def test_sales_invoice_submission_post_account_freezing_date(self):
|
||||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", add_days(getdate(), 1))
|
|
||||||
si = create_sales_invoice(do_not_save=True)
|
si = create_sales_invoice(do_not_save=True)
|
||||||
si.posting_date = add_days(getdate(), 1)
|
si.posting_date = add_days(getdate(), 1)
|
||||||
si.save()
|
si.save()
|
||||||
@ -3083,8 +3096,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
si.posting_date = getdate()
|
si.posting_date = getdate()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
|
||||||
|
|
||||||
def test_over_billing_case_against_delivery_note(self):
|
def test_over_billing_case_against_delivery_note(self):
|
||||||
"""
|
"""
|
||||||
Test a case where duplicating the item with qty = 1 in the invoice
|
Test a case where duplicating the item with qty = 1 in the invoice
|
||||||
@ -3113,6 +3124,13 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", over_billing_allowance)
|
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", over_billing_allowance)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{
|
||||||
|
"book_deferred_entries_via_journal_entry": 1,
|
||||||
|
"submit_journal_entries": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
def test_multi_currency_deferred_revenue_via_journal_entry(self):
|
def test_multi_currency_deferred_revenue_via_journal_entry(self):
|
||||||
deferred_account = create_account(
|
deferred_account = create_account(
|
||||||
account_name="Deferred Revenue",
|
account_name="Deferred Revenue",
|
||||||
@ -3120,11 +3138,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
company="_Test Company",
|
company="_Test Company",
|
||||||
)
|
)
|
||||||
|
|
||||||
acc_settings = frappe.get_single("Accounts Settings")
|
|
||||||
acc_settings.book_deferred_entries_via_journal_entry = 1
|
|
||||||
acc_settings.submit_journal_entries = 1
|
|
||||||
acc_settings.save()
|
|
||||||
|
|
||||||
item = create_item("_Test Item for Deferred Accounting")
|
item = create_item("_Test Item for Deferred Accounting")
|
||||||
item.enable_deferred_expense = 1
|
item.enable_deferred_expense = 1
|
||||||
item.item_defaults[0].deferred_revenue_account = deferred_account
|
item.item_defaults[0].deferred_revenue_account = deferred_account
|
||||||
@ -3190,13 +3203,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(expected_gle[i][2], gle.debit)
|
self.assertEqual(expected_gle[i][2], gle.debit)
|
||||||
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||||
|
|
||||||
acc_settings = frappe.get_single("Accounts Settings")
|
|
||||||
acc_settings.book_deferred_entries_via_journal_entry = 0
|
|
||||||
acc_settings.submit_journal_entries = 0
|
|
||||||
acc_settings.save()
|
|
||||||
|
|
||||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
|
||||||
|
|
||||||
def test_standalone_serial_no_return(self):
|
def test_standalone_serial_no_return(self):
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1
|
item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils.data import (
|
from frappe.utils.data import (
|
||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
@ -21,11 +22,15 @@ from erpnext.accounts.doctype.subscription.subscription import get_prorata_facto
|
|||||||
test_dependencies = ("UOM", "Item Group", "Item")
|
test_dependencies = ("UOM", "Item Group", "Item")
|
||||||
|
|
||||||
|
|
||||||
class TestSubscription(unittest.TestCase):
|
class TestSubscription(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
make_plans()
|
make_plans()
|
||||||
create_parties()
|
create_parties()
|
||||||
reset_settings()
|
reset_settings()
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_create_subscription_with_trial_with_correct_period(self):
|
def test_create_subscription_with_trial_with_correct_period(self):
|
||||||
subscription = create_subscription(
|
subscription = create_subscription(
|
||||||
|
@ -133,15 +133,17 @@ class General_Payment_Ledger_Comparison(object):
|
|||||||
self.gle_balances = set(val.gle) | self.gle_balances
|
self.gle_balances = set(val.gle) | self.gle_balances
|
||||||
self.ple_balances = set(val.ple) | self.ple_balances
|
self.ple_balances = set(val.ple) | self.ple_balances
|
||||||
|
|
||||||
self.diff1 = self.gle_balances.difference(self.ple_balances)
|
self.variation_in_payment_ledger = self.gle_balances.difference(self.ple_balances)
|
||||||
self.diff2 = self.ple_balances.difference(self.gle_balances)
|
self.variation_in_general_ledger = self.ple_balances.difference(self.gle_balances)
|
||||||
self.diff = frappe._dict({})
|
self.diff = frappe._dict({})
|
||||||
|
|
||||||
for x in self.diff1:
|
for x in self.variation_in_payment_ledger:
|
||||||
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
|
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
|
||||||
|
|
||||||
for x in self.diff2:
|
for x in self.variation_in_general_ledger:
|
||||||
self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]}))
|
self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update(
|
||||||
|
frappe._dict({"pl_balance": x[4]})
|
||||||
|
)
|
||||||
|
|
||||||
def generate_data(self):
|
def generate_data(self):
|
||||||
self.data = []
|
self.data = []
|
||||||
|
@ -544,6 +544,8 @@ class GrossProfitGenerator(object):
|
|||||||
new_row.qty += flt(row.qty)
|
new_row.qty += flt(row.qty)
|
||||||
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
||||||
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
||||||
|
if self.filters.get("group_by") == "Sales Person":
|
||||||
|
new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision)
|
||||||
new_row = self.set_average_rate(new_row)
|
new_row = self.set_average_rate(new_row)
|
||||||
self.grouped_data.append(new_row)
|
self.grouped_data.append(new_row)
|
||||||
|
|
||||||
|
@ -68,7 +68,11 @@ def get_result(
|
|||||||
tax_amount += entry.credit - entry.debit
|
tax_amount += entry.credit - entry.debit
|
||||||
|
|
||||||
if net_total_map.get(name):
|
if net_total_map.get(name):
|
||||||
total_amount, grand_total, base_total = net_total_map.get(name)
|
if voucher_type == "Journal Entry":
|
||||||
|
# back calcalute total amount from rate and tax_amount
|
||||||
|
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||||
|
else:
|
||||||
|
total_amount, grand_total, base_total = net_total_map.get(name)
|
||||||
else:
|
else:
|
||||||
total_amount += entry.credit
|
total_amount += entry.credit
|
||||||
|
|
||||||
|
@ -337,7 +337,7 @@ frappe.ui.form.on('Asset', {
|
|||||||
|
|
||||||
|
|
||||||
item_code: function(frm) {
|
item_code: function(frm) {
|
||||||
if(frm.doc.item_code && frm.doc.calculate_depreciation) {
|
if(frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
|
||||||
frm.trigger('set_finance_book');
|
frm.trigger('set_finance_book');
|
||||||
} else {
|
} else {
|
||||||
frm.set_value('finance_books', []);
|
frm.set_value('finance_books', []);
|
||||||
@ -490,7 +490,7 @@ frappe.ui.form.on('Asset', {
|
|||||||
|
|
||||||
calculate_depreciation: function(frm) {
|
calculate_depreciation: function(frm) {
|
||||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||||
if (frm.doc.item_code && frm.doc.calculate_depreciation ) {
|
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
|
||||||
frm.trigger("set_finance_book");
|
frm.trigger("set_finance_book");
|
||||||
} else {
|
} else {
|
||||||
frm.set_value("finance_books", []);
|
frm.set_value("finance_books", []);
|
||||||
|
@ -13,6 +13,7 @@ from frappe.utils import (
|
|||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
cint,
|
cint,
|
||||||
|
comma_and,
|
||||||
flt,
|
flt,
|
||||||
fmt_money,
|
fmt_money,
|
||||||
formatdate,
|
formatdate,
|
||||||
@ -181,6 +182,17 @@ class AccountsController(TransactionBase):
|
|||||||
self.validate_party_account_currency()
|
self.validate_party_account_currency()
|
||||||
|
|
||||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
||||||
|
if invalid_advances := [
|
||||||
|
x for x in self.advances if not x.reference_type or not x.reference_name
|
||||||
|
]:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry."
|
||||||
|
).format(
|
||||||
|
frappe.bold(comma_and([x.idx for x in invalid_advances])), frappe.bold(_("Advance Payments"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||||
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
||||||
self.set_advances()
|
self.set_advances()
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
// Copyright (c) 2018, Frappe Technologies and contributors
|
|
||||||
// For license information, please see license.txt
|
|
||||||
|
|
||||||
frappe.ui.form.on('GoCardless Mandate', {
|
|
||||||
});
|
|
@ -1,184 +0,0 @@
|
|||||||
{
|
|
||||||
"allow_copy": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"autoname": "field:mandate",
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2018-02-08 11:33:15.721919",
|
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "disabled",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Disabled",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "customer",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Customer",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Customer",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "mandate",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Mandate",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "gocardless_customer",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "GoCardless Customer",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2018-02-11 12:28:03.183095",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "ERPNext Integrations",
|
|
||||||
"name": "GoCardless Mandate",
|
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"amend": 0,
|
|
||||||
"apply_user_permissions": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"quick_entry": 1,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
|
|
||||||
class GoCardlessMandate(Document):
|
|
||||||
pass
|
|
@ -1,8 +0,0 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies and Contributors
|
|
||||||
# See license.txt
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class TestGoCardlessMandate(unittest.TestCase):
|
|
||||||
pass
|
|
@ -1,89 +0,0 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def webhooks():
|
|
||||||
r = frappe.request
|
|
||||||
if not r:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not authenticate_signature(r):
|
|
||||||
raise frappe.AuthenticationError
|
|
||||||
|
|
||||||
gocardless_events = json.loads(r.get_data()) or []
|
|
||||||
for event in gocardless_events["events"]:
|
|
||||||
set_status(event)
|
|
||||||
|
|
||||||
return 200
|
|
||||||
|
|
||||||
|
|
||||||
def set_status(event):
|
|
||||||
resource_type = event.get("resource_type", {})
|
|
||||||
|
|
||||||
if resource_type == "mandates":
|
|
||||||
set_mandate_status(event)
|
|
||||||
|
|
||||||
|
|
||||||
def set_mandate_status(event):
|
|
||||||
mandates = []
|
|
||||||
if isinstance(event["links"], (list,)):
|
|
||||||
for link in event["links"]:
|
|
||||||
mandates.append(link["mandate"])
|
|
||||||
else:
|
|
||||||
mandates.append(event["links"]["mandate"])
|
|
||||||
|
|
||||||
if (
|
|
||||||
event["action"] == "pending_customer_approval"
|
|
||||||
or event["action"] == "pending_submission"
|
|
||||||
or event["action"] == "submitted"
|
|
||||||
or event["action"] == "active"
|
|
||||||
):
|
|
||||||
disabled = 0
|
|
||||||
else:
|
|
||||||
disabled = 1
|
|
||||||
|
|
||||||
for mandate in mandates:
|
|
||||||
frappe.db.set_value("GoCardless Mandate", mandate, "disabled", disabled)
|
|
||||||
|
|
||||||
|
|
||||||
def authenticate_signature(r):
|
|
||||||
"""Returns True if the received signature matches the generated signature"""
|
|
||||||
received_signature = frappe.get_request_header("Webhook-Signature")
|
|
||||||
|
|
||||||
if not received_signature:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for key in get_webhook_keys():
|
|
||||||
computed_signature = hmac.new(key.encode("utf-8"), r.get_data(), hashlib.sha256).hexdigest()
|
|
||||||
if hmac.compare_digest(str(received_signature), computed_signature):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_webhook_keys():
|
|
||||||
def _get_webhook_keys():
|
|
||||||
webhook_keys = [
|
|
||||||
d.webhooks_secret
|
|
||||||
for d in frappe.get_all(
|
|
||||||
"GoCardless Settings",
|
|
||||||
fields=["webhooks_secret"],
|
|
||||||
)
|
|
||||||
if d.webhooks_secret
|
|
||||||
]
|
|
||||||
|
|
||||||
return webhook_keys
|
|
||||||
|
|
||||||
return frappe.cache().get_value("gocardless_webhooks_secret", _get_webhook_keys)
|
|
||||||
|
|
||||||
|
|
||||||
def clear_cache():
|
|
||||||
frappe.cache().delete_value("gocardless_webhooks_secret")
|
|
@ -1,8 +0,0 @@
|
|||||||
// Copyright (c) 2018, Frappe Technologies and contributors
|
|
||||||
// For license information, please see license.txt
|
|
||||||
|
|
||||||
frappe.ui.form.on('GoCardless Settings', {
|
|
||||||
refresh: function(frm) {
|
|
||||||
erpnext.utils.check_payments_app();
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,211 +0,0 @@
|
|||||||
{
|
|
||||||
"allow_copy": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"autoname": "field:gateway_name",
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2018-02-06 16:11:10.028249",
|
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "gateway_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Payment Gateway Name",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "section_break_2",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "access_token",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Access Token",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "webhooks_secret",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Webhooks Secret",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "use_sandbox",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Use Sandbox",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2022-02-12 14:18:47.209114",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "ERPNext Integrations",
|
|
||||||
"name": "GoCardless Settings",
|
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"amend": 0,
|
|
||||||
"apply_user_permissions": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
|
@ -1,220 +0,0 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
import gocardless_pro
|
|
||||||
from frappe import _
|
|
||||||
from frappe.integrations.utils import create_request_log
|
|
||||||
from frappe.model.document import Document
|
|
||||||
from frappe.utils import call_hook_method, cint, flt, get_url
|
|
||||||
|
|
||||||
from erpnext.utilities import payment_app_import_guard
|
|
||||||
|
|
||||||
|
|
||||||
class GoCardlessSettings(Document):
|
|
||||||
supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"]
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
self.initialize_client()
|
|
||||||
|
|
||||||
def initialize_client(self):
|
|
||||||
self.environment = self.get_environment()
|
|
||||||
try:
|
|
||||||
self.client = gocardless_pro.Client(
|
|
||||||
access_token=self.access_token, environment=self.environment
|
|
||||||
)
|
|
||||||
return self.client
|
|
||||||
except Exception as e:
|
|
||||||
frappe.throw(e)
|
|
||||||
|
|
||||||
def on_update(self):
|
|
||||||
with payment_app_import_guard():
|
|
||||||
from payments.utils import create_payment_gateway
|
|
||||||
|
|
||||||
create_payment_gateway(
|
|
||||||
"GoCardless-" + self.gateway_name, settings="GoCardLess Settings", controller=self.gateway_name
|
|
||||||
)
|
|
||||||
call_hook_method("payment_gateway_enabled", gateway="GoCardless-" + self.gateway_name)
|
|
||||||
|
|
||||||
def on_payment_request_submission(self, data):
|
|
||||||
if data.reference_doctype != "Fees":
|
|
||||||
customer_data = frappe.db.get_value(
|
|
||||||
data.reference_doctype, data.reference_name, ["company", "customer_name"], as_dict=1
|
|
||||||
)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"amount": flt(data.grand_total, data.precision("grand_total")),
|
|
||||||
"title": customer_data.company.encode("utf-8"),
|
|
||||||
"description": data.subject.encode("utf-8"),
|
|
||||||
"reference_doctype": data.doctype,
|
|
||||||
"reference_docname": data.name,
|
|
||||||
"payer_email": data.email_to or frappe.session.user,
|
|
||||||
"payer_name": customer_data.customer_name,
|
|
||||||
"order_id": data.name,
|
|
||||||
"currency": data.currency,
|
|
||||||
}
|
|
||||||
|
|
||||||
valid_mandate = self.check_mandate_validity(data)
|
|
||||||
if valid_mandate is not None:
|
|
||||||
data.update(valid_mandate)
|
|
||||||
|
|
||||||
self.create_payment_request(data)
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def check_mandate_validity(self, data):
|
|
||||||
|
|
||||||
if frappe.db.exists("GoCardless Mandate", dict(customer=data.get("payer_name"), disabled=0)):
|
|
||||||
registered_mandate = frappe.db.get_value(
|
|
||||||
"GoCardless Mandate", dict(customer=data.get("payer_name"), disabled=0), "mandate"
|
|
||||||
)
|
|
||||||
self.initialize_client()
|
|
||||||
mandate = self.client.mandates.get(registered_mandate)
|
|
||||||
|
|
||||||
if (
|
|
||||||
mandate.status == "pending_customer_approval"
|
|
||||||
or mandate.status == "pending_submission"
|
|
||||||
or mandate.status == "submitted"
|
|
||||||
or mandate.status == "active"
|
|
||||||
):
|
|
||||||
return {"mandate": registered_mandate}
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_environment(self):
|
|
||||||
if self.use_sandbox:
|
|
||||||
return "sandbox"
|
|
||||||
else:
|
|
||||||
return "live"
|
|
||||||
|
|
||||||
def validate_transaction_currency(self, currency):
|
|
||||||
if currency not in self.supported_currencies:
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Please select another payment method. Go Cardless does not support transactions in currency '{0}'"
|
|
||||||
).format(currency)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_payment_url(self, **kwargs):
|
|
||||||
return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs)))
|
|
||||||
|
|
||||||
def create_payment_request(self, data):
|
|
||||||
self.data = frappe._dict(data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.integration_request = create_request_log(self.data, "Host", "GoCardless")
|
|
||||||
return self.create_charge_on_gocardless()
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
frappe.log_error("Gocardless payment reqeust failed")
|
|
||||||
return {
|
|
||||||
"redirect_to": frappe.redirect_to_message(
|
|
||||||
_("Server Error"),
|
|
||||||
_(
|
|
||||||
"There seems to be an issue with the server's GoCardless configuration. Don't worry, in case of failure, the amount will get refunded to your account."
|
|
||||||
),
|
|
||||||
),
|
|
||||||
"status": 401,
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_charge_on_gocardless(self):
|
|
||||||
redirect_to = self.data.get("redirect_to") or None
|
|
||||||
redirect_message = self.data.get("redirect_message") or None
|
|
||||||
|
|
||||||
reference_doc = frappe.get_doc(
|
|
||||||
self.data.get("reference_doctype"), self.data.get("reference_docname")
|
|
||||||
)
|
|
||||||
self.initialize_client()
|
|
||||||
|
|
||||||
try:
|
|
||||||
payment = self.client.payments.create(
|
|
||||||
params={
|
|
||||||
"amount": cint(reference_doc.grand_total * 100),
|
|
||||||
"currency": reference_doc.currency,
|
|
||||||
"links": {"mandate": self.data.get("mandate")},
|
|
||||||
"metadata": {
|
|
||||||
"reference_doctype": reference_doc.doctype,
|
|
||||||
"reference_document": reference_doc.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
headers={
|
|
||||||
"Idempotency-Key": self.data.get("reference_docname"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
payment.status == "pending_submission"
|
|
||||||
or payment.status == "pending_customer_approval"
|
|
||||||
or payment.status == "submitted"
|
|
||||||
):
|
|
||||||
self.integration_request.db_set("status", "Authorized", update_modified=False)
|
|
||||||
self.flags.status_changed_to = "Completed"
|
|
||||||
self.integration_request.db_set("output", payment.status, update_modified=False)
|
|
||||||
|
|
||||||
elif payment.status == "confirmed" or payment.status == "paid_out":
|
|
||||||
self.integration_request.db_set("status", "Completed", update_modified=False)
|
|
||||||
self.flags.status_changed_to = "Completed"
|
|
||||||
self.integration_request.db_set("output", payment.status, update_modified=False)
|
|
||||||
|
|
||||||
elif (
|
|
||||||
payment.status == "cancelled"
|
|
||||||
or payment.status == "customer_approval_denied"
|
|
||||||
or payment.status == "charged_back"
|
|
||||||
):
|
|
||||||
self.integration_request.db_set("status", "Cancelled", update_modified=False)
|
|
||||||
frappe.log_error("Gocardless payment cancelled")
|
|
||||||
self.integration_request.db_set("error", payment.status, update_modified=False)
|
|
||||||
else:
|
|
||||||
self.integration_request.db_set("status", "Failed", update_modified=False)
|
|
||||||
frappe.log_error("Gocardless payment failed")
|
|
||||||
self.integration_request.db_set("error", payment.status, update_modified=False)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
frappe.log_error("GoCardless Payment Error")
|
|
||||||
|
|
||||||
if self.flags.status_changed_to == "Completed":
|
|
||||||
status = "Completed"
|
|
||||||
if "reference_doctype" in self.data and "reference_docname" in self.data:
|
|
||||||
custom_redirect_to = None
|
|
||||||
try:
|
|
||||||
custom_redirect_to = frappe.get_doc(
|
|
||||||
self.data.get("reference_doctype"), self.data.get("reference_docname")
|
|
||||||
).run_method("on_payment_authorized", self.flags.status_changed_to)
|
|
||||||
except Exception:
|
|
||||||
frappe.log_error("Gocardless redirect failed")
|
|
||||||
|
|
||||||
if custom_redirect_to:
|
|
||||||
redirect_to = custom_redirect_to
|
|
||||||
|
|
||||||
redirect_url = redirect_to
|
|
||||||
else:
|
|
||||||
status = "Error"
|
|
||||||
redirect_url = "payment-failed"
|
|
||||||
|
|
||||||
if redirect_message:
|
|
||||||
redirect_url += "&" + urlencode({"redirect_message": redirect_message})
|
|
||||||
|
|
||||||
redirect_url = get_url(redirect_url)
|
|
||||||
|
|
||||||
return {"redirect_to": redirect_url, "status": status}
|
|
||||||
|
|
||||||
|
|
||||||
def get_gateway_controller(doc):
|
|
||||||
payment_request = frappe.get_doc("Payment Request", doc)
|
|
||||||
gateway_controller = frappe.db.get_value(
|
|
||||||
"Payment Gateway", payment_request.payment_gateway, "gateway_controller"
|
|
||||||
)
|
|
||||||
return gateway_controller
|
|
||||||
|
|
||||||
|
|
||||||
def gocardless_initialization(doc):
|
|
||||||
gateway_controller = get_gateway_controller(doc)
|
|
||||||
settings = frappe.get_doc("GoCardless Settings", gateway_controller)
|
|
||||||
client = settings.initialize_client()
|
|
||||||
return client
|
|
@ -1,8 +0,0 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies and Contributors
|
|
||||||
# See license.txt
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class TestGoCardlessSettings(unittest.TestCase):
|
|
||||||
pass
|
|
@ -1,28 +0,0 @@
|
|||||||
|
|
||||||
{% if not jQuery.isEmptyObject(data) %}
|
|
||||||
<h5 style="margin-top: 20px;"> {{ __("Balance Details") }} </h5>
|
|
||||||
<table class="table table-bordered small">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="width: 20%">{{ __("Account Type") }}</th>
|
|
||||||
<th style="width: 20%" class="text-right">{{ __("Current Balance") }}</th>
|
|
||||||
<th style="width: 20%" class="text-right">{{ __("Available Balance") }}</th>
|
|
||||||
<th style="width: 20%" class="text-right">{{ __("Reserved Balance") }}</th>
|
|
||||||
<th style="width: 20%" class="text-right">{{ __("Uncleared Balance") }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for(const [key, value] of Object.entries(data)) { %}
|
|
||||||
<tr>
|
|
||||||
<td> {%= key %} </td>
|
|
||||||
<td class="text-right"> {%= value["current_balance"] %} </td>
|
|
||||||
<td class="text-right"> {%= value["available_balance"] %} </td>
|
|
||||||
<td class="text-right"> {%= value["reserved_balance"] %} </td>
|
|
||||||
<td class="text-right"> {%= value["uncleared_balance"] %} </td>
|
|
||||||
</tr>
|
|
||||||
{% } %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<p style="margin-top: 30px;"> Account Balance Information Not Available. </p>
|
|
||||||
{% endif %}
|
|
@ -1,149 +0,0 @@
|
|||||||
import base64
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from requests.auth import HTTPBasicAuth
|
|
||||||
|
|
||||||
|
|
||||||
class MpesaConnector:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
env="sandbox",
|
|
||||||
app_key=None,
|
|
||||||
app_secret=None,
|
|
||||||
sandbox_url="https://sandbox.safaricom.co.ke",
|
|
||||||
live_url="https://api.safaricom.co.ke",
|
|
||||||
):
|
|
||||||
"""Setup configuration for Mpesa connector and generate new access token."""
|
|
||||||
self.env = env
|
|
||||||
self.app_key = app_key
|
|
||||||
self.app_secret = app_secret
|
|
||||||
if env == "sandbox":
|
|
||||||
self.base_url = sandbox_url
|
|
||||||
else:
|
|
||||||
self.base_url = live_url
|
|
||||||
self.authenticate()
|
|
||||||
|
|
||||||
def authenticate(self):
|
|
||||||
"""
|
|
||||||
This method is used to fetch the access token required by Mpesa.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
access_token (str): This token is to be used with the Bearer header for further API calls to Mpesa.
|
|
||||||
"""
|
|
||||||
authenticate_uri = "/oauth/v1/generate?grant_type=client_credentials"
|
|
||||||
authenticate_url = "{0}{1}".format(self.base_url, authenticate_uri)
|
|
||||||
r = requests.get(authenticate_url, auth=HTTPBasicAuth(self.app_key, self.app_secret))
|
|
||||||
self.authentication_token = r.json()["access_token"]
|
|
||||||
return r.json()["access_token"]
|
|
||||||
|
|
||||||
def get_balance(
|
|
||||||
self,
|
|
||||||
initiator=None,
|
|
||||||
security_credential=None,
|
|
||||||
party_a=None,
|
|
||||||
identifier_type=None,
|
|
||||||
remarks=None,
|
|
||||||
queue_timeout_url=None,
|
|
||||||
result_url=None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
This method uses Mpesa's Account Balance API to to enquire the balance on a M-Pesa BuyGoods (Till Number).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
initiator (str): Username used to authenticate the transaction.
|
|
||||||
security_credential (str): Generate from developer portal.
|
|
||||||
command_id (str): AccountBalance.
|
|
||||||
party_a (int): Till number being queried.
|
|
||||||
identifier_type (int): Type of organization receiving the transaction. (MSISDN/Till Number/Organization short code)
|
|
||||||
remarks (str): Comments that are sent along with the transaction(maximum 100 characters).
|
|
||||||
queue_timeout_url (str): The url that handles information of timed out transactions.
|
|
||||||
result_url (str): The url that receives results from M-Pesa api call.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
OriginatorConverstionID (str): The unique request ID for tracking a transaction.
|
|
||||||
ConversationID (str): The unique request ID returned by mpesa for each request made
|
|
||||||
ResponseDescription (str): Response Description message
|
|
||||||
"""
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"Initiator": initiator,
|
|
||||||
"SecurityCredential": security_credential,
|
|
||||||
"CommandID": "AccountBalance",
|
|
||||||
"PartyA": party_a,
|
|
||||||
"IdentifierType": identifier_type,
|
|
||||||
"Remarks": remarks,
|
|
||||||
"QueueTimeOutURL": queue_timeout_url,
|
|
||||||
"ResultURL": result_url,
|
|
||||||
}
|
|
||||||
headers = {
|
|
||||||
"Authorization": "Bearer {0}".format(self.authentication_token),
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
saf_url = "{0}{1}".format(self.base_url, "/mpesa/accountbalance/v1/query")
|
|
||||||
r = requests.post(saf_url, headers=headers, json=payload)
|
|
||||||
return r.json()
|
|
||||||
|
|
||||||
def stk_push(
|
|
||||||
self,
|
|
||||||
business_shortcode=None,
|
|
||||||
passcode=None,
|
|
||||||
amount=None,
|
|
||||||
callback_url=None,
|
|
||||||
reference_code=None,
|
|
||||||
phone_number=None,
|
|
||||||
description=None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
This method uses Mpesa's Express API to initiate online payment on behalf of a customer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
business_shortcode (int): The short code of the organization.
|
|
||||||
passcode (str): Get from developer portal
|
|
||||||
amount (int): The amount being transacted
|
|
||||||
callback_url (str): A CallBack URL is a valid secure URL that is used to receive notifications from M-Pesa API.
|
|
||||||
reference_code(str): Account Reference: This is an Alpha-Numeric parameter that is defined by your system as an Identifier of the transaction for CustomerPayBillOnline transaction type.
|
|
||||||
phone_number(int): The Mobile Number to receive the STK Pin Prompt.
|
|
||||||
description(str): This is any additional information/comment that can be sent along with the request from your system. MAX 13 characters
|
|
||||||
|
|
||||||
Success Response:
|
|
||||||
CustomerMessage(str): Messages that customers can understand.
|
|
||||||
CheckoutRequestID(str): This is a global unique identifier of the processed checkout transaction request.
|
|
||||||
ResponseDescription(str): Describes Success or failure
|
|
||||||
MerchantRequestID(str): This is a global unique Identifier for any submitted payment request.
|
|
||||||
ResponseCode(int): 0 means success all others are error codes. e.g.404.001.03
|
|
||||||
|
|
||||||
Error Reponse:
|
|
||||||
requestId(str): This is a unique requestID for the payment request
|
|
||||||
errorCode(str): This is a predefined code that indicates the reason for request failure.
|
|
||||||
errorMessage(str): This is a predefined code that indicates the reason for request failure.
|
|
||||||
"""
|
|
||||||
|
|
||||||
time = (
|
|
||||||
str(datetime.datetime.now()).split(".")[0].replace("-", "").replace(" ", "").replace(":", "")
|
|
||||||
)
|
|
||||||
password = "{0}{1}{2}".format(str(business_shortcode), str(passcode), time)
|
|
||||||
encoded = base64.b64encode(bytes(password, encoding="utf8"))
|
|
||||||
payload = {
|
|
||||||
"BusinessShortCode": business_shortcode,
|
|
||||||
"Password": encoded.decode("utf-8"),
|
|
||||||
"Timestamp": time,
|
|
||||||
"Amount": amount,
|
|
||||||
"PartyA": int(phone_number),
|
|
||||||
"PartyB": reference_code,
|
|
||||||
"PhoneNumber": int(phone_number),
|
|
||||||
"CallBackURL": callback_url,
|
|
||||||
"AccountReference": reference_code,
|
|
||||||
"TransactionDesc": description,
|
|
||||||
"TransactionType": "CustomerPayBillOnline"
|
|
||||||
if self.env == "sandbox"
|
|
||||||
else "CustomerBuyGoodsOnline",
|
|
||||||
}
|
|
||||||
headers = {
|
|
||||||
"Authorization": "Bearer {0}".format(self.authentication_token),
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
saf_url = "{0}{1}".format(self.base_url, "/mpesa/stkpush/v1/processrequest")
|
|
||||||
r = requests.post(saf_url, headers=headers, json=payload)
|
|
||||||
return r.json()
|
|
@ -1,56 +0,0 @@
|
|||||||
import frappe
|
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
|
||||||
|
|
||||||
|
|
||||||
def create_custom_pos_fields():
|
|
||||||
"""Create custom fields corresponding to POS Settings and POS Invoice."""
|
|
||||||
pos_field = {
|
|
||||||
"POS Invoice": [
|
|
||||||
{
|
|
||||||
"fieldname": "request_for_payment",
|
|
||||||
"label": "Request for Payment",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"hidden": 1,
|
|
||||||
"insert_after": "contact_email",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "mpesa_receipt_number",
|
|
||||||
"label": "Mpesa Receipt Number",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"read_only": 1,
|
|
||||||
"insert_after": "company",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
if not frappe.get_meta("POS Invoice").has_field("request_for_payment"):
|
|
||||||
create_custom_fields(pos_field)
|
|
||||||
|
|
||||||
record_dict = [
|
|
||||||
{
|
|
||||||
"doctype": "POS Field",
|
|
||||||
"fieldname": "contact_mobile",
|
|
||||||
"label": "Mobile No",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"options": "Phone",
|
|
||||||
"parenttype": "POS Settings",
|
|
||||||
"parent": "POS Settings",
|
|
||||||
"parentfield": "invoice_fields",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"doctype": "POS Field",
|
|
||||||
"fieldname": "request_for_payment",
|
|
||||||
"label": "Request for Payment",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"parenttype": "POS Settings",
|
|
||||||
"parent": "POS Settings",
|
|
||||||
"parentfield": "invoice_fields",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
create_pos_settings(record_dict)
|
|
||||||
|
|
||||||
|
|
||||||
def create_pos_settings(record_dict):
|
|
||||||
for record in record_dict:
|
|
||||||
if frappe.db.exists("POS Field", {"fieldname": record.get("fieldname")}):
|
|
||||||
continue
|
|
||||||
frappe.get_doc(record).insert()
|
|
@ -1,39 +0,0 @@
|
|||||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
// For license information, please see license.txt
|
|
||||||
|
|
||||||
frappe.ui.form.on('Mpesa Settings', {
|
|
||||||
onload_post_render: function(frm) {
|
|
||||||
frm.events.setup_account_balance_html(frm);
|
|
||||||
},
|
|
||||||
|
|
||||||
refresh: function(frm) {
|
|
||||||
erpnext.utils.check_payments_app();
|
|
||||||
|
|
||||||
frappe.realtime.on("refresh_mpesa_dashboard", function(){
|
|
||||||
frm.reload_doc();
|
|
||||||
frm.events.setup_account_balance_html(frm);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
get_account_balance: function(frm) {
|
|
||||||
if (!frm.doc.initiator_name && !frm.doc.security_credential) {
|
|
||||||
frappe.throw(__("Please set the initiator name and the security credential"));
|
|
||||||
}
|
|
||||||
frappe.call({
|
|
||||||
method: "get_account_balance_info",
|
|
||||||
doc: frm.doc
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setup_account_balance_html: function(frm) {
|
|
||||||
if (!frm.doc.account_balance) return;
|
|
||||||
$("div").remove(".form-dashboard-section.custom");
|
|
||||||
frm.dashboard.add_section(
|
|
||||||
frappe.render_template('account_balance', {
|
|
||||||
data: JSON.parse(frm.doc.account_balance)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
frm.dashboard.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
@ -1,152 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"autoname": "field:payment_gateway_name",
|
|
||||||
"creation": "2020-09-10 13:21:27.398088",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"payment_gateway_name",
|
|
||||||
"consumer_key",
|
|
||||||
"consumer_secret",
|
|
||||||
"initiator_name",
|
|
||||||
"till_number",
|
|
||||||
"transaction_limit",
|
|
||||||
"sandbox",
|
|
||||||
"column_break_4",
|
|
||||||
"business_shortcode",
|
|
||||||
"online_passkey",
|
|
||||||
"security_credential",
|
|
||||||
"get_account_balance",
|
|
||||||
"account_balance"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "payment_gateway_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Payment Gateway Name",
|
|
||||||
"reqd": 1,
|
|
||||||
"unique": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "consumer_key",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Consumer Key",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "consumer_secret",
|
|
||||||
"fieldtype": "Password",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Consumer Secret",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "till_number",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Till Number",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "sandbox",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Sandbox"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_4",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "online_passkey",
|
|
||||||
"fieldtype": "Password",
|
|
||||||
"label": " Online PassKey",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "initiator_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Initiator Name"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "security_credential",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Security Credential"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "account_balance",
|
|
||||||
"fieldtype": "Long Text",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Account Balance",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "get_account_balance",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"label": "Get Account Balance"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval:(doc.sandbox==0)",
|
|
||||||
"fieldname": "business_shortcode",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Business Shortcode",
|
|
||||||
"mandatory_depends_on": "eval:(doc.sandbox==0)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "150000",
|
|
||||||
"fieldname": "transaction_limit",
|
|
||||||
"fieldtype": "Float",
|
|
||||||
"label": "Transaction Limit",
|
|
||||||
"non_negative": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-03-02 17:35:14.084342",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "ERPNext Integrations",
|
|
||||||
"name": "Mpesa Settings",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Accounts Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Accounts User",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
@ -1,354 +0,0 @@
|
|||||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
|
|
||||||
from json import dumps, loads
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe import _
|
|
||||||
from frappe.integrations.utils import create_request_log
|
|
||||||
from frappe.model.document import Document
|
|
||||||
from frappe.utils import call_hook_method, fmt_money, get_request_site_address
|
|
||||||
|
|
||||||
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_connector import MpesaConnector
|
|
||||||
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_custom_fields import (
|
|
||||||
create_custom_pos_fields,
|
|
||||||
)
|
|
||||||
from erpnext.erpnext_integrations.utils import create_mode_of_payment
|
|
||||||
from erpnext.utilities import payment_app_import_guard
|
|
||||||
|
|
||||||
|
|
||||||
class MpesaSettings(Document):
|
|
||||||
supported_currencies = ["KES"]
|
|
||||||
|
|
||||||
def validate_transaction_currency(self, currency):
|
|
||||||
if currency not in self.supported_currencies:
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Please select another payment method. Mpesa does not support transactions in currency '{0}'"
|
|
||||||
).format(currency)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_update(self):
|
|
||||||
with payment_app_import_guard():
|
|
||||||
from payments.utils import create_payment_gateway
|
|
||||||
|
|
||||||
create_custom_pos_fields()
|
|
||||||
create_payment_gateway(
|
|
||||||
"Mpesa-" + self.payment_gateway_name,
|
|
||||||
settings="Mpesa Settings",
|
|
||||||
controller=self.payment_gateway_name,
|
|
||||||
)
|
|
||||||
call_hook_method(
|
|
||||||
"payment_gateway_enabled", gateway="Mpesa-" + self.payment_gateway_name, payment_channel="Phone"
|
|
||||||
)
|
|
||||||
|
|
||||||
# required to fetch the bank account details from the payment gateway account
|
|
||||||
frappe.db.commit()
|
|
||||||
create_mode_of_payment("Mpesa-" + self.payment_gateway_name, payment_type="Phone")
|
|
||||||
|
|
||||||
def request_for_payment(self, **kwargs):
|
|
||||||
args = frappe._dict(kwargs)
|
|
||||||
request_amounts = self.split_request_amount_according_to_transaction_limit(args)
|
|
||||||
|
|
||||||
for i, amount in enumerate(request_amounts):
|
|
||||||
args.request_amount = amount
|
|
||||||
if frappe.flags.in_test:
|
|
||||||
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import (
|
|
||||||
get_payment_request_response_payload,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = frappe._dict(get_payment_request_response_payload(amount))
|
|
||||||
else:
|
|
||||||
response = frappe._dict(generate_stk_push(**args))
|
|
||||||
|
|
||||||
self.handle_api_response("CheckoutRequestID", args, response)
|
|
||||||
|
|
||||||
def split_request_amount_according_to_transaction_limit(self, args):
|
|
||||||
request_amount = args.request_amount
|
|
||||||
if request_amount > self.transaction_limit:
|
|
||||||
# make multiple requests
|
|
||||||
request_amounts = []
|
|
||||||
requests_to_be_made = frappe.utils.ceil(
|
|
||||||
request_amount / self.transaction_limit
|
|
||||||
) # 480/150 = ceil(3.2) = 4
|
|
||||||
for i in range(requests_to_be_made):
|
|
||||||
amount = self.transaction_limit
|
|
||||||
if i == requests_to_be_made - 1:
|
|
||||||
amount = request_amount - (
|
|
||||||
self.transaction_limit * i
|
|
||||||
) # for 4th request, 480 - (150 * 3) = 30
|
|
||||||
request_amounts.append(amount)
|
|
||||||
else:
|
|
||||||
request_amounts = [request_amount]
|
|
||||||
|
|
||||||
return request_amounts
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_account_balance_info(self):
|
|
||||||
payload = dict(
|
|
||||||
reference_doctype="Mpesa Settings", reference_docname=self.name, doc_details=vars(self)
|
|
||||||
)
|
|
||||||
|
|
||||||
if frappe.flags.in_test:
|
|
||||||
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import (
|
|
||||||
get_test_account_balance_response,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = frappe._dict(get_test_account_balance_response())
|
|
||||||
else:
|
|
||||||
response = frappe._dict(get_account_balance(payload))
|
|
||||||
|
|
||||||
self.handle_api_response("ConversationID", payload, response)
|
|
||||||
|
|
||||||
def handle_api_response(self, global_id, request_dict, response):
|
|
||||||
"""Response received from API calls returns a global identifier for each transaction, this code is returned during the callback."""
|
|
||||||
# check error response
|
|
||||||
if getattr(response, "requestId"):
|
|
||||||
req_name = getattr(response, "requestId")
|
|
||||||
error = response
|
|
||||||
else:
|
|
||||||
# global checkout id used as request name
|
|
||||||
req_name = getattr(response, global_id)
|
|
||||||
error = None
|
|
||||||
|
|
||||||
if not frappe.db.exists("Integration Request", req_name):
|
|
||||||
create_request_log(request_dict, "Host", "Mpesa", req_name, error)
|
|
||||||
|
|
||||||
if error:
|
|
||||||
frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
|
|
||||||
|
|
||||||
|
|
||||||
def generate_stk_push(**kwargs):
|
|
||||||
"""Generate stk push by making a API call to the stk push API."""
|
|
||||||
args = frappe._dict(kwargs)
|
|
||||||
try:
|
|
||||||
callback_url = (
|
|
||||||
get_request_site_address(True)
|
|
||||||
+ "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.verify_transaction"
|
|
||||||
)
|
|
||||||
|
|
||||||
mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
|
|
||||||
env = "production" if not mpesa_settings.sandbox else "sandbox"
|
|
||||||
# for sandbox, business shortcode is same as till number
|
|
||||||
business_shortcode = (
|
|
||||||
mpesa_settings.business_shortcode if env == "production" else mpesa_settings.till_number
|
|
||||||
)
|
|
||||||
|
|
||||||
connector = MpesaConnector(
|
|
||||||
env=env,
|
|
||||||
app_key=mpesa_settings.consumer_key,
|
|
||||||
app_secret=mpesa_settings.get_password("consumer_secret"),
|
|
||||||
)
|
|
||||||
|
|
||||||
mobile_number = sanitize_mobile_number(args.sender)
|
|
||||||
|
|
||||||
response = connector.stk_push(
|
|
||||||
business_shortcode=business_shortcode,
|
|
||||||
amount=args.request_amount,
|
|
||||||
passcode=mpesa_settings.get_password("online_passkey"),
|
|
||||||
callback_url=callback_url,
|
|
||||||
reference_code=mpesa_settings.till_number,
|
|
||||||
phone_number=mobile_number,
|
|
||||||
description="POS Payment",
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
frappe.log_error("Mpesa Express Transaction Error")
|
|
||||||
frappe.throw(
|
|
||||||
_("Issue detected with Mpesa configuration, check the error logs for more details"),
|
|
||||||
title=_("Mpesa Express Error"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_mobile_number(number):
|
|
||||||
"""Add country code and strip leading zeroes from the phone number."""
|
|
||||||
return "254" + str(number).lstrip("0")
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def verify_transaction(**kwargs):
|
|
||||||
"""Verify the transaction result received via callback from stk."""
|
|
||||||
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
|
|
||||||
|
|
||||||
checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
|
|
||||||
if not isinstance(checkout_id, str):
|
|
||||||
frappe.throw(_("Invalid Checkout Request ID"))
|
|
||||||
|
|
||||||
integration_request = frappe.get_doc("Integration Request", checkout_id)
|
|
||||||
transaction_data = frappe._dict(loads(integration_request.data))
|
|
||||||
total_paid = 0 # for multiple integration request made against a pos invoice
|
|
||||||
success = False # for reporting successfull callback to point of sale ui
|
|
||||||
|
|
||||||
if transaction_response["ResultCode"] == 0:
|
|
||||||
if integration_request.reference_doctype and integration_request.reference_docname:
|
|
||||||
try:
|
|
||||||
item_response = transaction_response["CallbackMetadata"]["Item"]
|
|
||||||
amount = fetch_param_value(item_response, "Amount", "Name")
|
|
||||||
mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
|
|
||||||
pr = frappe.get_doc(
|
|
||||||
integration_request.reference_doctype, integration_request.reference_docname
|
|
||||||
)
|
|
||||||
|
|
||||||
mpesa_receipts, completed_payments = get_completed_integration_requests_info(
|
|
||||||
integration_request.reference_doctype, integration_request.reference_docname, checkout_id
|
|
||||||
)
|
|
||||||
|
|
||||||
total_paid = amount + sum(completed_payments)
|
|
||||||
mpesa_receipts = ", ".join(mpesa_receipts + [mpesa_receipt])
|
|
||||||
|
|
||||||
if total_paid >= pr.grand_total:
|
|
||||||
pr.run_method("on_payment_authorized", "Completed")
|
|
||||||
success = True
|
|
||||||
|
|
||||||
frappe.db.set_value("POS Invoice", pr.reference_name, "mpesa_receipt_number", mpesa_receipts)
|
|
||||||
integration_request.handle_success(transaction_response)
|
|
||||||
except Exception:
|
|
||||||
integration_request.handle_failure(transaction_response)
|
|
||||||
frappe.log_error("Mpesa: Failed to verify transaction")
|
|
||||||
|
|
||||||
else:
|
|
||||||
integration_request.handle_failure(transaction_response)
|
|
||||||
|
|
||||||
frappe.publish_realtime(
|
|
||||||
event="process_phone_payment",
|
|
||||||
doctype="POS Invoice",
|
|
||||||
docname=transaction_data.payment_reference,
|
|
||||||
user=integration_request.owner,
|
|
||||||
message={
|
|
||||||
"amount": total_paid,
|
|
||||||
"success": success,
|
|
||||||
"failure_message": transaction_response["ResultDesc"]
|
|
||||||
if transaction_response["ResultCode"] != 0
|
|
||||||
else "",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_completed_integration_requests_info(reference_doctype, reference_docname, checkout_id):
|
|
||||||
output_of_other_completed_requests = frappe.get_all(
|
|
||||||
"Integration Request",
|
|
||||||
filters={
|
|
||||||
"name": ["!=", checkout_id],
|
|
||||||
"reference_doctype": reference_doctype,
|
|
||||||
"reference_docname": reference_docname,
|
|
||||||
"status": "Completed",
|
|
||||||
},
|
|
||||||
pluck="output",
|
|
||||||
)
|
|
||||||
|
|
||||||
mpesa_receipts, completed_payments = [], []
|
|
||||||
|
|
||||||
for out in output_of_other_completed_requests:
|
|
||||||
out = frappe._dict(loads(out))
|
|
||||||
item_response = out["CallbackMetadata"]["Item"]
|
|
||||||
completed_amount = fetch_param_value(item_response, "Amount", "Name")
|
|
||||||
completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
|
|
||||||
completed_payments.append(completed_amount)
|
|
||||||
mpesa_receipts.append(completed_mpesa_receipt)
|
|
||||||
|
|
||||||
return mpesa_receipts, completed_payments
|
|
||||||
|
|
||||||
|
|
||||||
def get_account_balance(request_payload):
|
|
||||||
"""Call account balance API to send the request to the Mpesa Servers."""
|
|
||||||
try:
|
|
||||||
mpesa_settings = frappe.get_doc("Mpesa Settings", request_payload.get("reference_docname"))
|
|
||||||
env = "production" if not mpesa_settings.sandbox else "sandbox"
|
|
||||||
connector = MpesaConnector(
|
|
||||||
env=env,
|
|
||||||
app_key=mpesa_settings.consumer_key,
|
|
||||||
app_secret=mpesa_settings.get_password("consumer_secret"),
|
|
||||||
)
|
|
||||||
|
|
||||||
callback_url = (
|
|
||||||
get_request_site_address(True)
|
|
||||||
+ "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = connector.get_balance(
|
|
||||||
mpesa_settings.initiator_name,
|
|
||||||
mpesa_settings.security_credential,
|
|
||||||
mpesa_settings.till_number,
|
|
||||||
4,
|
|
||||||
mpesa_settings.name,
|
|
||||||
callback_url,
|
|
||||||
callback_url,
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
except Exception:
|
|
||||||
frappe.log_error("Mpesa: Failed to get account balance")
|
|
||||||
frappe.throw(_("Please check your configuration and try again"), title=_("Error"))
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def process_balance_info(**kwargs):
|
|
||||||
"""Process and store account balance information received via callback from the account balance API call."""
|
|
||||||
account_balance_response = frappe._dict(kwargs["Result"])
|
|
||||||
|
|
||||||
conversation_id = getattr(account_balance_response, "ConversationID", "")
|
|
||||||
if not isinstance(conversation_id, str):
|
|
||||||
frappe.throw(_("Invalid Conversation ID"))
|
|
||||||
|
|
||||||
request = frappe.get_doc("Integration Request", conversation_id)
|
|
||||||
|
|
||||||
if request.status == "Completed":
|
|
||||||
return
|
|
||||||
|
|
||||||
transaction_data = frappe._dict(loads(request.data))
|
|
||||||
|
|
||||||
if account_balance_response["ResultCode"] == 0:
|
|
||||||
try:
|
|
||||||
result_params = account_balance_response["ResultParameters"]["ResultParameter"]
|
|
||||||
|
|
||||||
balance_info = fetch_param_value(result_params, "AccountBalance", "Key")
|
|
||||||
balance_info = format_string_to_json(balance_info)
|
|
||||||
|
|
||||||
ref_doc = frappe.get_doc(transaction_data.reference_doctype, transaction_data.reference_docname)
|
|
||||||
ref_doc.db_set("account_balance", balance_info)
|
|
||||||
|
|
||||||
request.handle_success(account_balance_response)
|
|
||||||
frappe.publish_realtime(
|
|
||||||
"refresh_mpesa_dashboard",
|
|
||||||
doctype="Mpesa Settings",
|
|
||||||
docname=transaction_data.reference_docname,
|
|
||||||
user=transaction_data.owner,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
request.handle_failure(account_balance_response)
|
|
||||||
frappe.log_error(
|
|
||||||
title="Mpesa Account Balance Processing Error", message=account_balance_response
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
request.handle_failure(account_balance_response)
|
|
||||||
|
|
||||||
|
|
||||||
def format_string_to_json(balance_info):
|
|
||||||
"""
|
|
||||||
Format string to json.
|
|
||||||
|
|
||||||
e.g: '''Working Account|KES|481000.00|481000.00|0.00|0.00'''
|
|
||||||
=> {'Working Account': {'current_balance': '481000.00',
|
|
||||||
'available_balance': '481000.00',
|
|
||||||
'reserved_balance': '0.00',
|
|
||||||
'uncleared_balance': '0.00'}}
|
|
||||||
"""
|
|
||||||
balance_dict = frappe._dict()
|
|
||||||
for account_info in balance_info.split("&"):
|
|
||||||
account_info = account_info.split("|")
|
|
||||||
balance_dict[account_info[0]] = dict(
|
|
||||||
current_balance=fmt_money(account_info[2], currency="KES"),
|
|
||||||
available_balance=fmt_money(account_info[3], currency="KES"),
|
|
||||||
reserved_balance=fmt_money(account_info[4], currency="KES"),
|
|
||||||
uncleared_balance=fmt_money(account_info[5], currency="KES"),
|
|
||||||
)
|
|
||||||
return dumps(balance_dict)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_param_value(response, key, key_field):
|
|
||||||
"""Fetch the specified key from list of dictionary. Key is identified via the key field."""
|
|
||||||
for param in response:
|
|
||||||
if param[key_field] == key:
|
|
||||||
return param["Value"]
|
|
@ -1,361 +0,0 @@
|
|||||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# See license.txt
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
from json import dumps
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
|
|
||||||
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
|
||||||
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import (
|
|
||||||
process_balance_info,
|
|
||||||
verify_transaction,
|
|
||||||
)
|
|
||||||
from erpnext.erpnext_integrations.utils import create_mode_of_payment
|
|
||||||
|
|
||||||
|
|
||||||
class TestMpesaSettings(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
# create payment gateway in setup
|
|
||||||
create_mpesa_settings(payment_gateway_name="_Test")
|
|
||||||
create_mpesa_settings(payment_gateway_name="_Account Balance")
|
|
||||||
create_mpesa_settings(payment_gateway_name="Payment")
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
frappe.db.sql("delete from `tabMpesa Settings`")
|
|
||||||
frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'")
|
|
||||||
|
|
||||||
def test_creation_of_payment_gateway(self):
|
|
||||||
mode_of_payment = create_mode_of_payment("Mpesa-_Test", payment_type="Phone")
|
|
||||||
self.assertTrue(frappe.db.exists("Payment Gateway Account", {"payment_gateway": "Mpesa-_Test"}))
|
|
||||||
self.assertTrue(mode_of_payment.name)
|
|
||||||
self.assertEqual(mode_of_payment.type, "Phone")
|
|
||||||
|
|
||||||
def test_processing_of_account_balance(self):
|
|
||||||
mpesa_doc = create_mpesa_settings(payment_gateway_name="_Account Balance")
|
|
||||||
mpesa_doc.get_account_balance_info()
|
|
||||||
|
|
||||||
callback_response = get_account_balance_callback_payload()
|
|
||||||
process_balance_info(**callback_response)
|
|
||||||
integration_request = frappe.get_doc("Integration Request", "AG_20200927_00007cdb1f9fb6494315")
|
|
||||||
|
|
||||||
# test integration request creation and successful update of the status on receiving callback response
|
|
||||||
self.assertTrue(integration_request)
|
|
||||||
self.assertEqual(integration_request.status, "Completed")
|
|
||||||
|
|
||||||
# test formatting of account balance received as string to json with appropriate currency symbol
|
|
||||||
mpesa_doc.reload()
|
|
||||||
self.assertEqual(
|
|
||||||
mpesa_doc.account_balance,
|
|
||||||
dumps(
|
|
||||||
{
|
|
||||||
"Working Account": {
|
|
||||||
"current_balance": "Sh 481,000.00",
|
|
||||||
"available_balance": "Sh 481,000.00",
|
|
||||||
"reserved_balance": "Sh 0.00",
|
|
||||||
"uncleared_balance": "Sh 0.00",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
integration_request.delete()
|
|
||||||
|
|
||||||
def test_processing_of_callback_payload(self):
|
|
||||||
mpesa_account = frappe.db.get_value(
|
|
||||||
"Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account"
|
|
||||||
)
|
|
||||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
|
||||||
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
|
|
||||||
|
|
||||||
pos_invoice = create_pos_invoice(do_not_submit=1)
|
|
||||||
pos_invoice.append(
|
|
||||||
"payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 500}
|
|
||||||
)
|
|
||||||
pos_invoice.contact_mobile = "093456543894"
|
|
||||||
pos_invoice.currency = "KES"
|
|
||||||
pos_invoice.save()
|
|
||||||
|
|
||||||
pr = pos_invoice.create_payment_request()
|
|
||||||
# test payment request creation
|
|
||||||
self.assertEqual(pr.payment_gateway, "Mpesa-Payment")
|
|
||||||
|
|
||||||
# submitting payment request creates integration requests with random id
|
|
||||||
integration_req_ids = frappe.get_all(
|
|
||||||
"Integration Request",
|
|
||||||
filters={
|
|
||||||
"reference_doctype": pr.doctype,
|
|
||||||
"reference_docname": pr.name,
|
|
||||||
},
|
|
||||||
pluck="name",
|
|
||||||
)
|
|
||||||
|
|
||||||
callback_response = get_payment_callback_payload(
|
|
||||||
Amount=500, CheckoutRequestID=integration_req_ids[0]
|
|
||||||
)
|
|
||||||
verify_transaction(**callback_response)
|
|
||||||
# test creation of integration request
|
|
||||||
integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
|
|
||||||
|
|
||||||
# test integration request creation and successful update of the status on receiving callback response
|
|
||||||
self.assertTrue(integration_request)
|
|
||||||
self.assertEqual(integration_request.status, "Completed")
|
|
||||||
|
|
||||||
pos_invoice.reload()
|
|
||||||
integration_request.reload()
|
|
||||||
self.assertEqual(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
|
|
||||||
self.assertEqual(integration_request.status, "Completed")
|
|
||||||
|
|
||||||
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
|
|
||||||
integration_request.delete()
|
|
||||||
pr.reload()
|
|
||||||
pr.cancel()
|
|
||||||
pr.delete()
|
|
||||||
pos_invoice.delete()
|
|
||||||
|
|
||||||
def test_processing_of_multiple_callback_payload(self):
|
|
||||||
mpesa_account = frappe.db.get_value(
|
|
||||||
"Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account"
|
|
||||||
)
|
|
||||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
|
||||||
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
|
|
||||||
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
|
|
||||||
|
|
||||||
pos_invoice = create_pos_invoice(do_not_submit=1)
|
|
||||||
pos_invoice.append(
|
|
||||||
"payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 1000}
|
|
||||||
)
|
|
||||||
pos_invoice.contact_mobile = "093456543894"
|
|
||||||
pos_invoice.currency = "KES"
|
|
||||||
pos_invoice.save()
|
|
||||||
|
|
||||||
pr = pos_invoice.create_payment_request()
|
|
||||||
# test payment request creation
|
|
||||||
self.assertEqual(pr.payment_gateway, "Mpesa-Payment")
|
|
||||||
|
|
||||||
# submitting payment request creates integration requests with random id
|
|
||||||
integration_req_ids = frappe.get_all(
|
|
||||||
"Integration Request",
|
|
||||||
filters={
|
|
||||||
"reference_doctype": pr.doctype,
|
|
||||||
"reference_docname": pr.name,
|
|
||||||
},
|
|
||||||
pluck="name",
|
|
||||||
)
|
|
||||||
|
|
||||||
# create random receipt nos and send it as response to callback handler
|
|
||||||
mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
|
|
||||||
|
|
||||||
integration_requests = []
|
|
||||||
for i in range(len(integration_req_ids)):
|
|
||||||
callback_response = get_payment_callback_payload(
|
|
||||||
Amount=500,
|
|
||||||
CheckoutRequestID=integration_req_ids[i],
|
|
||||||
MpesaReceiptNumber=mpesa_receipt_numbers[i],
|
|
||||||
)
|
|
||||||
# handle response manually
|
|
||||||
verify_transaction(**callback_response)
|
|
||||||
# test completion of integration request
|
|
||||||
integration_request = frappe.get_doc("Integration Request", integration_req_ids[i])
|
|
||||||
self.assertEqual(integration_request.status, "Completed")
|
|
||||||
integration_requests.append(integration_request)
|
|
||||||
|
|
||||||
# check receipt number once all the integration requests are completed
|
|
||||||
pos_invoice.reload()
|
|
||||||
self.assertEqual(pos_invoice.mpesa_receipt_number, ", ".join(mpesa_receipt_numbers))
|
|
||||||
|
|
||||||
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
|
|
||||||
[d.delete() for d in integration_requests]
|
|
||||||
pr.reload()
|
|
||||||
pr.cancel()
|
|
||||||
pr.delete()
|
|
||||||
pos_invoice.delete()
|
|
||||||
|
|
||||||
def test_processing_of_only_one_succes_callback_payload(self):
|
|
||||||
mpesa_account = frappe.db.get_value(
|
|
||||||
"Payment Gateway Account", {"payment_gateway": "Mpesa-Payment"}, "payment_account"
|
|
||||||
)
|
|
||||||
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
|
||||||
frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
|
|
||||||
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
|
|
||||||
|
|
||||||
pos_invoice = create_pos_invoice(do_not_submit=1)
|
|
||||||
pos_invoice.append(
|
|
||||||
"payments", {"mode_of_payment": "Mpesa-Payment", "account": mpesa_account, "amount": 1000}
|
|
||||||
)
|
|
||||||
pos_invoice.contact_mobile = "093456543894"
|
|
||||||
pos_invoice.currency = "KES"
|
|
||||||
pos_invoice.save()
|
|
||||||
|
|
||||||
pr = pos_invoice.create_payment_request()
|
|
||||||
# test payment request creation
|
|
||||||
self.assertEqual(pr.payment_gateway, "Mpesa-Payment")
|
|
||||||
|
|
||||||
# submitting payment request creates integration requests with random id
|
|
||||||
integration_req_ids = frappe.get_all(
|
|
||||||
"Integration Request",
|
|
||||||
filters={
|
|
||||||
"reference_doctype": pr.doctype,
|
|
||||||
"reference_docname": pr.name,
|
|
||||||
},
|
|
||||||
pluck="name",
|
|
||||||
)
|
|
||||||
|
|
||||||
# create random receipt nos and send it as response to callback handler
|
|
||||||
mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
|
|
||||||
|
|
||||||
callback_response = get_payment_callback_payload(
|
|
||||||
Amount=500,
|
|
||||||
CheckoutRequestID=integration_req_ids[0],
|
|
||||||
MpesaReceiptNumber=mpesa_receipt_numbers[0],
|
|
||||||
)
|
|
||||||
# handle response manually
|
|
||||||
verify_transaction(**callback_response)
|
|
||||||
# test completion of integration request
|
|
||||||
integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
|
|
||||||
self.assertEqual(integration_request.status, "Completed")
|
|
||||||
|
|
||||||
# now one request is completed
|
|
||||||
# second integration request fails
|
|
||||||
# now retrying payment request should make only one integration request again
|
|
||||||
pr = pos_invoice.create_payment_request()
|
|
||||||
new_integration_req_ids = frappe.get_all(
|
|
||||||
"Integration Request",
|
|
||||||
filters={
|
|
||||||
"reference_doctype": pr.doctype,
|
|
||||||
"reference_docname": pr.name,
|
|
||||||
"name": ["not in", integration_req_ids],
|
|
||||||
},
|
|
||||||
pluck="name",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(len(new_integration_req_ids), 1)
|
|
||||||
|
|
||||||
frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
|
|
||||||
frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'")
|
|
||||||
pr.reload()
|
|
||||||
pr.cancel()
|
|
||||||
pr.delete()
|
|
||||||
pos_invoice.delete()
|
|
||||||
|
|
||||||
|
|
||||||
def create_mpesa_settings(payment_gateway_name="Express"):
|
|
||||||
if frappe.db.exists("Mpesa Settings", payment_gateway_name):
|
|
||||||
return frappe.get_doc("Mpesa Settings", payment_gateway_name)
|
|
||||||
|
|
||||||
doc = frappe.get_doc(
|
|
||||||
dict( # nosec
|
|
||||||
doctype="Mpesa Settings",
|
|
||||||
sandbox=1,
|
|
||||||
payment_gateway_name=payment_gateway_name,
|
|
||||||
consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
|
|
||||||
consumer_secret="VI1oS3oBGPJfh3JyvLHw",
|
|
||||||
online_passkey="LVI1oS3oBGPJfh3JyvLHwZOd",
|
|
||||||
till_number="174379",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
doc.insert(ignore_permissions=True)
|
|
||||||
return doc
|
|
||||||
|
|
||||||
|
|
||||||
def get_test_account_balance_response():
|
|
||||||
"""Response received after calling the account balance API."""
|
|
||||||
return {
|
|
||||||
"ResultType": 0,
|
|
||||||
"ResultCode": 0,
|
|
||||||
"ResultDesc": "The service request has been accepted successfully.",
|
|
||||||
"OriginatorConversationID": "10816-694520-2",
|
|
||||||
"ConversationID": "AG_20200927_00007cdb1f9fb6494315",
|
|
||||||
"TransactionID": "LGR0000000",
|
|
||||||
"ResultParameters": {
|
|
||||||
"ResultParameter": [
|
|
||||||
{"Key": "ReceiptNo", "Value": "LGR919G2AV"},
|
|
||||||
{"Key": "Conversation ID", "Value": "AG_20170727_00004492b1b6d0078fbe"},
|
|
||||||
{"Key": "FinalisedTime", "Value": 20170727101415},
|
|
||||||
{"Key": "Amount", "Value": 10},
|
|
||||||
{"Key": "TransactionStatus", "Value": "Completed"},
|
|
||||||
{"Key": "ReasonType", "Value": "Salary Payment via API"},
|
|
||||||
{"Key": "TransactionReason"},
|
|
||||||
{"Key": "DebitPartyCharges", "Value": "Fee For B2C Payment|KES|33.00"},
|
|
||||||
{"Key": "DebitAccountType", "Value": "Utility Account"},
|
|
||||||
{"Key": "InitiatedTime", "Value": 20170727101415},
|
|
||||||
{"Key": "Originator Conversation ID", "Value": "19455-773836-1"},
|
|
||||||
{"Key": "CreditPartyName", "Value": "254708374149 - John Doe"},
|
|
||||||
{"Key": "DebitPartyName", "Value": "600134 - Safaricom157"},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ReferenceData": {"ReferenceItem": {"Key": "Occasion", "Value": "aaaa"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_payment_request_response_payload(Amount=500):
|
|
||||||
"""Response received after successfully calling the stk push process request API."""
|
|
||||||
|
|
||||||
CheckoutRequestID = frappe.utils.random_string(10)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"MerchantRequestID": "8071-27184008-1",
|
|
||||||
"CheckoutRequestID": CheckoutRequestID,
|
|
||||||
"ResultCode": 0,
|
|
||||||
"ResultDesc": "The service request is processed successfully.",
|
|
||||||
"CallbackMetadata": {
|
|
||||||
"Item": [
|
|
||||||
{"Name": "Amount", "Value": Amount},
|
|
||||||
{"Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R"},
|
|
||||||
{"Name": "TransactionDate", "Value": 20201006113336},
|
|
||||||
{"Name": "PhoneNumber", "Value": 254723575670},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_payment_callback_payload(
|
|
||||||
Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R"
|
|
||||||
):
|
|
||||||
"""Response received from the server as callback after calling the stkpush process request API."""
|
|
||||||
return {
|
|
||||||
"Body": {
|
|
||||||
"stkCallback": {
|
|
||||||
"MerchantRequestID": "19465-780693-1",
|
|
||||||
"CheckoutRequestID": CheckoutRequestID,
|
|
||||||
"ResultCode": 0,
|
|
||||||
"ResultDesc": "The service request is processed successfully.",
|
|
||||||
"CallbackMetadata": {
|
|
||||||
"Item": [
|
|
||||||
{"Name": "Amount", "Value": Amount},
|
|
||||||
{"Name": "MpesaReceiptNumber", "Value": MpesaReceiptNumber},
|
|
||||||
{"Name": "Balance"},
|
|
||||||
{"Name": "TransactionDate", "Value": 20170727154800},
|
|
||||||
{"Name": "PhoneNumber", "Value": 254721566839},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_account_balance_callback_payload():
|
|
||||||
"""Response received from the server as callback after calling the account balance API."""
|
|
||||||
return {
|
|
||||||
"Result": {
|
|
||||||
"ResultType": 0,
|
|
||||||
"ResultCode": 0,
|
|
||||||
"ResultDesc": "The service request is processed successfully.",
|
|
||||||
"OriginatorConversationID": "16470-170099139-1",
|
|
||||||
"ConversationID": "AG_20200927_00007cdb1f9fb6494315",
|
|
||||||
"TransactionID": "OIR0000000",
|
|
||||||
"ResultParameters": {
|
|
||||||
"ResultParameter": [
|
|
||||||
{"Key": "AccountBalance", "Value": "Working Account|KES|481000.00|481000.00|0.00|0.00"},
|
|
||||||
{"Key": "BOCompletedTime", "Value": 20200927234123},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ReferenceData": {
|
|
||||||
"ReferenceItem": {
|
|
||||||
"Key": "QueueTimeoutURL",
|
|
||||||
"Value": "https://internalsandbox.safaricom.co.ke/mpesa/abresults/v1/submit",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -43,40 +43,6 @@ class TestPlaidSettings(unittest.TestCase):
|
|||||||
add_account_subtype("loan")
|
add_account_subtype("loan")
|
||||||
self.assertEqual(frappe.get_doc("Bank Account Subtype", "loan").name, "loan")
|
self.assertEqual(frappe.get_doc("Bank Account Subtype", "loan").name, "loan")
|
||||||
|
|
||||||
def test_default_bank_account(self):
|
|
||||||
if not frappe.db.exists("Bank", "Citi"):
|
|
||||||
frappe.get_doc({"doctype": "Bank", "bank_name": "Citi"}).insert()
|
|
||||||
|
|
||||||
bank_accounts = {
|
|
||||||
"account": {
|
|
||||||
"subtype": "checking",
|
|
||||||
"mask": "0000",
|
|
||||||
"type": "depository",
|
|
||||||
"id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK",
|
|
||||||
"name": "Plaid Checking",
|
|
||||||
},
|
|
||||||
"account_id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK",
|
|
||||||
"link_session_id": "db673d75-61aa-442a-864f-9b3f174f3725",
|
|
||||||
"accounts": [
|
|
||||||
{
|
|
||||||
"type": "depository",
|
|
||||||
"subtype": "checking",
|
|
||||||
"mask": "0000",
|
|
||||||
"id": "6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK",
|
|
||||||
"name": "Plaid Checking",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"institution": {"institution_id": "ins_6", "name": "Citi"},
|
|
||||||
}
|
|
||||||
|
|
||||||
bank = json.dumps(frappe.get_doc("Bank", "Citi").as_dict(), default=json_handler)
|
|
||||||
company = frappe.db.get_single_value("Global Defaults", "default_company")
|
|
||||||
frappe.db.set_value("Company", company, "default_bank_account", None)
|
|
||||||
|
|
||||||
self.assertRaises(
|
|
||||||
frappe.ValidationError, add_bank_accounts, response=bank_accounts, bank=bank, company=company
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_new_transaction(self):
|
def test_new_transaction(self):
|
||||||
if not frappe.db.exists("Bank", "Citi"):
|
if not frappe.db.exists("Bank", "Citi"):
|
||||||
frappe.get_doc({"doctype": "Bank", "bank_name": "Citi"}).insert()
|
frappe.get_doc({"doctype": "Bank", "bank_name": "Citi"}).insert()
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe import _
|
|
||||||
from frappe.integrations.utils import create_request_log
|
|
||||||
|
|
||||||
from erpnext.utilities import payment_app_import_guard
|
|
||||||
|
|
||||||
|
|
||||||
def create_stripe_subscription(gateway_controller, data):
|
|
||||||
with payment_app_import_guard():
|
|
||||||
import stripe
|
|
||||||
|
|
||||||
stripe_settings = frappe.get_doc("Stripe Settings", gateway_controller)
|
|
||||||
stripe_settings.data = frappe._dict(data)
|
|
||||||
|
|
||||||
stripe.api_key = stripe_settings.get_password(fieldname="secret_key", raise_exception=False)
|
|
||||||
stripe.default_http_client = stripe.http_client.RequestsClient()
|
|
||||||
|
|
||||||
try:
|
|
||||||
stripe_settings.integration_request = create_request_log(stripe_settings.data, "Host", "Stripe")
|
|
||||||
stripe_settings.payment_plans = frappe.get_doc(
|
|
||||||
"Payment Request", stripe_settings.data.reference_docname
|
|
||||||
).subscription_plans
|
|
||||||
return create_subscription_on_stripe(stripe_settings)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
stripe_settings.log_error("Unable to create Stripe subscription")
|
|
||||||
return {
|
|
||||||
"redirect_to": frappe.redirect_to_message(
|
|
||||||
_("Server Error"),
|
|
||||||
_(
|
|
||||||
"It seems that there is an issue with the server's stripe configuration. In case of failure, the amount will get refunded to your account."
|
|
||||||
),
|
|
||||||
),
|
|
||||||
"status": 401,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def create_subscription_on_stripe(stripe_settings):
|
|
||||||
with payment_app_import_guard():
|
|
||||||
import stripe
|
|
||||||
|
|
||||||
items = []
|
|
||||||
for payment_plan in stripe_settings.payment_plans:
|
|
||||||
plan = frappe.db.get_value("Subscription Plan", payment_plan.plan, "product_price_id")
|
|
||||||
items.append({"price": plan, "quantity": payment_plan.qty})
|
|
||||||
|
|
||||||
try:
|
|
||||||
customer = stripe.Customer.create(
|
|
||||||
source=stripe_settings.data.stripe_token_id,
|
|
||||||
description=stripe_settings.data.payer_name,
|
|
||||||
email=stripe_settings.data.payer_email,
|
|
||||||
)
|
|
||||||
|
|
||||||
subscription = stripe.Subscription.create(customer=customer, items=items)
|
|
||||||
|
|
||||||
if subscription.status == "active":
|
|
||||||
stripe_settings.integration_request.db_set("status", "Completed", update_modified=False)
|
|
||||||
stripe_settings.flags.status_changed_to = "Completed"
|
|
||||||
|
|
||||||
else:
|
|
||||||
stripe_settings.integration_request.db_set("status", "Failed", update_modified=False)
|
|
||||||
frappe.log_error(f"Stripe Subscription ID {subscription.id}: Payment failed")
|
|
||||||
except Exception:
|
|
||||||
stripe_settings.integration_request.db_set("status", "Failed", update_modified=False)
|
|
||||||
stripe_settings.log_error("Unable to create Stripe subscription")
|
|
||||||
|
|
||||||
return stripe_settings.finalize_request()
|
|
@ -6,8 +6,6 @@ from urllib.parse import urlparse
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
from erpnext import get_default_company
|
|
||||||
|
|
||||||
|
|
||||||
def validate_webhooks_request(doctype, hmac_key, secret_key="secret"):
|
def validate_webhooks_request(doctype, hmac_key, secret_key="secret"):
|
||||||
def innerfn(fn):
|
def innerfn(fn):
|
||||||
@ -47,35 +45,6 @@ def get_webhook_address(connector_name, method, exclude_uri=False, force_https=F
|
|||||||
return server_url
|
return server_url
|
||||||
|
|
||||||
|
|
||||||
def create_mode_of_payment(gateway, payment_type="General"):
|
|
||||||
payment_gateway_account = frappe.db.get_value(
|
|
||||||
"Payment Gateway Account", {"payment_gateway": gateway}, ["payment_account"]
|
|
||||||
)
|
|
||||||
|
|
||||||
mode_of_payment = frappe.db.exists("Mode of Payment", gateway)
|
|
||||||
if not mode_of_payment and payment_gateway_account:
|
|
||||||
mode_of_payment = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Mode of Payment",
|
|
||||||
"mode_of_payment": gateway,
|
|
||||||
"enabled": 1,
|
|
||||||
"type": payment_type,
|
|
||||||
"accounts": [
|
|
||||||
{
|
|
||||||
"doctype": "Mode of Payment Account",
|
|
||||||
"company": get_default_company(),
|
|
||||||
"default_account": payment_gateway_account,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
mode_of_payment.insert(ignore_permissions=True)
|
|
||||||
|
|
||||||
return mode_of_payment
|
|
||||||
elif mode_of_payment:
|
|
||||||
return frappe.get_doc("Mode of Payment", mode_of_payment)
|
|
||||||
|
|
||||||
|
|
||||||
def get_tracking_url(carrier, tracking_number):
|
def get_tracking_url(carrier, tracking_number):
|
||||||
# Return the formatted Tracking URL.
|
# Return the formatted Tracking URL.
|
||||||
tracking_url = ""
|
tracking_url = ""
|
||||||
|
@ -8,7 +8,6 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint
|
from frappe import _, msgprint
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder import Case
|
|
||||||
from frappe.query_builder.functions import IfNull, Sum
|
from frappe.query_builder.functions import IfNull, Sum
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
@ -1618,21 +1617,13 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
|||||||
table = frappe.qb.DocType("Production Plan")
|
table = frappe.qb.DocType("Production Plan")
|
||||||
child = frappe.qb.DocType("Material Request Plan Item")
|
child = frappe.qb.DocType("Material Request Plan Item")
|
||||||
|
|
||||||
completed_production_plans = get_completed_production_plans()
|
non_completed_production_plans = get_non_completed_production_plans()
|
||||||
|
|
||||||
case = Case()
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(table)
|
frappe.qb.from_(table)
|
||||||
.inner_join(child)
|
.inner_join(child)
|
||||||
.on(table.name == child.parent)
|
.on(table.name == child.parent)
|
||||||
.select(
|
.select(Sum(child.required_bom_qty))
|
||||||
Sum(
|
|
||||||
child.quantity
|
|
||||||
* IfNull(
|
|
||||||
case.when(child.material_request_type == "Purchase", child.conversion_factor).else_(1.0), 1.0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.where(
|
.where(
|
||||||
(table.docstatus == 1)
|
(table.docstatus == 1)
|
||||||
& (child.item_code == item_code)
|
& (child.item_code == item_code)
|
||||||
@ -1641,8 +1632,8 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if completed_production_plans:
|
if non_completed_production_plans:
|
||||||
query = query.where(table.name.notin(completed_production_plans))
|
query = query.where(table.name.isin(non_completed_production_plans))
|
||||||
|
|
||||||
query = query.run()
|
query = query.run()
|
||||||
|
|
||||||
@ -1653,7 +1644,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
|||||||
|
|
||||||
reserved_qty_for_production = flt(
|
reserved_qty_for_production = flt(
|
||||||
get_reserved_qty_for_production(
|
get_reserved_qty_for_production(
|
||||||
item_code, warehouse, completed_production_plans, check_production_plan=True
|
item_code, warehouse, non_completed_production_plans, check_production_plan=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1663,7 +1654,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
|||||||
return reserved_qty_for_production_plan - reserved_qty_for_production
|
return reserved_qty_for_production_plan - reserved_qty_for_production
|
||||||
|
|
||||||
|
|
||||||
def get_completed_production_plans():
|
def get_non_completed_production_plans():
|
||||||
table = frappe.qb.DocType("Production Plan")
|
table = frappe.qb.DocType("Production Plan")
|
||||||
child = frappe.qb.DocType("Production Plan Item")
|
child = frappe.qb.DocType("Production Plan Item")
|
||||||
|
|
||||||
@ -1675,7 +1666,7 @@ def get_completed_production_plans():
|
|||||||
.where(
|
.where(
|
||||||
(table.docstatus == 1)
|
(table.docstatus == 1)
|
||||||
& (table.status.notin(["Completed", "Closed"]))
|
& (table.status.notin(["Completed", "Closed"]))
|
||||||
& (child.ordered_qty >= child.planned_qty)
|
& (child.planned_qty > child.ordered_qty)
|
||||||
)
|
)
|
||||||
).run(as_dict=True)
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@ from frappe.utils import add_to_date, flt, getdate, now_datetime, nowdate
|
|||||||
|
|
||||||
from erpnext.controllers.item_variant import create_variant
|
from erpnext.controllers.item_variant import create_variant
|
||||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||||
get_completed_production_plans,
|
|
||||||
get_items_for_material_requests,
|
get_items_for_material_requests,
|
||||||
|
get_non_completed_production_plans,
|
||||||
get_sales_orders,
|
get_sales_orders,
|
||||||
get_warehouse_list,
|
get_warehouse_list,
|
||||||
)
|
)
|
||||||
@ -1143,9 +1143,9 @@ class TestProductionPlan(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(after_qty, before_qty)
|
self.assertEqual(after_qty, before_qty)
|
||||||
|
|
||||||
completed_plans = get_completed_production_plans()
|
completed_plans = get_non_completed_production_plans()
|
||||||
for plan in plans:
|
for plan in plans:
|
||||||
self.assertTrue(plan in completed_plans)
|
self.assertFalse(plan in completed_plans)
|
||||||
|
|
||||||
def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self):
|
def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self):
|
||||||
from erpnext.stock.utils import get_or_make_bin
|
from erpnext.stock.utils import get_or_make_bin
|
||||||
|
@ -1515,7 +1515,7 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
|
|||||||
def get_reserved_qty_for_production(
|
def get_reserved_qty_for_production(
|
||||||
item_code: str,
|
item_code: str,
|
||||||
warehouse: str,
|
warehouse: str,
|
||||||
completed_production_plans: list = None,
|
non_completed_production_plans: list = None,
|
||||||
check_production_plan: bool = False,
|
check_production_plan: bool = False,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Get total reserved quantity for any item in specified warehouse"""
|
"""Get total reserved quantity for any item in specified warehouse"""
|
||||||
@ -1538,19 +1538,22 @@ def get_reserved_qty_for_production(
|
|||||||
& (wo_item.parent == wo.name)
|
& (wo_item.parent == wo.name)
|
||||||
& (wo.docstatus == 1)
|
& (wo.docstatus == 1)
|
||||||
& (wo_item.source_warehouse == warehouse)
|
& (wo_item.source_warehouse == warehouse)
|
||||||
& (wo.status.notin(["Stopped", "Completed", "Closed"]))
|
|
||||||
& (
|
|
||||||
(wo_item.required_qty > wo_item.transferred_qty)
|
|
||||||
| (wo_item.required_qty > wo_item.consumed_qty)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if check_production_plan:
|
if check_production_plan:
|
||||||
query = query.where(wo.production_plan.isnotnull())
|
query = query.where(wo.production_plan.isnotnull())
|
||||||
|
else:
|
||||||
|
query = query.where(
|
||||||
|
(wo.status.notin(["Stopped", "Completed", "Closed"]))
|
||||||
|
& (
|
||||||
|
(wo_item.required_qty > wo_item.transferred_qty)
|
||||||
|
| (wo_item.required_qty > wo_item.consumed_qty)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if completed_production_plans:
|
if non_completed_production_plans:
|
||||||
query = query.where(wo.production_plan.notin(completed_production_plans))
|
query = query.where(wo.production_plan.isin(non_completed_production_plans))
|
||||||
|
|
||||||
return query.run()[0][0] or 0.0
|
return query.run()[0][0] or 0.0
|
||||||
|
|
||||||
|
@ -344,5 +344,6 @@ erpnext.patches.v15_0.delete_woocommerce_settings_doctype
|
|||||||
erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults
|
erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults
|
||||||
erpnext.patches.v14_0.update_invoicing_period_in_subscription
|
erpnext.patches.v14_0.update_invoicing_period_in_subscription
|
||||||
execute:frappe.delete_doc("Page", "welcome-to-erpnext")
|
execute:frappe.delete_doc("Page", "welcome-to-erpnext")
|
||||||
|
erpnext.patches.v15_0.delete_payment_gateway_doctypes
|
||||||
# below migration patch should always run last
|
# below migration patch should always run last
|
||||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||||
|
6
erpnext/patches/v15_0/delete_payment_gateway_doctypes.py
Normal file
6
erpnext/patches/v15_0/delete_payment_gateway_doctypes.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
for dt in ("GoCardless Settings", "GoCardless Mandate", "Mpesa Settings"):
|
||||||
|
frappe.delete_doc("DocType", dt, ignore_missing=True)
|
@ -116,7 +116,7 @@ erpnext.accounts.taxes = {
|
|||||||
account_head: function(frm, cdt, cdn) {
|
account_head: function(frm, cdt, cdn) {
|
||||||
let d = locals[cdt][cdn];
|
let d = locals[cdt][cdn];
|
||||||
|
|
||||||
if (doc.docstatus == 1) {
|
if (d.docstatus == 1) {
|
||||||
// Should not trigger any changes on change post submit
|
// Should not trigger any changes on change post submit
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,12 @@ frappe.ui.form.on('Material Request', {
|
|||||||
|
|
||||||
if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') {
|
if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') {
|
||||||
let precision = frappe.defaults.get_default("float_precision");
|
let precision = frappe.defaults.get_default("float_precision");
|
||||||
|
|
||||||
|
if (flt(frm.doc.per_received, precision) < 100) {
|
||||||
|
frm.add_custom_button(__('Stop'),
|
||||||
|
() => frm.events.update_status(frm, 'Stopped'));
|
||||||
|
}
|
||||||
|
|
||||||
if (flt(frm.doc.per_ordered, precision) < 100) {
|
if (flt(frm.doc.per_ordered, precision) < 100) {
|
||||||
let add_create_pick_list_button = () => {
|
let add_create_pick_list_button = () => {
|
||||||
frm.add_custom_button(__('Pick List'),
|
frm.add_custom_button(__('Pick List'),
|
||||||
@ -148,11 +154,6 @@ frappe.ui.form.on('Material Request', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
frm.page.set_inner_btn_group_as_primary(__('Create'));
|
frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||||
|
|
||||||
// stop
|
|
||||||
frm.add_custom_button(__('Stop'),
|
|
||||||
() => frm.events.update_status(frm, 'Stopped'));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
$(document).ready(function() {
|
|
||||||
var data = {{ frappe.form_dict | json }};
|
|
||||||
var doctype = "{{ reference_doctype }}"
|
|
||||||
var docname = "{{ reference_docname }}"
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: "erpnext.templates.pages.integrations.gocardless_checkout.check_mandate",
|
|
||||||
freeze: true,
|
|
||||||
headers: {
|
|
||||||
"X-Requested-With": "XMLHttpRequest"
|
|
||||||
},
|
|
||||||
args: {
|
|
||||||
"data": JSON.stringify(data),
|
|
||||||
"reference_doctype": doctype,
|
|
||||||
"reference_docname": docname
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
window.location.href = r.message.redirect_to
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
@ -1,24 +0,0 @@
|
|||||||
$(document).ready(function() {
|
|
||||||
var redirect_flow_id = "{{ redirect_flow_id }}";
|
|
||||||
var doctype = "{{ reference_doctype }}";
|
|
||||||
var docname = "{{ reference_docname }}";
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: "erpnext.templates.pages.integrations.gocardless_confirmation.confirm_payment",
|
|
||||||
freeze: true,
|
|
||||||
headers: {
|
|
||||||
"X-Requested-With": "XMLHttpRequest"
|
|
||||||
},
|
|
||||||
args: {
|
|
||||||
"redirect_flow_id": redirect_flow_id,
|
|
||||||
"reference_doctype": doctype,
|
|
||||||
"reference_docname": docname
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
window.location.href = r.message.redirect_to;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -1,16 +0,0 @@
|
|||||||
{% extends "templates/web.html" %}
|
|
||||||
|
|
||||||
{% block title %} Payment {% endblock %}
|
|
||||||
|
|
||||||
{%- block header -%}{% endblock %}
|
|
||||||
|
|
||||||
{% block script %}
|
|
||||||
<script>{% include "templates/includes/integrations/gocardless_checkout.js" %}</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{%- block page_content -%}
|
|
||||||
<p class='lead text-center'>
|
|
||||||
<span class='gocardless-loading'>{{ _("Loading Payment System") }}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,100 +0,0 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe import _
|
|
||||||
from frappe.utils import flt, get_url
|
|
||||||
|
|
||||||
from erpnext.erpnext_integrations.doctype.gocardless_settings.gocardless_settings import (
|
|
||||||
get_gateway_controller,
|
|
||||||
gocardless_initialization,
|
|
||||||
)
|
|
||||||
|
|
||||||
no_cache = 1
|
|
||||||
|
|
||||||
expected_keys = (
|
|
||||||
"amount",
|
|
||||||
"title",
|
|
||||||
"description",
|
|
||||||
"reference_doctype",
|
|
||||||
"reference_docname",
|
|
||||||
"payer_name",
|
|
||||||
"payer_email",
|
|
||||||
"order_id",
|
|
||||||
"currency",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_context(context):
|
|
||||||
context.no_cache = 1
|
|
||||||
|
|
||||||
# all these keys exist in form_dict
|
|
||||||
if not (set(expected_keys) - set(frappe.form_dict.keys())):
|
|
||||||
for key in expected_keys:
|
|
||||||
context[key] = frappe.form_dict[key]
|
|
||||||
|
|
||||||
context["amount"] = flt(context["amount"])
|
|
||||||
|
|
||||||
gateway_controller = get_gateway_controller(context.reference_docname)
|
|
||||||
context["header_img"] = frappe.db.get_value(
|
|
||||||
"GoCardless Settings", gateway_controller, "header_img"
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
frappe.redirect_to_message(
|
|
||||||
_("Some information is missing"),
|
|
||||||
_("Looks like someone sent you to an incomplete URL. Please ask them to look into it."),
|
|
||||||
)
|
|
||||||
frappe.local.flags.redirect_location = frappe.local.response.location
|
|
||||||
raise frappe.Redirect
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def check_mandate(data, reference_doctype, reference_docname):
|
|
||||||
data = json.loads(data)
|
|
||||||
|
|
||||||
client = gocardless_initialization(reference_docname)
|
|
||||||
|
|
||||||
payer = frappe.get_doc("Customer", data["payer_name"])
|
|
||||||
|
|
||||||
if payer.customer_type == "Individual" and payer.customer_primary_contact is not None:
|
|
||||||
primary_contact = frappe.get_doc("Contact", payer.customer_primary_contact)
|
|
||||||
prefilled_customer = {
|
|
||||||
"company_name": payer.name,
|
|
||||||
"given_name": primary_contact.first_name,
|
|
||||||
}
|
|
||||||
if primary_contact.last_name is not None:
|
|
||||||
prefilled_customer.update({"family_name": primary_contact.last_name})
|
|
||||||
|
|
||||||
if primary_contact.email_id is not None:
|
|
||||||
prefilled_customer.update({"email": primary_contact.email_id})
|
|
||||||
else:
|
|
||||||
prefilled_customer.update({"email": frappe.session.user})
|
|
||||||
|
|
||||||
else:
|
|
||||||
prefilled_customer = {"company_name": payer.name, "email": frappe.session.user}
|
|
||||||
|
|
||||||
success_url = get_url(
|
|
||||||
"./integrations/gocardless_confirmation?reference_doctype="
|
|
||||||
+ reference_doctype
|
|
||||||
+ "&reference_docname="
|
|
||||||
+ reference_docname
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
redirect_flow = client.redirect_flows.create(
|
|
||||||
params={
|
|
||||||
"description": _("Pay {0} {1}").format(data["amount"], data["currency"]),
|
|
||||||
"session_token": frappe.session.user,
|
|
||||||
"success_redirect_url": success_url,
|
|
||||||
"prefilled_customer": prefilled_customer,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"redirect_to": redirect_flow.redirect_url}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
frappe.log_error("GoCardless Payment Error")
|
|
||||||
return {"redirect_to": "/integrations/payment-failed"}
|
|
@ -1,16 +0,0 @@
|
|||||||
{% extends "templates/web.html" %}
|
|
||||||
|
|
||||||
{% block title %} Payment {% endblock %}
|
|
||||||
|
|
||||||
{%- block header -%}{% endblock %}
|
|
||||||
|
|
||||||
{% block script %}
|
|
||||||
<script>{% include "templates/includes/integrations/gocardless_confirmation.js" %}</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{%- block page_content -%}
|
|
||||||
<p class='lead text-center'>
|
|
||||||
<span class='gocardless-loading'>{{ _("Payment Confirmation") }}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,106 +0,0 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe import _
|
|
||||||
|
|
||||||
from erpnext.erpnext_integrations.doctype.gocardless_settings.gocardless_settings import (
|
|
||||||
get_gateway_controller,
|
|
||||||
gocardless_initialization,
|
|
||||||
)
|
|
||||||
|
|
||||||
no_cache = 1
|
|
||||||
|
|
||||||
expected_keys = ("redirect_flow_id", "reference_doctype", "reference_docname")
|
|
||||||
|
|
||||||
|
|
||||||
def get_context(context):
|
|
||||||
context.no_cache = 1
|
|
||||||
|
|
||||||
# all these keys exist in form_dict
|
|
||||||
if not (set(expected_keys) - set(frappe.form_dict.keys())):
|
|
||||||
for key in expected_keys:
|
|
||||||
context[key] = frappe.form_dict[key]
|
|
||||||
|
|
||||||
else:
|
|
||||||
frappe.redirect_to_message(
|
|
||||||
_("Some information is missing"),
|
|
||||||
_("Looks like someone sent you to an incomplete URL. Please ask them to look into it."),
|
|
||||||
)
|
|
||||||
frappe.local.flags.redirect_location = frappe.local.response.location
|
|
||||||
raise frappe.Redirect
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def confirm_payment(redirect_flow_id, reference_doctype, reference_docname):
|
|
||||||
|
|
||||||
client = gocardless_initialization(reference_docname)
|
|
||||||
|
|
||||||
try:
|
|
||||||
redirect_flow = client.redirect_flows.complete(
|
|
||||||
redirect_flow_id, params={"session_token": frappe.session.user}
|
|
||||||
)
|
|
||||||
|
|
||||||
confirmation_url = redirect_flow.confirmation_url
|
|
||||||
gocardless_success_page = frappe.get_hooks("gocardless_success_page")
|
|
||||||
if gocardless_success_page:
|
|
||||||
confirmation_url = frappe.get_attr(gocardless_success_page[-1])(
|
|
||||||
reference_doctype, reference_docname
|
|
||||||
)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"mandate": redirect_flow.links.mandate,
|
|
||||||
"customer": redirect_flow.links.customer,
|
|
||||||
"redirect_to": confirmation_url,
|
|
||||||
"redirect_message": "Mandate successfully created",
|
|
||||||
"reference_doctype": reference_doctype,
|
|
||||||
"reference_docname": reference_docname,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
create_mandate(data)
|
|
||||||
except Exception as e:
|
|
||||||
frappe.log_error("GoCardless Mandate Registration Error")
|
|
||||||
|
|
||||||
gateway_controller = get_gateway_controller(reference_docname)
|
|
||||||
frappe.get_doc("GoCardless Settings", gateway_controller).create_payment_request(data)
|
|
||||||
|
|
||||||
return {"redirect_to": confirmation_url}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
frappe.log_error("GoCardless Payment Error")
|
|
||||||
return {"redirect_to": "/integrations/payment-failed"}
|
|
||||||
|
|
||||||
|
|
||||||
def create_mandate(data):
|
|
||||||
data = frappe._dict(data)
|
|
||||||
frappe.logger().debug(data)
|
|
||||||
|
|
||||||
mandate = data.get("mandate")
|
|
||||||
|
|
||||||
if frappe.db.exists("GoCardless Mandate", mandate):
|
|
||||||
return
|
|
||||||
|
|
||||||
else:
|
|
||||||
reference_doc = frappe.db.get_value(
|
|
||||||
data.get("reference_doctype"),
|
|
||||||
data.get("reference_docname"),
|
|
||||||
["reference_doctype", "reference_name"],
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
erpnext_customer = frappe.db.get_value(
|
|
||||||
reference_doc.reference_doctype, reference_doc.reference_name, ["customer_name"], as_dict=1
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "GoCardless Mandate",
|
|
||||||
"mandate": mandate,
|
|
||||||
"customer": erpnext_customer.customer_name,
|
|
||||||
"gocardless_customer": data.get("customer"),
|
|
||||||
}
|
|
||||||
).insert(ignore_permissions=True)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
frappe.log_error("Gocardless: Unable to create mandate")
|
|
@ -4586,7 +4586,7 @@ ACC-PINV-.YYYY.-,ACC-PINV-.JJJJ.-,
|
|||||||
Tax Withholding Category,Steuereinbehalt Kategorie,
|
Tax Withholding Category,Steuereinbehalt Kategorie,
|
||||||
Edit Posting Date and Time,Buchungsdatum und -uhrzeit bearbeiten,
|
Edit Posting Date and Time,Buchungsdatum und -uhrzeit bearbeiten,
|
||||||
Is Paid,Ist bezahlt,
|
Is Paid,Ist bezahlt,
|
||||||
Is Return (Debit Note),ist Rücklieferung (Lastschrift),
|
Is Return (Debit Note),Ist Rechnungskorrektur (Retoure),
|
||||||
Apply Tax Withholding Amount,Steuereinbehaltungsbetrag anwenden,
|
Apply Tax Withholding Amount,Steuereinbehaltungsbetrag anwenden,
|
||||||
Accounting Dimensions ,Buchhaltung Dimensionen,
|
Accounting Dimensions ,Buchhaltung Dimensionen,
|
||||||
Supplier Invoice Details,Lieferant Rechnungsdetails,
|
Supplier Invoice Details,Lieferant Rechnungsdetails,
|
||||||
@ -4710,7 +4710,7 @@ Item Wise Tax Detail ,Item Wise Tax Detail,
|
|||||||
ACC-SINV-.YYYY.-,ACC-SINV-.JJJJ.-,
|
ACC-SINV-.YYYY.-,ACC-SINV-.JJJJ.-,
|
||||||
Include Payment (POS),(POS) Zahlung einschließen,
|
Include Payment (POS),(POS) Zahlung einschließen,
|
||||||
Offline POS Name,Offline-Verkaufsstellen-Name,
|
Offline POS Name,Offline-Verkaufsstellen-Name,
|
||||||
Is Return (Credit Note),ist Rücklieferung (Gutschrift),
|
Is Return (Credit Note),Ist Rechnungskorrektur (Retoure),
|
||||||
Update Billed Amount in Sales Order,Aktualisierung des Rechnungsbetrags im Auftrag,
|
Update Billed Amount in Sales Order,Aktualisierung des Rechnungsbetrags im Auftrag,
|
||||||
Customer PO Details,Auftragsdetails,
|
Customer PO Details,Auftragsdetails,
|
||||||
Customer's Purchase Order,Bestellung des Kunden,
|
Customer's Purchase Order,Bestellung des Kunden,
|
||||||
@ -6998,7 +6998,7 @@ Customs Tariff Number,Zolltarifnummer,
|
|||||||
Tariff Number,Tarifnummer,
|
Tariff Number,Tarifnummer,
|
||||||
Delivery To,Lieferung an,
|
Delivery To,Lieferung an,
|
||||||
MAT-DN-.YYYY.-,MAT-DN-.YYYY.-,
|
MAT-DN-.YYYY.-,MAT-DN-.YYYY.-,
|
||||||
Is Return,Ist Rückgabe,
|
Is Return,Ist Retoure,
|
||||||
Issue Credit Note,Gutschrift ausgeben,
|
Issue Credit Note,Gutschrift ausgeben,
|
||||||
Return Against Delivery Note,Zurück zum Lieferschein,
|
Return Against Delivery Note,Zurück zum Lieferschein,
|
||||||
Customer's Purchase Order No,Bestellnummer des Kunden,
|
Customer's Purchase Order No,Bestellnummer des Kunden,
|
||||||
|
Can't render this file because it is too large.
|
@ -16,11 +16,9 @@ dependencies = [
|
|||||||
"holidays~=0.28",
|
"holidays~=0.28",
|
||||||
|
|
||||||
# integration dependencies
|
# integration dependencies
|
||||||
"gocardless-pro~=1.22.0",
|
|
||||||
"googlemaps",
|
"googlemaps",
|
||||||
"plaid-python~=7.2.1",
|
"plaid-python~=7.2.1",
|
||||||
"python-youtube~=0.8.0",
|
"python-youtube~=0.8.0",
|
||||||
"tweepy~=4.14.0",
|
|
||||||
|
|
||||||
# Not used directly - required by PyQRCode for PNG generation
|
# Not used directly - required by PyQRCode for PNG generation
|
||||||
"pypng~=0.20220715.0",
|
"pypng~=0.20220715.0",
|
||||||
|
Loading…
Reference in New Issue
Block a user