From 994917ffbc91ebf7f852ed9f117b69031f901efe Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Fri, 16 May 2025 12:30:56 +0400 Subject: [PATCH] Added bulk edit, all english, and every doctype of invoice az is read-only --- invoice_az/api.py | 757 +++++++++--------- invoice_az/client/purchase_order.js | 191 +++-- .../e_taxes_settings/e_taxes_settings.js | 259 ++++-- 3 files changed, 680 insertions(+), 527 deletions(-) diff --git a/invoice_az/api.py b/invoice_az/api.py index 37997d3..c3c6422 100644 --- a/invoice_az/api.py +++ b/invoice_az/api.py @@ -8,7 +8,7 @@ from difflib import SequenceMatcher @frappe.whitelist() def get_auth_token(phone, user_id): - """Получение токена авторизации""" + """Getting authorization token""" url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/start" payload = { @@ -29,10 +29,10 @@ def get_auth_token(phone, user_id): response = requests.post(url, data=json.dumps(payload), headers=headers) response.raise_for_status() - # Получаем токен из заголовка ответа + # Getting token from the response header bearer_token = response.headers.get('x-authorization', '') - # Получаем данные ответа + # Getting response data resp_data = response.json() return { @@ -44,7 +44,7 @@ def get_auth_token(phone, user_id): @frappe.whitelist() def check_auth_status(bearer_token): - """Проверка статуса авторизации""" + """Checking authorization status""" url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/status" headers = { @@ -55,7 +55,7 @@ def check_auth_status(bearer_token): "Referer": "https://new.e-taxes.gov.az/eportal/az/login/asan" } - # Добавляем токен в заголовки, если он есть + # Adding token to headers if it exists if bearer_token: headers["x-authorization"] = f"Bearer {bearer_token}" @@ -63,7 +63,7 @@ def check_auth_status(bearer_token): response = requests.get(url, headers=headers) response.raise_for_status() - # Получаем данные о статусе + # Getting status data status_data = response.json() return status_data @@ -72,10 +72,10 @@ def check_auth_status(bearer_token): @frappe.whitelist() def get_invoices(token, filters=None): - """Получение списка счетов-фактур с возможностью фильтрации""" + """Getting list of invoices with filtering options""" url = "https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/find.inbox" - # Базовые параметры запроса + # Base request parameters payload = { "sortBy": "creationDate", "sortAsc": True, @@ -102,12 +102,12 @@ def get_invoices(token, filters=None): "actionOwner": None } - # Применяем пользовательские фильтры, если они переданы + # Apply custom filters if they are passed if filters: try: filters_dict = json.loads(filters) for key, value in filters_dict.items(): - if key in payload and value: # Обновляем только существующие ключи и непустые значения + if key in payload and value: # Update only existing keys and non-empty values payload[key] = value except Exception as e: return @@ -129,7 +129,7 @@ def get_invoices(token, filters=None): "status_code": 500 } - # Проверяем статус ответа для обработки 401 ошибки + # Check response status to handle 401 error if response.status_code == 401: return { "error": "unauthorized", @@ -141,7 +141,7 @@ def get_invoices(token, filters=None): return response.json() except requests.exceptions.HTTPError as e: - # Проверяем, является ли ошибка 401 Unauthorized + # Check if the error is 401 Unauthorized if e.response.status_code == 401: return { "error": "unauthorized", @@ -160,7 +160,7 @@ def get_invoices(token, filters=None): @frappe.whitelist() def get_certificates(bearer_token): - """Получение доступных сертификатов""" + """Getting available certificates""" url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/certificates" headers = { @@ -182,7 +182,7 @@ def get_certificates(bearer_token): "status_code": 500 } - # Проверяем на 401 ошибку + # Check for 401 error if response.status_code == 401: return { "error": "unauthorized", @@ -194,7 +194,7 @@ def get_certificates(bearer_token): return response.json() except requests.exceptions.HTTPError as e: - # Проверяем, является ли ошибка 401 Unauthorized + # Check if the error is 401 Unauthorized if e.response.status_code == 401: return { "error": "unauthorized", @@ -213,16 +213,16 @@ def get_certificates(bearer_token): @frappe.whitelist() def choose_taxpayer(bearer_token, owner_type, tin): - """Выбор налогоплательщика после выбора сертификата""" + """Selecting taxpayer after certificate selection""" url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/chooseTaxpayer" payload = { - "ownerType": owner_type, # "legal" или "individual" + "ownerType": owner_type, # "legal" or "individual" "legalTin": tin if owner_type == "legal" else None, "individualFin": tin if owner_type == "individual" else None } - # Удаляем None значения из payload + # Remove None values from payload payload = {k: v for k, v in payload.items() if v is not None} headers = { @@ -245,7 +245,7 @@ def choose_taxpayer(bearer_token, owner_type, tin): "status_code": 500 } - # Проверяем на 401 ошибку + # Check for 401 error if response.status_code == 401: return { "error": "unauthorized", @@ -254,10 +254,10 @@ def choose_taxpayer(bearer_token, owner_type, tin): response.raise_for_status() - # Получаем основной токен из заголовка ответа + # Get main token from response header main_token = response.headers.get('x-authorization', '') - # Получаем данные ответа + # Get response data resp_data = response.json() return { @@ -266,7 +266,7 @@ def choose_taxpayer(bearer_token, owner_type, tin): } except requests.exceptions.HTTPError as e: - # Проверяем, является ли ошибка 401 Unauthorized + # Check if the error is 401 Unauthorized if e.response.status_code == 401: return { "error": "unauthorized", @@ -286,7 +286,7 @@ def choose_taxpayer(bearer_token, owner_type, tin): @frappe.whitelist() def get_invoice_details(token, invoice_id): - """Получение детальной информации о счете-фактуре""" + """Getting detailed information about an invoice""" url = f"https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/{invoice_id}?sourceSystem=avis" headers = { @@ -306,7 +306,7 @@ def get_invoice_details(token, invoice_id): "status_code": 500 } - # Проверяем на 401 ошибку + # Check for 401 error if response.status_code == 401: return { "error": "unauthorized", @@ -318,7 +318,7 @@ def get_invoice_details(token, invoice_id): return response.json() except requests.exceptions.HTTPError as e: - # Проверяем, является ли ошибка 401 Unauthorized + # Check if the error is 401 Unauthorized if e.response.status_code == 401: return { "error": "unauthorized", @@ -337,9 +337,9 @@ def get_invoice_details(token, invoice_id): @frappe.whitelist() def get_default_asan_login(): - """Получить настройки авторизации по умолчанию""" + """Get default authorization settings""" try: - # Ищем настройку с флагом is_default = 1 + # Look for setting with is_default = 1 default_login = frappe.get_list( "Asan Login", filters={"is_default": 1}, @@ -347,7 +347,7 @@ def get_default_asan_login(): ) if default_login: - # Если найдена настройка по умолчанию, возвращаем её данные + # If default setting is found, return its data asan_login = frappe.get_doc("Asan Login", default_login[0].name) return { "found": True, @@ -360,7 +360,7 @@ def get_default_asan_login(): "selected_certificate": asan_login.selected_certificate } else: - # Если не найдена настройка по умолчанию, ищем любую первую настройку + # If no default setting is found, look for any first setting any_login = frappe.get_list( "Asan Login", fields=["name"] @@ -386,32 +386,32 @@ def get_default_asan_login(): @frappe.whitelist() def handle_authentication(asan_login_name=None): - """Обрабатывает процесс аутентификации для указанного или настройки по умолчанию""" + """Handles the authentication process for the specified or default setting""" try: - # Если имя настройки не указано, получаем имя настройки по умолчанию + # If setting name is not specified, get default setting name if not asan_login_name: default_settings = get_default_asan_login() if not default_settings.get("found", False): return {"success": False, "message": "No Asan Login settings found."} asan_login_name = default_settings.get("name") - # Получаем документ Asan Login + # Get Asan Login document asan_login = frappe.get_doc("Asan Login", asan_login_name) - # Проверяем, что есть номер телефона и ID пользователя + # Check that there is a phone number and user ID if not asan_login.phone or not asan_login.user_id: return {"success": False, "message": "Phone and User ID are required."} - # Запускаем процесс авторизации + # Start the authorization process auth_result = get_auth_token(asan_login.phone, asan_login.user_id) if auth_result and auth_result.get("bearer_token"): - # Сохраняем токен и меняем статус + # Save token and change status asan_login.bearer_token = auth_result.get("bearer_token") asan_login.auth_status = "Waiting for confirmation" asan_login.save() - # Извлекаем код верификации из ответа, если он есть + # Extract verification code from response if it exists verification_code = None if auth_result.get("response_data") and isinstance(auth_result.get("response_data"), dict): verification_code = auth_result.get("response_data").get("verificationCode", None) @@ -432,16 +432,16 @@ def handle_authentication(asan_login_name=None): @frappe.whitelist() def poll_auth_status(asan_login_name, bearer_token): - """Проверяет статус аутентификации""" + """Checks authentication status""" try: - # Проверяем статус + # Check status status_result = check_auth_status(bearer_token) - # Получаем документ Asan Login + # Get Asan Login document asan_login = frappe.get_doc("Asan Login", asan_login_name) if status_result and status_result.get("successful") == True: - # Обновляем статус и токен + # Update status and token asan_login.auth_status = "Authenticated" if status_result.get("bearer_token"): asan_login.bearer_token = status_result.get("bearer_token") @@ -464,19 +464,19 @@ def poll_auth_status(asan_login_name, bearer_token): @frappe.whitelist() def get_auth_certificates(asan_login_name): - """Получает сертификаты для указанной настройки Asan Login""" + """Gets certificates for the specified Asan Login setting""" try: - # Получаем документ Asan Login + # Get Asan Login document asan_login = frappe.get_doc("Asan Login", asan_login_name) - # Проверяем статус авторизации + # Check authorization status if asan_login.auth_status != "Authenticated" and asan_login.auth_status != "Fully Authenticated": return {"success": False, "message": "Not authenticated. Please login first."} - # Получаем сертификаты + # Get certificates certificates_result = get_certificates(asan_login.bearer_token) - # Проверяем на ошибку авторизации + # Check for authorization error if certificates_result.get("error") == "unauthorized": return { "success": False, @@ -484,9 +484,9 @@ def get_auth_certificates(asan_login_name): "message": "Authentication required. Please login again." } - # Проверяем наличие сертификатов в ответе + # Check for certificates in the response if certificates_result and certificates_result.get("certificates"): - # Сохраняем сертификаты в документе + # Save certificates in the document asan_login.certificates_json = json.dumps(certificates_result.get("certificates")) asan_login.save() @@ -503,20 +503,20 @@ def get_auth_certificates(asan_login_name): @frappe.whitelist() def select_certificate(asan_login_name, certificate_data, certificate_name): - """Выбирает сертификат для указанной настройки Asan Login""" + """Selects certificate for the specified Asan Login setting""" try: - # Получаем документ Asan Login + # Get Asan Login document asan_login = frappe.get_doc("Asan Login", asan_login_name) - # Проверяем статус авторизации + # Check authorization status if asan_login.auth_status != "Authenticated" and asan_login.auth_status != "Fully Authenticated": return {"success": False, "message": "Not authenticated. Please login first."} - # Преобразуем строку JSON в объект, если необходимо + # Convert JSON string to object if necessary if isinstance(certificate_data, str): certificate_data = json.loads(certificate_data) - # Сохраняем выбранный сертификат + # Save selected certificate asan_login.selected_certificate = certificate_name asan_login.selected_certificate_json = json.dumps(certificate_data) asan_login.save() @@ -532,23 +532,23 @@ def select_certificate(asan_login_name, certificate_data, certificate_name): @frappe.whitelist() def select_taxpayer(asan_login_name): - """Выбирает налогоплательщика для указанной настройки Asan Login""" + """Selects taxpayer for the specified Asan Login setting""" try: - # Получаем документ Asan Login + # Get Asan Login document asan_login = frappe.get_doc("Asan Login", asan_login_name) - # Проверяем статус авторизации + # Check authorization status if asan_login.auth_status != "Authenticated" and asan_login.auth_status != "Fully Authenticated": return {"success": False, "message": "Not authenticated. Please login first."} - # Проверяем наличие выбранного сертификата + # Check for selected certificate if not asan_login.selected_certificate_json: return {"success": False, "message": "No certificate selected."} - # Парсим данные сертификата + # Parse certificate data cert_data = json.loads(asan_login.selected_certificate_json) - # Определяем тип и идентификатор налогоплательщика + # Determine taxpayer type and ID owner_type = cert_data.get("taxpayerType", "") tin = "" @@ -562,16 +562,16 @@ def select_taxpayer(asan_login_name): if not owner_type or not tin: return {"success": False, "message": "Invalid certificate data. Cannot determine taxpayer type or ID."} - # Логируем попытку выбора налогоплательщика + # Log taxpayer selection attempt frappe.log_error( f"Attempting to choose taxpayer: type={owner_type}, tin={tin}, bearer_token={asan_login.bearer_token[:10]}...", "Choose Taxpayer" ) - # Выбираем налогоплательщика + # Select taxpayer taxpayer_result = choose_taxpayer(asan_login.bearer_token, owner_type, tin) - # Проверяем на ошибку авторизации + # Check for authorization error if taxpayer_result.get("error") == "unauthorized": return { "success": False, @@ -579,15 +579,15 @@ def select_taxpayer(asan_login_name): "message": "Authentication required. Please login again." } - # Проверяем успешность выбора + # Check selection success if taxpayer_result and taxpayer_result.get("main_token"): - # Сохраняем основной токен и ответ + # Save main token and response asan_login.main_token = taxpayer_result.get("main_token") asan_login.choose_taxpayer_response = json.dumps(taxpayer_result.get("response_data", {})) asan_login.auth_status = "Fully Authenticated" asan_login.save() - # Логируем успешный выбор налогоплательщика + # Log successful taxpayer selection frappe.log_error( f"Successfully chose taxpayer: type={owner_type}, tin={tin}, main_token={asan_login.main_token[:10]}...", "Choose Taxpayer Success" @@ -598,7 +598,7 @@ def select_taxpayer(asan_login_name): "message": "Taxpayer selected successfully." } else: - # Логируем ошибку выбора налогоплательщика + # Log taxpayer selection error frappe.log_error( f"Failed to choose taxpayer: type={owner_type}, tin={tin}, response={taxpayer_result}", "Choose Taxpayer Error" @@ -612,40 +612,40 @@ def select_taxpayer(asan_login_name): @frappe.whitelist() def import_invoice_to_purchase_order(invoice_data, purchase_order=None): - """Импортирует данные счета-фактуры в Purchase Order""" + """Imports invoice data to Purchase Order""" try: - # Проверяем и преобразуем данные счета + # Check and convert invoice data if isinstance(invoice_data, str): invoice_data = json.loads(invoice_data) - # Определяем, создавать новый Purchase Order или обновить существующий + # 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") - # Ищем поставщика по TIN или создаем нового + # 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")) @@ -662,7 +662,7 @@ def import_invoice_to_purchase_order(invoice_data, purchase_order=None): po_doc.append("items", po_item) - # Сохраняем документ + # Save document po_doc.save() return { @@ -675,16 +675,16 @@ def import_invoice_to_purchase_order(invoice_data, purchase_order=None): 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: - # Проверяем настройки для автоматического создания + # Check settings for automatic creation try: settings = frappe.get_doc("ETaxes Settings") auto_create = settings.auto_create_items except: - auto_create = True # По умолчанию создаем новые товары + auto_create = True # Create new items by default - # Ищем товар по коду, если он указан + # Find item by code if specified if item_code: items = frappe.get_list( "Item", @@ -695,7 +695,7 @@ def find_or_create_item(item_name, item_code=None, unit=None): if items: return items[0].name - # Ищем товар по названию + # Find item by name items = frappe.get_list( "Item", filters={"item_name": item_name}, @@ -705,9 +705,9 @@ def find_or_create_item(item_name, item_code=None, unit=None): if items: return items[0].name - # Если не найден и разрешено автоматическое создание, создаем новый товар + # If not found and automatic creation is allowed, create a new item if auto_create: - # Получаем настройки для создания товаров + # Get settings for item creation try: item_group = settings.default_item_group default_uom = settings.default_uom @@ -719,7 +719,7 @@ def find_or_create_item(item_name, item_code=None, unit=None): is_stock_item = 1 is_purchase_item = 1 - # Создаем новый товар + # Create new item item_doc = frappe.new_doc("Item") item_doc.item_code = item_code or item_name item_doc.item_name = item_name @@ -738,16 +738,16 @@ def find_or_create_item(item_name, item_code=None, unit=None): return None def find_or_create_supplier(supplier_name, supplier_tin): - """Находит или создает поставщика по имени и TIN""" + """Finds or creates a supplier by name and TIN""" try: - # Проверяем настройки для автоматического создания + # Check settings for automatic creation try: settings = frappe.get_doc("ETaxes Settings") auto_create = settings.auto_create_suppliers except: - auto_create = True # По умолчанию создаем новых поставщиков + auto_create = True # Create new suppliers by default - # Ищем поставщика по TIN + # Find supplier by TIN suppliers = frappe.get_list( "Supplier", filters={"tax_id": supplier_tin}, @@ -757,7 +757,7 @@ def find_or_create_supplier(supplier_name, supplier_tin): if suppliers: return suppliers[0].name - # Ищем поставщика по названию + # Find supplier by name suppliers = frappe.get_list( "Supplier", filters={"supplier_name": supplier_name}, @@ -767,9 +767,9 @@ def find_or_create_supplier(supplier_name, supplier_tin): if suppliers: return suppliers[0].name - # Если не найден и разрешено автоматическое создание, создаем нового поставщика + # If not found and automatic creation is allowed, create a new supplier if auto_create: - # Получаем настройки для создания поставщиков + # Get settings for supplier creation try: supplier_group = settings.default_supplier_group supplier_type = settings.supplier_type @@ -779,7 +779,7 @@ def find_or_create_supplier(supplier_name, supplier_tin): supplier_type = "Company" country = "Azerbaijan" - # Создаем нового поставщика + # Create new supplier supplier_doc = frappe.new_doc("Supplier") supplier_doc.supplier_name = supplier_name supplier_doc.supplier_type = supplier_type @@ -799,8 +799,8 @@ def find_or_create_supplier(supplier_name, supplier_tin): return None def get_uom_for_unit(unit_name): - """Преобразует название единицы измерения из счета-фактуры в UOM системы""" - # Словарь для преобразования названий + """Converts unit name from invoice to system UOM""" + # Conversion dictionary uom_mapping = { "шт": "Nos", "кг": "Kg", @@ -811,47 +811,47 @@ def get_uom_for_unit(unit_name): "м3": "Cubic Meter" } - # Возвращаем соответствующий UOM или "Nos" по умолчанию + # 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 # Если настроек нет или возникла ошибка, продолжаем стандартный поиск + 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 # Если настроек нет или возникла ошибка, продолжаем стандартный поиск + 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 @@ -859,43 +859,43 @@ def find_or_create_supplier_with_mapping(supplier_name, supplier_tin): @frappe.whitelist() def import_invoice_to_purchase_order_with_mapping(invoice_data, purchase_order=None): - """Импортирует данные счета-фактуры в Purchase Order с учетом настроек сопоставления""" + """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) - # Определяем, создавать новый Purchase Order или обновить существующий + # 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"), @@ -914,7 +914,7 @@ def import_invoice_to_purchase_order_with_mapping(invoice_data, purchase_order=N po_doc.append("items", po_item) - # Сохраняем документ + # Save document po_doc.save() return { @@ -930,12 +930,12 @@ def import_invoice_to_purchase_order_with_mapping(invoice_data, purchase_order=N def load_items_from_invoices(date_from, date_to, max_count=200): """Loading items from invoices for a period""" try: - # Счетчики для отслеживания + # Counters for tracking created_count = 0 skipped_count = 0 failed_count = 0 - # Получаем настройки по умолчанию + # Get default settings asan_login_settings = get_default_asan_login() if not asan_login_settings.get('found'): @@ -1008,12 +1008,12 @@ def load_items_from_invoices(date_from, date_to, max_count=200): failed_count += 1 continue - # Проверяем существует ли уже такой элемент + # Check if such an element already exists if frappe.db.exists('E-Taxes Item', {'etaxes_item_name': item_name}): skipped_count += 1 continue - # Создаем новый элемент + # Create new element try: doc = frappe.get_doc({ 'doctype': 'E-Taxes Item', @@ -1049,12 +1049,12 @@ def load_items_from_invoices(date_from, date_to, max_count=200): def load_parties_from_invoices(date_from, date_to, max_count=200): """Loading parties from invoices for a period""" try: - # Счетчики для отслеживания + # Counters for tracking created_count = 0 skipped_count = 0 failed_count = 0 - # Получаем настройки по умолчанию + # Get default settings asan_login_settings = get_default_asan_login() if not asan_login_settings.get('found'): @@ -1113,18 +1113,18 @@ def load_parties_from_invoices(date_from, date_to, max_count=200): 'message': 'No invoices found for the specified period' } - # Для хранения уникальных контрагентов (поставщики и клиенты) + # For storing unique parties (suppliers and customers) unique_parties = {} # Process each invoice processed_count = 0 for invoice in invoices_data: processed_count += 1 - invoice_id = invoice.get('id', 'Нет ID') + invoice_id = invoice.get('id', 'No ID') serial_number = invoice.get('serialNumber', '') - # Обработка отправителя (Sender -> Supplier) + # Processing sender (Sender -> Supplier) sender = invoice.get('sender', {}) if sender: @@ -1135,7 +1135,7 @@ def load_parties_from_invoices(date_from, date_to, max_count=200): if not sender_name or not sender_tin: continue - # Создаем ключ для проверки уникальности + # Create key for uniqueness check sender_key = f"{sender_tin}|{sender_name}" if sender_key not in unique_parties: @@ -1149,7 +1149,7 @@ def load_parties_from_invoices(date_from, date_to, max_count=200): 'status': 'New' } - # Обработка получателя (Receiver -> Customer) + # Processing receiver (Receiver -> Customer) receiver = invoice.get('receiver', {}) if receiver: @@ -1160,7 +1160,7 @@ def load_parties_from_invoices(date_from, date_to, max_count=200): 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: @@ -1174,19 +1174,19 @@ def load_parties_from_invoices(date_from, date_to, max_count=200): 'status': 'New' } - # Добавляем контрагентов в систему + # Add parties to the system for party_key, party_data in unique_parties.items(): party_name = party_data.get('etaxes_party_name') party_tin = party_data.get('etaxes_tax_id') - # Проверяем существует ли уже такой контрагент + # Check if such party already exists existing = frappe.db.exists('E-Taxes Parties', {'etaxes_party_name': party_name}) if existing: skipped_count += 1 continue - # Создаем нового контрагента + # Create new party try: doc = frappe.get_doc({ 'doctype': 'E-Taxes Parties', @@ -1222,7 +1222,7 @@ def load_parties_from_invoices(date_from, date_to, max_count=200): @frappe.whitelist() def get_default_settings(): - """Получение активных настроек E-Taxes""" + """Getting active E-Taxes settings""" try: settings_list = frappe.get_all('E-Taxes Settings', filters={'is_active': 1}, @@ -1246,11 +1246,11 @@ def get_default_settings(): } def normalize_azeri_text(text): - """Нормализация азербайджанского текста для сопоставления""" + """Normalization of Azerbaijani text for matching""" if not text: return text - # Словарь замен + # Replacements dictionary replacements = { 'ə': 'e', 'Ə': 'E', @@ -1268,7 +1268,7 @@ def normalize_azeri_text(text): 'Ş': 'S' } - # Также добавим вариант ə->a и ş->sh + # Also add variant ə->a and ş->sh replacements_alt = replacements.copy() replacements_alt.update({ 'ə': 'a', @@ -1277,7 +1277,7 @@ def normalize_azeri_text(text): 'Ş': 'SH' }) - # Создаем нормализованные версии + # Create normalized versions text_normalized = text for azeri_char, latin_char in replacements.items(): text_normalized = text_normalized.replace(azeri_char, latin_char) @@ -1289,7 +1289,7 @@ def normalize_azeri_text(text): return text_normalized, text_normalized_alt def calculate_similarity(str1, str2, consider_azeri=True): - """Вычисление сходства между строками""" + """Calculating similarity between strings""" if not str1 or not str2: return 0 @@ -1299,7 +1299,7 @@ def calculate_similarity(str1, str2, consider_azeri=True): str1_norm, str1_alt = normalize_azeri_text(str1.lower()) str2_norm, str2_alt = normalize_azeri_text(str2.lower()) - # Проверяем все комбинации + # Check all combinations similarities = [ SequenceMatcher(None, str1_norm, str2_norm).ratio(), SequenceMatcher(None, str1_norm, str2_alt).ratio(), @@ -1313,38 +1313,38 @@ def calculate_similarity(str1, str2, consider_azeri=True): @frappe.whitelist() def get_unmapped_items(): - """Получение несопоставленных товаров из E-Taxes""" + """Getting unmapped items from E-Taxes""" try: - # Получаем активные настройки + # Get active settings settings = get_active_settings() if not settings: return { 'success': False, - 'message': 'Не найдены активные настройки E-Taxes' + 'message': 'No active E-Taxes settings found' } - # Получаем все товары из E-Taxes без сопоставления + # Get all unmapped items from E-Taxes unmapped_items = [] - # 1. Получаем все товары со статусом 'New' + # 1. Get all items with 'New' status etaxes_items = frappe.get_all('E-Taxes Item', filters={'status': 'New'}, fields=['name', 'etaxes_item_name', 'etaxes_item_code', 'etaxes_unit', 'etaxes_price']) - # 2. Проверяем, нет ли их уже в настройках + # 2. Check if they are already in settings existing_mappings = {} for mapping in settings.item_mappings: existing_mappings[mapping.etaxes_item_name] = mapping.erp_item - # 3. Добавляем только те, которых нет в настройках + # 3. Add only those that are not in settings for item in etaxes_items: if item.name not in existing_mappings: - # Создаем объект товара с настройками по умолчанию + # Create item object with default settings item_data = { 'name': item.name, 'etaxes_item_name': item.etaxes_item_name, 'etaxes_item_code': item.etaxes_item_code, - # Добавляем значения по умолчанию + # Add default values 'item_group': settings.default_item_group, 'uom': settings.default_uom if not item.etaxes_unit else get_uom_for_unit(item.etaxes_unit), 'is_stock_item': settings.is_stock_item, @@ -1352,7 +1352,7 @@ def get_unmapped_items(): 'item_tax_template': settings.default_item_tax_template } - # Добавляем цену, если она есть + # Add price if it exists if hasattr(item, 'etaxes_price') and item.etaxes_price: item_data['standard_rate'] = item.etaxes_price @@ -1370,61 +1370,61 @@ def get_unmapped_items(): @frappe.whitelist() def get_unmapped_parties(): - """Получение несопоставленных контрагентов из E-Taxes Parties""" + """Getting unmapped parties from E-Taxes Parties""" try: - # Получаем активные настройки + # Get active settings settings = get_active_settings() if not settings: return { 'success': False, - 'message': 'Не найдены активные настройки E-Taxes' + 'message': 'No active E-Taxes settings found' } - # Получаем всех контрагентов из E-Taxes Parties без сопоставления + # Get all unmapped parties from E-Taxes Parties unmapped_parties = [] - # Получаем список существующих сопоставлений + # Get list of existing mappings existing_mappings = {} for mapping in settings.party_mappings: existing_mappings[mapping.etaxes_party_name] = mapping.erp_party - # Получаем несопоставленные контрагенты из E-Taxes Parties + # Get unmapped parties from E-Taxes Parties etaxes_parties = frappe.get_all('E-Taxes Parties', filters={'status': 'New'}, fields=['name', 'etaxes_party_name', 'etaxes_tax_id', 'etaxes_party_type', 'erp_party_type', 'etaxes_address', 'source_invoice']) - # Фильтруем только те, которых нет в маппингах + # Filter only those that are not in mappings for party in etaxes_parties: if party.etaxes_party_name not in existing_mappings: - # Определяем тип контрагента (Customer/Supplier) + # Determine party type (Customer/Supplier) party_type = party.erp_party_type or ('Supplier' if party.etaxes_party_type == 'Sender' else 'Customer') - # Создаем базовый объект с общими данными + # Create base object with common data party_data = { 'name': party.name, 'etaxes_party_name': party.etaxes_party_name, 'etaxes_tax_id': party.etaxes_tax_id, 'etaxes_party_type': party.etaxes_party_type, 'erp_party_type': party_type, - # Общие настройки + # Common settings 'payment_terms': settings.default_payment_terms } - # Добавляем адрес, если он есть + # Add address if it exists if party.etaxes_address: party_data['address'] = party.etaxes_address - # Добавляем источник, если есть + # Add source if it exists if party.source_invoice: party_data['source'] = party.source_invoice - # Добавляем специфические поля в зависимости от типа + # Add specific fields depending on type if party_type == 'Supplier': party_data['supplier_group'] = settings.default_supplier_group else: # Customer party_data['customer_group'] = settings.default_customer_group - party_data['territory'] = 'All Territories' # Значение по умолчанию + party_data['territory'] = 'All Territories' # Default value unmapped_parties.append(party_data) @@ -1440,77 +1440,77 @@ def get_unmapped_parties(): @frappe.whitelist() def match_similar_items(): - """Сопоставление товаров по похожим названиям""" + """Matching items by similar names""" try: import re from difflib import SequenceMatcher - # Получаем активные настройки + # Get active settings settings = get_active_settings() if not settings: return { 'success': False, - 'message': 'Не найдены активные настройки E-Taxes' + 'message': 'No active E-Taxes settings found' } - # Получаем порог схожести из настроек + # Get similarity threshold from settings similarity_threshold = float(settings.similarity_threshold) / 100.0 if settings.similarity_threshold else 0.8 consider_azeri = settings.consider_azeri_chars - # Получаем все товары со статусом "New" из E-Taxes + # Get all items with "New" status from E-Taxes etaxes_items = frappe.get_all('E-Taxes Item', filters={'status': 'New'}, fields=['name', 'etaxes_item_name', 'etaxes_item_code']) - # Логируем товары + # Log items - # Получаем существующие маппинги + # Get existing mappings existing_mappings = {} for mapping in settings.item_mappings: existing_mappings[mapping.etaxes_item_name] = mapping.erp_item - # Логируем существующие маппинги + # Log existing mappings - # Получаем товары с пустым сопоставлением (есть в таблице, но erp_item is None или пустой) + # Get items with empty mapping (exist in table but erp_item is None or empty) items_with_empty_mapping = [] for etaxes_id, erp_item in existing_mappings.items(): - if not erp_item: # Если erp_item is None или пустая строка - # Находим соответствующий товар E-Taxes + if not erp_item: # If erp_item is None or empty string + # Find corresponding E-Taxes item for item in etaxes_items: if item.name == etaxes_id: items_with_empty_mapping.append(item) break - # Фильтруем только те, которых нет в маппингах + # Filter only those that are not in mappings unmapped_items = [] for item in etaxes_items: if item.name not in existing_mappings: unmapped_items.append(item) - # Объединяем два списка: товары без маппингов и товары с пустыми маппингами + # Combine two lists: items without mappings and items with empty mappings items_to_process = unmapped_items + items_with_empty_mapping - # Логируем итоговый список товаров для обработки + # Log final list of items to process - # Получаем все товары из системы + # Get all items from system system_items = frappe.get_all('Item', fields=['name', 'item_name', 'item_code']) matched_count = 0 total_processed = len(items_to_process) - # Получаем свежую копию документа настроек + # Get fresh copy of settings document doc = frappe.get_doc("E-Taxes Settings", settings.name) def normalize_string(text, consider_azeri=False): if not text: return "" - # Преобразуем текст в нижний регистр + # Convert text to lowercase text = 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' @@ -1518,42 +1518,42 @@ def match_similar_items(): for az_char, lat_char in replacements.items(): text = text.replace(az_char, lat_char) - # Удаляем все символы, кроме букв и цифр + # Remove all characters except letters and numbers text = re.sub(r'[^a-z0-9]', '', text) return text - # Проходим по всем товарам для обработки + # Process all items for etaxes_item in items_to_process: - # Логируем обрабатываемый товар + # Log item being processed - # Ищем похожий товар + # Find similar item best_match = None best_score = 0 - # Нормализуем имя E-Taxes товара + # Normalize E-Taxes item name etaxes_name = normalize_string(etaxes_item.etaxes_item_name, consider_azeri) for item in system_items: - # Нормализуем имя системного товара + # Normalize system item name item_name = normalize_string(item.item_name, consider_azeri) - # Вычисляем коэффициент схожести + # Calculate similarity coefficient name_score = SequenceMatcher(None, etaxes_name, item_name).ratio() - # Логируем высокие совпадения (более 0.8) + # Log high matches (more than 0.8) if name_score > 0.8: print("test") - # Проверяем, лучше ли это совпадение + # Check if this is a better match if name_score > best_score and name_score >= similarity_threshold: best_score = name_score best_match = item if best_match: - # Логируем найденное совпадение + # Log found match - # ВАЖНО: проверяем, есть ли уже запись для этого товара в маппингах + # IMPORTANT: check if there is already a record for this item in mappings existing_row = None for idx, mapping in enumerate(doc.item_mappings): if mapping.etaxes_item_name == etaxes_item.name: @@ -1561,11 +1561,11 @@ def match_similar_items(): break if existing_row is not None: - # Если строка уже существует, обновляем её значение erp_item + # If row already exists, update its erp_item value doc.item_mappings[existing_row].erp_item = best_match.name doc.item_mappings[existing_row].mapping_type = 'Automatic' else: - # Если строки нет, добавляем новую + # If no row, add a new one doc.append('item_mappings', { 'etaxes_item_name': etaxes_item.name, 'erp_item': best_match.name, @@ -1574,7 +1574,7 @@ def match_similar_items(): matched_count += 1 - # Обновляем статус E-Taxes Item только для товаров, для которых найдено соответствие + # Update E-Taxes Item status only for items that have a match item_doc = frappe.get_doc('E-Taxes Item', etaxes_item.name) item_doc.status = 'Mapped' item_doc.mapped_item = best_match.name @@ -1582,17 +1582,17 @@ def match_similar_items(): else: print("test") - # Сохраняем настройки только если были найдены соответствия + # Save settings only if matches were found if matched_count > 0: doc.save() - # Логируем итоговый результат + # Log final result return { 'success': True, 'matched_count': matched_count, 'total_processed': total_processed, - 'message': f'Сопоставлено {matched_count} из {total_processed} товаров' + 'message': f'Matched {matched_count} out of {total_processed} items' } except Exception as e: @@ -1603,99 +1603,99 @@ def match_similar_items(): @frappe.whitelist() def match_similar_parties(): - """Сопоставление контрагентов по похожим названиям""" + """Matching parties by similar names""" try: import re from difflib import SequenceMatcher - # Получаем активные настройки + # Get active settings settings = get_active_settings() if not settings: return { 'success': False, - 'message': 'Не найдены активные настройки E-Taxes' + 'message': 'No active E-Taxes settings found' } - # Получаем порог схожести из настроек + # Get similarity threshold from settings similarity_threshold = float(settings.similarity_threshold) / 100.0 if settings.similarity_threshold else 0.8 consider_azeri = settings.consider_azeri_chars - # Получаем все контрагенты со статусом "New" из E-Taxes Parties + # Get all parties with "New" status from E-Taxes Parties etaxes_parties = frappe.get_all('E-Taxes Parties', filters={'status': 'New'}, fields=['name', 'etaxes_party_name', 'etaxes_tax_id', 'etaxes_party_type', 'erp_party_type']) - # Логируем контрагентов + # Log parties frappe.log_error(f"E-Taxes parties with status New: {etaxes_parties}", "Match Parties Log") - # Получаем существующие маппинги + # Get existing mappings existing_mappings = {} for mapping in settings.party_mappings: existing_mappings[mapping.etaxes_party_name] = mapping.erp_party - # Логируем существующие маппинги + # Log existing mappings frappe.log_error(f"Existing mappings: {existing_mappings}", "Match Parties Log") - # Фильтруем только те, которых нет в маппингах или у которых пустые маппинги + # Filter only those that are not in mappings or have empty mappings parties_to_process = [] for party in etaxes_parties: if party.etaxes_party_name not in existing_mappings or not existing_mappings[party.etaxes_party_name]: parties_to_process.append(party) - # Логируем итоговый список контрагентов для обработки + # Log final list of parties to process frappe.log_error(f"Parties to process: {parties_to_process}", "Match Parties Log") matched_count = 0 total_processed = len(parties_to_process) - # Получаем свежую копию документа настроек + # Get fresh copy of settings document doc = frappe.get_doc("E-Taxes Settings", settings.name) - # Проходим по всем контрагентам для обработки + # Process all parties for etaxes_party in parties_to_process: - # Определяем тип контрагента в ERP на основе etaxes_party_type + # Determine ERP party type based on etaxes_party_type erp_party_type = etaxes_party.erp_party_type - # Логируем обрабатываемого контрагента + # Log party being processed frappe.log_error(f"Processing party: {etaxes_party.etaxes_party_name} (Type: {erp_party_type})", "Match Parties Log") - # Получаем всех контрагентов соответствующего типа из системы + # Get all parties of corresponding type from system system_parties = frappe.get_all(erp_party_type, fields=['name', 'supplier_name' if erp_party_type == 'Supplier' else 'customer_name']) - # Ищем похожего контрагента + # Find similar party best_match = None best_score = 0 - # Нормализуем имя E-Taxes контрагента + # Normalize E-Taxes party name etaxes_name = normalize_string(etaxes_party.etaxes_party_name, consider_azeri) for party in system_parties: - # Получаем имя контрагента в зависимости от типа + # Get party name depending on type party_name = party.supplier_name if erp_party_type == 'Supplier' else party.customer_name - # Нормализуем имя системного контрагента + # Normalize system party name party_name_normalized = normalize_string(party_name, consider_azeri) - # Вычисляем коэффициент схожести + # Calculate similarity coefficient name_score = SequenceMatcher(None, etaxes_name, party_name_normalized).ratio() - # Логируем высокие совпадения (более 0.8) + # Log high matches (more than 0.8) if name_score > 0.8: frappe.log_error(f"High score: '{etaxes_party.etaxes_party_name}' vs '{party_name}' = {name_score}", "Match Parties Log") - # Проверяем, лучше ли это совпадение + # Check if this is a better match if name_score > best_score and name_score >= similarity_threshold: best_score = name_score best_match = party if best_match: - # Логируем найденное совпадение + # Log found match frappe.log_error(f"Match found: '{etaxes_party.etaxes_party_name}' matches with '{best_match.name}' (score: {best_score})", "Match Parties Log") - # ВАЖНО: проверяем, есть ли уже запись для этого контрагента в маппингах + # IMPORTANT: check if there is already a record for this party in mappings existing_row = None for idx, mapping in enumerate(doc.party_mappings): if mapping.etaxes_party_name == etaxes_party.etaxes_party_name: @@ -1703,12 +1703,12 @@ def match_similar_parties(): break if existing_row is not None: - # Если строка уже существует, обновляем её значение erp_party + # If row already exists, update its erp_party value doc.party_mappings[existing_row].erp_party = best_match.name doc.party_mappings[existing_row].party_type = erp_party_type doc.party_mappings[existing_row].mapping_type = 'Automatic' else: - # Если строки нет, добавляем новую + # If no row, add a new one doc.append('party_mappings', { 'etaxes_party_name': etaxes_party.etaxes_party_name, 'etaxes_tax_id': etaxes_party.etaxes_tax_id, @@ -1719,7 +1719,7 @@ def match_similar_parties(): matched_count += 1 - # Обновляем запись в E-Taxes Parties + # Update record in E-Taxes Parties try: party_doc = frappe.get_doc('E-Taxes Parties', etaxes_party.name) party_doc.status = 'Mapped' @@ -1728,21 +1728,21 @@ def match_similar_parties(): except Exception as e: frappe.log_error(f"Error updating party status: {str(e)}", "Match Parties Log") else: - # Для контрагентов, для которых не найдено соответствие, НЕ меняем статус + # For parties that have no match, DO NOT change status frappe.log_error(f"No match found for '{etaxes_party.etaxes_party_name}', keeping status as 'New'", "Match Parties Log") - # Сохраняем настройки после добавления всех сопоставлений + # Save settings after adding all mappings if matched_count > 0: doc.save() - # Логируем итоговый результат + # Log final result frappe.log_error(f"Final result: matched {matched_count} out of {total_processed} parties", "Match Parties Log") return { 'success': True, 'matched_count': matched_count, 'total_processed': total_processed, - 'message': f'Сопоставлено {matched_count} из {total_processed} контрагентов' + 'message': f'Matched {matched_count} out of {total_processed} parties' } except Exception as e: @@ -1755,20 +1755,20 @@ def match_similar_parties(): @frappe.whitelist() def create_unmapped_items(settings_name): - """Создание товаров для несопоставленных элементов из табличной части настроек E-Taxes""" + """Creating items for unmapped elements from E-Taxes settings table""" try: import re - # Получаем настройки + # Get settings settings_doc = frappe.get_doc("E-Taxes Settings", settings_name) - # Получаем элементы из табличной части, у которых нет сопоставления + # Get elements from table that have no mapping unmapped_items = [] for mapping_item in settings_doc.item_mappings: - # Проверяем, что у товара нет сопоставления или оно пустое + # Check that item has no mapping or it's empty if not mapping_item.erp_item: - # Получаем документ E-Taxes Item для этой записи + # Get E-Taxes Item document for this record if frappe.db.exists('E-Taxes Item', mapping_item.etaxes_item_name): etaxes_item = frappe.get_doc('E-Taxes Item', mapping_item.etaxes_item_name) unmapped_items.append(etaxes_item) @@ -1778,13 +1778,13 @@ def create_unmapped_items(settings_name): "success": True, "created_count": 0, "error_count": 0, - "message": "Нет несопоставленных товаров в табличной части для создания" + "message": "No unmapped items in table to create" } created_count = 0 error_count = 0 - # Собираем индивидуальные настройки для каждого элемента + # Collect individual settings for each element mapping_settings = {} for mapping in settings_doc.item_mappings: item_settings = {} @@ -1804,22 +1804,22 @@ def create_unmapped_items(settings_name): for etaxes_item in unmapped_items: try: - # Проверяем, существует ли уже товар с таким именем + # Check if an item with this name already exists item_exists = frappe.db.exists('Item', {'item_name': etaxes_item.etaxes_item_name}) if item_exists: continue - # Создаем новый товар + # Create new item item_doc = frappe.new_doc("Item") - # Обеспечиваем уникальность item_code, удаляя недопустимые символы + # Ensure item_code uniqueness, removing invalid characters item_code = etaxes_item.etaxes_item_code or etaxes_item.etaxes_item_name item_code = re.sub(r'[^a-zA-Z0-9_.-]', '', item_code) if not item_code or len(item_code) < 3: item_code = "ITEM-" + frappe.generate_hash(length=8) - # Проверяем, что item_code уникален + # Check that item_code is unique if frappe.db.exists('Item', {'item_code': item_code}): item_code = item_code + "-" + frappe.generate_hash(length=5) @@ -1827,25 +1827,25 @@ def create_unmapped_items(settings_name): item_doc.item_name = etaxes_item.etaxes_item_name item_doc.description = etaxes_item.etaxes_item_name - # Проверяем, есть ли индивидуальные настройки для данного элемента + # Check if there are individual settings for this element individual_settings = mapping_settings.get(etaxes_item.name, {}) - # Применяем настройки с учетом индивидуальных приоритетов + # Apply settings considering individual priorities item_group = individual_settings.get('item_group', settings_doc.default_item_group) stock_uom = individual_settings.get('uom', settings_doc.default_uom) - # Проверяем существование item_group + # Check item_group existence if not frappe.db.exists('Item Group', item_group): item_group = "All Item Groups" - # Проверяем существование UOM + # Check UOM existence if not frappe.db.exists('UOM', stock_uom): stock_uom = "Nos" item_doc.item_group = item_group item_doc.stock_uom = stock_uom - # Для булевых полей проверяем, что они не None + # For boolean fields check that they are not None if 'is_stock_item' in individual_settings: item_doc.is_stock_item = individual_settings['is_stock_item'] else: @@ -1856,7 +1856,7 @@ def create_unmapped_items(settings_name): else: item_doc.is_purchase_item = getattr(settings_doc, 'is_purchase_item', 1) - # Правильно добавляем налоговый шаблон + # Correctly add tax template if 'item_tax_template' in individual_settings: tax_template = individual_settings['item_tax_template'] if frappe.db.exists('Item Tax Template', tax_template): @@ -1868,17 +1868,17 @@ def create_unmapped_items(settings_name): tax_row = item_doc.append('taxes', {}) tax_row.item_tax_template = tax_template - # Сохраняем товар + # Save item item_doc.insert() - # Обновляем маппинг в настройках + # Update mapping in settings for mapping in settings_doc.item_mappings: if mapping.etaxes_item_name == etaxes_item.name: mapping.erp_item = item_doc.name mapping.mapping_type = 'Automatic' break - # Обновляем статус E-Taxes Item + # Update E-Taxes Item status etaxes_item.status = 'Mapped' etaxes_item.mapped_item = item_doc.name etaxes_item.save() @@ -1888,7 +1888,7 @@ def create_unmapped_items(settings_name): except Exception as e: error_count += 1 - # Сохраняем настройки после добавления всех сопоставлений + # Save settings after adding all mappings if created_count > 0: settings_doc.save() @@ -1896,7 +1896,7 @@ def create_unmapped_items(settings_name): "success": True, "created_count": created_count, "error_count": error_count, - "message": f"Создано {created_count} товаров, ошибок: {error_count}" + "message": f"Created {created_count} items, errors: {error_count}" } except Exception as e: @@ -1907,21 +1907,21 @@ def create_unmapped_items(settings_name): @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""" try: - # Десериализация если необходимо + # Deserialize if necessary if isinstance(invoice_data, str): invoice_data = json.loads(invoice_data) - # Получаем активные настройки + # Get active settings settings = get_active_settings() if not settings: return { 'success': False, - 'message': 'Не найдены активные настройки E-Taxes' + 'message': 'No active E-Taxes settings found' } - # Создаем словари сопоставлений + # Create mapping dictionaries item_mappings = {} for mapping in settings.item_mappings: key = f"{mapping.etaxes_item_name}|{mapping.etaxes_item_code}" @@ -1932,20 +1932,20 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule key = f"{mapping.etaxes_party_name}|{mapping.etaxes_tax_id}" party_mappings[key] = (mapping.erp_party, mapping.party_type) - # Получаем склад по умолчанию + # Get default warehouse default_warehouse = warehouse if not default_warehouse: - # Если склад не указан, пробуем взять из настроек + # If warehouse not specified, try to get from settings if hasattr(settings, 'default_warehouse') and settings.default_warehouse: default_warehouse = settings.default_warehouse else: - # Если склад не указан в настройках, попробуем получить склад по умолчанию для компании + # If warehouse not specified in settings, try to get default warehouse for company company = frappe.defaults.get_user_default('Company') if company: default_warehouse = frappe.db.get_value('Company', company, 'default_warehouse') - # Если все еще нет склада, возьмем первый активный склад + # If still no warehouse, take first active warehouse if not default_warehouse: warehouses = frappe.get_all('Warehouse', filters={'is_group': 0, 'disabled': 0}, @@ -1954,29 +1954,29 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule if warehouses: default_warehouse = warehouses[0].name - # Проверяем наличие склада + # Check warehouse existence if not default_warehouse: return { 'success': False, - 'message': 'Не найден склад по умолчанию. Пожалуйста, укажите склад в настройках E-Taxes.' + 'message': 'Default warehouse not found. Please specify warehouse in E-Taxes settings.' } - # Получаем или создаем Purchase Order + # Get or create Purchase Order if purchase_order_name: po = frappe.get_doc('Purchase Order', purchase_order_name) else: - # Создаем новый Purchase Order + # Create new Purchase Order po = frappe.new_doc('Purchase Order') - # Устанавливаем поставщика + # Set supplier sender = invoice_data.get('sender', {}) sender_key = f"{sender.get('name', '')}|{sender.get('tin', '')}" if sender_key in party_mappings: po.supplier = party_mappings[sender_key][0] else: - # Добавляем поставщика в список несопоставленных - # Сначала сохраняем информацию о контрагенте, если его еще нет + # Add supplier to unmapped list + # First save party information if it doesn't exist yet if not frappe.db.exists('E-Taxes Parties', {'etaxes_party_name': sender.get('name', ''), 'etaxes_tax_id': sender.get('tin', '')}): etaxes_party = frappe.new_doc('E-Taxes Parties') etaxes_party.etaxes_party_name = sender.get('name', '') @@ -1992,13 +1992,13 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule 'tin': sender.get('tin', ''), 'type': 'Sender' }], - 'message': f'Не найдено сопоставление для поставщика: {sender.get("name", "")}' + 'message': f'No mapping found for supplier: {sender.get("name", "")}' } - # Устанавливаем даты + # Set dates po.transaction_date = frappe.utils.today() - # Получаем дату из инвойса или параметра, если он указан + # Get date from invoice or parameter if specified if schedule_date: po.schedule_date = schedule_date elif invoice_data.get("creationDate"): @@ -2008,15 +2008,15 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule po.company = frappe.defaults.get_user_default('Company') - # Добавляем/обновляем дополнительные поля из счета-фактуры + # Add/update additional fields from invoice po.title = f"Invoice {invoice_data.get('serialNumber', '')}" po.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "") - # Очищаем существующие элементы, если нужно обновить их полностью + # Clear existing items if need to update them completely if purchase_order_name and invoice_data.get("items"): po.items = [] - # ВАЖНО: Установим date_to_use для строк + # IMPORTANT: Set date_to_use for rows date_to_use = None if schedule_date: date_to_use = schedule_date @@ -2025,40 +2025,40 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule else: date_to_use = frappe.utils.today() - # Переменная для отслеживания добавленных элементов + # Variable for tracking added items added_items_count = 0 unmatched_items = [] - # Добавляем элементы из счета-фактуры + # Add items from invoice if invoice_data.get("items"): for item in invoice_data.get("items", []): - # Подготовка ключа для поиска сопоставления + # Prepare key for mapping search item_name = item.get("productName", "") item_code = item.get("itemId", "") item_key = f"{item_name}|{item_code}" - # Ищем сопоставление + # Look for mapping mapped_item = None - # Сначала ищем в маппингах + # First look in mappings if item_key in item_mappings: mapped_item = item_mappings[item_key] else: - # Пытаемся найти товар по коду, если указан + # Try to find item by code if specified if item_code: item_list = frappe.get_all('Item', filters={'item_code': item_code}, limit=1) if item_list: mapped_item = item_list[0].name - # Если не нашли по коду, ищем по наименованию + # If not found by code, look by name if not mapped_item: item_list = frappe.get_all('Item', filters={'item_name': item_name}, limit=1) if item_list: mapped_item = item_list[0].name - # Если не нашли ни по коду, ни по имени + # If not found by code or name if not mapped_item: - # Сохраняем информацию о товаре, если его еще нет + # Save item information if it doesn't exist yet if not frappe.db.exists('E-Taxes Item', {'etaxes_item_name': item_name, 'etaxes_item_code': item_code}): etaxes_item = frappe.new_doc('E-Taxes Item') etaxes_item.etaxes_item_name = item_name @@ -2068,14 +2068,14 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule etaxes_item.status = 'New' etaxes_item.insert() - # Добавляем в список несопоставленных + # Add to unmapped list unmatched_items.append({ 'name': item_name, 'code': item_code }) - continue # Переходим к следующему элементу + continue # Move to next element - # Добавляем позицию в PO + # Add position to PO po_item = frappe.new_doc("Purchase Order Item") po_item.parent = po.name po_item.parenttype = "Purchase Order" @@ -2089,24 +2089,24 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule po_item.amount = item.get("cost", 0) po_item.uom = get_uom_for_unit(item.get("unit", "")) - # ВАЖНО: Устанавливаем schedule_date для каждой строки + # IMPORTANT: Set schedule_date for each row po_item.schedule_date = date_to_use - # ВАЖНО: Устанавливаем склад по умолчанию + # IMPORTANT: Set default warehouse po_item.warehouse = default_warehouse po.append("items", po_item) added_items_count += 1 - # Проверяем, есть ли несопоставленные товары + # Check if there are unmapped items if unmatched_items: return { 'success': False, 'unmatched_items': unmatched_items, - 'message': f'Не найдено сопоставление для {len(unmatched_items)} товаров' + 'message': f'No mapping found for {len(unmatched_items)} items' } - # Проверяем, что в табличной части есть хотя бы один элемент + # Check that there is at least one element in table if added_items_count == 0: frappe.log_error( f"No items were added to PO. Invoice data: {invoice_data}", @@ -2114,25 +2114,25 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule ) return { 'success': False, - 'message': 'Не удалось добавить ни одного товара в заказ' + 'message': 'Could not add any items to order' } - # Сохраняем документ + # Save document po.save() - # Дополнительная проверка, что все schedule_date и warehouse установлены + # Additional check that all schedule_date and warehouse are set for item in po.items: if not item.schedule_date: item.schedule_date = date_to_use if not item.warehouse: item.warehouse = default_warehouse - # Сохраняем ещё раз, чтобы точно применить изменения + # Save again to ensure changes are applied po.save() return { 'success': True, - 'message': 'Данные счета-фактуры импортированы успешно.', + 'message': 'Invoice data imported successfully.', 'purchase_order': po.name } @@ -2143,10 +2143,10 @@ def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule 'message': str(e) } -# Вспомогательные функции +# Helper functions def get_active_settings(): - """Получение активных настроек E-Taxes""" + """Getting active E-Taxes settings""" settings_list = frappe.get_all('E-Taxes Settings', filters={'is_active': 1}, order_by='creation desc', @@ -2158,20 +2158,20 @@ def get_active_settings(): return frappe.get_doc('E-Taxes Settings', settings_list[0].name) def normalize_string(text, consider_azeri=True): - """Нормализация строки для сравнения""" + """Normalizing string for comparison""" import re if not text: return '' - # Приводим к нижнему регистру + # Convert to lowercase text = text.lower() - # Удаляем все специальные символы и цифры + # Remove all special characters and numbers text = re.sub(r'[^\w\s]', '', text) text = re.sub(r'\d+', '', text) - # Заменяем азербайджанские символы, если нужно + # Replace Azerbaijani characters if needed if consider_azeri: azeri_replacements = { 'ə': 'e', @@ -2186,17 +2186,17 @@ def normalize_string(text, consider_azeri=True): for azeri_char, latin_char in azeri_replacements.items(): text = text.replace(azeri_char, latin_char) - # Удаляем лишние пробелы + # Remove extra spaces text = ' '.join(text.split()) return text def get_uom_for_unit(unit_name): - """Получение UOM для единицы измерения из E-Taxes""" + """Getting UOM for unit from E-Taxes""" if not unit_name: - return 'Nos' # По умолчанию + return 'Nos' # Default - # Сопоставление единиц измерения + # Unit mapping uom_mappings = { 'ədəd': 'Nos', 'kq': 'Kg', @@ -2214,35 +2214,35 @@ def get_uom_for_unit(unit_name): if normalized_unit in uom_mappings: return uom_mappings[normalized_unit] - # Проверяем, существует ли такая единица измерения в системе + # Check if such unit exists in system if frappe.db.exists('UOM', {'uom_name': unit_name}): return unit_name - # Если не найдено соответствие, возвращаем Nos по умолчанию + # 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' @@ -2253,7 +2253,7 @@ def debug_azeri_matching(search_term, compare_with): else: text_replaced = text_lower - # Удаляем все символы, кроме букв и цифр + # Remove all characters except letters and numbers text_clean = re.sub(r'[^a-z0-9]', '', text_replaced) return { @@ -2263,11 +2263,11 @@ def debug_azeri_matching(search_term, compare_with): '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 { @@ -2292,18 +2292,18 @@ def debug_azeri_matching(search_term, compare_with): @frappe.whitelist() def create_unmapped_parties(settings_name): - """Создание контрагентов для несопоставленных элементов из табличной части настроек E-Taxes""" + """Creating parties for unmapped elements from E-Taxes settings table""" try: - # Получаем настройки + # Get settings settings_doc = frappe.get_doc("E-Taxes Settings", settings_name) - # Получаем элементы из табличной части, у которых нет сопоставления + # Get elements from table that have no mapping unmapped_parties = [] for mapping_party in settings_doc.party_mappings: - # Проверяем, что у контрагента нет сопоставления или оно пустое + # Check that party has no mapping or it's empty if not mapping_party.erp_party: - # Получаем документ E-Taxes Parties для этой записи + # Get E-Taxes Parties document for this record parties = frappe.get_all('E-Taxes Parties', filters={ 'etaxes_party_name': mapping_party.etaxes_party_name, @@ -2312,7 +2312,7 @@ def create_unmapped_parties(settings_name): fields=['name']) if parties: etaxes_party = frappe.get_doc('E-Taxes Parties', parties[0].name) - # Добавляем тип контрагента из настроек + # Add party type from settings etaxes_party.erp_party_type = mapping_party.party_type unmapped_parties.append(etaxes_party) @@ -2321,13 +2321,13 @@ def create_unmapped_parties(settings_name): "success": True, "created_count": 0, "error_count": 0, - "message": "Нет несопоставленных контрагентов в табличной части для создания" + "message": "No unmapped partners in table to create" } created_count = 0 error_count = 0 - # Собираем индивидуальные настройки для каждого элемента + # Collect individual settings for each element mapping_settings = {} for mapping in settings_doc.party_mappings: party_settings = {} @@ -2346,73 +2346,73 @@ def create_unmapped_parties(settings_name): for etaxes_party in unmapped_parties: try: - # Определяем тип контрагента (Customer/Supplier) + # Determine party type (Customer/Supplier) party_type = etaxes_party.erp_party_type - # Проверяем, существует ли уже контрагент с таким названием и типом + # Check if such party with this name and type already exists party_field = 'supplier_name' if party_type == 'Supplier' else 'customer_name' party_exists = frappe.db.exists(party_type, {party_field: etaxes_party.etaxes_party_name}) if party_exists: continue - # Создаем нового контрагента + # Create new party party_doc = frappe.new_doc(party_type) - # Получаем индивидуальные настройки + # Get individual settings key = f"{etaxes_party.etaxes_party_name}|{etaxes_party.etaxes_tax_id}" individual_settings = mapping_settings.get(key, {}) - # Задаем основные поля + # Set basic fields if party_type == "Supplier": party_doc.supplier_name = etaxes_party.etaxes_party_name - party_doc.supplier_type = "Company" # Можно настроить при необходимости + party_doc.supplier_type = "Company" # Can be configured if needed - # Применяем настройки с учетом индивидуальных приоритетов + # Apply settings considering individual priorities supplier_group = individual_settings.get('supplier_group', settings_doc.default_supplier_group) if not frappe.db.exists('Supplier Group', supplier_group): supplier_group = "All Supplier Groups" party_doc.supplier_group = supplier_group - # Если указан tax_id, добавляем его + # If tax_id specified, add it if etaxes_party.etaxes_tax_id: party_doc.tax_id = etaxes_party.etaxes_tax_id - # Если указаны payment terms, добавляем их + # If payment terms specified, add them if 'payment_terms' in individual_settings: party_doc.payment_terms = individual_settings['payment_terms'] elif hasattr(settings_doc, 'default_payment_terms') and settings_doc.default_payment_terms: party_doc.payment_terms = settings_doc.default_payment_terms else: # party_type == "Customer" party_doc.customer_name = etaxes_party.etaxes_party_name - party_doc.customer_type = "Company" # Можно настроить при необходимости + party_doc.customer_type = "Company" # Can be configured if needed - # Применяем настройки с учетом индивидуальных приоритетов + # Apply settings considering individual priorities customer_group = individual_settings.get('customer_group', settings_doc.default_customer_group) if not frappe.db.exists('Customer Group', customer_group): customer_group = "All Customer Groups" party_doc.customer_group = customer_group - # Территория + # Territory territory = individual_settings.get('territory', "All Territories") if not frappe.db.exists('Territory', territory): territory = "All Territories" party_doc.territory = territory - # Если указан tax_id, добавляем его + # If tax_id specified, add it if etaxes_party.etaxes_tax_id: party_doc.tax_id = etaxes_party.etaxes_tax_id - # Если указаны payment terms, добавляем их + # If payment terms specified, add them if 'payment_terms' in individual_settings: party_doc.payment_terms = individual_settings['payment_terms'] elif hasattr(settings_doc, 'default_payment_terms') and settings_doc.default_payment_terms: party_doc.payment_terms = settings_doc.default_payment_terms - # Сохраняем контрагента + # Save party party_doc.insert() - # Обновляем маппинг в настройках + # Update mapping in settings for mapping in settings_doc.party_mappings: if mapping.etaxes_party_name == etaxes_party.etaxes_party_name and mapping.etaxes_tax_id == etaxes_party.etaxes_tax_id: mapping.erp_party = party_doc.name @@ -2420,7 +2420,7 @@ def create_unmapped_parties(settings_name): mapping.mapping_type = 'Automatic' break - # Обновляем статус E-Taxes Parties + # Update E-Taxes Parties status etaxes_party.status = 'Mapped' etaxes_party.mapped_party = party_doc.name etaxes_party.save() @@ -2430,7 +2430,7 @@ def create_unmapped_parties(settings_name): except Exception as e: error_count += 1 - # Сохраняем настройки после добавления всех сопоставлений + # Save settings after adding all mappings if created_count > 0: settings_doc.save() @@ -2438,7 +2438,7 @@ def create_unmapped_parties(settings_name): "success": True, "created_count": created_count, "error_count": error_count, - "message": f"Создано {created_count} контрагентов, ошибок: {error_count}" + "message": f"Created {created_count} parties, errors: {error_count}" } except Exception as e: @@ -2446,10 +2446,10 @@ def create_unmapped_parties(settings_name): "success": False, "message": str(e) } - + def handle_unauthorized_request(asan_login_name, operation_name="API request"): - """Общая функция для обработки неавторизованных запросов""" - # Логирование события + """Common function for handling unauthorized requests""" + # Log event frappe.log_error( f"Unauthorized request for operation: {operation_name}, asan_login_name: {asan_login_name}", "Unauthorized API Request" @@ -2463,33 +2463,33 @@ def handle_unauthorized_request(asan_login_name, operation_name="API request"): @frappe.whitelist() def check_token_validity(asan_login_name=None): - """Проверяет актуальность токена для указанной настройки Asan Login""" + """Checks token validity for specified Asan Login setting""" 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") - # Получаем документ Asan Login + # Get Asan Login document asan_login = frappe.get_doc("Asan Login", asan_login_name) - # Проверяем наличие main_token (это важно для E-Taxes) + # Check for main_token (important for E-Taxes) 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) - # Проверяем на ошибку авторизации + # Check for authorization error if certificates_result.get("error") == "unauthorized": return { "valid": False, "message": "Authentication required. Token is expired." } - # Если нет ошибки, токен действителен + # If no error, token is valid return {"valid": True, "message": "Token is valid."} except Exception as e: @@ -2498,9 +2498,9 @@ def check_token_validity(asan_login_name=None): @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 { @@ -2515,12 +2515,12 @@ def check_invoice_exists(serial_number): } @frappe.whitelist() def get_etaxes_purchases(): - """Получает список всех записей E-Taxes Purchase""" + """Gets list of all E-Taxes Purchase records""" try: purchases = frappe.get_all('E-Taxes Purchase', fields=['name', 'etaxes_id', 'date', 'party', 'total']) - # Логирование для отладки + # Debug logging frappe.logger().info(f"Retrieved {len(purchases)} E-Taxes Purchase records") return { @@ -2536,18 +2536,18 @@ def get_etaxes_purchases(): @frappe.whitelist() def create_etaxes_purchase(etaxes_id, date, party, total): - """Создает запись E-Taxes Purchase для отслеживания импортированных инвойсов""" + """Creates E-Taxes Purchase record for tracking imported invoices""" try: - # Нормализуем ID для хранения + # Normalize ID for storage etaxes_id = str(etaxes_id).strip() if etaxes_id else "" - # Логирование для отладки + # Debug logging frappe.logger().info(f"Creating E-Taxes Purchase: ID={etaxes_id}, date={date}, party={party}, total={total}") - # Проверяем, существует ли уже запись с таким etaxes_id + # Check if record with such etaxes_id already exists existing = frappe.db.get_value('E-Taxes Purchase', {'etaxes_id': etaxes_id}, 'name') if existing: - # Логирование для отладки + # Debug logging frappe.logger().info(f"E-Taxes Purchase already exists: {existing}") return { @@ -2556,7 +2556,7 @@ def create_etaxes_purchase(etaxes_id, date, party, total): 'name': existing } - # Создаем новую запись + # Create new record etaxes_purchase = frappe.new_doc('E-Taxes Purchase') etaxes_purchase.etaxes_id = etaxes_id etaxes_purchase.date = date @@ -2564,9 +2564,9 @@ def create_etaxes_purchase(etaxes_id, date, party, total): etaxes_purchase.total = total etaxes_purchase.insert() - frappe.db.commit() # Явный commit + frappe.db.commit() # Explicit commit - # Логирование для отладки + # Debug logging frappe.logger().info(f"Created new E-Taxes Purchase: {etaxes_purchase.name}") return { @@ -2582,12 +2582,12 @@ def create_etaxes_purchase(etaxes_id, date, party, total): @frappe.whitelist() def link_purchase_order_to_etaxes(purchase_order, etaxes_purchase): - """Устанавливает связь между Purchase Order и E-Taxes Purchase""" + """Sets link between Purchase Order and E-Taxes Purchase""" try: - # Логирование для отладки + # Debug logging frappe.logger().info(f"Linking Purchase Order {purchase_order} to E-Taxes Purchase {etaxes_purchase}") - # Проверяем, существуют ли оба документа + # Check if both documents exist po_exists = frappe.db.exists('Purchase Order', purchase_order) etaxes_exists = frappe.db.exists('E-Taxes Purchase', etaxes_purchase) @@ -2598,18 +2598,18 @@ def link_purchase_order_to_etaxes(purchase_order, etaxes_purchase): 'message': 'One or both documents do not exist' } - # Получаем документ Purchase Order + # Get Purchase Order document po_doc = frappe.get_doc('Purchase Order', purchase_order) - # Устанавливаем связь с E-Taxes Purchase + # Set link to E-Taxes Purchase po_doc.is_taxes_doc = 1 po_doc.taxes_doc = etaxes_purchase - # Сохраняем изменения + # Save changes po_doc.save() - frappe.db.commit() # Явный commit + frappe.db.commit() # Explicit commit - # Логирование для отладки + # Debug logging frappe.logger().info(f"Successfully linked PO and E-Taxes Purchase") return { @@ -2623,12 +2623,12 @@ def link_purchase_order_to_etaxes(purchase_order, etaxes_purchase): } def on_delete_purchase_order(doc, method): - """Удаляет связанную запись E-Taxes Purchase при удалении Purchase Order""" + """Deletes related E-Taxes Purchase record when Purchase Order is deleted""" if doc.is_taxes_doc and doc.taxes_doc: try: frappe.logger().info(f"Deleting E-Taxes Purchase {doc.taxes_doc} due to Purchase Order {doc.name} deletion") - # Проверяем, существует ли запись E-Taxes Purchase + # Check if E-Taxes Purchase record exists if frappe.db.exists("E-Taxes Purchase", doc.taxes_doc): frappe.delete_doc('E-Taxes Purchase', doc.taxes_doc, ignore_permissions=True, force=True) frappe.db.commit() @@ -2636,3 +2636,4 @@ def on_delete_purchase_order(doc, method): 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 diff --git a/invoice_az/client/purchase_order.js b/invoice_az/client/purchase_order.js index 368aa84..239cf6f 100644 --- a/invoice_az/client/purchase_order.js +++ b/invoice_az/client/purchase_order.js @@ -1,4 +1,3 @@ - frappe.ui.form.on('Purchase Order', { refresh: function(frm) { // Проверяем, установлена ли галочка is_taxes_doc @@ -7,7 +6,7 @@ frappe.ui.form.on('Purchase Order', { frm.set_read_only(true); // Альтернативно, можно добавить визуальное предупреждение - frm.page.set_indicator(__('Налоговый документ'), 'blue'); + frm.page.set_indicator(__('Tax Document'), 'blue'); } }, @@ -42,7 +41,7 @@ frappe.listview_settings['Purchase Order'] = { // Добавляем кнопку в верхнее меню списка onload: function(listview) { // Добавляем кнопку для импорта из E-Taxes - listview.page.add_menu_item(__('Импорт из E-Taxes'), function() { + listview.page.add_menu_item(__('Import from E-Taxes'), function() { show_etaxes_import_dialog_for_list(); }); } @@ -58,7 +57,7 @@ var cancel_loading = false; function show_loading_dialog(message, show_progress = false) { if (!loading_dialog) { loading_dialog = new frappe.ui.Dialog({ - title: __('Загрузка'), + title: __('Loading'), fields: [ { fieldname: 'status_html', @@ -100,8 +99,8 @@ function show_loading_dialog(message, show_progress = false) { }
${__('Операция может занять некоторое время')}
+${__('The operation may take some time')}