chore(quality): apply ruff autofixes and formatting (29 fixes, 41 files reformatted)
This commit is contained in:
@@ -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:
|
||||||
|
batch_op.create_foreign_key(
|
||||||
|
"fk_users_role_id_roles", "roles", ["role_id"], ["id"], ondelete="SET NULL"
|
||||||
)
|
)
|
||||||
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')
|
|
||||||
|
|
||||||
# ### 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 ###
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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 ###
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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]
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}>"
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user