落地销售调价:按自然语言改价并单独记销售价,拉群和后台不再把调后价当成 TMS 原价。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -180,7 +180,8 @@ def send_air_brief_and_sheet(
|
||||
tms = dict(tms_quote) if tms_quote is not None else {}
|
||||
manual = dict(product_quote or {})
|
||||
if tms_quote is None:
|
||||
if view_q and not copy.quote_is_product(view_q):
|
||||
tms = copy.resolve_tms_quote(None, view)
|
||||
if not tms and view_q and copy.quote_is_tms(view_q):
|
||||
tms = view_q
|
||||
if not manual and copy.quote_is_product(override or view_q):
|
||||
manual = override or view_q
|
||||
@@ -389,6 +390,10 @@ def apply_air_quote(
|
||||
_park_air_pending(flow, no, quote, sender_id)
|
||||
send_group_text(client, chat_id, copy.group_ask_tms_or_product())
|
||||
return "wait_tms_choice"
|
||||
from agent.schema.quote_adjust import quote_fee_rows
|
||||
|
||||
if quote_fee_rows(current):
|
||||
quote = merge_quote_fees(current, quote)
|
||||
return _commit_quote(flow, ticket, quote, chat_id, sender_id)
|
||||
|
||||
|
||||
@@ -807,14 +812,11 @@ def _clear_air_pending(flow: Any, work_order_no: str, sender_id: str) -> None:
|
||||
|
||||
|
||||
def _has_tms_quote(quote: dict[str, Any]) -> bool:
|
||||
"""当前价来自 TMS 才要问;已经是航线/产品价不再替用户选。"""
|
||||
"""当前价来自 TMS 才要问;航线/产品/销售调价不再替用户选。"""
|
||||
src = dict(quote or {})
|
||||
label = str(src.get("source_label") or src.get("source") or "")
|
||||
if "航线" in label or "产品" in label:
|
||||
return False
|
||||
if not (src.get("total") or src.get("quoteId") or src.get("quote_id")):
|
||||
return False
|
||||
return "TMS" in label.upper() or "标准报价" in label or bool(src.get("quoteId") or src.get("quote_id"))
|
||||
return copy.quote_is_tms(src)
|
||||
|
||||
|
||||
def _is_product_quote(quote: dict[str, Any]) -> bool:
|
||||
|
||||
@@ -42,6 +42,7 @@ class FlowSession:
|
||||
first_or_same: str = "first"
|
||||
history_work_order_no: str = ""
|
||||
quote: dict[str, Any] = field(default_factory=dict)
|
||||
tms_quote: dict[str, Any] = field(default_factory=dict)
|
||||
status: str = ""
|
||||
allowed: tuple[str, ...] = ()
|
||||
immutable_text: str = ""
|
||||
@@ -78,6 +79,7 @@ def _session_from_payload(raw: dict[str, Any]) -> FlowSession:
|
||||
first_or_same=str(raw.get("first_or_same") or "first"),
|
||||
history_work_order_no=str(raw.get("history_work_order_no") or ""),
|
||||
quote=dict(raw.get("quote") or {}),
|
||||
tms_quote=dict(raw.get("tms_quote") or raw.get("tmsQuote") or {}),
|
||||
status=str(raw.get("status") or ""),
|
||||
allowed=tuple(str(x) for x in (raw.get("allowed") or ())),
|
||||
immutable_text=str(raw.get("immutable_text") or ""),
|
||||
@@ -480,6 +482,12 @@ class AirTextInquiryFlow:
|
||||
and not (injected_facts and injected_mode)
|
||||
):
|
||||
return self._capture_lost_reason(sess, text, reply)
|
||||
if sess:
|
||||
from agent.policy.quote_adjust_ops import try_private_adjust
|
||||
|
||||
adjusted = try_private_adjust(self, sess, text, reply)
|
||||
if adjusted:
|
||||
return adjusted
|
||||
|
||||
snap = extract_inquiry_snapshot(
|
||||
text,
|
||||
@@ -661,6 +669,7 @@ class AirTextInquiryFlow:
|
||||
self._save(sess)
|
||||
return "ledger_fail"
|
||||
sess.quote = quote
|
||||
copy.remember_tms_quote(sess, quote)
|
||||
sess.status = "已报价"
|
||||
sess.phase = "wait_collab"
|
||||
sess.allowed = (copy.BTN_SKIP_COLLAB,)
|
||||
@@ -966,6 +975,7 @@ class AirTextInquiryFlow:
|
||||
work_order_no=sess.work_order_no,
|
||||
quote=sess.quote,
|
||||
facts=sess.facts,
|
||||
business_line=sess.business_line or "AIR",
|
||||
).get("template_card") or {}
|
||||
reply(
|
||||
"\n".join(
|
||||
@@ -978,6 +988,7 @@ class AirTextInquiryFlow:
|
||||
status=sess.status or "已报价",
|
||||
quote=sess.quote,
|
||||
facts=sess.facts,
|
||||
business_line=sess.business_line or "AIR",
|
||||
),
|
||||
]
|
||||
),
|
||||
@@ -1003,11 +1014,13 @@ class AirTextInquiryFlow:
|
||||
status=sess.status or "已报价",
|
||||
quote=sess.quote,
|
||||
facts=sess.facts,
|
||||
business_line=sess.business_line or "AIR",
|
||||
),
|
||||
copy.deal_wecom_payload(
|
||||
work_order_no=sess.work_order_no,
|
||||
quote=sess.quote,
|
||||
facts=sess.facts,
|
||||
business_line=sess.business_line or "AIR",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from agent.schema.field_validate import (
|
||||
display_name,
|
||||
)
|
||||
from agent.schema.land_options import option_prompt as _land_option_prompt
|
||||
from agent.schema.quote_adjust import total_price_label
|
||||
|
||||
# 空运补问:与合同 8 项一致。报价日期默认当天,不进待补充。
|
||||
_AIR_EXAMPLES = {
|
||||
@@ -671,6 +672,38 @@ def money_text(amount: Any, currency: str = "") -> str:
|
||||
return f"{code} {raw}"
|
||||
|
||||
|
||||
def _total_label(
|
||||
quote: dict[str, Any] | None = None,
|
||||
business_line: str = "",
|
||||
transport_mode: str = "",
|
||||
) -> str:
|
||||
"""空运总销售价;海运/陆运看这版报价是成本还是销售调价。"""
|
||||
src = dict(quote or {})
|
||||
return total_price_label(
|
||||
business_line or str(src.get("business_line") or ""),
|
||||
transport_mode,
|
||||
quote=src,
|
||||
)
|
||||
|
||||
|
||||
def _manual_quote_lines(
|
||||
manual: dict[str, Any],
|
||||
*,
|
||||
business_line: str = "",
|
||||
transport_mode: str = "",
|
||||
) -> list[str]:
|
||||
"""人工报价段:费用行 + 总销售价,不写合计。"""
|
||||
src = dict(manual or {})
|
||||
lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)]
|
||||
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
||||
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
|
||||
if total and total != "-":
|
||||
lines.append(f"{_total_label(src, business_line, transport_mode)}:{total}")
|
||||
elif not lines:
|
||||
lines.append(f"{_total_label(src, business_line, transport_mode)}:{total or '-'}")
|
||||
return lines
|
||||
|
||||
|
||||
def quote_fee_lines(quote: dict[str, Any] | None) -> list[tuple[str, str]]:
|
||||
"""
|
||||
价格明细行:优先 TMS fee_lines,否则用运费/其它/合计兜底。
|
||||
@@ -707,9 +740,13 @@ def quote_fee_lines(quote: dict[str, Any] | None) -> list[tuple[str, str]]:
|
||||
return lines
|
||||
|
||||
|
||||
def quote_area_fee_text(quote: dict[str, Any] | None) -> str:
|
||||
def quote_area_fee_text(
|
||||
quote: dict[str, Any] | None,
|
||||
business_line: str = "",
|
||||
transport_mode: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
卡片引用条正文:费用行 + 总销售价合计。
|
||||
卡片引用条正文:费用行 + 总销售价。
|
||||
|
||||
必须走 quote_text。塞进 quote_area.title 会被企微整段藏掉。
|
||||
"""
|
||||
@@ -717,7 +754,7 @@ def quote_area_fee_text(quote: dict[str, Any] | None) -> str:
|
||||
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
||||
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
|
||||
fee_lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)]
|
||||
fee_lines.append(f"总销售价合计:{total}")
|
||||
fee_lines.append(f"{_total_label(src, business_line, transport_mode)}:{total}")
|
||||
return "\n".join(fee_lines)
|
||||
|
||||
|
||||
@@ -725,22 +762,27 @@ def quote_area_fee_text(quote: dict[str, Any] | None) -> str:
|
||||
_QUOTE_AREA_SAFE_CHARS = 80
|
||||
|
||||
|
||||
def quote_area_safe_text(quote: dict[str, Any] | None) -> str:
|
||||
def quote_area_safe_text(
|
||||
quote: dict[str, Any] | None,
|
||||
business_line: str = "",
|
||||
transport_mode: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
卡片引用条:短报价原样;超长只留合计,避免半截费用名。
|
||||
卡片引用条:短报价原样;超长只留总价名称,避免半截费用名。
|
||||
|
||||
完整明细由 sea_quote_detail_text 另发一条文本。空运卡仍用 quote_area_fee_text。
|
||||
"""
|
||||
full = quote_area_fee_text(quote)
|
||||
full = quote_area_fee_text(quote, business_line=business_line, transport_mode=transport_mode)
|
||||
if len(full) <= _QUOTE_AREA_SAFE_CHARS:
|
||||
return full
|
||||
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)
|
||||
n = len(quote_fee_lines(src))
|
||||
label = _total_label(src, business_line, transport_mode)
|
||||
if n:
|
||||
return f"总销售价合计:{total}\n共{n}项费用,详见上一条明细"
|
||||
return f"总销售价合计:{total}"
|
||||
return f"{label}:{total}\n共{n}项费用,详见上一条明细"
|
||||
return f"{label}:{total}"
|
||||
|
||||
|
||||
def sea_quote_detail_text(
|
||||
@@ -750,9 +792,10 @@ def sea_quote_detail_text(
|
||||
first_or_same: str = "first",
|
||||
history_work_order_no: str = "",
|
||||
status: str = "已报价",
|
||||
business_line: str = "SEA",
|
||||
) -> str:
|
||||
"""
|
||||
海运完整价格明细文本:来源、工单号、费用行、合计。
|
||||
海运/陆运完整价格明细文本:来源、工单号、费用行、总销售价。
|
||||
|
||||
时效、报价有效期只在卡片上出现,这里不写,避免和卡重复。
|
||||
不受卡片引用条字数限制。first_or_same 只兼容调用方。
|
||||
@@ -761,7 +804,11 @@ def sea_quote_detail_text(
|
||||
src = dict(quote or {})
|
||||
source = str(src.get("source_label") or "TMS标准报价").replace(" ", "")
|
||||
return "\n".join(
|
||||
[source, f"{work_order_no}价格明细", quote_area_fee_text(src)]
|
||||
[
|
||||
source,
|
||||
f"{work_order_no}价格明细",
|
||||
quote_area_fee_text(src, business_line=business_line or "SEA"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -824,9 +871,11 @@ def quote_detail_lines(
|
||||
first_or_same: str = "first",
|
||||
history_work_order_no: str = "",
|
||||
status: str = "已报价",
|
||||
business_line: str = "",
|
||||
transport_mode: str = "",
|
||||
) -> list[str]:
|
||||
"""
|
||||
价格明细正文:TMS 费用、合计、时效、有效期。
|
||||
价格明细正文:TMS 费用、总销售价、时效、有效期。
|
||||
|
||||
给文本兜底和企微引用条共用,禁止在这里编时效/有效期。
|
||||
价格卡不展示工单状态,也不展示相同询价内容检索。
|
||||
@@ -836,7 +885,7 @@ def quote_detail_lines(
|
||||
header_ccy = str(src.get("currency") or src.get("currencyCode") or "").strip()
|
||||
total = money_text(str(src.get("total") or "-").strip() or "-", header_ccy)
|
||||
lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)]
|
||||
lines.append(f"总销售价合计:{total}")
|
||||
lines.append(f"{_total_label(src, business_line, transport_mode)}:{total}")
|
||||
lines.append(f"时效:{tms_eta_display(src)}")
|
||||
lines.append(f"报价有效期:{tms_valid_until_display(src)}")
|
||||
return lines
|
||||
@@ -864,6 +913,7 @@ def tms_hit_card(
|
||||
first_or_same=first_or_same,
|
||||
history_work_order_no=history_work_order_no,
|
||||
status=status,
|
||||
business_line="AIR",
|
||||
)
|
||||
)
|
||||
lines.append("")
|
||||
@@ -884,7 +934,7 @@ def tms_hit_wecom_payload(
|
||||
一张价格卡,销售侧顺序:
|
||||
|
||||
价格明细
|
||||
┌ 引用条:空运费 / 其它费用 / 总销售价合计
|
||||
┌ 引用条:空运费 / 其它费用 / 总销售价
|
||||
时效、报价有效期
|
||||
协同说明(普通文字,贴按钮上方)
|
||||
按钮
|
||||
@@ -912,7 +962,7 @@ def tms_hit_wecom_payload(
|
||||
"main_title": {"title": f"{work_order_no}价格明细"},
|
||||
"quote_area": {
|
||||
"type": 0,
|
||||
"quote_text": quote_area_fee_text(src),
|
||||
"quote_text": quote_area_fee_text(src, business_line="AIR"),
|
||||
},
|
||||
"sub_title_text": meta,
|
||||
"task_id": card_task_id("quote", work_order_no),
|
||||
@@ -1047,8 +1097,9 @@ def sea_tms_hit_card(
|
||||
first_or_same: str = "first",
|
||||
history_work_order_no: str = "",
|
||||
status: str = "已报价",
|
||||
business_line: str = "SEA",
|
||||
) -> str:
|
||||
"""海运有价:价格明细 + 拉群问句 + 两个按钮。空运卡片函数保持不动。"""
|
||||
"""海运/陆运有价:价格明细 + 拉群问句 + 两个按钮。空运卡片函数保持不动。"""
|
||||
src = dict(quote or {})
|
||||
source = str(src.get("source_label") or "TMS标准报价").replace(" ", "")
|
||||
lines = [source, f"{work_order_no}价格明细"]
|
||||
@@ -1058,6 +1109,7 @@ def sea_tms_hit_card(
|
||||
first_or_same=first_or_same,
|
||||
history_work_order_no=history_work_order_no,
|
||||
status=status,
|
||||
business_line=business_line or "SEA",
|
||||
)
|
||||
)
|
||||
lines.append("")
|
||||
@@ -1074,8 +1126,9 @@ def sea_tms_hit_wecom_payload(
|
||||
first_or_same: str = "first",
|
||||
history_work_order_no: str = "",
|
||||
status: str = "已报价",
|
||||
business_line: str = "SEA",
|
||||
) -> dict[str, Any]:
|
||||
"""海运有价企微卡:价格明细 + 问句 + 拉群 / 跳过协同。"""
|
||||
"""海运/陆运有价企微卡:价格明细 + 问句 + 拉群 / 跳过协同。"""
|
||||
_ = first_or_same, history_work_order_no, status
|
||||
src = dict(quote or {})
|
||||
source = str(src.get("source_label") or "TMS标准报价").replace(" ", "")
|
||||
@@ -1095,7 +1148,7 @@ def sea_tms_hit_wecom_payload(
|
||||
"main_title": {"title": f"{work_order_no}价格明细"},
|
||||
"quote_area": {
|
||||
"type": 0,
|
||||
"quote_text": quote_area_safe_text(src),
|
||||
"quote_text": quote_area_safe_text(src, business_line=business_line or "SEA"),
|
||||
},
|
||||
"sub_title_text": meta,
|
||||
"task_id": card_task_id("quote", work_order_no),
|
||||
@@ -1217,10 +1270,84 @@ def sea_group_create_fail(*, reason: str = "") -> str:
|
||||
|
||||
def quote_is_product(quote: dict[str, Any] | None) -> bool:
|
||||
"""来源是产品/航线人工,不能写进 TMS 报价段。"""
|
||||
label = str((quote or {}).get("source_label") or (quote or {}).get("source") or "")
|
||||
src = dict(quote or {})
|
||||
code = str(src.get("source") or "").strip().upper()
|
||||
if code == "PRODUCT":
|
||||
return True
|
||||
if code in {"TMS", "SALES_ADJUST"}:
|
||||
return False
|
||||
label = str(src.get("source_label") or src.get("sourceLabel") or src.get("source") or "")
|
||||
return "产品" in label or "航线" in label
|
||||
|
||||
|
||||
def quote_is_sales_adjust(quote: dict[str, Any] | None) -> bool:
|
||||
"""销售调过价的这一版,禁止再写进 TMS 报价段。"""
|
||||
src = dict(quote or {})
|
||||
code = str(src.get("source") or "").strip().upper()
|
||||
if code == "SALES_ADJUST":
|
||||
return True
|
||||
if code in {"TMS", "PRODUCT"}:
|
||||
return False
|
||||
label = str(src.get("source_label") or src.get("sourceLabel") or "").replace(" ", "")
|
||||
return "销售调价" in label
|
||||
|
||||
|
||||
def quote_is_tms(quote: dict[str, Any] | None) -> bool:
|
||||
"""
|
||||
只有 TMS 原价能进 TMS 段。
|
||||
|
||||
产品报价、销售调价即使金额还在,也不能冒充 TMS。
|
||||
"""
|
||||
src = dict(quote or {})
|
||||
if not src:
|
||||
return False
|
||||
if quote_is_product(src) or quote_is_sales_adjust(src):
|
||||
return False
|
||||
code = str(src.get("source") or "").strip().upper()
|
||||
if code == "TMS":
|
||||
return True
|
||||
label = str(src.get("source_label") or src.get("sourceLabel") or src.get("source") or "")
|
||||
return "TMS" in label.upper() or "标准报价" in label or not label.strip()
|
||||
|
||||
|
||||
def remember_tms_quote(sess: Any, quote: dict[str, Any] | None) -> None:
|
||||
"""
|
||||
书签里单独记住 TMS 原价。调价只改 sess.quote,不准覆盖这一份。
|
||||
|
||||
副作用:写 sess.tms_quote。调用方在 TMS 命中或调价前调用。
|
||||
"""
|
||||
if sess is None:
|
||||
return
|
||||
src = dict(quote or {})
|
||||
if not quote_is_tms(src):
|
||||
return
|
||||
sess.tms_quote = dict(src)
|
||||
|
||||
|
||||
def resolve_tms_quote(sess: Any = None, ticket: Any = None) -> dict[str, Any]:
|
||||
"""
|
||||
拉群/群摘要用的 TMS 原价:书签 tms_quote → 主账 tms_quote → 当前价若仍是 TMS。
|
||||
|
||||
调后最新一条是销售调价,不能拿来当 TMS。
|
||||
"""
|
||||
candidates: list[dict[str, Any]] = []
|
||||
if sess is not None:
|
||||
candidates.append(dict(getattr(sess, "tms_quote", None) or {}))
|
||||
view = dict(ticket or {}) if isinstance(ticket, dict) else {}
|
||||
if view:
|
||||
candidates.append(dict(view.get("tms_quote") or view.get("tmsQuote") or {}))
|
||||
elif ticket is not None:
|
||||
candidates.append(dict(getattr(ticket, "tms_quote", None) or {}))
|
||||
if sess is not None:
|
||||
candidates.append(dict(getattr(sess, "quote", None) or {}))
|
||||
if view:
|
||||
candidates.append(dict(view.get("quote") or {}))
|
||||
for item in candidates:
|
||||
if quote_is_tms(item) and (item.get("total") or item.get("fee_rows") or item.get("fee_lines")):
|
||||
return item
|
||||
return {}
|
||||
|
||||
|
||||
def sea_group_brief(
|
||||
*,
|
||||
work_order_no: str,
|
||||
@@ -1237,8 +1364,8 @@ def sea_group_brief(
|
||||
"""
|
||||
群里自动摘要:询价 + 协同 + TMS 报价 + 人工报价。
|
||||
|
||||
TMS 无价只写「TMS 暂无匹配报价」,产品报的价单独放人工报价,不能混进 TMS。
|
||||
陆运传入 transport_mode=陆运,始发站/货物字段跟核对卡同一套。
|
||||
TMS 无价只写「TMS 暂无匹配报价」,产品报的价单独放人工报价,销售调价另起一段。
|
||||
禁止把调后价写进 TMS 段。陆运传入 transport_mode=陆运,始发站/货物字段跟核对卡同一套。
|
||||
"""
|
||||
no = (work_order_no or "").strip()
|
||||
sales = (sales_name or "").strip() or "销售"
|
||||
@@ -1258,33 +1385,60 @@ def sea_group_brief(
|
||||
label = collab_display_name(key, line)
|
||||
val = (collab.get(key) or collab.get(label) or "").strip() or "-"
|
||||
lines.append(f"{label}:{val}")
|
||||
tms = dict(tms_quote) if tms_quote is not None else {}
|
||||
manual = dict(product_quote or {})
|
||||
if tms_quote is None:
|
||||
src = dict(quote or {})
|
||||
if has_price and src and not quote_is_product(src):
|
||||
tms = src
|
||||
elif quote_is_product(src):
|
||||
manual = manual or src
|
||||
tms, manual, adjust = _split_brief_quotes(
|
||||
quote=quote,
|
||||
tms_quote=tms_quote,
|
||||
product_quote=product_quote,
|
||||
has_price=has_price,
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("TMS报价:")
|
||||
if tms and (tms.get("total") or quote_fee_lines(tms)):
|
||||
lines.extend(quote_detail_lines(tms))
|
||||
lines.extend(quote_detail_lines(tms, transport_mode=mode))
|
||||
lines.append(group_tms_confirm_hint())
|
||||
else:
|
||||
lines.append("TMS 暂无匹配报价")
|
||||
if manual and (quote_fee_lines(manual) or str(manual.get("total") or "").strip() not in {"", "-"}):
|
||||
lines.append("")
|
||||
lines.append("人工报价:")
|
||||
fee_lines = quote_fee_lines(manual)
|
||||
if fee_lines:
|
||||
lines.extend(f"{name}:{amount}" for name, amount in fee_lines)
|
||||
else:
|
||||
header_ccy = str(manual.get("currency") or "").strip()
|
||||
lines.append(f"合计:{money_text(str(manual.get('total') or '-'), header_ccy)}")
|
||||
lines.extend(_manual_quote_lines(manual, transport_mode=mode))
|
||||
if adjust and (quote_fee_lines(adjust) or str(adjust.get("total") or "").strip() not in {"", "-"}):
|
||||
lines.append("")
|
||||
lines.append("销售调价:")
|
||||
lines.extend(_manual_quote_lines(adjust, transport_mode=mode))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _split_brief_quotes(
|
||||
*,
|
||||
quote: dict[str, Any] | None,
|
||||
tms_quote: dict[str, Any] | None,
|
||||
product_quote: dict[str, Any] | None,
|
||||
has_price: bool,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
||||
"""
|
||||
群摘要三段价:TMS 原价 / 人工 / 销售调价。调后价不准掉进 TMS。
|
||||
|
||||
tms_quote 显式传入(含空字典)时不再用当前 quote 兜底。
|
||||
"""
|
||||
src = dict(quote or {})
|
||||
tms = dict(tms_quote) if tms_quote is not None else {}
|
||||
manual = dict(product_quote or {})
|
||||
if tms_quote is None:
|
||||
if has_price and src and quote_is_tms(src):
|
||||
tms = src
|
||||
elif quote_is_product(src):
|
||||
manual = manual or src
|
||||
if quote_is_sales_adjust(tms) or quote_is_product(tms):
|
||||
tms = {}
|
||||
if quote_is_sales_adjust(manual):
|
||||
manual = {}
|
||||
adjust = src if quote_is_sales_adjust(src) else {}
|
||||
if quote_is_sales_adjust(product_quote) and not adjust:
|
||||
adjust = dict(product_quote or {})
|
||||
return tms, manual, adjust
|
||||
|
||||
|
||||
def scheme_card(
|
||||
*,
|
||||
work_order_no: str,
|
||||
@@ -1314,7 +1468,7 @@ def scheme_card(
|
||||
f"【{work_order_no}报价方案】",
|
||||
body,
|
||||
"价格明细",
|
||||
quote_area_fee_text(src),
|
||||
quote_area_fee_text(src, transport_mode=transport_mode),
|
||||
f"时效:{eta or '-'}",
|
||||
f"报价有效期:{valid}",
|
||||
"请确认是否采用该报价方案:",
|
||||
@@ -1455,9 +1609,9 @@ def scheme_wecom_payload(
|
||||
"quote_area": {
|
||||
"type": 0,
|
||||
"quote_text": (
|
||||
quote_area_safe_text(src)
|
||||
if (transport_mode or "").strip() == "海运"
|
||||
else quote_area_fee_text(src)
|
||||
quote_area_safe_text(src, transport_mode=transport_mode)
|
||||
if (transport_mode or "").strip() in {"海运", "陆运"}
|
||||
else quote_area_fee_text(src, transport_mode=transport_mode)
|
||||
),
|
||||
},
|
||||
"horizontal_content_list": scheme_horizontal_rows(
|
||||
@@ -1489,7 +1643,68 @@ def scheme_wecom_payload(
|
||||
|
||||
|
||||
def reject_ack() -> str:
|
||||
return "请问如何调整报价单,以满足您的需求?您可使用「报价加 10%」等调价指令。"
|
||||
return "请问如何调整报价单,以满足您的需求?请说明要调哪一项费用,例如「提货费加 10%」。"
|
||||
|
||||
|
||||
def adjust_confirm(applied: list[dict[str, Any]]) -> str:
|
||||
"""识别成功确认。applied 每项含 name、how。"""
|
||||
bits = [f"{x.get('name')}在当前报价基础上{x.get('how')}" for x in applied if x.get("name")]
|
||||
body = ";".join(bits) or "已识别调价指令"
|
||||
return f"已识别调价指令:{body}。正在刷新报价"
|
||||
|
||||
|
||||
def adjust_ledger_remark(applied: list[dict[str, Any]]) -> str:
|
||||
"""
|
||||
主账状态流转备注:每次销售调价一条,记下当时改了哪几项。
|
||||
|
||||
后台「状态流转与备注」原样展示;不含利润点。
|
||||
"""
|
||||
bits = [f"{x.get('name')}{x.get('how')}" for x in applied if x.get("name")]
|
||||
return "销售调价:" + (";".join(bits) if bits else "已改价")
|
||||
|
||||
|
||||
def adjust_need_fee() -> str:
|
||||
return "请说明要调哪一项费用明细,例如「提货费加 10%」。只说报价或总价,系统不会改价。"
|
||||
|
||||
|
||||
def adjust_need_lines() -> str:
|
||||
return "请分别对明细价格进行调价,系统不分摊合计。"
|
||||
|
||||
|
||||
def adjust_currency_hint() -> str:
|
||||
return "请分别对不同币种进行调价,系统不做汇率换算。"
|
||||
|
||||
|
||||
def adjust_unmatched(missing: list[str], available: list[str]) -> str:
|
||||
miss = "、".join(x for x in missing if x) or "这些费用"
|
||||
have = "、".join(available) if available else "(当前没有费用明细)"
|
||||
return f"卡上没有{miss}。当前能调的费用:{have}"
|
||||
|
||||
|
||||
def adjust_ambiguous(name: str, hits: list[str]) -> str:
|
||||
return f"「{name}」对上了多项({'、'.join(hits)}),请说明要调哪一项。"
|
||||
|
||||
|
||||
def adjust_ineligible(reason: str) -> str:
|
||||
if reason == "already_collab":
|
||||
return "这张单已经拉群协同,请到群里发送调价指令。"
|
||||
if reason == "closed":
|
||||
return "这张单已经结案,不能再按这套调价。"
|
||||
return "当前工单还没有可调的报价,不能按这套调价。"
|
||||
|
||||
|
||||
def adjust_only_sales() -> str:
|
||||
return "只有这张单的销售能调价。航线/产品请继续报价,价格未改。"
|
||||
|
||||
|
||||
def adjust_group_body(quote: dict[str, Any] | None, business_line: str = "") -> str:
|
||||
"""群里调后全量明细,利润点不写给销售。"""
|
||||
src = dict(quote or {})
|
||||
lines = [f"{name}:{amount}" for name, amount in quote_fee_lines(src)]
|
||||
header = str(src.get("currency") or "")
|
||||
total = money_text(str(src.get("total") or "-").strip() or "-", header)
|
||||
lines.append(f"{_total_label(src, business_line)}:{total}")
|
||||
return "调价后价格:\n" + "\n".join(lines)
|
||||
|
||||
|
||||
def adopt_excel_ack() -> str:
|
||||
@@ -1520,17 +1735,20 @@ def deal_horizontal_rows(
|
||||
*,
|
||||
quote: dict[str, Any] | None,
|
||||
facts: dict[str, str] | None = None,
|
||||
business_line: str = "",
|
||||
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 [
|
||||
{"keyname": "总销售价合计", "value": _clip(total, 26)},
|
||||
{"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)},
|
||||
]
|
||||
@@ -1543,6 +1761,7 @@ def deal_card(
|
||||
summary: str = "",
|
||||
quote: dict[str, Any] | None = None,
|
||||
facts: dict[str, str] | None = None,
|
||||
business_line: str = "",
|
||||
) -> str:
|
||||
"""
|
||||
成交跟进文字兜底(模板卡发失败时仍给销售看)。
|
||||
@@ -1551,7 +1770,7 @@ def deal_card(
|
||||
"""
|
||||
_ = status, summary
|
||||
no = (work_order_no or "").strip()
|
||||
rows = deal_horizontal_rows(quote=quote, facts=facts)
|
||||
rows = deal_horizontal_rows(quote=quote, facts=facts, business_line=business_line)
|
||||
lines = [
|
||||
f"询价工单 {no}",
|
||||
DEAL_SUBTITLE,
|
||||
@@ -1568,6 +1787,7 @@ def deal_wecom_payload(
|
||||
quote: dict[str, Any] | None = None,
|
||||
facts: dict[str, str] | None = None,
|
||||
card_version: int = 0,
|
||||
business_line: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
成交跟进卡:对齐现网 button_interaction。
|
||||
@@ -1590,7 +1810,9 @@ def deal_wecom_payload(
|
||||
"desc": DEAL_SUBTITLE,
|
||||
},
|
||||
"sub_title_text": DEAL_HINT,
|
||||
"horizontal_content_list": deal_horizontal_rows(quote=src, facts=facts),
|
||||
"horizontal_content_list": deal_horizontal_rows(
|
||||
quote=src, facts=facts, business_line=business_line
|
||||
),
|
||||
"task_id": card_task_id("deal", no),
|
||||
"button_list": [
|
||||
{
|
||||
@@ -1900,30 +2122,27 @@ def air_group_brief(
|
||||
for key in AIR_COLLAB_FIELDS:
|
||||
val = (collab.get(key) or "").strip() or "-"
|
||||
lines.append(f"{key}:{val}")
|
||||
tms = dict(tms_quote) if tms_quote is not None else {}
|
||||
manual = dict(product_quote or {})
|
||||
if tms_quote is None:
|
||||
src = dict(quote or {})
|
||||
if has_price and src and not quote_is_product(src):
|
||||
tms = src
|
||||
elif quote_is_product(src):
|
||||
manual = manual or src
|
||||
tms, manual, adjust = _split_brief_quotes(
|
||||
quote=quote,
|
||||
tms_quote=tms_quote,
|
||||
product_quote=product_quote,
|
||||
has_price=has_price,
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("TMS报价:")
|
||||
if tms and (tms.get("total") or quote_fee_lines(tms)):
|
||||
lines.extend(quote_detail_lines(tms))
|
||||
lines.extend(quote_detail_lines(tms, business_line="AIR"))
|
||||
lines.append(group_tms_confirm_hint())
|
||||
else:
|
||||
lines.append("TMS 暂无匹配报价")
|
||||
if manual and (quote_fee_lines(manual) or str(manual.get("total") or "").strip() not in {"", "-"}):
|
||||
lines.append("")
|
||||
lines.append("人工报价:")
|
||||
fee_lines = quote_fee_lines(manual)
|
||||
if fee_lines:
|
||||
lines.extend(f"{name}:{amount}" for name, amount in fee_lines)
|
||||
else:
|
||||
header_ccy = str(manual.get("currency") or "").strip()
|
||||
lines.append(f"合计:{money_text(str(manual.get('total') or '-'), header_ccy)}")
|
||||
lines.extend(_manual_quote_lines(manual, business_line="AIR"))
|
||||
if adjust and (quote_fee_lines(adjust) or str(adjust.get("total") or "").strip() not in {"", "-"}):
|
||||
lines.append("")
|
||||
lines.append("销售调价:")
|
||||
lines.extend(_manual_quote_lines(adjust, business_line="AIR"))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -1945,7 +2164,7 @@ def air_activate_summary(
|
||||
quote=src,
|
||||
collab_facts=collab_facts,
|
||||
has_price=bool(src.get("total") or src.get("quoteId")),
|
||||
tms_quote=src if src and not quote_is_product(src) else {},
|
||||
tms_quote=src if quote_is_tms(src) else {},
|
||||
product_quote=src if quote_is_product(src) else {},
|
||||
)
|
||||
|
||||
|
||||
@@ -98,6 +98,12 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
and not (injected_facts and injected_mode)
|
||||
):
|
||||
return self._capture_lost_reason(sess, text, reply)
|
||||
if sess:
|
||||
from agent.policy.quote_adjust_ops import try_private_adjust
|
||||
|
||||
adjusted = try_private_adjust(self, sess, text, reply)
|
||||
if adjusted:
|
||||
return adjusted
|
||||
if sess and phase not in _LAND_FRONT:
|
||||
# 查价后半段书签还在时,销售又发新陆运:只走陆运前半段。
|
||||
# 禁止再 super 去海运 on_text,海运会把 LAND 转回来,递归到死、一句都不回。
|
||||
@@ -362,6 +368,7 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
self._save(sess)
|
||||
return "ledger_fail"
|
||||
sess.quote = quote
|
||||
copy.remember_tms_quote(sess, quote)
|
||||
sess.status = "已报价"
|
||||
sess.phase = "wait_collab"
|
||||
sess.allowed = (copy.BTN_PULL_COLLAB, copy.BTN_SKIP_COLLAB)
|
||||
@@ -374,6 +381,7 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
first_or_same=sess.first_or_same,
|
||||
history_work_order_no=sess.history_work_order_no,
|
||||
status="已报价",
|
||||
business_line=sess.business_line or "LAND",
|
||||
)
|
||||
return "wait_collab"
|
||||
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
"""
|
||||
调价执行:私聊出新卡,群里出全量文本 + Excel + 成交跟进。
|
||||
|
||||
本文件职责:资格、抽项、落主账、出站。算价只调用 schema.quote_adjust。
|
||||
禁止:回调线程同步跑;模型改价;群里出 PDF。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.schema.quote_adjust import (
|
||||
apply_adjust,
|
||||
extract_adjust,
|
||||
private_session_can_adjust,
|
||||
quote_fee_rows,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_ADJUST_PHASES = frozenset({"wait_collab", "wait_adopt", "wait_adjust", "wait_deal"})
|
||||
|
||||
|
||||
def looks_like_adjust(text: str) -> bool:
|
||||
"""本地兜底:正式认调价走 DeepSeek A,这里只给无网单测。"""
|
||||
return extract_adjust(text, allow_model=False).is_adjust
|
||||
|
||||
|
||||
def session_quoted(sess: Any) -> bool:
|
||||
"""当前书签是否处在可尝试调价的阶段。"""
|
||||
if sess is None:
|
||||
return False
|
||||
phase = str(getattr(sess, "phase", "") or "")
|
||||
status = str(getattr(sess, "status", "") or "")
|
||||
if phase in _ADJUST_PHASES:
|
||||
return True
|
||||
if status == "协商中" and (getattr(sess, "quote", None) or getattr(sess, "work_order_no", "")):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def quoted_adjust_flow(sender_id: str):
|
||||
"""
|
||||
三条线里谁有可调报价就交给谁;多条时用最新工单号。
|
||||
|
||||
私聊调价常被判成闲聊,选线不能再按「有海运书签就走海运」,
|
||||
否则陆运刚出的卡会被旧海运书签抢走、回开场白。
|
||||
"""
|
||||
from agent.policy.air_text_flow import get_air_text_flow
|
||||
from agent.policy.land_text_flow import get_land_text_flow
|
||||
from agent.policy.sea_text_flow import get_sea_text_flow
|
||||
|
||||
hits: list[tuple[str, Any]] = []
|
||||
for flow in (get_land_text_flow(), get_air_text_flow(), get_sea_text_flow()):
|
||||
sess = flow.session_of(sender_id)
|
||||
if session_quoted(sess):
|
||||
hits.append((str(getattr(sess, "work_order_no", "") or ""), flow))
|
||||
if not hits:
|
||||
return None
|
||||
hits.sort(key=lambda item: item[0], reverse=True)
|
||||
return hits[0][1]
|
||||
|
||||
|
||||
def _baselines(quote: dict[str, Any]) -> tuple[str, str]:
|
||||
src = dict(quote or {})
|
||||
cost = str(src.get("tms_baseline_cost") or src.get("cost") or src.get("costTotal") or "")
|
||||
sale = str(src.get("tms_baseline_sale") or src.get("total") or "")
|
||||
return cost, sale
|
||||
|
||||
|
||||
def _reply_apply_messages(result, reply) -> None:
|
||||
names = [str(x.get("name") or "") for x in quote_fee_rows(result.quote)]
|
||||
if result.applied:
|
||||
reply(copy.adjust_confirm(result.applied))
|
||||
if result.unmatched:
|
||||
reply(copy.adjust_unmatched(result.unmatched, names))
|
||||
if result.ambiguous:
|
||||
for name in result.ambiguous:
|
||||
reply(copy.adjust_ambiguous(name, names))
|
||||
if result.lines_hint:
|
||||
reply(copy.adjust_need_lines())
|
||||
if result.currency_hint:
|
||||
reply(copy.adjust_currency_hint())
|
||||
|
||||
|
||||
def _emit_private_card(flow: Any, sess: Any, reply) -> None:
|
||||
"""新价格明细卡:海运/陆运带拉群,空运只带跳过协同。"""
|
||||
line = (sess.business_line or "").upper()
|
||||
no = sess.work_order_no
|
||||
quote = dict(sess.quote or {})
|
||||
if line:
|
||||
quote["business_line"] = line
|
||||
first = getattr(sess, "first_or_same", "first") or "first"
|
||||
history = getattr(sess, "history_work_order_no", "") or ""
|
||||
if line in {"SEA", "LAND"} and hasattr(flow, "_emit_sea_hit"):
|
||||
sess.allowed = (copy.BTN_PULL_COLLAB, copy.BTN_SKIP_COLLAB)
|
||||
flow._emit_sea_hit(
|
||||
reply,
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
first_or_same=first,
|
||||
history_work_order_no=history,
|
||||
status="已报价",
|
||||
business_line=line or "SEA",
|
||||
)
|
||||
return
|
||||
sess.allowed = (copy.BTN_SKIP_COLLAB,)
|
||||
reply(
|
||||
copy.tms_hit_card(
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
first_or_same=first,
|
||||
history_work_order_no=history,
|
||||
status="已报价",
|
||||
),
|
||||
copy.tms_hit_wecom_payload(
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
first_or_same=first,
|
||||
history_work_order_no=history,
|
||||
status="已报价",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def try_private_adjust(
|
||||
flow: Any,
|
||||
sess: Any,
|
||||
text: str,
|
||||
reply,
|
||||
*,
|
||||
injected_adjust=None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
私聊调价。不是调价或不够资格尝试则返回 None,让原流程继续。
|
||||
|
||||
副作用:确认话术、主账 upsert、新卡、书签 waitVersion+1。
|
||||
"""
|
||||
if not session_quoted(sess):
|
||||
return None
|
||||
names = [str(r.get("name") or "") for r in quote_fee_rows(getattr(sess, "quote", None))]
|
||||
extract = extract_adjust(text, fee_names=names, injected=injected_adjust)
|
||||
if not extract.is_adjust:
|
||||
return None
|
||||
reason = private_session_can_adjust(
|
||||
str(getattr(sess, "status", "") or ""),
|
||||
bool(quote_fee_rows(getattr(sess, "quote", None))),
|
||||
str(getattr(sess, "collab_chat_id", "") or ""),
|
||||
)
|
||||
if reason:
|
||||
reply(copy.adjust_ineligible(reason))
|
||||
return "adjust_blocked"
|
||||
result = apply_adjust(
|
||||
dict(sess.quote or {}),
|
||||
extract,
|
||||
business_line=str(getattr(sess, "business_line", "") or ""),
|
||||
tms_cost=_baselines(sess.quote)[0],
|
||||
tms_sale=_baselines(sess.quote)[1],
|
||||
)
|
||||
if result.code == "need_fee":
|
||||
reply(copy.adjust_need_fee())
|
||||
return "adjust_need_fee"
|
||||
if result.code == "need_lines":
|
||||
reply(copy.adjust_need_lines())
|
||||
return "adjust_need_lines"
|
||||
if result.code == "none_matched":
|
||||
reply(copy.adjust_unmatched(result.unmatched, names))
|
||||
return "adjust_none"
|
||||
if result.code == "ambiguous":
|
||||
reply(copy.adjust_ambiguous((extract.items[0].fee_name if extract.items else ""), names))
|
||||
return "adjust_ambiguous"
|
||||
if not result.ok:
|
||||
return None
|
||||
copy.remember_tms_quote(sess, sess.quote)
|
||||
persist = dict(result.quote)
|
||||
persist["source"] = "SALES_ADJUST"
|
||||
persist["source_label"] = "销售调价"
|
||||
result.quote["source"] = persist["source"]
|
||||
result.quote["source_label"] = persist["source_label"]
|
||||
writer = getattr(flow.ledger, "upsert_product_quote", None) or flow.ledger.upsert_quote
|
||||
remark = copy.adjust_ledger_remark(result.applied)
|
||||
try:
|
||||
up = writer(
|
||||
work_order_no=sess.work_order_no,
|
||||
quote=persist,
|
||||
to_status="已报价",
|
||||
remark=remark,
|
||||
kind="adjust",
|
||||
) or {}
|
||||
except TypeError:
|
||||
up = writer(work_order_no=sess.work_order_no, quote=persist, to_status="已报价") or {}
|
||||
if not up.get("ok"):
|
||||
reply("主账写入报价失败,调价未入账。")
|
||||
return "ledger_fail"
|
||||
_reply_apply_messages(result, reply)
|
||||
sess.quote = persist
|
||||
sess.status = "已报价"
|
||||
sess.phase = "wait_collab"
|
||||
sess.wait_version += 1
|
||||
flow._save(sess)
|
||||
_emit_private_card(flow, sess, reply)
|
||||
return "wait_collab"
|
||||
|
||||
|
||||
def apply_group_adjust(
|
||||
*,
|
||||
flow: Any,
|
||||
ticket: object,
|
||||
sender_id: str,
|
||||
text: str,
|
||||
sess: Any = None,
|
||||
chat_id: str = "",
|
||||
injected_adjust=None,
|
||||
) -> str:
|
||||
"""
|
||||
群内销售调价。非销售只说明、不当报价。
|
||||
|
||||
至少改成一项:全量文本 + Excel + 成交跟进。一项都没有:只说明。
|
||||
"""
|
||||
from agent.policy.sea_group_ops import (
|
||||
_current_quote,
|
||||
_facts_for_fill,
|
||||
_render_and_send_sheet,
|
||||
_save_product_quote,
|
||||
emit_group_deal,
|
||||
group_client_of,
|
||||
hydrate_ticket,
|
||||
is_sales,
|
||||
is_terminal,
|
||||
send_group_text,
|
||||
ticket_no,
|
||||
ticket_status,
|
||||
ticket_view,
|
||||
)
|
||||
|
||||
ticket = hydrate_ticket(flow, ticket)
|
||||
no = ticket_no(ticket)
|
||||
view = ticket_view(ticket)
|
||||
room = (chat_id or str((sess.collab_chat_id if sess else "") or view.get("collab_chat_id") or "")).strip()
|
||||
client = group_client_of(flow)
|
||||
if is_terminal(ticket):
|
||||
send_group_text(client, room, copy.ticket_locked(work_order_no=no, status=ticket_status(ticket)))
|
||||
return "ticket_locked"
|
||||
status = ticket_status(ticket)
|
||||
if status not in {"已报价", "协商中"}:
|
||||
send_group_text(client, room, copy.adjust_ineligible("no_quote"))
|
||||
return "adjust_blocked"
|
||||
if not is_sales(ticket, sender_id):
|
||||
send_group_text(client, room, copy.adjust_only_sales())
|
||||
return "adjust_not_sales"
|
||||
current = _current_quote(view, sess)
|
||||
names = [str(r.get("name") or "") for r in quote_fee_rows(current)]
|
||||
extract = extract_adjust(text, fee_names=names, injected=injected_adjust)
|
||||
if not extract.is_adjust:
|
||||
return "not_adjust"
|
||||
result = apply_adjust(
|
||||
current,
|
||||
extract,
|
||||
business_line=str(view.get("business_line") or getattr(sess, "business_line", "") or ""),
|
||||
tms_cost=_baselines(current)[0],
|
||||
tms_sale=_baselines(current)[1],
|
||||
)
|
||||
if result.code == "need_fee":
|
||||
send_group_text(client, room, copy.adjust_need_fee())
|
||||
return "adjust_need_fee"
|
||||
if result.code == "need_lines":
|
||||
send_group_text(client, room, copy.adjust_need_lines())
|
||||
return "adjust_need_lines"
|
||||
if result.code == "none_matched":
|
||||
send_group_text(client, room, copy.adjust_unmatched(result.unmatched, names))
|
||||
return "adjust_none"
|
||||
if not result.ok:
|
||||
send_group_text(client, room, copy.adjust_unmatched(result.unmatched or ["该项"], names))
|
||||
return "adjust_none"
|
||||
copy.remember_tms_quote(sess, current)
|
||||
_save_product_quote(
|
||||
flow,
|
||||
no,
|
||||
result.quote,
|
||||
sess,
|
||||
remark=copy.adjust_ledger_remark(result.applied),
|
||||
kind="adjust",
|
||||
)
|
||||
ticket2 = ticket_view(ticket)
|
||||
ticket2["quote"] = result.quote
|
||||
ticket2["status"] = "已报价"
|
||||
ticket2["work_order_no"] = no
|
||||
parts = [copy.adjust_confirm(result.applied)]
|
||||
if result.unmatched:
|
||||
parts.append(copy.adjust_unmatched(result.unmatched, [str(r.get("name") or "") for r in quote_fee_rows(result.quote)]))
|
||||
if result.lines_hint:
|
||||
parts.append(copy.adjust_need_lines())
|
||||
if result.currency_hint:
|
||||
parts.append(copy.adjust_currency_hint())
|
||||
line = str(view.get("business_line") or getattr(sess, "business_line", "") or "SEA")
|
||||
parts.append(copy.adjust_group_body(result.quote, business_line=line))
|
||||
send_group_text(client, room, "\n".join(parts))
|
||||
facts = dict(view.get("facts") or {})
|
||||
if sess is not None and getattr(sess, "facts", None):
|
||||
facts.update(dict(sess.facts or {}))
|
||||
collab = dict(view.get("collab_facts") or {})
|
||||
if sess is not None and getattr(sess, "collab_facts", None):
|
||||
collab.update(dict(sess.collab_facts or {}))
|
||||
_render_and_send_sheet(
|
||||
flow=flow,
|
||||
chat_id=room,
|
||||
work_order_no=no,
|
||||
facts=_facts_for_fill(facts, collab, business_line=line),
|
||||
quote=result.quote,
|
||||
sales_id=sender_id,
|
||||
business_line=line,
|
||||
)
|
||||
emit_group_deal(flow=flow, ticket=ticket2, sess=sess, quote=result.quote)
|
||||
logger.info("group_adjust wo=%s applied=%s", no, [x.get("name") for x in result.applied])
|
||||
return "group_adjusted"
|
||||
@@ -65,6 +65,7 @@ def ticket_view(ticket: object) -> dict[str, Any]:
|
||||
)
|
||||
view.setdefault("sales_name", view.get("salesName") or view.get("sales_name") or "")
|
||||
view.setdefault("lost_reason", view.get("lostReason") or view.get("lost_reason") or "")
|
||||
view.setdefault("tms_quote", view.get("tmsQuote") or view.get("tms_quote") or {})
|
||||
facts = dict(view.get("collab_facts") or view.get("collabFacts") or {})
|
||||
view.setdefault("collab_facts", facts)
|
||||
flag = str(facts.get("__ended") or view.get("collab_ended") or view.get("collabEnded") or "").strip().lower()
|
||||
@@ -83,6 +84,7 @@ def ticket_view(ticket: object) -> dict[str, Any]:
|
||||
"business_line": getattr(ticket, "business_line", "") or "SEA",
|
||||
"facts": dict(getattr(ticket, "facts", {}) or {}),
|
||||
"quote": dict(getattr(ticket, "quote", {}) or {}),
|
||||
"tms_quote": dict(getattr(ticket, "tms_quote", None) or {}),
|
||||
"collab_facts": dict(getattr(ticket, "collab_facts", {}) or {}),
|
||||
"collab_chat_id": getattr(ticket, "collab_chat_id", "") or "",
|
||||
"collab_member_ids": list(getattr(ticket, "collab_member_ids", []) or []),
|
||||
@@ -327,7 +329,9 @@ def hydrate_ticket(flow: Any, ticket: object) -> object:
|
||||
quote = dict(view.get("quote") or {})
|
||||
has_quote = bool(quote.get("total") or _quote_fee_rows(quote))
|
||||
has_status = bool(ticket_status(ticket))
|
||||
if has_facts and has_quote and has_status:
|
||||
has_tms = bool(dict(view.get("tms_quote") or view.get("tmsQuote") or {}))
|
||||
# 调价后 latest quote 仍有价,但不能因此跳过补 TMS 原价。
|
||||
if has_facts and has_quote and has_status and has_tms:
|
||||
return ticket
|
||||
no = ticket_no(ticket)
|
||||
sales = str(view.get("sales_wecom_id") or view.get("salesWecomId") or "").strip()
|
||||
@@ -352,6 +356,8 @@ def hydrate_ticket(flow: Any, ticket: object) -> object:
|
||||
merged["facts"] = dict(full_view.get("facts") or {})
|
||||
if (not has_quote) and full_view.get("quote"):
|
||||
merged["quote"] = dict(full_view.get("quote") or {})
|
||||
if full_view.get("tms_quote") or full_view.get("tmsQuote"):
|
||||
merged["tms_quote"] = dict(full_view.get("tms_quote") or full_view.get("tmsQuote") or {})
|
||||
if (not has_status) and full_view.get("status"):
|
||||
merged["status"] = full_view.get("status")
|
||||
collab = dict(full_view.get("collab_facts") or {})
|
||||
@@ -393,10 +399,7 @@ def send_brief_and_sheet(
|
||||
tms = dict(tms_quote) if tms_quote is not None else {}
|
||||
manual = dict(product_quote or {})
|
||||
if tms_quote is None:
|
||||
if _has_tms_quote(view_q) and not copy.quote_is_product(view_q):
|
||||
tms = view_q
|
||||
elif _has_tms_quote(sess_q) and not copy.quote_is_product(sess_q):
|
||||
tms = sess_q
|
||||
tms = copy.resolve_tms_quote(sess, view)
|
||||
if not manual:
|
||||
if copy.quote_is_product(override):
|
||||
manual = override
|
||||
@@ -691,6 +694,8 @@ def apply_product_quote(
|
||||
_park_pending_quote(no, quote, sess, flow)
|
||||
send_group_text(client, chat_id, copy.group_ask_tms_or_product())
|
||||
return "wait_tms_choice"
|
||||
if _quote_fee_rows(base):
|
||||
quote = merge_quote_fees(base, quote)
|
||||
_save_product_quote(flow, no, quote, sess)
|
||||
ticket2 = _reload(flow, chat_id, no) or ticket
|
||||
send_brief_and_sheet(
|
||||
@@ -954,7 +959,18 @@ def emit_group_deal(
|
||||
text = copy.group_deal_followup(sales_name=sales_name, work_order_no=no)
|
||||
send_group_text(client, chat_id, text)
|
||||
carder = getattr(client, "send_group_card", None)
|
||||
payload = copy.deal_wecom_payload(work_order_no=no, quote=q, facts=facts, card_version=ver)
|
||||
payload = copy.deal_wecom_payload(
|
||||
work_order_no=no,
|
||||
quote=q,
|
||||
facts=facts,
|
||||
card_version=ver,
|
||||
business_line=str(
|
||||
view.get("business_line")
|
||||
or getattr(sess, "business_line", "")
|
||||
or q.get("business_line")
|
||||
or ""
|
||||
),
|
||||
)
|
||||
if callable(carder):
|
||||
carder(chat_id=chat_id, template_card=payload.get("template_card") or {})
|
||||
|
||||
@@ -1104,14 +1120,11 @@ def _current_quote(view: dict[str, Any], sess: Any) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _has_tms_quote(quote: dict[str, Any] | None) -> bool:
|
||||
"""当前价来自 TMS 才要问;已经是产品价不再替用户选。"""
|
||||
"""当前价来自 TMS 才要问;产品价、销售调价不再替用户选。"""
|
||||
src = dict(quote or {})
|
||||
label = str(src.get("source_label") or src.get("source") or "")
|
||||
if "产品" in label:
|
||||
return False
|
||||
if not (src.get("total") or _quote_fee_rows(src)):
|
||||
return False
|
||||
return "TMS" in label.upper() or "标准报价" in label or not label
|
||||
return copy.quote_is_tms(src)
|
||||
|
||||
|
||||
def _park_pending_quote(no: str, quote: dict[str, Any], sess: Any, flow: Any) -> None:
|
||||
@@ -1378,12 +1391,39 @@ def merge_quote_fees(base: dict[str, Any] | None, overlay: dict[str, Any] | None
|
||||
return out
|
||||
|
||||
|
||||
def _save_product_quote(flow: Any, no: str, quote: dict[str, Any], sess: Any) -> None:
|
||||
def _save_product_quote(
|
||||
flow: Any,
|
||||
no: str,
|
||||
quote: dict[str, Any],
|
||||
sess: Any,
|
||||
*,
|
||||
remark: str = "",
|
||||
kind: str = "",
|
||||
) -> None:
|
||||
writer = getattr(flow.ledger, "upsert_product_quote", None) or getattr(
|
||||
flow.ledger, "upsert_quote", None
|
||||
)
|
||||
persist = dict(quote or {})
|
||||
if (kind or "").strip() == "adjust":
|
||||
persist["source"] = "SALES_ADJUST"
|
||||
persist["source_label"] = "销售调价"
|
||||
quote["source"] = persist["source"]
|
||||
quote["source_label"] = persist["source_label"]
|
||||
else:
|
||||
persist.setdefault("source", "PRODUCT")
|
||||
label = str(persist.get("source_label") or "")
|
||||
if (not label) or ("TMS" in label.upper()):
|
||||
persist["source_label"] = "手工报价"
|
||||
if callable(writer):
|
||||
saved = writer(work_order_no=no, quote=quote, to_status="已报价") or {}
|
||||
extras: dict[str, Any] = {}
|
||||
if remark:
|
||||
extras["remark"] = remark
|
||||
if kind:
|
||||
extras["kind"] = kind
|
||||
try:
|
||||
saved = writer(work_order_no=no, quote=persist, to_status="已报价", **extras) or {}
|
||||
except TypeError:
|
||||
saved = writer(work_order_no=no, quote=persist, to_status="已报价") or {}
|
||||
if not saved.get("ok") and saved.get("error"):
|
||||
logger.warning("产品报价未入主账 wo=%s err=%s", no, saved.get("error"))
|
||||
if sess is not None:
|
||||
|
||||
@@ -92,6 +92,12 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
and not (injected_facts and injected_mode)
|
||||
):
|
||||
return self._capture_lost_reason(sess, text, reply)
|
||||
if sess:
|
||||
from agent.policy.quote_adjust_ops import try_private_adjust
|
||||
|
||||
adjusted = try_private_adjust(self, sess, text, reply)
|
||||
if adjusted:
|
||||
return adjusted
|
||||
|
||||
snap = extract_inquiry_snapshot(
|
||||
text,
|
||||
@@ -271,6 +277,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
self._save(sess)
|
||||
return "ledger_fail"
|
||||
sess.quote = quote
|
||||
copy.remember_tms_quote(sess, quote)
|
||||
sess.status = "已报价"
|
||||
sess.phase = "wait_collab"
|
||||
sess.allowed = (copy.BTN_PULL_COLLAB, copy.BTN_SKIP_COLLAB)
|
||||
@@ -283,6 +290,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
first_or_same=first,
|
||||
history_work_order_no=history_wo,
|
||||
status="已报价",
|
||||
business_line=sess.business_line or "SEA",
|
||||
)
|
||||
return "wait_collab"
|
||||
|
||||
@@ -347,35 +355,44 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
first_or_same: str = "first",
|
||||
history_work_order_no: str = "",
|
||||
status: str = "已报价",
|
||||
business_line: str = "",
|
||||
) -> None:
|
||||
"""
|
||||
先发完整价格明细文本,再发带按钮的卡。
|
||||
|
||||
企微只发卡时,引用条超长会被截断;文本不受限。
|
||||
"""
|
||||
line = (business_line or getattr(self, "business_line", "") or "SEA").upper()
|
||||
if line not in {"SEA", "LAND"}:
|
||||
line = "SEA"
|
||||
src = dict(quote or {})
|
||||
src["business_line"] = line
|
||||
reply(
|
||||
copy.sea_quote_detail_text(
|
||||
work_order_no=work_order_no,
|
||||
quote=quote,
|
||||
quote=src,
|
||||
first_or_same=first_or_same,
|
||||
history_work_order_no=history_work_order_no,
|
||||
status=status,
|
||||
business_line=line,
|
||||
)
|
||||
)
|
||||
reply(
|
||||
copy.sea_tms_hit_card(
|
||||
work_order_no=work_order_no,
|
||||
quote=quote,
|
||||
quote=src,
|
||||
first_or_same=first_or_same,
|
||||
history_work_order_no=history_work_order_no,
|
||||
status=status,
|
||||
business_line=line,
|
||||
),
|
||||
copy.sea_tms_hit_wecom_payload(
|
||||
work_order_no=work_order_no,
|
||||
quote=quote,
|
||||
quote=src,
|
||||
first_or_same=first_or_same,
|
||||
history_work_order_no=history_work_order_no,
|
||||
status=status,
|
||||
business_line=line,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -388,6 +405,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
first_or_same=sess.first_or_same,
|
||||
history_work_order_no=sess.history_work_order_no,
|
||||
status=sess.status or "已报价",
|
||||
business_line=sess.business_line or "SEA",
|
||||
)
|
||||
return "wait_collab"
|
||||
if sess.business_line == "SEA" and sess.phase == "tms_miss":
|
||||
@@ -541,6 +559,8 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
allowed_keys=keys,
|
||||
)
|
||||
|
||||
ticket_view = ticket if isinstance(ticket, dict) else {}
|
||||
tms = copy.resolve_tms_quote(sess, ticket_view)
|
||||
brief = copy.sea_group_brief(
|
||||
work_order_no=no,
|
||||
facts=sess.facts,
|
||||
@@ -548,7 +568,9 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
collab_facts=sess.collab_facts,
|
||||
sales_name=sales_name,
|
||||
product_names=product_names,
|
||||
has_price=bool(sess.quote and sess.status == "已报价"),
|
||||
has_price=bool(tms or sess.quote),
|
||||
tms_quote=tms,
|
||||
product_quote=sess.quote if copy.quote_is_product(sess.quote) else None,
|
||||
transport_mode=self._group_brief_mode(sess),
|
||||
)
|
||||
client.send_group(chat_id=chat_id, content=brief)
|
||||
|
||||
Reference in New Issue
Block a user