迁移合同费用通用附件

1、删除旧费用附件 API、CRUD、模型和专用前端面板,统一改用通用附件能力。

2、调整合同费用接口为 study_id 语义,并补充合同、凭证、发票等附件类型的权限映射。

3、将合同费用新建和编辑改为抽屉流程,详情页与列表页复用同一编辑组件。

4、补充数据库迁移、附件列表、合同费用抽屉和权限场景测试,覆盖旧权限移除与新流程。
This commit is contained in:
Cheng Zhou
2026-06-04 11:07:13 +08:00
parent ab4f0d93ed
commit 720c98765f
36 changed files with 2576 additions and 1875 deletions
+6 -6
View File
@@ -16,7 +16,7 @@ async def create_contract_fee(
contract_in: ContractFeeCreate,
) -> ContractFee:
contract = ContractFee(
project_id=contract_in.project_id,
project_id=contract_in.study_id,
center_id=contract_in.center_id,
contract_no=contract_in.contract_no,
signed_date=contract_in.signed_date,
@@ -39,18 +39,18 @@ async def get_contract_fee(db: AsyncSession, contract_id: uuid.UUID) -> Contract
return result.scalar_one_or_none()
async def get_contract_fee_by_project_center(
db: AsyncSession, project_id: uuid.UUID, center_id: uuid.UUID
async def get_contract_fee_by_study_center(
db: AsyncSession, study_id: uuid.UUID, center_id: uuid.UUID
) -> ContractFee | None:
result = await db.execute(
select(ContractFee).where(ContractFee.project_id == project_id, ContractFee.center_id == center_id)
select(ContractFee).where(ContractFee.project_id == study_id, ContractFee.center_id == center_id)
)
return result.scalar_one_or_none()
async def list_contract_fees(
db: AsyncSession,
project_id: uuid.UUID,
study_id: uuid.UUID,
center_id: uuid.UUID | None = None,
center_ids: set[uuid.UUID] | None = None,
q: str | None = None,
@@ -81,7 +81,7 @@ async def list_contract_fees(
)
.join(Site, Site.id == ContractFee.center_id)
.outerjoin(ContractFeePayment, ContractFeePayment.contract_fee_id == ContractFee.id)
.where(ContractFee.project_id == project_id)
.where(ContractFee.project_id == study_id)
.group_by(ContractFee.id, Site.name)
)
-70
View File
@@ -1,70 +0,0 @@
import uuid
from typing import Sequence
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.fee_attachment import FeeAttachment
async def create_attachment(
db: AsyncSession,
*,
entity_type: str,
entity_id: uuid.UUID,
file_type: str,
filename: str,
mime_type: str | None,
size: int,
storage_key: str | None,
url: str | None,
uploaded_by: uuid.UUID,
) -> FeeAttachment:
attachment = FeeAttachment(
entity_type=entity_type,
entity_id=entity_id,
file_type=file_type,
filename=filename,
mime_type=mime_type,
size=size,
storage_key=storage_key,
url=url,
uploaded_by=uploaded_by,
)
db.add(attachment)
await db.commit()
await db.refresh(attachment)
return attachment
async def list_attachments(
db: AsyncSession,
*,
entity_type: str,
entity_id: uuid.UUID,
) -> Sequence[FeeAttachment]:
result = await db.execute(
select(FeeAttachment)
.where(
FeeAttachment.entity_type == entity_type,
FeeAttachment.entity_id == entity_id,
FeeAttachment.is_deleted.is_(False),
)
.order_by(FeeAttachment.uploaded_at.desc())
)
return result.scalars().all()
async def get_attachment(db: AsyncSession, attachment_id: uuid.UUID) -> FeeAttachment | None:
result = await db.execute(
select(FeeAttachment).where(FeeAttachment.id == attachment_id, FeeAttachment.is_deleted.is_(False))
)
return result.scalar_one_or_none()
async def soft_delete_attachment(db: AsyncSession, attachment: FeeAttachment) -> FeeAttachment:
attachment.is_deleted = True
db.add(attachment)
await db.commit()
await db.refresh(attachment)
return attachment