feat(backend): sync endpoints + all models + seed + order workflow
- All business models: Vehicle, Order, OrderLine, Invoice, Appointment, CatalogMarca/Model/Ansamblu/Norma/Pret/TipDeviz/TipMotor, Mecanic - Sync endpoints: GET /sync/full, GET /sync/changes?since=, POST /sync/push with tenant isolation and last-write-wins conflict resolution - Order CRUD with state machine: DRAFT -> VALIDAT -> FACTURAT Auto-recalculates totals (manopera + materiale) - Vehicle CRUD: list, create, get, update - Seed data: 24 marci, 11 ansamble, 6 tipuri deviz, 5 tipuri motoare, 3 preturi - Alembic migration for all business models - 13 passing tests (auth + sync + orders) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,35 @@
|
||||
from app.db.models.tenant import Tenant
|
||||
from app.db.models.user import User
|
||||
from app.db.models.vehicle import Vehicle
|
||||
from app.db.models.order import Order
|
||||
from app.db.models.order_line import OrderLine
|
||||
from app.db.models.catalog import (
|
||||
CatalogMarca,
|
||||
CatalogModel,
|
||||
CatalogAnsamblu,
|
||||
CatalogNorma,
|
||||
CatalogPret,
|
||||
CatalogTipDeviz,
|
||||
CatalogTipMotor,
|
||||
)
|
||||
from app.db.models.invoice import Invoice
|
||||
from app.db.models.appointment import Appointment
|
||||
from app.db.models.mecanic import Mecanic
|
||||
|
||||
__all__ = ["Tenant", "User"]
|
||||
__all__ = [
|
||||
"Tenant",
|
||||
"User",
|
||||
"Vehicle",
|
||||
"Order",
|
||||
"OrderLine",
|
||||
"CatalogMarca",
|
||||
"CatalogModel",
|
||||
"CatalogAnsamblu",
|
||||
"CatalogNorma",
|
||||
"CatalogPret",
|
||||
"CatalogTipDeviz",
|
||||
"CatalogTipMotor",
|
||||
"Invoice",
|
||||
"Appointment",
|
||||
"Mecanic",
|
||||
]
|
||||
|
||||
11
backend/app/db/models/appointment.py
Normal file
11
backend/app/db/models/appointment.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from sqlalchemy import String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
|
||||
|
||||
class Appointment(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "appointments"
|
||||
vehicle_id: Mapped[str] = mapped_column(String(36))
|
||||
data: Mapped[str] = mapped_column(Text)
|
||||
descriere: Mapped[str | None] = mapped_column(Text)
|
||||
44
backend/app/db/models/catalog.py
Normal file
44
backend/app/db/models/catalog.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from sqlalchemy import Float, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
|
||||
|
||||
class CatalogMarca(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "catalog_marci"
|
||||
nume: Mapped[str] = mapped_column(String(100))
|
||||
|
||||
|
||||
class CatalogModel(Base, UUIDMixin, TimestampMixin):
|
||||
__tablename__ = "catalog_modele"
|
||||
marca_id: Mapped[str] = mapped_column(String(36), index=True)
|
||||
nume: Mapped[str] = mapped_column(String(100))
|
||||
|
||||
|
||||
class CatalogAnsamblu(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "catalog_ansamble"
|
||||
nume: Mapped[str] = mapped_column(String(100))
|
||||
|
||||
|
||||
class CatalogNorma(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "catalog_norme"
|
||||
ansamblu_id: Mapped[str] = mapped_column(String(36), index=True)
|
||||
descriere: Mapped[str] = mapped_column(Text)
|
||||
ore: Mapped[float] = mapped_column(Float, default=0)
|
||||
|
||||
|
||||
class CatalogPret(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "catalog_preturi"
|
||||
denumire: Mapped[str] = mapped_column(String(200))
|
||||
pret: Mapped[float] = mapped_column(Float, default=0)
|
||||
um: Mapped[str] = mapped_column(String(10))
|
||||
|
||||
|
||||
class CatalogTipDeviz(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "catalog_tipuri_deviz"
|
||||
nume: Mapped[str] = mapped_column(String(100))
|
||||
|
||||
|
||||
class CatalogTipMotor(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "catalog_tipuri_motoare"
|
||||
nume: Mapped[str] = mapped_column(String(50))
|
||||
13
backend/app/db/models/invoice.py
Normal file
13
backend/app/db/models/invoice.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from sqlalchemy import Float, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
|
||||
|
||||
class Invoice(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "invoices"
|
||||
order_id: Mapped[str] = mapped_column(String(36), index=True)
|
||||
nr_factura: Mapped[str] = mapped_column(String(50))
|
||||
data_factura: Mapped[str | None] = mapped_column(Text)
|
||||
total: Mapped[float] = mapped_column(Float, default=0)
|
||||
status: Mapped[str] = mapped_column(String(20), default="EMISA")
|
||||
10
backend/app/db/models/mecanic.py
Normal file
10
backend/app/db/models/mecanic.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
|
||||
|
||||
class Mecanic(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "mecanici"
|
||||
nume: Mapped[str] = mapped_column(String(200))
|
||||
telefon: Mapped[str | None] = mapped_column(String(20))
|
||||
19
backend/app/db/models/order.py
Normal file
19
backend/app/db/models/order.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from sqlalchemy import Float, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
|
||||
|
||||
class Order(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "orders"
|
||||
vehicle_id: Mapped[str] = mapped_column(String(36))
|
||||
tip_deviz_id: Mapped[str | None] = mapped_column(String(36))
|
||||
status: Mapped[str] = mapped_column(String(20), default="DRAFT")
|
||||
data_comanda: Mapped[str | None] = mapped_column(Text)
|
||||
km_intrare: Mapped[int | None] = mapped_column(Integer)
|
||||
observatii: Mapped[str | None] = mapped_column(Text)
|
||||
mecanic_id: Mapped[str | None] = mapped_column(String(36))
|
||||
total_manopera: Mapped[float] = mapped_column(Float, default=0)
|
||||
total_materiale: Mapped[float] = mapped_column(Float, default=0)
|
||||
total_general: Mapped[float] = mapped_column(Float, default=0)
|
||||
token_client: Mapped[str | None] = mapped_column(String(36))
|
||||
17
backend/app/db/models/order_line.py
Normal file
17
backend/app/db/models/order_line.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Float, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
|
||||
|
||||
class OrderLine(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "order_lines"
|
||||
order_id: Mapped[str] = mapped_column(String(36), index=True)
|
||||
tip: Mapped[str] = mapped_column(String(20)) # manopera | material
|
||||
descriere: Mapped[str] = mapped_column(Text)
|
||||
ore: Mapped[float] = mapped_column(Float, default=0)
|
||||
pret_ora: Mapped[float] = mapped_column(Float, default=0)
|
||||
cantitate: Mapped[float] = mapped_column(Float, default=0)
|
||||
pret_unitar: Mapped[float] = mapped_column(Float, default=0)
|
||||
um: Mapped[str | None] = mapped_column(String(10))
|
||||
total: Mapped[float] = mapped_column(Float, default=0)
|
||||
21
backend/app/db/models/vehicle.py
Normal file
21
backend/app/db/models/vehicle.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy import Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
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))
|
||||
vin: Mapped[str | None] = mapped_column(String(17))
|
||||
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)
|
||||
tip_motor_id: Mapped[str | None] = mapped_column(String(36))
|
||||
capacitate_motor: Mapped[str | None] = mapped_column(String(20))
|
||||
putere_kw: Mapped[str | None] = mapped_column(String(20))
|
||||
client_nume: Mapped[str | None] = mapped_column(String(200))
|
||||
client_telefon: Mapped[str | None] = mapped_column(String(20))
|
||||
client_email: Mapped[str | None] = mapped_column(String(200))
|
||||
client_cui: Mapped[str | None] = mapped_column(String(20))
|
||||
client_adresa: Mapped[str | None] = mapped_column(Text)
|
||||
119
backend/app/db/seed.py
Normal file
119
backend/app/db/seed.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.base import uuid7
|
||||
from app.db.models.catalog import (
|
||||
CatalogAnsamblu,
|
||||
CatalogMarca,
|
||||
CatalogPret,
|
||||
CatalogTipDeviz,
|
||||
CatalogTipMotor,
|
||||
)
|
||||
|
||||
MARCI = [
|
||||
"Audi",
|
||||
"BMW",
|
||||
"Citroen",
|
||||
"Dacia",
|
||||
"Fiat",
|
||||
"Ford",
|
||||
"Honda",
|
||||
"Hyundai",
|
||||
"Kia",
|
||||
"Mazda",
|
||||
"Mercedes-Benz",
|
||||
"Mitsubishi",
|
||||
"Nissan",
|
||||
"Opel",
|
||||
"Peugeot",
|
||||
"Renault",
|
||||
"Seat",
|
||||
"Skoda",
|
||||
"Suzuki",
|
||||
"Toyota",
|
||||
"Volkswagen",
|
||||
"Volvo",
|
||||
"Alfa Romeo",
|
||||
"Jeep",
|
||||
]
|
||||
|
||||
ANSAMBLE = [
|
||||
"Motor",
|
||||
"Cutie de viteze",
|
||||
"Frane",
|
||||
"Directie",
|
||||
"Suspensie",
|
||||
"Climatizare",
|
||||
"Electrica",
|
||||
"Caroserie",
|
||||
"Esapament",
|
||||
"Transmisie",
|
||||
"Revizie",
|
||||
]
|
||||
|
||||
TIPURI_DEVIZ = [
|
||||
"Deviz reparatie",
|
||||
"Deviz revizie",
|
||||
"Deviz diagnosticare",
|
||||
"Deviz estimativ",
|
||||
"Deviz vulcanizare",
|
||||
"Deviz ITP",
|
||||
]
|
||||
|
||||
TIPURI_MOTOARE = ["Benzina", "Diesel", "Hibrid", "Electric", "GPL"]
|
||||
|
||||
PRETURI = [
|
||||
{"denumire": "Manopera standard", "pret": 150.0, "um": "ora"},
|
||||
{"denumire": "Revizie ulei + filtru", "pret": 250.0, "um": "buc"},
|
||||
{"denumire": "Diagnosticare", "pret": 100.0, "um": "buc"},
|
||||
]
|
||||
|
||||
|
||||
async def seed_catalog(db: AsyncSession, tenant_id: str) -> dict:
|
||||
now = datetime.now(UTC).isoformat()
|
||||
counts = {}
|
||||
|
||||
# Marci
|
||||
for name in MARCI:
|
||||
db.add(
|
||||
CatalogMarca(id=uuid7(), tenant_id=tenant_id, nume=name)
|
||||
)
|
||||
counts["marci"] = len(MARCI)
|
||||
|
||||
# Ansamble
|
||||
for name in ANSAMBLE:
|
||||
db.add(
|
||||
CatalogAnsamblu(id=uuid7(), tenant_id=tenant_id, nume=name)
|
||||
)
|
||||
counts["ansamble"] = len(ANSAMBLE)
|
||||
|
||||
# Tipuri deviz
|
||||
for name in TIPURI_DEVIZ:
|
||||
db.add(
|
||||
CatalogTipDeviz(id=uuid7(), tenant_id=tenant_id, nume=name)
|
||||
)
|
||||
counts["tipuri_deviz"] = len(TIPURI_DEVIZ)
|
||||
|
||||
# Tipuri motoare
|
||||
for name in TIPURI_MOTOARE:
|
||||
db.add(
|
||||
CatalogTipMotor(id=uuid7(), tenant_id=tenant_id, nume=name)
|
||||
)
|
||||
counts["tipuri_motoare"] = len(TIPURI_MOTOARE)
|
||||
|
||||
# Preturi
|
||||
for p in PRETURI:
|
||||
db.add(
|
||||
CatalogPret(
|
||||
id=uuid7(),
|
||||
tenant_id=tenant_id,
|
||||
denumire=p["denumire"],
|
||||
pret=p["pret"],
|
||||
um=p["um"],
|
||||
)
|
||||
)
|
||||
counts["preturi"] = len(PRETURI)
|
||||
|
||||
await db.commit()
|
||||
return counts
|
||||
Reference in New Issue
Block a user