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 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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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