From 32eef5b000bd1f56415c5d9adc3443a49afeaaa9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 15:42:51 +0000 Subject: [PATCH] fix(objects): refuse a non-department unit as a contract's department clm_contract.department's lookupFilters shape the picker only: a REST PATCH or POST, /api/v1/batch, an import, or a headless launch_contract stored a company or an office as the requesting department (#129). A conditional rule, the #125 pattern: `when` admits only a write that sets or changes department to a value (null and "" are skipped, since a hop through an empty reference has no row and the engine refuses that write as unevaluable), and the cross_field `then` refuses a unit whose kind is not department. It does not judge `active`, so a renewal that copies a department retired since still inserts. Message in en and zh-CN under _validations. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn --- src/objects/contract.object.ts | 46 +++++++++++++++++++--- src/translations/en/objects.contract.ts | 3 ++ src/translations/zh-CN/objects.contract.ts | 3 ++ 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/objects/contract.object.ts b/src/objects/contract.object.ts index 057aefd..262ac7f 100644 --- a/src/objects/contract.object.ts +++ b/src/objects/contract.object.ts @@ -210,11 +210,13 @@ export const Contract = ObjectSchema.create({ group: 'parties', description: 'The department that launched the contract. The choices are the department-kind business units in Setup → Business Units. Sharing contracts by department is a customer-specific extension, not part of the standard product.', // The picker offers active departments only, never a company, division, - // office or cost centre. The picker is the only check on kind: a write - // that bypasses it may name a unit of another kind (measured on 18.1.0, - // 200), while an id that names no unit is refused by the engine - // (`reference_not_found`). No write-side rule like `our_entity`'s - // `contract_entity_is_active_company` exists for it (#109 reports why). + // office or cost centre. The picker is convenience; the rule on kind is + // `contract_department_is_department` below, which refuses a unit of + // another kind on every write path that sets or changes the department + // (#129). An id that names no unit is refused by the engine + // (`reference_not_found`). Unlike `our_entity`'s rule it does not judge + // `active`: a department retired since must not block the renewal that + // copies it, so only the picker hides retired units. lookupFilters: [ { field: 'kind', operator: 'eq', value: 'department' }, { field: 'active', operator: 'eq', value: true }, @@ -660,6 +662,40 @@ export const Contract = ObjectSchema.create({ message: 'Our Signing Entity must be one of our active legal entities: a business unit of kind Company that is still active. A department, an office or a retired company cannot sign. Choose an entity from the list, or ask an administrator to add or reactivate it in Setup → Business Units.', }, }, + // `department`'s lookupFilters likewise shape the picker only: a REST + // PATCH, an import or a headless `launch_contract` stored a company or an + // office as the requesting department (#129). The same rule shape as + // `contract_entity_set` above, for the same reasons, with two differences: + // + // - `department` is optional, so `when` also requires a value. An empty + // department is a valid answer, and a hop through an empty reference + // has no row to read: the engine refuses that write as unevaluable + // rather than passing it, so the guard must sit in `when`, outside the + // hop. Empty means `null` or `""`: the engine stores a REST write of + // `""` as given (measured on 18.1.0), and without the second half that + // write was refused "could not be evaluated". + // - it judges `kind` only, never `active`. The department is a reporting + // field, and F12 / F13 renewals insert a new contract that copies it; + // judging `active` would refuse the renewal of every contract whose + // department has been retired since. The picker already hides retired + // units for a person choosing one. + { + type: 'conditional' as const, + name: 'contract_department_set', + label: 'Requesting department set or changed', + description: 'Judges department only on a write that sets or changes it to a unit; an empty or unchanged department is never judged.', + when: P`record.department != null && record.department != "" && (previous == null || previous.department != record.department)`, + message: 'Requesting Department must be a department.', + then: { + type: 'cross_field' as const, + name: 'contract_department_is_department', + label: 'Requesting department is a department', + description: 'A write that sets department must name a department-kind business unit.', + condition: P`record.department.kind != "department"`, + fields: ['department'], + message: 'Requesting Department must be a department: a business unit of kind Department. A company, a division, an office or a cost centre cannot be the requesting department. Choose a department from the list, leave the field empty, or ask an administrator to add the department in Setup → Business Units.', + }, + }, ], indexes: [ diff --git a/src/translations/en/objects.contract.ts b/src/translations/en/objects.contract.ts index e0750e2..f9bc11a 100644 --- a/src/translations/en/objects.contract.ts +++ b/src/translations/en/objects.contract.ts @@ -537,6 +537,9 @@ export const contract: Record = { contract_entity_is_active_company: { message: 'Our Signing Entity must be one of our active legal entities: a business unit of kind Company that is still active. A department, an office or a retired company cannot sign. Choose an entity from the list, or ask an administrator to add or reactivate it in Setup → Business Units.', }, + contract_department_is_department: { + message: 'Requesting Department must be a department: a business unit of kind Department. A company, a division, an office or a cost centre cannot be the requesting department. Choose a department from the list, leave the field empty, or ask an administrator to add the department in Setup → Business Units.', + }, }, }, diff --git a/src/translations/zh-CN/objects.contract.ts b/src/translations/zh-CN/objects.contract.ts index 711e69c..2acbee9 100644 --- a/src/translations/zh-CN/objects.contract.ts +++ b/src/translations/zh-CN/objects.contract.ts @@ -537,6 +537,9 @@ export const contract: Record = { contract_entity_is_active_company: { message: '我方签约主体必须是仍在启用的我方法律主体,即类型为公司且处于启用状态的业务单元。部门、办事处或已停用的公司不能作为签约方。请从列表中选择签约主体,或请管理员在「设置 → 业务单元」中新增或重新启用它。', }, + contract_department_is_department: { + message: '发起部门必须是部门,即类型为部门的业务单元。公司、事业部、办事处或成本中心不能作为发起部门。请从列表中选择部门、将此字段留空,或请管理员在「设置 → 业务单元」中新增该部门。', + }, }, },