diff --git a/backend/alembic/versions/20260512_02_remove_special_fee_modules.py b/backend/alembic/versions/20260512_02_remove_special_fee_modules.py new file mode 100644 index 00000000..e79172ff --- /dev/null +++ b/backend/alembic/versions/20260512_02_remove_special_fee_modules.py @@ -0,0 +1,114 @@ +"""remove special fee modules + +Revision ID: 20260512_02 +Revises: 20260512_01 +Create Date: 2026-05-12 10:15:00.000000 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +revision: str = "20260512_02" +down_revision: Union[str, None] = "20260512_01" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def _table_exists(inspector: sa.Inspector, table_name: str) -> bool: + return table_name in inspector.get_table_names() + + +def upgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + + if _table_exists(inspector, "attachments"): + op.execute( + """ + DELETE FROM attachments + WHERE entity_type IN ( + 'finance_special', + 'special_expense', + 'special_expense_voucher', + 'special_expense_invoice', + 'special_expense_other' + ) + """ + ) + + if _table_exists(inspector, "fee_attachments"): + op.execute( + """ + DELETE FROM fee_attachments + WHERE entity_type = 'special_expense' + """ + ) + + if _table_exists(inspector, "audit_logs"): + op.execute( + """ + DELETE FROM audit_logs + WHERE entity_type IN ('finance_special', 'special_expense') + """ + ) + + if _table_exists(inspector, "finance_specials"): + op.drop_table("finance_specials") + + if _table_exists(inspector, "special_expenses"): + op.drop_table("special_expenses") + + +def downgrade() -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + + if not _table_exists(inspector, "special_expenses"): + op.create_table( + "special_expenses", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("center_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("category", sa.String(length=50), nullable=False), + sa.Column("amount", sa.Numeric(12, 2), nullable=False), + sa.Column("happen_date", sa.Date(), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("is_paid", sa.Boolean(), server_default="false", nullable=False), + sa.Column("paid_date", sa.Date(), nullable=True), + sa.Column("is_verified", sa.Boolean(), server_default="false", nullable=False), + sa.Column("verified_date", sa.Date(), nullable=True), + sa.Column("created_by", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.ForeignKeyConstraint(["center_id"], ["sites.id"]), + sa.ForeignKeyConstraint(["created_by"], ["users.id"]), + sa.ForeignKeyConstraint(["project_id"], ["studies.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_special_expenses_project_id", "special_expenses", ["project_id"]) + op.create_index("ix_special_expenses_center_id", "special_expenses", ["center_id"]) + + if not _table_exists(inspector, "finance_specials"): + op.create_table( + "finance_specials", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("study_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("site_name", sa.String(length=255), nullable=False), + sa.Column("fee_type", sa.String(length=50), nullable=False), + sa.Column("amount", sa.Numeric(12, 2), nullable=False), + sa.Column("occur_date", sa.Date(), nullable=True), + sa.Column("staff_name", sa.String(length=100), nullable=True), + sa.Column("remark", sa.Text(), nullable=True), + sa.Column("created_by", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.ForeignKeyConstraint(["created_by"], ["users.id"]), + sa.ForeignKeyConstraint(["study_id"], ["studies.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_finance_specials_study_id", "finance_specials", ["study_id"]) diff --git a/backend/app/api/v1/fees_attachments.py b/backend/app/api/v1/fees_attachments.py index d07251cc..a9bf6e0b 100644 --- a/backend/app/api/v1/fees_attachments.py +++ b/backend/app/api/v1/fees_attachments.py @@ -14,7 +14,6 @@ from app.crud import contract_fee as contract_fee_crud from app.crud import contract_fee_payment as payment_crud from app.crud import fee_attachment as fee_attachment_crud from app.crud import member as member_crud -from app.crud import special_expense as special_crud from app.crud import user as user_crud from app.schemas.fee_attachment import FeeAttachmentRead from app.schemas.fee_common import FeeApiResponse @@ -24,11 +23,10 @@ router = APIRouter() UPLOAD_ROOT = Path(__file__).resolve().parent.parent.parent / "uploads" / "fees" -ALLOWED_ENTITY_TYPES = {"contract_fee", "contract_payment", "special_expense"} +ALLOWED_ENTITY_TYPES = {"contract_fee", "contract_payment"} ALLOWED_FILE_TYPES = { "contract_fee": {"contract", "voucher", "invoice"}, "contract_payment": {"voucher", "invoice"}, - "special_expense": {"voucher", "invoice", "other"}, } @@ -46,11 +44,6 @@ async def _resolve_project_id(db: AsyncSession, entity_type: str, entity_id: uui if not contract: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="合同费用不存在") return contract.project_id - if entity_type == "special_expense": - expense = await special_crud.get_special_expense(db, entity_id) - if not expense: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在") - return expense.project_id raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="不支持的附件类型") diff --git a/backend/app/api/v1/fees_specials.py b/backend/app/api/v1/fees_specials.py deleted file mode 100644 index 821e9e65..00000000 --- a/backend/app/api/v1/fees_specials.py +++ /dev/null @@ -1,245 +0,0 @@ -import uuid -from datetime import date - -from fastapi import APIRouter, Depends, HTTPException, Query, status -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_not_locked -from app.crud import audit as audit_crud -from app.crud import member as member_crud -from app.crud import special_expense as special_crud -from app.crud import study as study_crud -from app.crud import site as site_crud -from app.schemas.fee_common import FeeApiResponse -from app.schemas.special_expense import ( - SpecialExpenseCreate, - SpecialExpenseListItem, - SpecialExpenseRead, - SpecialExpenseUpdate, -) - -router = APIRouter() - -ALLOWED_CATEGORIES = {"travel", "meal", "meeting", "supplies", "other"} - - -async def _ensure_project_access(db: AsyncSession, project_id: uuid.UUID, current_user, write: bool = False): - study = await study_crud.get(db, project_id) - if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") - role_value = current_user.role.value if hasattr(current_user.role, "value") else current_user.role - if role_value == "ADMIN": - return None - membership = await member_crud.get_member(db, project_id, current_user.id) - if not membership or not membership.is_active: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="不是项目成员") - if write and membership.role_in_study != "PM": - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="项目权限不足") - return membership - - -def _validate_special_rules(payload: SpecialExpenseCreate | SpecialExpenseUpdate): - if payload.is_verified is True and payload.is_paid is False: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="核销需先打款") - if payload.is_paid and not payload.paid_date: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="已打款需填写打款日期") - if payload.is_verified and not payload.verified_date: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="已核销需填写核销日期") - - -def _validate_category(value: str | None): - if value and value not in ALLOWED_CATEGORIES: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="费用类别无效") - - -async def _ensure_center_active(db: AsyncSession, project_id: uuid.UUID, center_id: uuid.UUID | None): - if not center_id: - return - site = await site_crud.get_site(db, center_id) - if not site or site.study_id != project_id or not site.is_active: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="中心已停用") - - -@router.get( - "/special", - response_model=FeeApiResponse[list[SpecialExpenseListItem]], - dependencies=[Depends(get_current_user)], -) -async def list_special_expenses( - project_id: uuid.UUID = Query(..., alias="projectId"), - center_id: uuid.UUID | None = Query(None, alias="centerId"), - category: str | None = None, - date_from: date | None = Query(None, alias="dateFrom"), - date_to: date | None = Query(None, alias="dateTo"), - db: AsyncSession = Depends(get_db_session), - current_user=Depends(get_current_user), -) -> FeeApiResponse[list[SpecialExpenseListItem]]: - await _ensure_project_access(db, project_id, current_user, write=False) - _validate_category(category) - cra_scope = await get_cra_site_scope(db, project_id, current_user) - center_ids = cra_scope[0] if cra_scope else None - if center_id and center_ids is not None and center_id not in center_ids: - return FeeApiResponse(data=[], meta={"total": 0}) - rows = await special_crud.list_special_expenses( - db, - project_id, - center_id=center_id, - center_ids=center_ids, - category=category, - date_from=date_from, - date_to=date_to, - ) - items: list[SpecialExpenseListItem] = [] - for expense, center_name, attachments_count in rows: - items.append( - SpecialExpenseListItem( - id=expense.id, - project_id=expense.project_id, - center_id=expense.center_id, - category=expense.category, - amount=expense.amount, - happen_date=expense.happen_date, - description=expense.description, - is_paid=expense.is_paid, - paid_date=expense.paid_date, - is_verified=expense.is_verified, - verified_date=expense.verified_date, - created_by=expense.created_by, - created_at=expense.created_at, - updated_at=expense.updated_at, - center_name=center_name, - attachments_count=attachments_count or 0, - ) - ) - return FeeApiResponse(data=items, meta={"total": len(items)}) - - -@router.get( - "/special/{expense_id}", - response_model=FeeApiResponse[SpecialExpenseRead], - dependencies=[Depends(get_current_user)], -) -async def get_special_expense( - expense_id: uuid.UUID, - db: AsyncSession = Depends(get_db_session), - current_user=Depends(get_current_user), -) -> FeeApiResponse[SpecialExpenseRead]: - expense = await special_crud.get_special_expense(db, expense_id) - if not expense: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在") - await _ensure_project_access(db, expense.project_id, current_user, write=False) - cra_scope = await get_cra_site_scope(db, expense.project_id, current_user) - if cra_scope and expense.center_id not in cra_scope[0]: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") - return FeeApiResponse(data=SpecialExpenseRead.model_validate(expense)) - - -@router.post( - "/special", - response_model=FeeApiResponse[SpecialExpenseRead], - status_code=status.HTTP_201_CREATED, - dependencies=[Depends(get_current_user), Depends(require_study_not_locked())], -) -async def create_special_expense( - expense_in: SpecialExpenseCreate, - db: AsyncSession = Depends(get_db_session), - current_user=Depends(get_current_user), -) -> FeeApiResponse[SpecialExpenseRead]: - await _ensure_project_access(db, expense_in.project_id, current_user, write=True) - _validate_category(expense_in.category) - _validate_special_rules(expense_in) - if expense_in.center_id: - site = await site_crud.get_site(db, expense_in.center_id) - if not site or site.study_id != expense_in.project_id or not site.is_active: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="中心不存在或已停用") - await _ensure_center_active(db, expense_in.project_id, expense_in.center_id) - expense = await special_crud.create_special_expense(db, expense_in, created_by=current_user.id) - await audit_crud.log_action( - db, - study_id=expense.project_id, - entity_type="special_expense", - entity_id=expense.id, - action="CREATE_SPECIAL_EXPENSE", - detail="特殊费用已创建", - operator_id=current_user.id, - operator_role=current_user.role, - ) - return FeeApiResponse(data=SpecialExpenseRead.model_validate(expense)) - - -@router.patch( - "/special/{expense_id}", - response_model=FeeApiResponse[SpecialExpenseRead], - dependencies=[Depends(get_current_user), Depends(require_study_not_locked())], -) -async def update_special_expense( - expense_id: uuid.UUID, - expense_in: SpecialExpenseUpdate, - db: AsyncSession = Depends(get_db_session), - current_user=Depends(get_current_user), -) -> FeeApiResponse[SpecialExpenseRead]: - expense = await special_crud.get_special_expense(db, expense_id) - if not expense: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在") - await _ensure_project_access(db, expense.project_id, current_user, write=True) - _validate_category(expense_in.category) - merged = SpecialExpenseCreate( - project_id=expense.project_id, - center_id=expense_in.center_id if expense_in.center_id is not None else expense.center_id, - category=expense_in.category if expense_in.category is not None else expense.category, - amount=expense_in.amount if expense_in.amount is not None else expense.amount, - happen_date=expense_in.happen_date if expense_in.happen_date is not None else expense.happen_date, - description=expense_in.description if expense_in.description is not None else expense.description, - is_paid=expense_in.is_paid if expense_in.is_paid is not None else expense.is_paid, - paid_date=expense_in.paid_date if expense_in.paid_date is not None else expense.paid_date, - is_verified=expense_in.is_verified if expense_in.is_verified is not None else expense.is_verified, - verified_date=expense_in.verified_date if expense_in.verified_date is not None else expense.verified_date, - ) - _validate_special_rules(merged) - if expense_in.center_id: - site = await site_crud.get_site(db, expense_in.center_id) - if not site or site.study_id != expense.project_id or not site.is_active: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="中心不存在或已停用") - await _ensure_center_active(db, expense.project_id, expense_in.center_id) - else: - await _ensure_center_active(db, expense.project_id, expense.center_id) - expense = await special_crud.update_special_expense(db, expense, expense_in) - await audit_crud.log_action( - db, - study_id=expense.project_id, - entity_type="special_expense", - entity_id=expense_id, - action="UPDATE_SPECIAL_EXPENSE", - detail="特殊费用已更新", - operator_id=current_user.id, - operator_role=current_user.role, - ) - return FeeApiResponse(data=SpecialExpenseRead.model_validate(expense)) - - -@router.delete( - "/special/{expense_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(get_current_user), Depends(require_study_not_locked())], -) -async def delete_special_expense( - expense_id: uuid.UUID, - db: AsyncSession = Depends(get_db_session), - current_user=Depends(get_current_user), -) -> None: - expense = await special_crud.get_special_expense(db, expense_id) - if not expense: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在") - await _ensure_project_access(db, expense.project_id, current_user, write=True) - await _ensure_center_active(db, expense.project_id, expense.center_id) - await special_crud.delete_special_expense(db, expense) - await audit_crud.log_action( - db, - study_id=expense.project_id, - entity_type="special_expense", - entity_id=expense_id, - action="DELETE_SPECIAL_EXPENSE", - detail="特殊费用已删除", - operator_id=current_user.id, - operator_role=current_user.role, - ) diff --git a/backend/app/api/v1/finance_specials.py b/backend/app/api/v1/finance_specials.py deleted file mode 100644 index ccb97809..00000000 --- a/backend/app/api/v1/finance_specials.py +++ /dev/null @@ -1,177 +0,0 @@ -import uuid - -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.deps import get_cra_site_scope, get_current_user, get_db_session, require_study_member, require_study_roles, require_study_not_locked -from app.crud import audit as audit_crud -from app.crud import finance_special as special_crud -from app.crud import site as site_crud -from app.crud import study as study_crud -from app.schemas.finance_special import FinanceSpecialCreate, FinanceSpecialRead, FinanceSpecialUpdate - -router = APIRouter() - - -async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): - study = await study_crud.get(db, study_id) - if not study: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="项目不存在") - return study - - -async def _ensure_site_name_active(db: AsyncSession, study_id: uuid.UUID, site_name: str | None): - if not site_name: - return - active_names = await site_crud.list_active_names(db, study_id) - if site_name not in active_names: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="中心已停用") - - -@router.post( - "/specials", - response_model=FinanceSpecialRead, - status_code=status.HTTP_201_CREATED, - dependencies=[Depends(require_study_roles(["PM", "CRA"]))], -) -async def create_special( - study_id: uuid.UUID, - special_in: FinanceSpecialCreate, - db: AsyncSession = Depends(get_db_session), - current_user=Depends(get_current_user), -) -> FinanceSpecialRead: - await _ensure_study_exists(db, study_id) - cra_scope = await get_cra_site_scope(db, study_id, current_user) - if cra_scope and special_in.site_name not in cra_scope[1]: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") - await _ensure_site_name_active(db, study_id, special_in.site_name) - item = await special_crud.create_special(db, study_id, special_in, created_by=current_user.id) - await audit_crud.log_action( - db, - study_id=study_id, - entity_type="finance_special", - entity_id=item.id, - action="CREATE_FINANCE_SPECIAL", - detail=f"特殊费用 {item.id} 已创建", - operator_id=current_user.id, - operator_role=current_user.role, - ) - return FinanceSpecialRead.model_validate(item) - - -@router.get( - "/specials", - response_model=list[FinanceSpecialRead], - dependencies=[Depends(require_study_member())], -) -async def list_specials( - study_id: uuid.UUID, - site_name: str | None = None, - fee_type: str | None = None, - skip: int = 0, - limit: int = 100, - db: AsyncSession = Depends(get_db_session), - current_user=Depends(get_current_user), -) -> list[FinanceSpecialRead]: - await _ensure_study_exists(db, study_id) - cra_scope = await get_cra_site_scope(db, study_id, current_user) - site_names = cra_scope[1] if cra_scope else None - if site_name and site_names is not None and site_name not in site_names: - return [] - items = await special_crud.list_specials( - db, - study_id, - site_name=site_name, - site_names=site_names, - fee_type=fee_type, - skip=skip, - limit=limit, - ) - return [FinanceSpecialRead.model_validate(item) for item in items] - - -@router.get( - "/specials/{special_id}", - response_model=FinanceSpecialRead, - dependencies=[Depends(require_study_member())], -) -async def get_special( - study_id: uuid.UUID, - special_id: uuid.UUID, - db: AsyncSession = Depends(get_db_session), - current_user=Depends(get_current_user), -) -> FinanceSpecialRead: - await _ensure_study_exists(db, study_id) - item = await special_crud.get_special(db, special_id) - if not item or item.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在") - cra_scope = await get_cra_site_scope(db, study_id, current_user) - if cra_scope and item.site_name not in cra_scope[1]: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") - return FinanceSpecialRead.model_validate(item) - - -@router.patch( - "/specials/{special_id}", - response_model=FinanceSpecialRead, - dependencies=[Depends(require_study_roles(["PM", "CRA"]))], -) -async def update_special( - study_id: uuid.UUID, - special_id: uuid.UUID, - special_in: FinanceSpecialUpdate, - db: AsyncSession = Depends(get_db_session), - current_user=Depends(get_current_user), -) -> FinanceSpecialRead: - await _ensure_study_exists(db, study_id) - item = await special_crud.get_special(db, special_id) - if not item or item.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在") - cra_scope = await get_cra_site_scope(db, study_id, current_user) - if cra_scope and item.site_name not in cra_scope[1]: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") - await _ensure_site_name_active(db, study_id, item.site_name) - item = await special_crud.update_special(db, item, special_in) - await audit_crud.log_action( - db, - study_id=study_id, - entity_type="finance_special", - entity_id=special_id, - action="UPDATE_FINANCE_SPECIAL", - detail=f"特殊费用 {special_id} 已更新", - operator_id=current_user.id, - operator_role=current_user.role, - ) - return FinanceSpecialRead.model_validate(item) - - -@router.delete( - "/specials/{special_id}", - status_code=status.HTTP_204_NO_CONTENT, - dependencies=[Depends(require_study_roles(["PM", "CRA"]))], -) -async def delete_special( - study_id: uuid.UUID, - special_id: uuid.UUID, - db: AsyncSession = Depends(get_db_session), - current_user=Depends(get_current_user), -) -> None: - await _ensure_study_exists(db, study_id) - item = await special_crud.get_special(db, special_id) - if not item or item.study_id != study_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="特殊费用不存在") - cra_scope = await get_cra_site_scope(db, study_id, current_user) - if cra_scope and item.site_name not in cra_scope[1]: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="权限不足") - await _ensure_site_name_active(db, study_id, item.site_name) - await special_crud.delete_special(db, item) - await audit_crud.log_action( - db, - study_id=study_id, - entity_type="finance_special", - entity_id=special_id, - action="DELETE_FINANCE_SPECIAL", - detail=f"特殊费用 {special_id} 已删除", - operator_id=current_user.id, - operator_role=current_user.role, - ) diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index e1fbb5fb..b811f85d 100644 --- a/backend/app/api/v1/router.py +++ b/backend/app/api/v1/router.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.v1 import auth, users, admin_users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, finance_contracts, finance_specials, fees_contracts, fees_specials, fees_attachments, drug_shipments, material_equipments, project_milestones, startup, knowledge_notes, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, overview, notifications, monitoring_visit_issues +from app.api.v1 import auth, users, admin_users, studies, sites, members, attachments, audit_logs, dashboard, subjects, visits, aes, finance_dashboard, finance_contracts, fees_contracts, fees_attachments, drug_shipments, material_equipments, project_milestones, startup, knowledge_notes, subject_histories, subject_pds, study_subject_pds, faq_categories, faqs, documents, overview, notifications, monitoring_visit_issues api_router = APIRouter() @@ -21,9 +21,7 @@ api_router.include_router(visits.router, prefix="/studies/{study_id}/subjects/{s api_router.include_router(aes.router, prefix="/studies/{study_id}/aes", tags=["aes"]) api_router.include_router(finance_dashboard.router, prefix="/studies/{study_id}/finance", tags=["finance"]) api_router.include_router(finance_contracts.router, prefix="/studies/{study_id}/finance", tags=["finance-contracts"]) -api_router.include_router(finance_specials.router, prefix="/studies/{study_id}/finance", tags=["finance-specials"]) api_router.include_router(fees_contracts.router, prefix="/fees", tags=["fees-contracts"]) -api_router.include_router(fees_specials.router, prefix="/fees", tags=["fees-specials"]) api_router.include_router(fees_attachments.router, prefix="/fees", tags=["fees-attachments"]) api_router.include_router(drug_shipments.router, prefix="/studies/{study_id}/drug", tags=["drug-shipments"]) api_router.include_router(material_equipments.router, prefix="/studies/{study_id}/materials", tags=["material-equipments"]) diff --git a/backend/app/crud/finance_special.py b/backend/app/crud/finance_special.py deleted file mode 100644 index 3ff51152..00000000 --- a/backend/app/crud/finance_special.py +++ /dev/null @@ -1,77 +0,0 @@ -import uuid -from typing import Sequence - -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.models.finance_special import FinanceSpecial -from app.schemas.finance_special import FinanceSpecialCreate, FinanceSpecialUpdate - - -async def create_special( - db: AsyncSession, - study_id: uuid.UUID, - special_in: FinanceSpecialCreate, - created_by: uuid.UUID | None, -) -> FinanceSpecial: - special = FinanceSpecial( - study_id=study_id, - site_name=special_in.site_name, - fee_type=special_in.fee_type, - amount=special_in.amount, - occur_date=special_in.occur_date, - staff_name=special_in.staff_name, - remark=special_in.remark, - created_by=created_by, - ) - db.add(special) - await db.commit() - await db.refresh(special) - return special - - -async def get_special(db: AsyncSession, special_id: uuid.UUID) -> FinanceSpecial | None: - result = await db.execute(select(FinanceSpecial).where(FinanceSpecial.id == special_id)) - return result.scalar_one_or_none() - - -async def list_specials( - db: AsyncSession, - study_id: uuid.UUID, - site_name: str | None = None, - site_names: set[str] | None = None, - fee_type: str | None = None, - skip: int = 0, - limit: int = 100, -) -> Sequence[FinanceSpecial]: - stmt = ( - select(FinanceSpecial) - .where(FinanceSpecial.study_id == study_id) - ) - if site_names is not None: - if not site_names: - return [] - stmt = stmt.where(FinanceSpecial.site_name.in_(site_names)) - if site_name: - stmt = stmt.where(FinanceSpecial.site_name.ilike(f"%{site_name}%")) - if fee_type: - stmt = stmt.where(FinanceSpecial.fee_type == fee_type) - stmt = stmt.order_by(FinanceSpecial.created_at.desc()).offset(skip).limit(limit) - result = await db.execute(stmt) - return result.scalars().all() - - -async def update_special( - db: AsyncSession, special: FinanceSpecial, special_in: FinanceSpecialUpdate -) -> FinanceSpecial: - update_data = special_in.model_dump(exclude_unset=True) - for key, value in update_data.items(): - setattr(special, key, value) - await db.commit() - await db.refresh(special) - return special - - -async def delete_special(db: AsyncSession, special: FinanceSpecial) -> None: - await db.delete(special) - await db.commit() diff --git a/backend/app/crud/site.py b/backend/app/crud/site.py index c1464d2c..f64bc54e 100644 --- a/backend/app/crud/site.py +++ b/backend/app/crud/site.py @@ -17,13 +17,11 @@ from app.models.drug_shipment import DrugShipment from app.models.fee_attachment import FeeAttachment from app.models.finance import FinanceItem from app.models.finance_contract import FinanceContract -from app.models.finance_special import FinanceSpecial from app.models.kickoff_meeting import KickoffMeeting from app.models.knowledge_note import KnowledgeNote from app.models.milestone import Milestone from app.models.monitoring_visit_issue import MonitoringVisitIssue from app.models.site import Site -from app.models.special_expense import SpecialExpense from app.models.startup_ethics import StartupEthics from app.models.startup_feasibility import StartupFeasibility from app.models.study_center_confirm import StudyCenterConfirm @@ -158,9 +156,6 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None: contract_fee_ids = ( await db.execute(select(ContractFee.id).where(ContractFee.center_id == site_id)) ).scalars().all() - special_expense_ids = ( - await db.execute(select(SpecialExpense.id).where(SpecialExpense.center_id == site_id)) - ).scalars().all() drug_shipment_ids = ( await db.execute(select(DrugShipment.id).where(DrugShipment.center_id == site_id)) ).scalars().all() @@ -189,14 +184,6 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None: ) ) ).scalars().all() - finance_special_ids = ( - await db.execute( - select(FinanceSpecial.id).where( - FinanceSpecial.study_id == study_id, - FinanceSpecial.site_name == site_name, - ) - ) - ).scalars().all() knowledge_note_ids = ( await db.execute( select(KnowledgeNote.id).where( @@ -233,7 +220,6 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None: "startup_kickoff_ppt": kickoff_ids, "training_authorization": training_ids, "finance_contract": finance_contract_ids, - "finance_special": finance_special_ids, "knowledge_note": knowledge_note_ids, "drug_shipment": drug_shipment_ids, } @@ -279,23 +265,6 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None: ) ) await db.execute(delete(ContractFeePayment).where(ContractFeePayment.contract_fee_id.in_(contract_fee_ids))) - if special_expense_ids: - fee_attachment_paths.extend( - ( - await db.execute( - select(FeeAttachment.storage_key).where( - FeeAttachment.entity_type == "special_expense", - FeeAttachment.entity_id.in_(special_expense_ids), - ) - ) - ).scalars().all() - ) - await db.execute( - delete(FeeAttachment).where( - FeeAttachment.entity_type == "special_expense", - FeeAttachment.entity_id.in_(special_expense_ids), - ) - ) if distribution_ids: await db.execute(delete(Acknowledgement).where(Acknowledgement.distribution_id.in_(distribution_ids))) @@ -327,7 +296,6 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None: await db.execute(delete(StartupEthics).where(StartupEthics.site_id == site_id)) await db.execute(delete(ContractFee).where(ContractFee.center_id == site_id)) - await db.execute(delete(SpecialExpense).where(SpecialExpense.center_id == site_id)) await db.execute(delete(DrugShipment).where(DrugShipment.center_id == site_id)) await db.execute( update(MonitoringVisitIssue) @@ -347,12 +315,6 @@ async def delete_site_and_related(db: AsyncSession, site: Site) -> None: FinanceContract.site_name == site_name, ) ) - await db.execute( - delete(FinanceSpecial).where( - FinanceSpecial.study_id == study_id, - FinanceSpecial.site_name == site_name, - ) - ) await db.execute( delete(KnowledgeNote).where( KnowledgeNote.study_id == study_id, diff --git a/backend/app/crud/special_expense.py b/backend/app/crud/special_expense.py deleted file mode 100644 index 6d3a1a66..00000000 --- a/backend/app/crud/special_expense.py +++ /dev/null @@ -1,104 +0,0 @@ -import uuid -from datetime import date -from typing import Sequence - -from sqlalchemy import and_, func, select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.models.attachment import Attachment -from app.models.special_expense import SpecialExpense -from app.models.site import Site -from app.schemas.special_expense import SpecialExpenseCreate, SpecialExpenseUpdate - - -async def create_special_expense( - db: AsyncSession, - expense_in: SpecialExpenseCreate, - created_by: uuid.UUID | None, -) -> SpecialExpense: - expense = SpecialExpense( - project_id=expense_in.project_id, - center_id=expense_in.center_id, - category=expense_in.category, - amount=expense_in.amount, - happen_date=expense_in.happen_date, - description=expense_in.description, - is_paid=expense_in.is_paid, - paid_date=expense_in.paid_date, - is_verified=expense_in.is_verified, - verified_date=expense_in.verified_date, - created_by=created_by, - ) - db.add(expense) - await db.commit() - await db.refresh(expense) - return expense - - -async def get_special_expense(db: AsyncSession, expense_id: uuid.UUID) -> SpecialExpense | None: - result = await db.execute(select(SpecialExpense).where(SpecialExpense.id == expense_id)) - return result.scalar_one_or_none() - - -async def list_special_expenses( - db: AsyncSession, - project_id: uuid.UUID, - center_id: uuid.UUID | None = None, - center_ids: set[uuid.UUID] | None = None, - category: str | None = None, - date_from: date | None = None, - date_to: date | None = None, -) -> Sequence[tuple[SpecialExpense, str | None, int]]: - attachment_count = func.count(Attachment.id).label("attachments_count") - attachment_types = ["special_expense_voucher", "special_expense_invoice", "special_expense_other"] - stmt = ( - select( - SpecialExpense, - Site.name.label("center_name"), - attachment_count, - ) - .outerjoin(Site, Site.id == SpecialExpense.center_id) - .outerjoin( - Attachment, - and_( - Attachment.entity_type.in_(attachment_types), - Attachment.entity_id == SpecialExpense.id, - Attachment.is_deleted.is_(False), - ), - ) - .where(SpecialExpense.project_id == project_id) - .group_by(SpecialExpense.id, Site.name) - .order_by(SpecialExpense.happen_date.desc().nullslast(), SpecialExpense.created_at.desc()) - ) - - if center_id: - stmt = stmt.where(SpecialExpense.center_id == center_id) - if center_ids is not None: - if not center_ids: - return [] - stmt = stmt.where(SpecialExpense.center_id.in_(center_ids)) - if category: - stmt = stmt.where(SpecialExpense.category == category) - if date_from: - stmt = stmt.where(SpecialExpense.happen_date >= date_from) - if date_to: - stmt = stmt.where(SpecialExpense.happen_date <= date_to) - - result = await db.execute(stmt) - return result.all() - - -async def update_special_expense( - db: AsyncSession, expense: SpecialExpense, expense_in: SpecialExpenseUpdate -) -> SpecialExpense: - update_data = expense_in.model_dump(exclude_unset=True) - for key, value in update_data.items(): - setattr(expense, key, value) - await db.commit() - await db.refresh(expense) - return expense - - -async def delete_special_expense(db: AsyncSession, expense: SpecialExpense) -> None: - await db.delete(expense) - await db.commit() diff --git a/backend/app/crud/study.py b/backend/app/crud/study.py index 7568c9fe..f7f020b3 100644 --- a/backend/app/crud/study.py +++ b/backend/app/crud/study.py @@ -100,10 +100,8 @@ async def delete(db: AsyncSession, study_id: uuid.UUID) -> None: from app.models.ae import AdverseEvent from app.models.finance import FinanceItem from app.models.finance_contract import FinanceContract - from app.models.finance_special import FinanceSpecial from app.models.contract_fee import ContractFee from app.models.contract_fee_payment import ContractFeePayment - from app.models.special_expense import SpecialExpense from app.models.milestone import Milestone from app.models.document import Document from app.models.attachment import Attachment @@ -152,7 +150,6 @@ async def delete(db: AsyncSession, study_id: uuid.UUID) -> None: await db.execute(sa_delete(Milestone).where(Milestone.study_id == study_id)) # 9. 删除财务相关 - await db.execute(sa_delete(SpecialExpense).where(SpecialExpense.project_id == study_id)) await db.execute( sa_delete(ContractFeePayment).where( ContractFeePayment.contract_fee_id.in_( @@ -161,7 +158,6 @@ async def delete(db: AsyncSession, study_id: uuid.UUID) -> None: ) ) await db.execute(sa_delete(ContractFee).where(ContractFee.project_id == study_id)) - await db.execute(sa_delete(FinanceSpecial).where(FinanceSpecial.study_id == study_id)) await db.execute(sa_delete(FinanceContract).where(FinanceContract.study_id == study_id)) await db.execute(sa_delete(FinanceItem).where(FinanceItem.study_id == study_id)) diff --git a/backend/app/db/base.py b/backend/app/db/base.py index 4b5068b2..5b6ae59c 100644 --- a/backend/app/db/base.py +++ b/backend/app/db/base.py @@ -18,10 +18,8 @@ from app.models.visit import Visit # noqa: F401 from app.models.ae import AdverseEvent # noqa: F401 from app.models.finance import FinanceItem # noqa: F401 from app.models.finance_contract import FinanceContract # noqa: F401 -from app.models.finance_special import FinanceSpecial # noqa: F401 from app.models.contract_fee import ContractFee # noqa: F401 from app.models.contract_fee_payment import ContractFeePayment # noqa: F401 -from app.models.special_expense import SpecialExpense # noqa: F401 from app.models.fee_attachment import FeeAttachment # noqa: F401 from app.models.drug_shipment import DrugShipment # noqa: F401 from app.models.material_equipment import MaterialEquipment # noqa: F401 diff --git a/backend/app/models/finance_special.py b/backend/app/models/finance_special.py deleted file mode 100644 index ef951b76..00000000 --- a/backend/app/models/finance_special.py +++ /dev/null @@ -1,26 +0,0 @@ -import uuid -from datetime import date, datetime - -from sqlalchemy import Date, DateTime, ForeignKey, Numeric, String, Text, func -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import Mapped, mapped_column - -from app.db.base_class import Base - - -class FinanceSpecial(Base): - __tablename__ = "finance_specials" - - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False) - site_name: Mapped[str] = mapped_column(String(255), nullable=False) - fee_type: Mapped[str] = mapped_column(String(50), nullable=False) - amount: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) - occur_date: Mapped[date | None] = mapped_column(Date, nullable=True) - staff_name: Mapped[str | None] = mapped_column(String(100), nullable=True) - remark: Mapped[str | None] = mapped_column(Text, nullable=True) - created_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) - updated_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() - ) diff --git a/backend/app/models/special_expense.py b/backend/app/models/special_expense.py deleted file mode 100644 index 1e66a96d..00000000 --- a/backend/app/models/special_expense.py +++ /dev/null @@ -1,30 +0,0 @@ -import uuid -from datetime import date, datetime -from decimal import Decimal - -from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Numeric, String, Text, func -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import Mapped, mapped_column - -from app.db.base_class import Base - - -class SpecialExpense(Base): - __tablename__ = "special_expenses" - - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False) - center_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("sites.id"), index=True, nullable=True) - category: Mapped[str] = mapped_column(String(50), nullable=False) - amount: Mapped[Decimal] = mapped_column(Numeric(12, 2), nullable=False) - happen_date: Mapped[date | None] = mapped_column(Date, nullable=True) - description: Mapped[str | None] = mapped_column(Text, nullable=True) - is_paid: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false") - paid_date: Mapped[date | None] = mapped_column(Date, nullable=True) - is_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false") - verified_date: Mapped[date | None] = mapped_column(Date, nullable=True) - created_by: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) - updated_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() - ) diff --git a/backend/app/schemas/finance_special.py b/backend/app/schemas/finance_special.py deleted file mode 100644 index 6dcfcefc..00000000 --- a/backend/app/schemas/finance_special.py +++ /dev/null @@ -1,40 +0,0 @@ -import uuid -from datetime import date, datetime -from decimal import Decimal -from typing import Optional - -from pydantic import BaseModel, ConfigDict, Field - - -class FinanceSpecialCreate(BaseModel): - site_name: str - fee_type: str - amount: Decimal = Field(gt=0) - occur_date: Optional[date] = None - staff_name: Optional[str] = None - remark: Optional[str] = None - - -class FinanceSpecialUpdate(BaseModel): - site_name: Optional[str] = None - fee_type: Optional[str] = None - amount: Optional[Decimal] = Field(default=None, gt=0) - occur_date: Optional[date] = None - staff_name: Optional[str] = None - remark: Optional[str] = None - - -class FinanceSpecialRead(BaseModel): - id: uuid.UUID - study_id: uuid.UUID - site_name: str - fee_type: str - amount: Decimal - occur_date: Optional[date] - staff_name: Optional[str] - remark: Optional[str] - created_by: Optional[uuid.UUID] - created_at: datetime - updated_at: datetime - - model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/special_expense.py b/backend/app/schemas/special_expense.py deleted file mode 100644 index b55ea6ff..00000000 --- a/backend/app/schemas/special_expense.py +++ /dev/null @@ -1,55 +0,0 @@ -import uuid -from datetime import date, datetime -from decimal import Decimal -from typing import Optional - -from pydantic import BaseModel, ConfigDict, Field - - -class SpecialExpenseCreate(BaseModel): - project_id: uuid.UUID - center_id: Optional[uuid.UUID] = None - category: str - amount: Decimal = Field(ge=0) - happen_date: Optional[date] = None - description: Optional[str] = None - is_paid: bool = False - paid_date: Optional[date] = None - is_verified: bool = False - verified_date: Optional[date] = None - - -class SpecialExpenseUpdate(BaseModel): - center_id: Optional[uuid.UUID] = None - category: Optional[str] = None - amount: Optional[Decimal] = Field(default=None, ge=0) - happen_date: Optional[date] = None - description: Optional[str] = None - is_paid: Optional[bool] = None - paid_date: Optional[date] = None - is_verified: Optional[bool] = None - verified_date: Optional[date] = None - - -class SpecialExpenseRead(BaseModel): - id: uuid.UUID - project_id: uuid.UUID - center_id: Optional[uuid.UUID] - category: str - amount: Decimal - happen_date: Optional[date] - description: Optional[str] - is_paid: bool - paid_date: Optional[date] - is_verified: bool - verified_date: Optional[date] - created_by: Optional[uuid.UUID] - created_at: datetime - updated_at: datetime - - model_config = ConfigDict(from_attributes=True) - - -class SpecialExpenseListItem(SpecialExpenseRead): - center_name: Optional[str] - attachments_count: int diff --git a/database/init.sql b/database/init.sql index 42e8a715..fd5ce1fe 100644 --- a/database/init.sql +++ b/database/init.sql @@ -214,22 +214,6 @@ CREATE TABLE IF NOT EXISTS public.finance_contracts ( CONSTRAINT fk_finance_contracts_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT ); -CREATE TABLE IF NOT EXISTS public.finance_specials ( - id uuid PRIMARY KEY, - study_id uuid NOT NULL, - site_name character varying(255) NOT NULL, - fee_type character varying(50) NOT NULL, - amount numeric(12, 2) NOT NULL, - occur_date date, - staff_name character varying(100), - remark text, - created_by uuid, - created_at timestamp with time zone NOT NULL DEFAULT now(), - updated_at timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT fk_finance_specials_study_id FOREIGN KEY (study_id) REFERENCES public.studies(id) ON DELETE RESTRICT, - CONSTRAINT fk_finance_specials_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT -); - CREATE TABLE IF NOT EXISTS public.contract_fees ( id uuid PRIMARY KEY, project_id uuid NOT NULL, @@ -261,26 +245,6 @@ CREATE TABLE IF NOT EXISTS public.contract_fee_payments ( CONSTRAINT fk_contract_fee_payments_contract_fee_id FOREIGN KEY (contract_fee_id) REFERENCES public.contract_fees(id) ON DELETE CASCADE ); -CREATE TABLE IF NOT EXISTS public.special_expenses ( - id uuid PRIMARY KEY, - project_id uuid NOT NULL, - center_id uuid, - category character varying(50) NOT NULL, - amount numeric(12, 2) NOT NULL, - happen_date date, - description text, - is_paid boolean NOT NULL DEFAULT false, - paid_date date, - is_verified boolean NOT NULL DEFAULT false, - verified_date date, - created_by uuid, - created_at timestamp with time zone NOT NULL DEFAULT now(), - updated_at timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT fk_special_expenses_project_id FOREIGN KEY (project_id) REFERENCES public.studies(id) ON DELETE RESTRICT, - CONSTRAINT fk_special_expenses_center_id FOREIGN KEY (center_id) REFERENCES public.sites(id) ON DELETE RESTRICT, - CONSTRAINT fk_special_expenses_created_by FOREIGN KEY (created_by) REFERENCES public.users(id) ON DELETE RESTRICT -); - CREATE TABLE IF NOT EXISTS public.fee_attachments ( id uuid PRIMARY KEY, entity_type character varying(50) NOT NULL, @@ -549,7 +513,6 @@ CREATE INDEX IF NOT EXISTS ix_adverse_events_study_id ON public.adverse_events ( CREATE INDEX IF NOT EXISTS ix_adverse_events_site_id ON public.adverse_events (site_id); CREATE INDEX IF NOT EXISTS ix_adverse_events_subject_id ON public.adverse_events (subject_id); CREATE INDEX IF NOT EXISTS ix_finance_contracts_study_id ON public.finance_contracts (study_id); -CREATE INDEX IF NOT EXISTS ix_finance_specials_study_id ON public.finance_specials (study_id); CREATE INDEX IF NOT EXISTS ix_finance_items_study_id ON public.finance_items (study_id); CREATE INDEX IF NOT EXISTS ix_finance_items_site_id ON public.finance_items (site_id); CREATE INDEX IF NOT EXISTS ix_finance_items_subject_id ON public.finance_items (subject_id); @@ -650,14 +613,6 @@ INSERT INTO public.finance_contracts ( ('77777777-aaaa-bbbb-cccc-222222222222', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '上海瑞金医院', 'CON-002', '2025-01-10', 980000.00, 'CNY', '分中心合同签署', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-10 09:00:00+00', '2025-01-10 09:00:00+00') ON CONFLICT (id) DO NOTHING; -INSERT INTO public.finance_specials ( - id, study_id, site_name, fee_type, amount, occur_date, staff_name, remark, created_by, created_at, updated_at -) VALUES - ('88888888-aaaa-bbbb-cccc-111111111111', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '北京协和医院', '差旅', 5200.00, '2025-01-15', '赵敏', 'SIV 差旅费', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-15 10:00:00+00', '2025-01-15 10:00:00+00'), - ('88888888-aaaa-bbbb-cccc-222222222222', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '上海瑞金医院', '住宿', 3200.00, '2025-01-18', '李雷', '监查住宿费', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-18 10:00:00+00', '2025-01-18 10:00:00+00'), - ('88888888-aaaa-bbbb-cccc-333333333333', (SELECT id FROM public.studies WHERE code = 'DEMO-CTMS'), '北京协和医院', '交通', 860.00, '2025-01-20', '韩梅梅', '市内交通费', (SELECT id FROM public.users WHERE email = 'pm@example.com'), '2025-01-20 10:00:00+00', '2025-01-20 10:00:00+00') -ON CONFLICT (id) DO NOTHING; - INSERT INTO public.finance_items ( id, study_id, site_id, subject_id, visit_id, category, title, description, currency, amount, occur_date, status, submitted_at, approved_at, rejected_at, paid_at, approver_id, payer_id, reject_reason, created_by, created_at, updated_at ) VALUES diff --git a/frontend/scripts/verify-ui-contract.mjs b/frontend/scripts/verify-ui-contract.mjs index 0fc7ca2a..572c7ec4 100644 --- a/frontend/scripts/verify-ui-contract.mjs +++ b/frontend/scripts/verify-ui-contract.mjs @@ -69,7 +69,6 @@ for (const className of requiredOverviewClasses) { const financeAndFilePages = [ "src/views/fees/ContractFees.vue", - "src/views/fees/SpecialExpenses.vue", "src/views/ia/FileVersionManagement.vue" ]; diff --git a/frontend/src/api/feeSpecials.ts b/frontend/src/api/feeSpecials.ts deleted file mode 100644 index f517e1c4..00000000 --- a/frontend/src/api/feeSpecials.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { apiDelete, apiGet, apiPatch, apiPost } from "./axios"; -import type { FeeApiResponse } from "../types/api"; - -export interface SpecialExpensePayload { - project_id: string; - center_id?: string | null; - category: string; - amount: number; - happen_date?: string | null; - description?: string | null; - is_paid?: boolean; - paid_date?: string | null; - is_verified?: boolean; - verified_date?: string | null; -} - -export interface SpecialExpenseUpdatePayload { - center_id?: string | null; - category?: string; - amount?: number; - happen_date?: string | null; - description?: string | null; - is_paid?: boolean; - paid_date?: string | null; - is_verified?: boolean; - verified_date?: string | null; -} - -export const listSpecialExpenses = (params: { - projectId: string; - centerId?: string; - category?: string; - dateFrom?: string; - dateTo?: string; -}) => apiGet>("/api/v1/fees/special", { params }); - -export const getSpecialExpense = (expenseId: string) => apiGet>(`/api/v1/fees/special/${expenseId}`); - -export const createSpecialExpense = (payload: SpecialExpensePayload) => - apiPost>("/api/v1/fees/special", payload); - -export const updateSpecialExpense = (expenseId: string, payload: SpecialExpenseUpdatePayload) => - apiPatch>(`/api/v1/fees/special/${expenseId}`, payload); - -export const deleteSpecialExpense = (expenseId: string) => apiDelete(`/api/v1/fees/special/${expenseId}`); diff --git a/frontend/src/api/financeSpecials.ts b/frontend/src/api/financeSpecials.ts deleted file mode 100644 index 1e54f4df..00000000 --- a/frontend/src/api/financeSpecials.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { apiDelete, apiGet, apiPatch, apiPost } from "./axios"; - -export const listFinanceSpecials = (studyId: string, params?: Record) => - apiGet(`/api/v1/studies/${studyId}/finance/specials`, { params }); - -export const getFinanceSpecial = (studyId: string, specialId: string) => - apiGet(`/api/v1/studies/${studyId}/finance/specials/${specialId}`); - -export const createFinanceSpecial = (studyId: string, payload: Record) => - apiPost(`/api/v1/studies/${studyId}/finance/specials`, payload); - -export const updateFinanceSpecial = (studyId: string, specialId: string, payload: Record) => - apiPatch(`/api/v1/studies/${studyId}/finance/specials/${specialId}`, payload); - -export const deleteFinanceSpecial = (studyId: string, specialId: string) => - apiDelete(`/api/v1/studies/${studyId}/finance/specials/${specialId}`); diff --git a/frontend/src/audit/index.ts b/frontend/src/audit/index.ts index 4c03684d..e5520cc3 100644 --- a/frontend/src/audit/index.ts +++ b/frontend/src/audit/index.ts @@ -30,10 +30,8 @@ const entityTypeLabelMap: Record = { visit: "访视", ae: "不良事件", finance_contract: "费用合同", - finance_special: "费用特殊费用", contract_fee: "合同费用条目", contract_fee_payment: "合同费用回款", - special_expense: "特殊费用条目", drug_shipment: "药品流向", startup_feasibility: "立项可行性", startup_ethics: "立项伦理", diff --git a/frontend/src/components/Layout.vue b/frontend/src/components/Layout.vue index 4ba7d7d6..9b90473c 100644 --- a/frontend/src/components/Layout.vue +++ b/frontend/src/components/Layout.vue @@ -53,7 +53,6 @@ {{ TEXT.menu.finance }} {{ TEXT.menu.feeContracts }} - {{ TEXT.menu.feeSpecials }}