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
27 changes: 27 additions & 0 deletions .changeset/2161-accepted-quote-frees-contact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'hotcrm': patch
---

A contact or account whose only quote is accepted can now be deleted

Handling a data-deletion request used to stop at the first quote the customer had accepted. Deleting
the contact, or the prospect account above it, was refused with *"… 1 active quote(s) … Close or
reassign the opportunities and quotes first"*, and neither could be done: an accepted quote is
settled and frozen.

Now both deletes go through. **The accepted quote stays, with its Contact cleared** and everything
else on it unchanged, the way a closed deal already keeps its record when its primary contact is
deleted. When the accepted quote sits under the account being deleted, it goes with that account, as
every quote under a deleted account does.

What still holds a contact is open work: an open opportunity or open quote naming them, or a
contract with them as its primary contact. The refusal now lists only what holds the contact, with
real plurals (*"1 open opportunity and 2 open quotes"*, not *"1 open opportunity(ies), 2 active
quote(s), 0 active contract(s)"*), and names only steps that free the contact: close the opportunity
or give it another primary contact, give the quote another contact or reject it, give the contract
another primary contact.

A quote's Contact is still required on the save that moves the quote to Presented or Accepted, and
cannot be emptied while the quote is Presented, so a contract is never drafted without a recipient.
The Contact field's help text now says that. *Import & Export › Data deletion requests* and *Quotes*
describe the new behaviour.
4 changes: 2 additions & 2 deletions content/docs/guides/import-and-export.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ None of it exists. There is no Privacy group in Setup, no data-subject-request o

What you can do today is delete or edit the records directly:

- **Delete the contact.** The event-attendee rows naming that person go with it; the meetings themselves stay, so the team's history does not develop holes.
- **Delete the account** and its contacts cascade with it. Check what else hangs off it first — that cascade is worth planning rather than discovering.
- **Delete the contact.** The event-attendee rows naming that person go with it; the meetings themselves stay, so the team's history does not develop holes. Settled records stay too, with the person cleared from them: a deal already won or lost, and a quote the customer accepted, rejected or let expire, keep everything but the contact. The delete is refused while an open opportunity or open quote names the person, or while a contract has them as its primary contact, and the message says what to change first.
- **Delete the account** and its contacts cascade with it, as do the quotes under it, accepted ones included. Check what else hangs off it first — that cascade is worth planning rather than discovering. A record that would hold one of its contacts, as above, refuses the account delete too.
- **Anonymise by hand** by clearing the identifying fields on the record you keep. There is no one-click equivalent, and no certificate comes out the other end.

## Backups & disaster recovery
Expand Down
4 changes: 2 additions & 2 deletions content/docs/guides/import-and-export.zh-Hans.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ description: 数据进出 HotCRM 真正落地的那部分——列表视图上

今天你能做的,是直接删除或编辑记录:

- **删除该联系人。** 指名这个人的参会人行会随之删除;会议本身保留,因此团队的历史不会因此出现空洞。
- **删除该客户**,它的联系人会级联删除。先看清还有什么挂在它下面——这条级联值得事先规划,而不是事后发现。
- **删除该联系人。** 指名这个人的参会人行会随之删除;会议本身保留,因此团队的历史不会因此出现空洞。已经了结的记录也会保留,只是把这个人从上面清掉:已经赢单或丢单的商机,以及客户已接受、已拒绝或已过期的报价,除联系人以外一切照旧。只要还有未结的商机或报价指名这个人,或者还有合同以他为主要联系人,删除就会被拒绝,报错会说明先要改什么。
- **删除该客户**,它的联系人会级联删除,挂在它下面的报价也一样,已接受的也不例外。先看清还有什么挂在它下面——这条级联值得事先规划,而不是事后发现。上面那些会挡住联系人的记录,同样会挡住客户的删除。
- **手工匿名化**:在你要保留的那条记录上清空可识别字段。没有一键等价物,末尾也不会产出任何证书。

## 备份与灾难恢复
Expand Down
4 changes: 2 additions & 2 deletions content/docs/guides/import-and-export.zh-Hant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ description: 資料進出 HotCRM 真正落地的那部分——清單檢視上

今天你能做的,是直接刪除或編輯記錄:

- **刪除該聯絡人。** 指名這個人的與會人列會隨之刪除;會議本身保留,因此團隊的歷史不會因此出現空洞。
- **刪除該客戶**,它的聯絡人會級聯刪除。先看清還有什麼掛在它下面——這條級聯值得事先規劃,而不是事後發現。
- **刪除該聯絡人。** 指名這個人的與會人列會隨之刪除;會議本身保留,因此團隊的歷史不會因此出現空洞。已經了結的記錄也會保留,只是把這個人從上面清掉:已經贏單或丟單的商機,以及客戶已接受、已拒絕或已過期的報價,除聯絡人以外一切照舊。只要還有未結的商機或報價指名這個人,或者還有合約以他為主要聯絡人,刪除就會被拒絕,報錯會說明先要改什麼。
- **刪除該客戶**,它的聯絡人會級聯刪除,掛在它下面的報價也一樣,已接受的也不例外。先看清還有什麼掛在它下面——這條級聯值得事先規劃,而不是事後發現。上面那些會擋住聯絡人的記錄,同樣會擋住客戶的刪除。
- **手動匿名化**:在你要保留的那筆記錄上清空可識別欄位。沒有一鍵等價物,末尾也不會產出任何憑證。

## 備份與災難復原
Expand Down
6 changes: 3 additions & 3 deletions content/docs/sales/quotes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ On the quote's *Related* tab, the line items panel shows each line's product, qu
- **Expiration date must be after the quote date** — no expired-on-creation quotes.
- **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 *Expired* (which the nightly expiry sweep sets where it runs), both still go through without a contact.
- **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; while a quote is *Presented*, Contact cannot be emptied either. Once a quote is *Accepted* the requirement has done its job — the contract was drafted from that save — so deleting the person later, for a data-deletion request say, leaves the accepted quote in place with **Contact** cleared. Quotes that are never sent are unaffected: *Rejected* straight out of review, and *Expired* (which the nightly expiry sweep sets where it runs), 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
Expand Down Expand Up @@ -117,7 +117,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). 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. Its line items are frozen with it: a line can no longer be added, changed or removed, and the same holds for an *Expired* quote. Deleting the whole quote is not affected, and its lines go with it.
- **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. The one change it still takes is a link being emptied: when the contact or opportunity it names is deleted, the quote stays and that field is cleared. Its line items are frozen with it: a line can no longer be added, changed or removed, and the same holds for an *Expired* quote. Deleting the whole quote is not affected, and its lines go with it.
- **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 Expand Up @@ -164,7 +164,7 @@ A brand-new org has no quotes at all — they are generated from opportunities,

- The daily expiration sweep runs at 1 AM, and only on a deployment that has switched scheduled work on: a default install boots with the `quote_expiration` flow unbound, and the switch is described in [Administration › Automation](/docs/administration/automation#scheduled-automation). The 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.
- 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 Contact requirement is checked **when a quote moves into Presented or Accepted** (and, while it is Presented, on any save that would empty it), 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.
- The ceiling value lives in source, not in Setup: it is `QUOTE_DISCOUNT_CEILING` in `src/sales/objects/_thresholds.ts`, interpolated into the condition and the message of *both* rules — the one on the quote object and the one on the quote line item. There is only ever one number to change, and changing it is a code change plus a redeploy.
Expand Down
2 changes: 1 addition & 1 deletion content/docs/sales/quotes.zh-Hans.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ HotCRM 不会替你发送报价。报价记录上没有发送按钮,应用既

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

- **已接受**——把报价改为*已接受*的那次保存,就是起草合同并把商机推进为成交的那个动作,详见上文的[自动发生的事](#自动发生的事)。商机被审批卡住时,这次保存会被拒绝(见[内置规则](#内置规则))。此后这份报价被冻结:除**内部备注**以外,任何字段的修改都会被拒绝,状态也不例外。它的行项目也随之冻结:不能再新增、修改或删除任何一行,*已过期*的报价同样如此。删除整份报价不受影响,它的行项目会随之一起删除。
- **已接受**——把报价改为*已接受*的那次保存,就是起草合同并把商机推进为成交的那个动作,详见上文的[自动发生的事](#自动发生的事)。商机被审批卡住时,这次保存会被拒绝(见[内置规则](#内置规则))。此后这份报价被冻结:除**内部备注**以外,任何字段的修改都会被拒绝,状态也不例外。唯一还能落地的改动是清空一条链接:它指名的联系人或商机被删除时,报价保留,那一栏被清空。它的行项目也随之冻结:不能再新增、修改或删除任何一行,*已过期*的报价同样如此。删除整份报价不受影响,它的行项目会随之一起删除。
- **已拒绝**——不会引发任何其他变化。不会起草合同,商机保持原阶段,所以你可以重新报价:把这份被拒绝的报价改回*草稿*,修改之后再次提交;或者在商机上点击**生成报价单**,另生成一份新的。

报价顶部那一排状态只显示这份报价当前所处的位置,它是一个显示,不是一组按钮。
Expand Down
2 changes: 1 addition & 1 deletion content/docs/sales/quotes.zh-Hant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ HotCRM 不會替你發送報價。報價記錄上沒有發送按鈕,應用既

接受和拒絕也都沒有按鈕。客戶的決定用同樣的方式記錄:在報價上點擊 **Edit**,修改**狀態**,點擊 **Update**。

- **已接受**——把報價改為*已接受*的那次儲存,就是起草合約並把商機推進為成交的那個動作,詳見上文的[自動發生的事](#自動發生的事)。商機被審批卡住時,這次儲存會被拒絕(見[內建規則](#內建規則))。此後這份報價被凍結:除**內部備註**以外,任何欄位的修改都會被拒絕,狀態也不例外。它的行項目也隨之凍結:不能再新增、修改或刪除任何一行,*已過期*的報價同樣如此。刪除整份報價不受影響,它的行項目會隨之一起刪除。
- **已接受**——把報價改為*已接受*的那次儲存,就是起草合約並把商機推進為成交的那個動作,詳見上文的[自動發生的事](#自動發生的事)。商機被審批卡住時,這次儲存會被拒絕(見[內建規則](#內建規則))。此後這份報價被凍結:除**內部備註**以外,任何欄位的修改都會被拒絕,狀態也不例外。唯一還能落地的改動是清空一條連結:它指名的聯絡人或商機被刪除時,報價保留,那一欄被清空。它的行項目也隨之凍結:不能再新增、修改或刪除任何一行,*已過期*的報價同樣如此。刪除整份報價不受影響,它的行項目會隨之一起刪除。
- **已拒絕**——不會引發任何其他變化。不會起草合約,商機保持原階段,所以你可以重新報價:把這份被拒絕的報價改回*草稿*,修改之後再次提交;或者在商機上點擊**生成報價單**,另生成一份新的。

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