fix: use Plaid's new API (develop) (#23318)

Co-authored-by: Mangesh-Khairnar <mkhairnar10@gmail.com>
This commit is contained in:
Rohan 2020-09-21 13:14:07 +05:30 committed by GitHub
parent 44c692ed40
commit e42f08085b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 149 additions and 132 deletions

View File

@ -72,7 +72,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation {
check_plaid_status() {
const me = this;
frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => {
if (r && r.enabled == "1") {
if (r && r.enabled === "1") {
me.plaid_status = "active"
} else {
me.plaid_status = "inactive"
@ -139,7 +139,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
}
make() {
const me = this;
const me = this;
new frappe.ui.FileUploader({
method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement',
allow_multiple: 0,
@ -214,31 +214,35 @@ erpnext.accounts.bankTransactionSync = class bankTransactionSync {
init_config() {
const me = this;
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration')
.then(result => {
me.plaid_env = result.plaid_env;
me.plaid_public_key = result.plaid_public_key;
me.client_name = result.client_name;
me.sync_transactions()
})
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration')
.then(result => {
me.plaid_env = result.plaid_env;
me.client_name = result.client_name;
me.link_token = result.link_token;
me.sync_transactions();
})
}
sync_transactions() {
const me = this;
frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (v) => {
frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => {
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', {
bank: v['bank'],
bank: r.bank,
bank_account: me.parent.bank_account,
freeze: true
})
.then((result) => {
let result_title = (result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized")
let result_title = (result && result.length > 0)
? __("{0} bank transaction(s) created", [result.length])
: __("This bank account is already synchronized");
let result_msg = `
<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
<h5 class="text-muted">${result_title}</h5>
</div>`
<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
<h5 class="text-muted">${result_title}</h5>
</div>`
this.parent.$main_section.append(result_msg)
frappe.show_alert({message:__("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator:'green'});
frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' });
})
})
}
@ -384,7 +388,7 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow {
})
frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments',
{bank_transaction: data, freeze:true, freeze_message:__("Finding linked payments")}
{ bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") }
).then((result) => {
me.make_dialog(result)
})

View File

@ -2,81 +2,84 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe import _
from frappe.utils.password import get_decrypted_password
from plaid import Client
from plaid.errors import APIError, ItemError
import plaid
import requests
from plaid.errors import APIError, ItemError, InvalidRequestError
import frappe
import requests
from frappe import _
class PlaidConnector():
def __init__(self, access_token=None):
plaid_settings = frappe.get_single("Plaid Settings")
self.config = {
"plaid_client_id": plaid_settings.plaid_client_id,
"plaid_secret": get_decrypted_password("Plaid Settings", "Plaid Settings", 'plaid_secret'),
"plaid_public_key": plaid_settings.plaid_public_key,
"plaid_env": plaid_settings.plaid_env
}
self.client = Client(client_id=self.config.get("plaid_client_id"),
secret=self.config.get("plaid_secret"),
public_key=self.config.get("plaid_public_key"),
environment=self.config.get("plaid_env")
)
self.access_token = access_token
self.settings = frappe.get_single("Plaid Settings")
self.products = ["auth", "transactions"]
self.client_name = frappe.local.site
self.client = plaid.Client(
client_id=self.settings.plaid_client_id,
secret=self.settings.get_password("plaid_secret"),
environment=self.settings.plaid_env,
api_version="2019-05-29"
)
def get_access_token(self, public_token):
if public_token is None:
frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error"))
response = self.client.Item.public_token.exchange(public_token)
access_token = response['access_token']
access_token = response["access_token"]
return access_token
def get_link_token(self):
token_request = {
"client_name": self.client_name,
"client_id": self.settings.plaid_client_id,
"secret": self.settings.plaid_secret,
"products": self.products,
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
"country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"],
"user": {
"client_user_id": frappe.generate_hash(frappe.session.user, length=32)
}
}
try:
response = self.client.LinkToken.create(token_request)
except InvalidRequestError:
frappe.log_error(frappe.get_traceback(), _("Plaid invalid request error"))
frappe.msgprint(_("Please check your Plaid client ID and secret values"))
except APIError as e:
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
frappe.throw(_(str(e)), title=_("Authentication Failed"))
else:
return response["link_token"]
def auth(self):
try:
self.client.Auth.get(self.access_token)
print("Authentication successful.....")
except ItemError as e:
if e.code == 'ITEM_LOGIN_REQUIRED':
pass
else:
if e.code == "ITEM_LOGIN_REQUIRED":
pass
except APIError as e:
if e.code == 'PLANNED_MAINTENANCE':
pass
else:
if e.code == "PLANNED_MAINTENANCE":
pass
except requests.Timeout:
pass
except Exception as e:
print(e)
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
frappe.msgprint({"title": _("Authentication Failed"), "message":e, "raise_exception":1, "indicator":'red'})
frappe.throw(_(str(e)), title=_("Authentication Failed"))
def get_transactions(self, start_date, end_date, account_id=None):
self.auth()
account_ids = list(account_id) if account_id else None
try:
self.auth()
if account_id:
account_ids = [account_id]
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids)
else:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date)
transactions = response['transactions']
while len(transactions) < response['total_transactions']:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids)
transactions = response["transactions"]
while len(transactions) < response["total_transactions"]:
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
transactions.extend(response['transactions'])
transactions.extend(response["transactions"])
return transactions
except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))

View File

@ -4,14 +4,14 @@
frappe.provide("erpnext.integrations");
frappe.ui.form.on('Plaid Settings', {
enabled: function(frm) {
enabled: function (frm) {
frm.toggle_reqd('plaid_client_id', frm.doc.enabled);
frm.toggle_reqd('plaid_secret', frm.doc.enabled);
frm.toggle_reqd('plaid_public_key', frm.doc.enabled);
frm.toggle_reqd('plaid_env', frm.doc.enabled);
},
refresh: function(frm) {
if(frm.doc.enabled) {
refresh: function (frm) {
if (frm.doc.enabled) {
frm.add_custom_button('Link a new bank account', () => {
new erpnext.integrations.plaidLink(frm);
});
@ -22,17 +22,16 @@ frappe.ui.form.on('Plaid Settings', {
erpnext.integrations.plaidLink = class plaidLink {
constructor(parent) {
this.frm = parent;
this.product = ["transactions", "auth"];
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
this.init_config();
}
init_config() {
const me = this;
me.plaid_env = me.frm.doc.plaid_env;
me.plaid_public_key = me.frm.doc.plaid_public_key;
me.client_name = frappe.boot.sitename;
me.init_plaid();
async init_config() {
this.product = ["auth", "transactions"];
this.plaid_env = this.frm.doc.plaid_env;
this.client_name = frappe.boot.sitename;
this.token = await this.frm.call("get_link_token").then(resp => resp.message);
this.init_plaid();
}
init_plaid() {
@ -69,17 +68,17 @@ erpnext.integrations.plaidLink = class plaidLink {
}
onScriptLoaded(me) {
me.linkHandler = window.Plaid.create({
me.linkHandler = Plaid.create({
clientName: me.client_name,
product: me.product,
env: me.plaid_env,
key: me.plaid_public_key,
onSuccess: me.plaid_success,
product: me.product
token: me.token,
onSuccess: me.plaid_success
});
}
onScriptError(error) {
frappe.msgprint('There was an issue loading the link-initialize.js script');
frappe.msgprint("There was an issue connecting to Plaid's authentication server");
frappe.msgprint(error);
}
@ -87,21 +86,25 @@ erpnext.integrations.plaidLink = class plaidLink {
const me = this;
frappe.prompt({
fieldtype:"Link",
fieldtype: "Link",
options: "Company",
label:__("Company"),
fieldname:"company",
reqd:1
label: __("Company"),
fieldname: "company",
reqd: 1
}, (data) => {
me.company = data.company;
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {token: token, response: response})
.then((result) => {
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {response: response,
bank: result, company: me.company});
})
.then(() => {
frappe.show_alert({message:__("Bank accounts added"), indicator:'green'});
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {
token: token,
response: response
}).then((result) => {
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {
response: response,
bank: result,
company: me.company
});
}).then(() => {
frappe.show_alert({ message: __("Bank accounts added"), indicator: 'green' });
});
}, __("Select a company"), __("Continue"));
}
};

View File

@ -1,5 +1,4 @@
{
"actions": [],
"creation": "2018-10-25 10:02:48.656165",
"doctype": "DocType",
"editable_grid": 1,
@ -12,7 +11,6 @@
"plaid_client_id",
"plaid_secret",
"column_break_7",
"plaid_public_key",
"plaid_env"
],
"fields": [
@ -41,12 +39,6 @@
"in_list_view": 1,
"label": "Plaid Secret"
},
{
"fieldname": "plaid_public_key",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Plaid Public Key"
},
{
"fieldname": "plaid_env",
"fieldtype": "Select",
@ -69,8 +61,7 @@
}
],
"issingle": 1,
"links": [],
"modified": "2020-02-07 15:21:11.616231",
"modified": "2020-09-12 02:31:44.542385",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Plaid Settings",

View File

@ -2,30 +2,36 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe import _
from frappe.model.document import Document
import frappe
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
from frappe.utils import getdate, formatdate, today, add_months
from frappe import _
from frappe.desk.doctype.tag.tag import add_tag
from frappe.model.document import Document
from frappe.utils import add_months, formatdate, getdate, today
class PlaidSettings(Document):
pass
@staticmethod
def get_link_token():
plaid = PlaidConnector()
return plaid.get_link_token()
@frappe.whitelist()
def plaid_configuration():
def get_plaid_configuration():
if frappe.db.get_single_value("Plaid Settings", "enabled"):
plaid_settings = frappe.get_single("Plaid Settings")
return {
"plaid_public_key": plaid_settings.plaid_public_key,
"plaid_env": plaid_settings.plaid_env,
"link_token": plaid_settings.get_link_token(),
"client_name": frappe.local.site
}
else:
return "disabled"
return "disabled"
@frappe.whitelist()
def add_institution(token, response):
@ -33,6 +39,7 @@ def add_institution(token, response):
plaid = PlaidConnector()
access_token = plaid.get_access_token(token)
bank = None
if not frappe.db.exists("Bank", response["institution"]["name"]):
try:
@ -44,7 +51,6 @@ def add_institution(token, response):
bank.insert()
except Exception:
frappe.throw(frappe.get_traceback())
else:
bank = frappe.get_doc("Bank", response["institution"]["name"])
bank.plaid_access_token = access_token
@ -52,6 +58,7 @@ def add_institution(token, response):
return bank
@frappe.whitelist()
def add_bank_accounts(response, bank, company):
try:
@ -92,9 +99,8 @@ def add_bank_accounts(response, bank, company):
new_account.insert()
result.append(new_account.name)
except frappe.UniqueValidationError:
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(new_account.account_name))
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"]))
except Exception:
frappe.throw(frappe.get_traceback())
@ -103,6 +109,7 @@ def add_bank_accounts(response, bank, company):
return result
def add_account_type(account_type):
try:
frappe.get_doc({
@ -122,10 +129,11 @@ def add_account_subtype(account_subtype):
except Exception:
frappe.throw(frappe.get_traceback())
@frappe.whitelist()
def sync_transactions(bank, bank_account):
'''Sync transactions based on the last integration date as the start date, after the sync is completed
add the transaction date of the oldest transaction as the last integration date'''
"""Sync transactions based on the last integration date as the start date, after sync is completed
add the transaction date of the oldest transaction as the last integration date."""
last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date")
if last_transaction_date:
start_date = formatdate(last_transaction_date, "YYYY-MM-dd")
@ -147,10 +155,10 @@ def sync_transactions(bank, bank_account):
len(result), bank_account, start_date, end_date))
frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date)
except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
access_token = None
@ -168,6 +176,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
return transactions
def new_bank_transaction(transaction):
result = []
@ -182,8 +191,8 @@ def new_bank_transaction(transaction):
status = "Pending" if transaction["pending"] == "True" else "Settled"
tags = []
try:
tags = []
tags += transaction["category"]
tags += ["Plaid Cat. {}".format(transaction["category_id"])]
except KeyError:
@ -216,6 +225,7 @@ def new_bank_transaction(transaction):
return result
def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
@ -223,4 +233,8 @@ def automatic_synchronization():
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
for plaid_account in plaid_accounts:
frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name)
frappe.enqueue(
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
bank=plaid_account.bank,
bank_account=plaid_account.name
)

View File

@ -1,14 +1,17 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import unittest
import frappe
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import plaid_configuration, add_account_type, add_account_subtype, new_bank_transaction, add_bank_accounts
import json
from frappe.utils.response import json_handler
import unittest
import frappe
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import (
add_account_subtype, add_account_type, add_bank_accounts,
new_bank_transaction, get_plaid_configuration)
from frappe.utils.response import json_handler
class TestPlaidSettings(unittest.TestCase):
def setUp(self):
@ -31,7 +34,7 @@ class TestPlaidSettings(unittest.TestCase):
def test_plaid_disabled(self):
frappe.db.set_value("Plaid Settings", None, "enabled", 0)
self.assertTrue(plaid_configuration() == "disabled")
self.assertTrue(get_plaid_configuration() == "disabled")
def test_add_account_type(self):
add_account_type("brokerage")
@ -64,7 +67,7 @@ class TestPlaidSettings(unittest.TestCase):
'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking'
}],
}],
'institution': {
'institution_id': 'ins_6',
'name': 'Citi'
@ -100,7 +103,7 @@ class TestPlaidSettings(unittest.TestCase):
'mask': '0000',
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
'name': 'Plaid Checking'
}],
}],
'institution': {
'institution_id': 'ins_6',
'name': 'Citi'
@ -152,4 +155,4 @@ class TestPlaidSettings(unittest.TestCase):
new_bank_transaction(transactions)
self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1)
self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1)

View File

@ -4,17 +4,16 @@
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings")
plaid_settings = frappe.get_single("Plaid Settings")
if plaid_settings.enabled:
if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env \
and frappe.conf.plaid_public_key and frappe.conf.plaid_secret):
if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env and frappe.conf.plaid_secret):
plaid_settings.enabled = 0
else:
plaid_settings.update({
"plaid_client_id": frappe.conf.plaid_client_id,
"plaid_public_key": frappe.conf.plaid_public_key,
"plaid_env": frappe.conf.plaid_env,
"plaid_secret": frappe.conf.plaid_secret
})

View File

@ -3,7 +3,7 @@ frappe
gocardless-pro==1.11.0
googlemaps==3.1.1
pandas==1.0.5
plaid-python==3.4.0
plaid-python==6.0.0
pycountry==19.8.18
PyGithub==1.44.1
python-stdnum==1.12