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
51 changes: 51 additions & 0 deletions .changeset/contracts-docs-residual-drift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
'hotcrm': patch
---

Rewrite the three remaining fabricated passages on the Contracts page against the
flows and the object that actually run, in all three languages. #805 fixed only
the activation section; these three were on the same page, and none of them was
caused by that change.

**The expiration sweep sends one notification, not two.** The loop body of
`src/flows/contract-expiration.flow.ts` holds exactly two nodes — `mark_expired`
and `notify_owner`, whose recipient list is `{currentContract.owner_id}` alone.
Nothing in the flow reads the account's owner, so the page's third step
("Notification to the account owner") described a message nobody ever receives —
the failure mode where an account owner believes they will be told a contract has
lapsed. The page now says the contract owner is the only recipient and that
anyone else has to be arranged by an admin, and adds the two facts that decide
whether a given contract is touched at all: the sweep reads *Activated* contracts
only, and takes at most 500 per run.

**The renewal reminder has no one-click action, and it does more than remind.**
`notify_owner` in `src/flows/contract-renewal.flow.ts` carries
`actionUrl: '/crm_contract/{currentContract.id}'` — the contract detail page.
There is no "Create Renewal Opportunity" button on it, and opening it creates
nothing; the renewal opportunity is created *by the flow itself*, unprompted, when
`auto_renewal` is on and the account has no open renewal deal. The section now
lists what the sweep does in the order it does it — the renewal task it files
first (which the page never mentioned), the notification, and the conditional
renewal opportunity with the values it copies — plus the idempotency the task
provides: an open *Renewal due* task means the day is already handled, and
completing it lets the next morning file a fresh one. The 120-day look-ahead is
written down too, because it caps notice windows set longer than that.

**Four field names on the record table do not exist.** `activation_date`,
`renewal_terms`, `account_exec` and `order_form` are each zero hits under `src/`.
Following #792, the table is rebuilt from the six `fieldGroups` declared in
`src/objects/contract.object.ts`, so it is now the full inventory rather than an
invented one, with the required fields called out. The four absent names are
addressed head-on rather than quietly dropped, since a reader who went looking for
them deserves to know where the capability really lives: activation stamps
**Signed Date**, renewal instructions belong in **Special Terms**, ownership is the
single **Contract Owner** lookup, and **Contract Document** is a URL — the executed
PDF is an attachment on the record, and there is no *Order Form* field.
*Renewal Terms* was recommended a second time further down the same page, in the
Renewals section and in the tips for contract owners; both now point at Special
Terms. The zh pages take **计费周期 / 計費週期** for billing frequency from the
locale pack, matching the field-group headings the table now uses.

Documentation only — no metadata, behaviour or field changes. Whether Contract
*should* carry an activation date, a renewal-terms field, an account exec or a
file-typed contract document is a product decision and stays open. Fixes #826.
60 changes: 42 additions & 18 deletions content/docs/revenue/contracts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,26 @@ Activation is the key transition — it triggers the account to be promoted to *

## What a contract record stores

| Section | Fields |
A contract record declares its fields in **six groups**. The contract form shows a curated subset of them across its tabs, so read this as the full inventory of what a contract carries rather than a screen-by-screen map:

| Group | What's in it |
| --- | --- |
| **Parties & Type** | Contract number *(auto)*, account, type, opportunity *(if originated from a deal)* |
| **Status & Dates** | Status, start date, end date, signed date, activation date |
| **Financials** | Contract value, billing frequency, payment terms |
| **Renewal** | Auto-renewal flag, renewal notice (days), renewal terms |
| **Ownership** | Contract owner, account exec |
| **Documents** | Signed contract (file), order form (file) |
| **Contract Information** | Contract Number *(auto-numbered `CTR-0001`)*, Contract Type, Contract Document, Description |
| **Parties** | Contract Owner, Account, Primary Contact, Related Opportunity — the contact and opportunity pickers are scoped to the account you pick first |
| **Terms & Dates** | Contract Term (Months), Start Date, End Date, Special Terms |
| **Contract Value** | Contract Value, Billing Frequency, Payment Terms, Billing Address |
| **Status & Approval** | Status, Signed Date, Signed By |
| **Renewal** | Auto Renewal, Renewal Notice (Days) |

Account, Primary Contact, Contract Term, Start Date, End Date, Contract Value and Status are all **required** — a contract cannot be saved without them.

Three field names people go looking for and do not find:

- **Activation Date.** There is none, and no other field records the moment of activation. Activation stamps **Signed Date** instead (see *On contract activation* below), so that is the date any "when did this go live" report has to read.
- **Renewal Terms.** Renewal is exactly the two fields above. Free-text renewal instructions belong in **Special Terms**, which travels with the contract.
- **Account Exec.** Ownership is the single **Contract Owner** lookup; a contract carries no second ownership field.

**Contract Document** is a **link**, not an upload — it holds a URL pointing at wherever the document lives. The executed PDF itself goes on the record as an **attachment**; files are enabled on Contract, which is what makes that possible. There is no separate *Order Form* field.

## Billing frequency

Expand All @@ -63,13 +75,14 @@ When the customer pays:

## Renewals

Three fields drive renewals:
Two fields drive renewals:

- **Auto Renewal** — off unless you turn it on. With it on, the daily renewal sweep opens the renewal deal for you; with it off, the sweep still reminds you but creates no deal.
- **Renewal Notice (Days)** — how many days before the end date the reminder fires. Defaults to 30, and each contract carries its own.

- **Auto-Renew** — does the contract roll over automatically?
- **Renewal Notice Days** — how many days before end-date to alert the team (e.g., 60 days)
- **Renewal Terms** — free-text instructions ("renew at 5% uplift unless customer opts out")
There is no *Renewal Terms* field. Instructions like "renew at 5% uplift unless the customer opts out" go in **Special Terms**, the free-text field on the contract itself.

When the contract enters its renewal window, the system creates a follow-up task and emails the contract owner.
When a contract enters its renewal window, the system files a follow-up task and emails the contract owner — see *Daily — renewal reminder* below for everything it creates.

## What the system does automatically

Expand All @@ -84,14 +97,25 @@ Nothing is sent and nobody is told. Activation raises no notification for the co
Both writes run *after* the save, with failures logged rather than raised, so the contract stays activated even if promoting the account fails. They also hang off an **update** — a contract that arrives already *Activated* (a data import, seed data, an integration that writes the record in one shot) triggers neither, and its account stays a *Prospect*.

### Daily — expiration sweep
Each night at midnight the system finds *Activated* contracts whose end date has passed:
Each night at midnight the system finds *Activated* contracts whose end date has passed, and does two things to each — only these two:

1. **Status → Expired**.
2. **Notification** to the contract owner.
3. **Notification** to the account owner.
2. **Notification to the contract owner** — inbox and email, linking to the contract.

Nobody else is told. There is no second notification: the account owner, the primary contact and the rest of the account team hear nothing when a contract lapses, so if anyone beyond the contract owner needs to know, an admin has to add that. The sweep also reads *Activated* contracts only — one still sitting in *Draft* or *In Approval* past its end date is left alone — and it takes at most 500 contracts per run.

### Daily — renewal reminder
At 8 AM the system finds contracts entering their renewal window (within `renewal_notice_days` of end date) and sends a reminder to the contract owner with a one-click action to "Create Renewal Opportunity".
At 8 AM the system finds *Activated* contracts whose end date falls inside that contract's own *Renewal Notice (Days)* window, and for each one, in this order:

1. **Files a renewal task** for the contract owner — *"Renewal due: contract CTR-0042"*, a high-priority follow-up due on the contract's end date, filed against the account.
2. **Notifies the contract owner** — inbox and email. The notification links to the contract record, and that is all it does: there is no one-click action on it, and opening it creates nothing.
3. **Opens a renewal opportunity** — but only when *Auto Renewal* is on. An *Existing Customer - Renewal* deal on the same account, at *Proposal* stage, amount copied from the contract value, close date set to the contract's end date, owner copied from the contract, next step *"Confirm renewal terms with customer"*. It is skipped when that account already has an open renewal deal.

So the renewal deal is never something you click for. With *Auto Renewal* on it is already in the pipeline by the time you read the reminder; with it off, nobody opens one for you and the task is the whole of it.

The task is also what keeps the sweep from repeating itself: the same contract matches every morning of its notice window, and an open *Renewal due* task for it means the day is already handled — no second task, no second notification, no second deal. Complete that task and the next morning's sweep files a fresh one, for as long as the contract is still inside its window.

Two bounds worth knowing: the sweep looks 120 days ahead at most, so a notice window set longer than that only starts firing at 120 days out; and it takes at most 500 contracts per run.

### Manual termination
The owner can change status to *Terminated*. This is a one-way action — terminated contracts can't be reactivated. Audit log records who and when.
Expand Down Expand Up @@ -123,8 +147,8 @@ If your org wants contracts to follow the account, an admin has to author that,
## Tips for contract owners

- ✅ **Attach the signed PDF** when activating — auditors will thank you.
- ✅ **Set Renewal Notice Days = 90** for enterprise deals — gives you a full quarter to negotiate.
- ✅ **Use Renewal Terms field** to capture the deal — "price-locked for 3 years; 5% uplift on renewal".
- ✅ **Set Renewal Notice (Days) = 90** for enterprise deals — gives you a full quarter to negotiate, and the sweep looks far enough ahead to honour it.
- ✅ **Record what you agreed in Special Terms** — "price-locked for 3 years; 5% uplift on renewal". There is no *Renewal Terms* field, and Special Terms is the note that will still be on the contract when the reminder lands.

## Tips for sales reps

Expand Down
64 changes: 44 additions & 20 deletions content/docs/revenue/contracts.zh-Hans.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,28 @@ description: 与客户签署的协议——条款、价值、续约,以及自

## 合同记录存储的内容

| 区块 | 字段 |
合同记录把自己的字段声明为 **6 个区块**。合同表单只把其中一部分挑进各个标签页,所以下表是一份合同携带的字段全集,而不是逐屏对照表:

| 区块 | 其中内容 |
| --- | --- |
| **Parties & Type** | 合同编号 *(自动)*、账户、类型、商机 *(如果源自某笔交易)* |
| **Status & Dates** | 状态、开始日期、结束日期、签署日期、激活日期 |
| **Financials** | 合同价值、计费频率、付款条款 |
| **Renewal** | 自动续约标记、续约通知(天数)、续约条款 |
| **Ownership** | 合同所有者、客户经理 |
| **Documents** | 已签署合同(文件)、订单表(文件) |
| **合同信息** | 合同编号 *(自动编号 `CTR-0001`)*、合同类型、合同文档、描述 |
| **签约方** | 合同负责人、所属客户、主要联系人、关联商机——联系人和商机的选择器都按你先选定的客户过滤 |
| **条款与日期** | 合同期限(月)、开始日期、结束日期、特殊条款 |
| **合同金额** | 合同金额、计费周期、付款条款、账单地址 |
| **状态与审批** | 状态、签署日期、签署人 |
| **续约** | 自动续约、续约通知(天) |

所属客户、主要联系人、合同期限、开始日期、结束日期、合同金额和状态都是**必填**的——缺一项合同就存不下来。

有三个字段名读者会去找,但它们并不存在:

## 计费频率
- **激活日期**。没有这个字段,也没有别的字段记录激活的那一刻。激活时系统盖的是**签署日期**(见下文*合同激活时*),所以"什么时候生效的"这类报表只能读它。
- **续约条款**。续约就是上面那两个字段。自由文本的续约说明写进**特殊条款**——它跟着合同一起走。
- **客户经理**。所有权只有**合同负责人**这一个查找字段,合同上没有第二个负责人字段。

**合同文档**是一个**链接**,不是上传——它存的是指向文档实际所在位置的 URL。已签署的 PDF 本身是作为**附件**挂在记录上的;Contract 对象启用了文件,这才使它成为可能。也没有单独的*订单表*字段。

## 计费周期

客户被开票的频率:

Expand All @@ -63,13 +75,14 @@ description: 与客户签署的协议——条款、价值、续约,以及自

## 续约

三个字段驱动续约:
两个字段驱动续约:

- **自动续约**——默认关闭,要你自己打开。打开后,每日续约扫描会替你开出续约商机;关着的时候,扫描照样提醒你,但不会建任何商机。
- **续约通知(天)**——在结束日期前多少天触发提醒。默认 30 天,而且每份合同各带各的。

- **Auto-Renew**——合同是否自动滚动续约?
- **Renewal Notice Days**——在结束日期前多少天提醒团队(例如 60 天)
- **Renewal Terms**——自由文本说明("续约时上调 5%,除非客户选择退出")
没有*续约条款*字段。"续约时上调 5%,除非客户选择退出"这类说明写进**特殊条款**——合同自己身上的那个自由文本字段。

当合同进入其续约窗口时,系统会创建一个跟进任务并向合同所有者发送邮件。
当合同进入其续约窗口时,系统会建一条跟进任务并向合同负责人发邮件——它到底建了些什么,见下文*每日——续约提醒*。

## 系统自动做的事

Expand All @@ -84,14 +97,25 @@ description: 与客户签署的协议——条款、价值、续约,以及自
这两次写入都在保存**之后**执行,失败只记日志、不向上抛,所以即使账户提升失败,合同也已经是激活状态。它们还只挂在**更新**上——创建时就已经是 *Activated* 的合同(数据导入、种子数据、一次性写入整条记录的集成)两次都不会触发,其账户仍然停留在潜在客户。

### 每日——到期扫描
每晚午夜,系统会查找结束日期已过的 *Activated* 合同:
每晚午夜,系统会查找结束日期已过的 *Activated* 合同,并对每一份做两件事,也只有这两件:

1. **状态 → Expired**。
2. 向合同所有者发送**通知**。
3. 向账户所有者发送**通知**。
2. 向**合同负责人**发送**通知**——站内消息加邮件,链接指向这份合同。

别的人一概不会被告知。这里没有第二条通知:合同到期时,客户负责人、主要联系人以及客户团队的其他人什么都收不到,所以如果合同负责人之外还有人需要知道,得由管理员另行配置。这次扫描也只看 *Activated* 的合同——过了结束日期却还停在 *Draft* 或 *In Approval* 的合同不会被动——而且每次运行最多处理 500 份合同。

### 每日——续约提醒
每天上午 8 点,系统会查找进入其续约窗口的合同(在结束日期的 `renewal_notice_days` 范围内),并向合同所有者发送一条提醒,带一个一键操作"创建续约商机"。
每天上午 8 点,系统会查找结束日期落在这份合同自己的*续约通知(天)*窗口内的 *Activated* 合同,并按以下顺序对每一份:

1. **给合同负责人建一条续约任务**——*"Renewal due: contract CTR-0042"*,一条高优先级的跟进任务,到期日就是合同的结束日期,归属在该客户名下。
2. **通知合同负责人**——站内消息加邮件。通知里的链接指向合同记录,也仅此而已:上面没有任何一键操作,点开它不会创建任何东西。
3. **开一笔续约商机**——但仅当*自动续约*打开时。在同一个客户下开一笔 *Existing Customer - Renewal* 商机,阶段为 *Proposal*,金额从合同金额复制,关闭日期取合同的结束日期,负责人从合同复制,下一步写 *"Confirm renewal terms with customer"*。如果该客户下已经有一笔在途的续约商机,这一步会被跳过。

所以续约商机从来不是你点出来的。*自动续约*开着时,你读到提醒的时候它已经在管道里了;关着时,没有人替你开,提醒和那条任务就是全部。

那条任务同时也是扫描不重复自己的原因:续约窗口内的每天早上,同一份合同都会被匹配上,而只要它还挂着一条未完成的 *Renewal due* 任务,这一天就算已经处理过了——不会有第二条任务、第二条通知、第二笔商机。把这条任务标记完成,只要合同仍在窗口内,第二天早上的扫描就会再建一条新的。

两个值得知道的边界:扫描最多往前看 120 天,所以续约通知天数设得比 120 更长时,它也要等到 120 天这个点上才开始触发;每次运行最多处理 500 份合同。

### 手动终止
所有者可以将状态更改为 *Terminated*。这是一个单向操作——已终止的合同无法重新激活。审计日志记录谁在何时操作。
Expand Down Expand Up @@ -123,8 +147,8 @@ description: 与客户签署的协议——条款、价值、续约,以及自
## 给合同所有者的提示

- ✅ **激活时附上已签署的 PDF**——审计人员会感谢你。
- ✅ **为企业级交易设置 Renewal Notice Days = 90**——给你整整一个季度去谈判。
- ✅ **使用 Renewal Terms 字段**来记录交易——"价格锁定 3 年;续约时上调 5%"。
- ✅ **为企业级交易把「续约通知(天)」设为 90**——给你整整一个季度去谈判,而且扫描往前看得足够远,撑得住这个设置。
- ✅ **把谈定的续约约定写进特殊条款**——"价格锁定 3 年;续约时上调 5%"。没有*续约条款*字段,而提醒到达时,特殊条款仍然写在这份合同上。

## 给销售代表的提示

Expand All @@ -134,4 +158,4 @@ description: 与客户签署的协议——条款、价值、续约,以及自
## 给管理员的提示

- 合同激活是一个**对象 hook**(`src/objects/contract.hook.ts`),不是 flow——盖签署日期和提升账户都写在那里,流程列表里找不到它。到期扫描和续约提醒**才是**定时 flow。参见 [管理 › 自动化](/zh-Hans/docs/administration/automation)。
- 合同类型和计费频率选项列表是可扩展的。
- 合同类型和计费周期选项列表是可扩展的。
Loading
Loading