added bank transaction
This commit is contained in:
parent
cc12c99608
commit
89a372c490
|
|
@ -0,0 +1,81 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Kapital Bank is a Frappe app integrating BIRBank B2B API with ERPNext. It imports bank accounts, cards, and transactions as Payment Entries, with fuzzy matching of counterparties and purpose-based GL account mapping.
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Start development server (from bench directory)
|
||||
bench start
|
||||
|
||||
# Install/reinstall the app
|
||||
bench install-app kapital_bank
|
||||
|
||||
# Run migrations after doctype changes
|
||||
bench migrate
|
||||
|
||||
# Run tests
|
||||
bench --site <site-name> run-tests --app kapital_bank
|
||||
|
||||
# Linting and formatting (from app directory)
|
||||
pre-commit run --all-files
|
||||
pre-commit run ruff --all-files # Python only
|
||||
pre-commit run eslint --all-files # JavaScript only
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
- **Python**: Ruff linter/formatter — tabs, double quotes, line-length 110, target Python 3.10
|
||||
- **JavaScript**: ESLint + Prettier — tabs, indent size 4
|
||||
- **Indentation**: Tabs everywhere (spaces only in JSON, indent size 1)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Modules
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `kapital_bank/auth.py` | JWT auth: login, token refresh, scheduled renewal (cron every 4 min) |
|
||||
| `kapital_bank/api.py` | `BIRBankClient` — HTTP wrapper with auto token refresh on 401 |
|
||||
| `kapital_bank/bank_api.py` | Main business logic: registry loading, fuzzy matching, transaction import (~1400 lines) |
|
||||
| `kapital_bank/hooks.py` | Frappe hooks: doctype_js, scheduler_events, doc_events |
|
||||
| `kapital_bank/client/payment_entry.js` | Frontend: Payment Entry form/list customization |
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Auth**: Credentials in `Kapital Bank Login` → JWT tokens stored as encrypted Password fields
|
||||
2. **Registry**: API data fetched into local doctypes (Account, Card, Customer, Supplier)
|
||||
3. **Mapping**: Settings tables link bank entities to ERPNext records (Bank Account, Customer, Supplier, GL Account)
|
||||
4. **Import**: Statement transactions → fuzzy match party/purpose → create Payment Entry + tracking record
|
||||
5. **Cleanup**: Doc events on Payment Entry (on_trash, on_cancel) auto-delete associated transaction records
|
||||
|
||||
### Matching Algorithm
|
||||
|
||||
Three-tier party matching with configurable similarity thresholds:
|
||||
1. Exact VÖEN (tax ID) match
|
||||
2. Name similarity via `SequenceMatcher` (defaults: customer 90%, supplier 80%, purpose 70%)
|
||||
3. Optional Azerbaijani character normalization (Ə→E, ə→e, etc.)
|
||||
|
||||
### DocTypes
|
||||
|
||||
- **Single doctypes**: `Kapital Bank Login` (credentials), `Kapital Bank Settings` (central config hub with mapping child tables)
|
||||
- **Registry doctypes**: Account, Card, Customer, Supplier — local copies of bank data
|
||||
- **Child tables**: Account Mapping, Card Mapping, Customer Mapping, Supplier Mapping, Purpose Mapping
|
||||
- **Tracking**: `Kapital Bank Transaction` — tracks imported Payment Entries by reference_no for deduplication
|
||||
- **Utility**: `Kapital Bank API Test` — manual API testing
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- All `@frappe.whitelist()` functions use `ignore_permissions=True` for system-level operations
|
||||
- Bulk imports run as background jobs via `frappe.enqueue()` with 10-min timeout
|
||||
- Progress published via `frappe.publish_realtime()` over Socket.IO
|
||||
- Sensitive data (tokens, passwords) stored in Password-type fields (encrypted in DB)
|
||||
- Error logging via `frappe.log_error()` with masked account numbers in debug output
|
||||
|
||||
## API Documentation
|
||||
|
||||
BIRBank B2B API documentation PDFs are in `birbank_docs/`. The base URL is `https://my.birbank.business/api/b2b`.
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import frappe
|
||||
import requests
|
||||
import json
|
||||
from frappe.utils import now_datetime
|
||||
|
||||
BASE_URL = "https://my.birbank.business/api/b2b"
|
||||
|
|
@ -107,16 +106,29 @@ def logout(login_name):
|
|||
return {"success": True, "message": "Logged out"}
|
||||
|
||||
|
||||
def _relogin(login_name):
|
||||
"""Auto re-login using stored credentials. Returns new jwt_token or raises."""
|
||||
frappe.logger().info(f"[KB] Attempting auto re-login for {login_name}")
|
||||
result = login(login_name)
|
||||
if result.get("success"):
|
||||
doc = _get_doc(login_name)
|
||||
new_token = doc.get_password("jwt_token") if doc.jwt_token else ""
|
||||
if new_token:
|
||||
frappe.logger().info(f"[KB] Auto re-login successful for {login_name}")
|
||||
return new_token
|
||||
frappe.log_error(f"Auto re-login failed for {login_name}: {result.get('message')}", "KB Relogin Error")
|
||||
raise frappe.AuthenticationError(f"Auto re-login failed: {result.get('message')}")
|
||||
|
||||
|
||||
def refresh_token(login_name):
|
||||
"""POST /api/b2b/refresh — get new JWT using refresh token. Returns new jwt_token or raises."""
|
||||
"""POST /api/b2b/refresh — get new JWT using refresh token.
|
||||
Falls back to full re-login if refresh token is expired/invalid."""
|
||||
doc = _get_doc(login_name)
|
||||
refresh_tok = doc.get_password("jwt_refresh_token") if doc.jwt_refresh_token else ""
|
||||
|
||||
if not refresh_tok:
|
||||
frappe.log_error(f"No refresh token for {login_name}", "KB Refresh Error")
|
||||
frappe.db.set_value("Kapital Bank Login", login_name, "auth_status", "Not Authenticated", update_modified=False)
|
||||
frappe.db.commit()
|
||||
raise frappe.AuthenticationError("No refresh token available")
|
||||
frappe.logger().info(f"[KB] No refresh token for {login_name}, attempting re-login")
|
||||
return _relogin(login_name)
|
||||
|
||||
payload = {"refreshToken": refresh_tok}
|
||||
|
||||
|
|
@ -135,9 +147,8 @@ def refresh_token(login_name):
|
|||
|
||||
resp_code = data.get("response", {}).get("code", "")
|
||||
if resp_code == "100":
|
||||
frappe.db.set_value("Kapital Bank Login", login_name, "auth_status", "Not Authenticated", update_modified=False)
|
||||
frappe.db.commit()
|
||||
raise frappe.AuthenticationError("Refresh token invalid (code 100)")
|
||||
frappe.logger().info(f"[KB] Refresh token invalid (code 100) for {login_name}, attempting re-login")
|
||||
return _relogin(login_name)
|
||||
|
||||
resp_data = data.get("responseData", {})
|
||||
new_jwt = resp_data.get("jwttoken", "")
|
||||
|
|
@ -154,9 +165,8 @@ def refresh_token(login_name):
|
|||
return new_jwt
|
||||
|
||||
elif resp.status_code == 401:
|
||||
frappe.db.set_value("Kapital Bank Login", login_name, "auth_status", "Not Authenticated", update_modified=False)
|
||||
frappe.db.commit()
|
||||
raise frappe.AuthenticationError("Refresh token expired (401)")
|
||||
frappe.logger().info(f"[KB] Refresh token expired (401) for {login_name}, attempting re-login")
|
||||
return _relogin(login_name)
|
||||
|
||||
else:
|
||||
frappe.log_error(
|
||||
|
|
@ -184,12 +194,15 @@ def renew_token(login_name=None):
|
|||
return {"success": False, "message": "No default Kapital Bank Login found"}
|
||||
|
||||
doc = _get_doc(login_name)
|
||||
if doc.auth_status != "Authenticated":
|
||||
return {"success": False, "message": f"Login {login_name} is not authenticated"}
|
||||
if doc.auth_status == "Authenticated":
|
||||
new_token = refresh_token(login_name)
|
||||
frappe.logger().info(f"[KB] Token renewed for {login_name}")
|
||||
return {"success": True, "message": "Token renewed"}
|
||||
|
||||
new_token = refresh_token(login_name)
|
||||
frappe.logger().info(f"[KB] Token renewed for {login_name}")
|
||||
return {"success": True, "message": "Token renewed"}
|
||||
# Not authenticated — try full re-login with stored credentials
|
||||
frappe.logger().info(f"[KB] Login {login_name} is {doc.auth_status}, attempting re-login")
|
||||
new_token = _relogin(login_name)
|
||||
return {"success": True, "message": "Re-login successful"}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(f"renew_token failed: {e}\n{frappe.get_traceback()}", "KB Token Renewal")
|
||||
|
|
|
|||
|
|
@ -1259,8 +1259,8 @@ def import_single_transaction(txn_data, account_iban, login_name=None):
|
|||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _create_transaction_record(reference_no, posting_date, amount, currency,
|
||||
party, source_type, source_label, payment_entry,
|
||||
commit=True):
|
||||
party, source_type, source_label, payment_entry=None,
|
||||
bank_transaction=None, commit=True):
|
||||
"""Create a Kapital Bank Transaction record to track imported transaction."""
|
||||
reference_no = str(reference_no).strip()
|
||||
if not reference_no:
|
||||
|
|
@ -1279,7 +1279,10 @@ def _create_transaction_record(reference_no, posting_date, amount, currency,
|
|||
doc.party = party
|
||||
doc.source_type = source_type
|
||||
doc.source_label = source_label
|
||||
doc.payment_entry = payment_entry
|
||||
if payment_entry:
|
||||
doc.payment_entry = payment_entry
|
||||
if bank_transaction:
|
||||
doc.bank_transaction = bank_transaction
|
||||
doc.status = "Imported"
|
||||
doc.import_date = frappe.utils.now_datetime()
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
|
@ -1331,6 +1334,213 @@ def on_cancel_payment_entry(doc, method):
|
|||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# BANK TRANSACTION CLEANUP
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def on_delete_bank_transaction(doc, method):
|
||||
"""Delete linked Kapital Bank Transaction records when Bank Transaction is deleted."""
|
||||
try:
|
||||
records = frappe.get_all(
|
||||
"Kapital Bank Transaction",
|
||||
filters={"bank_transaction": doc.name},
|
||||
fields=["name"],
|
||||
)
|
||||
for record in records:
|
||||
frappe.delete_doc("Kapital Bank Transaction", record.name, ignore_permissions=True, force=True)
|
||||
if records:
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Error deleting Kapital Bank Transaction for BT {doc.name}: {e}\n{frappe.get_traceback()}",
|
||||
"Kapital Bank Transaction Cleanup",
|
||||
)
|
||||
|
||||
|
||||
def on_cancel_bank_transaction(doc, method):
|
||||
"""Delete linked Kapital Bank Transaction records when Bank Transaction is cancelled."""
|
||||
try:
|
||||
records = frappe.get_all(
|
||||
"Kapital Bank Transaction",
|
||||
filters={"bank_transaction": doc.name},
|
||||
fields=["name"],
|
||||
)
|
||||
for record in records:
|
||||
frappe.db.delete("Kapital Bank Transaction", {"name": record.name})
|
||||
if records:
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Error deleting Kapital Bank Transaction for cancelled BT {doc.name}: {e}\n{frappe.get_traceback()}",
|
||||
"Kapital Bank Transaction Cleanup",
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# BANK TRANSACTION IMPORT (simpler path — no purpose/party mappings required)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _build_bank_account_lookup(settings):
|
||||
"""Build IBAN/card-account-number → Bank Account lookup from settings mappings."""
|
||||
ba_lookup = {}
|
||||
|
||||
for row in settings.account_mappings:
|
||||
if row.iban and row.bank_account:
|
||||
ba_lookup[row.iban.strip()] = row.bank_account
|
||||
|
||||
for row in settings.card_mappings:
|
||||
if row.account_number and row.bank_account:
|
||||
ba_lookup[row.account_number.strip()] = row.bank_account
|
||||
|
||||
return ba_lookup
|
||||
|
||||
|
||||
def _import_one_bank_transaction(txn_data, settings, voen_to_party, name_to_party,
|
||||
ba_lookup):
|
||||
"""Import a single transaction as a Bank Transaction document. Returns result dict."""
|
||||
ref_no = txn_data.get("ref_no", "")
|
||||
parsed_date = txn_data.get("date")
|
||||
amount = abs(flt(txn_data.get("amount", 0)))
|
||||
dr_cr = (txn_data.get("drcr") or "D").upper()
|
||||
purpose = (txn_data.get("purpose") or "").strip()
|
||||
contr_name = (txn_data.get("counterparty") or "").strip()
|
||||
contr_voen = (txn_data.get("contr_voen") or "").strip()
|
||||
currency = txn_data.get("currency", "AZN")
|
||||
source_type = txn_data.get("source_type", "account")
|
||||
source_label = txn_data.get("source_label", "")
|
||||
|
||||
# Determine bank_account from source identifier
|
||||
bank_account = None
|
||||
if source_type == "account":
|
||||
# source_label is the IBAN for accounts
|
||||
source_id = txn_data.get("source_id", "")
|
||||
if source_id:
|
||||
bank_account = ba_lookup.get(source_id.strip())
|
||||
if not bank_account and source_label:
|
||||
bank_account = ba_lookup.get(source_label.strip())
|
||||
elif source_type == "card":
|
||||
# For cards, try to extract account number from source_label ("Card: XXXX")
|
||||
source_id = txn_data.get("source_id", "")
|
||||
if source_id:
|
||||
bank_account = ba_lookup.get(source_id.strip())
|
||||
if not bank_account and source_label:
|
||||
card_num = source_label.replace("Card: ", "").strip()
|
||||
bank_account = ba_lookup.get(card_num)
|
||||
|
||||
# Optional party mapping: VÖEN → name → None
|
||||
party_type = None
|
||||
erp_party = None
|
||||
if contr_voen and contr_voen in voen_to_party:
|
||||
party_type = voen_to_party[contr_voen]["party_type"]
|
||||
erp_party = voen_to_party[contr_voen]["erp_party"]
|
||||
elif contr_name and contr_name in name_to_party:
|
||||
party_type = name_to_party[contr_name]["party_type"]
|
||||
erp_party = name_to_party[contr_name]["erp_party"]
|
||||
|
||||
# Create Bank Transaction
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.date = parsed_date
|
||||
bt.deposit = amount if dr_cr == "C" else 0
|
||||
bt.withdrawal = amount if dr_cr == "D" else 0
|
||||
bt.currency = currency
|
||||
bt.description = purpose
|
||||
bt.reference_number = ref_no
|
||||
bt.transaction_id = ref_no
|
||||
bt.company = settings.default_company
|
||||
bt.bank_party_name = contr_name or purpose
|
||||
|
||||
if bank_account:
|
||||
bt.bank_account = bank_account
|
||||
|
||||
if party_type and erp_party:
|
||||
bt.party_type = party_type
|
||||
bt.party = erp_party
|
||||
|
||||
try:
|
||||
bt.insert(ignore_permissions=True)
|
||||
bt.submit()
|
||||
except Exception as bt_err:
|
||||
frappe.db.rollback()
|
||||
frappe.local.message_log = []
|
||||
return {
|
||||
"success": False,
|
||||
"error_type": "import_error",
|
||||
"message": str(bt_err),
|
||||
"txn_data": txn_data,
|
||||
}
|
||||
|
||||
# Create tracking record
|
||||
_create_transaction_record(
|
||||
reference_no=ref_no,
|
||||
posting_date=parsed_date,
|
||||
amount=amount,
|
||||
currency=currency,
|
||||
party=contr_name,
|
||||
source_type="Account" if source_type == "account" else "Card",
|
||||
source_label=source_label,
|
||||
bank_transaction=bt.name,
|
||||
commit=False,
|
||||
)
|
||||
|
||||
return {"success": True, "bank_transaction": bt.name}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_bulk_bank_transactions(txn_list):
|
||||
"""Enqueue bulk Bank Transaction import as a background job."""
|
||||
if isinstance(txn_list, str):
|
||||
txn_list = json.loads(txn_list)
|
||||
|
||||
user = frappe.session.user
|
||||
|
||||
frappe.enqueue(
|
||||
_process_bulk_bt_import,
|
||||
txn_list=txn_list,
|
||||
user=user,
|
||||
queue="default",
|
||||
timeout=600,
|
||||
)
|
||||
|
||||
return {"success": True, "enqueued": True, "total": len(txn_list)}
|
||||
|
||||
|
||||
def _process_bulk_bt_import(txn_list, user):
|
||||
"""Background job: import transactions as Bank Transaction with realtime progress."""
|
||||
frappe.set_user(user)
|
||||
|
||||
settings = frappe.get_single("Kapital Bank Settings")
|
||||
_purpose_rules, voen_to_party, name_to_party = _build_import_lookups(settings)
|
||||
ba_lookup = _build_bank_account_lookup(settings)
|
||||
|
||||
total = len(txn_list)
|
||||
imported_count = 0
|
||||
errors = []
|
||||
|
||||
for idx, txn_data in enumerate(txn_list):
|
||||
frappe.publish_realtime(
|
||||
"kb_bt_import_progress",
|
||||
{"current": idx + 1, "total": total},
|
||||
user=user,
|
||||
)
|
||||
|
||||
result = _import_one_bank_transaction(
|
||||
txn_data, settings, voen_to_party, name_to_party, ba_lookup,
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
imported_count += 1
|
||||
else:
|
||||
errors.append(result)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
frappe.publish_realtime(
|
||||
"kb_bt_import_complete",
|
||||
{"total": total, "imported": imported_count, "errors": errors},
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# INTERNAL HELPERS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -0,0 +1,704 @@
|
|||
const KBBTImport = {
|
||||
loadingErrors: [],
|
||||
};
|
||||
|
||||
// ======= ERROR MANAGEMENT =======
|
||||
KBBTImport.errors = {
|
||||
add: function(txnData, errorType, errorMessage) {
|
||||
const errorEntry = {
|
||||
ref_no: txnData.ref_no || 'Unknown',
|
||||
date: txnData.date || 'Unknown',
|
||||
counterparty: txnData.counterparty || 'Unknown',
|
||||
amount: txnData.amount || 0,
|
||||
source_label: txnData.source_label || '',
|
||||
error_type: errorType,
|
||||
error_message: errorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
KBBTImport.loadingErrors.push(errorEntry);
|
||||
console.error('KB BT Import Error:', errorEntry);
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
frappe.confirm(
|
||||
__('Are you sure you want to clear the error log?'),
|
||||
function() {
|
||||
KBBTImport.loadingErrors = [];
|
||||
frappe.show_alert({
|
||||
message: __('Error log cleared'),
|
||||
indicator: 'green'
|
||||
}, 2);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
showDetails: function() {
|
||||
if (KBBTImport.loadingErrors.length === 0) {
|
||||
frappe.msgprint(__('No errors to display'));
|
||||
return;
|
||||
}
|
||||
|
||||
let errorsHtml = '<div class="error-log-container">';
|
||||
|
||||
KBBTImport.loadingErrors.forEach(function(error) {
|
||||
let documentDate = 'No date';
|
||||
if (error.date && error.date !== 'Unknown') {
|
||||
try {
|
||||
documentDate = moment(error.date).format('DD.MM.YYYY');
|
||||
} catch (e) {
|
||||
documentDate = 'Invalid date';
|
||||
}
|
||||
}
|
||||
|
||||
errorsHtml += '<div class="error-item" style="margin-bottom: 20px; padding: 15px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-color);">';
|
||||
|
||||
// Header
|
||||
errorsHtml += '<div class="error-header" style="margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color);">';
|
||||
errorsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||||
errorsHtml += '<h5 style="margin: 0; color: var(--text-color);">' +
|
||||
'<strong>' + error.ref_no + '</strong></h5>';
|
||||
errorsHtml += '<span class="label label-danger">' + error.error_type + '</span>';
|
||||
errorsHtml += '</div>';
|
||||
errorsHtml += '<small style="color: var(--text-muted);">' + error.counterparty + '</small>';
|
||||
if (error.source_label) {
|
||||
errorsHtml += ' <small style="color: var(--text-muted);">(' + error.source_label + ')</small>';
|
||||
}
|
||||
errorsHtml += '</div>';
|
||||
|
||||
// Error message
|
||||
errorsHtml += '<div class="error-message" style="margin-bottom: 10px;">';
|
||||
errorsHtml += '<strong style="color: var(--text-color);">' + error.error_message + '</strong>';
|
||||
errorsHtml += '</div>';
|
||||
|
||||
errorsHtml += '<div style="text-align: right; margin-top: 10px;">';
|
||||
errorsHtml += '<small style="color: var(--text-muted);"><em>' + documentDate + '</em></small>';
|
||||
errorsHtml += '</div>';
|
||||
|
||||
errorsHtml += '</div>';
|
||||
});
|
||||
|
||||
errorsHtml += '</div>';
|
||||
|
||||
// Statistics
|
||||
const errorTypes = {};
|
||||
KBBTImport.loadingErrors.forEach(function(error) {
|
||||
errorTypes[error.error_type] = (errorTypes[error.error_type] || 0) + 1;
|
||||
});
|
||||
|
||||
let statsHtml = '<div style="margin-bottom: 20px; padding: 15px; background: var(--bg-light); border-radius: 6px; border: 1px solid var(--border-color);">';
|
||||
statsHtml += '<div style="display: flex; justify-content: space-between; align-items: center;">';
|
||||
statsHtml += '<div>';
|
||||
statsHtml += '<strong style="color: var(--text-color);">' + __('Total Errors:') + '</strong> ' + KBBTImport.loadingErrors.length + '<br>';
|
||||
statsHtml += '<span style="color: var(--text-muted);">';
|
||||
Object.keys(errorTypes).forEach(function(type, index) {
|
||||
if (index > 0) statsHtml += ' • ';
|
||||
statsHtml += type + ': ' + errorTypes[type];
|
||||
});
|
||||
statsHtml += '</span>';
|
||||
statsHtml += '</div>';
|
||||
statsHtml += '<div>';
|
||||
statsHtml += '<button class="btn btn-default btn-sm" id="kb-bt-export-csv-dialog-btn" style="margin-right: 5px;">' +
|
||||
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>';
|
||||
statsHtml += '<button class="btn btn-warning btn-sm" id="kb-bt-clear-log-dialog-btn">' +
|
||||
'<i class="fa fa-trash"></i> ' + __('Clear') + '</button>';
|
||||
statsHtml += '</div></div>';
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Error Details') + ' (' + KBBTImport.loadingErrors.length + ')',
|
||||
size: 'large',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'stats_html',
|
||||
fieldtype: 'HTML',
|
||||
options: statsHtml
|
||||
},
|
||||
{
|
||||
fieldname: 'errors_html',
|
||||
fieldtype: 'HTML',
|
||||
options: '<div style="max-height: 500px; overflow-y: auto;">' + errorsHtml + '</div>'
|
||||
}
|
||||
],
|
||||
primary_action_label: __('Close'),
|
||||
primary_action: function() {
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
|
||||
d.show();
|
||||
|
||||
setTimeout(function() {
|
||||
$('#kb-bt-export-csv-dialog-btn').off('click').on('click', function() {
|
||||
KBBTImport.errors.exportCSV();
|
||||
});
|
||||
|
||||
$('#kb-bt-clear-log-dialog-btn').off('click').on('click', function() {
|
||||
KBBTImport.errors.clear();
|
||||
d.hide();
|
||||
});
|
||||
}, 200);
|
||||
},
|
||||
|
||||
exportCSV: function() {
|
||||
if (KBBTImport.loadingErrors.length === 0) {
|
||||
frappe.msgprint(__('No errors to export'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let csvContent = "data:text/csv;charset=utf-8,";
|
||||
csvContent += "Ref No,Date,Counterparty,Amount,Source,Error Type,Error Message,Timestamp\n";
|
||||
|
||||
KBBTImport.loadingErrors.forEach(function(error) {
|
||||
const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' ');
|
||||
const cleanCounterparty = (error.counterparty || '').replace(/"/g, '""');
|
||||
|
||||
const row = [
|
||||
error.ref_no,
|
||||
error.date,
|
||||
cleanCounterparty,
|
||||
error.amount,
|
||||
error.source_label || '',
|
||||
error.error_type,
|
||||
cleanMessage,
|
||||
moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss')
|
||||
].map(function(field) {
|
||||
return '"' + (field || '') + '"';
|
||||
}).join(',');
|
||||
|
||||
csvContent += row + "\n";
|
||||
});
|
||||
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", encodedUri);
|
||||
link.setAttribute("download", "kb_bt_import_errors_" + moment().format('YYYY-MM-DD_HH-mm-ss') + ".csv");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
frappe.show_alert({
|
||||
message: __('Error log exported successfully'),
|
||||
indicator: 'green'
|
||||
}, 3);
|
||||
} catch (e) {
|
||||
console.error('Export error:', e);
|
||||
frappe.show_alert({
|
||||
message: __('Failed to export error log'),
|
||||
indicator: 'red'
|
||||
}, 3);
|
||||
}
|
||||
},
|
||||
|
||||
showSummary: function(totalTransactions, processedCount, errorsCount) {
|
||||
if (errorsCount === 0) {
|
||||
frappe.msgprint({
|
||||
title: __('Import Completed Successfully'),
|
||||
indicator: 'green',
|
||||
message: __('All ') + totalTransactions + __(' transactions were imported successfully.')
|
||||
});
|
||||
} else {
|
||||
const title = errorsCount === totalTransactions ? __('Import Failed') : __('Import Completed with Errors');
|
||||
const indicator = processedCount > 0 ? 'orange' : 'red';
|
||||
|
||||
let message = '<div style="margin-bottom: 15px;">';
|
||||
if (processedCount > 0) {
|
||||
message += __('Successfully imported: ') + '<strong>' + processedCount + '</strong>' + __(' transactions') + '<br>';
|
||||
}
|
||||
message += __('Failed: ') + '<strong>' + errorsCount + '</strong>' + __(' transactions');
|
||||
message += '</div>';
|
||||
|
||||
message += '<div style="text-align: center;">' +
|
||||
'<button class="btn btn-primary btn-sm" id="kb-bt-view-error-details-btn" style="margin-right: 10px;">' +
|
||||
'<i class="fa fa-list"></i> ' + __('View Error Details') + '</button>' +
|
||||
'<button class="btn btn-default btn-sm" id="kb-bt-export-error-log-btn">' +
|
||||
'<i class="fa fa-download"></i> ' + __('Export CSV') + '</button>' +
|
||||
'</div>';
|
||||
|
||||
const summaryDialog = frappe.msgprint({
|
||||
title: title,
|
||||
indicator: indicator,
|
||||
message: message
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
$('#kb-bt-view-error-details-btn').off('click').on('click', function() {
|
||||
summaryDialog.hide();
|
||||
KBBTImport.errors.showDetails();
|
||||
});
|
||||
|
||||
$('#kb-bt-export-error-log-btn').off('click').on('click', function() {
|
||||
KBBTImport.errors.exportCSV();
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ======= IMPORT MODULE =======
|
||||
KBBTImport.import = {
|
||||
showDialog: function() {
|
||||
// Load sources: try settings mappings first, fall back to registry doctypes
|
||||
frappe.call({
|
||||
method: 'frappe.client.get',
|
||||
args: { doctype: 'Kapital Bank Settings', name: 'Kapital Bank Settings' },
|
||||
callback: function(r) {
|
||||
if (!r.message) {
|
||||
frappe.msgprint({ title: __('Error'), indicator: 'red', message: __('Could not load Kapital Bank Settings') });
|
||||
return;
|
||||
}
|
||||
|
||||
const accountMappings = r.message.account_mappings || [];
|
||||
const cardMappings = r.message.card_mappings || [];
|
||||
|
||||
if (accountMappings.length > 0 || cardMappings.length > 0) {
|
||||
// Use mappings as source list
|
||||
KBBTImport.import._showDialogWithSources(
|
||||
accountMappings.map(function(m) {
|
||||
return { type: 'account', id: m.iban, label: m.account_label ? (m.iban + ' — ' + m.account_label) : m.iban };
|
||||
}).filter(function(s) { return s.id; }),
|
||||
cardMappings.map(function(m) {
|
||||
return { type: 'card', id: m.account_number, label: m.card_label ? (m.account_number + ' — ' + m.card_label) : m.account_number };
|
||||
}).filter(function(s) { return s.id; })
|
||||
);
|
||||
} else {
|
||||
// Fall back to registry doctypes
|
||||
KBBTImport.import._loadRegistrySources();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_loadRegistrySources: function() {
|
||||
let accounts = [];
|
||||
let cards = [];
|
||||
let pending = 2;
|
||||
|
||||
function onDone() {
|
||||
pending--;
|
||||
if (pending > 0) return;
|
||||
|
||||
if (accounts.length === 0 && cards.length === 0) {
|
||||
frappe.msgprint({
|
||||
title: __('No Sources'),
|
||||
indicator: 'orange',
|
||||
message: __('No bank accounts or cards found. Please load registry data in Kapital Bank Settings first.')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
KBBTImport.import._showDialogWithSources(accounts, cards);
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: 'frappe.client.get_list',
|
||||
args: {
|
||||
doctype: 'Kapital Bank Account',
|
||||
fields: ['iban', 'account_desc', 'currency'],
|
||||
filters: { status: 'Active' },
|
||||
limit_page_length: 0
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
accounts = r.message.map(function(a) {
|
||||
const label = a.account_desc ? (a.iban + ' — ' + a.account_desc) : a.iban;
|
||||
return { type: 'account', id: a.iban, label: label };
|
||||
});
|
||||
}
|
||||
onDone();
|
||||
},
|
||||
error: function() { onDone(); }
|
||||
});
|
||||
|
||||
frappe.call({
|
||||
method: 'frappe.client.get_list',
|
||||
args: {
|
||||
doctype: 'Kapital Bank Card',
|
||||
fields: ['account_number', 'card_type', 'pan', 'currency'],
|
||||
filters: { status: 'Active' },
|
||||
limit_page_length: 0
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
cards = r.message.map(function(c) {
|
||||
const label = c.pan ? (c.account_number + ' — ' + c.pan) : c.account_number;
|
||||
return { type: 'card', id: c.account_number, label: label };
|
||||
});
|
||||
}
|
||||
onDone();
|
||||
},
|
||||
error: function() { onDone(); }
|
||||
});
|
||||
},
|
||||
|
||||
_showDialogWithSources: function(accounts, cards) {
|
||||
let sourcesHtml = '<div style="max-height: 300px; overflow-y: auto;">';
|
||||
sourcesHtml += '<div style="margin-bottom: 10px;">';
|
||||
sourcesHtml += '<label><input type="checkbox" class="kb-bt-select-all-sources"> <strong>' + __('Select All') + '</strong></label>';
|
||||
sourcesHtml += '</div>';
|
||||
|
||||
if (accounts.length > 0) {
|
||||
sourcesHtml += '<div style="margin-bottom: 8px; font-weight: 600; color: var(--text-color);">' + __('Accounts') + '</div>';
|
||||
accounts.forEach(function(a) {
|
||||
sourcesHtml += '<div style="margin-left: 20px; margin-bottom: 4px;">';
|
||||
sourcesHtml += '<label><input type="checkbox" class="kb-bt-source-check" data-type="' + a.type + '" data-id="' + a.id + '"> ' + a.label + '</label>';
|
||||
sourcesHtml += '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
if (cards.length > 0) {
|
||||
sourcesHtml += '<div style="margin-bottom: 8px; margin-top: 10px; font-weight: 600; color: var(--text-color);">' + __('Cards') + '</div>';
|
||||
cards.forEach(function(c) {
|
||||
sourcesHtml += '<div style="margin-left: 20px; margin-bottom: 4px;">';
|
||||
sourcesHtml += '<label><input type="checkbox" class="kb-bt-source-check" data-type="' + c.type + '" data-id="' + c.id + '"> ' + c.label + '</label>';
|
||||
sourcesHtml += '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
sourcesHtml += '</div>';
|
||||
|
||||
const today = frappe.datetime.get_today();
|
||||
const first_day = frappe.datetime.month_start(today);
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Import Bank Transactions'),
|
||||
fields: [
|
||||
{ fieldname: 'sec_period', fieldtype: 'Section Break', label: __('Period') },
|
||||
{ fieldname: 'date_from', fieldtype: 'Date', label: __('Date From'), reqd: 1, default: first_day },
|
||||
{ fieldname: 'col_break', fieldtype: 'Column Break' },
|
||||
{ fieldname: 'date_to', fieldtype: 'Date', label: __('Date To'), reqd: 1, default: today },
|
||||
{ fieldname: 'sec_sources', fieldtype: 'Section Break', label: __('Sources') },
|
||||
{
|
||||
fieldname: 'sources_html',
|
||||
fieldtype: 'HTML',
|
||||
options: sourcesHtml
|
||||
}
|
||||
],
|
||||
primary_action_label: __('Search'),
|
||||
primary_action: function() {
|
||||
const values = d.get_values();
|
||||
const selectedSources = [];
|
||||
|
||||
d.$wrapper.find('.kb-bt-source-check:checked').each(function() {
|
||||
selectedSources.push({
|
||||
type: $(this).data('type'),
|
||||
id: $(this).data('id')
|
||||
});
|
||||
});
|
||||
|
||||
if (selectedSources.length === 0) {
|
||||
frappe.msgprint({
|
||||
title: __('Warning'),
|
||||
indicator: 'orange',
|
||||
message: __('Please select at least one account or card')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Card statements are limited to 90 days by the API
|
||||
const hasCards = selectedSources.some(s => s.type === 'card');
|
||||
if (hasCards) {
|
||||
const diffDays = frappe.datetime.get_diff(values.date_to, values.date_from);
|
||||
if (diffDays > 90) {
|
||||
frappe.msgprint({
|
||||
title: __('Date Range Too Large'),
|
||||
indicator: 'orange',
|
||||
message: __('Card statements are limited to 90 days. Please shorten the date range or deselect cards.')
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
d.hide();
|
||||
KBBTImport.import.loadTransactionsMulti(values.date_from, values.date_to, selectedSources);
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
|
||||
// Select All handler
|
||||
d.$wrapper.find('.kb-bt-select-all-sources').on('change', function() {
|
||||
const isChecked = $(this).prop('checked');
|
||||
d.$wrapper.find('.kb-bt-source-check').prop('checked', isChecked);
|
||||
});
|
||||
},
|
||||
|
||||
loadTransactionsMulti: function(fromDate, toDate, sources) {
|
||||
const totalSources = sources.length;
|
||||
let currentSourceIdx = 0;
|
||||
let allTransactions = [];
|
||||
let totalSkipped = 0;
|
||||
|
||||
function fetchNext() {
|
||||
if (currentSourceIdx >= totalSources) {
|
||||
frappe.hide_progress();
|
||||
|
||||
if (allTransactions.length === 0) {
|
||||
let msg = __('No new transactions found for the specified period.');
|
||||
if (totalSkipped > 0) {
|
||||
msg += ' ' + __('({0} already imported)', [totalSkipped]);
|
||||
}
|
||||
frappe.msgprint({
|
||||
title: __('Information'),
|
||||
indicator: 'blue',
|
||||
message: msg
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalSkipped > 0) {
|
||||
frappe.show_alert({
|
||||
message: __('Skipped {0} already imported transactions', [totalSkipped]),
|
||||
indicator: 'blue'
|
||||
}, 5);
|
||||
}
|
||||
|
||||
KBBTImport.import.showTransactionSelection(allTransactions);
|
||||
return;
|
||||
}
|
||||
|
||||
const source = sources[currentSourceIdx];
|
||||
const sourceLabel = source.type === 'account'
|
||||
? source.id
|
||||
: __('Card: {0}', [source.id]);
|
||||
|
||||
frappe.show_progress(
|
||||
__('Fetching Statements'),
|
||||
currentSourceIdx,
|
||||
totalSources,
|
||||
__('Fetching statement {0} of {1}: {2}', [currentSourceIdx + 1, totalSources, sourceLabel])
|
||||
);
|
||||
|
||||
const method = source.type === 'account'
|
||||
? 'kapital_bank.bank_api.get_statement_transactions'
|
||||
: 'kapital_bank.bank_api.get_card_statement_transactions';
|
||||
|
||||
const args = source.type === 'account'
|
||||
? { from_date: fromDate, to_date: toDate, account_iban: source.id }
|
||||
: { from_date: fromDate, to_date: toDate, card_account_number: source.id };
|
||||
|
||||
frappe.call({
|
||||
method: method,
|
||||
args: args,
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
const txns = (r.message.transactions || []).map(function(txn) {
|
||||
txn.source_id = source.id;
|
||||
return txn;
|
||||
});
|
||||
allTransactions = allTransactions.concat(txns);
|
||||
totalSkipped += (r.message.skipped_duplicates || 0);
|
||||
} else {
|
||||
frappe.show_alert({
|
||||
message: __('Error fetching {0}: {1}', [sourceLabel, r.message ? r.message.message : 'unknown']),
|
||||
indicator: 'red'
|
||||
}, 5);
|
||||
}
|
||||
currentSourceIdx++;
|
||||
setTimeout(fetchNext, 100);
|
||||
},
|
||||
error: function() {
|
||||
frappe.show_alert({
|
||||
message: __('Network error fetching {0}', [sourceLabel]),
|
||||
indicator: 'red'
|
||||
}, 5);
|
||||
currentSourceIdx++;
|
||||
setTimeout(fetchNext, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchNext();
|
||||
},
|
||||
|
||||
showTransactionSelection: function(txns) {
|
||||
let txnTable = '<div style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered kb-bt-txn-table" style="width: 100%; table-layout: fixed;">';
|
||||
txnTable += '<thead><tr>' +
|
||||
'<th style="width: 4%;"><input type="checkbox" class="kb-bt-select-all-txns"></th>' +
|
||||
'<th style="width: 14%;">' + __('Ref No') + '</th>' +
|
||||
'<th style="width: 10%;">' + __('Date') + '</th>' +
|
||||
'<th style="width: 14%;">' + __('Account') + '</th>' +
|
||||
'<th style="width: 20%;">' + __('Counterparty') + '</th>' +
|
||||
'<th style="width: 12%; text-align: right;">' + __('Amount') + '</th>' +
|
||||
'<th style="width: 6%; text-align: center;">' + __('Type') + '</th>' +
|
||||
'<th style="width: 20%;">' + __('Purpose') + '</th>' +
|
||||
'</tr></thead><tbody>';
|
||||
|
||||
txns.forEach(function(txn, idx) {
|
||||
const displayDate = txn.date ? moment(txn.date).format('DD.MM.YYYY') : '';
|
||||
const amountFmt = parseFloat(txn.amount || 0).toFixed(2);
|
||||
const drcrLabel = txn.drcr === 'D'
|
||||
? '<span class="label label-danger">' + __('Pay') + '</span>'
|
||||
: '<span class="label label-success">' + __('Receive') + '</span>';
|
||||
const sourceLabel = txn.source_label || '';
|
||||
|
||||
txnTable += '<tr>' +
|
||||
'<td><input type="checkbox" class="kb-bt-select-txn" data-idx="' + idx + '"></td>' +
|
||||
'<td style="word-break: break-word;">' + (txn.ref_no || '') + '</td>' +
|
||||
'<td>' + displayDate + '</td>' +
|
||||
'<td style="word-break: break-word; font-size: 0.85em;">' + sourceLabel + '</td>' +
|
||||
'<td style="word-break: break-word;">' + (txn.counterparty || '') + '</td>' +
|
||||
'<td style="text-align: right;">' + amountFmt + ' ' + (txn.currency || '') + '</td>' +
|
||||
'<td style="text-align: center;">' + drcrLabel + '</td>' +
|
||||
'<td style="word-break: break-word; font-size: 0.9em;">' + (txn.purpose || '') + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
|
||||
txnTable += '</tbody></table></div>';
|
||||
|
||||
const infoMessage = '<div class="alert alert-info">' +
|
||||
__('Transactions found: ') + txns.length +
|
||||
'</div>';
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Select Transactions to Import'),
|
||||
size: 'large',
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'info_html',
|
||||
fieldtype: 'HTML',
|
||||
options: infoMessage
|
||||
},
|
||||
{
|
||||
fieldname: 'txns_html',
|
||||
fieldtype: 'HTML',
|
||||
options: txnTable
|
||||
}
|
||||
],
|
||||
primary_action_label: __('Load selected'),
|
||||
primary_action: function() {
|
||||
const selectedTxns = [];
|
||||
d.$wrapper.find('.kb-bt-select-txn:checked').each(function() {
|
||||
const idx = $(this).data('idx');
|
||||
selectedTxns.push(txns[idx]);
|
||||
});
|
||||
|
||||
if (selectedTxns.length === 0) {
|
||||
frappe.msgprint({
|
||||
title: __('Warning'),
|
||||
indicator: 'orange',
|
||||
message: __('No transactions selected')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
d.hide();
|
||||
KBBTImport.import.loadSelectedTransactions(selectedTxns);
|
||||
},
|
||||
secondary_action_label: __('Cancel'),
|
||||
secondary_action: function() {
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
|
||||
d.$wrapper.find('.modal-dialog').css({
|
||||
'max-width': '90%',
|
||||
'width': '90%',
|
||||
'margin': '30px auto'
|
||||
});
|
||||
|
||||
d.$wrapper.find('.modal-body').css({
|
||||
'padding': '15px'
|
||||
});
|
||||
|
||||
d.show();
|
||||
|
||||
d.$wrapper.find('.kb-bt-select-all-txns').on('change', function() {
|
||||
const isChecked = $(this).prop('checked');
|
||||
d.$wrapper.find('.kb-bt-select-txn').prop('checked', isChecked);
|
||||
});
|
||||
},
|
||||
|
||||
loadSelectedTransactions: function(selectedTxns) {
|
||||
KBBTImport.loadingErrors = [];
|
||||
const total = selectedTxns.length;
|
||||
|
||||
frappe.show_progress(
|
||||
__('Importing Transactions'), 0, total,
|
||||
__('Starting import of {0} transactions...', [total])
|
||||
);
|
||||
|
||||
// Listen for realtime progress from background job
|
||||
frappe.realtime.off('kb_bt_import_progress');
|
||||
frappe.realtime.on('kb_bt_import_progress', function(data) {
|
||||
frappe.show_progress(
|
||||
__('Importing Transactions'), data.current, data.total,
|
||||
__('Importing transaction {0} of {1}', [data.current, data.total])
|
||||
);
|
||||
});
|
||||
|
||||
// Listen for completion from background job
|
||||
frappe.realtime.off('kb_bt_import_complete');
|
||||
frappe.realtime.on('kb_bt_import_complete', function(data) {
|
||||
frappe.realtime.off('kb_bt_import_progress');
|
||||
frappe.realtime.off('kb_bt_import_complete');
|
||||
KBBTImport.import._onBulkComplete(data);
|
||||
});
|
||||
|
||||
frappe.call({
|
||||
method: 'kapital_bank.bank_api.import_bulk_bank_transactions',
|
||||
args: { txn_list: JSON.stringify(selectedTxns) },
|
||||
callback: function(r) {
|
||||
// Job enqueued — progress & completion come via realtime events
|
||||
if (!r.message || !r.message.enqueued) {
|
||||
frappe.realtime.off('kb_bt_import_progress');
|
||||
frappe.realtime.off('kb_bt_import_complete');
|
||||
KBBTImport.import._closeProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: __('Failed to start import job')
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
frappe.realtime.off('kb_bt_import_progress');
|
||||
frappe.realtime.off('kb_bt_import_complete');
|
||||
KBBTImport.import._closeProgress();
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: __('Network error starting import')
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_onBulkComplete: function(data) {
|
||||
KBBTImport.import._closeProgress();
|
||||
|
||||
// Collect errors for the error panel
|
||||
(data.errors || []).forEach(function(err) {
|
||||
const txnData = err.txn_data || {};
|
||||
KBBTImport.errors.add(txnData, 'Import Error', err.message);
|
||||
});
|
||||
|
||||
KBBTImport.errors.showSummary(data.total, data.imported, data.errors ? data.errors.length : 0);
|
||||
|
||||
if (cur_list) {
|
||||
cur_list.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
_closeProgress: function() {
|
||||
if (frappe.cur_progress) {
|
||||
try {
|
||||
frappe.cur_progress.$wrapper.modal('hide');
|
||||
frappe.cur_progress.$wrapper.remove();
|
||||
} catch(e) {}
|
||||
frappe.cur_progress = null;
|
||||
}
|
||||
if ($('.modal:visible').length === 0) {
|
||||
$('.modal-backdrop').remove();
|
||||
$('body').removeClass('modal-open');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ======= LIST VIEW SETUP =======
|
||||
frappe.listview_settings['Bank Transaction'] = {
|
||||
onload(listview) {
|
||||
listview.page.add_inner_button(__('Import'), function() {
|
||||
KBBTImport.import.showDialog();
|
||||
}, __('Kapital Bank'));
|
||||
}
|
||||
};
|
||||
|
|
@ -661,8 +661,8 @@ KBImport.import = {
|
|||
// ======= LIST VIEW SETUP =======
|
||||
frappe.listview_settings['Payment Entry'] = {
|
||||
onload(listview) {
|
||||
listview.page.add_menu_item(__('Import from Kapital Bank'), function() {
|
||||
listview.page.add_inner_button(__('Import'), function() {
|
||||
KBImport.import.showDialog();
|
||||
});
|
||||
}, __('Kapital Bank'));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ after_migrate = "kapital_bank.setup.after_migrate"
|
|||
|
||||
# JS for ERPNext forms
|
||||
doctype_js = {
|
||||
"Payment Entry": "client/payment_entry.js"
|
||||
"Payment Entry": "client/payment_entry.js",
|
||||
"Bank Transaction": "client/bank_transaction.js",
|
||||
}
|
||||
|
||||
doctype_list_js = {
|
||||
"Payment Entry": "client/payment_entry.js"
|
||||
"Payment Entry": "client/payment_entry.js",
|
||||
"Bank Transaction": "client/bank_transaction.js",
|
||||
}
|
||||
|
||||
# Scheduled Tasks (refresh JWT every 4 minutes)
|
||||
|
|
@ -155,7 +157,11 @@ doc_events = {
|
|||
"Payment Entry": {
|
||||
"on_trash": "kapital_bank.bank_api.on_delete_payment_entry",
|
||||
"on_cancel": "kapital_bank.bank_api.on_cancel_payment_entry",
|
||||
}
|
||||
},
|
||||
"Bank Transaction": {
|
||||
"on_trash": "kapital_bank.bank_api.on_delete_bank_transaction",
|
||||
"on_cancel": "kapital_bank.bank_api.on_cancel_bank_transaction",
|
||||
},
|
||||
}
|
||||
|
||||
# Scheduled Tasks
|
||||
|
|
|
|||
|
|
@ -34,12 +34,14 @@
|
|||
{
|
||||
"fieldname": "bank_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Bank Account",
|
||||
"options": "Bank Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "gl_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "GL Account",
|
||||
"options": "Account"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,12 +35,14 @@
|
|||
{
|
||||
"fieldname": "bank_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Bank Account",
|
||||
"options": "Bank Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "gl_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "GL Account",
|
||||
"options": "Account"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
"default": "Manual",
|
||||
"fieldname": "mapping_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Mapping Type",
|
||||
"options": "Manual\nAutomatic",
|
||||
"read_only": 1
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
"default": "Manual",
|
||||
"fieldname": "mapping_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Mapping Type",
|
||||
"options": "Manual\nAutomatic",
|
||||
"read_only": 1
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"source_label",
|
||||
"link_section",
|
||||
"payment_entry",
|
||||
"bank_transaction",
|
||||
"status",
|
||||
"column_break_2",
|
||||
"import_date",
|
||||
|
|
@ -81,6 +82,13 @@
|
|||
"options": "Payment Entry",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_transaction",
|
||||
"fieldtype": "Link",
|
||||
"label": "Bank Transaction",
|
||||
"options": "Bank Transaction",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "Imported",
|
||||
"fieldname": "status",
|
||||
|
|
@ -109,7 +117,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-27 00:00:00.000000",
|
||||
"modified": "2026-03-03 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Kapital Bank",
|
||||
"name": "Kapital Bank Transaction",
|
||||
|
|
|
|||
Loading…
Reference in New Issue