|
@@ -7,7 +7,7 @@ from datetime import datetime
|
|
|
from decimal import Decimal
|
|
from decimal import Decimal
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
|
|
|
|
-from sqlalchemy import and_, func, or_
|
|
|
|
|
|
|
+from sqlalchemy import and_, exists, func, or_
|
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
from app.core.utils import generate_uuid, parse_datetime
|
|
from app.core.utils import generate_uuid, parse_datetime
|
|
@@ -25,6 +25,7 @@ from app.services.protocol_transfer_status import (
|
|
|
can_submit_receipt,
|
|
can_submit_receipt,
|
|
|
schedule_paid_and_arrears,
|
|
schedule_paid_and_arrears,
|
|
|
)
|
|
)
|
|
|
|
|
+from app.services.sample_payment_progress import compute_sample_payment_progress
|
|
|
|
|
|
|
|
|
|
|
|
|
def _dec(v: Any) -> Optional[Decimal]:
|
|
def _dec(v: Any) -> Optional[Decimal]:
|
|
@@ -353,7 +354,7 @@ def update_transfer_draft(
|
|
|
) -> BizProtocolPaymentTransfer:
|
|
) -> BizProtocolPaymentTransfer:
|
|
|
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
|
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
|
|
if t.pay_status != "dfj":
|
|
if t.pay_status != "dfj":
|
|
|
- raise ValueError("仅待缴费状态可编辑转账笔")
|
|
|
|
|
|
|
+ raise ValueError("仅已缴费状态可编辑转账笔")
|
|
|
if "transfer_type" in fields or "transferType" in fields:
|
|
if "transfer_type" in fields or "transferType" in fields:
|
|
|
v = fields.get("transfer_type", fields.get("transferType"))
|
|
v = fields.get("transfer_type", fields.get("transferType"))
|
|
|
t.transfer_type = (str(v) if v is not None else "").strip() or None
|
|
t.transfer_type = (str(v) if v is not None else "").strip() or None
|
|
@@ -402,7 +403,7 @@ def submit_receipt_finance(
|
|
|
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
|
t = _get_transfer_or_raise(db, transfer_id=transfer_id, tenant_no=tenant_no)
|
|
|
if draft_fields:
|
|
if draft_fields:
|
|
|
if t.pay_status != "dfj":
|
|
if t.pay_status != "dfj":
|
|
|
- raise ValueError("仅待缴费状态可编辑转账笔")
|
|
|
|
|
|
|
+ raise ValueError("仅已缴费状态可编辑转账笔")
|
|
|
# 就地改草稿字段(不单独 commit)
|
|
# 就地改草稿字段(不单独 commit)
|
|
|
if "transfer_type" in draft_fields or "transferType" in draft_fields:
|
|
if "transfer_type" in draft_fields or "transferType" in draft_fields:
|
|
|
v = draft_fields.get("transfer_type", draft_fields.get("transferType"))
|
|
v = draft_fields.get("transfer_type", draft_fields.get("transferType"))
|
|
@@ -425,7 +426,7 @@ def submit_receipt_finance(
|
|
|
)
|
|
)
|
|
|
t.transfer_attachments = (str(v) if v is not None else "").strip() or None
|
|
t.transfer_attachments = (str(v) if v is not None else "").strip() or None
|
|
|
if not can_submit_receipt(t.pay_status):
|
|
if not can_submit_receipt(t.pay_status):
|
|
|
- raise ValueError("仅待缴费状态可提交财务确认")
|
|
|
|
|
|
|
+ raise ValueError("仅已缴费状态可提交财务确认")
|
|
|
_validate_transfer_submit_ready(t)
|
|
_validate_transfer_submit_ready(t)
|
|
|
t.pay_status = "jfqrz"
|
|
t.pay_status = "jfqrz"
|
|
|
t.update_by = user_id
|
|
t.update_by = user_id
|
|
@@ -933,3 +934,282 @@ def list_transfers_for_finance(
|
|
|
item["payMode"] = (sp.pay_mode if sp else "") or ""
|
|
item["payMode"] = (sp.pay_mode if sp else "") or ""
|
|
|
out.append(item)
|
|
out.append(item)
|
|
|
return out, int(total)
|
|
return out, int(total)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _filter_and_page_progress(
|
|
|
|
|
+ rows: List[Dict[str, Any]],
|
|
|
|
|
+ progress_status: Optional[str],
|
|
|
|
|
+ page: int,
|
|
|
|
|
+ page_size: int,
|
|
|
|
|
+) -> Tuple[List[Dict[str, Any]], int]:
|
|
|
|
|
+ """先按 progressStatus 过滤,再分页切片(禁止先 LIMIT 再筛进度)。"""
|
|
|
|
|
+ filtered = rows
|
|
|
|
|
+ ps = (progress_status or "").strip()
|
|
|
|
|
+ if ps:
|
|
|
|
|
+ filtered = [r for r in rows if (r.get("progressStatus") or "") == ps]
|
|
|
|
|
+ total = len(filtered)
|
|
|
|
|
+ page = max(1, page)
|
|
|
|
|
+ page_size = min(max(1, page_size), 100)
|
|
|
|
|
+ start = (page - 1) * page_size
|
|
|
|
|
+ return filtered[start : start + page_size], total
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _progress_to_json_fields(progress: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
|
+ paid = progress.get("paidAmount")
|
|
|
|
|
+ due_remain = progress.get("dueRemainAmount")
|
|
|
|
|
+ return {
|
|
|
|
|
+ "progressStatus": progress.get("progressStatus") or "",
|
|
|
|
|
+ "paidAmount": float(paid) if paid is not None else 0.0,
|
|
|
|
|
+ "dueRemainAmount": float(due_remain) if due_remain is not None else 0.0,
|
|
|
|
|
+ "clearedScheduleCount": progress.get("clearedScheduleCount") or 0,
|
|
|
|
|
+ "activeScheduleCount": progress.get("activeScheduleCount") or 0,
|
|
|
|
|
+ "canConfirm": bool(progress.get("canConfirm")),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _load_payment_children_batch(
|
|
|
|
|
+ db: Session,
|
|
|
|
|
+ sample_payment_ids: List[str],
|
|
|
|
|
+ tenant_no: str,
|
|
|
|
|
+) -> Tuple[
|
|
|
|
|
+ Dict[str, List[BizProtocolPaymentSchedule]],
|
|
|
|
|
+ Dict[str, List[BizProtocolPaymentTransfer]],
|
|
|
|
|
+]:
|
|
|
|
|
+ schedules_by_payment: Dict[str, List[BizProtocolPaymentSchedule]] = {}
|
|
|
|
|
+ transfers_by_payment: Dict[str, List[BizProtocolPaymentTransfer]] = {}
|
|
|
|
|
+ if not sample_payment_ids:
|
|
|
|
|
+ return schedules_by_payment, transfers_by_payment
|
|
|
|
|
+ schedules = (
|
|
|
|
|
+ db.query(BizProtocolPaymentSchedule)
|
|
|
|
|
+ .filter(
|
|
|
|
|
+ BizProtocolPaymentSchedule.sample_payment_id.in_(sample_payment_ids),
|
|
|
|
|
+ BizProtocolPaymentSchedule.tenant_no == tenant_no,
|
|
|
|
|
+ BizProtocolPaymentSchedule.del_flag == 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ .order_by(
|
|
|
|
|
+ BizProtocolPaymentSchedule.order_num.asc(),
|
|
|
|
|
+ BizProtocolPaymentSchedule.schedule_no.asc(),
|
|
|
|
|
+ )
|
|
|
|
|
+ .all()
|
|
|
|
|
+ )
|
|
|
|
|
+ sids = [s.id for s in schedules]
|
|
|
|
|
+ transfers: List[BizProtocolPaymentTransfer] = []
|
|
|
|
|
+ if sids:
|
|
|
|
|
+ transfers = (
|
|
|
|
|
+ db.query(BizProtocolPaymentTransfer)
|
|
|
|
|
+ .filter(
|
|
|
|
|
+ BizProtocolPaymentTransfer.schedule_id.in_(sids),
|
|
|
|
|
+ BizProtocolPaymentTransfer.tenant_no == tenant_no,
|
|
|
|
|
+ BizProtocolPaymentTransfer.del_flag == 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ .order_by(BizProtocolPaymentTransfer.create_time.asc())
|
|
|
|
|
+ .all()
|
|
|
|
|
+ )
|
|
|
|
|
+ for s in schedules:
|
|
|
|
|
+ schedules_by_payment.setdefault(s.sample_payment_id, []).append(s)
|
|
|
|
|
+ schedule_to_payment = {s.id: s.sample_payment_id for s in schedules}
|
|
|
|
|
+ for t in transfers:
|
|
|
|
|
+ pid = schedule_to_payment.get(t.schedule_id) or t.sample_payment_id
|
|
|
|
|
+ if pid:
|
|
|
|
|
+ transfers_by_payment.setdefault(pid, []).append(t)
|
|
|
|
|
+ return schedules_by_payment, transfers_by_payment
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _compute_progress_for_payment(
|
|
|
|
|
+ schedules: List[BizProtocolPaymentSchedule],
|
|
|
|
|
+ transfers: List[BizProtocolPaymentTransfer],
|
|
|
|
|
+) -> Dict[str, Any]:
|
|
|
|
|
+ sch_dicts = [
|
|
|
|
|
+ {
|
|
|
|
|
+ "scheduleStatus": s.schedule_status or "",
|
|
|
|
|
+ "dueAmount": s.due_amount,
|
|
|
|
|
+ }
|
|
|
|
|
+ for s in schedules
|
|
|
|
|
+ ]
|
|
|
|
|
+ tr_dicts = [
|
|
|
|
|
+ {
|
|
|
|
|
+ "payStatus": t.pay_status or "",
|
|
|
|
|
+ "transferAmount": t.transfer_amount,
|
|
|
|
|
+ }
|
|
|
|
|
+ for t in transfers
|
|
|
|
|
+ ]
|
|
|
|
|
+ return compute_sample_payment_progress(schedules=sch_dicts, transfers=tr_dicts)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _build_list_summary_row(
|
|
|
|
|
+ sp: BizProtocolSamplePayment,
|
|
|
|
|
+ proto: BizCollectionProtocol,
|
|
|
|
|
+ party: Optional[BizProtocolPartyCustomer],
|
|
|
|
|
+ progress: Dict[str, Any],
|
|
|
|
|
+) -> Dict[str, Any]:
|
|
|
|
|
+ row: Dict[str, Any] = {
|
|
|
|
|
+ "id": sp.id,
|
|
|
|
|
+ "collectionProtocolId": sp.collection_protocol_id,
|
|
|
|
|
+ "protocolNo": (proto.protocol_no if proto else "") or "",
|
|
|
|
|
+ "protocolStatus": (proto.protocol_status if proto else "") or "",
|
|
|
|
|
+ "voidType": (proto.void_type if proto else "") or "",
|
|
|
|
|
+ "motherName": (party.mother_name if party else "") or "",
|
|
|
|
|
+ "motherPhone": (party.mother_phone if party else "") or "",
|
|
|
|
|
+ "sampleCode": sp.sample_code or "",
|
|
|
|
|
+ "payMode": sp.pay_mode or "",
|
|
|
|
|
+ "contractAmount": float(sp.contract_amount)
|
|
|
|
|
+ if sp.contract_amount is not None
|
|
|
|
|
+ else None,
|
|
|
|
|
+ }
|
|
|
|
|
+ row.update(_progress_to_json_fields(progress))
|
|
|
|
|
+ return row
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def list_sample_payments_page(
|
|
|
|
|
+ db: Session,
|
|
|
|
|
+ *,
|
|
|
|
|
+ tenant_no: str,
|
|
|
|
|
+ keyword: Optional[str] = None,
|
|
|
|
|
+ pay_mode: Optional[str] = None,
|
|
|
|
|
+ progress_status: Optional[str] = None,
|
|
|
|
|
+ due_date_start: Optional[str] = None,
|
|
|
|
|
+ due_date_end: Optional[str] = None,
|
|
|
|
|
+ page: int = 1,
|
|
|
|
|
+ page_size: int = 10,
|
|
|
|
|
+ user_info=None,
|
|
|
|
|
+) -> Tuple[List[Dict[str, Any]], int]:
|
|
|
|
|
+ if not tenant_no:
|
|
|
|
|
+ return [], 0
|
|
|
|
|
+ q = (
|
|
|
|
|
+ db.query(
|
|
|
|
|
+ BizProtocolSamplePayment,
|
|
|
|
|
+ BizCollectionProtocol,
|
|
|
|
|
+ BizProtocolPartyCustomer,
|
|
|
|
|
+ )
|
|
|
|
|
+ .join(
|
|
|
|
|
+ BizCollectionProtocol,
|
|
|
|
|
+ and_(
|
|
|
|
|
+ BizCollectionProtocol.id
|
|
|
|
|
+ == BizProtocolSamplePayment.collection_protocol_id,
|
|
|
|
|
+ BizCollectionProtocol.del_flag == 0,
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ .outerjoin(
|
|
|
|
|
+ BizProtocolPartyCustomer,
|
|
|
|
|
+ and_(
|
|
|
|
|
+ BizProtocolPartyCustomer.collection_protocol_id
|
|
|
|
|
+ == BizProtocolSamplePayment.collection_protocol_id,
|
|
|
|
|
+ BizProtocolPartyCustomer.tenant_no == tenant_no,
|
|
|
|
|
+ BizProtocolPartyCustomer.del_flag == 0,
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ .filter(
|
|
|
|
|
+ BizProtocolSamplePayment.tenant_no == tenant_no,
|
|
|
|
|
+ BizProtocolSamplePayment.del_flag == 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ kw = (keyword or "").strip()
|
|
|
|
|
+ if kw:
|
|
|
|
|
+ like = f"%{kw}%"
|
|
|
|
|
+ q = q.filter(
|
|
|
|
|
+ or_(
|
|
|
|
|
+ BizCollectionProtocol.protocol_no.like(like),
|
|
|
|
|
+ BizProtocolSamplePayment.sample_code.like(like),
|
|
|
|
|
+ BizProtocolPartyCustomer.mother_name.like(like),
|
|
|
|
|
+ BizProtocolPartyCustomer.mother_phone.like(like),
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ mode = (pay_mode or "").strip()
|
|
|
|
|
+ if mode:
|
|
|
|
|
+ q = q.filter(BizProtocolSamplePayment.pay_mode == mode)
|
|
|
|
|
+ ds = (due_date_start or "").strip().replace("-", "")[:8]
|
|
|
|
|
+ de = (due_date_end or "").strip().replace("-", "")[:8]
|
|
|
|
|
+ if ds or de:
|
|
|
|
|
+ due_conditions = [
|
|
|
|
|
+ BizProtocolPaymentSchedule.sample_payment_id
|
|
|
|
|
+ == BizProtocolSamplePayment.id,
|
|
|
|
|
+ BizProtocolPaymentSchedule.tenant_no == tenant_no,
|
|
|
|
|
+ BizProtocolPaymentSchedule.del_flag == 0,
|
|
|
|
|
+ BizProtocolPaymentSchedule.schedule_status != "void",
|
|
|
|
|
+ ]
|
|
|
|
|
+ if ds:
|
|
|
|
|
+ due_conditions.append(BizProtocolPaymentSchedule.due_date >= ds)
|
|
|
|
|
+ if de:
|
|
|
|
|
+ due_conditions.append(BizProtocolPaymentSchedule.due_date <= de)
|
|
|
|
|
+ q = q.filter(exists().where(and_(*due_conditions)))
|
|
|
|
|
+ if user_info:
|
|
|
|
|
+ from app.core.data_scope import apply_data_scope
|
|
|
|
|
+
|
|
|
|
|
+ q = apply_data_scope(q, BizCollectionProtocol, user_info, db, tenant_no)
|
|
|
|
|
+ candidates = q.order_by(BizProtocolSamplePayment.create_time.desc()).all()
|
|
|
|
|
+ if not candidates:
|
|
|
|
|
+ return [], 0
|
|
|
|
|
+ pids = [sp.id for sp, _, _ in candidates]
|
|
|
|
|
+ schedules_by_payment, transfers_by_payment = _load_payment_children_batch(
|
|
|
|
|
+ db, pids, tenant_no
|
|
|
|
|
+ )
|
|
|
|
|
+ rows: List[Dict[str, Any]] = []
|
|
|
|
|
+ for sp, proto, party in candidates:
|
|
|
|
|
+ schedules = schedules_by_payment.get(sp.id, [])
|
|
|
|
|
+ transfers = transfers_by_payment.get(sp.id, [])
|
|
|
|
|
+ progress = _compute_progress_for_payment(schedules, transfers)
|
|
|
|
|
+ rows.append(_build_list_summary_row(sp, proto, party, progress))
|
|
|
|
|
+ return _filter_and_page_progress(rows, progress_status, page, page_size)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def get_sample_payment_detail(
|
|
|
|
|
+ db: Session,
|
|
|
|
|
+ *,
|
|
|
|
|
+ sample_payment_id: str,
|
|
|
|
|
+ tenant_no: str,
|
|
|
|
|
+ user_info=None,
|
|
|
|
|
+) -> Optional[Dict[str, Any]]:
|
|
|
|
|
+ q = (
|
|
|
|
|
+ db.query(
|
|
|
|
|
+ BizProtocolSamplePayment,
|
|
|
|
|
+ BizCollectionProtocol,
|
|
|
|
|
+ BizProtocolPartyCustomer,
|
|
|
|
|
+ )
|
|
|
|
|
+ .join(
|
|
|
|
|
+ BizCollectionProtocol,
|
|
|
|
|
+ and_(
|
|
|
|
|
+ BizCollectionProtocol.id
|
|
|
|
|
+ == BizProtocolSamplePayment.collection_protocol_id,
|
|
|
|
|
+ BizCollectionProtocol.del_flag == 0,
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ .outerjoin(
|
|
|
|
|
+ BizProtocolPartyCustomer,
|
|
|
|
|
+ and_(
|
|
|
|
|
+ BizProtocolPartyCustomer.collection_protocol_id
|
|
|
|
|
+ == BizProtocolSamplePayment.collection_protocol_id,
|
|
|
|
|
+ BizProtocolPartyCustomer.tenant_no == tenant_no,
|
|
|
|
|
+ BizProtocolPartyCustomer.del_flag == 0,
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ .filter(
|
|
|
|
|
+ BizProtocolSamplePayment.id == sample_payment_id,
|
|
|
|
|
+ BizProtocolSamplePayment.tenant_no == tenant_no,
|
|
|
|
|
+ BizProtocolSamplePayment.del_flag == 0,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ if user_info:
|
|
|
|
|
+ from app.core.data_scope import apply_data_scope
|
|
|
|
|
+
|
|
|
|
|
+ q = apply_data_scope(q, BizCollectionProtocol, user_info, db, tenant_no)
|
|
|
|
|
+ row = q.first()
|
|
|
|
|
+ if not row:
|
|
|
|
|
+ return None
|
|
|
|
|
+ sp, proto, party = row
|
|
|
|
|
+ schedules_by_payment, transfers_by_payment = _load_payment_children_batch(
|
|
|
|
|
+ db, [sp.id], tenant_no
|
|
|
|
|
+ )
|
|
|
|
|
+ schedules = schedules_by_payment.get(sp.id, [])
|
|
|
|
|
+ transfers = transfers_by_payment.get(sp.id, [])
|
|
|
|
|
+ by_schedule: Dict[str, List[BizProtocolPaymentTransfer]] = {}
|
|
|
|
|
+ for t in transfers:
|
|
|
|
|
+ by_schedule.setdefault(t.schedule_id, []).append(t)
|
|
|
|
|
+ detail = _sample_payment_to_dict(db, sp, schedules, by_schedule, tenant_no)
|
|
|
|
|
+ detail["protocolNo"] = (proto.protocol_no if proto else "") or ""
|
|
|
|
|
+ detail["protocolStatus"] = (proto.protocol_status if proto else "") or ""
|
|
|
|
|
+ detail["voidType"] = (proto.void_type if proto else "") or ""
|
|
|
|
|
+ detail["motherName"] = (party.mother_name if party else "") or ""
|
|
|
|
|
+ detail["motherPhone"] = (party.mother_phone if party else "") or ""
|
|
|
|
|
+ progress = _compute_progress_for_payment(schedules, transfers)
|
|
|
|
|
+ detail.update(_progress_to_json_fields(progress))
|
|
|
|
|
+ return detail
|