fix(backend): sync push error handling + validation
- apply_push now uses PRAGMA table_info() to get valid column names per
table and filters incoming data to only known columns, preventing
"no such column" SQLite errors from frontend-only fields like oracle_id
- Wrap each operation in try/except so one bad op doesn't abort the
whole batch; errors are returned in the conflicts list instead of 500
- Add server_default to all NOT NULL float/int columns so raw SQL
INSERT OR REPLACE without those fields still succeeds
- Align DB models with frontend schema.js:
- orders: add nr_comanda, client_nume, client_telefon, nr_auto,
marca_denumire, model_denumire, created_by
- order_lines: add norma_id, mecanic_id, ordine
- vehicles: add serie_sasiu, client_cod_fiscal (keep vin, client_cui
for REST API compat)
- catalog_*: rename nume → denumire to match frontend schema
- catalog_norme: align fields (cod, denumire, ore_normate)
- invoices: add serie_factura, modalitate_plata, client_cod_fiscal,
nr_auto, total_fara_tva, tva, total_general; keep total for compat
- appointments: add client_nume, client_telefon, data_ora, durata_minute,
status, order_id
- mecanici: add user_id, prenume, activ
- Fix seed.py to use denumire= instead of nome= after catalog rename
- Add 3 new sync push tests covering order insert with frontend fields,
unknown column filtering, and order_line with norma_id/mecanic_id/ordine
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import String, Text
|
||||
from sqlalchemy import Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
@@ -6,6 +6,11 @@ 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)
|
||||
vehicle_id: Mapped[str | None] = mapped_column(String(36))
|
||||
client_nume: Mapped[str | None] = mapped_column(String(200))
|
||||
client_telefon: Mapped[str | None] = mapped_column(String(20))
|
||||
data_ora: Mapped[str | None] = mapped_column(Text)
|
||||
durata_minute: Mapped[int] = mapped_column(Integer, default=60, server_default="60")
|
||||
observatii: Mapped[str | None] = mapped_column(Text)
|
||||
status: Mapped[str] = mapped_column(String(20), default="PROGRAMAT", server_default="PROGRAMAT")
|
||||
order_id: Mapped[str | None] = mapped_column(String(36))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Float, String, Text
|
||||
from sqlalchemy import Float, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
@@ -6,39 +6,41 @@ 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))
|
||||
denumire: Mapped[str] = mapped_column(String(100))
|
||||
activ: Mapped[int] = mapped_column(Integer, default=1, server_default="1")
|
||||
|
||||
|
||||
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))
|
||||
denumire: Mapped[str] = mapped_column(String(100))
|
||||
|
||||
|
||||
class CatalogAnsamblu(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "catalog_ansamble"
|
||||
nume: Mapped[str] = mapped_column(String(100))
|
||||
denumire: 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)
|
||||
cod: Mapped[str | None] = mapped_column(String(50))
|
||||
denumire: Mapped[str] = mapped_column(Text)
|
||||
ore_normate: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
ansamblu_id: Mapped[str | None] = mapped_column(String(36), index=True)
|
||||
|
||||
|
||||
class CatalogPret(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "catalog_preturi"
|
||||
denumire: Mapped[str] = mapped_column(String(200))
|
||||
pret: Mapped[float] = mapped_column(Float, default=0)
|
||||
pret: Mapped[float] = mapped_column(Float, default=0, server_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))
|
||||
denumire: Mapped[str] = mapped_column(String(100))
|
||||
|
||||
|
||||
class CatalogTipMotor(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "catalog_tipuri_motoare"
|
||||
nume: Mapped[str] = mapped_column(String(50))
|
||||
denumire: Mapped[str] = mapped_column(String(50))
|
||||
|
||||
@@ -6,8 +6,17 @@ 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))
|
||||
order_id: Mapped[str | None] = mapped_column(String(36), index=True)
|
||||
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)
|
||||
total: Mapped[float] = mapped_column(Float, default=0)
|
||||
status: Mapped[str] = mapped_column(String(20), default="EMISA")
|
||||
modalitate_plata: Mapped[str | None] = mapped_column(String(50))
|
||||
client_nume: Mapped[str | None] = mapped_column(String(200))
|
||||
client_cod_fiscal: Mapped[str | None] = mapped_column(String(20))
|
||||
nr_auto: Mapped[str | None] = mapped_column(String(20))
|
||||
total_fara_tva: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
tva: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
total_general: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
# Legacy field kept for REST API service compatibility
|
||||
total: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
status: Mapped[str] = mapped_column(String(20), default="EMISA", server_default="EMISA")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
@@ -6,5 +6,7 @@ from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
|
||||
class Mecanic(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
__tablename__ = "mecanici"
|
||||
user_id: Mapped[str | None] = mapped_column(String(36))
|
||||
nume: Mapped[str] = mapped_column(String(200))
|
||||
telefon: Mapped[str | None] = mapped_column(String(20))
|
||||
prenume: Mapped[str | None] = mapped_column(String(200))
|
||||
activ: Mapped[int] = mapped_column(Integer, default=1, server_default="1")
|
||||
|
||||
@@ -6,15 +6,26 @@ 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))
|
||||
nr_comanda: Mapped[str | None] = mapped_column(String(50))
|
||||
vehicle_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")
|
||||
status: Mapped[str] = mapped_column(String(20), default="DRAFT", server_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)
|
||||
# Denormalized client/vehicle info for quick display
|
||||
client_nume: Mapped[str | None] = mapped_column(String(200))
|
||||
client_telefon: Mapped[str | None] = mapped_column(String(20))
|
||||
nr_auto: Mapped[str | None] = mapped_column(String(20))
|
||||
marca_denumire: Mapped[str | None] = mapped_column(String(100))
|
||||
model_denumire: Mapped[str | None] = mapped_column(String(100))
|
||||
# Totals — server_default ensures raw SQL INSERT without these fields still works
|
||||
total_manopera: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
total_materiale: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
total_general: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
# Client portal
|
||||
token_client: Mapped[str | None] = mapped_column(String(36))
|
||||
status_client: Mapped[str | None] = mapped_column(String(20))
|
||||
# Audit
|
||||
created_by: Mapped[str | None] = mapped_column(String(36))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Float, String, Text
|
||||
from sqlalchemy import Float, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
|
||||
@@ -9,9 +9,12 @@ class OrderLine(Base, UUIDMixin, TenantMixin, TimestampMixin):
|
||||
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)
|
||||
norma_id: Mapped[str | None] = mapped_column(String(36))
|
||||
ore: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
pret_ora: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
cantitate: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
pret_unitar: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
um: Mapped[str | None] = mapped_column(String(10))
|
||||
total: Mapped[float] = mapped_column(Float, default=0)
|
||||
total: Mapped[float] = mapped_column(Float, default=0, server_default="0")
|
||||
mecanic_id: Mapped[str | None] = mapped_column(String(36))
|
||||
ordine: Mapped[int | None] = mapped_column(Integer)
|
||||
|
||||
@@ -7,15 +7,19 @@ 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)
|
||||
# VIN / serie sasiu (vin kept for REST API compat, serie_sasiu for frontend sync)
|
||||
vin: Mapped[str | None] = mapped_column(String(17))
|
||||
serie_sasiu: Mapped[str | None] = mapped_column(String(50))
|
||||
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_cod_fiscal used by frontend; client_cui kept for REST API compat
|
||||
client_cod_fiscal: Mapped[str | None] = mapped_column(String(20))
|
||||
client_cui: Mapped[str | None] = mapped_column(String(20))
|
||||
client_adresa: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
@@ -77,28 +77,28 @@ async def seed_catalog(db: AsyncSession, tenant_id: str) -> dict:
|
||||
# Marci
|
||||
for name in MARCI:
|
||||
db.add(
|
||||
CatalogMarca(id=uuid7(), tenant_id=tenant_id, nume=name)
|
||||
CatalogMarca(id=uuid7(), tenant_id=tenant_id, denumire=name)
|
||||
)
|
||||
counts["marci"] = len(MARCI)
|
||||
|
||||
# Ansamble
|
||||
for name in ANSAMBLE:
|
||||
db.add(
|
||||
CatalogAnsamblu(id=uuid7(), tenant_id=tenant_id, nume=name)
|
||||
CatalogAnsamblu(id=uuid7(), tenant_id=tenant_id, denumire=name)
|
||||
)
|
||||
counts["ansamble"] = len(ANSAMBLE)
|
||||
|
||||
# Tipuri deviz
|
||||
for name in TIPURI_DEVIZ:
|
||||
db.add(
|
||||
CatalogTipDeviz(id=uuid7(), tenant_id=tenant_id, nume=name)
|
||||
CatalogTipDeviz(id=uuid7(), tenant_id=tenant_id, denumire=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)
|
||||
CatalogTipMotor(id=uuid7(), tenant_id=tenant_id, denumire=name)
|
||||
)
|
||||
counts["tipuri_motoare"] = len(TIPURI_MOTOARE)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user