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,categorysu traženi/opcioni parametri pri kreiranju.receiptUrlNIJE diocreateExpense()payload-a uopće — postavlja se isključivo naknadnim, odvojenim pozivom (uploadReceipt, linije ~653+).- DB kolona:
receiptUrl = varchar("receipt_url", 500).nullable()(models/Tables.ktlinija 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).
- 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.
- 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.ktlinija ~311,it[Expenses.status] = ExpenseStatus.PENDING). - Interni odobravatelj (vlasnik/menadžer) je na godišnjem odmoru; klikne "Approve" tek 05.08.2026 → status prelazi u
APPROVED. - 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 daljePENDING.- 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.
- Filter na liniji 545-552 ga isključuje:
- 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).
- 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):
- 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 —
receiptUrlnije obavezan ni za jedan status). - 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). - 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? - 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.
- 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).
No comments to display
No comments to display