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 += '' +
+ '| Type | ' +
+ 'Name | ' +
+ 'ID | ' +
+ 'Position | ' +
+ 'Status | ' +
+ 'Action | ' +
+ '
';
+
+ 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.taxpayerType || '') + ' | ' +
+ '' + name + ' | ' +
+ '' + id + ' | ' +
+ '' + (cert.position || '') + ' | ' +
+ '' + (cert.hasAccess ? 'Active' : 'Inactive') +
+ (cert.liquidated ? ' (Liquidated)' : '') + ' | ' +
+ ' | ' +
+ '
';
+ });
+
+ cert_table += '
';
+
+ // Отображаем диалоговое окно с таблицей
+ 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 += '' +
+ '| Serial Number | ' +
+ 'Sender | ' +
+ 'Receiver | ' +
+ 'Creation Date | ' +
+ 'Amount | ' +
+ 'Status | ' +
+ 'Actions | ' +
+ '
';
+
+ 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 += '' +
+ '| ' + serialNumber + ' | ' +
+ '' + senderName + ' | ' +
+ '' + receiverName + ' | ' +
+ '' + creationDate + ' | ' +
+ '' + amount + ' | ' +
+ '' + status + ' | ' +
+ ' | ' +
+ '
';
+ });
+
+ invoice_table += '
';
+
+ // Отображаем диалоговое окно с таблицей
+ 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 += '' +
+ '| Type | ' +
+ 'Name | ' +
+ 'ID | ' +
+ 'Position | ' +
+ 'Status | ' +
+ 'Action | ' +
+ '
';
+
+ 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.taxpayerType || '') + ' | ' +
+ '' + name + ' | ' +
+ '' + id + ' | ' +
+ '' + (cert.position || '') + ' | ' +
+ '' + (cert.hasAccess ? 'Active' : 'Inactive') +
+ (cert.liquidated ? ' (Liquidated)' : '') + ' | ' +
+ ' | ' +
+ '
';
+ });
+
+ cert_table += '
';
+
+ // Отображаем диалоговое окно с таблицей
+ 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')}
+
+
+
+
+ | ${__('Item ID')} |
+ ${__('Product Name')} |
+ ${__('Unit')} |
+ ${__('Quantity')} |
+ ${__('Price')} |
+ ${__('Cost')} |
+ ${__('VAT')} |
+
+
+
+ `;
+
+ if (invoice.items && invoice.items.length) {
+ invoice.items.forEach(function(item) {
+ items_table += `
+ | ${item.itemId || ''} |
+ ${item.productName || ''} |
+ ${item.unit || ''} |
+ ${item.quantity || 0} |
+ ${item.pricePerUnit || 0} |
+ ${item.cost || 0} |
+ ${item.vat18 ? 'Yes' : 'No'} |
+
`;
+ });
+ }
+
+ items_table += `
`;
+
+ // Объединяем всю информацию
+ 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 += '' +
+ '| Serial Number | ' +
+ 'Sender | ' +
+ 'Receiver | ' +
+ 'Creation Date | ' +
+ 'Amount | ' +
+ 'Status | ' +
+ 'Mapping Status | ' +
+ 'Actions | ' +
+ '
';
+
+ // Анализируем каждый инвойс для отображения
+ 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: `
+
+
+
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 += '' +
+ '| ' + serialNumber + ' | ' +
+ '' + senderName + ' | ' +
+ '' + receiverName + ' | ' +
+ '' + creationDate + ' | ' +
+ '' + amount + ' | ' +
+ '' + status + ' | ' +
+ '' + item.mapping_status + ' | ' +
+ ' | ' +
+ '
';
+ }
+
+ invoice_table += '
';
+
+ // Отображаем диалоговое окно с таблицей
+ 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 += '' +
+ '| Serial Number | ' +
+ 'Sender | ' +
+ 'Receiver | ' +
+ 'Creation Date | ' +
+ 'Amount | ' +
+ 'Status | ' +
+ 'Actions | ' +
+ '
';
+
+ 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 += '' +
+ '| ' + serialNumber + ' | ' +
+ '' + senderName + ' | ' +
+ '' + receiverName + ' | ' +
+ '' + creationDate + ' | ' +
+ '' + amount + ' | ' +
+ '' + status + ' | ' +
+ ' | ' +
+ '
';
+ });
+
+ invoice_table += '
';
+
+ // Отображаем диалоговое окно с таблицей
+ 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')}
+
+
+
+
+ | ${__('Item ID')} |
+ ${__('Product Name')} |
+ ${__('Unit')} |
+ ${__('Quantity')} |
+ ${__('Price')} |
+ ${__('Cost')} |
+ ${__('VAT')} |
+
+
+
+ `;
+
+ if (invoice.items && invoice.items.length) {
+ invoice.items.forEach(function(item) {
+ items_table += `
+ | ${item.itemId || ''} |
+ ${item.productName || ''} |
+ ${item.unit || ''} |
+ ${item.quantity || 0} |
+ ${item.pricePerUnit || 0} |
+ ${item.cost || 0} |
+ ${item.vat18 ? 'Yes' : 'No'} |
+
`;
+ });
+ }
+
+ items_table += `
`;
+
+ // Объединяем всю информацию
+ 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 += '' +
+ '| Type | ' +
+ 'Name | ' +
+ 'ID | ' +
+ 'Position | ' +
+ 'Status | ' +
+ 'Action | ' +
+ '
';
+
+ 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.taxpayerType || '') + ' | ' +
+ '' + name + ' | ' +
+ '' + id + ' | ' +
+ '' + (cert.position || '') + ' | ' +
+ '' + (cert.hasAccess ? 'Active' : 'Inactive') +
+ (cert.liquidated ? ' (Liquidated)' : '') + ' | ' +
+ ' | ' +
+ '
';
+ });
+
+ cert_table += '
';
+
+ // Отображаем диалоговое окно с таблицей
+ 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