落地销售调价:按自然语言改价并单独记销售价,拉群和后台不再把调后价当成 TMS 原价。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-20 14:40:20 +08:00
co-authored by Cursor
parent e4743541f7
commit 26dd5d72a1
36 changed files with 2795 additions and 176 deletions
+53 -13
View File
@@ -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: