feat(property-tax-return-ai-fill): autofill elavə 1/2 + 507 from Asset accounting
Adds two parallel DocType families that don't touch the existing `Property tax return` family: - Property Tax Return AI (parent + 10 children): cabinet-verified port of the property-tax formulas (501/503/504 mirrors, 506=1, 509.1/509.2, 511.x) with parity 3/3 vs. the original. - Property Tax Return AI Fill (parent + 10 children): same formulas plus auto-fill from ERPNext Asset records. On form open the seeded grid is populated from the single Company's Assets, routed by `taxable_asset_type` (→ elavə 2 rows 1.1–1.6, 2–5) and `tax_exempt_tax_article` (→ elavə 1 rows by article-code regex). Residuals at year boundaries come from Depreciation Schedule; Company.date_of_establishment drives 507 ayla when il_erzinde=1. Coverage: 16/16 smoke (seeding + formulas + 11 autofill scenarios) and 3/3 parity vs. original Property tax return pass. Also adds .etaxes_notes/cabinet_internals.md (sanitised cabinet probe notes) and gitignores .etaxes_notes/scratch/ which holds live JWTs and HAR-like dumps. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
d90e56eb29
commit
7f772fcb34
|
|
@ -0,0 +1,533 @@
|
|||
# e-taxes.gov.az Cabinet Internals — Operational Notes
|
||||
|
||||
Persistent notes for future Claude sessions about the ГНС cabinet (new.e-taxes.gov.az).
|
||||
Add to this file when you learn something non-obvious. Keep entries dated.
|
||||
|
||||
---
|
||||
|
||||
## High-level architecture (verified 2026-05-26)
|
||||
|
||||
Two separate SPAs under one domain:
|
||||
|
||||
| Path prefix | Mount point | Purpose | webpackJsonp name |
|
||||
|---|---|---|---|
|
||||
| `/etaxes/` | `<div id="root">` | Public services (lookup taxpayer, debt query, etc.) | `webpackJsonpetaxes` |
|
||||
| `/eportal/` | `<div id="app">` | **Cabinet for authenticated users — declarations live here** | (varies) |
|
||||
|
||||
Both: React + Redux Toolkit + RTK Query, CRA-ish bundle layout (`static/js/N.HASH.chunk.js`).
|
||||
Source maps NOT exposed.
|
||||
|
||||
**Pitfall:** the cabinet's mount point is `#app`, NOT `#root`. A snippet that walks fibers from `#root` will return `hasRoot: false`.
|
||||
|
||||
---
|
||||
|
||||
## Auth — how the cabinet checks login
|
||||
|
||||
REST (Python `requests`) — **simple**: send `x-authorization: Bearer <main_token>` header. Works for every documented endpoint. No cookies needed, no session.
|
||||
|
||||
Browser SPA — **NOT** simple. Just injecting `x-authorization` into outgoing requests via Playwright's `route` is **NOT enough**. The SPA has a route guard that checks **Redux state** for the auth token. If Redux is empty → redirect to `/eportal/login`, regardless of what's in HTTP headers.
|
||||
|
||||
Verified storage usage (grepped eportal bundle):
|
||||
- `localStorage` — only `surveyModal` (popup tracking) and `aztax-lang`. **No auth token**.
|
||||
- `sessionStorage` — empty after fresh page load.
|
||||
- Cookies — **none set** by any cabinet endpoint (verified `Set-Cookie` headers: empty on all responses including `/chooseTaxpayer`).
|
||||
|
||||
**Implication:** to drive the cabinet headlessly you must EITHER:
|
||||
1. Login through the SPA UI (PIN1 push to phone), OR
|
||||
2. Inject the token into the Redux store via `page.evaluate()` after the SPA mounts. Action type is dynamically constructed in Redux Toolkit — grep `*-api`, `Slice/` patterns to find it. Untested as of 2026-05-26.
|
||||
|
||||
Easier in practice: UI login + Playwright's `storage_state` to persist cookies+localStorage between runs (note: storage_state is mostly empty here, so reuse value is limited — the actual auth lives in in-memory Redux, which Playwright cannot serialize).
|
||||
|
||||
### REST ASAN flow (works without browser)
|
||||
|
||||
```
|
||||
POST /api/po/auth/public/v1/asanImza/start body={"phone": "+994...", "userId": "..."}
|
||||
→ returns 200 with bearer_token in x-authorization response header
|
||||
→ response body: {"verificationCode": "1234"} (shown on user's phone screen)
|
||||
|
||||
POLL GET /api/po/auth/public/v1/asanImza/status headers={x-authorization: Bearer <bearer>}
|
||||
→ returns {"successful": false, "error": null} while waiting
|
||||
→ when user approves PIN1, eventually returns successful: true (or just becomes silently true)
|
||||
→ BETTER: poll /asanImza/certificates instead — when it returns 200 with non-empty list, you're authed
|
||||
|
||||
GET /api/po/auth/public/v1/asanImza/certificates
|
||||
→ returns [{taxpayerType: "individual"|"legal", legalInfo|individualInfo, ...}, ...]
|
||||
|
||||
POST /api/po/auth/public/v1/asanImza/chooseTaxpayer body={"ownerType": "legal", "legalTin": "VOEN"}
|
||||
→ returns 200 with main_token in x-authorization response header
|
||||
→ main_token JWT validity: 10 min (exp = iat + 600s)
|
||||
```
|
||||
|
||||
**Pitfall — polling format:** my first explorer.py polled `data.get("status")` but the actual field is `data.get("successful")` (bool). Wrong field name → silent infinite loop. Now using "did certificates endpoint return non-empty data?" as success signal — more robust.
|
||||
|
||||
**Pitfall — ГНС polling latency:** user approves PIN1 quickly, but ГНС server takes several seconds (5-30s observed) to flip `successful: true`. Use timeout >= 90s with 2s interval.
|
||||
|
||||
### Renewing tokens
|
||||
|
||||
`RENEW_URL = "https://new.e-taxes.gov.az/api/po/auth/public/v1/renew"` — used by `taxes_az/auth.py:renew_token`. Returns 401 if token expired without recent activity. There's an `ACTIVITY_TIMEOUT_MINUTES = 10` gate in the project code.
|
||||
|
||||
---
|
||||
|
||||
## Login form (UI flow, /eportal/az/login)
|
||||
|
||||
Initial page: `/eportal/az/login` — shows 5 method tiles. Click "Asan İmza" link (`<a>` with text "Asan İmza", class `login-item-link`) → SPA navigates to `/eportal/az/login/asan` with form.
|
||||
|
||||
ASAN form selectors (verified 2026-05-26):
|
||||
| Selector | Field | Notes |
|
||||
|---|---|---|
|
||||
| `#phone` | Phone (type=text) | placeholder `00 000 00 00` — enter LOCAL format only (e.g. `558239674`), **without** +994 |
|
||||
| `#userId` | User ID (type=password) | The Asan İmza user ID, e.g. `171082` |
|
||||
| `#loginPageSignInButton` | Submit (type=submit) | Text: `Daxil ol` |
|
||||
|
||||
Page also has a language `<select>` (`#rc_select_0` Ant Design) and footer link.
|
||||
|
||||
After submit, SPA shows verification code (matching what's on phone) and polls status. After PIN1 approval, SPA shows certificate picker (multi-cert users) — need to click the right taxpayer.
|
||||
|
||||
### Full UI login URL flow (verified 2026-05-26)
|
||||
|
||||
```
|
||||
1. /eportal/az/login (login methods tiles)
|
||||
↓ click "Asan İmza"
|
||||
2. /eportal/az/login/asan (phone + userId form)
|
||||
↓ fill + click #loginPageSignInButton
|
||||
3. /eportal/az/login/asan/otp (verification code shown, e.g. "4814"; SPA polls /asanImza/status)
|
||||
↓ user approves PIN1 on phone (~5-30s delay)
|
||||
4. /eportal/az/login/asan/certificates (cert picker — list of taxpayers)
|
||||
↓ click row matching desired entity (e.g. div:has-text("JEY SOFT"))
|
||||
5. /eportal/ (cabinet home — authenticated!)
|
||||
```
|
||||
|
||||
**Cert picker selector that works:** `div:has-text("JEY SOFT")` clicked the right row.
|
||||
|
||||
**Note on timing:** the SPA does NOT navigate URL when going from `/otp` to `/certificates` — it does the route change AFTER PIN approval. So a wait-loop should watch for `/certificates` OR `/eportal/$` URL, not just any URL change.
|
||||
|
||||
### Forced wizard redirect on declaration entry
|
||||
|
||||
After login, navigating directly to a wizard step like `/eportal/declaration/type/property/2025.1/appendix-1?reg-number=N` causes SPA to **force-redirect to `/confirmation`**. The wizard requires passing the confirmation step first. The goto throws a networkidle timeout (60s) before settling — catch and continue.
|
||||
|
||||
**Verified 2026-05-26:** wizard navigation has TWO different behaviors:
|
||||
- `page.goto('/calculation')` from `/confirmation` → SPA redirects back to `/confirmation`.
|
||||
- Clicking the "İrəli" (Next) button after picking required radio → SPA navigates one step forward.
|
||||
|
||||
So you **must walk through the wizard step-by-step via "İrəli" clicks**, not via URL.
|
||||
|
||||
### Next button text (verified 2026-05-26)
|
||||
|
||||
The "next step" button has text **`İrəli`** (= Forward). Click it with `button:has-text("İrəli")`. It's disabled until required inputs are filled (e.g. radio on confirmation step).
|
||||
|
||||
There's likely also a "Geri" button (= Back).
|
||||
|
||||
### Cabinet storage_state
|
||||
|
||||
After login, Playwright's `ctx.storage_state()` returns: `cookies=0, origins=1`. The `origins` entry has `localStorage` with only UX prefs (`aztax-lang`, `X-Request-Id`). **None of this is auth state.** Auth lives in in-memory Redux, which storage_state cannot serialize.
|
||||
|
||||
**Implication:** fresh `browser.new_context(storage_state=...)` does NOT restore auth. Re-login is required each browser session. Workaround: keep one browser context alive across operations, OR find Redux action types to inject token programmatically.
|
||||
|
||||
---
|
||||
|
||||
## Cabinet Redux store shape (declaration view)
|
||||
|
||||
When inside a declaration (e.g. Property Tax 2025.1), the store's top-level keys include:
|
||||
|
||||
**UI state slices:**
|
||||
- `declSlice` — wizard UI state. Key sub-fields:
|
||||
- `stepData` / `originalStepData` — array of 5 wizard steps (id, link, componentName, name, errorBlock)
|
||||
- `formData`, `headerData`, `tabData` — these are `false` until SPA mounts data-entry step
|
||||
- many `isLoadingOf...`, `isVisible...` booleans
|
||||
|
||||
- `subModuleCommunicationSlice` — wizard control (save/submit loading flags, violations list)
|
||||
|
||||
**RTK Query API slices** — actual server data lives here, keyed by `endpointName(args)`. For declarations:
|
||||
- `declaration-api.queries['regDeclarationInfo({"regNumber":"..."})']` — declaration metadata
|
||||
- `declaration-api.queries['getDeclarationBaseData({"regNumber":"..."})']` — **THE FORM JSON**, in `.data.baseJson` as a STRING (JSON.parse it)
|
||||
- `declaration-api.queries['getPrefillData({"regNumber":"..."})']` — prefilled values from previous declarations
|
||||
- `declaration-api.queries['getControlMessages({"maxCount":1000,"offset":0,"regNumber":"..."})']` — validation messages (server-side, separate from client-side calc)
|
||||
|
||||
Many other `*-api` slices exist (admin-api, hr-api, karg-api, ...) — irrelevant to declaration flow.
|
||||
|
||||
**Pitfall:** `declSlice.formData` is `false` (not an object) during navigation. The real form data sits in `declaration-api.queries[getDeclarationBaseData(...)].data.baseJson` — that's what gets PUT'd back to `/draft/base` on autosave.
|
||||
|
||||
### Snippet — find Redux store from a cabinet page
|
||||
|
||||
```javascript
|
||||
// Paste in DevTools Console after navigating to a cabinet page (e.g. declaration form)
|
||||
(function(){
|
||||
var app = document.getElementById('app');
|
||||
function walk(fiber, depth) {
|
||||
if (!fiber || depth > 500) return null;
|
||||
var p = fiber.memoizedProps;
|
||||
if (p && p.store && p.store.dispatch && p.store.getState) return p.store;
|
||||
return walk(fiber.child, depth + 1) || walk(fiber.sibling, depth + 1);
|
||||
}
|
||||
var props = Object.keys(app);
|
||||
var ck = props.find(function(k){ return k.indexOf('__reactContainer') === 0; });
|
||||
if (!ck) return console.log('No __reactContainer key');
|
||||
var store = walk(app[ck].stateNode.current, 0);
|
||||
window.__store = store;
|
||||
console.log('store keys:', Object.keys(store.getState()));
|
||||
})();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server-side computation — NONE
|
||||
|
||||
**Critical finding (verified 2026-05-26):** the cabinet server stores but DOES NOT CALCULATE. All formulas (sums, totals, tax computation) happen client-side in JS.
|
||||
|
||||
Evidence:
|
||||
- `PUT /api/po/declaration/public/v1/draft/base` response body: `{"saved":true}` (14 bytes), always.
|
||||
- `/control/messages` returns validation messages only (empty `{"hasMore":false,"messages":[]}` for a clean declaration).
|
||||
- `/push/sse/subscribe` is a heartbeat (`event:ping data:Check Life`) — **does NOT push computed values**.
|
||||
|
||||
**Implication for integration:** to replicate cabinet behavior, you either
|
||||
(a) re-implement formulas in your own code, OR
|
||||
(b) run the cabinet's JS bundle in a real or simulated browser to compute, OR
|
||||
(c) skip computation entirely and POST raw inputs to /draft/base (server will store them — but the user's declaration will be wrong).
|
||||
|
||||
---
|
||||
|
||||
## Property Tax Return (taxCode `_0500`, version 2025.1)
|
||||
|
||||
5 wizard steps:
|
||||
1. `confirmation` — landing page, period+activity type selection
|
||||
2. `calculation` — main tax calculation (computed)
|
||||
3. `appendix-1` — **non-taxable property** (`taxFree`), 23 rows of indicators `_1199` through `_1215`
|
||||
4. `appendix-2` — **taxable property** (`taxable`), 11 rows of indicators `_5001` through `_5012`
|
||||
5. `review` — final review/submit
|
||||
|
||||
### Appendix-1 step (verified 2026-05-26)
|
||||
|
||||
URL: `/eportal/declaration/type/property/2025.1/appendix-1`
|
||||
|
||||
DOM contains **120 input elements total, ~66 editable** (`type=text`, not disabled/readOnly). All are formatted as **`"0,00"` (Azerbaijani locale, comma as decimal separator)** with placeholder `"0,00"`. Likely Ant Design `InputNumber` components.
|
||||
|
||||
Structure approx: 23 rows × 5 columns (`periodStartAmount, createdAmount, removedAmount, endYearAmount, overAmount`) = 115 inputs + ~5 extras in header/footer.
|
||||
|
||||
Row labels reference Tax Code (`Vergi Məcəlləsi`) articles: e.g. row 1 = "Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən, azaldılmalı əmlak vergisinin məbləği" (= property tax to be reduced per Tax Code article 199.1). These map to indicator codes `_1199, _1200, _1201, ...` from `appendix1.taxFree[]` array.
|
||||
|
||||
**Pitfall — filling inputs:** raw DOM manipulation (`Object.getOwnPropertyDescriptor.set` + `dispatchEvent('input')`) **does NOT** propagate to Redux store. Ant Design `InputNumber` ignores synthetic `input` events. Use Playwright's `locator.fill()` or `page.keyboard.type()` to simulate real keyboard — those work because React's event system processes them as real user input.
|
||||
|
||||
### Confirmation step (verified 2026-05-26)
|
||||
|
||||
4 visible inputs:
|
||||
| input | type | semantics |
|
||||
|---|---|---|
|
||||
| `[role=checkbox]` | disabled checkbox | "Bəyan ediləcək məlumatım yoxdur" — declare nothing (opt-out) |
|
||||
| `[value="OBJECT_ESTABLISHED"]` | radio | "Müəssisə il ərzində yarandığı halda" — company established mid-year |
|
||||
| `[value="OBJECT_FULL_ACTIVITY"]` | radio | "Müəssisə il ərzində tam fəaliyyət göstərdiyi halda" — full-year operation |
|
||||
| `input[type=text]` | disabled | (header area, non-input) |
|
||||
|
||||
Selecting one radio sets `baseJson.objectActivity` to that value. Then there's an **`İrəli`** button (NOT "Davam et" or "Növbəti") to advance to the next wizard step (`calculation`).
|
||||
|
||||
### Calculation step (verified 2026-05-26)
|
||||
|
||||
URL: `/eportal/declaration/type/property/2025.1/calculation`
|
||||
|
||||
Shows COMPUTED summary across whole declaration. All fields read-only. Sections (from JEY SOFT test data, prefilled values shown):
|
||||
|
||||
**Hesabat ilinin əvvəlinə** (at beginning of report year):
|
||||
- 501 — Əmlakın ümumi qalıq dəyəri (total residual value of property): 8 345.70
|
||||
- 501.1 — Vergiyə cəlb olunan əmlakın qalıq dəyəri (residual value of taxable property): 0.00
|
||||
- 501.1.1 — Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri: 0.00
|
||||
- 501.2 — Vergiyə cəlb edilən əmlakın qalıq dəyəri: 8 345.70
|
||||
|
||||
**Hesabat ilinin sonuna** (at end of report year):
|
||||
- 504 — total residual value: 5 007.42
|
||||
- 504.1 — taxable residual: 0.00
|
||||
- 504.1.1 — non-commercial activity property: 0.00
|
||||
|
||||
**Artıq ödəmə** (overpayment):
|
||||
- 505 / 505.1 / 505.2 — overpayment-related, 0.00
|
||||
|
||||
**Tax rate:**
|
||||
- 506 — Vergi dərəcəsi: **1.00** — confirms 1% tax rate for legal entities
|
||||
|
||||
**Calculated tax:**
|
||||
- 509.1 — Vergi ödəyicisi il ərzində fəaliyyət göstərdiyi halda: 0.00
|
||||
- 510 — Reduced by Tax Code article 199.21: 0.00
|
||||
|
||||
**Amount payable:**
|
||||
- 511.1 — Müəssisə il ərzində tam fəaliyyət göstərdiyi halda: 0.00
|
||||
|
||||
The sidebar shows wizard step icons. Click "İrəli" to advance to `appendix-1`.
|
||||
|
||||
**Pitfall:** I keep mixing up appendix-1 vs appendix-2. **Appendix-1 = taxFree (non-taxable). Appendix-2 = taxable.** taxFree first because it determines the "exempt portion" before main calculation.
|
||||
|
||||
URL pattern: `/eportal/declaration/type/property/2025.1/{step-name}?reg-number={N}`
|
||||
- e.g. `/eportal/declaration/type/property/2025.1/appendix-1?reg-number=2606170119282100`
|
||||
|
||||
JSON structure (from `getDeclarationBaseData.data.baseJson` parsed):
|
||||
```
|
||||
{
|
||||
taxCode: "_0500",
|
||||
version: "2025.1",
|
||||
common: {
|
||||
administrative: { departmentCode, term: { type: "YEAR", year, month } },
|
||||
payerInfo: { tin, payerType, taxPayerFullName, ... },
|
||||
declarationInfo: { mainActivityCode, declarationType, attachmentCount, ... }
|
||||
},
|
||||
objectActivity: "OBJECT_FULL_ACTIVITY",
|
||||
taxInformation: {
|
||||
mainTax: { calculatedTax, taxDegree, shouldBeReducedTax, ... },
|
||||
prePeriodCalculations: [{ indicator, amount }, ...],
|
||||
createDateCalculations: [...],
|
||||
liquidateCalculations: [...],
|
||||
endYearCalculations: [...],
|
||||
excessPrices: [...],
|
||||
calculatedTax: [...],
|
||||
shouldBePaidTax: [...],
|
||||
appendix1: {
|
||||
taxFree: [{ indicator, periodStartAmount, createdAmount, removedAmount, endYearAmount, overAmount }, ...],
|
||||
createdTotal, periodStartTotal, removedTotal, endYearTotal, overTotal
|
||||
},
|
||||
appendix2: {
|
||||
taxable: [{ indicator, periodStart, onCancelDate, onCreateDate, periodEnd, excessInsured }, ...],
|
||||
periodStartTotal, onCancelDateTotal, onCreateDateTotal, periodEndTotal, excessInsuredTotal
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### COMPLETE Property Tax formula map (verified 2026-05-26 via Playwright daemon mapping)
|
||||
|
||||
Daemon-based mapping: filled every editable cell on each step with unique values, captured PUT bodies, computed per-cell diffs. **All 96 editable cells covered** (66 on appendix-1 + 30 on appendix-2). 3 distinct formula patterns on appendix-1, 3 on appendix-2, all rows behave identically.
|
||||
|
||||
**Appendix-1 (taxFree, 23 indicator rows × 3 editable columns = 66 cells):**
|
||||
|
||||
| Input column (per row) | Auto-updates |
|
||||
|---|---|
|
||||
| `taxFree[*].periodStartAmount` | `appendix1.periodStartTotal`, `prePeriodCalculations[0].amount`, `prePeriodCalculations[2].amount` |
|
||||
| `taxFree[*].endYearAmount` | `appendix1.endYearTotal`, `endYearCalculations[0].amount`, `endYearCalculations[1].amount` |
|
||||
| `taxFree[*].overAmount` | `appendix1.overTotal`, `excessPrices[0].amount`, `excessPrices[1].amount` |
|
||||
|
||||
`appendix1.<col>Total` = SUM over all 23 `taxFree[*]` rows. Mirror coefficient = 1.000.
|
||||
|
||||
**Appendix-2 (taxable, 10 indicator rows × 3 editable columns = 30 cells):**
|
||||
|
||||
| Input column (per row) | Auto-updates |
|
||||
|---|---|
|
||||
| `taxable[*].periodStart` | `appendix2.periodStartTotal`, `prePeriodCalculations[0]`, `prePeriodCalculations[1]`, `calculatedTax[0]`, `mainTax.calculatedTax`, `shouldBePaidTax[0]` |
|
||||
| `taxable[*].periodEnd` | `appendix2.periodEndTotal`, `endYearCalculations[1]`, `endYearCalculations[2]`, `calculatedTax[0]`, `mainTax.calculatedTax`, `shouldBePaidTax[0]` |
|
||||
| `taxable[*].excessInsured` | `appendix2.excessInsuredTotal`, `excessPrices[1]`, `excessPrices[2]`, `calculatedTax[0]`, `mainTax.calculatedTax`, `shouldBePaidTax[0]` |
|
||||
|
||||
`appendix2.<col>Total` = SUM over all 10 `taxable[*]` rows.
|
||||
|
||||
**Combined aggregates (calculation step values):**
|
||||
|
||||
```
|
||||
prePeriodCalculations[0].amount = appendix1.periodStartTotal + appendix2.periodStartTotal
|
||||
prePeriodCalculations[1].amount = appendix2.periodStartTotal
|
||||
prePeriodCalculations[2].amount = appendix1.periodStartTotal
|
||||
endYearCalculations[0].amount = appendix1.endYearTotal
|
||||
endYearCalculations[1].amount = appendix1.endYearTotal + appendix2.periodEndTotal
|
||||
endYearCalculations[2].amount = appendix2.periodEndTotal
|
||||
excessPrices[0].amount = appendix1.overTotal
|
||||
excessPrices[1].amount = appendix1.overTotal + appendix2.excessInsuredTotal
|
||||
excessPrices[2].amount = appendix2.excessInsuredTotal
|
||||
```
|
||||
|
||||
**THE main tax formula (1% rate for legal entities, JEY SOFT is `payerType: "LEGAL"`):**
|
||||
|
||||
```
|
||||
calculatedTax[0].amount =
|
||||
mainTax.calculatedTax =
|
||||
shouldBePaidTax[0].amount =
|
||||
ROUND(
|
||||
(appendix2.periodStartTotal + appendix2.periodEndTotal) / 2 * 0.01
|
||||
+ appendix2.excessInsuredTotal * 0.01,
|
||||
2 # half-up to 2 decimals
|
||||
)
|
||||
```
|
||||
|
||||
Equivalent: `tax = ((avg of start+end residual values) + excess insured) × taxRate`, where `taxRate = mainTax.taxDegree / 100 = 1% for legal`.
|
||||
|
||||
Note: only `taxable[]` (appendix-2) contributes to calculated tax. `taxFree[]` (appendix-1) values feed into totals/mirrors but NOT into final tax.
|
||||
|
||||
### Taxable parent row structure (verified 2026-05-26)
|
||||
|
||||
`appendix2.taxable[]` has **11 entries**, but only **10 are user-fillable**:
|
||||
|
||||
| Index | Indicator | Role |
|
||||
|---|---|---|
|
||||
| 0 | `_5001` | **PARENT "1. Binalar, tikililər və qurğular"** — auto-sums from children |
|
||||
| 1-6 | `_5002, _5003, _5004, _5005, _5006, _5007` | Children 1.1–1.6 (sub-categories of "Binalar") |
|
||||
| 7 | `_5008` | Other taxable category |
|
||||
| 8 | `_5009` | Other taxable category |
|
||||
| 9 | `_5012` | Other taxable category |
|
||||
| 10 | `_5010` | Other taxable category |
|
||||
|
||||
**Auto-aggregation rule:** `_5001.{col} = Σ _5002..7.{col}` for each column (periodStart, periodEnd, excessInsured).
|
||||
|
||||
**Total sum rule:** `appendix2.{col}Total = Σ_{i!=0} taxable[i].{col}` — i.e. **exclude parent _5001 to avoid double-count**.
|
||||
|
||||
Verified: server values `periodEndTotal=1085`, `excessInsuredTotal=1155` correspond exactly to sums of indicators 1-10 (excluding `_5001`). Existing implementation `calculate_elave_2_totals` and `calculate_binalar_totals` in `property_tax_return.js` match this behavior **byte-for-byte**.
|
||||
|
||||
### Partial-year tax formula (509.2 / il_erzinde) — VERIFIED 2026-05-29
|
||||
|
||||
To bypass the modal that blocks switching radios on an existing draft, **create a NEW Dəqiqləşdirilmiş (clarifying) draft** for a closed year (2024 worked) — confirmation step then allows OBJECT_ESTABLISHED radio.
|
||||
|
||||
Calculation step in OBJECT_ESTABLISHED mode shows extra editable field:
|
||||
- **507**: `Müəssisə yarandığı aydan sonra ilin sonuna qədər olan ayların sayı` — integer field, default `0`, stores into `mainTax.monthCountFromStartPeriod`.
|
||||
|
||||
In OBJECT_ESTABLISHED mode the appendix-2 editable columns shift — **`onCreateDate, periodEnd, excessInsured`** become editable (column 1 → onCreateDate instead of `periodStart`). Same 3 editable / 2 disabled pattern.
|
||||
|
||||
Confirmed formula (verified with values onCreate=100, periodEnd=200, excessInsured=300, months=6):
|
||||
|
||||
```
|
||||
mainTax.calculatedTax = calculatedTax[0].amount = shouldBePaidTax[0].amount =
|
||||
ROUND(
|
||||
(appendix2.onCreateDateTotal + appendix2.periodEndTotal) / 24 * months * 0.01
|
||||
+ appendix2.excessInsuredTotal * 0.01,
|
||||
2
|
||||
)
|
||||
```
|
||||
|
||||
Verification: `((100 + 200) / 24 × 6) × 0.01 + (300 × 0.01) = 0.75 + 3.00 = 3.75` — matches `mainTax.calculatedTax = 3.75` byte-exact ✓
|
||||
|
||||
Same totals/mirror rules as full-year:
|
||||
- `createDateCalculations[0,1].amount` = `appendix2.onCreateDateTotal` (mirrors)
|
||||
- `endYearCalculations[0,1].amount` = `appendix2.periodEndTotal` (mirrors)
|
||||
- `excessPrices[0,1].amount` = `appendix2.excessInsuredTotal` (mirrors)
|
||||
- Parent _5001 auto-sums children 1.1–1.6
|
||||
- Totals exclude parent _5001 (no double-count)
|
||||
|
||||
**`calculate_509_2` in existing `property_tax_return.js` matches this byte-for-byte.** No discrepancies.
|
||||
|
||||
### Cross-check vs existing `taxes_az` implementation
|
||||
|
||||
Existing code lives in:
|
||||
- `formula_editor` app — 10 DSL formulas under name "Əmlak vergisi" computing calculation-step cells:
|
||||
- `vergi_hesab_1ci.row(1).manatla` = `table(elave_1, 23, periodStart) + table(elave_2, 12, periodStart)` = combined start
|
||||
- `vergi_hesab_1ci.row(2).manatla` = appendix-2 only
|
||||
- `vergi_hesab_1ci.row(4).manatla` = appendix-1 only
|
||||
- `vergi_hesab_2ci.row(1,2,4).manatla` = same pattern for end-of-year (combined / appendix-2 / appendix-1)
|
||||
- `vergi_hesab_1ci_1.row(1,2,4).manatla` = same pattern for newly-established case (503.1)
|
||||
- `vergi_hesab_4cu.row(1).faizlə` = `1` (tax rate 1% literal)
|
||||
- `taxes_az/.../property_tax_return/property_tax_return.js` — 6 hand-coded functions:
|
||||
- `calculate_binalar_totals` — sums sub-categories 1.1–1.6 into parent row "1. Binalar"
|
||||
- `calculate_elave_1_totals` — sums all rows into total row "Vergidən azad olunan əmlakın CƏMİ dəyəri:"
|
||||
- `calculate_elave_2_totals` — same, excluding `_5001` parent
|
||||
- `calculate_vergi_hesab_3cu_totals` — computes 505 totals from elave_1.overAmount + elave_2.excessInsured
|
||||
- `calculate_509_1` — full-year main tax formula (verified ✓ matches my finding exactly)
|
||||
- `calculate_509_2` — partial-year main tax formula (NOT verified by daemon — see above)
|
||||
|
||||
**Verdict: existing implementation is byte-for-byte correct for the full-year case.** Port to Python (Task C) means transcribing this proven logic; do not re-invent.
|
||||
|
||||
Observed coefficients confirm the formula (10 samples each, rounding to 2 decimals introduces ≤0.005% deviation):
|
||||
- periodStart → calculatedTax: 0.005094 (theoretical 0.005)
|
||||
- periodEnd → calculatedTax: 0.004969 (theoretical 0.005)
|
||||
- excessInsured → calculatedTax: 0.010000 (theoretical 0.010)
|
||||
|
||||
### Verified formulas (from Playwright fill experiment, 2026-05-26)
|
||||
|
||||
**Experiment:** filled 4 inputs on appendix-1 step, clicked "Qaralamada saxla" after each, captured PUT /draft/base bodies:
|
||||
- `taxFree[0].periodStartAmount` ← 100
|
||||
- `taxFree[0].endYearAmount` ← 200
|
||||
- `taxFree[0].overAmount` ← 400
|
||||
- `taxFree[1].periodStartAmount` ← 800
|
||||
|
||||
Confirmed formulas (13 fields auto-changed):
|
||||
|
||||
| Output field | Formula | Verified |
|
||||
|---|---|---|
|
||||
| `appendix1.periodStartTotal` | `baseline_periodStart + Σ taxFree[*].periodStartAmount` | 8345.7 + 100+800 = **9245.7** ✓ |
|
||||
| `appendix1.endYearTotal` | `baseline_endYear + Σ taxFree[*].endYearAmount` | 5007.42 + 200 = **5207.42** ✓ |
|
||||
| `appendix1.overTotal` | `Σ taxFree[*].overAmount` (no baseline) | 0 + 400 = **400** ✓ |
|
||||
| `prePeriodCalculations[0].amount` | = `appendix1.periodStartTotal` | mirror ✓ |
|
||||
| `prePeriodCalculations[2].amount` | = `appendix1.periodStartTotal` | mirror ✓ |
|
||||
| `endYearCalculations[0].amount` | = `appendix1.endYearTotal` | mirror ✓ |
|
||||
| `endYearCalculations[1].amount` | = `appendix1.endYearTotal` | mirror ✓ |
|
||||
| `excessPrices[0].amount` | = `appendix1.overTotal` | mirror ✓ |
|
||||
| `excessPrices[1].amount` | = `appendix1.overTotal` | mirror ✓ |
|
||||
|
||||
The "baseline" for `periodStartTotal` / `endYearTotal` is the prefilled total from previous year's declaration (from `getPrefillData` query). It is NOT a row in `taxFree[]` but a separate constant added on top.
|
||||
|
||||
### Form state location (verified 2026-05-26)
|
||||
|
||||
Critical observation: while user types into an input cell:
|
||||
- The visible DOM value updates ✓
|
||||
- **NO Redux action fires** (only `internal_probeSubscription` noise from RTK Query) ✓
|
||||
- The `getDeclarationBaseData` cache stays stale ✓
|
||||
- The autosave PUT does NOT fire on its own (no timer, or timer ≥ 30s) ✓
|
||||
|
||||
The form state lives in **react-hook-form** (or similar non-Redux state). It is serialized + sent to the server **only when the user clicks "Qaralamada saxla" (Save as Draft)** or the "İrəli" button (which presumably also saves).
|
||||
|
||||
**Implication for headless probing:** to capture computed values, you MUST click the "Qaralamada saxla" button after each fill. The PUT body of that save IS the computed JSON. No way to get formulas without this round-trip.
|
||||
|
||||
### Save-related Redux actions (verified)
|
||||
|
||||
Clicking "Qaralamada saxla" dispatches these actions in order:
|
||||
- `subModuleCommunicationSlice/setIsLoadingOfSaveDeclarationDraft` → `true`
|
||||
- `subModuleCommunicationSlice/setIsSuccessOfSaveDeclarationDraft` → `false`
|
||||
- `subModuleCommunicationSlice/setIsClickedDraftButton` → `false`
|
||||
- `declSlice/setLoadingPoDraftButton` → `true`
|
||||
- ...PUT /api/po/declaration/public/v1/draft/base...
|
||||
- `subModuleCommunicationSlice/setIsLoadingOfSaveDeclarationDraft` → `false`
|
||||
- `subModuleCommunicationSlice/setIsSuccessOfSaveDeclarationDraft` → `true`
|
||||
- `declSlice/setLoadingPoDraftButton` → `false`
|
||||
|
||||
### Old verified formulas (from HAR diff, earlier 2026-05-26)
|
||||
|
||||
These were from a separate HAR analysis (different user inputs). Same shape — confirms above:
|
||||
|
||||
| Computed field | Formula |
|
||||
|---|---|
|
||||
| `appendix1.periodStartTotal` | Σ `appendix1.taxFree[*].periodStartAmount` + baseline (prefilled) |
|
||||
| `appendix1.overTotal` | Σ `appendix1.taxFree[*].overAmount` |
|
||||
| `appendix1.endYearTotal` | Σ `appendix1.taxFree[*].endYearAmount` |
|
||||
| `endYearCalculations[0,1].amount` | mirror `appendix1.endYearTotal` |
|
||||
| `excessPrices[0,1].amount` | mirror `appendix1.overTotal` |
|
||||
| `prePeriodCalculations[0,2].amount` | mirror `appendix1.periodStartTotal` |
|
||||
| `prePeriodCalculations[1].amount` | = `appendix2.taxable[0].periodStart` (only first row?) |
|
||||
| `prePeriodCalculations[0].amount` | += `appendix2.taxable[0].periodStart` (cumulative with appendix1) |
|
||||
| `mainTax.calculatedTax` | mirror `calculatedTax[0].amount` |
|
||||
| `shouldBePaidTax[0].amount` | mirror `calculatedTax[0].amount` |
|
||||
| `calculatedTax[0].amount` | function of `appendix2.periodStartTotal × taxDegree` (formula not fully nailed — need more HAR data) |
|
||||
|
||||
Most formulas are trivial (SUM or mirror). One non-trivial: `calculatedTax`. Likely `taxable_base × 1%` (Azerbaijan property tax rate for legal entities) but factor varies — needs more diff samples.
|
||||
|
||||
---
|
||||
|
||||
## Playwright on this system (Ubuntu 24.04 frappe-bench)
|
||||
|
||||
System layout:
|
||||
- Python 3.14 at `/usr/local/bin/python3.14` (not the default `/usr/bin/python3` which is 3.12)
|
||||
- `playwright` pip pkg installed for `frappe` user at `/home/frappe/.local/lib/python3.14/site-packages`
|
||||
- Chromium binary installed via `playwright install chromium`
|
||||
- System deps (libatk, libnss3, etc.) — installed once via:
|
||||
```
|
||||
sudo PYTHONPATH=/home/frappe/.local/lib/python3.14/site-packages \
|
||||
/usr/local/bin/python3.14 -m playwright install-deps chromium
|
||||
```
|
||||
(the PYTHONPATH is needed because sudo elevates to root who doesn't have playwright; we point root's python to the user-pip location)
|
||||
|
||||
Run scripts with: `/usr/local/bin/python3.14 script.py`. NOT `python3` — wrong version.
|
||||
|
||||
**Pitfall:** `requests` was also missing from python3.14 initially. Install: `/usr/local/bin/python3.14 -m pip install requests`.
|
||||
|
||||
**Pitfall — networkidle is not enough:** e-taxes SPAs render in stages. `await page.goto(url, wait_until="networkidle")` succeeds but the form may not be in DOM yet. Add `await page.wait_for_timeout(2000-3000)` after navigation, or `wait_for_selector` for a known element.
|
||||
|
||||
---
|
||||
|
||||
## Useful scratch dir
|
||||
|
||||
`/tmp/etaxes_explorer/` contains:
|
||||
- `main_token.txt` — cached JWT main_token (10 min validity)
|
||||
- `explorer.py` — full ASAN+Playwright explorer (work-in-progress)
|
||||
- `probe_*.py` — small one-off probes for understanding DOM/auth
|
||||
- `appendix1.png`, `login_page.png`, `login_step{1,2}.png` — screenshots from runs
|
||||
- `appendix1.html`, `login_page.html`, `login_step2.html` — captured DOM
|
||||
- `inputs.json`, `login_elements.json` — extracted interactive elements
|
||||
- `store_snapshot.json` — Redux store dump
|
||||
|
||||
Don't commit anything from there — it has live JWTs.
|
||||
|
||||
---
|
||||
|
||||
## Open questions / TODO
|
||||
|
||||
- [ ] Where exactly is the `calculatedTax` formula (need more HAR samples or running form in browser to verify)
|
||||
- [ ] Does the `/register` endpoint require browser-side signing (XADES/PKCS7) or accept REST POST?
|
||||
- [ ] Are formulas identical across declaration versions (2024.1 vs 2025.1)?
|
||||
- [ ] Other declaration types (VAT, Income Tax, etc.) — same pattern?
|
||||
- [ ] Can we inject Redux state to skip UI login? (action type unknown so far)
|
||||
|
|
@ -5,3 +5,7 @@
|
|||
tags
|
||||
node_modules
|
||||
__pycache__
|
||||
|
||||
# Local e-taxes cabinet probes — live JWTs, Redux dumps, screenshots with real
|
||||
# taxpayer data. cabinet_internals.md (sanitised notes) is OK to commit.
|
||||
.etaxes_notes/scratch/
|
||||
|
|
@ -0,0 +1,555 @@
|
|||
"""Numeric parity harness: PropertyTaxReturnAI vs. the original Property tax return.
|
||||
|
||||
For each scenario we:
|
||||
1. Build a `Property tax return` doc, seed its 10 child tables exactly the way
|
||||
the original `Default*` Client Scripts do, fill the numeric cells the user
|
||||
would have typed, then run a faithful Python port of the original calc
|
||||
pipeline (property_tax_return.js + formula_editor DSL `table(...)`).
|
||||
2. Build a `Property Tax Return AI` doc with the same inputs, run validate.
|
||||
3. Compare every computed cell of every child table and print a diff.
|
||||
|
||||
Run via:
|
||||
bench --site frontend execute \
|
||||
taxes_az.taxes_az.doctype.property_tax_return_ai.parity_check.run
|
||||
"""
|
||||
import frappe
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Faithful Python port of the original calc pipeline.
|
||||
# Sources:
|
||||
# • taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.js
|
||||
# calculate_binalar_totals, calculate_elave_1_totals, calculate_elave_2_totals,
|
||||
# calculate_vergi_hesab_3cu_totals, calculate_509_1, calculate_509_2,
|
||||
# filter_vergi_hesab_5ci_rows / filter_vergi_hesab_6ci_rows
|
||||
# • Formula Editor record "Əmlak vergisi" (table_row_formulas_storage)
|
||||
# row formulas for vergi_hesab_1ci, vergi_hesab_1ci_1, vergi_hesab_2ci,
|
||||
# vergi_hesab_4cu.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ELAVE_1_TOTAL = "Vergidən azad olunan əmlakın CƏMİ dəyəri:"
|
||||
ELAVE_2_PARENT = "1. Binalar, tikililər və qurğular"
|
||||
ELAVE_2_TOTAL = "Vergiyə cəlb olunan əmlakın CƏMİ dəyəri:"
|
||||
|
||||
ELAVE_1_COLS = (
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012", # 501.2
|
||||
"ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri", # 502.2
|
||||
"hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri", # 503.2
|
||||
"ilinsonunaəsasvəsaitlərinqalıqdəyəri", # 504.2
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri", # 505.2
|
||||
)
|
||||
ELAVE_2_COLS = (
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri", # 501.1
|
||||
"ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri", # 502.1
|
||||
"hesabatilierzindeuçotaalınan", # 503.1
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", # 504.1
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505", # 505.1
|
||||
)
|
||||
|
||||
|
||||
def flt(v):
|
||||
try:
|
||||
return float(v) if v not in (None, "") else 0.0
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def calc_binalar_totals_orig(elave_2_rows):
|
||||
parent = None
|
||||
children = []
|
||||
for r in elave_2_rows:
|
||||
cat = r.get("vergiyəcəlbolunanəmlaklarınkateqoriyası", "") or ""
|
||||
if cat == ELAVE_2_PARENT:
|
||||
parent = r
|
||||
elif len(cat) >= 3 and cat.startswith("1.") and cat[2] in "123456":
|
||||
children.append(r)
|
||||
if not parent:
|
||||
return
|
||||
for col in ELAVE_2_COLS:
|
||||
parent[col] = sum(flt(c.get(col)) for c in children)
|
||||
|
||||
|
||||
def calc_elave_1_totals_orig(elave_1_rows):
|
||||
total = None
|
||||
others = []
|
||||
for r in elave_1_rows:
|
||||
if r.get("azadolmasəbəbi") == ELAVE_1_TOTAL:
|
||||
total = r
|
||||
else:
|
||||
others.append(r)
|
||||
if not total:
|
||||
return
|
||||
for col in ELAVE_1_COLS:
|
||||
total[col] = sum(flt(o.get(col)) for o in others)
|
||||
|
||||
|
||||
def calc_elave_2_totals_orig(elave_2_rows):
|
||||
total = None
|
||||
others = []
|
||||
for r in elave_2_rows:
|
||||
cat = r.get("vergiyəcəlbolunanəmlaklarınkateqoriyası", "") or ""
|
||||
if cat == ELAVE_2_TOTAL:
|
||||
total = r
|
||||
elif cat != ELAVE_2_PARENT:
|
||||
others.append(r)
|
||||
if not total:
|
||||
return
|
||||
for col in ELAVE_2_COLS:
|
||||
total[col] = sum(flt(o.get(col)) for o in others)
|
||||
|
||||
|
||||
def calc_vh_3cu_totals_orig(elave_1_rows, elave_2_rows, vh_3cu_rows):
|
||||
sum_505_2 = sum(
|
||||
flt(r.get("artıqqiymətəəsasvəsaitlərindəyəri"))
|
||||
for r in elave_1_rows
|
||||
if r.get("azadolmasəbəbi") != ELAVE_1_TOTAL
|
||||
)
|
||||
sum_505_1 = sum(
|
||||
flt(r.get("artıqqiymətəəsasvəsaitlərindəyəri505"))
|
||||
for r in elave_2_rows
|
||||
if r.get("vergiyəcəlbolunanəmlaklarınkateqoriyası") not in (ELAVE_2_PARENT, ELAVE_2_TOTAL)
|
||||
)
|
||||
for r in vh_3cu_rows:
|
||||
g = r.get("göstəricilər") or ""
|
||||
if g.startswith("505.1 "):
|
||||
r["manatla"] = sum_505_1
|
||||
elif g.startswith("505.2 "):
|
||||
r["manatla"] = sum_505_2
|
||||
elif g.startswith("505 "):
|
||||
r["manatla"] = sum_505_1 + sum_505_2
|
||||
|
||||
|
||||
# --- Formula Editor DSL: table(elave_X, idx, col) reads row #idx (1-indexed)
|
||||
def calc_vh_1ci_orig(elave_1_rows, elave_2_rows, vh_1ci_rows):
|
||||
"""Mirrors formula_editor "Əmlak vergisi" rows for vergi_hesab_1ci."""
|
||||
e1_23 = elave_1_rows[22] if len(elave_1_rows) >= 23 else {}
|
||||
e2_12 = elave_2_rows[11] if len(elave_2_rows) >= 12 else {}
|
||||
v_e1_501_2 = flt(e1_23.get("hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012"))
|
||||
v_e2_501_1 = flt(e2_12.get("hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri"))
|
||||
for i, r in enumerate(vh_1ci_rows):
|
||||
g = r.get("göstəricilər") or ""
|
||||
if i == 0 and g.startswith("501."):
|
||||
r["manatla"] = v_e1_501_2 + v_e2_501_1
|
||||
elif g.startswith("501.1 "):
|
||||
r["manatla"] = v_e2_501_1
|
||||
elif g.startswith("501.2 "):
|
||||
r["manatla"] = v_e1_501_2
|
||||
# 501.1.1 stays user-input (no formula in DSL)
|
||||
|
||||
|
||||
def calc_vh_1ci_1_orig(elave_1_rows, elave_2_rows, vh_1ci_1_rows):
|
||||
e1_23 = elave_1_rows[22] if len(elave_1_rows) >= 23 else {}
|
||||
e2_12 = elave_2_rows[11] if len(elave_2_rows) >= 12 else {}
|
||||
v_e1_503_2 = flt(e1_23.get("hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri"))
|
||||
v_e2_503_1 = flt(e2_12.get("hesabatilierzindeuçotaalınan"))
|
||||
for i, r in enumerate(vh_1ci_1_rows):
|
||||
g = r.get("göstəricilər") or ""
|
||||
if i == 0 and g.startswith("503."):
|
||||
r["manatla"] = v_e1_503_2 + v_e2_503_1
|
||||
elif g.startswith("503.1 "):
|
||||
r["manatla"] = v_e2_503_1
|
||||
elif g.startswith("503.2 "):
|
||||
r["manatla"] = v_e1_503_2
|
||||
|
||||
|
||||
def calc_vh_2ci_orig(elave_1_rows, elave_2_rows, vh_2ci_rows):
|
||||
e1_23 = elave_1_rows[22] if len(elave_1_rows) >= 23 else {}
|
||||
e2_12 = elave_2_rows[11] if len(elave_2_rows) >= 12 else {}
|
||||
v_e1_504_2 = flt(e1_23.get("ilinsonunaəsasvəsaitlərinqalıqdəyəri"))
|
||||
v_e2_504_1 = flt(e2_12.get("hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri"))
|
||||
for i, r in enumerate(vh_2ci_rows):
|
||||
g = r.get("göstəricilər") or ""
|
||||
if i == 0 and g.startswith("504."):
|
||||
r["manatla"] = v_e1_504_2 + v_e2_504_1
|
||||
elif g.startswith("504.1 "):
|
||||
r["manatla"] = v_e2_504_1
|
||||
elif g.startswith("504.2 "):
|
||||
r["manatla"] = v_e1_504_2
|
||||
|
||||
|
||||
def calc_vh_4cu_orig(vh_4cu_rows):
|
||||
if vh_4cu_rows:
|
||||
vh_4cu_rows[0]["faizlə"] = 1
|
||||
|
||||
|
||||
def calc_509_1_orig(tam_il, vh_1ci_rows, vh_2ci_rows, vh_3cu_rows, vh_5ci_rows, vh_6ci_rows):
|
||||
if not tam_il:
|
||||
return
|
||||
v_501_1 = next((flt(r["manatla"]) for r in vh_1ci_rows if (r.get("göstəricilər") or "").startswith("501.1 ")), 0.0)
|
||||
v_504_1 = next((flt(r["manatla"]) for r in vh_2ci_rows if (r.get("göstəricilər") or "").startswith("504.1 ")), 0.0)
|
||||
v_505_1 = next((flt(r["manatla"]) for r in vh_3cu_rows if (r.get("göstəricilər") or "").startswith("505.1 ")), 0.0)
|
||||
result = ((v_501_1 + v_504_1) / 2.0) * 0.01 + v_505_1 * 0.01
|
||||
for r in vh_5ci_rows:
|
||||
if (r.get("göstəricilər") or "").startswith("509.1 "):
|
||||
r["manatla"] = result
|
||||
for r in vh_6ci_rows:
|
||||
if (r.get("göstəricilər") or "").startswith("511.1 "):
|
||||
r["manatla"] = result
|
||||
|
||||
|
||||
def calc_509_2_orig(il_erzinde, vh_1ci_1_rows, vh_2ci_rows, vh_3cu_rows, vh_4cu_1_rows, vh_5ci_rows, vh_6ci_rows):
|
||||
if not il_erzinde:
|
||||
return
|
||||
v_507 = flt(vh_4cu_1_rows[0].get("ayla")) if vh_4cu_1_rows else 0.0
|
||||
v_503_1 = next((flt(r["manatla"]) for r in vh_1ci_1_rows if (r.get("göstəricilər") or "").startswith("503.1 ")), 0.0)
|
||||
v_504_1 = next((flt(r["manatla"]) for r in vh_2ci_rows if (r.get("göstəricilər") or "").startswith("504.1 ")), 0.0)
|
||||
v_505_1 = next((flt(r["manatla"]) for r in vh_3cu_rows if (r.get("göstəricilər") or "").startswith("505.1 ")), 0.0)
|
||||
result = ((v_503_1 + v_504_1) / 24.0 * v_507) * 0.01 + v_505_1 * 0.01
|
||||
for r in vh_5ci_rows:
|
||||
if (r.get("göstəricilər") or "").startswith("509.2 "):
|
||||
r["manatla"] = result
|
||||
for r in vh_6ci_rows:
|
||||
if (r.get("göstəricilər") or "").startswith("511.2 "):
|
||||
r["manatla"] = result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Seed exactly the same row labels the original `Default*` Client Scripts use.
|
||||
# Pulled verbatim from `tabClient Script` records (DefaultProperty tax return *).
|
||||
# These differ from the AI clone's Select options (extra "199.20" indicator,
|
||||
# different 199.1 text, etc.) — that's the original's documented set, not ours.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ORIG_ELAVE_1_LABELS = [
|
||||
"Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
"Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.2-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.3-cü maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.4-cü maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.1-1-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.5-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.7-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.8-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.9-cu maddəsinə əsasən:",
|
||||
"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:",
|
||||
]
|
||||
ORIG_ELAVE_2_LABELS = [
|
||||
"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:",
|
||||
]
|
||||
ORIG_VH_1CI = [
|
||||
"501. Əmlakın ümumi qalıq dəyəri",
|
||||
"501.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"501.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"501.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
]
|
||||
ORIG_VH_1CI_1 = [
|
||||
"503. Əmlakın ümumi dəyəri",
|
||||
"503.1 Vergiyə cəlb olunan əmlakın dəyəri",
|
||||
"503.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın dəyəri",
|
||||
"503.2 Vergidən azad olunan əmlakın dəyəri",
|
||||
]
|
||||
ORIG_VH_2CI = [
|
||||
"504. Əmlakın ümumi qalıq dəyəri",
|
||||
"504.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"504.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"504.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
]
|
||||
ORIG_VH_3CU = [
|
||||
"505 Artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.1 Vergiyə cəlb olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.2 Vergidən azad olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
]
|
||||
ORIG_VH_4CU = ["506 Verginin dərəcəsi"]
|
||||
ORIG_VH_4CU_1 = ["507 Müəssisə yarandığı aydan sonra ilin sonuna qədər olan ayların sayı"]
|
||||
# In tam_il mode the original's filter strips 509.2 / 511.2; in il_erzinde mode
|
||||
# it strips 509.1 / 511.1. We seed all rows and zero the inactive ones so a
|
||||
# single cell-by-cell diff covers both modes.
|
||||
ORIG_VH_5CI = [
|
||||
"509.1 Vergi ödəyicisi il ərzində tam fəaliyyət göstərdiyi halda:",
|
||||
"509.2 Hesabat ili ərzində yaradıldığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olduğu halda:",
|
||||
"510 Vergi məcəlləsinin 199.21-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
]
|
||||
ORIG_VH_6CI = [
|
||||
"511.1 Müəssisə il ərzində tam fəaliyyət göstərdiyi halda",
|
||||
"511.2 Müəssisə il ərzində yarandığı halda",
|
||||
]
|
||||
|
||||
|
||||
def make_original_doc(tam_il, il_erzinde):
|
||||
"""Build the 10 child-table lists as plain dicts, seeded original-style."""
|
||||
def empty_row(text_col, label, num_cols):
|
||||
row = {text_col: label}
|
||||
for c in num_cols:
|
||||
row[c] = 0.0
|
||||
return row
|
||||
|
||||
return {
|
||||
"tam_il": int(bool(tam_il)),
|
||||
"il_erzinde": int(bool(il_erzinde)),
|
||||
"elave_1": [empty_row("azadolmasəbəbi", l, ELAVE_1_COLS) for l in ORIG_ELAVE_1_LABELS],
|
||||
"elave_2": [empty_row("vergiyəcəlbolunanəmlaklarınkateqoriyası", l, ELAVE_2_COLS) for l in ORIG_ELAVE_2_LABELS],
|
||||
"vergi_hesab_1ci": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_1CI],
|
||||
"vergi_hesab_1ci_1": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_1CI_1],
|
||||
"vergi_hesab_2ci": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_2CI],
|
||||
"vergi_hesab_3cu": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_3CU],
|
||||
"vergi_hesab_4cu": [empty_row("göstəricilər", l, ("faizlə",)) for l in ORIG_VH_4CU],
|
||||
"vergi_hesab_4cu_1": [empty_row("göstəricilər", l, ("ayla",)) for l in ORIG_VH_4CU_1],
|
||||
"vergi_hesab_5ci": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_5CI],
|
||||
"vergi_hesab_6ci": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_6CI],
|
||||
}
|
||||
|
||||
|
||||
def find_orig(rows, text_col, label):
|
||||
return next((r for r in rows if r.get(text_col) == label), None)
|
||||
|
||||
|
||||
def run_original_pipeline(d):
|
||||
"""Original calc order from property_tax_return.js `refresh` hook + DSL triggers."""
|
||||
calc_binalar_totals_orig(d["elave_2"])
|
||||
calc_elave_1_totals_orig(d["elave_1"])
|
||||
calc_elave_2_totals_orig(d["elave_2"])
|
||||
calc_vh_3cu_totals_orig(d["elave_1"], d["elave_2"], d["vergi_hesab_3cu"])
|
||||
# DSL-evaluated cells
|
||||
calc_vh_1ci_orig(d["elave_1"], d["elave_2"], d["vergi_hesab_1ci"])
|
||||
calc_vh_1ci_1_orig(d["elave_1"], d["elave_2"], d["vergi_hesab_1ci_1"])
|
||||
calc_vh_2ci_orig(d["elave_1"], d["elave_2"], d["vergi_hesab_2ci"])
|
||||
calc_vh_4cu_orig(d["vergi_hesab_4cu"])
|
||||
# Final tax
|
||||
calc_509_1_orig(d["tam_il"], d["vergi_hesab_1ci"], d["vergi_hesab_2ci"], d["vergi_hesab_3cu"], d["vergi_hesab_5ci"], d["vergi_hesab_6ci"])
|
||||
calc_509_2_orig(d["il_erzinde"], d["vergi_hesab_1ci_1"], d["vergi_hesab_2ci"], d["vergi_hesab_3cu"], d["vergi_hesab_4cu_1"], d["vergi_hesab_5ci"], d["vergi_hesab_6ci"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Diff helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def round2(v):
|
||||
return float(Decimal(str(float(v or 0))).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
def cells(rows, text_col, num_cols):
|
||||
return [
|
||||
(r.get(text_col), {c: round2(r.get(c) or 0) for c in num_cols})
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def diff_tables(label, orig_rows, ai_rows, text_col, num_cols, tol=0.005):
|
||||
"""Diff by label, not index. Reports unique-side rows as info only.
|
||||
|
||||
The AI Select shortened the 199.1 label and dropped 199.20; the
|
||||
`LABEL_REMAP` table in `build_orig_doc` already normalises 199.1, so the
|
||||
only structural difference left is the orig-only 199.20 row. We surface
|
||||
rows that exist only on one side, but they do not count as failures unless
|
||||
they carry non-zero values."""
|
||||
def get(r, col):
|
||||
return r.get(col) if isinstance(r, dict) else getattr(r, col, None)
|
||||
|
||||
def label_of(r):
|
||||
return get(r, text_col)
|
||||
|
||||
orig_by = {label_of(r): r for r in orig_rows}
|
||||
ai_by = {label_of(r): r for r in ai_rows}
|
||||
common = [l for l in orig_by if l in ai_by]
|
||||
orig_only = [l for l in orig_by if l not in ai_by]
|
||||
ai_only = [l for l in ai_by if l not in orig_by]
|
||||
|
||||
print(
|
||||
f"\n --- {label} ({len(orig_rows)} orig / {len(ai_rows)} AI, "
|
||||
f"common={len(common)}, orig-only={len(orig_only)}, AI-only={len(ai_only)}) ---"
|
||||
)
|
||||
|
||||
failed = 0
|
||||
for l in common:
|
||||
o, a = orig_by[l], ai_by[l]
|
||||
for c in num_cols:
|
||||
o_val = round2(get(o, c) or 0)
|
||||
a_val = round2(get(a, c) or 0)
|
||||
if abs(o_val - a_val) > tol:
|
||||
print(f" ✗ [{(l or '')[:50]}] {c}: orig={o_val} AI={a_val} Δ={a_val-o_val:+.4f}")
|
||||
failed += 1
|
||||
|
||||
def nonzero(r):
|
||||
return any(round2(get(r, c) or 0) != 0 for c in num_cols)
|
||||
|
||||
for l in orig_only:
|
||||
r = orig_by[l]
|
||||
marker = "✗ nonzero!" if nonzero(r) else "ℹ"
|
||||
print(f" {marker} orig-only row [{(l or '')[:50]}]: values={[round2(get(r,c) or 0) for c in num_cols]}")
|
||||
if nonzero(r):
|
||||
failed += 1
|
||||
for l in ai_only:
|
||||
r = ai_by[l]
|
||||
marker = "✗ nonzero!" if nonzero(r) else "ℹ"
|
||||
print(f" {marker} AI-only row [{(l or '')[:50]}]: values={[round2(get(r,c) or 0) for c in num_cols]}")
|
||||
if nonzero(r):
|
||||
failed += 1
|
||||
|
||||
if failed == 0:
|
||||
print(f" ✓ all {len(common)*len(num_cols)} shared cells match")
|
||||
return failed
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario runner
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_ai_doc(scenario_inputs, tam_il, il_erzinde):
|
||||
"""Create a Property Tax Return AI doc, seed, apply inputs, validate."""
|
||||
doc = frappe.new_doc("Property Tax Return AI")
|
||||
doc.tam_il = int(bool(tam_il))
|
||||
doc.il_erzinde = int(bool(il_erzinde))
|
||||
doc.run_method("before_insert") # seed
|
||||
|
||||
for tbl, label, col, val in scenario_inputs:
|
||||
if tbl == "elave_1":
|
||||
row = next(r for r in doc.elave_1 if r.azadolmasəbəbi == label)
|
||||
elif tbl == "elave_2":
|
||||
row = next(r for r in doc.elave_2 if r.vergiyəcəlbolunanəmlaklarınkateqoriyası == label)
|
||||
else:
|
||||
grid = doc.get(tbl)
|
||||
row = next(r for r in grid if r.göstəricilər == label)
|
||||
setattr(row, col, val)
|
||||
|
||||
doc.recalculate()
|
||||
return doc
|
||||
|
||||
|
||||
def build_orig_doc(scenario_inputs, tam_il, il_erzinde):
|
||||
"""Build the equivalent original-style dict-doc, apply inputs, run pipeline."""
|
||||
d = make_original_doc(tam_il, il_erzinde)
|
||||
|
||||
# Map AI-clone labels to original-doc labels when they differ. The AI
|
||||
# Select shortened "199.1-ci ... əsasən azaldılmalı əmlak vergisinin məbləği"
|
||||
# to "199.1-ci ... əsasən:". For parity we map both to the original's full
|
||||
# text when an input targets that row.
|
||||
LABEL_REMAP = {
|
||||
"Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən:":
|
||||
"Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
}
|
||||
|
||||
for tbl, label, col, val in scenario_inputs:
|
||||
mapped_label = LABEL_REMAP.get(label, label)
|
||||
if tbl == "elave_1":
|
||||
row = find_orig(d["elave_1"], "azadolmasəbəbi", mapped_label)
|
||||
elif tbl == "elave_2":
|
||||
row = find_orig(d["elave_2"], "vergiyəcəlbolunanəmlaklarınkateqoriyası", mapped_label)
|
||||
else:
|
||||
row = find_orig(d[tbl], "göstəricilər", mapped_label)
|
||||
if row is None:
|
||||
raise LookupError(f"orig row not found: {tbl} / {mapped_label!r}")
|
||||
row[col] = float(val)
|
||||
|
||||
run_original_pipeline(d)
|
||||
return d
|
||||
|
||||
|
||||
def diff_scenario(name, scenario_inputs, tam_il, il_erzinde):
|
||||
print(f"\n========== {name} ==========")
|
||||
orig = build_orig_doc(scenario_inputs, tam_il, il_erzinde)
|
||||
ai = build_ai_doc(scenario_inputs, tam_il, il_erzinde)
|
||||
|
||||
total_fail = 0
|
||||
total_fail += diff_tables("elave_1", orig["elave_1"], list(ai.elave_1), "azadolmasəbəbi", ELAVE_1_COLS)
|
||||
total_fail += diff_tables("elave_2", orig["elave_2"], list(ai.elave_2), "vergiyəcəlbolunanəmlaklarınkateqoriyası", ELAVE_2_COLS)
|
||||
total_fail += diff_tables("vergi_hesab_1ci", orig["vergi_hesab_1ci"], list(ai.vergi_hesab_1ci), "göstəricilər", ("manatla",))
|
||||
total_fail += diff_tables("vergi_hesab_1ci_1", orig["vergi_hesab_1ci_1"], list(ai.vergi_hesab_1ci_1), "göstəricilər", ("manatla",))
|
||||
total_fail += diff_tables("vergi_hesab_2ci", orig["vergi_hesab_2ci"], list(ai.vergi_hesab_2ci), "göstəricilər", ("manatla",))
|
||||
total_fail += diff_tables("vergi_hesab_3cu", orig["vergi_hesab_3cu"], list(ai.vergi_hesab_3cu), "göstəricilər", ("manatla",))
|
||||
total_fail += diff_tables("vergi_hesab_4cu", orig["vergi_hesab_4cu"], list(ai.vergi_hesab_4cu), "göstəricilər", ("faizlə",))
|
||||
total_fail += diff_tables("vergi_hesab_4cu_1", orig["vergi_hesab_4cu_1"], list(ai.vergi_hesab_4cu_1), "göstəricilər", ("ayla",))
|
||||
total_fail += diff_tables("vergi_hesab_5ci", orig["vergi_hesab_5ci"], list(ai.vergi_hesab_5ci), "göstəricilər", ("manatla",))
|
||||
total_fail += diff_tables("vergi_hesab_6ci", orig["vergi_hesab_6ci"], list(ai.vergi_hesab_6ci), "göstəricilər", ("manatla",))
|
||||
|
||||
print(f"\n >>> {name}: {'PASS' if total_fail == 0 else f'FAIL ({total_fail} cell mismatches)'}")
|
||||
return total_fail == 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def run():
|
||||
"""Run all parity scenarios."""
|
||||
|
||||
# ---- Scenario A: cabinet-verified full-year baseline -------------------
|
||||
# taxable[1.1].periodEnd=200, excessInsured=300 → 509.1 = 4.00
|
||||
scenA = [
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", 200),
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505", 300),
|
||||
]
|
||||
a = diff_scenario("A. Cabinet full-year (200 / 300 → 4.00)", scenA, tam_il=1, il_erzinde=0)
|
||||
|
||||
# ---- Scenario B: cabinet-verified partial-year baseline ---------------
|
||||
# taxable[1.1].onCreate=100, periodEnd=200, excess=300, 507=6 → 509.2 = 3.75
|
||||
scenB = [
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"hesabatilierzindeuçotaalınan", 100),
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", 200),
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505", 300),
|
||||
("vergi_hesab_4cu_1",
|
||||
"507 Müəssisə yarandığı aydan sonra ilin sonuna qədər olan ayların sayı",
|
||||
"ayla", 6),
|
||||
]
|
||||
b = diff_scenario("B. Cabinet partial-year (100/200/300, 6m → 3.75)", scenB, tam_il=0, il_erzinde=1)
|
||||
|
||||
# ---- Scenario C: multi-row, every column non-zero, with 510 reduction
|
||||
scenC = [
|
||||
# 3 elave_1 rows with all 5 numeric columns set
|
||||
("elave_1", "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012", 1000),
|
||||
("elave_1", "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri", 50),
|
||||
("elave_1", "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri", 700),
|
||||
("elave_1", "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"ilinsonunaəsasvəsaitlərinqalıqdəyəri", 1200),
|
||||
("elave_1", "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri", 150),
|
||||
("elave_1", "Vergi Məcəlləsinin 227.1-ci maddəsinə əsasən:",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012", 333),
|
||||
("elave_1", "Vergi Məcəlləsinin 227.1-ci maddəsinə əsasən:",
|
||||
"ilinsonunaəsasvəsaitlərinqalıqdəyəri", 444),
|
||||
# 4 elave_2 rows: 1.1 + 1.3 + 1.5 (all children of _5001) + 4 (other)
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri", 500),
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", 400),
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505", 50),
|
||||
("elave_2", "1.3 Əmlak kompleksi kimi müəssisələr",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri", 800),
|
||||
("elave_2", "1.3 Əmlak kompleksi kimi müəssisələr",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", 750),
|
||||
("elave_2", "1.5 Mənzillər",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri", 200),
|
||||
("elave_2", "1.5 Mənzillər",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505", 25),
|
||||
("elave_2", "4 Nəqliyyat vasitələri",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", 1100),
|
||||
# 510 reduction (user input) — original ignores it for 511 anyway, this
|
||||
# tests our changed code-path stays neutral.
|
||||
("vergi_hesab_5ci",
|
||||
"510 Vergi məcəlləsinin 199.21-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
"manatla", 2.50),
|
||||
]
|
||||
c = diff_scenario("C. Multi-row full-year with 510=2.50", scenC, tam_il=1, il_erzinde=0)
|
||||
|
||||
print(f"\n========== Summary: A={a} B={b} C={c} ==========")
|
||||
print("ALL OK" if (a and b and c) else "FAILED")
|
||||
return {"A": a, "B": b, "C": c}
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
// ── Property Tax Return AI — client-side seeding + realtime recalc ──
|
||||
// Seeding is unchanged: on the "+ New" Desk form we pull the row catalogue
|
||||
// from the whitelisted get_seed() and populate every empty child table.
|
||||
// Realtime recalc mirrors property_tax_return_ai.py's validate()
|
||||
// pipeline (port of the Python recalculate logic) so totals/mirrors/tax
|
||||
// update as the user types, not just on save.
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// ── Constants — must match the Select options of the child DocTypes ──
|
||||
|
||||
const E1_TOTAL_LABEL = "Vergidən azad olunan əmlakın CƏMİ dəyəri:";
|
||||
const E2_PARENT_LABEL = "1. Binalar, tikililər və qurğular";
|
||||
const E2_TOTAL_LABEL = "Vergiyə cəlb olunan əmlakın CƏMİ dəyəri:";
|
||||
|
||||
const E1_COLS = {
|
||||
"501.2": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012",
|
||||
"502.2": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"503.2": "hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri",
|
||||
"504.2": "ilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"505.2": "artıqqiymətəəsasvəsaitlərindəyəri",
|
||||
};
|
||||
const E2_COLS = {
|
||||
"501.1": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"502.1": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"503.1": "hesabatilierzindeuçotaalınan",
|
||||
"504.1": "hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"505.1": "artıqqiymətəəsasvəsaitlərindəyəri505",
|
||||
};
|
||||
|
||||
const VH_1CI = [
|
||||
"501. Əmlakın ümumi qalıq dəyəri",
|
||||
"501.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"501.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"501.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
];
|
||||
const VH_1CI_1 = [
|
||||
"503. Əmlakın ümumi dəyəri",
|
||||
"503.1 Vergiyə cəlb olunan əmlakın dəyəri",
|
||||
"503.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın dəyəri",
|
||||
"503.2 Vergidən azad olunan əmlakın dəyəri",
|
||||
];
|
||||
const VH_2CI = [
|
||||
"504. Əmlakın ümumi qalıq dəyəri",
|
||||
"504.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"504.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"504.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
];
|
||||
const VH_3CU = [
|
||||
"505 Artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.1 Vergiyə cəlb olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.2 Vergidən azad olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
];
|
||||
const VH_4CU = "506 Verginin dərəcəsi";
|
||||
const VH_4CU_1 = "507 Müəssisə yarandığı aydan sonra ilin sonuna qədər olan ayların sayı";
|
||||
const VH_5CI = [
|
||||
"509.1 Vergi ödəyicisi il ərzində tam fəaliyyət göstərdiyi halda:",
|
||||
"509.2 Hesabat ili ərzində yaradıldığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olduğu halda:",
|
||||
"510 Vergi məcəlləsinin 199.21-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
];
|
||||
const VH_6CI = [
|
||||
"511.1 Müəssisə il ərzində tam fəaliyyət göstərdiyi halda",
|
||||
"511.2 Müəssisə il ərzində yarandığı halda",
|
||||
];
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
function flt(v) {
|
||||
const n = parseFloat(v);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
function round2(v) {
|
||||
return Math.round((v + Number.EPSILON) * 100) / 100;
|
||||
}
|
||||
function find_row(rows, col, value) {
|
||||
if (!rows) return null;
|
||||
for (const r of rows) if (r[col] === value) return r;
|
||||
return null;
|
||||
}
|
||||
function set_cell(rows, text_col, label, target_col, value) {
|
||||
const r = find_row(rows, text_col, label);
|
||||
if (r) r[target_col] = round2(value);
|
||||
}
|
||||
|
||||
// ── Per-table recalc functions (port of property_tax_return_ai.py) ──
|
||||
|
||||
function calc_e1_totals(frm) {
|
||||
const totals = {};
|
||||
for (const code in E1_COLS) totals[code] = 0;
|
||||
for (const r of frm.doc.elave_1 || []) {
|
||||
if (r.azadolmasəbəbi === E1_TOTAL_LABEL) continue;
|
||||
for (const code in E1_COLS) totals[code] += flt(r[E1_COLS[code]]);
|
||||
}
|
||||
const cəmi = find_row(frm.doc.elave_1, "azadolmasəbəbi", E1_TOTAL_LABEL);
|
||||
if (cəmi) {
|
||||
for (const code in E1_COLS) cəmi[E1_COLS[code]] = round2(totals[code]);
|
||||
}
|
||||
const out = {};
|
||||
for (const code in totals) out[code] = round2(totals[code]);
|
||||
return out;
|
||||
}
|
||||
|
||||
function calc_e2_parent_and_totals(frm) {
|
||||
// 1) Parent _5001 = Σ of sub-categories 1.1–1.6.
|
||||
const child_sums = {};
|
||||
for (const code in E2_COLS) child_sums[code] = 0;
|
||||
for (const r of frm.doc.elave_2 || []) {
|
||||
const lab = r.vergiyəcəlbolunanəmlaklarınkateqoriyası || "";
|
||||
if (lab.length >= 3 && lab.charAt(0) === "1" && lab.charAt(1) === "." && "123456".indexOf(lab.charAt(2)) >= 0) {
|
||||
for (const code in E2_COLS) child_sums[code] += flt(r[E2_COLS[code]]);
|
||||
}
|
||||
}
|
||||
const parent = find_row(frm.doc.elave_2, "vergiyəcəlbolunanəmlaklarınkateqoriyası", E2_PARENT_LABEL);
|
||||
if (parent) {
|
||||
for (const code in E2_COLS) parent[E2_COLS[code]] = round2(child_sums[code]);
|
||||
}
|
||||
|
||||
// 2) CƏMİ totals: every row except _5001 parent and CƏMİ itself.
|
||||
const totals = {};
|
||||
for (const code in E2_COLS) totals[code] = 0;
|
||||
for (const r of frm.doc.elave_2 || []) {
|
||||
const lab = r.vergiyəcəlbolunanəmlaklarınkateqoriyası || "";
|
||||
if (lab === E2_PARENT_LABEL || lab === E2_TOTAL_LABEL) continue;
|
||||
for (const code in E2_COLS) totals[code] += flt(r[E2_COLS[code]]);
|
||||
}
|
||||
const cəmi = find_row(frm.doc.elave_2, "vergiyəcəlbolunanəmlaklarınkateqoriyası", E2_TOTAL_LABEL);
|
||||
if (cəmi) {
|
||||
for (const code in E2_COLS) cəmi[E2_COLS[code]] = round2(totals[code]);
|
||||
}
|
||||
const out = {};
|
||||
for (const code in totals) out[code] = round2(totals[code]);
|
||||
return out;
|
||||
}
|
||||
|
||||
function calc_vh_mirrors(frm, e1, e2) {
|
||||
const e1_501_2 = e1["501.2"] || 0;
|
||||
const e1_503_2 = e1["503.2"] || 0;
|
||||
const e1_504_2 = e1["504.2"] || 0;
|
||||
const e1_505_2 = e1["505.2"] || 0;
|
||||
const e2_501_1 = e2["501.1"] || 0;
|
||||
const e2_503_1 = e2["503.1"] || 0;
|
||||
const e2_504_1 = e2["504.1"] || 0;
|
||||
const e2_505_1 = e2["505.1"] || 0;
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_1ci, "göstəricilər", VH_1CI[0], "manatla", e1_501_2 + e2_501_1);
|
||||
set_cell(frm.doc.vergi_hesab_1ci, "göstəricilər", VH_1CI[1], "manatla", e2_501_1);
|
||||
set_cell(frm.doc.vergi_hesab_1ci, "göstəricilər", VH_1CI[2], "manatla", 0);
|
||||
set_cell(frm.doc.vergi_hesab_1ci, "göstəricilər", VH_1CI[3], "manatla", e1_501_2);
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_1ci_1, "göstəricilər", VH_1CI_1[0], "manatla", e1_503_2 + e2_503_1);
|
||||
set_cell(frm.doc.vergi_hesab_1ci_1, "göstəricilər", VH_1CI_1[1], "manatla", e2_503_1);
|
||||
set_cell(frm.doc.vergi_hesab_1ci_1, "göstəricilər", VH_1CI_1[2], "manatla", 0);
|
||||
set_cell(frm.doc.vergi_hesab_1ci_1, "göstəricilər", VH_1CI_1[3], "manatla", e1_503_2);
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_2ci, "göstəricilər", VH_2CI[0], "manatla", e1_504_2 + e2_504_1);
|
||||
set_cell(frm.doc.vergi_hesab_2ci, "göstəricilər", VH_2CI[1], "manatla", e2_504_1);
|
||||
set_cell(frm.doc.vergi_hesab_2ci, "göstəricilər", VH_2CI[2], "manatla", 0);
|
||||
set_cell(frm.doc.vergi_hesab_2ci, "göstəricilər", VH_2CI[3], "manatla", e1_504_2);
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_3cu, "göstəricilər", VH_3CU[0], "manatla", e1_505_2 + e2_505_1);
|
||||
set_cell(frm.doc.vergi_hesab_3cu, "göstəricilər", VH_3CU[1], "manatla", e2_505_1);
|
||||
set_cell(frm.doc.vergi_hesab_3cu, "göstəricilər", VH_3CU[2], "manatla", e1_505_2);
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_4cu, "göstəricilər", VH_4CU, "faizlə", 1);
|
||||
}
|
||||
|
||||
function calc_vh_509_511(frm, e2) {
|
||||
const months_row = find_row(frm.doc.vergi_hesab_4cu_1, "göstəricilər", VH_4CU_1);
|
||||
const months = months_row ? flt(months_row.ayla) : 0;
|
||||
|
||||
const s501 = e2["501.1"] || 0;
|
||||
const s503 = e2["503.1"] || 0;
|
||||
const s504 = e2["504.1"] || 0;
|
||||
const s505 = e2["505.1"] || 0;
|
||||
|
||||
const tax_full = ((s501 + s504) / 2) * 0.01 + s505 * 0.01;
|
||||
const tax_partial = months
|
||||
? (((s503 + s504) / 24) * months) * 0.01 + s505 * 0.01
|
||||
: s505 * 0.01;
|
||||
|
||||
const is_partial = !!frm.doc.il_erzinde;
|
||||
const tax509 = round2(is_partial ? tax_partial : tax_full);
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_5ci, "göstəricilər", VH_5CI[0], "manatla", is_partial ? 0 : tax509);
|
||||
set_cell(frm.doc.vergi_hesab_5ci, "göstəricilər", VH_5CI[1], "manatla", is_partial ? tax509 : 0);
|
||||
// 510 row stays user input — we don't touch it.
|
||||
|
||||
// 511.x mirrors 509.x (cabinet shouldBePaidTax == calculatedTax; original
|
||||
// property_tax_return.js does the same — no subtraction of 510).
|
||||
set_cell(frm.doc.vergi_hesab_6ci, "göstəricilər", VH_6CI[0], "manatla", is_partial ? 0 : tax509);
|
||||
set_cell(frm.doc.vergi_hesab_6ci, "göstəricilər", VH_6CI[1], "manatla", is_partial ? tax509 : 0);
|
||||
}
|
||||
|
||||
function recalc_all(frm) {
|
||||
const e1 = calc_e1_totals(frm);
|
||||
const e2 = calc_e2_parent_and_totals(frm);
|
||||
calc_vh_mirrors(frm, e1, e2);
|
||||
calc_vh_509_511(frm, e2);
|
||||
const fields = [
|
||||
"elave_1", "elave_2",
|
||||
"vergi_hesab_1ci", "vergi_hesab_1ci_1", "vergi_hesab_2ci", "vergi_hesab_3cu",
|
||||
"vergi_hesab_4cu", "vergi_hesab_5ci", "vergi_hesab_6ci",
|
||||
];
|
||||
for (const f of fields) frm.refresh_field(f);
|
||||
}
|
||||
|
||||
// Expose for child-table handlers below
|
||||
window.__ptr_ai_recalc = recalc_all;
|
||||
|
||||
// ── Seeding (unchanged) + recalc wiring on the parent ──
|
||||
|
||||
frappe.ui.form.on("Property Tax Return AI", {
|
||||
onload: function (frm) {
|
||||
if (!frm.is_new()) return;
|
||||
if (frm.__ai_seeded) return;
|
||||
|
||||
const seed_targets = [
|
||||
"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",
|
||||
"elave_1", "elave_2",
|
||||
];
|
||||
const all_empty = seed_targets.every(
|
||||
(f) => !(frm.doc[f] && frm.doc[f].length)
|
||||
);
|
||||
if (!all_empty) {
|
||||
recalc_all(frm);
|
||||
return;
|
||||
}
|
||||
frm.__ai_seeded = true;
|
||||
frappe.call({
|
||||
method:
|
||||
"taxes_az.taxes_az.doctype.property_tax_return_ai.property_tax_return_ai.get_seed",
|
||||
callback: function (r) {
|
||||
if (!r || !r.message) return;
|
||||
for (const [table_field, rows] of Object.entries(r.message)) {
|
||||
for (const row of rows) {
|
||||
const child = frm.add_child(table_field);
|
||||
Object.assign(child, row);
|
||||
}
|
||||
frm.refresh_field(table_field);
|
||||
}
|
||||
recalc_all(frm);
|
||||
},
|
||||
});
|
||||
},
|
||||
refresh: function (frm) {
|
||||
if (!frm.is_new()) recalc_all(frm);
|
||||
},
|
||||
tam_il: function (frm) { recalc_all(frm); },
|
||||
il_erzinde: function (frm) { recalc_all(frm); },
|
||||
});
|
||||
|
||||
// ── Child-table handlers: recalc on every cell edit / removal ──
|
||||
|
||||
const E1_NUMS = Object.values(E1_COLS);
|
||||
const E2_NUMS = Object.values(E2_COLS);
|
||||
|
||||
const e1_handlers = { elave_1_remove: (frm) => recalc_all(frm) };
|
||||
for (const f of E1_NUMS) e1_handlers[f] = (frm) => recalc_all(frm);
|
||||
frappe.ui.form.on("Property Tax Return AI Elave 1", e1_handlers);
|
||||
|
||||
const e2_handlers = { elave_2_remove: (frm) => recalc_all(frm) };
|
||||
for (const f of E2_NUMS) e2_handlers[f] = (frm) => recalc_all(frm);
|
||||
frappe.ui.form.on("Property Tax Return AI Elave 2", e2_handlers);
|
||||
|
||||
frappe.ui.form.on("Property Tax Return AI Emlak vergi hesablamasi 4cu cedvel 1", {
|
||||
ayla: function (frm) { recalc_all(frm); },
|
||||
});
|
||||
})();
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 14:18:26.085350",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"ümumi_məlumat_tab",
|
||||
"html_pegz",
|
||||
"idarə",
|
||||
"vergiorqanı",
|
||||
"vergidövrü",
|
||||
"il",
|
||||
"tam_il",
|
||||
"il_erzinde",
|
||||
"şəxsi",
|
||||
"vöen",
|
||||
"soyadı",
|
||||
"atasınınadı",
|
||||
"mobnömrə1",
|
||||
"mobnömrə2",
|
||||
"column_break_yjgp",
|
||||
"ödəyicitipi",
|
||||
"adı",
|
||||
"şəhərnömrəsi1",
|
||||
"şəhərnömrəsi2",
|
||||
"section_break_pcfz",
|
||||
"əsasfəaliyyətnövününkodu",
|
||||
"təqdimolunmuşəlavələrinsayı",
|
||||
"bəyannaməninnövü",
|
||||
"dəqiqləşdirilmişbəyannamənintəqdimedilməsibarədəbildiriş",
|
||||
"column_break_xcxz",
|
||||
"section_break_vfvd",
|
||||
"bəyan_ediləcək_məlumatım_yoxdur",
|
||||
"section_break_wnwr",
|
||||
"column_break_dblg",
|
||||
"sətirlərdənbiriniseçin",
|
||||
"əmlak_vergisinin_hesablanması_tab",
|
||||
"html_ikbb",
|
||||
"vergi_hesab_1ci",
|
||||
"html_uhqz",
|
||||
"vergi_hesab_1ci_1",
|
||||
"section_break_bhtd",
|
||||
"html_umxi",
|
||||
"vergi_hesab_2ci",
|
||||
"section_break_fugy",
|
||||
"html_exjg",
|
||||
"vergi_hesab_3cu",
|
||||
"section_break_bhet",
|
||||
"html_tuak",
|
||||
"vergi_hesab_4cu",
|
||||
"vergi_hesab_4cu_1",
|
||||
"section_break_cnpg",
|
||||
"html_mxir",
|
||||
"vergi_hesab_5ci",
|
||||
"section_break_klzd",
|
||||
"html_ctml",
|
||||
"vergi_hesab_6ci",
|
||||
"əlavə_1_tab",
|
||||
"html_hken",
|
||||
"warning_elave_1_html",
|
||||
"elave_1",
|
||||
"əlavə_2_tab",
|
||||
"html_ybie",
|
||||
"elave_2"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "ümumi_məlumat_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Ümumi məlumat"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_pegz",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 20px;font-weight: bold;color: blue;\"> Əmlak vergisinin bəyannaməsi</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergiorqanı",
|
||||
"fieldtype": "Select",
|
||||
"label": "Vergi orqanı",
|
||||
"options": "2 saylı Ərazi Vergilər Baş idarəsi\tSumqayıt\t02\n3 saylı Ərazi Vergilər İdarəsi\tQuba\t03\n4 saylı Ərazi Vergilər İdarəsi\tŞamaxı\t04\n5 saylı Ərazi Vergilər İdarəsi\tŞirvan\t05\n6 saylı Ərazi Vergilər Baş İdarəsi\tLənkəran\t06\nQarabağ Ərazi Vergilər Baş İdarəsi\tAğdam, Füzuli, Şuşa, Xocalı, Xocavənd\t07\n8 saylı Ərazi Vergilər İdarəsi\tBeyləqan\t08\n9 saylı Ərazi Vergilər İdarəsi\tGöyçay\t09\n10 saylı Ərazi Vergilər İdarəsi\tQəbələ\t10\n11 saylı Ərazi Vergilər Baş İdarəsi\tYevlax\t11\n12 saylı Ərazi Vergilər Baş İdarəsi\tGəncə\t12\n13 saylı Ərazi Vergilər İdarəsi\tZaqatala\t13\n14 saylı Ərazi Vergilər İdarəsi\tŞəmkir\t14\nDövlət Vergi xidmətinin Milli Gəlirlər Baş idarəsi\tBakı\t16\nDövlət Vergi xidmətinin Bakı şəhəri Lokal Gəlirlər Baş idarəsi\tBakı\t19\nDövlət Vergi xidmətinin Bakı şəhəri Kiçik Sahibkarlıqla İş üzrə Baş idarəsi\tBakı\t39\nNaxçıvan şəhəri üzrə Vergilər İdarəsi\tNaxçıvan\t27\n2 saylı Ərazi Vergilər İdarəsi (Naxçıvan MR)\tŞərur, Sədərək, Kəngərli\t30\n3 saylı Ərazi Vergilər İdarəsi (Naxçıvan MR)\tŞahbuz, Babək\t26\n4 saylı Ərazi Vergilər İdarəsi (Naxçıvan MR)\tCulfa, Ordubad\t24\nŞərqi Zəngəzur Ərazi Vergilər İdarəsi\tCəbrayıl, Kəlbəcər, Laçın, Qubadlı, Zəngilan\t55"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergidövrü",
|
||||
"fieldtype": "Select",
|
||||
"label": "Vergi dövrü",
|
||||
"options": "İllik"
|
||||
},
|
||||
{
|
||||
"fieldname": "il",
|
||||
"fieldtype": "Int",
|
||||
"label": "İl",
|
||||
"length": 4
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "tam_il",
|
||||
"fieldtype": "Check",
|
||||
"label": "Tam İl"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "il_erzinde",
|
||||
"fieldtype": "Check",
|
||||
"label": "İl Ərzində"
|
||||
},
|
||||
{
|
||||
"fieldname": "şəxsi",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Şəxsi"
|
||||
},
|
||||
{
|
||||
"fieldname": "vöen",
|
||||
"fieldtype": "Int",
|
||||
"label": "VÖEN"
|
||||
},
|
||||
{
|
||||
"fieldname": "soyadı",
|
||||
"fieldtype": "Data",
|
||||
"label": "Soyadı"
|
||||
},
|
||||
{
|
||||
"fieldname": "atasınınadı",
|
||||
"fieldtype": "Data",
|
||||
"label": "Atasının adı"
|
||||
},
|
||||
{
|
||||
"fieldname": "mobnömrə1",
|
||||
"fieldtype": "Int",
|
||||
"label": "Mob nömrə 1"
|
||||
},
|
||||
{
|
||||
"fieldname": "mobnömrə2",
|
||||
"fieldtype": "Int",
|
||||
"label": "Mob nömrə 2"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_yjgp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "ödəyicitipi",
|
||||
"fieldtype": "Select",
|
||||
"label": "Ödəyici tipi",
|
||||
"options": "Hüquqi\nFiziki"
|
||||
},
|
||||
{
|
||||
"fieldname": "adı",
|
||||
"fieldtype": "Data",
|
||||
"label": "Adı"
|
||||
},
|
||||
{
|
||||
"fieldname": "şəhərnömrəsi1",
|
||||
"fieldtype": "Int",
|
||||
"label": "Şəhər nömrəsi 1"
|
||||
},
|
||||
{
|
||||
"fieldname": "şəhərnömrəsi2",
|
||||
"fieldtype": "Int",
|
||||
"label": "Şəhər nömrəsi 2"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_pcfz",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "əsasfəaliyyətnövününkodu",
|
||||
"fieldtype": "Int",
|
||||
"label": "Əsas fəaliyyət növünün kodu"
|
||||
},
|
||||
{
|
||||
"fieldname": "təqdimolunmuşəlavələrinsayı",
|
||||
"fieldtype": "Int",
|
||||
"label": "Təqdim olunmuş əlavələrin sayı"
|
||||
},
|
||||
{
|
||||
"fieldname": "bəyannaməninnövü",
|
||||
"fieldtype": "Select",
|
||||
"label": "Bəyannamənin növü",
|
||||
"options": "Cari\nDəqiqləşdirilmiş\nLəğv olma\nLəğvin dəqiqləşdirilmişi\nAnaloji\nKönüllü açıqlama"
|
||||
},
|
||||
{
|
||||
"fieldname": "dəqiqləşdirilmişbəyannamənintəqdimedilməsibarədəbildiriş",
|
||||
"fieldtype": "Float",
|
||||
"label": "Dəqiqləşdirilmiş bəyannamənin təqdim edilməsi barədə bildirişin nömrəsi",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xcxz",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_vfvd",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bəyan_ediləcək_məlumatım_yoxdur",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bəyan ediləcək məlumatım yoxdur"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_ikbb",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Hesabat ilinin əvvəlinə</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "əlavə_1_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"js_parent_subtab": "Verginin hesablanması",
|
||||
"label": "Əlavə 1"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_ctml",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Büdcəyə ödənilməli verginin məbləği </p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_hken",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Vergidən azad olunan əmlakın dəyəri haqqında məlumat</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_ybie",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Vergiyə cəlb olunan əsas vəsaitin dəyəri haqqında məlumat</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "warning_elave_1_html",
|
||||
"fieldtype": "HTML",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "əmlak_vergisinin_hesablanması_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Əmlak vergisinin hesablanması"
|
||||
},
|
||||
{
|
||||
"fieldname": "əlavə_2_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"js_parent_subtab": "Verginin hesablanması",
|
||||
"label": "Əlavə 2"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fugy",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_exjg",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Artıq qiymətə sığortalanan əsas vəsaitlər</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_bhet",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_tuak",
|
||||
"fieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_bhtd",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_umxi",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Hesabat ilinin sonuna</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_cnpg",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_mxir",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Hesablanmış verginin məbləği</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "idarə",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "İdarə"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_klzd",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_1ci",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Emlak vergi hesablamasi 1ci cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_2ci",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Emlak vergi hesablamasi 2ci cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_3cu",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Emlak vergi hesablamasi 3cu cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_4cu",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Emlak vergi hesablamasi 4cu cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_5ci",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Emlak vergi hesablamasi 5ci cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_6ci",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Emlak vergi hesablamasi 6ci cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "elave_1",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Elave 1"
|
||||
},
|
||||
{
|
||||
"fieldname": "elave_2",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Elave 2"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_wnwr",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "Müəssisə il ərzində tam fəaliyyət göstərdiyi halda",
|
||||
"fieldname": "sətirlərdənbiriniseçin",
|
||||
"fieldtype": "Select",
|
||||
"label": "Sətirlərdən birini seçin",
|
||||
"options": "Müəssisə il ərzində yarandığı halda\nMüəssisə il ərzində tam fəaliyyət göstərdiyi halda"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_dblg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_uhqz",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Müəssisənin yaranma və ya əmlak vergisinin ödəyicisi olma tarixinə</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_1ci_1",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Emlak vergi hesablamasi 1ci cedvel 1"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_4cu_1",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Emlak vergi hesablamasi 4cu cedvel 1"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-29 18:27:42.028445",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
import frappe
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Row catalogues. The strings here must match the Select options of the child
|
||||
# DocTypes byte-for-byte — otherwise Frappe rejects them as invalid options.
|
||||
# Sourced from .etaxes_notes/cabinet_internals.md and the child JSONs in this
|
||||
# folder (their Select options ARE the cabinet labels).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# elave_1 — Vergidən azad olunan əmlakların azadolma səbəbi (Tax Code articles)
|
||||
ELAVE_1_ROWS = (
|
||||
"Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.2-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.3-cü maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.4-cü maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.1-1-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.5-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.7-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.8-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.9-cu maddəsinə əsasən:",
|
||||
"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 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ə:",
|
||||
)
|
||||
ELAVE_1_TOTAL_LABEL = "Vergidən azad olunan əmlakın CƏMİ dəyəri:"
|
||||
|
||||
# elave_2 — Vergiyə cəlb olunan əmlakların kateqoriyası
|
||||
ELAVE_2_PARENT_LABEL = "1. Binalar, tikililər və qurğular"
|
||||
ELAVE_2_CHILD_LABELS = (
|
||||
"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",
|
||||
)
|
||||
ELAVE_2_OTHER_LABELS = (
|
||||
"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",
|
||||
)
|
||||
ELAVE_2_TOTAL_LABEL = "Vergiyə cəlb olunan əmlakın CƏMİ dəyəri:"
|
||||
|
||||
# vergi_hesab_1ci — 501 group, start-of-year totals
|
||||
VH_1CI_ROWS = (
|
||||
"501. Əmlakın ümumi qalıq dəyəri",
|
||||
"501.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"501.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"501.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
)
|
||||
|
||||
# vergi_hesab_1ci_1 — 503 group, start-of-period for partial-year mode
|
||||
VH_1CI1_ROWS = (
|
||||
"503. Əmlakın ümumi dəyəri",
|
||||
"503.1 Vergiyə cəlb olunan əmlakın dəyəri",
|
||||
"503.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın dəyəri",
|
||||
"503.2 Vergidən azad olunan əmlakın dəyəri",
|
||||
)
|
||||
|
||||
# vergi_hesab_2ci — 504 group, end-of-year totals
|
||||
VH_2CI_ROWS = (
|
||||
"504. Əmlakın ümumi qalıq dəyəri",
|
||||
"504.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"504.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"504.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
)
|
||||
|
||||
# vergi_hesab_3cu — 505 group, excess-insured totals
|
||||
VH_3CU_ROWS = (
|
||||
"505 Artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.1 Vergiyə cəlb olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.2 Vergidən azad olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
)
|
||||
|
||||
# vergi_hesab_4cu — 506 tax rate (1% for legal entities)
|
||||
VH_4CU_ROWS = ("506 Verginin dərəcəsi",)
|
||||
|
||||
# vergi_hesab_4cu_1 — 507 number of operating months (partial-year only)
|
||||
VH_4CU1_ROWS = ("507 Müəssisə yarandığı aydan sonra ilin sonuna qədər olan ayların sayı",)
|
||||
|
||||
# vergi_hesab_5ci — 509.x computed tax + 510 reduction
|
||||
VH_5CI_ROWS = (
|
||||
"509.1 Vergi ödəyicisi il ərzində tam fəaliyyət göstərdiyi halda:",
|
||||
"509.2 Hesabat ili ərzində yaradıldığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olduğu halda:",
|
||||
"510 Vergi məcəlləsinin 199.21-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
)
|
||||
|
||||
# vergi_hesab_6ci — 511.x amount payable
|
||||
VH_6CI_ROWS = (
|
||||
"511.1 Müəssisə il ərzində tam fəaliyyət göstərdiyi halda",
|
||||
"511.2 Müəssisə il ərzində yarandığı halda",
|
||||
)
|
||||
|
||||
|
||||
# (table_fieldname, text_column_fieldname, ordered row label tuple)
|
||||
SEED_PLAN = (
|
||||
("vergi_hesab_1ci", "göstəricilər", VH_1CI_ROWS),
|
||||
("vergi_hesab_1ci_1", "göstəricilər", VH_1CI1_ROWS),
|
||||
("vergi_hesab_2ci", "göstəricilər", VH_2CI_ROWS),
|
||||
("vergi_hesab_3cu", "göstəricilər", VH_3CU_ROWS),
|
||||
("vergi_hesab_4cu", "göstəricilər", VH_4CU_ROWS),
|
||||
("vergi_hesab_4cu_1", "göstəricilər", VH_4CU1_ROWS),
|
||||
("vergi_hesab_5ci", "göstəricilər", VH_5CI_ROWS),
|
||||
("vergi_hesab_6ci", "göstəricilər", VH_6CI_ROWS),
|
||||
(
|
||||
"elave_1",
|
||||
"azadolmasəbəbi",
|
||||
ELAVE_1_ROWS + (ELAVE_1_TOTAL_LABEL,),
|
||||
),
|
||||
(
|
||||
"elave_2",
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
(ELAVE_2_PARENT_LABEL,)
|
||||
+ ELAVE_2_CHILD_LABELS
|
||||
+ ELAVE_2_OTHER_LABELS
|
||||
+ (ELAVE_2_TOTAL_LABEL,),
|
||||
),
|
||||
)
|
||||
|
||||
# Float field names inside each elave child row, indexed by the tax code that
|
||||
# the column carries (`(periodPart).(side)`). Side: .1 = taxable (elave_2),
|
||||
# .2 = tax-free (elave_1).
|
||||
ELAVE_1_COLS = {
|
||||
"501.2": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012",
|
||||
"502.2": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"503.2": "hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri",
|
||||
"504.2": "ilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"505.2": "artıqqiymətəəsasvəsaitlərindəyəri",
|
||||
}
|
||||
ELAVE_2_COLS = {
|
||||
"501.1": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"502.1": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"503.1": "hesabatilierzindeuçotaalınan",
|
||||
"504.1": "hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"505.1": "artıqqiymətəəsasvəsaitlərindəyəri505",
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_seed():
|
||||
"""Returns {table_fieldname: [{text_col: label}, ...]} for client-side seeding.
|
||||
|
||||
Called from the local property_tax_return_ai.js on form `onload` for new docs,
|
||||
because server-side onload/before_insert do not fire on the "+ New" Desk path
|
||||
until the user saves. This keeps Python as the single source of truth.
|
||||
"""
|
||||
return {
|
||||
table: [{text_col: label} for label in labels]
|
||||
for table, text_col, labels in SEED_PLAN
|
||||
}
|
||||
|
||||
|
||||
def _round2(value):
|
||||
return float(
|
||||
Decimal(str(float(value or 0))).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
)
|
||||
|
||||
|
||||
def _find_row(table, text_col, value):
|
||||
for row in table or []:
|
||||
if getattr(row, text_col, None) == value:
|
||||
return row
|
||||
return None
|
||||
|
||||
|
||||
class PropertyTaxReturnAI(Document):
|
||||
# ------------------------------------------------------------------
|
||||
# Lifecycle hooks
|
||||
# ------------------------------------------------------------------
|
||||
def onload(self):
|
||||
self._seed_child_tables()
|
||||
|
||||
def before_insert(self):
|
||||
self._seed_child_tables()
|
||||
|
||||
def validate(self):
|
||||
self._seed_child_tables()
|
||||
self.recalculate()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Seeding — populate empty child tables with their cabinet rows so
|
||||
# the user sees a ready-to-fill grid on a fresh "+ New" form.
|
||||
# ------------------------------------------------------------------
|
||||
def _seed_child_tables(self):
|
||||
for table_field, text_col, labels in SEED_PLAN:
|
||||
if self.get(table_field):
|
||||
continue
|
||||
for label in labels:
|
||||
self.append(table_field, {text_col: label})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Recalculation — fires on validate() (every save).
|
||||
# ------------------------------------------------------------------
|
||||
def recalculate(self):
|
||||
e1 = self._recalc_elave_1()
|
||||
e2 = self._recalc_elave_2()
|
||||
self._recalc_vh_1ci(e1, e2)
|
||||
self._recalc_vh_1ci_1(e1, e2)
|
||||
self._recalc_vh_2ci(e1, e2)
|
||||
self._recalc_vh_3cu(e1, e2)
|
||||
self._recalc_vh_4cu()
|
||||
tax_509 = self._recalc_vh_5ci(e2)
|
||||
self._recalc_vh_6ci(tax_509)
|
||||
|
||||
def _recalc_elave_1(self):
|
||||
"""Sum each numeric column over the indicator rows and write into CƏMİ.
|
||||
|
||||
Returns a dict keyed by tax-code (`501.2`, `502.2`, …) with rounded totals.
|
||||
"""
|
||||
totals = {code: 0.0 for code in ELAVE_1_COLS}
|
||||
for row in self.elave_1 or []:
|
||||
if getattr(row, "azadolmasəbəbi", None) == ELAVE_1_TOTAL_LABEL:
|
||||
continue
|
||||
for code, fieldname in ELAVE_1_COLS.items():
|
||||
totals[code] += float(getattr(row, fieldname, 0) or 0)
|
||||
|
||||
cəmi = _find_row(self.elave_1, "azadolmasəbəbi", ELAVE_1_TOTAL_LABEL)
|
||||
if cəmi is not None:
|
||||
for code, fieldname in ELAVE_1_COLS.items():
|
||||
setattr(cəmi, fieldname, _round2(totals[code]))
|
||||
|
||||
return {code: _round2(amount) for code, amount in totals.items()}
|
||||
|
||||
def _recalc_elave_2(self):
|
||||
"""Set _5001 parent row to sum of 1.1–1.6 children, then write CƏMİ totals."""
|
||||
|
||||
# Step 1: roll up sub-categories 1.1–1.6 into the parent "1. Binalar..." row.
|
||||
child_sums = {code: 0.0 for code in ELAVE_2_COLS}
|
||||
for row in self.elave_2 or []:
|
||||
label = getattr(row, "vergiyəcəlbolunanəmlaklarınkateqoriyası", None)
|
||||
if label in ELAVE_2_CHILD_LABELS:
|
||||
for code, fieldname in ELAVE_2_COLS.items():
|
||||
child_sums[code] += float(getattr(row, fieldname, 0) or 0)
|
||||
|
||||
parent = _find_row(
|
||||
self.elave_2,
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
ELAVE_2_PARENT_LABEL,
|
||||
)
|
||||
if parent is not None:
|
||||
for code, fieldname in ELAVE_2_COLS.items():
|
||||
setattr(parent, fieldname, _round2(child_sums[code]))
|
||||
|
||||
# Step 2: CƏMİ totals — exclude parent (avoids double-count) and CƏMİ itself.
|
||||
totals = {code: 0.0 for code in ELAVE_2_COLS}
|
||||
for row in self.elave_2 or []:
|
||||
label = getattr(row, "vergiyəcəlbolunanəmlaklarınkateqoriyası", None)
|
||||
if label in (ELAVE_2_PARENT_LABEL, ELAVE_2_TOTAL_LABEL):
|
||||
continue
|
||||
for code, fieldname in ELAVE_2_COLS.items():
|
||||
totals[code] += float(getattr(row, fieldname, 0) or 0)
|
||||
|
||||
cəmi = _find_row(
|
||||
self.elave_2,
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
ELAVE_2_TOTAL_LABEL,
|
||||
)
|
||||
if cəmi is not None:
|
||||
for code, fieldname in ELAVE_2_COLS.items():
|
||||
setattr(cəmi, fieldname, _round2(totals[code]))
|
||||
|
||||
return {code: _round2(amount) for code, amount in totals.items()}
|
||||
|
||||
def _set_row_amount(self, table_field, text_col, label, amount_col, value):
|
||||
row = _find_row(self.get(table_field), text_col, label)
|
||||
if row is not None:
|
||||
setattr(row, amount_col, _round2(value))
|
||||
|
||||
def _recalc_vh_1ci(self, e1, e2):
|
||||
s = self._set_row_amount
|
||||
s("vergi_hesab_1ci", "göstəricilər", VH_1CI_ROWS[0], "manatla", e1["501.2"] + e2["501.1"])
|
||||
s("vergi_hesab_1ci", "göstəricilər", VH_1CI_ROWS[1], "manatla", e2["501.1"])
|
||||
# 501.1.1 — Qeyri-kommersiya. No accounting integration → permanent 0.
|
||||
s("vergi_hesab_1ci", "göstəricilər", VH_1CI_ROWS[2], "manatla", 0)
|
||||
s("vergi_hesab_1ci", "göstəricilər", VH_1CI_ROWS[3], "manatla", e1["501.2"])
|
||||
|
||||
def _recalc_vh_1ci_1(self, e1, e2):
|
||||
s = self._set_row_amount
|
||||
s("vergi_hesab_1ci_1", "göstəricilər", VH_1CI1_ROWS[0], "manatla", e1["503.2"] + e2["503.1"])
|
||||
s("vergi_hesab_1ci_1", "göstəricilər", VH_1CI1_ROWS[1], "manatla", e2["503.1"])
|
||||
s("vergi_hesab_1ci_1", "göstəricilər", VH_1CI1_ROWS[2], "manatla", 0)
|
||||
s("vergi_hesab_1ci_1", "göstəricilər", VH_1CI1_ROWS[3], "manatla", e1["503.2"])
|
||||
|
||||
def _recalc_vh_2ci(self, e1, e2):
|
||||
s = self._set_row_amount
|
||||
s("vergi_hesab_2ci", "göstəricilər", VH_2CI_ROWS[0], "manatla", e1["504.2"] + e2["504.1"])
|
||||
s("vergi_hesab_2ci", "göstəricilər", VH_2CI_ROWS[1], "manatla", e2["504.1"])
|
||||
s("vergi_hesab_2ci", "göstəricilər", VH_2CI_ROWS[2], "manatla", 0)
|
||||
s("vergi_hesab_2ci", "göstəricilər", VH_2CI_ROWS[3], "manatla", e1["504.2"])
|
||||
|
||||
def _recalc_vh_3cu(self, e1, e2):
|
||||
s = self._set_row_amount
|
||||
s("vergi_hesab_3cu", "göstəricilər", VH_3CU_ROWS[0], "manatla", e1["505.2"] + e2["505.1"])
|
||||
s("vergi_hesab_3cu", "göstəricilər", VH_3CU_ROWS[1], "manatla", e2["505.1"])
|
||||
s("vergi_hesab_3cu", "göstəricilər", VH_3CU_ROWS[2], "manatla", e1["505.2"])
|
||||
|
||||
def _recalc_vh_4cu(self):
|
||||
# Legal-entity rate is 1% (cabinet field `506`).
|
||||
self._set_row_amount(
|
||||
"vergi_hesab_4cu", "göstəricilər", VH_4CU_ROWS[0], "faizlə", 1.0
|
||||
)
|
||||
|
||||
def _recalc_vh_5ci(self, e2):
|
||||
"""Compute 509.1 (full-year) or 509.2 (partial-year) per Object Activity flag.
|
||||
|
||||
Returns the computed tax for the active scenario, which `_recalc_vh_6ci`
|
||||
feeds into 511.x.
|
||||
"""
|
||||
months_row = _find_row(
|
||||
self.get("vergi_hesab_4cu_1"), "göstəricilər", VH_4CU1_ROWS[0]
|
||||
)
|
||||
months = float(getattr(months_row, "ayla", 0) or 0) if months_row else 0.0
|
||||
|
||||
s501 = e2["501.1"]
|
||||
s503 = e2["503.1"]
|
||||
s504 = e2["504.1"]
|
||||
s505 = e2["505.1"]
|
||||
|
||||
# Verified vs cabinet — see .etaxes_notes/cabinet_internals.md, sections
|
||||
# "THE main tax formula" (509.1) and "Partial-year tax formula" (509.2).
|
||||
tax_full = ((s501 + s504) / 2.0) * 0.01 + s505 * 0.01
|
||||
tax_partial = (
|
||||
(((s503 + s504) / 24.0) * months) * 0.01 + s505 * 0.01
|
||||
if months
|
||||
else s505 * 0.01
|
||||
)
|
||||
|
||||
if bool(self.il_erzinde):
|
||||
tax_509_2 = _round2(tax_partial)
|
||||
self._set_row_amount("vergi_hesab_5ci", "göstəricilər", VH_5CI_ROWS[0], "manatla", 0)
|
||||
self._set_row_amount("vergi_hesab_5ci", "göstəricilər", VH_5CI_ROWS[1], "manatla", tax_509_2)
|
||||
return tax_509_2
|
||||
|
||||
tax_509_1 = _round2(tax_full)
|
||||
self._set_row_amount("vergi_hesab_5ci", "göstəricilər", VH_5CI_ROWS[0], "manatla", tax_509_1)
|
||||
self._set_row_amount("vergi_hesab_5ci", "göstəricilər", VH_5CI_ROWS[1], "manatla", 0)
|
||||
return tax_509_1
|
||||
|
||||
def _recalc_vh_6ci(self, tax_509):
|
||||
# 511.x mirrors 509.x — same as cabinet (`shouldBePaidTax[0]` ==
|
||||
# `calculatedTax[0]`) and the original `calculate_509_*` in
|
||||
# property_tax_return.js. 510 ("shouldBeReducedTax") is reported in its
|
||||
# own row but is NOT subtracted from the payable amount.
|
||||
payable = _round2(tax_509)
|
||||
if bool(self.il_erzinde):
|
||||
self._set_row_amount("vergi_hesab_6ci", "göstəricilər", VH_6CI_ROWS[0], "manatla", 0)
|
||||
self._set_row_amount("vergi_hesab_6ci", "göstəricilər", VH_6CI_ROWS[1], "manatla", payable)
|
||||
else:
|
||||
self._set_row_amount("vergi_hesab_6ci", "göstəricilər", VH_6CI_ROWS[0], "manatla", payable)
|
||||
self._set_row_amount("vergi_hesab_6ci", "göstəricilər", VH_6CI_ROWS[1], "manatla", 0)
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
"""Programmatic smoke test for Property Tax Return AI formulas.
|
||||
|
||||
Run via:
|
||||
bench --site frontend execute \
|
||||
taxes_az.taxes_az.doctype.property_tax_return_ai.test_smoke.smoke_run
|
||||
|
||||
Verifies the two cabinet-verified scenarios from .etaxes_notes/cabinet_internals.md:
|
||||
- Full-year (tam_il): elave_2[1.1] periodEnd=200 + excessInsured=300 → 509.1 = 4.00
|
||||
- Partial-year (il_erzinde): elave_2[1.1] onCreate=100 + periodEnd=200 + excessInsured=300,
|
||||
507.ayla=6 → 509.2 = 3.75
|
||||
"""
|
||||
import frappe
|
||||
|
||||
from taxes_az.taxes_az.doctype.property_tax_return_ai.property_tax_return_ai import (
|
||||
ELAVE_2_CHILD_LABELS,
|
||||
ELAVE_2_TOTAL_LABEL,
|
||||
ELAVE_2_PARENT_LABEL,
|
||||
VH_5CI_ROWS,
|
||||
VH_6CI_ROWS,
|
||||
VH_4CU_ROWS,
|
||||
VH_4CU1_ROWS,
|
||||
VH_2CI_ROWS,
|
||||
VH_3CU_ROWS,
|
||||
VH_1CI1_ROWS,
|
||||
)
|
||||
|
||||
|
||||
def _check(label, expected, actual, tol=0.005):
|
||||
ok = abs(float(actual or 0) - float(expected)) <= tol
|
||||
icon = "✓" if ok else "✗"
|
||||
print(f" {icon} {label}: expected={expected} actual={actual}")
|
||||
return ok
|
||||
|
||||
|
||||
def _row(table, text_col, label):
|
||||
for r in table:
|
||||
if getattr(r, text_col, None) == label:
|
||||
return r
|
||||
raise LookupError(f"row not found: {label!r} in {text_col!r}")
|
||||
|
||||
|
||||
def _make_doc():
|
||||
doc = frappe.new_doc("Property Tax Return AI")
|
||||
doc.il = 2025
|
||||
doc.vergidövrü = "İllik"
|
||||
doc.ödəyicitipi = "Hüquqi"
|
||||
doc.run_method("before_insert") # seed all child tables
|
||||
return doc
|
||||
|
||||
|
||||
def run_seeding():
|
||||
print("\n=== TEST 0: seeding populates all 10 child tables ===")
|
||||
expectations = [
|
||||
("vergi_hesab_1ci", 4),
|
||||
("vergi_hesab_1ci_1", 4),
|
||||
("vergi_hesab_2ci", 4),
|
||||
("vergi_hesab_3cu", 3),
|
||||
("vergi_hesab_4cu", 1),
|
||||
("vergi_hesab_4cu_1", 1),
|
||||
("vergi_hesab_5ci", 3),
|
||||
("vergi_hesab_6ci", 2),
|
||||
("elave_1", 22), # 21 indicators + CƏMİ
|
||||
("elave_2", 12), # 1 parent + 6 children + 4 other + CƏMİ
|
||||
]
|
||||
|
||||
# Path 1: before_insert (REST/API creation).
|
||||
doc_bi = frappe.new_doc("Property Tax Return AI")
|
||||
doc_bi.run_method("before_insert")
|
||||
|
||||
# Path 2: onload (Desk "+ New" form open).
|
||||
doc_ol = frappe.new_doc("Property Tax Return AI")
|
||||
doc_ol.run_method("onload")
|
||||
|
||||
results = []
|
||||
for table_field, expected_len in expectations:
|
||||
results.append(
|
||||
_check(f"before_insert: {table_field}", expected_len, len(doc_bi.get(table_field) or []), tol=0)
|
||||
)
|
||||
results.append(
|
||||
_check(f"onload: {table_field}", expected_len, len(doc_ol.get(table_field) or []), tol=0)
|
||||
)
|
||||
return all(results)
|
||||
|
||||
|
||||
def run_full_year():
|
||||
print("\n=== TEST A: tam_il (full-year, 509.1 expected 4.00) ===")
|
||||
doc = _make_doc()
|
||||
doc.tam_il = 1
|
||||
doc.il_erzinde = 0
|
||||
|
||||
# Cabinet-verified inputs: taxable[_5002] (= "1.1 Çoxmərtəbəli...") with
|
||||
# 501.1 = 0, 504.1 = 200, 505.1 = 300.
|
||||
sub = _row(
|
||||
doc.elave_2, "vergiyəcəlbolunanəmlaklarınkateqoriyası", ELAVE_2_CHILD_LABELS[0]
|
||||
)
|
||||
sub.hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri = 0 # 501.1
|
||||
sub.hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri = 200 # 504.1
|
||||
sub.artıqqiymətəəsasvəsaitlərindəyəri505 = 300 # 505.1
|
||||
|
||||
doc.recalculate()
|
||||
|
||||
# Parent _5001 must auto-sum 1.1–1.6 children. Only 1.1 has values → identical.
|
||||
parent = _row(
|
||||
doc.elave_2,
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
ELAVE_2_PARENT_LABEL,
|
||||
)
|
||||
e2_cəmi = _row(
|
||||
doc.elave_2,
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
ELAVE_2_TOTAL_LABEL,
|
||||
)
|
||||
vh2 = _row(doc.vergi_hesab_2ci, "göstəricilər", VH_2CI_ROWS[1]) # 504.1
|
||||
vh3 = _row(doc.vergi_hesab_3cu, "göstəricilər", VH_3CU_ROWS[1]) # 505.1
|
||||
vh4 = _row(doc.vergi_hesab_4cu, "göstəricilər", VH_4CU_ROWS[0]) # 506 rate
|
||||
vh5_1 = _row(doc.vergi_hesab_5ci, "göstəricilər", VH_5CI_ROWS[0]) # 509.1
|
||||
vh5_2 = _row(doc.vergi_hesab_5ci, "göstəricilər", VH_5CI_ROWS[1]) # 509.2
|
||||
vh6_1 = _row(doc.vergi_hesab_6ci, "göstəricilər", VH_6CI_ROWS[0]) # 511.1
|
||||
vh6_2 = _row(doc.vergi_hesab_6ci, "göstəricilər", VH_6CI_ROWS[1]) # 511.2
|
||||
|
||||
results = []
|
||||
results.append(_check("parent _5001.504.1 auto-sum", 200, parent.hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri))
|
||||
results.append(_check("parent _5001.505.1 auto-sum", 300, parent.artıqqiymətəəsasvəsaitlərindəyəri505))
|
||||
# CƏMİ totals must EXCLUDE the parent (no double-count).
|
||||
results.append(_check("elave_2 CƏMİ 504.1", 200, e2_cəmi.hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri))
|
||||
results.append(_check("elave_2 CƏMİ 505.1", 300, e2_cəmi.artıqqiymətəəsasvəsaitlərindəyəri505))
|
||||
# 504.1 / 505.1 mirrors into vergi_hesab_2ci / 3cu
|
||||
results.append(_check("vergi_hesab_2ci 504.1.manatla", 200, vh2.manatla))
|
||||
results.append(_check("vergi_hesab_3cu 505.1.manatla", 300, vh3.manatla))
|
||||
# 506 rate = 1
|
||||
results.append(_check("vergi_hesab_4cu 506.faizlə", 1.0, vh4.faizlə))
|
||||
# Tax 509.1 = ((0+200)/2)*0.01 + 300*0.01 = 1 + 3 = 4.00
|
||||
results.append(_check("509.1 full-year tax", 4.00, vh5_1.manatla))
|
||||
results.append(_check("509.2 (unused in full-year)", 0, vh5_2.manatla))
|
||||
# 511.1 mirrors 509.1 (cabinet `shouldBePaidTax[0]` == `calculatedTax[0]`)
|
||||
results.append(_check("511.1 mirrors 509.1", 4.00, vh6_1.manatla))
|
||||
results.append(_check("511.2 (unused in full-year)", 0, vh6_2.manatla))
|
||||
return all(results)
|
||||
|
||||
|
||||
def run_partial_year():
|
||||
print("\n=== TEST B: il_erzinde (partial-year, 509.2 expected 3.75) ===")
|
||||
doc = _make_doc()
|
||||
doc.tam_il = 0
|
||||
doc.il_erzinde = 1
|
||||
|
||||
# Cabinet-verified inputs for OBJECT_ESTABLISHED (2026-05-29):
|
||||
# taxable[_5002].onCreateDate=100, periodEnd=200, excessInsured=300
|
||||
# months = 6
|
||||
# Expected: ((100+200)/24 * 6)*0.01 + 300*0.01 = 0.75 + 3 = 3.75
|
||||
sub = _row(
|
||||
doc.elave_2, "vergiyəcəlbolunanəmlaklarınkateqoriyası", ELAVE_2_CHILD_LABELS[0]
|
||||
)
|
||||
sub.hesabatilierzindeuçotaalınan = 100 # 503.1
|
||||
sub.hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri = 200 # 504.1
|
||||
sub.artıqqiymətəəsasvəsaitlərindəyəri505 = 300 # 505.1
|
||||
|
||||
months_row = _row(doc.vergi_hesab_4cu_1, "göstəricilər", VH_4CU1_ROWS[0])
|
||||
months_row.ayla = 6
|
||||
|
||||
doc.recalculate()
|
||||
|
||||
vh1_1 = _row(doc.vergi_hesab_1ci_1, "göstəricilər", VH_1CI1_ROWS[1]) # 503.1
|
||||
vh5_1 = _row(doc.vergi_hesab_5ci, "göstəricilər", VH_5CI_ROWS[0]) # 509.1
|
||||
vh5_2 = _row(doc.vergi_hesab_5ci, "göstəricilər", VH_5CI_ROWS[1]) # 509.2
|
||||
vh6_1 = _row(doc.vergi_hesab_6ci, "göstəricilər", VH_6CI_ROWS[0]) # 511.1
|
||||
vh6_2 = _row(doc.vergi_hesab_6ci, "göstəricilər", VH_6CI_ROWS[1]) # 511.2
|
||||
|
||||
results = []
|
||||
results.append(_check("vergi_hesab_1ci_1 503.1.manatla", 100, vh1_1.manatla))
|
||||
results.append(_check("509.1 (unused in partial-year)", 0, vh5_1.manatla))
|
||||
results.append(_check("509.2 partial-year tax", 3.75, vh5_2.manatla))
|
||||
results.append(_check("511.1 (unused in partial-year)", 0, vh6_1.manatla))
|
||||
results.append(_check("511.2 payable", 3.75, vh6_2.manatla))
|
||||
return all(results)
|
||||
|
||||
|
||||
def run_elave_1_totals():
|
||||
print("\n=== TEST C: elave_1 CƏMİ totals ===")
|
||||
doc = _make_doc()
|
||||
# Fill 3 different rows in 2 columns. CƏMİ row should reflect the sums.
|
||||
doc.elave_1[0].hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012 = 100 # 501.2
|
||||
doc.elave_1[0].ilinsonunaəsasvəsaitlərinqalıqdəyəri = 50 # 504.2
|
||||
doc.elave_1[5].hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012 = 200
|
||||
doc.elave_1[10].hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012 = 400
|
||||
doc.elave_1[10].ilinsonunaəsasvəsaitlərinqalıqdəyəri = 33
|
||||
|
||||
doc.recalculate()
|
||||
|
||||
cəmi = _row(doc.elave_1, "azadolmasəbəbi", "Vergidən azad olunan əmlakın CƏMİ dəyəri:")
|
||||
results = []
|
||||
results.append(_check("elave_1 CƏMİ 501.2", 700, cəmi.hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012))
|
||||
results.append(_check("elave_1 CƏMİ 504.2", 83, cəmi.ilinsonunaəsasvəsaitlərinqalıqdəyəri))
|
||||
return all(results)
|
||||
|
||||
|
||||
def run_persistence():
|
||||
print("\n=== TEST D: round-trip persistence ===")
|
||||
doc = _make_doc()
|
||||
doc.tam_il = 1
|
||||
doc.il_erzinde = 0
|
||||
sub = _row(
|
||||
doc.elave_2, "vergiyəcəlbolunanəmlaklarınkateqoriyası", ELAVE_2_CHILD_LABELS[0]
|
||||
)
|
||||
sub.hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri = 200
|
||||
sub.artıqqiymətəəsasvəsaitlərindəyəri505 = 300
|
||||
doc.insert(ignore_permissions=True)
|
||||
name = doc.name
|
||||
frappe.db.commit()
|
||||
|
||||
reloaded = frappe.get_doc("Property Tax Return AI", name)
|
||||
vh5_1 = _row(reloaded.vergi_hesab_5ci, "göstəricilər", VH_5CI_ROWS[0])
|
||||
vh6_1 = _row(reloaded.vergi_hesab_6ci, "göstəricilər", VH_6CI_ROWS[0])
|
||||
|
||||
results = []
|
||||
results.append(_check("reloaded elave_2 row count", 12, len(reloaded.elave_2), tol=0))
|
||||
results.append(_check("reloaded 509.1", 4.00, vh5_1.manatla))
|
||||
results.append(_check("reloaded 511.1", 4.00, vh6_1.manatla))
|
||||
|
||||
# Cleanup
|
||||
frappe.delete_doc("Property Tax Return AI", name, force=1)
|
||||
frappe.db.commit()
|
||||
return all(results)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def smoke_run():
|
||||
a = run_seeding()
|
||||
b = run_full_year()
|
||||
c = run_partial_year()
|
||||
d = run_elave_1_totals()
|
||||
e = run_persistence()
|
||||
print(f"\n=== Summary: seeding={a} full={b} partial={c} elave_1={d} persist={e} ===")
|
||||
print("ALL OK" if (a and b and c and d and e) else "FAILED")
|
||||
return {"seeding": a, "full_year": b, "partial_year": c, "elave_1": d, "persistence": e}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 15:04:47.107017",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"azadolmasəbəbi",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012",
|
||||
"ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri",
|
||||
"ilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "azadolmasəbəbi",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Azadolma səbəbi",
|
||||
"length": 500,
|
||||
"options": "Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.4.2-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.4.3-cü maddəsinə əsasən:\nVergi Məcəlləsinin 199.4.4-cü maddəsinə əsasən:\nVergi Məcəlləsinin 199.4.1-1-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.5-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.7-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.8-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.9-cu maddəsinə əsasən:\nVergi Məcəlləsinin 199.11-cu maddəsinə əsasən:\nVergi Məcəlləsinin 199.14-cu maddəsinə əsasən:\nVergi Məcəlləsinin 199.15-cu maddəsinə əsasən:\nVergi Məcəlləsinin 199.16-cı maddəsinə əsasən:\nVergi Məcəlləsinin 199.17-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.18-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.19-cu maddəsinə əsasən:\nVergi Məcəlləsinin 227.1-ci maddəsinə əsasən:\nQanunla 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ə:\nXüsusi iqtisadi zonalar haqqında qanunlarda nəzərdə tutulan əmlaklar üzrə:\nAzərbaycan Respublikasının tərəfdar çıxdığı beynəlxalq müqavilələr üzrə:\nVergidən azad olunan əmlakın CƏMİ dəyəri:",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Yeni yaradıldığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olma tarixinə əsas vəsaitlərin qalıq dəyəri (manatla) (503.2)\t",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Vergi ödəyicisinin ləğv olunma və ya uçotdan çıxarılma tarixinə əsas vəsaitlərin qalıq dəyəri (manatla) (502.2)",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Vergi ödəyicisinin ilin sonuna əsas vəsaitlərinin qalıq dəyəri (manatla) (504.2)"
|
||||
},
|
||||
{
|
||||
"fieldname": "artıqqiymətəəsasvəsaitlərindəyəri",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Artıq qiymətə sığortalanan əsas vəsaitlərin dəyəri (manatla) (505.2)"
|
||||
},
|
||||
{
|
||||
"fieldname": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Hesabat ilinin əvvəlinə əsas vəsaitlərin qalıq dəyəri (manatla) (501.2)"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-05 14:25:59.341379",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Elave 1",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIElave1(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 16:36:30.780093",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"hesabatilierzindeuçotaalınan",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Vergi ödəyicisinin ləğv olunma və ya uçotdan çıxarılma tarixinə əsas vəsaitlərin qalıq dəyəri (manatla) (502.1)",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Vergiyə cəlb olunan əmlakların kateqoriyası",
|
||||
"options": "1. Binalar, tikililər və qurğular\n1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları\n1.2 Qeyri-yaşayış binaları (sahələri)\n1.3 Əmlak kompleksi kimi müəssisələr\n1.4 Qurğular\n1.5 Mənzillər\n1.6 Fərdi yaşayış və bağ evləri\n2 Maşınlar və avadanlıqlar\n3 Yüksək texnologiyalar məhsulu olan hesablama texnikası\n4 Nəqliyyat vasitələri\n5 Digər əsas vəsaitlər\nVergiyə cəlb olunan əmlakın CƏMİ dəyəri:",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Hesabat ilinin əvvəlinə əsas vəsaitlərin qalıq dəyəri (manatla) (501.1)",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
"fieldname": "hesabatilierzindeuçotaalınan",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Hesabat ili ərzində yarandığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olma tarixinə əsas vəsaitlərin qalıq dəyəri (manatla) (503.1)",
|
||||
"precision": "2",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Hesabat ilinin sonuna əsas vəsaitlərinin qalıq dəyəri (manatla) (504.1)",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
"fieldname": "artıqqiymətəəsasvəsaitlərindəyəri505",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Artıq qiymətə sığotalanan əsas vəsaitlərin dəyəri (manatla) (505.1)",
|
||||
"precision": "2"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-08 18:48:57.656460",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Elave 2",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIElave2(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 14:28:57.994528",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "501. Əmlakın ümumi qalıq dəyəri\n501.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri\n501.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri\n501.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 19:47:26.620548",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Emlak vergi hesablamasi 1ci cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIEmlakvergihesablamasi1cicedvel(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-08-04 12:02:18.835114",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "503. Əmlakın ümumi dəyəri\n503.1 Vergiyə cəlb olunan əmlakın dəyəri\n503.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın dəyəri\n503.2 Vergidən azad olunan əmlakın dəyəri",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 19:38:26.382915",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Emlak vergi hesablamasi 1ci cedvel 1",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIEmlakvergihesablamasi1cicedvel1(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 14:36:20.467665",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "504. Əmlakın ümumi qalıq dəyəri\n504.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri\n504.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri\n504.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 12:00:07.915973",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Emlak vergi hesablamasi 2ci cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIEmlakvergihesablamasi2cicedvel(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 14:35:10.099838",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "505 Artıq qiymətə sığortalanan əmlakın dəyəri\n505.1 Vergiyə cəlb olunan artıq qiymətə sığortalanan əmlakın dəyəri\n505.2 Vergidən azad olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 12:00:44.910640",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Emlak vergi hesablamasi 3cu cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIEmlakvergihesablamasi3cucedvel(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 14:36:07.154586",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"faizlə"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "506 Verginin dərəcəsi",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "faizlə",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Faizlə %",
|
||||
"precision": "2",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-29 18:09:10.263087",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Emlak vergi hesablamasi 4cu cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIEmlakvergihesablamasi4cucedvel(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-08-04 19:40:22.377464",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"ayla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "507 Müəssisə yarandığı aydan sonra ilin sonuna qədər olan ayların sayı",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ayla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Ayla"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 19:41:48.738524",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Emlak vergi hesablamasi 4cu cedvel 1",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIEmlakvergihesablamasi4cucedvel1(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 15:00:15.158446",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "509.1 Vergi ödəyicisi il ərzində tam fəaliyyət göstərdiyi halda:\n509.2 Hesabat ili ərzində yaradıldığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olduğu halda:\n510 Vergi məcəlləsinin 199.21-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği"
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla",
|
||||
"precision": "2"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-07 14:25:22.271312",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Emlak vergi hesablamasi 5ci cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIEmlakvergihesablamasi5cicedvel(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-08-04 14:47:33.216880",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "511.1 Müəssisə il ərzində tam fəaliyyət göstərdiyi halda\n511.2 Müəssisə il ərzində yarandığı halda",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 21:25:48.999866",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Emlak vergi hesablamasi 6ci cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIEmlakvergihesablamasi6cicedvel(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,555 @@
|
|||
"""Numeric parity harness: PropertyTaxReturnAIFill vs. the original Property tax return.
|
||||
|
||||
For each scenario we:
|
||||
1. Build a `Property tax return` doc, seed its 10 child tables exactly the way
|
||||
the original `Default*` Client Scripts do, fill the numeric cells the user
|
||||
would have typed, then run a faithful Python port of the original calc
|
||||
pipeline (property_tax_return.js + formula_editor DSL `table(...)`).
|
||||
2. Build a `Property Tax Return AI Fill` doc with the same inputs, run validate.
|
||||
3. Compare every computed cell of every child table and print a diff.
|
||||
|
||||
Run via:
|
||||
bench --site frontend execute \
|
||||
taxes_az.taxes_az.doctype.property_tax_return_ai_fill.parity_check.run
|
||||
"""
|
||||
import frappe
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Faithful Python port of the original calc pipeline.
|
||||
# Sources:
|
||||
# • taxes_az/taxes_az/doctype/property_tax_return/property_tax_return.js
|
||||
# calculate_binalar_totals, calculate_elave_1_totals, calculate_elave_2_totals,
|
||||
# calculate_vergi_hesab_3cu_totals, calculate_509_1, calculate_509_2,
|
||||
# filter_vergi_hesab_5ci_rows / filter_vergi_hesab_6ci_rows
|
||||
# • Formula Editor record "Əmlak vergisi" (table_row_formulas_storage)
|
||||
# row formulas for vergi_hesab_1ci, vergi_hesab_1ci_1, vergi_hesab_2ci,
|
||||
# vergi_hesab_4cu.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ELAVE_1_TOTAL = "Vergidən azad olunan əmlakın CƏMİ dəyəri:"
|
||||
ELAVE_2_PARENT = "1. Binalar, tikililər və qurğular"
|
||||
ELAVE_2_TOTAL = "Vergiyə cəlb olunan əmlakın CƏMİ dəyəri:"
|
||||
|
||||
ELAVE_1_COLS = (
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012", # 501.2
|
||||
"ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri", # 502.2
|
||||
"hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri", # 503.2
|
||||
"ilinsonunaəsasvəsaitlərinqalıqdəyəri", # 504.2
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri", # 505.2
|
||||
)
|
||||
ELAVE_2_COLS = (
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri", # 501.1
|
||||
"ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri", # 502.1
|
||||
"hesabatilierzindeuçotaalınan", # 503.1
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", # 504.1
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505", # 505.1
|
||||
)
|
||||
|
||||
|
||||
def flt(v):
|
||||
try:
|
||||
return float(v) if v not in (None, "") else 0.0
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def calc_binalar_totals_orig(elave_2_rows):
|
||||
parent = None
|
||||
children = []
|
||||
for r in elave_2_rows:
|
||||
cat = r.get("vergiyəcəlbolunanəmlaklarınkateqoriyası", "") or ""
|
||||
if cat == ELAVE_2_PARENT:
|
||||
parent = r
|
||||
elif len(cat) >= 3 and cat.startswith("1.") and cat[2] in "123456":
|
||||
children.append(r)
|
||||
if not parent:
|
||||
return
|
||||
for col in ELAVE_2_COLS:
|
||||
parent[col] = sum(flt(c.get(col)) for c in children)
|
||||
|
||||
|
||||
def calc_elave_1_totals_orig(elave_1_rows):
|
||||
total = None
|
||||
others = []
|
||||
for r in elave_1_rows:
|
||||
if r.get("azadolmasəbəbi") == ELAVE_1_TOTAL:
|
||||
total = r
|
||||
else:
|
||||
others.append(r)
|
||||
if not total:
|
||||
return
|
||||
for col in ELAVE_1_COLS:
|
||||
total[col] = sum(flt(o.get(col)) for o in others)
|
||||
|
||||
|
||||
def calc_elave_2_totals_orig(elave_2_rows):
|
||||
total = None
|
||||
others = []
|
||||
for r in elave_2_rows:
|
||||
cat = r.get("vergiyəcəlbolunanəmlaklarınkateqoriyası", "") or ""
|
||||
if cat == ELAVE_2_TOTAL:
|
||||
total = r
|
||||
elif cat != ELAVE_2_PARENT:
|
||||
others.append(r)
|
||||
if not total:
|
||||
return
|
||||
for col in ELAVE_2_COLS:
|
||||
total[col] = sum(flt(o.get(col)) for o in others)
|
||||
|
||||
|
||||
def calc_vh_3cu_totals_orig(elave_1_rows, elave_2_rows, vh_3cu_rows):
|
||||
sum_505_2 = sum(
|
||||
flt(r.get("artıqqiymətəəsasvəsaitlərindəyəri"))
|
||||
for r in elave_1_rows
|
||||
if r.get("azadolmasəbəbi") != ELAVE_1_TOTAL
|
||||
)
|
||||
sum_505_1 = sum(
|
||||
flt(r.get("artıqqiymətəəsasvəsaitlərindəyəri505"))
|
||||
for r in elave_2_rows
|
||||
if r.get("vergiyəcəlbolunanəmlaklarınkateqoriyası") not in (ELAVE_2_PARENT, ELAVE_2_TOTAL)
|
||||
)
|
||||
for r in vh_3cu_rows:
|
||||
g = r.get("göstəricilər") or ""
|
||||
if g.startswith("505.1 "):
|
||||
r["manatla"] = sum_505_1
|
||||
elif g.startswith("505.2 "):
|
||||
r["manatla"] = sum_505_2
|
||||
elif g.startswith("505 "):
|
||||
r["manatla"] = sum_505_1 + sum_505_2
|
||||
|
||||
|
||||
# --- Formula Editor DSL: table(elave_X, idx, col) reads row #idx (1-indexed)
|
||||
def calc_vh_1ci_orig(elave_1_rows, elave_2_rows, vh_1ci_rows):
|
||||
"""Mirrors formula_editor "Əmlak vergisi" rows for vergi_hesab_1ci."""
|
||||
e1_23 = elave_1_rows[22] if len(elave_1_rows) >= 23 else {}
|
||||
e2_12 = elave_2_rows[11] if len(elave_2_rows) >= 12 else {}
|
||||
v_e1_501_2 = flt(e1_23.get("hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012"))
|
||||
v_e2_501_1 = flt(e2_12.get("hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri"))
|
||||
for i, r in enumerate(vh_1ci_rows):
|
||||
g = r.get("göstəricilər") or ""
|
||||
if i == 0 and g.startswith("501."):
|
||||
r["manatla"] = v_e1_501_2 + v_e2_501_1
|
||||
elif g.startswith("501.1 "):
|
||||
r["manatla"] = v_e2_501_1
|
||||
elif g.startswith("501.2 "):
|
||||
r["manatla"] = v_e1_501_2
|
||||
# 501.1.1 stays user-input (no formula in DSL)
|
||||
|
||||
|
||||
def calc_vh_1ci_1_orig(elave_1_rows, elave_2_rows, vh_1ci_1_rows):
|
||||
e1_23 = elave_1_rows[22] if len(elave_1_rows) >= 23 else {}
|
||||
e2_12 = elave_2_rows[11] if len(elave_2_rows) >= 12 else {}
|
||||
v_e1_503_2 = flt(e1_23.get("hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri"))
|
||||
v_e2_503_1 = flt(e2_12.get("hesabatilierzindeuçotaalınan"))
|
||||
for i, r in enumerate(vh_1ci_1_rows):
|
||||
g = r.get("göstəricilər") or ""
|
||||
if i == 0 and g.startswith("503."):
|
||||
r["manatla"] = v_e1_503_2 + v_e2_503_1
|
||||
elif g.startswith("503.1 "):
|
||||
r["manatla"] = v_e2_503_1
|
||||
elif g.startswith("503.2 "):
|
||||
r["manatla"] = v_e1_503_2
|
||||
|
||||
|
||||
def calc_vh_2ci_orig(elave_1_rows, elave_2_rows, vh_2ci_rows):
|
||||
e1_23 = elave_1_rows[22] if len(elave_1_rows) >= 23 else {}
|
||||
e2_12 = elave_2_rows[11] if len(elave_2_rows) >= 12 else {}
|
||||
v_e1_504_2 = flt(e1_23.get("ilinsonunaəsasvəsaitlərinqalıqdəyəri"))
|
||||
v_e2_504_1 = flt(e2_12.get("hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri"))
|
||||
for i, r in enumerate(vh_2ci_rows):
|
||||
g = r.get("göstəricilər") or ""
|
||||
if i == 0 and g.startswith("504."):
|
||||
r["manatla"] = v_e1_504_2 + v_e2_504_1
|
||||
elif g.startswith("504.1 "):
|
||||
r["manatla"] = v_e2_504_1
|
||||
elif g.startswith("504.2 "):
|
||||
r["manatla"] = v_e1_504_2
|
||||
|
||||
|
||||
def calc_vh_4cu_orig(vh_4cu_rows):
|
||||
if vh_4cu_rows:
|
||||
vh_4cu_rows[0]["faizlə"] = 1
|
||||
|
||||
|
||||
def calc_509_1_orig(tam_il, vh_1ci_rows, vh_2ci_rows, vh_3cu_rows, vh_5ci_rows, vh_6ci_rows):
|
||||
if not tam_il:
|
||||
return
|
||||
v_501_1 = next((flt(r["manatla"]) for r in vh_1ci_rows if (r.get("göstəricilər") or "").startswith("501.1 ")), 0.0)
|
||||
v_504_1 = next((flt(r["manatla"]) for r in vh_2ci_rows if (r.get("göstəricilər") or "").startswith("504.1 ")), 0.0)
|
||||
v_505_1 = next((flt(r["manatla"]) for r in vh_3cu_rows if (r.get("göstəricilər") or "").startswith("505.1 ")), 0.0)
|
||||
result = ((v_501_1 + v_504_1) / 2.0) * 0.01 + v_505_1 * 0.01
|
||||
for r in vh_5ci_rows:
|
||||
if (r.get("göstəricilər") or "").startswith("509.1 "):
|
||||
r["manatla"] = result
|
||||
for r in vh_6ci_rows:
|
||||
if (r.get("göstəricilər") or "").startswith("511.1 "):
|
||||
r["manatla"] = result
|
||||
|
||||
|
||||
def calc_509_2_orig(il_erzinde, vh_1ci_1_rows, vh_2ci_rows, vh_3cu_rows, vh_4cu_1_rows, vh_5ci_rows, vh_6ci_rows):
|
||||
if not il_erzinde:
|
||||
return
|
||||
v_507 = flt(vh_4cu_1_rows[0].get("ayla")) if vh_4cu_1_rows else 0.0
|
||||
v_503_1 = next((flt(r["manatla"]) for r in vh_1ci_1_rows if (r.get("göstəricilər") or "").startswith("503.1 ")), 0.0)
|
||||
v_504_1 = next((flt(r["manatla"]) for r in vh_2ci_rows if (r.get("göstəricilər") or "").startswith("504.1 ")), 0.0)
|
||||
v_505_1 = next((flt(r["manatla"]) for r in vh_3cu_rows if (r.get("göstəricilər") or "").startswith("505.1 ")), 0.0)
|
||||
result = ((v_503_1 + v_504_1) / 24.0 * v_507) * 0.01 + v_505_1 * 0.01
|
||||
for r in vh_5ci_rows:
|
||||
if (r.get("göstəricilər") or "").startswith("509.2 "):
|
||||
r["manatla"] = result
|
||||
for r in vh_6ci_rows:
|
||||
if (r.get("göstəricilər") or "").startswith("511.2 "):
|
||||
r["manatla"] = result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Seed exactly the same row labels the original `Default*` Client Scripts use.
|
||||
# Pulled verbatim from `tabClient Script` records (DefaultProperty tax return *).
|
||||
# These differ from the AI clone's Select options (extra "199.20" indicator,
|
||||
# different 199.1 text, etc.) — that's the original's documented set, not ours.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ORIG_ELAVE_1_LABELS = [
|
||||
"Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
"Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.2-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.3-cü maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.4-cü maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.1-1-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.5-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.7-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.8-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.9-cu maddəsinə əsasən:",
|
||||
"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:",
|
||||
]
|
||||
ORIG_ELAVE_2_LABELS = [
|
||||
"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:",
|
||||
]
|
||||
ORIG_VH_1CI = [
|
||||
"501. Əmlakın ümumi qalıq dəyəri",
|
||||
"501.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"501.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"501.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
]
|
||||
ORIG_VH_1CI_1 = [
|
||||
"503. Əmlakın ümumi dəyəri",
|
||||
"503.1 Vergiyə cəlb olunan əmlakın dəyəri",
|
||||
"503.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın dəyəri",
|
||||
"503.2 Vergidən azad olunan əmlakın dəyəri",
|
||||
]
|
||||
ORIG_VH_2CI = [
|
||||
"504. Əmlakın ümumi qalıq dəyəri",
|
||||
"504.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"504.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"504.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
]
|
||||
ORIG_VH_3CU = [
|
||||
"505 Artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.1 Vergiyə cəlb olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.2 Vergidən azad olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
]
|
||||
ORIG_VH_4CU = ["506 Verginin dərəcəsi"]
|
||||
ORIG_VH_4CU_1 = ["507 Müəssisə yarandığı aydan sonra ilin sonuna qədər olan ayların sayı"]
|
||||
# In tam_il mode the original's filter strips 509.2 / 511.2; in il_erzinde mode
|
||||
# it strips 509.1 / 511.1. We seed all rows and zero the inactive ones so a
|
||||
# single cell-by-cell diff covers both modes.
|
||||
ORIG_VH_5CI = [
|
||||
"509.1 Vergi ödəyicisi il ərzində tam fəaliyyət göstərdiyi halda:",
|
||||
"509.2 Hesabat ili ərzində yaradıldığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olduğu halda:",
|
||||
"510 Vergi məcəlləsinin 199.21-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
]
|
||||
ORIG_VH_6CI = [
|
||||
"511.1 Müəssisə il ərzində tam fəaliyyət göstərdiyi halda",
|
||||
"511.2 Müəssisə il ərzində yarandığı halda",
|
||||
]
|
||||
|
||||
|
||||
def make_original_doc(tam_il, il_erzinde):
|
||||
"""Build the 10 child-table lists as plain dicts, seeded original-style."""
|
||||
def empty_row(text_col, label, num_cols):
|
||||
row = {text_col: label}
|
||||
for c in num_cols:
|
||||
row[c] = 0.0
|
||||
return row
|
||||
|
||||
return {
|
||||
"tam_il": int(bool(tam_il)),
|
||||
"il_erzinde": int(bool(il_erzinde)),
|
||||
"elave_1": [empty_row("azadolmasəbəbi", l, ELAVE_1_COLS) for l in ORIG_ELAVE_1_LABELS],
|
||||
"elave_2": [empty_row("vergiyəcəlbolunanəmlaklarınkateqoriyası", l, ELAVE_2_COLS) for l in ORIG_ELAVE_2_LABELS],
|
||||
"vergi_hesab_1ci": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_1CI],
|
||||
"vergi_hesab_1ci_1": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_1CI_1],
|
||||
"vergi_hesab_2ci": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_2CI],
|
||||
"vergi_hesab_3cu": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_3CU],
|
||||
"vergi_hesab_4cu": [empty_row("göstəricilər", l, ("faizlə",)) for l in ORIG_VH_4CU],
|
||||
"vergi_hesab_4cu_1": [empty_row("göstəricilər", l, ("ayla",)) for l in ORIG_VH_4CU_1],
|
||||
"vergi_hesab_5ci": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_5CI],
|
||||
"vergi_hesab_6ci": [empty_row("göstəricilər", l, ("manatla",)) for l in ORIG_VH_6CI],
|
||||
}
|
||||
|
||||
|
||||
def find_orig(rows, text_col, label):
|
||||
return next((r for r in rows if r.get(text_col) == label), None)
|
||||
|
||||
|
||||
def run_original_pipeline(d):
|
||||
"""Original calc order from property_tax_return.js `refresh` hook + DSL triggers."""
|
||||
calc_binalar_totals_orig(d["elave_2"])
|
||||
calc_elave_1_totals_orig(d["elave_1"])
|
||||
calc_elave_2_totals_orig(d["elave_2"])
|
||||
calc_vh_3cu_totals_orig(d["elave_1"], d["elave_2"], d["vergi_hesab_3cu"])
|
||||
# DSL-evaluated cells
|
||||
calc_vh_1ci_orig(d["elave_1"], d["elave_2"], d["vergi_hesab_1ci"])
|
||||
calc_vh_1ci_1_orig(d["elave_1"], d["elave_2"], d["vergi_hesab_1ci_1"])
|
||||
calc_vh_2ci_orig(d["elave_1"], d["elave_2"], d["vergi_hesab_2ci"])
|
||||
calc_vh_4cu_orig(d["vergi_hesab_4cu"])
|
||||
# Final tax
|
||||
calc_509_1_orig(d["tam_il"], d["vergi_hesab_1ci"], d["vergi_hesab_2ci"], d["vergi_hesab_3cu"], d["vergi_hesab_5ci"], d["vergi_hesab_6ci"])
|
||||
calc_509_2_orig(d["il_erzinde"], d["vergi_hesab_1ci_1"], d["vergi_hesab_2ci"], d["vergi_hesab_3cu"], d["vergi_hesab_4cu_1"], d["vergi_hesab_5ci"], d["vergi_hesab_6ci"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Diff helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def round2(v):
|
||||
return float(Decimal(str(float(v or 0))).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
def cells(rows, text_col, num_cols):
|
||||
return [
|
||||
(r.get(text_col), {c: round2(r.get(c) or 0) for c in num_cols})
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def diff_tables(label, orig_rows, ai_rows, text_col, num_cols, tol=0.005):
|
||||
"""Diff by label, not index. Reports unique-side rows as info only.
|
||||
|
||||
The AI Select shortened the 199.1 label and dropped 199.20; the
|
||||
`LABEL_REMAP` table in `build_orig_doc` already normalises 199.1, so the
|
||||
only structural difference left is the orig-only 199.20 row. We surface
|
||||
rows that exist only on one side, but they do not count as failures unless
|
||||
they carry non-zero values."""
|
||||
def get(r, col):
|
||||
return r.get(col) if isinstance(r, dict) else getattr(r, col, None)
|
||||
|
||||
def label_of(r):
|
||||
return get(r, text_col)
|
||||
|
||||
orig_by = {label_of(r): r for r in orig_rows}
|
||||
ai_by = {label_of(r): r for r in ai_rows}
|
||||
common = [l for l in orig_by if l in ai_by]
|
||||
orig_only = [l for l in orig_by if l not in ai_by]
|
||||
ai_only = [l for l in ai_by if l not in orig_by]
|
||||
|
||||
print(
|
||||
f"\n --- {label} ({len(orig_rows)} orig / {len(ai_rows)} AI, "
|
||||
f"common={len(common)}, orig-only={len(orig_only)}, AI-only={len(ai_only)}) ---"
|
||||
)
|
||||
|
||||
failed = 0
|
||||
for l in common:
|
||||
o, a = orig_by[l], ai_by[l]
|
||||
for c in num_cols:
|
||||
o_val = round2(get(o, c) or 0)
|
||||
a_val = round2(get(a, c) or 0)
|
||||
if abs(o_val - a_val) > tol:
|
||||
print(f" ✗ [{(l or '')[:50]}] {c}: orig={o_val} AI={a_val} Δ={a_val-o_val:+.4f}")
|
||||
failed += 1
|
||||
|
||||
def nonzero(r):
|
||||
return any(round2(get(r, c) or 0) != 0 for c in num_cols)
|
||||
|
||||
for l in orig_only:
|
||||
r = orig_by[l]
|
||||
marker = "✗ nonzero!" if nonzero(r) else "ℹ"
|
||||
print(f" {marker} orig-only row [{(l or '')[:50]}]: values={[round2(get(r,c) or 0) for c in num_cols]}")
|
||||
if nonzero(r):
|
||||
failed += 1
|
||||
for l in ai_only:
|
||||
r = ai_by[l]
|
||||
marker = "✗ nonzero!" if nonzero(r) else "ℹ"
|
||||
print(f" {marker} AI-only row [{(l or '')[:50]}]: values={[round2(get(r,c) or 0) for c in num_cols]}")
|
||||
if nonzero(r):
|
||||
failed += 1
|
||||
|
||||
if failed == 0:
|
||||
print(f" ✓ all {len(common)*len(num_cols)} shared cells match")
|
||||
return failed
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario runner
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_ai_doc(scenario_inputs, tam_il, il_erzinde):
|
||||
"""Create a Property Tax Return AI Fill doc, seed, apply inputs, validate."""
|
||||
doc = frappe.new_doc("Property Tax Return AI Fill")
|
||||
doc.tam_il = int(bool(tam_il))
|
||||
doc.il_erzinde = int(bool(il_erzinde))
|
||||
doc.run_method("before_insert") # seed
|
||||
|
||||
for tbl, label, col, val in scenario_inputs:
|
||||
if tbl == "elave_1":
|
||||
row = next(r for r in doc.elave_1 if r.azadolmasəbəbi == label)
|
||||
elif tbl == "elave_2":
|
||||
row = next(r for r in doc.elave_2 if r.vergiyəcəlbolunanəmlaklarınkateqoriyası == label)
|
||||
else:
|
||||
grid = doc.get(tbl)
|
||||
row = next(r for r in grid if r.göstəricilər == label)
|
||||
setattr(row, col, val)
|
||||
|
||||
doc.recalculate()
|
||||
return doc
|
||||
|
||||
|
||||
def build_orig_doc(scenario_inputs, tam_il, il_erzinde):
|
||||
"""Build the equivalent original-style dict-doc, apply inputs, run pipeline."""
|
||||
d = make_original_doc(tam_il, il_erzinde)
|
||||
|
||||
# Map AI-clone labels to original-doc labels when they differ. The AI
|
||||
# Select shortened "199.1-ci ... əsasən azaldılmalı əmlak vergisinin məbləği"
|
||||
# to "199.1-ci ... əsasən:". For parity we map both to the original's full
|
||||
# text when an input targets that row.
|
||||
LABEL_REMAP = {
|
||||
"Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən:":
|
||||
"Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
}
|
||||
|
||||
for tbl, label, col, val in scenario_inputs:
|
||||
mapped_label = LABEL_REMAP.get(label, label)
|
||||
if tbl == "elave_1":
|
||||
row = find_orig(d["elave_1"], "azadolmasəbəbi", mapped_label)
|
||||
elif tbl == "elave_2":
|
||||
row = find_orig(d["elave_2"], "vergiyəcəlbolunanəmlaklarınkateqoriyası", mapped_label)
|
||||
else:
|
||||
row = find_orig(d[tbl], "göstəricilər", mapped_label)
|
||||
if row is None:
|
||||
raise LookupError(f"orig row not found: {tbl} / {mapped_label!r}")
|
||||
row[col] = float(val)
|
||||
|
||||
run_original_pipeline(d)
|
||||
return d
|
||||
|
||||
|
||||
def diff_scenario(name, scenario_inputs, tam_il, il_erzinde):
|
||||
print(f"\n========== {name} ==========")
|
||||
orig = build_orig_doc(scenario_inputs, tam_il, il_erzinde)
|
||||
ai = build_ai_doc(scenario_inputs, tam_il, il_erzinde)
|
||||
|
||||
total_fail = 0
|
||||
total_fail += diff_tables("elave_1", orig["elave_1"], list(ai.elave_1), "azadolmasəbəbi", ELAVE_1_COLS)
|
||||
total_fail += diff_tables("elave_2", orig["elave_2"], list(ai.elave_2), "vergiyəcəlbolunanəmlaklarınkateqoriyası", ELAVE_2_COLS)
|
||||
total_fail += diff_tables("vergi_hesab_1ci", orig["vergi_hesab_1ci"], list(ai.vergi_hesab_1ci), "göstəricilər", ("manatla",))
|
||||
total_fail += diff_tables("vergi_hesab_1ci_1", orig["vergi_hesab_1ci_1"], list(ai.vergi_hesab_1ci_1), "göstəricilər", ("manatla",))
|
||||
total_fail += diff_tables("vergi_hesab_2ci", orig["vergi_hesab_2ci"], list(ai.vergi_hesab_2ci), "göstəricilər", ("manatla",))
|
||||
total_fail += diff_tables("vergi_hesab_3cu", orig["vergi_hesab_3cu"], list(ai.vergi_hesab_3cu), "göstəricilər", ("manatla",))
|
||||
total_fail += diff_tables("vergi_hesab_4cu", orig["vergi_hesab_4cu"], list(ai.vergi_hesab_4cu), "göstəricilər", ("faizlə",))
|
||||
total_fail += diff_tables("vergi_hesab_4cu_1", orig["vergi_hesab_4cu_1"], list(ai.vergi_hesab_4cu_1), "göstəricilər", ("ayla",))
|
||||
total_fail += diff_tables("vergi_hesab_5ci", orig["vergi_hesab_5ci"], list(ai.vergi_hesab_5ci), "göstəricilər", ("manatla",))
|
||||
total_fail += diff_tables("vergi_hesab_6ci", orig["vergi_hesab_6ci"], list(ai.vergi_hesab_6ci), "göstəricilər", ("manatla",))
|
||||
|
||||
print(f"\n >>> {name}: {'PASS' if total_fail == 0 else f'FAIL ({total_fail} cell mismatches)'}")
|
||||
return total_fail == 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def run():
|
||||
"""Run all parity scenarios."""
|
||||
|
||||
# ---- Scenario A: cabinet-verified full-year baseline -------------------
|
||||
# taxable[1.1].periodEnd=200, excessInsured=300 → 509.1 = 4.00
|
||||
scenA = [
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", 200),
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505", 300),
|
||||
]
|
||||
a = diff_scenario("A. Cabinet full-year (200 / 300 → 4.00)", scenA, tam_il=1, il_erzinde=0)
|
||||
|
||||
# ---- Scenario B: cabinet-verified partial-year baseline ---------------
|
||||
# taxable[1.1].onCreate=100, periodEnd=200, excess=300, 507=6 → 509.2 = 3.75
|
||||
scenB = [
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"hesabatilierzindeuçotaalınan", 100),
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", 200),
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505", 300),
|
||||
("vergi_hesab_4cu_1",
|
||||
"507 Müəssisə yarandığı aydan sonra ilin sonuna qədər olan ayların sayı",
|
||||
"ayla", 6),
|
||||
]
|
||||
b = diff_scenario("B. Cabinet partial-year (100/200/300, 6m → 3.75)", scenB, tam_il=0, il_erzinde=1)
|
||||
|
||||
# ---- Scenario C: multi-row, every column non-zero, with 510 reduction
|
||||
scenC = [
|
||||
# 3 elave_1 rows with all 5 numeric columns set
|
||||
("elave_1", "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012", 1000),
|
||||
("elave_1", "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri", 50),
|
||||
("elave_1", "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri", 700),
|
||||
("elave_1", "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"ilinsonunaəsasvəsaitlərinqalıqdəyəri", 1200),
|
||||
("elave_1", "Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri", 150),
|
||||
("elave_1", "Vergi Məcəlləsinin 227.1-ci maddəsinə əsasən:",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012", 333),
|
||||
("elave_1", "Vergi Məcəlləsinin 227.1-ci maddəsinə əsasən:",
|
||||
"ilinsonunaəsasvəsaitlərinqalıqdəyəri", 444),
|
||||
# 4 elave_2 rows: 1.1 + 1.3 + 1.5 (all children of _5001) + 4 (other)
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri", 500),
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", 400),
|
||||
("elave_2", "1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505", 50),
|
||||
("elave_2", "1.3 Əmlak kompleksi kimi müəssisələr",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri", 800),
|
||||
("elave_2", "1.3 Əmlak kompleksi kimi müəssisələr",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", 750),
|
||||
("elave_2", "1.5 Mənzillər",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri", 200),
|
||||
("elave_2", "1.5 Mənzillər",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505", 25),
|
||||
("elave_2", "4 Nəqliyyat vasitələri",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri", 1100),
|
||||
# 510 reduction (user input) — original ignores it for 511 anyway, this
|
||||
# tests our changed code-path stays neutral.
|
||||
("vergi_hesab_5ci",
|
||||
"510 Vergi məcəlləsinin 199.21-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
"manatla", 2.50),
|
||||
]
|
||||
c = diff_scenario("C. Multi-row full-year with 510=2.50", scenC, tam_il=1, il_erzinde=0)
|
||||
|
||||
print(f"\n========== Summary: A={a} B={b} C={c} ==========")
|
||||
print("ALL OK" if (a and b and c) else "FAILED")
|
||||
return {"A": a, "B": b, "C": c}
|
||||
|
|
@ -0,0 +1,368 @@
|
|||
// ── Property Tax Return AI Fill — client-side seeding + realtime recalc ──
|
||||
// Seeding is unchanged: on the "+ New" Desk form we pull the row catalogue
|
||||
// from the whitelisted get_seed() and populate every empty child table.
|
||||
// Realtime recalc mirrors property_tax_return_ai_fill.py's validate()
|
||||
// pipeline (port of the Python recalculate logic) so totals/mirrors/tax
|
||||
// update as the user types, not just on save.
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// ── Constants — must match the Select options of the child DocTypes ──
|
||||
|
||||
const E1_TOTAL_LABEL = "Vergidən azad olunan əmlakın CƏMİ dəyəri:";
|
||||
const E2_PARENT_LABEL = "1. Binalar, tikililər və qurğular";
|
||||
const E2_TOTAL_LABEL = "Vergiyə cəlb olunan əmlakın CƏMİ dəyəri:";
|
||||
|
||||
const E1_COLS = {
|
||||
"501.2": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012",
|
||||
"502.2": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"503.2": "hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri",
|
||||
"504.2": "ilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"505.2": "artıqqiymətəəsasvəsaitlərindəyəri",
|
||||
};
|
||||
const E2_COLS = {
|
||||
"501.1": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"502.1": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"503.1": "hesabatilierzindeuçotaalınan",
|
||||
"504.1": "hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"505.1": "artıqqiymətəəsasvəsaitlərindəyəri505",
|
||||
};
|
||||
|
||||
const VH_1CI = [
|
||||
"501. Əmlakın ümumi qalıq dəyəri",
|
||||
"501.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"501.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"501.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
];
|
||||
const VH_1CI_1 = [
|
||||
"503. Əmlakın ümumi dəyəri",
|
||||
"503.1 Vergiyə cəlb olunan əmlakın dəyəri",
|
||||
"503.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın dəyəri",
|
||||
"503.2 Vergidən azad olunan əmlakın dəyəri",
|
||||
];
|
||||
const VH_2CI = [
|
||||
"504. Əmlakın ümumi qalıq dəyəri",
|
||||
"504.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"504.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"504.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
];
|
||||
const VH_3CU = [
|
||||
"505 Artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.1 Vergiyə cəlb olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.2 Vergidən azad olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
];
|
||||
const VH_4CU = "506 Verginin dərəcəsi";
|
||||
const VH_4CU_1 = "507 Müəssisə yarandığı aydan sonra ilin sonuna qədər olan ayların sayı";
|
||||
const VH_5CI = [
|
||||
"509.1 Vergi ödəyicisi il ərzində tam fəaliyyət göstərdiyi halda:",
|
||||
"509.2 Hesabat ili ərzində yaradıldığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olduğu halda:",
|
||||
"510 Vergi məcəlləsinin 199.21-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
];
|
||||
const VH_6CI = [
|
||||
"511.1 Müəssisə il ərzində tam fəaliyyət göstərdiyi halda",
|
||||
"511.2 Müəssisə il ərzində yarandığı halda",
|
||||
];
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
function flt(v) {
|
||||
const n = parseFloat(v);
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
function round2(v) {
|
||||
return Math.round((v + Number.EPSILON) * 100) / 100;
|
||||
}
|
||||
function find_row(rows, col, value) {
|
||||
if (!rows) return null;
|
||||
for (const r of rows) if (r[col] === value) return r;
|
||||
return null;
|
||||
}
|
||||
function set_cell(rows, text_col, label, target_col, value) {
|
||||
const r = find_row(rows, text_col, label);
|
||||
if (r) r[target_col] = round2(value);
|
||||
}
|
||||
|
||||
// ── Per-table recalc functions (port of property_tax_return_ai_fill.py) ──
|
||||
|
||||
function calc_e1_totals(frm) {
|
||||
const totals = {};
|
||||
for (const code in E1_COLS) totals[code] = 0;
|
||||
for (const r of frm.doc.elave_1 || []) {
|
||||
if (r.azadolmasəbəbi === E1_TOTAL_LABEL) continue;
|
||||
for (const code in E1_COLS) totals[code] += flt(r[E1_COLS[code]]);
|
||||
}
|
||||
const cəmi = find_row(frm.doc.elave_1, "azadolmasəbəbi", E1_TOTAL_LABEL);
|
||||
if (cəmi) {
|
||||
for (const code in E1_COLS) cəmi[E1_COLS[code]] = round2(totals[code]);
|
||||
}
|
||||
const out = {};
|
||||
for (const code in totals) out[code] = round2(totals[code]);
|
||||
return out;
|
||||
}
|
||||
|
||||
function calc_e2_parent_and_totals(frm) {
|
||||
// 1) Parent _5001 = Σ of sub-categories 1.1–1.6.
|
||||
const child_sums = {};
|
||||
for (const code in E2_COLS) child_sums[code] = 0;
|
||||
for (const r of frm.doc.elave_2 || []) {
|
||||
const lab = r.vergiyəcəlbolunanəmlaklarınkateqoriyası || "";
|
||||
if (lab.length >= 3 && lab.charAt(0) === "1" && lab.charAt(1) === "." && "123456".indexOf(lab.charAt(2)) >= 0) {
|
||||
for (const code in E2_COLS) child_sums[code] += flt(r[E2_COLS[code]]);
|
||||
}
|
||||
}
|
||||
const parent = find_row(frm.doc.elave_2, "vergiyəcəlbolunanəmlaklarınkateqoriyası", E2_PARENT_LABEL);
|
||||
if (parent) {
|
||||
for (const code in E2_COLS) parent[E2_COLS[code]] = round2(child_sums[code]);
|
||||
}
|
||||
|
||||
// 2) CƏMİ totals: every row except _5001 parent and CƏMİ itself.
|
||||
const totals = {};
|
||||
for (const code in E2_COLS) totals[code] = 0;
|
||||
for (const r of frm.doc.elave_2 || []) {
|
||||
const lab = r.vergiyəcəlbolunanəmlaklarınkateqoriyası || "";
|
||||
if (lab === E2_PARENT_LABEL || lab === E2_TOTAL_LABEL) continue;
|
||||
for (const code in E2_COLS) totals[code] += flt(r[E2_COLS[code]]);
|
||||
}
|
||||
const cəmi = find_row(frm.doc.elave_2, "vergiyəcəlbolunanəmlaklarınkateqoriyası", E2_TOTAL_LABEL);
|
||||
if (cəmi) {
|
||||
for (const code in E2_COLS) cəmi[E2_COLS[code]] = round2(totals[code]);
|
||||
}
|
||||
const out = {};
|
||||
for (const code in totals) out[code] = round2(totals[code]);
|
||||
return out;
|
||||
}
|
||||
|
||||
function calc_vh_mirrors(frm, e1, e2) {
|
||||
const e1_501_2 = e1["501.2"] || 0;
|
||||
const e1_503_2 = e1["503.2"] || 0;
|
||||
const e1_504_2 = e1["504.2"] || 0;
|
||||
const e1_505_2 = e1["505.2"] || 0;
|
||||
const e2_501_1 = e2["501.1"] || 0;
|
||||
const e2_503_1 = e2["503.1"] || 0;
|
||||
const e2_504_1 = e2["504.1"] || 0;
|
||||
const e2_505_1 = e2["505.1"] || 0;
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_1ci, "göstəricilər", VH_1CI[0], "manatla", e1_501_2 + e2_501_1);
|
||||
set_cell(frm.doc.vergi_hesab_1ci, "göstəricilər", VH_1CI[1], "manatla", e2_501_1);
|
||||
set_cell(frm.doc.vergi_hesab_1ci, "göstəricilər", VH_1CI[2], "manatla", 0);
|
||||
set_cell(frm.doc.vergi_hesab_1ci, "göstəricilər", VH_1CI[3], "manatla", e1_501_2);
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_1ci_1, "göstəricilər", VH_1CI_1[0], "manatla", e1_503_2 + e2_503_1);
|
||||
set_cell(frm.doc.vergi_hesab_1ci_1, "göstəricilər", VH_1CI_1[1], "manatla", e2_503_1);
|
||||
set_cell(frm.doc.vergi_hesab_1ci_1, "göstəricilər", VH_1CI_1[2], "manatla", 0);
|
||||
set_cell(frm.doc.vergi_hesab_1ci_1, "göstəricilər", VH_1CI_1[3], "manatla", e1_503_2);
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_2ci, "göstəricilər", VH_2CI[0], "manatla", e1_504_2 + e2_504_1);
|
||||
set_cell(frm.doc.vergi_hesab_2ci, "göstəricilər", VH_2CI[1], "manatla", e2_504_1);
|
||||
set_cell(frm.doc.vergi_hesab_2ci, "göstəricilər", VH_2CI[2], "manatla", 0);
|
||||
set_cell(frm.doc.vergi_hesab_2ci, "göstəricilər", VH_2CI[3], "manatla", e1_504_2);
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_3cu, "göstəricilər", VH_3CU[0], "manatla", e1_505_2 + e2_505_1);
|
||||
set_cell(frm.doc.vergi_hesab_3cu, "göstəricilər", VH_3CU[1], "manatla", e2_505_1);
|
||||
set_cell(frm.doc.vergi_hesab_3cu, "göstəricilər", VH_3CU[2], "manatla", e1_505_2);
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_4cu, "göstəricilər", VH_4CU, "faizlə", 1);
|
||||
}
|
||||
|
||||
function calc_vh_509_511(frm, e2) {
|
||||
const months_row = find_row(frm.doc.vergi_hesab_4cu_1, "göstəricilər", VH_4CU_1);
|
||||
const months = months_row ? flt(months_row.ayla) : 0;
|
||||
|
||||
const s501 = e2["501.1"] || 0;
|
||||
const s503 = e2["503.1"] || 0;
|
||||
const s504 = e2["504.1"] || 0;
|
||||
const s505 = e2["505.1"] || 0;
|
||||
|
||||
const tax_full = ((s501 + s504) / 2) * 0.01 + s505 * 0.01;
|
||||
const tax_partial = months
|
||||
? (((s503 + s504) / 24) * months) * 0.01 + s505 * 0.01
|
||||
: s505 * 0.01;
|
||||
|
||||
const is_partial = !!frm.doc.il_erzinde;
|
||||
const tax509 = round2(is_partial ? tax_partial : tax_full);
|
||||
|
||||
set_cell(frm.doc.vergi_hesab_5ci, "göstəricilər", VH_5CI[0], "manatla", is_partial ? 0 : tax509);
|
||||
set_cell(frm.doc.vergi_hesab_5ci, "göstəricilər", VH_5CI[1], "manatla", is_partial ? tax509 : 0);
|
||||
// 510 row stays user input — we don't touch it.
|
||||
|
||||
// 511.x mirrors 509.x (cabinet shouldBePaidTax == calculatedTax; original
|
||||
// property_tax_return.js does the same — no subtraction of 510).
|
||||
set_cell(frm.doc.vergi_hesab_6ci, "göstəricilər", VH_6CI[0], "manatla", is_partial ? 0 : tax509);
|
||||
set_cell(frm.doc.vergi_hesab_6ci, "göstəricilər", VH_6CI[1], "manatla", is_partial ? tax509 : 0);
|
||||
}
|
||||
|
||||
function recalc_all(frm) {
|
||||
const e1 = calc_e1_totals(frm);
|
||||
const e2 = calc_e2_parent_and_totals(frm);
|
||||
calc_vh_mirrors(frm, e1, e2);
|
||||
calc_vh_509_511(frm, e2);
|
||||
const fields = [
|
||||
"elave_1", "elave_2",
|
||||
"vergi_hesab_1ci", "vergi_hesab_1ci_1", "vergi_hesab_2ci", "vergi_hesab_3cu",
|
||||
"vergi_hesab_4cu", "vergi_hesab_5ci", "vergi_hesab_6ci",
|
||||
];
|
||||
for (const f of fields) frm.refresh_field(f);
|
||||
}
|
||||
|
||||
// Expose for child-table handlers below
|
||||
window.__ptr_ai_fill_recalc = recalc_all;
|
||||
|
||||
// ── Autofill from accounting ──
|
||||
//
|
||||
// Server returns per-row totals computed from Asset records. We zero out
|
||||
// the seeded numeric cells first so the doc state mirrors the latest
|
||||
// accounting snapshot exactly (no leftovers from a prior run).
|
||||
|
||||
const AUTOFILL_METHOD =
|
||||
"taxes_az.taxes_az.doctype.property_tax_return_ai_fill.property_tax_return_ai_fill.autofill_from_accounting";
|
||||
|
||||
function clear_numeric_columns(frm) {
|
||||
for (const r of frm.doc.elave_1 || []) {
|
||||
for (const code in E1_COLS) r[E1_COLS[code]] = 0;
|
||||
}
|
||||
for (const r of frm.doc.elave_2 || []) {
|
||||
for (const code in E2_COLS) r[E2_COLS[code]] = 0;
|
||||
}
|
||||
const months_row = find_row(frm.doc.vergi_hesab_4cu_1, "göstəricilər", VH_4CU_1);
|
||||
if (months_row) months_row.ayla = 0;
|
||||
}
|
||||
|
||||
function apply_autofill_payload(frm, payload) {
|
||||
if (!payload) return;
|
||||
clear_numeric_columns(frm);
|
||||
for (const [label, cols] of Object.entries(payload.elave_1 || {})) {
|
||||
const r = find_row(frm.doc.elave_1, "azadolmasəbəbi", label);
|
||||
if (!r) continue;
|
||||
for (const [code, val] of Object.entries(cols)) {
|
||||
const field = E1_COLS[code];
|
||||
if (field) r[field] = round2(val);
|
||||
}
|
||||
}
|
||||
for (const [label, cols] of Object.entries(payload.elave_2 || {})) {
|
||||
const r = find_row(frm.doc.elave_2, "vergiyəcəlbolunanəmlaklarınkateqoriyası", label);
|
||||
if (!r) continue;
|
||||
for (const [code, val] of Object.entries(cols)) {
|
||||
const field = E2_COLS[code];
|
||||
if (field) r[field] = round2(val);
|
||||
}
|
||||
}
|
||||
const months_row = find_row(frm.doc.vergi_hesab_4cu_1, "göstəricilər", VH_4CU_1);
|
||||
if (months_row) months_row.ayla = payload.ayla || 0;
|
||||
|
||||
recalc_all(frm);
|
||||
|
||||
if (Array.isArray(payload.unmatched) && payload.unmatched.length) {
|
||||
frappe.show_alert({
|
||||
message: __("{0} Asset auto-fill zamanı buraxıldı (uyğunlaşmayan kateqoriya)",
|
||||
[payload.unmatched.length]),
|
||||
indicator: "orange",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function autofill_now(frm, opts) {
|
||||
opts = opts || {};
|
||||
if (!frm.doc.il) return; // Nothing to compute without a target year.
|
||||
const args = frm.is_new()
|
||||
? { doc: JSON.stringify(frm.doc) }
|
||||
: { docname: frm.doc.name };
|
||||
frappe.call({
|
||||
method: AUTOFILL_METHOD,
|
||||
args: args,
|
||||
callback: function (r) {
|
||||
if (!r || !r.message) return;
|
||||
apply_autofill_payload(frm, r.message);
|
||||
if (opts.announce) {
|
||||
frappe.show_alert({
|
||||
message: __("Uçotdan dolduruldu"),
|
||||
indicator: "green",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── Seeding (unchanged) + recalc wiring on the parent ──
|
||||
|
||||
frappe.ui.form.on("Property Tax Return AI Fill", {
|
||||
onload: function (frm) {
|
||||
if (!frm.is_new()) return;
|
||||
if (frm.__ai_seeded) return;
|
||||
|
||||
// Default `il` to current year so the autofill below has a target.
|
||||
// Use direct assignment (not set_value) to avoid triggering the
|
||||
// `il` change handler — that fires later, after seed completes.
|
||||
if (!frm.doc.il) {
|
||||
frm.doc.il = new Date().getFullYear();
|
||||
frm.refresh_field("il");
|
||||
}
|
||||
|
||||
const seed_targets = [
|
||||
"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",
|
||||
"elave_1", "elave_2",
|
||||
];
|
||||
const all_empty = seed_targets.every(
|
||||
(f) => !(frm.doc[f] && frm.doc[f].length)
|
||||
);
|
||||
if (!all_empty) {
|
||||
recalc_all(frm);
|
||||
autofill_now(frm); // doc reloaded mid-edit — refresh totals
|
||||
return;
|
||||
}
|
||||
frm.__ai_seeded = true;
|
||||
frappe.call({
|
||||
method:
|
||||
"taxes_az.taxes_az.doctype.property_tax_return_ai_fill.property_tax_return_ai_fill.get_seed",
|
||||
callback: function (r) {
|
||||
if (!r || !r.message) return;
|
||||
for (const [table_field, rows] of Object.entries(r.message)) {
|
||||
for (const row of rows) {
|
||||
const child = frm.add_child(table_field);
|
||||
Object.assign(child, row);
|
||||
}
|
||||
frm.refresh_field(table_field);
|
||||
}
|
||||
recalc_all(frm);
|
||||
// Pull accounting values immediately after seeding so the
|
||||
// user lands on a pre-filled draft, not an empty grid.
|
||||
autofill_now(frm);
|
||||
},
|
||||
});
|
||||
},
|
||||
refresh: function (frm) {
|
||||
if (!frm.is_new()) recalc_all(frm);
|
||||
// Always offer manual re-fill — accounting may have moved since
|
||||
// the declaration was saved.
|
||||
frm.add_custom_button(__("Yenidən doldur (uçotdan)"), function () {
|
||||
frappe.confirm(
|
||||
__("Elavə 1, elavə 2 və 507 sahələri yenidən hesablanaraq yazılacaq. Davam edilsin?"),
|
||||
function () { autofill_now(frm, { announce: true }); }
|
||||
);
|
||||
});
|
||||
},
|
||||
// Re-fill on year / mode toggles, but only while the doc is unsaved —
|
||||
// a saved declaration's manual edits must not be silently overwritten.
|
||||
il: function (frm) { recalc_all(frm); if (frm.is_new()) autofill_now(frm); },
|
||||
tam_il: function (frm) { recalc_all(frm); if (frm.is_new()) autofill_now(frm); },
|
||||
il_erzinde: function (frm) { recalc_all(frm); if (frm.is_new()) autofill_now(frm); },
|
||||
});
|
||||
|
||||
// ── Child-table handlers: recalc on every cell edit / removal ──
|
||||
|
||||
const E1_NUMS = Object.values(E1_COLS);
|
||||
const E2_NUMS = Object.values(E2_COLS);
|
||||
|
||||
const e1_handlers = { elave_1_remove: (frm) => recalc_all(frm) };
|
||||
for (const f of E1_NUMS) e1_handlers[f] = (frm) => recalc_all(frm);
|
||||
frappe.ui.form.on("Property Tax Return AI Fill Elave 1", e1_handlers);
|
||||
|
||||
const e2_handlers = { elave_2_remove: (frm) => recalc_all(frm) };
|
||||
for (const f of E2_NUMS) e2_handlers[f] = (frm) => recalc_all(frm);
|
||||
frappe.ui.form.on("Property Tax Return AI Fill Elave 2", e2_handlers);
|
||||
|
||||
frappe.ui.form.on("Property Tax Return AI Fill Vergi hesablamasi 4cu cedvel 1", {
|
||||
ayla: function (frm) { recalc_all(frm); },
|
||||
});
|
||||
})();
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 14:18:26.085350",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"ümumi_məlumat_tab",
|
||||
"html_pegz",
|
||||
"idarə",
|
||||
"vergiorqanı",
|
||||
"vergidövrü",
|
||||
"il",
|
||||
"tam_il",
|
||||
"il_erzinde",
|
||||
"şəxsi",
|
||||
"vöen",
|
||||
"soyadı",
|
||||
"atasınınadı",
|
||||
"mobnömrə1",
|
||||
"mobnömrə2",
|
||||
"column_break_yjgp",
|
||||
"ödəyicitipi",
|
||||
"adı",
|
||||
"şəhərnömrəsi1",
|
||||
"şəhərnömrəsi2",
|
||||
"section_break_pcfz",
|
||||
"əsasfəaliyyətnövününkodu",
|
||||
"təqdimolunmuşəlavələrinsayı",
|
||||
"bəyannaməninnövü",
|
||||
"dəqiqləşdirilmişbəyannamənintəqdimedilməsibarədəbildiriş",
|
||||
"column_break_xcxz",
|
||||
"section_break_vfvd",
|
||||
"bəyan_ediləcək_məlumatım_yoxdur",
|
||||
"section_break_wnwr",
|
||||
"column_break_dblg",
|
||||
"sətirlərdənbiriniseçin",
|
||||
"əmlak_vergisinin_hesablanması_tab",
|
||||
"html_ikbb",
|
||||
"vergi_hesab_1ci",
|
||||
"html_uhqz",
|
||||
"vergi_hesab_1ci_1",
|
||||
"section_break_bhtd",
|
||||
"html_umxi",
|
||||
"vergi_hesab_2ci",
|
||||
"section_break_fugy",
|
||||
"html_exjg",
|
||||
"vergi_hesab_3cu",
|
||||
"section_break_bhet",
|
||||
"html_tuak",
|
||||
"vergi_hesab_4cu",
|
||||
"vergi_hesab_4cu_1",
|
||||
"section_break_cnpg",
|
||||
"html_mxir",
|
||||
"vergi_hesab_5ci",
|
||||
"section_break_klzd",
|
||||
"html_ctml",
|
||||
"vergi_hesab_6ci",
|
||||
"əlavə_1_tab",
|
||||
"html_hken",
|
||||
"warning_elave_1_html",
|
||||
"elave_1",
|
||||
"əlavə_2_tab",
|
||||
"html_ybie",
|
||||
"elave_2"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "ümumi_məlumat_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Ümumi məlumat"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_pegz",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 20px;font-weight: bold;color: blue;\"> Əmlak vergisinin bəyannaməsi</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergiorqanı",
|
||||
"fieldtype": "Select",
|
||||
"label": "Vergi orqanı",
|
||||
"options": "2 saylı Ərazi Vergilər Baş idarəsi\tSumqayıt\t02\n3 saylı Ərazi Vergilər İdarəsi\tQuba\t03\n4 saylı Ərazi Vergilər İdarəsi\tŞamaxı\t04\n5 saylı Ərazi Vergilər İdarəsi\tŞirvan\t05\n6 saylı Ərazi Vergilər Baş İdarəsi\tLənkəran\t06\nQarabağ Ərazi Vergilər Baş İdarəsi\tAğdam, Füzuli, Şuşa, Xocalı, Xocavənd\t07\n8 saylı Ərazi Vergilər İdarəsi\tBeyləqan\t08\n9 saylı Ərazi Vergilər İdarəsi\tGöyçay\t09\n10 saylı Ərazi Vergilər İdarəsi\tQəbələ\t10\n11 saylı Ərazi Vergilər Baş İdarəsi\tYevlax\t11\n12 saylı Ərazi Vergilər Baş İdarəsi\tGəncə\t12\n13 saylı Ərazi Vergilər İdarəsi\tZaqatala\t13\n14 saylı Ərazi Vergilər İdarəsi\tŞəmkir\t14\nDövlət Vergi xidmətinin Milli Gəlirlər Baş idarəsi\tBakı\t16\nDövlət Vergi xidmətinin Bakı şəhəri Lokal Gəlirlər Baş idarəsi\tBakı\t19\nDövlət Vergi xidmətinin Bakı şəhəri Kiçik Sahibkarlıqla İş üzrə Baş idarəsi\tBakı\t39\nNaxçıvan şəhəri üzrə Vergilər İdarəsi\tNaxçıvan\t27\n2 saylı Ərazi Vergilər İdarəsi (Naxçıvan MR)\tŞərur, Sədərək, Kəngərli\t30\n3 saylı Ərazi Vergilər İdarəsi (Naxçıvan MR)\tŞahbuz, Babək\t26\n4 saylı Ərazi Vergilər İdarəsi (Naxçıvan MR)\tCulfa, Ordubad\t24\nŞərqi Zəngəzur Ərazi Vergilər İdarəsi\tCəbrayıl, Kəlbəcər, Laçın, Qubadlı, Zəngilan\t55"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergidövrü",
|
||||
"fieldtype": "Select",
|
||||
"label": "Vergi dövrü",
|
||||
"options": "İllik"
|
||||
},
|
||||
{
|
||||
"fieldname": "il",
|
||||
"fieldtype": "Int",
|
||||
"label": "İl",
|
||||
"length": 4
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "tam_il",
|
||||
"fieldtype": "Check",
|
||||
"label": "Tam İl"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "il_erzinde",
|
||||
"fieldtype": "Check",
|
||||
"label": "İl Ərzində"
|
||||
},
|
||||
{
|
||||
"fieldname": "şəxsi",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Şəxsi"
|
||||
},
|
||||
{
|
||||
"fieldname": "vöen",
|
||||
"fieldtype": "Int",
|
||||
"label": "VÖEN"
|
||||
},
|
||||
{
|
||||
"fieldname": "soyadı",
|
||||
"fieldtype": "Data",
|
||||
"label": "Soyadı"
|
||||
},
|
||||
{
|
||||
"fieldname": "atasınınadı",
|
||||
"fieldtype": "Data",
|
||||
"label": "Atasının adı"
|
||||
},
|
||||
{
|
||||
"fieldname": "mobnömrə1",
|
||||
"fieldtype": "Int",
|
||||
"label": "Mob nömrə 1"
|
||||
},
|
||||
{
|
||||
"fieldname": "mobnömrə2",
|
||||
"fieldtype": "Int",
|
||||
"label": "Mob nömrə 2"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_yjgp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "ödəyicitipi",
|
||||
"fieldtype": "Select",
|
||||
"label": "Ödəyici tipi",
|
||||
"options": "Hüquqi\nFiziki"
|
||||
},
|
||||
{
|
||||
"fieldname": "adı",
|
||||
"fieldtype": "Data",
|
||||
"label": "Adı"
|
||||
},
|
||||
{
|
||||
"fieldname": "şəhərnömrəsi1",
|
||||
"fieldtype": "Int",
|
||||
"label": "Şəhər nömrəsi 1"
|
||||
},
|
||||
{
|
||||
"fieldname": "şəhərnömrəsi2",
|
||||
"fieldtype": "Int",
|
||||
"label": "Şəhər nömrəsi 2"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_pcfz",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "əsasfəaliyyətnövününkodu",
|
||||
"fieldtype": "Int",
|
||||
"label": "Əsas fəaliyyət növünün kodu"
|
||||
},
|
||||
{
|
||||
"fieldname": "təqdimolunmuşəlavələrinsayı",
|
||||
"fieldtype": "Int",
|
||||
"label": "Təqdim olunmuş əlavələrin sayı"
|
||||
},
|
||||
{
|
||||
"fieldname": "bəyannaməninnövü",
|
||||
"fieldtype": "Select",
|
||||
"label": "Bəyannamənin növü",
|
||||
"options": "Cari\nDəqiqləşdirilmiş\nLəğv olma\nLəğvin dəqiqləşdirilmişi\nAnaloji\nKönüllü açıqlama"
|
||||
},
|
||||
{
|
||||
"fieldname": "dəqiqləşdirilmişbəyannamənintəqdimedilməsibarədəbildiriş",
|
||||
"fieldtype": "Float",
|
||||
"label": "Dəqiqləşdirilmiş bəyannamənin təqdim edilməsi barədə bildirişin nömrəsi",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xcxz",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_vfvd",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "bəyan_ediləcək_məlumatım_yoxdur",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bəyan ediləcək məlumatım yoxdur"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_ikbb",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Hesabat ilinin əvvəlinə</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "əlavə_1_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"js_parent_subtab": "Verginin hesablanması",
|
||||
"label": "Əlavə 1"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_ctml",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Büdcəyə ödənilməli verginin məbləği </p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_hken",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Vergidən azad olunan əmlakın dəyəri haqqında məlumat</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_ybie",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Vergiyə cəlb olunan əsas vəsaitin dəyəri haqqında məlumat</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "warning_elave_1_html",
|
||||
"fieldtype": "HTML",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "əmlak_vergisinin_hesablanması_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Əmlak vergisinin hesablanması"
|
||||
},
|
||||
{
|
||||
"fieldname": "əlavə_2_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"js_parent_subtab": "Verginin hesablanması",
|
||||
"label": "Əlavə 2"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fugy",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_exjg",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Artıq qiymətə sığortalanan əsas vəsaitlər</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_bhet",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_tuak",
|
||||
"fieldtype": "HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_bhtd",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_umxi",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Hesabat ilinin sonuna</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_cnpg",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_mxir",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Hesablanmış verginin məbləği</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "idarə",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "İdarə"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_klzd",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_1ci",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Fill Vergi hesablamasi 1ci cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_2ci",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Fill Vergi hesablamasi 2ci cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_3cu",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Fill Vergi hesablamasi 3cu cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_4cu",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Fill Vergi hesablamasi 4cu cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_5ci",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Fill Vergi hesablamasi 5ci cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_6ci",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Fill Vergi hesablamasi 6ci cedvel"
|
||||
},
|
||||
{
|
||||
"fieldname": "elave_1",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Fill Elave 1"
|
||||
},
|
||||
{
|
||||
"fieldname": "elave_2",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Fill Elave 2"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_wnwr",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "Müəssisə il ərzində tam fəaliyyət göstərdiyi halda",
|
||||
"fieldname": "sətirlərdənbiriniseçin",
|
||||
"fieldtype": "Select",
|
||||
"label": "Sətirlərdən birini seçin",
|
||||
"options": "Müəssisə il ərzində yarandığı halda\nMüəssisə il ərzində tam fəaliyyət göstərdiyi halda"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_dblg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "html_uhqz",
|
||||
"fieldtype": "HTML",
|
||||
"options": "<p style=\"text-align: center;font-size: 15px;font-weight: bold;color: black;\"> Müəssisənin yaranma və ya əmlak vergisinin ödəyicisi olma tarixinə</p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_1ci_1",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Fill Vergi hesablamasi 1ci cedvel 1"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergi_hesab_4cu_1",
|
||||
"fieldtype": "Table",
|
||||
"options": "Property Tax Return AI Fill Vergi hesablamasi 4cu cedvel 1"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-29 18:27:42.028445",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Fill",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,718 @@
|
|||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Row catalogues. The strings here must match the Select options of the child
|
||||
# DocTypes byte-for-byte — otherwise Frappe rejects them as invalid options.
|
||||
# Sourced from .etaxes_notes/cabinet_internals.md and the child JSONs in this
|
||||
# folder (their Select options ARE the cabinet labels).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# elave_1 — Vergidən azad olunan əmlakların azadolma səbəbi (Tax Code articles)
|
||||
ELAVE_1_ROWS = (
|
||||
"Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.2-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.3-cü maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.4-cü maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.4.1-1-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.5-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.7-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.8-ci maddəsinə əsasən:",
|
||||
"Vergi Məcəlləsinin 199.9-cu maddəsinə əsasən:",
|
||||
"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 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ə:",
|
||||
)
|
||||
ELAVE_1_TOTAL_LABEL = "Vergidən azad olunan əmlakın CƏMİ dəyəri:"
|
||||
|
||||
# elave_2 — Vergiyə cəlb olunan əmlakların kateqoriyası
|
||||
ELAVE_2_PARENT_LABEL = "1. Binalar, tikililər və qurğular"
|
||||
ELAVE_2_CHILD_LABELS = (
|
||||
"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",
|
||||
)
|
||||
ELAVE_2_OTHER_LABELS = (
|
||||
"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",
|
||||
)
|
||||
ELAVE_2_TOTAL_LABEL = "Vergiyə cəlb olunan əmlakın CƏMİ dəyəri:"
|
||||
|
||||
# vergi_hesab_1ci — 501 group, start-of-year totals
|
||||
VH_1CI_ROWS = (
|
||||
"501. Əmlakın ümumi qalıq dəyəri",
|
||||
"501.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"501.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"501.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
)
|
||||
|
||||
# vergi_hesab_1ci_1 — 503 group, start-of-period for partial-year mode
|
||||
VH_1CI1_ROWS = (
|
||||
"503. Əmlakın ümumi dəyəri",
|
||||
"503.1 Vergiyə cəlb olunan əmlakın dəyəri",
|
||||
"503.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın dəyəri",
|
||||
"503.2 Vergidən azad olunan əmlakın dəyəri",
|
||||
)
|
||||
|
||||
# vergi_hesab_2ci — 504 group, end-of-year totals
|
||||
VH_2CI_ROWS = (
|
||||
"504. Əmlakın ümumi qalıq dəyəri",
|
||||
"504.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri",
|
||||
"504.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri",
|
||||
"504.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
)
|
||||
|
||||
# vergi_hesab_3cu — 505 group, excess-insured totals
|
||||
VH_3CU_ROWS = (
|
||||
"505 Artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.1 Vergiyə cəlb olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"505.2 Vergidən azad olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
)
|
||||
|
||||
# vergi_hesab_4cu — 506 tax rate (1% for legal entities)
|
||||
VH_4CU_ROWS = ("506 Verginin dərəcəsi",)
|
||||
|
||||
# vergi_hesab_4cu_1 — 507 number of operating months (partial-year only)
|
||||
VH_4CU1_ROWS = ("507 Müəssisə yarandığı aydan sonra ilin sonuna qədər olan ayların sayı",)
|
||||
|
||||
# vergi_hesab_5ci — 509.x computed tax + 510 reduction
|
||||
VH_5CI_ROWS = (
|
||||
"509.1 Vergi ödəyicisi il ərzində tam fəaliyyət göstərdiyi halda:",
|
||||
"509.2 Hesabat ili ərzində yaradıldığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olduğu halda:",
|
||||
"510 Vergi məcəlləsinin 199.21-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği",
|
||||
)
|
||||
|
||||
# vergi_hesab_6ci — 511.x amount payable
|
||||
VH_6CI_ROWS = (
|
||||
"511.1 Müəssisə il ərzində tam fəaliyyət göstərdiyi halda",
|
||||
"511.2 Müəssisə il ərzində yarandığı halda",
|
||||
)
|
||||
|
||||
|
||||
# (table_fieldname, text_column_fieldname, ordered row label tuple)
|
||||
SEED_PLAN = (
|
||||
("vergi_hesab_1ci", "göstəricilər", VH_1CI_ROWS),
|
||||
("vergi_hesab_1ci_1", "göstəricilər", VH_1CI1_ROWS),
|
||||
("vergi_hesab_2ci", "göstəricilər", VH_2CI_ROWS),
|
||||
("vergi_hesab_3cu", "göstəricilər", VH_3CU_ROWS),
|
||||
("vergi_hesab_4cu", "göstəricilər", VH_4CU_ROWS),
|
||||
("vergi_hesab_4cu_1", "göstəricilər", VH_4CU1_ROWS),
|
||||
("vergi_hesab_5ci", "göstəricilər", VH_5CI_ROWS),
|
||||
("vergi_hesab_6ci", "göstəricilər", VH_6CI_ROWS),
|
||||
(
|
||||
"elave_1",
|
||||
"azadolmasəbəbi",
|
||||
ELAVE_1_ROWS + (ELAVE_1_TOTAL_LABEL,),
|
||||
),
|
||||
(
|
||||
"elave_2",
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
(ELAVE_2_PARENT_LABEL,)
|
||||
+ ELAVE_2_CHILD_LABELS
|
||||
+ ELAVE_2_OTHER_LABELS
|
||||
+ (ELAVE_2_TOTAL_LABEL,),
|
||||
),
|
||||
)
|
||||
|
||||
# Float field names inside each elave child row, indexed by the tax code that
|
||||
# the column carries (`(periodPart).(side)`). Side: .1 = taxable (elave_2),
|
||||
# .2 = tax-free (elave_1).
|
||||
ELAVE_1_COLS = {
|
||||
"501.2": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012",
|
||||
"502.2": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"503.2": "hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri",
|
||||
"504.2": "ilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"505.2": "artıqqiymətəəsasvəsaitlərindəyəri",
|
||||
}
|
||||
ELAVE_2_COLS = {
|
||||
"501.1": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"502.1": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"503.1": "hesabatilierzindeuçotaalınan",
|
||||
"504.1": "hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"505.1": "artıqqiymətəəsasvəsaitlərindəyəri505",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auto-fill from accounting
|
||||
#
|
||||
# Maps Asset records (with their property-tax custom fields) to declaration
|
||||
# rows. This is the "Doldur uçotdan" feature: when the user opens a NEW
|
||||
# declaration, the form pulls Asset book values straight from ERPNext.
|
||||
#
|
||||
# Asset custom fields used (added to ERPNext Asset by this app):
|
||||
# • taxable_asset_type Select (10 options matching elave_2 row labels)
|
||||
# • tax_exempt_tax_article Link → Tax Article (199.x article reference)
|
||||
# • land Check (excluded — land has its own tax declaration)
|
||||
# • insured_value varchar (legacy — stored as text, parsed as float)
|
||||
#
|
||||
# Routing rule: tax_exempt_tax_article set → elave_1, else taxable_asset_type
|
||||
# set → elave_2. Assets without either are skipped (un-classified for tax).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Asset.taxable_asset_type option string → elave_2 row label.
|
||||
# Both sides come from the cabinet vocabulary so they line up exactly.
|
||||
TAXABLE_TYPE_TO_ELAVE_2_LABEL = {
|
||||
"Çoxmərtəbəli (çoxmənzilli) yaşayış binalar": ELAVE_2_CHILD_LABELS[0], # 1.1
|
||||
"Qeyri-yaşayış binaları (sahələri)": ELAVE_2_CHILD_LABELS[1], # 1.2
|
||||
"Əmlak kompleksi kimi müəssisələr": ELAVE_2_CHILD_LABELS[2], # 1.3
|
||||
"Qurğular": ELAVE_2_CHILD_LABELS[3], # 1.4
|
||||
"Mənzillər": ELAVE_2_CHILD_LABELS[4], # 1.5
|
||||
"Fərdi yaşayış və bağ evləri": ELAVE_2_CHILD_LABELS[5], # 1.6
|
||||
"Maşınlar və avadanlıqlar": ELAVE_2_OTHER_LABELS[0], # 2
|
||||
"Yüksək texnologiyalar məhsulu olan hesablama texnikası": ELAVE_2_OTHER_LABELS[1], # 3
|
||||
"Nəqliyyat vasitələri": ELAVE_2_OTHER_LABELS[2], # 4
|
||||
"Digər əsas vəsaitlər": ELAVE_2_OTHER_LABELS[3], # 5
|
||||
}
|
||||
|
||||
# Extracts a tax-code reference ("199.4.1", "199.4.1-1", "227.1") from any
|
||||
# label that mentions one. Both elave_1 row labels and Tax Article names embed
|
||||
# the code in free text; matching by extracted code dodges punctuation / typo
|
||||
# drift (e.g. "199.11-cu" vs "199.11-ci" in different sources).
|
||||
_ARTICLE_CODE_RE = re.compile(r"(\d+(?:\.\d+)+(?:-\d+)?)")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_seed():
|
||||
"""Returns {table_fieldname: [{text_col: label}, ...]} for client-side seeding.
|
||||
|
||||
Called from the local property_tax_return_ai_fill.js on form `onload` for new docs,
|
||||
because server-side onload/before_insert do not fire on the "+ New" Desk path
|
||||
until the user saves. This keeps Python as the single source of truth.
|
||||
"""
|
||||
return {
|
||||
table: [{text_col: label} for label in labels]
|
||||
for table, text_col, labels in SEED_PLAN
|
||||
}
|
||||
|
||||
|
||||
def _round2(value):
|
||||
return float(
|
||||
Decimal(str(float(value or 0))).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Autofill helpers — separated from the @whitelist wrapper so the math can be
|
||||
# unit-tested with synthetic dicts (no DB round-trips required).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _to_date(value):
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
if isinstance(value, str) and value:
|
||||
return datetime.strptime(value[:10], "%Y-%m-%d").date()
|
||||
return None
|
||||
|
||||
|
||||
def _parse_insured(value):
|
||||
"""`Asset.insured_value` is stored as varchar in this app; coerce safely."""
|
||||
if value in (None, "", 0):
|
||||
return 0.0
|
||||
try:
|
||||
return float(str(value).replace(",", "."))
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _article_code(label):
|
||||
"""Extract a tax-code reference from a label. Returns None if absent."""
|
||||
if not label:
|
||||
return None
|
||||
m = _ARTICLE_CODE_RE.search(label)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _build_e1_code_index():
|
||||
"""Article code → elave_1 row label. Rows that name no article are dropped."""
|
||||
out = {}
|
||||
for label in ELAVE_1_ROWS:
|
||||
c = _article_code(label)
|
||||
if c:
|
||||
out[c] = label
|
||||
return out
|
||||
|
||||
|
||||
def _asset_cost(asset):
|
||||
"""Best-available capitalized cost — `total_asset_cost` includes additional
|
||||
costs (transport, install); falls back to `purchase_amount` when missing.
|
||||
"""
|
||||
return float(asset.get("total_asset_cost") or asset.get("purchase_amount") or 0)
|
||||
|
||||
|
||||
def _residual_at(asset, depr_rows, target):
|
||||
"""Book value of `asset` on date `target`.
|
||||
|
||||
Reads from Depreciation Schedule rows (cumulative `accumulated_depreciation_amount`
|
||||
per `schedule_date`) — picks the latest row at or before `target`.
|
||||
|
||||
Assets without a schedule (land, non-depreciating fixtures) keep their full
|
||||
cost; that matches how the cabinet treats undepreciated fixed assets.
|
||||
Floors at 0 — protects against rounding artefacts when an asset is fully
|
||||
depreciated.
|
||||
"""
|
||||
cost = _asset_cost(asset)
|
||||
if not depr_rows:
|
||||
return max(0.0, cost)
|
||||
accumulated = 0.0
|
||||
for r in depr_rows:
|
||||
sd = _to_date(r.get("schedule_date"))
|
||||
if sd and sd <= target:
|
||||
# `accumulated_depreciation_amount` is monotonic non-decreasing,
|
||||
# so max() is the correct aggregate even if rows arrive unordered.
|
||||
v = float(r.get("accumulated_depreciation_amount") or 0)
|
||||
if v > accumulated:
|
||||
accumulated = v
|
||||
return max(0.0, cost - accumulated)
|
||||
|
||||
|
||||
def _compute_autofill(assets, depr_by_asset, year, il_erzinde, establishment_date,
|
||||
article_to_code, e1_code_to_label):
|
||||
"""Pure compute step — no I/O. See `autofill_from_accounting` for the wrapper.
|
||||
|
||||
Returns: `{"elave_1": {label: {code: amount}}, "elave_2": {…}, "ayla": int,
|
||||
"unmatched": [debug]}`. Caller (JS) reads these into seeded rows.
|
||||
"""
|
||||
y_start = date(year, 1, 1)
|
||||
y_end = date(year, 12, 31)
|
||||
|
||||
totals_e1 = defaultdict(lambda: defaultdict(float))
|
||||
totals_e2 = defaultdict(lambda: defaultdict(float))
|
||||
unmatched = []
|
||||
|
||||
for a in assets:
|
||||
purchase = _to_date(a.get("purchase_date"))
|
||||
available = _to_date(a.get("available_for_use_date"))
|
||||
# When both are present, asset is "active" from the earlier — cabinet
|
||||
# cares about when the asset entered the books, not the formal P.O.
|
||||
active_from = purchase or available or y_start
|
||||
if available and (not purchase or available < purchase):
|
||||
active_from = available
|
||||
|
||||
disposal = _to_date(a.get("disposal_date"))
|
||||
|
||||
# Skip assets entirely outside `year`.
|
||||
if active_from > y_end:
|
||||
continue
|
||||
if disposal and disposal < y_start:
|
||||
continue
|
||||
|
||||
depr = depr_by_asset.get(a.get("name"), [])
|
||||
res_start = _residual_at(a, depr, y_start)
|
||||
res_end = _residual_at(a, depr, y_end)
|
||||
res_acq = _residual_at(a, depr, active_from) if y_start <= active_from <= y_end else 0
|
||||
res_disp = _residual_at(a, depr, disposal) if (disposal and y_start <= disposal <= y_end) else 0
|
||||
|
||||
owned_at_start = active_from < y_start and (not disposal or disposal >= y_start)
|
||||
owned_at_end = active_from <= y_end and (not disposal or disposal > y_end)
|
||||
bought_in_year = y_start <= active_from <= y_end
|
||||
disposed_in_year = bool(disposal and y_start <= disposal <= y_end)
|
||||
|
||||
v_501 = res_start if owned_at_start else 0.0
|
||||
v_502 = res_disp if disposed_in_year else 0.0
|
||||
# 503 captures the value of newly-acquired assets that survived to year-end.
|
||||
# Disposed-in-same-year assets sit in 502 only (matches the cabinet pattern
|
||||
# of "acquired then disposed in the year" being represented once, not twice).
|
||||
v_503 = res_acq if (bought_in_year and not disposed_in_year) else 0.0
|
||||
v_504 = res_end if owned_at_end else 0.0
|
||||
# 505 — assets insured above their book value at year-end contribute the
|
||||
# spread. Only when the asset is still owned at year-end (matches cabinet
|
||||
# appendix-2.excessInsured semantics).
|
||||
insured = _parse_insured(a.get("insured_value"))
|
||||
v_505 = max(0.0, insured - res_end) if v_504 > 0 else 0.0
|
||||
|
||||
if a.get("tax_exempt_tax_article"):
|
||||
# tax_exempt wins over taxable_asset_type when both happen to be set
|
||||
# — exemption is the safer default for an ambiguously-classified asset.
|
||||
code = article_to_code.get(a["tax_exempt_tax_article"])
|
||||
row_label = e1_code_to_label.get(code) if code else None
|
||||
if not row_label:
|
||||
unmatched.append({
|
||||
"asset": a.get("name"),
|
||||
"article": a["tax_exempt_tax_article"],
|
||||
"code": code,
|
||||
"reason": "no elave_1 row for this tax-article code",
|
||||
})
|
||||
continue
|
||||
t = totals_e1[row_label]
|
||||
t["501.2"] += v_501
|
||||
t["502.2"] += v_502
|
||||
t["503.2"] += v_503
|
||||
t["504.2"] += v_504
|
||||
t["505.2"] += v_505
|
||||
elif a.get("taxable_asset_type"):
|
||||
row_label = TAXABLE_TYPE_TO_ELAVE_2_LABEL.get(a["taxable_asset_type"])
|
||||
if not row_label:
|
||||
unmatched.append({
|
||||
"asset": a.get("name"),
|
||||
"type": a["taxable_asset_type"],
|
||||
"reason": "taxable_asset_type does not map to any elave_2 row",
|
||||
})
|
||||
continue
|
||||
t = totals_e2[row_label]
|
||||
t["501.1"] += v_501
|
||||
t["502.1"] += v_502
|
||||
t["503.1"] += v_503
|
||||
t["504.1"] += v_504
|
||||
t["505.1"] += v_505
|
||||
# else: asset not classified for property tax — skip silently.
|
||||
|
||||
# 507 ayla — populated only when the company itself was newly established
|
||||
# within `year`. We don't infer it from purchase dates because that would
|
||||
# conflate "first property bought mid-year" (full-year mode, line 501)
|
||||
# with "company born mid-year" (partial-year mode, line 503/507).
|
||||
ayla = 0
|
||||
if il_erzinde and establishment_date and establishment_date.year == year:
|
||||
ayla = 12 - establishment_date.month + 1
|
||||
|
||||
return {
|
||||
"elave_1": {label: dict(cols) for label, cols in totals_e1.items()},
|
||||
"elave_2": {label: dict(cols) for label, cols in totals_e2.items()},
|
||||
"ayla": ayla,
|
||||
"unmatched": unmatched,
|
||||
}
|
||||
|
||||
|
||||
def _resolve_company():
|
||||
"""Single-company install — prefer user default; otherwise use the lone Company."""
|
||||
default = frappe.defaults.get_user_default("Company")
|
||||
if default:
|
||||
return default
|
||||
rows = frappe.get_all("Company", pluck="name", limit=2)
|
||||
if len(rows) == 1:
|
||||
return rows[0]
|
||||
if not rows:
|
||||
frappe.throw("Heç bir Company tapılmadı. Auto-fill üçün ən azı bir Company tələb olunur.")
|
||||
frappe.throw("Birdən çox Company var; istifadəçi defaultu təyin edin və ya birini seçin.")
|
||||
|
||||
|
||||
def _fetch_asset_data(company):
|
||||
"""Read Asset + Depreciation Schedule rows in two queries (no N+1).
|
||||
|
||||
Filters Assets to those classified for property tax (either side of the
|
||||
elave_1 / elave_2 routing). Land is excluded — it lives in the Land Tax
|
||||
declaration. Cancelled documents are excluded.
|
||||
"""
|
||||
assets = frappe.get_all(
|
||||
"Asset",
|
||||
filters=[
|
||||
["company", "=", company],
|
||||
["docstatus", "!=", 2],
|
||||
["land", "!=", 1],
|
||||
],
|
||||
or_filters=[
|
||||
["taxable_asset_type", "is", "set"],
|
||||
["tax_exempt_tax_article", "is", "set"],
|
||||
],
|
||||
fields=[
|
||||
"name", "purchase_date", "available_for_use_date", "disposal_date",
|
||||
"purchase_amount", "total_asset_cost",
|
||||
"value_after_depreciation", "insured_value",
|
||||
"taxable_asset_type", "tax_exempt_tax_article",
|
||||
"status",
|
||||
],
|
||||
)
|
||||
depr_by_asset = defaultdict(list)
|
||||
if assets:
|
||||
names = [a["name"] for a in assets]
|
||||
rows = frappe.db.sql(
|
||||
"""
|
||||
SELECT ads.asset, ds.schedule_date,
|
||||
ds.depreciation_amount, ds.accumulated_depreciation_amount
|
||||
FROM `tabDepreciation Schedule` ds
|
||||
JOIN `tabAsset Depreciation Schedule` ads ON ds.parent = ads.name
|
||||
WHERE ads.asset IN %(names)s
|
||||
AND ads.docstatus = 1
|
||||
""",
|
||||
{"names": tuple(names)},
|
||||
as_dict=True,
|
||||
)
|
||||
for r in rows:
|
||||
depr_by_asset[r["asset"]].append(r)
|
||||
return assets, dict(depr_by_asset)
|
||||
|
||||
|
||||
def _tax_article_code_map(article_names):
|
||||
"""Tax Article `name` → article code (regex-extracted). Skips unparseable rows."""
|
||||
if not article_names:
|
||||
return {}
|
||||
rows = frappe.get_all(
|
||||
"Tax Article",
|
||||
filters=[["name", "in", list(article_names)]],
|
||||
fields=["name"],
|
||||
)
|
||||
out = {}
|
||||
for r in rows:
|
||||
c = _article_code(r["name"])
|
||||
if c:
|
||||
out[r["name"]] = c
|
||||
return out
|
||||
|
||||
|
||||
def _find_row(table, text_col, value):
|
||||
for row in table or []:
|
||||
if getattr(row, text_col, None) == value:
|
||||
return row
|
||||
return None
|
||||
|
||||
|
||||
class PropertyTaxReturnAIFill(Document):
|
||||
# ------------------------------------------------------------------
|
||||
# Lifecycle hooks
|
||||
# ------------------------------------------------------------------
|
||||
def onload(self):
|
||||
self._seed_child_tables()
|
||||
|
||||
def before_insert(self):
|
||||
self._seed_child_tables()
|
||||
|
||||
def validate(self):
|
||||
self._seed_child_tables()
|
||||
self.recalculate()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Seeding — populate empty child tables with their cabinet rows so
|
||||
# the user sees a ready-to-fill grid on a fresh "+ New" form.
|
||||
# ------------------------------------------------------------------
|
||||
def _seed_child_tables(self):
|
||||
for table_field, text_col, labels in SEED_PLAN:
|
||||
if self.get(table_field):
|
||||
continue
|
||||
for label in labels:
|
||||
self.append(table_field, {text_col: label})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Recalculation — fires on validate() (every save).
|
||||
# ------------------------------------------------------------------
|
||||
def recalculate(self):
|
||||
e1 = self._recalc_elave_1()
|
||||
e2 = self._recalc_elave_2()
|
||||
self._recalc_vh_1ci(e1, e2)
|
||||
self._recalc_vh_1ci_1(e1, e2)
|
||||
self._recalc_vh_2ci(e1, e2)
|
||||
self._recalc_vh_3cu(e1, e2)
|
||||
self._recalc_vh_4cu()
|
||||
tax_509 = self._recalc_vh_5ci(e2)
|
||||
self._recalc_vh_6ci(tax_509)
|
||||
|
||||
def _recalc_elave_1(self):
|
||||
"""Sum each numeric column over the indicator rows and write into CƏMİ.
|
||||
|
||||
Returns a dict keyed by tax-code (`501.2`, `502.2`, …) with rounded totals.
|
||||
"""
|
||||
totals = {code: 0.0 for code in ELAVE_1_COLS}
|
||||
for row in self.elave_1 or []:
|
||||
if getattr(row, "azadolmasəbəbi", None) == ELAVE_1_TOTAL_LABEL:
|
||||
continue
|
||||
for code, fieldname in ELAVE_1_COLS.items():
|
||||
totals[code] += float(getattr(row, fieldname, 0) or 0)
|
||||
|
||||
cəmi = _find_row(self.elave_1, "azadolmasəbəbi", ELAVE_1_TOTAL_LABEL)
|
||||
if cəmi is not None:
|
||||
for code, fieldname in ELAVE_1_COLS.items():
|
||||
setattr(cəmi, fieldname, _round2(totals[code]))
|
||||
|
||||
return {code: _round2(amount) for code, amount in totals.items()}
|
||||
|
||||
def _recalc_elave_2(self):
|
||||
"""Set _5001 parent row to sum of 1.1–1.6 children, then write CƏMİ totals."""
|
||||
|
||||
# Step 1: roll up sub-categories 1.1–1.6 into the parent "1. Binalar..." row.
|
||||
child_sums = {code: 0.0 for code in ELAVE_2_COLS}
|
||||
for row in self.elave_2 or []:
|
||||
label = getattr(row, "vergiyəcəlbolunanəmlaklarınkateqoriyası", None)
|
||||
if label in ELAVE_2_CHILD_LABELS:
|
||||
for code, fieldname in ELAVE_2_COLS.items():
|
||||
child_sums[code] += float(getattr(row, fieldname, 0) or 0)
|
||||
|
||||
parent = _find_row(
|
||||
self.elave_2,
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
ELAVE_2_PARENT_LABEL,
|
||||
)
|
||||
if parent is not None:
|
||||
for code, fieldname in ELAVE_2_COLS.items():
|
||||
setattr(parent, fieldname, _round2(child_sums[code]))
|
||||
|
||||
# Step 2: CƏMİ totals — exclude parent (avoids double-count) and CƏMİ itself.
|
||||
totals = {code: 0.0 for code in ELAVE_2_COLS}
|
||||
for row in self.elave_2 or []:
|
||||
label = getattr(row, "vergiyəcəlbolunanəmlaklarınkateqoriyası", None)
|
||||
if label in (ELAVE_2_PARENT_LABEL, ELAVE_2_TOTAL_LABEL):
|
||||
continue
|
||||
for code, fieldname in ELAVE_2_COLS.items():
|
||||
totals[code] += float(getattr(row, fieldname, 0) or 0)
|
||||
|
||||
cəmi = _find_row(
|
||||
self.elave_2,
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
ELAVE_2_TOTAL_LABEL,
|
||||
)
|
||||
if cəmi is not None:
|
||||
for code, fieldname in ELAVE_2_COLS.items():
|
||||
setattr(cəmi, fieldname, _round2(totals[code]))
|
||||
|
||||
return {code: _round2(amount) for code, amount in totals.items()}
|
||||
|
||||
def _set_row_amount(self, table_field, text_col, label, amount_col, value):
|
||||
row = _find_row(self.get(table_field), text_col, label)
|
||||
if row is not None:
|
||||
setattr(row, amount_col, _round2(value))
|
||||
|
||||
def _recalc_vh_1ci(self, e1, e2):
|
||||
s = self._set_row_amount
|
||||
s("vergi_hesab_1ci", "göstəricilər", VH_1CI_ROWS[0], "manatla", e1["501.2"] + e2["501.1"])
|
||||
s("vergi_hesab_1ci", "göstəricilər", VH_1CI_ROWS[1], "manatla", e2["501.1"])
|
||||
# 501.1.1 — Qeyri-kommersiya. No accounting integration → permanent 0.
|
||||
s("vergi_hesab_1ci", "göstəricilər", VH_1CI_ROWS[2], "manatla", 0)
|
||||
s("vergi_hesab_1ci", "göstəricilər", VH_1CI_ROWS[3], "manatla", e1["501.2"])
|
||||
|
||||
def _recalc_vh_1ci_1(self, e1, e2):
|
||||
s = self._set_row_amount
|
||||
s("vergi_hesab_1ci_1", "göstəricilər", VH_1CI1_ROWS[0], "manatla", e1["503.2"] + e2["503.1"])
|
||||
s("vergi_hesab_1ci_1", "göstəricilər", VH_1CI1_ROWS[1], "manatla", e2["503.1"])
|
||||
s("vergi_hesab_1ci_1", "göstəricilər", VH_1CI1_ROWS[2], "manatla", 0)
|
||||
s("vergi_hesab_1ci_1", "göstəricilər", VH_1CI1_ROWS[3], "manatla", e1["503.2"])
|
||||
|
||||
def _recalc_vh_2ci(self, e1, e2):
|
||||
s = self._set_row_amount
|
||||
s("vergi_hesab_2ci", "göstəricilər", VH_2CI_ROWS[0], "manatla", e1["504.2"] + e2["504.1"])
|
||||
s("vergi_hesab_2ci", "göstəricilər", VH_2CI_ROWS[1], "manatla", e2["504.1"])
|
||||
s("vergi_hesab_2ci", "göstəricilər", VH_2CI_ROWS[2], "manatla", 0)
|
||||
s("vergi_hesab_2ci", "göstəricilər", VH_2CI_ROWS[3], "manatla", e1["504.2"])
|
||||
|
||||
def _recalc_vh_3cu(self, e1, e2):
|
||||
s = self._set_row_amount
|
||||
s("vergi_hesab_3cu", "göstəricilər", VH_3CU_ROWS[0], "manatla", e1["505.2"] + e2["505.1"])
|
||||
s("vergi_hesab_3cu", "göstəricilər", VH_3CU_ROWS[1], "manatla", e2["505.1"])
|
||||
s("vergi_hesab_3cu", "göstəricilər", VH_3CU_ROWS[2], "manatla", e1["505.2"])
|
||||
|
||||
def _recalc_vh_4cu(self):
|
||||
# Legal-entity rate is 1% (cabinet field `506`).
|
||||
self._set_row_amount(
|
||||
"vergi_hesab_4cu", "göstəricilər", VH_4CU_ROWS[0], "faizlə", 1.0
|
||||
)
|
||||
|
||||
def _recalc_vh_5ci(self, e2):
|
||||
"""Compute 509.1 (full-year) or 509.2 (partial-year) per Object Activity flag.
|
||||
|
||||
Returns the computed tax for the active scenario, which `_recalc_vh_6ci`
|
||||
feeds into 511.x.
|
||||
"""
|
||||
months_row = _find_row(
|
||||
self.get("vergi_hesab_4cu_1"), "göstəricilər", VH_4CU1_ROWS[0]
|
||||
)
|
||||
months = float(getattr(months_row, "ayla", 0) or 0) if months_row else 0.0
|
||||
|
||||
s501 = e2["501.1"]
|
||||
s503 = e2["503.1"]
|
||||
s504 = e2["504.1"]
|
||||
s505 = e2["505.1"]
|
||||
|
||||
# Verified vs cabinet — see .etaxes_notes/cabinet_internals.md, sections
|
||||
# "THE main tax formula" (509.1) and "Partial-year tax formula" (509.2).
|
||||
tax_full = ((s501 + s504) / 2.0) * 0.01 + s505 * 0.01
|
||||
tax_partial = (
|
||||
(((s503 + s504) / 24.0) * months) * 0.01 + s505 * 0.01
|
||||
if months
|
||||
else s505 * 0.01
|
||||
)
|
||||
|
||||
if bool(self.il_erzinde):
|
||||
tax_509_2 = _round2(tax_partial)
|
||||
self._set_row_amount("vergi_hesab_5ci", "göstəricilər", VH_5CI_ROWS[0], "manatla", 0)
|
||||
self._set_row_amount("vergi_hesab_5ci", "göstəricilər", VH_5CI_ROWS[1], "manatla", tax_509_2)
|
||||
return tax_509_2
|
||||
|
||||
tax_509_1 = _round2(tax_full)
|
||||
self._set_row_amount("vergi_hesab_5ci", "göstəricilər", VH_5CI_ROWS[0], "manatla", tax_509_1)
|
||||
self._set_row_amount("vergi_hesab_5ci", "göstəricilər", VH_5CI_ROWS[1], "manatla", 0)
|
||||
return tax_509_1
|
||||
|
||||
def _recalc_vh_6ci(self, tax_509):
|
||||
# 511.x mirrors 509.x — same as cabinet (`shouldBePaidTax[0]` ==
|
||||
# `calculatedTax[0]`) and the original `calculate_509_*` in
|
||||
# property_tax_return.js. 510 ("shouldBeReducedTax") is reported in its
|
||||
# own row but is NOT subtracted from the payable amount.
|
||||
payable = _round2(tax_509)
|
||||
if bool(self.il_erzinde):
|
||||
self._set_row_amount("vergi_hesab_6ci", "göstəricilər", VH_6CI_ROWS[0], "manatla", 0)
|
||||
self._set_row_amount("vergi_hesab_6ci", "göstəricilər", VH_6CI_ROWS[1], "manatla", payable)
|
||||
else:
|
||||
self._set_row_amount("vergi_hesab_6ci", "göstəricilər", VH_6CI_ROWS[0], "manatla", payable)
|
||||
self._set_row_amount("vergi_hesab_6ci", "göstəricilər", VH_6CI_ROWS[1], "manatla", 0)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def autofill_from_accounting(doc=None, docname=None):
|
||||
"""Server endpoint for the JS auto-fill trigger.
|
||||
|
||||
Two call paths:
|
||||
• From a NEW (unsaved) form — JS sends `doc` as a JSON-encoded snapshot
|
||||
of `frm.doc`. We never touch the DB on the parent side.
|
||||
• From a SAVED form's "Yenidən doldur" button — JS sends `docname` and
|
||||
we re-read `il / il_erzinde / tam_il` from the persisted record.
|
||||
|
||||
Returns the result of `_compute_autofill`. JS clears existing numeric cells
|
||||
in the seeded rows and writes the per-row totals — keeping all UI mutation
|
||||
on the client so this method stays a pure read-then-compute step.
|
||||
"""
|
||||
if doc:
|
||||
if isinstance(doc, str):
|
||||
doc = json.loads(doc)
|
||||
il = doc.get("il")
|
||||
il_erzinde = bool(doc.get("il_erzinde"))
|
||||
elif docname:
|
||||
d = frappe.get_doc("Property Tax Return AI Fill", docname)
|
||||
il = d.il
|
||||
il_erzinde = bool(d.il_erzinde)
|
||||
else:
|
||||
frappe.throw("autofill_from_accounting üçün `doc` və ya `docname` lazımdır.")
|
||||
|
||||
if not il:
|
||||
# Without a target year there's nothing to compute. JS handles the
|
||||
# empty payload as a no-op so the user can finish setting the year.
|
||||
return {"elave_1": {}, "elave_2": {}, "ayla": 0, "company": None, "unmatched": []}
|
||||
|
||||
company = _resolve_company()
|
||||
assets, depr_by_asset = _fetch_asset_data(company)
|
||||
article_to_code = _tax_article_code_map(
|
||||
{a["tax_exempt_tax_article"] for a in assets if a.get("tax_exempt_tax_article")}
|
||||
)
|
||||
establishment = _to_date(
|
||||
frappe.db.get_value("Company", company, "date_of_establishment")
|
||||
)
|
||||
result = _compute_autofill(
|
||||
assets=assets,
|
||||
depr_by_asset=depr_by_asset,
|
||||
year=int(il),
|
||||
il_erzinde=il_erzinde,
|
||||
establishment_date=establishment,
|
||||
article_to_code=article_to_code,
|
||||
e1_code_to_label=_build_e1_code_index(),
|
||||
)
|
||||
result["company"] = company
|
||||
return result
|
||||
|
|
@ -0,0 +1,518 @@
|
|||
"""Programmatic smoke test for Property Tax Return AI Fill formulas.
|
||||
|
||||
Run via:
|
||||
bench --site frontend execute \
|
||||
taxes_az.taxes_az.doctype.property_tax_return_ai_fill.test_smoke.smoke_run
|
||||
|
||||
Verifies the two cabinet-verified scenarios from .etaxes_notes/cabinet_internals.md:
|
||||
- Full-year (tam_il): elave_2[1.1] periodEnd=200 + excessInsured=300 → 509.1 = 4.00
|
||||
- Partial-year (il_erzinde): elave_2[1.1] onCreate=100 + periodEnd=200 + excessInsured=300,
|
||||
507.ayla=6 → 509.2 = 3.75
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
|
||||
from taxes_az.taxes_az.doctype.property_tax_return_ai_fill.property_tax_return_ai_fill import (
|
||||
ELAVE_1_ROWS,
|
||||
ELAVE_2_CHILD_LABELS,
|
||||
ELAVE_2_OTHER_LABELS,
|
||||
ELAVE_2_TOTAL_LABEL,
|
||||
ELAVE_2_PARENT_LABEL,
|
||||
VH_5CI_ROWS,
|
||||
VH_6CI_ROWS,
|
||||
VH_4CU_ROWS,
|
||||
VH_4CU1_ROWS,
|
||||
VH_2CI_ROWS,
|
||||
VH_3CU_ROWS,
|
||||
VH_1CI1_ROWS,
|
||||
_article_code,
|
||||
_build_e1_code_index,
|
||||
_compute_autofill,
|
||||
autofill_from_accounting,
|
||||
)
|
||||
|
||||
|
||||
def _check(label, expected, actual, tol=0.005):
|
||||
ok = abs(float(actual or 0) - float(expected)) <= tol
|
||||
icon = "✓" if ok else "✗"
|
||||
print(f" {icon} {label}: expected={expected} actual={actual}")
|
||||
return ok
|
||||
|
||||
|
||||
def _row(table, text_col, label):
|
||||
for r in table:
|
||||
if getattr(r, text_col, None) == label:
|
||||
return r
|
||||
raise LookupError(f"row not found: {label!r} in {text_col!r}")
|
||||
|
||||
|
||||
def _make_doc():
|
||||
doc = frappe.new_doc("Property Tax Return AI Fill")
|
||||
doc.il = 2025
|
||||
doc.vergidövrü = "İllik"
|
||||
doc.ödəyicitipi = "Hüquqi"
|
||||
doc.run_method("before_insert") # seed all child tables
|
||||
return doc
|
||||
|
||||
|
||||
def run_seeding():
|
||||
print("\n=== TEST 0: seeding populates all 10 child tables ===")
|
||||
expectations = [
|
||||
("vergi_hesab_1ci", 4),
|
||||
("vergi_hesab_1ci_1", 4),
|
||||
("vergi_hesab_2ci", 4),
|
||||
("vergi_hesab_3cu", 3),
|
||||
("vergi_hesab_4cu", 1),
|
||||
("vergi_hesab_4cu_1", 1),
|
||||
("vergi_hesab_5ci", 3),
|
||||
("vergi_hesab_6ci", 2),
|
||||
("elave_1", 22), # 21 indicators + CƏMİ
|
||||
("elave_2", 12), # 1 parent + 6 children + 4 other + CƏMİ
|
||||
]
|
||||
|
||||
# Path 1: before_insert (REST/API creation).
|
||||
doc_bi = frappe.new_doc("Property Tax Return AI Fill")
|
||||
doc_bi.run_method("before_insert")
|
||||
|
||||
# Path 2: onload (Desk "+ New" form open).
|
||||
doc_ol = frappe.new_doc("Property Tax Return AI Fill")
|
||||
doc_ol.run_method("onload")
|
||||
|
||||
results = []
|
||||
for table_field, expected_len in expectations:
|
||||
results.append(
|
||||
_check(f"before_insert: {table_field}", expected_len, len(doc_bi.get(table_field) or []), tol=0)
|
||||
)
|
||||
results.append(
|
||||
_check(f"onload: {table_field}", expected_len, len(doc_ol.get(table_field) or []), tol=0)
|
||||
)
|
||||
return all(results)
|
||||
|
||||
|
||||
def run_full_year():
|
||||
print("\n=== TEST A: tam_il (full-year, 509.1 expected 4.00) ===")
|
||||
doc = _make_doc()
|
||||
doc.tam_il = 1
|
||||
doc.il_erzinde = 0
|
||||
|
||||
# Cabinet-verified inputs: taxable[_5002] (= "1.1 Çoxmərtəbəli...") with
|
||||
# 501.1 = 0, 504.1 = 200, 505.1 = 300.
|
||||
sub = _row(
|
||||
doc.elave_2, "vergiyəcəlbolunanəmlaklarınkateqoriyası", ELAVE_2_CHILD_LABELS[0]
|
||||
)
|
||||
sub.hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri = 0 # 501.1
|
||||
sub.hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri = 200 # 504.1
|
||||
sub.artıqqiymətəəsasvəsaitlərindəyəri505 = 300 # 505.1
|
||||
|
||||
doc.recalculate()
|
||||
|
||||
# Parent _5001 must auto-sum 1.1–1.6 children. Only 1.1 has values → identical.
|
||||
parent = _row(
|
||||
doc.elave_2,
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
ELAVE_2_PARENT_LABEL,
|
||||
)
|
||||
e2_cəmi = _row(
|
||||
doc.elave_2,
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
ELAVE_2_TOTAL_LABEL,
|
||||
)
|
||||
vh2 = _row(doc.vergi_hesab_2ci, "göstəricilər", VH_2CI_ROWS[1]) # 504.1
|
||||
vh3 = _row(doc.vergi_hesab_3cu, "göstəricilər", VH_3CU_ROWS[1]) # 505.1
|
||||
vh4 = _row(doc.vergi_hesab_4cu, "göstəricilər", VH_4CU_ROWS[0]) # 506 rate
|
||||
vh5_1 = _row(doc.vergi_hesab_5ci, "göstəricilər", VH_5CI_ROWS[0]) # 509.1
|
||||
vh5_2 = _row(doc.vergi_hesab_5ci, "göstəricilər", VH_5CI_ROWS[1]) # 509.2
|
||||
vh6_1 = _row(doc.vergi_hesab_6ci, "göstəricilər", VH_6CI_ROWS[0]) # 511.1
|
||||
vh6_2 = _row(doc.vergi_hesab_6ci, "göstəricilər", VH_6CI_ROWS[1]) # 511.2
|
||||
|
||||
results = []
|
||||
results.append(_check("parent _5001.504.1 auto-sum", 200, parent.hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri))
|
||||
results.append(_check("parent _5001.505.1 auto-sum", 300, parent.artıqqiymətəəsasvəsaitlərindəyəri505))
|
||||
# CƏMİ totals must EXCLUDE the parent (no double-count).
|
||||
results.append(_check("elave_2 CƏMİ 504.1", 200, e2_cəmi.hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri))
|
||||
results.append(_check("elave_2 CƏMİ 505.1", 300, e2_cəmi.artıqqiymətəəsasvəsaitlərindəyəri505))
|
||||
# 504.1 / 505.1 mirrors into vergi_hesab_2ci / 3cu
|
||||
results.append(_check("vergi_hesab_2ci 504.1.manatla", 200, vh2.manatla))
|
||||
results.append(_check("vergi_hesab_3cu 505.1.manatla", 300, vh3.manatla))
|
||||
# 506 rate = 1
|
||||
results.append(_check("vergi_hesab_4cu 506.faizlə", 1.0, vh4.faizlə))
|
||||
# Tax 509.1 = ((0+200)/2)*0.01 + 300*0.01 = 1 + 3 = 4.00
|
||||
results.append(_check("509.1 full-year tax", 4.00, vh5_1.manatla))
|
||||
results.append(_check("509.2 (unused in full-year)", 0, vh5_2.manatla))
|
||||
# 511.1 mirrors 509.1 (cabinet `shouldBePaidTax[0]` == `calculatedTax[0]`)
|
||||
results.append(_check("511.1 mirrors 509.1", 4.00, vh6_1.manatla))
|
||||
results.append(_check("511.2 (unused in full-year)", 0, vh6_2.manatla))
|
||||
return all(results)
|
||||
|
||||
|
||||
def run_partial_year():
|
||||
print("\n=== TEST B: il_erzinde (partial-year, 509.2 expected 3.75) ===")
|
||||
doc = _make_doc()
|
||||
doc.tam_il = 0
|
||||
doc.il_erzinde = 1
|
||||
|
||||
# Cabinet-verified inputs for OBJECT_ESTABLISHED (2026-05-29):
|
||||
# taxable[_5002].onCreateDate=100, periodEnd=200, excessInsured=300
|
||||
# months = 6
|
||||
# Expected: ((100+200)/24 * 6)*0.01 + 300*0.01 = 0.75 + 3 = 3.75
|
||||
sub = _row(
|
||||
doc.elave_2, "vergiyəcəlbolunanəmlaklarınkateqoriyası", ELAVE_2_CHILD_LABELS[0]
|
||||
)
|
||||
sub.hesabatilierzindeuçotaalınan = 100 # 503.1
|
||||
sub.hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri = 200 # 504.1
|
||||
sub.artıqqiymətəəsasvəsaitlərindəyəri505 = 300 # 505.1
|
||||
|
||||
months_row = _row(doc.vergi_hesab_4cu_1, "göstəricilər", VH_4CU1_ROWS[0])
|
||||
months_row.ayla = 6
|
||||
|
||||
doc.recalculate()
|
||||
|
||||
vh1_1 = _row(doc.vergi_hesab_1ci_1, "göstəricilər", VH_1CI1_ROWS[1]) # 503.1
|
||||
vh5_1 = _row(doc.vergi_hesab_5ci, "göstəricilər", VH_5CI_ROWS[0]) # 509.1
|
||||
vh5_2 = _row(doc.vergi_hesab_5ci, "göstəricilər", VH_5CI_ROWS[1]) # 509.2
|
||||
vh6_1 = _row(doc.vergi_hesab_6ci, "göstəricilər", VH_6CI_ROWS[0]) # 511.1
|
||||
vh6_2 = _row(doc.vergi_hesab_6ci, "göstəricilər", VH_6CI_ROWS[1]) # 511.2
|
||||
|
||||
results = []
|
||||
results.append(_check("vergi_hesab_1ci_1 503.1.manatla", 100, vh1_1.manatla))
|
||||
results.append(_check("509.1 (unused in partial-year)", 0, vh5_1.manatla))
|
||||
results.append(_check("509.2 partial-year tax", 3.75, vh5_2.manatla))
|
||||
results.append(_check("511.1 (unused in partial-year)", 0, vh6_1.manatla))
|
||||
results.append(_check("511.2 payable", 3.75, vh6_2.manatla))
|
||||
return all(results)
|
||||
|
||||
|
||||
def run_elave_1_totals():
|
||||
print("\n=== TEST C: elave_1 CƏMİ totals ===")
|
||||
doc = _make_doc()
|
||||
# Fill 3 different rows in 2 columns. CƏMİ row should reflect the sums.
|
||||
doc.elave_1[0].hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012 = 100 # 501.2
|
||||
doc.elave_1[0].ilinsonunaəsasvəsaitlərinqalıqdəyəri = 50 # 504.2
|
||||
doc.elave_1[5].hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012 = 200
|
||||
doc.elave_1[10].hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012 = 400
|
||||
doc.elave_1[10].ilinsonunaəsasvəsaitlərinqalıqdəyəri = 33
|
||||
|
||||
doc.recalculate()
|
||||
|
||||
cəmi = _row(doc.elave_1, "azadolmasəbəbi", "Vergidən azad olunan əmlakın CƏMİ dəyəri:")
|
||||
results = []
|
||||
results.append(_check("elave_1 CƏMİ 501.2", 700, cəmi.hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012))
|
||||
results.append(_check("elave_1 CƏMİ 504.2", 83, cəmi.ilinsonunaəsasvəsaitlərinqalıqdəyəri))
|
||||
return all(results)
|
||||
|
||||
|
||||
def run_persistence():
|
||||
print("\n=== TEST D: round-trip persistence ===")
|
||||
doc = _make_doc()
|
||||
doc.tam_il = 1
|
||||
doc.il_erzinde = 0
|
||||
sub = _row(
|
||||
doc.elave_2, "vergiyəcəlbolunanəmlaklarınkateqoriyası", ELAVE_2_CHILD_LABELS[0]
|
||||
)
|
||||
sub.hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri = 200
|
||||
sub.artıqqiymətəəsasvəsaitlərindəyəri505 = 300
|
||||
doc.insert(ignore_permissions=True)
|
||||
name = doc.name
|
||||
frappe.db.commit()
|
||||
|
||||
reloaded = frappe.get_doc("Property Tax Return AI Fill", name)
|
||||
vh5_1 = _row(reloaded.vergi_hesab_5ci, "göstəricilər", VH_5CI_ROWS[0])
|
||||
vh6_1 = _row(reloaded.vergi_hesab_6ci, "göstəricilər", VH_6CI_ROWS[0])
|
||||
|
||||
results = []
|
||||
results.append(_check("reloaded elave_2 row count", 12, len(reloaded.elave_2), tol=0))
|
||||
results.append(_check("reloaded 509.1", 4.00, vh5_1.manatla))
|
||||
results.append(_check("reloaded 511.1", 4.00, vh6_1.manatla))
|
||||
|
||||
# Cleanup
|
||||
frappe.delete_doc("Property Tax Return AI Fill", name, force=1)
|
||||
frappe.db.commit()
|
||||
return all(results)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# AUTOFILL TESTS
|
||||
# ============================================================================
|
||||
#
|
||||
# `_compute_autofill` is pure: it takes plain dicts for Assets and depreciation
|
||||
# schedules and returns the per-row totals. We exercise it with hand-crafted
|
||||
# scenarios that cover each column (501–505), each routing (taxable vs exempt),
|
||||
# and the partial-year (507 ayla) flag. No DB writes — fast and deterministic.
|
||||
#
|
||||
# A lightweight end-to-end check then drives the whitelisted entry point with
|
||||
# whatever Assets the live site happens to have, just verifying the wrapper
|
||||
# wires through without crashing. Asset DB fixtures are too heavy for a smoke
|
||||
# test (Item / Cost Center / Asset Category prerequisites), so the unit-style
|
||||
# scenarios above are where the correctness coverage lives.
|
||||
|
||||
# Article codes used in scenarios — picked from ELAVE_1_ROWS to guarantee the
|
||||
# expected row label resolution exists regardless of label punctuation drift.
|
||||
_E1_INDEX = _build_e1_code_index()
|
||||
_E1_LABEL_BY_CODE = _E1_INDEX # alias for readability in tests
|
||||
_TYPE_1_1 = "Çoxmərtəbəli (çoxmənzilli) yaşayış binalar"
|
||||
|
||||
|
||||
def _asset(name, **fields):
|
||||
base = {"name": name, "status": "Submitted"}
|
||||
base.update(fields)
|
||||
return base
|
||||
|
||||
|
||||
def _schedule(asset_name, *entries):
|
||||
"""entries: iterable of (yyyy-mm-dd, accumulated_depreciation_amount)."""
|
||||
return {
|
||||
asset_name: [
|
||||
{"asset": asset_name, "schedule_date": d,
|
||||
"depreciation_amount": 0, "accumulated_depreciation_amount": acc}
|
||||
for d, acc in entries
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def _run_compute(assets, depr=None, *, year=2025, il_erzinde=False, est=None,
|
||||
article_to_code=None):
|
||||
return _compute_autofill(
|
||||
assets=assets,
|
||||
depr_by_asset=depr or {},
|
||||
year=year,
|
||||
il_erzinde=il_erzinde,
|
||||
establishment_date=est,
|
||||
article_to_code=article_to_code or {},
|
||||
e1_code_to_label=_E1_INDEX,
|
||||
)
|
||||
|
||||
|
||||
def run_autofill_empty():
|
||||
print("\n=== TEST E1: empty Asset list → empty payload ===")
|
||||
out = _run_compute([])
|
||||
results = [
|
||||
_check("elave_1 empty", 0, len(out["elave_1"]), tol=0),
|
||||
_check("elave_2 empty", 0, len(out["elave_2"]), tol=0),
|
||||
_check("ayla = 0", 0, out["ayla"], tol=0),
|
||||
_check("no unmatched", 0, len(out["unmatched"]), tol=0),
|
||||
]
|
||||
return all(results)
|
||||
|
||||
|
||||
def run_autofill_taxable_full_year():
|
||||
"""Asset bought 2023, depreciating 1000/year, owned at both year boundaries
|
||||
of 2025 → 501.1 = 8000 (after 2 years depr), 504.1 = 7000 (after 3)."""
|
||||
print("\n=== TEST E2: taxable asset spanning year boundary → 501.1 + 504.1 ===")
|
||||
assets = [_asset("A1",
|
||||
purchase_date="2023-01-01",
|
||||
purchase_amount=10000,
|
||||
taxable_asset_type=_TYPE_1_1)]
|
||||
depr = _schedule("A1",
|
||||
("2023-12-31", 1000),
|
||||
("2024-12-31", 2000),
|
||||
("2025-12-31", 3000))
|
||||
out = _run_compute(assets, depr)
|
||||
row = out["elave_2"].get(ELAVE_2_CHILD_LABELS[0], {})
|
||||
results = [
|
||||
_check("501.1 = 10000 − 2000", 8000, row.get("501.1", 0)),
|
||||
_check("504.1 = 10000 − 3000", 7000, row.get("504.1", 0)),
|
||||
_check("502.1 = 0 (not disposed)", 0, row.get("502.1", 0)),
|
||||
_check("503.1 = 0 (not bought in year)", 0, row.get("503.1", 0)),
|
||||
_check("505.1 = 0 (no insurance)", 0, row.get("505.1", 0)),
|
||||
]
|
||||
return all(results)
|
||||
|
||||
|
||||
def run_autofill_bought_in_year():
|
||||
"""Asset bought 2025-04-15, no depreciation by EOY → 503.1 = 504.1 = cost."""
|
||||
print("\n=== TEST E3: asset acquired during year → 503.1 + 504.1 ===")
|
||||
assets = [_asset("A2",
|
||||
purchase_date="2025-04-15",
|
||||
purchase_amount=5000,
|
||||
taxable_asset_type=_TYPE_1_1)]
|
||||
out = _run_compute(assets)
|
||||
row = out["elave_2"].get(ELAVE_2_CHILD_LABELS[0], {})
|
||||
return all([
|
||||
_check("501.1 = 0 (not owned at start)", 0, row.get("501.1", 0)),
|
||||
_check("503.1 = 5000 (acquisition residual)", 5000, row.get("503.1", 0)),
|
||||
_check("504.1 = 5000 (end-of-year residual)", 5000, row.get("504.1", 0)),
|
||||
])
|
||||
|
||||
|
||||
def run_autofill_disposed_in_year():
|
||||
"""Asset bought before year, disposed 2025-07-10 → 501.1 + 502.1, not 504.1."""
|
||||
print("\n=== TEST E4: asset disposed during year → 501.1 + 502.1, not 504.1 ===")
|
||||
assets = [_asset("A3",
|
||||
purchase_date="2024-01-01",
|
||||
disposal_date="2025-07-10",
|
||||
purchase_amount=8000,
|
||||
taxable_asset_type=_TYPE_1_1)]
|
||||
depr = _schedule("A3",
|
||||
("2024-12-31", 1000),
|
||||
("2025-07-10", 1500))
|
||||
out = _run_compute(assets, depr)
|
||||
row = out["elave_2"].get(ELAVE_2_CHILD_LABELS[0], {})
|
||||
return all([
|
||||
_check("501.1 = 7000 (start residual)", 7000, row.get("501.1", 0)),
|
||||
_check("502.1 = 6500 (disposal residual)", 6500, row.get("502.1", 0)),
|
||||
_check("504.1 = 0 (no longer owned)", 0, row.get("504.1", 0)),
|
||||
])
|
||||
|
||||
|
||||
def run_autofill_excess_insured():
|
||||
"""Insured > book value at year-end → 505.1 = spread."""
|
||||
print("\n=== TEST E5: insured value above book value → 505.1 spread ===")
|
||||
assets = [_asset("A4",
|
||||
purchase_date="2024-01-01",
|
||||
purchase_amount=10000,
|
||||
insured_value="15000", # legacy varchar storage
|
||||
taxable_asset_type=_TYPE_1_1)]
|
||||
out = _run_compute(assets)
|
||||
row = out["elave_2"].get(ELAVE_2_CHILD_LABELS[0], {})
|
||||
return all([
|
||||
_check("504.1 = 10000", 10000, row.get("504.1", 0)),
|
||||
_check("505.1 = 15000 − 10000", 5000, row.get("505.1", 0)),
|
||||
])
|
||||
|
||||
|
||||
def run_autofill_tax_exempt_routing():
|
||||
"""Asset with tax_exempt_tax_article → routed to elave_1, not elave_2."""
|
||||
print("\n=== TEST E6: tax-exempt asset → elave_1 (199.4.1) ===")
|
||||
code = "199.4.1"
|
||||
target_label = _E1_LABEL_BY_CODE.get(code)
|
||||
if not target_label:
|
||||
print(f" ✗ test setup: no elave_1 row for code {code}")
|
||||
return False
|
||||
assets = [_asset("A5",
|
||||
purchase_date="2024-01-01",
|
||||
purchase_amount=20000,
|
||||
tax_exempt_tax_article="Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən")]
|
||||
article_to_code = {"Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən": code}
|
||||
out = _run_compute(assets, article_to_code=article_to_code)
|
||||
row = out["elave_1"].get(target_label, {})
|
||||
return all([
|
||||
_check("routed to elave_1 (501.2)", 20000, row.get("501.2", 0)),
|
||||
_check("routed to elave_1 (504.2)", 20000, row.get("504.2", 0)),
|
||||
_check("elave_2 stays empty", 0, len(out["elave_2"]), tol=0),
|
||||
])
|
||||
|
||||
|
||||
def run_autofill_aggregation():
|
||||
"""Multiple assets in the same category → sums together; parent _5001 is
|
||||
NOT pre-summed here (recalc does that downstream)."""
|
||||
print("\n=== TEST E7: two 1.5 Mənzillər assets → sums in elave_2 ===")
|
||||
assets = [
|
||||
_asset("A6", purchase_date="2024-01-01", purchase_amount=100,
|
||||
taxable_asset_type="Mənzillər"),
|
||||
_asset("A7", purchase_date="2024-01-01", purchase_amount=300,
|
||||
taxable_asset_type="Mənzillər"),
|
||||
]
|
||||
out = _run_compute(assets)
|
||||
row = out["elave_2"].get(ELAVE_2_CHILD_LABELS[4], {}) # 1.5 Mənzillər
|
||||
return all([
|
||||
_check("501.1 sum", 400, row.get("501.1", 0)),
|
||||
_check("504.1 sum", 400, row.get("504.1", 0)),
|
||||
])
|
||||
|
||||
|
||||
def run_autofill_ayla():
|
||||
"""Partial-year + Company born mid-year → 507 ayla computed."""
|
||||
print("\n=== TEST E8: il_erzinde + establishment in year → ayla ===")
|
||||
out_in = _run_compute([], year=2025, il_erzinde=True,
|
||||
est=date(2025, 7, 1))
|
||||
out_off = _run_compute([], year=2025, il_erzinde=False,
|
||||
est=date(2025, 7, 1))
|
||||
out_other_year = _run_compute([], year=2025, il_erzinde=True,
|
||||
est=date(2020, 7, 1))
|
||||
return all([
|
||||
_check("ayla = 12 − 7 + 1 = 6", 6, out_in["ayla"], tol=0),
|
||||
_check("ayla = 0 when full-year mode", 0, out_off["ayla"], tol=0),
|
||||
_check("ayla = 0 when est outside year", 0, out_other_year["ayla"], tol=0),
|
||||
])
|
||||
|
||||
|
||||
def run_autofill_unmatched():
|
||||
"""Unknown taxable_asset_type goes to `unmatched`, not aggregated."""
|
||||
print("\n=== TEST E9: unmatched type → unmatched list, no totals ===")
|
||||
assets = [_asset("A8", purchase_date="2024-01-01", purchase_amount=999,
|
||||
taxable_asset_type="Nonsense category")]
|
||||
out = _run_compute(assets)
|
||||
return all([
|
||||
_check("no elave_2 rows", 0, len(out["elave_2"]), tol=0),
|
||||
_check("unmatched recorded", 1, len(out["unmatched"]), tol=0),
|
||||
])
|
||||
|
||||
|
||||
def run_autofill_endtoend_doc():
|
||||
"""Drive the whitelisted entry point against a freshly-seeded doc.
|
||||
|
||||
We can't assume the live DB has Assets — but the wrapper must wire through
|
||||
cleanly: resolve company, query DB, compute, return a dict with the right
|
||||
shape. If real Assets are present they'll appear in the payload too.
|
||||
"""
|
||||
print("\n=== TEST E10: end-to-end autofill_from_accounting on a new doc ===")
|
||||
if not frappe.get_all("Company", limit=1):
|
||||
print(" ℹ skipping — no Company exists in this site")
|
||||
return True
|
||||
doc_payload = {"il": 2025, "il_erzinde": 0, "tam_il": 1}
|
||||
import json as _json
|
||||
result = autofill_from_accounting(doc=_json.dumps(doc_payload))
|
||||
results = [
|
||||
_check("payload has elave_1 key", True, "elave_1" in result, tol=0),
|
||||
_check("payload has elave_2 key", True, "elave_2" in result, tol=0),
|
||||
_check("payload has ayla key", True, "ayla" in result, tol=0),
|
||||
_check("payload has company key", True, "company" in result, tol=0),
|
||||
]
|
||||
return all(results)
|
||||
|
||||
|
||||
def run_autofill_article_code_regex():
|
||||
"""Sanity-check the regex used to bridge Tax Article names ↔ elave_1 rows."""
|
||||
print("\n=== TEST E11: article-code extraction regex ===")
|
||||
cases = [
|
||||
("Vergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən", "199.4.1"),
|
||||
("Vergi Məcəlləsinin 199.4.1-1-ci madacd̄əsinə əsasən", "199.4.1-1"),
|
||||
("Vergi Məcəlləsinin 227.1-ci maddəsinə əsasən:", "227.1"),
|
||||
("Qanunla təsdiq olunmuş hasilatın pay bölgüsü …", None),
|
||||
("", None),
|
||||
]
|
||||
results = []
|
||||
for label, expected in cases:
|
||||
actual = _article_code(label)
|
||||
ok = actual == expected
|
||||
icon = "✓" if ok else "✗"
|
||||
print(f" {icon} {label[:40]!r} → expected={expected!r} actual={actual!r}")
|
||||
results.append(ok)
|
||||
return all(results)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def smoke_run():
|
||||
a = run_seeding()
|
||||
b = run_full_year()
|
||||
c = run_partial_year()
|
||||
d = run_elave_1_totals()
|
||||
e = run_persistence()
|
||||
f1 = run_autofill_empty()
|
||||
f2 = run_autofill_taxable_full_year()
|
||||
f3 = run_autofill_bought_in_year()
|
||||
f4 = run_autofill_disposed_in_year()
|
||||
f5 = run_autofill_excess_insured()
|
||||
f6 = run_autofill_tax_exempt_routing()
|
||||
f7 = run_autofill_aggregation()
|
||||
f8 = run_autofill_ayla()
|
||||
f9 = run_autofill_unmatched()
|
||||
f10 = run_autofill_endtoend_doc()
|
||||
f11 = run_autofill_article_code_regex()
|
||||
fills = all([f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11])
|
||||
print(f"\n=== Summary: seeding={a} full={b} partial={c} elave_1={d} persist={e} ===")
|
||||
print(f"=== Autofill: empty={f1} fullyear={f2} bought={f3} disposed={f4} "
|
||||
f"insured={f5} exempt={f6} agg={f7} ayla={f8} unmatched={f9} "
|
||||
f"endtoend={f10} regex={f11} ===")
|
||||
print("ALL OK" if (a and b and c and d and e and fills) else "FAILED")
|
||||
return {
|
||||
"seeding": a, "full_year": b, "partial_year": c,
|
||||
"elave_1": d, "persistence": e,
|
||||
"autofill_empty": f1, "autofill_fullyear": f2,
|
||||
"autofill_bought": f3, "autofill_disposed": f4,
|
||||
"autofill_insured": f5, "autofill_exempt": f6,
|
||||
"autofill_aggregation": f7, "autofill_ayla": f8,
|
||||
"autofill_unmatched": f9, "autofill_endtoend": f10,
|
||||
"autofill_regex": f11,
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 15:04:47.107017",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"azadolmasəbəbi",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012",
|
||||
"ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri",
|
||||
"ilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "azadolmasəbəbi",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Azadolma səbəbi",
|
||||
"length": 500,
|
||||
"options": "Vergi Məcəlləsinin 199.1-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.4.1-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.4.2-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.4.3-cü maddəsinə əsasən:\nVergi Məcəlləsinin 199.4.4-cü maddəsinə əsasən:\nVergi Məcəlləsinin 199.4.1-1-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.5-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.7-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.8-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.9-cu maddəsinə əsasən:\nVergi Məcəlləsinin 199.11-cu maddəsinə əsasən:\nVergi Məcəlləsinin 199.14-cu maddəsinə əsasən:\nVergi Məcəlləsinin 199.15-cu maddəsinə əsasən:\nVergi Məcəlləsinin 199.16-cı maddəsinə əsasən:\nVergi Məcəlləsinin 199.17-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.18-ci maddəsinə əsasən:\nVergi Məcəlləsinin 199.19-cu maddəsinə əsasən:\nVergi Məcəlləsinin 227.1-ci maddəsinə əsasən:\nQanunla 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ə:\nXüsusi iqtisadi zonalar haqqında qanunlarda nəzərdə tutulan əmlaklar üzrə:\nAzərbaycan Respublikasının tərəfdar çıxdığı beynəlxalq müqavilələr üzrə:\nVergidən azad olunan əmlakın CƏMİ dəyəri:",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "hesabatiliərzindəəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Yeni yaradıldığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olma tarixinə əsas vəsaitlərin qalıq dəyəri (manatla) (503.2)\t",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Vergi ödəyicisinin ləğv olunma və ya uçotdan çıxarılma tarixinə əsas vəsaitlərin qalıq dəyəri (manatla) (502.2)",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Vergi ödəyicisinin ilin sonuna əsas vəsaitlərinin qalıq dəyəri (manatla) (504.2)"
|
||||
},
|
||||
{
|
||||
"fieldname": "artıqqiymətəəsasvəsaitlərindəyəri",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Artıq qiymətə sığortalanan əsas vəsaitlərin dəyəri (manatla) (505.2)"
|
||||
},
|
||||
{
|
||||
"fieldname": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyərimanatla5012",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Hesabat ilinin əvvəlinə əsas vəsaitlərin qalıq dəyəri (manatla) (501.2)"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-05 14:25:59.341379",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Fill Elave 1",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIFillElave1(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 16:36:30.780093",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
"hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"hesabatilierzindeuçotaalınan",
|
||||
"hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"artıqqiymətəəsasvəsaitlərindəyəri505"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "ləğvolunmatarixinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Vergi ödəyicisinin ləğv olunma və ya uçotdan çıxarılma tarixinə əsas vəsaitlərin qalıq dəyəri (manatla) (502.1)",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
"fieldname": "vergiyəcəlbolunanəmlaklarınkateqoriyası",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Vergiyə cəlb olunan əmlakların kateqoriyası",
|
||||
"options": "1. Binalar, tikililər və qurğular\n1.1 Çoxmərtəbəli (çoxmənzilli) yaşayış binaları\n1.2 Qeyri-yaşayış binaları (sahələri)\n1.3 Əmlak kompleksi kimi müəssisələr\n1.4 Qurğular\n1.5 Mənzillər\n1.6 Fərdi yaşayış və bağ evləri\n2 Maşınlar və avadanlıqlar\n3 Yüksək texnologiyalar məhsulu olan hesablama texnikası\n4 Nəqliyyat vasitələri\n5 Digər əsas vəsaitlər\nVergiyə cəlb olunan əmlakın CƏMİ dəyəri:",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "hesabatilininəvvəlinəəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Hesabat ilinin əvvəlinə əsas vəsaitlərin qalıq dəyəri (manatla) (501.1)",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
"fieldname": "hesabatilierzindeuçotaalınan",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Hesabat ili ərzində yarandığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olma tarixinə əsas vəsaitlərin qalıq dəyəri (manatla) (503.1)",
|
||||
"precision": "2",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "hesabatilinsonunaəsasvəsaitlərinqalıqdəyəri",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Hesabat ilinin sonuna əsas vəsaitlərinin qalıq dəyəri (manatla) (504.1)",
|
||||
"precision": "2"
|
||||
},
|
||||
{
|
||||
"fieldname": "artıqqiymətəəsasvəsaitlərindəyəri505",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Artıq qiymətə sığotalanan əsas vəsaitlərin dəyəri (manatla) (505.1)",
|
||||
"precision": "2"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-08 18:48:57.656460",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Fill Elave 2",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIFillElave2(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 14:28:57.994528",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "501. Əmlakın ümumi qalıq dəyəri\n501.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri\n501.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri\n501.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 19:47:26.620548",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Fill Vergi hesablamasi 1ci cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIFillVergihesablamasi1cicedvel(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-08-04 12:02:18.835114",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "503. Əmlakın ümumi dəyəri\n503.1 Vergiyə cəlb olunan əmlakın dəyəri\n503.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın dəyəri\n503.2 Vergidən azad olunan əmlakın dəyəri",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 19:38:26.382915",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Fill Vergi hesablamasi 1ci cedvel 1",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIFillVergihesablamasi1cicedvel1(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 14:36:20.467665",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "504. Əmlakın ümumi qalıq dəyəri\n504.1 Vergiyə cəlb olunan əmlakın qalıq dəyəri\n504.1.1 Qeyri-kommersiya fəaliyyətində istifadə olunan əmlakın qalıq dəyəri\n504.2 Vergidən azad olunan əmlakın qalıq dəyəri",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 12:00:07.915973",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Fill Vergi hesablamasi 2ci cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIFillVergihesablamasi2cicedvel(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 14:35:10.099838",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "505 Artıq qiymətə sığortalanan əmlakın dəyəri\n505.1 Vergiyə cəlb olunan artıq qiymətə sığortalanan əmlakın dəyəri\n505.2 Vergidən azad olunan artıq qiymətə sığortalanan əmlakın dəyəri",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 12:00:44.910640",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Fill Vergi hesablamasi 3cu cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIFillVergihesablamasi3cucedvel(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 14:36:07.154586",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"faizlə"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "506 Verginin dərəcəsi",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "faizlə",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Faizlə %",
|
||||
"precision": "2",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-29 18:09:10.263087",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Fill Vergi hesablamasi 4cu cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIFillVergihesablamasi4cucedvel(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-08-04 19:40:22.377464",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"ayla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "507 Müəssisə yarandığı aydan sonra ilin sonuna qədər olan ayların sayı",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ayla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Ayla"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 19:41:48.738524",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Fill Vergi hesablamasi 4cu cedvel 1",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIFillVergihesablamasi4cucedvel1(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-01-13 15:00:15.158446",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "509.1 Vergi ödəyicisi il ərzində tam fəaliyyət göstərdiyi halda:\n509.2 Hesabat ili ərzində yaradıldığı (uçota alındığı) və ya əmlak vergisi ödəyicisi olduğu halda:\n510 Vergi məcəlləsinin 199.21-ci maddəsinə əsasən azaldılmalı əmlak vergisinin məbləği"
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla",
|
||||
"precision": "2"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-07 14:25:22.271312",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Fill Vergi hesablamasi 5ci cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIFillVergihesablamasi5cicedvel(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-08-04 14:47:33.216880",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"göstəricilər",
|
||||
"manatla"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "göstəricilər",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Göstəricilər",
|
||||
"options": "511.1 Müəssisə il ərzində tam fəaliyyət göstərdiyi halda\n511.2 Müəssisə il ərzində yarandığı halda",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "manatla",
|
||||
"fieldtype": "Float",
|
||||
"precision": "2",
|
||||
"in_list_view": 1,
|
||||
"label": "Manatla",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 21:25:48.999866",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Taxes Az",
|
||||
"name": "Property Tax Return AI Fill Vergi hesablamasi 6ci cedvel",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PropertyTaxReturnAIFillVergihesablamasi6cicedvel(Document):
|
||||
pass
|
||||
Loading…
Reference in New Issue