added import from etaxes to journal entry, with all bugfixes
This commit is contained in:
parent
bd9bf9e2cc
commit
4ed778e567
|
|
@ -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
|
|
@ -9,7 +9,8 @@ doctype_js = {
|
||||||
"Purchase Order": "client/purchase_order.js",
|
"Purchase Order": "client/purchase_order.js",
|
||||||
"Purchase Invoice": "client/purchase_invoice.js",
|
"Purchase Invoice": "client/purchase_invoice.js",
|
||||||
"Sales Order": "client/sales_order.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 = {
|
doctype_list_js = {
|
||||||
|
|
@ -21,7 +22,8 @@ doctype_list_js = {
|
||||||
"Purchase Order": "client/purchase_order.js",
|
"Purchase Order": "client/purchase_order.js",
|
||||||
"Purchase Invoice": "client/purchase_invoice.js",
|
"Purchase Invoice": "client/purchase_invoice.js",
|
||||||
"Sales Order": "client/sales_order.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
|
# Хуки для добавления обработчиков событий Purchase Order
|
||||||
|
|
@ -38,6 +40,10 @@ doc_events = {
|
||||||
"on_trash": "invoice_az.send_sales_api.on_delete_sales_invoice",
|
"on_trash": "invoice_az.send_sales_api.on_delete_sales_invoice",
|
||||||
"on_cancel": "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": {
|
"E-Taxes Settings": {
|
||||||
"on_update": "invoice_az.api.update_mapped_statuses"
|
"on_update": "invoice_az.api.update_mapped_statuses"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
))
|
||||||
|
|
@ -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)}'
|
||||||
|
}
|
||||||
|
|
@ -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, только логируем ошибку
|
||||||
Loading…
Reference in New Issue