Files
roaauto/backend/app/orders/router.py
Marius Mutu 9db4e746e3 feat: add clients nomenclator, order edit/delete/devalidate, invoice types, dashboard redesign
- New clients table with PF/PJ support, fiscal data (CUI, IBAN, eFactura fields)
- Full CRUD API for clients with search, sync integration
- Order lifecycle: edit header (DRAFT), devalidate (VALIDAT→DRAFT), delete order/invoice
- Invoice types: FACTURA (B2B) vs BON_FISCAL (B2C) with different nr formats
- OrderCreateView redesigned as multi-step flow (client→vehicle→details)
- Autocomplete from catalog_norme/catalog_preturi in OrderLineForm
- Dashboard now combines stats + full orders table with filter tabs and search
- ClientPicker and VehiclePicker with inline creation capability
- Frontend schema aligned with backend (missing columns causing sync errors)
- Mobile responsive fixes for OrderDetailView buttons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 00:36:40 +02:00

273 lines
7.8 KiB
Python

from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response
from sqlalchemy import select, text
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.orders import schemas, service
from app.pdf.service import generate_deviz
router = APIRouter()
@router.get("")
async def list_orders(
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
return await service.list_orders(db, tenant_id)
@router.post("")
async def create_order(
data: schemas.CreateOrderRequest,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
order = await service.create_order(
db,
tenant_id,
data.vehicle_id,
data.tip_deviz_id,
data.km_intrare,
data.observatii,
)
return {"id": order.id}
@router.get("/{order_id}")
async def get_order(
order_id: str,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
result = await service.get_order(db, tenant_id, order_id)
if not result:
raise HTTPException(status_code=404, detail="Order not found")
return result
@router.post("/{order_id}/lines")
async def add_line(
order_id: str,
data: schemas.AddLineRequest,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
try:
line = await service.add_line(
db,
tenant_id,
order_id,
data.tip,
data.descriere,
data.ore,
data.pret_ora,
data.cantitate,
data.pret_unitar,
data.um,
)
return {"id": line.id}
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
@router.post("/{order_id}/validate")
async def validate_order(
order_id: str,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
try:
order = await service.validate_order(db, tenant_id, order_id)
return {"status": order.status}
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
@router.get("/{order_id}/pdf/deviz")
async def get_deviz_pdf(
order_id: str,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
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 HTTPException(status_code=404, detail="Order not found")
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,
"data_comanda": order.data_comanda,
"nr_auto": vehicle.nr_inmatriculare if vehicle else "",
"client_nume": vehicle.client_nume if vehicle else "",
"marca_denumire": "",
"model_denumire": "",
"total_manopera": order.total_manopera,
"total_materiale": order.total_materiale,
"total_general": order.total_general,
}
tenant_data = {
"nume": tenant.nume,
"cui": tenant.cui,
"adresa": tenant.adresa,
"telefon": tenant.telefon,
}
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_deviz(order_data, lines_data, tenant_data)
return Response(
content=pdf_bytes,
media_type="application/pdf",
headers={
"Content-Disposition": f'inline; filename="deviz-{order.id[:8]}.pdf"'
},
)
@router.put("/{order_id}")
async def update_order(
order_id: str,
data: schemas.UpdateOrderRequest,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
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 HTTPException(status_code=404, detail="Order not found")
if order.status != "DRAFT":
raise HTTPException(status_code=422, detail="Can only update DRAFT orders")
update_data = data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(order, key, value)
order.updated_at = datetime.now(UTC).isoformat()
await db.commit()
await db.refresh(order)
return {
"id": order.id,
"vehicle_id": order.vehicle_id,
"client_id": order.client_id,
"tip_deviz_id": order.tip_deviz_id,
"status": order.status,
"km_intrare": order.km_intrare,
"observatii": order.observatii,
"client_nume": order.client_nume,
"client_telefon": order.client_telefon,
"nr_auto": order.nr_auto,
"marca_denumire": order.marca_denumire,
"model_denumire": order.model_denumire,
}
@router.post("/{order_id}/devalidate")
async def devalidate_order(
order_id: str,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
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 HTTPException(status_code=404, detail="Order not found")
if order.status != "VALIDAT":
raise HTTPException(status_code=422, detail="Can only devalidate VALIDAT orders")
# Check no invoice exists for this order
r = await db.execute(
select(Invoice).where(
Invoice.order_id == order_id, Invoice.tenant_id == tenant_id
)
)
invoice = r.scalar_one_or_none()
if invoice:
raise HTTPException(
status_code=422,
detail="Cannot devalidate order with existing invoice"
)
order.status = "DRAFT"
order.updated_at = datetime.now(UTC).isoformat()
await db.commit()
return {"ok": True, "status": "DRAFT"}
@router.delete("/{order_id}")
async def delete_order(
order_id: str,
tenant_id: str = Depends(get_tenant_id),
db: AsyncSession = Depends(get_db),
):
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 HTTPException(status_code=404, detail="Order not found")
if order.status == "FACTURAT":
# Check if invoice exists
r = await db.execute(
select(Invoice).where(
Invoice.order_id == order_id, Invoice.tenant_id == tenant_id
)
)
invoice = r.scalar_one_or_none()
if invoice:
raise HTTPException(
status_code=422,
detail="Cannot delete order with existing invoice"
)
# Delete order lines first
await db.execute(
text("DELETE FROM order_lines WHERE order_id = :oid AND tenant_id = :tid"),
{"oid": order_id, "tid": tenant_id},
)
# Delete the order
await db.execute(
text("DELETE FROM orders WHERE id = :oid AND tenant_id = :tid"),
{"oid": order_id, "tid": tenant_id},
)
await db.commit()
return {"ok": True}