pl_plan_archive_zip_export.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. """运输计划归档 ZIP 导出(计划特有附件收集 + 公共打包)。"""
  2. from __future__ import annotations
  3. from typing import Any, Callable, Mapping, Optional, Sequence
  4. from fastapi.responses import StreamingResponse
  5. from sqlalchemy.orm import Session
  6. from app.services.export_attachment_zip import (
  7. ARCHIVE_ZIP_ATTACH_COL_MAX,
  8. ARCHIVE_ZIP_MAX_BYTES,
  9. ARCHIVE_ZIP_MAX_FILES,
  10. build_attachment_filename,
  11. build_attachment_zip_bytes,
  12. build_attachment_zip_streaming_response,
  13. build_hyperlink_formula,
  14. first_row_no_by_entity_id,
  15. parse_url_list,
  16. sanitize_zip_segment,
  17. )
  18. from app.utils.excel_export import ExportColumn
  19. __all__ = [
  20. "ARCHIVE_ZIP_ATTACH_COL_MAX",
  21. "ARCHIVE_ZIP_MAX_BYTES",
  22. "ARCHIVE_ZIP_MAX_FILES",
  23. "sanitize_zip_segment",
  24. "build_attachment_filename",
  25. "first_row_no_by_plan_id",
  26. "build_hyperlink_formula",
  27. "parse_url_list",
  28. "collect_hazard_cert_items",
  29. "merge_plan_attachment_items",
  30. "load_plan_products_by_plan_ids",
  31. "build_archive_zip_bytes",
  32. "build_archive_zip_streaming_response",
  33. ]
  34. def first_row_no_by_plan_id(rows: Sequence[Mapping[str, Any]]) -> dict[str, int]:
  35. return first_row_no_by_entity_id(rows, id_field="planId")
  36. def collect_hazard_cert_items(products: Sequence[Any]) -> list[dict[str, str]]:
  37. out: list[dict[str, str]] = []
  38. for product in products or []:
  39. pname = sanitize_zip_segment(getattr(product, "product_name", None) or "")
  40. specs = (
  41. ("购买证", getattr(product, "purchase_cert_url", None)),
  42. ("运输证", getattr(product, "transport_cert_url", None)),
  43. ("合法使用证明", getattr(product, "legal_use_cert_url", None)),
  44. )
  45. for base_label, raw in specs:
  46. for url in parse_url_list(raw):
  47. label = base_label if not pname or pname == "未命名" else f"{base_label}·{pname}"
  48. out.append({"url": url, "label": label})
  49. return out
  50. def merge_plan_attachment_items(
  51. document_items: Sequence[Mapping[str, str]],
  52. hazard_items: Sequence[Mapping[str, str]],
  53. ) -> list[dict[str, str]]:
  54. seen: set[str] = set()
  55. out: list[dict[str, str]] = []
  56. for item in list(document_items or []) + list(hazard_items or []):
  57. url = str(item.get("url") or "").strip()
  58. if not url or url in seen:
  59. continue
  60. seen.add(url)
  61. out.append({"url": url, "label": str(item.get("label") or "附件")})
  62. return out
  63. def load_plan_products_by_plan_ids(db, *, tenant_no: str, plan_ids: list[str]):
  64. from app.models.pl_models import PlPlanProduct
  65. if not plan_ids:
  66. return {}
  67. rows = (
  68. db.query(PlPlanProduct)
  69. .filter(
  70. PlPlanProduct.plan_id.in_(plan_ids),
  71. PlPlanProduct.tenant_no == tenant_no,
  72. PlPlanProduct.del_flag == 0,
  73. )
  74. .order_by(PlPlanProduct.order_num, PlPlanProduct.create_time)
  75. .all()
  76. )
  77. grouped: dict[str, list] = {pid: [] for pid in plan_ids}
  78. for row in rows:
  79. grouped.setdefault(row.plan_id, []).append(row)
  80. return grouped
  81. def build_archive_zip_bytes(
  82. *,
  83. db: Optional[Session],
  84. tenant_no: str,
  85. columns: Sequence[ExportColumn],
  86. rows: Sequence[Mapping[str, Any]],
  87. attachments_by_plan: Mapping[str, Sequence[Mapping[str, str]]],
  88. stamp: str,
  89. download_fn: Optional[Callable[[str], bytes]] = None,
  90. ) -> tuple[bytes, list[str]]:
  91. """打包归档 ZIP:xlsx + 附件/;返回 (zip_bytes, failure_messages)。"""
  92. return build_attachment_zip_bytes(
  93. db=db,
  94. tenant_no=tenant_no,
  95. columns=columns,
  96. rows=rows,
  97. attachments_by_id=attachments_by_plan,
  98. stamp=stamp,
  99. package_label="计划归档",
  100. id_field="planId",
  101. sheet_title="计划归档",
  102. xlsx_basename="(总表)计划归档.xlsx",
  103. download_fn=download_fn,
  104. )
  105. def build_archive_zip_streaming_response(
  106. *,
  107. db: Session,
  108. tenant_no: str,
  109. columns: Sequence[ExportColumn],
  110. rows: Sequence[Mapping[str, Any]],
  111. attachments_by_plan: Mapping[str, Sequence[Mapping[str, str]]],
  112. stamp: str,
  113. download_fn: Optional[Callable[[str], bytes]] = None,
  114. ) -> StreamingResponse:
  115. return build_attachment_zip_streaming_response(
  116. db=db,
  117. tenant_no=tenant_no,
  118. columns=columns,
  119. rows=rows,
  120. attachments_by_id=attachments_by_plan,
  121. stamp=stamp,
  122. package_label="计划归档",
  123. id_field="planId",
  124. sheet_title="计划归档",
  125. xlsx_basename="(总表)计划归档.xlsx",
  126. download_fn=download_fn,
  127. )