sf_express_settlement.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. """顺丰月结列表、结算、补填。只处理 transport_mode = sfkd。"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from typing import Any, Dict, List, Optional, Tuple
  5. from sqlalchemy.orm import Session
  6. from app.core.data_scope import apply_data_scope
  7. from app.crud.sample_freight import _fmt_dt, _normalize_date_bound, _user_nick_name_map
  8. from app.models.models import BizSampleFreight
  9. from app.services.sf_express_settlement import (
  10. SF_TRANSPORT_MODE,
  11. SETTLE_SETTLED,
  12. assert_settle_request,
  13. freight_amount_label,
  14. plan_fill_amount,
  15. plan_settle,
  16. )
  17. def _base_query(db: Session, *, tenant_no: str, user_info: Optional[dict]):
  18. query = db.query(BizSampleFreight).filter(
  19. BizSampleFreight.tenant_no == tenant_no,
  20. BizSampleFreight.del_flag == 0,
  21. BizSampleFreight.transport_mode == SF_TRANSPORT_MODE,
  22. )
  23. return apply_data_scope(query, BizSampleFreight, user_info, db, tenant_no)
  24. def _apply_filters(
  25. query,
  26. *,
  27. waybill_no: Optional[str],
  28. settle_status: Optional[str],
  29. create_time_from: Optional[str],
  30. create_time_to: Optional[str],
  31. ):
  32. waybill = (waybill_no or "").strip()
  33. if waybill:
  34. query = query.filter(BizSampleFreight.waybill_no.like(f"%{waybill}%"))
  35. status = (settle_status or "").strip()
  36. if status:
  37. query = query.filter(BizSampleFreight.settle_status == status)
  38. if create_time_from and str(create_time_from).strip():
  39. query = query.filter(
  40. BizSampleFreight.create_time
  41. >= _normalize_date_bound(str(create_time_from).strip())
  42. )
  43. if create_time_to and str(create_time_to).strip():
  44. query = query.filter(
  45. BizSampleFreight.create_time
  46. <= _normalize_date_bound(str(create_time_to).strip(), end_of_day=True)
  47. )
  48. return query
  49. def _to_row(obj: BizSampleFreight, create_by_name: str) -> Dict[str, Any]:
  50. amount = obj.freight_amount
  51. status = obj.settle_status or ""
  52. return {
  53. "id": obj.id,
  54. "waybillNo": obj.waybill_no or "",
  55. "createBy": obj.create_by or "",
  56. "createByName": create_by_name or obj.create_by or "",
  57. "createTime": _fmt_dt(obj.create_time),
  58. "settleStatus": status,
  59. "settleStatusLabel": "已结算" if status == SETTLE_SETTLED else "未结算",
  60. "freightAmount": float(amount) if amount is not None else None,
  61. "freightAmountLabel": freight_amount_label(obj.transport_mode, amount),
  62. "settleTime": _fmt_dt(obj.settle_time) if obj.settle_time else "",
  63. "transportMode": obj.transport_mode or "",
  64. }
  65. def list_sf_settlements(
  66. db,
  67. *,
  68. tenant_no: str,
  69. user_info: Optional[dict],
  70. page: int = 1,
  71. page_size: int = 10,
  72. waybill_no: Optional[str] = None,
  73. settle_status: Optional[str] = None,
  74. create_time_from: Optional[str] = None,
  75. create_time_to: Optional[str] = None,
  76. ) -> Tuple[List[Dict[str, Any]], int]:
  77. page = max(1, int(page or 1))
  78. page_size = max(1, min(100, int(page_size or 10)))
  79. query = _apply_filters(
  80. _base_query(db, tenant_no=tenant_no, user_info=user_info),
  81. waybill_no=waybill_no,
  82. settle_status=settle_status,
  83. create_time_from=create_time_from,
  84. create_time_to=create_time_to,
  85. )
  86. total = query.count()
  87. rows = (
  88. query.order_by(BizSampleFreight.create_time.desc(), BizSampleFreight.id.desc())
  89. .offset((page - 1) * page_size)
  90. .limit(page_size)
  91. .all()
  92. )
  93. names = _user_nick_name_map(db, [row.create_by for row in rows if row.create_by])
  94. return [_to_row(row, names.get(row.create_by or "", "")) for row in rows], total
  95. def list_sf_settlements_for_export(
  96. db,
  97. *,
  98. tenant_no: str,
  99. user_info: Optional[dict],
  100. waybill_no: Optional[str] = None,
  101. settle_status: Optional[str] = None,
  102. create_time_from: Optional[str] = None,
  103. create_time_to: Optional[str] = None,
  104. ) -> List[Dict[str, Any]]:
  105. query = _apply_filters(
  106. _base_query(db, tenant_no=tenant_no, user_info=user_info),
  107. waybill_no=waybill_no,
  108. settle_status=settle_status,
  109. create_time_from=create_time_from,
  110. create_time_to=create_time_to,
  111. )
  112. rows = query.order_by(
  113. BizSampleFreight.create_time.desc(), BizSampleFreight.id.desc()
  114. ).all()
  115. if not rows:
  116. raise ValueError("没有可导出的数据")
  117. names = _user_nick_name_map(db, [row.create_by for row in rows if row.create_by])
  118. return [_to_row(row, names.get(row.create_by or "", "")) for row in rows]
  119. def _lock_rows(
  120. db, *, tenant_no: str, user_info: Optional[dict], ids: List[str]
  121. ) -> List[BizSampleFreight]:
  122. wanted = [str(item or "").strip() for item in ids if str(item or "").strip()]
  123. rows = (
  124. _base_query(db, tenant_no=tenant_no, user_info=user_info)
  125. .filter(BizSampleFreight.id.in_(wanted))
  126. .all()
  127. )
  128. if len(rows) != len(set(wanted)):
  129. raise ValueError("运费批次不存在")
  130. return rows
  131. def settle_sf_freights(
  132. db,
  133. *,
  134. tenant_no: str,
  135. user_id: Optional[str],
  136. user_info: Optional[dict],
  137. ids: List[str],
  138. freight_amount: Any = None,
  139. ):
  140. assert_settle_request(ids, freight_amount)
  141. rows = _lock_rows(db, tenant_no=tenant_no, user_info=user_info, ids=ids)
  142. now = datetime.now()
  143. submitted = None if len(ids) != 1 else freight_amount
  144. for obj in rows:
  145. if obj.transport_mode != SF_TRANSPORT_MODE:
  146. raise ValueError("只能结算顺丰快递运费")
  147. if (obj.settle_status or "") == SETTLE_SETTLED:
  148. raise ValueError("该运费已结算")
  149. amount, status, settle_time = plan_settle(
  150. current_amount=obj.freight_amount, submitted_amount=submitted, now=now
  151. )
  152. obj.freight_amount = amount
  153. obj.settle_status = status
  154. obj.settle_time = settle_time
  155. obj.update_by = user_id
  156. obj.update_time = now
  157. db.commit()
  158. return {"ids": [row.id for row in rows]}
  159. def fill_sf_freight_amount(
  160. db,
  161. *,
  162. tenant_no: str,
  163. user_id: Optional[str],
  164. user_info: Optional[dict],
  165. freight_id: str,
  166. freight_amount: Any,
  167. ):
  168. rows = _lock_rows(db, tenant_no=tenant_no, user_info=user_info, ids=[freight_id])
  169. obj = rows[0]
  170. if obj.transport_mode != SF_TRANSPORT_MODE:
  171. raise ValueError("只能补填顺丰快递运费")
  172. obj.freight_amount = plan_fill_amount(
  173. current_amount=obj.freight_amount,
  174. settle_status=obj.settle_status,
  175. submitted_amount=freight_amount,
  176. )
  177. obj.update_by = user_id
  178. obj.update_time = datetime.now()
  179. db.commit()
  180. return {"id": obj.id}