From b24e35071d97c68559dbdda5349a151068e7d48a Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Wed, 21 May 2025 16:30:39 +0400 Subject: [PATCH] fixed a lot of bugs, optimized code and a lot more --- invoice_az/api.py | 904 +++++++++++------- invoice_az/client/e_taxes_items_list.js | 182 ++-- invoice_az/client/e_taxes_parties_list.js | 175 ++-- invoice_az/client/purchase_order.js | 844 ++++++++-------- invoice_az/hooks.py | 11 + .../doctype/asan_login/asan_login.json | 12 +- 6 files changed, 1271 insertions(+), 857 deletions(-) diff --git a/invoice_az/api.py b/invoice_az/api.py index c3c6422..bf18eca 100644 --- a/invoice_az/api.py +++ b/invoice_az/api.py @@ -1,14 +1,307 @@ import frappe import requests import json -from frappe.utils import nowdate +from frappe.utils import nowdate, now, cint, now_datetime, get_datetime from frappe.model.document import Document import re from difflib import SequenceMatcher +import datetime +import random +import time + +# Константы +RENEW_URL = "https://new.e-taxes.gov.az/api/po/auth/public/v1/renew" +MAX_RETRY_COUNT = 3 +DEFAULT_HEADERS = { + "Accept": "application/json, text/plain, */*", + "Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Referer": "https://new.e-taxes.gov.az/eportal/az/login/asan" +} +ACTIVITY_TIMEOUT_MINUTES = 30 # Настраиваемый параметр времени неактивности + +@frappe.whitelist() +def record_etaxes_activity(asan_login_name=None): + """Записывает время последней активности с e-taxes с задержкой 2 секунды""" + try: + # Если имя не указано, получаем дефолтный профиль + if not asan_login_name: + default_settings = frappe.get_all( + "Asan Login", + filters={"is_default": 1}, + fields=["name"], + limit=1 + ) + + if not default_settings: + return + + asan_login_name = default_settings[0].name + + # Запускаем отложенную задачу для обновления поля last_activity_time + frappe.enqueue( + 'invoice_az.api.update_activity_time', + asan_login_name=asan_login_name, + queue='short', + timeout=300 + ) + except Exception as e: + frappe.log_error(f"Error recording activity: {str(e)}", "Activity Recording Error") + +@frappe.whitelist() +def update_activity_time(asan_login_name): + """Выполняет фактическое обновление поля last_activity_time с задержкой""" + try: + # Добавляем задержку для избежания конфликтов + time.sleep(2) + + # Обновляем поле last_activity_time напрямую через БД + frappe.db.set_value("Asan Login", asan_login_name, "last_activity_time", now_datetime(), update_modified=False) + frappe.db.commit() + except Exception as e: + frappe.log_error(f"Error updating activity time: {str(e)}", "Activity Update Error") + +def check_recent_activity(asan_login_name=None): + """Проверяет, была ли активность пользователя за последние N минут""" + try: + # Если имя не указано, получаем дефолтный профиль + if not asan_login_name: + default_settings = frappe.get_all( + "Asan Login", + filters={"is_default": 1}, + fields=["name"], + limit=1 + ) + + if not default_settings: + return False + + asan_login_name = default_settings[0].name + + # Получаем время последней активности напрямую из БД + last_activity = frappe.db.get_value("Asan Login", asan_login_name, "last_activity_time") + + # Если последней активности не было, возвращаем False + if not last_activity: + return False + + # Проверяем, была ли активность за последние N минут + time_diff = now_datetime() - get_datetime(last_activity) + minutes_diff = time_diff.total_seconds() / 60 + + return minutes_diff <= ACTIVITY_TIMEOUT_MINUTES + except Exception as e: + frappe.log_error(f"Error checking activity: {str(e)}", "Activity Check Error") + return False # В случае ошибки считаем, что активности не было + + +@frappe.whitelist() +def renew_token(asan_login_name=None, retry_count=0, force=False): + """Renews the main token with optimized performance and error handling""" + try: + # Получаем документ Asan Login + if not asan_login_name: + # Простой прямой запрос для получения дефолтного логина + default_settings = frappe.get_all( + "Asan Login", + filters={"is_default": 1}, + fields=["name"], + limit=1 + ) + + if not default_settings: + frappe.log_error("No default Asan Login found", "Token Renewal Error") + return {"success": False, "message": "No default Asan Login found"} + + asan_login_name = default_settings[0].name + + # Проверяем наличие активности, если force=False + if not check_recent_activity(asan_login_name): + frappe.logger().info(f"Token renewal skipped due to inactivity for {asan_login_name}") + return {"success": True, "message": "Token renewal skipped due to inactivity"} + + # Получаем документ (без блокировки, поскольку запуск один) + asan_login = frappe.get_doc("Asan Login", asan_login_name) + + # Проверяем наличие токена + if not asan_login.main_token: + frappe.log_error(f"No main token found for {asan_login_name}", "Token Renewal Error") + return {"success": False, "message": "No main token found"} + + # Формируем заголовок авторизации + auth_header = f"Bearer {asan_login.main_token}" + + # Подготавливаем заголовки запроса + headers = DEFAULT_HEADERS.copy() + headers["x-authorization"] = auth_header + + # Отправляем запрос с таймаутом + response = requests.post(RENEW_URL, headers=headers, timeout=10) + + # Check for specific HTTP status codes + if response.status_code == 500: + return { + "error": "server_error", + "message": "Service temporarily unavailable. Please try again in a few minutes.", + "status_code": 500 + } + + # Обрабатываем ответ + if response.status_code == 200: + # Получаем новый токен из заголовков ответа + new_token = response.headers.get('x-authorization') + + if new_token: + frappe.logger().info(f"Token renewal successful for {asan_login_name}") + + # Проверяем, изменился ли токен + if new_token != asan_login.main_token: + # Обновляем токен напрямую через БД, чтобы избежать конфликта версий + frappe.db.set_value("Asan Login", asan_login_name, "main_token", new_token) + frappe.db.commit() + frappe.logger().info(f"Token updated for {asan_login_name}") + else: + frappe.logger().info(f"Token unchanged for {asan_login_name}") + + return {"success": True, "message": "Token renewed successfully"} + else: + frappe.log_error( + "Token renewal received 200 but no token in response headers", + f"Token Renewal for {asan_login_name}" + ) + return {"success": False, "message": "No token in response"} + + elif response.status_code == 401: + # В случае ошибки авторизации меняем статус + frappe.db.set_value("Asan Login", asan_login_name, "auth_status", "Not Authenticated") + frappe.db.commit() + + frappe.log_error( + f"Authorization error (401) for {asan_login_name}", + "Token Renewal Error" + ) + return { + "error": "unauthorized", + "message": "Authentication required. Please login again.", + "status_code": 401 + } + + else: + # Механизм повторных попыток при временных ошибках + if retry_count < MAX_RETRY_COUNT and response.status_code >= 500: + # Экспоненциальная задержка между попытками + wait_time = (2 ** retry_count) * (0.5 + random.random()) + time.sleep(wait_time) + return renew_token(asan_login_name, retry_count + 1) + + frappe.log_error( + f"Failed with status {response.status_code}: {response.text[:200]}", + f"Token Renewal for {asan_login_name}" + ) + return {"success": False, "message": f"Failed with status {response.status_code}"} + + except requests.RequestException as e: + # Сетевые ошибки подключения + if retry_count < MAX_RETRY_COUNT: + wait_time = (2 ** retry_count) * (0.5 + random.random()) + time.sleep(wait_time) + return renew_token(asan_login_name, retry_count + 1) + + # Check if the error is HTTP 401 or 500 + if hasattr(e, 'response'): + if e.response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again.", + "status_code": 401 + } + elif e.response.status_code == 500: + return { + "error": "server_error", + "message": "Service temporarily unavailable. Please try again in a few minutes.", + "status_code": 500 + } + + frappe.log_error( + f"Network error: {str(e)}", + f"Token Renewal for {asan_login_name}" + ) + return {"success": False, "message": f"Network error: {str(e)}"} + + except Exception as e: + frappe.log_error( + f"Unexpected error: {str(e)}\n{frappe.get_traceback()}", + f"Token Renewal for {asan_login_name}" + ) + return {"success": False, "message": "An unknown error occurred, please try again in a few minutes."} + +@frappe.whitelist() +def setup_token_renewal(): + """Настраивает задание планировщика для обновления токенов""" + try: + # Проверка существования задания + job_exists = frappe.db.exists("Scheduled Job Type", "renew_etaxes_token") + + if not job_exists: + # Создаем новое задание + doc = frappe.new_doc("Scheduled Job Type") + doc.update({ + "name": "renew_etaxes_token", + "method": "invoice_az.api.renew_token", # Прямой вызов renew_token + "frequency": "Cron", + "cron_format": "*/4 * * * *", # Каждые 4 минуты + "create_log": 1, + "execute_alone": 1, + "enabled": 1 + }) + doc.insert(ignore_permissions=True) + frappe.db.commit() + + # Добавляем в планировщик немедленно + try: + from frappe.utils.background_jobs import get_scheduler + scheduler = get_scheduler() + if scheduler: + scheduler.add_scheduled_job(doc) + except Exception as e: + frappe.log_error(f"Could not add job to scheduler: {str(e)}", "Scheduler Error") + + return {"success": True, "message": "Scheduler job created"} + else: + # Обновляем существующее задание если нужно + job = frappe.get_doc("Scheduled Job Type", "renew_etaxes_token") + + if job.method != "invoice_az.api.renew_token" or job.cron_format != "*/4 * * * *": + job.method = "invoice_az.api.renew_token" + job.cron_format = "*/4 * * * *" + job.save(ignore_permissions=True) + frappe.db.commit() + + # Обновляем в планировщике + try: + from frappe.utils.background_jobs import get_scheduler + scheduler = get_scheduler() + if scheduler: + scheduler.remove_scheduled_job(job.name) + scheduler.add_scheduled_job(job) + except Exception as e: + frappe.log_error(f"Could not update scheduler: {str(e)}", "Scheduler Error") + + return {"success": True, "message": "Scheduler job updated"} + + return {"success": True, "message": "Scheduler job already exists"} + + except Exception as e: + frappe.log_error(f"Error setting up token renewal: {str(e)}\n{frappe.get_traceback()}", "Token Renewal Setup Error") + return {"success": False, "message": str(e)} @frappe.whitelist() def get_auth_token(phone, user_id): """Getting authorization token""" + # Записываем активность + record_etaxes_activity() + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/start" payload = { @@ -27,6 +320,22 @@ def get_auth_token(phone, user_id): try: response = requests.post(url, data=json.dumps(payload), headers=headers) + + # Check for specific HTTP status codes + if response.status_code == 500: + return { + "error": "server_error", + "message": "Service temporarily unavailable. Please try again in a few minutes.", + "status_code": 500 + } + + if response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again.", + "status_code": 401 + } + response.raise_for_status() # Getting token from the response header @@ -39,12 +348,32 @@ def get_auth_token(phone, user_id): 'bearer_token': bearer_token, 'response_data': resp_data } + except requests.exceptions.HTTPError as e: + # Check if the error is HTTP 401 or 500 + if e.response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again.", + "status_code": 401 + } + elif e.response.status_code == 500: + return { + "error": "server_error", + "message": "Service temporarily unavailable. Please try again in a few minutes.", + "status_code": 500 + } + frappe.log_error(f"HTTP error in get_auth_token: {str(e)}", "API Error") + return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."} except Exception as e: - frappe.throw(f"Error getting auth token: {str(e)}") + frappe.log_error(f"Error getting auth token: {str(e)}", "API Error") + return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."} @frappe.whitelist() def check_auth_status(bearer_token): """Checking authorization status""" + # Записываем активность + record_etaxes_activity() + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/status" headers = { @@ -61,18 +390,54 @@ def check_auth_status(bearer_token): try: response = requests.get(url, headers=headers) + + # Check for specific HTTP status codes + if response.status_code == 500: + return { + "error": "server_error", + "message": "Service temporarily unavailable. Please try again in a few minutes.", + "status_code": 500 + } + + if response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again.", + "status_code": 401 + } + response.raise_for_status() # Getting status data status_data = response.json() return status_data + except requests.exceptions.HTTPError as e: + # Check if the error is HTTP 401 or 500 + if e.response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again.", + "status_code": 401 + } + elif e.response.status_code == 500: + return { + "error": "server_error", + "message": "Service temporarily unavailable. Please try again in a few minutes.", + "status_code": 500 + } + frappe.log_error(f"HTTP error in check_auth_status: {str(e)}", "API Error") + return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."} except Exception as e: - frappe.throw(f"Error checking auth status: {str(e)}") + frappe.log_error(f"Error checking auth status: {str(e)}", "API Error") + return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."} @frappe.whitelist() def get_invoices(token, filters=None): """Getting list of invoices with filtering options""" + # Записываем активность + record_etaxes_activity() + url = "https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/find.inbox" # Base request parameters @@ -154,13 +519,18 @@ def get_invoices(token, filters=None): "status_code": 500 } - frappe.throw(f"Error getting invoices: {str(e)}") + frappe.log_error(f"HTTP error in get_invoices: {str(e)}", "API Error") + return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."} except Exception as e: - frappe.throw(f"Error getting invoices: {str(e)}") + frappe.log_error(f"Error getting invoices: {str(e)}", "API Error") + return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."} @frappe.whitelist() def get_certificates(bearer_token): """Getting available certificates""" + # Записываем активность + record_etaxes_activity() + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/certificates" headers = { @@ -207,13 +577,18 @@ def get_certificates(bearer_token): "status_code": 500 } - frappe.throw(f"Error getting certificates: {str(e)}") + frappe.log_error(f"HTTP error in get_certificates: {str(e)}", "API Error") + return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."} except Exception as e: - frappe.throw(f"Error getting certificates: {str(e)}") + frappe.log_error(f"Error getting certificates: {str(e)}", "API Error") + return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."} @frappe.whitelist() def choose_taxpayer(bearer_token, owner_type, tin): """Selecting taxpayer after certificate selection""" + # Записываем активность + record_etaxes_activity() + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/chooseTaxpayer" payload = { @@ -278,15 +653,19 @@ def choose_taxpayer(bearer_token, owner_type, tin): "message": "Service temporarily unavailable. Please try again in a few minutes.", "status_code": 500 } - - - frappe.throw(f"Error choosing taxpayer: {str(e)}") + + frappe.log_error(f"HTTP error in choose_taxpayer: {str(e)}", "API Error") + return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."} except Exception as e: - frappe.throw(f"Error choosing taxpayer: {str(e)}") + frappe.log_error(f"Error choosing taxpayer: {str(e)}", "API Error") + return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."} @frappe.whitelist() def get_invoice_details(token, invoice_id): """Getting detailed information about an invoice""" + # Записываем активность + record_etaxes_activity() + url = f"https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/{invoice_id}?sourceSystem=avis" headers = { @@ -331,9 +710,11 @@ def get_invoice_details(token, invoice_id): "status_code": 500 } - frappe.throw(f"Error getting invoice details: {str(e)}") + frappe.log_error(f"HTTP error in get_invoice_details: {str(e)}", "API Error") + return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."} except Exception as e: - frappe.throw(f"Error getting invoice details: {str(e)}") + frappe.log_error(f"Error getting invoice details: {str(e)}", "API Error") + return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."} @frappe.whitelist() def get_default_asan_login(): @@ -387,6 +768,9 @@ def get_default_asan_login(): @frappe.whitelist() def handle_authentication(asan_login_name=None): """Handles the authentication process for the specified or default setting""" + # Записываем активность + record_etaxes_activity() + try: # If setting name is not specified, get default setting name if not asan_login_name: @@ -405,6 +789,10 @@ def handle_authentication(asan_login_name=None): # Start the authorization process auth_result = get_auth_token(asan_login.phone, asan_login.user_id) + # Check for errors + if auth_result and auth_result.get("error"): + return {"success": False, "message": auth_result.get("message")} + if auth_result and auth_result.get("bearer_token"): # Save token and change status asan_login.bearer_token = auth_result.get("bearer_token") @@ -428,15 +816,22 @@ def handle_authentication(asan_login_name=None): except Exception as e: frappe.log_error(f"Error in handle_authentication: {str(e)}\n{frappe.get_traceback()}", "Authentication Error") - return {"success": False, "message": f"Error: {str(e)}"} + return {"success": False, "message": "An unknown error occurred, please try again in a few minutes."} @frappe.whitelist() def poll_auth_status(asan_login_name, bearer_token): """Checks authentication status""" + # Записываем активность + record_etaxes_activity() + try: # Check status status_result = check_auth_status(bearer_token) + # Check for errors + if status_result and status_result.get("error"): + return {"success": False, "message": status_result.get("message")} + # Get Asan Login document asan_login = frappe.get_doc("Asan Login", asan_login_name) @@ -460,11 +855,14 @@ def poll_auth_status(asan_login_name, bearer_token): } except Exception as e: - return {"success": False, "authenticated": False, "message": f"Error: {str(e)}"} + return {"success": False, "authenticated": False, "message": "An unknown error occurred, please try again in a few minutes."} @frappe.whitelist() def get_auth_certificates(asan_login_name): """Gets certificates for the specified Asan Login setting""" + # Записываем активность + record_etaxes_activity() + try: # Get Asan Login document asan_login = frappe.get_doc("Asan Login", asan_login_name) @@ -484,6 +882,20 @@ def get_auth_certificates(asan_login_name): "message": "Authentication required. Please login again." } + # Check for server error + if certificates_result.get("error") == "server_error": + return { + "success": False, + "message": "Service temporarily unavailable. Please try again in a few minutes." + } + + # Check for other errors + if certificates_result.get("error"): + return { + "success": False, + "message": certificates_result.get("message", "An unknown error occurred, please try again in a few minutes.") + } + # Check for certificates in the response if certificates_result and certificates_result.get("certificates"): # Save certificates in the document @@ -499,11 +911,14 @@ def get_auth_certificates(asan_login_name): return {"success": False, "message": "No certificates found."} except Exception as e: - return {"success": False, "message": f"Error: {str(e)}"} + return {"success": False, "message": "An unknown error occurred, please try again in a few minutes."} @frappe.whitelist() def select_certificate(asan_login_name, certificate_data, certificate_name): """Selects certificate for the specified Asan Login setting""" + # Записываем активность + record_etaxes_activity() + try: # Get Asan Login document asan_login = frappe.get_doc("Asan Login", asan_login_name) @@ -528,11 +943,14 @@ def select_certificate(asan_login_name, certificate_data, certificate_name): except Exception as e: frappe.log_error(f"Error in select_certificate: {str(e)}\n{frappe.get_traceback()}", "Select Certificate Error") - return {"success": False, "message": f"Error: {str(e)}"} + return {"success": False, "message": "An unknown error occurred, please try again in a few minutes."} @frappe.whitelist() def select_taxpayer(asan_login_name): """Selects taxpayer for the specified Asan Login setting""" + # Записываем активность + record_etaxes_activity() + try: # Get Asan Login document asan_login = frappe.get_doc("Asan Login", asan_login_name) @@ -579,6 +997,20 @@ def select_taxpayer(asan_login_name): "message": "Authentication required. Please login again." } + # Check for server error + if taxpayer_result.get("error") == "server_error": + return { + "success": False, + "message": "Service temporarily unavailable. Please try again in a few minutes." + } + + # Check for other errors + if taxpayer_result.get("error"): + return { + "success": False, + "message": taxpayer_result.get("message", "An unknown error occurred, please try again in a few minutes.") + } + # Check selection success if taxpayer_result and taxpayer_result.get("main_token"): # Save main token and response @@ -608,72 +1040,9 @@ def select_taxpayer(asan_login_name): except Exception as e: frappe.log_error(f"Error in select_taxpayer: {str(e)}\n{frappe.get_traceback()}", "Choose Taxpayer Exception") - return {"success": False, "message": f"Error: {str(e)}"} + return {"success": False, "message": "An unknown error occurred, please try again in a few minutes."} @frappe.whitelist() -def import_invoice_to_purchase_order(invoice_data, purchase_order=None): - """Imports invoice data to Purchase Order""" - try: - # Check and convert invoice data - if isinstance(invoice_data, str): - invoice_data = json.loads(invoice_data) - - # Determine whether to create a new Purchase Order or update an existing one - if purchase_order: - # Get existing document - po_doc = frappe.get_doc("Purchase Order", purchase_order) - else: - # Create new document - po_doc = frappe.new_doc("Purchase Order") - - # Set basic fields for new document - supplier_name = invoice_data.get("sender", {}).get("name") or invoice_data.get("senderName") - supplier_tin = invoice_data.get("sender", {}).get("tin") or invoice_data.get("senderTin") - - # Find supplier by TIN or create a new one - supplier = find_or_create_supplier(supplier_name, supplier_tin) - - po_doc.supplier = supplier - po_doc.transaction_date = frappe.utils.today() - po_doc.schedule_date = frappe.utils.today() - - # Add/update additional fields from invoice - po_doc.title = f"Invoice {invoice_data.get('serialNumber', '')}" - po_doc.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "") - - # Clear existing items if need to update them completely - if purchase_order and invoice_data.get("items"): - po_doc.items = [] - - # Add items from invoice - if invoice_data.get("items"): - for item in invoice_data.get("items", []): - item_code = find_or_create_item(item.get("productName"), item.get("itemId")) - - po_item = { - "item_code": item_code, - "item_name": item.get("productName", ""), - "description": item.get("productName", ""), - "qty": item.get("quantity", 0), - "rate": item.get("pricePerUnit", 0), - "amount": item.get("cost", 0), - "uom": get_uom_for_unit(item.get("unit", "")) - } - - po_doc.append("items", po_item) - - # Save document - po_doc.save() - - return { - "success": True, - "message": "Invoice data imported successfully.", - "purchase_order": po_doc.name - } - - except Exception as e: - return {"success": False, "message": f"Error: {str(e)}"} - def find_or_create_item(item_name, item_code=None, unit=None): """Finds or creates an item by name and code""" try: @@ -737,6 +1106,7 @@ def find_or_create_item(item_name, item_code=None, unit=None): except Exception as e: return None +@frappe.whitelist() def find_or_create_supplier(supplier_name, supplier_tin): """Finds or creates a supplier by name and TIN""" try: @@ -798,6 +1168,7 @@ def find_or_create_supplier(supplier_name, supplier_tin): except Exception as e: return None +@frappe.whitelist() def get_uom_for_unit(unit_name): """Converts unit name from invoice to system UOM""" # Conversion dictionary @@ -814,121 +1185,12 @@ def get_uom_for_unit(unit_name): # Return corresponding UOM or "Nos" by default return uom_mapping.get(unit_name.lower(), "Nos") -@frappe.whitelist() -def find_or_create_item_with_mapping(item_name, item_code=None, unit=None): - """Finds or creates an item taking into account mapping settings""" - try: - # First check for mapping in settings - try: - settings = frappe.get_doc("ETaxes Settings") - - # Look for mapping by item name - for mapping in settings.item_mappings: - if mapping.etaxes_item_name == item_name: - # If mapping found, use specified item - return mapping.item_code - except: - pass # If there are no settings or an error occurred, continue standard search - - # If mapping not found, use standard search - return find_or_create_item(item_name, item_code, unit) - except Exception as e: - return None - -@frappe.whitelist() -def find_or_create_supplier_with_mapping(supplier_name, supplier_tin): - """Finds or creates a supplier taking into account mapping settings""" - try: - # First check for mapping in settings - try: - settings = frappe.get_doc("ETaxes Settings") - - # Look for mapping by supplier TIN - for mapping in settings.supplier_mappings: - if mapping.etaxes_supplier_tin == supplier_tin: - # If mapping found, use specified supplier - return mapping.supplier - except: - pass # If there are no settings or an error occurred, continue standard search - - # If mapping not found, use standard search - return find_or_create_supplier(supplier_name, supplier_tin) - except Exception as e: - return None - - -@frappe.whitelist() -def import_invoice_to_purchase_order_with_mapping(invoice_data, purchase_order=None): - """Imports invoice data to Purchase Order taking into account mapping settings""" - try: - # Check and convert invoice data - if isinstance(invoice_data, str): - invoice_data = json.loads(invoice_data) - - # Determine whether to create a new Purchase Order or update an existing one - if purchase_order: - # Get existing document - po_doc = frappe.get_doc("Purchase Order", purchase_order) - else: - # Create new document - po_doc = frappe.new_doc("Purchase Order") - - # Set basic fields for new document - supplier_name = invoice_data.get("sender", {}).get("name") or invoice_data.get("senderName") - supplier_tin = invoice_data.get("sender", {}).get("tin") or invoice_data.get("senderTin") - - # Find supplier taking into account mappings - supplier = find_or_create_supplier_with_mapping(supplier_name, supplier_tin) - - po_doc.supplier = supplier - po_doc.transaction_date = frappe.utils.today() - po_doc.schedule_date = frappe.utils.today() - - # Add/update additional fields from invoice - po_doc.title = f"Invoice {invoice_data.get('serialNumber', '')}" - po_doc.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "") - - # Clear existing items if need to update them completely - if purchase_order and invoice_data.get("items"): - po_doc.items = [] - - # Add items from invoice - if invoice_data.get("items"): - for item in invoice_data.get("items", []): - # Use function taking into account mappings - item_code = find_or_create_item_with_mapping( - item.get("productName"), - item.get("itemId"), - item.get("unit") - ) - - po_item = { - "item_code": item_code, - "item_name": item.get("productName", ""), - "description": item.get("productName", ""), - "qty": item.get("quantity", 0), - "rate": item.get("pricePerUnit", 0), - "amount": item.get("cost", 0), - "uom": get_uom_for_unit(item.get("unit", "")) - } - - po_doc.append("items", po_item) - - # Save document - po_doc.save() - - return { - "success": True, - "message": "Invoice data imported successfully.", - "purchase_order": po_doc.name - } - - except Exception as e: - return {"success": False, "message": f"Error: {str(e)}"} - @frappe.whitelist() def load_items_from_invoices(date_from, date_to, max_count=200): """Loading items from invoices for a period""" + # Записываем активность + record_etaxes_activity() + try: # Counters for tracking created_count = 0 @@ -978,7 +1240,7 @@ def load_items_from_invoices(date_from, date_to, max_count=200): if 'error' in response: return { 'success': False, - 'message': response['error'] + 'message': response['message'] if 'message' in response else 'Failed to retrieve invoices' } # Extract data from response @@ -1042,12 +1304,15 @@ def load_items_from_invoices(date_from, date_to, max_count=200): except Exception as e: return { 'success': False, - 'message': str(e) + 'message': "An unknown error occurred, please try again in a few minutes." } @frappe.whitelist() -def load_parties_from_invoices(date_from, date_to, max_count=200): +def load_parties_from_invoices(date_from, date_to, max_count=200, invoice_type="purchase"): """Loading parties from invoices for a period""" + # Записываем активность + record_etaxes_activity() + try: # Counters for tracking created_count = 0 @@ -1097,7 +1362,7 @@ def load_parties_from_invoices(date_from, date_to, max_count=200): if 'error' in response: return { 'success': False, - 'message': response['error'] + 'message': response['message'] if 'message' in response else 'Failed to retrieve invoices' } # Extract data from response @@ -1149,30 +1414,31 @@ def load_parties_from_invoices(date_from, date_to, max_count=200): 'status': 'New' } - # Processing receiver (Receiver -> Customer) - receiver = invoice.get('receiver', {}) - - if receiver: - receiver_name = receiver.get('name', '') - receiver_tin = receiver.get('tin', '') - receiver_address = receiver.get('address', '') + # Processing receiver (Receiver -> Customer) - Skip for purchase invoices + if invoice_type.lower() != "purchase": + receiver = invoice.get('receiver', {}) - if not receiver_name or not receiver_tin: - continue - - # Create key for uniqueness check - receiver_key = f"{receiver_tin}|{receiver_name}" - - if receiver_key not in unique_parties: - unique_parties[receiver_key] = { - 'etaxes_party_name': receiver_name, - 'etaxes_tax_id': receiver_tin, - 'etaxes_address': receiver_address, - 'etaxes_party_type': 'Receiver', - 'erp_party_type': 'Customer', - 'source_invoice': serial_number, - 'status': 'New' - } + if receiver: + receiver_name = receiver.get('name', '') + receiver_tin = receiver.get('tin', '') + receiver_address = receiver.get('address', '') + + if not receiver_name or not receiver_tin: + continue + + # Create key for uniqueness check + receiver_key = f"{receiver_tin}|{receiver_name}" + + if receiver_key not in unique_parties: + unique_parties[receiver_key] = { + 'etaxes_party_name': receiver_name, + 'etaxes_tax_id': receiver_tin, + 'etaxes_address': receiver_address, + 'etaxes_party_type': 'Receiver', + 'erp_party_type': 'Customer', + 'source_invoice': serial_number, + 'status': 'New' + } # Add parties to the system for party_key, party_data in unique_parties.items(): @@ -1216,10 +1482,9 @@ def load_parties_from_invoices(date_from, date_to, max_count=200): except Exception as e: return { 'success': False, - 'message': str(e) + 'message': "An unknown error occurred, please try again in a few minutes." } - @frappe.whitelist() def get_default_settings(): """Getting active E-Taxes settings""" @@ -1245,6 +1510,7 @@ def get_default_settings(): 'message': str(e) } +@frappe.whitelist() def normalize_azeri_text(text): """Normalization of Azerbaijani text for matching""" if not text: @@ -1288,6 +1554,7 @@ def normalize_azeri_text(text): return text_normalized, text_normalized_alt +@frappe.whitelist() def calculate_similarity(str1, str2, consider_azeri=True): """Calculating similarity between strings""" if not str1 or not str2: @@ -1365,7 +1632,7 @@ def get_unmapped_items(): except Exception as e: return { 'success': False, - 'message': str(e) + 'message': "An unknown error occurred, please try again in a few minutes." } @frappe.whitelist() @@ -1435,12 +1702,15 @@ def get_unmapped_parties(): except Exception as e: return { 'success': False, - 'message': str(e) + 'message': "An unknown error occurred, please try again in a few minutes." } @frappe.whitelist() def match_similar_items(): """Matching items by similar names""" + # Записываем активность + record_etaxes_activity() + try: import re from difflib import SequenceMatcher @@ -1598,12 +1868,15 @@ def match_similar_items(): except Exception as e: return { 'success': False, - 'message': str(e) + 'message': "An unknown error occurred, please try again in a few minutes." } @frappe.whitelist() def match_similar_parties(): """Matching parties by similar names""" + # Записываем активность + record_etaxes_activity() + try: import re from difflib import SequenceMatcher @@ -1749,13 +2022,15 @@ def match_similar_parties(): frappe.log_error(frappe.get_traceback(), f"Error in match_similar_parties: {str(e)}") return { 'success': False, - 'message': str(e) + 'message': "An unknown error occurred, please try again in a few minutes." } - @frappe.whitelist() def create_unmapped_items(settings_name): """Creating items for unmapped elements from E-Taxes settings table""" + # Записываем активность + record_etaxes_activity() + try: import re @@ -1902,12 +2177,15 @@ def create_unmapped_items(settings_name): except Exception as e: return { "success": False, - "message": str(e) + "message": "An unknown error occurred, please try again in a few minutes." } @frappe.whitelist() def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule_date=None, warehouse=None): """Import invoice taking into account mappings and processing unmapped elements""" + # Записываем активность + record_etaxes_activity() + try: # Deserialize if necessary if isinstance(invoice_data, str): @@ -1936,7 +2214,7 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule default_warehouse = warehouse if not default_warehouse: - # If warehouse not specified, try to get from settings +# If warehouse not specified, try to get from settings if hasattr(settings, 'default_warehouse') and settings.default_warehouse: default_warehouse = settings.default_warehouse else: @@ -2016,7 +2294,6 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule if purchase_order_name and invoice_data.get("items"): po.items = [] - # IMPORTANT: Set date_to_use for rows date_to_use = None if schedule_date: date_to_use = schedule_date @@ -2140,11 +2417,12 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule frappe.log_error(f"Error in import_invoice_with_mapping: {str(e)}\n{frappe.get_traceback()}", "Import Error") return { 'success': False, - 'message': str(e) + 'message': "An unknown error occurred, please try again in a few minutes." } # Helper functions +@frappe.whitelist() def get_active_settings(): """Getting active E-Taxes settings""" settings_list = frappe.get_all('E-Taxes Settings', @@ -2157,6 +2435,7 @@ def get_active_settings(): return frappe.get_doc('E-Taxes Settings', settings_list[0].name) +@frappe.whitelist() def normalize_string(text, consider_azeri=True): """Normalizing string for comparison""" import re @@ -2191,6 +2470,7 @@ def normalize_string(text, consider_azeri=True): return text +@frappe.whitelist() def get_uom_for_unit(unit_name): """Getting UOM for unit from E-Taxes""" if not unit_name: @@ -2221,78 +2501,12 @@ def get_uom_for_unit(unit_name): # If no match found, return Nos by default return 'Nos' -@frappe.whitelist() -def debug_azeri_matching(search_term, compare_with): - """Debug function for testing string comparison with Azerbaijani characters""" - try: - import re - from difflib import SequenceMatcher - - # Get active settings - settings = get_active_settings() - consider_azeri = settings.consider_azeri_chars if settings else False - - # String normalization function - def normalize_string(text, consider_azeri=False): - if not text: - return "" - - # Convert text to lowercase - text_lower = text.lower() - - # If need to consider Azerbaijani characters, perform replacements - if consider_azeri: - # Replace Azerbaijani characters with Latin equivalents for comparison - replacements = { - 'ə': 'e', 'ı': 'i', 'ö': 'o', 'ü': 'u', 'ş': 's', 'ç': 'c', 'ğ': 'g', - 'Ə': 'e', 'I': 'i', 'Ö': 'o', 'Ü': 'u', 'Ş': 's', 'Ç': 'c', 'Ğ': 'g' - } - text_replaced = text_lower - for az_char, lat_char in replacements.items(): - text_replaced = text_replaced.replace(az_char, lat_char) - else: - text_replaced = text_lower - - # Remove all characters except letters and numbers - text_clean = re.sub(r'[^a-z0-9]', '', text_replaced) - - return { - 'original': text, - 'lowercase': text_lower, - 'replaced': text_replaced if consider_azeri else None, - 'final': text_clean - } - - # Normalize both strings - search_norm = normalize_string(search_term, consider_azeri) - compare_norm = normalize_string(compare_with, consider_azeri) - - # Calculate similarity coefficient - score = SequenceMatcher(None, search_norm['final'], compare_norm['final']).ratio() - - return { - 'success': True, - 'consider_azeri': consider_azeri, - 'search_term': { - 'original': search_term, - 'normalized': search_norm, - }, - 'compare_with': { - 'original': compare_with, - 'normalized': compare_norm, - }, - 'score': score - } - - except Exception as e: - return { - 'success': False, - 'message': str(e) - } - @frappe.whitelist() def create_unmapped_parties(settings_name): """Creating parties for unmapped elements from E-Taxes settings table""" + # Записываем активность + record_etaxes_activity() + try: # Get settings settings_doc = frappe.get_doc("E-Taxes Settings", settings_name) @@ -2444,9 +2658,10 @@ def create_unmapped_parties(settings_name): except Exception as e: return { "success": False, - "message": str(e) + "message": "An unknown error occurred, please try again in a few minutes." } +@frappe.whitelist() def handle_unauthorized_request(asan_login_name, operation_name="API request"): """Common function for handling unauthorized requests""" # Log event @@ -2463,56 +2678,67 @@ def handle_unauthorized_request(asan_login_name, operation_name="API request"): @frappe.whitelist() def check_token_validity(asan_login_name=None): - """Checks token validity for specified Asan Login setting""" + """Проверяет валидность main_token для указанных настроек Asan Login""" + # Записываем активность + record_etaxes_activity() + try: - # If name not specified, get default settings + # Если имя не указано, получаем настройки по умолчанию if not asan_login_name: default_settings = get_default_asan_login() if not default_settings.get("found", False): return {"valid": False, "message": "No Asan Login settings found."} asan_login_name = default_settings.get("name") - # Get Asan Login document + # Получаем документ Asan Login asan_login = frappe.get_doc("Asan Login", asan_login_name) - # Check for main_token (important for E-Taxes) + # Проверяем наличие main_token if not asan_login.main_token: return {"valid": False, "message": "Main token not found."} - # Try to get certificates to check token - certificates_result = get_certificates(asan_login.bearer_token) + # Создаем минимальный фильтр для запроса инвойсов + minimal_filter = { + "maxCount": 1, # Запрашиваем только один инвойс для минимальной нагрузки + "offset": 0 + } - # Check for authorization error - if certificates_result.get("error") == "unauthorized": + # Пробуем получить список инвойсов для проверки main_token + invoices_result = get_invoices(asan_login.main_token, json.dumps(minimal_filter)) + + # Проверяем на ошибку авторизации + if invoices_result.get("error") == "unauthorized": + # Обновляем статус авторизации в документе + asan_login.auth_status = "Not Authenticated" + asan_login.save(ignore_permissions=True) + frappe.db.commit() + return { "valid": False, - "message": "Authentication required. Token is expired." + "message": "Authentication required. Main token is expired." + } + + # Проверяем на ошибку сервера + if invoices_result.get("error") == "server_error": + return { + "valid": False, + "message": "Service temporarily unavailable. Please try again in a few minutes." + } + + # Проверяем на другие ошибки + if invoices_result.get("error"): + return { + "valid": False, + "message": invoices_result.get("message", "An unknown error occurred.") } - # If no error, token is valid - return {"valid": True, "message": "Token is valid."} + # Если нет ошибок, токен валиден + return {"valid": True, "message": "Main token is valid."} except Exception as e: frappe.log_error(f"Error in check_token_validity: {str(e)}\n{frappe.get_traceback()}", "Token Validity Check") - return {"valid": False, "message": f"Error: {str(e)}"} - -@frappe.whitelist() -def check_invoice_exists(serial_number): - """Checks if invoice with specified serial number already exists""" - try: - # Check if order with specified serial number exists - exists = frappe.db.exists('Purchase Order', {'invoice_serial_number': serial_number, 'docstatus': ['!=', 2]}) - - return { - 'exists': bool(exists), - 'serial_number': serial_number - } - except Exception as e: - frappe.log_error(f"Error in check_invoice_exists: {str(e)}", "API Error") - return { - 'exists': False, - 'error': str(e) - } + return {"valid": False, "message": "An unknown error occurred, please try again in a few minutes."} + @frappe.whitelist() def get_etaxes_purchases(): """Gets list of all E-Taxes Purchase records""" @@ -2531,12 +2757,15 @@ def get_etaxes_purchases(): frappe.log_error(f"Error in get_etaxes_purchases: {str(e)}", "API Error") return { 'success': False, - 'message': str(e) + 'message': "An unknown error occurred, please try again in a few minutes." } @frappe.whitelist() def create_etaxes_purchase(etaxes_id, date, party, total): """Creates E-Taxes Purchase record for tracking imported invoices""" + # Записываем активность + record_etaxes_activity() + try: # Normalize ID for storage etaxes_id = str(etaxes_id).strip() if etaxes_id else "" @@ -2577,12 +2806,15 @@ def create_etaxes_purchase(etaxes_id, date, party, total): frappe.log_error(f"Error in create_etaxes_purchase: {str(e)}\n{frappe.get_traceback()}", "API Error") return { 'success': False, - 'message': str(e) + 'message': "An unknown error occurred, please try again in a few minutes." } @frappe.whitelist() def link_purchase_order_to_etaxes(purchase_order, etaxes_purchase): """Sets link between Purchase Order and E-Taxes Purchase""" + # Записываем активность + record_etaxes_activity() + try: # Debug logging frappe.logger().info(f"Linking Purchase Order {purchase_order} to E-Taxes Purchase {etaxes_purchase}") @@ -2619,9 +2851,10 @@ def link_purchase_order_to_etaxes(purchase_order, etaxes_purchase): frappe.log_error(f"Error in link_purchase_order_to_etaxes: {str(e)}\n{frappe.get_traceback()}", "API Error") return { 'success': False, - 'message': str(e) + 'message': "An unknown error occurred, please try again in a few minutes." } +@frappe.whitelist() def on_delete_purchase_order(doc, method): """Deletes related E-Taxes Purchase record when Purchase Order is deleted""" if doc.is_taxes_doc and doc.taxes_doc: @@ -2635,5 +2868,4 @@ def on_delete_purchase_order(doc, method): frappe.logger().info(f"Successfully deleted E-Taxes Purchase {doc.taxes_doc}") except Exception as e: frappe.log_error(f"Error deleting E-Taxes Purchase {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}", - "Purchase Order Delete Error") - \ No newline at end of file + "Purchase Order Delete Error") \ No newline at end of file diff --git a/invoice_az/client/e_taxes_items_list.js b/invoice_az/client/e_taxes_items_list.js index 71e87b0..4bf4358 100644 --- a/invoice_az/client/e_taxes_items_list.js +++ b/invoice_az/client/e_taxes_items_list.js @@ -575,9 +575,9 @@ function show_certificate_selector(certificates, asan_login_name, callback) { }); } -// Function to display the invoice filter dialog function show_invoice_filter_dialog() { // Get previous year for start date and current date for end date + const minDate = moment("2020-01-01", "YYYY-MM-DD"); // Minimum date - January 1st, 2020 const prevYear = moment().subtract(1, 'year').year(); const startDate = prevYear + "-01-01"; // January 1st of previous year const endDate = moment().format('YYYY-MM-DD'); // Today @@ -602,24 +602,29 @@ function show_invoice_filter_dialog() { fieldtype: 'Date', label: __('To Date'), default: endDate - }, - { - fieldname: 'options_section', - fieldtype: 'Section Break', - label: __('Options') - }, - { - fieldname: 'maxCount', - fieldtype: 'Int', - label: __('Maximum Number of Invoices'), - default: 200, - reqd: true } + // Убираем фильтр по максимальному количеству инвойсов, т.к. загружаем все через пагинацию ], primary_action_label: __('Load Items'), primary_action: function() { var values = d.get_values(); + // Validate date range + const fromDateMoment = moment(values.creationDateFrom, "YYYY-MM-DD"); + const toDateMoment = moment(values.creationDateTo, "YYYY-MM-DD"); + const todayMoment = moment(); + + // Check if the dates are within the allowed range + if (fromDateMoment.isBefore(minDate)) { + frappe.msgprint(__('From Date cannot be earlier than January 1, 2020')); + return; + } + + if (toDateMoment.isAfter(todayMoment)) { + frappe.msgprint(__('To Date cannot be later than today')); + return; + } + // Close dialog d.hide(); @@ -634,58 +639,111 @@ function show_invoice_filter_dialog() { `${fromDate} - ${toDate}` ); - // Load items - frappe.call({ - method: 'invoice_az.api.load_items_from_invoices', - args: { - 'date_from': fromDate, - 'date_to': toDate, - 'max_count': values.maxCount || 200 - }, - callback: function(r) { - if (r.message && r.message.success) { - set_loading_success( - __('Items Loaded Successfully'), - __('Created {0} unique items. Skipped {1} duplicates.', - [r.message.created_count, r.message.skipped_count || 0]), - function() { - // Refresh list - cur_list.refresh(); - }, - 2000 // Автоматически закроется через 2 секунды - ); - } else if (r.message && r.message.unauthorized) { - // Если ошибка авторизации, запускаем процесс авторизации заново - set_loading_error( - __('Authentication Required'), - __('Your session has expired. Please authenticate again.'), - false // Не показываем кнопку закрытия - ); - - // Добавляем кнопку "Authenticate" - loading_dialog.set_primary_action(__('Authenticate'), function() { - hide_loading_dialog(); - - // Запускаем процесс авторизации и после него повторяем загрузку - check_and_process_token(function() { - show_invoice_filter_dialog(); - }); - }); - - // Добавляем кнопку "Cancel" - loading_dialog.set_secondary_action(__('Cancel'), function() { - hide_loading_dialog(); - }); - } else { - set_loading_error( - __('Error'), - r.message ? r.message.message : __('An error occurred while loading items') - ); - } - } + // Начинаем загрузку с пагинацией + load_items_with_pagination(fromDate, toDate, 0, { + created_count: 0, + skipped_count: 0, + failed_count: 0, + total_invoices: 0 }); } }); d.show(); +} + +// Функция для загрузки элементов с поддержкой пагинации +function load_items_with_pagination(fromDate, toDate, offset = 0, accumulated_data = null) { + // Используем фиксированный размер партии + const maxCount = 200; + + // Обновляем сообщение о загрузке с информацией о текущей партии + if (offset > 0) { + update_loading_message( + __('Loading items from E-Taxes (batch {0})', [(offset / maxCount) + 1]), + __('Processing invoices starting from {0}...', [offset]) + ); + } + + // Загружаем элементы из E-Taxes + frappe.call({ + method: 'invoice_az.api.load_items_from_invoices', + args: { + 'date_from': fromDate, + 'date_to': toDate, + 'max_count': maxCount, // Всегда используем фиксированный размер + 'offset': offset // Указываем текущее смещение + }, + callback: function(r) { + if (r.message && r.message.success) { + // Обновляем накопленные данные + accumulated_data.created_count += r.message.created_count || 0; + accumulated_data.skipped_count += r.message.skipped_count || 0; + accumulated_data.failed_count += r.message.failed_count || 0; + accumulated_data.total_invoices += r.message.total_invoices || 0; + + // Проверяем, есть ли еще данные для загрузки + let hasMore = r.message.hasMore || false; + + if (hasMore) { + // Если есть еще данные, увеличиваем смещение и загружаем следующую партию + let newOffset = offset + maxCount; + + // Показываем промежуточный результат + update_loading_message( + __('Loaded {0} items so far...', [accumulated_data.created_count]), + __('Loading more data...') + ); + + // Рекурсивно вызываем функцию для следующей партии + load_items_with_pagination(fromDate, toDate, newOffset, accumulated_data); + } else { + // Если больше нет данных, показываем итоговый результат + set_loading_success( + __('Items Loaded Successfully'), + __('Created {0} unique items. Skipped {1} duplicates.', + [accumulated_data.created_count, accumulated_data.skipped_count]), + function() { + // Refresh list + cur_list.refresh(); + }, + 2000 // Автоматически закроется через 2 секунды + ); + } + } else if (r.message && r.message.error === 'unauthorized') { + // Если ошибка авторизации, предлагаем авторизоваться заново + set_loading_error( + __('Authentication Required'), + __('Your session has expired. Please authenticate again.'), + false // Не показываем кнопку закрытия + ); + + // Добавляем кнопку "Authenticate" + loading_dialog.set_primary_action(__('Authenticate'), function() { + hide_loading_dialog(); + + // Запускаем процесс аутентификации и после него повторяем загрузку + check_and_process_token(function() { + show_invoice_filter_dialog(); + }); + }); + + // Добавляем кнопку "Cancel" + loading_dialog.set_secondary_action(__('Cancel'), function() { + hide_loading_dialog(); + }); + } else { + set_loading_error( + __('Error'), + r.message ? r.message.message : __('An error occurred while loading items') + ); + } + }, + error: function(xhr, status, error) { + set_loading_error( + __('Network Error'), + __('Failed to connect to server: ') + (error || 'Unknown error') + ); + } + }); } \ No newline at end of file diff --git a/invoice_az/client/e_taxes_parties_list.js b/invoice_az/client/e_taxes_parties_list.js index 0841cac..dbe8c48 100644 --- a/invoice_az/client/e_taxes_parties_list.js +++ b/invoice_az/client/e_taxes_parties_list.js @@ -571,8 +571,10 @@ function show_certificate_selector(certificates, asan_login_name, callback) { } // Function to display the invoice filter dialog +// Function to display the invoice filter dialog for partners function show_invoice_filter_dialog() { // Get previous year for start date and current date for end date + const minDate = moment("2020-01-01", "YYYY-MM-DD"); // Minimum date - January 1st, 2020 const prevYear = moment().subtract(1, 'year').year(); const startDate = prevYear + "-01-01"; // January 1st of previous year const endDate = moment().format('YYYY-MM-DD'); // Today @@ -597,24 +599,29 @@ function show_invoice_filter_dialog() { fieldtype: 'Date', label: __('To Date'), default: endDate - }, - { - fieldname: 'options_section', - fieldtype: 'Section Break', - label: __('Options') - }, - { - fieldname: 'maxCount', - fieldtype: 'Int', - label: __('Maximum Number of Invoices'), - default: 200, - reqd: true } + // Убраем фильтр по максимальному количеству инвойсов, т.к. загружаем все через пагинацию ], primary_action_label: __('Load Parties'), primary_action: function() { var values = d.get_values(); + // Validate date range + const fromDateMoment = moment(values.creationDateFrom, "YYYY-MM-DD"); + const toDateMoment = moment(values.creationDateTo, "YYYY-MM-DD"); + const todayMoment = moment(); + + // Check if the dates are within the allowed range + if (fromDateMoment.isBefore(minDate)) { + frappe.msgprint(__('From Date cannot be earlier than January 1, 2020')); + return; + } + + if (toDateMoment.isAfter(todayMoment)) { + frappe.msgprint(__('To Date cannot be later than today')); + return; + } + // Close dialog d.hide(); @@ -629,57 +636,105 @@ function show_invoice_filter_dialog() { `${fromDate} - ${toDate}` ); - // Load parties - frappe.call({ - method: 'invoice_az.api.load_parties_from_invoices', - args: { - 'date_from': fromDate, - 'date_to': toDate, - 'max_count': values.maxCount || 200 - }, - callback: function(r) { - if (r.message && r.message.success) { - set_loading_success( - __('Parties Loaded Successfully'), - __('Created {0} unique parties. Skipped {1} duplicates.', - [r.message.created_count, r.message.skipped_count || 0]), - function() { - // Refresh list - cur_list.refresh(); - } - ); - } else if (r.message && r.message.unauthorized) { - // Если ошибка авторизации, запускаем процесс авторизации заново - set_loading_error( - __('Authentication Required'), - __('Your session has expired. Please authenticate again.'), - false // Не показываем кнопку закрытия - ); - - // Добавляем кнопку "Authenticate" - loading_dialog.set_primary_action(__('Authenticate'), function() { - hide_loading_dialog(); - - // Запускаем процесс авторизации и после него повторяем загрузку - check_and_process_token(function() { - show_invoice_filter_dialog(); - }); - }); - - // Добавляем кнопку "Cancel" - loading_dialog.set_secondary_action(__('Cancel'), function() { - hide_loading_dialog(); - }); - } else { - set_loading_error( - __('Error'), - r.message ? r.message.message : __('An error occurred while loading parties') - ); - } - } + // Начинаем загрузку с пагинацией + load_parties_with_pagination(fromDate, toDate, 0, { + created_count: 0, + skipped_count: 0, + failed_count: 0, + total_invoices: 0, + unique_parties: 0 }); } }); d.show(); +} + +// Функция для загрузки партнеров с поддержкой пагинации +function load_parties_with_pagination(fromDate, toDate, offset = 0, accumulated_data = null) { + // Используем фиксированный размер партии + const maxCount = 200; + + // Загружаем партнеров из E-Taxes + frappe.call({ + method: 'invoice_az.api.load_parties_from_invoices', + args: { + 'date_from': fromDate, + 'date_to': toDate, + 'max_count': maxCount, // Всегда используем фиксированный размер + 'offset': offset // Указываем текущее смещение + }, + callback: function(r) { + if (r.message && r.message.success) { + // Обновляем накопленные данные + accumulated_data.created_count += r.message.created_count || 0; + accumulated_data.skipped_count += r.message.skipped_count || 0; + accumulated_data.failed_count += r.message.failed_count || 0; + accumulated_data.total_invoices += r.message.total_invoices || 0; + accumulated_data.unique_parties += r.message.unique_parties || 0; + + // Проверяем, есть ли еще данные для загрузки + let hasMore = r.message.hasMore || false; + + if (hasMore) { + // Если есть еще данные, увеличиваем смещение и загружаем следующую партию + let newOffset = offset + maxCount; + + // Обновляем сообщение о загрузке + update_loading_message( + __('Processing invoices...'), + __('Loaded {0} parties so far, loading more...', [accumulated_data.created_count]) + ); + + // Рекурсивно вызываем функцию для следующей партии + load_parties_with_pagination(fromDate, toDate, newOffset, accumulated_data); + } else { + // Если больше нет данных, показываем итоговый результат + set_loading_success( + __('Parties Loaded Successfully'), + __('Created {0} unique parties. Skipped {1} duplicates.', + [accumulated_data.created_count, accumulated_data.skipped_count]), + function() { + // Refresh list + cur_list.refresh(); + }, + 2000 // Автоматически закроется через 2 секунды + ); + } + } else if (r.message && r.message.error === 'unauthorized') { + // Если ошибка авторизации, предлагаем авторизоваться заново + set_loading_error( + __('Authentication Required'), + __('Your session has expired. Please authenticate again.'), + false // Не показываем кнопку закрытия + ); + + // Добавляем кнопку "Authenticate" + loading_dialog.set_primary_action(__('Authenticate'), function() { + hide_loading_dialog(); + + // Запускаем процесс авторизации и после него повторяем загрузку + check_and_process_token(function() { + show_invoice_filter_dialog(); + }); + }); + + // Добавляем кнопку "Cancel" + loading_dialog.set_secondary_action(__('Cancel'), function() { + hide_loading_dialog(); + }); + } else { + set_loading_error( + __('Error'), + r.message ? r.message.message : __('An error occurred while loading parties') + ); + } + }, + error: function(xhr, status, error) { + set_loading_error( + __('Network Error'), + __('Failed to connect to server: ') + (error || 'Unknown error') + ); + } + }); } \ No newline at end of file diff --git a/invoice_az/client/purchase_order.js b/invoice_az/client/purchase_order.js index 239cf6f..859b408 100644 --- a/invoice_az/client/purchase_order.js +++ b/invoice_az/client/purchase_order.js @@ -44,19 +44,41 @@ frappe.listview_settings['Purchase Order'] = { listview.page.add_menu_item(__('Import from E-Taxes'), function() { show_etaxes_import_dialog_for_list(); }); + + // Добавляем поле как колонку (правильный способ) + listview.columns.push({ + type: 'Check', + df: { + label: __('Tax Doc'), + fieldname: 'is_taxes_doc' + }, + width: 120 + }); + + // Принудительно обновляем список с новой колонкой + listview.refresh(); + }, + + // Форматируем отображение поля is_taxes_doc + formatters: { + is_taxes_doc: function(value) { + return value ? + `` : + ``; + } } }; -// Глобальная переменная для хранения диалога загрузки -var loading_dialog = null; +// Глобальная переменная для хранения диалога загрузки для Asan Login +var asan_login_dialog = null; // Глобальная переменная для отмены процесса загрузки var cancel_loading = false; -// Функция для отображения диалога загрузки с прогресс-баром -function show_loading_dialog(message, show_progress = false) { - if (!loading_dialog) { - loading_dialog = new frappe.ui.Dialog({ +// Функция для отображения диалога загрузки Asan Login с синим дизайном +function show_asan_loading_dialog(message) { + if (!asan_login_dialog) { + asan_login_dialog = new frappe.ui.Dialog({ title: __('Loading'), fields: [ { @@ -72,31 +94,6 @@ function show_loading_dialog(message, show_progress = false) { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } - .progress-container { - width: 100%; - margin-top: 20px; - display: none; - } - .progress-bar { - width: 100%; - background-color: #f3f3f3; - border-radius: 4px; - height: 20px; - position: relative; - overflow: hidden; - } - .progress-bar-fill { - height: 100%; - background-color: #4e73df; - border-radius: 4px; - width: 0%; - transition: width 0.3s ease; - } - .progress-text { - text-align: center; - font-weight: bold; - margin-top: 5px; - }

${message || __('Please wait...')}

@@ -109,13 +106,6 @@ function show_loading_dialog(message, show_progress = false) {

Verification Code

- -
-
-
-
-
0%
-
` } @@ -123,9 +113,9 @@ function show_loading_dialog(message, show_progress = false) { primary_action_label: __('Cancel'), primary_action: function() { cancel_loading = true; - loading_dialog.$wrapper.find('.primary-action').prop('disabled', true); - loading_dialog.$wrapper.find('.primary-action').html(__('Cancelling...')); - $('#status_message p').text(__('Cancelling the loading. Already loaded invoices will be saved.')); + asan_login_dialog.$wrapper.find('.primary-action').prop('disabled', true); + asan_login_dialog.$wrapper.find('.primary-action').html(__('Cancelling...')); + $('#status_message p').text(__('Cancelling the operation.')); } }); } else { @@ -134,59 +124,29 @@ function show_loading_dialog(message, show_progress = false) { $('#status_message p').text(__('The operation may take some time')); } - // Показываем или скрываем прогресс-бар - if (show_progress) { - $('#progress_container').show(); - // Сбрасываем прогресс - $('#progress_bar_fill').css('width', '0%'); - $('#progress_text').text('0%'); - } else { - $('#progress_container').hide(); - } - // Сбрасываем флаг отмены cancel_loading = false; - loading_dialog.show(); + asan_login_dialog.show(); } -// Функция для обновления прогресса -function update_progress(current, total) { - if (loading_dialog) { - var percent = Math.round((current / total) * 100); - $('#progress_bar_fill').css('width', percent + '%'); - $('#progress_text').text(current + ' of ' + total + ' (' + percent + '%)'); - } -} - -// Функция для скрытия диалога загрузки -function hide_loading_dialog() { - if (loading_dialog) { +// Функция для скрытия диалога загрузки Asan Login +function hide_asan_loading_dialog() { + if (asan_login_dialog) { try { - loading_dialog.hide(); + asan_login_dialog.hide(); } catch (e) { - console.error("Error hiding loading dialog:", e); + console.error("Error hiding Asan loading dialog:", e); } - loading_dialog = null; + asan_login_dialog = null; } // Сбрасываем флаг отмены cancel_loading = false; } -function ensure_loading_dialog_closed(timeout = 10000) { - // Устанавливаем таймаут для закрытия диалога - if (loading_dialog) { - setTimeout(function() { - if (loading_dialog) { - hide_loading_dialog(); - } - }, timeout); - } -} - -// Функция для обновления сообщения в диалоге загрузки -function update_loading_message(message, submessage) { - if (loading_dialog) { +// Функция для обновления сообщения в диалоге загрузки Asan Login +function update_asan_loading_message(message, submessage) { + if (asan_login_dialog) { $('#status_message h4').text(message); if (submessage !== undefined) { $('#status_message p').text(submessage); @@ -194,20 +154,20 @@ function update_loading_message(message, submessage) { } } -// Функция для отображения кода верификации -function show_verification_code(code) { - if (loading_dialog && code) { +// Функция для отображения кода верификации в диалоге Asan Login +function show_asan_verification_code(code) { + if (asan_login_dialog && code) { $('#verification_code').text(code); $('#verification_code_container').show(); $('#verification_code_container').attr('style', 'display: block !important; margin-top: 15px;'); } } -// Функция для установки статуса успеха -function set_loading_success(message, submessage, callback, delay = 2000) { - if (loading_dialog) { +// Функция для установки статуса успеха в диалоге Asan Login +function set_asan_loading_success(message, submessage, callback, delay = 2000) { + if (asan_login_dialog) { // Меняем анимацию на галочку - loading_dialog.$wrapper.find('.loading-animation').html(` + asan_login_dialog.$wrapper.find('.loading-animation').html(`
@@ -219,19 +179,18 @@ function set_loading_success(message, submessage, callback, delay = 2000) { $('#status_message p').text(submessage); } - // Скрываем код верификации и прогресс + // Скрываем код верификации $('#verification_code_container').hide(); - $('#progress_container').hide(); // Удаляем кнопки - loading_dialog.set_primary_action(null); - loading_dialog.set_secondary_action(null); + asan_login_dialog.set_primary_action(null); + asan_login_dialog.set_secondary_action(null); // Автоматически закрываем через указанную задержку setTimeout(function() { - if (loading_dialog) { - loading_dialog.hide(); - loading_dialog = null; + if (asan_login_dialog) { + asan_login_dialog.hide(); + asan_login_dialog = null; } if (callback) callback(); }, delay); @@ -240,11 +199,11 @@ function set_loading_success(message, submessage, callback, delay = 2000) { } } -// Функция для установки статуса ошибки -function set_loading_error(message, submessage, show_close_button = true) { - if (loading_dialog) { +// Функция для установки статуса ошибки в диалоге Asan Login +function set_asan_loading_error(message, submessage, show_close_button = true) { + if (asan_login_dialog) { // Меняем анимацию на крестик - loading_dialog.$wrapper.find('.loading-animation').html(` + asan_login_dialog.$wrapper.find('.loading-animation').html(`
@@ -256,14 +215,13 @@ function set_loading_error(message, submessage, show_close_button = true) { $('#status_message p').text(submessage); } - // Скрываем код верификации и прогресс + // Скрываем код верификации $('#verification_code_container').hide(); - $('#progress_container').hide(); // При необходимости добавляем кнопку закрытия if (show_close_button) { - loading_dialog.set_primary_action(__('Close'), function() { - hide_loading_dialog(); + asan_login_dialog.set_primary_action(__('Close'), function() { + hide_asan_loading_dialog(); }); } } @@ -308,15 +266,15 @@ function check_token_before_api_call(callback) { // Функция для запуска полного процесса аутентификации function start_authentication_process(callback) { - show_loading_dialog(__('Starting Authentication')); - update_loading_message(__('Getting Asan Login settings...'), __('Please wait')); + show_asan_loading_dialog(__('Starting Authentication')); + update_asan_loading_message(__('Getting Asan Login settings...'), __('Please wait')); frappe.call({ method: 'invoice_az.api.get_default_asan_login', callback: function(r) { if (r.message && r.message.found) { var asan_login_name = r.message.name; - update_loading_message( + update_asan_loading_message( __('Sending authentication request...'), __('This will send a request to your Asan Imza mobile app') ); @@ -333,11 +291,11 @@ function start_authentication_process(callback) { // Отображаем код верификации, если он есть в ответе if (r.message.verification_code) { - show_verification_code(r.message.verification_code); + show_asan_verification_code(r.message.verification_code); } // Изменяем сообщение для ожидания подтверждения - update_loading_message( + update_asan_loading_message( __('Waiting for confirmation on your phone'), __('Please check your phone and confirm the authentication request') ); @@ -349,7 +307,7 @@ function start_authentication_process(callback) { complete_authentication(asan_login_name, callback); } else { // Если аутентификация не удалась, показываем сообщение об ошибке - set_loading_error( + set_asan_loading_error( __('Authentication Failed'), __('Could not authenticate with Asan Imza') ); @@ -357,7 +315,7 @@ function start_authentication_process(callback) { }); } else { // Если запрос не удался, показываем сообщение об ошибке - set_loading_error( + set_asan_loading_error( __('Authentication Error'), r.message ? r.message.message : __('Authentication request failed') ); @@ -366,7 +324,7 @@ function start_authentication_process(callback) { }); } else { // Если настройки не найдены, показываем сообщение об ошибке - set_loading_error( + set_asan_loading_error( __('No Asan Login Settings'), __('Please configure Asan Login settings first') ); @@ -388,7 +346,7 @@ function poll_authentication_status(asan_login_name, bearer_token, callback) { if (attempts >= maxAttempts || stopPolling) { if (attempts >= maxAttempts) { // Показываем сообщение о таймауте - set_loading_error( + set_asan_loading_error( __('Authentication Timeout'), __('The authentication request has timed out. Please try again.') ); @@ -400,7 +358,7 @@ function poll_authentication_status(asan_login_name, bearer_token, callback) { attempts++; // Обновляем счетчик попыток в сообщении - update_loading_message( + update_asan_loading_message( __('Waiting for confirmation on your phone'), __('Please check your phone and confirm the authentication request') + ' (' + attempts + '/' + maxAttempts + ')' @@ -418,7 +376,7 @@ function poll_authentication_status(asan_login_name, bearer_token, callback) { if (r.message.authenticated) { // Авторизация успешна stopPolling = true; - set_loading_success( + set_asan_loading_success( __('Authentication Successful'), __('You are now authenticated with Asan Imza'), function() { @@ -432,7 +390,7 @@ function poll_authentication_status(asan_login_name, bearer_token, callback) { } else { // Если ошибка, останавливаем опрос и показываем сообщение stopPolling = true; - set_loading_error( + set_asan_loading_error( __('Authentication Error'), r.message ? r.message.message : __('An unknown error occurred') ); @@ -441,7 +399,7 @@ function poll_authentication_status(asan_login_name, bearer_token, callback) { }, error: function(err) { // В случае ошибки продолжаем опрос - setTimeout(pollStatus, 6000); + setTimeout(pollStatus, 6000); } }); } @@ -452,8 +410,8 @@ function poll_authentication_status(asan_login_name, bearer_token, callback) { // Функция для завершения аутентификации - получения сертификатов и выбора налогоплательщика function complete_authentication(asan_login_name, callback) { - show_loading_dialog(__('Completing Authentication')); - update_loading_message(__('Getting certificates...'), __('Please wait')); + show_asan_loading_dialog(__('Completing Authentication')); + update_asan_loading_message(__('Getting certificates...'), __('Please wait')); // Получаем сертификаты frappe.call({ @@ -463,7 +421,7 @@ function complete_authentication(asan_login_name, callback) { }, callback: function(cert_r) { if (cert_r.message && cert_r.message.success) { - update_loading_message( + update_asan_loading_message( __('Certificates retrieved'), __('Selecting certificate and taxpayer') ); @@ -481,7 +439,7 @@ function complete_authentication(asan_login_name, callback) { const cert_data = JSON.parse(r.message.selected_certificate_json); const cert_name = r.message.selected_certificate; - update_loading_message( + update_asan_loading_message( __('Selecting certificate'), cert_name ); @@ -496,7 +454,7 @@ function complete_authentication(asan_login_name, callback) { }, callback: function(select_r) { if (select_r.message && select_r.message.success) { - update_loading_message( + update_asan_loading_message( __('Selecting taxpayer'), __('Using certificate: ') + cert_name ); @@ -509,35 +467,35 @@ function complete_authentication(asan_login_name, callback) { }, callback: function(tax_r) { if (tax_r.message && tax_r.message.success) { - set_loading_success( + set_asan_loading_success( __('Authentication Complete'), __('You can now access E-Taxes services'), callback ); } else { - hide_loading_dialog(); + hide_asan_loading_dialog(); show_certificate_selector(cert_r.message.certificates, asan_login_name, callback); } } }); } else { - hide_loading_dialog(); + hide_asan_loading_dialog(); show_certificate_selector(cert_r.message.certificates, asan_login_name, callback); } } }); } catch (e) { - hide_loading_dialog(); + hide_asan_loading_dialog(); show_certificate_selector(cert_r.message.certificates, asan_login_name, callback); } } else { - hide_loading_dialog(); + hide_asan_loading_dialog(); show_certificate_selector(cert_r.message.certificates, asan_login_name, callback); } } }); } else { - set_loading_error( + set_asan_loading_error( __('Error'), cert_r.message ? cert_r.message.message : __('Failed to get certificates') ); @@ -610,8 +568,7 @@ function show_certificate_selector(certificates, asan_login_name, callback) { dialog.hide(); // Показываем прогресс - show_loading_dialog(__('Selecting Certificate')); - update_loading_message(__('Processing your selection...'), certName); + show_asan_loading_dialog(__('Selecting Certificate: ') + certName); // Отправляем запрос на выбор сертификата frappe.call({ @@ -623,7 +580,7 @@ function show_certificate_selector(certificates, asan_login_name, callback) { }, callback: function(r) { if (r.message && r.message.success) { - update_loading_message( + update_asan_loading_message( __('Certificate selected'), __('Now selecting taxpayer information') ); @@ -636,13 +593,13 @@ function show_certificate_selector(certificates, asan_login_name, callback) { }, callback: function(r) { if (r.message && r.message.success) { - set_loading_success( + set_asan_loading_success( __('Authentication Complete'), __('You can now access E-Taxes services'), callback ); } else { - set_loading_error( + set_asan_loading_error( __('Error'), r.message ? r.message.message : __('Failed to select taxpayer') ); @@ -650,7 +607,7 @@ function show_certificate_selector(certificates, asan_login_name, callback) { } }); } else { - set_loading_error( + set_asan_loading_error( __('Error'), r.message ? r.message.message : __('Failed to select certificate') ); @@ -660,87 +617,12 @@ function show_certificate_selector(certificates, asan_login_name, callback) { }); } -// Функция для отображения диалога импорта -function show_etaxes_import_dialog(frm) { - // Сначала проверяем токен - check_token_before_api_call(function() { - // Создаем диалог для выбора периода и склада - var d = new frappe.ui.Dialog({ - title: __('Import invoice from E-Taxes'), - fields: [ - { - fieldname: 'date_range_section', - fieldtype: 'Section Break', - label: __('Period') - }, - { - fieldname: 'creationDateFrom', - fieldtype: 'Date', - label: __('Date from'), - default: moment().subtract(1, 'month').format('YYYY-MM-DD') - }, - { - fieldname: 'creationDateTo', - fieldtype: 'Date', - label: __('Date to'), - default: moment().format('YYYY-MM-DD') - }, - { - fieldname: 'settings_section', - fieldtype: 'Section Break', - label: __('Settings') - }, - { - fieldname: 'warehouse', - fieldtype: 'Link', - label: __('Default warehouse'), - options: 'Warehouse', - reqd: true, - get_query: function() { - return { - filters: { - 'is_group': 0, - 'disabled': 0 - } - }; - } - } - ], - primary_action_label: __('Search'), - primary_action: function() { - var values = d.get_values(); - - // Форматируем даты - let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : ''; - let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : ''; - let warehouse = values.warehouse; - - if (!warehouse) { - frappe.msgprint({ - title: __('Warning'), - indicator: 'orange', - message: __('Please select a default warehouse') - }); - return; - } - - // Закрываем диалог - d.hide(); - show_loading_dialog(__('Loading invoices...')); - - // Загружаем счета-фактуры - load_etaxes_invoices(frm, fromDate, toDate, warehouse); - } - }); - - d.show(); - }); -} - -// Функция для отображения диалога импорта при работе из списка function show_etaxes_import_dialog_for_list() { // Сначала проверяем токен check_token_before_api_call(function() { + // Определяем минимальную дату - 1 января 2020 + const minDate = moment("2020-01-01", "YYYY-MM-DD"); + // Создаем диалог для выбора периода и склада var d = new frappe.ui.Dialog({ title: __('Import invoice from E-Taxes'), @@ -787,6 +669,22 @@ function show_etaxes_import_dialog_for_list() { primary_action: function() { var values = d.get_values(); + // Validate date range + const fromDateMoment = moment(values.creationDateFrom, "YYYY-MM-DD"); + const toDateMoment = moment(values.creationDateTo, "YYYY-MM-DD"); + const todayMoment = moment(); + + // Check if the dates are within the allowed range + if (fromDateMoment.isBefore(minDate)) { + frappe.msgprint(__('From Date cannot be earlier than January 1, 2020')); + return; + } + + if (toDateMoment.isAfter(todayMoment)) { + frappe.msgprint(__('To Date cannot be later than today')); + return; + } + // Форматируем даты let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : ''; let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : ''; @@ -803,10 +701,9 @@ function show_etaxes_import_dialog_for_list() { // Закрываем диалог d.hide(); - show_loading_dialog(__('Loading invoices...')); - // Загружаем счета-фактуры - load_etaxes_invoices(null, fromDate, toDate, warehouse); + // Загружаем счета-фактуры (с пустым массивом и offset=0 для начала) + load_etaxes_invoices(null, fromDate, toDate, warehouse, [], 0); } }); @@ -815,16 +712,26 @@ function show_etaxes_import_dialog_for_list() { } // Функция для загрузки инвойсов с фильтрацией дубликатов -function load_etaxes_invoices(frm, fromDate, toDate, warehouse) { - // Устанавливаем таймаут для автоматического закрытия диалога - ensure_loading_dialog_closed(30000); // 30 секунд максимум +function load_etaxes_invoices(frm, fromDate, toDate, warehouse, accumulated_invoices = [], offset = 0) { + // Устанавливаем флаг отмены в false при начале новой загрузки + cancel_loading = false; + + // Удаляем существующие обработчики отмены, если они есть + $(document).off('progress-cancel.etaxes_import'); + + // Показываем индикатор только в начале загрузки (при первом запросе) + if (offset === 0) { + frappe.show_alert({ + message: __('Fetching E-Taxes invoices...'), + indicator: 'blue' + }, 3); + } // Сначала получаем настройки авторизации frappe.call({ method: 'invoice_az.api.get_default_asan_login', callback: function(login_response) { - if (!loading_dialog) { - // Если диалог был закрыт (пользователь ушел со страницы), прекращаем операцию + if (cancel_loading) { return; } @@ -833,7 +740,6 @@ function load_etaxes_invoices(frm, fromDate, toDate, warehouse) { let main_token = login_response.message.main_token; if (!main_token) { - hide_loading_dialog(); frappe.msgprint({ title: __('Authentication Error'), indicator: 'red', @@ -842,6 +748,9 @@ function load_etaxes_invoices(frm, fromDate, toDate, warehouse) { return; } + // Используем фиксированный размер партии (maxCount) - 200 для продакшена + const maxCount = 200; + // Загружаем счета-фактуры с использованием полученного токена frappe.call({ method: 'invoice_az.api.get_invoices', @@ -849,121 +758,129 @@ function load_etaxes_invoices(frm, fromDate, toDate, warehouse) { 'token': main_token, 'filters': JSON.stringify({ "creationDateFrom": fromDate, - "creationDateTo": toDate + "creationDateTo": toDate, + "maxCount": maxCount, + "offset": offset }) }, callback: function(r) { - if (!loading_dialog) { - // Если диалог был закрыт, прекращаем операцию + if (cancel_loading) { return; } if (r.message && !r.message.error) { - // Получаем данные о счетах-фактурах - let invoices = r.message.data || r.message.invoices || []; + // Получаем данные о счетах-фактурах из текущей партии + let currentInvoices = r.message.data || r.message.invoices || []; - if (invoices.length > 0) { - // Показываем диалог загрузки для проверки дубликатов - update_loading_message( - __('Checking for duplicates...'), - __('Filtering already imported invoices') - ); + // Проверяем, есть ли в ответе hasMore + let hasMore = r.message.hasMore || false; + + // Добавляем полученные инвойсы к уже накопленным + let allInvoices = accumulated_invoices.concat(currentInvoices); + + // Не показываем промежуточные сообщения о каждой новой партии + + if (hasMore && currentInvoices.length > 0) { + // Если есть еще инвойсы (hasMore = true), загружаем следующую партию + let newOffset = offset + currentInvoices.length; - // Получаем список уже импортированных инвойсов из E-Taxes Purchase - frappe.call({ - method: 'invoice_az.api.get_etaxes_purchases', - callback: function(etaxes_r) { - // Проверяем, что диалог все еще открыт - if (!loading_dialog) { - return; - } - - try { - // Преобразуем данные E-Taxes Purchase в хеш-таблицу для быстрого поиска - let importedInvoices = {}; - let importedCount = 0; - - if (etaxes_r.message && etaxes_r.message.success && etaxes_r.message.purchases) { - importedCount = etaxes_r.message.purchases.length; - - etaxes_r.message.purchases.forEach(function(purchase) { - // Проверяем и нормализуем ID - if (purchase.etaxes_id) { - // Нормализуем ID для более надежного сравнения - let normId = String(purchase.etaxes_id).trim(); - importedInvoices[normId] = true; - } - }); - } - - // Фильтруем список инвойсов, исключая дубликаты - let filteredInvoices = []; - let skippedCount = 0; - - for (let i = 0; i < invoices.length; i++) { - const invoice = invoices[i]; - // Нормализуем ID инвойса из API - let invoiceId = String(invoice.id).trim(); - - // Проверяем, есть ли запись с таким ID в E-Taxes Purchase - if (!importedInvoices.hasOwnProperty(invoiceId)) { - filteredInvoices.push(invoice); - } else { - skippedCount++; - } - } - - // Скрываем диалог загрузки - hide_loading_dialog(); - - if (filteredInvoices.length > 0) { - // Если есть не импортированные инвойсы, показываем их для выбора - show_invoice_selection_dialog(frm, filteredInvoices, main_token, warehouse); - } else { - frappe.msgprint({ - title: __('Information'), - indicator: 'blue', - message: __('No new invoices found for the specified period (all ' + - invoices.length + ' are already imported)') - }); - } - } catch (e) { - console.error("Error processing purchase data:", e); - hide_loading_dialog(); - frappe.msgprint({ - title: __('Error'), - indicator: 'red', - message: __('An error occurred while processing data: ') + e.message - }); - } - }, - error: function(err) { - console.error("Error fetching purchases:", err); - hide_loading_dialog(); - - // В случае ошибки показываем все инвойсы без фильтрации - show_invoice_selection_dialog(frm, invoices, main_token, warehouse); - - frappe.show_alert({ - message: __('Failed to check for duplicates. Showing all invoices.'), - indicator: 'orange' - }, 5); - } - }); + // Рекурсивно вызываем функцию с увеличенным смещением + load_etaxes_invoices(frm, fromDate, toDate, warehouse, allInvoices, newOffset); } else { - // Скрываем диалог загрузки - hide_loading_dialog(); - - frappe.msgprint({ - title: __('Information'), - indicator: 'blue', - message: __('No invoices found for the specified period') - }); + // Если больше нет инвойсов или получили пустой массив, переходим к обработке + if (allInvoices.length > 0) { + // Показываем итоговое сообщение только когда все инвойсы загружены + frappe.show_alert({ + message: __('Processing ') + allInvoices.length + __(' invoices...'), + indicator: 'blue' + }, 3); + + // Получаем список уже импортированных инвойсов из E-Taxes Purchase + frappe.call({ + method: 'invoice_az.api.get_etaxes_purchases', + callback: function(etaxes_r) { + try { + if (cancel_loading) { + return; + } + + // Преобразуем данные E-Taxes Purchase в хеш-таблицу для быстрого поиска + let importedInvoices = {}; + let importedCount = 0; + + if (etaxes_r.message && etaxes_r.message.success && etaxes_r.message.purchases) { + importedCount = etaxes_r.message.purchases.length; + + etaxes_r.message.purchases.forEach(function(purchase) { + // Проверяем и нормализуем ID + if (purchase.etaxes_id) { + // Нормализуем ID для более надежного сравнения + let normId = String(purchase.etaxes_id).trim(); + importedInvoices[normId] = true; + } + }); + } + + // Фильтруем список инвойсов, исключая дубликаты + let filteredInvoices = []; + let skippedCount = 0; + + for (let i = 0; i < allInvoices.length; i++) { + const invoice = allInvoices[i]; + // Нормализуем ID инвойса из API + let invoiceId = String(invoice.id).trim(); + + // Проверяем, есть ли запись с таким ID в E-Taxes Purchase + if (!importedInvoices.hasOwnProperty(invoiceId)) { + filteredInvoices.push(invoice); + } else { + skippedCount++; + } + } + + if (filteredInvoices.length > 0) { + // Если есть не импортированные инвойсы, показываем их для выбора + show_invoice_selection_dialog(frm, filteredInvoices, main_token, warehouse); + } else { + frappe.msgprint({ + title: __('Information'), + indicator: 'blue', + message: __('No new invoices found for the specified period (all ' + + allInvoices.length + ' are already imported)') + }); + } + } catch (e) { + console.error("Error processing purchase data:", e); + + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: __('An error occurred while processing data: ') + e.message + }); + } + }, + error: function(err) { + console.error("Error fetching purchases:", err); + + // В случае ошибки показываем все инвойсы без фильтрации + show_invoice_selection_dialog(frm, allInvoices, main_token, warehouse); + + frappe.show_alert({ + message: __('Failed to check for duplicates. Showing all invoices.'), + indicator: 'orange' + }, 5); + } + }); + } else { + frappe.msgprint({ + title: __('Information'), + indicator: 'blue', + message: __('No invoices found for the specified period') + }); + } } } else if (r.message && r.message.error === 'unauthorized') { // Если ошибка авторизации, предлагаем авторизоваться - hide_loading_dialog(); - frappe.confirm( __('Your E-Taxes session has expired. Would you like to authenticate now?'), function() { @@ -982,9 +899,6 @@ function load_etaxes_invoices(frm, fromDate, toDate, warehouse) { } ); } else { - // Скрываем диалог загрузки - hide_loading_dialog(); - frappe.msgprint({ title: __('Error'), indicator: 'red', @@ -994,7 +908,7 @@ function load_etaxes_invoices(frm, fromDate, toDate, warehouse) { }, error: function(xhr, status, error) { console.error("Error fetching invoices:", error); - hide_loading_dialog(); + frappe.msgprint({ title: __('Network Error'), indicator: 'red', @@ -1003,7 +917,6 @@ function load_etaxes_invoices(frm, fromDate, toDate, warehouse) { } }); } else { - hide_loading_dialog(); frappe.msgprint({ title: __('Error'), indicator: 'red', @@ -1013,7 +926,7 @@ function load_etaxes_invoices(frm, fromDate, toDate, warehouse) { }, error: function(xhr, status, error) { console.error("Error fetching Asan login settings:", error); - hide_loading_dialog(); + frappe.msgprint({ title: __('Network Error'), indicator: 'red', @@ -1106,10 +1019,8 @@ function show_invoice_selection_dialog(frm, invoices, token, warehouse) { // Закрываем диалог d.hide(); - // Загружаем выбранные инвойсы последовательно - // Показываем диалог с прогрессом - show_loading_dialog(__('Loading invoices...'), true); - update_progress(0, selected_invoice_ids.length); + // Сбрасываем флаг отмены + cancel_loading = false; // Загружаем выбранные инвойсы последовательно load_selected_invoices(frm, selected_invoice_ids, token, warehouse, 0); @@ -1131,49 +1042,66 @@ function show_invoice_selection_dialog(frm, invoices, token, warehouse) { // Функция для загрузки выбранных инвойсов с прогресс-баром и механизмом отмены function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count = 0) { + // Удаляем существующие обработчики отмены, если они есть + $(document).off('progress-cancel.loading_invoices'); + // Если все инвойсы обработаны или был запрос на отмену - if (invoice_ids.length === 0 || cancel_loading) { - // Если был запрос на отмену, показываем соответствующее сообщение - if (cancel_loading) { - set_loading_success( - __('Loading cancelled'), - __('Loaded ' + processed_count + ' invoices out of ' + (processed_count + invoice_ids.length)), - function() { - if (frm) { - frm.reload_doc(); - } else { - cur_list.refresh(); - } - } - ); - } else { - // Все успешно загружено - set_loading_success( - __('Loading completed'), - __('Loaded ' + processed_count + ' invoices'), - function() { - if (frm) { - frm.reload_doc(); - } else { - cur_list.refresh(); - } - } - ); + if (invoice_ids.length === 0) { + // Скрываем прогресс-бар + frappe.hide_progress(); + + // Все успешно загружено или загрузка отменена + let message = cancel_loading ? + __('Loading cancelled. Loaded ' + processed_count + ' invoices.') : + __('Loading completed. Loaded ' + processed_count + ' invoices.'); + + let indicator = cancel_loading ? 'orange' : 'green'; + + frappe.msgprint({ + title: __('Import Result'), + indicator: indicator, + message: message + }); + + // Обновляем форму или список + if (frm) { + frm.reload_doc(); + } else if (cur_list) { + cur_list.refresh(); } + + return; + } + + // Проверяем, была ли отменена загрузка + if (cancel_loading) { + // Если была отмена, пропускаем все оставшиеся инвойсы + invoice_ids = []; + // Явно скрываем прогресс-бар перед вызовом + frappe.hide_progress(); + load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count); return; } // Берем первый ID из списка var invoice_id = invoice_ids.shift(); + var is_last_invoice = invoice_ids.length === 0; // Обновляем прогресс-бар - update_progress(processed_count + 1, processed_count + invoice_ids.length + 1); + frappe.show_progress(__('Importing Invoices'), processed_count, processed_count + invoice_ids.length + 1, + __('Loading invoice ') + (processed_count + 1) + __(' of ') + (processed_count + invoice_ids.length + 1), null, true); - // Обновляем текст загрузки - update_loading_message( - __('Loading invoice (' + (processed_count + 1) + ' of ' + (processed_count + invoice_ids.length + 1) + ')'), - __('Getting invoice data...') - ); + // Обработчик отмены загрузки - используем пространство имен для предотвращения дублирования + $(document).on('progress-cancel.loading_invoices', function() { + cancel_loading = true; + frappe.show_alert({ + message: __('Cancelling import... Finishing current invoice.'), + indicator: 'orange' + }, 3); + + // Скрываем прогресс-бар при отмене + frappe.hide_progress(); + }); // Получаем детали счета-фактуры frappe.call({ @@ -1183,12 +1111,6 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co 'invoice_id': invoice_id }, callback: function(r) { - if (cancel_loading) { - // Если был запрос на отмену во время запроса, прекращаем загрузку - load_selected_invoices(frm, [], token, warehouse, processed_count); - return; - } - if (r.message && !r.message.error) { // Получаем дату создания инвойса let creationDate = r.message.creationDate || r.message.date || frappe.datetime.nowdate(); @@ -1197,10 +1119,8 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co let senderName = r.message.sender ? r.message.sender.name : ''; let total = r.message.totalAmount || r.message.amount || 0; - update_loading_message( - __('Loading invoice (' + (processed_count + 1) + ' of ' + (processed_count + invoice_ids.length + 1) + ')'), - __('Importing invoice data: ') + serialNumber - ); + frappe.show_progress(__('Importing Invoices'), processed_count, processed_count + invoice_ids.length + 1, + __('Importing invoice: ') + serialNumber); // Импортируем данные инвойса с указанием даты и склада frappe.call({ @@ -1208,16 +1128,10 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co args: { 'invoice_data': r.message, 'purchase_order_name': frm ? frm.doc.name : null, - 'schedule_date': scheduleDate, // Передаем дату в API - 'warehouse': warehouse // Передаем склад в API + 'schedule_date': scheduleDate, + 'warehouse': warehouse }, callback: function(import_r) { - if (cancel_loading) { - // Если был запрос на отмену во время запроса, прекращаем загрузку - load_selected_invoices(frm, [], token, warehouse, processed_count); - return; - } - if (import_r.message && import_r.message.success) { // Показываем уведомление об успешном импорте frappe.show_alert({ @@ -1229,7 +1143,6 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co let purchase_order_name = import_r.message.purchase_order; // Сохраняем информацию в E-Taxes Purchase - frappe.call({ method: 'invoice_az.api.create_etaxes_purchase', args: { @@ -1239,10 +1152,8 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co 'total': total }, callback: function(etaxes_r) { - if (etaxes_r.message && etaxes_r.message.success) { // Устанавливаем связь между Purchase Order и E-Taxes Purchase - frappe.call({ method: 'invoice_az.api.link_purchase_order_to_etaxes', args: { @@ -1250,33 +1161,89 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co 'etaxes_purchase': etaxes_r.message.name }, callback: function(link_r) { - // Увеличиваем счетчик обработанных инвойсов processed_count++; + // Если это был последний инвойс, явно скрываем прогресс-бар + if (is_last_invoice) { + frappe.hide_progress(); + $(document).off('progress-cancel.loading_invoices'); + + // Показываем сообщение о завершении + frappe.msgprint({ + title: __('Import Result'), + indicator: 'green', + message: __('Loading completed. Loaded ' + processed_count + ' invoices.') + }); + + // Обновляем форму или список + if (frm) { + frm.reload_doc(); + } else if (cur_list) { + cur_list.refresh(); + } + + return; + } + // Продолжаем с обработкой следующего инвойса setTimeout(function() { // Если создан новый заказ, обновляем ссылку на документ if (import_r.message.purchase_order && frm && (import_r.message.purchase_order !== frm.doc.name)) { - frm = null; + frm = null; } + + // Перед вызовом явно скрываем текущий прогресс-бар + if (invoice_ids.length === 0) { + frappe.hide_progress(); + } + load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count); }, 300); } }); } else { // Обрабатываем ошибку создания E-Taxes Purchase - frappe.show_alert({ + frappe.show_alert({ message: __('Error creating E-Taxes Purchase'), indicator: 'red' }, 3); // Всё равно увеличиваем счетчик и переходим к следующему processed_count++; + + // Если это был последний инвойс, явно скрываем прогресс-бар + if (is_last_invoice) { + frappe.hide_progress(); + $(document).off('progress-cancel.loading_invoices'); + + // Показываем сообщение о завершении + frappe.msgprint({ + title: __('Import Result'), + indicator: 'green', + message: __('Loading completed. Loaded ' + processed_count + ' invoices.') + }); + + // Обновляем форму или список + if (frm) { + frm.reload_doc(); + } else if (cur_list) { + cur_list.refresh(); + } + + return; + } + setTimeout(function() { if (import_r.message.purchase_order && frm && (import_r.message.purchase_order !== frm.doc.name)) { frm = null; } + + // Перед вызовом явно скрываем текущий прогресс-бар + if (invoice_ids.length === 0) { + frappe.hide_progress(); + } + load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count); }, 300); } @@ -1284,7 +1251,8 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co }); } else if (import_r.message && (import_r.message.unmatched_items || import_r.message.unmatched_parties)) { // Если есть несопоставленные элементы или контрагенты - hide_loading_dialog(); + frappe.hide_progress(); + $(document).off('progress-cancel.loading_invoices'); if (import_r.message.unmatched_items) { show_unmatched_items_dialog(r.message, import_r.message.unmatched_items); @@ -1304,8 +1272,35 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co indicator: 'red' }, 3); + // Если это был последний инвойс, явно скрываем прогресс-бар + if (is_last_invoice) { + frappe.hide_progress(); + $(document).off('progress-cancel.loading_invoices'); + + // Показываем сообщение о завершении + frappe.msgprint({ + title: __('Import Result'), + indicator: 'orange', + message: __('Loading completed with some errors. Loaded ' + processed_count + ' invoices.') + }); + + // Обновляем форму или список + if (frm) { + frm.reload_doc(); + } else if (cur_list) { + cur_list.refresh(); + } + + return; + } + // Переходим к следующему инвойсу, не увеличивая счетчик успешно обработанных setTimeout(function() { + // Перед вызовом явно скрываем текущий прогресс-бар + if (invoice_ids.length === 0) { + frappe.hide_progress(); + } + load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count); }, 300); } @@ -1313,7 +1308,8 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co }); } else if (r.message && r.message.error === 'unauthorized') { // Если ошибка авторизации, предлагаем авторизоваться заново - hide_loading_dialog(); + frappe.hide_progress(); + $(document).off('progress-cancel.loading_invoices'); frappe.confirm( __('Your E-Taxes session has expired. Would you like to authenticate now?'), @@ -1325,10 +1321,6 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co method: 'invoice_az.api.get_default_asan_login', callback: function(login_r) { if (login_r.message && login_r.message.found && login_r.message.main_token) { - // Показываем диалог с прогрессом снова - show_loading_dialog(__('Resuming invoice loading...'), true); - update_progress(processed_count, processed_count + invoice_ids.length + 1); - // Возвращаем текущий инвойс в начало списка invoice_ids.unshift(invoice_id); @@ -1359,11 +1351,73 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse, processed_co indicator: 'red' }, 3); + // Если это был последний инвойс, явно скрываем прогресс-бар + if (is_last_invoice) { + frappe.hide_progress(); + $(document).off('progress-cancel.loading_invoices'); + + // Показываем сообщение о завершении + frappe.msgprint({ + title: __('Import Result'), + indicator: 'orange', + message: __('Loading completed with some errors. Loaded ' + processed_count + ' invoices.') + }); + + // Обновляем форму или список + if (frm) { + frm.reload_doc(); + } else if (cur_list) { + cur_list.refresh(); + } + + return; + } + // Переходим к следующему инвойсу setTimeout(function() { + // Перед вызовом явно скрываем текущий прогресс-бар + if (invoice_ids.length === 0) { + frappe.hide_progress(); + } + load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count); }, 300); } + }, + error: function(xhr, status, error) { + console.error("Error fetching invoice details:", error); + + // Если это был последний инвойс, явно скрываем прогресс-бар + if (is_last_invoice) { + frappe.hide_progress(); + $(document).off('progress-cancel.loading_invoices'); + + // Показываем сообщение о завершении + frappe.msgprint({ + title: __('Import Result'), + indicator: 'orange', + message: __('Loading completed with some errors. Loaded ' + processed_count + ' invoices.') + }); + + // Обновляем форму или список + if (frm) { + frm.reload_doc(); + } else if (cur_list) { + cur_list.refresh(); + } + + return; + } + + // В случае ошибки сети пропускаем этот инвойс и переходим к следующему + setTimeout(function() { + // Перед вызовом явно скрываем текущий прогресс-бар + if (invoice_ids.length === 0) { + frappe.hide_progress(); + } + + load_selected_invoices(frm, invoice_ids, token, warehouse, processed_count); + }, 300); } }); } diff --git a/invoice_az/hooks.py b/invoice_az/hooks.py index 60e19e4..0ade915 100644 --- a/invoice_az/hooks.py +++ b/invoice_az/hooks.py @@ -23,6 +23,17 @@ doc_events = { } } +scheduler_events = { + "cron": { + "*/4 * * * *": [ + "invoice_az.api.renew_token" + ] + } +} + +after_install = "invoice_az.api.setup_token_renewal" +after_migrate = "invoice_az.api.setup_token_renewal" + # Apps # ------------------ diff --git a/invoice_az/invoice_az/doctype/asan_login/asan_login.json b/invoice_az/invoice_az/doctype/asan_login/asan_login.json index 0ee0e3d..dc0e1b8 100644 --- a/invoice_az/invoice_az/doctype/asan_login/asan_login.json +++ b/invoice_az/invoice_az/doctype/asan_login/asan_login.json @@ -12,6 +12,7 @@ "bearer_token", "auth_status", "verification_code", + "last_activity_time", "column_break_1", "is_default", "certificates_section", @@ -108,9 +109,7 @@ "description": "Primary token for all API operations", "fieldname": "main_token", "fieldtype": "Small Text", - "hidden": 1, - "label": "Main Token", - "read_only": 1 + "label": "Main Token" }, { "fieldname": "choose_taxpayer_response", @@ -124,11 +123,16 @@ "fieldname": "verification_code", "fieldtype": "Data", "label": "Verification Code" + }, + { + "fieldname": "last_activity_time", + "fieldtype": "Datetime", + "label": "Last Activity Time" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-05-13 14:13:35.366465", + "modified": "2025-05-21 14:13:47.059215", "modified_by": "Administrator", "module": "Invoice Az", "name": "Asan Login",