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>
This commit is contained in:
2026-03-14 00:36:40 +02:00
parent 3e449d0b0b
commit 9db4e746e3
34 changed files with 2221 additions and 211 deletions

View File

@@ -1,8 +1,11 @@
from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import Response
from sqlalchemy import select
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
@@ -155,3 +158,115 @@ async def get_deviz_pdf(
"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}