feat(property-tax-gov): rework property_tax_gov client script; add RU architecture doc

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali 2026-06-10 17:52:03 +00:00
parent 6d6ff50afe
commit d370d74e32
2 changed files with 348 additions and 74 deletions

View File

@ -0,0 +1,218 @@
# Property Tax Gov — полный разбор
## Назначение
Это доктайп для формирования **декларации по налогу на имущество** (Əmlak vergisinin
bəyannaməsi) для азербайджанской системы E-Taxes. Он берёт данные об основных средствах
(Asset) из ERPNext, агрегирует их, рассчитывает налог по формулам, извлечённым из самого
кабинета new.e-taxes.gov.az, и экспортирует результат в XML для подачи.
Это часть пилота **Property Tax Gov** — нового семейства доктайпов с расчётом «по странице,
без авторизации», заменяющего старые `property_tax_return_ai`.
## Файлы доктайпа
| Файл | Роль |
|------|------|
| `property_tax_gov.json` | Определение полей: 3 таба, ~8 дочерних таблиц |
| `property_tax_gov.py` | Бэкенд: агрегация Asset → строки таблиц + мост к Node-калькулятору |
| `property_tax_gov.js` | Клиент: автозаполнение, расчёт формул, валидация, XML-экспорт |
| `gns_property_calc.bundle.js` | Извлечённый из кабинета ГНС `useCalculation` (эталон формул) |
| `gns_property_calc_cli.js` | stdin/stdout-обёртка Node для bundle |
---
## 1. Структура формы (JSON)
Три основных таба:
1. **Ümumi məlumat** (Общая информация) — шапка декларации: налоговый орган
(`vergiorqanı` — большой Select со всеми территориальными управлениями), год (`il`),
ВОЕН, ФИО/название плательщика, тип плательщика (Hüquqi/Fiziki), вид деклараций и т.д.
2. **Əmlak vergisinin hesablanması** (Расчёт налога на имущество) — 8 «cədvəl»
(расчётных таблиц):
- `vergi_hesab_1ci` / `vergi_hesab_1ci_1` — остаток на начало года (501) / на дату создания (503)
- `vergi_hesab_2ci` — остаток на конец года (504)
- `vergi_hesab_3cu` — сверхзастрахованные ОС (505)
- `vergi_hesab_4cu` — ставка налога (506), `vergi_hesab_4cu_1` — кол-во месяцев (507)
- `vergi_hesab_5ci` — рассчитанный налог (509/510)
- `vergi_hesab_6ci` — сумма к уплате в бюджет (511)
3. **Əlavə 1** (`elave_1`) и **Əlavə 2** (`elave_2`) — два приложения, **источники данных**:
- **Əlavə 1** — имущество, **освобождённое** от налога, сгруппированное по статьям НК
(199.1, 199.4.1, 207.3 …). Ключевое поле строки — `azadolmasəbəbi` (основание освобождения).
- **Əlavə 2** — имущество, **подлежащее** налогообложению, по категориям (Binalar,
Mənzillər, Nəqliyyat vasitələri …). Ключевое поле — `vergiyəcəlbolunanəmlaklarınkateqoriyası`.
### Ключевая развилка: два режима
Два взаимоисключающих чекбокса в шапке управляют всей логикой:
- **`tam_il`** (Tam İl) — компания работала **полный год**.
- **`il_erzinde`** (İl Ərzində) — компания **создана в течение года** (неполный период).
Клиентский код (`setup_checkbox_exclusivity`, обработчики `tam_il`/`il_erzinde`) гарантирует,
что ровно один из них всегда включён.
---
## 2. Автозаполнение из Asset (Python-бэкенд)
Когда выбран год и режим, JS вызывает `reload_property_tax_data()` → серверный
`populate_property_tax_tables()`.
**Поток данных:**
```
tabAsset (docstatus=1, company, purchase_date<=to_date)
├─ taxable_asset_type заполнен ──→ get_taxable_assets_aggregated() ──→ Əlavə 2
└─ industrial/tax_exempt_tax_article ─→ get_tax_exempt_assets_aggregated() ─→ Əlavə 1
```
Сердце расчёта — `get_asset_balances_using_standard_logic()`. Это **точная репликация
стандартного отчёта ERPNext «Asset Depreciations and Balances»**:
- `get_asset_cost_details` — валовая стоимость (на начало периода, новые покупки,
проданные/списанные/капитализированные);
- `get_asset_depreciation_details` — накопленная амортизация из GL Entry
(через `tabAsset Category Account`);
- `get_asset_value_adjustments` — переоценки.
Из них считается `net_asset_value_as_on_from_date/to_date` (остаточная стоимость).
Плюс `insurance_value` = (застрахованная сумма валовая стоимость), если объект
застрахован выше стоимости.
**Версионная совместимость:** код проверяет колонки таблицы Asset и сам выбирает
`gross_purchase_amount` / `net_purchase_amount` / `purchase_amount`, потому что ERPNext v15
переименовал поле (`gross_purchase_amount` → `net_purchase_amount`).
**Сопоставление со строками таблиц** — нечёткое (fuzzy):
- `extract_article_number()` достаёт «199.1» из «Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən»
регуляркой;
- `find_matching_azadolma_sebeb()` ищет строку Əlavə 1, чей `azadolmasəbəbi` содержит этот номер;
- `find_matching_category()` сопоставляет `taxable_asset_type` со строкой Əlavə 2 по частичному
вхождению («Mənzillər» ↔ «1.5 Mənzillər»).
**Бизнес-правило для İl Ərzində:** если у компании, созданной в течение года, нет новых покупок
(`value_of_new_purchase < 0.01`), все поля строки обнуляются. Также в зависимости от режима
данные кладутся в разные колонки: в `tam_il` — в «остаток на начало года» (501),
в `il_erzinde` — в «принято на учёт в течение года» (503).
Сервер возвращает не строки, а **карты `{ключ_строки: {поле: значение}}`**, а уже клиент
применяет их к существующим строкам через `update_child_table_values()`.
---
## 3. Расчётный движок (клиент)
Здесь самое тонкое место. Файл содержит **два поколения кода расчёта**, и работает только второе.
### Старый код (строки 269919)
Ручные функции: `calculate_509_1`, `calculate_509_2`, `calculate_binalar_totals`,
`calculate_elave_*_totals`, `calculate_vergi_hesab_3cu_totals`. Они напрямую считали формулы:
- `509.1 = ((501.1 + 504.1) / 2) * 1% + 505.1 * 1%` (полный год);
- `509.2 = ((503.1 + 504.1) / 24 * 507) * 1% + 505.1 * 1%` (неполный год, 507 = число месяцев).
### Новый код (строки 10421264) — **активный**
В конце файла все шесть `calculate_*` **переопределены заново** как тонкие делегаторы:
```js
function calculate_509_1(frm) { gns_property_recalc(frm); }
// ... и т.д.
```
Это работает за счёт **hoisting функций в JS: последнее объявление побеждает**. То есть все
старые триггеры (обработчики дочерних таблиц на строках 9241040) теперь незаметно вызывают
новый единый движок `gns_property_recalc()`. Старые тела функций мертвы, но оставлены.
**Как считает новый движок** (`_gns_property_recalc_now`):
1. `ptgov_sum()` суммирует строки `elave_1` → объект `a1` (приложение 1, освобождённые),
`elave_2``a2` (облагаемые). При суммировании Əlavə 2 исключаются «итоговые» строки —
родитель «1. Binalar» и «CƏMİ» (`ptgov_is_excluded_e2`).
2. `full = !il_erzinde`, `months` берётся из строки 507.
3. `ptgov_calc_byArticle()` строит карту `{номер_статьи: значение}` — те же формулы, что и в
старом коде, но в одном месте, с округлением half-up до 2 знаков (`ptgov_round2`). Облагается
**только** Əlavə 2; 511 (к уплате) = 509.
4. Результаты разносятся по всем выходным cədvəl-таблицам: по номеру статьи (`ptgov_article`
парсит «509.1» из метки `göstəricilər`) в поле `manatla`; ставка 506 → `faizlə = 1`.
**Защита от рекурсии:** флаг `frm.__ptgov_writing` ставится перед записью значений, чтобы наши же
`set_value` не зациклили обработчики `manatla`. Плюс debounce 250 мс (`_ptgov_timer`).
### Про Node-мост (`compute_via_gns` + bundle)
По задумке предполагалось гонять **родной калькулятор кабинета** (`useCalculation`, извлечённый
в `gns_property_calc.bundle.js`) офлайн через Node-subprocess. Сейчас же, как явно сказано в
комментарии (строки 11031107), **формулы переписаны нативно в JS**, а bundle и
`compute_via_gns` остаются в репо как **эталонный источник**, из которого формулы выведены и
сверены. То есть в рантайме Node не вызывается — всё считается в браузере.
---
## 4. Условная логика отображения
`apply_conditional_table_logic()` показывает/прячет таблицы под режим:
- `tam_il` → видна `vergi_hesab_1ci` (501); строки 509.1/511.1 в 5-й/6-й таблицах;
- `il_erzinde` → видны `vergi_hesab_1ci_1` (503) и `vergi_hesab_4cu_1` (месяцы); строки
509.2/511.2; колонки 501.x скрываются (`toggle_field_readonly`).
Строки 5-й/6-й таблиц **полностью пересоздаются** под режим (`filter_vergi_hesab_5ci_rows`),
при этом старые значения сохраняются в `window.property_tax_saved_values` и восстанавливаются.
`calculate_months_for_vergi_hesab_4cu_1()` (Python) находит самый ранний Asset по
`available_for_use_date` в году и считает число месяцев до конца года для формулы 509.2.
---
## 5. Валидация
`validate_minimum_asset_value()` — только в режиме `il_erzinde`: стоимость ОС в колонках
503.1/503.2 не может быть **< 500 манат** (но > 0). При нарушении:
- блокирует сохранение (`frm.validated = false`);
- подсвечивает ячейки красным (`highlight_invalid_cells`);
- показывает HTML-предупреждение и ставит 🔴 в заголовок таба (`update_tab_title`).
---
## 6. Шаблонные строки и кнопки
- `ptgov_ensure_rows()` — при `refresh` заполняет пустые дочерние таблицы каноническими наборами
строк (`PTGOV_E1_ROWS` — 23 статьи освобождения, `PTGOV_E2_ROWS` — 12 категорий, cədvəl-таблицы
— из `options` поля `göstəricilər`).
- `ptgov_set_readonly()` — делает вычисляемые колонки (`manatla`, `faizlə`) read-only.
- Кнопки: **«Şablon sətirləri yarat»** (создать шаблонные строки), **«ГНС ilə hesabla»**
(пересчитать вручную), **«Export XML»** (через `xml_mapping_tool.generate_xml`).
---
## Сводный поток работы
```
Пользователь: выбирает год + режим (Tam İl / İl Ərzində)
JS reload_property_tax_data → Python populate_property_tax_tables
tabAsset → агрегация (ERPNext balance-логика) → карты {строка: значения}
JS раскладывает по Əlavə 1 / Əlavə 2
любое изменение строки → gns_property_recalc (debounce 250ms)
ptgov_sum → ptgov_calc_byArticle → разнос по cədvəl (501→511)
валидация (min 500 ман.) → Export XML → подача в E-Taxes
```
---
## Главное при правках этого файла
1. Активен только **второй** блок расчёта (делегаторы внизу). Старые `calculate_509_*`
мёртвый код, переопределён hoisting'ом. Правь `ptgov_calc_byArticle`, а не их.
2. Сопоставление Asset ↔ строки таблиц — **нечёткое по подстроке**, легко ломается при
изменении меток строк.
3. Поля имеют азербайджанские имена с диакритикой (`göstəricilər`, `manatla`) — копируй точно.
4. Node-мост (`compute_via_gns`) сейчас не в рантайме, а как эталон формул.

View File

@ -1097,64 +1097,76 @@ function gns_property_recalc(frm) {
_ptgov_timer = setTimeout(function () { _gns_property_recalc_now(frm); }, 250); _ptgov_timer = setTimeout(function () { _gns_property_recalc_now(frm); }, 250);
} }
// half-up rounding to 2 decimals (matches the cabinet's decimal rounding)
function ptgov_round2(x) { return Math.round((flt(x) + Number.EPSILON) * 100) / 100; }
// LOCAL implementation of the property-tax formulas. The formulas/logic were extracted &
// verified from the GNS cabinet (new.e-taxes.gov.az) — see .etaxes_notes/scratch/calc_extract.
// Only the +,-,*,/ run here (locally, offline). The cabinet is the SOURCE of the formulas,
// not a runtime dependency; compute_via_gns + the extracted bundle stay in the repo as the
// reference from which these were derived.
// a1 = appendix1 (taxFree) totals, a2 = appendix2 (taxable) totals, full = Tam İl
function ptgov_calc_byArticle(a1, a2, full, months) {
const RATE = 0.01; // 506 taxDegree = 1% for legal entities
const by = {};
if (full) {
// 501 prePeriod (beginning of year): total = taxable + tax-exempt
by['501.1'] = a2.periodStart; // taxable residual
by['501.2'] = a1.periodStart; // tax-exempt residual
by['501'] = a2.periodStart + a1.periodStart;
} else {
// 503 createDate (newly established during the year)
by['503.1'] = a2.onCreate;
by['503.2'] = a1.created;
by['503'] = a2.onCreate + a1.created;
}
// 504 endYear
by['504.1'] = a2.periodEnd;
by['504.2'] = a1.endYear;
by['504'] = a2.periodEnd + a1.endYear;
// 505 excess-insured
by['505.1'] = a2.excess;
by['505.2'] = a1.over;
by['505'] = a2.excess + a1.over;
// 509 calculated tax — only taxable (appendix2) is taxed; 511 shouldBePaid mirrors it
let tax;
if (full) {
tax = ptgov_round2((a2.periodStart + a2.periodEnd) / 2 * RATE + a2.excess * RATE);
by['509.1'] = tax; by['511.1'] = tax;
} else {
tax = ptgov_round2((a2.onCreate + a2.periodEnd) / 24 * flt(months) * RATE + a2.excess * RATE);
by['509.2'] = tax; by['511.2'] = tax;
}
return by;
}
function _gns_property_recalc_now(frm) { function _gns_property_recalc_now(frm) {
const e1 = ptgov_sum(frm.doc.elave_1, PTGOV_E1); const a1 = ptgov_sum(frm.doc.elave_1, PTGOV_E1);
const e2 = ptgov_sum(frm.doc.elave_2, PTGOV_E2, ptgov_is_excluded_e2); const a2 = ptgov_sum(frm.doc.elave_2, PTGOV_E2, ptgov_is_excluded_e2);
const full = !frm.doc.il_erzinde;
const activity = frm.doc.il_erzinde ? 'OBJECT_ESTABLISHED' : 'OBJECT_FULL_ACTIVITY'; let months = 0;
// months (507): read from the 4cu cedvel 1 (ayla) row, used only for OBJECT_ESTABLISHED
let months = null;
(frm.doc.vergi_hesab_4cu_1 || []).forEach(function (r) { (frm.doc.vergi_hesab_4cu_1 || []).forEach(function (r) {
if (ptgov_article(r.göstəricilər) === '507') months = flt(r.ayla); if (ptgov_article(r.göstəricilər) === '507') months = flt(r.ayla);
}); });
const byArt = ptgov_calc_byArticle(a1, a2, full, months);
const payload = { frm.__ptgov_writing = true; // guard: our set_value writes retrigger manatla handlers
objectActivity: activity, try {
calculation: { PTGOV_OUT_TABLES.forEach(function (tbl) {
monthCountFromStartPeriod: activity === 'OBJECT_ESTABLISHED' ? months : null, (frm.doc[tbl] || []).forEach(function (row) {
monthCountToLiquidation: null, const art = ptgov_article(row.göstəricilər);
}, frappe.model.set_value(row.doctype, row.name, 'manatla', flt(byArt[art] || 0));
appendix1: { });
periodStartTotal: e1.periodStart, createdTotal: e1.created, });
removedTotal: e1.removed, endYearTotal: e1.endYear, overTotal: e1.over, (frm.doc.vergi_hesab_4cu || []).forEach(function (row) {
}, if (ptgov_article(row.göstəricilər) === '506') {
appendix2: { frappe.model.set_value(row.doctype, row.name, 'faizlə', 1);
periodStartTotal: e2.periodStart, onCreateDateTotal: e2.onCreate,
onCancelDateTotal: e2.onCancel, periodEndTotal: e2.periodEnd,
excessInsuredTotal: e2.excess,
},
};
frappe.call({
method: 'taxes_az.taxes_az.doctype.property_tax_gov.property_tax_gov.compute_via_gns',
args: { payload: JSON.stringify(payload) },
callback: function (r) {
if (!r.message || !r.message.byArticle) return;
const byArt = r.message.byArticle;
frm.__ptgov_writing = true; // guard: our set_value writes retrigger manatla handlers
try {
// write computed values into the calculation cedvel rows (manatla) by article number
PTGOV_OUT_TABLES.forEach(function (tbl) {
(frm.doc[tbl] || []).forEach(function (row) {
const art = ptgov_article(row.göstəricilər);
if (art != null && byArt[art] != null) {
frappe.model.set_value(row.doctype, row.name, 'manatla', flt(byArt[art]));
}
});
});
// tax rate (506) is the constant 1% for legal entities
(frm.doc.vergi_hesab_4cu || []).forEach(function (row) {
if (ptgov_article(row.göstəricilər) === '506') {
frappe.model.set_value(row.doctype, row.name, 'faizlə', 1);
}
});
} finally {
PTGOV_OUT_TABLES.concat(['vergi_hesab_4cu']).forEach(function (t) { frm.refresh_field(t); });
setTimeout(function () { frm.__ptgov_writing = false; }, 0); // release after sync triggers flush
} }
}, });
}); } finally {
PTGOV_OUT_TABLES.concat(['vergi_hesab_4cu']).forEach(function (t) { frm.refresh_field(t); });
setTimeout(function () { frm.__ptgov_writing = false; }, 0); // release after sync triggers flush
}
} }
// ---- delegators: override the old hand-coded calc (hoisting: these win) ---- // ---- delegators: override the old hand-coded calc (hoisting: these win) ----
@ -1165,41 +1177,85 @@ function calculate_vergi_hesab_3cu_totals(frm) { gns_property_recalc(frm); }
function calculate_509_1(frm) { gns_property_recalc(frm); } function calculate_509_1(frm) { gns_property_recalc(frm); }
function calculate_509_2(frm) { gns_property_recalc(frm); } function calculate_509_2(frm) { gns_property_recalc(frm); }
// ---- template rows: create one row per Select option for empty tables ---- // ---- template rows: identical to the original doctype's Default Client Scripts ----
// (the form shipped without default rows for elave_1/2 and most cedvel tables; // elave_1/elave_2 use the exact curated row sets; cedvel tables use their göstərici options.
// autofill matches asset data to these rows and the calc writes results into them) const PTGOV_E1_ROWS = [
const PTGOV_ROW_TEMPLATES = [ "Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
['elave_1', 'azadolmasəbəbi'], "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
['elave_2', 'vergiyəcəlbolunanəmlaklarınkateqoriyası'], "Vergi Məcəlləsinin 199.4.2-ci maddəsinə əsasən:",
['vergi_hesab_1ci', 'göstəricilər'], "Vergi Məcəlləsinin 199.4.3-cü maddəsinə əsasən:",
['vergi_hesab_1ci_1', 'göstəricilər'], "Vergi Məcəlləsinin 199.4.4-cü maddəsinə əsasən:",
['vergi_hesab_2ci', 'göstəricilər'], "Vergi Məcəlləsinin 199.4.1-1-ci maddəsinə əsasən:",
['vergi_hesab_3cu', 'göstəricilər'], "Vergi Məcəlləsinin 199.5-ci maddəsinə əsasən:",
['vergi_hesab_4cu', 'göstəricilər'], "Vergi Məcəlləsinin 199.7-ci maddəsinə əsasən:",
['vergi_hesab_4cu_1', 'göstəricilər'], "Vergi Məcəlləsinin 199.8-ci maddəsinə əsasən:",
['vergi_hesab_5ci', 'göstəricilər'], "Vergi Məcəlləsinin 199.9-cu maddəsinə əsasən:",
['vergi_hesab_6ci', 'göstəricilər'], "Vergi Məcəlləsinin 199.11-cu maddəsinə əsasən:",
"Vergi Məcəlləsinin 199.14-cu maddəsinə əsasən:",
"Vergi Məcəlləsinin 199.15-cu maddəsinə əsasən:",
"Vergi Məcəlləsinin 199.16-cı maddəsinə əsasən:",
"Vergi Məcəlləsinin 199.17-ci maddəsinə əsasən:",
"Vergi Məcəlləsinin 199.18-ci maddəsinə əsasən:",
"Vergi Məcəlləsinin 199.19-cu maddəsinə əsasən:",
"Vergi Məcəlləsinin 199.20-ci maddəsinə əsasən:",
"Vergi Məcəlləsinin 227.1-ci maddəsinə əsasən:",
"Qanunla təsdiq olunmuş hasilatın pay bölgüsü haqqında, əsas boru kəməri haqqında və digər bu qəbildən olan sazişlərdə və ya qanunlarda, o cümlədən neft və qaz haqqında, ixrac məqsədli neft-qaz fəaliyyəti üzrə:",
"Xüsusi iqtisadi zonalar haqqında qanunlarda nəzərdə tutulan əmlaklar üzrə:",
"Azərbaycan Respublikasının tərəfdar çıxdığı beynəlxalq müqavilələr üzrə:",
"Vergidən azad olunan əmlakın CƏMİ dəyəri:",
]; ];
const PTGOV_E2_ROWS = [
"1. Binalar, tikililər və qurğular",
"1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
"1.2 Qeyri-yaşayış binaları (sahələri)",
"1.3 Əmlak kompleksi kimi müəssisələr",
"1.4 Qurğular",
"1.5 Mənzillər",
"1.6 Fərdi yaşayış və bağ evləri",
"2 Maşınlar və avadanlıqlar",
"3 Yüksək texnologiyalar məhsulu olan hesablama texnikası",
"4 Nəqliyyat vasitələri",
"5 Digər əsas vəsaitlər",
"Vergiyə cəlb olunan əmlakın CƏMİ dəyəri:",
];
const PTGOV_CEDVEL_TABLES = ['vergi_hesab_1ci', 'vergi_hesab_1ci_1', 'vergi_hesab_2ci', 'vergi_hesab_3cu',
'vergi_hesab_4cu', 'vergi_hesab_4cu_1', 'vergi_hesab_5ci', 'vergi_hesab_6ci'];
function ptgov_ensure_rows(frm) { function ptgov_ensure_rows(frm) {
let added = false; let added = false;
PTGOV_ROW_TEMPLATES.forEach(function (pair) { function fill(tbl, fld, vals) {
const tbl = pair[0], fld = pair[1]; if ((frm.doc[tbl] || []).length) return; // already has rows -> skip
if ((frm.doc[tbl] || []).length) return; // already has rows -> skip vals.forEach(function (v) { frm.add_child(tbl)[fld] = v; });
if (vals.length) { frm.refresh_field(tbl); added = true; }
}
fill('elave_1', 'azadolmasəbəbi', PTGOV_E1_ROWS);
fill('elave_2', 'vergiyəcəlbolunanəmlaklarınkateqoriyası', PTGOV_E2_ROWS);
PTGOV_CEDVEL_TABLES.forEach(function (tbl) {
const field = frm.get_field(tbl); const field = frm.get_field(tbl);
if (!field) return; if (!field) return;
const childDt = field.df.options; const df = frappe.meta.get_docfield(field.df.options, 'göstəricilər');
const df = frappe.meta.get_docfield(childDt, fld);
const opts = ((df && df.options) || '').split('\n').filter(function (x) { return x.trim(); }); const opts = ((df && df.options) || '').split('\n').filter(function (x) { return x.trim(); });
opts.forEach(function (opt) { frm.add_child(tbl)[fld] = opt; }); fill(tbl, 'göstəricilər', opts);
if (opts.length) { frm.refresh_field(tbl); added = true; }
}); });
return added; return added;
} }
// make computed output columns read-only (the cedvel manatla/faizlə are calc results)
function ptgov_set_readonly(frm) {
const cols = [['vergi_hesab_1ci', 'manatla'], ['vergi_hesab_1ci_1', 'manatla'], ['vergi_hesab_2ci', 'manatla'],
['vergi_hesab_3cu', 'manatla'], ['vergi_hesab_5ci', 'manatla'], ['vergi_hesab_6ci', 'manatla'],
['vergi_hesab_4cu', 'faizlə']];
cols.forEach(function (pair) {
const f = frm.get_field(pair[0]);
if (f && f.grid) f.grid.update_docfield_property(pair[1], 'read_only', 1);
});
}
// manual recalc button (autofill runs automatically on İl / Tam İl / İl Ərzində change) // manual recalc button (autofill runs automatically on İl / Tam İl / İl Ərzində change)
frappe.ui.form.on('Property Tax Gov', { frappe.ui.form.on('Property Tax Gov', {
refresh: function (frm) { refresh: function (frm) {
ptgov_ensure_rows(frm); // populate empty child tables with their template rows ptgov_ensure_rows(frm); // populate empty child tables with their template rows
ptgov_set_readonly(frm); // lock computed cedvel columns
frm.add_custom_button('Şablon sətirləri yarat', function () { frm.add_custom_button('Şablon sətirləri yarat', function () {
if (ptgov_ensure_rows(frm)) frappe.show_alert({ message: 'Şablon sətirləri yaradıldı', indicator: 'green' }); if (ptgov_ensure_rows(frm)) frappe.show_alert({ message: 'Şablon sətirləri yaradıldı', indicator: 'green' });
}); });