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; - }
Verification Code
- -