|
|
@@ -12,6 +12,7 @@ from sqlalchemy.orm import Session
|
|
|
|
|
|
from app.core.utils import generate_uuid
|
|
|
from app.models.models import SysUser
|
|
|
+from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
|
|
|
from app.models.pl_models import (
|
|
|
PlFreightSettlement,
|
|
|
PlFreightSettlementApproval,
|
|
|
@@ -29,7 +30,9 @@ from app.schemas.pl.freight import (
|
|
|
_FMT = "%Y-%m-%d %H:%M:%S"
|
|
|
|
|
|
TAB_STATUS_MAP = {
|
|
|
- "bills": ("pending", "approving"),
|
|
|
+ "bills": ("pending", "approving", "approved", "confirmed"),
|
|
|
+ "approval_pending": ("approving",),
|
|
|
+ "approval_done": ("approved", "confirmed"),
|
|
|
"disburse": ("approved",),
|
|
|
"settled": ("confirmed",),
|
|
|
}
|
|
|
@@ -61,7 +64,14 @@ def _occupied_transport_ids_subquery(db: Session, tenant_no: str):
|
|
|
)
|
|
|
|
|
|
|
|
|
-def get_settlement_row(db: Session, tenant_no: str, settlement_id: str) -> PlFreightSettlement:
|
|
|
+def get_settlement_row(
|
|
|
+ db: Session,
|
|
|
+ tenant_no: str,
|
|
|
+ settlement_id: str,
|
|
|
+ *,
|
|
|
+ user=None,
|
|
|
+ skip_scope: bool = False,
|
|
|
+) -> PlFreightSettlement:
|
|
|
row = (
|
|
|
db.query(PlFreightSettlement)
|
|
|
.filter(
|
|
|
@@ -73,6 +83,8 @@ def get_settlement_row(db: Session, tenant_no: str, settlement_id: str) -> PlFre
|
|
|
)
|
|
|
if not row:
|
|
|
raise HTTPException(status_code=404, detail="结算单不存在")
|
|
|
+ if user and not skip_scope:
|
|
|
+ assert_pl_row_in_scope(db, row, PlFreightSettlement, user, tenant_no)
|
|
|
return row
|
|
|
|
|
|
|
|
|
@@ -80,7 +92,9 @@ def list_unsettled_plans(
|
|
|
db: Session,
|
|
|
*,
|
|
|
tenant_no: str,
|
|
|
+ user=None,
|
|
|
q: Optional[str] = None,
|
|
|
+ carrier_id: Optional[str] = None,
|
|
|
plan_type: Optional[str] = None,
|
|
|
license_plate: Optional[str] = None,
|
|
|
page: int = 1,
|
|
|
@@ -112,11 +126,16 @@ def list_unsettled_plans(
|
|
|
PlPlan.supplier_name.contains(q),
|
|
|
)
|
|
|
)
|
|
|
+ if carrier_id:
|
|
|
+ query = query.filter(PlTransport.carrier_id == carrier_id)
|
|
|
if plan_type:
|
|
|
query = query.filter(PlPlan.plan_type == plan_type)
|
|
|
if license_plate:
|
|
|
query = query.filter(PlPlan.license_plate.contains(license_plate))
|
|
|
|
|
|
+ if user:
|
|
|
+ query = apply_pl_scope(query, PlPlan, user, db, tenant_no)
|
|
|
+
|
|
|
total = query.count()
|
|
|
rows = (
|
|
|
query.order_by(PlPlan.update_time.desc())
|
|
|
@@ -238,8 +257,10 @@ def create_settlement(
|
|
|
return settlement
|
|
|
|
|
|
|
|
|
-def delete_draft_settlement(db: Session, *, tenant_no: str, settlement_id: str, user_id: str) -> None:
|
|
|
- settlement = get_settlement_row(db, tenant_no, settlement_id)
|
|
|
+def delete_draft_settlement(
|
|
|
+ db: Session, *, tenant_no: str, settlement_id: str, user_id: str, user=None
|
|
|
+) -> None:
|
|
|
+ settlement = get_settlement_row(db, tenant_no, settlement_id, user=user)
|
|
|
if settlement.settlement_status != "pending":
|
|
|
raise HTTPException(status_code=400, detail="仅草稿结算单可删除")
|
|
|
|
|
|
@@ -363,6 +384,84 @@ def create_settlement_approval_records(
|
|
|
db.flush()
|
|
|
|
|
|
|
|
|
+def _clear_settlement_approval_records(db: Session, settlement_id: str) -> None:
|
|
|
+ """软删除结算单历史审批记录(重新提交审批前清理)"""
|
|
|
+ now = datetime.now()
|
|
|
+ rows = (
|
|
|
+ db.query(PlFreightSettlementApproval)
|
|
|
+ .filter(
|
|
|
+ PlFreightSettlementApproval.settlement_id == settlement_id,
|
|
|
+ PlFreightSettlementApproval.del_flag == 0,
|
|
|
+ )
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ for row in rows:
|
|
|
+ row.del_flag = 1
|
|
|
+ row.update_time = now
|
|
|
+ if rows:
|
|
|
+ db.flush()
|
|
|
+
|
|
|
+
|
|
|
+def _sync_settlement_status_if_approval_complete(
|
|
|
+ db: Session, settlement: PlFreightSettlement
|
|
|
+) -> bool:
|
|
|
+ """审批记录已全部完成但结算单仍停留在 approving 时自动纠正(兼容历史脏数据)"""
|
|
|
+ if settlement.settlement_status != "approving":
|
|
|
+ return False
|
|
|
+ if not _settlement_all_approved(db, settlement.id):
|
|
|
+ return False
|
|
|
+ settlement.settlement_status = "approved"
|
|
|
+ settlement.update_time = datetime.now()
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
+def _sync_settlement_status_if_disbursed(
|
|
|
+ db: Session, settlement: PlFreightSettlement
|
|
|
+) -> bool:
|
|
|
+ """已有汇款凭证但结算单仍停留在 approved 时自动纠正(兼容历史脏数据)"""
|
|
|
+ if settlement.settlement_status != "approved":
|
|
|
+ return False
|
|
|
+ voucher = (
|
|
|
+ db.query(PlRemittanceVoucher)
|
|
|
+ .filter(
|
|
|
+ PlRemittanceVoucher.settlement_id == settlement.id,
|
|
|
+ PlRemittanceVoucher.del_flag == 0,
|
|
|
+ )
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+ if not voucher:
|
|
|
+ return False
|
|
|
+ settlement.settlement_status = "confirmed"
|
|
|
+ settlement.update_time = datetime.now()
|
|
|
+ return True
|
|
|
+
|
|
|
+
|
|
|
+def _sync_disbursed_settlements(db: Session, tenant_no: str) -> None:
|
|
|
+ """批量纠正已有汇款凭证但仍为 approved 的结算单"""
|
|
|
+ rows = (
|
|
|
+ db.query(PlFreightSettlement)
|
|
|
+ .filter(
|
|
|
+ PlFreightSettlement.tenant_no == tenant_no,
|
|
|
+ PlFreightSettlement.del_flag == 0,
|
|
|
+ PlFreightSettlement.settlement_status == "approved",
|
|
|
+ db.query(PlRemittanceVoucher.id)
|
|
|
+ .filter(
|
|
|
+ PlRemittanceVoucher.settlement_id == PlFreightSettlement.id,
|
|
|
+ PlRemittanceVoucher.del_flag == 0,
|
|
|
+ )
|
|
|
+ .exists(),
|
|
|
+ )
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ if not rows:
|
|
|
+ return
|
|
|
+ now = datetime.now()
|
|
|
+ for row in rows:
|
|
|
+ row.settlement_status = "confirmed"
|
|
|
+ row.update_time = now
|
|
|
+ db.commit()
|
|
|
+
|
|
|
+
|
|
|
def can_user_approve_settlement(db: Session, settlement_id: str, user_id: str) -> bool:
|
|
|
if not settlement_id or not user_id:
|
|
|
return False
|
|
|
@@ -413,6 +512,20 @@ def _advance_after_approval(
|
|
|
if result == "rejected":
|
|
|
settlement.settlement_status = "pending"
|
|
|
settlement.update_time = now
|
|
|
+ others = (
|
|
|
+ db.query(PlFreightSettlementApproval)
|
|
|
+ .filter(
|
|
|
+ PlFreightSettlementApproval.settlement_id == settlement.id,
|
|
|
+ PlFreightSettlementApproval.id != current.id,
|
|
|
+ PlFreightSettlementApproval.approval_result == "pending",
|
|
|
+ PlFreightSettlementApproval.del_flag == 0,
|
|
|
+ )
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ for row in others:
|
|
|
+ row.approval_result = "skipped"
|
|
|
+ row.is_current = 0
|
|
|
+ row.update_time = now
|
|
|
return
|
|
|
|
|
|
approval_type = current.approval_type or "sequential"
|
|
|
@@ -477,8 +590,9 @@ def submit_settlement_approval(
|
|
|
body: FreightSettlementSubmitApprovalBody,
|
|
|
user_id: str,
|
|
|
dept_id: str,
|
|
|
+ user=None,
|
|
|
) -> PlFreightSettlement:
|
|
|
- settlement = get_settlement_row(db, tenant_no, settlement_id)
|
|
|
+ settlement = get_settlement_row(db, tenant_no, settlement_id, user=user)
|
|
|
if settlement.settlement_status != "pending":
|
|
|
raise HTTPException(status_code=400, detail="仅草稿状态可提交审批")
|
|
|
|
|
|
@@ -488,6 +602,8 @@ def submit_settlement_approval(
|
|
|
settlement.update_by = user_id
|
|
|
db.flush()
|
|
|
|
|
|
+ _clear_settlement_approval_records(db, settlement_id)
|
|
|
+
|
|
|
create_settlement_approval_records(
|
|
|
db,
|
|
|
tenant_no=tenant_no,
|
|
|
@@ -523,7 +639,7 @@ def process_settlement_approval(
|
|
|
if not approval:
|
|
|
raise HTTPException(status_code=404, detail="审批记录不存在")
|
|
|
|
|
|
- settlement = get_settlement_row(db, tenant_no, approval.settlement_id)
|
|
|
+ settlement = get_settlement_row(db, tenant_no, approval.settlement_id, skip_scope=True)
|
|
|
if settlement.settlement_status != "approving":
|
|
|
raise HTTPException(status_code=400, detail="结算单不在审批中")
|
|
|
|
|
|
@@ -541,6 +657,8 @@ def process_settlement_approval(
|
|
|
approval.update_by = user_id
|
|
|
approval.update_time = now
|
|
|
|
|
|
+ # SessionLocal 关闭了 autoflush,须先 flush 再查询 pending 计数,否则当前审批仍会被算作 pending
|
|
|
+ db.flush()
|
|
|
_advance_after_approval(db, settlement, approval, result)
|
|
|
settlement.update_by = user_id
|
|
|
db.commit()
|
|
|
@@ -549,9 +667,9 @@ def process_settlement_approval(
|
|
|
|
|
|
|
|
|
def withdraw_settlement_approval(
|
|
|
- db: Session, *, tenant_no: str, settlement_id: str, user_id: str
|
|
|
+ db: Session, *, tenant_no: str, settlement_id: str, user_id: str, user=None
|
|
|
) -> PlFreightSettlement:
|
|
|
- settlement = get_settlement_row(db, tenant_no, settlement_id)
|
|
|
+ settlement = get_settlement_row(db, tenant_no, settlement_id, user=user)
|
|
|
if settlement.settlement_status != "approving":
|
|
|
raise HTTPException(status_code=400, detail="仅审批中可撤回")
|
|
|
if settlement.create_by != user_id:
|
|
|
@@ -586,8 +704,9 @@ def disburse_settlement(
|
|
|
body: FreightSettlementDisburseBody,
|
|
|
user_id: str,
|
|
|
dept_id: str,
|
|
|
+ user=None,
|
|
|
) -> PlFreightSettlement:
|
|
|
- settlement = get_settlement_row(db, tenant_no, settlement_id)
|
|
|
+ settlement = get_settlement_row(db, tenant_no, settlement_id, user=user)
|
|
|
if settlement.settlement_status != "approved":
|
|
|
raise HTTPException(status_code=400, detail="仅待发放状态可登记")
|
|
|
|
|
|
@@ -710,6 +829,7 @@ def enrich_settlement_list_item(
|
|
|
"invoiceAmount": settlement.invoice_amount,
|
|
|
"invoiceNo": settlement.invoice_no,
|
|
|
"invoiceDate": settlement.invoice_date,
|
|
|
+ "invoiceAttachmentUrls": settlement.invoice_attachment_urls,
|
|
|
"settlementStatus": settlement.settlement_status,
|
|
|
"remark": settlement.remark,
|
|
|
"tenantNo": settlement.tenant_no,
|
|
|
@@ -739,25 +859,49 @@ def list_settlements_enriched(
|
|
|
*,
|
|
|
tenant_no: str,
|
|
|
user_id: str,
|
|
|
+ user=None,
|
|
|
q: Optional[str] = None,
|
|
|
+ carrier_id: Optional[str] = None,
|
|
|
settlement_status: Optional[str] = None,
|
|
|
tab: Optional[str] = None,
|
|
|
page: int = 1,
|
|
|
page_size: int = 20,
|
|
|
) -> Tuple[int, List[Dict[str, Any]]]:
|
|
|
+ _sync_disbursed_settlements(db, tenant_no)
|
|
|
query = db.query(PlFreightSettlement).filter(
|
|
|
PlFreightSettlement.tenant_no == tenant_no,
|
|
|
PlFreightSettlement.del_flag == 0,
|
|
|
)
|
|
|
|
|
|
if tab and tab in TAB_STATUS_MAP:
|
|
|
- query = query.filter(PlFreightSettlement.settlement_status.in_(TAB_STATUS_MAP[tab]))
|
|
|
+ tab_statuses = TAB_STATUS_MAP[tab]
|
|
|
+ if settlement_status and settlement_status in tab_statuses:
|
|
|
+ query = query.filter(PlFreightSettlement.settlement_status == settlement_status)
|
|
|
+ else:
|
|
|
+ query = query.filter(PlFreightSettlement.settlement_status.in_(tab_statuses))
|
|
|
elif settlement_status:
|
|
|
query = query.filter(PlFreightSettlement.settlement_status == settlement_status)
|
|
|
|
|
|
if q:
|
|
|
query = query.filter(PlFreightSettlement.settlement_no.contains(q))
|
|
|
|
|
|
+ if carrier_id:
|
|
|
+ carrier_settlement_ids = (
|
|
|
+ db.query(PlFreightSettlementTransport.settlement_id)
|
|
|
+ .join(PlTransport, PlTransport.id == PlFreightSettlementTransport.transport_id)
|
|
|
+ .filter(
|
|
|
+ PlFreightSettlementTransport.tenant_no == tenant_no,
|
|
|
+ PlFreightSettlementTransport.del_flag == 0,
|
|
|
+ PlTransport.del_flag == 0,
|
|
|
+ PlTransport.carrier_id == carrier_id,
|
|
|
+ )
|
|
|
+ .distinct()
|
|
|
+ )
|
|
|
+ query = query.filter(PlFreightSettlement.id.in_(carrier_settlement_ids))
|
|
|
+
|
|
|
+ if user and tab != "approval_pending":
|
|
|
+ query = apply_pl_scope(query, PlFreightSettlement, user, db, tenant_no)
|
|
|
+
|
|
|
total = query.count()
|
|
|
rows = (
|
|
|
query.order_by(PlFreightSettlement.create_time.desc())
|
|
|
@@ -765,13 +909,34 @@ def list_settlements_enriched(
|
|
|
.limit(page_size)
|
|
|
.all()
|
|
|
)
|
|
|
+ status_fixed = False
|
|
|
+ for row in rows:
|
|
|
+ if _sync_settlement_status_if_approval_complete(db, row):
|
|
|
+ status_fixed = True
|
|
|
+ if _sync_settlement_status_if_disbursed(db, row):
|
|
|
+ status_fixed = True
|
|
|
+ if status_fixed:
|
|
|
+ db.commit()
|
|
|
+ for row in rows:
|
|
|
+ db.refresh(row)
|
|
|
+
|
|
|
+ if tab == "bills":
|
|
|
+ bills_statuses = set(TAB_STATUS_MAP["bills"])
|
|
|
+ rows = [r for r in rows if r.settlement_status in bills_statuses]
|
|
|
+
|
|
|
return total, [enrich_settlement_list_item(db, r, user_id) for r in rows]
|
|
|
|
|
|
|
|
|
def get_settlement_detail(
|
|
|
- db: Session, *, tenant_no: str, settlement_id: str, user_id: str
|
|
|
+ db: Session, *, tenant_no: str, settlement_id: str, user_id: str, user=None
|
|
|
) -> Dict[str, Any]:
|
|
|
- settlement = get_settlement_row(db, tenant_no, settlement_id)
|
|
|
+ settlement = get_settlement_row(db, tenant_no, settlement_id, user=user)
|
|
|
+ if _sync_settlement_status_if_approval_complete(db, settlement):
|
|
|
+ db.commit()
|
|
|
+ db.refresh(settlement)
|
|
|
+ if _sync_settlement_status_if_disbursed(db, settlement):
|
|
|
+ db.commit()
|
|
|
+ db.refresh(settlement)
|
|
|
base = enrich_settlement_list_item(db, settlement, user_id)
|
|
|
|
|
|
transport_ids = _get_transport_ids(db, settlement_id)
|
|
|
@@ -812,9 +977,12 @@ def get_settlement_detail(
|
|
|
"transportNo": t.transport_no,
|
|
|
"licensePlate": t.license_plate,
|
|
|
"driverName": t.driver_name,
|
|
|
+ "carrierName": t.carrier_name,
|
|
|
+ "transportMedium": t.transport_medium,
|
|
|
+ "startPoint": t.start_point,
|
|
|
+ "endPoint": t.end_point,
|
|
|
"freightAmount": t.freight_amount,
|
|
|
"transportStatus": t.transport_status,
|
|
|
- "carrierName": t.carrier_name,
|
|
|
}
|
|
|
)
|
|
|
|
|
|
@@ -847,12 +1015,19 @@ def update_settlement_draft(
|
|
|
settlement_id: str,
|
|
|
payload: FreightSettlementUpdate,
|
|
|
user_id: str,
|
|
|
+ user=None,
|
|
|
) -> PlFreightSettlement:
|
|
|
- settlement = get_settlement_row(db, tenant_no, settlement_id)
|
|
|
- if settlement.settlement_status != "pending":
|
|
|
- raise HTTPException(status_code=400, detail="仅草稿可编辑")
|
|
|
+ settlement = get_settlement_row(db, tenant_no, settlement_id, user=user)
|
|
|
+ status = settlement.settlement_status
|
|
|
+ if status not in ("pending", "approved"):
|
|
|
+ raise HTTPException(status_code=400, detail="当前状态不可编辑")
|
|
|
|
|
|
update_data = payload.model_dump(exclude_unset=True, by_alias=False)
|
|
|
+ if status == "approved":
|
|
|
+ allowed = {"invoice_amount", "invoice_no", "invoice_date", "invoice_attachment_urls", "remark"}
|
|
|
+ update_data = {k: v for k, v in update_data.items() if k in allowed}
|
|
|
+ if not update_data:
|
|
|
+ raise HTTPException(status_code=400, detail="无有效更新字段")
|
|
|
for key, value in update_data.items():
|
|
|
setattr(settlement, key, value)
|
|
|
settlement.update_by = user_id
|