Skip to main content

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):

// ── 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):

/**
 * 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.):

// 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):

// 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), e81db44aExpenses.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).