From 07450e3ad401a5905901d5e4a39427f3ed673845 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Tue, 6 May 2025 17:27:43 +0400 Subject: [PATCH] first commit --- invoice_az/api.py | 1519 +++++++++++++++++ invoice_az/client/e_taxes_items_list.js | 124 ++ invoice_az/client/e_taxes_parties_list.js | 240 +++ invoice_az/hooks.py | 17 + invoice_az/invoice_az/doctype/__init__.py | 0 .../invoice_az/doctype/asan_login/__init__.py | 0 .../doctype/asan_login/asan_login.js | 533 ++++++ .../doctype/asan_login/asan_login.json | 139 ++ .../doctype/asan_login/asan_login.py | 29 + .../doctype/asan_login/test_asan_login.py | 30 + .../doctype/e_taxes_item/__init__.py | 0 .../doctype/e_taxes_item/e_taxes_item.js | 8 + .../doctype/e_taxes_item/e_taxes_item.json | 123 ++ .../doctype/e_taxes_item/e_taxes_item.py | 9 + .../e_taxes_item/test_e_taxes_items.py | 30 + .../doctype/e_taxes_item_mapping/__init__.py | 0 .../e_taxes_item_mapping.js | 8 + .../e_taxes_item_mapping.json | 52 + .../e_taxes_item_mapping.py | 9 + .../test_e_taxes_item_mapping.py | 30 + .../doctype/e_taxes_parties/__init__.py | 0 .../e_taxes_parties/e_taxes_parties.js | 8 + .../e_taxes_parties/e_taxes_parties.json | 134 ++ .../e_taxes_parties/e_taxes_parties.py | 9 + .../e_taxes_parties/test_e_taxes_parties.py | 30 + .../doctype/e_taxes_party_mapping/__init__.py | 0 .../e_taxes_party_mapping.js | 8 + .../e_taxes_party_mapping.json | 60 + .../e_taxes_party_mapping.py | 9 + .../test_e_taxes_party_mapping.py | 30 + .../doctype/e_taxes_settings/__init__.py | 0 .../e_taxes_settings/e_taxes_settings.js | 99 ++ .../e_taxes_settings/e_taxes_settings.json | 162 ++ .../e_taxes_settings/e_taxes_settings.py | 805 +++++++++ .../e_taxes_settings/test_e_taxes_settings.py | 30 + .../invoice_az/doctype/testapi/__init__.py | 0 .../doctype/testapi/test_testapi.py | 30 + .../invoice_az/doctype/testapi/testapi.js | 942 ++++++++++ .../invoice_az/doctype/testapi/testapi.json | 138 ++ .../invoice_az/doctype/testapi/testapi.py | 221 +++ invoice_az/public/js/purchase_order.js | 1471 ++++++++++++++++ 41 files changed, 7086 insertions(+) create mode 100644 invoice_az/api.py create mode 100644 invoice_az/client/e_taxes_items_list.js create mode 100644 invoice_az/client/e_taxes_parties_list.js create mode 100644 invoice_az/invoice_az/doctype/__init__.py create mode 100644 invoice_az/invoice_az/doctype/asan_login/__init__.py create mode 100644 invoice_az/invoice_az/doctype/asan_login/asan_login.js create mode 100644 invoice_az/invoice_az/doctype/asan_login/asan_login.json create mode 100644 invoice_az/invoice_az/doctype/asan_login/asan_login.py create mode 100644 invoice_az/invoice_az/doctype/asan_login/test_asan_login.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_item/__init__.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.js create mode 100644 invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.json create mode 100644 invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_item/test_e_taxes_items.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_item_mapping/__init__.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.js create mode 100644 invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.json create mode 100644 invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_item_mapping/test_e_taxes_item_mapping.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_parties/__init__.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.js create mode 100644 invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.json create mode 100644 invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_parties/test_e_taxes_parties.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_party_mapping/__init__.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.js create mode 100644 invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.json create mode 100644 invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_party_mapping/test_e_taxes_party_mapping.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_settings/__init__.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js create mode 100644 invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.json create mode 100644 invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.py create mode 100644 invoice_az/invoice_az/doctype/e_taxes_settings/test_e_taxes_settings.py create mode 100644 invoice_az/invoice_az/doctype/testapi/__init__.py create mode 100644 invoice_az/invoice_az/doctype/testapi/test_testapi.py create mode 100644 invoice_az/invoice_az/doctype/testapi/testapi.js create mode 100644 invoice_az/invoice_az/doctype/testapi/testapi.json create mode 100644 invoice_az/invoice_az/doctype/testapi/testapi.py create mode 100644 invoice_az/public/js/purchase_order.js diff --git a/invoice_az/api.py b/invoice_az/api.py new file mode 100644 index 0000000..f600feb --- /dev/null +++ b/invoice_az/api.py @@ -0,0 +1,1519 @@ +import frappe +import requests +import json +from frappe.utils import nowdate +from frappe.model.document import Document + + +@frappe.whitelist() +def get_auth_token(phone, user_id): + """Получение токена авторизации""" + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/start" + + payload = { + "phone": phone, + "userId": user_id + } + + headers = { + "Accept": "application/json, text/plain, */*", + "Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7", + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Referer": "https://new.e-taxes.gov.az/eportal/az/login/asan" + } + + try: + response = requests.post(url, data=json.dumps(payload), headers=headers) + response.raise_for_status() + + # Получаем токен из заголовка ответа + bearer_token = response.headers.get('x-authorization', '') + + # Получаем данные ответа + resp_data = response.json() + + return { + 'bearer_token': bearer_token, + 'response_data': resp_data + } + except Exception as e: + frappe.log_error(f"Error getting auth token: {str(e)}", "Asan Login") + frappe.throw(f"Error getting auth token: {str(e)}") + +@frappe.whitelist() +def check_auth_status(bearer_token): + """Проверка статуса авторизации""" + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/status" + + 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" + } + + # Добавляем токен в заголовки, если он есть + if bearer_token: + headers["x-authorization"] = f"Bearer {bearer_token}" + + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + + # Получаем данные о статусе + status_data = response.json() + + return status_data + except Exception as e: + frappe.log_error(f"Error checking auth status: {str(e)}", "Asan Login") + frappe.throw(f"Error checking auth status: {str(e)}") + +@frappe.whitelist() +def get_invoices(token, filters=None): + """Получение списка счетов-фактур с возможностью фильтрации""" + url = "https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/find.inbox" + + # Базовые параметры запроса + payload = { + "sortBy": "creationDate", + "sortAsc": True, + "statuses": ["approved", "onApproval", "updateApproval", "updateRequested", + "cancelRequested", "approvedBySystem", "onApprovalEdited", + "deactivated", "cancelationRefused", "correctionRefused"], + "types": ["current", "corrected"], + "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", + "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", + "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], + "serialNumber": None, + "senderTin": None, + "senderName": None, + "productName": None, + "productCode": None, + "receiverTin": None, + "receiverName": None, + "creationDateFrom": "01-01-2024 00:00", + "creationDateTo": "31-12-2024 23:59", + "amountFrom": None, + "amountTo": None, + "offset": 0, + "maxCount": 200, + "actionOwner": None + } + + # Применяем пользовательские фильтры, если они переданы + if filters: + try: + filters_dict = json.loads(filters) + for key, value in filters_dict.items(): + if key in payload and value: # Обновляем только существующие ключи и непустые значения + payload[key] = value + except Exception as e: + frappe.log_error(f"Error parsing filters: {str(e)}", "Asan Login") + + headers = { + "Accept": "application/json, text/plain, */*", + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "x-authorization": f"Bearer {token}" + } + + try: + response = requests.post(url, data=json.dumps(payload), headers=headers) + + # Проверяем статус ответа для обработки 401 ошибки + if response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + + response.raise_for_status() + + return response.json() + except requests.exceptions.HTTPError as e: + frappe.log_error(f"HTTP Error getting invoices: {str(e)}", "Asan Login") + + # Проверяем, является ли ошибка 401 Unauthorized + if e.response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + + frappe.throw(f"Error getting invoices: {str(e)}") + except Exception as e: + frappe.log_error(f"Error getting invoices: {str(e)}", "Asan Login") + frappe.throw(f"Error getting invoices: {str(e)}") + +@frappe.whitelist() +def get_certificates(bearer_token): + """Получение доступных сертификатов""" + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/certificates" + + 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/verification/asan", + "x-authorization": f"Bearer {bearer_token}" + } + + try: + response = requests.get(url, headers=headers) + + # Проверяем на 401 ошибку + if response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + + response.raise_for_status() + + return response.json() + except requests.exceptions.HTTPError as e: + frappe.log_error(f"HTTP Error getting certificates: {str(e)}", "Asan Login") + + # Проверяем, является ли ошибка 401 Unauthorized + if e.response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + + frappe.throw(f"Error getting certificates: {str(e)}") + except Exception as e: + frappe.log_error(f"Error getting certificates: {str(e)}", "Asan Login") + frappe.throw(f"Error getting certificates: {str(e)}") + +@frappe.whitelist() +def choose_taxpayer(bearer_token, owner_type, tin): + """Выбор налогоплательщика после выбора сертификата""" + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/chooseTaxpayer" + + payload = { + "ownerType": owner_type, # "legal" или "individual" + "legalTin": tin if owner_type == "legal" else None, + "individualFin": tin if owner_type == "individual" else None + } + + # Удаляем None значения из payload + payload = {k: v for k, v in payload.items() if v is not None} + + headers = { + "Accept": "application/json, text/plain, */*", + "Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7", + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Referer": "https://new.e-taxes.gov.az/eportal/az/login/asan", + "x-authorization": f"Bearer {bearer_token}" + } + + try: + response = requests.post(url, data=json.dumps(payload), headers=headers) + + # Проверяем на 401 ошибку + if response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + + response.raise_for_status() + + # Получаем основной токен из заголовка ответа + main_token = response.headers.get('x-authorization', '') + + # Получаем данные ответа + resp_data = response.json() + + return { + 'main_token': main_token, + 'response_data': resp_data + } + except requests.exceptions.HTTPError as e: + frappe.log_error(f"HTTP Error choosing taxpayer: {str(e)}", "Asan Login") + + # Проверяем, является ли ошибка 401 Unauthorized + if e.response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + + frappe.throw(f"Error choosing taxpayer: {str(e)}") + except Exception as e: + frappe.log_error(f"Error choosing taxpayer: {str(e)}", "Asan Login") + frappe.throw(f"Error choosing taxpayer: {str(e)}") + +@frappe.whitelist() +def get_invoice_details(token, invoice_id): + """Получение детальной информации о счете-фактуре""" + url = f"https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/{invoice_id}?sourceSystem=avis" + + headers = { + "Accept": "application/json, text/plain, */*", + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "x-authorization": f"Bearer {token}" + } + + try: + response = requests.get(url, headers=headers) + + # Проверяем на 401 ошибку + if response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + + response.raise_for_status() + + return response.json() + except requests.exceptions.HTTPError as e: + frappe.log_error(f"HTTP Error getting invoice details: {str(e)}", "Asan Login") + + # Проверяем, является ли ошибка 401 Unauthorized + if e.response.status_code == 401: + return { + "error": "unauthorized", + "message": "Authentication required. Please login again." + } + + frappe.throw(f"Error getting invoice details: {str(e)}") + except Exception as e: + frappe.log_error(f"Error getting invoice details: {str(e)}", "Asan Login") + frappe.throw(f"Error getting invoice details: {str(e)}") + +@frappe.whitelist() +def get_default_asan_login(): + """Получить настройки авторизации по умолчанию""" + try: + # Ищем настройку с флагом is_default = 1 + default_login = frappe.get_list( + "Asan Login", + filters={"is_default": 1}, + fields=["name"] + ) + + if default_login: + # Если найдена настройка по умолчанию, возвращаем её данные + asan_login = frappe.get_doc("Asan Login", default_login[0].name) + return { + "found": True, + "name": asan_login.name, + "phone": asan_login.phone, + "user_id": asan_login.user_id, + "auth_status": asan_login.auth_status, + "bearer_token": asan_login.bearer_token, + "main_token": asan_login.main_token, + "selected_certificate": asan_login.selected_certificate + } + else: + # Если не найдена настройка по умолчанию, ищем любую первую настройку + any_login = frappe.get_list( + "Asan Login", + fields=["name"] + ) + + if any_login: + asan_login = frappe.get_doc("Asan Login", any_login[0].name) + return { + "found": True, + "name": asan_login.name, + "phone": asan_login.phone, + "user_id": asan_login.user_id, + "auth_status": asan_login.auth_status, + "bearer_token": asan_login.bearer_token, + "main_token": asan_login.main_token, + "selected_certificate": asan_login.selected_certificate + } + else: + return {"found": False, "message": "No Asan Login settings found."} + + except Exception as e: + frappe.log_error(f"Error getting default Asan Login: {str(e)}", "Asan Login") + return {"found": False, "error": str(e)} + +@frappe.whitelist() +def handle_authentication(asan_login_name=None): + """Обрабатывает процесс аутентификации для указанного или настройки по умолчанию""" + try: + # Если имя настройки не указано, получаем имя настройки по умолчанию + 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 + asan_login = frappe.get_doc("Asan Login", asan_login_name) + + # Проверяем, что есть номер телефона и ID пользователя + if not asan_login.phone or not asan_login.user_id: + return {"success": False, "message": "Phone and User ID are required."} + + # Запускаем процесс авторизации + auth_result = get_auth_token(asan_login.phone, asan_login.user_id) + + if auth_result and auth_result.get("bearer_token"): + # Сохраняем токен и меняем статус + asan_login.bearer_token = auth_result.get("bearer_token") + asan_login.auth_status = "Waiting for confirmation" + asan_login.save() + + return { + "success": True, + "message": "Authentication initiated. Please confirm on your phone.", + "bearer_token": auth_result.get("bearer_token"), + "asan_login_name": asan_login_name + } + else: + return {"success": False, "message": "Failed to get authentication token."} + + except Exception as e: + frappe.log_error(f"Error in handle_authentication: {str(e)}", "Asan Login") + return {"success": False, "message": f"Error: {str(e)}"} + +@frappe.whitelist() +def poll_auth_status(asan_login_name, bearer_token): + """Проверяет статус аутентификации""" + try: + # Проверяем статус + status_result = check_auth_status(bearer_token) + + # Получаем документ Asan Login + asan_login = frappe.get_doc("Asan Login", asan_login_name) + + if status_result and status_result.get("successful") == True: + # Обновляем статус и токен + asan_login.auth_status = "Authenticated" + if status_result.get("bearer_token"): + asan_login.bearer_token = status_result.get("bearer_token") + asan_login.save() + + return { + "success": True, + "authenticated": True, + "message": "Authentication successful." + } + else: + return { + "success": True, + "authenticated": False, + "message": "Waiting for confirmation." + } + + except Exception as e: + frappe.log_error(f"Error in poll_auth_status: {str(e)}", "Asan Login") + return {"success": False, "authenticated": False, "message": f"Error: {str(e)}"} + +@frappe.whitelist() +def get_auth_certificates(asan_login_name): + """Получает сертификаты для указанной настройки Asan Login""" + try: + # Получаем документ Asan Login + asan_login = frappe.get_doc("Asan Login", asan_login_name) + + # Проверяем статус авторизации + if asan_login.auth_status != "Authenticated" and asan_login.auth_status != "Fully Authenticated": + return {"success": False, "message": "Not authenticated. Please login first."} + + # Получаем сертификаты + certificates_result = get_certificates(asan_login.bearer_token) + + # Проверяем на ошибку авторизации + if certificates_result.get("error") == "unauthorized": + return { + "success": False, + "unauthorized": True, + "message": "Authentication required. Please login again." + } + + # Проверяем наличие сертификатов в ответе + if certificates_result and certificates_result.get("certificates"): + # Сохраняем сертификаты в документе + asan_login.certificates_json = json.dumps(certificates_result.get("certificates")) + asan_login.save() + + return { + "success": True, + "message": "Certificates retrieved successfully.", + "certificates": certificates_result.get("certificates") + } + else: + return {"success": False, "message": "No certificates found."} + + except Exception as e: + frappe.log_error(f"Error in get_auth_certificates: {str(e)}", "Asan Login") + return {"success": False, "message": f"Error: {str(e)}"} + +@frappe.whitelist() +def select_certificate(asan_login_name, certificate_data, certificate_name): + """Выбирает сертификат для указанной настройки Asan Login""" + try: + # Получаем документ Asan Login + asan_login = frappe.get_doc("Asan Login", asan_login_name) + + # Проверяем статус авторизации + if asan_login.auth_status != "Authenticated" and asan_login.auth_status != "Fully Authenticated": + return {"success": False, "message": "Not authenticated. Please login first."} + + # Преобразуем строку JSON в объект, если необходимо + if isinstance(certificate_data, str): + certificate_data = json.loads(certificate_data) + + # Сохраняем выбранный сертификат + asan_login.selected_certificate = certificate_name + asan_login.selected_certificate_json = json.dumps(certificate_data) + asan_login.save() + + return { + "success": True, + "message": "Certificate selected successfully." + } + + except Exception as e: + frappe.log_error(f"Error in select_certificate: {str(e)}", "Asan Login") + return {"success": False, "message": f"Error: {str(e)}"} + +@frappe.whitelist() +def select_taxpayer(asan_login_name): + """Выбирает налогоплательщика для указанной настройки Asan Login""" + try: + # Получаем документ Asan Login + asan_login = frappe.get_doc("Asan Login", asan_login_name) + + # Проверяем статус авторизации + if asan_login.auth_status != "Authenticated" and asan_login.auth_status != "Fully Authenticated": + return {"success": False, "message": "Not authenticated. Please login first."} + + # Проверяем наличие выбранного сертификата + if not asan_login.selected_certificate_json: + return {"success": False, "message": "No certificate selected."} + + # Парсим данные сертификата + cert_data = json.loads(asan_login.selected_certificate_json) + + # Определяем тип и идентификатор налогоплательщика + owner_type = cert_data.get("taxpayerType", "") + tin = "" + + if owner_type == "individual" and cert_data.get("individualInfo"): + tin = cert_data.get("individualInfo", {}).get("fin", "") + owner_type = "individual" + elif cert_data.get("legalInfo"): + owner_type = "legal" + tin = cert_data.get("legalInfo", {}).get("tin", "") or cert_data.get("legalInfo", {}).get("voen", "") + + if not owner_type or not tin: + return {"success": False, "message": "Invalid certificate data. Cannot determine taxpayer type or ID."} + + # Выбираем налогоплательщика + taxpayer_result = choose_taxpayer(asan_login.bearer_token, owner_type, tin) + + # Проверяем на ошибку авторизации + if taxpayer_result.get("error") == "unauthorized": + return { + "success": False, + "unauthorized": True, + "message": "Authentication required. Please login again." + } + + # Проверяем успешность выбора + if taxpayer_result and taxpayer_result.get("main_token"): + # Сохраняем основной токен и ответ + 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() + + return { + "success": True, + "message": "Taxpayer selected successfully." + } + else: + return {"success": False, "message": "Failed to select taxpayer."} + + except Exception as e: + frappe.log_error(f"Error in select_taxpayer: {str(e)}", "Asan Login") + return {"success": False, "message": f"Error: {str(e)}"} + +@frappe.whitelist() +def import_invoice_to_purchase_order(invoice_data, purchase_order=None): + """Импортирует данные счета-фактуры в Purchase Order""" + try: + # Проверяем и преобразуем данные счета + if isinstance(invoice_data, str): + invoice_data = json.loads(invoice_data) + + # Определяем, создавать новый Purchase Order или обновить существующий + if purchase_order: + # Получаем существующий документ + po_doc = frappe.get_doc("Purchase Order", purchase_order) + else: + # Создаем новый документ + po_doc = frappe.new_doc("Purchase Order") + + # Устанавливаем основные поля для нового документа + 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 или создаем нового + 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() + + # Добавляем/обновляем дополнительные поля из счета-фактуры + po_doc.title = f"Invoice {invoice_data.get('serialNumber', '')}" + po_doc.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "") + + # Очищаем существующие элементы, если нужно обновить их полностью + if purchase_order and invoice_data.get("items"): + po_doc.items = [] + + # Добавляем элементы из счета-фактуры + 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) + + # Сохраняем документ + po_doc.save() + + return { + "success": True, + "message": "Invoice data imported successfully.", + "purchase_order": po_doc.name + } + + except Exception as e: + frappe.log_error(f"Error in import_invoice_to_purchase_order: {str(e)}", "Asan Login") + return {"success": False, "message": f"Error: {str(e)}"} + +def find_or_create_item(item_name, item_code=None, unit=None): + """Находит или создает товар по названию и коду""" + try: + # Проверяем настройки для автоматического создания + try: + settings = frappe.get_doc("ETaxes Settings") + auto_create = settings.auto_create_items + except: + auto_create = True # По умолчанию создаем новые товары + + # Ищем товар по коду, если он указан + if item_code: + items = frappe.get_list( + "Item", + filters={"item_code": item_code}, + fields=["name"] + ) + + if items: + return items[0].name + + # Ищем товар по названию + items = frappe.get_list( + "Item", + filters={"item_name": item_name}, + fields=["name"] + ) + + if items: + return items[0].name + + # Если не найден и разрешено автоматическое создание, создаем новый товар + if auto_create: + # Получаем настройки для создания товаров + try: + item_group = settings.default_item_group + default_uom = settings.default_uom + is_stock_item = settings.is_stock_item + is_purchase_item = settings.is_purchase_item + except: + item_group = "All Item Groups" + default_uom = "Nos" + is_stock_item = 1 + is_purchase_item = 1 + + # Создаем новый товар + item_doc = frappe.new_doc("Item") + item_doc.item_code = item_code or item_name + item_doc.item_name = item_name + item_doc.description = item_name + item_doc.item_group = item_group + item_doc.stock_uom = unit or default_uom + item_doc.is_stock_item = is_stock_item + item_doc.is_purchase_item = is_purchase_item + item_doc.save() + + return item_doc.name + + return None + + except Exception as e: + frappe.log_error(f"Error in find_or_create_item: {str(e)}", "E-Taxes Integration") + return None + +def find_or_create_supplier(supplier_name, supplier_tin): + """Находит или создает поставщика по имени и TIN""" + try: + # Проверяем настройки для автоматического создания + try: + settings = frappe.get_doc("ETaxes Settings") + auto_create = settings.auto_create_suppliers + except: + auto_create = True # По умолчанию создаем новых поставщиков + + # Ищем поставщика по TIN + suppliers = frappe.get_list( + "Supplier", + filters={"tax_id": supplier_tin}, + fields=["name"] + ) + + if suppliers: + return suppliers[0].name + + # Ищем поставщика по названию + suppliers = frappe.get_list( + "Supplier", + filters={"supplier_name": supplier_name}, + fields=["name"] + ) + + if suppliers: + return suppliers[0].name + + # Если не найден и разрешено автоматическое создание, создаем нового поставщика + if auto_create: + # Получаем настройки для создания поставщиков + try: + supplier_group = settings.default_supplier_group + supplier_type = settings.supplier_type + country = settings.country + except: + supplier_group = "All Supplier Groups" + supplier_type = "Company" + country = "Azerbaijan" + + # Создаем нового поставщика + supplier_doc = frappe.new_doc("Supplier") + supplier_doc.supplier_name = supplier_name + supplier_doc.supplier_type = supplier_type + supplier_doc.tax_id = supplier_tin + supplier_doc.supplier_group = supplier_group + + if country: + supplier_doc.country = country + + supplier_doc.save() + + return supplier_doc.name + + return None + + except Exception as e: + frappe.log_error(f"Error in find_or_create_supplier: {str(e)}", "E-Taxes Integration") + return None + +def get_uom_for_unit(unit_name): + """Преобразует название единицы измерения из счета-фактуры в UOM системы""" + # Словарь для преобразования названий + uom_mapping = { + "шт": "Nos", + "кг": "Kg", + "г": "Gram", + "л": "Litre", + "м": "Meter", + "м2": "Square Meter", + "м3": "Cubic Meter" + } + + # Возвращаем соответствующий UOM или "Nos" по умолчанию + return uom_mapping.get(unit_name.lower(), "Nos") + +@frappe.whitelist() +def find_or_create_item_with_mapping(item_name, item_code=None, unit=None): + """Находит или создает товар с учетом настроек сопоставления""" + try: + # Сначала проверяем наличие сопоставления в настройках + try: + settings = frappe.get_doc("ETaxes Settings") + + # Ищем сопоставление по имени товара + for mapping in settings.item_mappings: + if mapping.etaxes_item_name == item_name: + # Если найдено сопоставление, используем указанный товар + return mapping.item_code + except: + pass # Если настроек нет или возникла ошибка, продолжаем стандартный поиск + + # Если сопоставление не найдено, используем стандартный поиск + return find_or_create_item(item_name, item_code, unit) + except Exception as e: + frappe.log_error(f"Error in find_or_create_item_with_mapping: {str(e)}", "E-Taxes Integration") + return None + +@frappe.whitelist() +def find_or_create_supplier_with_mapping(supplier_name, supplier_tin): + """Находит или создает поставщика с учетом настроек сопоставления""" + try: + # Сначала проверяем наличие сопоставления в настройках + try: + settings = frappe.get_doc("ETaxes Settings") + + # Ищем сопоставление по ИНН поставщика + for mapping in settings.supplier_mappings: + if mapping.etaxes_supplier_tin == supplier_tin: + # Если найдено сопоставление, используем указанного поставщика + return mapping.supplier + except: + pass # Если настроек нет или возникла ошибка, продолжаем стандартный поиск + + # Если сопоставление не найдено, используем стандартный поиск + return find_or_create_supplier(supplier_name, supplier_tin) + except Exception as e: + frappe.log_error(f"Error in find_or_create_supplier_with_mapping: {str(e)}", "E-Taxes Integration") + return None + + +@frappe.whitelist() +def import_invoice_to_purchase_order_with_mapping(invoice_data, purchase_order=None): + """Импортирует данные счета-фактуры в Purchase Order с учетом настроек сопоставления""" + try: + # Проверяем и преобразуем данные счета + if isinstance(invoice_data, str): + invoice_data = json.loads(invoice_data) + + # Определяем, создавать новый Purchase Order или обновить существующий + if purchase_order: + # Получаем существующий документ + po_doc = frappe.get_doc("Purchase Order", purchase_order) + else: + # Создаем новый документ + po_doc = frappe.new_doc("Purchase Order") + + # Устанавливаем основные поля для нового документа + 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") + + # Ищем поставщика с учетом сопоставлений + 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() + + # Добавляем/обновляем дополнительные поля из счета-фактуры + po_doc.title = f"Invoice {invoice_data.get('serialNumber', '')}" + po_doc.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "") + + # Очищаем существующие элементы, если нужно обновить их полностью + if purchase_order and invoice_data.get("items"): + po_doc.items = [] + + # Добавляем элементы из счета-фактуры + if invoice_data.get("items"): + for item in invoice_data.get("items", []): + # Используем функцию с учетом сопоставлений + 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) + + # Сохраняем документ + po_doc.save() + + return { + "success": True, + "message": "Invoice data imported successfully.", + "purchase_order": po_doc.name + } + + except Exception as e: + frappe.log_error(f"Error in import_invoice_to_purchase_order_with_mapping: {str(e)}", "E-Taxes Integration") + return {"success": False, "message": f"Error: {str(e)}"} + +@frappe.whitelist() +def load_items_from_invoices(date_from, date_to): + """Loading items from invoices for a period""" + try: + # Get default settings + asan_login_settings = get_default_asan_login() + + if not asan_login_settings.get('found'): + return { + 'success': False, + 'message': 'No Asan Login settings found' + } + + # Format filters in the exact same order as in the original + filters = { + "actionOwner": None, + "amountFrom": None, + "amountTo": None, + "creationDateFrom": date_from, + "creationDateTo": date_to, + "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], + "maxCount": 50, + "offset": 0, + "productCode": None, + "productName": None, + "receiverName": None, + "receiverTin": None, + "senderName": None, + "senderTin": None, + "serialNumber": None, + "sortAsc": True, + "sortBy": "creationDate", + "statuses": ["approved", "onApproval", "updateApproval", "updateRequested", "cancelRequested", "approvedBySystem", "onApprovalEdited", "deactivated", "cancelationRefused", "correctionRefused"], + "types": ["current", "corrected"] + } + + response = get_invoices(asan_login_settings['main_token'], json.dumps(filters)) + + if 'error' in response: + return { + 'success': False, + 'message': response['error'] + } + + # Extract unique items + unique_items = {} + items_data = response.get('data', []) or response.get('invoices', []) + + # Get details for each invoice + processed_count = 0 + for invoice in items_data: + invoice_id = invoice.get('id', '') + if invoice_id: + # Get detailed information about the invoice + invoice_details = get_invoice_details(asan_login_settings['main_token'], invoice_id) + + # Check for errors + if isinstance(invoice_details, dict) and 'error' in invoice_details: + continue + + serial_number = invoice_details.get('serialNumber', '') + items = invoice_details.get('items', []) + + frappe.logger().debug(f"Processing invoice {serial_number} with {len(items)} items") + + for item in items: + item_name = item.get('productName', '') + item_code = item.get('itemId', '') + + # Create a unique key + item_key = f"{item_name}|{item_code}" + + if item_key not in unique_items: + unique_items[item_key] = { + 'etaxes_item_name': item_name, + 'etaxes_item_code': item_code, + 'etaxes_unit': item.get('unit', ''), + 'etaxes_price': item.get('pricePerUnit', 0), + 'source_invoice': serial_number, + 'status': 'New' + } + + processed_count += 1 + + # Check which items already exist + existing_items = frappe.get_all('E-Taxes Item', + fields=['etaxes_item_name', 'etaxes_item_code']) + + existing_keys = [f"{item['etaxes_item_name']}|{item['etaxes_item_code']}" + for item in existing_items] + + # Add only new items + created_count = 0 + for key, item_data in unique_items.items(): + if key not in existing_keys: + # Create document name based on item name + product_name = item_data['etaxes_item_name'] + # Limit length to 140 characters (accounting for possible suffixes) + safe_name = product_name[:140] + # Remove invalid characters + safe_name = safe_name.replace('/', '-').replace(':', '-').replace('"', '').replace("'", '') + + doc = frappe.new_doc('E-Taxes Item') + doc.etaxes_item_name = item_data['etaxes_item_name'] + doc.etaxes_item_code = item_data['etaxes_item_code'] + doc.etaxes_unit = item_data['etaxes_unit'] + doc.etaxes_price = item_data['etaxes_price'] + doc.source_invoice = item_data['source_invoice'] + doc.status = item_data['status'] + + # Set document name + doc.name = safe_name + + # Try to save with desired name + try: + doc.insert() + created_count += 1 + except frappe.DuplicateEntryError: + # If name already exists, allow system to auto-assign + # a unique name with suffix + doc.name = None + doc.insert() + created_count += 1 + + return { + 'success': True, + 'created_count': created_count, + 'total_unique': len(unique_items), + 'invoices_processed': processed_count + } + + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error loading items from invoices') + return { + 'success': False, + 'message': str(e) + } + +@frappe.whitelist() +def load_parties_from_invoices(date_from, date_to): + """Загрузка контрагентов из инвойсов за период""" + try: + # Получаем настройки по умолчанию + asan_login_settings = get_default_asan_login() + + if not asan_login_settings.get('found'): + return { + 'success': False, + 'message': 'No Asan Login settings found' + } + + # Получаем инвойсы + filters = { + "sortBy": "creationDate", + "sortAsc": True, + "creationDateFrom": date_from, + "creationDateTo": date_to, + "maxCount": 1000 + } + + response = get_invoices(asan_login_settings['main_token'], json.dumps(filters)) + + if 'error' in response: + return { + 'success': False, + 'message': response['error'] + } + + # Извлекаем уникальных контрагентов + unique_parties = {} + items_data = response.get('data', []) or response.get('invoices', []) + + for invoice in items_data: + serial_number = invoice.get('serialNumber', '') + + # Отправитель + sender = invoice.get('sender', {}) + sender_key = f"sender|{sender.get('name', '')}|{sender.get('tin', '')}" + + if sender_key not in unique_parties: + unique_parties[sender_key] = { + 'etaxes_party_name': sender.get('name', ''), + 'etaxes_tax_id': sender.get('tin', ''), + 'etaxes_party_type': 'Sender', + 'etaxes_address': sender.get('address', ''), + 'source_invoice': serial_number + } + + # Получатель + receiver = invoice.get('receiver', {}) + receiver_key = f"receiver|{receiver.get('name', '')}|{receiver.get('tin', '')}" + + if receiver_key not in unique_parties: + unique_parties[receiver_key] = { + 'etaxes_party_name': receiver.get('name', ''), + 'etaxes_tax_id': receiver.get('tin', ''), + 'etaxes_party_type': 'Receiver', + 'etaxes_address': receiver.get('address', ''), + 'source_invoice': serial_number + } + + # Проверяем, какие контрагенты уже существуют + existing_parties = frappe.get_all('E-Taxes Party', + fields=['etaxes_party_name', 'etaxes_tax_id', 'etaxes_party_type']) + + existing_keys = [f"{party['etaxes_party_type']}|{party['etaxes_party_name']}|{party['etaxes_tax_id']}" + for party in existing_parties] + + # Добавляем только новых контрагентов + created_count = 0 + for key, party_data in unique_parties.items(): + if key not in existing_keys: + doc = frappe.get_doc({ + 'doctype': 'E-Taxes Party', + **party_data + }) + doc.insert() + created_count += 1 + + return { + 'success': True, + 'created_count': created_count, + 'total_unique': len(unique_parties) + } + + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error loading parties from invoices') + return { + 'success': False, + 'message': str(e) + } + +@frappe.whitelist() +def get_default_settings(): + """Получение активных настроек E-Taxes""" + try: + settings_list = frappe.get_all('E-Taxes Settings', + filters={'is_active': 1}, + limit=1) + + if settings_list: + settings = frappe.get_doc('E-Taxes Settings', settings_list[0].name) + return { + 'found': True, + 'settings': settings.as_dict() + } + else: + return { + 'found': False, + 'message': 'No active E-Taxes settings found' + } + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error getting E-Taxes settings') + return { + 'found': False, + 'message': str(e) + } + +def normalize_azeri_text(text): + """Нормализация азербайджанского текста для сопоставления""" + if not text: + return text + + # Словарь замен + replacements = { + 'ə': 'e', + 'Ə': 'E', + 'ü': 'u', + 'Ü': 'U', + 'ö': 'o', + 'Ö': 'O', + 'ğ': 'g', + 'Ğ': 'G', + 'ı': 'i', + 'I': 'I', + 'ç': 'c', + 'Ç': 'C', + 'ş': 's', + 'Ş': 'S' + } + + # Также добавим вариант ə->a и ş->sh + replacements_alt = replacements.copy() + replacements_alt.update({ + 'ə': 'a', + 'Ə': 'A', + 'ş': 'sh', + 'Ş': 'SH' + }) + + # Создаем нормализованные версии + text_normalized = text + for azeri_char, latin_char in replacements.items(): + text_normalized = text_normalized.replace(azeri_char, latin_char) + + text_normalized_alt = text + for azeri_char, latin_char in replacements_alt.items(): + text_normalized_alt = text_normalized_alt.replace(azeri_char, latin_char) + + return text_normalized, text_normalized_alt + +def calculate_similarity(str1, str2, consider_azeri=True): + """Вычисление сходства между строками""" + if not str1 or not str2: + return 0 + + from difflib import SequenceMatcher + + if consider_azeri: + str1_norm, str1_alt = normalize_azeri_text(str1.lower()) + str2_norm, str2_alt = normalize_azeri_text(str2.lower()) + + # Проверяем все комбинации + similarities = [ + SequenceMatcher(None, str1_norm, str2_norm).ratio(), + SequenceMatcher(None, str1_norm, str2_alt).ratio(), + SequenceMatcher(None, str1_alt, str2_norm).ratio(), + SequenceMatcher(None, str1_alt, str2_alt).ratio() + ] + + return max(similarities) * 100 + else: + return SequenceMatcher(None, str1.lower(), str2.lower()).ratio() * 100 + +@frappe.whitelist() +def match_similar_items(): + """Автоматическое сопоставление товаров по похожему имени""" + try: + # Получаем настройки + settings_result = get_default_settings() + if not settings_result.get('found'): + return { + 'success': False, + 'message': 'No active settings found' + } + + settings = settings_result['settings'] + threshold = settings.get('similarity_threshold', 80) + consider_azeri = settings.get('consider_azeri_chars', True) + + # Получаем все товары E-Taxes + etaxes_items = frappe.get_all('E-Taxes Item', + filters={'status': 'New'}, + fields=['name', 'etaxes_item_name', 'etaxes_item_code']) + + # Получаем все товары ERP + erp_items = frappe.get_all('Item', fields=['item_code', 'item_name']) + + # Существующие сопоставления + existing_mappings = {} + for mapping in settings.get('item_mappings', []): + key = f"{mapping.etaxes_item_name}|{mapping.etaxes_item_code}" + existing_mappings[key] = mapping.erp_item + + # Результаты сопоставления + matched_count = 0 + new_mappings = [] + + for etaxes_item in etaxes_items: + key = f"{etaxes_item['etaxes_item_name']}|{etaxes_item['etaxes_item_code']}" + + # Пропускаем, если уже есть сопоставление + if key in existing_mappings: + continue + + best_match = None + best_similarity = 0 + + # Ищем наиболее похожий товар + for erp_item in erp_items: + similarity = calculate_similarity(etaxes_item['etaxes_item_name'], + erp_item['item_name'], + consider_azeri) + + if similarity > best_similarity and similarity >= threshold: + best_similarity = similarity + best_match = erp_item['item_code'] + + # Если нашли хорошее совпадение - создаем сопоставление + if best_match: + new_mappings.append({ + 'etaxes_item_name': etaxes_item['etaxes_item_name'], + 'etaxes_item_code': etaxes_item['etaxes_item_code'], + 'erp_item': best_match, + 'mapping_type': 'Automatic' + }) + + # Обновляем статус товара E-Taxes + doc = frappe.get_doc('E-Taxes Item', etaxes_item['name']) + doc.status = 'Mapped' + doc.mapped_item = best_match + doc.save() + + matched_count += 1 + + # Обновляем настройки с новыми сопоставлениями + if new_mappings: + settings_doc = frappe.get_doc('E-Taxes Settings', settings['name']) + existing_mappings_list = list(settings_doc.item_mappings) if settings_doc.item_mappings else [] + + for mapping in new_mappings: + existing_mappings_list.append(mapping) + + settings_doc.item_mappings = existing_mappings_list + settings_doc.save() + + return { + 'success': True, + 'matched_count': matched_count, + 'total_processed': len(etaxes_items) + } + + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error matching similar items') + return { + 'success': False, + 'message': str(e) + } + +@frappe.whitelist() +def match_similar_parties(): + """Автоматическое сопоставление контрагентов по похожему имени""" + try: + # Получаем настройки + settings_result = get_default_settings() + if not settings_result.get('found'): + return { + 'success': False, + 'message': 'No active settings found' + } + + settings = settings_result['settings'] + threshold = settings.get('similarity_threshold', 80) + consider_azeri = settings.get('consider_azeri_chars', True) + + # Получаем контрагентов E-Taxes + etaxes_parties = frappe.get_all('E-Taxes Party', + filters={'status': 'New'}, + fields=['name', 'etaxes_party_name', 'etaxes_tax_id', 'etaxes_party_type']) + + # Получаем контрагентов из ERP + suppliers = frappe.get_all('Supplier', fields=['name', 'supplier_name', 'tax_id']) + customers = frappe.get_all('Customer', fields=['name', 'customer_name', 'tax_id']) + + # Существующие сопоставления + existing_mappings = {} + for mapping in settings.get('party_mappings', []): + key = f"{mapping.etaxes_party_name}|{mapping.etaxes_tax_id}" + existing_mappings[key] = (mapping.erp_party, mapping.party_type) + + # Результаты сопоставления + matched_count = 0 + new_mappings = [] + + for etaxes_party in etaxes_parties: + key = f"{etaxes_party['etaxes_party_name']}|{etaxes_party['etaxes_tax_id']}" + + # Пропускаем, если уже есть сопоставление + if key in existing_mappings: + continue + + best_match = None + best_similarity = 0 + match_type = None + + # Проверяем поставщиков, если это отправитель (поставщик для нас) + if etaxes_party['etaxes_party_type'] == 'Sender': + for supplier in suppliers: + similarity = calculate_similarity(etaxes_party['etaxes_party_name'], + supplier['supplier_name'], + consider_azeri) + + if similarity > best_similarity and similarity >= threshold: + best_similarity = similarity + best_match = supplier['name'] + match_type = 'Supplier' + + # Проверяем клиентов, если это получатель (клиент для нас) + if etaxes_party['etaxes_party_type'] == 'Receiver': + for customer in customers: + similarity = calculate_similarity(etaxes_party['etaxes_party_name'], + customer['customer_name'], + consider_azeri) + + if similarity > best_similarity and similarity >= threshold: + best_similarity = similarity + best_match = customer['name'] + match_type = 'Customer' + + # Если нашли хорошее совпадение - создаем сопоставление + if best_match: + new_mappings.append({ + 'etaxes_party_name': etaxes_party['etaxes_party_name'], + 'etaxes_tax_id': etaxes_party['etaxes_tax_id'], + 'erp_party': best_match, + 'party_type': match_type, + 'mapping_type': 'Automatic' + }) + + # Обновляем статус контрагента E-Taxes + doc = frappe.get_doc('E-Taxes Party', etaxes_party['name']) + doc.status = 'Mapped' + doc.mapped_party = best_match + doc.erp_party_type = match_type + doc.save() + + matched_count += 1 + + # Обновляем настройки с новыми сопоставлениями + if new_mappings: + settings_doc = frappe.get_doc('E-Taxes Settings', settings['name']) + existing_mappings_list = list(settings_doc.party_mappings) if settings_doc.party_mappings else [] + + for mapping in new_mappings: + existing_mappings_list.append(mapping) + + settings_doc.party_mappings = existing_mappings_list + settings_doc.save() + + return { + 'success': True, + 'matched_count': matched_count, + 'total_processed': len(etaxes_parties) + } + + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error matching similar parties') + return { + 'success': False, + 'message': str(e) + } + +@frappe.whitelist() +def import_invoice_with_mapping(invoice_data, purchase_order_name=None): + """Импорт инвойса с учетом сопоставлений""" + try: + # Десериализация если необходимо + if isinstance(invoice_data, str): + invoice_data = json.loads(invoice_data) + + # Получаем настройки + settings_result = get_default_settings() + if not settings_result.get('found'): + return { + 'success': False, + 'message': 'No active settings found' + } + + settings = settings_result['settings'] + + # Создаем словари сопоставлений + item_mappings = {} + for mapping in settings.get('item_mappings', []): + key = f"{mapping.etaxes_item_name}|{mapping.etaxes_item_code}" + item_mappings[key] = mapping.erp_item + + party_mappings = {} + for mapping in settings.get('party_mappings', []): + key = f"{mapping.etaxes_party_name}|{mapping.etaxes_tax_id}" + party_mappings[key] = (mapping.erp_party, mapping.party_type) + + # Получаем или создаем Purchase Order + if purchase_order_name: + po = frappe.get_doc('Purchase Order', purchase_order_name) + else: + # Создаем новый Purchase Order + po = frappe.new_doc('Purchase Order') + + # Устанавливаем поставщика + 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: + # Если нет сопоставления, создаем ошибку + return { + 'success': False, + 'message': f'No mapping found for supplier: {sender.get("name", "Unknown")}' + } + + po.transaction_date = frappe.utils.nowdate() + po.company = frappe.defaults.get_user_default('Company') + + # Обрабатываем позиции инвойса + items_to_add = [] + unmatched_items = [] + + for invoice_item in invoice_data.get('items', []): + item_name = invoice_item.get('productName', '') + item_code = invoice_item.get('itemId', '') + item_key = f"{item_name}|{item_code}" + + # Проверяем сопоставление + if item_key in item_mappings: + mapped_item = item_mappings[item_key] + else: + # Пытаемся найти Item по коду или имени + item_filter = {'item_code': item_code} if item_code else {'item_name': item_name} + item_list = frappe.get_all('Item', filters=item_filter, limit=1) + + if item_list: + mapped_item = item_list[0].name + else: + unmatched_items.append({ + 'name': item_name, + 'code': item_code + }) + continue + + # Добавляем позицию в PO + items_to_add.append({ + 'item_code': mapped_item, + 'qty': invoice_item.get('quantity', 1), + 'rate': invoice_item.get('pricePerUnit', 0), + 'uom': 'Unit' # По умолчанию, можно улучшить + }) + + # Если есть несопоставленные позиции - возвращаем ошибку + if unmatched_items: + return { + 'success': False, + 'unmatched_items': unmatched_items, + 'message': 'Some items are not mapped' + } + + # Удаляем текущие позиции (если редактируем существующий PO) + if purchase_order_name: + po.items = [] + + # Добавляем новые позиции + for item_data in items_to_add: + po.append('items', item_data) + + # Сохраняем Purchase Order + po.save() + + return { + 'success': True, + 'purchase_order': po.name, + 'invoice_id': invoice_data.get('id', ''), + 'invoice_serial': invoice_data.get('serialNumber', '') + } + + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error importing invoice with mapping') + return { + 'success': False, + 'message': str(e) + } \ 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 new file mode 100644 index 0000000..dff27f2 --- /dev/null +++ b/invoice_az/client/e_taxes_items_list.js @@ -0,0 +1,124 @@ +frappe.listview_settings['E-Taxes Item'] = { + add_fields: ['status', 'mapped_item'], + + get_indicator: function(doc) { + if (doc.status === 'Mapped') { + return [__('Mapped'), 'green', 'status,=,Mapped']; + } else if (doc.status === 'Processing') { + return [__('Processing'), 'orange', 'status,=,Processing']; + } else { + return [__('New'), 'red', 'status,=,New']; + } + }, + + onload: function(listview) { + // Add button to load items from E-Taxes + listview.page.add_menu_item(__('Load from E-Taxes'), function() { + show_invoice_filter_dialog(); + }); + } +}; + +// 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 prevYear = moment().subtract(1, 'year').year(); + const startDate = prevYear + "-01-01"; // January 1st of previous year + const endDate = moment().format('YYYY-MM-DD'); // Today + + // Create simple dialog for selecting period + var d = new frappe.ui.Dialog({ + title: __('Invoice Filters'), + fields: [ + { + fieldname: 'date_range_section', + fieldtype: 'Section Break', + label: __('Date Range') + }, + { + fieldname: 'creationDateFrom', + fieldtype: 'Date', + label: __('From Date'), + default: startDate + }, + { + fieldname: 'creationDateTo', + fieldtype: 'Date', + label: __('To Date'), + default: endDate + } + ], + primary_action_label: __('Load Items'), + primary_action: function() { + var values = d.get_values(); + + // Close dialog + d.hide(); + + // Create a loading dialog with the spinner like in asan login + const loading_dialog = new frappe.ui.Dialog({ + title: __('Loading Items'), + fields: [ + { + fieldname: 'status_html', + fieldtype: 'HTML', + options: ` +
+
+
+
+ +
+

Loading items from E-Taxes

+

Please wait, this process may take several minutes.

+
+
+ ` + } + ] + }); + + loading_dialog.show(); + + // Format dates correctly - convert from YYYY-MM-DD to DD-MM-YYYY HH:MM + let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : moment(startDate).format('DD-MM-YYYY 00:00'); + let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : moment(endDate).format('DD-MM-YYYY 23:59'); + + // Load items + frappe.call({ + method: 'invoice_az.api.load_items_from_invoices', + args: { + 'date_from': fromDate, + 'date_to': toDate + }, + callback: function(r) { + // Close progress dialog + loading_dialog.hide(); + + if (r.message && r.message.success) { + frappe.show_alert({ + message: __('Extracted and created {0} unique items', [r.message.created_count]), + indicator: 'green' + }, 5); + + // Refresh list + cur_list.refresh(); + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('An error occurred while loading items') + }); + } + } + }); + } + }); + + d.show(); +} \ 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 new file mode 100644 index 0000000..b8ccdf9 --- /dev/null +++ b/invoice_az/client/e_taxes_parties_list.js @@ -0,0 +1,240 @@ +frappe.listview_settings['E-Taxes Parties'] = { + add_fields: ['status', 'mapped_party', 'erp_party_type'], + + get_indicator: function(doc) { + if (doc.status === 'Сопоставлен') { + return [__('Сопоставлен'), 'green', 'status,=,Сопоставлен']; + } else if (doc.status === 'В обработке') { + return [__('В обработке'), 'orange', 'status,=,В обработке']; + } else { + return [__('Новый'), 'red', 'status,=,Новый']; + } + }, + + onload: function(listview) { + // Добавляем кнопку для загрузки контрагентов из E-Taxes + listview.page.add_menu_item(__('Загрузить из E-Taxes'), function() { + show_invoice_filter_dialog(); + }); + } +}; + +// Функция для отображения диалога выбора периода инвойсов +function show_invoice_filter_dialog() { + // Использовать значения дат, охватывающие продолжительный период + const startDate = "01-01-2020 00:00"; + const endDate = moment().add(1, 'year').format('DD-MM-YYYY HH:mm'); + + // Создаем диалог с полями для фильтрации + var d = new frappe.ui.Dialog({ + title: __('Фильтры инвойсов'), + fields: [ + { + fieldname: 'date_range_section', + fieldtype: 'Section Break', + label: __('Период') + }, + { + fieldname: 'creationDateFrom', + fieldtype: 'Data', + label: __('С даты'), + default: startDate + }, + { + fieldname: 'creationDateTo', + fieldtype: 'Data', + label: __('По дату'), + default: endDate + }, + { + fieldname: 'invoice_details_section', + fieldtype: 'Section Break', + label: __('Детали инвойса') + }, + { + fieldname: 'serialNumber', + fieldtype: 'Data', + label: __('Серийный номер') + }, + { + fieldname: 'party_section', + fieldtype: 'Section Break', + label: __('Отправитель/Получатель') + }, + { + fieldname: 'senderTin', + fieldtype: 'Data', + label: __('ИНН отправителя') + }, + { + fieldname: 'senderName', + fieldtype: 'Data', + label: __('Имя отправителя') + }, + { + fieldname: 'receiverTin', + fieldtype: 'Data', + label: __('ИНН получателя') + }, + { + fieldname: 'receiverName', + fieldtype: 'Data', + label: __('Имя получателя') + }, + { + fieldname: 'amount_section', + fieldtype: 'Section Break', + label: __('Сумма') + }, + { + fieldname: 'amountFrom', + fieldtype: 'Float', + label: __('Сумма от'), + precision: 2 + }, + { + fieldname: 'amountTo', + fieldtype: 'Float', + label: __('Сумма до'), + precision: 2 + }, + { + fieldname: 'limit_section', + fieldtype: 'Section Break', + label: __('Лимит') + }, + { + fieldname: 'maxCount', + fieldtype: 'Int', + label: __('Макс. кол-во записей'), + default: 200 + } + ], + primary_action_label: __('Получить инвойсы'), + primary_action: function() { + var values = d.get_values(); + + // Дополняем значения фильтров полными параметрами + const fullPayload = { + "sortBy": "creationDate", + "sortAsc": true, + "statuses": ["approved", "onApproval", "updateApproval", "updateRequested", + "cancelRequested", "approvedBySystem", "onApprovalEdited", + "deactivated", "cancelationRefused", "correctionRefused"], + "types": ["current", "corrected"], + "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", + "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", + "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], + "offset": 0, + "actionOwner": null, + + // Заполняем значениями из диалога + "serialNumber": values.serialNumber || null, + "senderTin": values.senderTin || null, + "senderName": values.senderName || null, + "productName": null, + "productCode": null, + "receiverTin": values.receiverTin || null, + "receiverName": values.receiverName || null, + "creationDateFrom": values.creationDateFrom || startDate, + "creationDateTo": values.creationDateTo || endDate, + "amountFrom": values.amountFrom || null, + "amountTo": values.amountTo || null, + "maxCount": values.maxCount || 200 + }; + + // Закрываем диалог + d.hide(); + + // Получаем настройки Asan Login + frappe.call({ + method: 'invoice_az.api.get_default_asan_login', + callback: function(r) { + if (r.message && r.message.found) { + if (r.message.auth_status === 'Fully Authenticated') { + // Показываем индикатор загрузки + frappe.show_alert({ + message: __('Получение инвойсов из E-Taxes, пожалуйста, подождите...'), + indicator: 'blue' + }, 10); + + // Получаем инвойсы + frappe.call({ + method: 'invoice_az.api.get_invoices', + args: { + 'token': r.message.main_token, + 'filters': JSON.stringify(fullPayload) + }, + callback: function(r2) { + if (r2.message) { + if (r2.message.error === "unauthorized") { + frappe.msgprint({ + title: __('Ошибка аутентификации'), + indicator: 'red', + message: __('Необходимо выполнить повторную аутентификацию в Asan Login') + }); + frappe.set_route('List', 'Asan Login'); + return; + } + + let invoicesData = null; + if (r2.message.data && r2.message.data.length) { + invoicesData = r2.message.data; + } else if (r2.message.invoices && r2.message.invoices.length) { + invoicesData = r2.message.invoices; + } + + if (invoicesData && invoicesData.length) { + // Извлекаем контрагентов из инвойсов + frappe.call({ + method: 'invoice_az.e_taxes_settings.extract_parties_from_invoices', + args: { + 'invoices': invoicesData + }, + callback: function(r3) { + if (r3.message && r3.message.success) { + frappe.show_alert({ + message: __('Извлечено и создано {0} уникальных контрагентов', [r3.message.parties_created]), + indicator: 'green' + }, 5); + + // Обновляем список + cur_list.refresh(); + } else { + frappe.msgprint({ + title: __('Ошибка'), + indicator: 'red', + message: r3.message ? r3.message.message : __('Произошла ошибка при извлечении контрагентов из инвойсов') + }); + } + } + }); + } else { + frappe.msgprint(__('Инвойсы не найдены по заданным фильтрам')); + } + } + }, + error: function(err) { + console.error('Error fetching invoices:', err); + frappe.msgprint(__('Ошибка при получении инвойсов. См. консоль для деталей.')); + } + }); + } else { + frappe.msgprint({ + title: __('Требуется аутентификация'), + indicator: 'orange', + message: __('Необходимо выполнить полную аутентификацию в Asan Login перед получением инвойсов') + }); + frappe.set_route('List', 'Asan Login'); + } + } else { + frappe.msgprint(__('Настройки Asan Login не найдены')); + frappe.set_route('List', 'Asan Login'); + } + } + }); + } + }); + + d.show(); +} \ No newline at end of file diff --git a/invoice_az/hooks.py b/invoice_az/hooks.py index b7c38d3..37d491d 100644 --- a/invoice_az/hooks.py +++ b/invoice_az/hooks.py @@ -5,6 +5,23 @@ app_description = "This application uploads and downloads invoices from e taxes app_email = "info@jeyerp.az" app_license = "unlicense" +doctype_js = { + "Purchase Order": "public/js/purchase_order.js" +} + +page_js = { + "purchase-order": "public/js/purchase_order.js" +} + +app_include_js = [ + "/assets/invoice_az/js/purchase_order.js" +] + +doctype_list_js = { + "E-Taxes Item": "client/e_taxes_items_list.js", + "E-Taxes Parties": "client/e_taxes_parties_list.js" +} + # Apps # ------------------ diff --git a/invoice_az/invoice_az/doctype/__init__.py b/invoice_az/invoice_az/doctype/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/invoice_az/invoice_az/doctype/asan_login/__init__.py b/invoice_az/invoice_az/doctype/asan_login/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/invoice_az/invoice_az/doctype/asan_login/asan_login.js b/invoice_az/invoice_az/doctype/asan_login/asan_login.js new file mode 100644 index 0000000..78b5af2 --- /dev/null +++ b/invoice_az/invoice_az/doctype/asan_login/asan_login.js @@ -0,0 +1,533 @@ +frappe.ui.form.on('Asan Login', { + refresh: function(frm) { + // Остановить любые существующие интервалы + if (window.authStatusInterval) { + clearInterval(window.authStatusInterval); + window.authStatusInterval = null; + } + + // Кнопка для получения токена + frm.add_custom_button(__('Login with Asan Imza'), function() { + // Проверяем, что поля phone и user_id заполнены + if (!frm.doc.phone || !frm.doc.user_id) { + frappe.msgprint(__('Please fill in Phone and User ID fields')); + return; + } + + // Показываем сообщение о том, что запрос отправлен + frappe.show_alert({ + message: __('Authentication request sent. Please confirm on your phone.'), + indicator: 'blue' + }, 5); + + // Отправляем запрос для начала процесса аутентификации + frappe.call({ + method: 'invoice_az.api.handle_authentication', + args: { + 'asan_login_name': frm.doc.name + }, + callback: function(r) { + if (r.message && r.message.success) { + // Сохраняем токен в документе и запускаем опрос статуса + if (r.message.bearer_token) { + frm.set_value('bearer_token', r.message.bearer_token); + } + + // Запускаем опрос статуса + startPollingWithTimeout(frm); + } else { + // Показываем сообщение об ошибке + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Authentication request failed.') + }); + } + } + }); + }, __('Login')); + + // Кнопка для получения сертификатов (доступна только если авторизация успешна) + if (frm.doc.auth_status === 'Authenticated' || frm.doc.auth_status === 'Fully Authenticated') { + frm.add_custom_button(__('Get Certificates'), function() { + frappe.call({ + method: 'invoice_az.api.get_auth_certificates', + args: { + 'asan_login_name': frm.doc.name + }, + callback: function(r) { + if (r.message && r.message.success) { + // Обновляем форму после успешного получения сертификатов + frm.reload_doc(); + + // Показываем сообщение об успешном получении сертификатов + frappe.msgprint({ + title: __('Success'), + indicator: 'green', + message: __('Certificates retrieved successfully') + }); + + // Отображаем список сертификатов в диалоговом окне + if (r.message.certificates) { + showCertificatesDialog(r.message.certificates, frm); + } + } else if (r.message && r.message.unauthorized) { + // Если ошибка авторизации, предлагаем авторизоваться заново + showAuthDialog(frm); + } else { + // Другие ошибки + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Failed to get certificates') + }); + } + } + }); + }, __('Actions')); + } + + // Кнопка для выбора налогоплательщика (доступна если авторизация успешна и сертификат выбран) + if ((frm.doc.auth_status === 'Authenticated' || frm.doc.auth_status === 'Fully Authenticated') && + frm.doc.selected_certificate) { + frm.add_custom_button(__('Choose Taxpayer'), function() { + frappe.call({ + method: 'invoice_az.api.select_taxpayer', + args: { + 'asan_login_name': frm.doc.name + }, + callback: function(r) { + if (r.message && r.message.success) { + // Обновляем форму после успешного выбора налогоплательщика + frm.reload_doc(); + + // Показываем сообщение об успешном выборе + frappe.msgprint({ + title: __('Success'), + indicator: 'green', + message: __('Taxpayer selected successfully') + }); + } else if (r.message && r.message.unauthorized) { + // Если ошибка авторизации, предлагаем авторизоваться заново + showAuthDialog(frm); + } else { + // Другие ошибки + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Failed to select taxpayer') + }); + } + } + }); + }, __('Actions')); + } + + // Обработчик изменения выбранного сертификата + if (frm.doc.certificates_json) { + try { + const certificates = JSON.parse(frm.doc.certificates_json); + + frm.add_custom_button(__('Select Certificate'), function() { + showCertificatesDialog(certificates, frm); + }, __('Actions')); + } catch (e) { + console.error('Error parsing certificates JSON:', e); + } + } + }, + + // Обработчик изменения выбранного сертификата (введенного вручную) + selected_certificate: function(frm) { + // Только если поле не пустое и сертификаты доступны + if (frm.doc.selected_certificate && frm.doc.certificates_json) { + try { + // Получаем данные сертификатов + const certificates = JSON.parse(frm.doc.certificates_json); + if (!Array.isArray(certificates) || certificates.length === 0) return; + + // Ищем сертификат по имени или его части + const searchStr = frm.doc.selected_certificate.toLowerCase(); + + // Сначала пытаемся найти точное совпадение по имени и идентификатору + let foundCert = null; + let foundIndex = -1; + + for (let i = 0; i < certificates.length; i++) { + const cert = certificates[i]; + let certName = ''; + let certId = ''; + + if (cert.taxpayerType === 'individual' && cert.individualInfo) { + certName = cert.individualInfo.name || ''; + certId = cert.individualInfo.fin || ''; + } else if (cert.legalInfo) { + certName = cert.legalInfo.name || ''; + certId = cert.legalInfo.voen || cert.legalInfo.tin || ''; + } + + const fullName = `${certName} (${certId})`.toLowerCase(); + + // Проверяем точное совпадение или содержание строки поиска + if (fullName === searchStr || fullName.includes(searchStr)) { + foundCert = cert; + foundIndex = i; + break; + } + } + + // Если нашли совпадение, устанавливаем данные сертификата + if (foundCert) { + frm.set_value('selected_certificate_index', foundIndex); + frm.set_value('selected_certificate_json', JSON.stringify(foundCert, null, 2)); + + frappe.show_alert({ + message: __('Certificate found and selected'), + indicator: 'green' + }, 3); + } + } catch (e) { + console.error('Error processing certificate selection:', e); + } + } + } +}); + +// Функция для запуска опроса через setTimeout вместо setInterval +function startPollingWithTimeout(frm) { + console.log('startPollingWithTimeout function has been called'); + + // Отображаем индикатор загрузки + frm.page.set_indicator(__('Waiting for confirmation'), 'orange'); + + // Создаем всплывающее окно с информацией + const status_dialog = new frappe.ui.Dialog({ + title: __('Authentication Status'), + fields: [ + { + fieldname: 'status_html', + fieldtype: 'HTML', + options: ` +
+
+
+
+ +
+

Waiting for confirmation on your phone

+

Please check your phone and confirm the authentication request.

+
+
+ ` + } + ], + primary_action_label: __('Cancel'), + primary_action: function() { + // Остановить опрос + window.stopPolling = true; + status_dialog.hide(); + frm.page.clear_indicator(); + frm.reload_doc(); + } + }); + + status_dialog.show(); + + // Счетчик попыток и флаг для остановки опроса + let attempts = 0; + const maxAttempts = 20; // 2 минут (20 попыток с интервалом 5 секунд) + window.stopPolling = false; + + // Функция опроса + function pollAuthStatus() { + // Если достигнуто максимальное количество попыток или установлен флаг остановки + if (attempts >= maxAttempts || window.stopPolling) { + console.log('Polling stopped: ' + (attempts >= maxAttempts ? 'max attempts reached' : 'manually cancelled')); + + if (attempts >= maxAttempts) { + // Обновляем информацию в диалоговом окне по таймауту + $('#status_message').html(` +

Authentication Timeout

+

The authentication request has timed out. Please try again.

+ `); + $('.lds-dual-ring').css('display', 'none'); + + // Обновляем поле auth_status + frm.set_value('auth_status', 'Failed'); + frm.save(); + + // Закрываем диалог через 3 секунды + setTimeout(function() { + status_dialog.hide(); + frm.reload_doc(); + }, 3000); + } + + return; + } + + attempts++; + console.log(`Polling auth status, attempt ${attempts} of ${maxAttempts}`); + + // Проверяем статус авторизации + frappe.call({ + method: 'invoice_az.api.poll_auth_status', + args: { + 'asan_login_name': frm.doc.name, + 'bearer_token': frm.doc.bearer_token + }, + callback: function(r) { + if (r.message && r.message.success) { + if (r.message.authenticated) { + // Авторизация успешна + window.stopPolling = true; + + // Обновляем информацию в диалоговом окне + $('#status_message').html(` +

Authentication Successful!

+

You are now authenticated. The page will reload shortly.

+ `); + $('.lds-dual-ring').css('display', 'none'); + + // Закрываем диалоговое окно через 2 секунды + setTimeout(function() { + status_dialog.hide(); + frm.reload_doc(); + }, 2000); + } else { + // Продолжаем опрос + setTimeout(pollAuthStatus, 5000); + } + } else { + // Если ошибка, останавливаем опрос и показываем сообщение + window.stopPolling = true; + + // Обновляем информацию в диалоговом окне + $('#status_message').html(` +

Authentication Error

+

${r.message ? r.message.message : 'An unknown error occurred.'}

+ `); + $('.lds-dual-ring').css('display', 'none'); + + // Обновляем поле auth_status + frm.set_value('auth_status', 'Failed'); + frm.save(); + + // Закрываем диалог через 3 секунды + setTimeout(function() { + status_dialog.hide(); + frm.reload_doc(); + }, 3000); + } + }, + error: function(err) { + // В случае ошибки продолжаем опрос + console.error('Error during status check:', err); + setTimeout(pollAuthStatus, 5000); + } + }); + } + + // Начинаем опрос + console.log('Starting first polling attempt'); + pollAuthStatus(); +} + +// Функция для отображения сертификатов в диалоговом окне +function showCertificatesDialog(certificates, frm) { + if (!certificates || !certificates.length) { + frappe.msgprint(__('No certificates data to display')); + return; + } + + // Создаем таблицу с сертификатами + var cert_table = '
'; + cert_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + + certificates.forEach(function(cert, index) { + var name = ''; + var id = ''; + + if (cert.taxpayerType === 'individual' && cert.individualInfo) { + name = cert.individualInfo.name || ''; + id = cert.individualInfo.fin || ''; + } else if (cert.legalInfo) { + name = cert.legalInfo.name || ''; + id = cert.legalInfo.tin || cert.legalInfo.voen || ''; + } + + cert_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }); + + cert_table += '
TypeNameIDPositionStatusAction
' + (cert.taxpayerType || '') + '' + name + '' + id + '' + (cert.position || '') + '' + (cert.hasAccess ? 'Active' : 'Inactive') + + (cert.liquidated ? ' (Liquidated)' : '') + '
'; + + // Отображаем диалоговое окно с таблицей + var d = new frappe.ui.Dialog({ + title: __('Certificates List'), + fields: [{ + fieldtype: 'HTML', + fieldname: 'certificate_list', + options: cert_table + }], + primary_action_label: __('Close'), + primary_action: function() { + d.hide(); + } + }); + + d.show(); + + // Добавляем обработчик для кнопки выбора сертификата + d.$wrapper.find('.select-cert').on('click', function() { + var index = $(this).data('index'); + var cert = certificates[index]; + + // Получаем имя для отображения + let certName = ''; + if (cert.taxpayerType === 'individual' && cert.individualInfo) { + certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`; + } else if (cert.legalInfo) { + certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`; + } else { + certName = `Certificate ${index + 1}`; + } + + // Отправляем запрос на выбор сертификата + frappe.call({ + method: 'invoice_az.api.select_certificate', + args: { + 'asan_login_name': frm.doc.name, + 'certificate_data': cert, + 'certificate_name': certName + }, + callback: function(r) { + if (r.message && r.message.success) { + frappe.show_alert({ + message: __('Certificate selected. Choosing taxpayer automatically...'), + indicator: 'blue' + }, 3); + + d.hide(); + + // После успешного выбора сертификата автоматически вызываем выбор налогоплательщика + frappe.call({ + method: 'invoice_az.api.select_taxpayer', + args: { + 'asan_login_name': frm.doc.name + }, + callback: function(r2) { + if (r2.message && r2.message.success) { + frappe.show_alert({ + message: __('Taxpayer selected automatically'), + indicator: 'green' + }, 5); + + // Перезагружаем страницу для обновления данных + frm.reload_doc(); + } else if (r2.message && r2.message.unauthorized) { + // Если ошибка авторизации, предлагаем авторизоваться заново + showAuthDialog(frm); + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r2.message ? r2.message.message : __('Failed to select taxpayer automatically') + }); + + // Все равно перезагружаем страницу, чтобы отобразить выбранный сертификат + frm.reload_doc(); + } + } + }); + } else if (r.message && r.message.unauthorized) { + d.hide(); + + // Если ошибка авторизации, предлагаем авторизоваться заново + showAuthDialog(frm); + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Failed to select certificate') + }); + } + } + }); + }); +} + +// Функция для отображения диалога повторной аутентификации +function showAuthDialog(frm) { + var d = new frappe.ui.Dialog({ + title: __('Authentication Required'), + fields: [ + { + fieldname: 'message', + fieldtype: 'HTML', + options: ` +
+

Your session has expired or authentication is required.

+

Would you like to authenticate again?

+
+ ` + } + ], + primary_action_label: __('Authenticate'), + primary_action: function() { + d.hide(); + + // Вызываем процесс аутентификации + frappe.call({ + method: 'invoice_az.api.handle_authentication', + args: { + 'asan_login_name': frm.doc.name + }, + callback: function(r) { + if (r.message && r.message.success) { + // Сохраняем токен в документе и запускаем опрос статуса + if (r.message.bearer_token) { + frm.set_value('bearer_token', r.message.bearer_token); + frm.save(); + } + + // Запускаем опрос статуса + startPollingWithTimeout(frm); + } else { + // Показываем сообщение об ошибке + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Authentication request failed.') + }); + } + } + }); + }, + secondary_action_label: __('Cancel'), + secondary_action: function() { + d.hide(); + } + }); + + d.show(); +} \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/asan_login/asan_login.json b/invoice_az/invoice_az/doctype/asan_login/asan_login.json new file mode 100644 index 0000000..f6199ca --- /dev/null +++ b/invoice_az/invoice_az/doctype/asan_login/asan_login.json @@ -0,0 +1,139 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "hash", + "creation": "2025-04-28 10:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "authentication_section", + "phone", + "user_id", + "bearer_token", + "auth_status", + "column_break_1", + "is_default", + "certificates_section", + "certificates_json", + "selected_certificate", + "selected_certificate_index", + "selected_certificate_json", + "taxpayer_section", + "main_token", + "choose_taxpayer_response" + ], + "fields": [ + { + "fieldname": "authentication_section", + "fieldtype": "Section Break", + "label": "Authentication" + }, + { + "fieldname": "phone", + "fieldtype": "Data", + "label": "Phone", + "reqd": 1 + }, + { + "fieldname": "user_id", + "fieldtype": "Data", + "label": "ID", + "reqd": 1 + }, + { + "fieldname": "bearer_token", + "fieldtype": "Small Text", + "label": "Bearer Token", + "read_only": 1 + }, + { + "fieldname": "auth_status", + "fieldtype": "Select", + "label": "Auth Status", + "options": "Not Authenticated\nWaiting for confirmation\nAuthenticated\nFully Authenticated\nFailed", + "default": "Not Authenticated", + "read_only": 1 + }, + { + "fieldname": "column_break_1", + "fieldtype": "Column Break" + }, + { + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" + }, + { + "fieldname": "certificates_section", + "fieldtype": "Section Break", + "label": "Certificates" + }, + { + "fieldname": "certificates_json", + "fieldtype": "Code", + "label": "Certificates JSON", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "selected_certificate", + "fieldtype": "Data", + "label": "Selected Certificate" + }, + { + "fieldname": "selected_certificate_index", + "fieldtype": "Data", + "hidden": 1, + "label": "Selected Certificate Index" + }, + { + "fieldname": "selected_certificate_json", + "fieldtype": "Code", + "label": "Selected Certificate Details", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "taxpayer_section", + "fieldtype": "Section Break", + "label": "Taxpayer Information" + }, + { + "fieldname": "main_token", + "fieldtype": "Small Text", + "label": "Main Token", + "description": "Primary token for all API operations", + "read_only": 1 + }, + { + "fieldname": "choose_taxpayer_response", + "fieldtype": "Code", + "label": "Taxpayer Response", + "options": "JSON", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-04-28 10:00:00.000000", + "modified_by": "Administrator", + "module": "Invoice Az", + "name": "Asan Login", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" + } \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/asan_login/asan_login.py b/invoice_az/invoice_az/doctype/asan_login/asan_login.py new file mode 100644 index 0000000..ccae683 --- /dev/null +++ b/invoice_az/invoice_az/doctype/asan_login/asan_login.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class AsanLogin(Document): + def before_save(self): + # Если установлен флаг is_default, сбрасываем его у других документов + if self.is_default: + existing_defaults = frappe.get_all( + "Asan Login", + filters={ + "is_default": 1, + "name": ["!=", self.name] + } + ) + + for default in existing_defaults: + doc = frappe.get_doc("Asan Login", default.name) + doc.is_default = 0 + doc.flags.ignore_permissions = True + doc.save() + + def validate(self): + # Проверяем обязательные поля + if not self.phone: + frappe.throw("Phone is required") + + if not self.user_id: + frappe.throw("User ID is required") \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/asan_login/test_asan_login.py b/invoice_az/invoice_az/doctype/asan_login/test_asan_login.py new file mode 100644 index 0000000..9cf8566 --- /dev/null +++ b/invoice_az/invoice_az/doctype/asan_login/test_asan_login.py @@ -0,0 +1,30 @@ +# Copyright (c) 2025, Jey ERP and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record depdendencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class UnitTestAsanLogin(UnitTestCase): + """ + Unit tests for AsanLogin. + Use this class for testing individual functions and methods. + """ + + pass + + +class IntegrationTestAsanLogin(IntegrationTestCase): + """ + Integration tests for AsanLogin. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/invoice_az/invoice_az/doctype/e_taxes_item/__init__.py b/invoice_az/invoice_az/doctype/e_taxes_item/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.js b/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.js new file mode 100644 index 0000000..f0c4650 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Jey ERP and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("E-Taxes Item", { +// refresh(frm) { + +// }, +// }); diff --git a/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.json b/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.json new file mode 100644 index 0000000..411cdb5 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.json @@ -0,0 +1,123 @@ +{ + "actions": [], + "allow_rename": 0, + "autoname": "autoincrement", + "creation": "2025-05-01 12:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "etaxes_item_name", + "etaxes_item_code", + "etaxes_unit", + "etaxes_price", + "source_invoice", + "status", + "mapped_item", + "item_creation_section", + "item_group", + "uom", + "item_tax_template" + ], + "fields": [ + { + "fieldname": "etaxes_item_name", + "fieldtype": "Data", + "label": "Item Name", + "reqd": 1 + }, + { + "fieldname": "etaxes_item_code", + "fieldtype": "Data", + "label": "Item Code" + }, + { + "fieldname": "etaxes_unit", + "fieldtype": "Data", + "label": "Unit of Measure" + }, + { + "fieldname": "etaxes_price", + "fieldtype": "Currency", + "label": "Price", + "options": "AZN" + }, + { + "fieldname": "source_invoice", + "fieldtype": "Data", + "label": "Source", + "description": "Source invoice number" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "New\nMapped\nProcessing", + "default": "New" + }, + { + "fieldname": "mapped_item", + "fieldtype": "Link", + "label": "Mapped Item", + "options": "Item" + }, + { + "fieldname": "item_creation_section", + "fieldtype": "Section Break", + "label": "Item Creation Parameters" + }, + { + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, + { + "fieldname": "uom", + "fieldtype": "Link", + "label": "Unit of Measure", + "options": "UOM" + }, + { + "fieldname": "item_tax_template", + "fieldtype": "Link", + "label": "Tax Template", + "options": "Item Tax Template" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-05-01 12:00:00.000000", + "modified_by": "Administrator", + "module": "Invoice Az", + "name": "E-Taxes Item", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 0, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 + } \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.py b/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.py new file mode 100644 index 0000000..b91df48 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_item/e_taxes_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Jey ERP and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ETaxesItem(Document): + pass diff --git a/invoice_az/invoice_az/doctype/e_taxes_item/test_e_taxes_items.py b/invoice_az/invoice_az/doctype/e_taxes_item/test_e_taxes_items.py new file mode 100644 index 0000000..a61bbda --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_item/test_e_taxes_items.py @@ -0,0 +1,30 @@ +# Copyright (c) 2025, Jey ERP and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record depdendencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class UnitTestETaxesItems(UnitTestCase): + """ + Unit tests for ETaxesItems. + Use this class for testing individual functions and methods. + """ + + pass + + +class IntegrationTestETaxesItems(IntegrationTestCase): + """ + Integration tests for ETaxesItems. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/invoice_az/invoice_az/doctype/e_taxes_item_mapping/__init__.py b/invoice_az/invoice_az/doctype/e_taxes_item_mapping/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.js b/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.js new file mode 100644 index 0000000..7725e38 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Jey ERP and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("E-Taxes Item Mapping", { +// refresh(frm) { + +// }, +// }); diff --git a/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.json b/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.json new file mode 100644 index 0000000..cfdaa31 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "allow_rename": 0, + "creation": "2025-05-01 12:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "etaxes_item_name", + "etaxes_item_code", + "erp_item", + "mapping_type" + ], + "fields": [ + { + "fieldname": "etaxes_item_name", + "fieldtype": "Data", + "label": "E-Taxes Item Name", + "reqd": 1 + }, + { + "fieldname": "etaxes_item_code", + "fieldtype": "Data", + "label": "E-Taxes Item Code" + }, + { + "fieldname": "erp_item", + "fieldtype": "Link", + "label": "System Item", + "options": "Item", + "reqd": 1 + }, + { + "fieldname": "mapping_type", + "fieldtype": "Select", + "label": "Mapping Type", + "options": "Manual\nAutomatic", + "default": "Manual" + } + ], + "index_web_pages_for_search": 0, + "istable": 1, + "links": [], + "modified": "2025-05-01 12:00:00.000000", + "modified_by": "Administrator", + "module": "Invoice Az", + "name": "E-Taxes Item Mapping", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 + } \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.py b/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.py new file mode 100644 index 0000000..075ffc5 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_item_mapping/e_taxes_item_mapping.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Jey ERP and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ETaxesItemMapping(Document): + pass diff --git a/invoice_az/invoice_az/doctype/e_taxes_item_mapping/test_e_taxes_item_mapping.py b/invoice_az/invoice_az/doctype/e_taxes_item_mapping/test_e_taxes_item_mapping.py new file mode 100644 index 0000000..1138b9a --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_item_mapping/test_e_taxes_item_mapping.py @@ -0,0 +1,30 @@ +# Copyright (c) 2025, Jey ERP and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record depdendencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class UnitTestETaxesItemMapping(UnitTestCase): + """ + Unit tests for ETaxesItemMapping. + Use this class for testing individual functions and methods. + """ + + pass + + +class IntegrationTestETaxesItemMapping(IntegrationTestCase): + """ + Integration tests for ETaxesItemMapping. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/invoice_az/invoice_az/doctype/e_taxes_parties/__init__.py b/invoice_az/invoice_az/doctype/e_taxes_parties/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.js b/invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.js new file mode 100644 index 0000000..e53d8cf --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Jey ERP and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("E-Taxes Parties", { +// refresh(frm) { + +// }, +// }); diff --git a/invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.json b/invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.json new file mode 100644 index 0000000..23b7a8e --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.json @@ -0,0 +1,134 @@ +{ + "actions": [], + "allow_rename": 0, + "autoname": "autoincrement", + "creation": "2025-05-01 12:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "etaxes_party_name", + "etaxes_tax_id", + "etaxes_party_type", + "etaxes_address", + "source_invoice", + "status", + "erp_party_type", + "mapped_party", + "party_creation_section", + "party_group", + "party_group_type", + "payment_terms" + ], + "fields": [ + { + "fieldname": "etaxes_party_name", + "fieldtype": "Data", + "label": "Party Name", + "reqd": 1 + }, + { + "fieldname": "etaxes_tax_id", + "fieldtype": "Data", + "label": "Tax ID" + }, + { + "fieldname": "etaxes_party_type", + "fieldtype": "Select", + "label": "E-Taxes Type", + "options": "Sender\nReceiver", + "default": "Sender" + }, + { + "fieldname": "etaxes_address", + "fieldtype": "Data", + "label": "Address" + }, + { + "fieldname": "source_invoice", + "fieldtype": "Data", + "label": "Source", + "description": "Source invoice number" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "New\nMapped\nProcessing", + "default": "New" + }, + { + "fieldname": "erp_party_type", + "fieldtype": "Select", + "label": "System Party Type", + "options": "Supplier\nCustomer", + "default": "Supplier" + }, + { + "fieldname": "mapped_party", + "fieldtype": "Dynamic Link", + "label": "Mapped Party", + "options": "erp_party_type" + }, + { + "fieldname": "party_creation_section", + "fieldtype": "Section Break", + "label": "Party Creation Parameters" + }, + { + "fieldname": "party_group", + "fieldtype": "Dynamic Link", + "label": "Party Group", + "options": "party_group_type" + }, + { + "fieldname": "party_group_type", + "fieldtype": "Select", + "label": "Party Group Type", + "options": "Supplier Group\nCustomer Group", + "default": "Supplier Group", + "hidden": 1 + }, + { + "fieldname": "payment_terms", + "fieldtype": "Link", + "label": "Payment Terms", + "options": "Payment Terms Template" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-05-01 12:00:00.000000", + "modified_by": "Administrator", + "module": "Invoice Az", + "name": "E-Taxes Parties", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 0, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 + } \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.py b/invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.py new file mode 100644 index 0000000..25805fc --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_parties/e_taxes_parties.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Jey ERP and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ETaxesParties(Document): + pass diff --git a/invoice_az/invoice_az/doctype/e_taxes_parties/test_e_taxes_parties.py b/invoice_az/invoice_az/doctype/e_taxes_parties/test_e_taxes_parties.py new file mode 100644 index 0000000..5b2d6b6 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_parties/test_e_taxes_parties.py @@ -0,0 +1,30 @@ +# Copyright (c) 2025, Jey ERP and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record depdendencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class UnitTestETaxesParties(UnitTestCase): + """ + Unit tests for ETaxesParties. + Use this class for testing individual functions and methods. + """ + + pass + + +class IntegrationTestETaxesParties(IntegrationTestCase): + """ + Integration tests for ETaxesParties. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/invoice_az/invoice_az/doctype/e_taxes_party_mapping/__init__.py b/invoice_az/invoice_az/doctype/e_taxes_party_mapping/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.js b/invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.js new file mode 100644 index 0000000..5e85456 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Jey ERP and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("E-Taxes Party Mapping", { +// refresh(frm) { + +// }, +// }); diff --git a/invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.json b/invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.json new file mode 100644 index 0000000..06e8928 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.json @@ -0,0 +1,60 @@ +{ + "actions": [], + "allow_rename": 0, + "creation": "2025-05-01 12:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "etaxes_party_name", + "etaxes_tax_id", + "party_type", + "erp_party", + "mapping_type" + ], + "fields": [ + { + "fieldname": "etaxes_party_name", + "fieldtype": "Data", + "label": "E-Taxes Party Name", + "reqd": 1 + }, + { + "fieldname": "etaxes_tax_id", + "fieldtype": "Data", + "label": "Tax ID" + }, + { + "fieldname": "party_type", + "fieldtype": "Select", + "label": "Party Type", + "options": "Supplier\nCustomer", + "reqd": 1 + }, + { + "fieldname": "erp_party", + "fieldtype": "Dynamic Link", + "label": "System Party", + "options": "party_type", + "reqd": 1 + }, + { + "fieldname": "mapping_type", + "fieldtype": "Select", + "label": "Mapping Type", + "options": "Manual\nAutomatic", + "default": "Manual" + } + ], + "index_web_pages_for_search": 0, + "istable": 1, + "links": [], + "modified": "2025-05-01 12:00:00.000000", + "modified_by": "Administrator", + "module": "Invoice Az", + "name": "E-Taxes Party Mapping", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 + } \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.py b/invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.py new file mode 100644 index 0000000..188c297 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_party_mapping/e_taxes_party_mapping.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Jey ERP and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ETaxesPartyMapping(Document): + pass diff --git a/invoice_az/invoice_az/doctype/e_taxes_party_mapping/test_e_taxes_party_mapping.py b/invoice_az/invoice_az/doctype/e_taxes_party_mapping/test_e_taxes_party_mapping.py new file mode 100644 index 0000000..b760842 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_party_mapping/test_e_taxes_party_mapping.py @@ -0,0 +1,30 @@ +# Copyright (c) 2025, Jey ERP and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record depdendencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class UnitTestETaxesPartyMapping(UnitTestCase): + """ + Unit tests for ETaxesPartyMapping. + Use this class for testing individual functions and methods. + """ + + pass + + +class IntegrationTestETaxesPartyMapping(IntegrationTestCase): + """ + Integration tests for ETaxesPartyMapping. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/invoice_az/invoice_az/doctype/e_taxes_settings/__init__.py b/invoice_az/invoice_az/doctype/e_taxes_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js new file mode 100644 index 0000000..4578038 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.js @@ -0,0 +1,99 @@ +frappe.ui.form.on('E-Taxes Settings', { + refresh: function(frm) { + // Добавляем кнопку для автоматического сопоставления + frm.add_custom_button(__('Сопоставить по похожему названию'), function() { + frappe.confirm( + __('Это действие попытается автоматически сопоставить все несопоставленные товары и контрагенты на основе схожести названий. Продолжить?'), + function() { + // Запускаем процесс автоматического сопоставления + frappe.call({ + method: 'invoice_az.invoice_az.doctype.e_taxes_settings.e_taxes_settings.auto_match_by_name', + args: { + 'settings_name': frm.doc.name + }, + callback: function(r) { + if (r.message && r.message.success) { + frappe.msgprint({ + title: __('Сопоставление выполнено'), + indicator: 'green', + message: __('Автоматически сопоставлено {0} товаров и {1} контрагентов.', [r.message.items_matched, r.message.parties_matched]) + }); + frm.reload_doc(); + } else { + frappe.msgprint({ + title: __('Ошибка'), + indicator: 'red', + message: r.message ? r.message.message : __('Произошла ошибка при автоматическом сопоставлении.') + }); + } + } + }); + } + ); + }, __('Действия')); + + // Добавляем кнопку для импорта товаров из CSV + frm.add_custom_button(__('Импорт сопоставлений из CSV'), function() { + // Логика импорта будет добавлена позже + frappe.msgprint(__('Функция в разработке')); + }, __('Действия')); + + // Кнопка для установки настроек по умолчанию + if (!frm.doc.is_default) { + frm.add_custom_button(__('Установить по умолчанию'), function() { + frappe.call({ + method: 'frappe.client.set_value', + args: { + doctype: 'E-Taxes Settings', + name: frm.doc.name, + fieldname: 'is_default', + value: 1 + }, + callback: function(r) { + if (r.message) { + frappe.msgprint({ + title: __('Успешно'), + indicator: 'green', + message: __('Настройки установлены по умолчанию.') + }); + frm.reload_doc(); + } + } + }); + }, __('Действия')); + } + }, + + // При изменении флага is_default, сбрасываем этот флаг у других настроек + is_default: function(frm) { + if (frm.doc.is_default) { + frappe.call({ + method: 'frappe.client.get_list', + args: { + doctype: 'E-Taxes Settings', + filters: { + 'is_default': 1, + 'name': ['!=', frm.doc.name] + }, + fields: ['name'] + }, + callback: function(r) { + if (r.message && r.message.length) { + // Сбрасываем флаг у других настроек + r.message.forEach(function(settings) { + frappe.call({ + method: 'frappe.client.set_value', + args: { + doctype: 'E-Taxes Settings', + name: settings.name, + fieldname: 'is_default', + value: 0 + } + }); + }); + } + } + }); + } + } +}); \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.json b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.json new file mode 100644 index 0000000..361dc5e --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.json @@ -0,0 +1,162 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:settings_name", + "creation": "2025-05-01 12:00:00.000000", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "settings_name", + "is_default", + "is_active", + "general_section", + "similarity_threshold", + "consider_azeri_chars", + "default_item_settings_section", + "default_item_group", + "default_uom", + "default_item_tax_template", + "default_party_settings_section", + "default_supplier_group", + "default_customer_group", + "default_payment_terms", + "item_mappings_section", + "item_mappings", + "party_mappings_section", + "party_mappings" + ], + "fields": [ + { + "fieldname": "settings_name", + "fieldtype": "Data", + "label": "Settings Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default", + "default": 0 + }, + { + "fieldname": "is_active", + "fieldtype": "Check", + "label": "Is Active", + "default": 1 + }, + { + "fieldname": "general_section", + "fieldtype": "Section Break", + "label": "General Settings" + }, + { + "fieldname": "similarity_threshold", + "fieldtype": "Percent", + "label": "Similarity Threshold (%)", + "default": 80, + "description": "Minimum similarity percentage for automatic mapping" + }, + { + "fieldname": "consider_azeri_chars", + "fieldtype": "Check", + "label": "Consider Azerbaijani Characters", + "default": 1, + "description": "Consider replacement of Azerbaijani letters with Latin equivalents when matching" + }, + { + "fieldname": "default_item_settings_section", + "fieldtype": "Section Break", + "label": "Default Item Settings" + }, + { + "fieldname": "default_item_group", + "fieldtype": "Link", + "label": "Default Item Group", + "options": "Item Group", + "reqd": 1 + }, + { + "fieldname": "default_uom", + "fieldtype": "Link", + "label": "Default Unit of Measure", + "options": "UOM", + "reqd": 1 + }, + { + "fieldname": "default_item_tax_template", + "fieldtype": "Link", + "label": "Default Item Tax Template", + "options": "Item Tax Template" + }, + { + "fieldname": "default_party_settings_section", + "fieldtype": "Section Break", + "label": "Default Business Partner Settings" + }, + { + "fieldname": "default_supplier_group", + "fieldtype": "Link", + "label": "Default Supplier Group", + "options": "Supplier Group" + }, + { + "fieldname": "default_customer_group", + "fieldtype": "Link", + "label": "Default Customer Group", + "options": "Customer Group" + }, + { + "fieldname": "default_payment_terms", + "fieldtype": "Link", + "label": "Default Payment Terms", + "options": "Payment Terms Template" + }, + { + "fieldname": "item_mappings_section", + "fieldtype": "Section Break", + "label": "Item Mappings" + }, + { + "fieldname": "item_mappings", + "fieldtype": "Table", + "label": "Item Mappings", + "options": "E-Taxes Item Mapping" + }, + { + "fieldname": "party_mappings_section", + "fieldtype": "Section Break", + "label": "Business Partner Mappings" + }, + { + "fieldname": "party_mappings", + "fieldtype": "Table", + "label": "Business Partner Mappings", + "options": "E-Taxes Party Mapping" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-05-01 12:00:00.000000", + "modified_by": "Administrator", + "module": "Invoice Az", + "name": "E-Taxes Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 + } \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.py b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.py new file mode 100644 index 0000000..547f504 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_settings/e_taxes_settings.py @@ -0,0 +1,805 @@ +import frappe +import json +import re +import difflib +from frappe.utils import now, cstr, cint, flt, get_datetime +from frappe.model.document import Document + +class ETaxesSettings(Document): + pass + +# Функция для получения настроек E-Taxes по умолчанию +@frappe.whitelist() +def get_default_settings(): + """Получение настроек E-Taxes по умолчанию""" + try: + # Получаем настройки E-Taxes + settings_list = frappe.get_all('E-Taxes Settings', + filters={'is_default': 1, 'is_active': 1}, + limit=1) + + if settings_list: + settings = frappe.get_doc('E-Taxes Settings', settings_list[0].name) + return { + 'found': True, + 'name': settings.name, + 'similarity_threshold': settings.similarity_threshold, + 'consider_azeri_chars': settings.consider_azeri_chars + } + else: + # Если настройки не найдены, пробуем взять любые доступные + all_settings = frappe.get_all('E-Taxes Settings', + filters={'is_active': 1}, + limit=1) + if all_settings: + settings = frappe.get_doc('E-Taxes Settings', all_settings[0].name) + return { + 'found': True, + 'name': settings.name, + 'similarity_threshold': settings.similarity_threshold, + 'consider_azeri_chars': settings.consider_azeri_chars + } + else: + return { + 'found': False, + 'message': 'No E-Taxes Settings found' + } + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error getting E-Taxes Settings') + return { + 'found': False, + 'message': 'Error: {0}'.format(str(e)) + } + +# Функция нормализации азербайджанских символов +def normalize_azeri_chars(text): + if not text: + return text + + # Азербайджанские буквы и их аналоги + azeri_chars = { + 'ə': 'e', # также может быть 'a' + 'ü': 'u', + 'ö': 'o', + 'ğ': 'g', + 'ı': 'i', + 'ç': 'c', + 'ş': 's' # также может быть 'sh' + } + + # Нормализуем текст + normalized = '' + for char in text: + if char.lower() in azeri_chars: + normalized += azeri_chars[char.lower()] if char.islower() else azeri_chars[char.lower()].upper() + else: + normalized += char + + return normalized + +# Функция для расчета схожести строк с учетом азербайджанских символов +def calculate_similarity(str1, str2, consider_azeri=True): + """Расчет процента схожести двух строк""" + if not str1 or not str2: + return 0 + + # Нормализуем строки (приводим к нижнему регистру) + str1 = str1.lower() + str2 = str2.lower() + + # Если нужно учитывать азербайджанские символы + if consider_azeri: + str1_normalized = normalize_azeri_chars(str1) + str2_normalized = normalize_azeri_chars(str2) + + # Рассчитываем схожесть между оригинальными и нормализованными строками + sim1 = difflib.SequenceMatcher(None, str1, str2).ratio() + sim2 = difflib.SequenceMatcher(None, str1_normalized, str2).ratio() + sim3 = difflib.SequenceMatcher(None, str1, str2_normalized).ratio() + sim4 = difflib.SequenceMatcher(None, str1_normalized, str2_normalized).ratio() + + # Берем максимальную схожесть + similarity = max(sim1, sim2, sim3, sim4) + else: + # Рассчитываем схожесть только между оригинальными строками + similarity = difflib.SequenceMatcher(None, str1, str2).ratio() + + return similarity * 100 # Возвращаем в процентах + +# Функция для поиска соответствий товаров +@frappe.whitelist() +def find_matching_items(invoice_items, settings_name): + """Поиск соответствий для товаров из инвойса""" + try: + if not invoice_items or not settings_name: + return { + 'success': False, + 'message': 'Missing required parameters' + } + + # Получаем настройки + settings = frappe.get_doc('E-Taxes Settings', settings_name) + + # Если параметр invoice_items передан как строка, преобразуем в объект + if isinstance(invoice_items, str): + invoice_items = json.loads(invoice_items) + + # Получаем список всех товаров в системе + all_items = frappe.get_all('Item', fields=['name', 'item_name', 'item_code']) + + # Получаем существующие сопоставления из настроек + item_mappings = {(m.etaxes_item_name, m.etaxes_item_code): m.erp_item for m in settings.item_mappings} + + # Результаты сопоставления + matched_items = [] + unmatched_items = [] + suggested_matches = [] + + # Проходим по каждому товару из инвойса + for item in invoice_items: + item_name = item.get('productName') or item.get('etaxes_item_name') + item_code = item.get('itemId') or item.get('etaxes_item_code') + + if not item_name: + continue + + # Проверяем, есть ли прямое соответствие в сопоставлениях + if (item_name, item_code) in item_mappings: + matched_items.append({ + 'etaxes_item_name': item_name, + 'etaxes_item_code': item_code, + 'erp_item': item_mappings[(item_name, item_code)], + 'mapping_type': 'manual' + }) + continue + + # Проверяем, есть ли товар с таким же именем или кодом в системе + direct_match = False + for erp_item in all_items: + if (erp_item.item_name == item_name or + (item_code and erp_item.item_code == item_code)): + matched_items.append({ + 'etaxes_item_name': item_name, + 'etaxes_item_code': item_code, + 'erp_item': erp_item.name, + 'mapping_type': 'auto' + }) + direct_match = True + break + + if direct_match: + continue + + # Ищем похожие товары + similar_items = [] + for erp_item in all_items: + similarity = calculate_similarity( + item_name, + erp_item.item_name, + settings.consider_azeri_chars + ) + + if similarity >= settings.similarity_threshold: + similar_items.append({ + 'item': erp_item.name, + 'similarity': similarity / 100 # Переводим обратно в дробь + }) + + # Сортируем похожие товары по схожести + similar_items.sort(key=lambda x: x['similarity'], reverse=True) + + # Если найдены похожие товары, добавляем их в предложения + if similar_items: + suggested_matches.append({ + 'etaxes_item_name': item_name, + 'etaxes_item_code': item_code, + 'similar_items': similar_items[:5] # Берем топ-5 похожих товаров + }) + else: + # Если не найдены похожие товары, добавляем в несопоставленные + unmatched_items.append({ + 'etaxes_item_name': item_name, + 'etaxes_item_code': item_code + }) + + return { + 'success': True, + 'result': { + 'matched_items': matched_items, + 'unmatched_items': unmatched_items, + 'suggested_matches': suggested_matches + } + } + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error finding matching items') + return { + 'success': False, + 'message': 'Error: {0}'.format(str(e)) + } + +# Функция для поиска соответствий контрагентов +@frappe.whitelist() +def find_matching_parties(invoice_data, settings_name): + """Поиск соответствий для контрагентов из инвойса""" + try: + if not invoice_data or not settings_name: + return { + 'success': False, + 'message': 'Missing required parameters' + } + + # Получаем настройки + settings = frappe.get_doc('E-Taxes Settings', settings_name) + + # Если параметр invoice_data передан как строка, преобразуем в объект + if isinstance(invoice_data, str): + invoice_data = json.loads(invoice_data) + + # Получаем данные отправителя и получателя из инвойса + sender_name = invoice_data.get('senderName') or (invoice_data.get('sender', {}) or {}).get('name') + sender_tin = invoice_data.get('senderTin') or (invoice_data.get('sender', {}) or {}).get('tin') + + receiver_name = invoice_data.get('receiverName') or (invoice_data.get('receiver', {}) or {}).get('name') + receiver_tin = invoice_data.get('receiverTin') or (invoice_data.get('receiver', {}) or {}).get('tin') + + # Результаты сопоставления + result = { + 'sender': { + 'etaxes_party_name': sender_name, + 'etaxes_tax_id': sender_tin, + 'matched': False + }, + 'receiver': { + 'etaxes_party_name': receiver_name, + 'etaxes_tax_id': receiver_tin, + 'matched': False + } + } + + # Проверяем соответствие отправителя + if sender_name and sender_tin: + # Проверяем существующие сопоставления + for mapping in settings.party_mappings: + if (mapping.etaxes_party_name == sender_name or + (sender_tin and mapping.etaxes_tax_id == sender_tin)): + result['sender'].update({ + 'matched': True, + 'party_type': mapping.party_type, + 'party': mapping.erp_party + }) + break + + # Если не найдено в сопоставлениях, ищем напрямую + if not result['sender']['matched']: + # Поиск среди поставщиков + suppliers = frappe.get_all('Supplier', + fields=['name', 'tax_id'], + filters=[ + ['name', '=', sender_name], + ['tax_id', '=', sender_tin] + ]) + if suppliers: + result['sender'].update({ + 'matched': True, + 'party_type': 'Supplier', + 'party': suppliers[0].name + }) + else: + # Поиск среди клиентов + customers = frappe.get_all('Customer', + fields=['name', 'tax_id'], + filters=[ + ['name', '=', sender_name], + ['tax_id', '=', sender_tin] + ]) + if customers: + result['sender'].update({ + 'matched': True, + 'party_type': 'Customer', + 'party': customers[0].name + }) + + # Проверяем соответствие получателя + if receiver_name and receiver_tin: + # Проверяем существующие сопоставления + for mapping in settings.party_mappings: + if (mapping.etaxes_party_name == receiver_name or + (receiver_tin and mapping.etaxes_tax_id == receiver_tin)): + result['receiver'].update({ + 'matched': True, + 'party_type': mapping.party_type, + 'party': mapping.erp_party + }) + break + + # Если не найдено в сопоставлениях, ищем напрямую + if not result['receiver']['matched']: + # Поиск среди поставщиков + suppliers = frappe.get_all('Supplier', + fields=['name', 'tax_id'], + filters=[ + ['name', '=', receiver_name], + ['tax_id', '=', receiver_tin] + ]) + if suppliers: + result['receiver'].update({ + 'matched': True, + 'party_type': 'Supplier', + 'party': suppliers[0].name + }) + else: + # Поиск среди клиентов + customers = frappe.get_all('Customer', + fields=['name', 'tax_id'], + filters=[ + ['name', '=', receiver_name], + ['tax_id', '=', receiver_tin] + ]) + if customers: + result['receiver'].update({ + 'matched': True, + 'party_type': 'Customer', + 'party': customers[0].name + }) + + return { + 'success': True, + 'result': result + } + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error finding matching parties') + return { + 'success': False, + 'message': 'Error: {0}'.format(str(e)) + } + +# Функция для добавления сопоставления товара +@frappe.whitelist() +def add_item_mapping(settings_name, etaxes_item_name, etaxes_item_code, erp_item): + """Добавление сопоставления товара в настройки""" + try: + if not settings_name or not etaxes_item_name or not erp_item: + return { + 'success': False, + 'message': 'Missing required parameters' + } + + # Получаем документ настроек + settings = frappe.get_doc('E-Taxes Settings', settings_name) + + # Проверяем, существует ли уже такое сопоставление + for mapping in settings.item_mappings: + if mapping.etaxes_item_name == etaxes_item_name and mapping.etaxes_item_code == etaxes_item_code: + # Обновляем существующее сопоставление + mapping.erp_item = erp_item + settings.save() + return { + 'success': True, + 'message': 'Item mapping updated successfully' + } + + # Если сопоставление не найдено, создаем новое + mapping = settings.append('item_mappings', { + 'etaxes_item_name': etaxes_item_name, + 'etaxes_item_code': etaxes_item_code, + 'erp_item': erp_item, + 'mapping_type': 'Ручное' + }) + + settings.save() + + # Обновляем статус в E-Taxes Item, если такие записи существуют + items = frappe.get_all('E-Taxes Item', + filters={ + 'etaxes_item_name': etaxes_item_name, + 'etaxes_item_code': etaxes_item_code + }) + + for item in items: + frappe.db.set_value('E-Taxes Item', item.name, { + 'status': 'Сопоставлен', + 'mapped_item': erp_item + }) + + return { + 'success': True, + 'message': 'Item mapping added successfully' + } + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error adding item mapping') + return { + 'success': False, + 'message': 'Error: {0}'.format(str(e)) + } + +# Функция для добавления сопоставления контрагента +@frappe.whitelist() +def add_party_mapping(settings_name, etaxes_party_name, etaxes_tax_id, party_type, erp_party): + """Добавление сопоставления контрагента в настройки""" + try: + if not settings_name or not etaxes_party_name or not party_type or not erp_party: + return { + 'success': False, + 'message': 'Missing required parameters' + } + + # Получаем документ настроек + settings = frappe.get_doc('E-Taxes Settings', settings_name) + + # Проверяем, существует ли уже такое сопоставление + for mapping in settings.party_mappings: + if mapping.etaxes_party_name == etaxes_party_name and mapping.etaxes_tax_id == etaxes_tax_id: + # Обновляем существующее сопоставление + mapping.party_type = party_type + mapping.erp_party = erp_party + settings.save() + return { + 'success': True, + 'message': 'Party mapping updated successfully' + } + + # Если сопоставление не найдено, создаем новое + mapping = settings.append('party_mappings', { + 'etaxes_party_name': etaxes_party_name, + 'etaxes_tax_id': etaxes_tax_id, + 'party_type': party_type, + 'erp_party': erp_party, + 'mapping_type': 'Ручное' + }) + + settings.save() + + # Обновляем статус в E-Taxes Parties, если такие записи существуют + parties = frappe.get_all('E-Taxes Parties', + filters={ + 'etaxes_party_name': etaxes_party_name, + 'etaxes_tax_id': etaxes_tax_id + }) + + for party in parties: + frappe.db.set_value('E-Taxes Parties', party.name, { + 'status': 'Сопоставлен', + 'erp_party_type': party_type, + 'mapped_party': erp_party + }) + + return { + 'success': True, + 'message': 'Party mapping added successfully' + } + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error adding party mapping') + return { + 'success': False, + 'message': 'Error: {0}'.format(str(e)) + } + +# Функция для автоматического сопоставления по похожим названиям +@frappe.whitelist() +def auto_match_by_name(settings_name): + """Автоматическое сопоставление товаров и контрагентов по похожим названиям""" + try: + if not settings_name: + return { + 'success': False, + 'message': 'Missing required parameter' + } + + # Получаем настройки + settings = frappe.get_doc('E-Taxes Settings', settings_name) + + # Получаем все несопоставленные товары + unmatched_items = frappe.get_all('E-Taxes Item', + filters={'status': 'Новый'}, + fields=['name', 'etaxes_item_name', 'etaxes_item_code']) + + # Получаем все товары в системе + all_items = frappe.get_all('Item', fields=['name', 'item_name', 'item_code']) + + # Счетчики для результата + items_matched = 0 + + # Проходим по каждому несопоставленному товару + for item in unmatched_items: + best_match = None + best_similarity = 0 + + # Ищем наиболее похожий товар + for erp_item in all_items: + similarity = calculate_similarity( + item.etaxes_item_name, + erp_item.item_name, + settings.consider_azeri_chars + ) + + if similarity >= settings.similarity_threshold and similarity > best_similarity: + best_match = erp_item.name + best_similarity = similarity + + # Если найден подходящий товар, создаем сопоставление + if best_match: + # Добавляем сопоставление в настройки + mapping = settings.append('item_mappings', { + 'etaxes_item_name': item.etaxes_item_name, + 'etaxes_item_code': item.etaxes_item_code, + 'erp_item': best_match, + 'mapping_type': 'Автоматическое' + }) + + # Обновляем статус товара + frappe.db.set_value('E-Taxes Item', item.name, { + 'status': 'Сопоставлен', + 'mapped_item': best_match + }) + + items_matched += 1 + + # Получаем все несопоставленные контрагенты + unmatched_parties = frappe.get_all('E-Taxes Parties', + filters={'status': 'Новый'}, + fields=['name', 'etaxes_party_name', 'etaxes_tax_id', 'etaxes_party_type']) + + # Получаем всех поставщиков и клиентов в системе + all_suppliers = frappe.get_all('Supplier', fields=['name', 'tax_id']) + all_customers = frappe.get_all('Customer', fields=['name', 'tax_id']) + + # Счетчик для результата + parties_matched = 0 + + # Проходим по каждому несопоставленному контрагенту + for party in unmatched_parties: + best_match = None + best_type = None + best_similarity = 0 + + # Пытаемся найти по налоговому номеру + if party.etaxes_tax_id: + # Ищем среди поставщиков + for supplier in all_suppliers: + if supplier.tax_id and supplier.tax_id == party.etaxes_tax_id: + best_match = supplier.name + best_type = 'Supplier' + break + + # Если не нашли среди поставщиков, ищем среди клиентов + if not best_match: + for customer in all_customers: + if customer.tax_id and customer.tax_id == party.etaxes_tax_id: + best_match = customer.name + best_type = 'Customer' + break + + # Если не нашли по налоговому номеру, ищем по названию + if not best_match: + # Ищем среди поставщиков + for supplier in all_suppliers: + similarity = calculate_similarity( + party.etaxes_party_name, + supplier.name, + settings.consider_azeri_chars + ) + + if similarity >= settings.similarity_threshold and similarity > best_similarity: + best_match = supplier.name + best_type = 'Supplier' + best_similarity = similarity + + # Ищем среди клиентов + for customer in all_customers: + similarity = calculate_similarity( + party.etaxes_party_name, + customer.name, + settings.consider_azeri_chars + ) + + if similarity >= settings.similarity_threshold and similarity > best_similarity: + best_match = customer.name + best_type = 'Customer' + best_similarity = similarity + + # Если найден подходящий контрагент, создаем сопоставление + if best_match and best_type: + # Добавляем сопоставление в настройки + mapping = settings.append('party_mappings', { + 'etaxes_party_name': party.etaxes_party_name, + 'etaxes_tax_id': party.etaxes_tax_id, + 'party_type': best_type, + 'erp_party': best_match, + 'mapping_type': 'Автоматическое' + }) + + # Обновляем статус контрагента + frappe.db.set_value('E-Taxes Parties', party.name, { + 'status': 'Сопоставлен', + 'erp_party_type': best_type, + 'mapped_party': best_match + }) + + parties_matched += 1 + + # Сохраняем настройки + settings.save() + + return { + 'success': True, + 'items_matched': items_matched, + 'parties_matched': parties_matched, + 'message': 'Auto-matched {0} items and {1} parties'.format(items_matched, parties_matched) + } + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error in auto-matching by name') + return { + 'success': False, + 'message': 'Error: {0}'.format(str(e)) + } + +# Функция для извлечения товаров из инвойсов +@frappe.whitelist() +def extract_items_from_invoices(invoices, source_doc=None): + """Извлечение уникальных товаров из списка инвойсов""" + try: + if not invoices: + return { + 'success': False, + 'message': 'No invoices provided' + } + + # Если передано как строка, преобразуем в объект + if isinstance(invoices, str): + invoices = json.loads(invoices) + + # Хеш-таблица для хранения уникальных товаров + unique_items = {} + + # Проходим по каждому инвойсу + for invoice in invoices: + # Получаем список товаров из инвойса + items = invoice.get('items', []) + + # Проходим по каждому товару + for item in items: + # Создаем ключ для уникальности + item_name = item.get('productName', '') + item_code = item.get('itemId', '') + key = f"{item_name}|{item_code}" + + # Если товар еще не добавлен, добавляем его + if key not in unique_items: + unique_items[key] = { + 'etaxes_item_name': item_name, + 'etaxes_item_code': item_code, + 'etaxes_unit': item.get('unit', ''), + 'etaxes_price': item.get('pricePerUnit', 0), + 'source_invoice': invoice.get('serialNumber', '') + } + + # Создаем записи в E-Taxes Item + items_created = 0 + for key, item_data in unique_items.items(): + # Проверяем, существует ли уже такой товар + existing_items = frappe.get_all('E-Taxes Item', + filters={ + 'etaxes_item_name': item_data['etaxes_item_name'], + 'etaxes_item_code': item_data['etaxes_item_code'] + }) + + if not existing_items: + # Создаем новый товар + item = frappe.new_doc('E-Taxes Item') + item.update(item_data) + item.insert() + items_created += 1 + + return { + 'success': True, + 'items_created': items_created, + 'message': 'Extracted and created {0} unique items'.format(items_created) + } + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error extracting items from invoices') + return { + 'success': False, + 'message': 'Error: {0}'.format(str(e)) + } + +# Функция для извлечения контрагентов из инвойсов +@frappe.whitelist() +def extract_parties_from_invoices(invoices, source_doc=None): + """Извлечение уникальных контрагентов из списка инвойсов""" + try: + if not invoices: + return { + 'success': False, + 'message': 'No invoices provided' + } + + # Если передано как строка, преобразуем в объект + if isinstance(invoices, str): + invoices = json.loads(invoices) + + # Хеш-таблицы для хранения уникальных контрагентов + unique_senders = {} + unique_receivers = {} + + # Проходим по каждому инвойсу + for invoice in invoices: + # Получаем данные отправителя + sender_name = invoice.get('senderName') or (invoice.get('sender', {}) or {}).get('name') + sender_tin = invoice.get('senderTin') or (invoice.get('sender', {}) or {}).get('tin') + + if sender_name and sender_tin: + key = f"{sender_name}|{sender_tin}" + + # Если отправитель еще не добавлен, добавляем его + if key not in unique_senders: + unique_senders[key] = { + 'etaxes_party_name': sender_name, + 'etaxes_tax_id': sender_tin, + 'etaxes_party_type': 'Отправитель', + 'etaxes_address': (invoice.get('sender', {}) or {}).get('address', ''), + 'source_invoice': invoice.get('serialNumber', '') + } + + # Получаем данные получателя + receiver_name = invoice.get('receiverName') or (invoice.get('receiver', {}) or {}).get('name') + receiver_tin = invoice.get('receiverTin') or (invoice.get('receiver', {}) or {}).get('tin') + + if receiver_name and receiver_tin: + key = f"{receiver_name}|{receiver_tin}" + + # Если получатель еще не добавлен, добавляем его + if key not in unique_receivers: + unique_receivers[key] = { + 'etaxes_party_name': receiver_name, + 'etaxes_tax_id': receiver_tin, + 'etaxes_party_type': 'Получатель', + 'etaxes_address': (invoice.get('receiver', {}) or {}).get('address', ''), + 'source_invoice': invoice.get('serialNumber', '') + } + + # Создаем записи в E-Taxes Parties + parties_created = 0 + + # Создаем отправителей + for key, party_data in unique_senders.items(): + # Проверяем, существует ли уже такой контрагент + existing_parties = frappe.get_all('E-Taxes Parties', + filters={ + 'etaxes_party_name': party_data['etaxes_party_name'], + 'etaxes_tax_id': party_data['etaxes_tax_id'] + }) + + if not existing_parties: + # Создаем нового контрагента + party = frappe.new_doc('E-Taxes Parties') + party.update(party_data) + party.insert() + parties_created += 1 + + # Создаем получателей + for key, party_data in unique_receivers.items(): + # Проверяем, существует ли уже такой контрагент + existing_parties = frappe.get_all('E-Taxes Parties', + filters={ + 'etaxes_party_name': party_data['etaxes_party_name'], + 'etaxes_tax_id': party_data['etaxes_tax_id'] + }) + + if not existing_parties: + # Создаем нового контрагента + party = frappe.new_doc('E-Taxes Parties') + party.update(party_data) + party.insert() + parties_created += 1 + + return { + 'success': True, + 'parties_created': parties_created, + 'message': 'Extracted and created {0} unique parties'.format(parties_created) + } + except Exception as e: + frappe.log_error(frappe.get_traceback(), 'Error extracting parties from invoices') + return { + 'success': False, + 'message': 'Error: {0}'.format(str(e)) + } diff --git a/invoice_az/invoice_az/doctype/e_taxes_settings/test_e_taxes_settings.py b/invoice_az/invoice_az/doctype/e_taxes_settings/test_e_taxes_settings.py new file mode 100644 index 0000000..95abbfe --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_settings/test_e_taxes_settings.py @@ -0,0 +1,30 @@ +# Copyright (c) 2025, Jey ERP and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record depdendencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class UnitTestETaxesSettings(UnitTestCase): + """ + Unit tests for ETaxesSettings. + Use this class for testing individual functions and methods. + """ + + pass + + +class IntegrationTestETaxesSettings(IntegrationTestCase): + """ + Integration tests for ETaxesSettings. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/invoice_az/invoice_az/doctype/testapi/__init__.py b/invoice_az/invoice_az/doctype/testapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/invoice_az/invoice_az/doctype/testapi/test_testapi.py b/invoice_az/invoice_az/doctype/testapi/test_testapi.py new file mode 100644 index 0000000..9024f32 --- /dev/null +++ b/invoice_az/invoice_az/doctype/testapi/test_testapi.py @@ -0,0 +1,30 @@ +# Copyright (c) 2025, Jey ERP and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase, UnitTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record depdendencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class UnitTestTestApi(UnitTestCase): + """ + Unit tests for TestApi. + Use this class for testing individual functions and methods. + """ + + pass + + +class IntegrationTestTestApi(IntegrationTestCase): + """ + Integration tests for TestApi. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/invoice_az/invoice_az/doctype/testapi/testapi.js b/invoice_az/invoice_az/doctype/testapi/testapi.js new file mode 100644 index 0000000..1c2e7df --- /dev/null +++ b/invoice_az/invoice_az/doctype/testapi/testapi.js @@ -0,0 +1,942 @@ +frappe.ui.form.on('TestApi', { + refresh: function(frm) { + // Остановить любые существующие интервалы + if (window.authStatusInterval) { + clearInterval(window.authStatusInterval); + window.authStatusInterval = null; + } + + // Кнопка для получения токена + frm.add_custom_button(__('Login with Asan Imza'), function() { + // Проверяем, что поля phone и user_id заполнены + if (!frm.doc.phone || !frm.doc.user_id) { + frappe.msgprint(__('Please fill in Phone and User ID fields')); + return; + } + + // Показываем сообщение о том, что запрос отправлен + frappe.show_alert({ + message: __('Authentication request sent. Please confirm on your phone.'), + indicator: 'blue' + }, 5); + + // Отправляем запрос для начала процесса аутентификации + frappe.call({ + method: 'invoice_az.invoice_az.doctype.testapi.testapi.get_auth_token', + args: { + 'phone': frm.doc.phone, + 'user_id': frm.doc.user_id + }, + callback: function(r) { + if (r.message) { + // Логируем полный ответ для отладки + console.log('Auth Response:', r.message); + + // Временно сохраняем токен если он есть, но не устанавливаем статус Authenticated + const bearer_token = r.message.bearer_token || ''; + if (bearer_token) { + // Сохраняем токен во временном хранилище + frm.set_value('bearer_token', bearer_token); + window.tempBearerToken = bearer_token; + } + + // Устанавливаем статус и сохраняем ОДИН раз + frm.set_value('auth_status', 'Waiting for confirmation'); + + // Сохраняем форму и после успешного сохранения запускаем опрос + frm.save() + .then(() => { + console.log('Form saved successfully, starting polling'); + startPollingWithTimeout(frm); // Используем новую функцию вместо setInterval + }) + .catch(err => { + console.error('Error saving form:', err); + frappe.msgprint(__('Error saving form. Please try again.')); + }); + } + } + }); + }, __('Login')); + + // Тестовая кнопка для проверки токена напрямую + frm.add_custom_button(__('Test Auth Status'), function() { + if (!frm.doc.bearer_token) { + frappe.msgprint(__('No bearer token available. Please login first.')); + return; + } + + console.log('Manual test of check_auth_status with token:', frm.doc.bearer_token); + frappe.call({ + method: 'invoice_az.invoice_az.doctype.testapi.testapi.check_auth_status', + args: { + 'bearer_token': frm.doc.bearer_token + }, + callback: function(r) { + console.log('Test status response:', r); + if (r.message) { + console.log('Status data:', r.message); + frappe.msgprint(__('Status check successful. See console for details.')); + } + }, + error: function(err) { + console.error('Error in test status check:', err); + frappe.msgprint(__('Error checking status. See console for details.')); + } + }); + }, __('Debug')); + + // Кнопка для получения сертификатов (доступна только если авторизация успешна) + if (frm.doc.auth_status === 'Authenticated' && frm.doc.bearer_token) { + frm.add_custom_button(__('Get Certificates'), function() { + frappe.call({ + method: 'invoice_az.invoice_az.doctype.testapi.testapi.get_certificates', + args: { + 'bearer_token': frm.doc.bearer_token + }, + callback: function(r) { + if (r.message && r.message.certificates) { + // Сохраняем сертификаты в поле формы в формате JSON + const certificates = r.message.certificates; + frm.set_value('certificates_json', JSON.stringify(certificates, null, 2)); + + // Сохраняем форму + frm.save(); + + // Показываем сообщение об успешном получении сертификатов + frappe.msgprint({ + title: __('Success'), + indicator: 'green', + message: __('Certificates retrieved successfully') + }); + + // Отображаем список сертификатов в диалоговом окне + showCertificatesDialog(certificates); + } else { + frappe.msgprint(__('No certificates found')); + } + } + }); + }, __('Actions')); + } + + // Кнопка для выбора налогоплательщика (доступна если авторизация успешна и сертификат выбран) + if (frm.doc.auth_status === 'Authenticated' && frm.doc.bearer_token && frm.doc.selected_certificate_json) { + frm.add_custom_button(__('Choose Taxpayer'), function() { + try { + // Парсим данные выбранного сертификата + const certData = JSON.parse(frm.doc.selected_certificate_json); + + // Определяем тип и идентификатор налогоплательщика + let ownerType = certData.taxpayerType || ''; + let tin = ''; + + if (ownerType === 'individual' && certData.individualInfo) { + tin = certData.individualInfo.fin || ''; + } else if (certData.legalInfo) { + ownerType = 'legal'; + // Используем tin вместо voen + tin = certData.legalInfo.tin || ''; // Здесь было certData.legalInfo.voen + } + + // Показываем сообщение о процессе + frappe.show_alert({ + message: __('Choosing taxpayer, please wait...'), + indicator: 'blue' + }, 5); + + // Отправляем запрос для выбора налогоплательщика + frappe.call({ + method: 'invoice_az.invoice_az.doctype.testapi.testapi.choose_taxpayer', + args: { + 'bearer_token': frm.doc.bearer_token, + 'owner_type': ownerType, + 'tin': tin + }, + callback: function(r) { + if (r.message && r.message.main_token) { + // Сохраняем основной токен + frm.set_value('main_token', r.message.main_token); + + // Сохраняем ответ в JSON формате + frm.set_value('choose_taxpayer_response', + JSON.stringify(r.message.response_data, null, 2)); + + // Обновляем статус авторизации + frm.set_value('auth_status', 'Fully Authenticated'); + + // Сохраняем форму + frm.save().then(() => { + frappe.show_alert({ + message: __('Taxpayer chosen successfully. Main token received.'), + indicator: 'green' + }, 5); + + // Перезагружаем страницу для обновления кнопок + frm.reload_doc(); + }); + } else { + frappe.msgprint(__('Failed to choose taxpayer. No main token received.')); + } + }, + error: function(err) { + console.error('Error choosing taxpayer:', err); + frappe.msgprint(__('Error choosing taxpayer. See console for details.')); + } + }); + } catch (e) { + console.error('Error parsing certificate data:', e); + frappe.msgprint(__('Error parsing certificate data')); + } + }, __('Actions')); + } + + // Кнопка для получения счетов (доступна если хотя бы базовая авторизация успешна) + if ((frm.doc.auth_status === 'Authenticated' || frm.doc.auth_status === 'Fully Authenticated') && + (frm.doc.bearer_token || frm.doc.main_token)) { + + frm.add_custom_button(__('Get Invoices'), function() { + // Открываем диалог для настройки фильтров + showInvoiceFilterDialog(frm); + }, __('Actions')); + } + }, + + // Обработчик изменения выбранного сертификата (введенного вручную) + selected_certificate: function(frm) { + // Только если поле не пустое и сертификаты доступны + if (frm.doc.selected_certificate && frm.doc.certificates_json) { + try { + // Получаем данные сертификатов + const certificates = JSON.parse(frm.doc.certificates_json); + if (!Array.isArray(certificates) || certificates.length === 0) return; + + // Ищем сертификат по имени или его части + const searchStr = frm.doc.selected_certificate.toLowerCase(); + + // Сначала пытаемся найти точное совпадение по имени и идентификатору + let foundCert = null; + let foundIndex = -1; + + for (let i = 0; i < certificates.length; i++) { + const cert = certificates[i]; + let certName = ''; + let certId = ''; + + if (cert.taxpayerType === 'individual' && cert.individualInfo) { + certName = cert.individualInfo.name || ''; + certId = cert.individualInfo.fin || ''; + } else if (cert.legalInfo) { + certName = cert.legalInfo.name || ''; + certId = cert.legalInfo.voen || ''; + } + + const fullName = `${certName} (${certId})`.toLowerCase(); + + // Проверяем точное совпадение или содержание строки поиска + if (fullName === searchStr || fullName.includes(searchStr)) { + foundCert = cert; + foundIndex = i; + break; + } + } + + // Если нашли совпадение, устанавливаем данные сертификата + if (foundCert) { + frm.set_value('selected_certificate_index', foundIndex); + frm.set_value('selected_certificate_json', JSON.stringify(foundCert, null, 2)); + + frappe.show_alert({ + message: __('Certificate found and selected'), + indicator: 'green' + }, 3); + } + } catch (e) { + console.error('Error processing certificate selection:', e); + } + } + }, + + // После того, как форма загружена + after_load: function(frm) { + // Обрабатываем случай, когда сертификат уже выбран, но данные JSON не установлены + if (frm.doc.selected_certificate && !frm.doc.selected_certificate_json && frm.doc.certificates_json) { + try { + const certificates = JSON.parse(frm.doc.certificates_json); + const searchStr = frm.doc.selected_certificate.toLowerCase(); + + for (let i = 0; i < certificates.length; i++) { + const cert = certificates[i]; + let certName = ''; + let certId = ''; + + if (cert.taxpayerType === 'individual' && cert.individualInfo) { + certName = cert.individualInfo.name || ''; + certId = cert.individualInfo.fin || ''; + } else if (cert.legalInfo) { + certName = cert.legalInfo.name || ''; + certId = cert.legalInfo.voen || ''; + } + + const fullName = `${certName} (${certId})`.toLowerCase(); + + if (fullName === searchStr || fullName.includes(searchStr)) { + frm.set_value('selected_certificate_index', i); + frm.set_value('selected_certificate_json', JSON.stringify(cert, null, 2)); + break; + } + } + } catch (e) { + console.error('Error auto-selecting certificate on load:', e); + } + } + } +}); + +// Показать диалог для настройки фильтров счетов +function showInvoiceFilterDialog(frm) { + // Используем параметры из оригинального запроса + const startDate = "01-01-2020 00:00"; // Начиная с 2020 года вместо текущего + const endDate = moment().add(1, 'year').endOf('year').format('DD-MM-YYYY HH:mm'); // Год вперед + + // Создаем диалог с полями для фильтрации + var d = new frappe.ui.Dialog({ + title: __('Invoice Filters'), + fields: [ + { + fieldname: 'date_range_section', + fieldtype: 'Section Break', + label: __('Date Range') + }, + { + fieldname: 'creationDateFrom', + fieldtype: 'Data', // Используем Data вместо Datetime для точного формата + label: __('From Date'), + default: startDate + }, + { + fieldname: 'creationDateTo', + fieldtype: 'Data', // Используем Data вместо Datetime для точного формата + label: __('To Date'), + default: endDate + }, + { + fieldname: 'invoice_details_section', + fieldtype: 'Section Break', + label: __('Invoice Details') + }, + { + fieldname: 'serialNumber', + fieldtype: 'Data', + label: __('Serial Number') + }, + { + fieldname: 'party_section', + fieldtype: 'Section Break', + label: __('Sender/Receiver') + }, + { + fieldname: 'senderTin', + fieldtype: 'Data', + label: __('Sender TIN') + }, + { + fieldname: 'senderName', + fieldtype: 'Data', + label: __('Sender Name') + }, + { + fieldname: 'receiverTin', + fieldtype: 'Data', + label: __('Receiver TIN') + }, + { + fieldname: 'receiverName', + fieldtype: 'Data', + label: __('Receiver Name') + }, + { + fieldname: 'amount_section', + fieldtype: 'Section Break', + label: __('Amount') + }, + { + fieldname: 'amountFrom', + fieldtype: 'Float', + label: __('Amount From'), + precision: 2 + }, + { + fieldname: 'amountTo', + fieldtype: 'Float', + label: __('Amount To'), + precision: 2 + }, + { + fieldname: 'limit_section', + fieldtype: 'Section Break', + label: __('Limit') + }, + { + fieldname: 'maxCount', + fieldtype: 'Int', + label: __('Max Records'), + default: 200 // Увеличено с 50 до 200 + } + ], + primary_action_label: __('Get Invoices'), + primary_action: function() { + var values = d.get_values(); + + // Используем без преобразования формата + // так как мы уже предоставляем правильный формат в полях Data + + // Дополняем значения фильтров полными параметрами из оригинального запроса + const fullPayload = { + "sortBy": "creationDate", + "sortAsc": true, + "statuses": ["approved", "onApproval", "updateApproval", "updateRequested", + "cancelRequested", "approvedBySystem", "onApprovalEdited", + "deactivated", "cancelationRefused", "correctionRefused"], + "types": ["current", "corrected"], + "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", + "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", + "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], + "offset": 0, + "actionOwner": null, + + // Заполняем значениями из диалога + "serialNumber": values.serialNumber || null, + "senderTin": values.senderTin || null, + "senderName": values.senderName || null, + "productName": null, + "productCode": null, + "receiverTin": values.receiverTin || null, + "receiverName": values.receiverName || null, + "creationDateFrom": values.creationDateFrom || startDate, + "creationDateTo": values.creationDateTo || endDate, + "amountFrom": values.amountFrom || null, + "amountTo": values.amountTo || null, + "maxCount": values.maxCount || 200 + }; + + // Используем основной токен, если он есть, иначе используем bearer_token + const token_to_use = frm.doc.main_token; + + // Закрываем диалог + d.hide(); + + // Показываем индикатор загрузки + frappe.show_alert({ + message: __('Fetching invoices, please wait...'), + indicator: 'blue' + }, 5); + + // Отправляем запрос для получения счетов с фильтрами + frappe.call({ + method: 'invoice_az.invoice_az.doctype.testapi.testapi.get_invoices', + args: { + 'token': token_to_use, + 'filters': JSON.stringify(fullPayload) + }, + callback: function(r) { + if (r.message) { + // Сохраняем результат в поле формы + frm.set_value('invoices_json', JSON.stringify(r.message, null, 2)); + frm.save(); + + // Показываем сообщение об успешном получении счетов + frappe.msgprint({ + title: __('Success'), + indicator: 'green', + message: __('Invoices retrieved successfully') + }); + + // Проверяем различные возможные структуры данных + let invoicesData = null; + if (r.message.data && r.message.data.length) { + invoicesData = r.message.data; + } else if (r.message.invoices && r.message.invoices.length) { + // Получаем данные из поля invoices, если data отсутствует + invoicesData = r.message.invoices; + } + + // Отображаем список счетов в диалоговом окне + if (invoicesData && invoicesData.length) { + showInvoicesDialog(invoicesData); + } else { + frappe.msgprint(__('No invoices found matching the filters')); + } + } + }, + error: function(err) { + console.error('Error fetching invoices:', err); + frappe.msgprint(__('Error fetching invoices. See console for details.')); + } + }); + } + }); + + d.show(); +} + +// Функция для запуска опроса через setTimeout вместо setInterval +function startPollingWithTimeout(frm) { + console.log('startPollingWithTimeout function has been called'); + + // Отображаем индикатор загрузки + frm.page.set_indicator(__('Waiting for confirmation'), 'orange'); + + // Создаем всплывающее окно с информацией + const status_dialog = new frappe.ui.Dialog({ + title: __('Authentication Status'), + fields: [ + { + fieldname: 'status_html', + fieldtype: 'HTML', + options: ` +
+
+
+
+ +
+

Waiting for confirmation on your phone

+

Please check your phone and confirm the authentication request.

+
+
+ ` + } + ], + primary_action_label: __('Cancel'), + primary_action: function() { + // Остановить опрос + window.stopPolling = true; + status_dialog.hide(); + frm.page.clear_indicator(); + frm.reload_doc(); + } + }); + + status_dialog.show(); + + // Счетчик попыток и флаг для остановки опроса + let attempts = 0; + const maxAttempts = 20; // 2 минут (20 попыток с интервалом 5 секунд) + window.stopPolling = false; + + // Функция опроса + function pollAuthStatus() { + // Если достигнуто максимальное количество попыток или установлен флаг остановки + if (attempts >= maxAttempts || window.stopPolling) { + console.log('Polling stopped: ' + (attempts >= maxAttempts ? 'max attempts reached' : 'manually cancelled')); + + if (attempts >= maxAttempts) { + // Обновляем информацию в диалоговом окне по таймауту + $('#status_message').html(` +

Authentication Timeout

+

The authentication request has timed out. Please try again.

+ `); + $('.lds-dual-ring').css('display', 'none'); + + // Очищаем временное хранилище + window.tempBearerToken = null; + + // Обновляем поле auth_status + frm.set_value('auth_status', 'Failed'); + frm.save(); + + // Закрываем диалог через 3 секунды + setTimeout(function() { + status_dialog.hide(); + frm.reload_doc(); + }, 3000); + } + + return; + } + + attempts++; + console.log(`Polling auth status, attempt ${attempts} of ${maxAttempts}`); + + // Проверяем наличие токена + if (!frm.doc.bearer_token) { + console.error('No bearer token available for status check'); + setTimeout(pollAuthStatus, 5000); + return; + } + + // Проверяем статус + frappe.call({ + method: 'invoice_az.invoice_az.doctype.testapi.testapi.check_auth_status', + args: { + 'bearer_token': frm.doc.bearer_token + }, + callback: function(r) { + console.log('Status check callback received'); + + if (r.message) { + console.log('Status check response:', r.message); + + // Проверяем successful статус + if (r.message.successful === true) { + // Авторизация успешна + window.stopPolling = true; + + // Обновляем информацию в диалоговом окне + $('#status_message').html(` +

Authentication Successful!

+

You are now authenticated. The page will reload shortly.

+ `); + $('.lds-dual-ring').css('display', 'none'); + + const bearer_token = r.message.bearer_token || ''; + if (bearer_token) { + frm.set_value('bearer_token', bearer_token); + } + + // Обновляем поле auth_status + frm.set_value('auth_status', 'Authenticated'); + + // Сохраняем ОДИН раз после всех изменений полей + frm.save() + .then(() => { + // Закрываем диалоговое окно через 2 секунды после успешного сохранения + setTimeout(function() { + status_dialog.hide(); + frm.reload_doc(); + }, 2000); + }); + } else { + // Продолжаем опрос + setTimeout(pollAuthStatus, 5000); + } + } else { + // Если нет ответа, продолжаем опрос + setTimeout(pollAuthStatus, 5000); + } + }, + error: function(err) { + // В случае ошибки логируем её и продолжаем опрос + console.error('Error during status check:', err); + setTimeout(pollAuthStatus, 5000); + } + }); + } + + // Начинаем опрос + console.log('Starting first polling attempt'); + pollAuthStatus(); +} + +// Функция для отображения счетов-фактур в диалоговом окне +function showInvoicesDialog(invoices) { + if (!invoices || !invoices.length) { + frappe.msgprint(__('No invoices data to display')); + return; + } + + console.log("Displaying invoices:", invoices); // Для отладки + + // Создаем таблицу со счетами + var invoice_table = '
'; + invoice_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + + invoices.forEach(function(invoice) { + // Проверяем наличие необходимых полей в инвойсе + const serialNumber = invoice.serialNumber || invoice.number || ''; + const senderName = invoice.senderName || (invoice.sender ? invoice.sender.name : '') || ''; + const receiverName = invoice.receiverName || (invoice.receiver ? invoice.receiver.name : '') || ''; + const creationDate = invoice.creationDate || invoice.createDate || ''; + const amount = invoice.amount || 0; + const status = invoice.status || ''; + const id = invoice.id || invoice._id || ''; + + invoice_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }); + + invoice_table += '
Serial NumberSenderReceiverCreation DateAmountStatusActions
' + serialNumber + '' + senderName + '' + receiverName + '' + creationDate + '' + amount + '' + status + '
'; + + // Отображаем диалоговое окно с таблицей + var d = new frappe.ui.Dialog({ + title: __('Invoices List'), + fields: [{ + fieldtype: 'HTML', + fieldname: 'invoice_list', + options: invoice_table + }], + primary_action_label: __('Close'), + primary_action: function() { + d.hide(); + } + }); + + d.show(); + + // Добавляем обработчик для кнопки просмотра счета + d.$wrapper.find('.view-invoice').on('click', function() { + var invoice_id = $(this).data('id'); + showInvoiceDetails(invoice_id); + }); +} + +// Функция для отображения сертификатов в диалоговом окне +function showCertificatesDialog(certificates) { + if (!certificates || !certificates.length) return; + + // Создаем таблицу с сертификатами + var cert_table = '
'; + cert_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + + certificates.forEach(function(cert, index) { + var name = ''; + var id = ''; + + // Исправлено: используем cert вместо certData + if (cert.taxpayerType === 'individual' && cert.individualInfo) { + name = cert.individualInfo.name || ''; + id = cert.individualInfo.fin || ''; + } else if (cert.legalInfo) { + name = cert.legalInfo.name || ''; + id = cert.legalInfo.tin || cert.legalInfo.voen || ''; // Используем tin или voen + } + + cert_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }); + + cert_table += '
TypeNameIDPositionStatusAction
' + (cert.taxpayerType || '') + '' + name + '' + id + '' + (cert.position || '') + '' + (cert.hasAccess ? 'Active' : 'Inactive') + + (cert.liquidated ? ' (Liquidated)' : '') + '
'; + + // Отображаем диалоговое окно с таблицей + var d = new frappe.ui.Dialog({ + title: __('Certificates List'), + fields: [{ + fieldtype: 'HTML', + fieldname: 'certificate_list', + options: cert_table + }], + primary_action_label: __('Close'), + primary_action: function() { + d.hide(); + } + }); + + d.show(); + + // Добавляем обработчик для кнопки выбора сертификата + d.$wrapper.find('.select-cert').on('click', function() { + var index = $(this).data('index'); + var cert = certificates[index]; + + // Находим активную форму + var frm = cur_frm; + if (!frm) return; + + // Получаем имя для отображения + let certName = ''; + if (cert.taxpayerType === 'individual' && cert.individualInfo) { + certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`; + } else if (cert.legalInfo) { + certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`; + } else { + certName = `Certificate ${index + 1}`; + } + + // Устанавливаем значение в поле выбора и индекс + frm.set_value('selected_certificate', certName); + frm.set_value('selected_certificate_index', index); + + // Сохраняем выбранный сертификат в JSON поле + frm.set_value('selected_certificate_json', JSON.stringify(cert, null, 2)); + + frappe.show_alert({ + message: __('Certificate selected'), + indicator: 'green' + }, 3); + + d.hide(); + + // Перезагружаем страницу для обновления кнопок + frm.reload_doc(); + }); +} + +// Функция для отображения деталей счета +function showInvoiceDetails(invoice_id) { + // Находим активную форму + var frm = cur_frm; + if (!frm) return; + + // Используем основной токен, если он есть, иначе используем bearer_token + const token_to_use = frm.doc.main_token || frm.doc.bearer_token; + + // Показываем индикатор загрузки + frappe.show_alert({ + message: __('Loading invoice details...'), + indicator: 'blue' + }, 3); + + // Получаем детали счета + frappe.call({ + method: 'invoice_az.invoice_az.doctype.testapi.testapi.get_invoice_details', + args: { + 'token': token_to_use, + 'invoice_id': invoice_id + }, + callback: function(r) { + if (r.message) { + // Отображаем детали счета + displayInvoiceDetails(r.message); + } else { + frappe.msgprint(__('Failed to get invoice details')); + } + }, + error: function(err) { + console.error('Error getting invoice details:', err); + frappe.msgprint(__('Error getting invoice details. See console for details.')); + } + }); +} + +// Функция для отображения деталей счета в диалоговом окне +function displayInvoiceDetails(invoice) { + // Основная информация о счете + let invoice_info = ` +
+
+
+
+
${__('Invoice')}: ${invoice.serialNumber || ''}
+

${__('Status')}: ${invoice.status || ''}

+

${__('Type')}: ${invoice.kind || ''}

+

${__('Amount')}: ${invoice.amount || 0}

+

${__('VAT')}: ${invoice.vat || 0}

+

${__('Created')}: ${moment(invoice.createdAt).format('DD.MM.YYYY HH:mm')}

+

${__('Last Updated')}: ${moment(invoice.lastUpdatedAt).format('DD.MM.YYYY HH:mm')}

+
+
+
+
+
${__('Sender')}
+

${invoice.sender.name || ''}

+

TIN: ${invoice.sender.tin || ''}

+

PIN: ${invoice.sender.pin || ''}

+
${__('Receiver')}
+

${invoice.receiver.name || ''}

+

TIN: ${invoice.receiver.tin || ''}

+

PIN: ${invoice.receiver.pin || ''}

+
+
+
+ +
+
+
${__('Comments')}
+

${invoice.invoiceComment || ''}

+

${invoice.invoiceComment2 || ''}

+
+
+
+ `; + + // Таблица с товарами/услугами + let items_table = ` +
+
${__('Items')}
+
+ + + + + + + + + + + + + + `; + + if (invoice.items && invoice.items.length) { + invoice.items.forEach(function(item) { + items_table += ` + + + + + + + + `; + }); + } + + items_table += `
${__('Item ID')}${__('Product Name')}${__('Unit')}${__('Quantity')}${__('Price')}${__('Cost')}${__('VAT')}
${item.itemId || ''}${item.productName || ''}${item.unit || ''}${item.quantity || 0}${item.pricePerUnit || 0}${item.cost || 0}${item.vat18 ? 'Yes' : 'No'}
`; + + // Объединяем всю информацию + let content = invoice_info + items_table; + + // Создаем диалоговое окно + var d = new frappe.ui.Dialog({ + title: __('Invoice Details'), + fields: [{ + fieldtype: 'HTML', + fieldname: 'details', + options: content + }], + primary_action_label: __('Close'), + primary_action: function() { + d.hide(); + }, + secondary_action_label: __('Export JSON'), + secondary_action: function() { + // Экспорт деталей счета в формате JSON + var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(invoice, null, 2)); + var downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute("href", dataStr); + downloadAnchorNode.setAttribute("download", "invoice_" + invoice.id + ".json"); + document.body.appendChild(downloadAnchorNode); + downloadAnchorNode.click(); + downloadAnchorNode.remove(); + } + }); + + d.show(); +} \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/testapi/testapi.json b/invoice_az/invoice_az/doctype/testapi/testapi.json new file mode 100644 index 0000000..ac93d4a --- /dev/null +++ b/invoice_az/invoice_az/doctype/testapi/testapi.json @@ -0,0 +1,138 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-04-24 18:30:42.231049", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "authentication_section", + "phone", + "user_id", + "bearer_token", + "auth_status", + "verification_code", + "certificates_section", + "certificates_json", + "selected_certificate", + "selected_certificate_index", + "selected_certificate_json", + "taxpayer_section", + "main_token", + "choose_taxpayer_response", + "invoices_section", + "invoices_json" + ], + "fields": [ + { + "fieldname": "phone", + "fieldtype": "Data", + "label": "Phone" + }, + { + "fieldname": "user_id", + "fieldtype": "Data", + "label": "ID" + }, + { + "fieldname": "bearer_token", + "fieldtype": "Small Text", + "label": "Token" + }, + { + "fieldname": "invoices_json", + "fieldtype": "Code", + "label": "Invoices Json", + "options": "JSON" + }, + { + "fieldname": "auth_status", + "fieldtype": "Select", + "label": "Auth Status", + "options": "Not Authenticated\nWaiting for confirmation\nAuthenticated\nFully Authenticated\nFailed" + }, + { + "fieldname": "verification_code", + "fieldtype": "Data", + "label": "Verification Code" + }, + { + "fieldname": "certificates_json", + "fieldtype": "Code", + "label": "Certificates JSON", + "options": "JSON" + }, + { + "fieldname": "selected_certificate", + "fieldtype": "Data", + "label": "Selected Certificate" + }, + { + "fieldname": "selected_certificate_index", + "fieldtype": "Data", + "hidden": 1, + "label": "Selected Certificate Index" + }, + { + "fieldname": "selected_certificate_json", + "fieldtype": "Code", + "label": "Selected Certificate Details", + "options": "JSON" + }, + { + "fieldname": "authentication_section", + "fieldtype": "Section Break", + "label": "Authentication" + }, + { + "fieldname": "certificates_section", + "fieldtype": "Section Break", + "label": "Certificates" + }, + { + "fieldname": "invoices_section", + "fieldtype": "Section Break", + "label": "Invoices" + }, + { + "fieldname": "main_token", + "fieldtype": "Small Text", + "label": "Main Token", + "description": "Primary token for all API operations" + }, + { + "fieldname": "taxpayer_section", + "fieldtype": "Section Break", + "label": "Taxpayer Information" + }, + { + "fieldname": "choose_taxpayer_response", + "fieldtype": "Code", + "label": "Taxpayer Response", + "options": "JSON" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-04-25 14:00:00.000000", + "modified_by": "Administrator", + "module": "Invoice Az", + "name": "TestApi", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] + } \ No newline at end of file diff --git a/invoice_az/invoice_az/doctype/testapi/testapi.py b/invoice_az/invoice_az/doctype/testapi/testapi.py new file mode 100644 index 0000000..f449661 --- /dev/null +++ b/invoice_az/invoice_az/doctype/testapi/testapi.py @@ -0,0 +1,221 @@ +import frappe +import requests +import json +from frappe.utils import nowdate +from frappe.model.document import Document + + +class TestApi(Document): + pass + +@frappe.whitelist() +def get_auth_token(phone, user_id): + """Получение токена авторизации""" + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/start" + + payload = { + "phone": phone, + "userId": user_id + } + + headers = { + "Accept": "application/json, text/plain, */*", + "Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7", + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Referer": "https://new.e-taxes.gov.az/eportal/az/login/asan" + } + + try: + response = requests.post(url, data=json.dumps(payload), headers=headers) + response.raise_for_status() + + # Получаем токен из заголовка ответа + bearer_token = response.headers.get('x-authorization', '') + + # Получаем данные ответа + resp_data = response.json() + + return { + 'bearer_token': bearer_token, + 'response_data': resp_data + } + except Exception as e: + frappe.log_error(f"Error getting auth token: {str(e)}", "TestApi") + frappe.throw(f"Error getting auth token: {str(e)}") + +@frappe.whitelist() +def check_auth_status(bearer_token): + """Проверка статуса авторизации""" + # Получаем значение token из DocType + + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/status" + + 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" + } + + # Добавляем токен в заголовки, если он есть + if bearer_token: + headers["x-authorization"] = f"Bearer {bearer_token}" + + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + + # Получаем данные о статусе + status_data = response.json() + + return status_data + except Exception as e: + frappe.log_error(f"Error checking auth status: {str(e)}", "TestApi") + frappe.throw(f"Error checking auth status: {str(e)}") + +@frappe.whitelist() +def get_invoices(token, filters=None): + """Получение списка счетов-фактур с возможностью фильтрации""" + url = "https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/find.inbox" + + # Базовые параметры запроса + payload = { + "sortBy": "creationDate", + "sortAsc": True, + "statuses": ["approved", "onApproval", "updateApproval", "updateRequested", + "cancelRequested", "approvedBySystem", "onApprovalEdited", + "deactivated", "cancelationRefused", "correctionRefused"], + "types": ["current", "corrected"], + "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", + "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", + "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], + "serialNumber": None, + "senderTin": None, + "senderName": None, + "productName": None, + "productCode": None, + "receiverTin": None, + "receiverName": None, + "creationDateFrom": "01-01-2024 00:00", + "creationDateTo": "31-12-2024 23:59", + "amountFrom": None, + "amountTo": None, + "offset": 0, + "maxCount": 200, + "actionOwner": None + } + + # Применяем пользовательские фильтры, если они переданы + if filters: + try: + filters_dict = json.loads(filters) + for key, value in filters_dict.items(): + if key in payload and value: # Обновляем только существующие ключи и непустые значения + payload[key] = value + except Exception as e: + frappe.log_error(f"Error parsing filters: {str(e)}", "TestApi") + + headers = { + "Accept": "application/json, text/plain, */*", + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "x-authorization": f"Bearer {token}" + } + + try: + response = requests.post(url, data=json.dumps(payload), headers=headers) + response.raise_for_status() + + return response.json() + except Exception as e: + frappe.log_error(f"Error getting invoices: {str(e)}", "TestApi") + frappe.throw(f"Error getting invoices: {str(e)}") + +@frappe.whitelist() +def get_certificates(bearer_token): + """Получение доступных сертификатов""" + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/certificates" + + 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/verification/asan", + "x-authorization": f"Bearer {bearer_token}" + } + + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + + return response.json() + except Exception as e: + frappe.log_error(f"Error getting certificates: {str(e)}", "TestApi") + frappe.throw(f"Error getting certificates: {str(e)}") + +@frappe.whitelist() +def choose_taxpayer(bearer_token, owner_type, tin): + """Выбор налогоплательщика после выбора сертификата""" + url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/chooseTaxpayer" + + payload = { + "ownerType": owner_type, # "legal" или "individual" + "legalTin": tin if owner_type == "legal" else None, + "individualFin": tin if owner_type == "individual" else None + } + + # Удаляем None значения из payload + payload = {k: v for k, v in payload.items() if v is not None} + + headers = { + "Accept": "application/json, text/plain, */*", + "Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7", + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Referer": "https://new.e-taxes.gov.az/eportal/az/login/asan", + "x-authorization": f"Bearer {bearer_token}" + } + + try: + response = requests.post(url, data=json.dumps(payload), headers=headers) + response.raise_for_status() + + # Получаем основной токен из заголовка ответа + main_token = response.headers.get('x-authorization', '') + + # Получаем данные ответа + resp_data = response.json() + + return { + 'main_token': main_token, + 'response_data': resp_data + } + except Exception as e: + frappe.log_error(f"Error choosing taxpayer: {str(e)}", "TestApi") + frappe.throw(f"Error choosing taxpayer: {str(e)}") + +@frappe.whitelist() +def get_invoice_details(token, invoice_id): + """Получение детальной информации о счете-фактуре""" + url = f"https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/{invoice_id}?sourceSystem=avis" + + headers = { + "Accept": "application/json, text/plain, */*", + "Content-Type": "application/json", + "Cache-Control": "no-cache", + "x-authorization": f"Bearer {token}" + } + + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + + return response.json() + except Exception as e: + frappe.log_error(f"Error getting invoice details: {str(e)}", "TestApi") + frappe.throw(f"Error getting invoice details: {str(e)}") \ No newline at end of file diff --git a/invoice_az/public/js/purchase_order.js b/invoice_az/public/js/purchase_order.js new file mode 100644 index 0000000..d34cc1a --- /dev/null +++ b/invoice_az/public/js/purchase_order.js @@ -0,0 +1,1471 @@ +// Purchase Order extensions for Asan Login integration +console.log('Loading invoice_az Purchase Order customizations (new version)'); + +// Add buttons to Purchase Order list view +frappe.listview_settings['Purchase Order'] = frappe.listview_settings['Purchase Order'] || {}; + +// Добавляем обработчик на загрузку страницы +$(document).on('page-change', function() { + console.log('Page changed event triggered'); + var current_route = frappe.get_route(); + + // Проверяем, что мы на странице списка Purchase Order + if (current_route && current_route[0] === 'List' && current_route[1] === 'Purchase Order') { + console.log('On Purchase Order list page, checking for menu button'); + + // Используем setTimeout, чтобы убедиться, что страница полностью загружена + setTimeout(function() { + var has_button = false; + + // Проверяем, есть ли уже наша кнопка в меню + $('.dropdown-item').each(function() { + if ($(this).text().includes('Import from E-Taxes')) { + has_button = true; + } + }); + + // Если кнопки нет, добавляем её + if (!has_button && cur_list && cur_list.page) { + console.log('Adding "Import from E-Taxes" button through page-change'); + cur_list.page.add_menu_item(__('Import from E-Taxes'), function() { + console.log('Import from E-Taxes button clicked (alternative method)'); + // Копируем логику из основной функции обработчика + frappe.call({ + method: 'invoice_az.api.get_default_asan_login', + callback: function(r) { + if (r.message && r.message.found) { + if (r.message.auth_status === 'Fully Authenticated') { + showInvoiceFilterDialog(r.message); + } else if (r.message.auth_status === 'Authenticated') { + frappe.msgprint({ + title: __('Authentication Incomplete'), + indicator: 'orange', + message: __('Please select a taxpayer in Asan Login before importing invoices.') + }); + frappe.set_route('Form', 'Asan Login', r.message.name); + } else { + showAuthenticationDialog(r.message); + } + } else { + showAsanLoginSetupDialog(); + } + } + }); + }); + } + }, 1000); + } +}); + +// Extend the list view settings +(function() { + console.log('Executing Purchase Order list view extension'); + + // Save original render_button_factory + var _original_render_button_factory = frappe.listview_settings['Purchase Order'].button || function() {}; + console.log('Original button function captured:', typeof _original_render_button_factory); + + // Override with extended function that adds our custom button + frappe.listview_settings['Purchase Order'].button = function(listview) { + console.log('Custom button function executing for Purchase Order list'); + console.log('Listview object:', listview ? 'exists' : 'undefined'); + + // Call original function first + _original_render_button_factory(listview); + console.log('Original button function executed'); + + // Add our own button for E-Taxes import + console.log('Adding "Import from E-Taxes" menu item'); + listview.page.add_menu_item(__('Import from E-Taxes'), function() { + console.log('Import from E-Taxes button clicked'); + // First, check if Asan Login is configured + frappe.call({ + method: 'invoice_az.api.get_default_asan_login', + callback: function(r) { + if (r.message && r.message.found) { + // If Asan Login is configured, check authentication status + if (r.message.auth_status === 'Fully Authenticated') { + // If authenticated, show invoice filter dialog + showInvoiceFilterDialog(r.message); + } else if (r.message.auth_status === 'Authenticated') { + // If only basic authentication is complete, show taxpayer selection + frappe.msgprint({ + title: __('Authentication Incomplete'), + indicator: 'orange', + message: __('Please select a taxpayer in Asan Login before importing invoices.') + }); + + // Redirect to Asan Login document + frappe.set_route('Form', 'Asan Login', r.message.name); + } else { + // If not authenticated, show authentication dialog + showAuthenticationDialog(r.message); + } + } else { + // If no Asan Login is configured, show setup dialog + showAsanLoginSetupDialog(); + } + } + }); + }); + }; +})(); + +// Add custom button to Purchase Order form +frappe.ui.form.on('Purchase Order', { + refresh: function(frm) { + console.log('Purchase Order form refresh event triggered'); + if(frm.doc.docstatus === 0) { + console.log('Adding Import from E-Taxes button to form'); + frm.add_custom_button(__('Import from E-Taxes'), function() { + console.log('Import from E-Taxes button clicked in form'); + // First, check if Asan Login is configured + frappe.call({ + method: 'invoice_az.api.get_default_asan_login', + callback: function(r) { + if (r.message && r.message.found) { + // If Asan Login is configured, check authentication status + if (r.message.auth_status === 'Fully Authenticated') { + // If authenticated, show invoice filter dialog + showInvoiceFilterDialog(r.message, frm.doc.name); + } else if (r.message.auth_status === 'Authenticated') { + // If only basic authentication is complete, show taxpayer selection + frappe.msgprint({ + title: __('Authentication Incomplete'), + indicator: 'orange', + message: __('Please select a taxpayer in Asan Login before importing invoices.') + }); + + // Redirect to Asan Login document + frappe.set_route('Form', 'Asan Login', r.message.name); + } else { + // If not authenticated, show authentication dialog + showAuthenticationDialog(r.message); + } + } else { + // If no Asan Login is configured, show setup dialog + showAsanLoginSetupDialog(); + } + } + }); + }, __('Create')); + } + } +}); + +// Function to display dialog for setting up Asan Login +function showAsanLoginSetupDialog() { + var d = new frappe.ui.Dialog({ + title: __('Asan Login Setup Required'), + fields: [ + { + fieldname: 'message', + fieldtype: 'HTML', + options: ` +
+

Asan Login configuration is required to import invoices.

+

Would you like to set up Asan Login now?

+
+ ` + } + ], + primary_action_label: __('Set Up Now'), + primary_action: function() { + d.hide(); + + // Create a new Asan Login document + frappe.new_doc('Asan Login'); + }, + secondary_action_label: __('Cancel'), + secondary_action: function() { + d.hide(); + } + }); + + d.show(); +} + +// Function to display authentication dialog +function showAuthenticationDialog(login_data) { + var d = new frappe.ui.Dialog({ + title: __('Authentication Required'), + fields: [ + { + fieldname: 'message', + fieldtype: 'HTML', + options: ` +
+

Authentication is required to import invoices.

+

Would you like to authenticate now?

+
+ ` + } + ], + primary_action_label: __('Authenticate'), + primary_action: function() { + d.hide(); + + // Вместо перенаправления - запускаем процесс аутентификации здесь + frappe.call({ + method: 'invoice_az.api.handle_authentication', + args: { + 'asan_login_name': login_data.name + }, + callback: function(r) { + if (r.message && r.message.success) { + // Показываем сообщение о том, что запрос отправлен + frappe.show_alert({ + message: __('Authentication request sent. Please confirm on your phone.'), + indicator: 'blue' + }, 5); + + // Запускаем опрос статуса + startPollingAuthStatusInDialog(login_data.name, r.message.bearer_token); + } else { + // Показываем сообщение об ошибке + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Authentication request failed.') + }); + } + } + }); + }, + secondary_action_label: __('Cancel'), + secondary_action: function() { + d.hide(); + } + }); + + d.show(); +} + +// Function to show invoice filter dialog +function showInvoiceFilterDialog(login_data, purchase_order = null) { + // Использовать значения дат, охватывающие продолжительный период + const startDate = "01-01-2020 00:00"; + const endDate = moment().add(1, 'year').format('DD-MM-YYYY HH:mm'); + + // Создаем диалог с полями для фильтрации + var d = new frappe.ui.Dialog({ + title: __('Invoice Filters'), + fields: [ + { + fieldname: 'date_range_section', + fieldtype: 'Section Break', + label: __('Date Range') + }, + { + fieldname: 'creationDateFrom', + fieldtype: 'Data', + label: __('From Date'), + default: startDate + }, + { + fieldname: 'creationDateTo', + fieldtype: 'Data', + label: __('To Date'), + default: endDate + }, + { + fieldname: 'invoice_details_section', + fieldtype: 'Section Break', + label: __('Invoice Details') + }, + { + fieldname: 'serialNumber', + fieldtype: 'Data', + label: __('Serial Number') + }, + { + fieldname: 'party_section', + fieldtype: 'Section Break', + label: __('Sender/Receiver') + }, + { + fieldname: 'senderTin', + fieldtype: 'Data', + label: __('Sender TIN') + }, + { + fieldname: 'senderName', + fieldtype: 'Data', + label: __('Sender Name') + }, + { + fieldname: 'receiverTin', + fieldtype: 'Data', + label: __('Receiver TIN') + }, + { + fieldname: 'receiverName', + fieldtype: 'Data', + label: __('Receiver Name') + }, + { + fieldname: 'amount_section', + fieldtype: 'Section Break', + label: __('Amount') + }, + { + fieldname: 'amountFrom', + fieldtype: 'Float', + label: __('Amount From'), + precision: 2 + }, + { + fieldname: 'amountTo', + fieldtype: 'Float', + label: __('Amount To'), + precision: 2 + }, + { + fieldname: 'limit_section', + fieldtype: 'Section Break', + label: __('Limit') + }, + { + fieldname: 'maxCount', + fieldtype: 'Int', + label: __('Max Records'), + default: 200 + } + ], + primary_action_label: __('Get Invoices'), + primary_action: function() { + var values = d.get_values(); + + // Дополняем значения фильтров полными параметрами + const fullPayload = { + "sortBy": "creationDate", + "sortAsc": true, + "statuses": ["approved", "onApproval", "updateApproval", "updateRequested", + "cancelRequested", "approvedBySystem", "onApprovalEdited", + "deactivated", "cancelationRefused", "correctionRefused"], + "types": ["current", "corrected"], + "kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163", + "taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled", + "exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"], + "offset": 0, + "actionOwner": null, + + // Заполняем значениями из диалога + "serialNumber": values.serialNumber || null, + "senderTin": values.senderTin || null, + "senderName": values.senderName || null, + "productName": null, + "productCode": null, + "receiverTin": values.receiverTin || null, + "receiverName": values.receiverName || null, + "creationDateFrom": values.creationDateFrom || startDate, + "creationDateTo": values.creationDateTo || endDate, + "amountFrom": values.amountFrom || null, + "amountTo": values.amountTo || null, + "maxCount": values.maxCount || 200 + }; + + // Используем основной токен для запроса + const token = login_data.main_token; + + // Закрываем диалог + d.hide(); + + // Показываем индикатор загрузки + frappe.show_alert({ + message: __('Fetching invoices, please wait...'), + indicator: 'blue' + }, 5); + + // Отправляем запрос для получения счетов с фильтрами + frappe.call({ + method: 'invoice_az.api.get_invoices', + args: { + 'token': token, + 'filters': JSON.stringify(fullPayload) + }, + callback: function(r) { + if (r.message) { + // Проверяем на ошибку авторизации + if (r.message.error === "unauthorized") { + // Показываем диалог для повторной авторизации + showReauthenticationDialog(login_data.name); + return; + } + + // Проверяем различные возможные структуры данных + let invoicesData = null; + if (r.message.data && r.message.data.length) { + invoicesData = r.message.data; + } else if (r.message.invoices && r.message.invoices.length) { + invoicesData = r.message.invoices; + } + + // Отображаем список счетов в диалоговом окне + if (invoicesData && invoicesData.length) { + // Проверяем наличие настроек E-Taxes + frappe.call({ + method: 'invoice_az.invoice_az.doctype.e_taxes_settings.e_taxes_settings.get_default_settings', + callback: function(settings_r) { + if (settings_r.message && settings_r.message.found) { + // Если настройки найдены, используем новый метод с сопоставлением + showInvoicesWithMappingDialog(invoicesData, purchase_order, settings_r.message.name); + } else { + // Если настройки не найдены, показываем диалог создания настроек + showEtaxesSetupDialog(invoicesData, purchase_order); + } + } + }); + } else { + frappe.msgprint(__('No invoices found matching the filters')); + } + } + }, + error: function(err) { + console.error('Error fetching invoices:', err); + frappe.msgprint(__('Error fetching invoices. See console for details.')); + } + }); + } + }); + + d.show(); +} + +// Функция для отображения диалога создания настроек E-Taxes +function showEtaxesSetupDialog(invoices, purchase_order) { + var d = new frappe.ui.Dialog({ + title: __('E-Taxes Settings Required'), + fields: [ + { + fieldname: 'message', + fieldtype: 'HTML', + options: ` +
+

E-Taxes settings are required for item and party mapping.

+

Would you like to set up E-Taxes settings now?

+
+ ` + } + ], + primary_action_label: __('Set Up Now'), + primary_action: function() { + d.hide(); + + // Create a new E-Taxes Settings document + frappe.new_doc('E-Taxes Settings', { + settings_name: 'Default Settings', + is_default: 1, + is_active: 1 + }); + }, + secondary_action_label: __('Continue Without Settings'), + secondary_action: function() { + d.hide(); + + // Show the old-style invoice dialog + showInvoicesDialog(invoices, purchase_order); + } + }); + + d.show(); +} + +// Функция для отображения счетов-фактур с учетом сопоставлений +function showInvoicesWithMappingDialog(invoices, purchase_order, settings_name) { + if (!invoices || !invoices.length) { + frappe.msgprint(__('No invoices data to display')); + return; + } + + // Создаем таблицу со счетами + var invoice_table = '
'; + invoice_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + + // Анализируем каждый инвойс для отображения + let invoicesWithMappingInfo = []; + let processedCount = 0; + let totalInvoices = invoices.length; + + // Показываем прогресс + let progressDialog = new frappe.ui.Dialog({ + title: __('Analyzing Invoices'), + fields: [ + { + fieldname: 'progress_html', + fieldtype: 'HTML', + options: ` +
+
+
0%
+
+

Analyzing invoices for mapping status...

+
+ ` + } + ] + }); + + progressDialog.show(); + + // Функция для анализа инвойса и обновления прогресса + function analyzeNextInvoice(index) { + if (index >= totalInvoices) { + // Завершили анализ, показываем таблицу + progressDialog.hide(); + displayInvoicesTable(); + return; + } + + let invoice = invoices[index]; + + // Обновляем прогресс + let percent = Math.round((index / totalInvoices) * 100); + $('.progress-bar').css('width', percent + '%').attr('aria-valuenow', percent).text(percent + '%'); + $('#progress_text').text('Analyzing invoice ' + (index + 1) + ' of ' + totalInvoices); + + // Проверяем сопоставления для товаров и контрагентов + frappe.call({ + method: 'invoice_az.invoice_az.doctype.e_taxes_settings.e_taxes_settings.find_matching_items', + args: { + 'invoice_items': invoice.items || [], + 'settings_name': settings_name + }, + callback: function(items_r) { + let itemsResult = items_r.message && items_r.message.success ? items_r.message.result : null; + + frappe.call({ + method: 'invoice_az.invoice_az.doctype.e_taxes_settings.e_taxes_settings.find_matching_parties', + args: { + 'invoice_data': invoice, + 'settings_name': settings_name + }, + callback: function(parties_r) { + let partiesResult = parties_r.message && parties_r.message.success ? parties_r.message.result : null; + + // Определяем статус сопоставления + let mappingStatus = 'Unknown'; + let statusClass = 'text-muted'; + + if (itemsResult && partiesResult) { + // Проверяем несопоставленные элементы + let unmatched_items_count = (itemsResult.unmatched_items ? itemsResult.unmatched_items.length : 0) + + (itemsResult.suggested_matches ? itemsResult.suggested_matches.length : 0); + + let has_unmatched_parties = !partiesResult.sender.matched || !partiesResult.receiver.matched; + + if (unmatched_items_count === 0 && !has_unmatched_parties) { + mappingStatus = 'Fully Mapped'; + statusClass = 'text-success'; + } else if (unmatched_items_count > 0 && has_unmatched_parties) { + mappingStatus = 'Not Mapped'; + statusClass = 'text-danger'; + } else { + mappingStatus = 'Partially Mapped'; + statusClass = 'text-warning'; + } + } + + // Добавляем информацию о сопоставлении + invoicesWithMappingInfo.push({ + invoice: invoice, + mapping_status: mappingStatus, + status_class: statusClass, + items_result: itemsResult, + parties_result: partiesResult + }); + + // Анализируем следующий инвойс + setTimeout(function() { + analyzeNextInvoice(index + 1); + }, 10); + } + }); + } + }); + } + + // Функция для отображения таблицы с инвойсами + function displayInvoicesTable() { + // Сортируем инвойсы по статусу сопоставления (полностью сопоставленные в начале) + invoicesWithMappingInfo.sort(function(a, b) { + const statusOrder = { + 'Fully Mapped': 0, + 'Partially Mapped': 1, + 'Not Mapped': 2, + 'Unknown': 3 + }; + + return statusOrder[a.mapping_status] - statusOrder[b.mapping_status]; + }); + + // Заполняем таблицу инвойсами + for (let item of invoicesWithMappingInfo) { + const invoice = item.invoice; + + // Проверяем наличие необходимых полей в инвойсе + const serialNumber = invoice.serialNumber || invoice.number || ''; + const senderName = invoice.senderName || (invoice.sender ? invoice.sender.name : '') || ''; + const receiverName = invoice.receiverName || (invoice.receiver ? invoice.receiver.name : '') || ''; + const creationDate = invoice.creationDate || invoice.createDate || ''; + const amount = invoice.amount || 0; + const status = invoice.status || ''; + const id = invoice.id || invoice._id || ''; + + invoice_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + } + + invoice_table += '
Serial NumberSenderReceiverCreation DateAmountStatusMapping StatusActions
' + serialNumber + '' + senderName + '' + receiverName + '' + creationDate + '' + amount + '' + status + '' + item.mapping_status + '
'; + + // Отображаем диалоговое окно с таблицей + var d = new frappe.ui.Dialog({ + title: __('Invoices List'), + fields: [{ + fieldtype: 'HTML', + fieldname: 'invoice_list', + options: invoice_table + }], + primary_action_label: __('Close'), + primary_action: function() { + d.hide(); + } + }); + + d.show(); + + // Добавляем обработчик для кнопки просмотра счета + d.$wrapper.find('.view-invoice').on('click', function() { + var invoice_id = $(this).data('id'); + showInvoiceDetails(invoice_id); + }); + + // Добавляем обработчик для кнопки импорта счета + d.$wrapper.find('.import-invoice').on('click', function() { + var invoice_id = $(this).data('id'); + var index = $(this).data('index'); + var invoiceWithMapping = invoicesWithMappingInfo[index]; + + // Импортируем счет с учетом сопоставлений + if (invoiceWithMapping) { + importInvoiceWithMappings( + invoiceWithMapping.invoice, + purchase_order, + invoiceWithMapping.items_result, + invoiceWithMapping.parties_result + ); + d.hide(); + } else { + // Если информация о сопоставлении недоступна, используем старый способ + importInvoiceDirectly(invoice_id, purchase_order); + d.hide(); + } + }); + } + + // Начинаем анализ инвойсов + analyzeNextInvoice(0); +} + +// Функция для отображения счетов-фактур в стандартном диалоговом окне (старый способ) +function showInvoicesDialog(invoices, purchase_order = null) { + if (!invoices || !invoices.length) { + frappe.msgprint(__('No invoices data to display')); + return; + } + + // Создаем таблицу со счетами + var invoice_table = '
'; + invoice_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + + invoices.forEach(function(invoice) { + // Проверяем наличие необходимых полей в инвойсе + const serialNumber = invoice.serialNumber || invoice.number || ''; + const senderName = invoice.senderName || (invoice.sender ? invoice.sender.name : '') || ''; + const receiverName = invoice.receiverName || (invoice.receiver ? invoice.receiver.name : '') || ''; + const creationDate = invoice.creationDate || invoice.createDate || ''; + const amount = invoice.amount || 0; + const status = invoice.status || ''; + const id = invoice.id || invoice._id || ''; + + invoice_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }); + + invoice_table += '
Serial NumberSenderReceiverCreation DateAmountStatusActions
' + serialNumber + '' + senderName + '' + receiverName + '' + creationDate + '' + amount + '' + status + '
'; + + // Отображаем диалоговое окно с таблицей + var d = new frappe.ui.Dialog({ + title: __('Invoices List'), + fields: [{ + fieldtype: 'HTML', + fieldname: 'invoice_list', + options: invoice_table + }], + primary_action_label: __('Close'), + primary_action: function() { + d.hide(); + } + }); + + d.show(); + + // Добавляем обработчик для кнопки просмотра счета + d.$wrapper.find('.view-invoice').on('click', function() { + var invoice_id = $(this).data('id'); + showInvoiceDetails(invoice_id); + }); + + // Добавляем обработчик для кнопки импорта счета + d.$wrapper.find('.import-invoice').on('click', function() { + var invoice_id = $(this).data('id'); + importInvoiceDirectly(invoice_id, purchase_order); + d.hide(); + }); +} + +// Функция для отображения деталей счета в диалоговом окне +function showInvoiceDetails(invoice_id) { + // Получаем настройки авторизации по умолчанию + frappe.call({ + method: 'invoice_az.api.get_default_asan_login', + callback: function(r) { + if (r.message && r.message.found) { + const token = r.message.main_token || r.message.bearer_token; + + // Показываем индикатор загрузки + frappe.show_alert({ + message: __('Loading invoice details...'), + indicator: 'blue' + }, 3); + + // Получаем детали счета + frappe.call({ + method: 'invoice_az.api.get_invoice_details', + args: { + 'token': token, + 'invoice_id': invoice_id + }, + callback: function(r) { + if (r.message) { + // Проверяем на ошибку авторизации + if (r.message.error === "unauthorized") { + // Показываем диалог для повторной авторизации + showReauthenticationDialog(r.message.name); + return; + } + + // Отображаем детали счета + displayInvoiceDetails(r.message); + } else { + frappe.msgprint(__('Failed to get invoice details')); + } + }, + error: function(err) { + console.error('Error getting invoice details:', err); + frappe.msgprint(__('Error getting invoice details. See console for details.')); + } + }); + } else { + frappe.msgprint(__('No Asan Login settings found')); + } + } + }); +} + +// Функция для отображения деталей счета +function displayInvoiceDetails(invoice) { + // Основная информация о счете + let invoice_info = ` +
+
+
+
+
${__('Invoice')}: ${invoice.serialNumber || ''}
+

${__('Status')}: ${invoice.status || ''}

+

${__('Type')}: ${invoice.kind || ''}

+

${__('Amount')}: ${invoice.amount || 0}

+

${__('VAT')}: ${invoice.vat || 0}

+

${__('Created')}: ${moment(invoice.createdAt).format('DD.MM.YYYY HH:mm')}

+

${__('Last Updated')}: ${moment(invoice.lastUpdatedAt).format('DD.MM.YYYY HH:mm')}

+
+
+
+
+
${__('Sender')}
+

${invoice.sender.name || ''}

+

TIN: ${invoice.sender.tin || ''}

+ +
${__('Receiver')}
+

${invoice.receiver.name || ''}

+

TIN: ${invoice.receiver.tin || ''}

+
+
+
+ +
+
+
${__('Comments')}
+

${invoice.invoiceComment || ''}

+

${invoice.invoiceComment2 || ''}

+
+
+
+ `; + + // Таблица с товарами/услугами + let items_table = ` +
+
${__('Items')}
+
+ + + + + + + + + + + + + + `; + + if (invoice.items && invoice.items.length) { + invoice.items.forEach(function(item) { + items_table += ` + + + + + + + + `; + }); + } + + items_table += `
${__('Item ID')}${__('Product Name')}${__('Unit')}${__('Quantity')}${__('Price')}${__('Cost')}${__('VAT')}
${item.itemId || ''}${item.productName || ''}${item.unit || ''}${item.quantity || 0}${item.pricePerUnit || 0}${item.cost || 0}${item.vat18 ? 'Yes' : 'No'}
`; + + // Объединяем всю информацию + let content = invoice_info + items_table; + + // Создаем диалоговое окно + var d = new frappe.ui.Dialog({ + title: __('Invoice Details'), + fields: [{ + fieldtype: 'HTML', + fieldname: 'details', + options: content + }], + primary_action_label: __('Close'), + primary_action: function() { + d.hide(); + }, + secondary_action_label: __('Export JSON'), + secondary_action: function() { + // Экспорт деталей счета в формате JSON + var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(invoice, null, 2)); + var downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute("href", dataStr); + downloadAnchorNode.setAttribute("download", "invoice_" + invoice.id + ".json"); + document.body.appendChild(downloadAnchorNode); + downloadAnchorNode.click(); + downloadAnchorNode.remove(); + } + }); + + d.show(); +} + +// Функция для импорта счета с использованием сопоставлений +function importInvoiceWithMappings(invoice_data, purchase_order, items_result, parties_result) { + // Показываем индикатор загрузки + frappe.show_alert({ + message: __('Importing invoice with mappings...'), + indicator: 'blue' + }, 5); + + // Подготавливаем данные для импорта + const modified_invoice_data = JSON.parse(JSON.stringify(invoice_data)); + + // Заменяем товары на сопоставленные + if (items_result && items_result.matched_items && items_result.matched_items.length && modified_invoice_data.items) { + const item_mappings = {}; + + // Создаем словарь сопоставлений + items_result.matched_items.forEach(function(mapping) { + const key = mapping.etaxes_item_name + '|' + (mapping.etaxes_item_code || ''); + item_mappings[key] = mapping.erp_item; + }); + + // Заменяем коды товаров в счете + modified_invoice_data.items.forEach(function(item) { + const key = item.productName + '|' + (item.itemId || ''); + if (item_mappings[key]) { + item.item_code = item_mappings[key]; + } + }); + } + + // Заменяем контрагентов на сопоставленных + if (parties_result && parties_result.sender && parties_result.sender.matched && modified_invoice_data.sender) { + modified_invoice_data.mapped_sender = { + party_type: parties_result.sender.party_type, + party: parties_result.sender.party + }; + } + + if (parties_result && parties_result.receiver && parties_result.receiver.matched && modified_invoice_data.receiver) { + modified_invoice_data.mapped_receiver = { + party_type: parties_result.receiver.party_type, + party: parties_result.receiver.party + }; + } + + // Импортируем счет + frappe.call({ + method: 'invoice_az.api.import_invoice_to_purchase_order', + args: { + 'invoice_data': modified_invoice_data, + 'purchase_order': purchase_order + }, + callback: function(r) { + if (r.message && r.message.success) { + // Показываем сообщение об успешном импорте + frappe.show_alert({ + message: __('Invoice imported successfully'), + indicator: 'green' + }, 5); + + // Перенаправляем на созданный Purchase Order + if (r.message.purchase_order) { + frappe.set_route('Form', 'Purchase Order', r.message.purchase_order); + } + + // Создаем товары и контрагенты в соответствующих доктайпах E-Taxes + createEtaxesItemsAndPartiesFromInvoice(invoice_data); + } else { + // Показываем сообщение об ошибке + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Failed to import invoice') + }); + } + }, + error: function(err) { + console.error('Error importing invoice:', err); + frappe.msgprint(__('Error importing invoice. See console for details.')); + } + }); +} + +// Функция для прямого импорта счета (старый способ) +function importInvoiceDirectly(invoice_id, purchase_order) { + // Получаем настройки авторизации по умолчанию + frappe.call({ + method: 'invoice_az.api.get_default_asan_login', + callback: function(r) { + if (r.message && r.message.found) { + const token = r.message.main_token || r.message.bearer_token; + + // Показываем индикатор загрузки + frappe.show_alert({ + message: __('Loading invoice data for import...'), + indicator: 'blue' + }, 3); + + // Получаем детали счета + frappe.call({ + method: 'invoice_az.api.get_invoice_details', + args: { + 'token': token, + 'invoice_id': invoice_id + }, + callback: function(r) { + if (r.message) { + // Проверяем на ошибку авторизации + if (r.message.error === "unauthorized") { + // Показываем диалог для повторной авторизации + showReauthenticationDialog(r.message.name); + return; + } + + // Показываем индикатор загрузки + frappe.show_alert({ + message: __('Importing invoice directly...'), + indicator: 'blue' + }, 5); + + frappe.call({ + method: 'invoice_az.api.import_invoice_to_purchase_order', + args: { + 'invoice_data': r.message, + 'purchase_order': purchase_order + }, + callback: function(r) { + if (r.message && r.message.success) { + // Показываем сообщение об успешном импорте + frappe.show_alert({ + message: __('Invoice imported successfully'), + indicator: 'green' + }, 5); + + // Перенаправляем на созданный Purchase Order + if (r.message.purchase_order) { + frappe.set_route('Form', 'Purchase Order', r.message.purchase_order); + } + + // Создаем товары и контрагенты в соответствующих доктайпах E-Taxes + createEtaxesItemsAndPartiesFromInvoice(r.message); + } else { + // Показываем сообщение об ошибке + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Failed to import invoice') + }); + } + }, + error: function(err) { + console.error('Error importing invoice:', err); + frappe.msgprint(__('Error importing invoice. See console for details.')); + } + }); + } else { + frappe.msgprint(__('Failed to get invoice details')); + } + }, + error: function(err) { + console.error('Error getting invoice details:', err); + frappe.msgprint(__('Error getting invoice details. See console for details.')); + } + }); + } else { + frappe.msgprint(__('No Asan Login settings found')); + } + } + }); +} + +// Функция для создания товаров и контрагентов из инвойса +function createEtaxesItemsAndPartiesFromInvoice(invoice_data) { + if (!invoice_data) return; + + // Получаем настройки E-Taxes + frappe.call({ + method: 'invoice_az.invoice_az.doctype.e_taxes_settings.e_taxes_settings.get_default_settings', + callback: function(r) { + if (r.message && r.message.found) { + // Извлекаем товары из инвойса + frappe.call({ + method: 'invoice_az.invoice_az.doctype.e_taxes_settings.e_taxes_settings.extract_items_from_invoices', + args: { + 'invoices': [invoice_data] + }, + callback: function(r) { + // Обрабатываем результат + console.log('Extracted items from invoice:', r.message); + } + }); + + // Извлекаем контрагентов из инвойса + frappe.call({ + method: 'invoice_az.invoice_az.doctype.e_taxes_settings.e_taxes_settings.extract_parties_from_invoices', + args: { + 'invoices': [invoice_data] + }, + callback: function(r) { + // Обрабатываем результат + console.log('Extracted parties from invoice:', r.message); + } + }); + } + } + }); +} + +// Функция для отображения диалога повторной аутентификации +function showReauthenticationDialog(asan_login_name) { + var d = new frappe.ui.Dialog({ + title: __('Authentication Required'), + fields: [ + { + fieldname: 'message', + fieldtype: 'HTML', + options: ` +
+

Your session has expired or authentication is required.

+

Would you like to authenticate again?

+
+ ` + } + ], + primary_action_label: __('Authenticate'), + primary_action: function() { + d.hide(); + + // Запускаем процесс аутентификации прямо здесь, не перенаправляя + frappe.call({ + method: 'invoice_az.api.handle_authentication', + args: { + 'asan_login_name': asan_login_name + }, + callback: function(r) { + if (r.message && r.message.success) { + // Показываем сообщение о том, что запрос отправлен + frappe.show_alert({ + message: __('Authentication request sent. Please confirm on your phone.'), + indicator: 'blue' + }, 5); + + // Запускаем опрос статуса + startPollingAuthStatusInDialog(asan_login_name, r.message.bearer_token); + } else { + // Показываем сообщение об ошибке + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Authentication request failed.') + }); + } + } + }); + }, + secondary_action_label: __('Cancel'), + secondary_action: function() { + d.hide(); + } + }); + + d.show(); +} + +// Функция для опроса статуса аутентификации в диалоговом окне +function startPollingAuthStatusInDialog(asan_login_name, bearer_token) { + console.log('startPollingAuthStatusInDialog function has been called'); + + // Создаем всплывающее окно с информацией + const status_dialog = new frappe.ui.Dialog({ + title: __('Authentication Status'), + fields: [ + { + fieldname: 'status_html', + fieldtype: 'HTML', + options: ` +
+
+
+
+ +
+

Waiting for confirmation on your phone

+

Please check your phone and confirm the authentication request.

+
+
+ ` + } + ], + primary_action_label: __('Cancel'), + primary_action: function() { + // Остановить опрос + window.stopPolling = true; + status_dialog.hide(); + } + }); + + status_dialog.show(); + + // Счетчик попыток и флаг для остановки опроса + let attempts = 0; + const maxAttempts = 20; // 2 минут (20 попыток с интервалом 5 секунд) + window.stopPolling = false; + + // Функция опроса + function pollAuthStatus() { + // Если достигнуто максимальное количество попыток или установлен флаг остановки + if (attempts >= maxAttempts || window.stopPolling) { + console.log('Polling stopped: ' + (attempts >= maxAttempts ? 'max attempts reached' : 'manually cancelled')); + + if (attempts >= maxAttempts) { + // Обновляем информацию в диалоговом окне по таймауту + $('#status_message').html(` +

Authentication Timeout

+

The authentication request has timed out. Please try again.

+ `); + $('.lds-dual-ring').css('display', 'none'); + + // Закрываем диалог через 3 секунды + setTimeout(function() { + status_dialog.hide(); + }, 3000); + } + + return; + } + + attempts++; + console.log(`Polling auth status, attempt ${attempts} of ${maxAttempts}`); + + // Проверяем статус авторизации + frappe.call({ + method: 'invoice_az.api.poll_auth_status', + args: { + 'asan_login_name': asan_login_name, + 'bearer_token': bearer_token + }, + callback: function(r) { + if (r.message && r.message.success) { + if (r.message.authenticated) { + // Авторизация успешна + window.stopPolling = true; + + // Обновляем информацию в диалоговом окне + $('#status_message').html(` +

Authentication Successful!

+

You are now authenticated.

+ `); + $('.lds-dual-ring').css('display', 'none'); + + // Закрываем диалоговое окно через 2 секунды + setTimeout(function() { + status_dialog.hide(); + + // Далее выполняем действие для получения сертификатов + frappe.call({ + method: 'invoice_az.api.get_auth_certificates', + args: { + 'asan_login_name': asan_login_name + }, + callback: function(certResult) { + if (certResult.message && certResult.message.success) { + // Показываем диалог выбора сертификата + showCertificatesDialogForInvoice(certResult.message.certificates, asan_login_name); + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: certResult.message ? certResult.message.message : __('Failed to get certificates') + }); + } + } + }); + }, 2000); + } else { + // Продолжаем опрос + setTimeout(pollAuthStatus, 5000); + } + } else { + // Если ошибка, останавливаем опрос и показываем сообщение + window.stopPolling = true; + + // Обновляем информацию в диалоговом окне + $('#status_message').html(` +

Authentication Error

+

${r.message ? r.message.message : 'An unknown error occurred.'}

+ `); + $('.lds-dual-ring').css('display', 'none'); + + // Закрываем диалог через 3 секунды + setTimeout(function() { + status_dialog.hide(); + }, 3000); + } + }, + error: function(err) { + // В случае ошибки продолжаем опрос + console.error('Error during status check:', err); + setTimeout(pollAuthStatus, 5000); + } + }); + } + + // Начинаем опрос + console.log('Starting first polling attempt'); + pollAuthStatus(); +} + +// Функция для отображения сертификатов в диалоговом окне для последующего выбора налогоплательщика +function showCertificatesDialogForInvoice(certificates, asan_login_name) { + if (!certificates || !certificates.length) { + frappe.msgprint(__('No certificates data to display')); + return; + } + + // Создаем таблицу с сертификатами + var cert_table = '
'; + cert_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + + certificates.forEach(function(cert, index) { + var name = ''; + var id = ''; + + if (cert.taxpayerType === 'individual' && cert.individualInfo) { + name = cert.individualInfo.name || ''; + id = cert.individualInfo.fin || ''; + } else if (cert.legalInfo) { + name = cert.legalInfo.name || ''; + id = cert.legalInfo.tin || cert.legalInfo.voen || ''; + } + + cert_table += '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }); + + cert_table += '
TypeNameIDPositionStatusAction
' + (cert.taxpayerType || '') + '' + name + '' + id + '' + (cert.position || '') + '' + (cert.hasAccess ? 'Active' : 'Inactive') + + (cert.liquidated ? ' (Liquidated)' : '') + '
'; + + // Отображаем диалоговое окно с таблицей + var d = new frappe.ui.Dialog({ + title: __('Certificates List'), + fields: [{ + fieldtype: 'HTML', + fieldname: 'certificate_list', + options: cert_table + }], + primary_action_label: __('Close'), + primary_action: function() { + d.hide(); + } + }); + + d.show(); + + // Добавляем обработчик для кнопки выбора сертификата + d.$wrapper.find('.select-cert').on('click', function() { + var index = $(this).data('index'); + var cert = certificates[index]; + + // Получаем имя для отображения + let certName = ''; + if (cert.taxpayerType === 'individual' && cert.individualInfo) { + certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`; + } else if (cert.legalInfo) { + certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`; + } else { + certName = `Certificate ${index + 1}`; + } + + d.hide(); + + // Показываем индикатор загрузки + frappe.show_alert({ + message: __('Selecting certificate and taxpayer, please wait...'), + indicator: 'blue' + }, 5); + + // Отправляем запрос на выбор сертификата + frappe.call({ + method: 'invoice_az.api.select_certificate', + args: { + 'asan_login_name': asan_login_name, + 'certificate_data': cert, + 'certificate_name': certName + }, + callback: function(r) { + if (r.message && r.message.success) { + // После успешного выбора сертификата автоматически вызываем выбор налогоплательщика + frappe.call({ + method: 'invoice_az.api.select_taxpayer', + args: { + 'asan_login_name': asan_login_name + }, + callback: function(r2) { + if (r2.message && r2.message.success) { + frappe.show_alert({ + message: __('Taxpayer selected successfully'), + indicator: 'green' + }, 5); + + // Показываем диалог фильтрации счетов + frappe.call({ + method: 'invoice_az.api.get_default_asan_login', + callback: function(r3) { + if (r3.message && r3.message.found) { + showInvoiceFilterDialog(r3.message); + } + } + }); + } else if (r2.message && r2.message.unauthorized) { + // Если ошибка авторизации, предлагаем авторизоваться заново + showReauthenticationDialog(asan_login_name); + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r2.message ? r2.message.message : __('Failed to select taxpayer') + }); + } + } + }); + } else if (r.message && r.message.unauthorized) { + // Если ошибка авторизации, предлагаем авторизоваться заново + showReauthenticationDialog(asan_login_name); + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Failed to select certificate') + }); + } + } + }); + }); +} \ No newline at end of file