Modifed property tax return
This commit is contained in:
parent
0335d577c7
commit
d268d1b7bf
|
|
@ -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 <commit_hash>
|
||||
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 |
|
||||
|
||||
---
|
||||
|
||||
**Конец документа**
|
||||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
);
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue