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
4 changes: 4 additions & 0 deletions .changeset/2024-quotes-approvals-phantom-buttons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Docs only, releases nothing. For maintainers: the Quotes page, in all three locales, no longer promises a **Send to Customer** button, **Mark Accepted** / **Mark Rejected** buttons or a quote PDF template, none of which the app has; it now says a quote's status is changed with **Edit** on the quote and what each status change does, and the Approvals page (en) names the pending label the app shows, *Pending*, instead of *In Approval* (#2024).
2 changes: 1 addition & 1 deletion content/docs/revenue/approvals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ A `> $500K` deal goes Manager → (decision gate) → Director. A `$100K–$500K
When a rep saves an opportunity whose amount reaches the threshold, the flow submits it automatically:

1. The opportunity is **locked** — the approval node holds the record until the step resolves.
2. `approval_status` flips to **pending** (shown as *In Approval*).
2. `approval_status` flips to **pending** (shown as *Pending*).
3. The first approver gets an in-app notification and email.

There's no separate "Submit for Approval" click — reaching the amount threshold on save is the trigger.
Expand Down
18 changes: 11 additions & 7 deletions content/docs/sales/quotes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,18 @@ The rep then reviews the draft, attaches the quote document (a PDF) to the quote

## Sending a quote to the customer

From the quote detail page, click **Send to Customer**. This sets the status to *Presented* and emails the PDF using a quote-cover-email template. The AI assistant can draft the cover email for you — *"Draft a cover note for this quote"*.
HotCRM does not send the quote for you. The quote record has no send button, and the app neither produces a quote PDF nor ships an email template for one. You send the document to the customer yourself, then record that it went out: on the quote, click **Edit**, set **Status** to *Presented* and click **Update**. That save is refused unless **Contact** is filled in (see [Built-in rules](#built-in-rules)), and it emails no one.

## Mark as Accepted / Rejected
To write the covering email inside HotCRM, use **Send Email** on the customer's contact record. You type the subject and body yourself, nothing is attached, and the message leaves only if your deployment has outgoing mail set up (see [Email & Calendar](/docs/guides/email-and-calendar)). The AI assistant can draft that text for you — *"Draft a cover note for this quote"* — and hands it back for you to paste; it does not send anything.

Two buttons on the detail screen close the loop:
## Recording the customer's answer

- **Mark Accepted** → status = Accepted; triggers the contract chain described above.
- **Mark Rejected** → status = Rejected; the opportunity stays open (you can re-quote at a lower price).
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.
- **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.

## Discount ceiling

Expand Down Expand Up @@ -149,14 +153,14 @@ A brand-new org has no quotes at all — they are generated from opportunities,
## Tips for sales reps

- ✅ Always include a discount reason in *Internal Notes* if you're discounting heavily.
- ✅ Use the **Generate Quote** button from an opportunity rather than creating quotes from scratch — it auto-fills everything.
- ✅ Use the **Generate Quote** button from an opportunity rather than creating quotes from scratch — it fills in the account, primary contact, dates, payment terms and opening price for you. The line items are yours to add.
- ✅ Check the validity (expiration date) matches your business cycle. Quarterly buyers need quotes valid till quarter-end.
- ❌ Don't change pricing on a Presented quote — it confuses the customer. Create a new revision instead.

## Tips for admins

- The daily expiration sweep runs at 1 AM. That time lives in source, not in Setup: it is the `schedule: '0 1 * * *'` on the start node of the `quote_expiration` flow (`src/revenue/flows/quote-expiration.flow.ts`). Changing it is a code change plus a redeploy — there is no setting to adjust on the quote object.
- The quote PDF template (cover and line item layout) is stored in your email-template settings.
- There is no quote PDF template to configure. HotCRM does not produce the quote document (the PDF a rep sends is made outside the app), and the one quote email template it ships is *Quote Created*, the notification a rep receives after **Generate Quote**.
- The Contact requirement is checked **when a quote moves into Presented or Accepted**, not on every save. A quote that was already sitting in *Presented* without a contact before this rule shipped keeps working normally — you can read and edit it, and filling the Contact in is an ordinary edit. It is the only case that can still reach *Accepted* without a recipient, and there it behaves as it always did: the contract cannot be drafted, and the reason lands in the server log. HotCRM's own demo data contains no such quote.
- The Discount ceiling is checked on **every save**, not only on a status change — the opposite boundary to the Contact requirement above, and deliberately so. "The discount may never exceed 60%" is an invariant; "a quote must have a recipient by the time it is presented" is a condition on a transition. The consequence worth knowing: a quote stored above the ceiling before this rule shipped would be frozen to every edit except lowering its discount, which is always allowed. HotCRM's own demo data contains no such quote — the deepest discount it seeds is 20%.
- The line-item ceiling behaves the same way and reaches the same legacy case: a line stored above 60 before the rule shipped is refused on its next edit until its discount comes down. One boundary is worth knowing: a validation is evaluated against the record being saved, so an over-ceiling *line* freezes that line, not its parent quote — the quote can still change status. Nothing HotCRM ships is in that state (the deepest line discount it seeds is 20%), and after this rule a line above the ceiling cannot be written at all.
Expand Down
20 changes: 12 additions & 8 deletions content/docs/sales/quotes.zh-Hans.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,20 @@ Total Price = Subtotal − Discount + Tax + Shipping & Handling

随后销售代表审核该草稿,把报价文件(一份 PDF)附到报价上,并在报价交给客户时将状态更改为*已提交*。

## 将报价发送给客户
## 将报价发送给客户 [#sending-a-quote-to-the-customer]

在报价详情页,点击**发送给客户**。这会将状态设为*已提交*,并使用报价封面邮件模板将 PDF 通过邮件发送。AI 助手可以为你起草封面邮件——*"Draft a cover note for this quote"*。
HotCRM 不会替你发送报价。报价记录上没有发送按钮,应用既不生成报价 PDF,也没有为它提供邮件模板。报价文件由你自己发给客户,发出之后再把这件事记下来:在报价上点击**编辑**,把**状态**改为*已提交*,再点击**更新**。报价进入*已提交*时必须已经填好**联系人**,否则这次保存会被拒绝;这次保存也不会给任何人发邮件。

## 标记为已接受 / 已拒绝
如果想在 HotCRM 里写随附的邮件,请在客户的联系人记录上使用**发送邮件**:主题和正文由你自己填写,不带任何附件,而且只有当你的部署配置了外发邮件时,邮件才会真正发出(见[邮件与日历](/zh-Hans/docs/guides/email-and-calendar))。AI 助手可以替你起草这段文字——*"Draft a cover note for this quote"*——起草好之后交还给你粘贴,它自己不会发送任何东西。

详情界面上的两个按钮可以闭合循环:
## 记录客户的答复 [#recording-the-customers-answer]

- **标记已接受** → 状态 = 已接受;触发上文所述的合同链。
- **标记已拒绝** → 状态 = 已拒绝;商机保持开放(你可以以更低的价格重新报价)。
接受和拒绝也都没有按钮。客户的决定用同样的方式记录:**编辑**报价,修改**状态**,点击**更新**。

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

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

## 折扣上限

Expand Down Expand Up @@ -146,14 +150,14 @@ Total Price = Subtotal − Discount + Tax + Shipping & Handling
## 给销售代表的提示

- ✅ 如果你大幅折扣,请始终在*内部备注*中包含折扣原因。
- ✅ 从商机使用**生成报价**按钮,而不是从头创建报价——它会自动填充所有内容。
- ✅ 从商机使用**生成报价单**按钮,而不是从头创建报价——它会替你填好客户、主要联系人、日期、付款条款和起始价格。行项目需要你自己添加。
- ✅ 检查有效期(到期日期)是否与你的业务周期相匹配。按季采购的买家需要有效期到季末的报价。
- ❌ 不要更改已提交报价上的定价——它会让客户困惑。改为创建一个新修订版。

## 给管理员的提示

- 每日过期扫描在凌晨 1 点运行。这个时间写在源码里,不在 Setup 界面上:它是 `quote_expiration` 流程(`src/revenue/flows/quote-expiration.flow.ts`)start 节点上的 `schedule: '0 1 * * *'`。要改就得改代码并重新部署——报价对象上没有可调的设置项。
- 报价 PDF 模板(封面和行项目布局)存储在你的邮件模板设置中。
- 没有可配置的报价 PDF 模板。HotCRM 不生成报价文件(销售代表发出去的 PDF 是在应用之外做好的),它随附的唯一一个与报价有关的邮件模板是*报价单已创建*,也就是销售代表点击**生成报价单**之后收到的那封通知。
- 折扣上限在**每一次保存**时都会检查,而不只是在状态变更时。要知道的后果:一份在这条规则上线之前就已经存着超限折扣的报价,会被冻结到只剩一种操作——把折扣调低,这一步永远是允许的。HotCRM 自己的演示数据里没有这样的报价——它种下的最深折扣是 20%。
- 行项目上的那条上限行为完全一样,也同样够得着历史数据:规则上线之前就存着超限折扣的行,在下一次编辑时会被拒绝,直到把折扣压下来为止。有一处边界值得知道:校验规则只对**正在保存的那条记录**求值,所以一条超限的*行*冻结的是这一行,不是它所属的报价——报价本身仍然可以变更状态。HotCRM 出厂的数据里没有这种行(它种下的最深行折扣是 20%),而且这条规则之后,超限的行根本写不进来。
- 上限的取值写在源码里,不在 Setup 界面上:它是 `src/sales/objects/_thresholds.ts` 里的 `QUOTE_DISCOUNT_CEILING`,同时被插值进**两条**规则的条件和报错信息——报价对象上那条,和报价行项目上那条。要改的数永远只有一个,改它就得改代码并重新部署。
Expand Down
20 changes: 12 additions & 8 deletions content/docs/sales/quotes.zh-Hant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,20 @@ Total Price = Subtotal − Discount + Tax + Shipping & Handling

隨後銷售代表審核該草稿,把報價文件(一份 PDF)附到報價上,並在報價交給客戶時將狀態更改為*已提交*。

## 將報價發送給客戶
## 將報價發送給客戶 [#sending-a-quote-to-the-customer]

在報價詳情頁,點擊**發送給客戶**。這會將狀態設為*已提交*,並使用報價封面郵件範本將 PDF 透過郵件發送。AI 助手可以為你起草封面郵件——*"Draft a cover note for this quote"*。
HotCRM 不會替你發送報價。報價記錄上沒有發送按鈕,應用既不產生報價 PDF,也沒有為它提供郵件範本。報價文件由你自己寄給客戶,寄出之後再把這件事記下來:在報價上點擊 **Edit**,把**狀態**改為*已提交*,再點擊 **Update**。報價進入*已提交*時必須已經填好**聯絡人**,否則這次儲存會被拒絕;這次儲存也不會寄郵件給任何人。

## 標記為已接受 / 已拒絕
如果想在 HotCRM 裡寫隨附的郵件,請在客戶的聯絡人記錄上使用**發送郵件**:主題和正文由你自己填寫,不帶任何附件,而且只有當你的部署設定了外寄郵件時,郵件才會真正寄出(見[郵件與行事曆](/zh-Hant/docs/guides/email-and-calendar))。AI 助手可以替你起草這段文字——*"Draft a cover note for this quote"*——起草好之後交還給你貼上,它自己不會寄出任何東西。

詳情介面上的兩個按鈕可以閉合循環:
## 記錄客戶的答覆 [#recording-the-customers-answer]

- **標記已接受** → 狀態 = 已接受;觸發上文所述的合約鏈。
- **標記已拒絕** → 狀態 = 已拒絕;商機保持開放(你可以以更低的價格重新報價)。
接受和拒絕也都沒有按鈕。客戶的決定用同樣的方式記錄:在報價上點擊 **Edit**,修改**狀態**,點擊 **Update**。

- **已接受**——把報價改為*已接受*的那次儲存,就是起草合約並把商機推進為成交的那個動作,詳見上文的[自動發生的事](#自動發生的事)。此後這份報價被凍結:除**內部備註**以外,任何欄位的修改都會被拒絕,狀態也不例外。
- **已拒絕**——不會引發任何其他變化。不會起草合約,商機保持原階段,所以你可以重新報價:把這份被拒絕的報價改回*草稿*,修改之後再次提交;或者在商機上點擊**生成報價單**,另生成一份新的。

報價頂部那一排狀態只顯示這份報價目前所處的位置,它是一個顯示,不是一組按鈕。

## 折扣上限

Expand Down Expand Up @@ -148,14 +152,14 @@ Total Price = Subtotal − Discount + Tax + Shipping & Handling
## 給銷售代表的提示

- ✅ 如果你大幅折扣,請始終在*內部備註*中包含折扣原因。
- ✅ 從商機使用**生成報價**按鈕,而不是從頭建立報價——它會自動填充所有內容。
- ✅ 從商機使用**生成報價單**按鈕,而不是從頭建立報價——它會替你填好客戶、主要聯絡人、日期、付款條款和起始價格。行項目需要你自己新增。
- ✅ 檢查有效期(到期日期)是否與你的業務週期相匹配。按季採購的買家需要有效期到季末的報價。
- ❌ 不要更改已提交報價上的定價——它會讓客戶困惑。改為建立一個新修訂版。

## 給管理員的提示

- 每日到期掃描在凌晨 1 點執行。這個時間寫在原始碼裡,不在 Setup 介面上:它是 `quote_expiration` 流程(`src/revenue/flows/quote-expiration.flow.ts`)start 節點上的 `schedule: '0 1 * * *'`。要改就得改程式碼並重新部署——報價物件上沒有可調的設定項。
- 報價 PDF 範本(封面和行項目版面)儲存在你的郵件範本設定中。
- 沒有可設定的報價 PDF 範本。HotCRM 不產生報價文件(銷售代表寄出去的 PDF 是在應用之外做好的),它隨附的唯一一個與報價有關的郵件範本是*報價單已創建*,也就是銷售代表點擊**生成報價單**之後收到的那封通知。
- 折扣上限在**每一次儲存**時都會檢查,而不只是在狀態變更時。要知道的後果:一份在這條規則上線之前就已經存著超限折扣的報價,會被凍結到只剩一種操作——把折扣調低,這一步永遠是允許的。HotCRM 自己的示範資料裡沒有這樣的報價——它種下的最深折扣是 20%。
- 行項目上的那條上限行為完全一樣,也同樣搆得著歷史資料:規則上線之前就存著超限折扣的行,在下一次編輯時會被拒絕,直到把折扣壓下來為止。有一處邊界值得知道:驗證規則只對**正在儲存的那筆記錄**求值,所以一條超限的*行*凍結的是這一行,不是它所屬的報價——報價本身仍然可以變更狀態。HotCRM 出廠的資料裡沒有這種行(它種下的最深行折扣是 20%),而且這條規則之後,超限的行根本寫不進來。
- 上限的取值寫在原始碼裡,不在 Setup 介面上:它是 `src/sales/objects/_thresholds.ts` 裡的 `QUOTE_DISCOUNT_CEILING`,同時被插值進**兩條**規則的條件和報錯訊息——報價物件上那條,和報價行項目上那條。要改的數永遠只有一個,改它就得改程式碼並重新部署。
Expand Down
Loading