- 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>
50 lines
1.3 KiB
Python
50 lines
1.3 KiB
Python
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
|