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
37 changes: 37 additions & 0 deletions .changeset/quotes-and-contracts-follow-the-account.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
'hotcrm': minor
---

**Quotes and contracts now follow the account** (#549). Both `crm_quote` and
`crm_contract` move from `sharingModel: 'private'` to `controlled_by_parent`
under `crm_account`, with the account lookup promoted to master-detail — the
same derivation contacts already use. A rep who receives an account through a
territory or team sharing rule now sees that account's *Quotes* and *Contracts*
related lists complete, renewals included, instead of the own-only keyhole this
issue reported. Access is computed per account, at every level: a quote under
an account you cannot see stays invisible, and so do its line items (measured
on `@objectstack/*` 17.4.0, which carries the two-level chain fix for
objectstack-ai/objectstack#11082 — the reason this change waited).

FROM → TO, per persona:

- **Sales Rep** — reads the quotes and contracts of every account they can see
(own, territory-shared, team-shared, manually shared); edits the quotes of
accounts they hold edit on (a territory share carries edit); still never
edits a contract (the object-level right is unchanged). The inert
`readScope: 'own'` on both objects is gone.
- **Sales Manager** — edits every contract, on every edition: the write gate
now asks for edit on the account, which the manager holds org-wide. The #880
hierarchy write depth (`own_and_reports`, Enterprise-only) is no longer
declared because it is inert on a parent-derived object; the
`hierarchy-security` capability declaration stays.
- **Tasks, events, cases and opportunities** stay own-only, as ruled.

An account delete would now cascade its quotes and contracts (master-detail),
so `account_protection` refuses to delete a customer account while an
*Activated* contract still references it — `Cannot delete customer account: 1
activated contract still references it. Terminate or reassign it first.` —
alongside the existing open-opportunity refusal. An account carrying only
drafts or ended contracts is still refused by the platform's own referential
pass (the contact cascade stops on the contract's required Primary Contact), so
in practice an account with contracts cannot be deleted until they are gone.
5 changes: 3 additions & 2 deletions content/docs/administration/profiles.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ Permission sets are **explicit-allow only**: if an object is not listed in a set

### 💼 Sales Representative

- Create, read and edit their **own** leads, accounts, opportunities, quotes and tasks
- Create, read and edit their **own** leads, accounts, opportunities and tasks
- **Quotes**: the quotes on accounts the rep can see, and edit on the quotes of accounts they can edit — Quote is Controlled by Parent under the account since #549, the same derivation as Contact below
- **Contacts**: the contacts on accounts the rep can see — their own, plus every account a territory rule or manual share put in reach. Contact is Controlled by Parent, and that derivation follows the account's sharing (see [Sharing & security](/docs/administration/sharing-and-security), *Controlled by Parent, in practice*)
- **Opportunity and quote line items** — full control, which is what the "Products" related list and quote building need. Controlled by Parent again, so this reaches the lines of the deals and quotes the rep can see, and no others
- Read the **product** catalog, **campaigns**, **campaign members** and **knowledge articles**
- Read their **own forecast snapshots** — a rep never writes one. The nightly forecast job opens and refreshes them, and a manager or an admin may also enter one by hand; a hand-entered current-quarter row stands that nightly job down for the owner and period until the row is deleted (see [Forecasting](/docs/sales/forecasting))
- Read their own **contracts** and **cases**; deleting is a manager privilege (their own tasks excepted)
- Read the **contracts** of every account they can see (Contract is Controlled by Parent under the account) and their own **cases**; deleting is a manager privilege (their own tasks excepted)

### 🎧 Service Agent

Expand Down
5 changes: 3 additions & 2 deletions content/docs/administration/profiles.zh-Hans.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ description: 六种内置权限集,以及每一种能查看、编辑和执行

### 💼 Sales Representative

- 创建、读取和编辑 **自己的** 线索、客户、商机、报价和任务
- 创建、读取和编辑 **自己的** 线索、客户、商机和任务
- **报价**:代表能看到的那些客户下面的报价,并且能编辑自己有编辑权的客户下的报价 —— 自 #549 起 Quote 是客户之下的“由父级控制”,与下面的联系人是同一种派生
- **联系人**:代表能看到的那些客户下面的联系人 —— 自己的客户,加上区域规则或手工共享送到手上的每一个客户。Contact 是“由父级控制”,而这份派生跟随客户的共享(参见[共享与安全](/zh-Hans/docs/administration/sharing-and-security),*“由父级控制”在实践中*)
- **商机行项和报价行项** —— 完整权限,这正是“Products”相关列表和报价编制所需要的。它们同样是“由父级控制”,因此这份权限达到的是代表能看到的那些交易和报价的行项,别的都到不了
- 读取 **产品** 目录、**营销活动**、**营销活动成员** 和 **知识文章**
- 读取 **自己的预测快照** —— 销售代表自己从不写入。每晚的预测作业负责开出并刷新它们,而经理或管理员也可以手工录入一条;手工录入的当前季度那一行,会让这个每晚的作业对该负责人和周期停手,直到那一行被删除(参见[预测](/zh-Hans/docs/sales/forecasting))
- 读取自己的 **合同** 和 **工单**;删除属于经理权限(自己的任务除外)
- 读取自己能看到的每一个客户的 **合同**(Contract 是客户之下的“由父级控制”)以及自己的 **工单**;删除属于经理权限(自己的任务除外)

### 🎧 Service Agent

Expand Down
5 changes: 3 additions & 2 deletions content/docs/administration/profiles.zh-Hant.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ description: 六種內建權限集,以及每一種能檢視、編輯和執行

### 💼 Sales Representative

- 建立、讀取和編輯 **自己的** 線索、客戶、商機、報價和任務
- 建立、讀取和編輯 **自己的** 線索、客戶、商機和任務
- **報價**:代表能看到的那些客戶底下的報價,並且能編輯自己有編輯權的客戶底下的報價 —— 自 #549 起 Quote 是客戶之下的「由父層控制」,與下面的聯絡人是同一種衍生
- **聯絡人**:代表能看到的那些客戶底下的聯絡人 —— 自己的客戶,加上區域規則或手動共用送到手上的每一個客戶。Contact 是「由父層控制」,而這份衍生跟隨客戶的共用(參見[共用與安全](/zh-Hant/docs/administration/sharing-and-security),*「由父層控制」在實務中*)
- **商機明細和報價明細** —— 完整權限,這正是「Products」相關清單和報價編製所需要的。它們同樣是「由父層控制」,因此這份權限達到的是代表能看到的那些交易和報價的明細,別的都到不了
- 讀取 **產品** 目錄、**行銷活動**、**行銷活動成員** 和 **知識文章**
- 讀取 **自己的預測快照** —— 銷售代表自己從不寫入。每晚的預測作業負責開出並刷新它們,而經理或管理員也可以手動錄入一筆;手動錄入的當前季度那一筆,會讓這個每晚的作業對該負責人和週期停手,直到那一筆被刪除(參見[預測](/zh-Hant/docs/sales/forecasting))
- 讀取自己的 **合約** 和 **工單**;刪除屬於經理權限(自己的任務除外)
- 讀取自己能看到的每一個客戶的 **合約**(Contract 是客戶之下的「由父層控制」)以及自己的 **工單**;刪除屬於經理權限(自己的任務除外)

### 🎧 Service Agent

Expand Down
20 changes: 10 additions & 10 deletions content/docs/administration/sharing-and-security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ OWD is the *most restrictive* setting for each object. Everything else widens it
| Contact | Controlled by Parent (Account) | A contact belongs to its account, and follows the account's sharing — see *Controlled by Parent, in practice* below |
| Opportunity | Private | Sensitive deal data |
| Opportunity Line Item | Controlled by Parent (Opportunity) | A product line is part of the deal |
| Quote | Private | Pricing is owner's business |
| Quote | Controlled by Parent (Account) | A quote is priced for an account and follows the account's sharing — see *Controlled by Parent, in practice* below |
| Quote Line Item | Controlled by Parent (Quote) | A line is part of the quote |
| Contract | Private | Sensitive commercials |
| Contract | Controlled by Parent (Account) | A contract *is* account data and follows the account's sharing — see *Controlled by Parent, in practice* below |
| Case | Private | An agent's queue is their own; escalations are shared |
| Task | Private | Personal activity items |
| Event | Private | Personal activity items, like tasks |
Expand All @@ -45,9 +45,9 @@ OWD is the *most restrictive* setting for each object. Everything else widens it

### Controlled by Parent, in practice

For the six parent-derived objects above, there is nothing to configure per user: access is *computed*.
For the eight parent-derived objects above, there is nothing to configure per user: access is *computed*.

- **Read** — you see the rows whose parent you can see, and "can see" means every route that would let you open the parent directly: you own it, a sharing rule granted it, or someone shared it with you by hand. Measured against the shipped stack (platform 17.0.0-rc.4): a rep who can read exactly one account reads **that** account's contacts and no others, and a rep who can read **no** quote reads no quote line items at all. The account reaching that rep only through a territory rule is the point — a parent held by a share counts as a parent you can see.
- **Read** — you see the rows whose parent you can see, and "can see" means every route that would let you open the parent directly: you own it, a sharing rule granted it, or someone shared it with you by hand. Measured against the shipped stack (platform 17.0.0-rc.4): a rep who can read exactly one account reads **that** account's contacts, quotes and contracts and no others, and reads the line items of exactly those quotes — the chain quote line item → quote → account is resolved level by level (platform 17.4.0, objectstack-ai/objectstack#11082). The account reaching that rep only through a territory rule is the point — a parent held by a share counts as a parent you can see.
- **Write** — adding, changing or removing a row requires **edit** access to the parent, resolved the same way as reads. A row under a parent you cannot edit is refused, naming the parent that stopped it.

> **This changed in platform 17.0.0-rc.4.** Through 17.0.0-rc.3 the computed answer was org-wide rather than parent-scoped: the derivation resolved accessible parents from the parent object's *row-level security policies* alone, and HotCRM authors almost none, so a Controlled by Parent object was readable — and writable — by every user whose profile granted the object at all. If you are running an earlier release, that is the behaviour you have, and quote and opportunity line pricing is visible org-wide. The repo test `test/parent-derived-reach.test.ts` pins the current reach in both directions (#549, #694; upstream fix objectstack-ai/objectstack#5386).
Expand Down Expand Up @@ -135,21 +135,21 @@ Most sharing rules are standing openings: a rule that matches a record today mat

### A rule widens one object, not the records underneath it

Sharing rules are authored **per object**. Widening `Account` widens accounts — the records hanging off a shared account keep their own baseline, and Contact is the only one derived from it:
Sharing rules are authored **per object**. Widening `Account` widens accounts — the records hanging off a shared account keep their own baseline, except the three derived from it (Contact, Quote, Contract):

| Related list on a shared account | What the recipient sees |
| --- | --- |
| Contacts | This account's contacts — Contact is Controlled by Parent, and the shared account counts as a parent they can see |
| Opportunities | Their own deals only, plus open deals ≥ $100,000 if they hold `sales_director` or `executive` |
| Quotes | Their own only |
| Contracts | Their own only |
| Quotes | This account's quotes — Quote follows the account, and so do the quotes' line items |
| Contracts | This account's contracts — Contract follows the account; whether they can *edit* one is the object-level right on their profile (a Sales Rep cannot) |
| Cases | Their own only, plus open critical cases if they hold `service_manager` or `service_director` |
| Tasks | Their own only |
| Events | Their own only |

So a territory rule hands over the account record; the deal, quote, contract, case, task and meeting history underneath stays with its owners, and those related lists can look empty or partial on an account the user can otherwise read in full. Contacts are the exception, and the intended one: they follow the account, so the Contacts list of a territory-shared account is complete.
So a territory rule hands over the account record with its contacts, quotes and contracts; the deal, case, task and meeting history underneath stays with its owners, and those related lists can look empty or partial on an account the user can otherwise read in full.

Making a child object follow the account is a deliberate widening, not a config detail. A sharing rule on the child object with criteria matching the account rules opens that object for **every** holder of it, not only for the territory recipients. Setting the child's OWD to Controlled by Parent is the narrower instrument: since platform 17.0.0-rc.4 it grants the child exactly to whoever can reach the parent (see *Controlled by Parent, in practice* above). HotCRM ships neither for Quote and Contract today; whether it should is an open decision (#549).
Making a child object follow the account is a deliberate widening, not a config detail. A sharing rule on the child object with criteria matching the account rules opens that object for **every** holder of it, not only for the territory recipients. Setting the child's OWD to Controlled by Parent is the narrower instrument: it grants the child exactly to whoever can reach the parent (see *Controlled by Parent, in practice* above). That is what HotCRM ships for Quote and Contract (#549): a rep who receives an account through a territory rule sees, and — holding edit on the account — may edit, its quotes; contracts they read but never edit. Deals, cases, tasks and events stay own-only on purpose — a task is a personal work item, and the pipeline question is deferred until a territory model exists.

## Layer 4 — Manual shares

Expand Down Expand Up @@ -245,6 +245,6 @@ There is no "AI bypass".
If you think you should be able to see a record and can't:

1. Check **who owns it**.
2. If you reached it from an account, check **which object it is**: quotes, contracts, tasks, cases and deals stay with their owner even when the account itself is shared with you. Contacts are the one related list that is never withheld — Contact is Controlled by Parent, so it follows the account and arrives with it (see *Controlled by Parent, in practice*).
2. If you reached it from an account, check **which object it is**: tasks, cases and deals stay with their owner even when the account itself is shared with you. Contacts, quotes and contracts are the related lists that are never withheld — all three are Controlled by Parent, so they follow the account and arrive with it (see *Controlled by Parent, in practice*).
3. Check whether the deal is flagged **Private**.
4. Ask your admin whether a **sharing rule** should cover your position.
Loading
Loading