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:
@@ -0,0 +1,73 @@
|
||||
"""add_clients_table_and_client_id_columns
|
||||
|
||||
Revision ID: 6d8b5bd44531
|
||||
Revises: 7df0fb1c1e6f
|
||||
Create Date: 2026-03-14 10:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '6d8b5bd44531'
|
||||
down_revision: Union[str, None] = '7df0fb1c1e6f'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create clients table
|
||||
op.create_table(
|
||||
'clients',
|
||||
sa.Column('id', sa.String(length=36), primary_key=True),
|
||||
sa.Column('tenant_id', sa.String(length=36), nullable=False, index=True),
|
||||
sa.Column('created_at', sa.Text(), nullable=True),
|
||||
sa.Column('updated_at', sa.Text(), nullable=True),
|
||||
sa.Column('tip_persoana', sa.String(length=2), nullable=True),
|
||||
sa.Column('denumire', sa.String(length=200), nullable=True),
|
||||
sa.Column('nume', sa.String(length=100), nullable=True),
|
||||
sa.Column('prenume', sa.String(length=100), nullable=True),
|
||||
sa.Column('cod_fiscal', sa.String(length=20), nullable=True),
|
||||
sa.Column('reg_com', sa.String(length=30), nullable=True),
|
||||
sa.Column('telefon', sa.String(length=20), nullable=True),
|
||||
sa.Column('email', sa.String(length=200), nullable=True),
|
||||
sa.Column('adresa', sa.Text(), nullable=True),
|
||||
sa.Column('judet', sa.String(length=50), nullable=True),
|
||||
sa.Column('oras', sa.String(length=100), nullable=True),
|
||||
sa.Column('cod_postal', sa.String(length=10), nullable=True),
|
||||
sa.Column('tara', sa.String(length=2), nullable=True),
|
||||
sa.Column('cont_iban', sa.String(length=34), nullable=True),
|
||||
sa.Column('banca', sa.String(length=100), nullable=True),
|
||||
sa.Column('activ', sa.Integer(), server_default='1', nullable=False),
|
||||
sa.Column('oracle_id', sa.Integer(), nullable=True),
|
||||
)
|
||||
|
||||
# Add client_id to vehicles
|
||||
with op.batch_alter_table('vehicles') as batch_op:
|
||||
batch_op.add_column(sa.Column('client_id', sa.String(length=36), nullable=True))
|
||||
|
||||
# Add client_id to orders
|
||||
with op.batch_alter_table('orders') as batch_op:
|
||||
batch_op.add_column(sa.Column('client_id', sa.String(length=36), nullable=True))
|
||||
|
||||
# Add client_id and tip_document to invoices
|
||||
with op.batch_alter_table('invoices') as batch_op:
|
||||
batch_op.add_column(sa.Column('client_id', sa.String(length=36), nullable=True))
|
||||
batch_op.add_column(sa.Column('tip_document', sa.String(length=20), server_default='FACTURA', nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table('invoices') as batch_op:
|
||||
batch_op.drop_column('tip_document')
|
||||
batch_op.drop_column('client_id')
|
||||
|
||||
with op.batch_alter_table('orders') as batch_op:
|
||||
batch_op.drop_column('client_id')
|
||||
|
||||
with op.batch_alter_table('vehicles') as batch_op:
|
||||
batch_op.drop_column('client_id')
|
||||
|
||||
op.drop_table('clients')
|
||||
0
backend/app/clients/__init__.py
Normal file
0
backend/app/clients/__init__.py
Normal file
163
backend/app/clients/router.py
Normal file
163
backend/app/clients/router.py
Normal file
@@ -0,0 +1,163 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.base import uuid7
|
||||
from app.db.models.client import Client
|
||||
from app.db.session import get_db
|
||||
from app.deps import get_tenant_id
|
||||
from app.clients import schemas
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_clients(
|
||||
search: str | None = None,
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
query = select(Client).where(Client.tenant_id == tenant_id)
|
||||
if search:
|
||||
pattern = f"%{search}%"
|
||||
query = query.where(
|
||||
or_(
|
||||
Client.denumire.ilike(pattern),
|
||||
Client.nume.ilike(pattern),
|
||||
Client.prenume.ilike(pattern),
|
||||
Client.cod_fiscal.ilike(pattern),
|
||||
Client.telefon.ilike(pattern),
|
||||
Client.email.ilike(pattern),
|
||||
)
|
||||
)
|
||||
r = await db.execute(query)
|
||||
clients = r.scalars().all()
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"tip_persoana": c.tip_persoana,
|
||||
"denumire": c.denumire,
|
||||
"nume": c.nume,
|
||||
"prenume": c.prenume,
|
||||
"cod_fiscal": c.cod_fiscal,
|
||||
"reg_com": c.reg_com,
|
||||
"telefon": c.telefon,
|
||||
"email": c.email,
|
||||
"adresa": c.adresa,
|
||||
"judet": c.judet,
|
||||
"oras": c.oras,
|
||||
"cod_postal": c.cod_postal,
|
||||
"tara": c.tara,
|
||||
"cont_iban": c.cont_iban,
|
||||
"banca": c.banca,
|
||||
"activ": c.activ,
|
||||
}
|
||||
for c in clients
|
||||
]
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_client(
|
||||
data: schemas.ClientCreate,
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
client = Client(
|
||||
id=data.id or uuid7(),
|
||||
tenant_id=tenant_id,
|
||||
tip_persoana=data.tip_persoana,
|
||||
denumire=data.denumire,
|
||||
nume=data.nume,
|
||||
prenume=data.prenume,
|
||||
cod_fiscal=data.cod_fiscal,
|
||||
reg_com=data.reg_com,
|
||||
telefon=data.telefon,
|
||||
email=data.email,
|
||||
adresa=data.adresa,
|
||||
judet=data.judet,
|
||||
oras=data.oras,
|
||||
cod_postal=data.cod_postal,
|
||||
tara=data.tara,
|
||||
cont_iban=data.cont_iban,
|
||||
banca=data.banca,
|
||||
activ=data.activ,
|
||||
)
|
||||
db.add(client)
|
||||
await db.commit()
|
||||
return {"id": client.id}
|
||||
|
||||
|
||||
@router.get("/{client_id}")
|
||||
async def get_client(
|
||||
client_id: str,
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
r = await db.execute(
|
||||
select(Client).where(
|
||||
Client.id == client_id, Client.tenant_id == tenant_id
|
||||
)
|
||||
)
|
||||
c = r.scalar_one_or_none()
|
||||
if not c:
|
||||
raise HTTPException(status_code=404, detail="Client not found")
|
||||
return {
|
||||
"id": c.id,
|
||||
"tip_persoana": c.tip_persoana,
|
||||
"denumire": c.denumire,
|
||||
"nume": c.nume,
|
||||
"prenume": c.prenume,
|
||||
"cod_fiscal": c.cod_fiscal,
|
||||
"reg_com": c.reg_com,
|
||||
"telefon": c.telefon,
|
||||
"email": c.email,
|
||||
"adresa": c.adresa,
|
||||
"judet": c.judet,
|
||||
"oras": c.oras,
|
||||
"cod_postal": c.cod_postal,
|
||||
"tara": c.tara,
|
||||
"cont_iban": c.cont_iban,
|
||||
"banca": c.banca,
|
||||
"activ": c.activ,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{client_id}")
|
||||
async def update_client(
|
||||
client_id: str,
|
||||
data: schemas.ClientUpdate,
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
r = await db.execute(
|
||||
select(Client).where(
|
||||
Client.id == client_id, Client.tenant_id == tenant_id
|
||||
)
|
||||
)
|
||||
c = r.scalar_one_or_none()
|
||||
if not c:
|
||||
raise HTTPException(status_code=404, detail="Client not found")
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(c, key, value)
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/{client_id}")
|
||||
async def delete_client(
|
||||
client_id: str,
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
r = await db.execute(
|
||||
select(Client).where(
|
||||
Client.id == client_id, Client.tenant_id == tenant_id
|
||||
)
|
||||
)
|
||||
c = r.scalar_one_or_none()
|
||||
if not c:
|
||||
raise HTTPException(status_code=404, detail="Client not found")
|
||||
await db.delete(c)
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
41
backend/app/clients/schemas.py
Normal file
41
backend/app/clients/schemas.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ClientCreate(BaseModel):
|
||||
id: Optional[str] = None
|
||||
tip_persoana: str = "PF"
|
||||
denumire: Optional[str] = None
|
||||
nume: Optional[str] = None
|
||||
prenume: Optional[str] = None
|
||||
cod_fiscal: Optional[str] = None
|
||||
reg_com: Optional[str] = None
|
||||
telefon: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
adresa: Optional[str] = None
|
||||
judet: Optional[str] = None
|
||||
oras: Optional[str] = None
|
||||
cod_postal: Optional[str] = None
|
||||
tara: str = "RO"
|
||||
cont_iban: Optional[str] = None
|
||||
banca: Optional[str] = None
|
||||
activ: int = 1
|
||||
|
||||
|
||||
class ClientUpdate(BaseModel):
|
||||
tip_persoana: Optional[str] = None
|
||||
denumire: Optional[str] = None
|
||||
nume: Optional[str] = None
|
||||
prenume: Optional[str] = None
|
||||
cod_fiscal: Optional[str] = None
|
||||
reg_com: Optional[str] = None
|
||||
telefon: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
adresa: Optional[str] = None
|
||||
judet: Optional[str] = None
|
||||
oras: Optional[str] = None
|
||||
cod_postal: Optional[str] = None
|
||||
tara: Optional[str] = None
|
||||
cont_iban: Optional[str] = None
|
||||
banca: Optional[str] = None
|
||||
activ: Optional[int] = None
|
||||
@@ -1,5 +1,6 @@
|
||||
from app.db.models.tenant import Tenant
|
||||
from app.db.models.user import User
|
||||
from app.db.models.client import Client
|
||||
from app.db.models.vehicle import Vehicle
|
||||
from app.db.models.order import Order
|
||||
from app.db.models.order_line import OrderLine
|
||||
@@ -20,6 +21,7 @@ from app.db.models.invite import InviteToken
|
||||
__all__ = [
|
||||
"Tenant",
|
||||
"User",
|
||||
"Client",
|
||||
"Vehicle",
|
||||
"Order",
|
||||
"OrderLine",
|
||||
|
||||
25
backend/app/db/models/client.py
Normal file
25
backend/app/db/models/client.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from sqlalchemy import Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
|
||||
|
||||
class Client(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "clients"
|
||||
tip_persoana: Mapped[str | None] = mapped_column(String(2), default="PF")
|
||||
denumire: Mapped[str | None] = mapped_column(String(200))
|
||||
nume: Mapped[str | None] = mapped_column(String(100))
|
||||
prenume: Mapped[str | None] = mapped_column(String(100))
|
||||
cod_fiscal: Mapped[str | None] = mapped_column(String(20))
|
||||
reg_com: Mapped[str | None] = mapped_column(String(30))
|
||||
telefon: Mapped[str | None] = mapped_column(String(20))
|
||||
email: Mapped[str | None] = mapped_column(String(200))
|
||||
adresa: Mapped[str | None] = mapped_column(Text)
|
||||
judet: Mapped[str | None] = mapped_column(String(50))
|
||||
oras: Mapped[str | None] = mapped_column(String(100))
|
||||
cod_postal: Mapped[str | None] = mapped_column(String(10))
|
||||
tara: Mapped[str | None] = mapped_column(String(2), default="RO")
|
||||
cont_iban: Mapped[str | None] = mapped_column(String(34))
|
||||
banca: Mapped[str | None] = mapped_column(String(100))
|
||||
activ: Mapped[int] = mapped_column(Integer, default=1, server_default="1")
|
||||
oracle_id: Mapped[int | None] = mapped_column(Integer)
|
||||
@@ -7,6 +7,8 @@ from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
class Invoice(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "invoices"
|
||||
order_id: Mapped[str | None] = mapped_column(String(36), index=True)
|
||||
client_id: Mapped[str | None] = mapped_column(String(36))
|
||||
tip_document: Mapped[str | None] = mapped_column(String(20), default="FACTURA", server_default="FACTURA")
|
||||
nr_factura: Mapped[str | None] = mapped_column(String(50))
|
||||
serie_factura: Mapped[str | None] = mapped_column(String(20))
|
||||
data_factura: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
@@ -8,6 +8,7 @@ class Order(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "orders"
|
||||
nr_comanda: Mapped[str | None] = mapped_column(String(50))
|
||||
vehicle_id: Mapped[str | None] = mapped_column(String(36))
|
||||
client_id: Mapped[str | None] = mapped_column(String(36))
|
||||
tip_deviz_id: Mapped[str | None] = mapped_column(String(36))
|
||||
status: Mapped[str] = mapped_column(String(20), default="DRAFT", server_default="DRAFT")
|
||||
data_comanda: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
@@ -7,6 +7,7 @@ from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
class Vehicle(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "vehicles"
|
||||
nr_inmatriculare: Mapped[str] = mapped_column(String(20))
|
||||
client_id: Mapped[str | None] = mapped_column(String(36))
|
||||
marca_id: Mapped[str | None] = mapped_column(String(36))
|
||||
model_id: Mapped[str | None] = mapped_column(String(36))
|
||||
an_fabricatie: Mapped[int | None] = mapped_column(Integer)
|
||||
|
||||
@@ -52,14 +52,7 @@ ANSAMBLE = [
|
||||
"Revizie",
|
||||
]
|
||||
|
||||
TIPURI_DEVIZ = [
|
||||
"Deviz reparatie",
|
||||
"Deviz revizie",
|
||||
"Deviz diagnosticare",
|
||||
"Deviz estimativ",
|
||||
"Deviz vulcanizare",
|
||||
"Deviz ITP",
|
||||
]
|
||||
TIPURI_DEVIZ = ["Service", "ITP", "Regie", "Constatare"]
|
||||
|
||||
TIPURI_MOTOARE = ["Benzina", "Diesel", "Hibrid", "Electric", "GPL"]
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import Response
|
||||
from sqlalchemy import select
|
||||
@@ -109,3 +111,39 @@ async def get_invoice_pdf(
|
||||
"Content-Disposition": f'inline; filename="factura-{invoice.nr_factura}.pdf"'
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{invoice_id}")
|
||||
async def delete_invoice(
|
||||
invoice_id: str,
|
||||
tenant_id: str = Depends(get_tenant_id),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
r = await db.execute(
|
||||
select(Invoice).where(
|
||||
Invoice.id == invoice_id, Invoice.tenant_id == tenant_id
|
||||
)
|
||||
)
|
||||
invoice = r.scalar_one_or_none()
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
order_id = invoice.order_id
|
||||
|
||||
# Delete the invoice
|
||||
await db.delete(invoice)
|
||||
|
||||
# Revert the associated order status back to VALIDAT
|
||||
if order_id:
|
||||
r = await db.execute(
|
||||
select(Order).where(
|
||||
Order.id == order_id, Order.tenant_id == tenant_id
|
||||
)
|
||||
)
|
||||
order = r.scalar_one_or_none()
|
||||
if order:
|
||||
order.status = "VALIDAT"
|
||||
order.updated_at = datetime.now(UTC).isoformat()
|
||||
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
@@ -5,6 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.auth.router import router as auth_router
|
||||
from app.client_portal.router import router as portal_router
|
||||
from app.clients.router import router as clients_router
|
||||
from app.config import settings
|
||||
from app.db.base import Base
|
||||
from app.db.session import engine
|
||||
@@ -35,6 +36,7 @@ app.add_middleware(
|
||||
)
|
||||
app.include_router(auth_router, prefix="/api/auth")
|
||||
app.include_router(sync_router, prefix="/api/sync")
|
||||
app.include_router(clients_router, prefix="/api/clients")
|
||||
app.include_router(orders_router, prefix="/api/orders")
|
||||
app.include_router(vehicles_router, prefix="/api/vehicles")
|
||||
app.include_router(invoices_router, prefix="/api/invoices")
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -8,6 +8,19 @@ class CreateOrderRequest(BaseModel):
|
||||
observatii: str | None = None
|
||||
|
||||
|
||||
class UpdateOrderRequest(BaseModel):
|
||||
vehicle_id: str | None = None
|
||||
tip_deviz_id: str | None = None
|
||||
km_intrare: int | None = None
|
||||
observatii: str | None = None
|
||||
client_id: str | None = None
|
||||
client_nume: str | None = None
|
||||
client_telefon: str | None = None
|
||||
nr_auto: str | None = None
|
||||
marca_denumire: str | None = None
|
||||
model_denumire: str | None = None
|
||||
|
||||
|
||||
class AddLineRequest(BaseModel):
|
||||
tip: str # manopera | material
|
||||
descriere: str
|
||||
|
||||
@@ -4,6 +4,7 @@ from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
SYNCABLE_TABLES = [
|
||||
"clients",
|
||||
"vehicles",
|
||||
"orders",
|
||||
"order_lines",
|
||||
|
||||
Reference in New Issue
Block a user