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>
This commit is contained in:
2026-03-13 17:34:36 +02:00
parent efc9545ae6
commit 3bdafad22a
17 changed files with 786 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
from datetime import UTC, datetime
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.base import uuid7
from app.db.models.invoice import Invoice
from app.db.models.order import Order
async def create_invoice(
db: AsyncSession, tenant_id: str, order_id: str
) -> Invoice:
r = await db.execute(
select(Order).where(Order.id == order_id, Order.tenant_id == tenant_id)
)
order = r.scalar_one_or_none()
if not order:
raise ValueError("Order not found")
if order.status != "VALIDAT":
raise ValueError("Order must be VALIDAT to create invoice")
# Generate next invoice number for this tenant
r = await db.execute(
text(
"SELECT COUNT(*) as cnt FROM invoices WHERE tenant_id = :tid"
),
{"tid": tenant_id},
)
count = r.scalar() + 1
now = datetime.now(UTC)
nr_factura = f"F-{now.year}-{count:04d}"
invoice = Invoice(
id=uuid7(),
tenant_id=tenant_id,
order_id=order_id,
nr_factura=nr_factura,
data_factura=now.isoformat().split("T")[0],
total=order.total_general,
)
db.add(invoice)
# Update order status to FACTURAT
order.status = "FACTURAT"
order.updated_at = now.isoformat()
await db.commit()
await db.refresh(invoice)
return invoice