陆运多条线路按段展示 TMS 费用名和时效,选定后再写回该条成本。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -901,6 +901,38 @@ def sea_bucket_fee_lines(quote: dict[str, Any] | None) -> list[str]:
|
||||
return sections
|
||||
|
||||
|
||||
def _land_grouped_fee_lines(quote: dict[str, Any] | None) -> list[str]:
|
||||
"""陆运:每段先写始发→目的,再写该段 TMS 费用名。"""
|
||||
from agent.schema.land_route_options import land_leg_fee_blocks
|
||||
|
||||
src = dict(quote or {})
|
||||
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
||||
lines: list[str] = []
|
||||
for title, fees, eta in land_leg_fee_blocks(src):
|
||||
lines.append(title)
|
||||
for fee in fees:
|
||||
name = str(fee.get("name") or "").strip()
|
||||
amount = str(fee.get("amount") or "").strip()
|
||||
line_ccy = str(fee.get("currency") or fee.get("currencyCode") or header_ccy).strip()
|
||||
if name and amount and amount != "-":
|
||||
lines.append(f"{name}:{money_text(amount, line_ccy)}")
|
||||
if eta:
|
||||
lines.append(f"时效:{eta}")
|
||||
return lines
|
||||
|
||||
|
||||
def _is_land_surface(*, quote: dict[str, Any] | None = None, business_line: str = "", transport_mode: str = "") -> bool:
|
||||
"""陆运销售面:有 landSegments / 陆运业务线就按段出明细。"""
|
||||
src = dict(quote or {})
|
||||
if src.get("landSegments") or src.get("land_segments") or src.get("selected_land_route"):
|
||||
return True
|
||||
raw_line = (business_line or "").strip()
|
||||
if raw_line and scheme_transport_mode(raw_line, fallback="") == "陆运":
|
||||
return True
|
||||
mode = (transport_mode or "").strip()
|
||||
return bool(mode) and scheme_transport_mode(mode, fallback="") == "陆运"
|
||||
|
||||
|
||||
def quote_area_fee_text(
|
||||
quote: dict[str, Any] | None,
|
||||
business_line: str = "",
|
||||
@@ -915,8 +947,13 @@ def quote_area_fee_text(
|
||||
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
||||
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
|
||||
bucket_lines = sea_bucket_fee_lines(src)
|
||||
land_lines = _land_grouped_fee_lines(src) if _is_land_surface(
|
||||
quote=src, business_line=business_line, transport_mode=transport_mode
|
||||
) else []
|
||||
if bucket_lines:
|
||||
fee_lines = list(bucket_lines)
|
||||
elif land_lines:
|
||||
fee_lines = list(land_lines)
|
||||
else:
|
||||
fee_lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)]
|
||||
fee_lines.append(f"{_total_label(src, business_line, transport_mode)}:{total}")
|
||||
@@ -968,20 +1005,28 @@ def sea_quote_detail_text(
|
||||
"""
|
||||
完整价格明细文本:来源、工单号、费用行、总价。海运/陆运/空运共用。
|
||||
|
||||
时效、报价有效期只在卡片上出现,这里不写,避免和卡重复。
|
||||
陆运分段时效跟在各段费用后面,用 TMS 原文。海运时效、报价有效期仍只在卡片上。
|
||||
空运:业务确认没有时效,总销售价下面只留报价有效期。
|
||||
不受卡片引用条字数限制。first_or_same 只兼容调用方。
|
||||
"""
|
||||
_ = first_or_same, history_work_order_no, status
|
||||
src = dict(quote or {})
|
||||
source = str(src.get("source_label") or "TMS标准报价").replace(" ", "")
|
||||
lines = [source, f"{work_order_no}价格明细"]
|
||||
if (business_line or "").upper() == "AIR":
|
||||
line = (business_line or "").upper()
|
||||
if line == "AIR":
|
||||
from agent.schema.air_route_options import route_codes
|
||||
|
||||
route = route_codes(None, {}, src)
|
||||
if route:
|
||||
lines.append(route)
|
||||
if line == "LAND":
|
||||
land_line = str(src.get("route_line") or "").strip()
|
||||
if land_line:
|
||||
lines.append(land_line)
|
||||
lines.append(quote_area_fee_text(src, business_line=business_line or "SEA"))
|
||||
if line == "AIR":
|
||||
lines.append(f"报价有效期:{tms_valid_until_display(src)}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -1048,10 +1093,10 @@ def quote_detail_lines(
|
||||
transport_mode: str = "",
|
||||
) -> list[str]:
|
||||
"""
|
||||
价格明细正文:TMS 费用、总销售价、时效、有效期。
|
||||
价格明细正文:TMS 费用、总销售价一行、有效期;海运/陆运再加时效。
|
||||
|
||||
给文本兜底和企微引用条共用,禁止在这里编时效/有效期。
|
||||
价格卡不展示工单状态,也不展示相同询价内容检索。
|
||||
空运业务确认没有时效,这里不写。禁止编时效/有效期。
|
||||
总销售价只写一次。价格卡不展示工单状态,也不展示相同询价内容检索。
|
||||
"""
|
||||
_ = status, first_or_same, history_work_order_no
|
||||
src = dict(quote or {})
|
||||
@@ -1060,13 +1105,18 @@ def quote_detail_lines(
|
||||
bucket_lines = sea_bucket_fee_lines(src) if (business_line or "").upper() == "SEA" or (
|
||||
transport_mode or ""
|
||||
).strip() == "海运" else []
|
||||
land_lines = _land_grouped_fee_lines(src) if _is_land_surface(
|
||||
quote=src, business_line=business_line, transport_mode=transport_mode
|
||||
) else []
|
||||
if bucket_lines:
|
||||
lines = list(bucket_lines)
|
||||
elif land_lines:
|
||||
lines = list(land_lines)
|
||||
else:
|
||||
lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)]
|
||||
lines.append(f"{_total_label(src, business_line, transport_mode)}:{total}")
|
||||
lines.append(f"{_total_label(src, business_line, transport_mode)}:{total}")
|
||||
lines.append(f"时效:{tms_eta_display(src)}")
|
||||
if not _is_air_surface(business_line=business_line, transport_mode=transport_mode):
|
||||
lines.append(f"时效:{tms_eta_display(src)}")
|
||||
lines.append(f"报价有效期:{tms_valid_until_display(src)}")
|
||||
return lines
|
||||
|
||||
@@ -1146,7 +1196,7 @@ def tms_hit_wecom_payload(
|
||||
|
||||
价格明细
|
||||
┌ 引用条:空运费 / 其它费用 / 总销售价
|
||||
时效、报价有效期
|
||||
报价有效期(空运不写时效)
|
||||
协同说明(普通文字,贴按钮上方)
|
||||
按钮
|
||||
|
||||
@@ -1159,7 +1209,6 @@ def tms_hit_wecom_payload(
|
||||
source = str(src.get("source_label") or "TMS标准报价").replace(" ", "")
|
||||
meta = "\n".join(
|
||||
[
|
||||
f"时效:{tms_eta_display(src)}",
|
||||
f"报价有效期:{tms_valid_until_display(src)}",
|
||||
"",
|
||||
collab_hint(work_order_no),
|
||||
@@ -1770,13 +1819,18 @@ def scheme_card(
|
||||
body,
|
||||
"价格明细",
|
||||
quote_area_fee_text(src, transport_mode=transport_mode),
|
||||
f"时效:{eta or '-'}",
|
||||
f"报价有效期:{valid}",
|
||||
"请确认是否采用该报价方案:",
|
||||
f"【{BTN_ADOPT_EXCEL_CARD}】",
|
||||
f"【{BTN_ADOPT_PDF_CARD}】",
|
||||
f"【{BTN_REJECT}】",
|
||||
]
|
||||
if not _is_air_surface(transport_mode=transport_mode):
|
||||
lines.append(f"时效:{eta or '-'}")
|
||||
lines.extend(
|
||||
[
|
||||
f"报价有效期:{valid}",
|
||||
"请确认是否采用该报价方案:",
|
||||
f"【{BTN_ADOPT_EXCEL_CARD}】",
|
||||
f"【{BTN_ADOPT_PDF_CARD}】",
|
||||
f"【{BTN_REJECT}】",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -1806,6 +1860,19 @@ def scheme_transport_mode(business_line: str = "", fallback: str = "空运") ->
|
||||
return (fallback or "空运").strip() or "空运"
|
||||
|
||||
|
||||
def _is_air_surface(*, business_line: str = "", transport_mode: str = "") -> bool:
|
||||
"""
|
||||
空运销售面:业务确认没有时效,展示层一律不写。
|
||||
|
||||
海运/陆运仍保留时效。两边都空时不算空运,避免误藏海运/陆运卡。
|
||||
"""
|
||||
raw_line = (business_line or "").strip()
|
||||
if raw_line and scheme_transport_mode(raw_line, fallback="") == "空运":
|
||||
return True
|
||||
mode = (transport_mode or "").strip()
|
||||
return bool(mode) and scheme_transport_mode(mode, fallback="") == "空运"
|
||||
|
||||
|
||||
def scheme_horizontal_rows(
|
||||
*,
|
||||
work_order_no: str,
|
||||
@@ -1857,7 +1924,6 @@ def scheme_horizontal_rows(
|
||||
{"keyname": "航线", "value": _clip(_air_scheme_route(q, src), 26)},
|
||||
{"keyname": "货物", "value": _clip(f"{cargo};{pieces};{weight};{volume}", 26)},
|
||||
{"keyname": "包装", "value": _clip(pack, 26)},
|
||||
{"keyname": "时效", "value": _clip(tms_eta_display(q), 26)},
|
||||
{"keyname": "报价有效期", "value": _clip(tms_valid_until_display(q), 26)},
|
||||
]
|
||||
|
||||
@@ -2044,19 +2110,22 @@ def deal_horizontal_rows(
|
||||
transport_mode: str = "",
|
||||
) -> list[dict[str, str]]:
|
||||
"""
|
||||
成交跟进卡字段:总销售价、时效、报价有效期。
|
||||
成交跟进卡字段:总销售价、报价有效期;海运/陆运再加时效。
|
||||
|
||||
空运业务确认没有时效,这里不写。
|
||||
不要历史报价 / 工单状态 / 来源。来源只留卡片顶部红字。
|
||||
"""
|
||||
_ = facts
|
||||
src = dict(quote or {})
|
||||
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
||||
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
|
||||
return [
|
||||
rows = [
|
||||
{"keyname": _total_label(src, business_line, transport_mode), "value": _clip(total, 26)},
|
||||
{"keyname": "时效", "value": _clip(tms_eta_display(src), 26)},
|
||||
{"keyname": "报价有效期", "value": _clip(tms_valid_until_display(src), 26)},
|
||||
]
|
||||
if not _is_air_surface(business_line=business_line, transport_mode=transport_mode):
|
||||
rows.append({"keyname": "时效", "value": _clip(tms_eta_display(src), 26)})
|
||||
rows.append({"keyname": "报价有效期", "value": _clip(tms_valid_until_display(src), 26)})
|
||||
return rows
|
||||
|
||||
|
||||
def deal_card(
|
||||
@@ -2098,7 +2167,7 @@ def deal_wecom_payload(
|
||||
成交跟进卡:对齐现网 button_interaction。
|
||||
|
||||
顶部来源红字;标题询价工单号;副标题跟进成交;三个按钮。
|
||||
字段只要总销售价、时效、有效期。
|
||||
字段:总销售价、报价有效期;海运/陆运再加时效。空运不写时效。
|
||||
"""
|
||||
src = dict(quote or {})
|
||||
source = str(src.get("source_label") or "TMS标准报价").replace(" ", "")
|
||||
@@ -2406,6 +2475,7 @@ def air_group_brief(
|
||||
空运群激活摘要:与海运进群同一套结构,字段用空运。
|
||||
|
||||
先 @ 销售和航线,再工单号 + 询价字段 + 协同补充(含电/含磁)+ TMS报价。
|
||||
TMS 段跟单聊同一套线路分项,总销售价只一行;禁止用「空运费=总价」冒充。
|
||||
不写「当前工单/询价要点/现在等到哪一步」。
|
||||
"""
|
||||
no = (work_order_no or "").strip()
|
||||
@@ -2433,6 +2503,10 @@ def air_group_brief(
|
||||
product_quote=product_quote,
|
||||
has_price=has_price,
|
||||
)
|
||||
if tms:
|
||||
from agent.schema.air_route_options import project_group_air_quote
|
||||
|
||||
tms = project_group_air_quote(tms, facts)
|
||||
lines.append("")
|
||||
lines.append("TMS报价:")
|
||||
if tms and (tms.get("total") or quote_fee_lines(tms)):
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
陆运文字询价确定性流程。
|
||||
|
||||
本文件职责:三项选项各自必填(不联动)→ 按类型补货物 → 无工单号核对卡 →(确定后)建单查 TMS。
|
||||
查价后:1 条完整线路直接出价格明细;多于 1 条发入口卡,H5 选定后再出该条明细。
|
||||
查价后半段复用海运拉群/报价单/成交,拉人按线路类别,禁止陆运岗全员。
|
||||
禁止:字段齐了就建单;直连 TMS;把海运「出卡即查价」改掉。
|
||||
禁止:字段齐了就建单;直连 TMS;把中转段当成另一条线路;把海运「出卡即查价」改掉。
|
||||
调用:handler 在 Worker/inbox 线程;禁止回调线程同步跑。
|
||||
"""
|
||||
|
||||
@@ -31,7 +32,7 @@ from agent.schema.tms_land_query import assemble_land_query
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_LAND_FRONT = frozenset({"need_mode", "need_land_options", "clarify", "wait_confirm", "idle", ""})
|
||||
_LAND_FRONT = frozenset({"need_mode", "need_land_options", "clarify", "wait_confirm", "wait_land_route", "idle", ""})
|
||||
|
||||
|
||||
class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
@@ -365,7 +366,11 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
reply: ReplyFn,
|
||||
retrying: bool,
|
||||
) -> str:
|
||||
"""查价回包落到有价/无价/技术失败。失败时把工单号记进待重查。"""
|
||||
"""
|
||||
查价回包落到有价/无价/技术失败。失败时把工单号记进待重查。
|
||||
|
||||
有价时按完整线路计条:1 条投影后出明细;多于 1 条进 H5。
|
||||
"""
|
||||
_ = retrying
|
||||
no = (sess.work_order_no or "").strip()
|
||||
if not tms.get("ok"):
|
||||
@@ -395,7 +400,17 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
self._save(sess)
|
||||
return "tms_miss"
|
||||
|
||||
quote = tms["quote"]
|
||||
quote = dict(tms["quote"] or {})
|
||||
from agent.schema.land_route_options import (
|
||||
group_land_routes,
|
||||
needs_route_pick,
|
||||
project_selected_route,
|
||||
)
|
||||
|
||||
raw_quote = dict(quote)
|
||||
routes = group_land_routes(raw_quote)
|
||||
if routes and not needs_route_pick(raw_quote):
|
||||
quote = project_selected_route(raw_quote, routes[0])
|
||||
up = self._ledger.upsert_quote(work_order_no=no, quote=quote, to_status="已报价")
|
||||
if not up.get("ok"):
|
||||
reply("主账写入报价失败,不能当作已报价。")
|
||||
@@ -403,8 +418,10 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
self._save(sess)
|
||||
return "ledger_fail"
|
||||
sess.quote = quote
|
||||
copy.remember_tms_quote(sess, quote)
|
||||
copy.remember_tms_quote(sess, raw_quote)
|
||||
sess.status = "已报价"
|
||||
if needs_route_pick(raw_quote):
|
||||
return self._offer_land_routes(sess=sess, work_order_no=no, quote=raw_quote, reply=reply)
|
||||
sess.phase = "wait_collab"
|
||||
sess.allowed = (copy.BTN_PULL_COLLAB, copy.BTN_SKIP_COLLAB)
|
||||
sess.wait_version += 1
|
||||
@@ -420,6 +437,110 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
)
|
||||
return "wait_collab"
|
||||
|
||||
def _offer_land_routes(
|
||||
self,
|
||||
*,
|
||||
sess: FlowSession,
|
||||
work_order_no: str,
|
||||
quote: dict[str, Any],
|
||||
reply: ReplyFn,
|
||||
) -> str:
|
||||
"""多条陆运线路:企微只发入口卡,选线在 H5。"""
|
||||
from agent.channel.h5.land_route_store import build_land_route_url, issue_land_route_token
|
||||
from agent.schema.land_route_options import group_land_routes
|
||||
|
||||
token = issue_land_route_token(
|
||||
sender_id=sess.sender_id,
|
||||
work_order_no=work_order_no,
|
||||
quote=quote,
|
||||
facts=dict(sess.facts),
|
||||
)
|
||||
url = build_land_route_url(token)
|
||||
count = len(group_land_routes(quote))
|
||||
sess.phase = "wait_land_route"
|
||||
sess.allowed = ()
|
||||
sess.wait_version += 1
|
||||
self._save(sess)
|
||||
reply(
|
||||
copy.air_route_entry_text(work_order_no=work_order_no, count=count),
|
||||
copy.air_route_entry_card(work_order_no=work_order_no, count=count, url=url),
|
||||
)
|
||||
return "wait_land_route"
|
||||
|
||||
def select_land_route(
|
||||
self,
|
||||
*,
|
||||
sender_id: str,
|
||||
index: int,
|
||||
reply: ReplyFn,
|
||||
) -> str:
|
||||
"""
|
||||
H5 选定一条陆运线路:只留该条各段,出价格明细。
|
||||
|
||||
已采用过则作废旧报价单后再出新卡。
|
||||
"""
|
||||
sess = self.session_of(sender_id)
|
||||
if sess is None or not (sess.work_order_no or "").strip():
|
||||
reply("线路选择已过期,请回企微重新询价。")
|
||||
return "route_expired"
|
||||
from agent.schema.land_route_options import group_land_routes, project_selected_route
|
||||
|
||||
raw_quote = dict(sess.tms_quote or sess.quote or {})
|
||||
routes = group_land_routes(raw_quote)
|
||||
if index < 0 or index >= len(routes):
|
||||
reply("没有这条线路,请重新打开页面选择。")
|
||||
return "route_invalid"
|
||||
route = routes[index]
|
||||
quote = project_selected_route(raw_quote, route)
|
||||
prev_phase = (sess.phase or "").strip()
|
||||
already_adopted = prev_phase in {"wait_deal", "wait_file", "wait_adopt"}
|
||||
if already_adopted:
|
||||
clearer = getattr(self._ledger, "void_quote_files", None)
|
||||
if callable(clearer):
|
||||
clearer(work_order_no=sess.work_order_no)
|
||||
from agent.redis_coord.quote_file import clear_quote_file_ready
|
||||
|
||||
clear_quote_file_ready(sess.work_order_no)
|
||||
self._ledger.upsert_quote(work_order_no=sess.work_order_no, quote=quote, to_status="已报价")
|
||||
sess.quote = quote
|
||||
sess.status = "已报价"
|
||||
sess.wait_version += 1
|
||||
if already_adopted:
|
||||
sess.phase = "wait_adopt"
|
||||
sess.allowed = (copy.BTN_ADOPT_EXCEL, copy.BTN_ADOPT_PDF, copy.BTN_REJECT)
|
||||
self._save(sess)
|
||||
mode = copy.scheme_transport_mode(sess.business_line)
|
||||
reply(
|
||||
copy.scheme_card(
|
||||
work_order_no=sess.work_order_no,
|
||||
status="已报价",
|
||||
facts=dict(sess.facts),
|
||||
quote=quote,
|
||||
transport_mode=mode,
|
||||
),
|
||||
copy.scheme_wecom_payload(
|
||||
work_order_no=sess.work_order_no,
|
||||
facts=dict(sess.facts),
|
||||
quote=quote,
|
||||
status="已报价",
|
||||
transport_mode=mode,
|
||||
),
|
||||
)
|
||||
return "wait_adopt"
|
||||
sess.phase = "wait_collab"
|
||||
sess.allowed = (copy.BTN_PULL_COLLAB, copy.BTN_SKIP_COLLAB)
|
||||
self._save(sess)
|
||||
self._emit_sea_hit(
|
||||
reply,
|
||||
work_order_no=sess.work_order_no,
|
||||
quote=quote,
|
||||
first_or_same=sess.first_or_same,
|
||||
history_work_order_no=sess.history_work_order_no,
|
||||
status="已报价",
|
||||
business_line="LAND",
|
||||
)
|
||||
return "wait_collab"
|
||||
|
||||
def _land_ready(self, facts: dict[str, str]) -> dict[str, Any]:
|
||||
"""选项+货物是否可出核对卡。"""
|
||||
opt = validate_land_options(facts)
|
||||
|
||||
Reference in New Issue
Block a user