feat(repairs): add repair estimates

This commit is contained in:
Schubert Ferenc 2026-07-05 00:57:02 +02:00
parent 6e7e75f864
commit 4436fe5f73
25 changed files with 1802 additions and 9 deletions

View file

@ -0,0 +1,125 @@
from datetime import UTC, datetime
from sqlalchemy import func, select
from sqlalchemy.orm import Session, selectinload
from app.models.repair_estimate import RepairEstimate, RepairEstimateEvent, RepairEstimateItem
class RepairEstimateRepository:
@staticmethod
def list_by_repair(db: Session, repair_id: int) -> list[RepairEstimate]:
return list(
db.scalars(
select(RepairEstimate)
.options(selectinload(RepairEstimate.items), selectinload(RepairEstimate.events))
.where(RepairEstimate.repair_id == repair_id)
.order_by(RepairEstimate.created_at.desc(), RepairEstimate.id.desc())
)
)
@staticmethod
def get(db: Session, *, repair_id: int, estimate_id: int) -> RepairEstimate | None:
return db.scalar(
select(RepairEstimate)
.options(selectinload(RepairEstimate.items), selectinload(RepairEstimate.events))
.where(RepairEstimate.repair_id == repair_id)
.where(RepairEstimate.id == estimate_id)
)
@staticmethod
def get_latest_public(db: Session, repair_id: int) -> RepairEstimate | None:
return db.scalar(
select(RepairEstimate)
.options(selectinload(RepairEstimate.items))
.where(RepairEstimate.repair_id == repair_id)
.where(RepairEstimate.status.in_(["sent", "approved", "declined"]))
.order_by(RepairEstimate.sent_at.desc().nullslast(), RepairEstimate.created_at.desc(), RepairEstimate.id.desc())
.limit(1)
)
@staticmethod
def get_next_number(db: Session, year: int) -> str:
prefix = f"KV{year}-"
latest = db.scalar(
select(RepairEstimate.estimate_number)
.where(RepairEstimate.estimate_number.like(f"{prefix}%"))
.order_by(RepairEstimate.estimate_number.desc())
.limit(1)
)
next_number = 1
if latest:
next_number = int(latest.split("-")[-1]) + 1
return f"{prefix}{next_number:06d}"
@staticmethod
def save(db: Session, estimate: RepairEstimate) -> RepairEstimate:
db.add(estimate)
db.commit()
db.refresh(estimate)
return RepairEstimateRepository.get(db, repair_id=estimate.repair_id, estimate_id=estimate.id) or estimate
@staticmethod
def replace_items(db: Session, estimate: RepairEstimate, items: list[RepairEstimateItem]) -> None:
estimate.items.clear()
db.flush()
for item in items:
estimate.items.append(item)
@staticmethod
def delete(db: Session, estimate: RepairEstimate) -> None:
db.delete(estimate)
db.commit()
@staticmethod
def add_event(
db: Session,
*,
estimate_id: int,
event_type: str,
actor_type: str,
actor_user_id: int | None = None,
note: str | None = None,
commit: bool = True,
) -> RepairEstimateEvent:
event = RepairEstimateEvent(
estimate_id=estimate_id,
event_type=event_type,
actor_type=actor_type,
actor_user_id=actor_user_id,
note=note,
)
db.add(event)
if commit:
db.commit()
db.refresh(event)
return event
@staticmethod
def mark_sent(db: Session, estimate: RepairEstimate) -> RepairEstimate:
estimate.status = "sent"
estimate.sent_at = datetime.now(UTC)
db.commit()
db.refresh(estimate)
return RepairEstimateRepository.get(db, repair_id=estimate.repair_id, estimate_id=estimate.id) or estimate
@staticmethod
def count_open(db: Session) -> int:
return db.scalar(select(func.count(RepairEstimate.id)).where(RepairEstimate.status.in_(["draft", "sent"]))) or 0
@staticmethod
def count_waiting(db: Session) -> int:
return db.scalar(select(func.count(RepairEstimate.id)).where(RepairEstimate.status == "sent")) or 0
@staticmethod
def count_approved_today(db: Session) -> int:
today = datetime.now(UTC).date()
return db.scalar(
select(func.count(RepairEstimate.id))
.where(RepairEstimate.status == "approved")
.where(func.date(RepairEstimate.approved_at) == today)
) or 0
@staticmethod
def count_declined(db: Session) -> int:
return db.scalar(select(func.count(RepairEstimate.id)).where(RepairEstimate.status == "declined")) or 0