# Pretporez accrual istraga + računovodstveni verdikt (MC #106536, 2026-08-02)

# MC #106536 — Istraga: ulazni PDV (pretporez) filtriran po internom statusu, ne po statutarnom kriteriju

**Status:** READ-ONLY ISTRAGA — kod NIJE mijenjan. Čeka verdikt bilko-racunovodstvo-hr / bilko-porez-fiskalizacija-hr prije bilo kakvog fixa.
**Repo:** `~/business/ALAI-Holding-AS/products/Bilko`
**Referentni commit:** `azdo/main` @ `e81db44a` (2026-08-02, fetched live — vidi napomenu o staleness ispod)
**Povezano:** #106361 (izlazna strana, DONE, ista klasa greške obrnutog predznaka), #106377 (dashboard OVERDUE)

---

## 0. Napomena o staleness lokalne kopije

Lokalni checkout `~/business/.../Bilko` (radni direktorij bez worktree-a) je na `HEAD=68578f35`, koji **NIJE ancestor** commita `9bc962ab` citiranog u MC opisu (`git merge-base --is-ancestor 9bc962ab HEAD` → NO). Lokalni `main` je zaostao/divergirao PRIJE nego je #106361 fix (NON_ISSUED_INVOICE_STATUSES) mergean — u lokalnom fajlu izlazna strana i dalje koristi stari allow-list `inList(SENT, VIEWED, PAID)` bez OVERDUE.

Zato je cijela ova istraga rađena protiv **`git show e81db44a:...`** (trenutni vrh `azdo/main`, uspješno fetch-ovan), ne protiv radnog stabla. `e81db44a` je iza `9bc962ab` (2 commita dalje) i sadrži #106361 fix. Sve linije citirane ispod odnose se na taj sadržaj. Ako se dispatch fixa radi u novom worktree-u, treba granati sa `azdo/main`, ne sa stale lokalnim `main`.

---

## 1. Tačan filter — ULAZNA strana (pretporez / input VAT)

`apps/api/src/main/kotlin/no/alai/bilko/services/ReportService.kt`, `getVATReport()`, `e81db44a` linije **524-551** (potvrđuje MC opis tačno):

```kotlin
// ── Input VAT (pretporez) — čl. 125.i st. 4 ZPDV ─────────────────────
// INVOICE_BASED: deduct when expense is recorded (expenseDate in period); status APPROVED or PAID.
// CASH_BASED: right to deduct arises only on PAYMENT of supplier invoice (čl. 125.i st. 4 ZPDV).
val expenses: List<ResultRow> = when (vatMethod) {
    VatAccountingMethod.CASH_BASED -> {
        Expenses.selectAll().where {
            (Expenses.organizationId eq UUID.fromString(organizationId)) and
                (Expenses.status eq ExpenseStatus.PAID) and
                (Expenses.paidAt.isNotNull()) and
                (Expenses.paidAt greaterEq paidAtFrom) and
                (Expenses.paidAt less paidAtTo)
        }.orderBy(Expenses.paidAt).toList()
    }
    else -> {
        // INVOICE_BASED (i VAT_EXEMPT/PAUSALNI — prazna lista invoices iznad, ali konzistentno):
        Expenses.selectAll().where {
            (Expenses.organizationId eq UUID.fromString(organizationId)) and
                (Expenses.expenseDate greaterEq fromDate) and
                (Expenses.expenseDate lessEq toDate) and
                (Expenses.status inList listOf(ExpenseStatus.APPROVED, ExpenseStatus.PAID))
        }.orderBy(Expenses.expenseDate).toList()
    }
}
```

`ExpenseStatus` enum (`models/ExpenseStatus.kt`): `PENDING, APPROVED, PAID, REJECTED`.

**INVOICE_BASED grana isključuje PENDING.** Perioda filter je ispravan (expenseDate u periodu — tačka oporezivanja na accrual osnovi), ali je nadograđen INTERNIM workflow statusom koji nema tekstualni oslonac u ZPDV-u.

---

## 2. Da li kod razlikuje accrual vs cash-basis? DA — i CASH_BASED grana je statutarno u redu

`CASH_BASED` grana (linije 530-537): `status eq PAID AND paidAt in period` — direktno implementira čl. 125.i st. 4 ZPDV (pretporez nastaje na dan plaćanja dobavljaču). **Ovo NE dirati** (task eksplicitno traži da CASH_BASED grana ostane netaknuta).

`VatAccountingMethod.INVOICE_BASED` je **statutarni default za sve HR organizacije** (`models/VatAccountingMethod.kt` komentar: "statutory default for all orgs"). Dakle bug u sekciji 1 pogađa VEĆINU/SVE organizacije koje nisu posebno prijavile obračun po naplaćenim naknadama (čl. 125.i-k ZPDV, prag ≤2M EUR prometa).

---

## 3. Usporedba s IZLAZNOM stranom (#106361 fix) — ista klasa greške, simetrično neriješena

Izlazna strana je **istim ovim commitom** (`e81db44a`, naslijeđeno iz #106361 fixa) prešla s allow-lista na statutarnu isključivost:

`ReportService.kt` linija 54 (companion object):
```kotlin
/**
 * Expressed as an EXCLUSION rather than an allow-list on purpose: the previous
 * allow-list `(SENT, VIEWED, PAID)` was written before OVERDUE existed as a
 * status that a cron SETS ON ALREADY-ISSUED invoices, so the day OVERDUE was
 * added, issued invoices began dropping out of the PDV return silently. ...
 */
internal val NON_ISSUED_INVOICE_STATUSES = listOf(InvoiceStatus.DRAFT, InvoiceStatus.CANCELLED)
```
Korišteno na liniji ~414 (`getVATReport` output grana): `(Invoices.status notInList NON_ISSUED_INVOICE_STATUSES)`.

Logika: jedini statusi koji legitimno znače "račun NIJE izdan" su DRAFT (nije poslan) i CANCELLED (storniran). Svaki drugi status (uključujući budući, još nepostojeći "collections marker" status) NE smije tiho ispasti iz PDV obračuna.

Analogni statutarni ekvivalent na ulaznoj strani bi bio: jedini status koji legitimno znači "trošak/račun se NE priznaje kao odbitna stavka" je **REJECTED** (interno odbijen — nije prihvaćen kao poslovni trošak). `PENDING` nema analognu statutarnu težinu — to je isključivo interni odobravajući workflow (menadžer/računovođa još nije kliknuo "Approve"), ne izjava o (ne)postojanju ili valjanosti dobavljačevog računa.

**Ovo NIJE riješeno na ulaznoj strani** — kod još uvijek koristi allow-list `inList(APPROVED, PAID)`, identičan uzorak kakav je izlazna strana imala PRIJE #106361 fixa.

---

## 4. Dodatni nalaz izvan izvornog scope-a: ISTA greška u KPR-u (Knjiga primljenih računa)

`getKprReport()`, `e81db44a` linije ~1403-1408 — mandatorni registar po Pravilniku o PDV-u (NN 79/13 et seq.):

```kotlin
// All incoming invoices in period — APPROVED or PAID (i.e. received and processed)
val expenseRows = Expenses.selectAll().where {
    (Expenses.organizationId eq orgUuid) and
        (Expenses.expenseDate greaterEq fromDate) and
        (Expenses.expenseDate lessEq toDate) and
        (Expenses.status inList listOf(ExpenseStatus.APPROVED, ExpenseStatus.PAID))
}.orderBy(Expenses.expenseDate, SortOrder.ASC).toList()
```

Komentar "APPROVED or PAID (i.e. received and processed)" eksplicitno miješa "primljen račun" (statutarni pojam iz naziva registra — Knjiga PRIMLJENIH računa) s "interno odobren" (workflow pojam). Isti bug, ista posljedica: PENDING trošak s valjanim ulaznim računom nedostaje iz obaveznog KPR registra za period u kojem je primljen.

`getKirReport()` (izlazna strana, analogni registar) koristi `NON_ISSUED_INVOICE_STATUSES` — konzistentno s fiksiranom PDV logikom. KPR nije dobio isti tretman.

Nije provjeravano dalje od KPR/PDV obračuna (npr. dashboard `getExpensesForPeriod`/`getPendingExpensesForPeriod` koriste isti status-split, ali to su P&L/KPI prikazi, ne PDV-obavezujući izračun — izvan strogog statutarnog scope-a ovog MC-a, spominjem radi potpunosti, ne kao dio fix-a).

---

## 5. Otvoreno pitanje koje NEMA odgovor u kodu: da li PENDING trošak uopće IMA valjan račun?

Ovo je bitna nijansa koju izlazna strana nema (jer je invoice=dokument koji Bilko SAM izdaje, pa DRAFT jasno znači "ne postoji izvan sistema još"). Na ulaznoj strani, `createExpense()` (`ExpenseService.kt` linije ~199-320):

- `expenseDate`, `amount`, `taxAmount`, `vendorId`, `category` su traženi/opcioni parametri pri kreiranju.
- **`receiptUrl` NIJE dio `createExpense()` payload-a uopće** — postavlja se isključivo naknadnim, odvojenim pozivom (`uploadReceipt`, linije ~653+).
- DB kolona: `receiptUrl = varchar("receipt_url", 500).nullable()` (`models/Tables.kt` linija 471) — **nullable, ničim ne enforced ni za PENDING ni za APPROVED ni za PAID.**

Zaključak: trenutni status (`PENDING` vs `APPROVED` vs `PAID`) **ne korelira pouzdano** s time da li postoji priložen dobavljačev račun. To znači:
(a) Trenutni filter (APPROVED/PAID) ne garantira ni za UKLJUČENE troškove da postoji priložen račun — potencijalno šira rupa od ovog tiketa, ali van njegovog scope-a.
(b) Prosto dodavanje PENDING u allow-listu (mirror #106361 pristupa: `status notEq REJECTED`) ne rješava (a) — rješava samo simetriju s izlaznom stranom po pitanju INTERNOG odobrenja kao (ne)legitimnog gate-a.

Ovo pitanje (da li "posjedovanje urednog računa" treba biti novi, eksplicitan statutarni uvjet vezan za `receiptUrl IS NOT NULL`, umjesto ili uz status) **mora riješiti računovodstvena persona** — nije nešto što read-only kod-istraga može zaključiti sama.

---

## 6. Scenario s konkretnim brojevima (INVOICE_BASED, HR, 25% stopa)

Org "Konoba Vlado d.o.o.", HR PDV obveznik, `vatAccountingMethod = INVOICE_BASED` (statutarni default).

1. **15.07.2026** — dobavljač "Nabava-Opskrba d.o.o." izdaje račun za uredski materijal: osnovica 1.000 EUR + 25% PDV = 250 EUR (ukupno 1.250 EUR). Fizički/PDF račun postoji i uručen je kupcu istog dana.
2. **16.07.2026** — knjigovođa unosi trošak u Bilko: `expenseDate=2026-07-15`, `amount=1250`, `taxAmount=250`, prilaže PDF (receiptUrl postavljen). Status pri kreiranju automatski = `PENDING` (`ExpenseService.kt` linija ~311, `it[Expenses.status] = ExpenseStatus.PENDING`).
3. Interni odobravatelj (vlasnik/menadžer) je na godišnjem odmoru; klikne "Approve" tek **05.08.2026** → status prelazi u `APPROVED`.
4. **01.08.2026** — knjigovođa generira PDV obračun (`getVATReport`) za period 01.07-31.07.2026, radi PDV-S prijave s rokom 20.08.2026. U tom trenutku trošak je i dalje `PENDING`.
   - Filter na liniji 545-552 ga isključuje: `status inList (APPROVED, PAID)` → PENDING pada van.
   - **Rezultat: pretporez za srpanj prijavljen kao 0 EUR za ovaj račun, iako je dobavljačeva obaveza nastala i PDV obveznik posjeduje uredan račun s datumom u tom periodu.**
5. Ako u julskom periodu nema drugih pretporeznih stavki, obveznik plaća PDV obavezu **250 EUR VIŠE nego što zakonski duguje** za srpanj (obrnuti predznak od #106361 — tamo je izlazni PDV bio potcijenjen/obveza podcijenjena; ovdje je pretporez potcijenjen pa je NETO obaveza precijenjena — organizacija plaća previše, šteta za korisnika/klijenta, ne za državu).
6. **05.08.2026** — trošak postaje APPROVED. Ako se PDV izvještaj za SRPANJ ponovo generira nakon tog datuma (npr. za internu rekonsolidaciju), trošak će se sada pojaviti u srpanjskom periodu (jer filter koristi `expenseDate`, ne datum promjene statusa) — **ALI ako je PDV-S prijava za srpanj već predana 01-20.08 na temelju prvog (nižeg) izračuna, razlika od 250 EUR nikad ne uđe u prijavu bez ručne korekcije (ispravak PDV prijave)**. Sustav ne upozorava korisnika da je ranije predana prijava sada zastarjela/netočna — nema traga/audit signala.

**Varijanta koja pokazuje nekonzistentnost bez čak i pitanja o roku prijave:** ako se PDV izvještaj za srpanj generira TEK 10.08.2026 (nakon što je odobreno 05.08.), trošak ULAZI u srpanjski izračun ispravno — potpuno isti trošak, isti `expenseDate`, ista statutarna činjenica, ali RAZLIČIT ishod izvještaja ovisno isključivo o TRENUTKU kad je netko kliknuo "Approve" u odnosu na trenutak kad je netko pokrenuo izvještaj. To je dokaz da filter nije statutarno determinističan — dva poziva istog reporta za isti period mogu vratiti različit pretporez ovisno o internom workflow-u koji nema veze s poreznim pravom.

---

## 7. Predloženi fix (BEZ implementacije — čeka gate)

Kandidat, po analogiji s #106361 (`NON_ISSUED_INVOICE_STATUSES`):

```kotlin
// Kandidat — NE implementirati bez verdikta bilko-racunovodstvo-hr:
internal val NON_DEDUCTIBLE_EXPENSE_STATUSES = listOf(ExpenseStatus.REJECTED)
// INVOICE_BASED grana:
(Expenses.status notInList NON_DEDUCTIBLE_EXPENSE_STATUSES)
```

Ovo bi uključilo PENDING (i APPROVED, PAID) po analognoj logici "svaki status osim eksplicitno-odbijenog nosi poreznu težinu", isključilo bi samo REJECTED.

**Otvoreno prije implementacije (obavezno pitati personu):**
1. Da li interni "PENDING" u Bilko workflowu SVAKI put implicira da postoji primljen/uredan dobavljačev račun s datumom = expenseDate — ili PENDING može postojati i za trošak koji je samo najavljen/očekivan bez stvarnog primljenog računa? (vidi §5 — `receiptUrl` nije obavezan ni za jedan status).
2. Ako odgovor na (1) je "ne uvijek" — treba li statutarni kriterij za pretporez biti `receiptUrl IS NOT NULL AND status != REJECTED`, umjesto samo status-baziran exclusion? To bi bio noviji, stroži uvjet nego što izlazna strana ima (jer izlazna strana nema ekvivalent "dokaz postojanja" polje — invoice JE dokument).
3. Treba li identičan fix primijeniti i na `getKprReport()` (§4) u istom PR-u, s obzirom da je to isti bug u istom statutarnom kontekstu (Pravilnik o PDV-u mandatorni registar), ili tretirati kao poseban tiket?
4. Tačan ZPDV/Pravilnik članak za OPĆE pravo na odbitak pretporeza na accrual (INVOICE_BASED) osnovi (analogno kako je čl. 125.i st. 4 citiran za CASH_BASED) — nisam mogao provjeriti primarni izvor uživo (nemam WebFetch u ovom sub-agentu); potrebna potvrda od bilko-porez-fiskalizacija-hr / bilko-racunovodstvo-hr prije bilo kakve statutarne tvrdnje u kodu-komentaru ili korisničkom UI-ju.
5. Retroaktivnost/korekcija: postoji li već mehanizam za ispravak već predane PDV-S prijave kad se status promijeni NAKON perioda podnošenja (§6, korak 6)? Ako ne, treba li ovaj fix uključivati i UI upozorenje ("ovaj trošak je odobren nakon što je PDV prijava za period X već generirana/predana")?

---

## 8. Sažetak za gate

| Pitanje iz MC #106536 | Nalaz |
|---|---|
| Tačan filter za ULAZNU stranu (fajl:linija) | `ReportService.kt:545-552` (INVOICE_BASED), `e81db44a` — `Expenses.status inList listOf(APPROVED, PAID)` |
| Razlikuje li kod accrual vs cash-basis | DA. CASH_BASED (`ReportService.kt:530-537`) je statutarno ispravan (čl. 125.i st.4) i ne treba dirati. |
| INVOICE_BASED grana je statutarno ispravna? | NE — koristi interni workflow status (APPROVED) kao gate umjesto statutarnog kriterija (posjedovanje računa + nastanak obaveze dobavljača). Ista klasa greške kao #106361, obrnut predznak (organizacija PRECJENJUJE PDV obavezu). |
| Konkretan scenario s brojevima | §6 — 250 EUR pretporeza izgubljeno/odgođeno za srpanj zbog kašnjenja internog odobrenja, bez veze s postojanjem/valjanošću dobavljačevog računa. |
| Dodatni nalaz | Isti bug u `getKprReport()` (mandatorni KPR registar), §4. |
| Blokira li nešto direktan fix po #106361 predlošku? | DA — §5: `receiptUrl` nije statutarno-enforced ni za jedan status, pa čak ni "notInList REJECTED" garantirano ne znači "ima uredan račun". Persona mora presuditi je li to relevantno za OVAJ tiket ili poseban gap. |

**NIJE FIXANO. NIJE ZATVORENO. Čeka bilko-racunovodstvo-hr / bilko-porez-fiskalizacija-hr verdikt prije dispatcha na implementaciju.**

---

## Računovodstveni gate verdikt (bilko-racunovodstvo-hr, 2026-08-02)

**Metoda:** nezavisno potvrđeno protiv `git show e81db44a` (ReportService.kt INVOICE_BASED ~536-544 `status inList(APPROVED,PAID)`; CASH_BASED ~528-535 ispravno; KPR ~1403-1408 isti filter) i Tables.kt:471 (receiptUrl nullable, unenforced).

**(a) PENDING isključenje na INVOICE_BASED grani statutarno pogrešno? DA.** Pravo na odbitak pretporeza kod accrual obračuna nastaje s nastankom porezne obveze kod dobavljača (tax point = datum isporuke/računa), ne s internim odobrenjem (princip analogan EU VAT Dir. 2006/112/EZ čl. 167). Interni "Approve" klik nema poreznu težinu. CASH_BASED grana ispravna (čl. 125.i st.4 ZPDV — iznimka, ne pravilo). Scenario §6 (250 EUR izgubljen jer je odobravatelj na godišnjem) = statutarno nedeterminističan ishod, dokaz pogrešnog kriterija.

**(b) Ispravan kriterij? DA — dvije komponente:** (1) tax point u periodu (expenseDate — već implementirano), (2) posjedovanje urednog računa (formalni uvjet OSTVARIVANJA prava). Interni status nije proxy ni za jedno; jedino REJECTED ima statutarnu težinu (trošak nepriznat kao poslovni).

**(c) Fix redoslijed:** status-filter fix (notInList(REJECTED)) ODMAH — ne uvodi novu klasu rizika (receipt-rupa VEĆ postoji na APPROVED/PAID), a ispravlja precjenjivanje PDV obaveze klijenta. Receipt-enforcement = ZASEBAN ali P1 compliance task (warning/blocker na APPROVED/PAID bez receiptUrl + upozorenje pri PDV izvještaju), ne nice-to-have. Ne moraju isti PR.

**(d) KPR:** PENDING trošak s primljenim računom NE smije izostati iz KPR-a (Pravilnik o PDV-u NN 79/13 i izm. — kriterij "primljen", ne "odobren"). getKprReport() i getVATReport() INVOICE_BASED da dijele ISTU konstantu/predikat (analogno KIR ↔ #106361) — isti PR, da se spriječi buduća divergencija.

**Granica nadležnosti:** tačni brojevi članaka ZPDV-a [IZ ZNANJA] — prije unosa u kod-komentare/UI moraju proći bilko-porez-fiskalizacija-hr live web provjeru (protokol iz #105640). Presuda o suštini stoji (nije post-2025 izmjena).