Files
roaauto/backend/app/invoices/router.py
Marius Mutu 3bdafad22a feat(backend): PDF deviz + portal client + SMS + invoice service
- PDF generation with WeasyPrint: deviz and factura templates (A4, branding)
- GET /orders/{id}/pdf/deviz returns PDF with order lines and totals
- Client portal (public, no auth): GET /p/{token}, POST /p/{token}/accept|reject
- SMS service (SMSAPI.ro) - skips in dev when no token configured
- Invoice service: create from validated order, auto-number (F-YYYY-NNNN)
- GET /invoices/{id}/pdf returns factura PDF
- Order status_client field for client accept/reject tracking
- Alembic migration for status_client
- 19 passing tests (auth + sync + orders + pdf + portal + invoices)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 17:34:36 +02:00

112 lines
3.3 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models.invoice import Invoice
from app.db.models.order import Order
from app.db.models.order_line import OrderLine
from app.db.models.tenant import Tenant
from app.db.models.vehicle import Vehicle
from app.db.session import get_db
from app.deps import get_tenant_id
from app.invoices import service
from app.pdf.service import generate_factura
router = APIRouter()
@router.post("")
async def create_invoice(
data: dict,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
order_id = data.get("order_id")
if not order_id:
raise HTTPException(status_code=422, detail="order_id required")
try:
invoice = await service.create_invoice(db, tenant_id, order_id)
return {"id": invoice.id, "nr_factura": invoice.nr_factura}
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
@router.get("/{invoice_id}/pdf")
async def get_invoice_pdf(
invoice_id: str,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
r = await db.execute(
select(Invoice).where(
Invoice.id == invoice_id, Invoice.tenant_id == tenant_id
)
)
invoice = r.scalar_one_or_none()
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
r = await db.execute(select(Order).where(Order.id == invoice.order_id))
order = r.scalar_one()
r = await db.execute(select(Vehicle).where(Vehicle.id == order.vehicle_id))
vehicle = r.scalar_one_or_none()
r = await db.execute(select(Tenant).where(Tenant.id == tenant_id))
tenant = r.scalar_one()
r = await db.execute(
select(OrderLine).where(OrderLine.order_id == order.id)
)
lines = r.scalars().all()
order_data = {
"id": order.id,
"nr_auto": vehicle.nr_inmatriculare if vehicle else "",
"client_nume": vehicle.client_nume if vehicle else "",
"client_cui": vehicle.client_cui if vehicle else "",
"client_adresa": vehicle.client_adresa if vehicle else "",
"total_manopera": order.total_manopera,
"total_materiale": order.total_materiale,
"total_general": order.total_general,
}
invoice_data = {
"nr_factura": invoice.nr_factura,
"data_factura": invoice.data_factura,
"total": invoice.total,
}
tenant_data = {
"nume": tenant.nume,
"cui": tenant.cui,
"reg_com": tenant.reg_com,
"adresa": tenant.adresa,
"iban": tenant.iban,
"banca": tenant.banca,
}
lines_data = [
{
"tip": l.tip,
"descriere": l.descriere,
"ore": l.ore,
"pret_ora": l.pret_ora,
"cantitate": l.cantitate,
"pret_unitar": l.pret_unitar,
"um": l.um,
"total": l.total,
}
for l in lines
]
pdf_bytes = generate_factura(invoice_data, order_data, lines_data, tenant_data)
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f'inline; filename="factura-{invoice.nr_factura}.pdf"'
},
)