Merge branch 'develop' into fix-import-supplier-invoice-perms

This commit is contained in:
Diptanil Saha
2026-10-29 14:31:02 +05:30
committed by GitHub
11 changed files with 75 additions and 23 deletions
+4 -4
View File
@@ -16,7 +16,7 @@ frappe.ui.form.on("Bank", {
}
if (frm.doc.plaid_access_token) {
frm.add_custom_button(__("Refresh Plaid Link"), () => {
new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token);
new erpnext.integrations.refreshPlaidLink(frm.doc.name);
});
}
},
@@ -42,8 +42,8 @@ let add_fields_to_mapping_table = function (frm) {
};
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
constructor(access_token) {
this.access_token = access_token;
constructor(bank) {
this.bank = bank;
this.plaidUrl = "https://cdn.plaid.com/link/v2/stable/link-initialize.js";
this.init_config();
}
@@ -57,7 +57,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
async get_link_token_for_update() {
const token = frappe.xcall(
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update",
{ access_token: this.access_token }
{ bank: this.bank }
);
if (!token) {
frappe.throw(__("Cannot retrieve link token for update. Check Error Log for more information"));
+2 -2
View File
@@ -96,7 +96,7 @@
},
{
"fieldname": "plaid_access_token",
"fieldtype": "Data",
"fieldtype": "Password",
"hidden": 1,
"label": "Plaid Access Token",
"no_copy": 1,
@@ -105,7 +105,7 @@
],
"icon": "landmark",
"links": [],
"modified": "2026-08-21 23:11:39.423431",
"modified": "2026-10-28 00:20:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank",
+1 -1
View File
@@ -24,7 +24,7 @@ class Bank(Document):
bank_name: DF.Data
bank_transaction_mapping: DF.Table[BankTransactionMapping]
plaid_access_token: DF.Data | None
plaid_access_token: DF.Password | None
swift_number: DF.Data | None
website: DF.Data | None
# end: auto-generated types
@@ -310,7 +310,12 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
finally:
frappe.flags.in_import = False
frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name})
frappe.publish_realtime(
"data_import_refresh",
{"data_import": data_import.name},
doctype=data_import.doctype,
docname=data_import.name,
)
def update_mapping_db(bank, template_options):
@@ -80,6 +80,8 @@ def start_merge(docname):
frappe.publish_realtime(
"ledger_merge_progress",
{"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total},
doctype=ledger_merge.doctype,
docname=ledger_merge.name,
)
except Exception:
frappe.db.rollback(save_point="ledger_merge_row")
@@ -92,4 +94,9 @@ def start_merge(docname):
else:
ledger_merge.db_set("status", "Error")
frappe.publish_realtime("ledger_merge_refresh", {"ledger_merge": ledger_merge.name})
frappe.publish_realtime(
"ledger_merge_refresh",
{"ledger_merge": ledger_merge.name},
doctype=ledger_merge.doctype,
docname=ledger_merge.name,
)
@@ -9,6 +9,7 @@ from frappe.desk.doctype.tag.tag import add_tag
from frappe.model.document import Document
from frappe.model.naming import append_number_if_name_exists
from frappe.utils import add_months, formatdate, getdate, sbool, today
from frappe.utils.password import get_decrypted_password
from plaid.errors import ItemError
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
@@ -268,10 +269,12 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
related_bank = frappe.db.get_values(
"Bank Account", bank_account, ["bank", "integration_id"], as_dict=True
)
access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token")
access_token = get_decrypted_password(
"Bank", related_bank[0].bank, "plaid_access_token", raise_exception=False
)
account_id = related_bank[0].integration_id
else:
access_token = frappe.db.get_value("Bank", bank, "plaid_access_token")
access_token = get_decrypted_password("Bank", bank, "plaid_access_token", raise_exception=False)
account_id = None
plaid = PlaidConnector(access_token)
@@ -372,10 +375,15 @@ def enqueue_synchronization():
@frappe.whitelist(methods=["POST"])
def get_link_token_for_update(access_token: str):
# `access_token` is caller-supplied and is used to mint a link token at Plaid, so this creates
# state at the provider even though it writes nothing here.
def get_link_token_for_update(bank: str):
# mints a link token at Plaid, so this creates state at the provider even though it writes
# nothing here.
frappe.has_permission("Plaid Settings", throw=True)
frappe.has_permission("Bank", "read", bank, throw=True)
access_token = get_decrypted_password("Bank", bank, "plaid_access_token", raise_exception=False)
if not access_token:
frappe.throw(_("Bank {0} is not linked with Plaid").format(bank))
plaid = PlaidConnector(access_token)
return plaid.get_link_token(update_mode=True)
@@ -11,8 +11,11 @@ from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import (
add_account_subtype,
add_account_type,
add_bank_accounts,
add_institution,
enqueue_synchronization,
get_link_token_for_update,
get_plaid_configuration,
get_transactions,
new_bank_transaction,
sync_transactions,
)
@@ -189,6 +192,24 @@ class TestPlaidSettings(ERPNextTestSuite):
self.assertIn(active_account, synced)
self.assertNotIn(disabled_account, synced)
def test_access_token_is_kept_out_of_the_bank_record(self):
token = "access-sandbox-plaid-token-test"
response = {"institution": {"institution_id": "ins_7", "name": "Plaid Token Test Bank"}}
with patch(f"{PLAID_SETTINGS}.PlaidConnector") as connector:
connector.return_value.get_access_token.return_value = token
add_institution("public-token", response)
bank = frappe.get_doc("Bank", "Plaid Token Test Bank")
self.assertNotEqual(frappe.db.get_value("Bank", bank.name, "plaid_access_token"), token)
self.assertNotIn(token, frappe.as_json(bank.as_dict()))
self.assertEqual(bank.get_password("plaid_access_token"), token)
with patch(f"{PLAID_SETTINGS}.PlaidConnector") as connector:
get_link_token_for_update(bank.name)
get_transactions(bank.name)
self.assertEqual([call.args for call in connector.call_args_list], [(token,), (token,)])
PLAID_SETTINGS = "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings"
+1
View File
@@ -564,3 +564,4 @@ erpnext.patches.v16_0.move_bom_creator_items_to_part_tree
erpnext.patches.v16_0.set_fg_qty_on_job_card_material_request_transfers
erpnext.patches.v16_0.encrypt_currency_exchange_access_key
erpnext.patches.v16_0.remove_all_role_from_import_supplier_invoice
erpnext.patches.v16_0.encrypt_bank_plaid_access_token
@@ -0,0 +1,16 @@
import frappe
from frappe.utils.password import set_encrypted_password
def execute():
"""Move Plaid access tokens to the password store."""
for bank in frappe.get_all(
"Bank", filters={"plaid_access_token": ("is", "set")}, fields=["name", "plaid_access_token"]
):
if set(bank.plaid_access_token) == {"*"}:
continue
set_encrypted_password("Bank", bank.name, bank.plaid_access_token, "plaid_access_token")
frappe.db.set_value(
"Bank", bank.name, "plaid_access_token", "*" * len(bank.plaid_access_token), update_modified=False
)
@@ -660,14 +660,7 @@ erpnext.PointOfSale.ItemCart = class {
function get_description_html() {
if (item_data.description) {
if (item_data.description.indexOf("<div>") != -1) {
try {
item_data.description = $(item_data.description).text();
} catch (error) {
item_data.description = item_data.description
.replace(/<div>/g, " ")
.replace(/<\/div>/g, " ")
.replace(/ +/g, " ");
}
item_data.description = frappe.utils.html2text(item_data.description);
}
item_data.description = frappe.ellipsis(item_data.description, 45);
return `<div class="item-desc">${frappe.utils.escape_html(item_data.description)}</div>`;
@@ -132,11 +132,12 @@ erpnext.PointOfSale.ItemDetails = class {
function get_description_html() {
if (description) {
description = frappe.utils.html2text(description);
description =
description.indexOf("...") === -1 && description.length > 140
? description.substr(0, 139) + "..."
: description;
return description;
return frappe.utils.escape_html(description);
}
return ``;
}