added import from etaxes to journal entry, with all bugfixes

This commit is contained in:
Ali 2026-01-13 22:31:30 +04:00
parent bd9bf9e2cc
commit 4ed778e567
8 changed files with 2631 additions and 2 deletions

359
BUGS_VAT_OPERATIONS.md Normal file
View File

@ -0,0 +1,359 @@
# Статус исправления багов VAT Operations Import
**Дата:** 2026-01-13
**Статус лимита токенов:** ~73K остаток
---
## ✅ ИСПРАВЛЕНО И РАБОТАЕТ
### БАГ 1: Фильтрация операций (income без expense)
**Статус:** ✅ РАБОТАЕТ
**Исправление:** Добавлена фильтрация в `vat_api.py` в функцию `get_vat_operations()` (строки 124-143)
**Проверено:** Пользователь подтвердил - фильтр работает
---
## ❌ НЕ РАБОТАЕТ / ЕСТЬ ПРОБЛЕМЫ
### БАГ 2: Визуальные баги (прогресс-бар, алерты)
**Статус:** ⚠️ ЧАСТИЧНО (требует проверки после bench build)
**Что исправлено в коде:**
- `journal_entry.js` строка 1198: `currentIndex - 1``currentIndex`
- `journal_entry.js` строки 1237-1239: убраны индивидуальные `frappe.show_alert()` во время импорта
**Почему может не работать:**
- Нужно выполнить `bench build --app invoice_az` чтобы скомпилировать JavaScript
- Нужно обновить страницу в браузере (Ctrl+F5)
**Проблема:** Пользователь не проверял после исправлений
---
### БАГ 3: Детальные ошибки не отображаются
**Статус:** ❌ НЕ РАБОТАЕТ
**Проблема:** При "Import Completed with Errors" нет информации о том, что именно пошло не так
**Что исправлено в коде:**
1. **vat_api.py** (строки 489-513): Добавлены поля `tin`, `customer_name`, `income`, `operation_date`, `operation_type`, `account_number`, `missing_tin` в errors
2. **journal_entry.js** (строка 1242): Изменено с `VATETaxes.errors.add(operation, error.error_type, error.message)` на `VATETaxes.errors.add(operation, error.error_type, error.message, error)` - передается полный объект
3. **journal_entry.js** (строки 628-654): Добавлено отображение Amount, Customer Name, Missing Account, Missing TIN
**Почему не работает:**
- JavaScript не пересобран: нужен `bench build --app invoice_az`
- Кэш браузера: нужен Ctrl+F5
**Как проверить что работает:**
1. Импортировать операцию с несуществующим TIN
2. Кликнуть "View Error Details"
3. Должны видеть:
- **Amount:** сумма в AZN
- **Customer Name:** название клиента из операции
- **Missing TIN:** TIN который не найден
- **Missing Account:** номер счета (226 или 211) который отсутствует
---
### БАГ 4: ПАРАДОКС УДАЛЕНИЯ (КРИТИЧЕСКАЯ ПРОБЛЕМА!)
**Статус:** 🔴 КРИТИЧЕСКИЙ БАГ - СОЗДАН DEADLOCK
**Проблема:**
1. Нельзя удалить **E-Taxes VAT Operations** → выходит ошибка "Cannot delete... because it is linked with Journal Entry"
2. Нельзя удалить **Journal Entry** → при попытке удалить JE, система пытается удалить связанный E-Taxes VAT Operations
3. E-Taxes VAT Operations выбрасывает ошибку из `on_trash()` → удаление JE прерывается
4. **РЕЗУЛЬТАТ: Невозможно удалить ни JE, ни VAT Operations!**
**Текущий код:**
**e_taxes_vat_operations.py** (строки 10-19):
```python
def on_trash(self):
"""Защита от удаления если связано с Journal Entry"""
if self.journal_entry:
# Проверяем, существует ли Journal Entry
if frappe.db.exists("Journal Entry", self.journal_entry):
frappe.throw(
_("Cannot delete or cancel E-Taxes VAT Operations {0} because it is linked with Journal Entry {1}").format(
self.name,
self.journal_entry
)
)
```
**vat_operations.py** (строки 28-34):
```python
# Удаляем с force=True чтобы обойти before_delete()
frappe.delete_doc(
'E-Taxes VAT Operations',
record.name,
ignore_permissions=True,
force=True # ⚠️ НЕ ОБХОДИТ on_trash!!!
)
```
**ПРОБЛЕМА:** `force=True` игнорирует только permissions, но **НЕ** игнорирует `on_trash()`!
---
## 🔧 РЕШЕНИЯ ДЛЯ БАГА 4 (ПАРАДОКС УДАЛЕНИЯ)
### Вариант 1: Использовать frappe.db.delete() (РЕКОМЕНДУЕТСЯ)
**Изменить `vat_operations.py`:**
```python
# ВМЕСТО:
frappe.delete_doc('E-Taxes VAT Operations', record.name, ignore_permissions=True, force=True)
# ИСПОЛЬЗОВАТЬ:
frappe.db.delete('E-Taxes VAT Operations', {'name': record.name})
frappe.db.commit()
```
**Плюсы:**
- Прямое удаление из БД, обходит все хуки
- Простое и надежное
**Минусы:**
- Не запускает cascade delete для связанных документов (но у VAT Operations нет связанных)
---
### Вариант 2: Проверка контекста в on_trash()
**Изменить `e_taxes_vat_operations.py`:**
```python
def on_trash(self):
"""Защита от удаления если связано с Journal Entry"""
# Проверяем флаг принудительного удаления
if getattr(self, '_skip_trash_validation', False):
return
if self.journal_entry:
if frappe.db.exists("Journal Entry", self.journal_entry):
frappe.throw(
_("Cannot delete or cancel E-Taxes VAT Operations {0} because it is linked with Journal Entry {1}").format(
self.name,
self.journal_entry
)
)
```
**Изменить `vat_operations.py`:**
```python
for record in vat_records:
# Получаем документ
doc = frappe.get_doc('E-Taxes VAT Operations', record.name)
# Устанавливаем флаг для обхода валидации
doc._skip_trash_validation = True
# Удаляем
doc.delete(ignore_permissions=True, force=True)
```
**Плюсы:**
- Сохраняет логику валидации для ручного удаления
- Разрешает каскадное удаление из кода
**Минусы:**
- Более сложная логика
- Использует "внутренний" флаг
---
### Вариант 3: Изменить логику защиты (САМЫЙ ПРАВИЛЬНЫЙ)
**Идея:** Разрешить удаление VAT Operations всегда, но проверять только если пытаемся удалить ВРУЧНУЮ (не программно)
**Изменить `e_taxes_vat_operations.py`:**
```python
def on_trash(self):
"""Защита от удаления если связано с Journal Entry"""
# Пропускаем проверку если удаление идет из кода (не от пользователя)
if frappe.flags.in_test or frappe.flags.in_migrate:
return
# Проверяем только при ручном удалении
if self.journal_entry:
if frappe.db.exists("Journal Entry", self.journal_entry):
frappe.throw(
_("Cannot delete E-Taxes VAT Operations {0} because it is linked with Journal Entry {1}. "
"Please delete the Journal Entry first.").format(
self.name,
self.journal_entry
)
)
```
**НО:** Это не различает программное удаление от ручного! Все равно заблокирует.
---
### Вариант 4: Вообще убрать защиту от удаления
**Просто удалить метод `on_trash()` из `e_taxes_vat_operations.py`**
**Плюсы:**
- Просто
- Нет парадокса
**Минусы:**
- Пользователь может случайно удалить VAT Operations вручную
- Нарушится связь с Journal Entry
---
## 📋 РЕКОМЕНДУЕМОЕ РЕШЕНИЕ
### Использовать **Вариант 1** - frappe.db.delete()
**Файл:** `/home/frappe/frappe-bench/apps/invoice_az/invoice_az/vat_operations.py`
**Заменить строки 28-34:**
```python
# СТАРЫЙ КОД (не работает):
frappe.delete_doc(
'E-Taxes VAT Operations',
record.name,
ignore_permissions=True,
force=True
)
# НОВЫЙ КОД:
frappe.db.delete('E-Taxes VAT Operations', {'name': record.name})
```
**Полная функция:**
```python
@frappe.whitelist()
def on_delete_journal_entry(doc, method):
"""Удаляет связанные E-Taxes VAT Operations при удалении Journal Entry"""
try:
vat_records = frappe.get_all(
"E-Taxes VAT Operations",
filters={"journal_entry": doc.name},
fields=["name"]
)
if vat_records:
for record in vat_records:
frappe.logger().info(
f"Deleting E-Taxes VAT Operations {record.name} "
f"due to Journal Entry {doc.name} deletion"
)
# Прямое удаление из БД (обходит on_trash)
frappe.db.delete('E-Taxes VAT Operations', {'name': record.name})
frappe.db.commit()
frappe.logger().info(f"Successfully deleted {len(vat_records)} VAT Operations records")
except Exception as e:
frappe.log_error(
f"Error deleting E-Taxes VAT Operations for Journal Entry {doc.name}: {str(e)}\n{frappe.get_traceback()}",
"Journal Entry Delete Error"
)
```
**После исправления:**
```bash
bench restart
```
**Тест:**
1. Импортировать операцию → создается JE + VAT Operations
2. Попытаться удалить VAT Operations вручную → ошибка (защита работает)
3. Удалить Journal Entry → VAT Operations автоматически удаляется (каскад работает)
---
## 📝 КОМАНДЫ ДЛЯ ПРИМЕНЕНИЯ ВСЕХ ИСПРАВЛЕНИЙ
```bash
# 1. Очистить кэш
bench clear-cache
# 2. Собрать JavaScript (для БАГ 2 и БАГ 3)
bench build --app invoice_az
# 3. Перезапустить сервер (для всех Python изменений)
bench restart
# 4. В браузере: Ctrl+F5 (полная перезагрузка страницы)
```
---
## 🧪 ТЕСТИРОВАНИЕ ПОСЛЕ ИСПРАВЛЕНИЙ
### Тест 1: Фильтрация
**Работает** - пользователь подтвердил
### Тест 2: Визуальные баги
**Проверить:**
1. Импортировать 5-10 операций
2. Прогресс-бар должен идти от 0% до 100% плавно
3. Не должно быть зеленых алертов во время импорта
4. Только один диалог в конце
### Тест 3: Детальные ошибки
**Проверить:**
1. Импортировать операцию с несуществующим TIN
2. Кликнуть "View Error Details"
3. Должны увидеть: Amount, Customer Name, Missing TIN
4. Экспорт в CSV должен содержать эти поля
### Тест 4: Удаление (после исправления)
**Проверить:**
1. Импортировать операцию → создается JE + VAT Operations
2. Открыть VAT Operations напрямую → попытаться удалить → должна быть ошибка
3. Открыть Journal Entry → удалить → JE удален + VAT Operations автоматически удален
4. Проверить что VAT Operations больше нет в списке
---
## 📊 ИТОГОВАЯ СТАТИСТИКА
| Баг | Статус | Что нужно |
|-----|--------|-----------|
| **1. Фильтрация** | ✅ Работает | - |
| **2. Визуальные баги** | ⚠️ Требует bench build | `bench build --app invoice_az` |
| **3. Детальные ошибки** | ⚠️ Требует bench build | `bench build --app invoice_az` |
| **4. Парадокс удаления** | 🔴 Критический | Заменить `frappe.delete_doc` на `frappe.db.delete` |
---
## 📁 ИЗМЕНЕННЫЕ ФАЙЛЫ
1. ✅ `/home/frappe/frappe-bench/apps/invoice_az/invoice_az/vat_api.py`
- Строки 124-143: фильтрация в `get_vat_operations()`
- Строки 489-513: детальные ошибки в `import_vat_operations()`
2. ✅ `/home/frappe/frappe-bench/apps/invoice_az/invoice_az/client/journal_entry.js`
- Строка 1198: исправлен прогресс-бар
- Строки 1237-1239: убраны алерты
- Строка 1242: передача полного объекта ошибки
- Строки 628-654: отображение детальной информации
3. ✅ `/home/frappe/frappe-bench/apps/invoice_az/invoice_az/invoice_az/doctype/e_taxes_vat_operations/e_taxes_vat_operations.py`
- Строки 10-19: защита от удаления через `on_trash()`
4. ✅ `/home/frappe/frappe-bench/apps/invoice_az/invoice_az/vat_operations.py`
- Создан новый файл с каскадным удалением
- ⚠️ **ТРЕБУЕТ ИСПРАВЛЕНИЯ** - заменить `frappe.delete_doc` на `frappe.db.delete`
5. ✅ `/home/frappe/frappe-bench/apps/invoice_az/invoice_az/hooks.py`
- Добавлен хук для Journal Entry `on_trash` и `on_cancel`
---
## 🎯 ПРИОРИТЕТ ИСПРАВЛЕНИЙ
1. **ВЫСОКИЙ:** БАГ 4 - Парадокс удаления (блокирует удаление документов)
2. **СРЕДНИЙ:** БАГ 3 - Детальные ошибки (нужно для отладки)
3. **НИЗКИЙ:** БАГ 2 - Визуальные баги (косметические)
---
**Конец документа**

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,8 @@ doctype_js = {
"Purchase Order": "client/purchase_order.js",
"Purchase Invoice": "client/purchase_invoice.js",
"Sales Order": "client/sales_order.js",
"Sales Invoice": "client/sales_invoice.js"
"Sales Invoice": "client/sales_invoice.js",
"Journal Entry": "client/journal_entry.js"
}
doctype_list_js = {
@ -21,7 +22,8 @@ doctype_list_js = {
"Purchase Order": "client/purchase_order.js",
"Purchase Invoice": "client/purchase_invoice.js",
"Sales Order": "client/sales_order.js",
"Sales Invoice": "client/sales_invoice.js"
"Sales Invoice": "client/sales_invoice.js",
"Journal Entry": "client/journal_entry.js"
}
# Хуки для добавления обработчиков событий Purchase Order
@ -38,6 +40,10 @@ doc_events = {
"on_trash": "invoice_az.send_sales_api.on_delete_sales_invoice",
"on_cancel": "invoice_az.send_sales_api.on_delete_sales_invoice"
},
"Journal Entry": {
"on_trash": "invoice_az.vat_operations.on_delete_journal_entry",
"on_cancel": "invoice_az.vat_operations.on_delete_journal_entry"
},
"E-Taxes Settings": {
"on_update": "invoice_az.api.update_mapped_statuses"
}

View File

@ -0,0 +1,205 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:ETVAT-{#####}",
"creation": "2026-01-13 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"operation_id",
"operation_date",
"operation_type",
"payment_type",
"column_break_1",
"tin",
"name_field",
"income",
"journal_entry_section",
"journal_entry",
"import_date",
"column_break_2",
"status",
"details_section",
"account",
"destination",
"receipt_number",
"explanation",
"tax_code_info",
"operator_section",
"operator_pin",
"operator_name",
"error_section",
"error_message"
],
"fields": [
{
"fieldname": "operation_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Operation ID",
"reqd": 1,
"unique": 1
},
{
"fieldname": "operation_date",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Operation Date"
},
{
"fieldname": "tin",
"fieldtype": "Data",
"label": "TIN"
},
{
"fieldname": "name_field",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Name"
},
{
"fieldname": "operation_type",
"fieldtype": "Data",
"label": "Operation Type"
},
{
"fieldname": "payment_type",
"fieldtype": "Data",
"label": "Payment Type"
},
{
"fieldname": "account",
"fieldtype": "Data",
"label": "Account"
},
{
"fieldname": "destination",
"fieldtype": "Data",
"label": "Destination"
},
{
"fieldname": "income",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Income"
},
{
"fieldname": "receipt_number",
"fieldtype": "Data",
"label": "Receipt Number"
},
{
"fieldname": "explanation",
"fieldtype": "Text",
"label": "Explanation"
},
{
"fieldname": "tax_code_info",
"fieldtype": "Small Text",
"label": "Tax Code Info"
},
{
"fieldname": "operator_pin",
"fieldtype": "Data",
"label": "Operator PIN"
},
{
"fieldname": "operator_name",
"fieldtype": "Data",
"label": "Operator Name"
},
{
"fieldname": "journal_entry",
"fieldtype": "Link",
"label": "Journal Entry",
"options": "Journal Entry",
"read_only": 1
},
{
"default": "Now",
"fieldname": "import_date",
"fieldtype": "Datetime",
"label": "Import Date",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "\nImported\nFailed",
"read_only": 1
},
{
"fieldname": "error_message",
"fieldtype": "Text",
"label": "Error Message",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "journal_entry_section",
"fieldtype": "Section Break",
"label": "Journal Entry"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "details_section",
"fieldtype": "Section Break",
"label": "Details"
},
{
"fieldname": "operator_section",
"fieldtype": "Section Break",
"label": "Operator Information"
},
{
"fieldname": "error_section",
"fieldtype": "Section Break",
"label": "Error Information"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-01-13 12:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes VAT Operations",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,37 @@
# Copyright (c) 2026, Jey ERP and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe import throw, _
class ETaxesVATOperations(Document):
def on_trash(self):
"""Защита от удаления если связано с Journal Entry"""
if self.journal_entry:
# Проверяем, существует ли Journal Entry
if frappe.db.exists("Journal Entry", self.journal_entry):
frappe.throw(
_("Cannot delete or cancel E-Taxes VAT Operations {0} because it is linked with Journal Entry {1}").format(
self.name,
self.journal_entry
)
)
def validate(self):
"""Валидация при сохранении"""
# Проверяем, что operation_id уникален
if not self.is_new():
return
existing = frappe.db.get_value(
'E-Taxes VAT Operations',
{'operation_id': self.operation_id, 'name': ['!=', self.name]},
'name'
)
if existing:
throw(_("Operation ID {0} already exists in {1}").format(
self.operation_id, existing
))

622
invoice_az/vat_api.py Normal file
View File

@ -0,0 +1,622 @@
# ======= 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"
}
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()
# ФИЛЬТРУЕМ: только операции с income и без expense
if 'operations' in result and isinstance(result['operations'], list):
operations = result['operations']
filtered_operations = []
for op in operations:
income = op.get('income', 0)
expense = op.get('expense', 0)
# Условие: есть income > 0 и нет expense (или expense == 0)
if income and income > 0 and (not expense or expense == 0):
filtered_operations.append(op)
frappe.logger().info(f"VAT Operations filtered: {len(operations)} -> {len(filtered_operations)} (income only)")
result['operations'] = filtered_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 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}'
}
# Получаем TIN и ищем клиента
tin = operation_data.get('tin', '').strip()
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
}
# Получаем сумму
income = operation_data.get('income', 0)
if not income or income <= 0:
return {
'success': False,
'message': 'Invalid income amount'
}
# Ищем счета
account_226 = find_account_by_number('226', company)
account_211 = find_account_by_number('211', company)
if not account_226:
return {
'success': False,
'message': 'Account 226 not found in chart of accounts',
'error_type': 'account_not_found',
'account_number': '226'
}
if not account_211:
return {
'success': False,
'message': 'Account 211 not found in chart of accounts',
'error_type': 'account_not_found',
'account_number': '211'
}
# Создаем 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: Дебет счета 226
je.append('accounts', {
'account': account_226,
'debit_in_account_currency': income,
'credit_in_account_currency': 0
})
# Строка 2: Кредит счета 211 с указанием клиента
je.append('accounts', {
'account': account_211,
'debit_in_account_currency': 0,
'credit_in_account_currency': income,
'party_type': 'Customer',
'party': customer
})
# Сохраняем и 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 = 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
# ДОБАВЛЯЕМ ДЕТАЛЬНУЮ ИНФОРМАЦИЮ
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': operation.get('income'),
'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')
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)
# ФИЛЬТРУЕМ: только операции с income и без expense
filtered_operations = []
for op in operations:
income = op.get('income', 0)
expense = op.get('expense', 0)
# Условие: есть income > 0 и нет expense (или expense == 0)
if income and income > 0 and (not expense or expense == 0):
filtered_operations.append(op)
frappe.logger().info(f"VAT Operations filtered: {len(operations)} -> {len(filtered_operations)} (income only)")
return {
'success': True,
'operations': filtered_operations,
'hasMore': has_more,
'token': token,
'total': len(filtered_operations),
'total_before_filter': 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)}'
}

View File

@ -0,0 +1,42 @@
# Copyright (c) 2026, Jey ERP and contributors
# For license information, please see license.txt
import frappe
@frappe.whitelist()
def on_delete_journal_entry(doc, method):
"""
Удаляет связанные E-Taxes VAT Operations при удалении Journal Entry
Args:
doc: Документ Journal Entry
method: Метод события (on_trash или on_cancel)
"""
try:
# Ищем все VAT Operations, связанные с этим Journal Entry
vat_records = frappe.get_all(
"E-Taxes VAT Operations",
filters={"journal_entry": doc.name},
fields=["name"]
)
if vat_records:
for record in vat_records:
frappe.logger().info(
f"Deleting E-Taxes VAT Operations {record.name} "
f"due to Journal Entry {doc.name} deletion"
)
# Прямое удаление из БД (обходит on_trash)
frappe.db.delete('E-Taxes VAT Operations', {'name': record.name})
frappe.db.commit()
frappe.logger().info(f"Successfully deleted {len(vat_records)} VAT Operations records")
except Exception as e:
frappe.log_error(
f"Error deleting E-Taxes VAT Operations for Journal Entry {doc.name}: {str(e)}\n{frappe.get_traceback()}",
"Journal Entry Delete Error"
)
# Не прерываем удаление JE, только логируем ошибку