1226 lines
53 KiB
Python
1226 lines
53 KiB
Python
# ======= VAT OPERATIONS API MODULE =======
|
||
# Module for importing VAT Account operations from E-Taxes Azərbaycan
|
||
# and creating Journal Entries in ERPNext
|
||
|
||
import frappe
|
||
import requests
|
||
import json
|
||
from datetime import datetime
|
||
from frappe.utils import now_datetime, getdate
|
||
from invoice_az.auth import record_etaxes_activity, get_default_asan_login
|
||
from invoice_az.utils import resolve_customer_group
|
||
from invoice_az import background_tasks
|
||
|
||
# API Endpoint
|
||
VAT_OPERATIONS_URL = "https://new.e-taxes.gov.az/api/po/vatacc/public/v1/operation/find.outbox"
|
||
|
||
DEFAULT_HEADERS = {
|
||
"Accept": "application/json, text/plain, */*",
|
||
"Content-Type": "application/json",
|
||
"Cache-Control": "no-cache"
|
||
}
|
||
|
||
# Маппинг типов операций из API в азербайджанские названия
|
||
OPERATION_TYPE_MAPPING = {
|
||
'SUB_TO_SUB': 'Sub uçot hesabı → Sub uçot hesabı',
|
||
'UNKNOWN_TO_OTHER_SUB': 'Naməlum → digər Sub hesab',
|
||
'CURRENT_TO_SUB': 'Cari → Sub uçot hesabı',
|
||
'CURRENT_TO_UNKNOWN': 'Cari → Naməlum',
|
||
'SUB_TO_IMPORT': 'Sub uçot hesabı → İdxal',
|
||
'UNKNOWN_TO_IMPORT': 'Naməlum → İdxal',
|
||
'SUB_TO_BUDGET': 'Sub uçot hesabı → Büdcə',
|
||
'UNKNOWN_TO_BUDGET': 'Naməlum → Büdcə',
|
||
'AUTO': 'Sub uçot hesabı → Büdcə' # AUTO операции трактуются как SUB_TO_BUDGET
|
||
}
|
||
|
||
|
||
def map_operation_type(api_operation_type):
|
||
"""
|
||
Конвертирует тип операции из API в азербайджанское название.
|
||
|
||
Args:
|
||
api_operation_type (str): Тип операции из API (например, SUB_TO_SUB)
|
||
|
||
Returns:
|
||
str: Азербайджанское название (например, Sub uçot hesabı → Sub uçot hesabı)
|
||
"""
|
||
return OPERATION_TYPE_MAPPING.get(api_operation_type, api_operation_type)
|
||
|
||
|
||
def parse_operation_date(operation_date_str):
|
||
"""
|
||
Парсит дату из формата e-taxes "20250319154646" в datetime.date
|
||
|
||
Args:
|
||
operation_date_str (str): Дата в формате "YYYYMMDDHHMMSS"
|
||
|
||
Returns:
|
||
datetime.date: Объект даты или None при ошибке
|
||
"""
|
||
try:
|
||
if not operation_date_str or len(operation_date_str) < 8:
|
||
frappe.log_error(f"Invalid operation date format: {operation_date_str}", "VAT Date Parse Error")
|
||
return None
|
||
|
||
# Формат: YYYYMMDDHHMMSS
|
||
year = int(operation_date_str[0:4])
|
||
month = int(operation_date_str[4:6])
|
||
day = int(operation_date_str[6:8])
|
||
|
||
return datetime(year, month, day).date()
|
||
except Exception as e:
|
||
frappe.log_error(f"Error parsing operation date '{operation_date_str}': {str(e)}", "VAT Date Parse Error")
|
||
return None
|
||
|
||
|
||
@frappe.whitelist()
|
||
def get_vat_operations(token, date_from, date_to, max_count=50, offset=0):
|
||
"""
|
||
Получение списка VAT Account операций из e-taxes
|
||
|
||
Args:
|
||
token (str): Bearer токен для авторизации
|
||
date_from (str): Дата начала в формате "YYYY-MM-DD"
|
||
date_to (str): Дата окончания в формате "YYYY-MM-DD"
|
||
max_count (int): Максимальное количество записей (по умолчанию 50)
|
||
offset (int): Смещение для пагинации (по умолчанию 0)
|
||
|
||
Returns:
|
||
dict: Словарь с результатами или ошибкой
|
||
"""
|
||
record_etaxes_activity()
|
||
|
||
try:
|
||
max_count = int(max_count)
|
||
offset = int(offset)
|
||
except (ValueError, TypeError):
|
||
max_count = 50
|
||
offset = 0
|
||
|
||
payload = {
|
||
"accounts": None,
|
||
"creationDateFrom": date_from,
|
||
"creationDateTo": date_to,
|
||
"maxCount": max_count,
|
||
"offset": offset,
|
||
"sortAsc": True,
|
||
"sortBy": "DATE",
|
||
"taxCodes": None
|
||
}
|
||
|
||
headers = DEFAULT_HEADERS.copy()
|
||
headers["x-authorization"] = f"Bearer {token}"
|
||
|
||
try:
|
||
response = requests.post(VAT_OPERATIONS_URL, json=payload, headers=headers)
|
||
|
||
# Handle 500 error
|
||
if response.status_code == 500:
|
||
frappe.log_error(f"[VAT_API] Server error 500", "E-Taxes Server Error")
|
||
return {
|
||
"error": "server_error",
|
||
"message": "Service temporarily unavailable. Please try again in a few minutes.",
|
||
"status_code": 500
|
||
}
|
||
|
||
# Handle 401 error - refresh token
|
||
if response.status_code == 401:
|
||
frappe.log_error(f"[VAT_API] Unauthorized 401 - token expired", "E-Taxes Auth Error")
|
||
|
||
asan_login_settings = get_default_asan_login()
|
||
if asan_login_settings.get('found'):
|
||
fresh_token = asan_login_settings['main_token']
|
||
headers["x-authorization"] = f"Bearer {fresh_token}"
|
||
|
||
frappe.log_error(f"[VAT_API] Retrying with fresh token", "E-Taxes Auth Retry")
|
||
|
||
response = requests.post(VAT_OPERATIONS_URL, json=payload, headers=headers)
|
||
|
||
if response.status_code == 401:
|
||
frappe.log_error(f"[VAT_API] Still unauthorized after token refresh", "E-Taxes Auth Failed")
|
||
return {
|
||
"error": "unauthorized",
|
||
"message": "Authentication required. Please login again."
|
||
}
|
||
else:
|
||
frappe.log_error(f"[VAT_API] No Asan Login settings found", "E-Taxes Auth Error")
|
||
return {
|
||
"error": "unauthorized",
|
||
"message": "Authentication required. Please login again."
|
||
}
|
||
|
||
response.raise_for_status()
|
||
|
||
# Получаем результат
|
||
result = response.json()
|
||
|
||
# Преобразуем типы операций в азербайджанские названия
|
||
if 'operations' in result and isinstance(result['operations'], list):
|
||
operations = result['operations']
|
||
|
||
for op in operations:
|
||
# Преобразуем тип операции в красивое азербайджанское название
|
||
if 'operationType' in op:
|
||
op['operationType'] = map_operation_type(op['operationType'])
|
||
|
||
frappe.logger().info(f"VAT Operations loaded: {len(operations)} operations")
|
||
|
||
return result
|
||
|
||
except requests.exceptions.HTTPError as e:
|
||
frappe.log_error(f"[VAT_API] HTTP error: {e.response.status_code} - {str(e)}", "E-Taxes HTTP Error")
|
||
|
||
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
|
||
}
|
||
|
||
return {"error": "http_error", "message": "An unknown error occurred, please try again."}
|
||
except Exception as e:
|
||
frappe.log_error(f"[VAT_API] Unexpected error: {str(e)}\n{frappe.get_traceback()}", "E-Taxes Error")
|
||
return {"error": "unknown_error", "message": "An unknown error occurred, please try again."}
|
||
|
||
|
||
def normalize_customer_name(name):
|
||
"""
|
||
Нормализует имя клиента для поиска
|
||
Использует normalize_string() из sales_api.py
|
||
|
||
Args:
|
||
name (str): Имя клиента
|
||
|
||
Returns:
|
||
str: Нормализованное имя
|
||
"""
|
||
if not name:
|
||
return ''
|
||
|
||
try:
|
||
from invoice_az.sales_api import normalize_string
|
||
return normalize_string(name, consider_azeri=True)
|
||
except Exception as e:
|
||
frappe.log_error(f"Error normalizing customer name '{name}': {str(e)}", "VAT Customer Normalize Error")
|
||
# Fallback to simple lowercase if normalize_string fails
|
||
return name.lower().strip()
|
||
|
||
|
||
def find_customer_by_tin(tin):
|
||
"""
|
||
Поиск клиента по TIN (напрямую в Customer.tax_id)
|
||
БУДУЩЕЕ: Переключение на customer_mappings
|
||
|
||
Args:
|
||
tin (str): TIN клиента
|
||
|
||
Returns:
|
||
str: Имя клиента или None
|
||
"""
|
||
if not tin:
|
||
return None
|
||
|
||
try:
|
||
# ТЕКУЩАЯ РЕАЛИЗАЦИЯ: Прямой поиск по tax_id
|
||
customer_name = frappe.db.get_value('Customer', {'tax_id': tin}, 'name')
|
||
|
||
if customer_name:
|
||
return customer_name
|
||
|
||
# БУДУЩЕЕ: Раскомментировать для использования customer_mappings
|
||
# settings = frappe.get_all('E-Taxes Settings',
|
||
# filters={'is_active': 1},
|
||
# limit=1)
|
||
# if settings:
|
||
# settings_doc = frappe.get_doc('E-Taxes Settings', settings[0].name)
|
||
# for mapping in settings_doc.customer_mappings:
|
||
# if mapping.etaxes_tax_id == tin and mapping.erp_customer:
|
||
# return mapping.erp_customer
|
||
|
||
return None
|
||
|
||
except Exception as e:
|
||
frappe.log_error(f"Error finding customer by TIN {tin}: {str(e)}", "VAT Customer Lookup Error")
|
||
return None
|
||
|
||
|
||
def find_or_create_customer_from_vat_operation(tin, name, settings_doc, auto_create=False):
|
||
"""
|
||
Найти или создать клиента из VAT операции с автосозданием маппингов
|
||
|
||
Логика поиска (ПРАВИЛЬНЫЙ ПРИОРИТЕТ):
|
||
0. Проверка маппингов (НАИВЫСШИЙ ПРИОРИТЕТ) - что указано в маппинге, то и используется
|
||
- Если найден маппинг с заполненным erp_customer → возвращаем его
|
||
- Если найден маппинг с пустым erp_customer → запоминаем и продолжаем поиск
|
||
1. Если auto_create=False → return None
|
||
2. Поиск Customer в базе (только если маппинг пустой или отсутствует):
|
||
- По tax_id
|
||
- По точному имени
|
||
- По нормализованному имени
|
||
- Если найден → создать/обновить маппинг
|
||
3. Создание нового Customer + создание/обновление маппинга
|
||
|
||
Args:
|
||
tin (str): TIN клиента
|
||
name (str): Имя клиента из операции
|
||
settings_doc (Document): Объект E-Taxes Settings
|
||
auto_create (bool): Разрешить автосоздание
|
||
|
||
Returns:
|
||
str: Имя клиента или None
|
||
"""
|
||
if not tin:
|
||
return None
|
||
|
||
try:
|
||
# Шаг 0: НАИВЫСШИЙ ПРИОРИТЕТ - Проверка маппингов
|
||
existing_mapping_row = None
|
||
if settings_doc and hasattr(settings_doc, 'customer_mappings'):
|
||
for mapping in settings_doc.customer_mappings:
|
||
if mapping.etaxes_tax_id == tin:
|
||
if mapping.erp_customer:
|
||
# Маппинг с заполненным erp_customer - это источник истины!
|
||
return mapping.erp_customer
|
||
else:
|
||
# Маппинг существует, но erp_customer пустой - запоминаем для обновления
|
||
existing_mapping_row = mapping
|
||
break
|
||
|
||
# Если auto_create=False, завершаем поиск
|
||
if not auto_create:
|
||
return None
|
||
|
||
# Шаг 1: Поиск Customer по tax_id в базе
|
||
customer_name = frappe.db.get_value('Customer', {'tax_id': tin}, 'name')
|
||
if customer_name:
|
||
# Найден Customer - создаем/обновляем маппинг
|
||
_create_or_update_etaxes_customer_and_mapping(tin, name, customer_name, settings_doc, existing_mapping_row)
|
||
return customer_name
|
||
|
||
# Шаг 2: Поиск Customer по точному имени
|
||
if name:
|
||
customer_name = frappe.db.get_value('Customer', {'customer_name': name}, 'name')
|
||
if customer_name:
|
||
# Обновляем tax_id если нужно (с защитой от конфликта)
|
||
try:
|
||
customer_doc = frappe.get_doc('Customer', customer_name)
|
||
if not customer_doc.tax_id or customer_doc.tax_id != tin:
|
||
# Проверяем нет ли уже другого Customer с таким tax_id
|
||
existing_customer_with_tin = frappe.db.get_value('Customer', {'tax_id': tin}, 'name')
|
||
if not existing_customer_with_tin or existing_customer_with_tin == customer_name:
|
||
customer_doc.tax_id = tin
|
||
customer_doc.save(ignore_permissions=True)
|
||
frappe.db.commit()
|
||
except Exception:
|
||
# Игнорируем ошибки обновления tax_id
|
||
pass
|
||
|
||
# Создаем/обновляем маппинг
|
||
_create_or_update_etaxes_customer_and_mapping(tin, name, customer_name, settings_doc, existing_mapping_row)
|
||
return customer_name
|
||
|
||
# Шаг 3: Поиск по нормализованному имени
|
||
if name:
|
||
normalized_search_name = normalize_customer_name(name)
|
||
if normalized_search_name:
|
||
all_customers = frappe.get_all('Customer', fields=['name', 'customer_name'])
|
||
for customer in all_customers:
|
||
customer_normalized = normalize_customer_name(customer.get('customer_name', ''))
|
||
if customer_normalized == normalized_search_name:
|
||
# Обновляем tax_id если нужно (с защитой от конфликта)
|
||
try:
|
||
customer_doc = frappe.get_doc('Customer', customer['name'])
|
||
if not customer_doc.tax_id or customer_doc.tax_id != tin:
|
||
# Проверяем нет ли уже другого Customer с таким tax_id
|
||
existing_customer_with_tin = frappe.db.get_value('Customer', {'tax_id': tin}, 'name')
|
||
if not existing_customer_with_tin or existing_customer_with_tin == customer['name']:
|
||
customer_doc.tax_id = tin
|
||
customer_doc.save(ignore_permissions=True)
|
||
frappe.db.commit()
|
||
except Exception:
|
||
# Игнорируем ошибки обновления tax_id
|
||
pass
|
||
|
||
# Создаем/обновляем маппинг
|
||
_create_or_update_etaxes_customer_and_mapping(tin, name, customer['name'], settings_doc, existing_mapping_row)
|
||
return customer['name']
|
||
|
||
# Шаг 4: Создание нового Customer
|
||
# Берём настроенную группу только если она валидна и не является group-узлом;
|
||
# иначе создаём Customer без группы (ERPNext запрещает group-type Customer Group).
|
||
default_customer_group = resolve_customer_group(
|
||
settings_doc.default_customer_group if settings_doc else None
|
||
)
|
||
|
||
# Создаем Customer (с защитой от race condition)
|
||
try:
|
||
customer_doc = frappe.new_doc('Customer')
|
||
customer_doc.customer_name = name or f"Customer-{tin}"
|
||
customer_doc.customer_type = 'Company'
|
||
if default_customer_group:
|
||
customer_doc.customer_group = default_customer_group
|
||
customer_doc.territory = 'All Territories'
|
||
customer_doc.tax_id = tin
|
||
|
||
# Payment terms если есть
|
||
if settings_doc and hasattr(settings_doc, 'default_payment_terms') and settings_doc.default_payment_terms:
|
||
customer_doc.payment_terms = settings_doc.default_payment_terms
|
||
|
||
customer_doc.insert(ignore_permissions=True)
|
||
frappe.db.commit()
|
||
|
||
# Создаем/обновляем маппинг
|
||
_create_or_update_etaxes_customer_and_mapping(tin, name, customer_doc.name, settings_doc, existing_mapping_row)
|
||
|
||
return customer_doc.name
|
||
|
||
except frappe.exceptions.DuplicateEntryError:
|
||
# Race condition: кто-то уже создал Customer с таким именем или tax_id
|
||
# Попробуем найти снова по tax_id
|
||
customer_name = frappe.db.get_value('Customer', {'tax_id': tin}, 'name')
|
||
if customer_name:
|
||
_create_or_update_etaxes_customer_and_mapping(tin, name, customer_name, settings_doc, existing_mapping_row)
|
||
return customer_name
|
||
# Если не нашли - возвращаем None
|
||
return None
|
||
|
||
except Exception as e:
|
||
frappe.log_error(
|
||
f"Error finding/creating customer for TIN {tin}, name {name}: {str(e)}\n{frappe.get_traceback()}",
|
||
"VAT Customer Auto-Create Error"
|
||
)
|
||
return None
|
||
|
||
|
||
def _create_or_update_etaxes_customer_and_mapping(tin, etaxes_name, erp_customer, settings_doc, existing_mapping_row=None):
|
||
"""
|
||
Вспомогательная функция для создания/обновления E-Taxes Customers и маппинга
|
||
Работает ТИХО - без сообщений пользователю
|
||
|
||
Args:
|
||
tin (str): TIN клиента
|
||
etaxes_name (str): Имя из e-taxes
|
||
erp_customer (str): Имя Customer в ERP
|
||
settings_doc (Document): Объект E-Taxes Settings
|
||
existing_mapping_row (object): Существующая строка маппинга с пустым erp_customer (если есть)
|
||
"""
|
||
# Сначала обновляем маппинг (это главное!)
|
||
try:
|
||
if settings_doc:
|
||
if existing_mapping_row:
|
||
# Используем прямое обновление child table через SQL
|
||
default_customer_group = resolve_customer_group(settings_doc.default_customer_group)
|
||
|
||
# Обновляем через frappe.db.sql напрямую
|
||
frappe.db.sql("""
|
||
UPDATE `tabE-Taxes Customer Mappings`
|
||
SET erp_customer = %s,
|
||
customer_group = COALESCE(NULLIF(customer_group, ''), %s),
|
||
territory = COALESCE(NULLIF(territory, ''), 'All Territories'),
|
||
mapping_type = COALESCE(NULLIF(mapping_type, ''), 'Automatic')
|
||
WHERE parent = %s
|
||
AND etaxes_tax_id = %s
|
||
""", (erp_customer, default_customer_group, settings_doc.name, tin))
|
||
frappe.db.commit()
|
||
else:
|
||
# Проверяем, нет ли уже маппинга с таким TIN (на всякий случай)
|
||
mapping_exists = False
|
||
if hasattr(settings_doc, 'customer_mappings'):
|
||
for mapping in settings_doc.customer_mappings:
|
||
if mapping.etaxes_tax_id == tin:
|
||
mapping_exists = True
|
||
break
|
||
|
||
if not mapping_exists:
|
||
# Создаем новую строку маппинга
|
||
settings_doc.append('customer_mappings', {
|
||
'etaxes_customer_name': etaxes_name or f"Customer-{tin}",
|
||
'etaxes_tax_id': tin,
|
||
'erp_customer': erp_customer,
|
||
'customer_group': resolve_customer_group(settings_doc.default_customer_group),
|
||
'territory': 'All Territories',
|
||
'mapping_type': 'Automatic'
|
||
})
|
||
settings_doc.save(ignore_permissions=True)
|
||
frappe.db.commit()
|
||
except Exception as e:
|
||
# Тихая обработка ошибок маппинга - только в лог
|
||
frappe.log_error(
|
||
f"Error updating mapping for TIN {tin}: {str(e)}\n{frappe.get_traceback()}",
|
||
"VAT Mapping Update Error"
|
||
)
|
||
|
||
# Потом пытаемся создать/обновить E-Taxes Customers (если упадет - не страшно)
|
||
try:
|
||
# Проверяем существование по полям, а не по name (autoname может генерировать другой формат!)
|
||
existing = frappe.db.get_value('E-Taxes Customers',
|
||
{'etaxes_tax_id': tin},
|
||
'name')
|
||
|
||
if existing:
|
||
try:
|
||
etaxes_customer_doc = frappe.get_doc('E-Taxes Customers', existing)
|
||
if not etaxes_customer_doc.mapped_customer:
|
||
etaxes_customer_doc.mapped_customer = erp_customer
|
||
etaxes_customer_doc.save(ignore_permissions=True)
|
||
frappe.db.commit()
|
||
except Exception:
|
||
# Игнорируем ошибки при обновлении существующего
|
||
pass
|
||
else:
|
||
try:
|
||
etaxes_customer_doc = frappe.new_doc('E-Taxes Customers')
|
||
etaxes_customer_doc.etaxes_party_name = etaxes_name or f"Customer-{tin}"
|
||
etaxes_customer_doc.etaxes_tax_id = tin
|
||
etaxes_customer_doc.etaxes_party_type = 'Receiver'
|
||
etaxes_customer_doc.status = 'New'
|
||
etaxes_customer_doc.mapped_customer = erp_customer
|
||
etaxes_customer_doc.insert(ignore_permissions=True)
|
||
frappe.db.commit()
|
||
except frappe.exceptions.DuplicateEntryError:
|
||
# Игнорируем ошибку дубликата - кто-то уже создал
|
||
pass
|
||
except Exception:
|
||
# Игнорируем любые другие ошибки создания E-Taxes Customers
|
||
pass
|
||
except Exception:
|
||
# Игнорируем ошибки E-Taxes Customers полностью
|
||
pass
|
||
|
||
|
||
def find_account_by_number(account_number, company):
|
||
"""
|
||
Поиск счета по номеру счета
|
||
|
||
Args:
|
||
account_number (str): Номер счета (например, "226" или "211")
|
||
company (str): Название компании
|
||
|
||
Returns:
|
||
str: Имя счета или None
|
||
"""
|
||
if not account_number or not company:
|
||
return None
|
||
|
||
try:
|
||
# Поиск по полю account_number
|
||
account_name = frappe.db.get_value('Account',
|
||
{'account_number': account_number,
|
||
'company': company},
|
||
'name')
|
||
|
||
if account_name:
|
||
return account_name
|
||
|
||
# Дополнительный поиск: счет может содержать номер в начале имени
|
||
# Например: "226 - НДС к возмещению"
|
||
accounts = frappe.get_all('Account',
|
||
filters={'company': company},
|
||
fields=['name', 'account_name'])
|
||
|
||
for account in accounts:
|
||
if account.get('name', '').startswith(account_number + ' '):
|
||
return account['name']
|
||
if account.get('account_name', '').startswith(account_number + ' '):
|
||
return account['name']
|
||
|
||
return None
|
||
|
||
except Exception as e:
|
||
frappe.log_error(f"Error finding account {account_number}: {str(e)}", "VAT Account Lookup Error")
|
||
return None
|
||
|
||
|
||
def get_accounts_for_operation_type(operation_type, company, expense_income_type, tax_code=None):
|
||
"""
|
||
Получить счета дебета и кредита для типа операции из E-Taxes Settings.
|
||
Если маппинг не найден, вернуть (None, None).
|
||
|
||
Логика строгого соответствия (БЕЗ fallback):
|
||
- Если передан tax_code - ищем ТОЛЬКО маппинг с operation_type + expense_income_type + этот tax_code
|
||
- Если НЕ передан tax_code - ищем ТОЛЬКО маппинг с operation_type + expense_income_type + пустой classification_code
|
||
- Не нашли точное совпадение - возвращаем (None, None)
|
||
|
||
Args:
|
||
operation_type (str): Тип операции (например, Sub uçot hesabı → Sub uçot hesabı, Sub uçot hesabı → Büdcə)
|
||
company (str): Название компании
|
||
expense_income_type (str): Тип операции - 'Expense' или 'Income'
|
||
tax_code (str, optional): Код классификации из taxCodeInfo.code
|
||
|
||
Returns:
|
||
tuple: (debit_account, credit_account) или (None, None) если маппинг не найден
|
||
"""
|
||
try:
|
||
settings_name = 'E-Taxes Settings'
|
||
|
||
if settings_name and operation_type:
|
||
# СТРОГОЕ СООТВЕТСТВИЕ: ищем в зависимости от наличия tax_code
|
||
if tax_code:
|
||
# Случай А: Есть tax_code - ищем ТОЛЬКО с этим конкретным classification_code
|
||
mapping = frappe.db.get_value('E-Taxes VAT Account Mapping',
|
||
{'parent': settings_name,
|
||
'parenttype': 'E-Taxes Settings',
|
||
'operation_type': operation_type,
|
||
'expense_income': expense_income_type,
|
||
'classification_code': tax_code},
|
||
['debit_account', 'credit_account'],
|
||
as_dict=True)
|
||
|
||
if mapping:
|
||
frappe.logger().info(f"Found mapping with classification_code: {tax_code}, expense/income: {expense_income_type}")
|
||
return (mapping.get('debit_account'), mapping.get('credit_account'))
|
||
else:
|
||
# Не нашли - возвращаем None (БЕЗ fallback на пустой classification_code)
|
||
frappe.logger().info(f"No mapping found for operation_type: {operation_type}, expense/income: {expense_income_type} with classification_code: {tax_code}")
|
||
return (None, None)
|
||
else:
|
||
# Случай Б: Нет tax_code - ищем ТОЛЬКО с пустым classification_code
|
||
mappings = frappe.get_all('E-Taxes VAT Account Mapping',
|
||
filters={
|
||
'parent': settings_name,
|
||
'parenttype': 'E-Taxes Settings',
|
||
'operation_type': operation_type,
|
||
'expense_income': expense_income_type
|
||
},
|
||
fields=['debit_account', 'credit_account', 'classification_code'])
|
||
|
||
# Ищем маппинг с пустым classification_code
|
||
for mapping in mappings:
|
||
if not mapping.get('classification_code'):
|
||
frappe.logger().info(f"Found default mapping (no classification_code) for expense/income: {expense_income_type}")
|
||
return (mapping.get('debit_account'), mapping.get('credit_account'))
|
||
|
||
# Не нашли маппинг с пустым classification_code
|
||
frappe.logger().info(f"No default mapping found for operation_type: {operation_type}, expense/income: {expense_income_type}")
|
||
return (None, None)
|
||
|
||
# Маппинг не найден - возвращаем None, None
|
||
return (None, None)
|
||
|
||
except Exception as e:
|
||
frappe.log_error(f"Error getting accounts for operation type {operation_type}: {str(e)}",
|
||
"VAT Account Mapping Error")
|
||
# В случае ошибки возвращаем None, None
|
||
return (None, None)
|
||
|
||
|
||
def ensure_classification_code(tax_code_info):
|
||
"""Вернуть name записи "Classification code" для taxCodeInfo, создав её
|
||
из payload (code + description), если её ещё нет в справочнике.
|
||
|
||
Возвращает код (str) или None, если taxCodeInfo пуст/без code. Никогда не
|
||
бросает исключений — отсутствующий təsnifat kodu не должен ломать создание
|
||
Journal Entry (поле — Link, поэтому ссылка должна резолвиться).
|
||
"""
|
||
if not tax_code_info or not isinstance(tax_code_info, dict):
|
||
return None
|
||
|
||
code = tax_code_info.get('code')
|
||
if not code:
|
||
return None
|
||
|
||
code = str(code).strip()
|
||
if not code:
|
||
return None
|
||
|
||
try:
|
||
if frappe.db.exists('Classification code', code):
|
||
return code
|
||
|
||
description = (tax_code_info.get('description') or '').strip()
|
||
doc = frappe.new_doc('Classification code')
|
||
doc.name = code
|
||
doc.classification_name = description
|
||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||
frappe.db.commit()
|
||
frappe.logger().info(f"[VAT] Created Classification code '{code}' from taxCodeInfo")
|
||
return code
|
||
|
||
except Exception as e:
|
||
frappe.log_error(f"Could not ensure Classification code '{code}': {e}\n{frappe.get_traceback()}",
|
||
"VAT Classification Code Error")
|
||
return None
|
||
|
||
|
||
def create_journal_entry_from_vat_operation(operation_data, company, create_as_draft=0, auto_create_customers=0):
|
||
"""
|
||
Создание Journal Entry из VAT операции
|
||
|
||
Args:
|
||
operation_data (dict): Данные операции из API
|
||
company (str): Название компании
|
||
create_as_draft (int): 1 для создания в Draft, 0 для auto-submit
|
||
auto_create_customers (int): 1 для автосоздания клиентов, 0 для отключения
|
||
|
||
Returns:
|
||
dict: Результат создания с именем JE или ошибкой
|
||
"""
|
||
try:
|
||
# Парсим дату операции
|
||
operation_date_str = operation_data.get('operationDate')
|
||
posting_date = parse_operation_date(operation_date_str)
|
||
|
||
if not posting_date:
|
||
return {
|
||
'success': False,
|
||
'message': f'Invalid operation date format: {operation_date_str}'
|
||
}
|
||
|
||
# Получаем сумму (income или expense)
|
||
income = operation_data.get('income', 0)
|
||
expense = operation_data.get('expense', 0)
|
||
|
||
# Определяем тип операции (Expense/Income) и сумму
|
||
if income and income > 0:
|
||
expense_income_type = 'Income'
|
||
amount = income
|
||
elif expense and expense > 0:
|
||
expense_income_type = 'Expense'
|
||
amount = expense
|
||
else:
|
||
return {
|
||
'success': False,
|
||
'message': 'Invalid amount: both income and expense are empty or zero'
|
||
}
|
||
|
||
# Получаем тип операции и tax code
|
||
api_operation_type = operation_data.get('operationType', '')
|
||
operation_type = map_operation_type(api_operation_type)
|
||
|
||
# Извлекаем tax code из taxCodeInfo
|
||
tax_code = None
|
||
tax_code_info = operation_data.get('taxCodeInfo')
|
||
if tax_code_info and isinstance(tax_code_info, dict):
|
||
tax_code = tax_code_info.get('code')
|
||
|
||
# Təsnifat kodu: ссылка на "Classification code" (заполняем ТОЛЬКО если код есть)
|
||
classification_code_name = ensure_classification_code(tax_code_info)
|
||
|
||
# Ищем счета по маппингу с учетом tax_code (если есть)
|
||
debit_account, credit_account = get_accounts_for_operation_type(
|
||
operation_type, company, expense_income_type, tax_code
|
||
)
|
||
|
||
# Проверяем что маппинг найден
|
||
if not debit_account or not credit_account:
|
||
# Формируем понятное сообщение об ошибке
|
||
expense_income_label = expense_income_type.lower() # "Income" → "income", "Expense" → "expense"
|
||
error_message = f"VAT account mapping not found: operation type '{operation_type}', {expense_income_label}"
|
||
|
||
if tax_code:
|
||
error_message += f", classification code '{tax_code}'"
|
||
|
||
error_dict = {
|
||
'success': False,
|
||
'message': error_message,
|
||
'error_type': 'mapping_not_found',
|
||
'operation_type': operation_type,
|
||
'expense_income_type': expense_income_type
|
||
}
|
||
|
||
if tax_code:
|
||
error_dict['classification_code'] = tax_code
|
||
|
||
return error_dict
|
||
|
||
# Проверяем типы счетов - нужен ли party
|
||
debit_account_type = frappe.db.get_value('Account', debit_account, 'account_type')
|
||
credit_account_type = frappe.db.get_value('Account', credit_account, 'account_type')
|
||
|
||
party_required_types = ('Receivable', 'Payable')
|
||
debit_needs_party = debit_account_type in party_required_types
|
||
credit_needs_party = credit_account_type in party_required_types
|
||
|
||
# Получаем TIN и ищем/создаем клиента (если хотя бы один счет требует party)
|
||
tin = operation_data.get('tin', '').strip()
|
||
customer_name_from_operation = operation_data.get('name', '')
|
||
customer = None
|
||
|
||
if debit_needs_party or credit_needs_party:
|
||
# Получаем настройки E-Taxes для маппингов
|
||
settings_doc = None
|
||
try:
|
||
settings_doc = frappe.get_single('E-Taxes Settings')
|
||
except Exception as e:
|
||
frappe.logger().warning(f"[VAT] Could not load E-Taxes Settings: {str(e)}")
|
||
|
||
# Используем новую функцию для поиска/создания клиента
|
||
customer = find_or_create_customer_from_vat_operation(
|
||
tin=tin,
|
||
name=customer_name_from_operation,
|
||
settings_doc=settings_doc,
|
||
auto_create=bool(auto_create_customers)
|
||
)
|
||
|
||
if not customer:
|
||
error_message = f'Customer not found for TIN: {tin}'
|
||
if auto_create_customers:
|
||
error_message = f'Could not find or create customer for TIN: {tin}'
|
||
|
||
return {
|
||
'success': False,
|
||
'message': error_message,
|
||
'error_type': 'customer_creation_failed' if auto_create_customers else 'customer_not_found',
|
||
'tin': tin
|
||
}
|
||
|
||
# Создаем Journal Entry
|
||
je = frappe.new_doc('Journal Entry')
|
||
je.posting_date = posting_date
|
||
je.company = company
|
||
je.voucher_type = 'Journal Entry'
|
||
je.user_remark = f"VAT Operation: {operation_data.get('explanation', '')}"
|
||
je.expense_income_type = expense_income_type
|
||
je.customer_name_etaxes = operation_data.get('name', '')
|
||
je.etaxes_document_type = 'ədv' # Always ədv for VAT operations
|
||
je.loaded_from_etaxes = 1 # Flag that this was loaded from e-taxes
|
||
je.etaxes_explanation = operation_data.get('explanation', '') # İzahat из VAT операции
|
||
|
||
# Təsnifat kodu из taxCodeInfo — только для документов, где он есть
|
||
if classification_code_name:
|
||
je.etaxes_classification_code = classification_code_name
|
||
|
||
# Строка 1: Дебет
|
||
debit_entry = {
|
||
'account': debit_account,
|
||
'debit_in_account_currency': amount,
|
||
'credit_in_account_currency': 0
|
||
}
|
||
|
||
# Добавляем party если дебетовый счет Receivable/Payable
|
||
if debit_needs_party and customer:
|
||
debit_entry['party_type'] = 'Customer'
|
||
debit_entry['party'] = customer
|
||
|
||
je.append('accounts', debit_entry)
|
||
|
||
# Строка 2: Кредит
|
||
credit_entry = {
|
||
'account': credit_account,
|
||
'debit_in_account_currency': 0,
|
||
'credit_in_account_currency': amount
|
||
}
|
||
|
||
# Добавляем party если кредитовый счет Receivable/Payable
|
||
if credit_needs_party and customer:
|
||
credit_entry['party_type'] = 'Customer'
|
||
credit_entry['party'] = customer
|
||
|
||
je.append('accounts', credit_entry)
|
||
|
||
# Сохраняем и submit (если не draft режим)
|
||
je.insert()
|
||
|
||
if not create_as_draft:
|
||
je.submit()
|
||
else:
|
||
frappe.logger().info(f"Journal Entry {je.name} created as Draft (not submitted)")
|
||
|
||
frappe.db.commit()
|
||
|
||
return {
|
||
'success': True,
|
||
'journal_entry': je.name,
|
||
'posting_date': str(posting_date),
|
||
'status': 'Draft' if create_as_draft else 'Submitted'
|
||
}
|
||
|
||
except Exception as e:
|
||
error_context = "creating draft" if create_as_draft else "creating and submitting"
|
||
frappe.log_error(f"Error {error_context} JE from VAT operation: {str(e)}\n{frappe.get_traceback()}",
|
||
"VAT JE Creation Error")
|
||
return {
|
||
'success': False,
|
||
'message': f'Error {error_context} Journal Entry: {str(e)}'
|
||
}
|
||
|
||
|
||
def create_vat_operation_record(operation_data, journal_entry_name=None, status='Imported', error_message=None):
|
||
"""
|
||
Создание записи E-Taxes VAT Operations для отслеживания
|
||
|
||
Args:
|
||
operation_data (dict): Данные операции из API
|
||
journal_entry_name (str): Имя созданного Journal Entry
|
||
status (str): Статус импорта
|
||
error_message (str): Сообщение об ошибке (если есть)
|
||
|
||
Returns:
|
||
dict: Результат создания записи
|
||
"""
|
||
try:
|
||
operation_id = operation_data.get('id')
|
||
|
||
# Проверяем, не импортирована ли уже эта операция
|
||
existing = frappe.db.get_value('E-Taxes VAT Operations',
|
||
{'operation_id': operation_id},
|
||
'name')
|
||
|
||
if existing:
|
||
return {
|
||
'success': True,
|
||
'message': 'Operation already imported',
|
||
'name': existing,
|
||
'already_exists': True
|
||
}
|
||
|
||
# Парсим дату
|
||
operation_date_str = operation_data.get('operationDate')
|
||
operation_date = parse_operation_date(operation_date_str)
|
||
|
||
# Создаем новую запись
|
||
vat_op = frappe.new_doc('E-Taxes VAT Operations')
|
||
vat_op.operation_id = operation_id
|
||
vat_op.operation_date = operation_date
|
||
vat_op.tin = operation_data.get('tin')
|
||
vat_op.name_field = operation_data.get('name')
|
||
vat_op.operation_type = map_operation_type(operation_data.get('operationType'))
|
||
vat_op.payment_type = operation_data.get('paymentType')
|
||
vat_op.account = operation_data.get('account')
|
||
vat_op.destination = operation_data.get('destination')
|
||
vat_op.income = operation_data.get('income')
|
||
vat_op.receipt_number = operation_data.get('receiptNumber')
|
||
vat_op.explanation = operation_data.get('explanation')
|
||
vat_op.operator_pin = operation_data.get('operatorPin')
|
||
vat_op.operator_name = operation_data.get('operatorName')
|
||
|
||
# Tax code info
|
||
tax_code_info = operation_data.get('taxCodeInfo', {})
|
||
if tax_code_info:
|
||
vat_op.tax_code_info = json.dumps(tax_code_info)
|
||
|
||
vat_op.journal_entry = journal_entry_name
|
||
vat_op.status = status
|
||
vat_op.error_message = error_message
|
||
|
||
vat_op.insert()
|
||
frappe.db.commit()
|
||
|
||
return {
|
||
'success': True,
|
||
'name': vat_op.name
|
||
}
|
||
|
||
except Exception as e:
|
||
frappe.log_error(f"Error creating VAT operation record: {str(e)}\n{frappe.get_traceback()}",
|
||
"VAT Record Creation Error")
|
||
return {
|
||
'success': False,
|
||
'message': f'Error creating tracking record: {str(e)}'
|
||
}
|
||
|
||
|
||
@frappe.whitelist()
|
||
def import_vat_operations(operations_data, company=None, create_as_draft=0, auto_create_customers=0):
|
||
"""
|
||
Импорт VAT операций и создание Journal Entries
|
||
|
||
Args:
|
||
operations_data (str/list): JSON строка или список операций
|
||
company (str): Название компании (опционально)
|
||
create_as_draft (int): 1 для создания в Draft, 0 для auto-submit
|
||
auto_create_customers (int): 1 для автосоздания клиентов, 0 для отключения
|
||
|
||
Returns:
|
||
dict: Результат импорта с статистикой
|
||
"""
|
||
record_etaxes_activity()
|
||
|
||
try:
|
||
# Десериализация если нужно
|
||
if isinstance(operations_data, str):
|
||
operations_data = json.loads(operations_data)
|
||
|
||
# Обработка create_as_draft параметра
|
||
try:
|
||
create_as_draft = int(create_as_draft)
|
||
except (ValueError, TypeError):
|
||
create_as_draft = 0
|
||
|
||
# Обработка auto_create_customers параметра
|
||
try:
|
||
auto_create_customers = int(auto_create_customers)
|
||
except (ValueError, TypeError):
|
||
auto_create_customers = 0
|
||
|
||
# Получаем компанию
|
||
if not company:
|
||
company = frappe.defaults.get_user_default('Company')
|
||
|
||
if not company:
|
||
return {
|
||
'success': False,
|
||
'message': 'Company not specified'
|
||
}
|
||
|
||
# Статистика
|
||
total_count = len(operations_data)
|
||
success_count = 0
|
||
failed_count = 0
|
||
skipped_count = 0
|
||
errors = []
|
||
|
||
for operation in operations_data:
|
||
operation_id = operation.get('id')
|
||
|
||
try:
|
||
# Проверяем, не импортирована ли уже
|
||
existing = frappe.db.get_value('E-Taxes VAT Operations',
|
||
{'operation_id': operation_id},
|
||
'name')
|
||
|
||
if existing:
|
||
skipped_count += 1
|
||
continue
|
||
|
||
# Создаем Journal Entry
|
||
je_result = create_journal_entry_from_vat_operation(operation, company, create_as_draft, auto_create_customers)
|
||
|
||
if je_result.get('success'):
|
||
# Создаем запись отслеживания
|
||
record_result = create_vat_operation_record(
|
||
operation,
|
||
je_result.get('journal_entry'),
|
||
'Imported',
|
||
None
|
||
)
|
||
|
||
if record_result.get('success'):
|
||
success_count += 1
|
||
else:
|
||
failed_count += 1
|
||
errors.append({
|
||
'operation_id': operation_id,
|
||
'message': 'Journal Entry created but tracking record failed'
|
||
})
|
||
else:
|
||
# НЕ создаем VAT Operations при ошибке - только логируем
|
||
failed_count += 1
|
||
|
||
# ДОБАВЛЯЕМ ДЕТАЛЬНУЮ ИНФОРМАЦИЮ
|
||
# Используем income если есть, иначе expense
|
||
amount = operation.get('income') or operation.get('expense')
|
||
error_details = {
|
||
'operation_id': operation_id,
|
||
'message': je_result.get('message'),
|
||
'error_type': je_result.get('error_type'),
|
||
# Добавляем контекстную информацию из операции
|
||
'tin': operation.get('tin'),
|
||
'customer_name': operation.get('name'),
|
||
'income': amount, # Используем 'income' как поле для amount (для обратной совместимости в UI)
|
||
'operation_date': operation.get('operationDate'),
|
||
'operation_type': operation.get('operation_type')
|
||
}
|
||
|
||
# Если ошибка связана со счетами, добавляем номер счета
|
||
if je_result.get('error_type') == 'account_not_found':
|
||
error_details['account_number'] = je_result.get('account_number')
|
||
|
||
# Если ошибка связана с клиентом, подчеркиваем TIN
|
||
if je_result.get('error_type') == 'customer_not_found':
|
||
error_details['missing_tin'] = operation.get('tin')
|
||
|
||
# Если ошибка связана с отсутствием маппинга, добавляем operation_type из результата
|
||
if je_result.get('error_type') == 'mapping_not_found':
|
||
error_details['operation_type'] = je_result.get('operation_type')
|
||
|
||
errors.append(error_details)
|
||
|
||
except Exception as e:
|
||
failed_count += 1
|
||
error_msg = f"Error processing operation {operation_id}: {str(e)}"
|
||
frappe.log_error(error_msg, "VAT Import Error")
|
||
errors.append({
|
||
'operation_id': operation_id,
|
||
'message': str(e)
|
||
})
|
||
|
||
return {
|
||
'success': True,
|
||
'total': total_count,
|
||
'imported': success_count,
|
||
'failed': failed_count,
|
||
'skipped': skipped_count,
|
||
'errors': errors,
|
||
'message': f'Imported {success_count} of {total_count} operations. Failed: {failed_count}, Skipped: {skipped_count}'
|
||
}
|
||
|
||
except Exception as e:
|
||
frappe.log_error(f"Error in import_vat_operations: {str(e)}\n{frappe.get_traceback()}",
|
||
"VAT Import Error")
|
||
return {
|
||
'success': False,
|
||
'message': f'Import failed: {str(e)}'
|
||
}
|
||
|
||
|
||
@frappe.whitelist()
|
||
def load_vat_operations_from_etaxes(date_from, date_to, max_count=50, offset=0):
|
||
"""
|
||
Загрузка VAT операций из e-taxes (для фронтенда)
|
||
|
||
Args:
|
||
date_from (str): Дата начала
|
||
date_to (str): Дата окончания
|
||
max_count (int): Максимум записей
|
||
offset (int): Смещение
|
||
|
||
Returns:
|
||
dict: Результат с операциями для обработки на фронтенде
|
||
"""
|
||
record_etaxes_activity()
|
||
|
||
try:
|
||
# Получаем токен
|
||
asan_login_settings = get_default_asan_login()
|
||
|
||
if not asan_login_settings.get('found'):
|
||
return {
|
||
'success': False,
|
||
'message': 'No Asan Login settings found'
|
||
}
|
||
|
||
token = asan_login_settings['main_token']
|
||
|
||
# Загружаем операции
|
||
response = get_vat_operations(token, date_from, date_to, max_count, offset)
|
||
|
||
if 'error' in response:
|
||
return {
|
||
'success': False,
|
||
'error': response.get('error'),
|
||
'message': response.get('message')
|
||
}
|
||
|
||
operations = response.get('operations', [])
|
||
has_more = response.get('hasMore', False)
|
||
|
||
frappe.logger().info(f"VAT Operations loaded: {len(operations)} operations")
|
||
|
||
return {
|
||
'success': True,
|
||
'operations': operations,
|
||
'hasMore': has_more,
|
||
'token': token,
|
||
'total': len(operations)
|
||
}
|
||
|
||
except Exception as e:
|
||
frappe.log_error(f"Error in load_vat_operations_from_etaxes: {str(e)}\n{frappe.get_traceback()}",
|
||
"VAT Load Error")
|
||
return {
|
||
'success': False,
|
||
'message': f'Load failed: {str(e)}'
|
||
}
|
||
|
||
|
||
@frappe.whitelist()
|
||
def import_bulk_vat_operations(operations_data, company=None, create_as_draft=0, auto_create_customers=0):
|
||
"""Enqueue bulk VAT operations import as a background job with realtime progress via Socket.IO."""
|
||
if isinstance(operations_data, str):
|
||
operations_data = json.loads(operations_data)
|
||
|
||
try:
|
||
create_as_draft = int(create_as_draft)
|
||
except (ValueError, TypeError):
|
||
create_as_draft = 0
|
||
|
||
try:
|
||
auto_create_customers = int(auto_create_customers)
|
||
except (ValueError, TypeError):
|
||
auto_create_customers = 0
|
||
|
||
user = frappe.session.user
|
||
job_id = background_tasks.new_job_id()
|
||
|
||
frappe.enqueue(
|
||
_process_bulk_vat_import,
|
||
operations=operations_data,
|
||
company=company,
|
||
create_as_draft=create_as_draft,
|
||
auto_create_customers=auto_create_customers,
|
||
user=user,
|
||
bg_job_id=job_id,
|
||
queue="default",
|
||
timeout=1200,
|
||
)
|
||
|
||
return {"success": True, "enqueued": True, "total": len(operations_data), "job_id": job_id}
|
||
|
||
|
||
def _process_bulk_vat_import(operations, company, create_as_draft, auto_create_customers, user, bg_job_id=None):
|
||
"""Background job: import each VAT operation with realtime progress."""
|
||
job_id = bg_job_id
|
||
frappe.set_user(user)
|
||
record_etaxes_activity()
|
||
|
||
total = len(operations)
|
||
imported_count = 0
|
||
errors = []
|
||
|
||
background_tasks.register_task(job_id, user, "vat_import", "Importing VAT operations", total=total)
|
||
|
||
for idx, operation in enumerate(operations):
|
||
if job_id and background_tasks.is_cancel_requested(job_id):
|
||
background_tasks.finish_task(job_id, user, "cancelled")
|
||
frappe.publish_realtime(
|
||
"vat_import_complete",
|
||
{"total": total, "imported": imported_count, "errors": errors,
|
||
"create_as_draft": create_as_draft, "cancelled": True},
|
||
user=user,
|
||
)
|
||
return
|
||
background_tasks.update_task(job_id, user, current=idx, total=total,
|
||
message=f"Importing operation {idx + 1}/{total}")
|
||
try:
|
||
result = import_vat_operations(
|
||
operations_data=json.dumps([operation]),
|
||
company=company,
|
||
create_as_draft=create_as_draft,
|
||
auto_create_customers=auto_create_customers,
|
||
)
|
||
|
||
if result and result.get("success"):
|
||
if result.get("imported", 0) > 0:
|
||
imported_count += 1
|
||
elif result.get("failed", 0) > 0 and result.get("errors"):
|
||
err = result["errors"][0]
|
||
errors.append({
|
||
"operation_id": operation.get("id", "Unknown"),
|
||
"error": err.get("message", "Unknown error"),
|
||
"error_type": err.get("error_type", "Import Error"),
|
||
"tin": err.get("tin"),
|
||
"customer_name": err.get("customer_name"),
|
||
})
|
||
else:
|
||
error_msg = result.get("message", "Unknown error") if result else "Empty result"
|
||
errors.append({"operation_id": operation.get("id", "Unknown"), "error": error_msg})
|
||
|
||
except Exception as e:
|
||
errors.append({"operation_id": operation.get("id", "Unknown"), "error": str(e)})
|
||
|
||
frappe.db.commit()
|
||
|
||
frappe.publish_realtime(
|
||
"vat_import_progress",
|
||
{"current": idx + 1, "total": total},
|
||
user=user,
|
||
)
|
||
|
||
background_tasks.finish_task(job_id, user, "done")
|
||
frappe.publish_realtime(
|
||
"vat_import_complete",
|
||
{
|
||
"total": total,
|
||
"imported": imported_count,
|
||
"errors": errors,
|
||
"create_as_draft": create_as_draft,
|
||
},
|
||
user=user,
|
||
)
|