diff --git a/backend/app/api/pl/plan.py b/backend/app/api/pl/plan.py index 16b6234b7..547870305 100644 --- a/backend/app/api/pl/plan.py +++ b/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] diff --git a/backend/app/services/pl_plan_contract_gap.py b/backend/app/services/pl_plan_contract_gap.py index 6441387a4..f0a9a4d77 100644 --- a/backend/app/services/pl_plan_contract_gap.py +++ b/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: