feat(istruttoria): verifica riga-per-riga con dual declared/verified
Replica il workflow del foglio Excel originale (REMISSIONE_DEL_DEBITO_5888.xlsm).
Istruttore ora verifica ogni fattura, ogni dipendente ULA, ogni documento singolarmente
invece di accettare/respingere la pratica intera.
Modello dati - nuove colonne su 3 tabelle:
- remission_invoice: taxable_verified, vat_verified, total_verified,
verification_status (PENDING/AMMESSA/PARZIALE/RESPINTA), verification_notes,
date_checks (JSONB con invoice_in_period/payment_in_period), verified_by, verified_at
- remission_ula_employee: fte_pct_verified, verification_status, verification_notes,
verified_by, verified_at
- remission_document: verification_status (PENDING/VALIDO/NON_VALIDO/SCADUTO),
verification_notes, verified_by, verified_at
- remission_practice: instructor_final_notes, instructor_checklist (JSONB 3 gate SI/NO),
verbale_date
Nuovi endpoint:
- PUT /instructor/{id}/invoices/{inv_id}/verify (status + rettifica importi + note)
- PUT /instructor/{id}/ula-employees/{emp_id}/verify (rettifica FTE + note)
- PUT /instructor/{id}/documents/{doc_code}/verify (VALIDO/NON_VALIDO/SCADUTO + note)
- PUT /instructor/{id}/final-notes (note sintetiche + checklist)
Ricalcolo gate_check dual track:
- grand_total_declared: sempre (importo richiesto dal beneficiario)
- grand_total_verified: somma solo fatture AMMESSA/PARZIALE (se PARZIALE usa verified)
- remission_due: usa verified se any_verified=True, altrimenti declared (backward compat)
- residuo_da_restituire: amount_erogato - remission_due
- flag any_verified e all_verified per gating decisione finale
_auto_check_dates: fattura in periodo? pagamento in periodo?
Legge period_start e period_end da schema.gate_rules (superadmin editor).
Template: aggiunto period_start/period_end_date come campi 'editable_by superadmin'
nella sezione general static_fields.
Schema editor FE (BandoRendicontazioneSchemaEdit): aggiunto Calendar period_start
accanto a period_end in section gate rules. period_start_rule dropdown per logica
(erogato_date|fixed) resta; period_start data fissa usata dal check.
This commit is contained in:
@@ -67,6 +67,9 @@ class RemissionPractice(Base):
|
|||||||
reviewed_by = Column(Integer, nullable=True)
|
reviewed_by = Column(Integer, nullable=True)
|
||||||
rejection_reason = Column(Text, nullable=True)
|
rejection_reason = Column(Text, nullable=True)
|
||||||
approved_remission = Column(Numeric(14, 2), nullable=True)
|
approved_remission = Column(Numeric(14, 2), nullable=True)
|
||||||
|
instructor_final_notes = Column(Text, nullable=True)
|
||||||
|
instructor_checklist = Column(JSONB, nullable=True, default=dict)
|
||||||
|
verbale_date = Column(Date, nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
|
||||||
@@ -101,6 +104,17 @@ class RemissionInvoice(Base):
|
|||||||
total = Column(Numeric(14, 2), nullable=False)
|
total = Column(Numeric(14, 2), nullable=False)
|
||||||
pdf_filename = Column(String(512), nullable=True) # per ora solo nome, upload vero dopo
|
pdf_filename = Column(String(512), nullable=True) # per ora solo nome, upload vero dopo
|
||||||
|
|
||||||
|
# Campi istruttoria (dual declared/verified)
|
||||||
|
taxable_verified = Column(Numeric(14, 2), nullable=True)
|
||||||
|
vat_verified = Column(Numeric(14, 2), nullable=True)
|
||||||
|
total_verified = Column(Numeric(14, 2), nullable=True)
|
||||||
|
verification_status = Column(String(16), nullable=False, default="PENDING")
|
||||||
|
# PENDING | AMMESSA | PARZIALE | RESPINTA
|
||||||
|
verification_notes = Column(Text, nullable=True)
|
||||||
|
date_checks = Column(JSONB, nullable=True, default=dict)
|
||||||
|
verified_by = Column(Integer, nullable=True)
|
||||||
|
verified_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||||
|
|
||||||
practice = relationship("RemissionPractice", back_populates="invoices")
|
practice = relationship("RemissionPractice", back_populates="invoices")
|
||||||
@@ -127,6 +141,13 @@ class RemissionUlaEmployee(Base):
|
|||||||
supporting_doc_type = Column(String(64), nullable=True)
|
supporting_doc_type = Column(String(64), nullable=True)
|
||||||
supporting_doc_filename = Column(String(512), nullable=True)
|
supporting_doc_filename = Column(String(512), nullable=True)
|
||||||
|
|
||||||
|
# Campi istruttoria
|
||||||
|
fte_pct_verified = Column(Numeric(5, 4), nullable=True)
|
||||||
|
verification_status = Column(String(16), nullable=False, default="PENDING")
|
||||||
|
verification_notes = Column(Text, nullable=True)
|
||||||
|
verified_by = Column(Integer, nullable=True)
|
||||||
|
verified_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
||||||
|
|
||||||
practice = relationship("RemissionPractice", back_populates="ula_employees")
|
practice = relationship("RemissionPractice", back_populates="ula_employees")
|
||||||
@@ -148,6 +169,13 @@ class RemissionDocument(Base):
|
|||||||
expires_at = Column(Date, nullable=True)
|
expires_at = Column(Date, nullable=True)
|
||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
# Campi istruttoria
|
||||||
|
verification_status = Column(String(16), nullable=False, default="PENDING")
|
||||||
|
# PENDING | VALIDO | NON_VALIDO | SCADUTO
|
||||||
|
verification_notes = Column(Text, nullable=True)
|
||||||
|
verified_by = Column(Integer, nullable=True)
|
||||||
|
verified_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
practice = relationship("RemissionPractice", back_populates="documents")
|
practice = relationship("RemissionPractice", back_populates="documents")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,13 @@ from ..models import RemissionPractice, RemissionAmendmentRequest
|
|||||||
from ..schemas import (
|
from ..schemas import (
|
||||||
AmendmentRequestCreate, AmendmentRequestOut, AmendmentResponseSubmit,
|
AmendmentRequestCreate, AmendmentRequestOut, AmendmentResponseSubmit,
|
||||||
ReviewApproveBody, ReviewRejectBody,
|
ReviewApproveBody, ReviewRejectBody,
|
||||||
InstructorQueueItem, PracticeOut, ApiResponse
|
InstructorQueueItem, PracticeOut, ApiResponse,
|
||||||
|
InvoiceVerifyBody, UlaVerifyBody, DocumentVerifyBody,
|
||||||
|
InstructorFinalNotesBody,
|
||||||
|
InvoiceOut, UlaEmployeeOut, DocumentOut
|
||||||
)
|
)
|
||||||
|
from ..models import RemissionInvoice, RemissionUlaEmployee, RemissionDocument
|
||||||
|
from datetime import date
|
||||||
from .practices import _compute_gate_check
|
from .practices import _compute_gate_check
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/remission-practices/instructor", tags=["instructor"])
|
router = APIRouter(prefix="/api/remission-practices/instructor", tags=["instructor"])
|
||||||
@@ -274,3 +279,153 @@ def respond_amendment_beneficiary(practice_id: UUID, amendment_id: UUID,
|
|||||||
db.refresh(ar)
|
db.refresh(ar)
|
||||||
return ApiResponse(message="Risposta registrata. L'istruttore verrà notificato.",
|
return ApiResponse(message="Risposta registrata. L'istruttore verrà notificato.",
|
||||||
data=AmendmentRequestOut.model_validate(ar).model_dump(mode="json"))
|
data=AmendmentRequestOut.model_validate(ar).model_dump(mode="json"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ========== VERIFICA SINGOLA RIGA ==========
|
||||||
|
|
||||||
|
def _auto_check_dates(inv, practice) -> dict:
|
||||||
|
"""Verifica automatica: fattura emessa e pagata dentro il periodo ammissibilita.
|
||||||
|
Legge period_start/period_end dal gate_rules dello schema_snapshot."""
|
||||||
|
rules = practice.schema_snapshot.get("gate_rules", {}) or {}
|
||||||
|
|
||||||
|
pstart = rules.get("period_start")
|
||||||
|
pend = rules.get("period_end")
|
||||||
|
try:
|
||||||
|
from datetime import date as _date
|
||||||
|
pstart_d = _date.fromisoformat(pstart) if pstart else None
|
||||||
|
pend_d = _date.fromisoformat(pend) if pend else None
|
||||||
|
except Exception:
|
||||||
|
pstart_d = pend_d = None
|
||||||
|
|
||||||
|
def _in_range(d):
|
||||||
|
if not d: return None
|
||||||
|
ok = True
|
||||||
|
if pstart_d: ok = ok and d >= pstart_d
|
||||||
|
if pend_d: ok = ok and d <= pend_d
|
||||||
|
return ok
|
||||||
|
|
||||||
|
return {
|
||||||
|
"period_start": pstart,
|
||||||
|
"period_end": pend,
|
||||||
|
"invoice_in_period": _in_range(inv.invoice_date),
|
||||||
|
"payment_in_period": _in_range(inv.payment_date)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{practice_id}/invoices/{invoice_id}/verify", response_model=ApiResponse)
|
||||||
|
def verify_invoice(practice_id: UUID, invoice_id: UUID, body: InvoiceVerifyBody,
|
||||||
|
db: Session = Depends(get_db), user: AuthUser = Depends(_require_instructor)):
|
||||||
|
"""Rettifica/verifica una singola fattura."""
|
||||||
|
p = _get_practice_or_404(db, practice_id)
|
||||||
|
if p.status not in ("UNDER_REVIEW", "AWAITING_AMENDMENT"):
|
||||||
|
raise HTTPException(status_code=409, detail=f"Pratica in stato {p.status}")
|
||||||
|
if body.verification_status not in ("AMMESSA", "PARZIALE", "RESPINTA", "PENDING"):
|
||||||
|
raise HTTPException(status_code=422, detail="verification_status non valido")
|
||||||
|
|
||||||
|
inv = db.query(RemissionInvoice).filter(
|
||||||
|
RemissionInvoice.id == invoice_id,
|
||||||
|
RemissionInvoice.practice_id == practice_id
|
||||||
|
).first()
|
||||||
|
if not inv:
|
||||||
|
raise HTTPException(status_code=404, detail="Fattura non trovata")
|
||||||
|
|
||||||
|
inv.verification_status = body.verification_status
|
||||||
|
inv.verification_notes = body.verification_notes
|
||||||
|
if body.verification_status in ("AMMESSA", "PARZIALE"):
|
||||||
|
# Se AMMESSA e nessuna rettifica: copio i dichiarati come verificati
|
||||||
|
if body.verification_status == "AMMESSA" and body.taxable_verified is None and body.total_verified is None:
|
||||||
|
inv.taxable_verified = inv.taxable
|
||||||
|
inv.vat_verified = inv.vat
|
||||||
|
inv.total_verified = inv.total
|
||||||
|
else:
|
||||||
|
if body.taxable_verified is not None: inv.taxable_verified = body.taxable_verified
|
||||||
|
if body.vat_verified is not None: inv.vat_verified = body.vat_verified
|
||||||
|
if body.total_verified is not None: inv.total_verified = body.total_verified
|
||||||
|
else: # RESPINTA | PENDING
|
||||||
|
inv.taxable_verified = inv.vat_verified = inv.total_verified = None
|
||||||
|
|
||||||
|
inv.date_checks = _auto_check_dates(inv, p)
|
||||||
|
inv.verified_by = user.user_id
|
||||||
|
inv.verified_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(inv)
|
||||||
|
return ApiResponse(message=f"Fattura {body.verification_status}",
|
||||||
|
data=InvoiceOut.model_validate(inv).model_dump(mode="json"))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{practice_id}/ula-employees/{employee_id}/verify", response_model=ApiResponse)
|
||||||
|
def verify_ula_employee(practice_id: UUID, employee_id: UUID, body: UlaVerifyBody,
|
||||||
|
db: Session = Depends(get_db), user: AuthUser = Depends(_require_instructor)):
|
||||||
|
p = _get_practice_or_404(db, practice_id)
|
||||||
|
if p.status not in ("UNDER_REVIEW", "AWAITING_AMENDMENT"):
|
||||||
|
raise HTTPException(status_code=409, detail=f"Pratica in stato {p.status}")
|
||||||
|
if body.verification_status not in ("AMMESSA", "PARZIALE", "RESPINTA", "PENDING"):
|
||||||
|
raise HTTPException(status_code=422, detail="verification_status non valido")
|
||||||
|
|
||||||
|
e = db.query(RemissionUlaEmployee).filter(
|
||||||
|
RemissionUlaEmployee.id == employee_id,
|
||||||
|
RemissionUlaEmployee.practice_id == practice_id
|
||||||
|
).first()
|
||||||
|
if not e:
|
||||||
|
raise HTTPException(status_code=404, detail="Dipendente non trovato")
|
||||||
|
|
||||||
|
e.verification_status = body.verification_status
|
||||||
|
e.verification_notes = body.verification_notes
|
||||||
|
if body.verification_status in ("AMMESSA", "PARZIALE"):
|
||||||
|
if body.verification_status == "AMMESSA" and body.fte_pct_verified is None:
|
||||||
|
e.fte_pct_verified = e.fte_pct
|
||||||
|
elif body.fte_pct_verified is not None:
|
||||||
|
e.fte_pct_verified = body.fte_pct_verified
|
||||||
|
else:
|
||||||
|
e.fte_pct_verified = None
|
||||||
|
|
||||||
|
e.verified_by = user.user_id
|
||||||
|
e.verified_at = datetime.now(timezone.utc)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(e)
|
||||||
|
return ApiResponse(message=f"Dipendente {body.verification_status}",
|
||||||
|
data=UlaEmployeeOut.model_validate(e).model_dump(mode="json"))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{practice_id}/documents/{doc_code}/verify", response_model=ApiResponse)
|
||||||
|
def verify_document(practice_id: UUID, doc_code: str, body: DocumentVerifyBody,
|
||||||
|
db: Session = Depends(get_db), user: AuthUser = Depends(_require_instructor)):
|
||||||
|
p = _get_practice_or_404(db, practice_id)
|
||||||
|
if p.status not in ("UNDER_REVIEW", "AWAITING_AMENDMENT"):
|
||||||
|
raise HTTPException(status_code=409, detail=f"Pratica in stato {p.status}")
|
||||||
|
if body.verification_status not in ("VALIDO", "NON_VALIDO", "SCADUTO", "PENDING"):
|
||||||
|
raise HTTPException(status_code=422, detail="verification_status non valido")
|
||||||
|
|
||||||
|
d = db.query(RemissionDocument).filter(
|
||||||
|
RemissionDocument.practice_id == practice_id,
|
||||||
|
RemissionDocument.doc_code == doc_code
|
||||||
|
).first()
|
||||||
|
if not d:
|
||||||
|
raise HTTPException(status_code=404, detail="Documento non trovato")
|
||||||
|
|
||||||
|
d.verification_status = body.verification_status
|
||||||
|
d.verification_notes = body.verification_notes
|
||||||
|
d.verified_by = user.user_id
|
||||||
|
d.verified_at = datetime.now(timezone.utc)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(d)
|
||||||
|
return ApiResponse(message=f"Documento {body.verification_status}",
|
||||||
|
data=DocumentOut.model_validate(d).model_dump(mode="json"))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{practice_id}/final-notes", response_model=ApiResponse)
|
||||||
|
def set_instructor_final_notes(practice_id: UUID, body: InstructorFinalNotesBody,
|
||||||
|
db: Session = Depends(get_db), user: AuthUser = Depends(_require_instructor)):
|
||||||
|
p = _get_practice_or_404(db, practice_id)
|
||||||
|
if p.status not in ("UNDER_REVIEW", "AWAITING_AMENDMENT"):
|
||||||
|
raise HTTPException(status_code=409, detail=f"Pratica in stato {p.status}")
|
||||||
|
if body.instructor_final_notes is not None:
|
||||||
|
p.instructor_final_notes = body.instructor_final_notes
|
||||||
|
if body.instructor_checklist is not None:
|
||||||
|
p.instructor_checklist = body.instructor_checklist
|
||||||
|
db.commit()
|
||||||
|
db.refresh(p)
|
||||||
|
return ApiResponse(message="Verbale aggiornato",
|
||||||
|
data=PracticeOut.model_validate(p).model_dump(mode="json"))
|
||||||
|
|||||||
@@ -52,20 +52,49 @@ def _ensure_editable(practice: RemissionPractice):
|
|||||||
|
|
||||||
|
|
||||||
def _compute_gate_check(practice: RemissionPractice) -> GateCheckResult:
|
def _compute_gate_check(practice: RemissionPractice) -> GateCheckResult:
|
||||||
"""Valuta le gate_rules dello schema snapshot contro il contenuto della pratica."""
|
"""Valuta le gate_rules dello schema snapshot contro il contenuto della pratica.
|
||||||
|
Calcola:
|
||||||
|
- per_category_declared: totali dichiarati dal beneficiario (sempre)
|
||||||
|
- per_category_verified: totali verificati dall istruttore (solo AMMESSA/PARZIALE)
|
||||||
|
- grand_total: totale dichiarato (rif. beneficiario)
|
||||||
|
- grand_total_verified: totale verificato (rif. remissione finale)
|
||||||
|
"""
|
||||||
rules = practice.schema_snapshot.get("gate_rules", {}) or {}
|
rules = practice.schema_snapshot.get("gate_rules", {}) or {}
|
||||||
sections = practice.schema_snapshot.get("sections", []) or []
|
sections = practice.schema_snapshot.get("sections", []) or []
|
||||||
|
|
||||||
# totali per categoria
|
|
||||||
per_category = {}
|
|
||||||
grand_total = Decimal("0")
|
|
||||||
iva_ordinario_only_taxable = rules.get("iva_ordinario_imponibile_only", True)
|
iva_ordinario_only_taxable = rules.get("iva_ordinario_imponibile_only", True)
|
||||||
use_taxable_only = (practice.iva_regime == "ORDINARIO" and iva_ordinario_only_taxable)
|
use_taxable_only = (practice.iva_regime == "ORDINARIO" and iva_ordinario_only_taxable)
|
||||||
|
|
||||||
|
per_category_declared = {}
|
||||||
|
per_category_verified = {}
|
||||||
|
grand_total = Decimal("0")
|
||||||
|
grand_total_verified = Decimal("0")
|
||||||
|
|
||||||
|
any_verified = False
|
||||||
|
all_verified = len(practice.invoices) > 0
|
||||||
|
|
||||||
for inv in practice.invoices:
|
for inv in practice.invoices:
|
||||||
amt = inv.taxable if use_taxable_only else inv.total
|
# Dichiarato (sempre)
|
||||||
per_category[inv.category_code] = per_category.get(inv.category_code, Decimal("0")) + amt
|
amt_decl = inv.taxable if use_taxable_only else inv.total
|
||||||
grand_total += amt
|
per_category_declared[inv.category_code] = per_category_declared.get(inv.category_code, Decimal("0")) + amt_decl
|
||||||
|
grand_total += amt_decl
|
||||||
|
|
||||||
|
# Verificato (solo se stato AMMESSA o PARZIALE)
|
||||||
|
if inv.verification_status in ("AMMESSA", "PARZIALE"):
|
||||||
|
any_verified = True
|
||||||
|
# se PARZIALE usa i valori verified; se AMMESSA ma verified sono null, usa dichiarato
|
||||||
|
if inv.verification_status == "PARZIALE":
|
||||||
|
tax_v = inv.taxable_verified if inv.taxable_verified is not None else inv.taxable
|
||||||
|
tot_v = inv.total_verified if inv.total_verified is not None else inv.total
|
||||||
|
else: # AMMESSA
|
||||||
|
tax_v = inv.taxable_verified if inv.taxable_verified is not None else inv.taxable
|
||||||
|
tot_v = inv.total_verified if inv.total_verified is not None else inv.total
|
||||||
|
amt_ver = tax_v if use_taxable_only else tot_v
|
||||||
|
per_category_verified[inv.category_code] = per_category_verified.get(inv.category_code, Decimal("0")) + amt_ver
|
||||||
|
grand_total_verified += amt_ver
|
||||||
|
elif inv.verification_status == "PENDING":
|
||||||
|
all_verified = False
|
||||||
|
# RESPINTA: non contribuisce ai verified
|
||||||
|
|
||||||
# cap remissione
|
# cap remissione
|
||||||
amt_erogato = practice.amount_erogato
|
amt_erogato = practice.amount_erogato
|
||||||
@@ -73,6 +102,14 @@ def _compute_gate_check(practice: RemissionPractice) -> GateCheckResult:
|
|||||||
cap_abs = Decimal(str(rules.get("cap_absolute", 12500)))
|
cap_abs = Decimal(str(rules.get("cap_absolute", 12500)))
|
||||||
max_remission = min(cap_pct * amt_erogato, cap_abs)
|
max_remission = min(cap_pct * amt_erogato, cap_abs)
|
||||||
|
|
||||||
|
# Se almeno 1 verifica fatta -> uso grand_total_verified per remission_due
|
||||||
|
# altrimenti uso grand_total (dichiarato) per preview pre-istruttoria
|
||||||
|
effective_total = grand_total_verified if any_verified else grand_total
|
||||||
|
remission_due = min(effective_total, max_remission)
|
||||||
|
|
||||||
|
# Per compatibilità: per_category e grand_total restano "dichiarato"
|
||||||
|
per_category = per_category_declared
|
||||||
|
|
||||||
checks = []
|
checks = []
|
||||||
|
|
||||||
# Check 1: regime IVA scelto
|
# Check 1: regime IVA scelto
|
||||||
@@ -140,17 +177,22 @@ def _compute_gate_check(practice: RemissionPractice) -> GateCheckResult:
|
|||||||
"detail": f"Erogato: {amt_erogato} EUR"
|
"detail": f"Erogato: {amt_erogato} EUR"
|
||||||
})
|
})
|
||||||
|
|
||||||
remission_due = min(grand_total, max_remission)
|
|
||||||
|
|
||||||
return GateCheckResult(
|
return GateCheckResult(
|
||||||
passed=all(c["passed"] for c in checks),
|
passed=all(c["passed"] for c in checks),
|
||||||
checks=checks,
|
checks=checks,
|
||||||
totals={
|
totals={
|
||||||
"per_category": {k: float(v) for k, v in per_category.items()},
|
"per_category": {k: float(v) for k, v in per_category.items()},
|
||||||
|
"per_category_declared": {k: float(v) for k, v in per_category_declared.items()},
|
||||||
|
"per_category_verified": {k: float(v) for k, v in per_category_verified.items()},
|
||||||
"grand_total": float(grand_total),
|
"grand_total": float(grand_total),
|
||||||
|
"grand_total_declared": float(grand_total),
|
||||||
|
"grand_total_verified": float(grand_total_verified),
|
||||||
"max_remission": float(max_remission),
|
"max_remission": float(max_remission),
|
||||||
"remission_due": float(remission_due),
|
"remission_due": float(remission_due),
|
||||||
"amount_erogato": float(amt_erogato)
|
"amount_erogato": float(amt_erogato),
|
||||||
|
"any_verified": any_verified,
|
||||||
|
"all_verified": all_verified,
|
||||||
|
"residuo_da_restituire": float(max(amt_erogato - Decimal(str(remission_due)), Decimal("0")))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,15 @@ class InvoiceOut(InvoiceCreate):
|
|||||||
id: UUID
|
id: UUID
|
||||||
practice_id: UUID
|
practice_id: UUID
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
# istruttoria
|
||||||
|
taxable_verified: Optional[Decimal] = None
|
||||||
|
vat_verified: Optional[Decimal] = None
|
||||||
|
total_verified: Optional[Decimal] = None
|
||||||
|
verification_status: str = "PENDING"
|
||||||
|
verification_notes: Optional[str] = None
|
||||||
|
date_checks: Optional[dict] = None
|
||||||
|
verified_by: Optional[int] = None
|
||||||
|
verified_at: Optional[datetime] = None
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
@@ -89,6 +98,12 @@ class UlaEmployeeOut(UlaEmployeeCreate):
|
|||||||
id: UUID
|
id: UUID
|
||||||
practice_id: UUID
|
practice_id: UUID
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
# istruttoria
|
||||||
|
fte_pct_verified: Optional[Decimal] = None
|
||||||
|
verification_status: str = "PENDING"
|
||||||
|
verification_notes: Optional[str] = None
|
||||||
|
verified_by: Optional[int] = None
|
||||||
|
verified_at: Optional[datetime] = None
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
@@ -110,6 +125,11 @@ class DocumentOut(BaseModel):
|
|||||||
uploaded_at: Optional[datetime] = None
|
uploaded_at: Optional[datetime] = None
|
||||||
expires_at: Optional[date] = None
|
expires_at: Optional[date] = None
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
# istruttoria
|
||||||
|
verification_status: str = "PENDING"
|
||||||
|
verification_notes: Optional[str] = None
|
||||||
|
verified_by: Optional[int] = None
|
||||||
|
verified_at: Optional[datetime] = None
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
@@ -130,6 +150,16 @@ class PracticeOut(BaseModel):
|
|||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
submitted_at: Optional[datetime] = None
|
submitted_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
# istruttoria
|
||||||
|
assigned_instructor_id: Optional[int] = None
|
||||||
|
reviewed_at: Optional[datetime] = None
|
||||||
|
reviewed_by: Optional[int] = None
|
||||||
|
rejection_reason: Optional[str] = None
|
||||||
|
approved_remission: Optional[Decimal] = None
|
||||||
|
instructor_final_notes: Optional[str] = None
|
||||||
|
instructor_checklist: Optional[dict] = None
|
||||||
|
verbale_date: Optional[date] = None
|
||||||
|
|
||||||
invoices: List[InvoiceOut] = []
|
invoices: List[InvoiceOut] = []
|
||||||
ula_employees: List[UlaEmployeeOut] = []
|
ula_employees: List[UlaEmployeeOut] = []
|
||||||
documents: List[DocumentOut] = []
|
documents: List[DocumentOut] = []
|
||||||
@@ -223,6 +253,35 @@ class InstructorQueueItem(BaseModel):
|
|||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Verifica singola fattura
|
||||||
|
class InvoiceVerifyBody(BaseModel):
|
||||||
|
verification_status: str # AMMESSA | PARZIALE | RESPINTA
|
||||||
|
taxable_verified: Optional[Decimal] = None
|
||||||
|
vat_verified: Optional[Decimal] = None
|
||||||
|
total_verified: Optional[Decimal] = None
|
||||||
|
verification_notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# Verifica singolo dipendente ULA
|
||||||
|
class UlaVerifyBody(BaseModel):
|
||||||
|
verification_status: str # AMMESSA | PARZIALE | RESPINTA
|
||||||
|
fte_pct_verified: Optional[Decimal] = None
|
||||||
|
verification_notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# Verifica singolo documento
|
||||||
|
class DocumentVerifyBody(BaseModel):
|
||||||
|
verification_status: str # VALIDO | NON_VALIDO | SCADUTO
|
||||||
|
verification_notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# Note finali istruttore + checklist
|
||||||
|
class InstructorFinalNotesBody(BaseModel):
|
||||||
|
instructor_final_notes: Optional[str] = None
|
||||||
|
instructor_checklist: Optional[dict] = None
|
||||||
|
|
||||||
|
|
||||||
# ====================== Wrapper ======================
|
# ====================== Wrapper ======================
|
||||||
|
|
||||||
class ApiResponse(BaseModel):
|
class ApiResponse(BaseModel):
|
||||||
|
|||||||
@@ -12,8 +12,24 @@ RESTART_TEMPLATE = {
|
|||||||
"type": "static_fields",
|
"type": "static_fields",
|
||||||
"id": "general",
|
"id": "general",
|
||||||
"label": "Dati generali",
|
"label": "Dati generali",
|
||||||
"description": "Regime IVA e dati base del beneficiario. ATECO e importo erogato sono pre-compilati dalla domanda approvata.",
|
"description": "Regime IVA, dati base del beneficiario, periodo di ammissibilità delle spese.",
|
||||||
"fields": [
|
"fields": [
|
||||||
|
{
|
||||||
|
"id": "period_start_date",
|
||||||
|
"type": "date",
|
||||||
|
"label": "Periodo ammissibilità — Data inizio",
|
||||||
|
"description": "Data minima di emissione/pagamento fatture ammissibili. Di norma: data erogazione del finanziamento.",
|
||||||
|
"editable_by": "superadmin",
|
||||||
|
"required": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "period_end_date",
|
||||||
|
"type": "date",
|
||||||
|
"label": "Periodo ammissibilità — Data fine",
|
||||||
|
"description": "Data massima di emissione/pagamento fatture ammissibili.",
|
||||||
|
"editable_by": "superadmin",
|
||||||
|
"required": True
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "iva_regime",
|
"id": "iva_regime",
|
||||||
"type": "select",
|
"type": "select",
|
||||||
|
|||||||
Reference in New Issue
Block a user