invoice_az/invoice_az/api.py

4198 lines
173 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import frappe
import requests
import json
from frappe.utils import nowdate, now, cint, now_datetime, get_datetime
from frappe.model.document import Document
import re
from difflib import SequenceMatcher
import datetime
import random
import time
# Константы
RENEW_URL = "https://new.e-taxes.gov.az/api/po/auth/public/v1/renew"
MAX_RETRY_COUNT = 3
DEFAULT_HEADERS = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Referer": "https://new.e-taxes.gov.az/eportal/az/login/asan"
}
ACTIVITY_TIMEOUT_MINUTES = 10 # Настраиваемый параметр времени неактивности
@frappe.whitelist()
def record_etaxes_activity(asan_login_name=None):
"""Записывает время последней активности с e-taxes с задержкой 2 секунды"""
try:
# Если имя не указано, получаем дефолтный профиль
if not asan_login_name:
default_settings = frappe.get_all(
"Asan Login",
filters={"is_default": 1},
fields=["name"],
limit=1
)
if not default_settings:
return
asan_login_name = default_settings[0].name
# Запускаем отложенную задачу для обновления поля last_activity_time
frappe.enqueue(
'invoice_az.api.update_activity_time',
asan_login_name=asan_login_name,
queue='short',
timeout=300
)
except Exception as e:
frappe.log_error(f"Error recording activity: {str(e)}", "Activity Recording Error")
@frappe.whitelist()
def update_activity_time(asan_login_name):
"""Выполняет фактическое обновление поля last_activity_time с задержкой"""
try:
# Добавляем задержку для избежания конфликтов
time.sleep(2)
# Обновляем поле last_activity_time напрямую через БД
frappe.db.set_value("Asan Login", asan_login_name, "last_activity_time", now_datetime(), update_modified=False)
frappe.db.commit()
except Exception as e:
frappe.log_error(f"Error updating activity time: {str(e)}", "Activity Update Error")
def check_recent_activity(asan_login_name=None):
"""Проверяет, была ли активность пользователя за последние N минут"""
try:
# Если имя не указано, получаем дефолтный профиль
if not asan_login_name:
default_settings = frappe.get_all(
"Asan Login",
filters={"is_default": 1},
fields=["name"],
limit=1
)
if not default_settings:
return False
asan_login_name = default_settings[0].name
# Получаем время последней активности напрямую из БД
last_activity = frappe.db.get_value("Asan Login", asan_login_name, "last_activity_time")
# Если последней активности не было, возвращаем False
if not last_activity:
return False
# Проверяем, была ли активность за последние N минут
time_diff = now_datetime() - get_datetime(last_activity)
minutes_diff = time_diff.total_seconds() / 60
return minutes_diff <= ACTIVITY_TIMEOUT_MINUTES
except Exception as e:
frappe.log_error(f"Error checking activity: {str(e)}", "Activity Check Error")
return False # В случае ошибки считаем, что активности не было
@frappe.whitelist()
def renew_token(asan_login_name=None, retry_count=0, force=False):
"""Renews the main token with optimized performance and error handling"""
try:
# Получаем документ Asan Login
if not asan_login_name:
# Простой прямой запрос для получения дефолтного логина
default_settings = frappe.get_all(
"Asan Login",
filters={"is_default": 1},
fields=["name"],
limit=1
)
if not default_settings:
frappe.log_error("No default Asan Login found", "Token Renewal Error")
return {"success": False, "message": "No default Asan Login found"}
asan_login_name = default_settings[0].name
# Проверяем наличие активности, если force=False
if not check_recent_activity(asan_login_name):
frappe.logger().info(f"Token renewal skipped due to inactivity for {asan_login_name}")
return {"success": True, "message": "Token renewal skipped due to inactivity"}
# Получаем документ (без блокировки, поскольку запуск один)
asan_login = frappe.get_doc("Asan Login", asan_login_name)
# Проверяем наличие токена
if not asan_login.main_token:
frappe.log_error(f"No main token found for {asan_login_name}", "Token Renewal Error")
return {"success": False, "message": "No main token found"}
# Формируем заголовок авторизации
auth_header = f"Bearer {asan_login.main_token}"
# Подготавливаем заголовки запроса
headers = DEFAULT_HEADERS.copy()
headers["x-authorization"] = auth_header
# Отправляем запрос с таймаутом
response = requests.post(RENEW_URL, headers=headers, timeout=10)
# Check for specific HTTP status codes
if response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
# Обрабатываем ответ
if response.status_code == 200:
# Получаем новый токен из заголовков ответа
new_token = response.headers.get('x-authorization')
if new_token:
frappe.logger().info(f"Token renewal successful for {asan_login_name}")
# Проверяем, изменился ли токен
if new_token != asan_login.main_token:
# Обновляем токен напрямую через БД, чтобы избежать конфликта версий
frappe.db.set_value("Asan Login", asan_login_name, "main_token", new_token)
frappe.db.commit()
frappe.logger().info(f"Token updated for {asan_login_name}")
else:
frappe.logger().info(f"Token unchanged for {asan_login_name}")
return {"success": True, "message": "Token renewed successfully"}
else:
frappe.log_error(
"Token renewal received 200 but no token in response headers",
f"Token Renewal for {asan_login_name}"
)
return {"success": False, "message": "No token in response"}
elif response.status_code == 401:
# В случае ошибки авторизации меняем статус
frappe.db.set_value("Asan Login", asan_login_name, "auth_status", "Not Authenticated")
frappe.db.commit()
frappe.log_error(
f"Authorization error (401) for {asan_login_name}",
"Token Renewal Error"
)
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
else:
# Механизм повторных попыток при временных ошибках
if retry_count < MAX_RETRY_COUNT and response.status_code >= 500:
# Экспоненциальная задержка между попытками
wait_time = (2 ** retry_count) * (0.5 + random.random())
time.sleep(wait_time)
return renew_token(asan_login_name, retry_count + 1)
frappe.log_error(
f"Failed with status {response.status_code}: {response.text[:200]}",
f"Token Renewal for {asan_login_name}"
)
return {"success": False, "message": f"Failed with status {response.status_code}"}
except requests.RequestException as e:
# Сетевые ошибки подключения
if retry_count < MAX_RETRY_COUNT:
wait_time = (2 ** retry_count) * (0.5 + random.random())
time.sleep(wait_time)
return renew_token(asan_login_name, retry_count + 1)
# Check if the error is HTTP 401 or 500
if hasattr(e, 'response'):
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(
f"Network error: {str(e)}",
f"Token Renewal for {asan_login_name}"
)
return {"success": False, "message": f"Network error: {str(e)}"}
except Exception as e:
frappe.log_error(
f"Unexpected error: {str(e)}\n{frappe.get_traceback()}",
f"Token Renewal for {asan_login_name}"
)
return {"success": False, "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def setup_token_renewal():
"""Настраивает задание планировщика для обновления токенов"""
try:
# Проверка существования задания
job_exists = frappe.db.exists("Scheduled Job Type", "renew_etaxes_token")
if not job_exists:
# Создаем новое задание
doc = frappe.new_doc("Scheduled Job Type")
doc.update({
"name": "renew_etaxes_token",
"method": "invoice_az.api.renew_token", # Прямой вызов renew_token
"frequency": "Cron",
"cron_format": "*/4 * * * *", # Каждые 4 минуты
"create_log": 1,
"execute_alone": 1,
"enabled": 1
})
doc.insert(ignore_permissions=True)
frappe.db.commit()
# Добавляем в планировщик немедленно
try:
from frappe.utils.background_jobs import get_scheduler
scheduler = get_scheduler()
if scheduler:
scheduler.add_scheduled_job(doc)
except Exception as e:
frappe.log_error(f"Could not add job to scheduler: {str(e)}", "Scheduler Error")
return {"success": True, "message": "Scheduler job created"}
else:
# Обновляем существующее задание если нужно
job = frappe.get_doc("Scheduled Job Type", "renew_etaxes_token")
if job.method != "invoice_az.api.renew_token" or job.cron_format != "*/4 * * * *":
job.method = "invoice_az.api.renew_token"
job.cron_format = "*/4 * * * *"
job.save(ignore_permissions=True)
frappe.db.commit()
# Обновляем в планировщике
try:
from frappe.utils.background_jobs import get_scheduler
scheduler = get_scheduler()
if scheduler:
scheduler.remove_scheduled_job(job.name)
scheduler.add_scheduled_job(job)
except Exception as e:
frappe.log_error(f"Could not update scheduler: {str(e)}", "Scheduler Error")
return {"success": True, "message": "Scheduler job updated"}
return {"success": True, "message": "Scheduler job already exists"}
except Exception as e:
frappe.log_error(f"Error setting up token renewal: {str(e)}\n{frappe.get_traceback()}", "Token Renewal Setup Error")
return {"success": False, "message": str(e)}
@frappe.whitelist()
def get_auth_token(phone, user_id):
"""Getting authorization token"""
# Записываем активность
record_etaxes_activity()
url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/start"
payload = {
"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)
# Check for specific HTTP status codes
if response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
if response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
response.raise_for_status()
# Getting token from the response header
bearer_token = response.headers.get('x-authorization', '')
# Getting response data
resp_data = response.json()
return {
'bearer_token': bearer_token,
'response_data': resp_data
}
except requests.exceptions.HTTPError as e:
# Check if the error is HTTP 401 or 500
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in get_auth_token: {str(e)}", "API Error")
return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."}
except Exception as e:
frappe.log_error(f"Error getting auth token: {str(e)}", "API Error")
return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def check_auth_status(bearer_token):
"""Checking authorization status"""
# Записываем активность
record_etaxes_activity()
url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/status"
headers = {
"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"
}
# Adding token to headers if it exists
if bearer_token:
headers["x-authorization"] = f"Bearer {bearer_token}"
try:
response = requests.get(url, headers=headers)
# Check for specific HTTP status codes
if response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
if response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
response.raise_for_status()
# Getting status data
status_data = response.json()
return status_data
except requests.exceptions.HTTPError as e:
# Check if the error is HTTP 401 or 500
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in check_auth_status: {str(e)}", "API Error")
return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."}
except Exception as e:
frappe.log_error(f"Error checking auth status: {str(e)}", "API Error")
return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def get_invoices(token, filters=None):
"""Getting list of invoices with filtering options"""
# Записываем активность
record_etaxes_activity()
url = "https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/find.inbox"
# Base request parameters - изменено: только approved и approvedBySystem статусы
payload = {
"sortBy": "creationDate",
"sortAsc": True,
"statuses": ["approved", "approvedBySystem"], # Только утвержденные документы
"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
}
# Apply custom filters if they are passed
if filters:
try:
filters_dict = json.loads(filters)
for key, value in filters_dict.items():
if key in payload and value: # Update only existing keys and non-empty values
payload[key] = value
except Exception as e:
return
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)
if response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
# Check response status to handle 401 error
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:
# Check if the error is 401 Unauthorized
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in get_invoices: {str(e)}", "API Error")
return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."}
except Exception as e:
frappe.log_error(f"Error getting invoices: {str(e)}", "API Error")
return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def get_certificates(bearer_token):
"""Getting available certificates"""
# Записываем активность
record_etaxes_activity()
url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/certificates"
headers = {
"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)
if response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
# Check for 401 error
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:
# Check if the error is 401 Unauthorized
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in get_certificates: {str(e)}", "API Error")
return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."}
except Exception as e:
frappe.log_error(f"Error getting certificates: {str(e)}", "API Error")
return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def choose_taxpayer(bearer_token, owner_type, tin):
"""Selecting taxpayer after certificate selection"""
# Записываем активность
record_etaxes_activity()
url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/chooseTaxpayer"
payload = {
"ownerType": owner_type, # "legal" or "individual"
"legalTin": tin if owner_type == "legal" else None,
"individualFin": tin if owner_type == "individual" else None
}
# Remove None values from 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)
if response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
# Check for 401 error
if response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
response.raise_for_status()
# Get main token from response header
main_token = response.headers.get('x-authorization', '')
# Get response data
resp_data = response.json()
return {
'main_token': main_token,
'response_data': resp_data
}
except requests.exceptions.HTTPError as e:
# Check if the error is 401 Unauthorized
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in choose_taxpayer: {str(e)}", "API Error")
return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."}
except Exception as e:
frappe.log_error(f"Error choosing taxpayer: {str(e)}", "API Error")
return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def get_invoice_details(token, invoice_id):
"""Getting detailed information about an invoice"""
# Записываем активность
record_etaxes_activity()
url = f"https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/{invoice_id}?sourceSystem=avis"
headers = {
"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)
if response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
# Check for 401 error
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:
# Check if the error is 401 Unauthorized
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in get_invoice_details: {str(e)}", "API Error")
return {"error": "http_error", "message": "An unknown error occurred, please try again in a few minutes."}
except Exception as e:
frappe.log_error(f"Error getting invoice details: {str(e)}", "API Error")
return {"error": "unknown_error", "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def get_default_asan_login():
"""Get default authorization settings"""
try:
# Look for setting with is_default = 1
default_login = frappe.get_list(
"Asan Login",
filters={"is_default": 1},
fields=["name"]
)
if default_login:
# If default setting is found, return its data
asan_login = frappe.get_doc("Asan Login", default_login[0].name)
return {
"found": True,
"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:
# If no default setting is found, look for any first setting
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:
return {"found": False, "error": str(e)}
@frappe.whitelist()
def handle_authentication(asan_login_name=None):
"""Handles the authentication process for the specified or default setting"""
# Записываем активность
record_etaxes_activity()
try:
# If setting name is not specified, get default setting name
if not asan_login_name:
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")
# Get Asan Login document
asan_login = frappe.get_doc("Asan Login", asan_login_name)
# Check that there is a phone number and user ID
if not asan_login.phone or not asan_login.user_id:
return {"success": False, "message": "Phone and User ID are required."}
# Start the authorization process
auth_result = get_auth_token(asan_login.phone, asan_login.user_id)
# Check for errors
if auth_result and auth_result.get("error"):
return {"success": False, "message": auth_result.get("message")}
if auth_result and auth_result.get("bearer_token"):
# Save token and change status
asan_login.bearer_token = auth_result.get("bearer_token")
asan_login.auth_status = "Waiting for confirmation"
asan_login.save()
# Extract verification code from response if it exists
verification_code = None
if auth_result.get("response_data") and isinstance(auth_result.get("response_data"), dict):
verification_code = auth_result.get("response_data").get("verificationCode", None)
return {
"success": True,
"message": "Authentication initiated. Please confirm on your phone.",
"bearer_token": auth_result.get("bearer_token"),
"asan_login_name": asan_login_name,
"verification_code": verification_code
}
else:
return {"success": False, "message": "Failed to get authentication token."}
except Exception as e:
frappe.log_error(f"Error in handle_authentication: {str(e)}\n{frappe.get_traceback()}", "Authentication Error")
return {"success": False, "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def poll_auth_status(asan_login_name, bearer_token):
"""Checks authentication status"""
# Записываем активность
record_etaxes_activity()
try:
# Check status
status_result = check_auth_status(bearer_token)
# Check for errors
if status_result and status_result.get("error"):
return {"success": False, "message": status_result.get("message")}
# Get Asan Login document
asan_login = frappe.get_doc("Asan Login", asan_login_name)
if status_result and status_result.get("successful") == True:
# Update status and token
asan_login.auth_status = "Authenticated"
if status_result.get("bearer_token"):
asan_login.bearer_token = status_result.get("bearer_token")
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:
return {"success": False, "authenticated": False, "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def get_auth_certificates(asan_login_name):
"""Gets certificates for the specified Asan Login setting"""
# Записываем активность
record_etaxes_activity()
try:
# Get Asan Login document
asan_login = frappe.get_doc("Asan Login", asan_login_name)
# Check authorization status
if asan_login.auth_status != "Authenticated" and asan_login.auth_status != "Fully Authenticated":
return {"success": False, "message": "Not authenticated. Please login first."}
# Get certificates
certificates_result = get_certificates(asan_login.bearer_token)
# Check for authorization error
if certificates_result.get("error") == "unauthorized":
return {
"success": False,
"unauthorized": True,
"message": "Authentication required. Please login again."
}
# Check for server error
if certificates_result.get("error") == "server_error":
return {
"success": False,
"message": "Service temporarily unavailable. Please try again in a few minutes."
}
# Check for other errors
if certificates_result.get("error"):
return {
"success": False,
"message": certificates_result.get("message", "An unknown error occurred, please try again in a few minutes.")
}
# Check for certificates in the response
if certificates_result and certificates_result.get("certificates"):
# Save certificates in the document
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:
return {"success": False, "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def select_certificate(asan_login_name, certificate_data, certificate_name):
"""Selects certificate for the specified Asan Login setting"""
# Записываем активность
record_etaxes_activity()
try:
# Get Asan Login document
asan_login = frappe.get_doc("Asan Login", asan_login_name)
# Check authorization status
if asan_login.auth_status != "Authenticated" and asan_login.auth_status != "Fully Authenticated":
return {"success": False, "message": "Not authenticated. Please login first."}
# Convert JSON string to object if necessary
if isinstance(certificate_data, str):
certificate_data = json.loads(certificate_data)
# Save selected certificate
asan_login.selected_certificate = certificate_name
asan_login.selected_certificate_json = json.dumps(certificate_data)
asan_login.save()
return {
"success": True,
"message": "Certificate selected successfully."
}
except Exception as e:
frappe.log_error(f"Error in select_certificate: {str(e)}\n{frappe.get_traceback()}", "Select Certificate Error")
return {"success": False, "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def select_taxpayer(asan_login_name):
"""Selects taxpayer for the specified Asan Login setting"""
# Записываем активность
record_etaxes_activity()
try:
# Get Asan Login document
asan_login = frappe.get_doc("Asan Login", asan_login_name)
# Check authorization status
if asan_login.auth_status != "Authenticated" and asan_login.auth_status != "Fully Authenticated":
return {"success": False, "message": "Not authenticated. Please login first."}
# Check for selected certificate
if not asan_login.selected_certificate_json:
return {"success": False, "message": "No certificate selected."}
# Parse certificate data
cert_data = json.loads(asan_login.selected_certificate_json)
# Determine taxpayer type and ID
owner_type = cert_data.get("taxpayerType", "")
tin = ""
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."}
# Log taxpayer selection attempt
frappe.log_error(
f"Attempting to choose taxpayer: type={owner_type}, tin={tin}, bearer_token={asan_login.bearer_token[:10]}...",
"Choose Taxpayer"
)
# Select taxpayer
taxpayer_result = choose_taxpayer(asan_login.bearer_token, owner_type, tin)
# Check for authorization error
if taxpayer_result.get("error") == "unauthorized":
return {
"success": False,
"unauthorized": True,
"message": "Authentication required. Please login again."
}
# Check for server error
if taxpayer_result.get("error") == "server_error":
return {
"success": False,
"message": "Service temporarily unavailable. Please try again in a few minutes."
}
# Check for other errors
if taxpayer_result.get("error"):
return {
"success": False,
"message": taxpayer_result.get("message", "An unknown error occurred, please try again in a few minutes.")
}
# Check selection success
if taxpayer_result and taxpayer_result.get("main_token"):
# Save main token and response
asan_login.main_token = taxpayer_result.get("main_token")
asan_login.choose_taxpayer_response = json.dumps(taxpayer_result.get("response_data", {}))
asan_login.auth_status = "Fully Authenticated"
asan_login.save()
# Log successful taxpayer selection
frappe.log_error(
f"Successfully chose taxpayer: type={owner_type}, tin={tin}, main_token={asan_login.main_token[:10]}...",
"Choose Taxpayer Success"
)
return {
"success": True,
"message": "Taxpayer selected successfully."
}
else:
# Log taxpayer selection error
frappe.log_error(
f"Failed to choose taxpayer: type={owner_type}, tin={tin}, response={taxpayer_result}",
"Choose Taxpayer Error"
)
return {"success": False, "message": "Failed to select taxpayer."}
except Exception as e:
frappe.log_error(f"Error in select_taxpayer: {str(e)}\n{frappe.get_traceback()}", "Choose Taxpayer Exception")
return {"success": False, "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def find_or_create_item(item_name, item_code=None, unit=None):
"""Finds or creates an item by name and code"""
try:
# Check settings for automatic creation
try:
settings = frappe.get_doc("ETaxes Settings")
auto_create = settings.auto_create_items
except:
auto_create = True # Create new items by default
# Find item by code if specified
if item_code:
items = frappe.get_list(
"Item",
filters={"item_code": item_code},
fields=["name"]
)
if items:
return items[0].name
# Find item by name
items = frappe.get_list(
"Item",
filters={"item_name": item_name},
fields=["name"]
)
if items:
return items[0].name
# If not found and automatic creation is allowed, create a new item
if auto_create:
# Get settings for item creation
try:
item_group = settings.default_item_group
default_uom = settings.default_uom
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
# Create new item
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:
return None
@frappe.whitelist()
def find_or_create_supplier(supplier_name, supplier_tin):
"""Finds or creates a supplier by name and TIN"""
try:
# Check settings for automatic creation
try:
settings = frappe.get_doc("ETaxes Settings")
auto_create = settings.auto_create_suppliers
except:
auto_create = True # Create new suppliers by default
# Find supplier by TIN
suppliers = frappe.get_list(
"Supplier",
filters={"tax_id": supplier_tin},
fields=["name"]
)
if suppliers:
return suppliers[0].name
# Find supplier by name
suppliers = frappe.get_list(
"Supplier",
filters={"supplier_name": supplier_name},
fields=["name"]
)
if suppliers:
return suppliers[0].name
# If not found and automatic creation is allowed, create a new supplier
if auto_create:
# Get settings for supplier creation
try:
supplier_group = settings.default_supplier_group
supplier_type = settings.supplier_type
country = settings.country
except:
supplier_group = "All Supplier Groups"
supplier_type = "Company"
country = "Azerbaijan"
# Create new supplier
supplier_doc = frappe.new_doc("Supplier")
supplier_doc.supplier_name = supplier_name
supplier_doc.supplier_type = supplier_type
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:
return None
@frappe.whitelist()
def get_uom_for_unit(unit_name):
"""Converts unit name from invoice to system UOM with support for mappings"""
if not unit_name:
return "Nos" # Default UOM if none provided
# Try to find in settings first
try:
# Get active settings
settings = get_active_settings()
if settings and hasattr(settings, 'unit_mappings'):
# First we need to find corresponding E-Taxes Unit record
etaxes_units = frappe.get_all('E-Taxes Unit',
filters={'etaxes_unit_name': unit_name},
fields=['name'],
limit=1)
if etaxes_units:
etaxes_unit_name = etaxes_units[0].name
# Check if there's a mapping for this unit (case-insensitive)
for mapping in settings.unit_mappings:
if (mapping.etaxes_unit_name == etaxes_unit_name and
mapping.erp_unit):
return mapping.erp_unit
except Exception as e:
frappe.log_error(f"Error getting UOM mapping: {str(e)}", "UOM Mapping Error")
# If no mapping found, create new E-Taxes Unit record for this unit
# so it can be mapped later
if unit_name and not frappe.db.exists('E-Taxes Unit', {'etaxes_unit_name': unit_name}):
try:
# Generate a consistent code for the unit
unit_code = "UNIT-" + ''.join(e for e in unit_name if e.isalnum()).upper()
etaxes_unit = frappe.new_doc('E-Taxes Unit')
etaxes_unit.etaxes_unit_name = unit_name
etaxes_unit.etaxes_unit_code = unit_code
etaxes_unit.status = 'New'
etaxes_unit.insert(ignore_permissions=True)
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Unit: {str(e)}", "UOM Mapping Error")
# If nothing matched, return default UOM
return "Nos"
@frappe.whitelist()
def load_items_from_invoices(date_from, date_to, max_count=200, offset=0):
"""Loading items from invoices for a period with duplicate prevention"""
# Записываем активность
record_etaxes_activity()
try:
# Counters for tracking
created_count = 0
skipped_count = 0
failed_count = 0
# Get default settings
asan_login_settings = get_default_asan_login()
if not asan_login_settings.get('found'):
return {
'success': False,
'message': 'No Asan Login settings found'
}
# Ensure max_count is an integer
try:
max_count = int(max_count)
except (ValueError, TypeError):
max_count = 200
# Format filters for the API request
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": max_count,
"offset": offset,
"productCode": None,
"productName": None,
"receiverName": None,
"receiverTin": None,
"senderName": None,
"senderTin": None,
"serialNumber": None,
"sortAsc": True,
"sortBy": "creationDate",
"statuses": ["approved", "approvedBySystem"],
"types": ["current", "corrected"]
}
response = get_invoices(asan_login_settings['main_token'], json.dumps(filters))
if 'error' in response:
return {
'success': False,
'error': response.get('error'),
'message': response['message'] if 'message' in response else 'Failed to retrieve invoices'
}
# Extract data from response
items_data = response.get('data', []) or response.get('invoices', [])
# Check if more data is available
hasMore = False
if 'hasMore' in response:
hasMore = response.get('hasMore')
elif response.get('total', 0) > offset + max_count:
hasMore = True
# Get details for each invoice
processed_count = 0
unique_items = {} # Используем словарь для предотвращения дубликатов
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', [])
for item in items:
item_name = item.get('productName', '')
item_code = item.get('itemId', '') or f"CODE-{serial_number}-{processed_count}"
# Skip empty items
if not item_name:
failed_count += 1
continue
# ИСПРАВЛЕНИЕ: Проверяем существование записи с таким именем
if frappe.db.exists('E-Taxes Item', item_name):
skipped_count += 1
continue
# Используем имя товара как ключ для предотвращения дубликатов в одной загрузке
if item_name in unique_items:
skipped_count += 1
continue
# Добавляем в словарь уникальных товаров
unique_items[item_name] = {
'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
# Создаём записи для уникальных товаров
for item_name, item_data in unique_items.items():
try:
# Двойная проверка перед созданием (на случай конкурентного доступа)
if frappe.db.exists('E-Taxes Item', item_name):
skipped_count += 1
continue
doc = frappe.get_doc({
'doctype': 'E-Taxes Item',
**item_data
})
doc.insert(ignore_permissions=True)
created_count += 1
except frappe.DuplicateEntryError:
# Если всё-таки произошло дублирование, считаем как пропущенный
skipped_count += 1
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Item {item_name}: {str(e)}", "Load Items Error")
failed_count += 1
return {
'success': True,
'created_count': created_count,
'skipped_count': skipped_count,
'failed_count': failed_count,
'total_invoices': processed_count,
'hasMore': hasMore
}
except Exception as e:
frappe.log_error(f"Error in load_items_from_invoices: {str(e)}\n{frappe.get_traceback()}", "Load Items Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def load_parties_from_invoices(date_from, date_to, max_count=200, offset=0, invoice_type="purchase"):
"""Loading parties from invoices for a period with duplicate prevention"""
# Записываем активность
record_etaxes_activity()
try:
# Counters for tracking
created_count = 0
skipped_count = 0
failed_count = 0
# Get default settings
asan_login_settings = get_default_asan_login()
if not asan_login_settings.get('found'):
return {
'success': False,
'message': 'No Asan Login settings found'
}
# Ensure max_count is an integer
try:
max_count = int(max_count)
except (ValueError, TypeError):
max_count = 200
# Format filters for the API request
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": max_count,
"offset": offset,
"productCode": None,
"productName": None,
"receiverName": None,
"receiverTin": None,
"senderName": None,
"senderTin": None,
"serialNumber": None,
"sortAsc": True,
"sortBy": "creationDate",
"statuses": ["approved", "approvedBySystem"],
"types": ["current", "corrected"]
}
response = get_invoices(asan_login_settings['main_token'], json.dumps(filters))
if 'error' in response:
return {
'success': False,
'error': response.get('error'),
'message': response['message'] if 'message' in response else 'Failed to retrieve invoices'
}
# Extract data from response
invoices_data = response.get('data', []) or response.get('invoices', [])
# Check if more data is available
hasMore = False
if 'hasMore' in response:
hasMore = response.get('hasMore')
elif response.get('total', 0) > offset + max_count:
hasMore = True
if len(invoices_data) == 0:
return {
'success': True,
'created_count': 0,
'skipped_count': 0,
'failed_count': 0,
'total_invoices': 0,
'unique_parties': 0,
'hasMore': False,
'message': 'No invoices found for the specified period'
}
# For storing unique parties (suppliers and customers)
unique_parties = {} # Используем словарь для предотвращения дубликатов
# Process each invoice
processed_count = 0
for invoice in invoices_data:
processed_count += 1
invoice_id = invoice.get('id', 'No ID')
serial_number = invoice.get('serialNumber', '')
# Processing sender (Sender -> Supplier)
sender = invoice.get('sender', {})
if sender:
sender_name = sender.get('name', '')
sender_tin = sender.get('tin', '')
sender_address = sender.get('address', '')
if not sender_name or not sender_tin:
continue
# ИСПРАВЛЕНИЕ: Проверяем существование записи
existing = frappe.db.exists('E-Taxes Parties', {
'etaxes_party_name': sender_name,
'etaxes_tax_id': sender_tin
})
if existing:
skipped_count += 1
continue
# Create key for uniqueness check (в пределах одной загрузки)
sender_key = f"{sender_tin}|{sender_name}"
if sender_key not in unique_parties:
unique_parties[sender_key] = {
'etaxes_party_name': sender_name,
'etaxes_tax_id': sender_tin,
'etaxes_address': sender_address,
'etaxes_party_type': 'Sender',
'erp_party_type': 'Supplier',
'source_invoice': serial_number,
'status': 'New'
}
# Processing receiver (Receiver -> Customer) - Skip for purchase invoices
if invoice_type.lower() != "purchase":
receiver = invoice.get('receiver', {})
if receiver:
receiver_name = receiver.get('name', '')
receiver_tin = receiver.get('tin', '')
receiver_address = receiver.get('address', '')
if not receiver_name or not receiver_tin:
continue
# ИСПРАВЛЕНИЕ: Проверяем существование записи
existing = frappe.db.exists('E-Taxes Parties', {
'etaxes_party_name': receiver_name,
'etaxes_tax_id': receiver_tin
})
if existing:
skipped_count += 1
continue
# Create key for uniqueness check (в пределах одной загрузки)
receiver_key = f"{receiver_tin}|{receiver_name}"
if receiver_key not in unique_parties:
unique_parties[receiver_key] = {
'etaxes_party_name': receiver_name,
'etaxes_tax_id': receiver_tin,
'etaxes_address': receiver_address,
'etaxes_party_type': 'Receiver',
'erp_party_type': 'Customer',
'source_invoice': serial_number,
'status': 'New'
}
# Создаём записи для уникальных контрагентов
for party_key, party_data in unique_parties.items():
try:
# Двойная проверка перед созданием (на случай конкурентного доступа)
existing = frappe.db.exists('E-Taxes Parties', {
'etaxes_party_name': party_data['etaxes_party_name'],
'etaxes_tax_id': party_data['etaxes_tax_id']
})
if existing:
skipped_count += 1
continue
doc = frappe.get_doc({
'doctype': 'E-Taxes Parties',
**party_data
})
doc.insert(ignore_permissions=True)
created_count += 1
except frappe.DuplicateEntryError:
# Если всё-таки произошло дублирование, считаем как пропущенный
skipped_count += 1
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Party {party_data['etaxes_party_name']}: {str(e)}", "Load Parties Error")
failed_count += 1
return {
'success': True,
'created_count': created_count,
'skipped_count': skipped_count,
'failed_count': failed_count,
'total_invoices': processed_count,
'unique_parties': len(unique_parties),
'hasMore': hasMore
}
except Exception as e:
frappe.log_error(f"Error in load_parties_from_invoices: {str(e)}\n{frappe.get_traceback()}", "Load Parties Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def get_default_settings():
"""Getting active E-Taxes settings"""
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:
return {
'found': False,
'message': str(e)
}
@frappe.whitelist()
def normalize_azeri_text(text):
"""Normalization of Azerbaijani text for matching"""
if not text:
return text
# Replacements dictionary
replacements = {
'ə': 'e',
'Ə': 'E',
'ü': 'u',
'Ü': 'U',
'ö': 'o',
'Ö': 'O',
'ğ': 'g',
'Ğ': 'G',
'ı': 'i',
'I': 'I',
'ç': 'c',
'Ç': 'C',
'ş': 's',
'Ş': 'S'
}
# Also add variant ə->a and ş->sh
replacements_alt = replacements.copy()
replacements_alt.update({
'ə': 'a',
'Ə': 'A',
'ş': 'sh',
'Ş': 'SH'
})
# Create normalized versions
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
@frappe.whitelist()
def calculate_similarity(str1, str2, consider_azeri=True):
"""Calculating similarity between strings"""
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())
# Check all combinations
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 get_unmapped_items():
"""Getting unmapped items from E-Taxes"""
try:
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get all unmapped items from E-Taxes
unmapped_items = []
# 1. Get all items with 'New' status
etaxes_items = frappe.get_all('E-Taxes Item',
filters={'status': 'New'},
fields=['name', 'etaxes_item_name', 'etaxes_item_code', 'etaxes_unit', 'etaxes_price'])
# 2. Check if they are already in settings
existing_mappings = {}
for mapping in settings.item_mappings:
existing_mappings[mapping.etaxes_item_name] = mapping.erp_item
# 3. Add only those that are not in settings
for item in etaxes_items:
if item.name not in existing_mappings:
# Create item object with default settings
item_data = {
'name': item.name,
'etaxes_item_name': item.etaxes_item_name,
'etaxes_item_code': item.etaxes_item_code,
# Add default values
'item_group': settings.default_item_group,
'uom': settings.default_uom if not item.etaxes_unit else get_uom_for_unit(item.etaxes_unit),
'is_stock_item': settings.is_stock_item,
'is_purchase_item': settings.is_purchase_item,
'item_tax_template': settings.default_item_tax_template
}
# Add price if it exists
if hasattr(item, 'etaxes_price') and item.etaxes_price:
item_data['standard_rate'] = item.etaxes_price
unmapped_items.append(item_data)
return {
'success': True,
'items': unmapped_items
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def get_unmapped_parties():
"""Getting unmapped parties from E-Taxes Parties"""
try:
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get all unmapped parties from E-Taxes Parties
unmapped_parties = []
# Get list of existing mappings
existing_mappings = {}
for mapping in settings.party_mappings:
existing_mappings[mapping.etaxes_party_name] = mapping.erp_party
# Get unmapped parties from E-Taxes Parties
etaxes_parties = frappe.get_all('E-Taxes Parties',
filters={'status': 'New'},
fields=['name', 'etaxes_party_name', 'etaxes_tax_id',
'etaxes_party_type', 'erp_party_type', 'etaxes_address', 'source_invoice'])
# Filter only those that are not in mappings
for party in etaxes_parties:
if party.etaxes_party_name not in existing_mappings:
# Determine party type (Customer/Supplier)
party_type = party.erp_party_type or ('Supplier' if party.etaxes_party_type == 'Sender' else 'Customer')
# Create base object with common data
party_data = {
'name': party.name,
'etaxes_party_name': party.etaxes_party_name,
'etaxes_tax_id': party.etaxes_tax_id,
'etaxes_party_type': party.etaxes_party_type,
'erp_party_type': party_type,
# Common settings
'payment_terms': settings.default_payment_terms
}
# Add address if it exists
if party.etaxes_address:
party_data['address'] = party.etaxes_address
# Add source if it exists
if party.source_invoice:
party_data['source'] = party.source_invoice
# Add specific fields depending on type
if party_type == 'Supplier':
party_data['supplier_group'] = settings.default_supplier_group
else: # Customer
party_data['customer_group'] = settings.default_customer_group
party_data['territory'] = 'All Territories' # Default value
unmapped_parties.append(party_data)
return {
'success': True,
'parties': unmapped_parties
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def match_similar_items():
"""Matching items by similar names"""
# Записываем активность
record_etaxes_activity()
try:
import re
from difflib import SequenceMatcher
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get similarity threshold from settings
similarity_threshold = float(settings.similarity_threshold) / 100.0 if settings.similarity_threshold else 0.8
consider_azeri = settings.consider_azeri_chars
# Get all items with "New" status from E-Taxes
etaxes_items = frappe.get_all('E-Taxes Item',
filters={'status': 'New'},
fields=['name', 'etaxes_item_name', 'etaxes_item_code'])
# Get existing mappings
existing_mappings = {}
for mapping in settings.item_mappings:
existing_mappings[mapping.etaxes_item_name] = mapping.erp_item
# Get items with empty mapping (exist in table but erp_item is None or empty)
items_with_empty_mapping = []
for etaxes_id, erp_item in existing_mappings.items():
if not erp_item: # If erp_item is None or empty string
# Find corresponding E-Taxes item
for item in etaxes_items:
if item.name == etaxes_id:
items_with_empty_mapping.append(item)
break
# Filter only those that are not in mappings
unmapped_items = []
for item in etaxes_items:
if item.name not in existing_mappings:
unmapped_items.append(item)
# Combine two lists: items without mappings and items with empty mappings
items_to_process = unmapped_items + items_with_empty_mapping
# Get all items from system
system_items = frappe.get_all('Item', fields=['name', 'item_name', 'item_code'])
matched_count = 0
total_processed = len(items_to_process)
# Get fresh copy of settings document
doc = frappe.get_doc("E-Taxes Settings", settings.name)
# Process all items
for etaxes_item in items_to_process:
# Find similar item
best_match = None
best_score = 0
# Normalize E-Taxes item name
etaxes_name = normalize_string(etaxes_item.etaxes_item_name, consider_azeri)
for item in system_items:
# Normalize system item name
item_name = normalize_string(item.item_name, consider_azeri)
# Calculate similarity coefficient
name_score = SequenceMatcher(None, etaxes_name, item_name).ratio()
# Check if this is a better match
if name_score > best_score and name_score >= similarity_threshold:
best_score = name_score
best_match = item
if best_match:
# IMPORTANT: check if there is already a record for this item in mappings
existing_row = None
for idx, mapping in enumerate(doc.item_mappings):
if mapping.etaxes_item_name == etaxes_item.name:
existing_row = idx
break
if existing_row is not None:
# If row already exists, update its erp_item value
doc.item_mappings[existing_row].erp_item = best_match.name
doc.item_mappings[existing_row].mapping_type = 'Automatic'
else:
# If no row, add a new one
doc.append('item_mappings', {
'etaxes_item_name': etaxes_item.name,
'erp_item': best_match.name,
'mapping_type': 'Automatic'
})
matched_count += 1
# Update E-Taxes Item status only for items that have a match
item_doc = frappe.get_doc('E-Taxes Item', etaxes_item.name)
item_doc.status = 'Mapped'
item_doc.mapped_item = best_match.name
item_doc.save()
# Save settings only if matches were found
if matched_count > 0:
doc.save()
return {
'success': True,
'matched_count': matched_count,
'total_processed': total_processed,
'message': f'Matched {matched_count} out of {total_processed} items'
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def match_similar_parties():
"""Matching parties by similar names"""
# Записываем активность
record_etaxes_activity()
try:
import re
from difflib import SequenceMatcher
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get similarity threshold from settings
similarity_threshold = float(settings.similarity_threshold) / 100.0 if settings.similarity_threshold else 0.8
consider_azeri = settings.consider_azeri_chars
# Get all parties with "New" status from E-Taxes Parties
etaxes_parties = frappe.get_all('E-Taxes Parties',
filters={'status': 'New'},
fields=['name', 'etaxes_party_name', 'etaxes_tax_id',
'etaxes_party_type', 'erp_party_type'])
# Get existing mappings
existing_mappings = {}
for mapping in settings.party_mappings:
existing_mappings[mapping.etaxes_party_name] = mapping.erp_party
# Filter only those that are not in mappings or have empty mappings
parties_to_process = []
for party in etaxes_parties:
if party.etaxes_party_name not in existing_mappings or not existing_mappings[party.etaxes_party_name]:
parties_to_process.append(party)
matched_count = 0
total_processed = len(parties_to_process)
# Get fresh copy of settings document
doc = frappe.get_doc("E-Taxes Settings", settings.name)
# Process all parties
for etaxes_party in parties_to_process:
# Determine ERP party type based on etaxes_party_type
erp_party_type = etaxes_party.erp_party_type
# Get all parties of corresponding type from system
system_parties = frappe.get_all(erp_party_type,
fields=['name',
'supplier_name' if erp_party_type == 'Supplier' else 'customer_name'])
# Find similar party
best_match = None
best_score = 0
# Normalize E-Taxes party name
etaxes_name = normalize_string(etaxes_party.etaxes_party_name, consider_azeri)
for party in system_parties:
# Get party name depending on type
party_name = party.supplier_name if erp_party_type == 'Supplier' else party.customer_name
# Normalize system party name
party_name_normalized = normalize_string(party_name, consider_azeri)
# Calculate similarity coefficient
name_score = SequenceMatcher(None, etaxes_name, party_name_normalized).ratio()
# Check if this is a better match
if name_score > best_score and name_score >= similarity_threshold:
best_score = name_score
best_match = party
if best_match:
# IMPORTANT: check if there is already a record for this party in mappings
existing_row = None
for idx, mapping in enumerate(doc.party_mappings):
if mapping.etaxes_party_name == etaxes_party.etaxes_party_name:
existing_row = idx
break
if existing_row is not None:
# If row already exists, update its erp_party value
doc.party_mappings[existing_row].erp_party = best_match.name
doc.party_mappings[existing_row].party_type = erp_party_type
doc.party_mappings[existing_row].mapping_type = 'Automatic'
else:
# If no row, add a new one
doc.append('party_mappings', {
'etaxes_party_name': etaxes_party.etaxes_party_name,
'etaxes_tax_id': etaxes_party.etaxes_tax_id,
'party_type': erp_party_type,
'erp_party': best_match.name,
'mapping_type': 'Automatic'
})
matched_count += 1
# Update record in E-Taxes Parties
try:
party_doc = frappe.get_doc('E-Taxes Parties', etaxes_party.name)
party_doc.status = 'Mapped'
party_doc.mapped_party = best_match.name
party_doc.save()
except Exception as e:
pass
# Save settings after adding all mappings
if matched_count > 0:
doc.save()
return {
'success': True,
'matched_count': matched_count,
'total_processed': total_processed,
'message': f'Matched {matched_count} out of {total_processed} parties'
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_unmapped_items(settings_name):
"""Creating items for unmapped elements from E-Taxes settings table"""
# Записываем активность
record_etaxes_activity()
try:
import re
# Get settings
settings_doc = frappe.get_doc("E-Taxes Settings", settings_name)
# Get elements from table that have no mapping
unmapped_items = []
for mapping_item in settings_doc.item_mappings:
# Check that item has no mapping or it's empty
if not mapping_item.erp_item:
# Get E-Taxes Item document for this record
if frappe.db.exists('E-Taxes Item', mapping_item.etaxes_item_name):
etaxes_item = frappe.get_doc('E-Taxes Item', mapping_item.etaxes_item_name)
unmapped_items.append(etaxes_item)
if len(unmapped_items) == 0:
return {
"success": True,
"created_count": 0,
"error_count": 0,
"message": "No unmapped items in table to create"
}
created_count = 0
error_count = 0
# Collect individual settings for each element
mapping_settings = {}
for mapping in settings_doc.item_mappings:
item_settings = {}
if mapping.item_group:
item_settings['item_group'] = mapping.item_group
if mapping.uom:
item_settings['uom'] = mapping.uom
if mapping.is_stock_item is not None:
item_settings['is_stock_item'] = mapping.is_stock_item
if mapping.is_purchase_item is not None:
item_settings['is_purchase_item'] = mapping.is_purchase_item
if mapping.item_tax_template:
item_settings['item_tax_template'] = mapping.item_tax_template
if item_settings:
mapping_settings[mapping.etaxes_item_name] = item_settings
for etaxes_item in unmapped_items:
try:
# Check if an item with this name already exists
item_exists = frappe.db.exists('Item', {'item_name': etaxes_item.etaxes_item_name})
if item_exists:
continue
# Create new item
item_doc = frappe.new_doc("Item")
# Ensure item_code uniqueness, removing invalid characters
item_code = etaxes_item.etaxes_item_code or etaxes_item.etaxes_item_name
item_code = re.sub(r'[^a-zA-Z0-9_.-]', '', item_code)
if not item_code or len(item_code) < 3:
item_code = "ITEM-" + frappe.generate_hash(length=8)
# Check that item_code is unique
if frappe.db.exists('Item', {'item_code': item_code}):
item_code = item_code + "-" + frappe.generate_hash(length=5)
item_doc.item_code = item_code
item_doc.item_name = etaxes_item.etaxes_item_name
item_doc.description = etaxes_item.etaxes_item_name
# Check if there are individual settings for this element
individual_settings = mapping_settings.get(etaxes_item.name, {})
# Apply settings considering individual priorities
item_group = individual_settings.get('item_group', settings_doc.default_item_group)
stock_uom = individual_settings.get('uom', settings_doc.default_uom)
# Check item_group existence
if not frappe.db.exists('Item Group', item_group):
item_group = "All Item Groups"
# Check UOM existence
if not frappe.db.exists('UOM', stock_uom):
stock_uom = "Nos"
item_doc.item_group = item_group
item_doc.stock_uom = stock_uom
# For boolean fields check that they are not None
if 'is_stock_item' in individual_settings:
item_doc.is_stock_item = individual_settings['is_stock_item']
else:
item_doc.is_stock_item = getattr(settings_doc, 'is_stock_item', 1)
if 'is_purchase_item' in individual_settings:
item_doc.is_purchase_item = individual_settings['is_purchase_item']
else:
item_doc.is_purchase_item = getattr(settings_doc, 'is_purchase_item', 1)
# Correctly add tax template
if 'item_tax_template' in individual_settings:
tax_template = individual_settings['item_tax_template']
if frappe.db.exists('Item Tax Template', tax_template):
tax_row = item_doc.append('taxes', {})
tax_row.item_tax_template = tax_template
elif hasattr(settings_doc, 'default_item_tax_template') and settings_doc.default_item_tax_template:
tax_template = settings_doc.default_item_tax_template
if frappe.db.exists('Item Tax Template', tax_template):
tax_row = item_doc.append('taxes', {})
tax_row.item_tax_template = tax_template
# Save item
item_doc.insert()
# Update mapping in settings
for mapping in settings_doc.item_mappings:
if mapping.etaxes_item_name == etaxes_item.name:
mapping.erp_item = item_doc.name
mapping.mapping_type = 'Automatic'
break
# Update E-Taxes Item status
etaxes_item.status = 'Mapped'
etaxes_item.mapped_item = item_doc.name
etaxes_item.save()
created_count += 1
except Exception as e:
error_count += 1
# Save settings after adding all mappings
if created_count > 0:
settings_doc.save()
return {
"success": True,
"created_count": created_count,
"error_count": error_count,
"message": f"Created {created_count} items, errors: {error_count}"
}
except Exception as e:
return {
"success": False,
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_purchase_invoice_from_order(purchase_order_name):
"""Создает Purchase Invoice из Purchase Order используя стандартную функцию ERPNext"""
try:
# Импортируем стандартную функцию из модуля Purchase Order
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
# Создаем Purchase Invoice из Purchase Order
pi_doc = make_purchase_invoice(purchase_order_name)
# Устанавливаем даты
pi_doc.posting_date = frappe.utils.nowdate()
pi_doc.due_date = frappe.utils.add_days(frappe.utils.nowdate(), 30) # 30 дней для оплаты
# ДОБАВЛЕНО: Устанавливаем галочку is_taxes_doc для Purchase Invoice
pi_doc.is_taxes_doc = 1
# Сохраняем документ
pi_doc.insert(ignore_permissions=True)
frappe.log_error(f"Purchase Invoice {pi_doc.name} created from Purchase Order {purchase_order_name}", "Purchase Invoice Creation")
# Возвращаем имя созданного Purchase Invoice
return pi_doc.name
except Exception as e:
frappe.log_error(f"Error creating Purchase Invoice from PO {purchase_order_name}: {str(e)}\n{frappe.get_traceback()}", "Purchase Invoice Creation Error")
return None
@frappe.whitelist()
def import_invoice_with_mapping(invoice_data, purchase_order_name=None, schedule_date=None, warehouse=None):
"""Import invoice taking into account mappings and processing unmapped elements"""
# Записываем активность
record_etaxes_activity()
try:
# Deserialize if necessary
if isinstance(invoice_data, str):
invoice_data = json.loads(invoice_data)
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Create mapping dictionaries
item_mappings = {}
for mapping in settings.item_mappings:
# mapping.etaxes_item_name содержит name документа E-Taxes Item
if mapping.etaxes_item_name and mapping.erp_item:
item_mappings[mapping.etaxes_item_name] = mapping.erp_item
party_mappings = {}
for mapping in settings.party_mappings:
if mapping.etaxes_party_name and mapping.erp_party:
key = f"{mapping.etaxes_party_name.lower()}|{mapping.etaxes_tax_id.lower() if mapping.etaxes_tax_id else ''}"
party_mappings[key] = (mapping.erp_party, mapping.party_type)
unit_mappings = {}
for mapping in settings.unit_mappings:
if mapping.etaxes_unit_name and mapping.erp_unit:
# mapping.etaxes_unit_name содержит name документа E-Taxes Unit
unit_mappings[mapping.etaxes_unit_name] = mapping.erp_unit
# Get default warehouse
default_warehouse = warehouse
if not default_warehouse:
# If warehouse not specified, try to get from settings
if hasattr(settings, 'default_warehouse') and settings.default_warehouse:
default_warehouse = settings.default_warehouse
else:
# If warehouse not specified in settings, try to get default warehouse for company
company = frappe.defaults.get_user_default('Company')
if company:
default_warehouse = frappe.db.get_value('Company', company, 'default_warehouse')
# If still no warehouse, take first active warehouse
if not default_warehouse:
warehouses = frappe.get_all('Warehouse',
filters={'is_group': 0, 'disabled': 0},
fields=['name'],
limit=1)
if warehouses:
default_warehouse = warehouses[0].name
# Check warehouse existence
if not default_warehouse:
return {
'success': False,
'message': 'Default warehouse not found. Please specify warehouse in E-Taxes settings.'
}
# Get or create Purchase Order
if purchase_order_name:
po = frappe.get_doc('Purchase Order', purchase_order_name)
else:
# Create new Purchase Order
po = frappe.new_doc('Purchase Order')
# Set supplier
sender = invoice_data.get('sender', {})
sender_key = f"{sender.get('name', '').lower()}|{sender.get('tin', '').lower()}"
if sender_key in party_mappings and party_mappings[sender_key][0]:
po.supplier = party_mappings[sender_key][0]
else:
return {
'success': False,
'unmatched_parties': [{
'name': sender.get('name', ''),
'tin': sender.get('tin', ''),
'type': 'Sender'
}],
'message': f'No mapping found for supplier: {sender.get("name", "")}'
}
# Set dates
po.transaction_date = frappe.utils.today()
# Get date from invoice or parameter if specified
if schedule_date:
po.schedule_date = schedule_date
elif invoice_data.get("creationDate"):
po.schedule_date = frappe.utils.getdate(invoice_data.get("creationDate"))
else:
po.schedule_date = frappe.utils.today()
po.company = frappe.defaults.get_user_default('Company')
# Add/update additional fields from invoice
po.title = f"Invoice {invoice_data.get('serialNumber', '')}"
po.invoice_serial_number = invoice_data.get("serialNumber") or invoice_data.get("number", "")
# Clear existing items if need to update them completely
if purchase_order_name and invoice_data.get("items"):
po.items = []
date_to_use = None
if schedule_date:
date_to_use = schedule_date
elif invoice_data.get("creationDate"):
date_to_use = frappe.utils.getdate(invoice_data.get("creationDate"))
else:
date_to_use = frappe.utils.today()
# Variable for tracking added items
added_items_count = 0
unmatched_items = []
unmatched_units = []
# Add items from invoice
if invoice_data.get("items"):
for item in invoice_data.get("items", []):
# Подготавливаем данные товара
item_name = item.get("productName", "")
item_code = item.get("itemId", "")
# ИСПРАВЛЕНО: Ищем соответствие товара
# Так как autoname = "field:etaxes_item_name", name документа E-Taxes Item = etaxes_item_name
# Поэтому проверяем напрямую по item_name в mappings
mapped_item = item_mappings.get(item_name)
if not mapped_item:
# Если соответствие не найдено, добавляем в список несопоставленных
unmatched_items.append({
'name': item_name,
'code': item_code
})
continue
# ИСПРАВЛЕНО: Для единиц измерения
unit_name = item.get("unit", "")
mapped_uom = None
if unit_name:
# Так как autoname = "field:etaxes_unit_name", name документа E-Taxes Unit = etaxes_unit_name
# Поэтому проверяем напрямую по unit_name в mappings
mapped_uom = unit_mappings.get(unit_name)
# Если соответствие единицы не найдено, получаем UOM из товара
if not mapped_uom:
if unit_name:
# Добавляем в список несопоставленных единиц
unmatched_units.append({
'name': unit_name
})
# Используем UOM из настроек товара
try:
item_doc = frappe.get_doc('Item', mapped_item)
mapped_uom = item_doc.stock_uom
except:
mapped_uom = "Nos" # Fallback
# Add position to PO
po_item = frappe.new_doc("Purchase Order Item")
po_item.parent = po.name
po_item.parenttype = "Purchase Order"
po_item.parentfield = "items"
po_item.item_code = mapped_item
# ИСПРАВЛЕНИЕ: Получаем название и описание из базы данных Item
try:
item_doc = frappe.get_doc('Item', mapped_item)
po_item.item_name = item_doc.item_name
po_item.description = item_doc.description or item_doc.item_name
except:
# Fallback на название из E-Taxes если не удалось получить из базы
po_item.item_name = item.get("productName", "")
po_item.description = item.get("productName", "")
po_item.qty = item.get("quantity", 0)
po_item.rate = item.get("pricePerUnit", 0)
po_item.amount = item.get("cost", 0)
po_item.uom = mapped_uom
# IMPORTANT: Set schedule_date for each row
po_item.schedule_date = date_to_use
# IMPORTANT: Set default warehouse
po_item.warehouse = default_warehouse
po.append("items", po_item)
added_items_count += 1
# Check if there are unmapped items
if unmatched_items:
return {
'success': False,
'unmatched_items': unmatched_items,
'message': f'No mapping found for {len(unmatched_items)} items'
}
# Check if there are unmapped units
if unmatched_units:
return {
'success': False,
'unmatched_units': unmatched_units,
'message': f'No mapping found for {len(unmatched_units)} units'
}
# Check that there is at least one element in table
if added_items_count == 0:
frappe.log_error(
f"No items were added to PO. Invoice data: {invoice_data}",
"Import Invoice Error"
)
return {
'success': False,
'message': 'Could not add any items to order'
}
# Save document
po.save()
# Additional check that all schedule_date and warehouse are set
for item in po.items:
if not item.schedule_date:
item.schedule_date = date_to_use
if not item.warehouse:
item.warehouse = default_warehouse
# Save again to ensure changes are applied
po.save()
# ИЗМЕНЕНИЕ: Создаем E-Taxes Purchase и устанавливаем связь ДО submit'а
invoice_id = invoice_data.get('id', '')
serial_number = invoice_data.get('serialNumber', '')
sender_name = invoice_data.get('sender', {}).get('name', '') if invoice_data.get('sender') else ''
total = invoice_data.get('totalAmount', 0) or invoice_data.get('amount', 0)
# Создаем E-Taxes Purchase
etaxes_purchase_result = create_etaxes_purchase(invoice_id, date_to_use, sender_name, total)
if etaxes_purchase_result and etaxes_purchase_result.get('success'):
# Устанавливаем поля E-Taxes ДО submit'а
po.is_taxes_doc = 1
po.taxes_doc = etaxes_purchase_result.get('name')
# Сохраняем изменения
po.save()
# ИЗМЕНЕНИЕ: Делаем Submit для Purchase Order
try:
po.submit()
frappe.log_error(f"Purchase Order {po.name} submitted successfully", "Import Invoice Success")
except Exception as e:
frappe.log_error(f"Error submitting Purchase Order {po.name}: {str(e)}", "Submit PO Error")
return {
'success': False,
'message': f'Failed to submit Purchase Order: {str(e)}'
}
# ИЗМЕНЕНИЕ: Создаем Purchase Invoice
try:
pi_name = create_purchase_invoice_from_order(po.name)
if pi_name:
# Делаем Submit для Purchase Invoice
pi = frappe.get_doc("Purchase Invoice", pi_name)
pi.submit()
frappe.log_error(f"Purchase Invoice {pi_name} created and submitted successfully", "Import Invoice Success")
return {
'success': True,
'message': 'Invoice data imported successfully. Purchase Order and Purchase Invoice created.',
'purchase_order': po.name,
'purchase_invoice': pi_name
}
else:
# Если не удалось создать Purchase Invoice, всё равно возвращаем успех с Purchase Order
return {
'success': True,
'message': 'Invoice data imported successfully. Purchase Order created, but Purchase Invoice creation failed.',
'purchase_order': po.name
}
except Exception as e:
frappe.log_error(f"Error creating or submitting Purchase Invoice: {str(e)}", "Submit PI Error")
# Если не удалось создать Purchase Invoice, всё равно возвращаем успех с Purchase Order
return {
'success': True,
'message': 'Invoice data imported successfully. Purchase Order created, but Purchase Invoice creation failed.',
'purchase_order': po.name
}
except Exception as e:
frappe.log_error(f"Error in import_invoice_with_mapping: {str(e)}\n{frappe.get_traceback()}", "Import Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
# Также нужно убрать создание новых записей из других функций:
@frappe.whitelist()
def load_items_from_invoices(date_from, date_to, max_count=200, offset=0):
"""Loading items from invoices for a period with duplicate prevention"""
# Записываем активность
record_etaxes_activity()
try:
# Counters for tracking
created_count = 0
skipped_count = 0
failed_count = 0
# Get default settings
asan_login_settings = get_default_asan_login()
if not asan_login_settings.get('found'):
return {
'success': False,
'message': 'No Asan Login settings found'
}
# Ensure max_count is an integer
try:
max_count = int(max_count)
except (ValueError, TypeError):
max_count = 200
# Format filters for the API request
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": max_count,
"offset": offset,
"productCode": None,
"productName": None,
"receiverName": None,
"receiverTin": None,
"senderName": None,
"senderTin": None,
"serialNumber": None,
"sortAsc": True,
"sortBy": "creationDate",
"statuses": ["approved", "approvedBySystem"],
"types": ["current", "corrected"]
}
response = get_invoices(asan_login_settings['main_token'], json.dumps(filters))
if 'error' in response:
return {
'success': False,
'error': response.get('error'),
'message': response['message'] if 'message' in response else 'Failed to retrieve invoices'
}
# Extract data from response
items_data = response.get('data', []) or response.get('invoices', [])
# Check if more data is available
hasMore = False
if 'hasMore' in response:
hasMore = response.get('hasMore')
elif response.get('total', 0) > offset + max_count:
hasMore = True
# Get details for each invoice
processed_count = 0
unique_items = {} # Используем словарь для предотвращения дубликатов
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', [])
for item in items:
item_name = item.get('productName', '')
item_code = item.get('itemId', '') or f"CODE-{serial_number}-{processed_count}"
# Skip empty items
if not item_name:
failed_count += 1
continue
# ИСПРАВЛЕНО: Проверяем существование записи с таким именем
# Так как autoname = "field:etaxes_item_name", то name документа = etaxes_item_name
if frappe.db.exists('E-Taxes Item', item_name):
skipped_count += 1
continue
# Используем имя товара как ключ для предотвращения дубликатов в одной загрузке
if item_name in unique_items:
skipped_count += 1
continue
# Добавляем в словарь уникальных товаров
unique_items[item_name] = {
'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
# Создаём записи для уникальных товаров
for item_name, item_data in unique_items.items():
try:
# Двойная проверка перед созданием (на случай конкурентного доступа)
if frappe.db.exists('E-Taxes Item', item_name):
skipped_count += 1
continue
doc = frappe.get_doc({
'doctype': 'E-Taxes Item',
**item_data
})
doc.insert(ignore_permissions=True)
created_count += 1
except frappe.DuplicateEntryError:
# Если всё-таки произошло дублирование, считаем как пропущенный
skipped_count += 1
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Item {item_name}: {str(e)}", "Load Items Error")
failed_count += 1
return {
'success': True,
'created_count': created_count,
'skipped_count': skipped_count,
'failed_count': failed_count,
'total_invoices': processed_count,
'hasMore': hasMore
}
except Exception as e:
frappe.log_error(f"Error in load_items_from_invoices: {str(e)}\n{frappe.get_traceback()}", "Load Items Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
# Helper functions
@frappe.whitelist()
def get_active_settings():
"""Getting active E-Taxes settings"""
settings_list = frappe.get_all('E-Taxes Settings',
filters={'is_active': 1},
order_by='creation desc',
limit=1)
if not settings_list:
return None
return frappe.get_doc('E-Taxes Settings', settings_list[0].name)
@frappe.whitelist()
def normalize_string(text, consider_azeri=True):
"""Normalizing string for comparison"""
import re
if not text:
return ''
# Convert to lowercase
text = text.lower()
# Remove all special characters and numbers
text = re.sub(r'[^\w\s]', '', text)
text = re.sub(r'\d+', '', text)
# Replace Azerbaijani characters if needed
if consider_azeri:
azeri_replacements = {
'ə': 'e',
'ü': 'u',
'ö': 'o',
'ğ': 'g',
'ı': 'i',
'ç': 'c',
'ş': 's'
}
for azeri_char, latin_char in azeri_replacements.items():
text = text.replace(azeri_char, latin_char)
# Remove extra spaces
text = ' '.join(text.split())
return text
@frappe.whitelist()
def create_unmapped_parties(settings_name):
"""Creating parties for unmapped elements from E-Taxes settings table"""
# Записываем активность
record_etaxes_activity()
try:
# Get settings
settings_doc = frappe.get_doc("E-Taxes Settings", settings_name)
# Get elements from table that have no mapping
unmapped_parties = []
for mapping_party in settings_doc.party_mappings:
# Check that party has no mapping or it's empty
if not mapping_party.erp_party:
# Get E-Taxes Parties document for this record
parties = frappe.get_all('E-Taxes Parties',
filters={
'etaxes_party_name': mapping_party.etaxes_party_name,
'etaxes_tax_id': mapping_party.etaxes_tax_id
},
fields=['name'])
if parties:
etaxes_party = frappe.get_doc('E-Taxes Parties', parties[0].name)
# Add party type from settings
etaxes_party.erp_party_type = mapping_party.party_type
unmapped_parties.append(etaxes_party)
if len(unmapped_parties) == 0:
return {
"success": True,
"created_count": 0,
"error_count": 0,
"message": "No unmapped partners in table to create"
}
created_count = 0
error_count = 0
# Collect individual settings for each element
mapping_settings = {}
for mapping in settings_doc.party_mappings:
party_settings = {}
if mapping.supplier_group:
party_settings['supplier_group'] = mapping.supplier_group
if mapping.customer_group:
party_settings['customer_group'] = mapping.customer_group
if mapping.territory:
party_settings['territory'] = mapping.territory
if mapping.payment_terms:
party_settings['payment_terms'] = mapping.payment_terms
if party_settings:
key = f"{mapping.etaxes_party_name}|{mapping.etaxes_tax_id}"
mapping_settings[key] = party_settings
for etaxes_party in unmapped_parties:
try:
# Determine party type (Customer/Supplier)
party_type = etaxes_party.erp_party_type
# Check if such party with this name and type already exists
party_field = 'supplier_name' if party_type == 'Supplier' else 'customer_name'
party_exists = frappe.db.exists(party_type, {party_field: etaxes_party.etaxes_party_name})
if party_exists:
continue
# Create new party
party_doc = frappe.new_doc(party_type)
# Get individual settings
key = f"{etaxes_party.etaxes_party_name}|{etaxes_party.etaxes_tax_id}"
individual_settings = mapping_settings.get(key, {})
# Set basic fields
if party_type == "Supplier":
party_doc.supplier_name = etaxes_party.etaxes_party_name
party_doc.supplier_type = "Company" # Can be configured if needed
# Apply settings considering individual priorities
supplier_group = individual_settings.get('supplier_group', settings_doc.default_supplier_group)
if not frappe.db.exists('Supplier Group', supplier_group):
supplier_group = "All Supplier Groups"
party_doc.supplier_group = supplier_group
# If tax_id specified, add it
if etaxes_party.etaxes_tax_id:
party_doc.tax_id = etaxes_party.etaxes_tax_id
# If payment terms specified, add them
if 'payment_terms' in individual_settings:
party_doc.payment_terms = individual_settings['payment_terms']
elif hasattr(settings_doc, 'default_payment_terms') and settings_doc.default_payment_terms:
party_doc.payment_terms = settings_doc.default_payment_terms
else: # party_type == "Customer"
party_doc.customer_name = etaxes_party.etaxes_party_name
party_doc.customer_type = "Company" # Can be configured if needed
# Apply settings considering individual priorities
customer_group = individual_settings.get('customer_group', settings_doc.default_customer_group)
if not frappe.db.exists('Customer Group', customer_group):
customer_group = "All Customer Groups"
party_doc.customer_group = customer_group
# Territory
territory = individual_settings.get('territory', "All Territories")
if not frappe.db.exists('Territory', territory):
territory = "All Territories"
party_doc.territory = territory
# If tax_id specified, add it
if etaxes_party.etaxes_tax_id:
party_doc.tax_id = etaxes_party.etaxes_tax_id
# If payment terms specified, add them
if 'payment_terms' in individual_settings:
party_doc.payment_terms = individual_settings['payment_terms']
elif hasattr(settings_doc, 'default_payment_terms') and settings_doc.default_payment_terms:
party_doc.payment_terms = settings_doc.default_payment_terms
# Save party
party_doc.insert()
# Update mapping in settings
for mapping in settings_doc.party_mappings:
if mapping.etaxes_party_name == etaxes_party.etaxes_party_name and mapping.etaxes_tax_id == etaxes_party.etaxes_tax_id:
mapping.erp_party = party_doc.name
mapping.party_type = party_type
mapping.mapping_type = 'Automatic'
break
# Update E-Taxes Parties status
etaxes_party.status = 'Mapped'
etaxes_party.mapped_party = party_doc.name
etaxes_party.save()
created_count += 1
except Exception as e:
error_count += 1
# Save settings after adding all mappings
if created_count > 0:
settings_doc.save()
return {
"success": True,
"created_count": created_count,
"error_count": error_count,
"message": f"Created {created_count} parties, errors: {error_count}"
}
except Exception as e:
return {
"success": False,
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def handle_unauthorized_request(asan_login_name, operation_name="API request"):
"""Common function for handling unauthorized requests"""
# Log event
frappe.log_error(
f"Unauthorized request for operation: {operation_name}, asan_login_name: {asan_login_name}",
"Unauthorized API Request"
)
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"operation": operation_name
}
@frappe.whitelist()
def check_token_validity(asan_login_name=None):
"""Проверяет валидность main_token для указанных настроек Asan Login"""
# Записываем активность
record_etaxes_activity()
try:
# Если имя не указано, получаем настройки по умолчанию
if not asan_login_name:
default_settings = get_default_asan_login()
if not default_settings.get("found", False):
return {"valid": False, "message": "No Asan Login settings found."}
asan_login_name = default_settings.get("name")
# Получаем документ Asan Login
asan_login = frappe.get_doc("Asan Login", asan_login_name)
# Проверяем наличие main_token
if not asan_login.main_token:
return {"valid": False, "message": "Main token not found."}
# Создаем минимальный фильтр для запроса инвойсов
minimal_filter = {
"maxCount": 1, # Запрашиваем только один инвойс для минимальной нагрузки
"offset": 0
}
# Пробуем получить список инвойсов для проверки main_token
invoices_result = get_invoices(asan_login.main_token, json.dumps(minimal_filter))
# Проверяем на ошибку авторизации
if invoices_result.get("error") == "unauthorized":
# Обновляем статус авторизации в документе
asan_login.auth_status = "Not Authenticated"
asan_login.save(ignore_permissions=True)
frappe.db.commit()
return {
"valid": False,
"message": "Authentication required. Main token is expired."
}
# Проверяем на ошибку сервера
if invoices_result.get("error") == "server_error":
return {
"valid": False,
"message": "Service temporarily unavailable. Please try again in a few minutes."
}
# Проверяем на другие ошибки
if invoices_result.get("error"):
return {
"valid": False,
"message": invoices_result.get("message", "An unknown error occurred.")
}
# Если нет ошибок, токен валиден
return {"valid": True, "message": "Main token is valid."}
except Exception as e:
frappe.log_error(f"Error in check_token_validity: {str(e)}\n{frappe.get_traceback()}", "Token Validity Check")
return {"valid": False, "message": "An unknown error occurred, please try again in a few minutes."}
@frappe.whitelist()
def get_etaxes_purchases():
"""Gets list of all E-Taxes Purchase records"""
try:
purchases = frappe.get_all('E-Taxes Purchase',
fields=['name', 'etaxes_id', 'date', 'party', 'total'])
# Debug logging
frappe.logger().info(f"Retrieved {len(purchases)} E-Taxes Purchase records")
return {
'success': True,
'purchases': purchases
}
except Exception as e:
frappe.log_error(f"Error in get_etaxes_purchases: {str(e)}", "API Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_etaxes_purchase(etaxes_id, date, party, total):
"""Creates E-Taxes Purchase record for tracking imported invoices"""
# Записываем активность
record_etaxes_activity()
try:
# Normalize ID for storage
etaxes_id = str(etaxes_id).strip() if etaxes_id else ""
# Debug logging
frappe.logger().info(f"Creating E-Taxes Purchase: ID={etaxes_id}, date={date}, party={party}, total={total}")
# Check if record with such etaxes_id already exists
existing = frappe.db.get_value('E-Taxes Purchase', {'etaxes_id': etaxes_id}, 'name')
if existing:
# Debug logging
frappe.logger().info(f"E-Taxes Purchase already exists: {existing}")
return {
'success': True,
'message': 'Record already exists',
'name': existing
}
# Create new record
etaxes_purchase = frappe.new_doc('E-Taxes Purchase')
etaxes_purchase.etaxes_id = etaxes_id
etaxes_purchase.date = date
etaxes_purchase.party = party
etaxes_purchase.total = total
etaxes_purchase.insert()
frappe.db.commit() # Explicit commit
# Debug logging
frappe.logger().info(f"Created new E-Taxes Purchase: {etaxes_purchase.name}")
return {
'success': True,
'name': etaxes_purchase.name
}
except Exception as e:
frappe.log_error(f"Error in create_etaxes_purchase: {str(e)}\n{frappe.get_traceback()}", "API Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def link_purchase_order_to_etaxes(purchase_order, etaxes_purchase):
"""Sets link between Purchase Order and E-Taxes Purchase"""
# Записываем активность
record_etaxes_activity()
try:
# Debug logging
frappe.logger().info(f"Linking Purchase Order {purchase_order} to E-Taxes Purchase {etaxes_purchase}")
# Check if both documents exist
po_exists = frappe.db.exists('Purchase Order', purchase_order)
etaxes_exists = frappe.db.exists('E-Taxes Purchase', etaxes_purchase)
if not po_exists or not etaxes_exists:
frappe.log_error(f"Link error: PO exists={po_exists}, E-Taxes exists={etaxes_exists}", "Link Error")
return {
'success': False,
'message': 'One or both documents do not exist'
}
# Get Purchase Order document
po_doc = frappe.get_doc('Purchase Order', purchase_order)
# Set link to E-Taxes Purchase
po_doc.is_taxes_doc = 1
po_doc.taxes_doc = etaxes_purchase
# Save changes
po_doc.save()
frappe.db.commit() # Explicit commit
# Debug logging
frappe.logger().info(f"Successfully linked PO and E-Taxes Purchase")
return {
'success': True
}
except Exception as e:
frappe.log_error(f"Error in link_purchase_order_to_etaxes: {str(e)}\n{frappe.get_traceback()}", "API Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def on_delete_purchase_order(doc, method):
"""Deletes related E-Taxes Purchase record when Purchase Order is deleted"""
if doc.is_taxes_doc and doc.taxes_doc:
try:
frappe.logger().info(f"Deleting E-Taxes Purchase {doc.taxes_doc} due to Purchase Order {doc.name} deletion")
# Check if E-Taxes Purchase record exists
if frappe.db.exists("E-Taxes Purchase", doc.taxes_doc):
frappe.delete_doc('E-Taxes Purchase', doc.taxes_doc, ignore_permissions=True, force=True)
frappe.db.commit()
frappe.logger().info(f"Successfully deleted E-Taxes Purchase {doc.taxes_doc}")
except Exception as e:
frappe.log_error(f"Error deleting E-Taxes Purchase {doc.taxes_doc}: {str(e)}\n{frappe.get_traceback()}",
"Purchase Order Delete Error")
@frappe.whitelist()
def get_unmapped_units():
"""Getting unmapped units from E-Taxes"""
try:
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get all unmapped units from E-Taxes
unmapped_units = []
# 1. Get all units with 'New' status
etaxes_units = frappe.get_all('E-Taxes Unit',
filters={'status': 'New'},
fields=['name', 'etaxes_unit_name', 'etaxes_unit_code'])
# 2. Check if they are already in settings
existing_mappings = {}
for mapping in settings.unit_mappings:
existing_mappings[mapping.etaxes_unit_name] = mapping.erp_unit
# 3. Add only those that are not in settings
for unit in etaxes_units:
if unit.name not in existing_mappings:
# ПРАВИЛЬНО: возвращаем объект с нужными полями для Link форматера
unit_data = {
'value': unit.name, # ID документа для Link поля
'label': unit.etaxes_unit_name, # Отображаемое имя
'etaxes_unit_name': unit.etaxes_unit_name, # Для link_formatter
'name': unit.name,
'etaxes_unit_code': unit.etaxes_unit_code
}
unmapped_units.append(unit_data)
return {
'success': True,
'units': unmapped_units
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def match_similar_units():
"""Matching units by similar names"""
# Записываем активность
record_etaxes_activity()
try:
import re
from difflib import SequenceMatcher
# Get active settings
settings = get_active_settings()
if not settings:
return {
'success': False,
'message': 'No active E-Taxes settings found'
}
# Get similarity threshold from settings
similarity_threshold = float(settings.similarity_threshold) / 100.0 if settings.similarity_threshold else 0.8
consider_azeri = settings.consider_azeri_chars
# Get all units with "New" status from E-Taxes
etaxes_units = frappe.get_all('E-Taxes Unit',
filters={'status': 'New'},
fields=['name', 'etaxes_unit_name', 'etaxes_unit_code'])
# Get existing mappings
existing_mappings = {}
for mapping in settings.unit_mappings:
existing_mappings[mapping.etaxes_unit_name] = mapping.erp_unit
# Get units with empty mapping (exist in table but erp_unit is None or empty)
units_with_empty_mapping = []
for etaxes_id, erp_unit in existing_mappings.items():
if not erp_unit: # If erp_unit is None or empty string
# Find corresponding E-Taxes unit
for unit in etaxes_units:
if unit.name == etaxes_id:
units_with_empty_mapping.append(unit)
break
# Filter only those that are not in mappings
unmapped_units = []
for unit in etaxes_units:
if unit.name not in existing_mappings:
unmapped_units.append(unit)
# Combine two lists: units without mappings and units with empty mappings
units_to_process = unmapped_units + units_with_empty_mapping
# Get all UOMs from system
system_units = frappe.get_all('UOM', fields=['name', 'uom_name'])
matched_count = 0
total_processed = len(units_to_process)
# Get fresh copy of settings document
doc = frappe.get_doc("E-Taxes Settings", settings.name)
# Process all units
for etaxes_unit in units_to_process:
# Find similar unit
best_match = None
best_score = 0
# Normalize E-Taxes unit name
etaxes_name = normalize_string(etaxes_unit.etaxes_unit_name, consider_azeri)
for unit in system_units:
# Normalize system unit name
unit_name = normalize_string(unit.uom_name, consider_azeri)
# Calculate similarity coefficient
name_score = SequenceMatcher(None, etaxes_name, unit_name).ratio()
# Check if this is a better match
if name_score > best_score and name_score >= similarity_threshold:
best_score = name_score
best_match = unit
if best_match:
# IMPORTANT: check if there is already a record for this unit in mappings
existing_row = None
for idx, mapping in enumerate(doc.unit_mappings):
if mapping.etaxes_unit_name == etaxes_unit.name:
existing_row = idx
break
if existing_row is not None:
# If row already exists, update its erp_unit value
doc.unit_mappings[existing_row].erp_unit = best_match.name
doc.unit_mappings[existing_row].mapping_type = 'Automatic'
else:
# If no row, add a new one
doc.append('unit_mappings', {
'etaxes_unit_name': etaxes_unit.name,
'erp_unit': best_match.name,
'mapping_type': 'Automatic'
})
matched_count += 1
# Update E-Taxes Unit status only for units that have a match
unit_doc = frappe.get_doc('E-Taxes Unit', etaxes_unit.name)
unit_doc.status = 'Mapped'
unit_doc.mapped_unit = best_match.name
unit_doc.save()
# Save settings only if matches were found
if matched_count > 0:
doc.save()
return {
'success': True,
'matched_count': matched_count,
'total_processed': total_processed,
'message': f'Matched {matched_count} out of {total_processed} units'
}
except Exception as e:
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def create_unmapped_units(settings_name):
"""Creating units for unmapped elements from E-Taxes settings table"""
# Записываем активность
record_etaxes_activity()
try:
import re
# Get settings
settings_doc = frappe.get_doc("E-Taxes Settings", settings_name)
# Get elements from table that have no mapping
unmapped_units = []
for mapping_unit in settings_doc.unit_mappings:
# Check that unit has no mapping or it's empty
if not mapping_unit.erp_unit:
# Get E-Taxes Unit document for this record
if frappe.db.exists('E-Taxes Unit', mapping_unit.etaxes_unit_name):
etaxes_unit = frappe.get_doc('E-Taxes Unit', mapping_unit.etaxes_unit_name)
unmapped_units.append(etaxes_unit)
if len(unmapped_units) == 0:
return {
"success": True,
"created_count": 0,
"error_count": 0,
"message": "No unmapped units in table to create"
}
created_count = 0
error_count = 0
for etaxes_unit in unmapped_units:
try:
# Check if a UOM with this name already exists
uom_exists = frappe.db.exists('UOM', {'uom_name': etaxes_unit.etaxes_unit_name})
if uom_exists:
continue
# Create new UOM
uom_doc = frappe.new_doc("UOM")
# Standard fields for UOM
uom_doc.uom_name = etaxes_unit.etaxes_unit_name
uom_doc.enabled = 1
# Set a default conversion factor if needed
uom_doc.must_be_whole_number = 0
# Save UOM
uom_doc.insert()
# Update mapping in settings
for mapping in settings_doc.unit_mappings:
if mapping.etaxes_unit_name == etaxes_unit.name:
mapping.erp_unit = uom_doc.name
mapping.mapping_type = 'Automatic'
break
# Update E-Taxes Unit status
etaxes_unit.status = 'Mapped'
etaxes_unit.mapped_unit = uom_doc.name
etaxes_unit.save()
created_count += 1
except Exception as e:
error_count += 1
# Save settings after adding all mappings
if created_count > 0:
settings_doc.save()
return {
"success": True,
"created_count": created_count,
"error_count": error_count,
"message": f"Created {created_count} units, errors: {error_count}"
}
except Exception as e:
return {
"success": False,
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def load_units_from_invoices(date_from, date_to, max_count=200, offset=0):
"""Loading units from invoices for a period with duplicate prevention"""
# Записываем активность
record_etaxes_activity()
try:
# Counters for tracking
created_count = 0
skipped_count = 0
failed_count = 0
# Get default settings
asan_login_settings = get_default_asan_login()
if not asan_login_settings.get('found'):
return {
'success': False,
'message': 'No Asan Login settings found'
}
# Ensure max_count is an integer
try:
max_count = int(max_count)
except (ValueError, TypeError):
max_count = 200
# Format filters for the API request
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": max_count,
"offset": offset,
"productCode": None,
"productName": None,
"receiverName": None,
"receiverTin": None,
"senderName": None,
"senderTin": None,
"serialNumber": None,
"sortAsc": True,
"sortBy": "creationDate",
"statuses": ["approved", "approvedBySystem"],
"types": ["current", "corrected"]
}
response = get_invoices(asan_login_settings['main_token'], json.dumps(filters))
if 'error' in response:
return {
'success': False,
'error': response.get('error'),
'message': response['message'] if 'message' in response else 'Failed to retrieve invoices'
}
# Extract data from response
items_data = response.get('data', []) or response.get('invoices', [])
# Check if more data is available
hasMore = False
if 'hasMore' in response:
hasMore = response.get('hasMore')
elif response.get('total', 0) > offset + max_count:
hasMore = True
# Get details for each invoice and collect unique units
processed_count = 0
unique_units = {} # Используем словарь для предотвращения дубликатов
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', [])
for item in items:
unit_name = item.get('unit', '')
# Skip empty units
if not unit_name:
continue
# ИСПРАВЛЕНИЕ: Проверяем существование записи с таким именем
if frappe.db.exists('E-Taxes Unit', {'etaxes_unit_name': unit_name}):
skipped_count += 1
continue
# Используем имя единицы как ключ для предотвращения дубликатов в одной загрузке
if unit_name in unique_units:
continue
# Generate a consistent code for the unit
unit_code = "UNIT-" + ''.join(e for e in unit_name if e.isalnum()).upper()
unique_units[unit_name] = {
'etaxes_unit_name': unit_name,
'etaxes_unit_code': unit_code,
'source_invoice': serial_number,
'status': 'New'
}
processed_count += 1
# Создаём записи для уникальных единиц
for unit_name, unit_data in unique_units.items():
try:
# Двойная проверка перед созданием (на случай конкурентного доступа)
if frappe.db.exists('E-Taxes Unit', {'etaxes_unit_name': unit_name}):
skipped_count += 1
continue
doc = frappe.get_doc({
'doctype': 'E-Taxes Unit',
**unit_data
})
doc.insert(ignore_permissions=True)
created_count += 1
except frappe.DuplicateEntryError:
# Если всё-таки произошло дублирование, считаем как пропущенный
skipped_count += 1
except Exception as e:
frappe.log_error(f"Error creating E-Taxes Unit {unit_name}: {str(e)}", "Load Units Error")
failed_count += 1
return {
'success': True,
'created_count': created_count,
'skipped_count': skipped_count,
'failed_count': failed_count,
'total_invoices': processed_count,
'unique_units': len(unique_units),
'hasMore': hasMore
}
except Exception as e:
frappe.log_error(f"Error in load_units_from_invoices: {str(e)}\n{frappe.get_traceback()}", "Load Units Error")
return {
'success': False,
'message': "An unknown error occurred, please try again in a few minutes."
}
# Обработчик для обновления статусов сопоставленных элементов
@frappe.whitelist()
def update_mapped_statuses(doc, method=None):
"""Update mapped statuses in E-Taxes DocTypes when settings are saved"""
try:
# 1. Handle E-Taxes Unit mappings
if hasattr(doc, 'unit_mappings'):
# Создаем словарь текущих соответствий
current_unit_mappings = {}
for mapping in doc.unit_mappings:
if mapping.etaxes_unit_name:
# Проверяем что erp_unit не None и не пустая строка
erp_unit = getattr(mapping, 'erp_unit', None)
current_unit_mappings[mapping.etaxes_unit_name] = erp_unit if erp_unit else None
# Обновляем статусы для всех единиц в mappings
for etaxes_unit_name, erp_unit in current_unit_mappings.items():
try:
if frappe.db.exists('E-Taxes Unit', etaxes_unit_name):
if erp_unit: # Если есть соответствие
frappe.db.set_value('E-Taxes Unit', etaxes_unit_name, {
'status': 'Mapped',
'mapped_unit': erp_unit
}, update_modified=False)
else: # Если соответствие пустое
frappe.db.set_value('E-Taxes Unit', etaxes_unit_name, {
'status': 'New',
'mapped_unit': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error updating E-Taxes Unit {etaxes_unit_name}: {str(e)}", "Status Update Error")
# Сбрасываем статус для единиц, которые были удалены из mappings
try:
mapped_units = frappe.get_all('E-Taxes Unit',
filters={'status': 'Mapped'},
fields=['name', 'mapped_unit'])
for unit in mapped_units:
if unit.name not in current_unit_mappings:
frappe.db.set_value('E-Taxes Unit', unit.name, {
'status': 'New',
'mapped_unit': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error resetting unmapped units: {str(e)}", "Status Update Error")
# 2. Handle E-Taxes Item mappings
if hasattr(doc, 'item_mappings'):
# Создаем словарь текущих соответствий
current_item_mappings = {}
for mapping in doc.item_mappings:
if mapping.etaxes_item_name:
# Проверяем что erp_item не None и не пустая строка
erp_item = getattr(mapping, 'erp_item', None)
current_item_mappings[mapping.etaxes_item_name] = erp_item if erp_item else None
# Обновляем статусы для всех товаров в mappings
for etaxes_item_name, erp_item in current_item_mappings.items():
try:
if frappe.db.exists('E-Taxes Item', etaxes_item_name):
if erp_item: # Если есть соответствие
frappe.db.set_value('E-Taxes Item', etaxes_item_name, {
'status': 'Mapped',
'mapped_item': erp_item
}, update_modified=False)
else: # Если соответствие пустое
frappe.db.set_value('E-Taxes Item', etaxes_item_name, {
'status': 'New',
'mapped_item': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error updating E-Taxes Item {etaxes_item_name}: {str(e)}", "Status Update Error")
# Сбрасываем статус для товаров, которые были удалены из mappings
try:
mapped_items = frappe.get_all('E-Taxes Item',
filters={'status': 'Mapped'},
fields=['name', 'mapped_item'])
for item in mapped_items:
if item.name not in current_item_mappings:
frappe.db.set_value('E-Taxes Item', item.name, {
'status': 'New',
'mapped_item': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error resetting unmapped items: {str(e)}", "Status Update Error")
# 3. Handle E-Taxes Parties mappings
if hasattr(doc, 'party_mappings'):
# Создаем словарь текущих соответствий
current_party_mappings = {}
for mapping in doc.party_mappings:
if mapping.etaxes_party_name:
# Проверяем что erp_party не None и не пустая строка
erp_party = getattr(mapping, 'erp_party', None)
current_party_mappings[mapping.etaxes_party_name] = erp_party if erp_party else None
# Получаем все контрагенты с их фактическими именами документов
try:
all_parties = frappe.get_all('E-Taxes Parties',
fields=['name', 'etaxes_party_name', 'status', 'mapped_party'])
# Создаем lookup от etaxes_party_name к фактическому имени документа
party_name_to_docname = {}
for party in all_parties:
party_name_to_docname[party.etaxes_party_name] = party.name
# Обновляем статусы для всех контрагентов в mappings
for etaxes_party_name, erp_party in current_party_mappings.items():
try:
if etaxes_party_name in party_name_to_docname:
docname = party_name_to_docname[etaxes_party_name]
if erp_party: # Если есть соответствие
frappe.db.set_value('E-Taxes Parties', docname, {
'status': 'Mapped',
'mapped_party': erp_party
}, update_modified=False)
else: # Если соответствие пустое
frappe.db.set_value('E-Taxes Parties', docname, {
'status': 'New',
'mapped_party': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error updating E-Taxes Party {etaxes_party_name}: {str(e)}", "Status Update Error")
# Сбрасываем статус для контрагентов, которые были удалены из mappings
for party in all_parties:
try:
if party.status == 'Mapped' and party.etaxes_party_name not in current_party_mappings:
frappe.db.set_value('E-Taxes Parties', party.name, {
'status': 'New',
'mapped_party': None
}, update_modified=False)
except Exception as e:
frappe.log_error(f"Error resetting unmapped party {party.name}: {str(e)}", "Status Update Error")
except Exception as e:
frappe.log_error(f"Error processing parties mappings: {str(e)}", "Status Update Error")
# Явно коммитим изменения
frappe.db.commit()
except Exception as e:
frappe.log_error(f"Error updating mapped statuses: {str(e)}\n{frappe.get_traceback()}",
"E-Taxes Settings Error")
@frappe.whitelist()
def refresh_unit_mappings_display(settings_name):
"""Операция-пустышка для обновления отображения unit mappings"""
try:
# Получаем документ настроек
settings_doc = frappe.get_doc("E-Taxes Settings", settings_name)
# Подготавливаем данные для обновления отображения на клиенте
updated_mappings = []
for mapping in settings_doc.unit_mappings:
if mapping.etaxes_unit_name:
# Получаем display name для Link поля
display_name = frappe.db.get_value('E-Taxes Unit', mapping.etaxes_unit_name, 'etaxes_unit_name')
updated_mappings.append({
'idx': mapping.idx,
'name': mapping.name,
'etaxes_unit_name_id': mapping.etaxes_unit_name,
'etaxes_unit_name_display': display_name or mapping.etaxes_unit_name
})
return {
'success': True,
'message': 'Display refreshed successfully',
'updated_mappings': updated_mappings
}
except Exception as e:
frappe.log_error(f"Error refreshing display: {str(e)}", "Display Refresh Error")
return {
'success': False,
'message': "An error occurred while refreshing display"
}
@frappe.whitelist()
def refresh_item_mappings_display(settings_name):
"""Операция-пустышка для обновления отображения item mappings"""
try:
# Получаем документ настроек
settings_doc = frappe.get_doc("E-Taxes Settings", settings_name)
# Подготавливаем данные для обновления отображения на клиенте
updated_mappings = []
for mapping in settings_doc.item_mappings:
if mapping.etaxes_item_name:
# Получаем display name для Link поля
display_name = frappe.db.get_value('E-Taxes Item', mapping.etaxes_item_name, 'etaxes_item_name')
updated_mappings.append({
'idx': mapping.idx,
'name': mapping.name,
'etaxes_item_name_id': mapping.etaxes_item_name,
'etaxes_item_name_display': display_name or mapping.etaxes_item_name
})
return {
'success': True,
'message': 'Display refreshed successfully',
'updated_mappings': updated_mappings
}
except Exception as e:
frappe.log_error(f"Error refreshing display: {str(e)}", "Display Refresh Error")
return {
'success': False,
'message': "An error occurred while refreshing display"
}
@frappe.whitelist()
def refresh_party_mappings_display(settings_name):
"""Операция-пустышка для обновления отображения party mappings"""
try:
# Получаем документ настроек
settings_doc = frappe.get_doc("E-Taxes Settings", settings_name)
# Подготавливаем данные для обновления отображения на клиенте
updated_mappings = []
for mapping in settings_doc.party_mappings:
if mapping.etaxes_party_name:
# Для parties используем etaxes_party_name как отображаемое имя
# так как это текстовое поле, а не Link
updated_mappings.append({
'idx': mapping.idx,
'name': mapping.name,
'etaxes_party_name_display': mapping.etaxes_party_name
})
return {
'success': True,
'message': 'Display refreshed successfully',
'updated_mappings': updated_mappings
}
except Exception as e:
frappe.log_error(f"Error refreshing display: {str(e)}", "Display Refresh Error")
return {
'success': False,
'message': "An error occurred while refreshing display"
}
@frappe.whitelist()
def get_company_profile(asan_login_name=None):
"""Получает профиль компании из E-Taxes"""
# Записываем активность
record_etaxes_activity()
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)
# Проверяем наличие main_token
if not asan_login.main_token:
return {"success": False, "message": "Main token not found. Please authenticate first."}
# Проверяем статус авторизации
if asan_login.auth_status != "Fully Authenticated":
return {"success": False, "message": "Not fully authenticated. Please complete authentication process."}
url = "https://new.e-taxes.gov.az/api/po/profile/public/v2/profile"
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",
"x-authorization": f"Bearer {asan_login.main_token}"
}
response = requests.get(url, headers=headers, timeout=15)
# Обработка стандартных ошибок
if response.status_code == 500:
return {
"error": "server_error",
"message": "E-Taxes service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
if response.status_code == 401:
# Обновляем статус авторизации
frappe.db.set_value("Asan Login", asan_login_name, "auth_status", "Not Authenticated")
frappe.db.commit()
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
response.raise_for_status()
# Получаем данные профиля
profile_data = response.json()
# Логируем успешный запрос
frappe.logger().info(f"Company profile retrieved successfully for {asan_login_name}")
return {
"success": True,
"data": profile_data,
"message": "Company profile retrieved successfully"
}
except requests.exceptions.HTTPError as e:
# Обработка HTTP ошибок
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "E-Taxes service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in get_company_profile: {str(e)}", "Company Profile API Error")
return {
"error": "http_error",
"message": "An error occurred while fetching company profile"
}
except requests.exceptions.RequestException as e:
frappe.log_error(f"Request error in get_company_profile: {str(e)}", "Company Profile API Error")
return {
"error": "network_error",
"message": "Network error occurred. Please check your connection and try again."
}
except Exception as e:
frappe.log_error(f"Unexpected error in get_company_profile: {str(e)}\n{frappe.get_traceback()}", "Company Profile Error")
return {
"error": "unknown_error",
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def get_tax_authorities(asan_login_name=None):
"""Получает справочник налоговых органов из E-Taxes"""
# Записываем активность
record_etaxes_activity()
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)
# Проверяем наличие main_token
if not asan_login.main_token:
return {"success": False, "message": "Main token not found. Please authenticate first."}
# Проверяем статус авторизации
if asan_login.auth_status != "Fully Authenticated":
return {"success": False, "message": "Not fully authenticated. Please complete authentication process."}
url = "https://new.e-taxes.gov.az/api/po/dictionary/public/v1/taxAuthorities"
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",
"x-authorization": f"Bearer {asan_login.main_token}"
}
response = requests.get(url, headers=headers, timeout=15)
# Обработка стандартных ошибок
if response.status_code == 500:
return {
"error": "server_error",
"message": "E-Taxes service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
if response.status_code == 401:
# Обновляем статус авторизации
frappe.db.set_value("Asan Login", asan_login_name, "auth_status", "Not Authenticated")
frappe.db.commit()
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
response.raise_for_status()
# Получаем данные справочника
tax_authorities_data = response.json()
# Логируем успешный запрос
frappe.logger().info(f"Tax authorities dictionary retrieved successfully for {asan_login_name}")
return {
"success": True,
"data": tax_authorities_data,
"message": "Tax authorities dictionary retrieved successfully"
}
except requests.exceptions.HTTPError as e:
# Обработка HTTP ошибок
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "E-Taxes service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in get_tax_authorities: {str(e)}", "Tax Authorities API Error")
return {
"error": "http_error",
"message": "An error occurred while fetching tax authorities dictionary"
}
except requests.exceptions.RequestException as e:
frappe.log_error(f"Request error in get_tax_authorities: {str(e)}", "Tax Authorities API Error")
return {
"error": "network_error",
"message": "Network error occurred. Please check your connection and try again."
}
except Exception as e:
frappe.log_error(f"Unexpected error in get_tax_authorities: {str(e)}\n{frappe.get_traceback()}", "Tax Authorities Error")
return {
"error": "unknown_error",
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def get_property_types(asan_login_name=None):
"""Получает справочник типов собственности из E-Taxes"""
# Записываем активность
record_etaxes_activity()
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)
# Проверяем наличие main_token
if not asan_login.main_token:
return {"success": False, "message": "Main token not found. Please authenticate first."}
# Проверяем статус авторизации
if asan_login.auth_status != "Fully Authenticated":
return {"success": False, "message": "Not fully authenticated. Please complete authentication process."}
url = "https://new.e-taxes.gov.az/api/po/profile/public/v1/dictionary/propertyTypes"
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",
"x-authorization": f"Bearer {asan_login.main_token}"
}
response = requests.get(url, headers=headers, timeout=15)
# Обработка стандартных ошибок
if response.status_code == 500:
return {
"error": "server_error",
"message": "E-Taxes service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
if response.status_code == 401:
# Обновляем статус авторизации
frappe.db.set_value("Asan Login", asan_login_name, "auth_status", "Not Authenticated")
frappe.db.commit()
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
response.raise_for_status()
# Получаем данные справочника
property_types_data = response.json()
# Логируем успешный запрос
frappe.logger().info(f"Property types dictionary retrieved successfully for {asan_login_name}")
return {
"success": True,
"data": property_types_data,
"message": "Property types dictionary retrieved successfully"
}
except requests.exceptions.HTTPError as e:
# Обработка HTTP ошибок
if e.response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again.",
"status_code": 401
}
elif e.response.status_code == 500:
return {
"error": "server_error",
"message": "E-Taxes service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
frappe.log_error(f"HTTP error in get_property_types: {str(e)}", "Property Types API Error")
return {
"error": "http_error",
"message": "An error occurred while fetching property types dictionary"
}
except requests.exceptions.RequestException as e:
frappe.log_error(f"Request error in get_property_types: {str(e)}", "Property Types API Error")
return {
"error": "network_error",
"message": "Network error occurred. Please check your connection and try again."
}
except Exception as e:
frappe.log_error(f"Unexpected error in get_property_types: {str(e)}\n{frappe.get_traceback()}", "Property Types Error")
return {
"error": "unknown_error",
"message": "An unknown error occurred, please try again in a few minutes."
}
@frappe.whitelist()
def get_tax_authority_name_by_code(code, asan_login_name=None):
"""Получает название налогового органа по коду"""
try:
# Получаем справочник налоговых органов
tax_authorities_result = get_tax_authorities(asan_login_name)
if not tax_authorities_result.get("success"):
return {
"success": False,
"message": tax_authorities_result.get("message", "Failed to get tax authorities dictionary")
}
tax_authorities = tax_authorities_result.get("data", [])
# Ищем налоговый орган по коду
if tax_authorities and isinstance(tax_authorities, list):
for authority in tax_authorities:
if authority.get("code") == code:
# Возвращаем название на азербайджанском языке
name = authority.get("name", {})
if isinstance(name, dict) and name.get("az"):
return {
"success": True,
"name": name.get("az"),
"code": code
}
elif isinstance(name, str):
return {
"success": True,
"name": name,
"code": code
}
# Если не найдено, возвращаем код как название
return {
"success": True,
"name": code,
"code": code,
"message": "Tax authority name not found, returning code"
}
except Exception as e:
frappe.log_error(f"Error in get_tax_authority_name_by_code: {str(e)}", "Tax Authority Name Error")
return {
"success": False,
"message": "An error occurred while getting tax authority name"
}
@frappe.whitelist()
def get_property_type_name_by_code(code, asan_login_name=None):
"""Получает название типа собственности по коду"""
try:
# Получаем справочник типов собственности
property_types_result = get_property_types(asan_login_name)
if not property_types_result.get("success"):
return {
"success": False,
"message": property_types_result.get("message", "Failed to get property types dictionary")
}
property_types = property_types_result.get("data", {})
# Ищем тип собственности по коду
if property_types and isinstance(property_types, dict):
# Проходим по всем значениям словаря
for property_type in property_types.values():
if isinstance(property_type, dict) and property_type.get("code") == code:
# Возвращаем название на азербайджанском языке
name = property_type.get("name", {})
if isinstance(name, dict) and name.get("az"):
return {
"success": True,
"name": name.get("az"),
"code": code
}
elif isinstance(name, str):
return {
"success": True,
"name": name,
"code": code
}
# Если не найдено, возвращаем код как название
return {
"success": True,
"name": code,
"code": code,
"message": "Property type name not found, returning code"
}
except Exception as e:
frappe.log_error(f"Error in get_property_type_name_by_code: {str(e)}", "Property Type Name Error")
return {
"success": False,
"message": "An error occurred while getting property type name"
}