Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/2014-accept-quote-under-approval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'hotcrm': patch
---

Accepting a quote whose opportunity is still waiting on an approval is now refused with a message saying which approval must be granted first, a sales rep's accepted quote now gets its draft contract, a deal opened on a Settlement Only account is refused even for someone who cannot see that account (and the message no longer names the account), and a service agent's completed task or held call now refreshes the account's last activity date.
2 changes: 2 additions & 0 deletions content/docs/administration/state-machines.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ There are **two** closed stages, not three: *Closed Won* and *Closed Lost*. Ther

Two of those edges exist for the CPQ path rather than for a rep clicking through stages: *→ Proposal* is legal from every pre-proposal stage because the `quote_generation` flow fast-forwards the deal to *Proposal* the moment a quote is generated, which can happen at any open stage; and *Proposal → Closed Won* is legal because the `quote_on_accepted` hook wins the linked deal straight from an accepted quote, with no separate negotiation step.

That close is never the way around an approval. While an approval holds the deal — its large-deal approval pending, or, where they are switched on, its status-change or qualification approval pending or rejected — accepting the quote is itself refused, so an accepted quote never sits on a deal it could not close. See [Quotes](/docs/sales/quotes#built-in-rules).

**Not in the table**: moving **backwards**. No open stage declares an edge to an earlier one, so *Negotiation* back to *Proposal* — a deal slipping — is outside the route and logs a warning like any other departure (it still saves, per the note above). Neither closed stage declares an edge either, so there is no reopen transition and no admin path back to an open stage.

A closed opportunity is separately **frozen**, and by more than the stage field: the `opportunity_lifecycle` hook refuses any user edit to a *Closed Won* or *Closed Lost* record except to `description`, `next_step` and `notes`, and that refusal is a thrown error naming the fields attempted — not a warning. Approval verdicts and platform-managed columns are exempted so an in-flight approval can still land, and a write with no authenticated user (a seed or a backfill) is not guarded at all.
Expand Down
2 changes: 2 additions & 0 deletions content/docs/administration/state-machines.zh-Hans.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ Prospecting / Qualification / Needs Analysis / Proposal / Negotiation ──▶

其中两条边是为 CPQ 路径存在的,不是给销售一个阶段一个阶段点出来的:*→ Proposal* 从每一个 Proposal 之前的阶段都合法,因为 `quote_generation` 流程一旦生成报价就把交易快进到 *Proposal*,而生成报价在任何开放阶段都可能发生;*Proposal → Closed Won* 合法,则是因为 `quote_on_accepted` 钩子会从一份被接受的报价上直接赢下关联交易,中间不再经过一次谈判。

这一步成交永远不是绕过审批的路。只要审批还卡着这笔交易——大额审批在审批中,或者在启用了它们的部署里,状态变更审批、立项审批在审批中或已驳回——接受报价这一步本身就会被拒绝,所以不会出现一份已接受的报价挂在一笔关不掉的交易上。见[报价](/zh-Hans/docs/sales/quotes#内置规则)。

**表里没有的**:**向后** 移动。没有任何一个开放阶段声明了通往更早阶段的边,所以从 *Negotiation* 退回 *Proposal* —— 交易滑落 —— 是走出了这条路线,和其他任何一次出格一样只记一条警告(照样保存,见上文)。两个已关闭阶段也都没有出边,所以既不存在「重新打开」这条转换,也不存在管理员绕回开放阶段的通道。

已关闭的商机另有一道 **冻结**,而且管的远不止阶段字段:`opportunity_lifecycle` 钩子会拒绝用户对 *Closed Won* / *Closed Lost* 记录的任何编辑,只放行 `description`、`next_step` 和 `notes` 三个字段;而且这次拒绝是抛出错误、并点名你试图改的那些字段 —— 不是警告。审批结论和平台托管的列被排除在外,好让一次在途审批仍能落地;而没有登录用户的写入(种子数据或回填)根本不受这道冻结管辖。
Expand Down
2 changes: 2 additions & 0 deletions content/docs/administration/state-machines.zh-Hant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ Prospecting / Qualification / Needs Analysis / Proposal / Negotiation ──▶

其中兩條邊是為 CPQ 路徑存在的,不是給業務一個階段一個階段點出來的:*→ Proposal* 從每一個 Proposal 之前的階段都合法,因為 `quote_generation` 流程一旦產生報價就把交易快進到 *Proposal*,而產生報價在任何開放階段都可能發生;*Proposal → Closed Won* 合法,則是因為 `quote_on_accepted` 鉤子會從一份被接受的報價上直接贏下關聯交易,中間不再經過一次談判。

這一步成交永遠不是繞過審批的路。只要審批還卡著這筆交易——大額審批在審批中,或者在啟用了它們的部署裡,狀態變更審批、立項審批在審批中或已駁回——接受報價這一步本身就會被拒絕,所以不會出現一份已接受的報價掛在一筆關不掉的交易上。見[報價](/zh-Hant/docs/sales/quotes#內建規則)。

**表裡沒有的**:**向後** 移動。沒有任何一個開放階段宣告了通往更早階段的邊,所以從 *Negotiation* 退回 *Proposal* —— 交易滑落 —— 是走出了這條路線,和其他任何一次出格一樣只記一條警告(照樣儲存,見上文)。兩個已關閉階段也都沒有出邊,所以既不存在「重新開啟」這條轉換,也不存在管理員繞回開放階段的通道。

已關閉的商機另有一道 **凍結**,而且管的遠不止階段欄位:`opportunity_lifecycle` 鉤子會拒絕使用者對 *Closed Won* / *Closed Lost* 記錄的任何編輯,只放行 `description`、`next_step` 和 `notes` 三個欄位;而且這次拒絕是拋出錯誤、並點名你試圖改的那些欄位 —— 不是警告。審批結論和平台託管的欄位被排除在外,好讓一次在途審批仍能落地;而沒有登入使用者的寫入(種子資料或回填)根本不受這道凍結管轄。
Expand Down
2 changes: 1 addition & 1 deletion content/docs/sales/accounts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ When a contract is activated for a prospect, the system automatically promotes t

Tender agencies, resellers-of-record, intra-group billing entities and dormant accounts are the usual *Settlement Only* cases: you still pay them and get paid through them, you just do not sell to them.

This is a **gate, not a label**. Pick an account marked *Settlement Only* on a new opportunity and the save is refused, with a message naming the account and telling you what to do instead. Two things it deliberately does **not** do:
This is a **gate, not a label**. Pick an account marked *Settlement Only* on a new opportunity and the save is refused, with a message saying that this account is *Settlement Only* and telling you what to do instead. It applies to everyone, including people who cannot see the account itself — and because of that the message never names the account or repeats anything else about it; you chose it on the same form. Two things it deliberately does **not** do:

- **It never breaks history.** Opportunities that already point at the account keep working and stay editable, including after someone reclassifies the account. Only the *new* link is refused.
- **It is not a compliance lock.** It is a classification a person writes down, and the same person can set it back to *Full* when the relationship changes.
Expand Down
2 changes: 1 addition & 1 deletion content/docs/sales/accounts.zh-Hans.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ description: 你与之开展业务的公司——你的潜在客户、客户和

招标代理、名义经销商、集团内部结算主体和休眠客户,是典型的*仅结算*场景:钱照样通过它们收付,只是不向它们销售。

这是一道**闸门,不是一个标签**。在新建商机上选择被标为*仅结算*的客户,保存会被拒绝,并给出一条点名该客户、说明该怎么办的提示。它刻意**不**做两件事:
这是一道**闸门,不是一个标签**。在新建商机上选择被标为*仅结算*的客户,保存会被拒绝,提示会说明这个客户是*仅结算*,并告诉你该怎么办。这条规则对所有人都生效,包括看不到这个客户本身的人——也正因为如此,提示从不点名该客户,也不透露它的任何其他信息;它就是你在同一张表单上选中的那个客户。它刻意**不**做两件事:

- **它绝不破坏历史数据。** 已经挂在该客户上的商机照常可用、照常可编辑,即便之后有人把该客户改成*仅结算*也是如此。被拒绝的只有*新建*的关联。
- **它不是合规锁。** 它是由人填写的一项分类,同一个人在关系变化时也可以把它改回*完整*。
Expand Down
2 changes: 1 addition & 1 deletion content/docs/sales/accounts.zh-Hant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ description: 你與之開展業務的公司——你的潛在客戶、客戶和

招標代理、名義經銷商、集團內部結算主體和休眠客戶,是典型的*僅結算*場景:錢照樣透過它們收付,只是不向它們銷售。

這是一道**閘門,不是一個標籤**。在新建商機上選擇被標為*僅結算*的客戶,儲存會被拒絕,並給出一條點名該客戶、說明該怎麼辦的提示。它刻意**不**做兩件事:
這是一道**閘門,不是一個標籤**。在新建商機上選擇被標為*僅結算*的客戶,儲存會被拒絕,提示會說明這個客戶是*僅結算*,並告訴你該怎麼辦。這條規則對所有人都生效,包括看不到這個客戶本身的人——也正因為如此,提示從不點名該客戶,也不透露它的任何其他資訊;它就是你在同一張表單上選中的那個客戶。它刻意**不**做兩件事:

- **它絕不破壞歷史資料。** 已經掛在該客戶上的商機照常可用、照常可編輯,即便之後有人把該客戶改成*僅結算*也是如此。被拒絕的只有*新建*的關聯。
- **它不是合規鎖。** 它是由人填寫的一項分類,同一個人在關係變化時也可以把它改回*完整*。
Expand Down
5 changes: 3 additions & 2 deletions content/docs/sales/quotes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ On the quote's *Related* tab, the line items panel shows each line's product, qu
- **Discount cannot exceed 60%.** This is a hard ceiling, not a warning: a quote whose **Discount %** is above 60 is refused on save, with *"Discount cannot exceed 60%"*. There is no approval that unblocks it — see [Discount ceiling](#discount-ceiling) below for what to do instead. (A discount above 100% was never possible; the field itself only accepts 0–100.)
- **No single line may exceed 60% either.** The same ceiling, the same 60, applied to each line item's own **Discount %**: a line above it is refused on save with *"Line discount cannot exceed 60%"*. Both numbers are needed, because a quote's total runs them one after the other — each line is discounted first, then the quote's percentage comes off the sum — so a quote sitting innocently at 0% with every line at 90% off used to price 90% below list and clear the quote rule outright.
- **A quote cannot be Presented or Accepted without a contact.** A *Draft* or *In Review* quote needs no recipient — you often price a deal before knowing who signs it. But the write that moves it to *Presented* (or straight to *Accepted*) is refused unless **Contact** is filled in, and the message points at that field. Quotes that are never sent are unaffected: *Rejected* straight out of review, and the nightly sweep's *Expired*, both still go through without a contact.
- **A quote cannot be Accepted while an approval holds its opportunity.** Accepting a quote closes its deal as *Closed Won*, so the save that sets *Accepted* is refused while that deal is waiting on its large-deal approval (**Approval Status** is *Pending*) — or, where your administrator has switched them on, while its status-change or qualification approval is *Pending* or *Rejected* (**Status Change Approval**, **Qualification Approval**). The message names the approval that holds the deal. Nothing else moves: the quote stays *Presented*, the deal keeps its stage and no contract is drafted. Record the acceptance once that approval has been granted.

## What happens automatically

- **Daily expiration sweep** — every night at 1 AM, the system marks any quote whose expiration has passed as **Expired** (unless it's already accepted or rejected). Keeps your active list clean.
- **On acceptance** — marking a quote *Accepted* is what drafts the contract. The system inserts a contract in **Draft** status, typed *Subscription*, on a 12-month term starting today, carrying the quote's account, primary contact, related opportunity, owner and total price. It then advances the linked opportunity to *Closed Won*, dated today, and — only when the rep recorded no win reason — stamps **Quote Accepted** as one. An opportunity already closed, won or lost, is left where it is.
- **On acceptance** — marking a quote *Accepted* is what drafts the contract. The system inserts a contract in **Draft** status, typed *Subscription*, on a 12-month term starting today, carrying the quote's account, primary contact, related opportunity, owner and total price. The draft is the system's own write, so it is made whoever records the acceptance — a sales rep included, although a rep cannot create a contract by hand. It then advances the linked opportunity to *Closed Won*, dated today, and — only when the rep recorded no win reason — stamps **Quote Accepted** as one. An opportunity already closed, won or lost, is left where it is.

Two things readers often expect at this point happen **later, when someone activates that contract**, not when the quote is accepted:

Expand Down Expand Up @@ -114,7 +115,7 @@ To write the covering email inside HotCRM, use **Send Email** on the customer's

There are no accept or reject buttons either. The customer's decision is recorded the same way: **Edit** the quote, set **Status**, click **Update**.

- **Accepted** — the save that makes a quote *Accepted* is what drafts the contract and close-wins the deal, as described in [What happens automatically](#what-happens-automatically). From then on the quote is frozen: a change to any field except **Internal Notes** is refused, Status included.
- **Accepted** — the save that makes a quote *Accepted* is what drafts the contract and close-wins the deal, as described in [What happens automatically](#what-happens-automatically). While an approval holds the deal, that save is refused instead (see [Built-in rules](#built-in-rules)). From then on the quote is frozen: a change to any field except **Internal Notes** is refused, Status included.
- **Rejected** — nothing else happens. No contract is drafted and the opportunity keeps its stage, so you can quote again: set the rejected quote back to *Draft*, revise it and present it again, or click **Generate Quote** on the opportunity for a fresh one.

The row of statuses across the top of the quote shows where it stands. It is a display, not a set of buttons.
Expand Down
5 changes: 3 additions & 2 deletions content/docs/sales/quotes.zh-Hans.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,12 @@ Total Price = Subtotal − Discount + Tax + Shipping & Handling
- **到期日期必须晚于报价日期**——不能创建即过期的报价。
- **折扣不能超过 60%。** 这是一条硬上限,不是提醒:**折扣 %** 高于 60 的报价在保存时就会被拒绝,报错 *"Discount cannot exceed 60%"*。没有任何审批能放行它——该怎么办见下面的[折扣上限](#折扣上限)。(超过 100% 的折扣本来就不可能——字段本身只接受 0–100。)
- **单行也不能超过 60%。** 同一条上限、同一个 60,也作用在每个行项目自己的**折扣 %** 上:超限的行在保存时被拒绝,报错 *"Line discount cannot exceed 60%"*。两个数都得管,因为报价的总额是把它们**先后**用两次的——先按行折扣算出各行金额,加总之后再减去报价级的百分比。所以一份报价级折扣写着 0%、每一行却打一折的报价,实际就是一折成交,而报价级那条规则完全挑不出毛病。
- **商机被审批卡住时,报价不能被接受。** 接受一份报价就会把它的商机推进为*成交*,所以当这笔交易还在等大额审批(**审批状态**为*审批中*)时——或者在管理员启用了状态变更审批、立项审批的部署里,当**状态变更审批**或**立项审批**为*审批中*或*已驳回*时——把报价改为*已接受*的那次保存会被拒绝,报错会点名卡住这笔交易的那项审批(例如 *"Approval Status is Pending"*)。其他什么都不会变:报价仍是*已提交*,商机保持原阶段,也不会起草合同。等那项审批批准之后再记录接受。

## 自动发生的事

- **每日过期扫描**——每晚凌晨 1 点,系统会将任何到期日期已过的报价标记为**已过期**(除非它已被接受或拒绝)。保持你的活跃列表干净整洁。
- **接受时**——把一份报价标记为*已接受*,就是起草合同的那个动作。系统会插入一份*草稿*状态的合同,合同类型为*订阅合同*,期限 12 个月、从今天起算,带着报价上的所属客户、主要联系人、关联商机、负责人和总金额。随后它把关联的商机推进到*成交*,预计成交日期取今天;并且仅当销售代表没有填赢单原因时,才补上**报价被接受**。已经关闭的商机——不论成交还是失败——原样不动。
- **接受时**——把一份报价标记为*已接受*,就是起草合同的那个动作。系统会插入一份*草稿*状态的合同,合同类型为*订阅合同*,期限 12 个月、从今天起算,带着报价上的所属客户、主要联系人、关联商机、负责人和总金额。起草合同是系统自己的写入,所以不论是谁记录了接受都会起草——销售代表也一样,尽管销售代表不能亲手新建合同。随后它把关联的商机推进到*成交*,预计成交日期取今天;并且仅当销售代表没有填赢单原因时,才补上**报价被接受**。已经关闭的商机——不论成交还是失败——原样不动。

有两件事读者常以为在这一步就发生了,其实要等到**有人去激活那份合同**才发生,不在接受报价这一步:

Expand Down Expand Up @@ -111,7 +112,7 @@ HotCRM 不会替你发送报价。报价记录上没有发送按钮,应用既

接受和拒绝也都没有按钮。客户的决定用同样的方式记录:**编辑**报价,修改**状态**,点击**更新**。

- **已接受**——把报价改为*已接受*的那次保存,就是起草合同并把商机推进为成交的那个动作,详见上文的[自动发生的事](#自动发生的事)。此后这份报价被冻结:除**内部备注**以外,任何字段的修改都会被拒绝,状态也不例外。
- **已接受**——把报价改为*已接受*的那次保存,就是起草合同并把商机推进为成交的那个动作,详见上文的[自动发生的事](#自动发生的事)。商机被审批卡住时,这次保存会被拒绝(见[内置规则](#内置规则))。此后这份报价被冻结:除**内部备注**以外,任何字段的修改都会被拒绝,状态也不例外。
- **已拒绝**——不会引发任何其他变化。不会起草合同,商机保持原阶段,所以你可以重新报价:把这份被拒绝的报价改回*草稿*,修改之后再次提交;或者在商机上点击**生成报价单**,另生成一份新的。

报价顶部那一排状态只显示这份报价当前所处的位置,它是一个显示,不是一组按钮。
Expand Down
Loading
Loading