mirror of
https://github.com/frappe/erpnext.git
synced 2026-10-29 22:57:42 +00:00
Merge branch 'develop' into fix-import-supplier-invoice-perms
This commit is contained in:
@@ -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"));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 ``;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user