Просмотр исходного кода

feat(pl): wire deliveryTimeFrom/To through plan list and export

Contract-gap list/export share the same stop-date EXISTS filter.

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong 1 месяц назад
Родитель
Сommit
204b5f48c8
2 измененных файлов с 52 добавлено и 2 удалено
  1. 37 0
      backend/app/api/pl/plan.py
  2. 15 2
      backend/app/services/pl_plan_contract_gap.py

+ 37 - 0
backend/app/api/pl/plan.py

@@ -117,6 +117,7 @@ from app.services.pl_plan_print_service import get_delivery_slip_print_context
 from app.services.pl_plan_stop_service import (
     attach_plan_stops_to_item,
     collect_stop_qty_warnings,
+    collect_unique_delivery_date_strings,
     load_plan_stops_grouped,
     replace_plan_stops,
     sync_plan_legacy_from_stops,
@@ -383,6 +384,8 @@ def _filters_from_params(params: dict, *, db: Session, tenant_no: str) -> list:
         license_plate=_param_get(params, "licensePlate", "license_plate"),
         plan_date_from=_param_get(params, "planDateFrom", "plan_date_from"),
         plan_date_to=_param_get(params, "planDateTo", "plan_date_to"),
+        delivery_time_from=_param_get(params, "deliveryTimeFrom", "delivery_time_from"),
+        delivery_time_to=_param_get(params, "deliveryTimeTo", "delivery_time_to"),
         cert_upload_status=_param_get(params, "certUploadStatus", "cert_upload_status"),
         hazard_cert_ready=_param_get(params, "hazardCertReady", "hazard_cert_ready"),
         hazard_cert_ready_in=_param_get(
@@ -431,6 +434,12 @@ def list_plans(
     license_plate: Optional[str] = Query(default=None, alias="licensePlate", description="车牌号"),
     plan_date_from: Optional[date] = Query(default=None, alias="planDateFrom", description="计划日期开始"),
     plan_date_to: Optional[date] = Query(default=None, alias="planDateTo", description="计划日期结束"),
+    delivery_time_from: Optional[date] = Query(
+        default=None, alias="deliveryTimeFrom", description="送货时间开始"
+    ),
+    delivery_time_to: Optional[date] = Query(
+        default=None, alias="deliveryTimeTo", description="送货时间结束"
+    ),
     has_hazard_product: Optional[int] = Query(
         default=None,
         alias="hasHazardProduct",
@@ -505,6 +514,8 @@ def list_plans(
         licensePlate=license_plate,
         planDateFrom=plan_date_from,
         planDateTo=plan_date_to,
+        deliveryTimeFrom=delivery_time_from,
+        deliveryTimeTo=delivery_time_to,
         certUploadStatus=cert_upload_status,
         hazardCertReady=hazard_cert_ready,
     )
@@ -633,6 +644,7 @@ PLAN_EXPORT_COLUMNS = [
     ExportColumn(title="计划编号", field="planNo"),
     ExportColumn(title="跟进状态", field="followStatus"),
     ExportColumn(title="计划日期", field="planDate"),
+    ExportColumn(title="送货时间", field="deliveryTimes"),
     ExportColumn(title="单据类型", field="planType"),
     ExportColumn(title="供方", field="supplierName"),
     ExportColumn(title="需方", field="customerName"),
@@ -703,6 +715,8 @@ class PlanExportBody(BaseModel):
     license_plate: Optional[str] = Field(default=None, alias="licensePlate")
     plan_date_from: Optional[date] = Field(default=None, alias="planDateFrom")
     plan_date_to: Optional[date] = Field(default=None, alias="planDateTo")
+    delivery_time_from: Optional[date] = Field(default=None, alias="deliveryTimeFrom")
+    delivery_time_to: Optional[date] = Field(default=None, alias="deliveryTimeTo")
     has_hazard_product: Optional[int] = Field(default=None, alias="hasHazardProduct")
     cert_upload_status: Optional[str] = Field(default=None, alias="certUploadStatus")
     hazard_cert_ready: Optional[int] = Field(default=None, alias="hazardCertReady")
@@ -771,6 +785,8 @@ def export_plans(
             sheet_title = "计划归档"
             filename_stem = f"计划归档_{date_str}"
         else:
+            plan_ids = [plan.id for plan in plans]
+            stops_by_plan = load_plan_stops_grouped(db, tenant_no, plan_ids)
             items = []
             for plan in plans:
                 data = PlanWithProducts.model_validate(plan).model_dump(by_alias=True)
@@ -778,6 +794,9 @@ def export_plans(
                     data["planDate"] = str(data["planDate"])[:10]
                 if data.get("erpEntered") is not None:
                     data["erpEntered"] = "是" if int(data["erpEntered"]) else "否"
+                _load_read, delivery_reads = stops_by_plan.get(plan.id, (None, []))
+                times = collect_unique_delivery_date_strings(delivery_reads)
+                data["deliveryTimes"] = " / ".join(times) if times else ""
                 items.append(data)
             cols = filter_export_columns(PLAN_EXPORT_COLUMNS, body.columns)
             sheet_title = "计划列表"
@@ -884,6 +903,8 @@ def list_contract_gaps(
     ),
     plan_date_from: Optional[date] = Query(default=None, alias="planDateFrom"),
     plan_date_to: Optional[date] = Query(default=None, alias="planDateTo"),
+    delivery_time_from: Optional[date] = Query(default=None, alias="deliveryTimeFrom"),
+    delivery_time_to: Optional[date] = Query(default=None, alias="deliveryTimeTo"),
     plan_type: Optional[str] = Query(default=None, alias="planType"),
     follow_status: Optional[str] = Query(default=None, alias="followStatus"),
     q: Optional[str] = Query(
@@ -901,6 +922,8 @@ def list_contract_gaps(
         customerKeyword=customer_keyword,
         planDateFrom=plan_date_from,
         planDateTo=plan_date_to,
+        deliveryTimeFrom=delivery_time_from,
+        deliveryTimeTo=delivery_time_to,
         planType=plan_type,
         followStatus=follow_status,
     )
@@ -915,6 +938,8 @@ def list_contract_gaps(
         customer_keyword=_param_get(params, "customerKeyword", "customer_keyword"),
         plan_date_from=_param_get(params, "planDateFrom", "plan_date_from"),
         plan_date_to=_param_get(params, "planDateTo", "plan_date_to"),
+        delivery_time_from=_param_get(params, "deliveryTimeFrom", "delivery_time_from"),
+        delivery_time_to=_param_get(params, "deliveryTimeTo", "delivery_time_to"),
         plan_type=_param_get(params, "planType", "plan_type"),
         plan_type_in=_param_get(params, "planTypeIn", "plan_type_in"),
         follow_status=_param_get(params, "followStatus", "followStatusIn", "follow_status"),
@@ -929,6 +954,7 @@ def list_contract_gaps(
 CONTRACT_GAP_EXPORT_COLUMNS = [
     ExportColumn(title="计划编号", field="planNo"),
     ExportColumn(title="计划日期", field="planDate"),
+    ExportColumn(title="送货时间", field="deliveryTimes"),
     ExportColumn(title="单据类型", field="planType"),
     ExportColumn(title="跟进状态", field="followStatus"),
     ExportColumn(title="供方", field="supplierName"),
@@ -943,6 +969,8 @@ class ContractGapExportBody(BaseModel):
     customer_keyword: Optional[str] = Field(default=None, alias="customerKeyword")
     plan_date_from: Optional[date] = Field(default=None, alias="planDateFrom")
     plan_date_to: Optional[date] = Field(default=None, alias="planDateTo")
+    delivery_time_from: Optional[date] = Field(default=None, alias="deliveryTimeFrom")
+    delivery_time_to: Optional[date] = Field(default=None, alias="deliveryTimeTo")
     q: Optional[str] = None
     ids: Optional[List[str]] = None
     columns: Optional[List[str]] = None
@@ -969,6 +997,12 @@ def export_contract_gaps(
             customer_keyword=_param_get(params, "customerKeyword", "customer_keyword"),
             plan_date_from=_param_get(params, "planDateFrom", "plan_date_from"),
             plan_date_to=_param_get(params, "planDateTo", "plan_date_to"),
+            delivery_time_from=_param_get(
+                params, "deliveryTimeFrom", "delivery_time_from"
+            ),
+            delivery_time_to=_param_get(
+                params, "deliveryTimeTo", "delivery_time_to"
+            ),
             plan_type=_param_get(params, "planType", "plan_type"),
             plan_type_in=_param_get(params, "planTypeIn", "plan_type_in"),
             follow_status=_param_get(
@@ -995,6 +1029,9 @@ def export_contract_gaps(
                 data["gapType"] = "仅缺需方合同"
             else:
                 data["gapType"] = ""
+            times = data.get("deliveryTimes") or []
+            if isinstance(times, list):
+                data["deliveryTimes"] = " / ".join(str(t) for t in times) if times else ""
             for k, v in list(data.items()):
                 if hasattr(v, "isoformat"):
                     data[k] = str(v)[:19]

+ 15 - 2
backend/app/services/pl_plan_contract_gap.py

@@ -22,8 +22,11 @@ from app.schemas.pl.plan import (
     PlanLoadRead,
     PlanProductRead,
 )
-from app.services.pl.plan_list_filters import build_plan_column_filters
-from app.services.pl_plan_stop_service import collect_unique_delivery_date_strings
+from app.services.pl.plan_list_filters import (
+    _parse_date,
+    build_plan_column_filters,
+    plan_delivery_time_exists,
+)
 from app.services.pl_data_scope import apply_pl_scope
 from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES, is_internal_customer_name
 from app.services.pl_plan_service import (
@@ -38,6 +41,7 @@ from app.services.pl_plan_stop_service import (
     PLAN_CONTRACT_SIDE_DELIVERY,
     PLAN_CONTRACT_SIDE_LOAD,
     assert_at_most_one_contract,
+    collect_unique_delivery_date_strings,
     load_plan_stops_grouped,
 )
 from app.services.pl_product_display import load_crm_product_alias_maps, resolve_product_display_names
@@ -330,6 +334,8 @@ def list_plan_contract_gaps(
     customer_keyword: Optional[str] = None,
     plan_date_from: Optional[date] = None,
     plan_date_to: Optional[date] = None,
+    delivery_time_from: Optional[date] = None,
+    delivery_time_to: Optional[date] = None,
     plan_type: Optional[str] = None,
     plan_type_in: Optional[str] = None,
     follow_status: Optional[str] = None,
@@ -419,6 +425,13 @@ def list_plan_contract_gaps(
     if column_filters:
         query = query.filter(*column_filters)
 
+    dt_from = _parse_date(delivery_time_from)
+    dt_to = _parse_date(delivery_time_to)
+    if dt_from is not None or dt_to is not None:
+        query = query.filter(
+            plan_delivery_time_exists(db, tenant_no, d_from=dt_from, d_to=dt_to)
+        )
+
     if q:
         q_kw = q.strip()
         if q_kw: