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:
49
backend/app/invoices/service.py
Normal file
49
backend/app/invoices/service.py
Normal 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
|
||||
Reference in New Issue
Block a user