diff --git a/.changeset/1995-qualification-approval-gate.md b/.changeset/1995-qualification-approval-gate.md index e93e5d53..efdca034 100644 --- a/.changeset/1995-qualification-approval-gate.md +++ b/.changeset/1995-qualification-approval-gate.md @@ -3,9 +3,10 @@ --- An opportunity can now require **qualification approval (立项)** before it is committed: -until the deal is approved, its stage cannot move, *Will Bid* cannot be recorded, and it -can be neither closed won/lost nor asked to be. Everything else stays open, so a new deal -can still be worked while it waits. **The gate ships off** (REQ-0006 step 11: +until the deal is approved, its stage cannot move, and it can be neither closed won/lost +nor asked to be. Everything else stays open, *Will Bid* included, so a new deal can still +be worked, and its bid intent recorded for the approver, while it waits. **The gate ships +off** (REQ-0006 step 11: 「销售立项需走审批流程;新增商机可跟进,立项通过后方可更新阶段、投标、赢丢单操作。」). **How it works, once armed.** The rep ticks *Request Qualification Approval* in the deal's @@ -19,8 +20,8 @@ is the customer's own 「新增商机可跟进」. **Off by default, bit for bit.** The switch is the field default on *Qualification Approval*, shipped as *Not Required*: no deal is ever born pending, the new flow -(*Opportunity Qualification Approval*) opens nothing, and moving the stage, *Will Bid* and -closing behave exactly as before. An install arms the gate by changing that one default +(*Opportunity Qualification Approval*) opens nothing, and moving the stage and closing +behave exactly as before. An install arms the gate by changing that one default to *Pending*; deals that already exist keep *Not Required*. **With the status-change gate armed too, qualification comes first.** A deal that is not diff --git a/.changeset/2004-will-bid-before-qualification.md b/.changeset/2004-will-bid-before-qualification.md new file mode 100644 index 00000000..186b2465 --- /dev/null +++ b/.changeset/2004-will-bid-before-qualification.md @@ -0,0 +1,19 @@ +--- +'hotcrm': patch +--- + +Qualification approval (立项) leaves *Will Bid* open + +While a deal waits for qualification approval, or after an approver rejects it, the rep can +now record and change *Will Bid*. Whether you intend to bid is part of what the approver +decides on: REQ-0006 step 8 has the rep fill 是否投标 as input to 立项, so holding it until +approval had the approver decide without it. Step 11's 投标 is the act of bidding, which +HotCRM does not model. + +The gate still holds the two acts that commit the deal: moving its stage (which includes +closing it directly), and asking for won or lost with *Requested Status*. Both are refused +`409 RECORD_LOCKED` until the deal is approved, and the refusal sentence now names only what +was held. The gate still ships off; nothing changes for an install that has not armed it. + +This narrows the qualification-approval gate that ships in this same release; its entry +above now describes the gate as it ships. diff --git a/content/docs/administration/automation.mdx b/content/docs/administration/automation.mdx index c9e149e9..e5a1dd8b 100644 --- a/content/docs/administration/automation.mdx +++ b/content/docs/administration/automation.mdx @@ -70,7 +70,7 @@ A flow fires one of three ways, set by its start node: | **Large Deal Approval** | Record change (update) | Tiered sign-off via approval nodes — Sales Manager ≥ $100K, Sales Director > $500K | | **Large Deal Approval (on create)** | Record change (insert) | The same intake for opportunities *created* at or above the threshold | | **Opportunity Status Change Approval** | Record change (update) | Sign-off a deal needs before it is declared won or lost. Ships switched OFF — it opens nothing until an install arms the gate on the opportunity's **Status Change Approval** field; armed, the rep sets **Requested Status** and the stage moves only when the request is approved | -| **Opportunity Qualification Approval** | Record change (update) | The qualification (立项) sign-off a new deal needs before its stage, its bid decision or its won/lost call may change. Ships switched OFF — it opens nothing until an install arms the gate on the opportunity's **Qualification Approval** field; armed, the rep ticks **Request Qualification Approval**, and everything else on the deal stays editable while it waits | +| **Opportunity Qualification Approval** | Record change (update) | The qualification (立项) sign-off a new deal needs before its stage or its won/lost call may change. Ships switched OFF — it opens nothing until an install arms the gate on the opportunity's **Qualification Approval** field; armed, the rep ticks **Request Qualification Approval**, and everything else on the deal stays editable while it waits | | **Lead Conversion Approval** | Record change (insert) | Sign-off a lead needs before it can be converted. Ships switched OFF — it opens nothing until an install arms the gate on the lead's **Conversion Approval** field | | **Account Approval** | Record change (insert) | Sign-off a newly created account needs before it counts as established data. Ships switched **ON** — a new account starts *Pending* and is decided in the approval inbox; an install that wants no account sign-off changes the default on the account's **Approval Status** field | | **Large Deal Won Alert** | Record change (update) | When an opportunity of $100K or more turns *Closed Won*, notify the owner — the owner alone, not their manager | @@ -111,7 +111,7 @@ Since ObjectStack 7.4, approvals are modeled as **`approval` nodes inside a flow HotCRM's built-in **Opportunity Approval** flow chains two approval nodes for tiered sign-off (manager → director). See [Revenue › Approvals](/docs/revenue/approvals) for thresholds, what approvers see, and the audit trail. -A second, independent gate — **Opportunity Status Change Approval** — asks for sign-off before a deal is declared won or lost. It ships switched off and keeps its verdict in its own field, so arming it never changes the amount-based sign-off. A third, **Opportunity Qualification Approval**, is the qualification (立项) sign-off a new deal needs before its stage, bid decision or won/lost call may change; it too ships off, and with both armed it comes first. See [Sales › Opportunity Qualification](/docs/sales/opportunity-qualification). +A second, independent gate — **Opportunity Status Change Approval** — asks for sign-off before a deal is declared won or lost. It ships switched off and keeps its verdict in its own field, so arming it never changes the amount-based sign-off. A third, **Opportunity Qualification Approval**, is the qualification (立项) sign-off a new deal needs before its stage or won/lost call may change; it too ships off, and with both armed it comes first. See [Sales › Opportunity Qualification](/docs/sales/opportunity-qualification). ## Order of operations diff --git a/content/docs/administration/automation.zh-Hans.mdx b/content/docs/administration/automation.zh-Hans.mdx index 5cd04317..ba42263f 100644 --- a/content/docs/administration/automation.zh-Hans.mdx +++ b/content/docs/administration/automation.zh-Hans.mdx @@ -70,7 +70,7 @@ description: 验证规则、流程、计划作业与审批 —— 无需你动 | **大额商机审批** | 记录变更(更新) | 通过 approval 节点分级签核 —— 销售经理 ≥ $100K,销售总监 > $500K | | **大额商机审批(新建时)** | 记录变更(插入) | 同一套受理逻辑,用于*创建时*金额已达到阈值的商机 | | **商机状态变更审批** | 记录变更(更新) | 商机宣布赢单或丢单之前需要的签核。出厂默认是**关闭**的:在商机的**状态变更审批**字段上打开闸门之前,它不会发起任何审批;打开之后,销售填写**申请变更状态**,请求获批后阶段才会变更 | -| **商机立项审批** | 记录变更(更新) | 新建商机在更新阶段、决定是否投标或赢丢单之前需要的立项签核。出厂默认是**关闭**的:在商机的**立项审批**字段上打开闸门之前,它不会发起任何审批;打开之后,销售勾选**申请立项审批**,等待审批期间商机的其他内容照常可以编辑 | +| **商机立项审批** | 记录变更(更新) | 新建商机在更新阶段或赢丢单之前需要的立项签核。出厂默认是**关闭**的:在商机的**立项审批**字段上打开闸门之前,它不会发起任何审批;打开之后,销售勾选**申请立项审批**,等待审批期间商机的其他内容照常可以编辑 | | **线索转化审批** | 记录变更(插入) | 线索转化前需要的签核。出厂默认是**关闭**的:在线索的**转化审批**字段上打开闸门之前,它不会发起任何审批 | | **客户审批** | 记录变更(插入) | 新建客户在成为正式数据之前需要的签核。出厂默认是**打开**的:新客户以**待审批**状态创建,并在审批收件箱中裁定;不需要客户签核的安装,改掉客户**审批状态**字段的默认值即可 | | **大额商机赢单提醒** | 记录变更(更新) | 金额 $100K 及以上的商机转为 *Closed Won* 时,通知负责人 —— 只通知负责人本人,不通知其经理 | @@ -111,7 +111,7 @@ description: 验证规则、流程、计划作业与审批 —— 无需你动 HotCRM 内置的 **商机审批** 流程串联两个 approval 节点实现分级签核(经理 → 总监)。关于阈值、审批人所见以及审计轨迹,参见 [营收云 › 审批](/zh-Hans/docs/revenue/approvals)。 -另有一道独立的闸门 —— **商机状态变更审批** —— 在商机宣布赢单或丢单之前要求签核。它出厂默认关闭,并把结论记在自己的字段里,因此打开它不会改变按金额分级的签核。第三道 —— **商机立项审批** —— 是新建商机在更新阶段、决定是否投标或赢丢单之前需要的立项签核;它同样出厂关闭,两道都打开时它排在前面。参见[销售云 › 商机资格评估](/zh-Hans/docs/sales/opportunity-qualification)。 +另有一道独立的闸门 —— **商机状态变更审批** —— 在商机宣布赢单或丢单之前要求签核。它出厂默认关闭,并把结论记在自己的字段里,因此打开它不会改变按金额分级的签核。第三道 —— **商机立项审批** —— 是新建商机在更新阶段或赢丢单之前需要的立项签核;它同样出厂关闭,两道都打开时它排在前面。参见[销售云 › 商机资格评估](/zh-Hans/docs/sales/opportunity-qualification)。 ## 操作顺序 diff --git a/content/docs/administration/automation.zh-Hant.mdx b/content/docs/administration/automation.zh-Hant.mdx index c47a482e..b41b1b44 100644 --- a/content/docs/administration/automation.zh-Hant.mdx +++ b/content/docs/administration/automation.zh-Hant.mdx @@ -72,7 +72,7 @@ description: 驗證規則、流程、排程作業與審批 —— 無需你動 | **大額商機審批** | 記錄變更(更新) | 透過 approval 節點分級簽核 —— 銷售經理 ≥ $100K,銷售總監 > $500K | | **大額商機審批(新建時)** | 記錄變更(插入) | 同一套受理邏輯,用於*建立時*金額已達到閾值的商機 | | **商機狀態變更審批** | 記錄變更(更新) | 商機宣布贏單或丟單之前需要的簽核。出廠預設是**關閉**的:在商機的**狀態變更審批**欄位上打開閘門之前,它不會發起任何審批;打開之後,銷售填寫**申請變更狀態**,請求獲批後階段才會變更 | -| **商機立項審批** | 記錄變更(更新) | 新建商機在更新階段、決定是否投標或贏丟單之前需要的立項簽核。出廠預設是**關閉**的:在商機的**立項審批**欄位上打開閘門之前,它不會發起任何審批;打開之後,銷售勾選**申請立項審批**,等待審批期間商機的其他內容照常可以編輯 | +| **商機立項審批** | 記錄變更(更新) | 新建商機在更新階段或贏丟單之前需要的立項簽核。出廠預設是**關閉**的:在商機的**立項審批**欄位上打開閘門之前,它不會發起任何審批;打開之後,銷售勾選**申請立項審批**,等待審批期間商機的其他內容照常可以編輯 | | **線索轉化審批** | 記錄變更(插入) | 線索轉化前需要的簽核。出廠預設是**關閉**的:在線索的**轉化審批**欄位上打開閘門之前,它不會發起任何審批 | | **客戶審批** | 記錄變更(插入) | 新建客戶在成為正式資料之前需要的簽核。出廠預設是**打開**的:新客戶以**待審批**狀態建立,並在審批收件匣中裁定;不需要客戶簽核的安裝,改掉客戶**審批狀態**欄位的預設值即可 | | **大額商機贏單提醒** | 記錄變更(更新) | 金額 $100K 及以上的商機轉為 *Closed Won* 時,通知負責人 —— 只通知負責人本人,不通知其經理 | @@ -113,7 +113,7 @@ description: 驗證規則、流程、排程作業與審批 —— 無需你動 HotCRM 內建的 **商機審批** 流程串聯兩個 approval 節點實作分級簽核(經理 → 總監)。關於閾值、審批人所見以及稽核軌跡,參見 [營收雲 › 審批](/zh-Hant/docs/revenue/approvals)。 -另有一道獨立的閘門 —— **商機狀態變更審批** —— 在商機宣布贏單或丟單之前要求簽核。它出廠預設關閉,並把結論記在自己的欄位裡,因此打開它不會改變按金額分級的簽核。第三道 —— **商機立項審批** —— 是新建商機在更新階段、決定是否投標或贏丟單之前需要的立項簽核;它同樣出廠關閉,兩道都打開時它排在前面。參見[銷售雲 › 商機資格評估](/zh-Hant/docs/sales/opportunity-qualification)。 +另有一道獨立的閘門 —— **商機狀態變更審批** —— 在商機宣布贏單或丟單之前要求簽核。它出廠預設關閉,並把結論記在自己的欄位裡,因此打開它不會改變按金額分級的簽核。第三道 —— **商機立項審批** —— 是新建商機在更新階段或贏丟單之前需要的立項簽核;它同樣出廠關閉,兩道都打開時它排在前面。參見[銷售雲 › 商機資格評估](/zh-Hant/docs/sales/opportunity-qualification)。 ## 操作順序 diff --git a/content/docs/sales/opportunity-qualification.mdx b/content/docs/sales/opportunity-qualification.mdx index de051ca9..176c3bdc 100644 --- a/content/docs/sales/opportunity-qualification.mdx +++ b/content/docs/sales/opportunity-qualification.mdx @@ -10,7 +10,7 @@ An opportunity already says what the deal is worth and where it stands in *your* - **Qualification** — should we pursue this deal at all, and how hard? - **The customer's calendar** — when the buyer plans to start, tender and sign, and for how much. - **Deal Narrative** — the written case for the deal, in parts a reviewer can read one at a time. -- **Qualification Approval** — whether a new deal has to be approved (立项) before its stage, its bid decision or its won/lost call may change. **Off unless your admin turns it on.** +- **Qualification Approval** — whether a new deal has to be approved (立项) before its stage or its won/lost call may change. **Off unless your admin turns it on.** - **Status Change Approval** — whether somebody has to sign off before a deal is declared won or lost. **Off unless your admin turns it on.** All of it sits on the opportunity's **Details** tab, in the **Qualification**, **Sales Process** and **Deal Narrative** sections. @@ -53,24 +53,24 @@ Attachments still go on the record's files, as they always have. ## Qualification approval -Some organisations approve a deal as a project — 立项 — before anyone commits to it. Until that approval, a new deal can be **worked**: log calls and meetings, write the narrative, record the customer's calendar and the amount. What waits is **committing** it: moving its stage, recording **Will Bid**, and the won/lost call — whether that is closing it directly or asking for it with **Requested Status**. The **Qualification Approval** field records where that stands: +Some organisations approve a deal as a project — 立项 — before anyone commits to it. Until that approval, a new deal can be **worked**: log calls and meetings, write the narrative, record the customer's calendar, the amount and **Will Bid**. What waits is **committing** it: moving its stage and the won/lost call — whether that is closing it directly or asking for it with **Requested Status**. **Will Bid** stays open on purpose: whether you intend to bid is part of what the approver decides on, so record it before you ask. The **Qualification Approval** field records where that stands: | Qualification Approval | What it means | | --- | --- | | **Not Required** | No qualification approval is asked for. This is how HotCRM ships, and it is what every deal shows unless your admin has turned the gate on. | | **Pending** | The gate is on for this deal and it has not been approved yet. | -| **Approved** | The deal is qualified. Its stage, Will Bid and won/lost call are open from now on. | +| **Approved** | The deal is qualified. Its stage and won/lost call are open from now on. | | **Rejected** | The approver said no to the last request. The deal stays held. | You never set this field yourself — it is filled in by the approval, and it is read-only. -**While a deal is Pending or Rejected, its stage, Will Bid and Requested Status cannot change.** To have it approved: +**While a deal is Pending or Rejected, its stage and Requested Status cannot change.** To have it approved: 1. Tick **Request Qualification Approval** in the deal's **Qualification** section. -2. An approval request opens in the approval inbox. The deal is **not** locked while it waits: everything other than those three stays editable, so keep working it. +2. An approval request opens in the approval inbox. The deal is **not** locked while it waits: everything other than those two stays editable, so keep working it. 3. If it is approved, the deal is qualified for good. If it is rejected, the box is unticked and the deal stays held — tick it again when something changes to ask again. -> **Nothing changes for you unless your admin arms the gate.** Out of the box every deal reads *Not Required*, and moving the stage, Will Bid and closing work exactly as they always have. Deals created before the gate was turned on are never held by it. +> **Nothing changes for you unless your admin arms the gate.** Out of the box every deal reads *Not Required*, and moving the stage and closing work exactly as they always have. Deals created before the gate was turned on are never held by it. **When both approvals are on, qualification comes first.** A deal that is not yet qualified cannot ask for a status change either; once it is approved, closing works as the next section describes. @@ -100,8 +100,8 @@ You never set this field yourself — it is filled in by the approval, and it is - ✅ Fill in **Will Bid** as soon as you have decided, and leave it empty until then. A pipeline review reads the empty ones as the decisions still to make. - ✅ Record the customer's tender date the day you hear it. It is the date your bid team plans against, and **Tender This Quarter** can only list the deals that carry one. - ✅ If closing is refused, look at **Status Change Approval** before asking anyone: *Pending* means set **Requested Status** instead, *Rejected* is a decision to talk about. -- ✅ Fill in the narrative and the customer's calendar before you request qualification approval. They are what the approver reads. -- ✅ If a stage move or **Will Bid** is refused, look at **Qualification Approval**: *Pending* means tick **Request Qualification Approval** (or wait for the request already open), *Rejected* is a decision to talk about. +- ✅ Fill in the narrative, the customer's calendar and **Will Bid** before you request qualification approval. They are what the approver reads. +- ✅ If a stage move is refused, look at **Qualification Approval**: *Pending* means tick **Request Qualification Approval** (or wait for the request already open), *Rejected* is a decision to talk about. - ⛔ Don't move **Close Date** to match the tender date. The two are different events, and the forecast depends on the close date being yours. ## Tips for admins diff --git a/content/docs/sales/opportunity-qualification.zh-Hans.mdx b/content/docs/sales/opportunity-qualification.zh-Hans.mdx index c0373f63..af1f5957 100644 --- a/content/docs/sales/opportunity-qualification.zh-Hans.mdx +++ b/content/docs/sales/opportunity-qualification.zh-Hans.mdx @@ -10,7 +10,7 @@ description: 这笔交易值不值得跟、客户自己的采购日程、写下 - **资格评估**——这笔交易要不要跟,花多大力气跟? - **客户的日程**——买方打算什么时候立项、招标、签约,各是多少金额。 - **商机叙述**——这笔交易的书面论证,拆成评审人可以逐段读的几部分。 -- **立项审批**——新商机在更新阶段、决定是否投标或赢丢单之前,是否需要先通过立项。**除非管理员打开,否则它是关着的。** +- **立项审批**——新商机在更新阶段或赢丢单之前,是否需要先通过立项。**除非管理员打开,否则它是关着的。** - **状态变更审批**——宣布赢单或丢单之前是否需要有人签核。**除非管理员打开,否则它是关着的。** 这些都在商机的 *Details* 标签页上,分别位于**资格评估**、**销售流程**和**商机叙述**三个分区里。 @@ -53,24 +53,24 @@ description: 这笔交易值不值得跟、客户自己的采购日程、写下 ## 立项审批 [#qualification-approval] -有些组织要先对一笔交易立项、批准之后,才愿意为它投入。立项通过之前,新商机照样可以**跟进**:记录电话和拜访、撰写商机叙述、登记客户的日程和金额。要等审批的是**推进**它的那几步:变更阶段、填写**是否投标**,以及赢单或丢单——不论是直接关单,还是通过**申请变更状态**去申请。商机上的**立项审批**字段记录这件事走到哪一步: +有些组织要先对一笔交易立项、批准之后,才愿意为它投入。立项通过之前,新商机照样可以**跟进**:记录电话和拜访、撰写商机叙述、登记客户的日程、金额和**是否投标**。要等审批的是**推进**它的那两步:变更阶段,以及赢单或丢单——不论是直接关单,还是通过**申请变更状态**去申请。**是否投标**有意不被挡住:你打不打算投标,本身就是审批人据以决定的内容之一,所以要在申请之前填好。商机上的**立项审批**字段记录这件事走到哪一步: | 立项审批 | 含义 | | --- | --- | | **无需审批** | 不要求立项审批。这是 HotCRM 的出厂状态,除非管理员打开了闸门,否则每笔交易都是这个值。 | | **审批中** | 这笔交易的闸门是开着的,还没有通过立项。 | -| **已批准** | 立项已通过。从此可以变更阶段、填写是否投标、赢单或丢单。 | +| **已批准** | 立项已通过。从此可以变更阶段、赢单或丢单。 | | **已驳回** | 审批人否决了上一次申请。交易仍然被挡着。 | 这个字段不由你填写——它由审批流程写入,是只读的。 -**只要交易处在"审批中"或"已驳回",它的阶段、是否投标和申请变更状态就不能改。** 要让它通过: +**只要交易处在"审批中"或"已驳回",它的阶段和申请变更状态就不能改。** 要让它通过: 1. 在交易的**资格评估**分区里勾选**申请立项审批**。 -2. 审批收件箱里会出现一条审批请求。等待期间交易**不会**被锁定:除了上面那三项,其他内容都照常可以编辑,跟进不必停。 +2. 审批收件箱里会出现一条审批请求。等待期间交易**不会**被锁定:除了上面那两项,其他内容都照常可以编辑,跟进不必停。 3. 获批后,这笔交易就一直处于已立项状态;被驳回时,勾选会被取消,交易仍然被挡着——情况有变时再勾选一次即可重新申请。 -> **管理员不打开闸门,你这边什么都不会变。** 出厂状态下每笔交易都显示*无需审批*,变更阶段、填写是否投标和关单都和以前完全一样。闸门打开之前创建的交易永远不会被它挡住。 +> **管理员不打开闸门,你这边什么都不会变。** 出厂状态下每笔交易都显示*无需审批*,变更阶段和关单都和以前完全一样。闸门打开之前创建的交易永远不会被它挡住。 **两道审批都打开时,立项排在前面。** 还没立项的交易也不能申请状态变更;立项通过之后,关单就按下一节说的方式进行。 @@ -100,8 +100,8 @@ description: 这笔交易值不值得跟、客户自己的采购日程、写下 - ✅ 一决定就填**是否投标**,没决定就留空。管线评审会把空着的那些当作还没做的决定来读。 - ✅ 听到客户的招标日期当天就把它记下来。投标团队按这个日期排工作,而**本季度预计招标**只能列出填了日期的交易。 - ✅ 关单被拒绝时,先看**状态变更审批**的值再去问人:*审批中*意味着应改为填写**申请变更状态**,*已驳回*是需要谈一谈的结论。 -- ✅ 申请立项审批之前,先把商机叙述和客户的日程填好。审批人读的就是这些。 -- ✅ 变更阶段或填写**是否投标**被拒绝时,看一下**立项审批**:*审批中*意味着应勾选**申请立项审批**(或等待已经发出的那条申请),*已驳回*是需要谈一谈的结论。 +- ✅ 申请立项审批之前,先把商机叙述、客户的日程和**是否投标**填好。审批人读的就是这些。 +- ✅ 变更阶段被拒绝时,看一下**立项审批**:*审批中*意味着应勾选**申请立项审批**(或等待已经发出的那条申请),*已驳回*是需要谈一谈的结论。 - ⛔ 不要把**预计成交日期**改成和招标日期一样。两者是不同的事件,而预测依赖的是你自己的预计成交日期。 ## 给管理员的建议 diff --git a/content/docs/sales/opportunity-qualification.zh-Hant.mdx b/content/docs/sales/opportunity-qualification.zh-Hant.mdx index 4aeac821..0bc7d445 100644 --- a/content/docs/sales/opportunity-qualification.zh-Hant.mdx +++ b/content/docs/sales/opportunity-qualification.zh-Hant.mdx @@ -12,7 +12,7 @@ description: 這筆交易值不值得跟、客戶自己的採購日程、寫下 - **資格評估**——這筆交易要不要跟,花多大力氣跟? - **客戶的日程**——買方打算什麼時候立項、招標、簽約,各是多少金額。 - **商機敘述**——這筆交易的書面論證,拆成評審人可以逐段讀的幾部分。 -- **立項審批**——新商機在更新階段、決定是否投標或贏丟單之前,是否需要先通過立項。**除非管理員打開,否則它是關著的。** +- **立項審批**——新商機在更新階段或贏丟單之前,是否需要先通過立項。**除非管理員打開,否則它是關著的。** - **狀態變更審批**——宣布贏單或丟單之前是否需要有人簽核。**除非管理員打開,否則它是關著的。** 這些都在商機的 *Details* 標籤頁上,分別位於**資格評估**、**銷售流程**和**商機敘述**三個分區裡。 @@ -55,24 +55,24 @@ description: 這筆交易值不值得跟、客戶自己的採購日程、寫下 ## 立項審批 [#qualification-approval] -有些組織要先對一筆交易立項、批准之後,才願意為它投入。立項通過之前,新商機照樣可以**跟進**:記錄電話和拜訪、撰寫商機敘述、登記客戶的日程和金額。要等審批的是**推進**它的那幾步:變更階段、填寫**是否投標**,以及贏單或丟單——不論是直接關單,還是透過**申請變更狀態**去申請。商機上的**立項審批**欄位記錄這件事走到哪一步: +有些組織要先對一筆交易立項、批准之後,才願意為它投入。立項通過之前,新商機照樣可以**跟進**:記錄電話和拜訪、撰寫商機敘述、登記客戶的日程、金額和**是否投標**。要等審批的是**推進**它的那兩步:變更階段,以及贏單或丟單——不論是直接關單,還是透過**申請變更狀態**去申請。**是否投標**有意不被擋住:你打不打算投標,本身就是審批人據以決定的內容之一,所以要在申請之前填好。商機上的**立項審批**欄位記錄這件事走到哪一步: | 立項審批 | 含義 | | --- | --- | | **無需審批** | 不要求立項審批。這是 HotCRM 的出廠狀態,除非管理員打開了閘門,否則每筆交易都是這個值。 | | **審批中** | 這筆交易的閘門是開著的,還沒有通過立項。 | -| **已批准** | 立項已通過。從此可以變更階段、填寫是否投標、贏單或丟單。 | +| **已批准** | 立項已通過。從此可以變更階段、贏單或丟單。 | | **已駁回** | 審批人否決了上一次申請。交易仍然被擋著。 | 這個欄位不由你填寫——它由審批流程寫入,是唯讀的。 -**只要交易處在「審批中」或「已駁回」,它的階段、是否投標和申請變更狀態就不能改。** 要讓它通過: +**只要交易處在「審批中」或「已駁回」,它的階段和申請變更狀態就不能改。** 要讓它通過: 1. 在交易的**資格評估**分區裡勾選**申請立項審批**。 -2. 審批收件匣裡會出現一條審批請求。等待期間交易**不會**被鎖定:除了上面那三項,其他內容都照常可以編輯,跟進不必停。 +2. 審批收件匣裡會出現一條審批請求。等待期間交易**不會**被鎖定:除了上面那兩項,其他內容都照常可以編輯,跟進不必停。 3. 獲批後,這筆交易就一直處於已立項狀態;被駁回時,勾選會被取消,交易仍然被擋著——情況有變時再勾選一次即可重新申請。 -> **管理員不打開閘門,你這邊什麼都不會變。** 出廠狀態下每筆交易都顯示*無需審批*,變更階段、填寫是否投標和關單都和以前完全一樣。閘門打開之前建立的交易永遠不會被它擋住。 +> **管理員不打開閘門,你這邊什麼都不會變。** 出廠狀態下每筆交易都顯示*無需審批*,變更階段和關單都和以前完全一樣。閘門打開之前建立的交易永遠不會被它擋住。 **兩道審批都打開時,立項排在前面。** 還沒立項的交易也不能申請狀態變更;立項通過之後,關單就按下一節說的方式進行。 @@ -102,8 +102,8 @@ description: 這筆交易值不值得跟、客戶自己的採購日程、寫下 - ✅ 一決定就填**是否投標**,沒決定就留空。管線評審會把空著的那些當作還沒做的決定來讀。 - ✅ 聽到客戶的招標日期當天就把它記下來。投標團隊按這個日期排工作,而**本季度預計招標**只能列出填了日期的交易。 - ✅ 關單被拒絕時,先看**狀態變更審批**的值再去問人:*審批中*意味著應改為填寫**申請變更狀態**,*已駁回*是需要談一談的結論。 -- ✅ 申請立項審批之前,先把商機敘述和客戶的日程填好。審批人讀的就是這些。 -- ✅ 變更階段或填寫**是否投標**被拒絕時,看一下**立項審批**:*審批中*意味著應勾選**申請立項審批**(或等待已經發出的那條申請),*已駁回*是需要談一談的結論。 +- ✅ 申請立項審批之前,先把商機敘述、客戶的日程和**是否投標**填好。審批人讀的就是這些。 +- ✅ 變更階段被拒絕時,看一下**立項審批**:*審批中*意味著應勾選**申請立項審批**(或等待已經發出的那條申請),*已駁回*是需要談一談的結論。 - ⛔ 不要把**預計成交日期**改成和招標日期一樣。兩者是不同的事件,而預測依賴的是你自己的預計成交日期。 ## 給管理員的建議 diff --git a/src/sales/flows/opportunity-qualification-approval.flow.ts b/src/sales/flows/opportunity-qualification-approval.flow.ts index e2035981..7e5644f5 100644 --- a/src/sales/flows/opportunity-qualification-approval.flow.ts +++ b/src/sales/flows/opportunity-qualification-approval.flow.ts @@ -6,7 +6,8 @@ type Flow = Automation.Flow; /** * Opportunity Qualification Approval — the 立项 sign-off a new deal needs - * before its stage, its bid decision or its won/lost call may change. + * before its stage or its won/lost call may change. Will Bid (是否投标) is + * not held: step 8 makes it input to 立项, for the approver to read (#2004). * * REQ-0006 step 11: 「销售立项需走审批流程;新增商机可跟进,立项通过后方可更新阶段、投标、 * 赢丢单操作。」 Expressed as an **approval node** (`type: 'approval'`, ADR-0019), @@ -18,7 +19,7 @@ type Flow = Automation.Flow; * field's `defaultValue`, the rep writes a REQUEST and the flow opens one * approval when the request is NEW, `rejected` re-opens on a new request, * `runAs: 'system'`, `onEmptyApprovers: 'admin_rescue'`. The refusal of the - * three gated acts lives beside the step-14 refusal in `opportunity.hook.ts`'s + * two gated acts lives beside the step-14 refusal in `opportunity.hook.ts`'s * `opportunity_lifecycle`. Where it differs, the reason is written beside it * (`lockRecord`). * @@ -60,7 +61,7 @@ export const OpportunityQualificationApprovalFlow: Flow = { name: 'opportunity_qualification_approval', label: 'Opportunity Qualification Approval', description: - 'The 立项 sign-off a deal needs before its stage, bid decision or won/lost call may change. Inert unless the install arms the gate on crm_opportunity.qualification_approval_status.', + 'The 立项 sign-off a deal needs before its stage or won/lost call may change. Inert unless the install arms the gate on crm_opportunity.qualification_approval_status.', type: 'record_change', status: 'active', // The reading both sibling gates record from `opportunity_approval`'s @@ -131,9 +132,9 @@ export const OpportunityQualificationApprovalFlow: Flow = { // ⚠️ `false`, NOT the status-change gate's `true` — the one term this // node deliberately takes from the LEAD gate instead. Step 11 says it // in as many words: 「新增商机可跟进」. A deal awaiting 立项 is still being - // worked — the narrative, the customer calendar and the amount are - // what the approver reads — and locking it would stop that work for as - // long as the approver takes. The three acts the gate exists to hold + // worked — the narrative, the customer calendar, the amount and Will + // Bid are what the approver reads — and locking it would stop that work + // for as long as the approver takes. The two acts the gate exists to hold // are refused by `opportunity_lifecycle` whether or not a request is // open, so the lock would add nothing but the freeze. lockRecord: false, diff --git a/src/sales/objects/opportunity.hook.ts b/src/sales/objects/opportunity.hook.ts index ac0454f5..c759d1de 100644 --- a/src/sales/objects/opportunity.hook.ts +++ b/src/sales/objects/opportunity.hook.ts @@ -124,13 +124,16 @@ const opportunityValidationHook: Hook = { // ─── 立项 (qualification) gate (REQ-0006 step 11) ─────────────────── // // 「新增商机可跟进,立项通过后方可更新阶段、投标、赢丢单操作。」 Until 立项 is - // approved, three acts are refused and every other edit stays open: + // approved, two acts are refused and every other edit stays open: // • any change of `stage` — 更新阶段, which includes a direct close // (赢丢单); - // • recording `will_bid` — 投标, the bid decision; // • a NEW `requested_status` — the won/lost request (赢丢单) the step-14 // gate opens. So with both gates armed 立项 comes FIRST: an unqualified // deal cannot even ask for a status change. + // `will_bid` (是否投标) is deliberately NOT held (#2004, ruled): step 8 has + // the rep fill it as INPUT to 立项, for the approver to read, so holding it + // until approval would have the approver decide without it. Step 11's 投标 + // is the act of bidding, which HotCRM does not model. // Built on the step-14 gate below and deliberately the same in every // load-bearing term: // • a TRANSITION GATE, not an invariant (AGENTS.md metadata semantics @@ -157,13 +160,12 @@ const opportunityValidationHook: Hook = { if (qualification === 'pending' || qualification === 'rejected') { const held: string[] = []; if (typeof input.stage === 'string' && input.stage !== previous.stage) held.push('Stage'); - if (typeof input.will_bid === 'boolean' && input.will_bid !== previous.will_bid) held.push('Will Bid'); if (typeof input.requested_status === 'string' && input.requested_status !== '' && input.requested_status !== previous.requested_status) { held.push('Requested Status'); } if (held.length > 0) { throw refuse( - `This deal needs qualification approval first: tick Request Qualification Approval. ${held.join(', ')} can change once it is approved.`, + `This deal needs qualification approval first: tick Request Qualification Approval. ${held.join(' and ')} can change once it is approved.`, 'RECORD_LOCKED', 409, ); diff --git a/test/opportunity-qualification-approval-gate.test.ts b/test/opportunity-qualification-approval-gate.test.ts index 98ec71e8..f9ba683d 100644 --- a/test/opportunity-qualification-approval-gate.test.ts +++ b/test/opportunity-qualification-approval-gate.test.ts @@ -191,16 +191,26 @@ const write = (input: AnyRec, previous: AnyRec, user: { id: string } | null = { const refusal = (input: AnyRec, previous: AnyRec) => write(input, previous).then(() => null, (e: AnyRec) => e); -/** The three acts step 11 holds until 立项 — 更新阶段、投标、赢丢单. */ +/** The two acts step 11 holds until 立项 — 更新阶段 and 赢丢单 (a direct close is a stage change). */ const HELD_ACTS: [string, AnyRec, string][] = [ ['a stage move', { stage: 'needs_analysis' }, 'Stage'], ['a direct close', { stage: 'closed_won', win_reason: 'best_fit' }, 'Stage'], - ['recording the bid decision (yes)', { will_bid: true }, 'Will Bid'], - ['recording the bid decision (no)', { will_bid: false }, 'Will Bid'], ['a won/lost request', { requested_status: 'closed_lost', loss_reason: 'price' }, 'Requested Status'], ]; -describe('the write path — opportunity_lifecycle holds three acts until 立项 is approved', () => { +/** + * Will Bid is RELEASED (#2004, the maintainer's ruling, option B). REQ-0006 + * step 8 has the rep fill 是否投标 as INPUT to 立项, for the approver to read; + * holding it until approval would have the approver decide without it. Step + * 11's 投标 is the act of bidding, which HotCRM does not model. + */ +const RELEASED_ACTS: [string, AnyRec, AnyRec][] = [ + ['recording the bid decision (yes)', { will_bid: true }, {}], + ['recording the bid decision (no)', { will_bid: false }, {}], + ['changing the bid decision', { will_bid: false }, { will_bid: true }], +]; + +describe('the write path — opportunity_lifecycle holds two acts until 立项 is approved', () => { for (const [shapeLabel, shape] of ARMED_SHAPES) { it.each(HELD_ACTS)(`refuses ${shapeLabel}: %s`, async (_l, input, named) => { const err = await refusal(input, shape); @@ -214,11 +224,25 @@ describe('the write path — opportunity_lifecycle holds three acts until 立项 }); } + for (const [shapeLabel, shape] of ARMED_SHAPES) { + it.each(RELEASED_ACTS)(`lets through, for a deal ${shapeLabel}: %s — input to 立项 (step 8)`, async (_l, input, prior) => { + await expect(write(input, { ...shape, ...prior })).resolves.toBeUndefined(); + }); + } + + it('names only what is still held when a write carries a held act beside Will Bid', async () => { + const err = await refusal({ stage: 'needs_analysis', will_bid: true }, { qualification_approval_status: 'pending' }); + expect(err?.code).toBe('RECORD_LOCKED'); + expect(err?.status).toBe(409); + expect(String(err?.message)).toContain('Stage'); + expect(String(err?.message)).not.toContain('Will Bid'); + }); + it.each(OFF_SHAPES)('lets every held act through for %s — today\'s behaviour, unchanged', async (_l, shape) => { for (const [, input] of HELD_ACTS) await expect(write(input, shape)).resolves.toBeUndefined(); }); - it('「新增商机可跟进」: an unqualified deal stays workable short of the three acts', async () => { + it('「新增商机可跟进」: an unqualified deal stays workable short of the two acts', async () => { const pending = { qualification_approval_status: 'pending', will_bid: true, requested_status: null }; await expect(write({ amount: 250, next_step: 'site visit', description: 'kick-off notes', @@ -263,7 +287,8 @@ describe('the write path — opportunity_lifecycle holds three acts until 立项 * `ok` = the write goes through; `立项` = refused by this gate; `status` = * refused by the step-14 gate. With only one gate armed the other is inert; * with both, 立项 answers first; once 立项 is approved the status-change - * gate behaves exactly as it does alone. + * gate behaves exactly as it does alone. The bid decision is held by neither + * (#2004): it is input to 立项, so it reads `ok` in every row. */ type Verdict = 'ok' | '立项' | 'status'; const ACTS: [string, AnyRec][] = [ @@ -276,15 +301,15 @@ const MATRIX: [string, AnyRec, Verdict[]][] = [ ['off/off', { qualification_approval_status: 'not_required', status_change_approval_status: 'not_required' }, ['ok', 'ok', 'ok', 'ok']], ['立项 on (pending) / status off', { qualification_approval_status: 'pending', status_change_approval_status: 'not_required' }, - ['立项', '立项', '立项', '立项']], + ['立项', 'ok', '立项', '立项']], ['立项 on (rejected) / status off', { qualification_approval_status: 'rejected', status_change_approval_status: 'not_required' }, - ['立项', '立项', '立项', '立项']], + ['立项', 'ok', '立项', '立项']], ['立项 approved / status off', { qualification_approval_status: 'approved', status_change_approval_status: 'not_required' }, ['ok', 'ok', 'ok', 'ok']], ['立项 off / status on', { qualification_approval_status: 'not_required', status_change_approval_status: 'pending' }, ['ok', 'ok', 'ok', 'status']], ['both on, before 立项', { qualification_approval_status: 'pending', status_change_approval_status: 'pending' }, - ['立项', '立项', '立项', '立项']], + ['立项', 'ok', '立项', '立项']], ['both on, 立项 approved', { qualification_approval_status: 'approved', status_change_approval_status: 'pending' }, ['ok', 'ok', 'ok', 'status']], ];