bx_codegen.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. """报销单号/标题生成(从 bx-backend 迁移,增加 tenant_no 隔离)。"""
  2. from __future__ import annotations
  3. import re
  4. from datetime import date, datetime
  5. from sqlalchemy import func, text
  6. from sqlalchemy.orm import Session
  7. from app.models.bx_models import BxReimburseApply
  8. _AUTO_TITLE_SUFFIX_RE = re.compile(r"报销单\d{11}$")
  9. def generate_apply_code(db: Session, prefix: str, tenant_no: str) -> str:
  10. """
  11. 生成顺序单号:{prefix}{YYYYMMDD}{3位序号}。
  12. prefix 示例:BYJ(备用金)、BX(报销)。
  13. """
  14. today = datetime.now().strftime("%Y%m%d")
  15. like_pattern = f"{prefix}{today}%"
  16. result = db.execute(
  17. text(
  18. "SELECT MAX(CAST(SUBSTRING(apply_code, -3) AS UNSIGNED)) FROM ("
  19. "SELECT apply_code FROM bx_advance_apply "
  20. "WHERE tenant_no = :tenant_no AND apply_code LIKE :p1 AND del_flag = 0 "
  21. "UNION ALL "
  22. "SELECT apply_code FROM bx_reimburse_apply "
  23. "WHERE tenant_no = :tenant_no AND apply_code LIKE :p2 AND del_flag = 0"
  24. ") t"
  25. ),
  26. {"tenant_no": tenant_no, "p1": like_pattern, "p2": like_pattern},
  27. ).scalar()
  28. seq = (result or 0) + 1
  29. return f"{prefix}{today}{seq:03d}"
  30. def parse_reimburse_date(value: str | date | datetime | None) -> date:
  31. if value is None:
  32. return datetime.now().date()
  33. if isinstance(value, datetime):
  34. return value.date()
  35. if isinstance(value, date):
  36. return value
  37. text_value = str(value).strip()
  38. if not text_value:
  39. return datetime.now().date()
  40. return datetime.strptime(text_value, "%Y-%m-%d").date()
  41. def format_reimburse_title(applicant_name: str, reimburse_date: date, seq: int) -> str:
  42. return f"{applicant_name}报销单{reimburse_date.strftime('%Y%m%d')}{seq:03d}"
  43. def is_auto_reimburse_title(title: str) -> bool:
  44. return bool(_AUTO_TITLE_SUFFIX_RE.search(str(title or "").strip()))
  45. def preview_reimburse_title_seq(
  46. db: Session,
  47. tenant_no: str,
  48. reimburse_date: date,
  49. exclude_apply_id: str | None = None,
  50. ) -> int:
  51. """预览序号(不加行锁,仅用于弹窗展示)。"""
  52. query = db.query(func.max(BxReimburseApply.title_seq)).filter(
  53. BxReimburseApply.tenant_no == tenant_no,
  54. BxReimburseApply.reimburse_date == reimburse_date,
  55. BxReimburseApply.del_flag == 0,
  56. BxReimburseApply.title_seq.isnot(None),
  57. )
  58. if exclude_apply_id:
  59. query = query.filter(BxReimburseApply.id != exclude_apply_id)
  60. max_seq = query.scalar()
  61. return int(max_seq or 0) + 1
  62. def allocate_reimburse_title_seq(
  63. db: Session,
  64. tenant_no: str,
  65. reimburse_date: date,
  66. exclude_apply_id: str | None = None,
  67. ) -> int:
  68. """
  69. 分配同租户、同报销日期下的标题序号(MAX+1)。
  70. 配合 uk_tenant_reimburse_date_title_seq 唯一约束,并发冲突时由上层重试。
  71. """
  72. query = db.query(func.max(BxReimburseApply.title_seq)).filter(
  73. BxReimburseApply.tenant_no == tenant_no,
  74. BxReimburseApply.reimburse_date == reimburse_date,
  75. BxReimburseApply.del_flag == 0,
  76. BxReimburseApply.title_seq.isnot(None),
  77. )
  78. if exclude_apply_id:
  79. query = query.filter(BxReimburseApply.id != exclude_apply_id)
  80. max_seq = query.with_for_update().scalar()
  81. return int(max_seq or 0) + 1
  82. def generate_title(
  83. db: Session,
  84. applicant_name: str,
  85. tenant_no: str,
  86. reimburse_date: str | date | datetime | None = None,
  87. exclude_apply_id: str | None = None,
  88. ) -> str:
  89. """预览用标题生成(提交时仍会在服务端重新分配序号以保证并发安全)。"""
  90. rd = parse_reimburse_date(reimburse_date)
  91. seq = preview_reimburse_title_seq(db, tenant_no, rd, exclude_apply_id)
  92. return format_reimburse_title(applicant_name, rd, seq)
  93. def resolve_reimburse_title(
  94. db: Session,
  95. applicant_name: str,
  96. tenant_no: str,
  97. reimburse_date: date,
  98. requested_title: str | None,
  99. exclude_apply_id: str | None = None,
  100. ) -> tuple[str, int | None]:
  101. """
  102. 确定最终标题与 title_seq。
  103. - 未填标题或仍为自动标题格式时:按报销日期重新分配序号并生成标题。
  104. - 自定义标题:title_seq 为 None(不参与唯一序号约束)。
  105. """
  106. custom_title = str(requested_title or "").strip()
  107. if custom_title and not is_auto_reimburse_title(custom_title):
  108. return custom_title, None
  109. seq = allocate_reimburse_title_seq(
  110. db, tenant_no, reimburse_date, exclude_apply_id=exclude_apply_id
  111. )
  112. title = format_reimburse_title(applicant_name, reimburse_date, seq)
  113. return title, seq