This commit is contained in:
zlwu committed 2026-08-27 19:38:12 +08:00
1 parent ebf006eeff
commit 16986f443e
30 files changed
+2319

No files matched your search

@@ -0,0 +1,313 @@
"""运输计划归档导出:按装货/送货段 × 段内产品展开,与列表展示规则一致。"""
from __future__ import annotations
from datetime import date, datetime
from decimal import Decimal
from typing import Optional, Union
from sqlalchemy.orm import Session
from app.models.pl_models import PlPlan, PlPlanProduct, PlTransport
from app.schemas.pl.plan import PlanDeliveryRead, PlanLoadRead, PlanStopProductRead
from app.services.pl_internal_customer import is_internal_customer_name
from app.services.pl_plan_stop_service import load_plan_stops_grouped
StopKind = str # "load" | "delivery"
PLAN_TYPE_LABELS = {
"direct": "直销单",
"inbound": "入库单",
"outbound": "出库单",
}
ARCHIVE_STATUS_LABELS = {
"archived": "已归档",
"unarchived": "未归档",
}
FOLLOW_STATUS_LABELS = {
"wait_notice": "等通知",
"pending_arrange": "待安排",
"arranged": "已安排",
"completed": "已完成",
"cancelled": "已取消",
"draft": "待安排",
"processing": "进行中",
}
FREIGHT_TYPE_LABELS = {
"by_weight": "按重量",
"by_trip": "按趟",
}
def resolve_archive_list_io_column(kind: StopKind, customer_name: Optional[str]) -> str:
"""装货/送货段入库出库列方向(与前端 resolveArchiveListIoColumn 一致)。"""
internal = is_internal_customer_name(customer_name)
if kind == "load":
return "outbound" if internal else "inbound"
return "inbound" if internal else "outbound"
def _format_date_value(value: Optional[Union[date, datetime, str]]) -> str:
if value is None:
return ""
return str(value)[:10]
def _format_plate_display(license_plate: Optional[str], trailer_no: Optional[str]) -> str:
plate = (license_plate or "").strip()
trailer = (trailer_no or "").strip()
if not plate and not trailer:
return ""
if plate and trailer:
return f"{plate}{trailer}"
return plate or trailer
def _get_stop_archive_actual_qty(product: PlanStopProductRead, kind: StopKind) -> Optional[Decimal]:
return product.inbound_qty if kind == "load" else product.outbound_qty
def _get_stop_archive_unit_price(product: PlanStopProductRead, kind: StopKind) -> Optional[Decimal]:
return product.inbound_price if kind == "load" else product.outbound_price
def _get_stop_archive_amount(product: PlanStopProductRead, kind: StopKind) -> Optional[Decimal]:
stored = product.inbound_amount if kind == "load" else product.outbound_amount
if stored is not None:
return stored
qty = _get_stop_archive_actual_qty(product, kind)
price = _get_stop_archive_unit_price(product, kind)
if qty is not None and price is not None:
return qty * price
return None
def _stop_kind_label(kind: StopKind, delivery_seq: Optional[int] = None) -> str:
if kind == "load":
return "装货"
return f"送货 #{delivery_seq or 1}"
def _stop_time(kind: StopKind, stop: Union[PlanLoadRead, PlanDeliveryRead]) -> str:
if kind == "load":
return _format_date_value(getattr(stop, "load_time", None))
return _format_date_value(getattr(stop, "delivery_time", None))
def _load_legacy_products_by_plan(
db: Session, tenant_no: str, plan_ids: list[str]
) -> dict[str, list[PlPlanProduct]]:
if not plan_ids:
return {}
products = (
db.query(PlPlanProduct)
.filter(
PlPlanProduct.tenant_no == tenant_no,
PlPlanProduct.del_flag == 0,
PlPlanProduct.plan_id.in_(plan_ids),
)
.order_by(PlPlanProduct.order_num)
.all()
)
grouped: dict[str, list[PlPlanProduct]] = {}
for product in products:
grouped.setdefault(product.plan_id, []).append(product)
return grouped
def _plan_common_export_fields(
plan: PlPlan,
transport: Optional[PlTransport],
) -> dict:
freight_type_raw = transport.freight_type if transport else "by_weight"
freight_type_label = FREIGHT_TYPE_LABELS.get(freight_type_raw, freight_type_raw or "")
freight_price = transport.freight_price if transport else None
freight_amount = plan.freight_amount
if freight_amount is None and transport is not None:
freight_amount = transport.freight_amount
plan_date = _format_date_value(plan.plan_date)
return {
"planNo": plan.plan_no or "",
"planDate": plan_date,
"archiveStatus": ARCHIVE_STATUS_LABELS.get(plan.archive_status or "", plan.archive_status or ""),
"followStatus": FOLLOW_STATUS_LABELS.get(plan.follow_status or "", plan.follow_status or ""),
"planType": PLAN_TYPE_LABELS.get(plan.plan_type or "", plan.plan_type or ""),
"licensePlate": _format_plate_display(plan.license_plate, plan.trailer_no),
"erpEntered": "是" if int(plan.erp_entered or 0) == 1 else "否",
"freightType": freight_type_label,
"freightPrice": freight_price,
"freightAmount": freight_amount,
"remark": plan.remark or "",
}
def _empty_product_fields() -> dict:
return {
"productName": "",
"model": "",
"packageSpec": "",
"unit": "",
"planQty": None,
"actualQty": None,
"inboundQty": None,
"outboundQty": None,
"unitPrice": None,
"amount": None,
}
def _product_row_from_stop_product(
product: PlanStopProductRead,
kind: StopKind,
customer_name: Optional[str],
) -> dict:
actual_qty = _get_stop_archive_actual_qty(product, kind)
unit_price = _get_stop_archive_unit_price(product, kind)
amount = _get_stop_archive_amount(product, kind)
io_col = resolve_archive_list_io_column(kind, customer_name)
inbound_qty = actual_qty if io_col == "inbound" else None
outbound_qty = actual_qty if io_col == "outbound" else None
return {
"productName": product.product_name or "",
"model": product.model or "",
"packageSpec": product.package_spec or "",
"unit": product.unit or "",
"planQty": product.plan_qty,
"actualQty": actual_qty,
"inboundQty": inbound_qty,
"outboundQty": outbound_qty,
"unitPrice": unit_price,
"amount": amount,
}
def _append_stop_product_rows(
rows: list[dict],
row_no: int,
plan_common: dict,
kind: StopKind,
stop: Union[PlanLoadRead, PlanDeliveryRead],
delivery_seq: Optional[int],
products: list[PlanStopProductRead],
) -> int:
customer_name = stop.customer_name or ""
stop_common = {
**plan_common,
"stopKind": _stop_kind_label(kind, delivery_seq),
"stopCustomer": customer_name,
"stopAddress": stop.address or "",
"stopTime": _stop_time(kind, stop),
}
product_rows = products or [None]
for product in product_rows:
row_no += 1
row = {
**stop_common,
"rowNo": row_no,
**_empty_product_fields(),
}
if product is not None:
row.update(_product_row_from_stop_product(product, kind, customer_name))
rows.append(row)
return row_no
def _append_legacy_plan_product_rows(
rows: list[dict],
row_no: int,
plan: PlPlan,
plan_common: dict,
products: list[PlPlanProduct],
) -> int:
"""无段表数据时回退计划产品级导出(兼容旧数据)。"""
stop_common = {
**plan_common,
"stopKind": "",
"stopCustomer": plan.customer_name or plan.supplier_name or "",
"stopAddress": plan.destination_name or "",
"stopTime": _format_date_value(plan.delivery_time),
}
product_rows = products or [None]
for product in product_rows:
row_no += 1
row = {
**stop_common,
"rowNo": row_no,
**_empty_product_fields(),
}
if product is not None:
row.update(
{
"productName": product.product_name or "",
"model": product.model or "",
"packageSpec": product.package_spec or "",
"unit": product.unit or "",
"planQty": product.quantity,
"actualQty": product.actual_qty,
"inboundQty": product.inbound_qty,
"outboundQty": product.outbound_qty,
"unitPrice": product.inbound_price or product.outbound_price,
"amount": product.inbound_amount or product.outbound_amount,
}
)
rows.append(row)
return row_no
def build_plan_archive_export_rows(
db: Session,
tenant_no: str,
plans: list[PlPlan],
transports_by_id: dict[str, PlTransport],
) -> list[dict]:
"""构建归档导出数据行:每计划 × 每段次 × 每段产品一行。"""
if not plans:
return []
plan_ids = [plan.id for plan in plans]
stops_by_plan = load_plan_stops_grouped(db, tenant_no, plan_ids)
legacy_products_by_plan = _load_legacy_products_by_plan(db, tenant_no, plan_ids)
rows: list[dict] = []
row_no = 0
for plan in plans:
transport = transports_by_id.get(plan.transport_id) if plan.transport_id else None
plan_common = _plan_common_export_fields(plan, transport)
load_read, delivery_reads = stops_by_plan.get(plan.id, (None, []))
has_stops = load_read is not None or bool(delivery_reads)
if not has_stops:
row_no = _append_legacy_plan_product_rows(
rows,
row_no,
plan,
plan_common,
legacy_products_by_plan.get(plan.id, []),
)
continue
if load_read is not None:
row_no = _append_stop_product_rows(
rows,
row_no,
plan_common,
"load",
load_read,
None,
load_read.products or [],
)
for idx, delivery_read in enumerate(delivery_reads):
delivery_seq = delivery_read.seq or (idx + 1)
row_no = _append_stop_product_rows(
rows,
row_no,
plan_common,
"delivery",
delivery_read,
delivery_seq,
delivery_read.products or [],
)
return rows
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_resolve_list_product_name", "label": "_resolve_list_product_name()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L145"}, {"id": "plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_resolve_create_date", "label": "_resolve_create_date()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L161"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L165"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_format_subpack_slip_no", "label": "_format_subpack_slip_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L197"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_normalize_subpack_slip_no", "label": "_normalize_subpack_slip_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L201"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_subpack_slip_no_seq", "label": "_subpack_slip_no_seq()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L226"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_ensure_subpack_slip_no", "label": "_ensure_subpack_slip_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L234"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_determine_plan_type", "label": "_determine_plan_type()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L266"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_get_plan_type_label", "label": "_get_plan_type_label()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L271"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_generate_notify_message", "label": "_generate_notify_message()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L280"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_plan_product_name_exists", "label": "_plan_product_name_exists()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L300"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_plan_plate_match", "label": "_plan_plate_match()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L314"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L324"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "planlistresponse", "label": "PlanListResponse", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_load_transports_by_id", "label": "_load_transports_by_id()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L577"}, {"id": "pltransport", "label": "PlTransport", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_planexportbody", "label": "PlanExportBody", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L592"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_export_plans", "label": "export_plans()", "file_type": "code", "source_file": "aLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_py", "label": "pl_plan_contract_gap.py", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_stop_contract_gap", "label": "stop_contract_gap()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L45"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_side_is_exempt", "label": "side_is_exempt()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L50"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_compute_contract_gap_sides", "label": "compute_contract_gap_sides()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L54"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_compute_plan_gap_sides_from_stops", "label": "compute_plan_gap_sides_from_stops()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L69"}, {"id": "planloadread", "label": "PlanLoadRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "plandeliveryread", "label": "PlanDeliveryRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_stop_contract_exists", "label": "_stop_contract_exists()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L103"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_load_stop_gap_expr", "label": "_load_stop_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L120"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_delivery_stop_gap_expr", "label": "_delivery_stop_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L143"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_legacy_supplier_gap_expr", "label": "_legacy_supplier_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L166"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_legacy_demander_gap_expr", "label": "_legacy_demander_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L192"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_plan_has_contract_gap_exists", "label": "plan_has_contract_gap_exists()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L218"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_supplier_gap_expr", "label": "_supplier_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L228"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_demander_gap_expr", "label": "_demander_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L232"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_build_plan_contract_gap_read", "label": "build_plan_contract_gap_read()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L236"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "plancontractlink", "label": "PlanContractLink", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "planproductread", "label": "PlanProductRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "plancontractgapread", "label": "PlanContractGapRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_load_products_by_plan_ids", "label": "_load_productsLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_resolve_list_product_name", "label": "_resolve_list_product_name()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L150"}, {"id": "plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_resolve_create_date", "label": "_resolve_create_date()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L166"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L170"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_format_subpack_slip_no", "label": "_format_subpack_slip_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L202"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_normalize_subpack_slip_no", "label": "_normalize_subpack_slip_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L206"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_subpack_slip_no_seq", "label": "_subpack_slip_no_seq()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L231"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_ensure_subpack_slip_no", "label": "_ensure_subpack_slip_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L239"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_determine_plan_type", "label": "_determine_plan_type()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L271"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_get_plan_type_label", "label": "_get_plan_type_label()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L276"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_generate_notify_message", "label": "_generate_notify_message()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L285"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_plan_product_name_exists", "label": "_plan_product_name_exists()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L305"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_plan_plate_match", "label": "_plan_plate_match()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L319"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L329"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "planlistresponse", "label": "PlanListResponse", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_load_transports_by_id", "label": "_load_transports_by_id()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L582"}, {"id": "pltransport", "label": "PlTransport", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_planexportbody", "label": "PlanExportBody", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L597"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_export_plans", "label": "export_plans()", "file_type": "code", "source_file": "aLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_py", "label": "pl_plan_archive_stop_service.py", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_require_qty", "label": "_require_qty()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L28"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_apply_stop_product_archive", "label": "_apply_stop_product_archive()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L37"}, {"id": "plplanloadproduct", "label": "PlPlanLoadProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "plplandeliveryproduct", "label": "PlPlanDeliveryProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "planarchivestopproductitem", "label": "PlanArchiveStopProductItem", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_brand_rows_from_item", "label": "_brand_rows_from_item()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L64"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_persist_stop_brand_qtys", "label": "_persist_stop_brand_qtys()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L80"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_apply_archive_stop_quantities", "label": "apply_archive_stop_quantities()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L114"}, {"id": "planarchiveloadblock", "label": "PlanArchiveLoadBlock", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "planarchivedeliveryblock", "label": "PlanArchiveDeliveryBlock", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_sync_plan_products_from_stop_archive", "label": "sync_plan_products_from_stop_archive()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L216"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_sum_delivery_outbound_qty", "label": "sum_delivery_outbound_qty()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L295"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_iter_stop_archive_inventory_lines", "label": "iter_stop_archive_inventory_lines()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L311"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_rationale_223", "label": "\u5c06\u6bb5\u7ea7\u5f52\u6863\u6570\u91cf\u6c47\u603b\u56de\u5199 pl_plan_product\uff08_row_to_dict \u6743\u5a01\u6765\u6e90\uff09\u3002", "file_type": "rationale", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L223"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_py", "target": "collections", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductbase", "label": "PlanProductBase", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L16"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductcreate", "label": "PlanProductCreate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L55"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductupdate", "label": "PlanProductUpdate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L59"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planarchivebrandqtyitem", "label": "PlanArchiveBrandQtyItem", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L81"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductread", "label": "PlanProductRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L89"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planstopproductbody", "label": "PlanStopProductBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L106"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planstopproductread", "label": "PlanStopProductRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L124"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planloadbody", "label": "PlanLoadBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L143"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plandeliverybody", "label": "PlanDeliveryBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L162"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planloadread", "label": "PlanLoadRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L182"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plandeliveryread", "label": "PlanDeliveryRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L193"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planbase", "label": "PlanBase", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L209"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plancontractlink", "label": "PlanContractLink", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L260"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plancreate", "label": "PlanCreate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L267"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plantransportcreateblock", "label": "PlanTransportCreateBlock", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L284"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planupdate", "label": "PlanUpdate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L298"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_hazardcertreadybody", "label": "HazardCertReadyBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L350"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_erpenteredbody", "label": "ErpEnteredBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L355"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plandocumentattachmentitem", "label": "PlanDocumentAttachmentItem", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L360"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planread", "label": "PlanRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L366"}, {"id": "planresourcesnapshotmixin", "label": "PlanResourceSnapshotMixin", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planwithproducts", "label": "PlanWithProducts", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L401"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_deliveryprintcustomeroption", "label": "DeliLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_py", "label": "pl_brand_archive.py", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_is_plan_level_row", "label": "_is_plan_level_row()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L12"}, {"id": "plplanproductbrandqty", "label": "PlPlanProductBrandQty", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_brand_archive.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_load_brand_qtys_by_plan_product_ids", "label": "load_brand_qtys_by_plan_product_ids()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L16"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_brand_archive.py"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_brand_archive.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_load_brand_qtys_by_stop_product_ids", "label": "load_brand_qtys_by_stop_product_ids()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L55"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_validate_brand_qty_rows", "label": "validate_brand_qty_rows()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L93"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_brand_archive.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_persist_plan_product_brand_qtys", "label": "persist_plan_product_brand_qtys()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L111"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_persist_stop_product_brand_qtys", "label": "persist_stop_product_brand_qtys()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L166"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_rationale_22", "label": "\u6309\u8ba1\u5212\u4ea7\u54c1\u660e\u7ec6 ID \u6279\u91cf\u52a0\u8f7d\u8ba1\u5212\u7ea7\u5f52\u6863\u54c1\u724c\u6570\u91cf\uff08brandId/brandName/qty\uff09\u3002", "file_type": "rationale", "source_file": "app/services/pl_brand_archive.py", "source_location": "L22"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_rationale_61", "label": "\u6309\u88c5\u8d27/\u9001\u8d27\u6bb5\u4ea7\u54c1\u884c ID \u6279\u91cf\u52a0\u8f7d\u6bb5\u7ea7\u5f52\u6863\u54c1\u724c\u6570\u91cf\u3002", "file_type": "rationale", "source_file": "app/services/pl_brand_archive.py", "source_location": "L61"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_rationale_122", "label": "\u8f6f\u5220\u8ba1\u5212\u7ea7\u65e7\u54c1\u724c\u91cf\u884c\u540e\u6309\u5f52\u6863\u7ed3\u679c\u63d2\u5165\uff1b\u65e0\u54c1\u724c\u7ea6\u5b9a\u5219\u53ea\u8f6f\u5220\u4e0d\u63d2\u5165\u3002\u4e0d\u89e6\u78b0\u6bb5\u7ea7\u884c\u3002", "file_type": "rationale", "source_file": "app/services/pl_brand_archive.py", "source_location": "L122"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_rationale_179", "label": "\u8f6f\u5220\u8be5\u6bb5\u4ea7\u54c1\u884c\u65e7\u54c1\u724c\u91cf\u540e\u5199\u5165\uff1b\u65e0\u6709\u6548\u6570\u91cf\u5219\u53ea\u8f6f\u5220\u3002", "file_type": "rationale", "source_file": "app/services/pl_brand_archive.py", "source_location": "L179"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_py", "target": "decimal", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/services/pl_brand_archive.py", "source_location": "L2", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_py", "target": "typing", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/services/pl_brand_archive.py", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_py", "target": "sqlalchemy", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/services/pl_brand_archive.py", "source_location": "L5", "Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_resolve_list_product_name", "label": "_resolve_list_product_name()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L144"}, {"id": "plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_resolve_create_date", "label": "_resolve_create_date()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L160"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_generate_plan_no", "label": "_generate_plan_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L164"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_format_subpack_slip_no", "label": "_format_subpack_slip_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L196"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_normalize_subpack_slip_no", "label": "_normalize_subpack_slip_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L200"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_subpack_slip_no_seq", "label": "_subpack_slip_no_seq()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L225"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_ensure_subpack_slip_no", "label": "_ensure_subpack_slip_no()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L233"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_determine_plan_type", "label": "_determine_plan_type()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L265"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_get_plan_type_label", "label": "_get_plan_type_label()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L270"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_generate_notify_message", "label": "_generate_notify_message()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L279"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_plan_product_name_exists", "label": "_plan_product_name_exists()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L299"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_plan_plate_match", "label": "_plan_plate_match()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L313"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_list_plans", "label": "list_plans()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L323"}, {"id": "dbusercontext", "label": "DbUserContext", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "planlistresponse", "label": "PlanListResponse", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_format_plan_delivery_date_for_export", "label": "_format_plan_delivery_date_for_export()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L574"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_load_transports_by_id", "label": "_load_transports_by_id()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_location": "L580"}, {"id": "pltransport", "label": "PlTransport", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/api/pl/plan.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_api_pl_plan_load_products_by_plan_id", "label": "_load_products_by_plan_id()", "file_type": "code", "source_file": "app/api/pl/plan.py", "source_locatiLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductbase", "label": "PlanProductBase", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L16"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductcreate", "label": "PlanProductCreate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L55"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductupdate", "label": "PlanProductUpdate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L59"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planarchivebrandqtyitem", "label": "PlanArchiveBrandQtyItem", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L81"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductread", "label": "PlanProductRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L89"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planstopproductbody", "label": "PlanStopProductBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L106"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planstopproductread", "label": "PlanStopProductRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L124"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planloadbody", "label": "PlanLoadBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L143"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plandeliverybody", "label": "PlanDeliveryBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L162"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planloadread", "label": "PlanLoadRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L182"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plandeliveryread", "label": "PlanDeliveryRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L193"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planbase", "label": "PlanBase", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L209"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plancontractlink", "label": "PlanContractLink", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L260"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plancreate", "label": "PlanCreate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L267"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plantransportcreateblock", "label": "PlanTransportCreateBlock", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L284"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planupdate", "label": "PlanUpdate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L298"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_hazardcertreadybody", "label": "HazardCertReadyBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L350"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_erpenteredbody", "label": "ErpEnteredBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L355"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plandocumentattachmentitem", "label": "PlanDocumentAttachmentItem", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L360"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planread", "label": "PlanRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L366"}, {"id": "planresourcesnapshotmixin", "label": "PlanResourceSnapshotMixin", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planwithproducts", "label": "PlanWithProducts", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L401"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_deliveryprintcustomeroption", "label": "DeliLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_py", "label": "pl_models.py", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plcarrier", "label": "PlCarrier", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L28"}, {"id": "base", "label": "Base", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plvehicle", "label": "PlVehicle", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L57"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plpersonnel", "label": "PlPersonnel", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L84"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pltransportaddress", "label": "PlTransportAddress", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L119"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plcustomerstoppref", "label": "PlCustomerStopPref", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L143"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plcert", "label": "PlCert", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L168"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pldriver", "label": "PlDriver", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L194"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plvehiclepersonnel", "label": "PlVehiclePersonnel", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L220"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plvehicledriver", "label": "PlVehicleDriver", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L241"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplan", "label": "PlPlan", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L266"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplancontract", "label": "PlPlanContract", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L375"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L398"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplanproductbrandqty", "label": "PlPlanProductBrandQty", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L457"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplanchange", "label": "PlPlanChange", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L485"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplanload", "label": "PlPlanLoad", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L507"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplanloadproduct", "label": "PlPlanLoadProduct", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L535"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplandelivery", "label": "PlPlanDelivery", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L566"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplandeliveryproduct", "label": "PlPlanDeliveryProduct", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L595"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pltransport", "label": "PlTransport", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L631"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pltransportproductfeedback", "label": "PlTransportProductFeedback", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L694"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pltransportproductbrandqty", "label": "PlTransportProductBrandQty", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L715"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pltransportdblink", "label": "PlTransportDbLink", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L738"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_appLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_py", "label": "pl_plan_service.py", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_determine_plan_type", "label": "determine_plan_type()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L48"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_is_ton_product_unit", "label": "is_ton_product_unit()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L56"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_product_quantity", "label": "_product_quantity()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L63"}, {"id": "planproductcreate", "label": "PlanProductCreate", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_product_create_actual_qty", "label": "_product_create_actual_qty()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L73"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_plan_product_actual_qty", "label": "_plan_product_actual_qty()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L79"}, {"id": "plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_sum_product_actual_quantities", "label": "sum_product_actual_quantities()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L85"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_sum_plan_product_actual_quantities", "label": "sum_plan_product_actual_quantities()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L89"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_estimate_qty_fields", "label": "_estimate_qty_fields()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L93"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_build_plan_product", "label": "build_plan_product()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L104"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_apply_vehicle_snapshot_to_plan_transport", "label": "_apply_vehicle_snapshot_to_plan_transport()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L154"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py"}, {"id": "pltransport", "label": "PlTransport", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py"}, {"id": "plvehicle", "label": "PlVehicle", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_resolve_driver", "label": "_resolve_driver()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L172"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_calc_freight_amount", "label": "_calc_freight_amount()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L218"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_fetch_hazard_flags_map", "label": "fetch_hazard_flags_map()", "file_type": "code", "source_file": "app/services/pl_plan_service.py", "source_location": "L239"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_service_resolve_hazard_flags_from_crm", "label": "resolve_hazard_flagLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_py", "label": "test_pl_plan_archive_export.py", "file_type": "code", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_test_resolve_archive_list_io_column", "label": "test_resolve_archive_list_io_column()", "file_type": "code", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L12"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_test_append_stop_product_rows_io_and_amount", "label": "test_append_stop_product_rows_io_and_amount()", "file_type": "code", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L20"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_test_append_stop_product_rows_delivery_external_outbound", "label": "test_append_stop_product_rows_delivery_external_outbound()", "file_type": "code", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L68"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_fakeplan", "label": "_FakePlan", "file_type": "code", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L103"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_test_plan_common_export_fields", "label": "test_plan_common_export_fields()", "file_type": "code", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L116"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_py", "target": "decimal", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_py", "target": "app_schemas_pl_plan", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_py", "target": "app_services_pl_internal_customer", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L4", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_py", "target": "app_services_pl_plan_archive_export", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L5", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_test_resolve_archive_list_io_column", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L12", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_test_append_stop_product_rows_io_and_amount", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L20", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_test_append_stop_product_rows_delivery_external_outbound", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L68", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_fakeplan", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L103", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_test_plan_common_export_fields", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_archive_export.py", "source_location": "L116", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_test_plan_common_export_fields", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_archive_export_fakeplan", "relation": "calls", "conteLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_py", "label": "pl_brand_archive.py", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_is_plan_level_row", "label": "_is_plan_level_row()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L12"}, {"id": "plplanproductbrandqty", "label": "PlPlanProductBrandQty", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_brand_archive.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_row_to_brand_dict", "label": "_row_to_brand_dict()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L16"}, {"id": "any", "label": "Any", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_brand_archive.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_parse_unit_price", "label": "_parse_unit_price()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L27"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_brand_archive.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_load_brand_qtys_by_plan_product_ids", "label": "load_brand_qtys_by_plan_product_ids()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L38"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_brand_archive.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_load_brand_qtys_by_stop_product_ids", "label": "load_brand_qtys_by_stop_product_ids()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L71"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_validate_brand_qty_rows", "label": "validate_brand_qty_rows()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L103"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_persist_plan_product_brand_qtys", "label": "persist_plan_product_brand_qtys()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L121"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_persist_stop_product_brand_qtys", "label": "persist_stop_product_brand_qtys()", "file_type": "code", "source_file": "app/services/pl_brand_archive.py", "source_location": "L177"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_rationale_44", "label": "\u6309\u8ba1\u5212\u4ea7\u54c1\u660e\u7ec6 ID \u6279\u91cf\u52a0\u8f7d\u8ba1\u5212\u7ea7\u5f52\u6863\u54c1\u724c\u6570\u91cf\uff08brandId/brandName/qty/unitPrice\uff09\u3002", "file_type": "rationale", "source_file": "app/services/pl_brand_archive.py", "source_location": "L44"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_rationale_77", "label": "\u6309\u88c5\u8d27/\u9001\u8d27\u6bb5\u4ea7\u54c1\u884c ID \u6279\u91cf\u52a0\u8f7d\u6bb5\u7ea7\u5f52\u6863\u54c1\u724c\u6570\u91cf\u3002", "file_type": "rationale", "source_file": "app/services/pl_brand_archive.py", "source_location": "L77"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_rationale_132", "label": "\u8f6f\u5220\u8ba1\u5212\u7ea7\u65e7\u54c1\u724c\u91cf\u884c\u540e\u6309\u5f52\u6863\u7ed3\u679c\u63d2\u5165\uff1b\u65e0\u54c1\u724c\u7ea6\u5b9a\u5219\u53ea\u8f6f\u5220\u4e0d\u63d2\u5165\u3002\u4e0d\u89e6\u78b0\u6bb5\u7ea7\u884c\u3002", "file_type": "rationale", "source_file": "app/services/pl_brand_archive.py", "source_location": "L132"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_rationale_190", "label": "\u8f6f\u5220\u8be5\u6bb5\u4ea7\u54c1\u884c\u65e7\u54c1\u724c\u91cf\u540e\u5199\u5165\uff1b\u65e0\u6709\u6548\u6570\u91cf\u5219\u53ea\u8f6f\u5220\u3002", "file_type": "rationale", "source_file": "app/services/pl_brand_archive.py", "source_location": "L190"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_brand_archive_py", "target": "decimal", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/services/pl_brand_archive.py", "source_location": "L2", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_servicesLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_py", "label": "pl_models.py", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plcarrier", "label": "PlCarrier", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L28"}, {"id": "base", "label": "Base", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plvehicle", "label": "PlVehicle", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L57"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plpersonnel", "label": "PlPersonnel", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L84"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pltransportaddress", "label": "PlTransportAddress", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L119"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plcustomerstoppref", "label": "PlCustomerStopPref", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L143"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plcert", "label": "PlCert", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L168"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pldriver", "label": "PlDriver", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L194"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plvehiclepersonnel", "label": "PlVehiclePersonnel", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L220"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plvehicledriver", "label": "PlVehicleDriver", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L241"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplan", "label": "PlPlan", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L266"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplancontract", "label": "PlPlanContract", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L375"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L398"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplanproductbrandqty", "label": "PlPlanProductBrandQty", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L457"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplanchange", "label": "PlPlanChange", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L484"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplanload", "label": "PlPlanLoad", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L506"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplanloadproduct", "label": "PlPlanLoadProduct", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L534"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplandelivery", "label": "PlPlanDelivery", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L565"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_plplandeliveryproduct", "label": "PlPlanDeliveryProduct", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L594"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pltransport", "label": "PlTransport", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L630"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pltransportproductfeedback", "label": "PlTransportProductFeedback", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L693"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pltransportproductbrandqty", "label": "PlTransportProductBrandQty", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L714"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_models_pl_models_pltransportdblink", "label": "PlTransportDbLink", "file_type": "code", "source_file": "app/models/pl_models.py", "source_location": "L737"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_appLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_py", "label": "pl_plan_contract_gap.py", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_stop_contract_gap", "label": "stop_contract_gap()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L46"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_side_is_exempt", "label": "side_is_exempt()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L51"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_compute_contract_gap_sides", "label": "compute_contract_gap_sides()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L55"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_compute_plan_gap_sides_from_stops", "label": "compute_plan_gap_sides_from_stops()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L70"}, {"id": "planloadread", "label": "PlanLoadRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "plandeliveryread", "label": "PlanDeliveryRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_stop_contract_exists", "label": "_stop_contract_exists()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L104"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_load_stop_gap_expr", "label": "_load_stop_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L121"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_delivery_stop_gap_expr", "label": "_delivery_stop_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L144"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_legacy_supplier_gap_expr", "label": "_legacy_supplier_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L167"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_legacy_demander_gap_expr", "label": "_legacy_demander_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L193"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_plan_has_contract_gap_exists", "label": "plan_has_contract_gap_exists()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L219"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_supplier_gap_expr", "label": "_supplier_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L229"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_demander_gap_expr", "label": "_demander_gap_expr()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L233"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_build_plan_contract_gap_read", "label": "build_plan_contract_gap_read()", "file_type": "code", "source_file": "app/services/pl_plan_contract_gap.py", "source_location": "L237"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "plancontractlink", "label": "PlanContractLink", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "planproductread", "label": "PlanProductRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "plancontractgapread", "label": "PlanContractGapRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_contract_gap.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_contract_gap_load_products_by_plan_ids", "label": "_load_productsLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_py", "label": "plan.py", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductbase", "label": "PlanProductBase", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L16"}, {"id": "basemodel", "label": "BaseModel", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductcreate", "label": "PlanProductCreate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L55"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductupdate", "label": "PlanProductUpdate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L59"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planarchivebrandqtyitem", "label": "PlanArchiveBrandQtyItem", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L81"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planproductread", "label": "PlanProductRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L88"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planstopproductbody", "label": "PlanStopProductBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L105"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planstopproductread", "label": "PlanStopProductRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L123"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planloadbody", "label": "PlanLoadBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L142"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plandeliverybody", "label": "PlanDeliveryBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L161"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planloadread", "label": "PlanLoadRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L181"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plandeliveryread", "label": "PlanDeliveryRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L192"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planbase", "label": "PlanBase", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L208"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plancontractlink", "label": "PlanContractLink", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L259"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plancreate", "label": "PlanCreate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L266"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plantransportcreateblock", "label": "PlanTransportCreateBlock", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L283"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planupdate", "label": "PlanUpdate", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L297"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_hazardcertreadybody", "label": "HazardCertReadyBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L349"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_erpenteredbody", "label": "ErpEnteredBody", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L354"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_plandocumentattachmentitem", "label": "PlanDocumentAttachmentItem", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L359"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planread", "label": "PlanRead", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L365"}, {"id": "planresourcesnapshotmixin", "label": "PlanResourceSnapshotMixin", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_planwithproducts", "label": "PlanWithProducts", "file_type": "code", "source_file": "app/schemas/pl/plan.py", "source_location": "L400"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_schemas_pl_plan_deliveryprintcustomeroption", "label": "DeliLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_py", "label": "pl_plan_stop_service.py", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_contract_link", "label": "_contract_link()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L46"}, {"id": "plplancontract", "label": "PlPlanContract", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "plancontractlink", "label": "PlanContractLink", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_attach_stop_brand_qtys", "label": "_attach_stop_brand_qtys()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L54"}, {"id": "planstopproductread", "label": "PlanStopProductRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_resolve_stop_list_product_name", "label": "_resolve_stop_list_product_name()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L66"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_apply_stop_list_product_name", "label": "_apply_stop_list_product_name()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L83"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_load_stop_product", "label": "_load_stop_product()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L98"}, {"id": "plplanloadproduct", "label": "PlPlanLoadProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_delivery_stop_product", "label": "_delivery_stop_product()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L117"}, {"id": "plplandeliveryproduct", "label": "PlPlanDeliveryProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_load_stop_contract_rows_grouped", "label": "load_stop_contract_rows_grouped()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L136"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_contracts_for_stop", "label": "_contracts_for_stop()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L159"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_to_decimal", "label": "_to_decimal()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L174"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_resolve_stop_address_from_library", "label": "resolve_stop_address_from_library()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L183"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_enrich_stop_addresses_from_library", "label": "enrich_stop_addresses_from_library()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L224"}, {"id": "planloadbody", "label": "PlanLoadBody", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "plandeliverybody", "label": "PlanDeliveryBody", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/apLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_py", "label": "pl_plan_stop_service.py", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_contract_link", "label": "_contract_link()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L45"}, {"id": "plplancontract", "label": "PlPlanContract", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "plancontractlink", "label": "PlanContractLink", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_attach_stop_brand_qtys", "label": "_attach_stop_brand_qtys()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L53"}, {"id": "planstopproductread", "label": "PlanStopProductRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_load_stop_product", "label": "_load_stop_product()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L65"}, {"id": "plplanloadproduct", "label": "PlPlanLoadProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_delivery_stop_product", "label": "_delivery_stop_product()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L75"}, {"id": "plplandeliveryproduct", "label": "PlPlanDeliveryProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_load_stop_contract_rows_grouped", "label": "load_stop_contract_rows_grouped()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L85"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_contracts_for_stop", "label": "_contracts_for_stop()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L108"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_to_decimal", "label": "_to_decimal()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L123"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_resolve_stop_address_from_library", "label": "resolve_stop_address_from_library()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L132"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_enrich_stop_addresses_from_library", "label": "enrich_stop_addresses_from_library()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L173"}, {"id": "planloadbody", "label": "PlanLoadBody", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "plandeliverybody", "label": "PlanDeliveryBody", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_stop_service_load_plan_stops_grouped", "label": "load_plan_stops_grouped()", "file_type": "code", "source_file": "app/services/pl_plan_stop_service.py", "source_location": "L192"}, {"id": "planloadread", "label": "PlanLoadRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_stop_service.py"}, {"id": "plandeliveryread", "laLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_py", "label": "pl_plan_archive_export.py", "file_type": "code", "source_file": "app/services/pl_plan_archive_export.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_resolve_archive_list_io_column", "label": "resolve_archive_list_io_column()", "file_type": "code", "source_file": "app/services/pl_plan_archive_export.py", "source_location": "L44"}, {"id": "stopkind", "label": "StopKind", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_format_date_value", "label": "_format_date_value()", "file_type": "code", "source_file": "app/services/pl_plan_archive_export.py", "source_location": "L52"}, {"id": "date", "label": "date", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py"}, {"id": "datetime", "label": "datetime", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_format_plate_display", "label": "_format_plate_display()", "file_type": "code", "source_file": "app/services/pl_plan_archive_export.py", "source_location": "L58"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_get_stop_archive_actual_qty", "label": "_get_stop_archive_actual_qty()", "file_type": "code", "source_file": "app/services/pl_plan_archive_export.py", "source_location": "L68"}, {"id": "planstopproductread", "label": "PlanStopProductRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_get_stop_archive_unit_price", "label": "_get_stop_archive_unit_price()", "file_type": "code", "source_file": "app/services/pl_plan_archive_export.py", "source_location": "L72"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_get_stop_archive_amount", "label": "_get_stop_archive_amount()", "file_type": "code", "source_file": "app/services/pl_plan_archive_export.py", "source_location": "L76"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_stop_kind_label", "label": "_stop_kind_label()", "file_type": "code", "source_file": "app/services/pl_plan_archive_export.py", "source_location": "L87"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_stop_time", "label": "_stop_time()", "file_type": "code", "source_file": "app/services/pl_plan_archive_export.py", "source_location": "L93"}, {"id": "planloadread", "label": "PlanLoadRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py"}, {"id": "plandeliveryread", "label": "PlanDeliveryRead", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_load_legacy_products_by_plan", "label": "_load_legacy_products_by_plan()", "file_type": "code", "source_file": "app/services/pl_plan_archive_export.py", "source_location": "L99"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py"}, {"id": "plplanproduct", "label": "PlPlanProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_export_plan_common_export_fields", "label": "_plan_common_export_fields()", "file_type": "code", "source_file": "app/services/pl_plan_archive_export.py", "source_location": "L120"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_export.py"}, {"id": "pltransport", "label": "PlTransport", "fiLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_py", "label": "pl_plan_archive_stop_service.py", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_require_qty", "label": "_require_qty()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L28"}, {"id": "decimal", "label": "Decimal", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_apply_stop_product_archive", "label": "_apply_stop_product_archive()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L37"}, {"id": "plplanloadproduct", "label": "PlPlanLoadProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "plplandeliveryproduct", "label": "PlPlanDeliveryProduct", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "planarchivestopproductitem", "label": "PlanArchiveStopProductItem", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_brand_rows_from_item", "label": "_brand_rows_from_item()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L64"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_persist_stop_brand_qtys", "label": "_persist_stop_brand_qtys()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L77"}, {"id": "session", "label": "Session", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "plplan", "label": "PlPlan", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_apply_archive_stop_quantities", "label": "apply_archive_stop_quantities()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L111"}, {"id": "planarchiveloadblock", "label": "PlanArchiveLoadBlock", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "planarchivedeliveryblock", "label": "PlanArchiveDeliveryBlock", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/wuzilong/work/00code/shanxia2.0/backend/app/services/pl_plan_archive_stop_service.py"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_sync_plan_products_from_stop_archive", "label": "sync_plan_products_from_stop_archive()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L213"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_sum_delivery_outbound_qty", "label": "sum_delivery_outbound_qty()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L292"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_iter_stop_archive_inventory_lines", "label": "iter_stop_archive_inventory_lines()", "file_type": "code", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L308"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_rationale_220", "label": "\u5c06\u6bb5\u7ea7\u5f52\u6863\u6570\u91cf\u6c47\u603b\u56de\u5199 pl_plan_product\uff08_row_to_dict \u6743\u5a01\u6765\u6e90\uff09\u3002", "file_type": "rationale", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "L220"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_app_services_pl_plan_archive_stop_service_py", "target": "collections", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "app/services/pl_plan_archive_stop_service.py", "source_location": "Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_py", "label": "test_pl_plan_contract_gap.py", "file_type": "code", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L1"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_test_both_external_no_contracts_gaps_both", "label": "test_both_external_no_contracts_gaps_both()", "file_type": "code", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L15"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_test_internal_supplier_external_demander_missing_only_demander", "label": "test_internal_supplier_external_demander_missing_only_demander()", "file_type": "code", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L25"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_test_both_internal_no_gap", "label": "test_both_internal_no_gap()", "file_type": "code", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L35"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_test_external_with_contracts_no_gap", "label": "test_external_with_contracts_no_gap()", "file_type": "code", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L45"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_test_empty_names_treated_as_non_internal_gap_both", "label": "test_empty_names_treated_as_non_internal_gap_both()", "file_type": "code", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L55"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_test_stop_contract_gap_internal_exempt", "label": "test_stop_contract_gap_internal_exempt()", "file_type": "code", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L65"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_test_compute_plan_gap_sides_from_stops_multi_delivery", "label": "test_compute_plan_gap_sides_from_stops_multi_delivery()", "file_type": "code", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L71"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_test_compute_plan_gap_sides_from_stops_internal_load_no_gap", "label": "test_compute_plan_gap_sides_from_stops_internal_load_no_gap()", "file_type": "code", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L108"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_compile", "label": "_compile()", "file_type": "code", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L133"}, {"id": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_test_plan_has_contract_gap_exists_mentions_contract_and_internal_names", "label": "test_plan_has_contract_gap_exists_mentions_contract_and_internal_names()", "file_type": "code", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L139"}], "edges": [{"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_py", "target": "sqlalchemy_dialects", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_py", "target": "app_schemas_pl_plan", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_py", "target": "app_services_pl_internal_customer", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L4", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_py", "target": "app_services_pl_plan_contract_gap", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L5", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_test_both_external_no_contracts_gaps_both", "relation": "contains", "confidence": "EXTRACTED", "source_file": "tests/test_pl_plan_contract_gap.py", "source_location": "L15", "weight": 1.0}, {"source": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_py", "target": "users_wuzilong_work_00code_shanxia2_0_backend_tests_test_pl_plan_contract_gap_test_internal_supplier_external_demLine truncated
@@ -0,0 +1,123 @@
from decimal import Decimal
from app.schemas.pl.plan import PlanDeliveryRead, PlanLoadRead, PlanStopProductRead
from app.services.pl_internal_customer import INTERNAL_CUSTOMER_NAMES
from app.services.pl_plan_archive_export import (
_append_stop_product_rows,
_plan_common_export_fields,
resolve_archive_list_io_column,
)
def test_resolve_archive_list_io_column():
internal = next(iter(INTERNAL_CUSTOMER_NAMES))
assert resolve_archive_list_io_column("load", internal) == "outbound"
assert resolve_archive_list_io_column("load", "外部客户") == "inbound"
assert resolve_archive_list_io_column("delivery", internal) == "inbound"
assert resolve_archive_list_io_column("delivery", "外部客户") == "outbound"
def test_append_stop_product_rows_io_and_amount():
internal = next(iter(INTERNAL_CUSTOMER_NAMES))
product = PlanStopProductRead(
id="sp1",
planId="p1",
tenantNo="t1",
productId="prod1",
productNo="P001",
productName="甲醇",
model="99%",
unit="吨",
packageSpec="散装",
planQty=Decimal("10"),
inboundQty=Decimal("9.5"),
inboundPrice=Decimal("100"),
inboundAmount=None,
createTime="2026-08-27T00:00:00",
updateTime="2026-08-27T00:00:00",
)
load = PlanLoadRead(
id="load1",
planId="p1",
tenantNo="t1",
customerName=internal,
address="装货地址",
loadTime="2026-08-27T08:00:00",
products=[product],
createTime="2026-08-27T00:00:00",
updateTime="2026-08-27T00:00:00",
)
rows: list[dict] = []
plan_common = {
"planNo": "PL20260827001",
"planDate": "2026-08-27",
}
row_no = _append_stop_product_rows(rows, 0, plan_common, "load", load, None, load.products)
assert row_no == 1
assert len(rows) == 1
row = rows[0]
assert row["stopKind"] == "装货"
assert row["stopCustomer"] == internal
assert row["actualQty"] == Decimal("9.5")
assert row["outboundQty"] == Decimal("9.5")
assert row["inboundQty"] is None
assert row["unitPrice"] == Decimal("100")
assert row["amount"] == Decimal("950")
def test_append_stop_product_rows_delivery_external_outbound():
product = PlanStopProductRead(
id="sp2",
planId="p1",
tenantNo="t1",
productId="prod1",
productNo="P001",
productName="甲醇",
outboundQty=Decimal("8"),
outboundPrice=Decimal("120"),
outboundAmount=Decimal("960"),
createTime="2026-08-27T00:00:00",
updateTime="2026-08-27T00:00:00",
)
delivery = PlanDeliveryRead(
id="del1",
planId="p1",
tenantNo="t1",
seq=1,
customerName="外部客户",
address="送货地址",
deliveryTime="2026-08-28T10:00:00",
products=[product],
createTime="2026-08-27T00:00:00",
updateTime="2026-08-27T00:00:00",
)
rows: list[dict] = []
row_no = _append_stop_product_rows(rows, 0, {"planNo": "P1"}, "delivery", delivery, 1, delivery.products)
row = rows[0]
assert row["stopKind"] == "送货 #1"
assert row["outboundQty"] == Decimal("8")
assert row["inboundQty"] is None
assert row["amount"] == Decimal("960")
class _FakePlan:
plan_no = "PL001"
plan_date = "2026-08-27"
archive_status = "archived"
follow_status = "arranged"
plan_type = "direct"
license_plate = "皖A12345"
trailer_no = "挂001"
erp_entered = 1
freight_amount = Decimal("500")
remark = "测试"
def test_plan_common_export_fields():
fields = _plan_common_export_fields(_FakePlan(), None)
assert fields["planNo"] == "PL001"
assert fields["archiveStatus"] == "已归档"
assert fields["followStatus"] == "已安排"
assert fields["planType"] == "直销单"
assert fields["licensePlate"] == "皖A12345挂001"
assert fields["erpEntered"] == "是"
@@ -0,0 +1,858 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fresh Mint · 运输计划合同完善 · 补充弹窗 Demo</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root {
--primary: #10b981;
--primary-dark: #059669;
--primary-bg: #ecfdf5;
--text: #1e293b;
--text-muted: #64748b;
--border: #d9e8e2;
--surface: #ffffff;
--table-head: #f8fafc;
--bg-page: #f8fafc;
--row-alt: #fafafa;
--row-hover: #f1f5f9;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 12px 40px rgba(15, 23, 42, 0.12);
--radius: 10px;
--warn: #d97706;
--warn-bg: #fffbeb;
--danger: #dc2626;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text);
background: var(--bg-page);
}
.demo-page { max-width: 1360px; margin: 0 auto; padding: 28px 24px 80px; }
.demo-head { margin-bottom: 20px; }
.demo-head h1 { margin: 0 0 8px; font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
.demo-head h1 span { color: var(--primary); }
.demo-head p { margin: 0; color: var(--text-muted); max-width: 920px; }
.meta { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.pill {
display: inline-flex; align-items: center;
padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500;
background: var(--primary-bg); color: var(--primary-dark); border: 1px solid #a7f3d0;
}
.pill.muted { background: #f1f5f9; color: var(--text-muted); border-color: #e2e8f0; }
.pill.warn { background: var(--warn-bg); color: var(--warn); border-color: rgba(217,119,6,.25); }
.note {
margin: 0 0 16px; padding: 12px 14px; border-radius: 8px;
background: var(--primary-bg); border: 1px solid #6ee7b7; color: var(--primary-dark);
font-size: 13px;
}
.note strong { color: var(--text); }
.panel {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
}
.panel-top {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; padding: 16px 20px 12px; border-bottom: 1px solid #eef2f6;
}
.panel-top h2 {
margin: 0; font-size: 16px; font-weight: 600;
display: flex; align-items: center;
}
.panel-top h2::before {
content: ""; display: inline-block; width: 3px; height: 16px;
background: var(--primary); border-radius: 2px; margin-right: 10px;
}
.stat-chip {
display: inline-flex; align-items: center;
padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 600;
background: var(--warn-bg); color: var(--warn); border: 1px solid rgba(217,119,6,.2);
}
.filter-block { padding: 14px 20px 6px; }
.filter-grid {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px 16px;
}
@media (max-width: 1100px) { .filter-grid { grid-template-columns: repeat(2, 1fr); } }
.field label {
display: block; font-size: 11px; color: var(--text-muted); margin-bottom: 4px;
}
.field input, .field select {
width: 100%; height: 36px; border: 1px solid var(--border); border-radius: 8px;
padding: 0 10px; font-size: 14px; color: var(--text); background: #fff;
}
.filter-actions {
display: flex; justify-content: flex-end; gap: 8px; padding: 10px 0 4px;
}
.list-toolbar {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
padding: 10px 20px; border-top: 1px solid #eef2f6;
}
.date-pills { display: flex; gap: 6px; flex-wrap: wrap; }
.date-pill {
height: 30px; padding: 0 10px; border-radius: 999px; border: 1px solid var(--border);
background: #fff; font-size: 12px; color: var(--text-muted); cursor: pointer;
}
.date-pill.active {
background: var(--primary-bg); border-color: #6ee7b7; color: var(--primary-dark); font-weight: 600;
}
.toolbar-spacer { flex: 1; }
.quick-search {
width: 220px; height: 36px; border: 1px solid var(--border); border-radius: 8px;
padding: 0 12px; font-size: 13px;
}
.btn {
appearance: none; display: inline-flex; align-items: center; gap: 6px;
height: 36px; padding: 0 14px; border-radius: 8px; font-size: 13px; font-weight: 500;
cursor: pointer; border: 1px solid var(--border); background: #fff; color: var(--text);
}
.btn:hover { background: #f8fafc; }
.btn-main { background: var(--primary); border-color: var(--primary); color: #fff; }
.btn-main:hover { background: var(--primary-dark); }
.btn-ghost { background: #fff; }
.btn-link {
height: auto; padding: 0; border: none; background: transparent;
color: var(--primary-dark); font-weight: 500; cursor: pointer; font-size: 13px;
}
.btn-link:hover { text-decoration: underline; }
.btn-link.warn { color: var(--warn); }
.btn-sm { height: 30px; padding: 0 10px; font-size: 12px; }
.btn-icon {
width: 28px; height: 28px; padding: 0; justify-content: center;
border: none; background: transparent; color: var(--text-muted); cursor: pointer;
border-radius: 6px;
}
.btn-icon:hover { background: #f1f5f9; color: var(--primary-dark); }
.btn-icon.danger:hover { color: var(--danger); background: #fef2f2; }
.list-body { border-top: 1px solid #eef2f6; overflow-x: auto; }
table.list-table {
width: 100%; border-collapse: collapse; min-width: 1100px;
}
.list-table th {
background: var(--table-head); text-align: left; font-weight: 600; font-size: 12px;
color: var(--text-muted); padding: 10px 12px; border-bottom: 1px solid #eef2f6;
white-space: nowrap;
}
.list-table td {
padding: 12px; border-bottom: 1px solid #f1f5f9; vertical-align: middle;
font-size: 13px;
}
.list-table tbody tr:hover { background: var(--row-hover); }
.list-table .mono { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--primary-dark); }
.list-table .muted { color: var(--text-muted); }
.list-table .merge-cell { background: #fff; }
.addr-time { display: flex; flex-direction: column; gap: 2px; }
.addr-time .addr { color: var(--text); }
.addr-time .time { font-size: 12px; color: var(--text-muted); }
.tag {
display: inline-flex; align-items: center; padding: 2px 8px;
border-radius: 6px; font-size: 12px; font-weight: 500; white-space: nowrap;
}
.tag.success { background: #059669; color: #fff; }
.tag.warn { background: #d97706; color: #fff; }
.tag.danger { background: #dc2626; color: #fff; }
.tag.info { background: transparent; color: #1e293b; border: 1px solid #e2e8f0; }
.tag.muted { background: #f1f5f9; color: var(--text-muted); }
.tag.internal { background: #059669; color: #fff; margin-left: 6px; }
.gap-cell {
display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
}
.gap-line {
display: flex; align-items: center; gap: 6px; font-size: 12px;
}
.gap-line .side { color: var(--text-muted); min-width: 2.5em; }
.gap-line .pending { color: var(--warn); font-weight: 500; }
.gap-line .ok { color: var(--primary-dark); font-weight: 500; }
.pager {
display: flex; align-items: center; justify-content: flex-end; gap: 12px;
padding: 12px 20px; border-top: 1px solid #eef2f6; color: var(--text-muted); font-size: 13px;
}
/* 弹窗 */
.modal-mask {
display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.45);
z-index: 1000; align-items: center; justify-content: center; padding: 24px;
backdrop-filter: none;
}
.modal-mask.open { display: flex; }
.dialog {
width: min(760px, 100%); max-height: calc(100vh - 100px);
background: #fff; border-radius: 12px; box-shadow: var(--shadow-lg);
display: flex; flex-direction: column; overflow: hidden;
}
.dialog-head {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px; border-bottom: 1px solid #eef2f6; flex-shrink: 0;
}
.dialog-head h3 { margin: 0; font-size: 16px; font-weight: 600; }
.dialog-body { padding: 18px 20px; overflow: auto; flex: 1; }
.dialog-foot {
display: flex; justify-content: flex-end; gap: 8px;
padding: 12px 20px; border-top: 1px solid #eef2f6; flex-shrink: 0;
}
.stop-summary {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
margin-bottom: 16px; padding: 12px 14px; background: #f8fafc; border-radius: 8px;
border: 1px solid #eef2f6;
}
@media (max-width: 700px) { .stop-summary { grid-template-columns: 1fr 1fr; } }
.kv .k { font-size: 11px; color: var(--text-muted); }
.kv .v { font-size: 13px; font-weight: 500; margin-top: 2px; }
.side-box {
background: #fff; border: 1px solid var(--border); border-radius: 8px; padding: 14px;
}
.side-box.need { border-color: rgba(217,119,6,.35); background: #fffdf8; }
.side-box.ok { border-color: #a7f3d0; background: #f6fffb; }
.side-box.exempt { border-color: #e2e8f0; background: #f8fafc; }
.side-box__head {
display: flex; align-items: center; justify-content: space-between;
gap: 8px; margin-bottom: 6px;
}
.side-box__head h4 { margin: 0; font-size: 14px; font-weight: 600; }
.side-box__customer { font-size: 13px; margin-bottom: 12px; color: var(--text); }
.side-box__exempt {
margin: 0; padding: 14px; border: 1px solid #e2e8f0; border-radius: 8px;
background: #fff; color: var(--text-muted); font-size: 13px; text-align: center;
}
.contract-toolbar {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px;
}
.contract-hint { font-size: 12px; color: var(--text-muted); }
.contract-hint.is-warn { color: var(--warn); }
.contract-cards { display: flex; flex-direction: column; gap: 10px; }
.contract-card {
border: 1px solid var(--border); border-radius: 8px; background: #fff; overflow: hidden;
}
.contract-card__head {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
padding: 10px 12px; background: #f8fafc; border-bottom: 1px solid #eef2f6;
}
.contract-card__meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.contract-card__no { font-weight: 600; color: var(--primary-dark); font-size: 13px; }
.contract-card__count { font-size: 12px; color: var(--text-muted); }
.contract-card__actions { display: flex; align-items: center; gap: 2px; }
.contract-card__body { padding: 0; }
.mini-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.mini-table th {
background: #fafafa; color: var(--text-muted); font-weight: 500;
padding: 6px 10px; text-align: left; border-bottom: 1px solid #f1f5f9;
}
.mini-table td {
padding: 6px 10px; border-bottom: 1px solid #f8fafc; color: var(--text);
}
.empty-hint {
margin: 0; padding: 18px; text-align: center; color: var(--text-muted);
font-size: 13px; border: 1px dashed #cbd5e1; border-radius: 8px; background: #fff;
}
/* 合同选择器(二级弹层) */
.selector-mask {
display: none; position: fixed; inset: 0; background: rgba(15, 23, 42, 0.4);
z-index: 1100; align-items: center; justify-content: center; padding: 24px;
}
.selector-mask.open { display: flex; }
.selector-dialog {
width: min(640px, 100%); max-height: calc(100vh - 120px);
background: #fff; border-radius: 12px; box-shadow: var(--shadow-lg);
display: flex; flex-direction: column; overflow: hidden;
}
.selector-list { overflow: auto; flex: 1; border-top: 1px solid #eef2f6; }
.selector-item {
display: flex; align-items: flex-start; gap: 10px;
padding: 12px 16px; border-bottom: 1px solid #f1f5f9; cursor: pointer;
}
.selector-item:hover, .selector-item.selected { background: var(--primary-bg); }
.selector-item .meta-line { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.selector-item input { margin-top: 3px; }
.toast {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%) translateY(-120%);
background: #059669; color: #fff; padding: 10px 18px; border-radius: 8px;
font-size: 13px; font-weight: 500; box-shadow: var(--shadow-lg);
transition: transform .25s ease; z-index: 2000;
}
.toast.show { transform: translateX(-50%) translateY(0); }
.legend {
margin-top: 20px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
}
@media (max-width: 800px) { .legend { grid-template-columns: 1fr; } }
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 14px 16px; box-shadow: var(--shadow);
}
.card h3 {
margin: 0 0 8px; font-size: 14px; font-weight: 600;
padding-left: 10px; border-left: 3px solid var(--primary); color: var(--primary-dark);
}
.card ul { margin: 0; padding-left: 18px; color: var(--text-muted); font-size: 13px; }
.card li { margin: 4px 0; }
.card li strong { color: var(--text); }
</style>
</head>
<body>
<div class="demo-page">
<header class="demo-head">
<h1>运输计划合同完善 · <span>补充弹窗</span></h1>
<p>
对齐当前线上列表(段次行合并):点击合同缺口旁的「补充」打开弹窗,
仅针对<strong>本段客户</strong>关联已有 CRM 合同并保存。不再使用行内展开。
</p>
<div class="meta">
<span class="pill">Fresh Mint · web-admin</span>
<span class="pill warn">点击「补充」体验弹窗</span>
<span class="pill muted">路径 /#/pl/plan-module/plan-contract</span>
</div>
</header>
<p class="note">
<strong>交互变更:</strong>原「行内展开 + 保存本段合同」→ 改为「弹窗补充」。
弹窗内复用合同多选卡片(选择合同 / 查看 / 移除),底栏「取消 / 保存本段合同」。
未保存关闭时二次确认(Demo 用 confirm)。
</p>
<section class="panel">
<div class="panel-top">
<h2>运输计划合同完善</h2>
<span class="stat-chip">待完善 3 条</span>
</div>
<div class="filter-block">
<div class="filter-grid">
<div class="field">
<label>计划编号</label>
<input placeholder="搜索计划编号" />
</div>
<div class="field">
<label>缺口类型</label>
<select>
<option>全部缺口</option>
<option>装货段缺合同</option>
<option>送货段缺合同</option>
<option>装货与送货均缺</option>
</select>
</div>
<div class="field">
<label>客户名称</label>
<input placeholder="段次客户名称" />
</div>
<div class="field">
<label>计划日期</label>
<input type="text" placeholder="开始日期 - 结束日期" />
</div>
</div>
<div class="filter-actions">
<button type="button" class="btn btn-ghost">重置</button>
<button type="button" class="btn btn-main">搜索</button>
</div>
</div>
<div class="list-toolbar">
<div class="date-pills">
<button type="button" class="date-pill active">全部</button>
<button type="button" class="date-pill">今天</button>
<button type="button" class="date-pill">近 7 天</button>
<button type="button" class="date-pill">本月</button>
</div>
<div class="toolbar-spacer"></div>
<input class="quick-search" placeholder="计划编号/客户名称" />
<button type="button" class="btn btn-ghost">导出</button>
<button type="button" class="btn btn-ghost">刷新</button>
<span class="muted" style="font-size:13px">待完善 3 条</span>
</div>
<div class="list-body">
<table class="list-table">
<thead>
<tr>
<th style="width:52px">序号</th>
<th style="width:130px">计划编号</th>
<th style="width:100px">计划日期</th>
<th style="width:80px">单据类型</th>
<th style="width:88px">跟进状态</th>
<th style="width:72px">段次</th>
<th style="width:160px">客户</th>
<th style="width:160px">地址 / 时间</th>
<th style="width:200px">合同缺口</th>
<th>产品名称</th>
</tr>
</thead>
<tbody>
<!-- 计划 1:装货缺合同,送货内置可免 -->
<tr>
<td>1</td>
<td rowspan="2" class="merge-cell">
<button type="button" class="btn-link mono">YP20260820001</button>
</td>
<td rowspan="2" class="merge-cell">2026-08-20</td>
<td rowspan="2" class="merge-cell">汽运</td>
<td rowspan="2" class="merge-cell"><span class="tag success">已安排</span></td>
<td>装货</td>
<td>合肥某某商贸有限公司</td>
<td>
<div class="addr-time">
<span class="addr">合肥市经开区某某路 88 号</span>
<span class="time">08:30</span>
</div>
</td>
<td>
<div class="gap-cell">
<div class="gap-line">
<span class="side">装货</span>
<span class="pending">待补充</span>
</div>
<button type="button" class="btn-link warn" data-open="load1">✎ 补充</button>
</div>
</td>
<td>液碱 32%</td>
</tr>
<tr>
<td>2</td>
<td>送货</td>
<td>
安庆市山峡化工有限公司
<span class="tag internal">内部</span>
</td>
<td>
<div class="addr-time">
<span class="addr">安庆市迎江区某某大道</span>
<span class="time">14:00</span>
</div>
</td>
<td>
<div class="gap-cell">
<div class="gap-line">
<span class="side">送货</span>
<span class="ok">内置可免</span>
</div>
</div>
</td>
<td>液碱 32%</td>
</tr>
<!-- 计划 2:送货缺合同 -->
<tr>
<td>3</td>
<td rowspan="2" class="merge-cell">
<button type="button" class="btn-link mono">YP20260819088</button>
</td>
<td rowspan="2" class="merge-cell">2026-08-19</td>
<td rowspan="2" class="merge-cell">汽运</td>
<td rowspan="2" class="merge-cell"><span class="tag info">待安排</span></td>
<td>装货</td>
<td>
安徽云栋化工有限公司
<span class="tag internal">内部</span>
</td>
<td>
<div class="addr-time">
<span class="addr">安庆市大观区某某工业园</span>
<span class="time">09:00</span>
</div>
</td>
<td>
<div class="gap-cell">
<div class="gap-line">
<span class="side">装货</span>
<span class="ok">内置可免</span>
</div>
</div>
</td>
<td>盐酸</td>
</tr>
<tr>
<td>4</td>
<td>送货</td>
<td>芜湖某某材料有限公司</td>
<td>
<div class="addr-time">
<span class="addr">芜湖市弋江区某某路</span>
<span class="time">16:00</span>
</div>
</td>
<td>
<div class="gap-cell">
<div class="gap-line">
<span class="side">送货</span>
<span class="pending">待补充</span>
</div>
<button type="button" class="btn-link warn" data-open="delivery2">✎ 补充</button>
</div>
</td>
<td>盐酸</td>
</tr>
<!-- 计划 3:两侧均缺,仅示装货行补充入口 -->
<tr>
<td>5</td>
<td class="merge-cell">
<button type="button" class="btn-link mono">YP20260818042</button>
</td>
<td class="merge-cell">2026-08-18</td>
<td class="merge-cell">汽运</td>
<td class="merge-cell"><span class="tag success">运输中</span></td>
<td>装货</td>
<td>马鞍山某某化工有限公司</td>
<td>
<div class="addr-time">
<span class="addr">马鞍山市雨山区某某路</span>
<span class="time">07:30</span>
</div>
</td>
<td>
<div class="gap-cell">
<div class="gap-line">
<span class="side">装货</span>
<span class="pending">待补充</span>
</div>
<button type="button" class="btn-link warn" data-open="load3">✎ 补充</button>
</div>
</td>
<td>片碱</td>
</tr>
</tbody>
</table>
</div>
<div class="pager">共 3 条 · 每页 20 条 · 1 / 1</div>
</section>
<section class="legend">
<div class="card">
<h3>弹窗行为</h3>
<ul>
<li>标题:<strong>补充合同 · 装货/送货</strong> + 计划编号</li>
<li>摘要:计划日期、跟进状态、本段客户、地址时间</li>
<li>主体:复用合同多选卡片(选择 / 查看 / 移除)</li>
<li>保存:仅提交本段合同;成功后刷新列表与角标</li>
</ul>
</div>
<div class="card">
<h3>与现网差异</h3>
<ul>
<li>❌ 去掉表格 <code>type="expand"</code> 行内展开区</li>
<li>✅ 「补充」改为打开弹窗(本 Demo)</li>
<li>✅ 内置客户行仍不展示「补充」入口</li>
<li>✅ 不在此页新建 / 编辑合同</li>
</ul>
</div>
</section>
</div>
<!-- 补充合同弹窗 -->
<div class="modal-mask" id="dialog-mask" role="dialog" aria-modal="true" aria-labelledby="dlg-title">
<div class="dialog">
<div class="dialog-head">
<h3 id="dlg-title">补充合同 · <span id="dlg-stop-kind">装货</span> · <span id="dlg-plan-no">YP20260820001</span></h3>
<button type="button" class="btn-link" id="dlg-close" aria-label="关闭">✕</button>
</div>
<div class="dialog-body">
<div class="stop-summary">
<div class="kv"><div class="k">计划日期</div><div class="v" id="dlg-date">2026-08-20</div></div>
<div class="kv"><div class="k">跟进状态</div><div class="v" id="dlg-status"><span class="tag success">已安排</span></div></div>
<div class="kv"><div class="k">本段客户</div><div class="v" id="dlg-customer">合肥某某商贸有限公司</div></div>
<div class="kv"><div class="k">地址 / 时间</div><div class="v" id="dlg-addr">合肥市经开区某某路 88 号 · 08:30</div></div>
</div>
<div class="side-box need" id="dlg-side-box">
<div class="side-box__head">
<h4 id="dlg-side-title">装货合同</h4>
<span class="tag warn" id="dlg-side-tag">待补充</span>
</div>
<p class="side-box__customer" id="dlg-side-customer">合肥某某商贸有限公司</p>
<div id="dlg-exempt" class="side-box__exempt" hidden>
内置公司(山峡/云栋),无需补充合同
</div>
<div id="dlg-editor">
<div class="contract-toolbar">
<button type="button" class="btn btn-ghost btn-sm" id="btn-pick">🔍 选择合同</button>
<span class="contract-hint is-warn" id="dlg-hint">请至少选择 1 份合同后保存</span>
</div>
<div class="contract-cards" id="dlg-cards">
<p class="empty-hint">暂未选择合同</p>
</div>
</div>
</div>
</div>
<div class="dialog-foot">
<button type="button" class="btn btn-ghost" id="dlg-cancel">✕ 取消</button>
<button type="button" class="btn btn-main" id="dlg-save">✓ 保存本段合同</button>
</div>
</div>
</div>
<!-- 选择合同(二级) -->
<div class="selector-mask" id="selector-mask" role="dialog" aria-modal="true">
<div class="selector-dialog">
<div class="dialog-head">
<h3>选择合同 · <span id="sel-customer">客户</span></h3>
<button type="button" class="btn-link" id="sel-close">✕</button>
</div>
<div style="padding:12px 16px;border-bottom:1px solid #eef2f6">
<input style="width:100%;height:36px;border:1px solid var(--border);border-radius:8px;padding:0 12px"
placeholder="搜索合同编号 / 产品" id="sel-search" />
</div>
<div class="selector-list" id="sel-list"></div>
<div class="dialog-foot">
<button type="button" class="btn btn-ghost" id="sel-cancel">取消</button>
<button type="button" class="btn btn-main" id="sel-confirm">确定</button>
</div>
</div>
</div>
<div class="toast" id="toast">本段合同已保存(Demo)</div>
<script>
const catalog = [
{ id: 'c1', no: 'HT-2026-0312', type: '销售合同', status: '执行中', product: '液碱 32%', qty: '30 吨' },
{ id: 'c2', no: 'HT-2026-0288', type: '框架合同', status: '已审批', product: '盐酸', qty: '20 吨' },
{ id: 'c3', no: 'HT-2026-0199', type: '销售合同', status: '执行中', product: '片碱', qty: '15 吨' }
]
const scenarios = {
load1: {
planNo: 'YP20260820001',
stopKind: '装货',
date: '2026-08-20',
statusHtml: '<span class="tag success">已安排</span>',
customer: '合肥某某商贸有限公司',
addr: '合肥市经开区某某路 88 号 · 08:30',
exempt: false,
selected: []
},
delivery2: {
planNo: 'YP20260819088',
stopKind: '送货',
date: '2026-08-19',
statusHtml: '<span class="tag info">待安排</span>',
customer: '芜湖某某材料有限公司',
addr: '芜湖市弋江区某某路 · 16:00',
exempt: false,
selected: []
},
load3: {
planNo: 'YP20260818042',
stopKind: '装货',
date: '2026-08-18',
statusHtml: '<span class="tag success">运输中</span>',
customer: '马鞍山某某化工有限公司',
addr: '马鞍山市雨山区某某路 · 07:30',
exempt: false,
selected: []
}
}
let currentKey = 'load1'
let draftIds = []
let baselineIds = []
let pendingPick = []
const mask = document.getElementById('dialog-mask')
const selMask = document.getElementById('selector-mask')
function isDirty() {
const a = [...draftIds].sort().join(',')
const b = [...baselineIds].sort().join(',')
return a !== b
}
function renderCards() {
const el = document.getElementById('dlg-cards')
const hint = document.getElementById('dlg-hint')
const box = document.getElementById('dlg-side-box')
const tag = document.getElementById('dlg-side-tag')
if (!draftIds.length) {
el.innerHTML = '<p class="empty-hint">暂未选择合同</p>'
hint.textContent = '请至少选择 1 份合同后保存'
hint.classList.add('is-warn')
box.className = 'side-box need'
tag.className = 'tag warn'
tag.textContent = '待补充'
return
}
hint.textContent = '已选 ' + draftIds.length + ' 份合同'
hint.classList.remove('is-warn')
box.className = 'side-box ok'
tag.className = 'tag success'
tag.textContent = '已选'
el.innerHTML = draftIds.map(id => {
const c = catalog.find(x => x.id === id)
if (!c) return ''
return `
<article class="contract-card" data-id="${c.id}">
<div class="contract-card__head">
<div class="contract-card__meta">
<span class="contract-card__no">${c.no}</span>
<span class="tag info">${c.type}</span>
<span class="contract-card__count">1 个产品</span>
</div>
<div class="contract-card__actions">
<button type="button" class="btn-icon" title="查看" data-view="${c.id}">👁</button>
<button type="button" class="btn-icon danger" title="移除" data-remove="${c.id}">🗑</button>
</div>
</div>
<div class="contract-card__body">
<table class="mini-table">
<thead>
<tr>
<th>产品名称</th>
<th>数量</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr>
<td>${c.product}</td>
<td>${c.qty}</td>
<td>${c.status}</td>
</tr>
</tbody>
</table>
</div>
</article>`
}).join('')
el.querySelectorAll('[data-remove]').forEach(btn => {
btn.addEventListener('click', () => {
draftIds = draftIds.filter(id => id !== btn.dataset.remove)
renderCards()
})
})
el.querySelectorAll('[data-view]').forEach(btn => {
btn.addEventListener('click', () => {
const c = catalog.find(x => x.id === btn.dataset.view)
alert('Demo:查看合同详情\\n' + (c ? c.no : ''))
})
})
}
function openDialog(key) {
currentKey = key
const s = scenarios[key] || scenarios.load1
draftIds = [...(s.selected || [])]
baselineIds = [...draftIds]
document.getElementById('dlg-stop-kind').textContent = s.stopKind
document.getElementById('dlg-plan-no').textContent = s.planNo
document.getElementById('dlg-date').textContent = s.date
document.getElementById('dlg-status').innerHTML = s.statusHtml
document.getElementById('dlg-customer').textContent = s.customer
document.getElementById('dlg-addr').textContent = s.addr
document.getElementById('dlg-side-title').textContent = s.stopKind + '合同'
document.getElementById('dlg-side-customer').textContent = s.customer
document.getElementById('sel-customer').textContent = s.customer
const exempt = !!s.exempt
document.getElementById('dlg-exempt').hidden = !exempt
document.getElementById('dlg-editor').hidden = exempt
document.getElementById('dlg-save').style.display = exempt ? 'none' : ''
if (exempt) {
document.getElementById('dlg-side-box').className = 'side-box exempt'
document.getElementById('dlg-side-tag').className = 'tag muted'
document.getElementById('dlg-side-tag').textContent = '可免'
} else {
renderCards()
}
mask.classList.add('open')
}
function tryCloseDialog() {
if (isDirty() && !confirm('当前有未保存的合同变更,确认关闭吗?')) return
mask.classList.remove('open')
}
function openSelector() {
pendingPick = [...draftIds]
const list = document.getElementById('sel-list')
list.innerHTML = catalog.map(c => {
const checked = pendingPick.includes(c.id) ? 'checked' : ''
const selected = pendingPick.includes(c.id) ? 'selected' : ''
return `
<label class="selector-item ${selected}">
<input type="checkbox" value="${c.id}" ${checked} />
<div>
<div style="font-weight:600;color:var(--primary-dark)">${c.no}</div>
<div class="meta-line">${c.type} · ${c.status} · ${c.product}</div>
</div>
</label>`
}).join('')
list.querySelectorAll('input').forEach(input => {
input.addEventListener('change', () => {
pendingPick = [...list.querySelectorAll('input:checked')].map(i => i.value)
list.querySelectorAll('.selector-item').forEach(item => {
const cb = item.querySelector('input')
item.classList.toggle('selected', cb.checked)
})
})
})
selMask.classList.add('open')
}
function closeSelector() {
selMask.classList.remove('open')
}
document.querySelectorAll('[data-open]').forEach(btn => {
btn.addEventListener('click', () => openDialog(btn.dataset.open))
})
document.getElementById('dlg-close').addEventListener('click', tryCloseDialog)
document.getElementById('dlg-cancel').addEventListener('click', tryCloseDialog)
mask.addEventListener('click', e => { if (e.target === mask) tryCloseDialog() })
document.getElementById('btn-pick').addEventListener('click', openSelector)
document.getElementById('sel-close').addEventListener('click', closeSelector)
document.getElementById('sel-cancel').addEventListener('click', closeSelector)
selMask.addEventListener('click', e => { if (e.target === selMask) closeSelector() })
document.getElementById('sel-confirm').addEventListener('click', () => {
draftIds = [...pendingPick]
closeSelector()
renderCards()
})
document.getElementById('dlg-save').addEventListener('click', () => {
if (!draftIds.length) {
alert('请先选择至少 1 份合同')
return
}
scenarios[currentKey].selected = [...draftIds]
baselineIds = [...draftIds]
mask.classList.remove('open')
const toast = document.getElementById('toast')
toast.classList.add('show')
setTimeout(() => toast.classList.remove('show'), 1800)
})
</script>
</body>
</html>
@@ -0,0 +1,9 @@
-- 段级归档品牌数量:扩展 pl_plan_product_brand_qty
-- stop_product_id 为空 = 计划产品级汇总;非空 = 装货/送货段产品行上的填写值
-- unit_price = 该品牌单价(金额 = qty * unit_price,展示时计算)
ALTER TABLE `pl_plan_product_brand_qty`
ADD COLUMN `stop_type` varchar(20) DEFAULT NULL COMMENT 'load/delivery,空=计划级汇总' AFTER `plan_product_id`,
ADD COLUMN `stop_product_id` varchar(64) DEFAULT NULL COMMENT '段产品行ID,空=计划级汇总' AFTER `stop_type`,
ADD COLUMN `unit_price` decimal(15,4) DEFAULT NULL COMMENT '本品牌归档单价' AFTER `qty`,
ADD KEY `idx_stop_product` (`stop_product_id`) USING BTREE;
@@ -0,0 +1,181 @@
<template>
<section v-if="items.length" class="pl-archive-brand-view">
<h4 class="pl-archive-brand-view__title">品牌归档明细</h4>
<div v-for="item in items" :key="item.key" class="pl-archive-brand-product">
<div class="pl-archive-brand-product__head">
<span class="pl-archive-brand-product__name">{{ item.productName }}</span>
<span class="pl-archive-brand-product__plan">
计划 {{ item.planQty ?? '—' }} {{ unitLabel(item.unit) }}
</span>
<span class="pl-archive-brand-product__segment-total">
本段合计 {{ formatQty(item.segmentQty) }} {{ unitLabel(item.unit) }}
</span>
<el-tag v-if="item.brandMode === 'required'" size="small" type="warning">指定品牌</el-tag>
<el-tag v-else-if="item.brandMode === 'any'" size="small" type="info">不限品牌</el-tag>
</div>
<div class="pl-archive-brand-table-wrap">
<table class="pl-archive-brand-table">
<thead>
<tr>
<th>品牌</th>
<th class="is-num">实际数量</th>
<th class="is-num">单价(元)</th>
<th class="is-num">金额(元)</th>
</tr>
</thead>
<tbody>
<tr v-for="brandRow in item.rows" :key="brandRow.brandId">
<td>{{ brandRow.brandName || '—' }}</td>
<td class="is-num">{{ formatQty(brandRow.qty) }}</td>
<td class="is-num">{{ formatPrice(brandRow.price) }}</td>
<td class="is-num pl-archive-brand-table__amount">
{{ formatArchiveBrandAmount(brandRow.qty, brandRow.price) }}
</td>
</tr>
</tbody>
<tfoot v-if="item.rows.length > 1">
<tr>
<td>合计</td>
<td class="is-num">{{ formatQty(sumQty(item.rows)) }}</td>
<td class="is-num">—</td>
<td class="is-num">{{ formatAmountTotal(item.rows) }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import {
type ArchiveBrandViewItem,
type ArchiveBrandViewRow,
formatArchiveBrandAmount
} from '@/utils/pl-plan-archive-brand-view'
import { useDictStore } from '@/store/modules/dict'
defineProps<{
items: ArchiveBrandViewItem[]
}>()
const dictStore = useDictStore()
function unitLabel(unit?: string) {
const code = (unit || '').trim()
return dictStore.getLabel('crm_product_unit', code) || code || '吨'
}
function formatQty(value?: number | null) {
if (value == null || !Number.isFinite(Number(value))) return '—'
return Number(value).toFixed(3)
}
function formatPrice(value?: number | null) {
if (value == null || !Number.isFinite(Number(value))) return '—'
return Number(value).toFixed(2)
}
function sumQty(rows: ArchiveBrandViewRow[]) {
return rows.reduce((sum, row) => sum + Number(row.qty || 0), 0)
}
function formatAmountTotal(rows: ArchiveBrandViewRow[]) {
const total = rows.reduce((sum, row) => sum + Number(row.qty || 0) * Number(row.price || 0), 0)
return Number.isFinite(total) ? total.toFixed(2) : '—'
}
</script>
<style scoped lang="scss">
.pl-archive-brand-view {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 4px;
padding-top: 10px;
border-top: 1px dashed var(--el-border-color-lighter, #e2e8f0);
}
.pl-archive-brand-view__title {
margin: 0;
padding-left: 10px;
font-size: 13px;
font-weight: 600;
color: var(--section-bar-title-color, var(--primary-dark, #059669));
border-left: 3px solid var(--section-bar-border-color, var(--primary, #10b981));
line-height: 1;
}
.pl-archive-brand-product {
padding: 12px;
border: 1px solid var(--el-border-color-lighter, #e2e8f0);
border-radius: 8px;
background: var(--mint-bg-soft, #f8fafc);
}
.pl-archive-brand-product__head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.pl-archive-brand-product__name {
font-size: 13px;
font-weight: 600;
color: #334155;
}
.pl-archive-brand-product__plan {
font-size: 12px;
color: var(--mint-text-muted, #64748b);
}
.pl-archive-brand-product__segment-total {
margin-left: auto;
font-size: 12px;
font-weight: 600;
color: #059669;
}
.pl-archive-brand-table-wrap {
overflow-x: auto;
}
.pl-archive-brand-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
th,
td {
padding: 8px 10px;
border-bottom: 1px solid #e2e8f0;
text-align: center;
vertical-align: middle;
}
th {
font-size: 12px;
font-weight: 600;
color: #64748b;
background: #fff;
}
tfoot td {
font-weight: 600;
color: #334155;
}
.is-num {
text-align: center;
white-space: nowrap;
}
}
.pl-archive-brand-table__amount {
font-variant-numeric: tabular-nums;
color: #334155;
}
</style>
@@ -0,0 +1,392 @@
<template>
<div class="pl-plan-view-stop-block pl-plan-archive-segment-block">
<div class="pl-plan-view-stop-meta">
<div class="pl-plan-view-party-field">
<span class="pl-plan-view-party-field__label">{{ customerLabel }}</span>
<span class="pl-plan-view-party-field__value">{{ meta.customerName || '—' }}</span>
</div>
<div class="pl-plan-view-party-field">
<span class="pl-plan-view-party-field__label">{{ timeLabel }}</span>
<span class="pl-plan-view-party-field__value">{{ formatViewDate(meta.time) }}</span>
</div>
</div>
<PlPlanContractMultiSelect
:model-value="meta.contracts || []"
:customer-id="meta.customerId"
readonly
compact-summary
/>
<PlPlanStopProductTable
v-if="viewProducts.length"
:products="viewProducts"
compact
show-stop-actual-qty
/>
<p v-else class="pl-plan-form-empty-hint">暂无产品</p>
<PlPlanStopFeedbackPanel
v-if="showStopFeedback"
:stop-label="feedbackStopLabel"
:customer-name="meta.customerName"
:generate-dibang="generateDibang"
:dibang-rows="dibangRows"
:driver-feedback="driverFeedback"
:site-feedback="siteFeedback"
:fallback-remark="fallbackRemark"
/>
<section class="pl-archive-segment-form">
<h4 class="pl-archive-segment-form__title">归档填写</h4>
<PlPlanFormProductTable
v-if="products.length"
:products="products"
archive-compact
show-archive-io-fields
:archive-io-inbound-only="archiveMode === 'inbound'"
:archive-io-outbound-only="archiveMode === 'outbound'"
archive-segment-labels
editable-archive-io-fields
/>
<p v-else class="pl-plan-form-empty-hint">暂无可归档产品</p>
<div
v-for="row in brandProducts"
:key="`brand-${row.id || row.productId}`"
class="pl-archive-brand-product"
>
<div class="pl-archive-brand-product__head">
<span class="pl-archive-brand-product__name">{{
row.standardProductName || row.productName
}}</span>
<span class="pl-archive-brand-product__plan"
>计划 {{ row.quantity ?? '—' }} {{ unitLabel(row.unit) }}</span
>
<span class="pl-archive-brand-product__segment-total">
本段合计 {{ formatSegmentActualQty(row) }} {{ unitLabel(row.unit) }}
</span>
<el-tag v-if="row.brandMode === 'required'" size="small" type="warning">指定品牌</el-tag>
<el-tag v-else size="small" type="info">不限品牌</el-tag>
</div>
<div class="pl-archive-brand-table-wrap">
<table class="pl-archive-brand-table">
<thead>
<tr>
<th>品牌</th>
<th class="is-num">实际数量</th>
<th class="is-num">单价(元)</th>
<th class="is-num">金额(元)</th>
</tr>
</thead>
<tbody>
<tr v-for="brandRow in activeBrandRows(row)" :key="brandRow.brandId">
<td>{{ brandRow.brandName }}</td>
<td class="is-num">
<el-input-number
v-model="brandRow.qty"
:min="0"
:precision="3"
:controls="false"
@change="() => syncProductFromBrandRows(row)"
/>
</td>
<td class="is-num">
<el-input-number
v-model="brandRow.price"
:min="0"
:precision="2"
:controls="false"
@change="() => syncProductFromBrandRows(row)"
/>
</td>
<td class="is-num pl-archive-brand-table__amount">
{{ formatBrandRowAmount(brandRow) }}
</td>
</tr>
</tbody>
<tfoot v-if="activeBrandRows(row).length > 1">
<tr>
<td>合计</td>
<td class="is-num">{{ formatBrandQtyTotal(row) }}</td>
<td class="is-num">—</td>
<td class="is-num">{{ formatBrandAmountTotal(row) }}</td>
</tr>
</tfoot>
</table>
</div>
<BrandQtyChipsField
v-if="row.brandMode === 'any' && (row.brandOptions || []).length > 1"
v-model="row.brandQtyRows"
brand-mode="any"
:brand-options="row.brandOptions || []"
:unit="unitLabel(row.unit)"
variant="admin"
@update:model-value="() => syncProductFromBrandRows(row)"
/>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import type { TransportStopFeedback } from '@/api/pl'
import BrandQtyChipsField from '@/components/business/brand-qty-chips-field/index.vue'
import PlPlanContractMultiSelect from '@/components/business/pl-plan-contract-multi-select/index.vue'
import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
import PlPlanStopFeedbackPanel from '@/components/business/pl-plan-stop-feedback-panel/index.vue'
import PlPlanStopProductTable from '@/components/business/pl-plan-stop-product-table/index.vue'
import type { PlPlanTransportDibangRow } from '@/composables/usePlPlanViewDetail'
import { type PlanFormProductRow } from '@/composables/usePlContractToPlan'
import { useDictStore } from '@/store/modules/dict'
import type { PlanStopProductFormRow } from '@/utils/pl-plan-stop-form'
export interface ArchiveSegmentMeta {
customerId?: string
customerName?: string
time?: string
contracts?: Array<{ contractId: string; contractNo?: string }>
}
export interface ArchiveBrandQtyRow {
brandId: string
brandName: string
qty: number
price?: number
}
const props = defineProps<{
stopType: 'load' | 'delivery'
meta: ArchiveSegmentMeta
viewProducts: PlanStopProductFormRow[]
products: PlanFormProductRow[]
archiveMode: 'inbound' | 'outbound'
showStopFeedback?: boolean
feedbackStopLabel?: string
generateDibang?: number
dibangRows?: PlPlanTransportDibangRow[]
driverFeedback?: TransportStopFeedback | null
siteFeedback?: TransportStopFeedback | null
fallbackRemark?: string | null
}>()
const dictStore = useDictStore()
const customerLabel = computed(() => (props.stopType === 'load' ? '装货客户' : '送货客户'))
const timeLabel = computed(() => (props.stopType === 'load' ? '装货时间' : '送货时间'))
const feedbackStopLabel = computed(
() => props.feedbackStopLabel || (props.stopType === 'load' ? '装货' : '送货')
)
const brandProducts = computed(() =>
(props.products || []).filter((row) => row.brandMode === 'any' || row.brandMode === 'required')
)
function formatSegmentActualQty(row: PlanFormProductRow) {
const qty =
props.archiveMode === 'inbound'
? row.inboundQty != null
? Number(row.inboundQty)
: null
: row.outboundQty != null
? Number(row.outboundQty)
: null
return qty != null && Number.isFinite(qty) ? qty.toFixed(3) : '—'
}
function formatViewDate(value?: string | null) {
const text = (value || '').trim()
if (!text) return '—'
return text.length >= 10 ? text.slice(0, 10) : text
}
function unitLabel(unit?: string) {
const code = (unit || '').trim()
return dictStore.getLabel('crm_product_unit', code) || code || '吨'
}
function activeBrandRows(row: PlanFormProductRow): ArchiveBrandQtyRow[] {
return (row.brandQtyRows || []) as ArchiveBrandQtyRow[]
}
function formatBrandRowAmount(brandRow: ArchiveBrandQtyRow) {
if (brandRow.qty == null || brandRow.price == null) return '—'
const amount = Number(brandRow.qty) * Number(brandRow.price)
return Number.isFinite(amount) ? amount.toFixed(2) : '—'
}
function formatBrandQtyTotal(row: PlanFormProductRow) {
const total = activeBrandRows(row).reduce((sum, item) => sum + Number(item.qty || 0), 0)
return total.toFixed(3)
}
function formatBrandAmountTotal(row: PlanFormProductRow) {
const total = activeBrandRows(row).reduce(
(sum, item) => sum + Number(item.qty || 0) * Number(item.price || 0),
0
)
return total.toFixed(2)
}
function syncProductFromBrandRows(row: PlanFormProductRow) {
const rows = activeBrandRows(row)
if (!rows.length) return
const totalQty = rows.reduce((sum, item) => sum + Number(item.qty || 0), 0)
const totalAmount = rows.reduce(
(sum, item) => sum + Number(item.qty || 0) * Number(item.price || 0),
0
)
const avgPrice = totalQty > 0 ? totalAmount / totalQty : undefined
if (props.archiveMode === 'inbound') {
row.inboundQty = totalQty
row.inboundPrice = avgPrice
} else {
row.outboundQty = totalQty
row.outboundPrice = avgPrice
}
}
</script>
<style scoped lang="scss">
.pl-plan-archive-segment-block {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.pl-plan-view-stop-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 16px;
}
.pl-plan-view-party-field {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0 12px;
min-width: 0;
}
.pl-plan-view-party-field__label {
font-size: 13px;
color: var(--mint-text-muted, #64748b);
white-space: nowrap;
}
.pl-plan-view-party-field__value {
font-size: 14px;
font-weight: 600;
color: #1e293b;
word-break: break-word;
line-height: 1.5;
}
.pl-plan-form-empty-hint {
margin: 0;
font-size: 13px;
color: var(--mint-text-muted, #64748b);
}
.pl-archive-segment-form {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 4px;
border-top: 1px solid var(--el-border-color-lighter, #e2e8f0);
}
.pl-archive-segment-form__title {
margin: 0;
padding-left: 10px;
font-size: 13px;
font-weight: 600;
color: var(--section-bar-title-color, var(--primary-dark, #059669));
border-left: 3px solid var(--section-bar-border-color, var(--primary, #10b981));
line-height: 1;
}
.pl-archive-brand-product {
padding: 12px;
border: 1px solid var(--el-border-color-lighter, #e2e8f0);
border-radius: 8px;
background: var(--mint-bg-soft, #f8fafc);
}
.pl-archive-brand-product__head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.pl-archive-brand-product__name {
font-size: 13px;
font-weight: 600;
color: #334155;
}
.pl-archive-brand-product__plan {
font-size: 12px;
color: var(--mint-text-muted, #64748b);
}
.pl-archive-brand-product__segment-total {
margin-left: auto;
font-size: 12px;
font-weight: 600;
color: #059669;
}
.pl-archive-brand-table-wrap {
overflow-x: auto;
}
.pl-archive-brand-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
th,
td {
padding: 8px 10px;
border-bottom: 1px solid #e2e8f0;
text-align: center;
vertical-align: middle;
}
th {
font-size: 12px;
font-weight: 600;
color: #64748b;
background: #fff;
}
tfoot td {
font-weight: 600;
color: #334155;
}
.is-num {
text-align: center;
white-space: nowrap;
}
:deep(.el-input-number) {
width: 120px;
margin: 0 auto;
}
:deep(.el-input-number .el-input__inner) {
text-align: center;
}
}
.pl-archive-brand-table__amount {
font-variant-numeric: tabular-nums;
color: #334155;
}
</style>
@@ -0,0 +1,84 @@
<template>
<div class="pl-plan-contract-cell">
<div
v-for="(line, index) in lines"
:key="`${line.side}-${line.contractId || line.no}-${index}`"
class="pl-plan-contract-cell__row"
>
<span class="pl-plan-contract-cell__side">{{ line.side }}</span>
<el-link
v-if="isClickable(line)"
type="primary"
class="pl-plan-contract-cell__no pl-plan-contract-cell__link"
@click.stop="emit('open', line.contractId!)"
>
{{ line.no }}
</el-link>
<span v-else class="pl-plan-contract-cell__no" :class="{ 'is-pending': line.pending }">
{{ line.no }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { PlanContractDisplayLine } from '@/utils/pl-plan-contract-display'
defineProps<{
lines: PlanContractDisplayLine[]
}>()
const emit = defineEmits<{
open: [contractId: string]
}>()
function isClickable(line: PlanContractDisplayLine) {
return !!line.contractId && !line.pending && line.no !== '—'
}
</script>
<style scoped lang="scss">
.pl-plan-contract-cell {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
width: 100%;
text-align: left;
}
.pl-plan-contract-cell__row {
display: flex;
align-items: baseline;
gap: 6px;
width: 100%;
min-width: 0;
font-size: 13px;
line-height: 1.5;
color: var(--text, #1e293b);
}
.pl-plan-contract-cell__side {
flex-shrink: 0;
font-weight: 600;
color: var(--mint-text-muted, #64748b);
}
.pl-plan-contract-cell__no {
min-width: 0;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
&.is-pending {
font-weight: 600;
color: #d97706;
}
}
.pl-plan-contract-cell__link {
font-size: 13px;
font-weight: 500;
vertical-align: baseline;
}
</style>
@@ -0,0 +1,47 @@
import { defineAsyncComponent, nextTick, ref, watch } from 'vue'
export const ContractDetailDialogAsync = defineAsyncComponent(
() => import('@/views/crm/contract/components/ContractDetailDialog.vue')
)
type ContractDetailDialogExpose = {
open: (id: string | number) => Promise<void>
}
export function useContractDetailDialog() {
const contractDetailRef = ref<ContractDetailDialogExpose>()
async function waitForContractDetailDialog() {
if (contractDetailRef.value) return contractDetailRef.value
await nextTick()
if (contractDetailRef.value) return contractDetailRef.value
return new Promise<ContractDetailDialogExpose>((resolve) => {
const stop = watch(
contractDetailRef,
(val) => {
if (val) {
stop()
resolve(val)
}
},
{ flush: 'post' }
)
})
}
async function openContractDetail(contractId: string | number | undefined | null) {
const id = String(contractId ?? '').trim()
if (!id) {
ElMessage.warning('暂无关联合同')
return
}
const dialog = await waitForContractDetailDialog()
await dialog.open(id)
}
return {
ContractDetailDialog: ContractDetailDialogAsync,
contractDetailRef,
openContractDetail
}
}
@@ -0,0 +1,81 @@
import { ref, watch, type Ref } from 'vue'
/** 是否为可提交的完整非负小数(空字符串视为清空) */
export function isCompleteDecimalInput(raw: string): boolean {
const trimmed = raw.trim()
return trimmed === '' || /^\d+(\.\d+)?$/.test(trimmed)
}
export function parseDecimalInput(raw: string, precision?: number): number | null {
const trimmed = raw.trim()
if (!trimmed) return null
if (!/^\d+(\.\d+)?$/.test(trimmed)) return null
const num = Number(trimmed)
if (!Number.isFinite(num) || num < 0) return null
if (precision == null) return num
return Number(num.toFixed(precision))
}
function formatDecimalDisplay(val: number, precision?: number): string {
if (precision == null) return String(val)
return Number(val.toFixed(precision)).toString()
}
function sanitizeDecimalText(raw: string): string {
let text = raw.replace(/[^\d.]/g, '')
const dotIndex = text.indexOf('.')
if (dotIndex >= 0) {
text = text.slice(0, dotIndex + 1) + text.slice(dotIndex + 1).replace(/\./g, '')
}
return text
}
/**
* 将 number | null 与 el-input 文本双向绑定;
* 仅在完整小数或清空时写入 source,避免打断输入,同时保证运费计算 watch 能触发。
*/
export function useDecimalInputBinding(
source: Ref<number | null>,
options?: { precision?: number; onCommit?: () => void }
) {
const text = ref('')
watch(
source,
(val) => {
const next =
val == null || Number.isNaN(Number(val))
? ''
: formatDecimalDisplay(Number(val), options?.precision)
if (text.value !== next) text.value = next
},
{ immediate: true }
)
function commitFromText(raw: string, force = false) {
const sanitized = sanitizeDecimalText(raw)
text.value = sanitized
if (!force && !isCompleteDecimalInput(sanitized)) return
const parsed = parseDecimalInput(sanitized, options?.precision)
const prev = source.value
const changed =
parsed == null ? prev != null : prev == null || Math.abs(Number(prev) - parsed) > 1e-9
source.value = parsed
if (parsed != null) {
text.value = formatDecimalDisplay(parsed, options?.precision)
} else if (force) {
text.value = ''
}
if (changed) options?.onCommit?.()
}
function handleInput(val: string) {
commitFromText(val, false)
}
function handleBlur() {
commitFromText(text.value, true)
}
return { text, handleInput, handleBlur }
}
@@ -0,0 +1,211 @@
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
import type { PlanStopProductFormRow } from '@/utils/pl-plan-stop-form'
export interface ArchiveBrandViewRow {
brandId: string
brandName: string
qty: number
price?: number
amount?: number
}
export interface ArchiveBrandViewItem {
key: string
productName: string
unit?: string
planQty?: number
segmentQty: number
brandMode?: string
rows: ArchiveBrandViewRow[]
}
function getSegmentQty(
row: PlanStopProductFormRow,
mode: 'inbound' | 'outbound',
planRow?: PlanFormProductRow
): number {
const primary = mode === 'inbound' ? row.inboundQty : row.outboundQty
if (primary != null && Number.isFinite(Number(primary))) return Number(primary)
const fallback = mode === 'inbound' ? planRow?.inboundQty : planRow?.outboundQty
if (fallback != null && Number.isFinite(Number(fallback))) return Number(fallback)
return 0
}
function getSegmentPrice(
row: PlanStopProductFormRow,
mode: 'inbound' | 'outbound',
planRow?: PlanFormProductRow
): number | undefined {
const primary = mode === 'inbound' ? row.inboundPrice : row.outboundPrice
if (primary != null && Number.isFinite(Number(primary))) return Number(primary)
const fallback = mode === 'inbound' ? planRow?.inboundPrice : planRow?.outboundPrice
if (fallback != null && Number.isFinite(Number(fallback))) return Number(fallback)
if (planRow?.taxPrice != null && Number.isFinite(Number(planRow.taxPrice))) {
return Number(planRow.taxPrice)
}
return undefined
}
function getPlanBrandRows(planRow?: PlanFormProductRow) {
return (planRow?.brandQtyRows || []).filter((row) => Number(row.qty) > 0)
}
function getStopBrandRows(stopRow: PlanStopProductFormRow) {
return (stopRow.brandQtys || []).filter((row) => Number(row.qty) > 0)
}
function getAllocationMap(
allocations: Map<string, Map<string, number>>,
planProductId: string
): Map<string, number> {
let map = allocations.get(planProductId)
if (!map) {
map = new Map()
allocations.set(planProductId, map)
}
return map
}
/** 按段顺序贪心分配计划级品牌量(旧数据无段级品牌明细时回退) */
function allocateBrandRows(
brandRows: Array<{ brandId: string; brandName: string; qty: number }>,
segmentQty: number,
price: number | undefined,
allocated: Map<string, number>
): ArchiveBrandViewRow[] {
if (segmentQty <= 0 || !brandRows.length) return []
let remaining = segmentQty
const result: ArchiveBrandViewRow[] = []
for (const brand of brandRows) {
const total = Number(brand.qty || 0)
if (total <= 0) continue
const used = allocated.get(brand.brandId) || 0
const available = total - used
if (available <= 0.0005) continue
const take = Math.min(available, remaining)
if (take <= 0.0005) continue
allocated.set(brand.brandId, used + take)
remaining -= take
result.push({
brandId: brand.brandId,
brandName: brand.brandName,
qty: take,
price,
amount: price != null ? take * price : undefined
})
if (remaining <= 0.0005) break
}
return result
}
function buildRowsFromStopBrandQtys(
stopRow: PlanStopProductFormRow,
fallbackPrice: number | undefined
): ArchiveBrandViewRow[] {
return getStopBrandRows(stopRow).map((brand) => {
const qty = Number(brand.qty || 0)
const price =
brand.unitPrice != null && Number.isFinite(Number(brand.unitPrice))
? Number(brand.unitPrice)
: fallbackPrice
return {
brandId: String(brand.brandId || ''),
brandName: String(brand.brandName || ''),
qty,
price,
amount: price != null ? qty * price : undefined
}
})
}
export function buildArchiveBrandViewItems(
stopProducts: PlanStopProductFormRow[],
planProducts: PlanFormProductRow[],
mode: 'inbound' | 'outbound',
allocations: Map<string, Map<string, number>>
): ArchiveBrandViewItem[] {
const planById = new Map(planProducts.filter((row) => row.id).map((row) => [String(row.id), row]))
const planByProductId = new Map(planProducts.map((row) => [String(row.productId), row]))
const items: ArchiveBrandViewItem[] = []
for (const stopRow of stopProducts) {
const planRow =
(stopRow.planProductId ? planById.get(String(stopRow.planProductId)) : undefined) ||
planByProductId.get(String(stopRow.productId))
const brandMode = stopRow.brandMode || planRow?.brandMode
if (brandMode !== 'any' && brandMode !== 'required') continue
const planProductId = String(stopRow.planProductId || planRow?.id || '').trim()
if (!planProductId) continue
const segmentQty = getSegmentQty(stopRow, mode, planRow)
if (segmentQty <= 0) continue
const price = getSegmentPrice(stopRow, mode, planRow)
// 优先使用段级归档时填写的品牌数量
let rows = buildRowsFromStopBrandQtys(stopRow, price)
if (!rows.length) {
const brandRows = getPlanBrandRows(planRow)
if (!brandRows.length) continue
const allocated = getAllocationMap(allocations, planProductId)
rows = allocateBrandRows(brandRows, segmentQty, price, allocated)
}
if (!rows.length) continue
items.push({
key: `${planProductId}-${stopRow.id || stopRow.productId}`,
productName:
stopRow.standardProductName ||
stopRow.productName ||
planRow?.standardProductName ||
planRow?.productName ||
'—',
unit: stopRow.unit || planRow?.unit,
planQty: stopRow.planQty ?? planRow?.quantity,
segmentQty,
brandMode,
rows
})
}
return items
}
export function buildArchiveBrandViewsBySegment(input: {
loadProducts: PlanStopProductFormRow[]
deliveries: Array<{ key: string; products: PlanStopProductFormRow[] }>
planProducts: PlanFormProductRow[]
}): Map<string, ArchiveBrandViewItem[]> {
const result = new Map<string, ArchiveBrandViewItem[]>()
// 装货(入库)与送货(出库)各自独立分配池,互不占位(仅旧数据回退贪心时使用)
const inboundAllocations = new Map<string, Map<string, number>>()
const outboundAllocations = new Map<string, Map<string, number>>()
result.set(
'load',
buildArchiveBrandViewItems(
input.loadProducts,
input.planProducts,
'inbound',
inboundAllocations
)
)
for (const delivery of input.deliveries) {
result.set(
delivery.key,
buildArchiveBrandViewItems(
delivery.products,
input.planProducts,
'outbound',
outboundAllocations
)
)
}
return result
}
export function formatArchiveBrandAmount(qty?: number, price?: number): string {
if (qty == null || price == null) return '—'
const amount = Number(qty) * Number(price)
return Number.isFinite(amount) ? amount.toFixed(2) : '—'
}