chore(quality): apply ruff autofixes and formatting (29 fixes, 41 files reformatted)

This commit is contained in:
Agent Zero
2026-06-10 21:31:41 +00:00
parent 054d4041e6
commit 7c12a96cdc
43 changed files with 1096 additions and 524 deletions
@@ -5,6 +5,7 @@ Revises: da13d44bd483
Create Date: 2026-05-31 11:50:59.485426 Create Date: 2026-05-31 11:50:59.485426
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op from alembic import op
@@ -12,47 +13,69 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '14f212e85ab6' revision: str = "14f212e85ab6"
down_revision: Union[str, None] = 'da13d44bd483' down_revision: Union[str, None] = "da13d44bd483"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('crew', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "crew",
sa.Column('account_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('first_name', sa.String(length=100), nullable=False), sa.Column("account_id", sa.String(length=36), nullable=False),
sa.Column('last_name', sa.String(length=100), nullable=False), sa.Column("first_name", sa.String(length=100), nullable=False),
sa.Column('email', sa.String(length=255), nullable=True), sa.Column("last_name", sa.String(length=100), nullable=False),
sa.Column('phone', sa.String(length=50), nullable=True), sa.Column("email", sa.String(length=255), nullable=True),
sa.Column('role_title', sa.String(length=100), nullable=True), sa.Column("phone", sa.String(length=50), nullable=True),
sa.Column('hourly_rate', sa.Float(), nullable=True), sa.Column("role_title", sa.String(length=100), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column("hourly_rate", sa.Float(), nullable=True),
sa.Column('notes', sa.Text(), nullable=True), sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("notes", sa.Text(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column(
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), "created_at",
sa.PrimaryKeyConstraint('id') sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('crew_availabilities', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "crew_availabilities",
sa.Column('crew_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('start_date', sa.DateTime(timezone=True), nullable=False), sa.Column("crew_id", sa.String(length=36), nullable=False),
sa.Column('end_date', sa.DateTime(timezone=True), nullable=False), sa.Column("start_date", sa.DateTime(timezone=True), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False), sa.Column("end_date", sa.DateTime(timezone=True), nullable=False),
sa.Column('notes', sa.Text(), nullable=True), sa.Column("status", sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("notes", sa.Text(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column(
sa.ForeignKeyConstraint(['crew_id'], ['crew.id'], ondelete='CASCADE'), "created_at",
sa.PrimaryKeyConstraint('id') sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(["crew_id"], ["crew.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('crew_availabilities') op.drop_table("crew_availabilities")
op.drop_table('crew') op.drop_table("crew")
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -5,6 +5,7 @@ Revises: 14f212e85ab6
Create Date: 2026-05-31 12:05:27.261340 Create Date: 2026-05-31 12:05:27.261340
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op from alembic import op
@@ -12,53 +13,75 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '32b02c118683' revision: str = "32b02c118683"
down_revision: Union[str, None] = '14f212e85ab6' down_revision: Union[str, None] = "14f212e85ab6"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('vehicles', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "vehicles",
sa.Column('account_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False), sa.Column("account_id", sa.String(length=36), nullable=False),
sa.Column('license_plate', sa.String(length=50), nullable=True), sa.Column("name", sa.String(length=255), nullable=False),
sa.Column('brand', sa.String(length=100), nullable=True), sa.Column("license_plate", sa.String(length=50), nullable=True),
sa.Column('model', sa.String(length=100), nullable=True), sa.Column("brand", sa.String(length=100), nullable=True),
sa.Column('year', sa.Integer(), nullable=True), sa.Column("model", sa.String(length=100), nullable=True),
sa.Column('color', sa.String(length=50), nullable=True), sa.Column("year", sa.Integer(), nullable=True),
sa.Column('vehicle_type', sa.String(length=50), nullable=True), sa.Column("color", sa.String(length=50), nullable=True),
sa.Column('payload_capacity_kg', sa.Float(), nullable=True), sa.Column("vehicle_type", sa.String(length=50), nullable=True),
sa.Column('load_volume_m3', sa.Float(), nullable=True), sa.Column("payload_capacity_kg", sa.Float(), nullable=True),
sa.Column('fuel_type', sa.String(length=30), nullable=True), sa.Column("load_volume_m3", sa.Float(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column("fuel_type", sa.String(length=30), nullable=True),
sa.Column('notes', sa.Text(), nullable=True), sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("notes", sa.Text(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column(
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), "created_at",
sa.PrimaryKeyConstraint('id'), sa.DateTime(timezone=True),
sa.UniqueConstraint('license_plate') server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("license_plate"),
) )
op.create_table('vehicle_assignments', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "vehicle_assignments",
sa.Column('vehicle_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('project_id', sa.String(length=36), nullable=True), sa.Column("vehicle_id", sa.String(length=36), nullable=False),
sa.Column('start_date', sa.DateTime(timezone=True), nullable=False), sa.Column("project_id", sa.String(length=36), nullable=True),
sa.Column('end_date', sa.DateTime(timezone=True), nullable=False), sa.Column("start_date", sa.DateTime(timezone=True), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False), sa.Column("end_date", sa.DateTime(timezone=True), nullable=False),
sa.Column('notes', sa.Text(), nullable=True), sa.Column("status", sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("notes", sa.Text(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column(
sa.ForeignKeyConstraint(['vehicle_id'], ['vehicles.id'], ondelete='CASCADE'), "created_at",
sa.PrimaryKeyConstraint('id') sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(["vehicle_id"], ["vehicles.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('vehicle_assignments') op.drop_table("vehicle_assignments")
op.drop_table('vehicles') op.drop_table("vehicles")
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -5,6 +5,7 @@ Revises: f90ee7806a25
Create Date: 2026-05-31 10:35:56.717393 Create Date: 2026-05-31 10:35:56.717393
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op from alembic import op
@@ -12,33 +13,36 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '714cc11a59c4' revision: str = "714cc11a59c4"
down_revision: Union[str, None] = 'f90ee7806a25' down_revision: Union[str, None] = "f90ee7806a25"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('roles', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "roles",
sa.Column('account_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False), sa.Column("account_id", sa.String(length=36), nullable=False),
sa.Column('description', sa.String(length=500), nullable=True), sa.Column("name", sa.String(length=100), nullable=False),
sa.Column('permissions', sa.JSON(), nullable=False), sa.Column("description", sa.String(length=500), nullable=True),
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), sa.Column("permissions", sa.JSON(), nullable=False),
sa.PrimaryKeyConstraint('id') sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
) )
with op.batch_alter_table('users', schema=None) as batch_op: with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.create_foreign_key('fk_users_role_id_roles', 'roles', ['role_id'], ['id'], ondelete='SET NULL') batch_op.create_foreign_key(
"fk_users_role_id_roles", "roles", ["role_id"], ["id"], ondelete="SET NULL"
)
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op: with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.drop_constraint(None, type_='foreignkey') batch_op.drop_constraint(None, type_="foreignkey")
op.drop_table('roles') op.drop_table("roles")
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -5,6 +5,7 @@ Revises: 714cc11a59c4
Create Date: 2026-05-31 10:58:21.063736 Create Date: 2026-05-31 10:58:21.063736
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op from alembic import op
@@ -12,64 +13,82 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = 'a5b1e93690ff' revision: str = "a5b1e93690ff"
down_revision: Union[str, None] = '714cc11a59c4' down_revision: Union[str, None] = "714cc11a59c4"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('contacts', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "contacts",
sa.Column('account_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('type', sa.String(length=10), nullable=False), sa.Column("account_id", sa.String(length=36), nullable=False),
sa.Column('company_name', sa.String(length=255), nullable=True), sa.Column("type", sa.String(length=10), nullable=False),
sa.Column('first_name', sa.String(length=100), nullable=True), sa.Column("company_name", sa.String(length=255), nullable=True),
sa.Column('last_name', sa.String(length=100), nullable=True), sa.Column("first_name", sa.String(length=100), nullable=True),
sa.Column('email', sa.String(length=255), nullable=True), sa.Column("last_name", sa.String(length=100), nullable=True),
sa.Column('phone', sa.String(length=50), nullable=True), sa.Column("email", sa.String(length=255), nullable=True),
sa.Column('mobile', sa.String(length=50), nullable=True), sa.Column("phone", sa.String(length=50), nullable=True),
sa.Column('website', sa.String(length=500), nullable=True), sa.Column("mobile", sa.String(length=50), nullable=True),
sa.Column('billing_street', sa.String(length=255), nullable=True), sa.Column("website", sa.String(length=500), nullable=True),
sa.Column('billing_number', sa.String(length=20), nullable=True), sa.Column("billing_street", sa.String(length=255), nullable=True),
sa.Column('billing_postalcode', sa.String(length=20), nullable=True), sa.Column("billing_number", sa.String(length=20), nullable=True),
sa.Column('billing_city', sa.String(length=100), nullable=True), sa.Column("billing_postalcode", sa.String(length=20), nullable=True),
sa.Column('billing_country', sa.String(length=100), nullable=True), sa.Column("billing_city", sa.String(length=100), nullable=True),
sa.Column('shipping_street', sa.String(length=255), nullable=True), sa.Column("billing_country", sa.String(length=100), nullable=True),
sa.Column('shipping_number', sa.String(length=20), nullable=True), sa.Column("shipping_street", sa.String(length=255), nullable=True),
sa.Column('shipping_postalcode', sa.String(length=20), nullable=True), sa.Column("shipping_number", sa.String(length=20), nullable=True),
sa.Column('shipping_city', sa.String(length=100), nullable=True), sa.Column("shipping_postalcode", sa.String(length=20), nullable=True),
sa.Column('shipping_country', sa.String(length=100), nullable=True), sa.Column("shipping_city", sa.String(length=100), nullable=True),
sa.Column('tax_number', sa.String(length=50), nullable=True), sa.Column("shipping_country", sa.String(length=100), nullable=True),
sa.Column('note', sa.String(), nullable=True), sa.Column("tax_number", sa.String(length=50), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("note", sa.String(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column(
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), "created_at",
sa.PrimaryKeyConstraint('id') sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('tags', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "tags",
sa.Column('account_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False), sa.Column("account_id", sa.String(length=36), nullable=False),
sa.Column('color', sa.String(length=7), nullable=True), sa.Column("name", sa.String(length=100), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("color", sa.String(length=7), nullable=True),
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), sa.Column(
sa.PrimaryKeyConstraint('id') "created_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('contact_tags', op.create_table(
sa.Column('contact_id', sa.String(length=36), nullable=False), "contact_tags",
sa.Column('tag_id', sa.String(length=36), nullable=False), sa.Column("contact_id", sa.String(length=36), nullable=False),
sa.ForeignKeyConstraint(['contact_id'], ['contacts.id'], ondelete='CASCADE'), sa.Column("tag_id", sa.String(length=36), nullable=False),
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(["contact_id"], ["contacts.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint('contact_id', 'tag_id') sa.ForeignKeyConstraint(["tag_id"], ["tags.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("contact_id", "tag_id"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('contact_tags') op.drop_table("contact_tags")
op.drop_table('tags') op.drop_table("tags")
op.drop_table('contacts') op.drop_table("contacts")
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -5,6 +5,7 @@ Revises: cfe638817921
Create Date: 2026-05-31 21:32:46.068246 Create Date: 2026-05-31 21:32:46.068246
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op from alembic import op
@@ -12,54 +13,65 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = 'b183c967088f' revision: str = "b183c967088f"
down_revision: Union[str, None] = 'cfe638817921' down_revision: Union[str, None] = "cfe638817921"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('project_equipment_groups', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "project_equipment_groups",
sa.Column('name', sa.String(length=255), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column("name", sa.String(length=255), nullable=False),
sa.Column('sort_order', sa.Integer(), nullable=False), sa.Column("project_id", sa.String(length=36), nullable=False),
sa.Column('start_date', sa.DateTime(timezone=True), nullable=True), sa.Column("sort_order", sa.Integer(), nullable=False),
sa.Column('end_date', sa.DateTime(timezone=True), nullable=True), sa.Column("start_date", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.Column("end_date", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id') sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('project_equipment', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "project_equipment",
sa.Column('project_equipment_group_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('equipment_id', sa.String(length=36), nullable=True), sa.Column("project_equipment_group_id", sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False), sa.Column("equipment_id", sa.String(length=36), nullable=True),
sa.Column('quantity', sa.Float(), nullable=False), sa.Column("name", sa.String(length=255), nullable=False),
sa.Column('price', sa.Float(), nullable=False), sa.Column("quantity", sa.Float(), nullable=False),
sa.Column('discount', sa.Float(), nullable=False), sa.Column("price", sa.Float(), nullable=False),
sa.Column('sort_order', sa.Integer(), nullable=False), sa.Column("discount", sa.Float(), nullable=False),
sa.ForeignKeyConstraint(['equipment_id'], ['equipment.id'], ondelete='SET NULL'), sa.Column("sort_order", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['project_equipment_group_id'], ['project_equipment_groups.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(
sa.PrimaryKeyConstraint('id') ["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', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "project_crew",
sa.Column('project_function_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('crew_id', sa.String(length=36), nullable=False), sa.Column("project_function_id", sa.String(length=36), nullable=False),
sa.Column('start_date', sa.DateTime(timezone=True), nullable=True), sa.Column("crew_id", sa.String(length=36), nullable=False),
sa.Column('end_date', sa.DateTime(timezone=True), nullable=True), sa.Column("start_date", sa.DateTime(timezone=True), nullable=True),
sa.Column('notes', sa.Text(), nullable=True), sa.Column("end_date", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['crew_id'], ['crew.id'], ondelete='CASCADE'), sa.Column("notes", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['project_function_id'], ['project_functions.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(["crew_id"], ["crew.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint('id') sa.ForeignKeyConstraint(
["project_function_id"], ["project_functions.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("id"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('project_crew') op.drop_table("project_crew")
op.drop_table('project_equipment') op.drop_table("project_equipment")
op.drop_table('project_equipment_groups') op.drop_table("project_equipment_groups")
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -5,6 +5,7 @@ Revises: f5772dd5cd55
Create Date: 2026-05-31 21:17:13.064051 Create Date: 2026-05-31 21:17:13.064051
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op from alembic import op
@@ -12,31 +13,37 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = 'cfe638817921' revision: str = "cfe638817921"
down_revision: Union[str, None] = 'f5772dd5cd55' down_revision: Union[str, None] = "f5772dd5cd55"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('project_function_groups', schema=None) as batch_op: with op.batch_alter_table("project_function_groups", schema=None) as batch_op:
batch_op.add_column(sa.Column('start_date', sa.DateTime(timezone=True), nullable=True)) batch_op.add_column(
batch_op.add_column(sa.Column('end_date', sa.DateTime(timezone=True), nullable=True)) sa.Column("start_date", sa.DateTime(timezone=True), nullable=True)
)
batch_op.add_column(
sa.Column("end_date", sa.DateTime(timezone=True), nullable=True)
)
with op.batch_alter_table('project_functions', schema=None) as batch_op: with op.batch_alter_table("project_functions", schema=None) as batch_op:
batch_op.add_column(sa.Column('function_type', sa.String(length=50), nullable=False)) batch_op.add_column(
sa.Column("function_type", sa.String(length=50), nullable=False)
)
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('project_functions', schema=None) as batch_op: with op.batch_alter_table("project_functions", schema=None) as batch_op:
batch_op.drop_column('function_type') batch_op.drop_column("function_type")
with op.batch_alter_table('project_function_groups', schema=None) as batch_op: with op.batch_alter_table("project_function_groups", schema=None) as batch_op:
batch_op.drop_column('end_date') batch_op.drop_column("end_date")
batch_op.drop_column('start_date') batch_op.drop_column("start_date")
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -5,6 +5,7 @@ Revises: a5b1e93690ff
Create Date: 2026-05-31 11:31:54.796382 Create Date: 2026-05-31 11:31:54.796382
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op from alembic import op
@@ -12,81 +13,121 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = 'da13d44bd483' revision: str = "da13d44bd483"
down_revision: Union[str, None] = 'a5b1e93690ff' down_revision: Union[str, None] = "a5b1e93690ff"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('stock_locations', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "stock_locations",
sa.Column('account_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False), sa.Column("account_id", sa.String(length=36), nullable=False),
sa.Column('address', sa.String(length=500), nullable=True), sa.Column("name", sa.String(length=255), nullable=False),
sa.Column('is_default', sa.Boolean(), nullable=False), sa.Column("address", sa.String(length=500), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("is_default", sa.Boolean(), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column(
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), "created_at",
sa.PrimaryKeyConstraint('id') sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('equipment', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "equipment",
sa.Column('account_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False), sa.Column("account_id", sa.String(length=36), nullable=False),
sa.Column('category', sa.String(length=100), nullable=True), sa.Column("name", sa.String(length=255), nullable=False),
sa.Column('brand', sa.String(length=255), nullable=True), sa.Column("category", sa.String(length=100), nullable=True),
sa.Column('serial_number', sa.String(length=100), nullable=True), sa.Column("brand", sa.String(length=255), nullable=True),
sa.Column('barcode', sa.String(length=100), nullable=True), sa.Column("serial_number", sa.String(length=100), nullable=True),
sa.Column('qr_code', sa.String(length=500), nullable=True), sa.Column("barcode", sa.String(length=100), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False), sa.Column("qr_code", sa.String(length=500), nullable=True),
sa.Column('purchase_price', sa.Float(), nullable=True), sa.Column("status", sa.String(length=20), nullable=False),
sa.Column('current_value', sa.Float(), nullable=True), sa.Column("purchase_price", sa.Float(), nullable=True),
sa.Column('weight_kg', sa.Float(), nullable=True), sa.Column("current_value", sa.Float(), nullable=True),
sa.Column('dimensions', sa.String(length=255), nullable=True), sa.Column("weight_kg", sa.Float(), nullable=True),
sa.Column('power_watt', sa.Float(), nullable=True), sa.Column("dimensions", sa.String(length=255), nullable=True),
sa.Column('notes', sa.Text(), nullable=True), sa.Column("power_watt", sa.Float(), nullable=True),
sa.Column('custom_fields', sa.Text(), nullable=True), sa.Column("notes", sa.Text(), nullable=True),
sa.Column('location_id', sa.String(length=36), nullable=True), sa.Column("custom_fields", sa.Text(), nullable=True),
sa.Column('supplier_id', sa.String(length=36), nullable=True), sa.Column("location_id", sa.String(length=36), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("supplier_id", sa.String(length=36), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column(
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), "created_at",
sa.ForeignKeyConstraint(['location_id'], ['stock_locations.id'], ondelete='SET NULL'), sa.DateTime(timezone=True),
sa.ForeignKeyConstraint(['supplier_id'], ['contacts.id'], ondelete='SET NULL'), server_default=sa.text("(CURRENT_TIMESTAMP)"),
sa.PrimaryKeyConstraint('id'), nullable=False,
sa.UniqueConstraint('barcode'), ),
sa.UniqueConstraint('serial_number') sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["location_id"], ["stock_locations.id"], ondelete="SET NULL"
),
sa.ForeignKeyConstraint(["supplier_id"], ["contacts.id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("barcode"),
sa.UniqueConstraint("serial_number"),
) )
op.create_table('equipment_groups', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "equipment_groups",
sa.Column('account_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False), sa.Column("account_id", sa.String(length=36), nullable=False),
sa.Column('description', sa.Text(), nullable=True), sa.Column("name", sa.String(length=255), nullable=False),
sa.Column('daily_rate', sa.Float(), nullable=True), sa.Column("description", sa.Text(), nullable=True),
sa.Column('default_location_id', sa.String(length=36), nullable=True), sa.Column("daily_rate", sa.Float(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("default_location_id", sa.String(length=36), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column(
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), "created_at",
sa.ForeignKeyConstraint(['default_location_id'], ['stock_locations.id'], ondelete='SET NULL'), sa.DateTime(timezone=True),
sa.PrimaryKeyConstraint('id') server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["default_location_id"], ["stock_locations.id"], ondelete="SET NULL"
),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('equipment_group_items', op.create_table(
sa.Column('group_id', sa.String(length=36), nullable=False), "equipment_group_items",
sa.Column('equipment_id', sa.String(length=36), nullable=False), sa.Column("group_id", sa.String(length=36), nullable=False),
sa.Column('quantity', sa.Float(), nullable=False), sa.Column("equipment_id", sa.String(length=36), nullable=False),
sa.ForeignKeyConstraint(['equipment_id'], ['equipment.id'], ondelete='CASCADE'), sa.Column("quantity", sa.Float(), nullable=False),
sa.ForeignKeyConstraint(['group_id'], ['equipment_groups.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(["equipment_id"], ["equipment.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint('group_id', 'equipment_id') sa.ForeignKeyConstraint(
["group_id"], ["equipment_groups.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("group_id", "equipment_id"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('equipment_group_items') op.drop_table("equipment_group_items")
op.drop_table('equipment_groups') op.drop_table("equipment_groups")
op.drop_table('equipment') op.drop_table("equipment")
op.drop_table('stock_locations') op.drop_table("stock_locations")
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -5,6 +5,7 @@ Revises: 32b02c118683
Create Date: 2026-05-31 13:59:30.308076 Create Date: 2026-05-31 13:59:30.308076
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op from alembic import op
@@ -12,68 +13,84 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = 'f5772dd5cd55' revision: str = "f5772dd5cd55"
down_revision: Union[str, None] = '32b02c118683' down_revision: Union[str, None] = "32b02c118683"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('projects', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "projects",
sa.Column('account_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False), sa.Column("account_id", sa.String(length=36), nullable=False),
sa.Column('description', sa.Text(), nullable=True), sa.Column("name", sa.String(length=255), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False), sa.Column("description", sa.Text(), nullable=True),
sa.Column('start_date', sa.DateTime(timezone=True), nullable=True), sa.Column("status", sa.String(length=20), nullable=False),
sa.Column('end_date', sa.DateTime(timezone=True), nullable=True), sa.Column("start_date", sa.DateTime(timezone=True), nullable=True),
sa.Column('budget', sa.Float(), nullable=True), sa.Column("end_date", sa.DateTime(timezone=True), nullable=True),
sa.Column('total_costs', sa.Float(), nullable=False), sa.Column("budget", sa.Float(), nullable=True),
sa.Column('notes', sa.Text(), nullable=True), sa.Column("total_costs", sa.Float(), nullable=False),
sa.Column('custom_fields', sa.Text(), nullable=True), sa.Column("notes", sa.Text(), nullable=True),
sa.Column('custom_field_defs', sa.Text(), nullable=True), sa.Column("custom_fields", sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("custom_field_defs", sa.Text(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column(
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), "created_at",
sa.PrimaryKeyConstraint('id') sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('project_function_groups', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "project_function_groups",
sa.Column('name', sa.String(length=255), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column("name", sa.String(length=255), nullable=False),
sa.Column('sort_order', sa.Integer(), nullable=False), sa.Column("project_id", sa.String(length=36), nullable=False),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.Column("sort_order", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id') sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('sub_projects', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "sub_projects",
sa.Column('name', sa.String(length=255), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column("name", sa.String(length=255), nullable=False),
sa.Column('parent_id', sa.String(length=36), nullable=True), sa.Column("project_id", sa.String(length=36), nullable=False),
sa.Column('sort_order', sa.Integer(), nullable=False), sa.Column("parent_id", sa.String(length=36), nullable=True),
sa.ForeignKeyConstraint(['parent_id'], ['sub_projects.id'], ondelete='CASCADE'), sa.Column("sort_order", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(["parent_id"], ["sub_projects.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint('id') sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('project_functions', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "project_functions",
sa.Column('name', sa.String(length=255), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('function_group_id', sa.String(length=36), nullable=False), sa.Column("name", sa.String(length=255), nullable=False),
sa.Column('quantity', sa.Float(), nullable=False), sa.Column("function_group_id", sa.String(length=36), nullable=False),
sa.Column('daily_costs', sa.Float(), nullable=False), sa.Column("quantity", sa.Float(), nullable=False),
sa.Column('total_costs', sa.Float(), nullable=False), sa.Column("daily_costs", sa.Float(), nullable=False),
sa.Column('sort_order', sa.Integer(), nullable=False), sa.Column("total_costs", sa.Float(), nullable=False),
sa.ForeignKeyConstraint(['function_group_id'], ['project_function_groups.id'], ondelete='CASCADE'), sa.Column("sort_order", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id') sa.ForeignKeyConstraint(
["function_group_id"], ["project_function_groups.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("id"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('project_functions') op.drop_table("project_functions")
op.drop_table('sub_projects') op.drop_table("sub_projects")
op.drop_table('project_function_groups') op.drop_table("project_function_groups")
op.drop_table('projects') op.drop_table("projects")
# ### end Alembic commands ### # ### end Alembic commands ###
@@ -1,10 +1,11 @@
"""create_accounts_and_users_tables """create_accounts_and_users_tables
Revision ID: f90ee7806a25 Revision ID: f90ee7806a25
Revises: Revises:
Create Date: 2026-05-31 10:25:47.726322 Create Date: 2026-05-31 10:25:47.726322
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op from alembic import op
@@ -12,7 +13,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = 'f90ee7806a25' revision: str = "f90ee7806a25"
down_revision: Union[str, None] = None down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
@@ -20,31 +21,48 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('accounts', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "accounts",
sa.Column('name', sa.String(length=255), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("name", sa.String(length=255), nullable=False),
sa.PrimaryKeyConstraint('id') sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('users', op.create_table(
sa.Column('id', sa.String(length=36), nullable=False), "users",
sa.Column('account_id', sa.String(length=36), nullable=False), sa.Column("id", sa.String(length=36), nullable=False),
sa.Column('email', sa.String(length=255), nullable=False), sa.Column("account_id", sa.String(length=36), nullable=False),
sa.Column('full_name', sa.String(length=255), nullable=False), sa.Column("email", sa.String(length=255), nullable=False),
sa.Column('password_hash', sa.String(length=255), nullable=False), sa.Column("full_name", sa.String(length=255), nullable=False),
sa.Column('role_id', sa.String(length=36), nullable=True), sa.Column("password_hash", sa.String(length=255), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column("role_id", sa.String(length=36), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), sa.Column(
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), "created_at",
sa.PrimaryKeyConstraint('id'), sa.DateTime(timezone=True),
sa.UniqueConstraint('email') server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("(CURRENT_TIMESTAMP)"),
nullable=False,
),
sa.ForeignKeyConstraint(["account_id"], ["accounts.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('users') op.drop_table("users")
op.drop_table('accounts') op.drop_table("accounts")
# ### end Alembic commands ### # ### end Alembic commands ###
+1 -3
View File
@@ -42,9 +42,7 @@ async def get_current_user(
# Load user with role (and its permissions) eagerly # Load user with role (and its permissions) eagerly
result = await session.execute( result = await session.execute(
select(User) select(User).options(joinedload(User.role)).where(User.id == user_id)
.options(joinedload(User.role))
.where(User.id == user_id)
) )
user = result.unique().scalars().first() user = result.unique().scalars().first()
+26 -9
View File
@@ -42,7 +42,9 @@ def _build_user_info(user: User, role: Role | None = None) -> UserInfo:
) )
@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED) @router.post(
"/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED
)
async def register( async def register(
body: RegisterRequest, body: RegisterRequest,
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
@@ -57,7 +59,9 @@ async def register(
) )
# Check if account name already exists # Check if account name already exists
result = await session.execute(select(Account).where(Account.name == body.account_name)) result = await session.execute(
select(Account).where(Account.name == body.account_name)
)
if result.scalars().first(): if result.scalars().first():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=status.HTTP_409_CONFLICT,
@@ -71,13 +75,26 @@ async def register(
# Create admin role # Create admin role
admin_permissions = [ admin_permissions = [
"projects:read", "projects:write", "projects:delete", "projects:read",
"equipment:read", "equipment:write", "equipment:delete", "projects:write",
"crew:read", "crew:write", "crew:delete", "projects:delete",
"vehicles:read", "vehicles:write", "vehicles:delete", "equipment:read",
"contacts:read", "contacts:write", "contacts:delete", "equipment:write",
"users:read", "users:write", "users:delete", "equipment:delete",
"roles:read", "roles:write", "crew:read",
"crew:write",
"crew:delete",
"vehicles:read",
"vehicles:write",
"vehicles:delete",
"contacts:read",
"contacts:write",
"contacts:delete",
"users:read",
"users:write",
"users:delete",
"roles:read",
"roles:write",
] ]
admin_role = Role( admin_role = Role(
account_id=account.id, account_id=account.id,
+6 -6
View File
@@ -3,12 +3,11 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy import select, func, or_ from sqlalchemy import select, func, or_
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm import joinedload
from app.api.deps import get_current_user, require_permission from app.api.deps import require_permission
from app.db.session import get_async_session from app.db.session import get_async_session
from app.models import User, Contact, Tag from app.models import User, Contact, Tag
from app.models.contact import contact_tags
from app.schemas.contact import ( from app.schemas.contact import (
ContactCreateRequest, ContactCreateRequest,
ContactUpdateRequest, ContactUpdateRequest,
@@ -24,7 +23,9 @@ async def list_contacts(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=100),
search: str | None = Query(None, description="Search in name, email, phone"), search: str | None = Query(None, description="Search in name, email, phone"),
type: str | None = Query(None, pattern="^(company|person)$", description="Filter by contact type"), type: str | None = Query(
None, pattern="^(company|person)$", description="Filter by contact type"
),
current_user: User = Depends(require_permission("contacts:read")), current_user: User = Depends(require_permission("contacts:read")),
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
): ):
@@ -56,8 +57,7 @@ async def list_contacts(
# Fetch page with tags eager-loaded # Fetch page with tags eager-loaded
q = ( q = (
base_q base_q.options(joinedload(Contact.tags))
.options(joinedload(Contact.tags))
.order_by(Contact.updated_at.desc()) .order_by(Contact.updated_at.desc())
.offset((page - 1) * size) .offset((page - 1) * size)
.limit(size) .limit(size)
+32 -17
View File
@@ -3,9 +3,9 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy import select, func, or_ from sqlalchemy import select, func, or_
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm import selectinload
from app.api.deps import get_current_user, require_permission from app.api.deps import require_permission
from app.db.session import get_async_session from app.db.session import get_async_session
from app.models import User, Crew, CrewAvailability from app.models import User, Crew, CrewAvailability
from app.schemas.crew import ( from app.schemas.crew import (
@@ -24,6 +24,7 @@ avail_router = APIRouter(prefix="/crew-availabilities", tags=["crew-availabiliti
# ===== Crew Endpoints ===== # ===== Crew Endpoints =====
@router.get("", response_model=CrewListResponse) @router.get("", response_model=CrewListResponse)
async def list_crew( async def list_crew(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
@@ -55,8 +56,7 @@ async def list_crew(
total = (await session.execute(count_q)).scalar() or 0 total = (await session.execute(count_q)).scalar() or 0
q = ( q = (
base_q base_q.options(selectinload(Crew.availabilities))
.options(selectinload(Crew.availabilities))
.order_by(Crew.last_name, Crew.first_name) .order_by(Crew.last_name, Crew.first_name)
.offset((page - 1) * size) .offset((page - 1) * size)
.limit(size) .limit(size)
@@ -102,7 +102,9 @@ async def get_crew(
) )
member = result.unique().scalars().first() member = result.unique().scalars().first()
if not member: if not member:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found"
)
return CrewResponse.model_validate(member) return CrewResponse.model_validate(member)
@@ -122,7 +124,9 @@ async def update_crew(
) )
member = result.unique().scalars().first() member = result.unique().scalars().first()
if not member: if not member:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found"
)
update_data = body.model_dump(exclude_unset=True) update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
@@ -146,7 +150,9 @@ async def delete_crew(
) )
member = result.scalars().first() member = result.scalars().first()
if not member: if not member:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found"
)
await session.delete(member) await session.delete(member)
await session.commit() await session.commit()
@@ -155,6 +161,7 @@ async def delete_crew(
# ===== CrewAvailability Endpoints ===== # ===== CrewAvailability Endpoints =====
@avail_router.get("", response_model=CrewAvailabilityListResponse) @avail_router.get("", response_model=CrewAvailabilityListResponse)
async def list_availabilities( async def list_availabilities(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
@@ -166,11 +173,7 @@ async def list_availabilities(
): ):
"""List availabilities with filters.""" """List availabilities with filters."""
account_id = current_user.account_id account_id = current_user.account_id
base_q = ( base_q = select(CrewAvailability).join(Crew).where(Crew.account_id == account_id)
select(CrewAvailability)
.join(Crew)
.where(Crew.account_id == account_id)
)
if crew_id: if crew_id:
base_q = base_q.where(CrewAvailability.crew_id == crew_id) base_q = base_q.where(CrewAvailability.crew_id == crew_id)
@@ -180,7 +183,11 @@ async def list_availabilities(
count_q = select(func.count()).select_from(base_q.subquery()) count_q = select(func.count()).select_from(base_q.subquery())
total = (await session.execute(count_q)).scalar() or 0 total = (await session.execute(count_q)).scalar() or 0
q = base_q.order_by(CrewAvailability.start_date.desc()).offset((page - 1) * size).limit(size) q = (
base_q.order_by(CrewAvailability.start_date.desc())
.offset((page - 1) * size)
.limit(size)
)
result = await session.execute(q) result = await session.execute(q)
items = result.scalars().all() items = result.scalars().all()
@@ -192,7 +199,9 @@ async def list_availabilities(
) )
@avail_router.post("", response_model=CrewAvailabilityResponse, status_code=status.HTTP_201_CREATED) @avail_router.post(
"", response_model=CrewAvailabilityResponse, status_code=status.HTTP_201_CREATED
)
async def create_availability( async def create_availability(
body: CrewAvailabilityCreateRequest, body: CrewAvailabilityCreateRequest,
current_user: User = Depends(require_permission("crew:write")), current_user: User = Depends(require_permission("crew:write")),
@@ -207,7 +216,9 @@ async def create_availability(
) )
) )
if not crew_result.scalars().first(): if not crew_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Crew member not found"
)
avail = CrewAvailability(**body.model_dump()) avail = CrewAvailability(**body.model_dump())
session.add(avail) session.add(avail)
@@ -234,7 +245,9 @@ async def update_availability(
) )
avail = result.scalars().first() avail = result.scalars().first()
if not avail: if not avail:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Availability not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Availability not found"
)
update_data = body.model_dump(exclude_unset=True) update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
@@ -262,7 +275,9 @@ async def delete_availability(
) )
avail = result.scalars().first() avail = result.scalars().first()
if not avail: if not avail:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Availability not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Availability not found"
)
await session.delete(avail) await session.delete(avail)
await session.commit() await session.commit()
+17 -10
View File
@@ -5,7 +5,7 @@ from sqlalchemy import select, func, or_
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from app.api.deps import get_current_user, require_permission from app.api.deps import require_permission
from app.db.session import get_async_session from app.db.session import get_async_session
from app.models import User, Equipment, StockLocation, Contact from app.models import User, Equipment, StockLocation, Contact
from app.schemas.equipment import ( from app.schemas.equipment import (
@@ -13,8 +13,6 @@ from app.schemas.equipment import (
EquipmentUpdateRequest, EquipmentUpdateRequest,
EquipmentResponse, EquipmentResponse,
EquipmentListResponse, EquipmentListResponse,
LocationRef,
SupplierRef,
) )
router = APIRouter(prefix="/equipment", tags=["equipment"]) router = APIRouter(prefix="/equipment", tags=["equipment"])
@@ -24,7 +22,9 @@ router = APIRouter(prefix="/equipment", tags=["equipment"])
async def list_equipment( async def list_equipment(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=100),
search: str | None = Query(None, description="Search in name, brand, serial_number"), search: str | None = Query(
None, description="Search in name, brand, serial_number"
),
category: str | None = Query(None, description="Filter by category"), category: str | None = Query(None, description="Filter by category"),
status: str | None = Query(None, description="Filter by status"), status: str | None = Query(None, description="Filter by status"),
location_id: str | None = Query(None, description="Filter by location"), location_id: str | None = Query(None, description="Filter by location"),
@@ -58,8 +58,7 @@ async def list_equipment(
total = (await session.execute(count_q)).scalar() or 0 total = (await session.execute(count_q)).scalar() or 0
q = ( q = (
base_q base_q.options(joinedload(Equipment.location), joinedload(Equipment.supplier))
.options(joinedload(Equipment.location), joinedload(Equipment.supplier))
.order_by(Equipment.updated_at.desc()) .order_by(Equipment.updated_at.desc())
.offset((page - 1) * size) .offset((page - 1) * size)
.limit(size) .limit(size)
@@ -142,7 +141,9 @@ async def get_equipment(
) )
item = result.unique().scalars().first() item = result.unique().scalars().first()
if not item: if not item:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found"
)
return EquipmentResponse.model_validate(item) return EquipmentResponse.model_validate(item)
@@ -162,7 +163,9 @@ async def update_equipment(
) )
item = result.unique().scalars().first() item = result.unique().scalars().first()
if not item: if not item:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found"
)
update_data = body.model_dump(exclude_unset=True) update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
@@ -182,11 +185,15 @@ async def delete_equipment(
"""Delete an equipment item.""" """Delete an equipment item."""
account_id = current_user.account_id account_id = current_user.account_id
result = await session.execute( result = await session.execute(
select(Equipment).where(Equipment.id == item_id, Equipment.account_id == account_id) select(Equipment).where(
Equipment.id == item_id, Equipment.account_id == account_id
)
) )
item = result.scalars().first() item = result.scalars().first()
if not item: if not item:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Equipment not found"
)
await session.delete(item) await session.delete(item)
await session.commit() await session.commit()
+31 -14
View File
@@ -1,22 +1,20 @@
"""EquipmentGroup (Bundle) CRUD endpoints.""" """EquipmentGroup (Bundle) CRUD endpoints."""
from fastapi import APIRouter, Depends, HTTPException, status, Query from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy import select, func, or_ from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm import joinedload, selectinload
from app.api.deps import get_current_user, require_permission from app.api.deps import require_permission
from app.db.session import get_async_session from app.db.session import get_async_session
from app.models import User, EquipmentGroup, Equipment, StockLocation from app.models import User, EquipmentGroup, Equipment
from app.models.equipment_group import equipment_group_items from app.models.equipment_group import equipment_group_items
from app.schemas.equipment_group import ( from app.schemas.equipment_group import (
EquipmentGroupCreateRequest, EquipmentGroupCreateRequest,
EquipmentGroupUpdateRequest, EquipmentGroupUpdateRequest,
EquipmentGroupResponse, EquipmentGroupResponse,
EquipmentGroupListResponse, EquipmentGroupListResponse,
EquipmentGroupItem as EquipmentGroupItemSchema,
) )
from app.schemas.equipment import LocationRef
router = APIRouter(prefix="/equipment-groups", tags=["equipment-groups"]) router = APIRouter(prefix="/equipment-groups", tags=["equipment-groups"])
@@ -48,8 +46,10 @@ async def list_equipment_groups(
total = (await session.execute(count_q)).scalar() or 0 total = (await session.execute(count_q)).scalar() or 0
q = ( q = (
base_q base_q.options(
.options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items)) joinedload(EquipmentGroup.default_location),
selectinload(EquipmentGroup.items),
)
.order_by(EquipmentGroup.name) .order_by(EquipmentGroup.name)
.offset((page - 1) * size) .offset((page - 1) * size)
.limit(size) .limit(size)
@@ -65,7 +65,9 @@ async def list_equipment_groups(
) )
@router.post("", response_model=EquipmentGroupResponse, status_code=status.HTTP_201_CREATED) @router.post(
"", response_model=EquipmentGroupResponse, status_code=status.HTTP_201_CREATED
)
async def create_equipment_group( async def create_equipment_group(
body: EquipmentGroupCreateRequest, body: EquipmentGroupCreateRequest,
current_user: User = Depends(require_permission("equipment:write")), current_user: User = Depends(require_permission("equipment:write")),
@@ -110,7 +112,10 @@ async def create_equipment_group(
# Reload with relationships # Reload with relationships
await session.execute( await session.execute(
select(EquipmentGroup) select(EquipmentGroup)
.options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items)) .options(
joinedload(EquipmentGroup.default_location),
selectinload(EquipmentGroup.items),
)
.where(EquipmentGroup.id == group.id) .where(EquipmentGroup.id == group.id)
) )
await session.refresh(group) await session.refresh(group)
@@ -128,12 +133,17 @@ async def get_equipment_group(
account_id = current_user.account_id account_id = current_user.account_id
result = await session.execute( result = await session.execute(
select(EquipmentGroup) select(EquipmentGroup)
.options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items)) .options(
joinedload(EquipmentGroup.default_location),
selectinload(EquipmentGroup.items),
)
.where(EquipmentGroup.id == group_id, EquipmentGroup.account_id == account_id) .where(EquipmentGroup.id == group_id, EquipmentGroup.account_id == account_id)
) )
group = result.unique().scalars().first() group = result.unique().scalars().first()
if not group: if not group:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found"
)
return EquipmentGroupResponse.model_validate(group) return EquipmentGroupResponse.model_validate(group)
@@ -148,12 +158,17 @@ async def update_equipment_group(
account_id = current_user.account_id account_id = current_user.account_id
result = await session.execute( result = await session.execute(
select(EquipmentGroup) select(EquipmentGroup)
.options(joinedload(EquipmentGroup.default_location), selectinload(EquipmentGroup.items)) .options(
joinedload(EquipmentGroup.default_location),
selectinload(EquipmentGroup.items),
)
.where(EquipmentGroup.id == group_id, EquipmentGroup.account_id == account_id) .where(EquipmentGroup.id == group_id, EquipmentGroup.account_id == account_id)
) )
group = result.unique().scalars().first() group = result.unique().scalars().first()
if not group: if not group:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found"
)
update_data = body.model_dump(exclude_unset=True, exclude={"items"}) update_data = body.model_dump(exclude_unset=True, exclude={"items"})
for field, value in update_data.items(): for field, value in update_data.items():
@@ -201,7 +216,9 @@ async def delete_equipment_group(
) )
group = result.scalars().first() group = result.scalars().first()
if not group: if not group:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Equipment group not found"
)
await session.delete(group) await session.delete(group)
await session.commit() await session.commit()
+225 -78
View File
@@ -1,11 +1,10 @@
"""Project CRUD endpoints including sub-projects, function groups, and functions.""" """Project CRUD endpoints including sub-projects, function groups, and functions."""
from fastapi import APIRouter, Depends, HTTPException, status, Query from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy import select, func, or_ from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.api.deps import get_current_user, require_permission from app.api.deps import require_permission
from app.db.session import get_async_session from app.db.session import get_async_session
from app.models import User, Project, SubProject, ProjectFunctionGroup, ProjectFunction from app.models import User, Project, SubProject, ProjectFunctionGroup, ProjectFunction
from app.schemas.project import ( from app.schemas.project import (
@@ -38,7 +37,10 @@ async def list_projects(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=100),
search: str | None = Query(None, description="Search in project name"), search: str | None = Query(None, description="Search in project name"),
status: str | None = Query(None, description="Filter by status (draft, confirmed, in_progress, completed, cancelled)"), status: str | None = Query(
None,
description="Filter by status (draft, confirmed, in_progress, completed, cancelled)",
),
current_user: User = Depends(require_permission("projects:read")), current_user: User = Depends(require_permission("projects:read")),
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
): ):
@@ -57,12 +59,7 @@ async def list_projects(
count_q = select(func.count()).select_from(base_q.subquery()) count_q = select(func.count()).select_from(base_q.subquery())
total = (await session.execute(count_q)).scalar() or 0 total = (await session.execute(count_q)).scalar() or 0
q = ( q = base_q.order_by(Project.updated_at.desc()).offset((page - 1) * size).limit(size)
base_q
.order_by(Project.updated_at.desc())
.offset((page - 1) * size)
.limit(size)
)
result = await session.execute(q) result = await session.execute(q)
items = result.scalars().all() items = result.scalars().all()
@@ -98,11 +95,15 @@ async def get_project(
"""Get a specific project.""" """Get a specific project."""
account_id = current_user.account_id account_id = current_user.account_id
result = await session.execute( result = await session.execute(
select(Project).where(Project.id == project_id, Project.account_id == account_id) select(Project).where(
Project.id == project_id, Project.account_id == account_id
)
) )
project = result.scalars().first() project = result.scalars().first()
if not project: if not project:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
return ProjectResponse.model_validate(project) return ProjectResponse.model_validate(project)
@@ -116,11 +117,15 @@ async def update_project(
"""Update an existing project.""" """Update an existing project."""
account_id = current_user.account_id account_id = current_user.account_id
result = await session.execute( result = await session.execute(
select(Project).where(Project.id == project_id, Project.account_id == account_id) select(Project).where(
Project.id == project_id, Project.account_id == account_id
)
) )
project = result.scalars().first() project = result.scalars().first()
if not project: if not project:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
update_data = body.model_dump(exclude_unset=True) update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
@@ -140,11 +145,15 @@ async def delete_project(
"""Delete a project.""" """Delete a project."""
account_id = current_user.account_id account_id = current_user.account_id
result = await session.execute( result = await session.execute(
select(Project).where(Project.id == project_id, Project.account_id == account_id) select(Project).where(
Project.id == project_id, Project.account_id == account_id
)
) )
project = result.scalars().first() project = result.scalars().first()
if not project: if not project:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
await session.delete(project) await session.delete(project)
await session.commit() await session.commit()
@@ -167,10 +176,14 @@ async def list_subprojects(
# Verify project ownership # Verify project ownership
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
base_q = select(SubProject).where(SubProject.project_id == project_id) base_q = select(SubProject).where(SubProject.project_id == project_id)
count_q = select(func.count()).select_from(base_q.subquery()) count_q = select(func.count()).select_from(base_q.subquery())
@@ -188,7 +201,11 @@ async def list_subprojects(
) )
@router.post("/{project_id}/subprojects", response_model=SubProjectResponse, status_code=status.HTTP_201_CREATED) @router.post(
"/{project_id}/subprojects",
response_model=SubProjectResponse,
status_code=status.HTTP_201_CREATED,
)
async def create_subproject( async def create_subproject(
project_id: str, project_id: str,
body: SubProjectCreateRequest, body: SubProjectCreateRequest,
@@ -200,10 +217,14 @@ async def create_subproject(
# Verify project ownership # Verify project ownership
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
# Verify parent subproject if provided # Verify parent subproject if provided
if body.parent_id: if body.parent_id:
@@ -238,17 +259,25 @@ async def get_subproject(
# Verify project ownership # Verify project ownership
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
result = await session.execute( result = await session.execute(
select(SubProject).where(SubProject.id == sub_id, SubProject.project_id == project_id) select(SubProject).where(
SubProject.id == sub_id, SubProject.project_id == project_id
)
) )
sub = result.scalars().first() sub = result.scalars().first()
if not sub: if not sub:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found"
)
return SubProjectResponse.model_validate(sub) return SubProjectResponse.model_validate(sub)
@@ -265,17 +294,25 @@ async def update_subproject(
# Verify project ownership # Verify project ownership
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
result = await session.execute( result = await session.execute(
select(SubProject).where(SubProject.id == sub_id, SubProject.project_id == project_id) select(SubProject).where(
SubProject.id == sub_id, SubProject.project_id == project_id
)
) )
sub = result.scalars().first() sub = result.scalars().first()
if not sub: if not sub:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found"
)
update_data = body.model_dump(exclude_unset=True) update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
@@ -286,7 +323,9 @@ async def update_subproject(
return SubProjectResponse.model_validate(sub) return SubProjectResponse.model_validate(sub)
@router.delete("/{project_id}/subprojects/{sub_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete(
"/{project_id}/subprojects/{sub_id}", status_code=status.HTTP_204_NO_CONTENT
)
async def delete_subproject( async def delete_subproject(
project_id: str, project_id: str,
sub_id: str, sub_id: str,
@@ -298,17 +337,25 @@ async def delete_subproject(
# Verify project ownership # Verify project ownership
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
result = await session.execute( result = await session.execute(
select(SubProject).where(SubProject.id == sub_id, SubProject.project_id == project_id) select(SubProject).where(
SubProject.id == sub_id, SubProject.project_id == project_id
)
) )
sub = result.scalars().first() sub = result.scalars().first()
if not sub: if not sub:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="SubProject not found"
)
await session.delete(sub) await session.delete(sub)
await session.commit() await session.commit()
@@ -318,7 +365,9 @@ async def delete_subproject(
# ===== ProjectFunctionGroup Endpoints ===== # ===== ProjectFunctionGroup Endpoints =====
@router.get("/{project_id}/function-groups", response_model=ProjectFunctionGroupListResponse) @router.get(
"/{project_id}/function-groups", response_model=ProjectFunctionGroupListResponse
)
async def list_function_groups( async def list_function_groups(
project_id: str, project_id: str,
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
@@ -330,16 +379,26 @@ async def list_function_groups(
account_id = current_user.account_id account_id = current_user.account_id
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
base_q = select(ProjectFunctionGroup).where(ProjectFunctionGroup.project_id == project_id) base_q = select(ProjectFunctionGroup).where(
ProjectFunctionGroup.project_id == project_id
)
count_q = select(func.count()).select_from(base_q.subquery()) count_q = select(func.count()).select_from(base_q.subquery())
total = (await session.execute(count_q)).scalar() or 0 total = (await session.execute(count_q)).scalar() or 0
q = base_q.order_by(ProjectFunctionGroup.sort_order).offset((page - 1) * size).limit(size) q = (
base_q.order_by(ProjectFunctionGroup.sort_order)
.offset((page - 1) * size)
.limit(size)
)
result = await session.execute(q) result = await session.execute(q)
items = result.scalars().all() items = result.scalars().all()
@@ -351,7 +410,11 @@ async def list_function_groups(
) )
@router.post("/{project_id}/function-groups", response_model=ProjectFunctionGroupResponse, status_code=status.HTTP_201_CREATED) @router.post(
"/{project_id}/function-groups",
response_model=ProjectFunctionGroupResponse,
status_code=status.HTTP_201_CREATED,
)
async def create_function_group( async def create_function_group(
project_id: str, project_id: str,
body: ProjectFunctionGroupCreateRequest, body: ProjectFunctionGroupCreateRequest,
@@ -362,10 +425,14 @@ async def create_function_group(
account_id = current_user.account_id account_id = current_user.account_id
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
fg = ProjectFunctionGroup(project_id=project_id, **body.model_dump()) fg = ProjectFunctionGroup(project_id=project_id, **body.model_dump())
session.add(fg) session.add(fg)
@@ -374,7 +441,9 @@ async def create_function_group(
return ProjectFunctionGroupResponse.model_validate(fg) return ProjectFunctionGroupResponse.model_validate(fg)
@router.get("/{project_id}/function-groups/{fg_id}", response_model=ProjectFunctionGroupResponse) @router.get(
"/{project_id}/function-groups/{fg_id}", response_model=ProjectFunctionGroupResponse
)
async def get_function_group( async def get_function_group(
project_id: str, project_id: str,
fg_id: str, fg_id: str,
@@ -385,10 +454,14 @@ async def get_function_group(
account_id = current_user.account_id account_id = current_user.account_id
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
result = await session.execute( result = await session.execute(
select(ProjectFunctionGroup).where( select(ProjectFunctionGroup).where(
@@ -398,11 +471,15 @@ async def get_function_group(
) )
fg = result.scalars().first() fg = result.scalars().first()
if not fg: if not fg:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found"
)
return ProjectFunctionGroupResponse.model_validate(fg) return ProjectFunctionGroupResponse.model_validate(fg)
@router.put("/{project_id}/function-groups/{fg_id}", response_model=ProjectFunctionGroupResponse) @router.put(
"/{project_id}/function-groups/{fg_id}", response_model=ProjectFunctionGroupResponse
)
async def update_function_group( async def update_function_group(
project_id: str, project_id: str,
fg_id: str, fg_id: str,
@@ -414,10 +491,14 @@ async def update_function_group(
account_id = current_user.account_id account_id = current_user.account_id
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
result = await session.execute( result = await session.execute(
select(ProjectFunctionGroup).where( select(ProjectFunctionGroup).where(
@@ -427,7 +508,9 @@ async def update_function_group(
) )
fg = result.scalars().first() fg = result.scalars().first()
if not fg: if not fg:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found"
)
update_data = body.model_dump(exclude_unset=True) update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
@@ -438,7 +521,9 @@ async def update_function_group(
return ProjectFunctionGroupResponse.model_validate(fg) return ProjectFunctionGroupResponse.model_validate(fg)
@router.delete("/{project_id}/function-groups/{fg_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete(
"/{project_id}/function-groups/{fg_id}", status_code=status.HTTP_204_NO_CONTENT
)
async def delete_function_group( async def delete_function_group(
project_id: str, project_id: str,
fg_id: str, fg_id: str,
@@ -449,10 +534,14 @@ async def delete_function_group(
account_id = current_user.account_id account_id = current_user.account_id
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
result = await session.execute( result = await session.execute(
select(ProjectFunctionGroup).where( select(ProjectFunctionGroup).where(
@@ -462,7 +551,9 @@ async def delete_function_group(
) )
fg = result.scalars().first() fg = result.scalars().first()
if not fg: if not fg:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found"
)
await session.delete(fg) await session.delete(fg)
await session.commit() await session.commit()
@@ -472,7 +563,10 @@ async def delete_function_group(
# ===== ProjectFunction Endpoints ===== # ===== ProjectFunction Endpoints =====
@router.get("/{project_id}/function-groups/{fg_id}/functions", response_model=ProjectFunctionListResponse) @router.get(
"/{project_id}/function-groups/{fg_id}/functions",
response_model=ProjectFunctionListResponse,
)
async def list_project_functions( async def list_project_functions(
project_id: str, project_id: str,
fg_id: str, fg_id: str,
@@ -485,10 +579,14 @@ async def list_project_functions(
account_id = current_user.account_id account_id = current_user.account_id
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
# Verify function group belongs to project # Verify function group belongs to project
fg_result = await session.execute( fg_result = await session.execute(
@@ -498,13 +596,19 @@ async def list_project_functions(
) )
) )
if not fg_result.scalars().first(): if not fg_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found"
)
base_q = select(ProjectFunction).where(ProjectFunction.function_group_id == fg_id) base_q = select(ProjectFunction).where(ProjectFunction.function_group_id == fg_id)
count_q = select(func.count()).select_from(base_q.subquery()) count_q = select(func.count()).select_from(base_q.subquery())
total = (await session.execute(count_q)).scalar() or 0 total = (await session.execute(count_q)).scalar() or 0
q = base_q.order_by(ProjectFunction.sort_order).offset((page - 1) * size).limit(size) q = (
base_q.order_by(ProjectFunction.sort_order)
.offset((page - 1) * size)
.limit(size)
)
result = await session.execute(q) result = await session.execute(q)
items = result.scalars().all() items = result.scalars().all()
@@ -516,7 +620,11 @@ async def list_project_functions(
) )
@router.post("/{project_id}/function-groups/{fg_id}/functions", response_model=ProjectFunctionResponse, status_code=status.HTTP_201_CREATED) @router.post(
"/{project_id}/function-groups/{fg_id}/functions",
response_model=ProjectFunctionResponse,
status_code=status.HTTP_201_CREATED,
)
async def create_project_function( async def create_project_function(
project_id: str, project_id: str,
fg_id: str, fg_id: str,
@@ -528,10 +636,14 @@ async def create_project_function(
account_id = current_user.account_id account_id = current_user.account_id
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
fg_result = await session.execute( fg_result = await session.execute(
select(ProjectFunctionGroup.id).where( select(ProjectFunctionGroup.id).where(
@@ -540,7 +652,9 @@ async def create_project_function(
) )
) )
if not fg_result.scalars().first(): if not fg_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found"
)
func_obj = ProjectFunction(function_group_id=fg_id, **body.model_dump()) func_obj = ProjectFunction(function_group_id=fg_id, **body.model_dump())
session.add(func_obj) session.add(func_obj)
@@ -549,7 +663,10 @@ async def create_project_function(
return ProjectFunctionResponse.model_validate(func_obj) return ProjectFunctionResponse.model_validate(func_obj)
@router.get("/{project_id}/function-groups/{fg_id}/functions/{func_id}", response_model=ProjectFunctionResponse) @router.get(
"/{project_id}/function-groups/{fg_id}/functions/{func_id}",
response_model=ProjectFunctionResponse,
)
async def get_project_function( async def get_project_function(
project_id: str, project_id: str,
fg_id: str, fg_id: str,
@@ -561,10 +678,14 @@ async def get_project_function(
account_id = current_user.account_id account_id = current_user.account_id
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
fg_result = await session.execute( fg_result = await session.execute(
select(ProjectFunctionGroup.id).where( select(ProjectFunctionGroup.id).where(
@@ -573,7 +694,9 @@ async def get_project_function(
) )
) )
if not fg_result.scalars().first(): if not fg_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found"
)
result = await session.execute( result = await session.execute(
select(ProjectFunction).where( select(ProjectFunction).where(
@@ -583,11 +706,16 @@ async def get_project_function(
) )
func_obj = result.scalars().first() func_obj = result.scalars().first()
if not func_obj: if not func_obj:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Function not found"
)
return ProjectFunctionResponse.model_validate(func_obj) return ProjectFunctionResponse.model_validate(func_obj)
@router.put("/{project_id}/function-groups/{fg_id}/functions/{func_id}", response_model=ProjectFunctionResponse) @router.put(
"/{project_id}/function-groups/{fg_id}/functions/{func_id}",
response_model=ProjectFunctionResponse,
)
async def update_project_function( async def update_project_function(
project_id: str, project_id: str,
fg_id: str, fg_id: str,
@@ -600,10 +728,14 @@ async def update_project_function(
account_id = current_user.account_id account_id = current_user.account_id
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
fg_result = await session.execute( fg_result = await session.execute(
select(ProjectFunctionGroup.id).where( select(ProjectFunctionGroup.id).where(
@@ -612,7 +744,9 @@ async def update_project_function(
) )
) )
if not fg_result.scalars().first(): if not fg_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found"
)
result = await session.execute( result = await session.execute(
select(ProjectFunction).where( select(ProjectFunction).where(
@@ -622,7 +756,9 @@ async def update_project_function(
) )
func_obj = result.scalars().first() func_obj = result.scalars().first()
if not func_obj: if not func_obj:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Function not found"
)
update_data = body.model_dump(exclude_unset=True) update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
@@ -633,7 +769,10 @@ async def update_project_function(
return ProjectFunctionResponse.model_validate(func_obj) return ProjectFunctionResponse.model_validate(func_obj)
@router.delete("/{project_id}/function-groups/{fg_id}/functions/{func_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete(
"/{project_id}/function-groups/{fg_id}/functions/{func_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_project_function( async def delete_project_function(
project_id: str, project_id: str,
fg_id: str, fg_id: str,
@@ -645,10 +784,14 @@ async def delete_project_function(
account_id = current_user.account_id account_id = current_user.account_id
proj_result = await session.execute( proj_result = await session.execute(
select(Project.id).where(Project.id == project_id, Project.account_id == account_id) select(Project.id).where(
Project.id == project_id, Project.account_id == account_id
)
) )
if not proj_result.scalars().first(): if not proj_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
)
fg_result = await session.execute( fg_result = await session.execute(
select(ProjectFunctionGroup.id).where( select(ProjectFunctionGroup.id).where(
@@ -657,7 +800,9 @@ async def delete_project_function(
) )
) )
if not fg_result.scalars().first(): if not fg_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Function group not found"
)
result = await session.execute( result = await session.execute(
select(ProjectFunction).where( select(ProjectFunction).where(
@@ -667,7 +812,9 @@ async def delete_project_function(
) )
func_obj = result.scalars().first() func_obj = result.scalars().first()
if not func_obj: if not func_obj:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Function not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Function not found"
)
await session.delete(func_obj) await session.delete(func_obj)
await session.commit() await session.commit()
+1 -1
View File
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user, require_permission from app.api.deps import require_permission
from app.db.session import get_async_session from app.db.session import get_async_session
from app.models import User, Role from app.models import User, Role
from app.schemas.role import RoleCreateRequest, RoleUpdateRequest, RoleResponse from app.schemas.role import RoleCreateRequest, RoleUpdateRequest, RoleResponse
+4 -1
View File
@@ -12,7 +12,10 @@ from app.api.v1.equipment import router as equipment_router
from app.api.v1.stock_locations import router as stock_locations_router from app.api.v1.stock_locations import router as stock_locations_router
from app.api.v1.equipment_groups import router as equipment_groups_router from app.api.v1.equipment_groups import router as equipment_groups_router
from app.api.v1.crew import router as crew_router, avail_router as crew_avail_router from app.api.v1.crew import router as crew_router, avail_router as crew_avail_router
from app.api.v1.vehicles import router as vehicles_router, assign_router as vehicle_assign_router from app.api.v1.vehicles import (
router as vehicles_router,
assign_router as vehicle_assign_router,
)
from app.api.v1.projects import router as projects_router from app.api.v1.projects import router as projects_router
api_v1_router = APIRouter() api_v1_router = APIRouter()
+16 -7
View File
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy import select, func from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user, require_permission from app.api.deps import require_permission
from app.db.session import get_async_session from app.db.session import get_async_session
from app.models import User, StockLocation from app.models import User, StockLocation
from app.schemas.stock_location import ( from app.schemas.stock_location import (
@@ -48,7 +48,9 @@ async def list_stock_locations(
) )
@router.post("", response_model=StockLocationResponse, status_code=status.HTTP_201_CREATED) @router.post(
"", response_model=StockLocationResponse, status_code=status.HTTP_201_CREATED
)
async def create_stock_location( async def create_stock_location(
body: StockLocationCreateRequest, body: StockLocationCreateRequest,
current_user: User = Depends(require_permission("equipment:write")), current_user: User = Depends(require_permission("equipment:write")),
@@ -60,8 +62,9 @@ async def create_stock_location(
# If this is set as default, unset others # If this is set as default, unset others
if body.is_default: if body.is_default:
await session.execute( await session.execute(
select(StockLocation) select(StockLocation).where(
.where(StockLocation.account_id == account_id, StockLocation.is_default == True) # noqa: E712 StockLocation.account_id == account_id, StockLocation.is_default == True
) # noqa: E712
) )
loc = StockLocation(account_id=account_id, **body.model_dump()) loc = StockLocation(account_id=account_id, **body.model_dump())
@@ -87,7 +90,9 @@ async def get_stock_location(
) )
loc = result.scalars().first() loc = result.scalars().first()
if not loc: if not loc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found"
)
return StockLocationResponse.model_validate(loc) return StockLocationResponse.model_validate(loc)
@@ -108,7 +113,9 @@ async def update_stock_location(
) )
loc = result.scalars().first() loc = result.scalars().first()
if not loc: if not loc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found"
)
update_data = body.model_dump(exclude_unset=True) update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
@@ -135,7 +142,9 @@ async def delete_stock_location(
) )
loc = result.scalars().first() loc = result.scalars().first()
if not loc: if not loc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Stock location not found"
)
await session.delete(loc) await session.delete(loc)
await session.commit() await session.commit()
+2 -2
View File
@@ -1,10 +1,10 @@
"""Tags endpoints for listing and creating tags within a tenant.""" """Tags endpoints for listing and creating tags within a tenant."""
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select, func from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user, require_permission from app.api.deps import require_permission
from app.db.session import get_async_session from app.db.session import get_async_session
from app.models import User, Tag from app.models import User, Tag
from app.schemas.contact import TagResponse from app.schemas.contact import TagResponse
+7 -2
View File
@@ -4,11 +4,16 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy import select, func from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user, require_permission from app.api.deps import require_permission
from app.core.security import get_password_hash from app.core.security import get_password_hash
from app.db.session import get_async_session from app.db.session import get_async_session
from app.models import User from app.models import User
from app.schemas.user import UserCreateRequest, UserUpdateRequest, UserResponse, UserListResponse from app.schemas.user import (
UserCreateRequest,
UserUpdateRequest,
UserResponse,
UserListResponse,
)
router = APIRouter(prefix="/users", tags=["users"]) router = APIRouter(prefix="/users", tags=["users"])
+82 -21
View File
@@ -5,7 +5,7 @@ from sqlalchemy import select, func, or_
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from app.api.deps import get_current_user, require_permission from app.api.deps import require_permission
from app.db.session import get_async_session from app.db.session import get_async_session
from app.models import User, Vehicle, VehicleAssignment from app.models import User, Vehicle, VehicleAssignment
from app.schemas.vehicle import ( from app.schemas.vehicle import (
@@ -25,6 +25,7 @@ assign_router = APIRouter(prefix="/vehicle-assignments", tags=["vehicle-assignme
# ===== Vehicle Endpoints ===== # ===== Vehicle Endpoints =====
@router.get("", response_model=VehicleListResponse) @router.get("", response_model=VehicleListResponse)
async def list_vehicles( async def list_vehicles(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
@@ -41,8 +42,12 @@ async def list_vehicles(
if search: if search:
pattern = f"%{search}%" pattern = f"%{search}%"
base_q = base_q.where( base_q = base_q.where(
or_(Vehicle.name.ilike(pattern), Vehicle.license_plate.ilike(pattern), or_(
Vehicle.brand.ilike(pattern), Vehicle.model.ilike(pattern)) Vehicle.name.ilike(pattern),
Vehicle.license_plate.ilike(pattern),
Vehicle.brand.ilike(pattern),
Vehicle.model.ilike(pattern),
)
) )
if vehicle_type: if vehicle_type:
base_q = base_q.where(Vehicle.vehicle_type == vehicle_type) base_q = base_q.where(Vehicle.vehicle_type == vehicle_type)
@@ -52,11 +57,21 @@ async def list_vehicles(
count_q = select(func.count()).select_from(base_q.subquery()) count_q = select(func.count()).select_from(base_q.subquery())
total = (await session.execute(count_q)).scalar() or 0 total = (await session.execute(count_q)).scalar() or 0
q = base_q.options(selectinload(Vehicle.assignments)).order_by(Vehicle.name).offset((page-1)*size).limit(size) q = (
base_q.options(selectinload(Vehicle.assignments))
.order_by(Vehicle.name)
.offset((page - 1) * size)
.limit(size)
)
result = await session.execute(q) result = await session.execute(q)
items = result.unique().scalars().all() items = result.unique().scalars().all()
return VehicleListResponse(items=[VehicleResponse.model_validate(i) for i in items], total=total, page=page, size=size) return VehicleListResponse(
items=[VehicleResponse.model_validate(i) for i in items],
total=total,
page=page,
size=size,
)
@router.post("", response_model=VehicleResponse, status_code=status.HTTP_201_CREATED) @router.post("", response_model=VehicleResponse, status_code=status.HTTP_201_CREATED)
@@ -81,12 +96,15 @@ async def get_vehicle(
): ):
account_id = current_user.account_id account_id = current_user.account_id
result = await session.execute( result = await session.execute(
select(Vehicle).options(selectinload(Vehicle.assignments)) select(Vehicle)
.options(selectinload(Vehicle.assignments))
.where(Vehicle.id == vehicle_id, Vehicle.account_id == account_id) .where(Vehicle.id == vehicle_id, Vehicle.account_id == account_id)
) )
vehicle = result.unique().scalars().first() vehicle = result.unique().scalars().first()
if not vehicle: if not vehicle:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found"
)
return VehicleResponse.model_validate(vehicle) return VehicleResponse.model_validate(vehicle)
@@ -99,12 +117,15 @@ async def update_vehicle(
): ):
account_id = current_user.account_id account_id = current_user.account_id
result = await session.execute( result = await session.execute(
select(Vehicle).options(selectinload(Vehicle.assignments)) select(Vehicle)
.options(selectinload(Vehicle.assignments))
.where(Vehicle.id == vehicle_id, Vehicle.account_id == account_id) .where(Vehicle.id == vehicle_id, Vehicle.account_id == account_id)
) )
vehicle = result.unique().scalars().first() vehicle = result.unique().scalars().first()
if not vehicle: if not vehicle:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found"
)
update_data = body.model_dump(exclude_unset=True) update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
setattr(vehicle, field, value) setattr(vehicle, field, value)
@@ -120,10 +141,16 @@ async def delete_vehicle(
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
): ):
account_id = current_user.account_id account_id = current_user.account_id
result = await session.execute(select(Vehicle).where(Vehicle.id == vehicle_id, Vehicle.account_id == account_id)) result = await session.execute(
select(Vehicle).where(
Vehicle.id == vehicle_id, Vehicle.account_id == account_id
)
)
vehicle = result.scalars().first() vehicle = result.scalars().first()
if not vehicle: if not vehicle:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found"
)
await session.delete(vehicle) await session.delete(vehicle)
await session.commit() await session.commit()
return None return None
@@ -131,6 +158,7 @@ async def delete_vehicle(
# ===== VehicleAssignment Endpoints ===== # ===== VehicleAssignment Endpoints =====
@assign_router.get("", response_model=VehicleAssignmentListResponse) @assign_router.get("", response_model=VehicleAssignmentListResponse)
async def list_assignments( async def list_assignments(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
@@ -141,28 +169,47 @@ async def list_assignments(
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
): ):
account_id = current_user.account_id account_id = current_user.account_id
base_q = select(VehicleAssignment).join(Vehicle).where(Vehicle.account_id == account_id) base_q = (
select(VehicleAssignment).join(Vehicle).where(Vehicle.account_id == account_id)
)
if vehicle_id: if vehicle_id:
base_q = base_q.where(VehicleAssignment.vehicle_id == vehicle_id) base_q = base_q.where(VehicleAssignment.vehicle_id == vehicle_id)
if status: if status:
base_q = base_q.where(VehicleAssignment.status == status) base_q = base_q.where(VehicleAssignment.status == status)
count_q = select(func.count()).select_from(base_q.subquery()) count_q = select(func.count()).select_from(base_q.subquery())
total = (await session.execute(count_q)).scalar() or 0 total = (await session.execute(count_q)).scalar() or 0
q = base_q.order_by(VehicleAssignment.start_date.desc()).offset((page-1)*size).limit(size) q = (
base_q.order_by(VehicleAssignment.start_date.desc())
.offset((page - 1) * size)
.limit(size)
)
result = await session.execute(q) result = await session.execute(q)
items = result.scalars().all() items = result.scalars().all()
return VehicleAssignmentListResponse(items=[VehicleAssignmentResponse.model_validate(i) for i in items], total=total, page=page, size=size) return VehicleAssignmentListResponse(
items=[VehicleAssignmentResponse.model_validate(i) for i in items],
total=total,
page=page,
size=size,
)
@assign_router.post("", response_model=VehicleAssignmentResponse, status_code=status.HTTP_201_CREATED) @assign_router.post(
"", response_model=VehicleAssignmentResponse, status_code=status.HTTP_201_CREATED
)
async def create_assignment( async def create_assignment(
body: VehicleAssignmentCreateRequest, body: VehicleAssignmentCreateRequest,
current_user: User = Depends(require_permission("vehicles:write")), current_user: User = Depends(require_permission("vehicles:write")),
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
): ):
v_result = await session.execute(select(Vehicle).where(Vehicle.id == body.vehicle_id, Vehicle.account_id == current_user.account_id)) v_result = await session.execute(
select(Vehicle).where(
Vehicle.id == body.vehicle_id, Vehicle.account_id == current_user.account_id
)
)
if not v_result.scalars().first(): if not v_result.scalars().first():
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Vehicle not found"
)
assign = VehicleAssignment(**body.model_dump()) assign = VehicleAssignment(**body.model_dump())
session.add(assign) session.add(assign)
await session.commit() await session.commit()
@@ -178,11 +225,18 @@ async def update_assignment(
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
): ):
result = await session.execute( result = await session.execute(
select(VehicleAssignment).join(Vehicle).where(VehicleAssignment.id == assign_id, Vehicle.account_id == current_user.account_id) select(VehicleAssignment)
.join(Vehicle)
.where(
VehicleAssignment.id == assign_id,
Vehicle.account_id == current_user.account_id,
)
) )
assign = result.scalars().first() assign = result.scalars().first()
if not assign: if not assign:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found"
)
update_data = body.model_dump(exclude_unset=True) update_data = body.model_dump(exclude_unset=True)
for field, value in update_data.items(): for field, value in update_data.items():
setattr(assign, field, value) setattr(assign, field, value)
@@ -198,11 +252,18 @@ async def delete_assignment(
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
): ):
result = await session.execute( result = await session.execute(
select(VehicleAssignment).join(Vehicle).where(VehicleAssignment.id == assign_id, Vehicle.account_id == current_user.account_id) select(VehicleAssignment)
.join(Vehicle)
.where(
VehicleAssignment.id == assign_id,
Vehicle.account_id == current_user.account_id,
)
) )
assign = result.scalars().first() assign = result.scalars().first()
if not assign: if not assign:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Assignment not found"
)
await session.delete(assign) await session.delete(assign)
await session.commit() await session.commit()
return None return None
+3 -2
View File
@@ -1,7 +1,6 @@
"""Application configuration using pydantic-settings.""" """Application configuration using pydantic-settings."""
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings): class Settings(BaseSettings):
@@ -38,7 +37,9 @@ class Settings(BaseSettings):
@property @property
def cors_origin_list(self) -> list[str]: def cors_origin_list(self) -> list[str]:
"""Return CORS origins as a list.""" """Return CORS origins as a list."""
return [origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()] return [
origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()
]
model_config = { model_config = {
"env_file": ".env", "env_file": ".env",
+15 -5
View File
@@ -21,26 +21,36 @@ def get_password_hash(password: str) -> str:
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def create_access_token(subject: str | Any, expires_delta: timedelta | None = None) -> str: def create_access_token(
subject: str | Any, expires_delta: timedelta | None = None
) -> str:
"""Create a short-lived JWT access token.""" """Create a short-lived JWT access token."""
if expires_delta is None: if expires_delta is None:
expires_delta = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) expires_delta = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
expire = now + expires_delta expire = now + expires_delta
to_encode = {"exp": expire, "sub": str(subject), "iat": now, "type": "access"} to_encode = {"exp": expire, "sub": str(subject), "iat": now, "type": "access"}
return jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM) return jwt.encode(
to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM
)
def create_refresh_token(subject: str | Any, expires_delta: timedelta | None = None) -> str: def create_refresh_token(
subject: str | Any, expires_delta: timedelta | None = None
) -> str:
"""Create a long-lived JWT refresh token.""" """Create a long-lived JWT refresh token."""
if expires_delta is None: if expires_delta is None:
expires_delta = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES) expires_delta = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
expire = now + expires_delta expire = now + expires_delta
to_encode = {"exp": expire, "sub": str(subject), "iat": now, "type": "refresh"} to_encode = {"exp": expire, "sub": str(subject), "iat": now, "type": "refresh"}
return jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM) return jwt.encode(
to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM
)
def verify_token(token: str) -> dict: def verify_token(token: str) -> dict:
"""Decode and verify a JWT token. Returns the payload dict.""" """Decode and verify a JWT token. Returns the payload dict."""
return jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]) return jwt.decode(
token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]
)
+1
View File
@@ -5,4 +5,5 @@ from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase): class Base(DeclarativeBase):
"""Base class for all database models.""" """Base class for all database models."""
pass pass
+18 -6
View File
@@ -25,11 +25,23 @@ class Account(Base):
# Relationships # Relationships
users: Mapped[list["User"]] = relationship("User", back_populates="account") users: Mapped[list["User"]] = relationship("User", back_populates="account")
roles: Mapped[list["Role"]] = relationship("Role", back_populates="account") roles: Mapped[list["Role"]] = relationship("Role", back_populates="account")
contacts: Mapped[list["Contact"]] = relationship("Contact", back_populates="account") contacts: Mapped[list["Contact"]] = relationship(
"Contact", back_populates="account"
)
tags: Mapped[list["Tag"]] = relationship("Tag", back_populates="account") tags: Mapped[list["Tag"]] = relationship("Tag", back_populates="account")
equipment: Mapped[list["Equipment"]] = relationship("Equipment", back_populates="account") equipment: Mapped[list["Equipment"]] = relationship(
stock_locations: Mapped[list["StockLocation"]] = relationship("StockLocation", back_populates="account") "Equipment", back_populates="account"
equipment_groups: Mapped[list["EquipmentGroup"]] = relationship("EquipmentGroup", back_populates="account") )
stock_locations: Mapped[list["StockLocation"]] = relationship(
"StockLocation", back_populates="account"
)
equipment_groups: Mapped[list["EquipmentGroup"]] = relationship(
"EquipmentGroup", back_populates="account"
)
crew_members: Mapped[list["Crew"]] = relationship("Crew", back_populates="account") crew_members: Mapped[list["Crew"]] = relationship("Crew", back_populates="account")
vehicles: Mapped[list["Vehicle"]] = relationship("Vehicle", back_populates="account") vehicles: Mapped[list["Vehicle"]] = relationship(
projects: Mapped[list["Project"]] = relationship("Project", back_populates="account") "Vehicle", back_populates="account"
)
projects: Mapped[list["Project"]] = relationship(
"Project", back_populates="account"
)
+1 -3
View File
@@ -63,9 +63,7 @@ class CrewAvailability(Base):
start_date: Mapped[datetime] = mapped_column( start_date: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False DateTime(timezone=True), nullable=False
) )
end_date: Mapped[datetime] = mapped_column( end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
DateTime(timezone=True), nullable=False
)
status: Mapped[str] = mapped_column( status: Mapped[str] = mapped_column(
String(20), nullable=False, default="available" String(20), nullable=False, default="available"
) # available, booked, unavailable ) # available, booked, unavailable
+4 -2
View File
@@ -3,7 +3,7 @@
import uuid import uuid
from datetime import datetime from datetime import datetime
from sqlalchemy import String, DateTime, ForeignKey, func, Float, Integer, Text from sqlalchemy import String, DateTime, ForeignKey, func, Float, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base from app.db.base import Base
@@ -23,7 +23,9 @@ class Equipment(Base):
name: Mapped[str] = mapped_column(String(255), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False)
category: Mapped[str | None] = mapped_column(String(100), nullable=True) category: Mapped[str | None] = mapped_column(String(100), nullable=True)
brand: Mapped[str | None] = mapped_column(String(255), nullable=True) brand: Mapped[str | None] = mapped_column(String(255), nullable=True)
serial_number: Mapped[str | None] = mapped_column(String(100), nullable=True, unique=True) serial_number: Mapped[str | None] = mapped_column(
String(100), nullable=True, unique=True
)
barcode: Mapped[str | None] = mapped_column(String(100), nullable=True, unique=True) barcode: Mapped[str | None] = mapped_column(String(100), nullable=True, unique=True)
qr_code: Mapped[str | None] = mapped_column(String(500), nullable=True) qr_code: Mapped[str | None] = mapped_column(String(500), nullable=True)
status: Mapped[str] = mapped_column( status: Mapped[str] = mapped_column(
+3 -1
View File
@@ -62,7 +62,9 @@ class EquipmentGroup(Base):
) )
# Relationships # Relationships
account: Mapped["Account"] = relationship("Account", back_populates="equipment_groups") account: Mapped["Account"] = relationship(
"Account", back_populates="equipment_groups"
)
default_location: Mapped["StockLocation"] = relationship( default_location: Mapped["StockLocation"] = relationship(
"StockLocation", back_populates="equipment_groups" "StockLocation", back_populates="equipment_groups"
) )
+24 -8
View File
@@ -111,8 +111,12 @@ class ProjectFunctionGroup(Base):
String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
) )
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
start_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) start_date: Mapped[datetime | None] = mapped_column(
end_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) DateTime(timezone=True), nullable=True
)
end_date: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Relationships # Relationships
project: Mapped["Project"] = relationship( project: Mapped["Project"] = relationship(
@@ -140,7 +144,9 @@ class ProjectFunction(Base):
ForeignKey("project_function_groups.id", ondelete="CASCADE"), ForeignKey("project_function_groups.id", ondelete="CASCADE"),
nullable=False, nullable=False,
) )
function_type: Mapped[str] = mapped_column(String(50), nullable=False, default='crew') function_type: Mapped[str] = mapped_column(
String(50), nullable=False, default="crew"
)
quantity: Mapped[float] = mapped_column(Float, nullable=False, default=1.0) quantity: Mapped[float] = mapped_column(Float, nullable=False, default=1.0)
daily_costs: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) daily_costs: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
total_costs: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) total_costs: Mapped[float] = mapped_column(Float, nullable=False, default=0.0)
@@ -171,11 +177,17 @@ class ProjectEquipmentGroup(Base):
String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False String(36), ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
) )
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
start_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) start_date: Mapped[datetime | None] = mapped_column(
end_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) DateTime(timezone=True), nullable=True
)
end_date: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
# Relationships # Relationships
project: Mapped["Project"] = relationship("Project", back_populates="equipment_groups") project: Mapped["Project"] = relationship(
"Project", back_populates="equipment_groups"
)
items: Mapped[list["ProjectEquipment"]] = relationship( items: Mapped[list["ProjectEquipment"]] = relationship(
"ProjectEquipment", back_populates="group", cascade="all, delete-orphan" "ProjectEquipment", back_populates="group", cascade="all, delete-orphan"
) )
@@ -234,8 +246,12 @@ class ProjectCrew(Base):
crew_id: Mapped[str] = mapped_column( crew_id: Mapped[str] = mapped_column(
String(36), ForeignKey("crew.id", ondelete="CASCADE"), nullable=False String(36), ForeignKey("crew.id", ondelete="CASCADE"), nullable=False
) )
start_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) start_date: Mapped[datetime | None] = mapped_column(
end_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) 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) notes: Mapped[str | None] = mapped_column(Text, nullable=True)
# Relationships # Relationships
+3 -1
View File
@@ -35,7 +35,9 @@ class StockLocation(Base):
) )
# Relationships # Relationships
account: Mapped["Account"] = relationship("Account", back_populates="stock_locations") account: Mapped["Account"] = relationship(
"Account", back_populates="stock_locations"
)
equipment_items: Mapped[list["Equipment"]] = relationship( equipment_items: Mapped[list["Equipment"]] = relationship(
"Equipment", back_populates="location" "Equipment", back_populates="location"
) )
+4 -1
View File
@@ -31,7 +31,10 @@ class User(Base):
DateTime(timezone=True), server_default=func.now(), nullable=False DateTime(timezone=True), server_default=func.now(), nullable=False
) )
updated_at: Mapped[datetime] = mapped_column( updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
) )
# Relationships # Relationships
+8 -8
View File
@@ -21,7 +21,9 @@ class Vehicle(Base):
String(36), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False String(36), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False
) )
name: Mapped[str] = mapped_column(String(255), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False)
license_plate: Mapped[str | None] = mapped_column(String(50), nullable=True, unique=True) license_plate: Mapped[str | None] = mapped_column(
String(50), nullable=True, unique=True
)
brand: Mapped[str | None] = mapped_column(String(100), nullable=True) brand: Mapped[str | None] = mapped_column(String(100), nullable=True)
model: Mapped[str | None] = mapped_column(String(100), nullable=True) model: Mapped[str | None] = mapped_column(String(100), nullable=True)
year: Mapped[int | None] = mapped_column(nullable=True) year: Mapped[int | None] = mapped_column(nullable=True)
@@ -64,15 +66,11 @@ class VehicleAssignment(Base):
vehicle_id: Mapped[str] = mapped_column( vehicle_id: Mapped[str] = mapped_column(
String(36), ForeignKey("vehicles.id", ondelete="CASCADE"), nullable=False String(36), ForeignKey("vehicles.id", ondelete="CASCADE"), nullable=False
) )
project_id: Mapped[str | None] = mapped_column( project_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
String(36), nullable=True
)
start_date: Mapped[datetime] = mapped_column( start_date: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False DateTime(timezone=True), nullable=False
) )
end_date: Mapped[datetime] = mapped_column( end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
DateTime(timezone=True), nullable=False
)
status: Mapped[str] = mapped_column( status: Mapped[str] = mapped_column(
String(20), nullable=False, default="assigned" String(20), nullable=False, default="assigned"
) # assigned, in_use, returned ) # assigned, in_use, returned
@@ -92,4 +90,6 @@ class VehicleAssignment(Base):
vehicle: Mapped["Vehicle"] = relationship("Vehicle", back_populates="assignments") vehicle: Mapped["Vehicle"] = relationship("Vehicle", back_populates="assignments")
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<VehicleAssignment {self.vehicle_id}: {self.start_date}-{self.end_date}>" return (
f"<VehicleAssignment {self.vehicle_id}: {self.start_date}-{self.end_date}>"
)
+15 -3
View File
@@ -5,25 +5,35 @@ from pydantic import BaseModel, EmailStr, Field
class RegisterRequest(BaseModel): class RegisterRequest(BaseModel):
"""Request body for self-service registration.""" """Request body for self-service registration."""
account_name: str = Field(..., min_length=2, max_length=255, description="Company/account name")
full_name: str = Field(..., min_length=2, max_length=255, description="Admin user's full name") account_name: str = Field(
..., min_length=2, max_length=255, description="Company/account name"
)
full_name: str = Field(
..., min_length=2, max_length=255, description="Admin user's full name"
)
email: EmailStr = Field(..., description="Admin user's email address") email: EmailStr = Field(..., description="Admin user's email address")
password: str = Field(..., min_length=8, max_length=128, description="Password (min 8 characters)") password: str = Field(
..., min_length=8, max_length=128, description="Password (min 8 characters)"
)
class LoginRequest(BaseModel): class LoginRequest(BaseModel):
"""Request body for login.""" """Request body for login."""
email: EmailStr email: EmailStr
password: str password: str
class RefreshRequest(BaseModel): class RefreshRequest(BaseModel):
"""Request body for token refresh.""" """Request body for token refresh."""
refresh_token: str refresh_token: str
class UserInfo(BaseModel): class UserInfo(BaseModel):
"""Basic user info returned with tokens (no password hash).""" """Basic user info returned with tokens (no password hash)."""
id: str id: str
email: str email: str
full_name: str full_name: str
@@ -35,6 +45,7 @@ class UserInfo(BaseModel):
class TokenResponse(BaseModel): class TokenResponse(BaseModel):
"""Response with access and refresh tokens, plus user info.""" """Response with access and refresh tokens, plus user info."""
access_token: str access_token: str
refresh_token: str refresh_token: str
token_type: str = "bearer" token_type: str = "bearer"
@@ -43,6 +54,7 @@ class TokenResponse(BaseModel):
class UserResponse(BaseModel): class UserResponse(BaseModel):
"""Public user representation (no password hash).""" """Public user representation (no password hash)."""
id: str id: str
email: str email: str
full_name: str full_name: str
+6 -1
View File
@@ -1,10 +1,11 @@
"""Pydantic schemas for contacts and tags.""" """Pydantic schemas for contacts and tags."""
from pydantic import BaseModel, Field, EmailStr from pydantic import BaseModel, Field
class TagResponse(BaseModel): class TagResponse(BaseModel):
"""Public tag representation.""" """Public tag representation."""
id: str id: str
name: str name: str
color: str | None = None color: str | None = None
@@ -14,6 +15,7 @@ class TagResponse(BaseModel):
class ContactCreateRequest(BaseModel): class ContactCreateRequest(BaseModel):
"""Request body for creating a new contact.""" """Request body for creating a new contact."""
type: str = Field(..., pattern="^(company|person)$") type: str = Field(..., pattern="^(company|person)$")
company_name: str | None = Field(None, max_length=255) company_name: str | None = Field(None, max_length=255)
first_name: str | None = Field(None, max_length=100) first_name: str | None = Field(None, max_length=100)
@@ -39,6 +41,7 @@ class ContactCreateRequest(BaseModel):
class ContactUpdateRequest(BaseModel): class ContactUpdateRequest(BaseModel):
"""Request body for updating an existing contact.""" """Request body for updating an existing contact."""
type: str | None = Field(None, pattern="^(company|person)$") type: str | None = Field(None, pattern="^(company|person)$")
company_name: str | None = Field(None, max_length=255) company_name: str | None = Field(None, max_length=255)
first_name: str | None = Field(None, max_length=100) first_name: str | None = Field(None, max_length=100)
@@ -64,6 +67,7 @@ class ContactUpdateRequest(BaseModel):
class ContactResponse(BaseModel): class ContactResponse(BaseModel):
"""Public contact representation with tags.""" """Public contact representation with tags."""
id: str id: str
account_id: str account_id: str
type: str type: str
@@ -95,6 +99,7 @@ class ContactResponse(BaseModel):
class ContactListResponse(BaseModel): class ContactListResponse(BaseModel):
"""Paginated list of contacts.""" """Paginated list of contacts."""
items: list[ContactResponse] items: list[ContactResponse]
total: int total: int
page: int page: int
+8 -1
View File
@@ -1,11 +1,11 @@
"""Pydantic schemas for Crew and CrewAvailability.""" """Pydantic schemas for Crew and CrewAvailability."""
from datetime import datetime
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
class CrewAvailabilityResponse(BaseModel): class CrewAvailabilityResponse(BaseModel):
"""Public crew availability representation.""" """Public crew availability representation."""
id: str id: str
crew_id: str crew_id: str
start_date: str start_date: str
@@ -20,6 +20,7 @@ class CrewAvailabilityResponse(BaseModel):
class CrewCreateRequest(BaseModel): class CrewCreateRequest(BaseModel):
"""Request body for creating a new crew member.""" """Request body for creating a new crew member."""
first_name: str = Field(..., max_length=100) first_name: str = Field(..., max_length=100)
last_name: str = Field(..., max_length=100) last_name: str = Field(..., max_length=100)
email: str | None = Field(None, max_length=255) email: str | None = Field(None, max_length=255)
@@ -32,6 +33,7 @@ class CrewCreateRequest(BaseModel):
class CrewUpdateRequest(BaseModel): class CrewUpdateRequest(BaseModel):
"""Request body for updating a crew member.""" """Request body for updating a crew member."""
first_name: str | None = Field(None, max_length=100) first_name: str | None = Field(None, max_length=100)
last_name: str | None = Field(None, max_length=100) last_name: str | None = Field(None, max_length=100)
email: str | None = Field(None, max_length=255) email: str | None = Field(None, max_length=255)
@@ -44,6 +46,7 @@ class CrewUpdateRequest(BaseModel):
class CrewResponse(BaseModel): class CrewResponse(BaseModel):
"""Public crew member representation.""" """Public crew member representation."""
id: str id: str
account_id: str account_id: str
first_name: str first_name: str
@@ -63,6 +66,7 @@ class CrewResponse(BaseModel):
class CrewListResponse(BaseModel): class CrewListResponse(BaseModel):
"""Paginated list of crew members.""" """Paginated list of crew members."""
items: list[CrewResponse] items: list[CrewResponse]
total: int total: int
page: int page: int
@@ -71,6 +75,7 @@ class CrewListResponse(BaseModel):
class CrewAvailabilityCreateRequest(BaseModel): class CrewAvailabilityCreateRequest(BaseModel):
"""Request body for creating crew availability.""" """Request body for creating crew availability."""
crew_id: str crew_id: str
start_date: str = Field(..., description="ISO datetime with timezone") start_date: str = Field(..., description="ISO datetime with timezone")
end_date: str = Field(..., description="ISO datetime with timezone") end_date: str = Field(..., description="ISO datetime with timezone")
@@ -80,6 +85,7 @@ class CrewAvailabilityCreateRequest(BaseModel):
class CrewAvailabilityUpdateRequest(BaseModel): class CrewAvailabilityUpdateRequest(BaseModel):
"""Request body for updating crew availability.""" """Request body for updating crew availability."""
start_date: str | None = None start_date: str | None = None
end_date: str | None = None end_date: str | None = None
status: str | None = Field(None, max_length=20) status: str | None = Field(None, max_length=20)
@@ -88,6 +94,7 @@ class CrewAvailabilityUpdateRequest(BaseModel):
class CrewAvailabilityListResponse(BaseModel): class CrewAvailabilityListResponse(BaseModel):
"""Paginated list of crew availabilities.""" """Paginated list of crew availabilities."""
items: list[CrewAvailabilityResponse] items: list[CrewAvailabilityResponse]
total: int total: int
page: int page: int
+6 -1
View File
@@ -1,11 +1,11 @@
"""Pydantic schemas for Equipment (inventory catalog).""" """Pydantic schemas for Equipment (inventory catalog)."""
from datetime import datetime
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
class EquipmentCreateRequest(BaseModel): class EquipmentCreateRequest(BaseModel):
"""Request body for creating new equipment.""" """Request body for creating new equipment."""
name: str = Field(..., max_length=255) name: str = Field(..., max_length=255)
category: str | None = Field(None, max_length=100) category: str | None = Field(None, max_length=100)
brand: str | None = Field(None, max_length=255) brand: str | None = Field(None, max_length=255)
@@ -26,6 +26,7 @@ class EquipmentCreateRequest(BaseModel):
class EquipmentUpdateRequest(BaseModel): class EquipmentUpdateRequest(BaseModel):
"""Request body for updating existing equipment.""" """Request body for updating existing equipment."""
name: str | None = Field(None, max_length=255) name: str | None = Field(None, max_length=255)
category: str | None = Field(None, max_length=100) category: str | None = Field(None, max_length=100)
brand: str | None = Field(None, max_length=255) brand: str | None = Field(None, max_length=255)
@@ -46,6 +47,7 @@ class EquipmentUpdateRequest(BaseModel):
class LocationRef(BaseModel): class LocationRef(BaseModel):
"""Minimal stock location reference.""" """Minimal stock location reference."""
id: str id: str
name: str name: str
@@ -54,6 +56,7 @@ class LocationRef(BaseModel):
class SupplierRef(BaseModel): class SupplierRef(BaseModel):
"""Minimal supplier (contact) reference.""" """Minimal supplier (contact) reference."""
id: str id: str
name: str | None = None name: str | None = None
company_name: str | None = None company_name: str | None = None
@@ -63,6 +66,7 @@ class SupplierRef(BaseModel):
class EquipmentResponse(BaseModel): class EquipmentResponse(BaseModel):
"""Public equipment representation.""" """Public equipment representation."""
id: str id: str
account_id: str account_id: str
name: str name: str
@@ -89,6 +93,7 @@ class EquipmentResponse(BaseModel):
class EquipmentListResponse(BaseModel): class EquipmentListResponse(BaseModel):
"""Paginated list of equipment.""" """Paginated list of equipment."""
items: list[EquipmentResponse] items: list[EquipmentResponse]
total: int total: int
page: int page: int
+5 -1
View File
@@ -1,7 +1,6 @@
"""Pydantic schemas for EquipmentGroup (Bundles).""" """Pydantic schemas for EquipmentGroup (Bundles)."""
from __future__ import annotations from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -11,6 +10,7 @@ if TYPE_CHECKING:
class EquipmentGroupItem(BaseModel): class EquipmentGroupItem(BaseModel):
"""An item inside a bundle with quantity.""" """An item inside a bundle with quantity."""
equipment_id: str equipment_id: str
name: str | None = None name: str | None = None
quantity: float = 1.0 quantity: float = 1.0
@@ -20,6 +20,7 @@ class EquipmentGroupItem(BaseModel):
class EquipmentGroupCreateRequest(BaseModel): class EquipmentGroupCreateRequest(BaseModel):
"""Request body for creating a new equipment group/bundle.""" """Request body for creating a new equipment group/bundle."""
name: str = Field(..., max_length=255) name: str = Field(..., max_length=255)
description: str | None = None description: str | None = None
daily_rate: float | None = None daily_rate: float | None = None
@@ -29,6 +30,7 @@ class EquipmentGroupCreateRequest(BaseModel):
class EquipmentGroupUpdateRequest(BaseModel): class EquipmentGroupUpdateRequest(BaseModel):
"""Request body for updating an equipment group.""" """Request body for updating an equipment group."""
name: str | None = Field(None, max_length=255) name: str | None = Field(None, max_length=255)
description: str | None = None description: str | None = None
daily_rate: float | None = None daily_rate: float | None = None
@@ -38,6 +40,7 @@ class EquipmentGroupUpdateRequest(BaseModel):
class EquipmentGroupResponse(BaseModel): class EquipmentGroupResponse(BaseModel):
"""Public equipment group representation.""" """Public equipment group representation."""
id: str id: str
account_id: str account_id: str
name: str name: str
@@ -53,6 +56,7 @@ class EquipmentGroupResponse(BaseModel):
class EquipmentGroupListResponse(BaseModel): class EquipmentGroupListResponse(BaseModel):
"""Paginated list of equipment groups.""" """Paginated list of equipment groups."""
items: list[EquipmentGroupResponse] items: list[EquipmentGroupResponse]
total: int total: int
page: int page: int
+31
View File
@@ -6,8 +6,10 @@ from pydantic import BaseModel, Field
# ===== Project Schemas ===== # ===== Project Schemas =====
class ProjectCreateRequest(BaseModel): class ProjectCreateRequest(BaseModel):
"""Request body for creating a new project.""" """Request body for creating a new project."""
name: str = Field(..., max_length=255) name: str = Field(..., max_length=255)
description: str | None = None description: str | None = None
status: str = Field("draft", max_length=20) status: str = Field("draft", max_length=20)
@@ -22,6 +24,7 @@ class ProjectCreateRequest(BaseModel):
class ProjectUpdateRequest(BaseModel): class ProjectUpdateRequest(BaseModel):
"""Request body for updating an existing project.""" """Request body for updating an existing project."""
name: str | None = Field(None, max_length=255) name: str | None = Field(None, max_length=255)
description: str | None = None description: str | None = None
status: str | None = Field(None, max_length=20) status: str | None = Field(None, max_length=20)
@@ -36,6 +39,7 @@ class ProjectUpdateRequest(BaseModel):
class ProjectResponse(BaseModel): class ProjectResponse(BaseModel):
"""Public project representation.""" """Public project representation."""
id: str id: str
account_id: str account_id: str
name: str name: str
@@ -56,6 +60,7 @@ class ProjectResponse(BaseModel):
class ProjectListResponse(BaseModel): class ProjectListResponse(BaseModel):
"""Paginated list of projects.""" """Paginated list of projects."""
items: list[ProjectResponse] items: list[ProjectResponse]
total: int total: int
page: int page: int
@@ -64,8 +69,10 @@ class ProjectListResponse(BaseModel):
# ===== SubProject Schemas ===== # ===== SubProject Schemas =====
class SubProjectCreateRequest(BaseModel): class SubProjectCreateRequest(BaseModel):
"""Request body for creating a subproject.""" """Request body for creating a subproject."""
name: str = Field(..., max_length=255) name: str = Field(..., max_length=255)
parent_id: str | None = None parent_id: str | None = None
sort_order: int = 0 sort_order: int = 0
@@ -73,6 +80,7 @@ class SubProjectCreateRequest(BaseModel):
class SubProjectUpdateRequest(BaseModel): class SubProjectUpdateRequest(BaseModel):
"""Request body for updating a subproject.""" """Request body for updating a subproject."""
name: str | None = Field(None, max_length=255) name: str | None = Field(None, max_length=255)
parent_id: str | None = None parent_id: str | None = None
sort_order: int | None = None sort_order: int | None = None
@@ -80,6 +88,7 @@ class SubProjectUpdateRequest(BaseModel):
class SubProjectResponse(BaseModel): class SubProjectResponse(BaseModel):
"""Public subproject representation.""" """Public subproject representation."""
id: str id: str
name: str name: str
project_id: str project_id: str
@@ -91,6 +100,7 @@ class SubProjectResponse(BaseModel):
class SubProjectListResponse(BaseModel): class SubProjectListResponse(BaseModel):
"""Paginated list of subprojects.""" """Paginated list of subprojects."""
items: list[SubProjectResponse] items: list[SubProjectResponse]
total: int total: int
page: int page: int
@@ -99,8 +109,10 @@ class SubProjectListResponse(BaseModel):
# ===== ProjectFunctionGroup Schemas ===== # ===== ProjectFunctionGroup Schemas =====
class ProjectFunctionGroupCreateRequest(BaseModel): class ProjectFunctionGroupCreateRequest(BaseModel):
"""Request body for creating a function group.""" """Request body for creating a function group."""
name: str = Field(..., max_length=255) name: str = Field(..., max_length=255)
sort_order: int = 0 sort_order: int = 0
start_date: datetime | None = None start_date: datetime | None = None
@@ -109,6 +121,7 @@ class ProjectFunctionGroupCreateRequest(BaseModel):
class ProjectFunctionGroupUpdateRequest(BaseModel): class ProjectFunctionGroupUpdateRequest(BaseModel):
"""Request body for updating a function group.""" """Request body for updating a function group."""
name: str | None = Field(None, max_length=255) name: str | None = Field(None, max_length=255)
sort_order: int | None = None sort_order: int | None = None
start_date: datetime | None = None start_date: datetime | None = None
@@ -117,6 +130,7 @@ class ProjectFunctionGroupUpdateRequest(BaseModel):
class ProjectFunctionGroupResponse(BaseModel): class ProjectFunctionGroupResponse(BaseModel):
"""Public function group representation.""" """Public function group representation."""
id: str id: str
name: str name: str
project_id: str project_id: str
@@ -129,6 +143,7 @@ class ProjectFunctionGroupResponse(BaseModel):
class ProjectFunctionGroupListResponse(BaseModel): class ProjectFunctionGroupListResponse(BaseModel):
"""Paginated list of function groups.""" """Paginated list of function groups."""
items: list[ProjectFunctionGroupResponse] items: list[ProjectFunctionGroupResponse]
total: int total: int
page: int page: int
@@ -137,8 +152,10 @@ class ProjectFunctionGroupListResponse(BaseModel):
# ===== ProjectFunction Schemas ===== # ===== ProjectFunction Schemas =====
class ProjectFunctionCreateRequest(BaseModel): class ProjectFunctionCreateRequest(BaseModel):
"""Request body for creating a project function.""" """Request body for creating a project function."""
name: str = Field(..., max_length=255) name: str = Field(..., max_length=255)
function_type: str = "crew" function_type: str = "crew"
quantity: float = 1.0 quantity: float = 1.0
@@ -149,6 +166,7 @@ class ProjectFunctionCreateRequest(BaseModel):
class ProjectFunctionUpdateRequest(BaseModel): class ProjectFunctionUpdateRequest(BaseModel):
"""Request body for updating a project function.""" """Request body for updating a project function."""
name: str | None = Field(None, max_length=255) name: str | None = Field(None, max_length=255)
function_type: str | None = None function_type: str | None = None
quantity: float | None = None quantity: float | None = None
@@ -159,6 +177,7 @@ class ProjectFunctionUpdateRequest(BaseModel):
class ProjectFunctionResponse(BaseModel): class ProjectFunctionResponse(BaseModel):
"""Public project function representation.""" """Public project function representation."""
id: str id: str
name: str name: str
function_group_id: str function_group_id: str
@@ -173,6 +192,7 @@ class ProjectFunctionResponse(BaseModel):
class ProjectFunctionListResponse(BaseModel): class ProjectFunctionListResponse(BaseModel):
"""Paginated list of project functions.""" """Paginated list of project functions."""
items: list[ProjectFunctionResponse] items: list[ProjectFunctionResponse]
total: int total: int
page: int page: int
@@ -181,8 +201,10 @@ class ProjectFunctionListResponse(BaseModel):
# ===== ProjectEquipmentGroup Schemas ===== # ===== ProjectEquipmentGroup Schemas =====
class ProjectEquipmentGroupCreateRequest(BaseModel): class ProjectEquipmentGroupCreateRequest(BaseModel):
"""Request body for creating an equipment group.""" """Request body for creating an equipment group."""
name: str = Field(..., max_length=255) name: str = Field(..., max_length=255)
sort_order: int = 0 sort_order: int = 0
start_date: datetime | None = None start_date: datetime | None = None
@@ -191,6 +213,7 @@ class ProjectEquipmentGroupCreateRequest(BaseModel):
class ProjectEquipmentGroupUpdateRequest(BaseModel): class ProjectEquipmentGroupUpdateRequest(BaseModel):
"""Request body for updating an equipment group.""" """Request body for updating an equipment group."""
name: str | None = Field(None, max_length=255) name: str | None = Field(None, max_length=255)
sort_order: int | None = None sort_order: int | None = None
start_date: datetime | None = None start_date: datetime | None = None
@@ -199,6 +222,7 @@ class ProjectEquipmentGroupUpdateRequest(BaseModel):
class ProjectEquipmentGroupResponse(BaseModel): class ProjectEquipmentGroupResponse(BaseModel):
"""Public equipment group representation.""" """Public equipment group representation."""
id: str id: str
name: str name: str
project_id: str project_id: str
@@ -211,6 +235,7 @@ class ProjectEquipmentGroupResponse(BaseModel):
class ProjectEquipmentGroupListResponse(BaseModel): class ProjectEquipmentGroupListResponse(BaseModel):
"""Paginated list of equipment groups.""" """Paginated list of equipment groups."""
items: list[ProjectEquipmentGroupResponse] items: list[ProjectEquipmentGroupResponse]
total: int total: int
page: int page: int
@@ -219,6 +244,7 @@ class ProjectEquipmentGroupListResponse(BaseModel):
class ProjectEquipmentGroupDetailResponse(BaseModel): class ProjectEquipmentGroupDetailResponse(BaseModel):
"""Equipment group with nested items.""" """Equipment group with nested items."""
id: str id: str
name: str name: str
project_id: str project_id: str
@@ -232,8 +258,10 @@ class ProjectEquipmentGroupDetailResponse(BaseModel):
# ===== ProjectEquipment Schemas ===== # ===== ProjectEquipment Schemas =====
class ProjectEquipmentCreateRequest(BaseModel): class ProjectEquipmentCreateRequest(BaseModel):
"""Request body for adding equipment to a project group.""" """Request body for adding equipment to a project group."""
equipment_id: str | None = None equipment_id: str | None = None
name: str = Field(..., max_length=255) name: str = Field(..., max_length=255)
quantity: float = 1.0 quantity: float = 1.0
@@ -244,6 +272,7 @@ class ProjectEquipmentCreateRequest(BaseModel):
class ProjectEquipmentUpdateRequest(BaseModel): class ProjectEquipmentUpdateRequest(BaseModel):
"""Request body for updating project equipment.""" """Request body for updating project equipment."""
equipment_id: str | None = None equipment_id: str | None = None
name: str | None = Field(None, max_length=255) name: str | None = Field(None, max_length=255)
quantity: float | None = None quantity: float | None = None
@@ -254,6 +283,7 @@ class ProjectEquipmentUpdateRequest(BaseModel):
class ProjectEquipmentResponse(BaseModel): class ProjectEquipmentResponse(BaseModel):
"""Public project equipment representation.""" """Public project equipment representation."""
id: str id: str
project_equipment_group_id: str project_equipment_group_id: str
equipment_id: str | None = None equipment_id: str | None = None
@@ -268,6 +298,7 @@ class ProjectEquipmentResponse(BaseModel):
class ProjectEquipmentListResponse(BaseModel): class ProjectEquipmentListResponse(BaseModel):
"""Paginated list of project equipment.""" """Paginated list of project equipment."""
items: list[ProjectEquipmentResponse] items: list[ProjectEquipmentResponse]
total: int total: int
page: int page: int
+3
View File
@@ -5,6 +5,7 @@ from pydantic import BaseModel, Field
class RoleCreateRequest(BaseModel): class RoleCreateRequest(BaseModel):
"""Request body for creating a new role.""" """Request body for creating a new role."""
name: str = Field(..., min_length=2, max_length=100) name: str = Field(..., min_length=2, max_length=100)
description: str | None = None description: str | None = None
permissions: list[str] = Field(default_factory=list) permissions: list[str] = Field(default_factory=list)
@@ -12,6 +13,7 @@ class RoleCreateRequest(BaseModel):
class RoleUpdateRequest(BaseModel): class RoleUpdateRequest(BaseModel):
"""Request body for updating an existing role.""" """Request body for updating an existing role."""
name: str | None = Field(None, min_length=2, max_length=100) name: str | None = Field(None, min_length=2, max_length=100)
description: str | None = None description: str | None = None
permissions: list[str] | None = None permissions: list[str] | None = None
@@ -19,6 +21,7 @@ class RoleUpdateRequest(BaseModel):
class RoleResponse(BaseModel): class RoleResponse(BaseModel):
"""Public role representation.""" """Public role representation."""
id: str id: str
account_id: str account_id: str
name: str name: str
+4 -1
View File
@@ -1,11 +1,11 @@
"""Pydantic schemas for StockLocation.""" """Pydantic schemas for StockLocation."""
from datetime import datetime
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
class StockLocationCreateRequest(BaseModel): class StockLocationCreateRequest(BaseModel):
"""Request body for creating a new stock location.""" """Request body for creating a new stock location."""
name: str = Field(..., max_length=255) name: str = Field(..., max_length=255)
address: str | None = Field(None, max_length=500) address: str | None = Field(None, max_length=500)
is_default: bool = False is_default: bool = False
@@ -13,6 +13,7 @@ class StockLocationCreateRequest(BaseModel):
class StockLocationUpdateRequest(BaseModel): class StockLocationUpdateRequest(BaseModel):
"""Request body for updating a stock location.""" """Request body for updating a stock location."""
name: str | None = Field(None, max_length=255) name: str | None = Field(None, max_length=255)
address: str | None = Field(None, max_length=500) address: str | None = Field(None, max_length=500)
is_default: bool | None = None is_default: bool | None = None
@@ -20,6 +21,7 @@ class StockLocationUpdateRequest(BaseModel):
class StockLocationResponse(BaseModel): class StockLocationResponse(BaseModel):
"""Public stock location representation.""" """Public stock location representation."""
id: str id: str
account_id: str account_id: str
name: str name: str
@@ -33,6 +35,7 @@ class StockLocationResponse(BaseModel):
class StockLocationListResponse(BaseModel): class StockLocationListResponse(BaseModel):
"""Paginated list of stock locations.""" """Paginated list of stock locations."""
items: list[StockLocationResponse] items: list[StockLocationResponse]
total: int total: int
page: int page: int
+4
View File
@@ -5,6 +5,7 @@ from pydantic import BaseModel, EmailStr, Field
class UserCreateRequest(BaseModel): class UserCreateRequest(BaseModel):
"""Request body for creating a new user (invite).""" """Request body for creating a new user (invite)."""
email: EmailStr email: EmailStr
full_name: str = Field(..., min_length=2, max_length=255) full_name: str = Field(..., min_length=2, max_length=255)
password: str = Field(..., min_length=8, max_length=128) password: str = Field(..., min_length=8, max_length=128)
@@ -13,6 +14,7 @@ class UserCreateRequest(BaseModel):
class UserUpdateRequest(BaseModel): class UserUpdateRequest(BaseModel):
"""Request body for updating an existing user.""" """Request body for updating an existing user."""
full_name: str | None = Field(None, min_length=2, max_length=255) full_name: str | None = Field(None, min_length=2, max_length=255)
role_id: str | None = None role_id: str | None = None
is_active: bool | None = None is_active: bool | None = None
@@ -20,6 +22,7 @@ class UserUpdateRequest(BaseModel):
class UserResponse(BaseModel): class UserResponse(BaseModel):
"""Public user representation (no password hash).""" """Public user representation (no password hash)."""
id: str id: str
account_id: str account_id: str
email: str email: str
@@ -34,6 +37,7 @@ class UserResponse(BaseModel):
class UserListResponse(BaseModel): class UserListResponse(BaseModel):
"""Paginated list of users.""" """Paginated list of users."""
items: list[UserResponse] items: list[UserResponse]
total: int total: int
page: int page: int
+8
View File
@@ -5,6 +5,7 @@ from pydantic import BaseModel, Field
class VehicleAssignmentResponse(BaseModel): class VehicleAssignmentResponse(BaseModel):
"""Public vehicle assignment representation.""" """Public vehicle assignment representation."""
id: str id: str
vehicle_id: str vehicle_id: str
project_id: str | None = None project_id: str | None = None
@@ -20,6 +21,7 @@ class VehicleAssignmentResponse(BaseModel):
class VehicleCreateRequest(BaseModel): class VehicleCreateRequest(BaseModel):
"""Request body for creating a new vehicle.""" """Request body for creating a new vehicle."""
name: str = Field(..., max_length=255) name: str = Field(..., max_length=255)
license_plate: str | None = Field(None, max_length=50) license_plate: str | None = Field(None, max_length=50)
brand: str | None = Field(None, max_length=100) brand: str | None = Field(None, max_length=100)
@@ -36,6 +38,7 @@ class VehicleCreateRequest(BaseModel):
class VehicleUpdateRequest(BaseModel): class VehicleUpdateRequest(BaseModel):
"""Request body for updating a vehicle.""" """Request body for updating a vehicle."""
name: str | None = Field(None, max_length=255) name: str | None = Field(None, max_length=255)
license_plate: str | None = Field(None, max_length=50) license_plate: str | None = Field(None, max_length=50)
brand: str | None = Field(None, max_length=100) brand: str | None = Field(None, max_length=100)
@@ -52,6 +55,7 @@ class VehicleUpdateRequest(BaseModel):
class VehicleResponse(BaseModel): class VehicleResponse(BaseModel):
"""Public vehicle representation.""" """Public vehicle representation."""
id: str id: str
account_id: str account_id: str
name: str name: str
@@ -75,6 +79,7 @@ class VehicleResponse(BaseModel):
class VehicleListResponse(BaseModel): class VehicleListResponse(BaseModel):
"""Paginated list of vehicles.""" """Paginated list of vehicles."""
items: list[VehicleResponse] items: list[VehicleResponse]
total: int total: int
page: int page: int
@@ -83,6 +88,7 @@ class VehicleListResponse(BaseModel):
class VehicleAssignmentCreateRequest(BaseModel): class VehicleAssignmentCreateRequest(BaseModel):
"""Request body for creating a vehicle assignment.""" """Request body for creating a vehicle assignment."""
vehicle_id: str vehicle_id: str
project_id: str | None = None project_id: str | None = None
start_date: str = Field(..., description="ISO datetime") start_date: str = Field(..., description="ISO datetime")
@@ -93,6 +99,7 @@ class VehicleAssignmentCreateRequest(BaseModel):
class VehicleAssignmentUpdateRequest(BaseModel): class VehicleAssignmentUpdateRequest(BaseModel):
"""Request body for updating a vehicle assignment.""" """Request body for updating a vehicle assignment."""
start_date: str | None = None start_date: str | None = None
end_date: str | None = None end_date: str | None = None
status: str | None = Field(None, max_length=20) status: str | None = Field(None, max_length=20)
@@ -101,6 +108,7 @@ class VehicleAssignmentUpdateRequest(BaseModel):
class VehicleAssignmentListResponse(BaseModel): class VehicleAssignmentListResponse(BaseModel):
"""Paginated list of vehicle assignments.""" """Paginated list of vehicle assignments."""
items: list[VehicleAssignmentResponse] items: list[VehicleAssignmentResponse]
total: int total: int
page: int page: int