T025: Add ProjectEquipmentGroup, ProjectEquipment, ProjectCrew models and migration

This commit is contained in:
Agent Zero
2026-05-31 21:35:09 +00:00
parent 6c435b28b4
commit 489cb68641
5 changed files with 225 additions and 2 deletions
+1 -1
View File
@@ -1,7 +1,6 @@
# Todo # Todo
## Open ## Open
- [ ] T024: Project function group and function editor UI
- [ ] T025: Create database models for ProjectEquipment and ProjectCrew - [ ] T025: Create database models for ProjectEquipment and ProjectCrew
- [ ] T026: Implement project equipment group and items API - [ ] T026: Implement project equipment group and items API
- [ ] T027: Project equipment management UI - [ ] T027: Project equipment management UI
@@ -42,3 +41,4 @@
- T021: Create database models for Project hierarchy - T021: Create database models for Project hierarchy
- T022: Implement Project CRUD API - T022: Implement Project CRUD API
- T023: Project list and detail UI - T023: Project list and detail UI
- T024: Project function group and function editor UI (function_type, start_date, end_date)
@@ -0,0 +1,65 @@
"""add_project_equipment_and_crew
Revision ID: b183c967088f
Revises: cfe638817921
Create Date: 2026-05-31 21:32:46.068246
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b183c967088f'
down_revision: Union[str, None] = 'cfe638817921'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('project_equipment_groups',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('project_id', sa.String(length=36), nullable=False),
sa.Column('sort_order', sa.Integer(), nullable=False),
sa.Column('start_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('end_date', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('project_equipment',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('project_equipment_group_id', sa.String(length=36), nullable=False),
sa.Column('equipment_id', sa.String(length=36), nullable=True),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('quantity', sa.Float(), nullable=False),
sa.Column('price', sa.Float(), nullable=False),
sa.Column('discount', sa.Float(), nullable=False),
sa.Column('sort_order', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['equipment_id'], ['equipment.id'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['project_equipment_group_id'], ['project_equipment_groups.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('project_crew',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('project_function_id', sa.String(length=36), nullable=False),
sa.Column('crew_id', sa.String(length=36), nullable=False),
sa.Column('start_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('end_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('notes', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['crew_id'], ['crew.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['project_function_id'], ['project_functions.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('project_crew')
op.drop_table('project_equipment')
op.drop_table('project_equipment_groups')
# ### end Alembic commands ###
+12 -1
View File
@@ -10,7 +10,15 @@ from app.models.stock_location import StockLocation
from app.models.equipment_group import EquipmentGroup from app.models.equipment_group import EquipmentGroup
from app.models.crew import Crew, CrewAvailability from app.models.crew import Crew, CrewAvailability
from app.models.vehicle import Vehicle, VehicleAssignment from app.models.vehicle import Vehicle, VehicleAssignment
from app.models.project import Project, SubProject, ProjectFunctionGroup, ProjectFunction from app.models.project import (
Project,
SubProject,
ProjectFunctionGroup,
ProjectFunction,
ProjectEquipmentGroup,
ProjectEquipment,
ProjectCrew,
)
__all__ = [ __all__ = [
"Account", "Account",
@@ -29,4 +37,7 @@ __all__ = [
"SubProject", "SubProject",
"ProjectFunctionGroup", "ProjectFunctionGroup",
"ProjectFunction", "ProjectFunction",
"ProjectEquipmentGroup",
"ProjectEquipment",
"ProjectCrew",
] ]
+96
View File
@@ -60,6 +60,9 @@ class Project(Base):
function_groups: Mapped[list["ProjectFunctionGroup"]] = relationship( function_groups: Mapped[list["ProjectFunctionGroup"]] = relationship(
"ProjectFunctionGroup", back_populates="project", cascade="all, delete-orphan" "ProjectFunctionGroup", back_populates="project", cascade="all, delete-orphan"
) )
equipment_groups: Mapped[list["ProjectEquipmentGroup"]] = relationship(
"ProjectEquipmentGroup", back_populates="project", cascade="all, delete-orphan"
)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Project {self.name} ({self.status})>" return f"<Project {self.name} ({self.status})>"
@@ -147,6 +150,99 @@ class ProjectFunction(Base):
function_group: Mapped["ProjectFunctionGroup"] = relationship( function_group: Mapped["ProjectFunctionGroup"] = relationship(
"ProjectFunctionGroup", back_populates="functions" "ProjectFunctionGroup", back_populates="functions"
) )
crew_assignments: Mapped[list["ProjectCrew"]] = relationship(
"ProjectCrew", back_populates="project_function", cascade="all, delete-orphan"
)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<ProjectFunction {self.name} x{self.quantity}>" return f"<ProjectFunction {self.name} x{self.quantity}>"
class ProjectEquipmentGroup(Base):
"""Groups project equipment under a project."""
__tablename__ = "project_equipment_groups"
id: Mapped[str] = mapped_column(
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
project_id: Mapped[str] = mapped_column(
String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
start_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
end_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# Relationships
project: Mapped["Project"] = relationship("Project", back_populates="equipment_groups")
items: Mapped[list["ProjectEquipment"]] = relationship(
"ProjectEquipment", back_populates="group", cascade="all, delete-orphan"
)
def __repr__(self) -> str:
return f"<ProjectEquipmentGroup {self.name}>"
class ProjectEquipment(Base):
"""Single equipment item within a project equipment group."""
__tablename__ = "project_equipment"
id: Mapped[str] = mapped_column(
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
)
project_equipment_group_id: Mapped[str] = mapped_column(
String(36),
ForeignKey("project_equipment_groups.id", ondelete="CASCADE"),
nullable=False,
)
equipment_id: Mapped[str | None] = mapped_column(
String(36), ForeignKey("equipment.id", ondelete="SET NULL"), nullable=True
)
name: Mapped[str] = mapped_column(String(255), nullable=False)
quantity: Mapped[float] = mapped_column(Float, nullable=False, default=1.0)
price: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
discount: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
# Relationships
group: Mapped["ProjectEquipmentGroup"] = relationship(
"ProjectEquipmentGroup", back_populates="items"
)
equipment: Mapped[Optional["Equipment"]] = relationship(
"Equipment", foreign_keys=[equipment_id]
)
def __repr__(self) -> str:
return f"<ProjectEquipment {self.name} x{self.quantity}>"
class ProjectCrew(Base):
"""Crew assignment to a project function."""
__tablename__ = "project_crew"
id: Mapped[str] = mapped_column(
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
)
project_function_id: Mapped[str] = mapped_column(
String(36),
ForeignKey("project_functions.id", ondelete="CASCADE"),
nullable=False,
)
crew_id: Mapped[str] = mapped_column(
String(36), ForeignKey("crew.id", ondelete="CASCADE"), nullable=False
)
start_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
end_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
# Relationships
project_function: Mapped["ProjectFunction"] = relationship(
"ProjectFunction", back_populates="crew_assignments"
)
crew_member: Mapped["Crew"] = relationship("Crew")
def __repr__(self) -> str:
return f"<ProjectCrew {self.crew_id} -> {self.project_function_id}>"
+51
View File
@@ -0,0 +1,51 @@
INFO: Started server process [405788]
INFO: Waiting for application startup.
2026-05-31 21:25:58,679 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2026-05-31 21:25:58,679 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("accounts")
2026-05-31 21:25:58,679 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,681 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("users")
2026-05-31 21:25:58,681 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,681 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("roles")
2026-05-31 21:25:58,681 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,682 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("contact_tags")
2026-05-31 21:25:58,682 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,682 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("contacts")
2026-05-31 21:25:58,682 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,683 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("tags")
2026-05-31 21:25:58,683 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,684 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("equipment")
2026-05-31 21:25:58,684 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,684 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("stock_locations")
2026-05-31 21:25:58,684 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,685 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("equipment_group_items")
2026-05-31 21:25:58,685 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,685 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("equipment_groups")
2026-05-31 21:25:58,685 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,686 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("crew")
2026-05-31 21:25:58,686 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,687 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("crew_availabilities")
2026-05-31 21:25:58,687 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,688 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("vehicles")
2026-05-31 21:25:58,688 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,688 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("vehicle_assignments")
2026-05-31 21:25:58,689 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,689 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("projects")
2026-05-31 21:25:58,689 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,689 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("sub_projects")
2026-05-31 21:25:58,690 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,690 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("project_function_groups")
2026-05-31 21:25:58,690 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,690 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("project_functions")
2026-05-31 21:25:58,691 INFO sqlalchemy.engine.Engine [raw sql] ()
2026-05-31 21:25:58,691 INFO sqlalchemy.engine.Engine COMMIT
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
2026-05-31 21:26:00,791 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2026-05-31 21:26:00,792 INFO sqlalchemy.engine.Engine SELECT 1
2026-05-31 21:26:00,792 INFO sqlalchemy.engine.Engine [generated in 0.00026s] ()
2026-05-31 21:26:00,794 INFO sqlalchemy.engine.Engine COMMIT
INFO: 127.0.0.1:51750 - "GET /api/v1/health HTTP/1.1" 200 OK
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [405788]