费用管理内容优化-初步

This commit is contained in:
Cheng Zhou
2026-01-13 16:49:55 +08:00
parent 0c7c03069a
commit 1db36f40b0
43 changed files with 5229 additions and 26 deletions
+112
View File
@@ -0,0 +1,112 @@
import uuid
from datetime import date
from typing import Sequence
from sqlalchemy import case, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.contract_fee import ContractFee
from app.models.contract_fee_payment import ContractFeePayment
from app.models.site import Site
from app.schemas.contract_fee import ContractFeeCreate, ContractFeeUpdate
async def create_contract_fee(
db: AsyncSession,
contract_in: ContractFeeCreate,
) -> ContractFee:
contract = ContractFee(
project_id=contract_in.project_id,
center_id=contract_in.center_id,
contract_amount=contract_in.contract_amount,
contract_cases=contract_in.contract_cases,
actual_cases=contract_in.actual_cases,
settlement_amount=contract_in.settlement_amount,
final_payment_amount=contract_in.final_payment_amount,
)
db.add(contract)
await db.commit()
await db.refresh(contract)
return contract
async def get_contract_fee(db: AsyncSession, contract_id: uuid.UUID) -> ContractFee | None:
result = await db.execute(select(ContractFee).where(ContractFee.id == contract_id))
return result.scalar_one_or_none()
async def get_contract_fee_by_project_center(
db: AsyncSession, project_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)
)
return result.scalar_one_or_none()
async def list_contract_fees(
db: AsyncSession,
project_id: uuid.UUID,
center_id: uuid.UUID | None = None,
q: str | None = None,
) -> Sequence[tuple[ContractFee, str, float, float, date | None, date | None]]:
paid_total = func.coalesce(
func.sum(case((ContractFeePayment.is_paid.is_(True), ContractFeePayment.amount), else_=0)),
0,
).label("paid_total")
verified_total = func.coalesce(
func.sum(case((ContractFeePayment.is_verified.is_(True), ContractFeePayment.amount), else_=0)),
0,
).label("verified_total")
last_paid_date = func.max(
case((ContractFeePayment.is_paid.is_(True), ContractFeePayment.paid_date), else_=None)
).label("last_paid_date")
last_verified_date = func.max(
case((ContractFeePayment.is_verified.is_(True), ContractFeePayment.verified_date), else_=None)
).label("last_verified_date")
stmt = (
select(
ContractFee,
Site.name.label("center_name"),
paid_total,
verified_total,
last_paid_date,
last_verified_date,
)
.join(Site, Site.id == ContractFee.center_id)
.outerjoin(ContractFeePayment, ContractFeePayment.contract_fee_id == ContractFee.id)
.where(ContractFee.project_id == project_id)
.group_by(ContractFee.id, Site.name)
)
if center_id:
stmt = stmt.where(ContractFee.center_id == center_id)
if q:
conditions = [Site.name.ilike(f"%{q}%")]
try:
conditions.append(Site.id == uuid.UUID(q))
except (ValueError, TypeError):
pass
stmt = stmt.where(or_(*conditions))
stmt = stmt.order_by(Site.name.asc())
result = await db.execute(stmt)
return result.all()
async def update_contract_fee(
db: AsyncSession, contract: ContractFee, contract_in: ContractFeeUpdate
) -> ContractFee:
update_data = contract_in.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(contract, key, value)
await db.commit()
await db.refresh(contract)
return contract
async def delete_contract_fee(db: AsyncSession, contract: ContractFee) -> None:
await db.delete(contract)
await db.commit()
+74
View File
@@ -0,0 +1,74 @@
import uuid
from typing import Sequence
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.contract_fee_payment import ContractFeePayment
from app.schemas.contract_fee_payment import ContractFeePaymentCreate, ContractFeePaymentUpdate
async def create_payment(
db: AsyncSession,
contract_fee_id: uuid.UUID,
payment_in: ContractFeePaymentCreate,
) -> ContractFeePayment:
result = await db.execute(
select(func.coalesce(func.max(ContractFeePayment.seq), 0)).where(
ContractFeePayment.contract_fee_id == contract_fee_id
)
)
next_seq = (result.scalar_one_or_none() or 0) + 1
payment = ContractFeePayment(
contract_fee_id=contract_fee_id,
seq=next_seq,
amount=payment_in.amount,
paid_date=payment_in.paid_date,
verified_date=payment_in.verified_date,
is_paid=payment_in.is_paid,
is_verified=payment_in.is_verified,
remark=payment_in.remark,
)
db.add(payment)
await db.commit()
await db.refresh(payment)
return payment
async def get_payment(db: AsyncSession, payment_id: uuid.UUID) -> ContractFeePayment | None:
result = await db.execute(select(ContractFeePayment).where(ContractFeePayment.id == payment_id))
return result.scalar_one_or_none()
async def list_payments(db: AsyncSession, contract_fee_id: uuid.UUID) -> Sequence[ContractFeePayment]:
result = await db.execute(
select(ContractFeePayment)
.where(ContractFeePayment.contract_fee_id == contract_fee_id)
.order_by(ContractFeePayment.seq.asc(), ContractFeePayment.created_at.asc())
)
return result.scalars().all()
async def update_payment(
db: AsyncSession, payment: ContractFeePayment, payment_in: ContractFeePaymentUpdate
) -> ContractFeePayment:
update_data = payment_in.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(payment, key, value)
await db.commit()
await db.refresh(payment)
return payment
async def delete_payment(db: AsyncSession, payment: ContractFeePayment) -> None:
await db.delete(payment)
await db.commit()
async def resequence_payments(db: AsyncSession, contract_fee_id: uuid.UUID) -> None:
payments = await list_payments(db, contract_fee_id)
for idx, payment in enumerate(payments, start=1):
if payment.seq != idx:
payment.seq = idx
db.add(payment)
await db.commit()
+70
View File
@@ -0,0 +1,70 @@
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
+98
View File
@@ -0,0 +1,98 @@
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.fee_attachment import FeeAttachment
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,
category: str | None = None,
date_from: date | None = None,
date_to: date | None = None,
) -> Sequence[tuple[SpecialExpense, str | None, int]]:
attachment_count = func.count(FeeAttachment.id).label("attachments_count")
stmt = (
select(
SpecialExpense,
Site.name.label("center_name"),
attachment_count,
)
.outerjoin(Site, Site.id == SpecialExpense.center_id)
.outerjoin(
FeeAttachment,
and_(
FeeAttachment.entity_type == "special_expense",
FeeAttachment.entity_id == SpecialExpense.id,
FeeAttachment.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 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()