769 lines
31 KiB
Python
769 lines
31 KiB
Python
# ======= VAT OPERATIONS API MODULE =======
|
||
# Module for importing VAT Account operations from E-Taxes Azerbaijan
|
||
# 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
|
||
|
||
# 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 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_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:
|
||
# Ищем дефолтные E-Taxes Settings
|
||
settings_name = frappe.db.get_value('E-Taxes Settings',
|
||
{'is_default': 1, 'is_active': 1},
|
||
'name')
|
||
|
||
if not settings_name:
|
||
# Если нет дефолтных, берем любые активные
|
||
settings_name = frappe.db.get_value('E-Taxes Settings',
|
||
{'is_active': 1},
|
||
'name')
|
||
|
||
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 create_journal_entry_from_vat_operation(operation_data, company):
|
||
"""
|
||
Создание Journal Entry из VAT операции
|
||
|
||
Args:
|
||
operation_data (dict): Данные операции из API
|
||
company (str): Название компании
|
||
|
||
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')
|
||
|
||
# Ищем счета по маппингу с учетом 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 = None
|
||
|
||
if debit_needs_party or credit_needs_party:
|
||
customer = find_customer_by_tin(tin)
|
||
if not customer:
|
||
return {
|
||
'success': False,
|
||
'message': f'Customer not found for TIN: {tin}',
|
||
'error_type': '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', '')}"
|
||
|
||
# Строка 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
|
||
je.insert()
|
||
je.submit()
|
||
|
||
frappe.db.commit()
|
||
|
||
return {
|
||
'success': True,
|
||
'journal_entry': je.name,
|
||
'posting_date': str(posting_date)
|
||
}
|
||
|
||
except Exception as e:
|
||
frappe.log_error(f"Error creating JE from VAT operation: {str(e)}\n{frappe.get_traceback()}",
|
||
"VAT JE Creation Error")
|
||
return {
|
||
'success': False,
|
||
'message': f'Error creating 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):
|
||
"""
|
||
Импорт VAT операций и создание Journal Entries
|
||
|
||
Args:
|
||
operations_data (str/list): JSON строка или список операций
|
||
company (str): Название компании (опционально)
|
||
|
||
Returns:
|
||
dict: Результат импорта с статистикой
|
||
"""
|
||
record_etaxes_activity()
|
||
|
||
try:
|
||
# Десериализация если нужно
|
||
if isinstance(operations_data, str):
|
||
operations_data = json.loads(operations_data)
|
||
|
||
# Получаем компанию
|
||
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)
|
||
|
||
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)}'
|
||
}
|