CC4 R7 ERP S2: DELETE invoice + /putni-nalozi alias + /placanja + /export/putni.xlsx

erp/ocr.py:
- DELETE /api/erp/invoices/{id} (samo pgz_admin) + cascade payment cleanup + audit
  (briše vezana payments, otkapča invoice_uploads.invoice_id NULL, audit log "delete")

erp/putni_nalozi.py:
- GET/POST /api/erp/putni-nalozi (alias plural od /putni-nalog) za CC1 brief kompatibilnost
- GET /api/erp/putni-nalozi/{id}
- PATCH /api/erp/putni-nalozi/{id} sa body.action: approve|reject|submit|pay (route kroz lifecycle)
- PATCH /api/erp/putni-nalog/{id} (singular alias)
- GET /api/erp/export/putni.xlsx — openpyxl 19 stupaca (klub, voditelj, ruta, datumi, km, dnevnice, ukupno, status...)
- GET /api/erp/placanja — lista neplaćenih računa + odobrenih putnih naloga (kandidati za isplatu)
- POST /api/erp/placanja {kind:invoice|putni_nalog, id, iban, model, opis, poziv_na_broj}
  → generira HUB-3 PDF + EPC QR (reuse crm.payments.build_hub3_pdf), pohranjuje u
  _data/uploads/placanja/{kind}_{id}_HUB3_*.pdf
- GET /api/erp/placanja/{kind}/{id}/pdf → streama zadnji generirani PDF, ili kreira on-demand
- Dodan from pathlib import Path (fix NameError)

Live tests:
- DELETE /invoices/4 → 200 (test invoice obrisan)
- GET /putni-nalozi → 200, /putni-nalozi/1 → 200
- GET /placanja → 200 lista; POST → ok pdf 11 KB; GET pdf → 200 application/pdf %PDF-
- /placanja invoice 1 (INA €63.15) i putni_nalog 2 (€133.08) PDF generirani
- /export/putni.xlsx → 200 application/vnd.openxmlformats... PK header valid
- OCR INA gorivo: vendor=INA, OIB=27759560625, brutto=€63.15, PDV=€12.63, cat=gorivo
- UI 3× "Ri.NET AI" / 0× "DeepSeek"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
CC4
2026-05-05 08:01:49 +02:00
parent c38f15a566
commit 8c97a5b778
2 changed files with 314 additions and 0 deletions
+20
View File
@@ -793,6 +793,26 @@ def invoices_update(invoice_id: int, body: dict = Body(...), authorization: Opti
return {"ok": True, "invoice": row}
@router.delete("/invoices/{invoice_id}")
def invoices_delete(invoice_id: int, authorization: Optional[str] = Header(None)):
"""Brisanje računa — samo pgz_admin."""
user = _resolve_user(authorization)
if user and not is_pgz_admin(user):
raise HTTPException(403, "Nemate ovlasti brisati račun")
with _db() as c:
cur = c.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute("SELECT * FROM pgz_sport.invoices WHERE id=%s", (invoice_id,))
inv = cur.fetchone()
if not inv:
raise HTTPException(404, "Račun ne postoji")
cur.execute("UPDATE pgz_sport.invoice_uploads SET invoice_id=NULL WHERE invoice_id=%s", (invoice_id,))
cur.execute("DELETE FROM pgz_sport.payments WHERE invoice_id=%s", (invoice_id,))
cur.execute("DELETE FROM pgz_sport.invoices WHERE id=%s", (invoice_id,))
audit_invoice(user, invoice_id, "delete", field="invoice_no",
old=inv.get("invoice_no"), new="(deleted)")
return {"ok": True, "deleted": invoice_id}
@router.post("/invoices/{invoice_id}/pay")
def invoices_pay(invoice_id: int, body: dict = Body(default={}),
authorization: Optional[str] = Header(None)):