diff --git a/BUGS_VAT_OPERATIONS.md b/BUGS_VAT_OPERATIONS.md new file mode 100644 index 0000000..41dd672 --- /dev/null +++ b/BUGS_VAT_OPERATIONS.md @@ -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 - Визуальные баги (косметические) + +--- + +**Конец документа** diff --git a/invoice_az/client/journal_entry.js b/invoice_az/client/journal_entry.js new file mode 100644 index 0000000..b79be1a --- /dev/null +++ b/invoice_az/client/journal_entry.js @@ -0,0 +1,1358 @@ +// ======= ОБЩИЕ УТИЛИТЫ И КОНСТАНТЫ ======= +const VATETaxes = { + // Константы + PROGRESS_UPDATE_DELAY: 50, + AUTH_POLL_INTERVAL: 6000, + AUTH_MAX_ATTEMPTS: 20, + + // Глобальные переменные + loadingDialog: null, + cancelLoading: false, + loadingErrors: [], + + // Кэш + cache: { + defaultLogin: null, + cacheTime: null, + cacheDuration: 300000 // 5 минут + } +}; + +// ======= БАЗОВЫЕ УТИЛИТЫ ======= +VATETaxes.utils = { + // Кэшированное получение настроек входа + getDefaultLogin: function(callback, useCache = true) { + const now = Date.now(); + + if (useCache && VATETaxes.cache.defaultLogin && VATETaxes.cache.cacheTime && + (now - VATETaxes.cache.cacheTime) < VATETaxes.cache.cacheDuration) { + callback(VATETaxes.cache.defaultLogin); + return; + } + + frappe.call({ + method: 'invoice_az.auth.get_default_asan_login', + callback: function(r) { + if (r.message) { + VATETaxes.cache.defaultLogin = r.message; + VATETaxes.cache.cacheTime = now; + } + callback(r.message); + }, + error: function() { + callback({found: false, error: 'Network error'}); + } + }); + }, + + // Проверка валидности токена + checkTokenValidity: function(callback) { + frappe.call({ + method: 'invoice_az.auth.check_token_validity', + callback: function(r) { + callback(r.message && r.message.valid); + }, + error: function() { + callback(false); + } + }); + }, + + // Форматирование валюты + formatCurrency: function(amount) { + return new Intl.NumberFormat('az-AZ', { + style: 'currency', + currency: 'AZN', + minimumFractionDigits: 2 + }).format(amount || 0); + }, + + // Парсинг даты операции из формата "20250319154646" + parseOperationDate: function(dateStr) { + if (!dateStr || dateStr.length < 8) { + return ''; + } + const year = dateStr.substring(0, 4); + const month = dateStr.substring(4, 6); + const day = dateStr.substring(6, 8); + return `${day}.${month}.${year}`; + }, + + // Гуманизация типа ошибки + humanizeErrorType: function(errorType) { + const errorTypeMap = { + 'customer_not_found': __('Customer Not Found'), + 'account_not_found': __('Account Not Found'), + 'import_error': __('Import Error'), + 'network_error': __('Network Error'), + 'validation_error': __('Validation Error'), + 'duplicate': __('Duplicate'), + 'permission_error': __('Permission Error') + }; + return errorTypeMap[errorType] || errorType; + }, + + // Очистка кэша + clearCache: function() { + VATETaxes.cache.defaultLogin = null; + VATETaxes.cache.cacheTime = null; + } +}; + +// ======= ДИАЛОГИ ЗАГРУЗКИ ======= +VATETaxes.dialogs = { + // Показать диалог загрузки + showLoading: function(title, message, submessage) { + if (VATETaxes.loadingDialog) { + this.updateLoading(title, message, submessage); + return; + } + + VATETaxes.loadingDialog = new frappe.ui.Dialog({ + title: title, + fields: [{ + fieldname: 'loading_html', + fieldtype: 'HTML', + options: this._getLoadingHTML(title, message, submessage) + }], + primary_action_label: __('Cancel'), + primary_action: function() { + VATETaxes.cancelLoading = true; + VATETaxes.loadingDialog.$wrapper.find('.primary-action').prop('disabled', true); + VATETaxes.loadingDialog.$wrapper.find('.primary-action').html(__('Cancelling...')); + $('#status_message p').text(__('Cancelling the operation.')); + } + }); + + VATETaxes.cancelLoading = false; + VATETaxes.loadingDialog.show(); + VATETaxes.loadingDialog.$wrapper.find('.modal-dialog').css('max-width', '450px'); + }, + + // Обновить сообщение в диалоге + updateLoading: function(title, message, submessage) { + if (!VATETaxes.loadingDialog) return; + + if (title) $('#loading_title').text(title); + if (message) $('#loading_message').text(message); + if (submessage !== undefined) { + $('#loading_submessage').text(submessage); + $('#loading_submessage').toggle(!!submessage); + } + }, + + // Показать код верификации + showVerificationCode: function(code) { + if (VATETaxes.loadingDialog && code) { + $('#verification_code').text(code); + $('#verification_code_container').show(); + } + }, + + // Установить статус успеха + setSuccess: function(message, submessage, callback, delay = 2000) { + if (!VATETaxes.loadingDialog) { + if (callback) callback(); + return; + } + + VATETaxes.loadingDialog.$wrapper.find('.loading-animation').html(` +
+ +
+ `); + + $('#loading_message').text(message); + if (submessage !== undefined) { + $('#loading_submessage').text(submessage); + $('#loading_submessage').toggle(!!submessage); + } + + $('#verification_code_container').hide(); + VATETaxes.loadingDialog.set_primary_action(null); + VATETaxes.loadingDialog.set_secondary_action(null); + + setTimeout(() => { + this.hide(); + if (callback) callback(); + }, delay); + }, + + // Установить статус ошибки + setError: function(message, submessage, showCloseButton = true) { + if (!VATETaxes.loadingDialog) return; + + VATETaxes.loadingDialog.$wrapper.find('.loading-animation').html(` +
+ +
+ `); + + $('#loading_message').text(message); + if (submessage !== undefined) { + $('#loading_submessage').text(submessage); + $('#loading_submessage').toggle(!!submessage); + } + + $('#verification_code_container').hide(); + + if (showCloseButton) { + VATETaxes.loadingDialog.set_primary_action(__('Close'), () => this.hide()); + } + }, + + // Скрыть диалог + hide: function() { + if (VATETaxes.loadingDialog) { + try { + VATETaxes.loadingDialog.hide(); + } catch (e) { + console.error("Error hiding loading dialog:", e); + } + VATETaxes.loadingDialog = null; + } + VATETaxes.cancelLoading = false; + }, + + // Получить HTML для диалога загрузки + _getLoadingHTML: function(title, message, submessage) { + return ` +
+
+
+
+ +
+

${title || __('Please wait...')}

+

${message || __('The operation may take some time')}

+

${submessage || ''}

+
+ +
+ `; + } +}; + +// ======= МОДУЛЬ АУТЕНТИФИКАЦИИ ======= +VATETaxes.auth = { + // Проверить токен и обработать аутентификацию при необходимости + checkAndProcess: function(callback) { + VATETaxes.utils.checkTokenValidity(function(isValid) { + if (isValid) { + callback(); + } else { + frappe.confirm( + __('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'), + function() { + VATETaxes.auth.startProcess(callback); + }, + function() { + frappe.show_alert({ + message: __('Authentication canceled. Operation cannot be completed.'), + indicator: 'red' + }, 5); + } + ); + } + }); + }, + + // Начать процесс аутентификации + startProcess: function(callback) { + VATETaxes.dialogs.showLoading( + __('Starting Authentication'), + __('Getting Asan Login settings...'), + __('Please wait') + ); + + VATETaxes.utils.getDefaultLogin(function(loginData) { + if (loginData && loginData.found) { + const asanLoginName = loginData.name; + VATETaxes.dialogs.updateLoading( + null, + __('Sending authentication request...'), + __('This will send a request to your Asan Imza mobile app') + ); + + frappe.call({ + method: 'invoice_az.auth.handle_authentication', + args: { 'asan_login_name': asanLoginName }, + callback: function(r) { + if (r.message && r.message.success) { + const bearerToken = r.message.bearer_token; + + if (r.message.verification_code) { + VATETaxes.dialogs.showVerificationCode(r.message.verification_code); + } + + VATETaxes.dialogs.updateLoading( + null, + __('Waiting for confirmation on your phone'), + __('Please check your phone and confirm the authentication request') + ); + + VATETaxes.auth.pollStatus(asanLoginName, bearerToken, function(success) { + if (success) { + VATETaxes.auth.complete(asanLoginName, callback); + } else { + VATETaxes.dialogs.setError( + __('Authentication Failed'), + __('Could not authenticate with Asan Imza') + ); + } + }); + } else { + VATETaxes.dialogs.setError( + __('Authentication Error'), + r.message ? r.message.message : __('Authentication request failed') + ); + } + } + }); + } else { + VATETaxes.dialogs.setError( + __('No Asan Login Settings'), + __('Please configure Asan Login settings first') + ); + } + }, false); // Не использовать кэш для аутентификации + }, + + // Опрос статуса аутентификации + pollStatus: function(asanLoginName, bearerToken, callback) { + let attempts = 0; + let stopPolling = false; + + function pollStatusStep() { + if (attempts >= VATETaxes.AUTH_MAX_ATTEMPTS || stopPolling) { + if (attempts >= VATETaxes.AUTH_MAX_ATTEMPTS) { + VATETaxes.dialogs.setError( + __('Authentication Timeout'), + __('The authentication request has timed out. Please try again.') + ); + callback(false); + } + return; + } + + attempts++; + + VATETaxes.dialogs.updateLoading( + null, + __('Waiting for confirmation on your phone'), + __('Please check your phone and confirm the authentication request') + + ' (' + attempts + '/' + VATETaxes.AUTH_MAX_ATTEMPTS + ')' + ); + + frappe.call({ + method: 'invoice_az.auth.poll_auth_status', + args: { + 'asan_login_name': asanLoginName, + 'bearer_token': bearerToken + }, + callback: function(r) { + if (r.message && r.message.success) { + if (r.message.authenticated) { + stopPolling = true; + VATETaxes.dialogs.setSuccess( + __('Authentication Successful'), + __('You are now authenticated with Asan Imza'), + function() { + callback(true); + } + ); + } else { + setTimeout(pollStatusStep, VATETaxes.AUTH_POLL_INTERVAL); + } + } else { + stopPolling = true; + VATETaxes.dialogs.setError( + __('Authentication Error'), + r.message ? r.message.message : __('An unknown error occurred') + ); + callback(false); + } + }, + error: function(err) { + console.error('Error during status check:', err); + setTimeout(pollStatusStep, VATETaxes.AUTH_POLL_INTERVAL); + } + }); + } + + pollStatusStep(); + }, + + // Завершение аутентификации + complete: function(asanLoginName, callback) { + VATETaxes.dialogs.showLoading( + __('Completing Authentication'), + __('Getting certificates...'), + __('Please wait') + ); + + frappe.call({ + method: 'invoice_az.auth.get_auth_certificates', + args: { 'asan_login_name': asanLoginName }, + callback: function(certR) { + if (certR.message && certR.message.success) { + VATETaxes.dialogs.updateLoading( + null, + __('Certificates retrieved'), + __('Selecting certificate and taxpayer') + ); + + frappe.call({ + method: 'frappe.client.get', + args: { + doctype: 'Asan Login', + name: asanLoginName + }, + callback: function(r) { + if (r.message && r.message.selected_certificate_json) { + try { + const certData = JSON.parse(r.message.selected_certificate_json); + const certName = r.message.selected_certificate; + + VATETaxes.dialogs.updateLoading( + null, + __('Selecting certificate'), + certName + ); + + VATETaxes.auth._selectCertificate(asanLoginName, certData, certName, callback); + } catch (e) { + console.error('Error parsing certificate:', e); + VATETaxes.dialogs.hide(); + VATETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback); + } + } else { + VATETaxes.dialogs.hide(); + VATETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback); + } + } + }); + } else { + VATETaxes.dialogs.setError( + __('Error'), + certR.message ? certR.message.message : __('Failed to get certificates') + ); + } + } + }); + }, + + // Выбор сертификата + _selectCertificate: function(asanLoginName, certData, certName, callback) { + frappe.call({ + method: 'invoice_az.auth.select_certificate', + args: { + 'asan_login_name': asanLoginName, + 'certificate_data': certData, + 'certificate_name': certName + }, + callback: function(r) { + if (r.message && r.message.success) { + VATETaxes.dialogs.updateLoading( + null, + __('Selecting taxpayer'), + __('Using certificate: ') + certName + ); + + frappe.call({ + method: 'invoice_az.auth.select_taxpayer', + args: { 'asan_login_name': asanLoginName }, + callback: function(r) { + if (r.message && r.message.success) { + VATETaxes.utils.clearCache(); // Очищаем кэш после успешной аутентификации + VATETaxes.dialogs.setSuccess( + __('Authentication Complete'), + __('You can now access E-Taxes services'), + callback + ); + } else { + VATETaxes.dialogs.setError( + __('Error'), + r.message ? r.message.message : __('Failed to select taxpayer') + ); + } + } + }); + } else { + VATETaxes.dialogs.setError( + __('Error'), + r.message ? r.message.message : __('Failed to select certificate') + ); + } + } + }); + }, + + // Показать селектор сертификатов + _showCertificateSelector: function(certificates, asanLoginName, callback) { + if (!certificates || !certificates.length) { + frappe.msgprint(__('No certificates available')); + return; + } + + let certHtml = '
'; + certHtml += ''; + + certificates.forEach(function(cert, index) { + let name = ''; + let id = ''; + + if (cert.taxpayerType === 'individual' && cert.individualInfo) { + name = cert.individualInfo.name || ''; + id = cert.individualInfo.fin || ''; + } else if (cert.legalInfo) { + name = cert.legalInfo.name || ''; + id = cert.legalInfo.tin || cert.legalInfo.voen || ''; + } + + certHtml += '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + }); + + certHtml += '
TypeNameIDPositionStatus
' + (cert.taxpayerType || '') + '' + name + '' + id + '' + (cert.position || '') + '' + (cert.hasAccess ? 'Active' : 'Inactive') + (cert.liquidated ? ' (Liquidated)' : '') + '
'; + + const dialog = new frappe.ui.Dialog({ + title: __('Select Certificate'), + fields: [{ + fieldtype: 'HTML', + fieldname: 'certificates', + options: certHtml + }] + }); + + dialog.show(); + + dialog.$wrapper.find('.select-cert').on('click', function() { + const index = $(this).data('index'); + const cert = certificates[index]; + + let certName = ''; + if (cert.taxpayerType === 'individual' && cert.individualInfo) { + certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`; + } else if (cert.legalInfo) { + certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`; + } else { + certName = `Certificate ${index + 1}`; + } + + dialog.hide(); + + VATETaxes.dialogs.showLoading( + __('Selecting Certificate'), + __('Processing your selection...'), + certName + ); + + VATETaxes.auth._selectCertificate(asanLoginName, cert, certName, callback); + }); + } +}; + +// ======= УПРАВЛЕНИЕ ОШИБКАМИ ======= +VATETaxes.errors = { + // Добавить ошибку в лог + add: function(operationData, errorType, errorMessage, additionalData = {}) { + const errorEntry = { + operation_id: operationData.id || 'Unknown', + tin: operationData.tin || 'Unknown', + name: operationData.name || 'Unknown', + operation_date: operationData.operationDate || 'Unknown', + error_type: errorType, + error_message: errorMessage, + timestamp: new Date().toISOString(), + ...additionalData + }; + + VATETaxes.loadingErrors.push(errorEntry); + console.error('VAT E-Taxes Import Error:', errorEntry); + }, + + // Очистить лог ошибок + clear: function() { + frappe.confirm( + __('Are you sure you want to clear the error log?'), + function() { + VATETaxes.loadingErrors = []; + frappe.show_alert({ + message: __('Error log cleared'), + indicator: 'green' + }, 2); + } + ); + }, + + // Показать детальный лог ошибок + showDetails: function() { + if (VATETaxes.loadingErrors.length === 0) { + frappe.msgprint(__('No errors to display')); + return; + } + + let errorsHtml = '
'; + + VATETaxes.loadingErrors.forEach(function(error, index) { + let documentDate = 'No date'; + if (error.operation_date && error.operation_date !== 'Unknown') { + try { + documentDate = VATETaxes.utils.parseOperationDate(error.operation_date); + } catch (e) { + documentDate = 'Invalid date'; + } + } + + errorsHtml += '
'; + + // Заголовок ошибки + errorsHtml += '
'; + errorsHtml += '
'; + errorsHtml += '' + VATETaxes.utils.humanizeErrorType(error.error_type) + ''; + errorsHtml += '
'; + errorsHtml += '' + error.name + ' (TIN: ' + error.tin + ')'; + errorsHtml += '
'; + + // Сообщение об ошибке + errorsHtml += '
'; + errorsHtml += '' + error.error_message + ''; + errorsHtml += '
'; + + // Дополнительная информация + if (error.income || error.account_number || error.customer_name) { + errorsHtml += '
'; + + if (error.income) { + errorsHtml += '
' + __('Amount:') + ' ' + VATETaxes.utils.formatCurrency(error.income) + '
'; + } + + if (error.customer_name) { + errorsHtml += '
' + __('Customer Name:') + ' ' + error.customer_name + '
'; + } + + if (error.account_number) { + errorsHtml += '
' + __('Missing Account:') + ' ' + error.account_number + '
'; + } + + if (error.missing_tin) { + errorsHtml += '
' + __('Missing TIN:') + ' ' + error.missing_tin + '
'; + } + + errorsHtml += '
'; + } + + errorsHtml += '
'; + errorsHtml += '' + documentDate + ''; + errorsHtml += '
'; + + errorsHtml += '
'; + }); + + errorsHtml += '
'; + + // Статистика + const errorTypes = {}; + VATETaxes.loadingErrors.forEach(function(error) { + errorTypes[error.error_type] = (errorTypes[error.error_type] || 0) + 1; + }); + + let statsHtml = '
'; + statsHtml += '
'; + statsHtml += '
'; + statsHtml += '' + __('Total Errors:') + ' ' + VATETaxes.loadingErrors.length + '
'; + statsHtml += ''; + Object.keys(errorTypes).forEach(function(type, index) { + if (index > 0) statsHtml += ' • '; + statsHtml += VATETaxes.utils.humanizeErrorType(type) + ': ' + errorTypes[type]; + }); + statsHtml += ''; + statsHtml += '
'; + statsHtml += '
'; + statsHtml += ''; + statsHtml += ''; + statsHtml += '
'; + + const d = new frappe.ui.Dialog({ + title: __('Error Details') + ' (' + VATETaxes.loadingErrors.length + ')', + size: 'large', + fields: [ + { + fieldname: 'stats_html', + fieldtype: 'HTML', + options: statsHtml + }, + { + fieldname: 'errors_html', + fieldtype: 'HTML', + options: '
' + errorsHtml + '
' + } + ], + primary_action_label: __('Close'), + primary_action: function() { + d.hide(); + } + }); + + d.show(); + + // Добавляем обработчики событий + setTimeout(function() { + $('#export-csv-dialog-btn').off('click').on('click', function() { + VATETaxes.errors.exportCSV(); + }); + + $('#clear-log-dialog-btn').off('click').on('click', function() { + VATETaxes.errors.clear(); + d.hide(); + }); + }, 200); + }, + + // Экспорт в CSV + exportCSV: function() { + if (VATETaxes.loadingErrors.length === 0) { + frappe.msgprint(__('No errors to export')); + return; + } + + try { + let csvContent = "data:text/csv;charset=utf-8,"; + csvContent += "Operation ID,Date,TIN,Name,Error Type,Error Message,Timestamp\n"; + + VATETaxes.loadingErrors.forEach(function(error) { + const cleanMessage = error.error_message.replace(/"/g, '""').replace(/\n/g, ' ').replace(/\r/g, ' '); + const cleanName = error.name.replace(/"/g, '""'); + + const row = [ + error.operation_id, + error.operation_date, + error.tin, + cleanName, + error.error_type, + cleanMessage, + moment(error.timestamp).format('DD.MM.YYYY HH:mm:ss') + ].map(function(field) { + return '"' + (field || '') + '"'; + }).join(','); + + csvContent += row + "\n"; + }); + + const encodedUri = encodeURI(csvContent); + const link = document.createElement("a"); + link.setAttribute("href", encodedUri); + link.setAttribute("download", "vat_import_errors_" + moment().format('YYYY-MM-DD_HH-mm-ss') + ".csv"); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + frappe.show_alert({ + message: __('Error log exported successfully'), + indicator: 'green' + }, 3); + } catch (e) { + console.error('Export error:', e); + frappe.show_alert({ + message: __('Failed to export error log'), + indicator: 'red' + }, 3); + } + }, + + // Показать сводку результатов + showSummary: function(totalOperations, processedCount, errorsCount) { + let title = ''; + let indicator = ''; + let message = ''; + + if (errorsCount === 0) { + title = __('Import Completed Successfully'); + indicator = 'green'; + message = __('All ') + totalOperations + __(' VAT operations were imported successfully.'); + + frappe.msgprint({ + title: title, + indicator: indicator, + message: message + }); + } else { + title = errorsCount === totalOperations ? __('Import Failed') : __('Import Completed with Errors'); + indicator = processedCount > 0 ? 'orange' : 'red'; + + message = '
'; + if (processedCount > 0) { + message += __('Successfully imported: ') + '' + processedCount + '' + __(' operations') + '
'; + } + message += __('Failed: ') + '' + errorsCount + '' + __(' operations'); + message += '
'; + + message += '
' + + '' + + '' + + '
'; + + const summaryDialog = frappe.msgprint({ + title: title, + indicator: indicator, + message: message + }); + + // Добавляем обработчики событий + setTimeout(function() { + $('#view-error-details-btn').off('click').on('click', function() { + summaryDialog.hide(); + VATETaxes.errors.showDetails(); + }); + + $('#export-error-log-btn').off('click').on('click', function() { + VATETaxes.errors.exportCSV(); + }); + }, 200); + } + } +}; + +// ======= МОДУЛЬ ИМПОРТА VAT ОПЕРАЦИЙ ======= +VATETaxes.import = { + // Показать диалог импорта из E-Taxes + showDialog: function() { + VATETaxes.auth.checkAndProcess(function() { + const d = new frappe.ui.Dialog({ + title: __('Import VAT Operations from E-Taxes'), + fields: [ + { + fieldname: 'date_range_section', + fieldtype: 'Section Break', + label: __('Period') + }, + { + fieldname: 'creationDateFrom', + fieldtype: 'Date', + label: __('Date from'), + default: moment().subtract(1, 'month').format('YYYY-MM-DD') + }, + { + fieldname: 'creationDateTo', + fieldtype: 'Date', + label: __('Date to'), + default: moment().format('YYYY-MM-DD') + }, + { + fieldname: 'settings_section', + fieldtype: 'Section Break', + label: __('Settings') + }, + { + fieldname: 'company', + fieldtype: 'Link', + label: __('Company'), + options: 'Company', + reqd: true, + default: frappe.defaults.get_user_default('Company') + } + ], + primary_action_label: __('Search'), + primary_action: function() { + const values = d.get_values(); + + // Validate date range + const fromDateMoment = moment(values.creationDateFrom, "YYYY-MM-DD"); + const toDateMoment = moment(values.creationDateTo, "YYYY-MM-DD"); + const todayMoment = moment(); + + if (toDateMoment.isAfter(todayMoment)) { + frappe.msgprint(__('To Date cannot be later than today')); + return; + } + + if (fromDateMoment.isAfter(toDateMoment)) { + frappe.msgprint(__('From Date cannot be later than To Date')); + return; + } + + if (!values.company) { + frappe.msgprint({ + title: __('Warning'), + indicator: 'orange', + message: __('Please select a company') + }); + return; + } + + const fromDate = values.creationDateFrom; + const toDate = values.creationDateTo; + + d.hide(); + VATETaxes.import.loadOperations(fromDate, toDate, values.company); + } + }); + + d.show(); + }); + }, + + // Загрузить операции с фильтрацией дубликатов + loadOperations: function(fromDate, toDate, company, accumulatedOperations = [], offset = 0) { + VATETaxes.cancelLoading = false; + $(document).off('progress-cancel.vat_import'); + + if (offset === 0) { + frappe.show_alert({ + message: __('Fetching VAT operations from E-Taxes...'), + indicator: 'blue' + }, 3); + } + + VATETaxes.utils.getDefaultLogin(function(loginResponse) { + if (VATETaxes.cancelLoading) return; + + if (loginResponse && loginResponse.found) { + const mainToken = loginResponse.main_token; + + if (!mainToken) { + frappe.msgprint({ + title: __('Authentication Error'), + indicator: 'red', + message: __('Authentication token not found. Please complete the authentication process in E-Taxes settings.') + }); + return; + } + + const maxCount = 50; + + frappe.call({ + method: 'invoice_az.vat_api.get_vat_operations', + args: { + 'token': mainToken, + 'date_from': fromDate, + 'date_to': toDate, + 'max_count': maxCount, + 'offset': offset + }, + callback: function(r) { + if (VATETaxes.cancelLoading) return; + + if (r.message && !r.message.error) { + const currentOperations = r.message.operations || []; + const hasMore = r.message.hasMore || false; + const allOperations = accumulatedOperations.concat(currentOperations); + + if (hasMore && currentOperations.length > 0) { + const newOffset = offset + currentOperations.length; + VATETaxes.import.loadOperations(fromDate, toDate, company, allOperations, newOffset); + } else { + if (allOperations.length > 0) { + frappe.show_alert({ + message: __('Processing ') + allOperations.length + __(' VAT operations...'), + indicator: 'blue' + }, 3); + + VATETaxes.import.filterDuplicates(allOperations, mainToken, company); + } else { + frappe.msgprint({ + title: __('Information'), + indicator: 'blue', + message: __('No VAT operations found for the specified period') + }); + } + } + } else if (r.message && r.message.error === 'unauthorized') { + frappe.confirm( + __('Your E-Taxes session has expired. Would you like to authenticate now?'), + function() { + VATETaxes.auth.startProcess(function() { + VATETaxes.import.loadOperations(fromDate, toDate, company); + }); + }, + function() { + frappe.show_alert({ + message: __('Authentication canceled. Operation cannot be completed.'), + indicator: 'red' + }, 5); + } + ); + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: r.message ? r.message.message : __('Error loading VAT operations') + }); + } + }, + error: function(xhr, status, error) { + console.error("Error fetching VAT operations:", error); + frappe.msgprint({ + title: __('Network Error'), + indicator: 'red', + message: __('Error fetching data from server: ') + (error || 'Unknown error') + }); + } + }); + } else { + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: __('E-Taxes authentication settings not found') + }); + } + }); + }, + + // Фильтрация дубликатов + filterDuplicates: function(allOperations, token, company) { + frappe.call({ + method: 'frappe.client.get_list', + args: { + doctype: 'E-Taxes VAT Operations', + fields: ['operation_id'], + limit_page_length: 0 + }, + callback: function(r) { + try { + if (VATETaxes.cancelLoading) return; + + const importedOperations = {}; + + if (r.message) { + r.message.forEach(function(op) { + if (op.operation_id) { + const normId = String(op.operation_id).trim(); + importedOperations[normId] = true; + } + }); + } + + const filteredOperations = []; + + for (let i = 0; i < allOperations.length; i++) { + const operation = allOperations[i]; + const operationId = String(operation.id).trim(); + + if (!importedOperations.hasOwnProperty(operationId)) { + filteredOperations.push(operation); + } + } + + if (filteredOperations.length > 0) { + VATETaxes.import.showOperationSelection(filteredOperations, token, company); + } else { + frappe.msgprint({ + title: __('Information'), + indicator: 'blue', + message: __('No new VAT operations found for the specified period (all ' + + allOperations.length + ' are already imported)') + }); + } + } catch (e) { + console.error("Error processing VAT operations data:", e); + frappe.msgprint({ + title: __('Error'), + indicator: 'red', + message: __('An error occurred while processing data: ') + e.message + }); + } + }, + error: function(err) { + console.error("Error fetching VAT operations:", err); + VATETaxes.import.showOperationSelection(allOperations, token, company); + frappe.show_alert({ + message: __('Failed to check for duplicates. Showing all operations.'), + indicator: 'orange' + }, 5); + } + }); + }, + + // Показать диалог выбора операций + showOperationSelection: function(operations, token, company) { + let operationTable = '
'; + operationTable += '' + + '' + + '' + + '' + + '' + + '' + + ''; + + operations.forEach(function(operation) { + const operationDate = VATETaxes.utils.parseOperationDate(operation.operationDate); + const tin = operation.tin || ''; + const name = operation.name || ''; + const income = operation.income || 0; + + operationTable += '' + + '' + + '' + + '' + + '' + + '' + + ''; + }); + + operationTable += '
' + __('Date') + '' + __('TIN') + '' + __('Name') + '' + __('Income') + '
' + operationDate + '' + tin + '' + name + '' + VATETaxes.utils.formatCurrency(income) + '
'; + + const infoMessage = '
' + + __('New VAT operations found: ') + operations.length + + '
'; + + const d = new frappe.ui.Dialog({ + title: __('Select VAT Operations'), + size: 'large', + fields: [ + { + fieldname: 'info_html', + fieldtype: 'HTML', + options: infoMessage + }, + { + fieldname: 'operations_html', + fieldtype: 'HTML', + options: operationTable + }, + { + fieldname: 'company_display', + fieldtype: 'HTML', + options: '
' + + __('Selected company: ') + '' + company + '
' + } + ], + primary_action_label: __('Load Selected'), + primary_action: function() { + const selectedOperationIds = []; + d.$wrapper.find('.select-operation:checked').each(function() { + selectedOperationIds.push($(this).data('id')); + }); + + if (selectedOperationIds.length === 0) { + frappe.msgprint({ + title: __('Warning'), + indicator: 'orange', + message: __('No operations selected') + }); + return; + } + + // Найти полные данные операций + const selectedOperations = []; + selectedOperationIds.forEach(function(id) { + const operation = operations.find(op => op.id === id); + if (operation) { + selectedOperations.push(operation); + } + }); + + d.hide(); + VATETaxes.cancelLoading = false; + VATETaxes.import.loadSelectedOperations(selectedOperations, token, company); + }, + secondary_action_label: __('Cancel'), + secondary_action: function() { + d.hide(); + } + }); + + d.$wrapper.find('.modal-dialog').css({ + 'max-width': '80%', + 'width': '80%', + 'margin': '30px auto' + }); + + d.$wrapper.find('.modal-body').css({ + 'padding': '15px' + }); + + d.show(); + + // Обработчик для чекбокса "выбрать все" + d.$wrapper.find('.select-all-operations').on('change', function() { + const isChecked = $(this).prop('checked'); + d.$wrapper.find('.select-operation').prop('checked', isChecked); + }); + }, + + // Загрузка выбранных операций + loadSelectedOperations: function(operations, token, company, processedCount = 0, currentIndex = 0) { + if (processedCount === 0 && currentIndex === 0) { + VATETaxes.loadingErrors = []; + window.totalOperationsToProcess = operations.length; + } + + $(document).off('progress-cancel.loading_operations'); + + if (operations.length === 0) { + const errorsCount = VATETaxes.loadingErrors.length; + const totalOperations = window.totalOperationsToProcess || processedCount; + + // Искусственная задержка перед показом summary, чтобы прогресс-бар успел закрыться + setTimeout(function() { + // Закрываем прогресс-бар прямо перед показом summary + frappe.hide_progress(); + + // Дополнительная микрозадержка для гарантии + setTimeout(function() { + VATETaxes.errors.showSummary(totalOperations, processedCount, errorsCount); + window.totalOperationsToProcess = null; + + if (cur_list) { + cur_list.refresh(); + } + }, 100); + }, 500); + + return; + } + + if (VATETaxes.cancelLoading) { + operations = []; + frappe.hide_progress(); + VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex); + return; + } + + const operation = operations.shift(); + const isLastOperation = operations.length === 0; + const totalToProcess = window.totalOperationsToProcess || (processedCount + operations.length + 1); + + currentIndex++; + + frappe.show_progress(__('Importing VAT Operations'), currentIndex, totalToProcess, + __('Processing operation ') + currentIndex + __(' of ') + totalToProcess, null, true); + + $(document).on('progress-cancel.loading_operations', function() { + VATETaxes.cancelLoading = true; + frappe.show_alert({ + message: __('Cancelling import... Finishing current operation.'), + indicator: 'orange' + }, 3); + frappe.hide_progress(); + }); + + // Импорт одной операции + frappe.call({ + method: 'invoice_az.vat_api.import_vat_operations', + args: { + 'operations_data': JSON.stringify([operation]), + 'company': company + }, + callback: function(r) { + VATETaxes.import._handleImportResult(r, operation, + operations, token, company, processedCount, currentIndex, isLastOperation); + }, + error: function(xhr, status, error) { + const errorMessage = 'Network error during import: ' + (error || 'Unknown network error'); + VATETaxes.errors.add(operation, 'Network Error', errorMessage, { + xhr_status: xhr.status, + xhr_response: xhr.responseText + }); + + VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation); + } + }); + }, + + // Обработка результата импорта + _handleImportResult: function(importR, operation, operations, token, company, processedCount, currentIndex, isLastOperation) { + if (importR.message && importR.message.success) { + if (importR.message.imported > 0) { + // НЕ показываем индивидуальные алерты во время массового импорта + // Только счетчик в прогресс-баре + processedCount++; + } else if (importR.message.failed > 0 && importR.message.errors && importR.message.errors.length > 0) { + const error = importR.message.errors[0]; + // Передаем ВСЕ данные ошибки включая tin, customer_name, income, account_number + VATETaxes.errors.add(operation, error.error_type || 'Import Error', error.message, error); + } + + VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation); + } else { + const errorMessage = importR.message ? importR.message.message : 'Unknown import error'; + VATETaxes.errors.add(operation, 'Import Error', errorMessage, { + server_response: importR.message + }); + + VATETaxes.import._continueOrFinish(operations, token, company, processedCount, currentIndex, isLastOperation); + } + }, + + // Продолжить или завершить обработку + _continueOrFinish: function(operations, token, company, processedCount, currentIndex, isLastOperation) { + if (isLastOperation) { + $(document).off('progress-cancel.loading_operations'); + + const totalToProcess = window.totalOperationsToProcess || processedCount; + const errorsCount = VATETaxes.loadingErrors.length; + + // Искусственная задержка перед показом summary, чтобы прогресс-бар успел закрыться + setTimeout(function() { + // Закрываем прогресс-бар прямо перед показом summary + frappe.hide_progress(); + + // Дополнительная микрозадержка для гарантии + setTimeout(function() { + VATETaxes.errors.showSummary(totalToProcess, processedCount, errorsCount); + + if (cur_list) { + cur_list.refresh(); + } + }, 100); + }, 500); + + return; + } + + setTimeout(function() { + if (operations.length === 0) { + frappe.hide_progress(); + } + + VATETaxes.import.loadSelectedOperations(operations, token, company, processedCount, currentIndex); + }, VATETaxes.PROGRESS_UPDATE_DELAY); + } +}; + +// ======= ОСНОВНЫЕ ОБРАБОТЧИКИ СПИСКОВ ======= + +// Journal Entry List +frappe.listview_settings['Journal Entry'] = { + add_fields: ['posting_date', 'voucher_type', 'total_debit', 'total_credit', 'docstatus'], + + get_indicator: function(doc) { + if (doc.docstatus == 1) { + return [__('Submitted'), 'blue', 'docstatus,=,1']; + } else if (doc.docstatus == 0) { + return [__('Draft'), 'red', 'docstatus,=,0']; + } else if (doc.docstatus == 2) { + return [__('Cancelled'), 'grey', 'docstatus,=,2']; + } + }, + + onload: function(listview) { + listview.page.add_menu_item(__('Import VAT Operations from E-Taxes'), function() { + VATETaxes.import.showDialog(); + }); + + listview.refresh(); + } +}; diff --git a/invoice_az/hooks.py b/invoice_az/hooks.py index dbde110..9a945fe 100644 --- a/invoice_az/hooks.py +++ b/invoice_az/hooks.py @@ -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" } diff --git a/invoice_az/invoice_az/doctype/e_taxes_vat_operations/__init__.py b/invoice_az/invoice_az/doctype/e_taxes_vat_operations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/invoice_az/invoice_az/doctype/e_taxes_vat_operations/e_taxes_vat_operations.json b/invoice_az/invoice_az/doctype/e_taxes_vat_operations/e_taxes_vat_operations.json new file mode 100644 index 0000000..8b955c4 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_vat_operations/e_taxes_vat_operations.json @@ -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": [] +} diff --git a/invoice_az/invoice_az/doctype/e_taxes_vat_operations/e_taxes_vat_operations.py b/invoice_az/invoice_az/doctype/e_taxes_vat_operations/e_taxes_vat_operations.py new file mode 100644 index 0000000..4643d47 --- /dev/null +++ b/invoice_az/invoice_az/doctype/e_taxes_vat_operations/e_taxes_vat_operations.py @@ -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 + )) diff --git a/invoice_az/vat_api.py b/invoice_az/vat_api.py new file mode 100644 index 0000000..2f67bed --- /dev/null +++ b/invoice_az/vat_api.py @@ -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)}' + } diff --git a/invoice_az/vat_operations.py b/invoice_az/vat_operations.py new file mode 100644 index 0000000..ba75f9d --- /dev/null +++ b/invoice_az/vat_operations.py @@ -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, только логируем ошибку