From d268d1b7bf7941a3614e57f1d1f3c13d1721643e Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Thu, 11 Dec 2025 17:26:57 +0400 Subject: [PATCH] Modifed property tax return --- ...L_SPECIFICATION_PROPERTY_TAX_AUTOMATION.md | 641 +++++++++++++++ .../property_tax_return.js | 207 ++++- .../property_tax_return.json | 14 + .../property_tax_return.py | 728 +++++++++++++++++- 4 files changed, 1557 insertions(+), 33 deletions(-) create mode 100644 TECHNICAL_SPECIFICATION_PROPERTY_TAX_AUTOMATION.md diff --git a/TECHNICAL_SPECIFICATION_PROPERTY_TAX_AUTOMATION.md b/TECHNICAL_SPECIFICATION_PROPERTY_TAX_AUTOMATION.md new file mode 100644 index 0000000..a87b176 --- /dev/null +++ b/TECHNICAL_SPECIFICATION_PROPERTY_TAX_AUTOMATION.md @@ -0,0 +1,641 @@ +# Техническое задание: Автоматизация заполнения Декларации по налогу на имущество + +## 1. Общее описание + +**Цель:** Автоматизировать заполнение дочерних таблиц Əlavə 1 и Əlavə 2 в декларации по налогу на имущество (Property Tax Return) на основе данных об активах (Assets) с использованием логики отчета "Asset Depreciations and Balances" из ERPNext. + +**Дата создания:** 2025-12-11 + +**Версия:** 1.0 + +--- + +## 2. Бизнес-требования + +### 2.1 Функциональные требования + +1. **Добавление полей выбора периода:** + - Добавить два взаимоисключающих чекбокса в форму Property Tax Return: + - `Tam İl` (Полный год) - по умолчанию включен + - `İl Ərzində` (В течение года) + - Чекбоксы должны располагаться сразу после поля `İl` (Год) на главной странице декларации + +2. **Автоматическое заполнение данных:** + - При изменении года (`İl`) или чекбоксов система должна автоматически: + - Получить данные об активах из базы данных + - Рассчитать балансовые стоимости активов + - Агрегировать данные по типам активов и налоговым статьям + - Заполнить соответствующие строки в дочерних таблицах + +3. **Источник данных:** + - Все расчеты должны базироваться на отчете ERPNext "Asset Depreciations and Balances" + - Использовать данные из doctype `Asset` компании пользователя + +### 2.2 Пользовательский сценарий + +1. Пользователь открывает/создает документ Property Tax Return +2. Заполняет поле `İl` (год декларации, например: 2024) +3. Выбирает режим расчета: + - `Tam İl` - если компания работала полный год + - `İl Ərzində` - если компания была создана в течение года +4. Система автоматически: + - Преобразует год в диапазон дат (01.01.YYYY - 31.12.YYYY) + - Получает данные об активах + - Заполняет таблицы Əlavə 1 и Əlavə 2 + +--- + +## 3. Технические требования + +### 3.1 Изменения в схеме данных + +#### 3.1.1 Property Tax Return DocType JSON + +**Файл:** `/home/frappe/frappe-bench/apps/taxes_az/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.json` + +**Новые поля:** + +```json +{ + "fieldname": "tam_il", + "fieldtype": "Check", + "label": "Tam İl", + "default": "1" +}, +{ + "fieldname": "il_erzinde", + "fieldtype": "Check", + "label": "İl Ərzində", + "default": "0" +} +``` + +**Позиция:** После поля `il` в массиве `field_order` + +### 3.2 Логика расчетов + +#### 3.2.1 Преобразование даты + +- **Вход:** Год (Int, например: 2024) +- **Выход:** + - `from_date` = "2024-01-01" + - `to_date` = "2024-12-31" + +#### 3.2.2 Режимы расчета + +##### Режим "Tam İl" (Полный год) + +**Для Əlavə 2 (Taxable Assets):** +- Поле `hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri` (501.1) ← Остаточная стоимость на начало года +- Поле `hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri` (504.1) ← Остаточная стоимость на конец года + +**Для Əlavə 1 (Tax Exempt Assets):** +- Поле `hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012` (501.2) ← Остаточная стоимость на начало года +- Поле `ilinsonunaəsasvəsaitlərinqalıqdəyəri` (504.2) ← Остаточная стоимость на конец года + +##### Режим "İl Ərzində" (В течение года) + +**Для Əlavə 2 (Taxable Assets):** +- Поле `hesabatilierzindeuçotaalınan` (503.1) ← Стоимость новых приобретений +- Поле `hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri` (504.1) ← Остаточная стоимость на конец года + +**Для Əlavə 1 (Tax Exempt Assets):** +- Поле `hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri` (503.2) ← Стоимость новых приобретений +- Поле `ilinsonunaəsasvəsaitlərinqalıqdəyəri` (504.2) ← Остаточная стоимость на конец года + +### 3.3 Правила агрегации данных + +#### 3.3.1 Для Əlavə 2 (Налогооблагаемые активы) + +**Источник данных:** +- Активы с заполненным полем `taxable_asset_type` + +**Правило сопоставления:** +- Значение `taxable_asset_type` из актива = Значение "Vergiyə cəlb olunan əmlakların kateqoriyası" в строке таблицы Əlavə 2 + +**Возможные значения `taxable_asset_type`:** +1. Çoxmərtəbəli (çoxmənzilli) yaşayış binaları +2. Qeyri-yaşayış binaları (sahələri) +3. Əmlak kompleksi kimi müəssisələr +4. Qurğular +5. Mənzillər +6. Fərdi yaşayış və bağ evləri +7. Maşınlar və avadanlıqlar +8. Yüksək texnologiyalar məhsulu olan hesablama texnikası +9. Nəqliyyat vasitələri +10. Digər əsas vəsaitlər + +**Логика:** +- Получить все активы компании с `taxable_asset_type IS NOT NULL` +- Для каждого актива рассчитать балансовые стоимости +- Агрегировать (суммировать) по `taxable_asset_type` +- Найти соответствующую строку в Əlavə 2 по совпадению `taxable_asset_type` +- Обновить значения полей в найденной строке + +#### 3.3.2 Для Əlavə 1 (Налогово-освобожденные активы) + +**Источник данных:** +- Активы с заполненным полем `industrial_tax_article` ИЛИ `tax_exempt_tax_article` +- ИСКЛЮЧИТЬ активы с `agricultural_tax_article` (налог на сельскохозяйственные земли) + +**Правило сопоставления:** +1. Извлечь номер статьи из названия Tax Article (например: "199.1", "207.3", "227.1") +2. Найти строку в Əlavə 1, где поле "Azadolma səbəbi" содержит этот номер + +**Примеры извлечения номера статьи:** +- "Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən" → "199.1" +- "Vergi Məcəlləsinin 207.3-cü maddəsinə əsasən" → "207.3" +- "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən" → "199.4.1" + +**Логика:** +- Получить все активы с `industrial_tax_article` или `tax_exempt_tax_article` +- Для каждого актива: + - Извлечь номер статьи regex: `(\d+(?:\.\d+)*(?:-\d+)?)` + - Рассчитать балансовые стоимости +- Агрегировать по номеру статьи +- Найти соответствующую строку в Əlavə 1 +- Обновить значения полей + +### 3.4 Расчет балансовых стоимостей + +Использовать логику стандартного отчета ERPNext "Asset Depreciations and Balances": + +**Формулы:** + +1. **Остаточная стоимость на начало периода (Opening Balance):** + ``` + Чистая стоимость активов, приобретенных до from_date + = Валовая стоимость - Накопленная амортизация на from_date + ``` + +2. **Стоимость новых приобретений (New Purchases):** + ``` + Валовая стоимость активов, приобретенных в период [from_date, to_date] + ``` + +3. **Остаточная стоимость на конец периода (Closing Balance):** + ``` + Opening Balance + + New Purchases + - Sold Assets + - Scrapped Assets + - Capitalized Assets + + Adjustments during period + - Accumulated Depreciation as on to_date + ``` + +**Источники данных для расчетов:** +- `tabAsset` - основные данные об активах +- `tabGL Entry` - записи амортизации и корректировок стоимости +- `tabAsset Category Account` - счета амортизации +- `tabAsset Capitalization` - капитализированные активы (если доступно в версии ERPNext) + +### 3.5 Важные ограничения + +1. **НЕ создавать новые строки** в дочерних таблицах + - Строки в Əlavə 1 и Əlavə 2 должны быть предварительно созданы клиентскими скриптами + - Система только ОБНОВЛЯЕТ значения в существующих строках + +2. **Сохранять строки без совпадений** + - Если для строки не найдено данных об активах, не очищать ее значения + - Оставлять строку как есть + +3. **Обработка дубликатов** + - Если актив имеет оба поля (`tax_exempt_tax_article` И `taxable_asset_type`), включить его в обе таблицы + - Приоритет при наличии нескольких tax article полей: `industrial_tax_article` > `tax_exempt_tax_article` + +--- + +## 4. Архитектура решения + +### 4.1 Серверная часть (Python) + +**Файл:** `property_tax_return.py` + +#### 4.1.1 Основной метод API + +```python +@frappe.whitelist() +def populate_property_tax_tables(company, year, from_date, to_date, + calculation_mode, existing_elave_1, existing_elave_2) +``` + +**Параметры:** +- `company` - Название компании +- `year` - Год декларации +- `from_date` - Дата начала периода (YYYY-MM-DD) +- `to_date` - Дата окончания периода (YYYY-MM-DD) +- `calculation_mode` - Режим расчета: 'tam_il' или 'il_erzinde' +- `existing_elave_1` - JSON массив существующих строк Əlavə 1 +- `existing_elave_2` - JSON массив существующих строк Əlavə 2 + +**Возвращает:** +```python +{ + 'success': True/False, + 'elave_1_data': { + 'matching_key': { + 'field_name': value, + ... + }, + ... + }, + 'elave_2_data': { + 'matching_key': { + 'field_name': value, + ... + }, + ... + }, + 'error': 'Error message' (если success=False) +} +``` + +#### 4.1.2 Вспомогательные функции + +1. **get_taxable_assets_aggregated(company, from_date, to_date)** + - Получает налогооблагаемые активы + - Агрегирует по `taxable_asset_type` + +2. **get_tax_exempt_assets_aggregated(company, from_date, to_date)** + - Получает налогово-освобожденные активы + - Агрегирует по номеру статьи + +3. **get_asset_balances_using_standard_logic(company, from_date, to_date, asset_names)** + - Рассчитывает балансовые стоимости активов + - Использует логику стандартного отчета ERPNext + +4. **extract_article_number(tax_article_name)** + - Извлекает номер статьи из названия Tax Article + - Использует regex: `(\d+(?:\.\d+)*(?:-\d+)?)` + +5. **find_matching_azadolma_sebeb(article_number, existing_rows)** + - Находит строку в Əlavə 1 по номеру статьи + +6. **check_erpnext_version()** + - Проверяет версию ERPNext для совместимости полей + +7. **get_asset_cost_details(...)** - Получает данные о стоимости активов + +8. **get_asset_depreciation_details(...)** - Получает данные об амортизации + +9. **get_asset_value_adjustments(...)** - Получает корректировки стоимости + +### 4.2 Клиентская часть (JavaScript) + +**Файл:** `property_tax_return.js` + +#### 4.2.1 Обработчики событий формы + +```javascript +frappe.ui.form.on('Property tax return', { + refresh: function(frm) { + // Инициализация взаимоисключающих чекбоксов + setup_checkbox_exclusivity(frm); + }, + + il: function(frm) { + // Автоматическая перезагрузка при изменении года + reload_property_tax_data(frm); + }, + + tam_il: function(frm) { + // Обработка изменения чекбокса "Полный год" + // Снимает il_erzinde, если tam_il включен + // Перезагружает данные + }, + + il_erzinde: function(frm) { + // Обработка изменения чекбокса "В течение года" + // Снимает tam_il, если il_erzinde включен + // Перезагружает данные + } +}); +``` + +#### 4.2.2 Вспомогательные функции + +1. **setup_checkbox_exclusivity(frm)** + - Гарантирует, что только один чекбокс выбран + - Если оба сняты, устанавливает tam_il=1 + +2. **reload_property_tax_data(frm)** + - Валидирует входные данные + - Показывает индикатор загрузки + - Вызывает серверный метод + - Обновляет дочерние таблицы + - Показывает сообщения об успехе/ошибке + +3. **update_child_table_values(frm, table_name, data_map)** + - Обновляет значения в строках дочерних таблиц + - Использует квадратные скобки для полей с юникодными символами + - Не создает новые строки + +--- + +## 5. Обработка ошибок + +### 5.1 Валидация на клиенте + +1. **Год не указан:** + ``` + Сообщение: "Please select a year (İl) first" + Действие: Прерывание выполнения + ``` + +2. **Ни один чекбокс не выбран:** + ``` + Сообщение: "Please select either Tam İl or İl Ərzində" + Действие: Прерывание выполнения + ``` + +### 5.2 Обработка на сервере + +1. **Ошибка получения данных:** + - Логирование в Error Log: `frappe.log_error()` + - Возврат `{'success': False, 'error': error_message}` + +2. **Нет активов:** + - Возврат пустых словарей для elave_1_data и elave_2_data + - `{'success': True, 'elave_1_data': {}, 'elave_2_data': {}}` + +3. **Отсутствующие custom fields:** + - Логирование предупреждения + - Продолжение работы без ошибки + +### 5.3 Визуальная обратная связь + +1. **Во время загрузки:** + ```javascript + frappe.dom.freeze(__('Loading property tax data...')); + ``` + +2. **При успехе:** + ```javascript + frappe.show_alert({ + message: __('Property tax data loaded successfully'), + indicator: 'green' + }); + ``` + +3. **При ошибке:** + ```javascript + frappe.msgprint({ + title: __('Error'), + message: error_message, + indicator: 'red' + }); + ``` + +--- + +## 6. Тестирование + +### 6.1 Модульное тестирование (Python) + +**Тестовые случаи:** + +1. **extract_article_number()** + - Входные данные: "Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən" + - Ожидаемый результат: "199.1" + +2. **get_taxable_assets_aggregated()** + - Создать 3 актива с одним `taxable_asset_type` + - Проверить корректность агрегации + +3. **get_asset_balances_using_standard_logic()** + - Создать активы с амортизацией + - Сравнить результаты с отчетом "Asset Depreciations and Balances" + +### 6.2 Интеграционное тестирование + +1. **Полный рабочий процесс - Tam İl:** + - Создать Property Tax Return + - Выбрать год 2024 + - Включить "Tam İl" + - Проверить заполнение полей 501.x и 504.x + +2. **Полный рабочий процесс - İl Ərzində:** + - Создать Property Tax Return + - Выбрать год 2024 + - Включить "İl Ərzində" + - Проверить заполнение полей 503.x и 504.x + +3. **Переключение между режимами:** + - Изменить с "Tam İl" на "İl Ərzində" + - Проверить пересчет значений + +### 6.3 Тестирование UI + +1. **Взаимоисключающие чекбоксы:** + - Включить "Tam İl" → "İl Ərzində" должен быть выключен + - Включить "İl Ərzində" → "Tam İl" должен быть выключен + - Снять оба → "Tam İl" должен автоматически включиться + +2. **Индикаторы загрузки:** + - Проверить отображение при вызове сервера + - Проверить исчезновение после завершения + +--- + +## 7. Производительность + +### 7.1 Оптимизация запросов + +1. **Единый вызов сервера:** + - Все данные получаются за один запрос + - Избегать множественных вызовов API + +2. **Пакетная обработка активов:** + - Получение всех активов одним SQL запросом + - Использование оператора IN для фильтрации + +3. **Агрегация в памяти:** + - Суммирование значений в Python, а не в SQL + - Уменьшение нагрузки на БД + +### 7.2 Ожидаемая производительность + +- **До 100 активов:** < 3 секунд +- **100-500 активов:** 3-10 секунд +- **500+ активов:** 10-30 секунд + +### 7.3 Timeout настройки + +```javascript +// В JavaScript нет explicit timeout, используется стандартный Frappe timeout +// Для Python метода можно увеличить через: +frappe.call({ + ..., + timeout: 120000 // 2 минуты +}); +``` + +--- + +## 8. Безопасность + +### 8.1 Аутентификация и авторизация + +1. **Метод доступен только аутентифицированным пользователям:** + ```python + @frappe.whitelist() + ``` + +2. **Проверка прав на doctype:** + - Пользователь должен иметь права на Property Tax Return + - Автоматически проверяется Frappe framework + +3. **Фильтрация по компании:** + - Данные фильтруются по компании пользователя + - Невозможно получить данные других компаний + +### 8.2 Валидация входных данных + +1. **Тип данных:** + - `year` должен быть Integer + - `from_date`, `to_date` должны быть в формате YYYY-MM-DD + - `calculation_mode` должен быть 'tam_il' или 'il_erzinde' + +2. **SQL Injection Prevention:** + - Использование параметризованных запросов + - Никаких прямых вставок пользовательского ввода в SQL + +--- + +## 9. Миграция и развертывание + +### 9.1 Шаги развертывания + +1. **Обновление JSON схемы:** + ```bash + cd /home/frappe/frappe-bench + bench --site site1 migrate + ``` + +2. **Очистка кэша:** + ```bash + bench --site site1 clear-cache + ``` + +3. **Перезапуск (опционально):** + ```bash + bench restart + ``` + +### 9.2 Откат изменений + +1. **Удаление новых полей через UI:** + - Customize Form → Property Tax Return + - Удалить поля tam_il и il_erzinde + +2. **Откат кода:** + ```bash + git revert + bench --site site1 migrate + ``` + +--- + +## 10. Документация для пользователей + +### 10.1 Краткая инструкция + +**Заголовок:** Автоматическое заполнение декларации по налогу на имущество + +**Шаги:** + +1. Откройте документ "Property Tax Return" (Декларация по налогу на имущество) +2. Заполните поле "İl" (Год), например: 2024 +3. Выберите период: + - **Tam İl** - если компания работала полный год + - **İl Ərzində** - если компания была создана в течение года +4. Система автоматически заполнит таблицы Əlavə 1 и Əlavə 2 данными из ваших активов + +**Примечания:** +- Для работы функции необходимо, чтобы в системе были созданы активы (Assets) с заполненными полями: + - `taxable_asset_type` - для налогооблагаемых активов + - `industrial_tax_article` или `tax_exempt_tax_article` - для освобожденных от налога активов +- Данные рассчитываются на основе фактической амортизации активов + +--- + +## 11. Список файлов + +### 11.1 Измененные файлы + +1. **property_tax_return.json** + - Путь: `/home/frappe/frappe-bench/apps/taxes_az/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.json` + - Изменения: Добавлены поля tam_il и il_erzinde + +2. **property_tax_return.py** + - Путь: `/home/frappe/frappe-bench/apps/taxes_az/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.py` + - Изменения: Реализован весь серверный код (~684 строки) + +3. **property_tax_return.js** + - Путь: `/home/frappe/frappe-bench/apps/taxes_az/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.js` + - Изменения: Добавлен клиентский код автозаполнения (~177 строк) + +### 11.2 Новые файлы + +1. **TECHNICAL_SPECIFICATION_PROPERTY_TAX_AUTOMATION.md** (этот документ) + - Путь: `/home/frappe/frappe-bench/apps/taxes_az/TECHNICAL_SPECIFICATION_PROPERTY_TAX_AUTOMATION.md` + +--- + +## 12. Известные ограничения + +1. **Предварительное создание строк:** + - Строки в Əlavə 1 и Əlavə 2 должны быть созданы заранее клиентскими скриптами + - Система не создает новые строки автоматически + +2. **Один год за раз:** + - Расчет выполняется только для одного календарного года + - Невозможно рассчитать для нескольких лет одновременно + +3. **Зависимость от ERPNext версии:** + - Код проверяет наличие полей net_purchase_amount и Asset Capitalization + - Может требовать адаптации для очень старых версий ERPNext + +4. **Производительность для больших объемов:** + - Для компаний с 1000+ активами может потребоваться оптимизация + - Рекомендуется тестирование на реальных данных + +--- + +## 13. Будущие улучшения + +### 13.1 Краткосрочные (v1.1) + +1. Добавить кнопку принудительного пересчета +2. Показывать количество обработанных активов +3. Логирование детальной информации о расчетах + +### 13.2 Долгосрочные (v2.0) + +1. Поддержка расчета за произвольный период (не только календарный год) +2. Экспорт детального отчета по расчетам в Excel +3. Предварительный просмотр изменений перед применением +4. Batch processing для очень больших объемов данных + +--- + +## 14. Контакты и поддержка + +**Разработчик:** Claude (Anthropic) +**Дата разработки:** 2025-12-11 +**Версия системы:** Frappe v15, ERPNext +**Git репозиторий:** `/home/frappe/frappe-bench/apps/taxes_az` + +--- + +## 15. История изменений + +| Версия | Дата | Описание | Автор | +|--------|------|----------|-------| +| 1.0 | 2025-12-11 | Первоначальная разработка и развертывание | Claude | +| 1.0.1 | 2025-12-11 | Исправление ошибок в JavaScript (квадратные скобки для Unicode полей) | Claude | + +--- + +**Конец документа** diff --git a/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.js b/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.js index bf79762..65c2ee8 100644 --- a/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.js +++ b/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.js @@ -1,33 +1,176 @@ -// Код для экспорта XML +// Код для экспорта XML и автоматического заполнения из Asset frappe.ui.form.on('Property tax return', { - refresh: function(frm) { - frm.add_custom_button(__('Export XML'), function() { - frappe.call({ - method: 'taxes_az.taxes_az.doctype.xml_mapping_tool.xml_mapping_tool.generate_xml', - args: { - mapping_tool: 'Property tax return', // Замените на имя вашего документа XML Mapping Tool - doctype: 'Property tax return', // Замените на имя DocType - docname: frm.docname - }, - callback: function(r) { - if (r.message) { - // Создаем временный элемент для скачивания - var element = document.createElement('a'); - element.setAttribute('href', 'data:text/xml;charset=utf-8,' + encodeURIComponent(r.message)); - element.setAttribute('download', frm.docname + '.xml'); - element.style.display = 'none'; - document.body.appendChild(element); - element.click(); - document.body.removeChild(element); - - frappe.show_alert({ - message: __('XML file generated and downloaded'), - indicator: 'green' - }); - } - } - }); - }, __('Actions')); - } - } -); \ No newline at end of file + refresh: function(frm) { + // Add Export XML button + frm.add_custom_button(__('Export XML'), function() { + frappe.call({ + method: 'taxes_az.taxes_az.doctype.xml_mapping_tool.xml_mapping_tool.generate_xml', + args: { + mapping_tool: 'Property tax return', + doctype: 'Property tax return', + docname: frm.docname + }, + callback: function(r) { + if (r.message) { + // Создаем временный элемент для скачивания + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/xml;charset=utf-8,' + encodeURIComponent(r.message)); + element.setAttribute('download', frm.docname + '.xml'); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + + frappe.show_alert({ + message: __('XML file generated and downloaded'), + indicator: 'green' + }); + } + } + }); + }, __('Actions')); + + // Ensure checkbox exclusivity on load + setup_checkbox_exclusivity(frm); + }, + + // Handle year changes + il: function(frm) { + if (frm.doc.il && (frm.doc.tam_il || frm.doc.il_erzinde)) { + reload_property_tax_data(frm); + } + }, + + // Checkbox handlers - ensure mutual exclusivity + tam_il: function(frm) { + if (frm.doc.tam_il) { + frm.set_value('il_erzinde', 0); + if (frm.doc.il) { + reload_property_tax_data(frm); + } + } else if (!frm.doc.il_erzinde) { + // If unchecking tam_il and il_erzinde is also unchecked, default to tam_il + frm.set_value('tam_il', 1); + } + }, + + il_erzinde: function(frm) { + if (frm.doc.il_erzinde) { + frm.set_value('tam_il', 0); + if (frm.doc.il) { + reload_property_tax_data(frm); + } + } else if (!frm.doc.tam_il) { + // If unchecking il_erzinde and tam_il is also unchecked, default to tam_il + frm.set_value('tam_il', 1); + } + } +}); + +function setup_checkbox_exclusivity(frm) { + // Ensure at least one is always checked + if (!frm.doc.tam_il && !frm.doc.il_erzinde) { + frm.set_value('tam_il', 1); + } + // Ensure only one is checked + if (frm.doc.tam_il && frm.doc.il_erzinde) { + frm.set_value('il_erzinde', 0); + } +} + +function reload_property_tax_data(frm) { + // Validate required fields + if (!frm.doc.il) { + frappe.msgprint(__('Please select a year (İl) first')); + return; + } + + if (!frm.doc.tam_il && !frm.doc.il_erzinde) { + frappe.msgprint(__('Please select either Tam İl or İl Ərzində')); + return; + } + + // Show loading indicator + frappe.dom.freeze(__('Loading property tax data...')); + + // Calculate date range from year + const from_date = `${frm.doc.il}-01-01`; + const to_date = `${frm.doc.il}-12-31`; + const calculation_mode = frm.doc.tam_il ? 'tam_il' : 'il_erzinde'; + + // Get default company + const company = frappe.defaults.get_user_default('Company'); + + // Call server-side method + frappe.call({ + method: 'taxes_az.taxes_az.doctype.property_tax_return.property_tax_return.populate_property_tax_tables', + args: { + company: company, + year: frm.doc.il, + from_date: from_date, + to_date: to_date, + calculation_mode: calculation_mode, + existing_elave_1: frm.doc.elave_1 || [], + existing_elave_2: frm.doc.elave_2 || [] + }, + callback: function(r) { + frappe.dom.unfreeze(); + + if (r.message && r.message.success) { + // Update Əlavə 1 (Tax Exempt) + update_child_table_values(frm, 'elave_1', r.message.elave_1_data); + + // Update Əlavə 2 (Taxable) + update_child_table_values(frm, 'elave_2', r.message.elave_2_data); + + frm.refresh_fields(); + + frappe.show_alert({ + message: __('Property tax data loaded successfully'), + indicator: 'green' + }); + } else { + frappe.msgprint({ + title: __('Error'), + message: r.message ? r.message.error : __('Failed to load data'), + indicator: 'red' + }); + } + }, + error: function(err) { + frappe.dom.unfreeze(); + frappe.msgprint({ + title: __('Error'), + message: __('Server error while loading data'), + indicator: 'red' + }); + } + }); +} + +function update_child_table_values(frm, table_name, data_map) { + if (!frm.doc[table_name]) { + return; + } + + frm.doc[table_name].forEach(function(row) { + // Find matching data based on the identifier field + let matching_key = null; + + if (table_name === 'elave_1') { + matching_key = row['azadolmasəbəbi']; + } else if (table_name === 'elave_2') { + matching_key = row['vergiyəcəlbolunanəmlaklarınkateqoriyası']; + } + + // Update row with values from data_map + if (matching_key && data_map[matching_key]) { + const field_data = data_map[matching_key]; + + Object.keys(field_data).forEach(function(field) { + const value = field_data[field]; + frappe.model.set_value(row.doctype, row.name, field, value); + }); + } + }); +} \ No newline at end of file diff --git a/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.json b/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.json index 9c5e760..0b760c1 100644 --- a/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.json +++ b/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.json @@ -12,6 +12,8 @@ "vergiorqan\u0131", "vergid\u00f6vr\u00fc", "il", + "tam_il", + "il_erzinde", "\u015f\u0259xsi", "v\u00f6en", "soyad\u0131", @@ -91,6 +93,18 @@ "label": "\u0130l", "length": 4 }, + { + "default": "1", + "fieldname": "tam_il", + "fieldtype": "Check", + "label": "Tam \u0130l" + }, + { + "default": "0", + "fieldname": "il_erzinde", + "fieldtype": "Check", + "label": "\u0130l \u018frzind\u0259" + }, { "fieldname": "\u015f\u0259xsi", "fieldtype": "Section Break", diff --git a/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.py b/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.py index 6fae33e..94dcf60 100644 --- a/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.py +++ b/taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.py @@ -1,9 +1,735 @@ # Copyright (c) 2025, Jey Soft and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from frappe.utils import flt, add_days +import re +import json class Propertytaxreturn(Document): pass + + +@frappe.whitelist() +def populate_property_tax_tables(company, year, from_date, to_date, calculation_mode, existing_elave_1, existing_elave_2): + """ + Main method to populate Property Tax Return child tables from Asset data + + Args: + company: Company name + year: Tax year (integer) + from_date: Start date (YYYY-MM-DD) + to_date: End date (YYYY-MM-DD) + calculation_mode: 'tam_il' or 'il_erzinde' + existing_elave_1: Existing Əlavə 1 rows (JSON array) + existing_elave_2: Existing Əlavə 2 rows (JSON array) + + Returns: + Dictionary with elave_1_data and elave_2_data mappings + """ + try: + # Parse JSON if needed + if isinstance(existing_elave_1, str): + existing_elave_1 = json.loads(existing_elave_1) + if isinstance(existing_elave_2, str): + existing_elave_2 = json.loads(existing_elave_2) + + # Get aggregated asset data + taxable_aggregated = get_taxable_assets_aggregated(company, from_date, to_date) + tax_exempt_aggregated = get_tax_exempt_assets_aggregated(company, from_date, to_date) + + # Map to rows with proper field names based on calculation_mode + # Use fuzzy matching for Əlavə 2 (Taxable Assets) + elave_2_data = {} + for category, data in taxable_aggregated.items(): + # Find matching row by partial match (e.g., "Mənzillər" matches "1.5 Mənzillər") + matching_key = find_matching_category(category, existing_elave_2) + + if matching_key: + if calculation_mode == 'tam_il': + elave_2_data[matching_key] = { + 'hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri': data['value_as_on_from_date'], + 'hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri': data['net_asset_value_as_on_to_date'], + 'hesabatilierzindeuçotaalınan': 0 + } + else: # il_erzinde + elave_2_data[matching_key] = { + 'hesabatilierzindeuçotaalınan': data['value_of_new_purchase'], + 'hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri': data['net_asset_value_as_on_to_date'], + 'hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri': 0 + } + + # Map to rows with fuzzy matching for Əlavə 1 (Tax Exempt Assets) + elave_1_data = {} + for article_number, data in tax_exempt_aggregated.items(): + # Find matching row by article number in "Azadolma səbəbi" + matching_key = find_matching_azadolma_sebeb(article_number, existing_elave_1) + + if matching_key: + if calculation_mode == 'tam_il': + elave_1_data[matching_key] = { + 'hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012': data['value_as_on_from_date'], + 'ilinsonunaəsasvəsaitlərinqalıqdəyəri': data['net_asset_value_as_on_to_date'], + 'hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri': 0 + } + else: # il_erzinde + elave_1_data[matching_key] = { + 'hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri': data['value_of_new_purchase'], + 'ilinsonunaəsasvəsaitlərinqalıqdəyəri': data['net_asset_value_as_on_to_date'], + 'hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012': 0 + } + + return { + 'success': True, + 'elave_1_data': elave_1_data, + 'elave_2_data': elave_2_data + } + + except Exception as e: + frappe.log_error(f"Error in populate_property_tax_tables: {str(e)}", "Property Tax Return") + return { + 'success': False, + 'error': str(e) + } + + +def get_taxable_assets_aggregated(company, from_date, to_date): + """ + Get aggregated data for taxable assets grouped by taxable_asset_type + + Returns: + Dictionary: { + 'taxable_asset_type_value': { + 'value_as_on_from_date': float, + 'value_of_new_purchase': float, + 'net_asset_value_as_on_to_date': float + } + } + """ + # Get all assets with taxable_asset_type populated + assets_with_taxable_type = frappe.db.sql(""" + SELECT + name, + taxable_asset_type + FROM + `tabAsset` + WHERE + docstatus = 1 + AND company = %(company)s + AND taxable_asset_type IS NOT NULL + AND taxable_asset_type != '' + AND purchase_date <= %(to_date)s + """, { + 'company': company, + 'to_date': to_date + }, as_dict=True) + + if not assets_with_taxable_type: + return {} + + # Get asset balances + asset_names = [asset['name'] for asset in assets_with_taxable_type] + asset_balances = get_asset_balances_using_standard_logic(company, from_date, to_date, asset_names) + + # Aggregate by taxable_asset_type + aggregated = {} + + for asset in assets_with_taxable_type: + asset_type = asset['taxable_asset_type'] + + # Find balance for this asset + balance = next( + (b for b in asset_balances if b['asset'] == asset['name']), + {} + ) + + # Initialize if not exists + if asset_type not in aggregated: + aggregated[asset_type] = { + 'value_as_on_from_date': 0.0, + 'value_of_new_purchase': 0.0, + 'net_asset_value_as_on_to_date': 0.0 + } + + # Accumulate values + aggregated[asset_type]['value_as_on_from_date'] += flt(balance.get('value_as_on_from_date', 0)) + aggregated[asset_type]['value_of_new_purchase'] += flt(balance.get('value_of_new_purchase', 0)) + aggregated[asset_type]['net_asset_value_as_on_to_date'] += flt(balance.get('net_asset_value_as_on_to_date', 0)) + + return aggregated + + +def get_tax_exempt_assets_aggregated(company, from_date, to_date): + """ + Get aggregated data for tax exempt assets grouped by tax article + + Returns: + Dictionary: { + 'tax_article_number': { # e.g., '199.1', '207.3' + 'value_as_on_from_date': float, + 'value_of_new_purchase': float, + 'net_asset_value_as_on_to_date': float + } + } + """ + # Get all assets with industrial_tax_article or tax_exempt_tax_article + # Exclude agricultural_tax_article as per requirements + assets_with_tax_articles = frappe.db.sql(""" + SELECT + a.name, + a.industrial_tax_article, + a.tax_exempt_tax_article, + COALESCE(NULLIF(a.industrial_tax_article, ''), a.tax_exempt_tax_article) as tax_article + FROM + `tabAsset` a + WHERE + a.docstatus = 1 + AND a.company = %(company)s + AND ( + (a.industrial_tax_article IS NOT NULL AND a.industrial_tax_article != '') + OR (a.tax_exempt_tax_article IS NOT NULL AND a.tax_exempt_tax_article != '') + ) + AND a.purchase_date <= %(to_date)s + """, { + 'company': company, + 'to_date': to_date + }, as_dict=True) + + if not assets_with_tax_articles: + return {} + + # Get asset balances + asset_names = [asset['name'] for asset in assets_with_tax_articles] + asset_balances = get_asset_balances_using_standard_logic(company, from_date, to_date, asset_names) + + # Aggregate by tax article number + aggregated = {} + + for asset in assets_with_tax_articles: + # Extract article number from Tax Article name + tax_article_full = asset['tax_article'] + article_number = extract_article_number(tax_article_full) + + if not article_number: + continue + + # Find balance for this asset + balance = next( + (b for b in asset_balances if b['asset'] == asset['name']), + {} + ) + + # Initialize if not exists + if article_number not in aggregated: + aggregated[article_number] = { + 'value_as_on_from_date': 0.0, + 'value_of_new_purchase': 0.0, + 'net_asset_value_as_on_to_date': 0.0 + } + + # Accumulate values + aggregated[article_number]['value_as_on_from_date'] += flt(balance.get('value_as_on_from_date', 0)) + aggregated[article_number]['value_of_new_purchase'] += flt(balance.get('value_of_new_purchase', 0)) + aggregated[article_number]['net_asset_value_as_on_to_date'] += flt(balance.get('net_asset_value_as_on_to_date', 0)) + + return aggregated + + +def extract_article_number(tax_article_name): + """ + Extract article number from Tax Article name + + Examples: + "Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən" → "199.1" + "Vergi Məcəlləsinin 207.3-cü maddəsinə əsasən" → "207.3" + "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən" → "199.4.1" + + Returns: + String: Article number or None if not found + """ + if not tax_article_name: + return None + + # Pattern to match article numbers like 199.1, 207.3, 199.4.1-1, 227.1 + pattern = r'(\d+(?:\.\d+)*(?:-\d+)?)' + match = re.search(pattern, tax_article_name) + + if match: + return match.group(1) + + return None + + +def find_matching_azadolma_sebeb(article_number, existing_rows): + """ + Find the "Azadolma səbəbi" value that contains the given article number + + Args: + article_number: String like "199.1" or "207.3" + existing_rows: List of existing Əlavə 1 rows + + Returns: + The matching "Azadolma səbəbi" value or None + """ + if not article_number: + return None + + for row in existing_rows: + azadolma_sebeb = row.get('azadolmasəbəbi', '') + if article_number in azadolma_sebeb: + return azadolma_sebeb + + return None + + +def find_matching_category(asset_category, existing_rows): + """ + Find matching category in Əlavə 2 rows using partial match + + Examples: + Asset has: "Mənzillər" + Row has: "1.5 Mənzillər" + Result: Match found + + Asset has: "Qeyri-yaşayış binaları (sahələri)" + Row has: "1.2 Qeyri-yaşayış binaları (sahələri)" + Result: Match found + + Args: + asset_category: Category from Asset's taxable_asset_type field + existing_rows: List of existing Əlavə 2 rows + + Returns: + The matching "Vergiyə cəlb olunan əmlakların kateqoriyası" value or None + """ + if not asset_category: + return None + + for row in existing_rows: + row_category = row.get('vergiyəcəlbolunanəmlaklarınkateqoriyası', '') + + # Check if asset_category is contained in row_category (e.g., "Mənzillər" in "1.5 Mənzillər") + if asset_category in row_category: + return row_category + + # Also check reverse - if row_category is contained in asset_category + # (in case asset has full name with prefix) + if row_category in asset_category: + return row_category + + return None + + +def get_asset_balances_using_standard_logic(company, from_date, to_date, asset_names): + """ + Calculate asset balances using ERPNext's Asset Depreciations and Balances report logic + + Returns: + List of dictionaries with asset balances + """ + if not asset_names: + return [] + + # Check ERPNext version for field compatibility + version_info = check_erpnext_version() + purchase_amount_field = version_info['purchase_amount_field'] + has_capitalization = version_info['has_capitalization'] + + # Get asset details (gross values) + asset_details = get_asset_cost_details( + company, from_date, to_date, asset_names, + purchase_amount_field, has_capitalization + ) + + # Get depreciation details + asset_depreciation = get_asset_depreciation_details( + company, from_date, to_date, asset_names + ) + + # Get value adjustments + asset_adjustments = get_asset_value_adjustments( + company, from_date, to_date, asset_names + ) + + # Combine and calculate net values + result = [] + + for asset_detail in asset_details: + asset_name = asset_detail.get('name') + + # Find depreciation + depreciation = next( + (dep for dep in asset_depreciation if dep['asset'] == asset_name), + {} + ) + + # Find adjustments + adjustments = asset_adjustments.get(asset_name, { + 'adjustment_before_from_date': 0.0, + 'adjustment_till_to_date': 0.0 + }) + + # Calculate values following ERPNext standard report logic + value_as_on_from_date = flt(asset_detail.get('cost_as_on_from_date', 0)) + value_as_on_from_date += adjustments['adjustment_before_from_date'] + + value_of_new_purchase = flt(asset_detail.get('cost_of_new_purchase', 0)) + + adjustment_during_period = ( + adjustments['adjustment_till_to_date'] - + adjustments['adjustment_before_from_date'] + ) + + value_as_on_to_date = ( + value_as_on_from_date + + value_of_new_purchase - + flt(asset_detail.get('cost_of_sold_asset', 0)) - + flt(asset_detail.get('cost_of_scrapped_asset', 0)) - + flt(asset_detail.get('cost_of_capitalized_asset', 0)) + + adjustment_during_period + ) + + accumulated_depreciation_as_on_from_date = flt( + depreciation.get('accumulated_depreciation_as_on_from_date', 0) + ) + + accumulated_depreciation_as_on_to_date = ( + accumulated_depreciation_as_on_from_date + + flt(depreciation.get('depreciation_amount_during_the_period', 0)) - + flt(depreciation.get('depreciation_eliminated_during_the_period', 0)) - + flt(depreciation.get('depreciation_eliminated_via_reversal', 0)) + ) + + net_asset_value_as_on_from_date = ( + value_as_on_from_date - accumulated_depreciation_as_on_from_date + ) + + net_asset_value_as_on_to_date = ( + value_as_on_to_date - accumulated_depreciation_as_on_to_date + ) + + balance_result = { + 'asset': asset_name, + 'value_as_on_from_date': net_asset_value_as_on_from_date, + 'value_of_new_purchase': value_of_new_purchase, + 'net_asset_value_as_on_to_date': net_asset_value_as_on_to_date + } + result.append(balance_result) + + return result + + +def check_erpnext_version(): + """Check ERPNext version for field compatibility""" + columns = frappe.db.get_table_columns("Asset") + has_net_purchase_amount = "net_purchase_amount" in columns + has_capitalization = "Asset Capitalization" in frappe.get_all("DocType", pluck="name") + + return { + "purchase_amount_field": "net_purchase_amount" if has_net_purchase_amount else "gross_purchase_amount", + "has_capitalization": has_capitalization + } + + +def get_asset_cost_details(company, from_date, to_date, asset_names, purchase_amount_field, has_capitalization): + """Get asset cost details (mimics ERPNext standard report)""" + + asset_placeholders = ", ".join(["%s"] * len(asset_names)) + + capitalization_check = "" + if has_capitalization: + capitalization_check = """ + AND NOT EXISTS( + SELECT 1 FROM `tabAsset Capitalization Asset Item` acai + JOIN `tabAsset Capitalization` ac ON acai.parent = ac.name + WHERE acai.asset = a.name + AND ac.posting_date < %s + AND ac.docstatus = 1 + ) + """ + + query = f""" + SELECT + a.name, + IFNULL(SUM( + CASE + WHEN a.purchase_date < %s THEN + CASE + WHEN IFNULL(a.disposal_date, 0) = 0 OR a.disposal_date >= %s + THEN a.{purchase_amount_field} + ELSE 0 + END + ELSE 0 + END + ), 0) as cost_as_on_from_date, + + IFNULL(SUM( + CASE + WHEN a.purchase_date >= %s + THEN a.{purchase_amount_field} + ELSE 0 + END + ), 0) as cost_of_new_purchase, + + IFNULL(SUM( + CASE + WHEN IFNULL(a.disposal_date, 0) != 0 + AND a.disposal_date >= %s + AND a.disposal_date <= %s + THEN + CASE WHEN a.status = 'Sold' THEN a.{purchase_amount_field} ELSE 0 END + ELSE 0 + END + ), 0) as cost_of_sold_asset, + + IFNULL(SUM( + CASE + WHEN IFNULL(a.disposal_date, 0) != 0 + AND a.disposal_date >= %s + AND a.disposal_date <= %s + THEN + CASE WHEN a.status = 'Scrapped' THEN a.{purchase_amount_field} ELSE 0 END + ELSE 0 + END + ), 0) as cost_of_scrapped_asset, + + IFNULL(SUM( + CASE + WHEN IFNULL(a.disposal_date, 0) != 0 + AND a.disposal_date >= %s + AND a.disposal_date <= %s + THEN + CASE WHEN a.status = 'Capitalized' THEN a.{purchase_amount_field} ELSE 0 END + ELSE 0 + END + ), 0) as cost_of_capitalized_asset + FROM + `tabAsset` a + WHERE + a.docstatus = 1 + AND a.company = %s + AND a.purchase_date <= %s + AND a.name IN ({asset_placeholders}) + {capitalization_check} + GROUP BY a.name + """ + + values = [ + from_date, # cost_as_on_from_date condition 1 + from_date, # cost_as_on_from_date condition 2 + from_date, # cost_of_new_purchase + from_date, # cost_of_sold_asset condition 1 + to_date, # cost_of_sold_asset condition 2 + from_date, # cost_of_scrapped_asset condition 1 + to_date, # cost_of_scrapped_asset condition 2 + from_date, # cost_of_capitalized_asset condition 1 + to_date, # cost_of_capitalized_asset condition 2 + company, # company filter + to_date, # purchase_date filter + *asset_names # asset names for IN clause + ] + + if has_capitalization: + values.append(from_date) + + return frappe.db.sql(query, values, as_dict=True) + + +def get_asset_depreciation_details(company, from_date, to_date, asset_names): + """Get asset depreciation details (mimics ERPNext standard report)""" + + asset_placeholders = ", ".join(["%s"] * len(asset_names)) + + query = f""" + SELECT + results.name as asset, + SUM(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, + SUM(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, + SUM(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period, + SUM(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal + FROM ( + SELECT + a.name as name, + IFNULL(SUM( + CASE + WHEN gle.posting_date < %s + AND (IFNULL(a.disposal_date, 0) = 0 OR a.disposal_date >= %s) + THEN gle.debit + ELSE 0 + END + ), 0) as accumulated_depreciation_as_on_from_date, + + IFNULL(SUM( + CASE + WHEN gle.posting_date <= %s + AND IFNULL(a.disposal_date, 0) = 0 + THEN gle.credit + ELSE 0 + END + ), 0) as depreciation_eliminated_via_reversal, + + IFNULL(SUM( + CASE + WHEN IFNULL(a.disposal_date, 0) != 0 + AND a.disposal_date >= %s + AND a.disposal_date <= %s + AND gle.posting_date <= a.disposal_date + THEN gle.debit + ELSE 0 + END + ), 0) as depreciation_eliminated_during_the_period, + + IFNULL(SUM( + CASE + WHEN gle.posting_date >= %s + AND gle.posting_date <= %s + AND (IFNULL(a.disposal_date, 0) = 0 OR gle.posting_date <= a.disposal_date) + THEN gle.debit + ELSE 0 + END + ), 0) as depreciation_amount_during_the_period + FROM + `tabGL Entry` gle + JOIN + `tabAsset` a ON gle.against_voucher = a.name + JOIN + `tabAsset Category Account` aca ON aca.parent = a.asset_category + AND aca.company_name = %s + JOIN + `tabCompany` company ON company.name = %s + WHERE + a.docstatus = 1 + AND a.company = %s + AND a.purchase_date <= %s + AND gle.is_cancelled = 0 + AND gle.account = IFNULL(aca.depreciation_expense_account, company.depreciation_expense_account) + AND a.name IN ({asset_placeholders}) + GROUP BY a.name + + UNION + + SELECT + a.name as name, + IFNULL(SUM( + CASE + WHEN IFNULL(a.disposal_date, 0) != 0 + AND a.disposal_date < %s + THEN 0 + ELSE a.opening_accumulated_depreciation + END + ), 0) as accumulated_depreciation_as_on_from_date, + 0 as depreciation_eliminated_via_reversal, + IFNULL(SUM( + CASE + WHEN a.disposal_date >= %s + AND a.disposal_date <= %s + THEN a.opening_accumulated_depreciation + ELSE 0 + END + ), 0) as depreciation_eliminated_during_the_period, + 0 as depreciation_amount_during_the_period + FROM + `tabAsset` a + WHERE + a.docstatus = 1 + AND a.company = %s + AND a.purchase_date <= %s + AND a.name IN ({asset_placeholders}) + GROUP BY a.name + ) as results + GROUP BY results.name + """ + + values = [ + from_date, # accumulated_depreciation_as_on_from_date condition 1 + from_date, # accumulated_depreciation_as_on_from_date condition 2 + to_date, # depreciation_eliminated_via_reversal + from_date, # depreciation_eliminated_during_the_period condition 1 + to_date, # depreciation_eliminated_during_the_period condition 2 + from_date, # depreciation_amount_during_the_period condition 1 + to_date, # depreciation_amount_during_the_period condition 2 + company, # aca.company_name + company, # company.name + company, # a.company + to_date, # a.purchase_date + *asset_names, # asset names for IN clause (first SELECT) + from_date, # disposal_date condition (second SELECT) + from_date, # disposal_date condition (second SELECT) + to_date, # disposal_date condition (second SELECT) + company, # a.company (second SELECT) + to_date, # a.purchase_date (second SELECT) + *asset_names # asset names for IN clause (second SELECT) + ] + + return frappe.db.sql(query, values, as_dict=True) + + +def get_asset_value_adjustments(company, from_date, to_date, asset_names): + """Get asset value adjustments (mimics ERPNext standard report)""" + + asset_placeholders = ", ".join(["%s"] * len(asset_names)) + + query = f""" + SELECT + a.name AS asset, + IFNULL( + SUM( + CASE + WHEN gle.posting_date < %s + AND (a.disposal_date IS NULL OR a.disposal_date >= %s) + THEN gle.debit - gle.credit + ELSE 0 + END + ), + 0) AS value_adjustment_before_from_date, + IFNULL( + SUM( + CASE + WHEN gle.posting_date <= %s + AND (a.disposal_date IS NULL OR a.disposal_date >= %s) + THEN gle.debit - gle.credit + ELSE 0 + END + ), + 0) AS value_adjustment_till_to_date + FROM + `tabGL Entry` gle + JOIN + `tabAsset` a ON gle.against_voucher = a.name + JOIN + `tabAsset Category Account` aca ON aca.parent = a.asset_category + AND aca.company_name = %s + WHERE + gle.is_cancelled = 0 + AND a.docstatus = 1 + AND a.company = %s + AND a.purchase_date <= %s + AND gle.account = aca.fixed_asset_account + AND a.name IN ({asset_placeholders}) + GROUP BY a.name + """ + + values = [ + from_date, # value_adjustment_before_from_date condition 1 + from_date, # value_adjustment_before_from_date condition 2 + to_date, # value_adjustment_till_to_date condition 1 + to_date, # value_adjustment_till_to_date condition 2 + company, # aca.company_name + company, # a.company + to_date, # a.purchase_date + *asset_names # asset names for IN clause + ] + + result = frappe.db.sql(query, values, as_dict=True) + + # Convert to dictionary + adjustment_map = {} + for r in result: + adjustment_map[r['asset']] = { + 'adjustment_before_from_date': flt(r.get('value_adjustment_before_from_date', 0)), + 'adjustment_till_to_date': flt(r.get('value_adjustment_till_to_date', 0)) + } + + return adjustment_map