diff --git a/README.md b/README.md index 7311a79..ea7f1d1 100644 --- a/README.md +++ b/README.md @@ -94,8 +94,8 @@ console does not list them; this table is where they are named. |---|---|---|---| | `Business Requester 1` · `2` · `3` | `requester1@example.com` · `requester2@example.com` · `requester3@example.com` | none — every member holds `clm_requester` | the 120 contracts' `owner_id`, dealt by counterparty — 43 / 43 / 34 · and the 143 delivery and reporting obligations under them — 50 / 53 / 40 · the 2 business reviews — 0 / 1 / 1 | | `Legal Counsel 1` · `2` | `counsel1@example.com` · `counsel2@example.com` | `clm_legal_counsel` | receives contracts on submission (round-robin by open load) and owns review · the `legal_owner` of every contract legal has accepted — 34 / 34 · all 56 compliance obligations — 29 / 27 · the `decided_by` of all 17 decided deviations — 10 / 7 · the reviewer of their contracts' 34 legal reviews — 16 / 18, the 7 still pending among them — 2 / 5 | -| `Head of Legal` | `legal.head@example.com` | `clm_legal_head` | rung 2 of the approval ladder whenever `route_legal_head` is stamped · the reviewer of 8 reviews: the legal review of the contract handed back to the queue, and the 7 finance reviews on contracts not yet approved | -| `Finance Controller` | `finance@example.com` | `clm_finance_controller` | rung 2/3, reads every contract with `route_finance`, and the only position that may edit bank details · the reviewer of the 16 finance reviews on approved-or-later contracts | +| `Head of Legal` | `legal.head@example.com` | `clm_legal_head` | rung 2 of the approval ladder whenever `route_legal_head` is stamped · the reviewer of the legal review of the contract handed back to the queue | +| `Finance Controller` | `finance@example.com` | `clm_finance_controller` | rung 2/3, reads every contract with `route_finance`, and the only position that may edit bank details · the reviewer of all 23 finance reviews, the 7 on contracts still in review or in approval among them, recorded there through **Record Finance Review** (记录财务审查) | | `Executive` | `executive@example.com` | `clm_executive` | rung 4, and reads every contract with `route_executive` | | `General Manager` | `gm@example.com` | `clm_general_manager` | rung 5, and reads every contract with `route_gm` | | `Records Manager` | `records@example.com` | `clm_records_manager` | execution formalities, the executed copy and the archive number | @@ -172,11 +172,13 @@ reminders go to somebody who is not going to act on them. The 60 reviews are dealt to the desk whose stage they are. A legal review is the contract's lawyer's, or the head of legal's on the one contract handed back to -the queue. A finance review is the finance controller's once its contract is -approved, and the head of legal's before that, because finance edits a contract -(and so its reviews) only from `approved` onward. A business review is the -requester's who launched the contract. A compliance review would be the head of -legal's; the demo deals none. `src/data/keys.ts` (`reviewerOf`) says why each. +the queue. A finance review is the finance controller's, before approval as well +as after: finance edits a contract (and so writes its reviews directly) only +from `approved` onward, so on a contract still in review or in approval it +records its review through the **Record Finance Review** (记录财务审查) button +on the contract instead. A business review is the requester's who launched the +contract. A compliance review would be the head of legal's; the demo deals +none. `src/data/keys.ts` (`reviewerOf`) says why each. ### Rung 1 of the approval ladder is a manager diff --git a/src/actions/finance-review.actions.ts b/src/actions/finance-review.actions.ts new file mode 100644 index 0000000..f2417f3 --- /dev/null +++ b/src/actions/finance-review.actions.ts @@ -0,0 +1,64 @@ +import type { Action } from '@objectstack/spec/ui'; +import { P } from '@objectstack/spec'; + +/** + * 记录财务审查 — finance records its own review of a contract in review or in + * approval (issue #190, ruling D, 2026-11-03). + * + * DESIGN.md §04 grants `clm_finance` "RCU(stage=finance)" on `clm_review`, + * but a `controlled_by_parent` detail is written only by a caller who may + * edit its master, and finance edits a contract only from `approved` onward. + * The ruling keeps finance off the contract and gives the review its own + * door instead: this button starts `record_finance_review` + * (`src/flows/finance-review.flow.ts`), which writes the caller's + * `stage: 'finance'` review system-elevated, creating it or updating the one + * they already gave, and writes nothing on the contract. The flow's header + * says what the elevation does and does not buy. + * + * ## The gate: `clm_finance.access` + * + * A token `clm_finance` already holds and no other set grants (§04), the + * same way `clm_legal.access` serves the legal-only actions; no capability + * is added. Declared twice with the same list, as `executed_upload` does: + * the flow's `requiredPermissions` stops the request at every door that + * starts it by name, and this copy hides the button, because the console's + * action strip and MCP `list_actions` read the action, not the flow behind + * it (objectstack#25643). The action route also refuses a caller without it, + * 403, before the flow is looked at. + * + * ## `visible` is a convenience, not the control + * + * Shown while the contract is `in_review` or `in_approval`, the window in + * which finance cannot write its review any other way. From `approved` + * onward finance edits the review directly, as §04 has always said. The + * flow repeats the status test as a refused end, so a caller who reaches the + * route on a contract in any other state is told so and nothing is written. + * + * ## The dialog + * + * The three fields of the review the reviewer decides, field-backed on + * `clm_review` (`objectOverride`) so each inherits that field's label, type + * and options in both locales. `decision` is required: a review recorded + * with no decision is not a review. The reviewer, the stage and the contract + * are not asked: the caller, `finance`, and the contract on screen. + */ +export const RecordFinanceReviewAction: Action = { + name: 'record_finance_review', + label: 'Record Finance Review', + objectName: 'clm_contract', + icon: 'calculator', + type: 'flow', + target: 'record_finance_review', + locations: ['record_header'], + description: 'Record your finance review of this contract. Pressing it again updates the review you already gave; a field left empty keeps what the review says. The contract itself is not changed.', + visible: P`record.status == 'in_review' || record.status == 'in_approval'`, + // The same list as the flow's (see the header): this copy hides the button. + requiredPermissions: ['clm_finance.access'], + params: [ + { field: 'decision', objectOverride: 'clm_review', required: true }, + { field: 'risk_level_assessed', objectOverride: 'clm_review' }, + { field: 'comments', objectOverride: 'clm_review', helpText: 'Visible to the requester.' }, + ], + successMessage: 'Finance review recorded.', + refreshAfter: true, +}; diff --git a/src/actions/index.ts b/src/actions/index.ts index 46a5151..d5908e2 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -11,3 +11,4 @@ export { StartRenewalAction, } from './contract-lifecycle.actions.js'; export { ExecutedUploadAction } from './contract-backfill.actions.js'; +export { RecordFinanceReviewAction } from './finance-review.actions.js'; diff --git a/src/data/keys.ts b/src/data/keys.ts index 187c5b1..adced66 100644 --- a/src/data/keys.ts +++ b/src/data/keys.ts @@ -1,7 +1,6 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. import { STRINGS } from './demo-locale.js'; -import { POST_APPROVAL_STATUSES } from '../sharing/_lifecycle.js'; import type { ObligationPlan, ReviewPlan, VersionPlan } from './plan-children.js'; import { CONTRACT_PLAN, type ContractPlan, type DeviationPlan } from './plan-contracts.js'; import { assertUniqueKeys } from './_shared.js'; @@ -348,7 +347,8 @@ export const FINANCE_CONTROLLER = 'Finance Controller'; * nobody (the contract handed back to the queue), the head of legal, who * receives that queue's escalations. * - **finance** — the finance controller, whose `clm_finance` holds - * `RCU(stage=finance)` on this object. + * `RCU(stage=finance)` on this object, on every contract the matrix + * routed to it, approved or not (see below). * - **compliance** — the head of legal: compliance is legal's work (the * argument {@link obligationOwnerOf} makes for compliance obligations) and * a high-risk contract's compliance sign-off is the senior lawyer's. @@ -357,22 +357,21 @@ export const FINANCE_CONTROLLER = 'Finance Controller'; * - **business** — the requester who launched the contract, {@link ownerOf}: * the business review is the launching desk's own sign-off. * - * ## Where the rule bends, measured on 18.2.0 + * ## A finance review before approval is still finance's * - * A finance review on a contract that has not reached `approved` goes to the - * head of legal, not the finance controller. `clm_review` is - * `controlled_by_parent`, so writing one needs EDIT on its contract, and + * 7 of the 23 finance reviews sit on a contract that has not reached + * `approved` (6 `in_review`, 1 `in_approval`, every one routed to finance). + * Finance cannot WRITE a review there directly: `clm_review` is + * `controlled_by_parent`, so a direct write needs edit on its contract, and * finance edits contracts only from `approved` onward - * (`contract_finance_post_approval`); before that it READS the contracts the - * matrix routed to it (`contract_finance_routed`) and no more. Measured: the - * finance controller's `PATCH` of a finance review on an `in_review` or an - * `in_approval` contract answers `403 PERMISSION_DENIED` ("requires edit - * access to its master record"), the same write on an `active` contract `200`. - * So on those rows the finance controller could neither have decided the - * review it would be named for nor change it now; the head of legal can - * (`review_legal_update_any_stage`, and `contract_legal_all_head` edits every - * contract), and is the account the rule already falls back to. 7 of the 23 - * finance reviews sit on such a contract. + * (`contract_finance_post_approval`) — measured on 18.2.0, finance@'s + * `PATCH` of such a review answers `403` "requires edit access to its master + * record". Until issue #190 those 7 went to the head of legal, who could. + * Since #190 (ruling D) finance records and updates its own review there + * through the 记录财务审查 action (`record_finance_review`, + * `src/flows/finance-review.flow.ts`), which is shown on exactly those two + * states, so the finance controller is the reviewer on all 23, and the review + * the account is named for is one it can change. * * A business review stays the requester's although a requester cannot change * it: `clm_requester` holds `R` on this object (§04), and both seeded business @@ -388,7 +387,7 @@ export const reviewerOf = (review: ReviewPlan): string => { case 'legal': return legalOwnerOf(contract) ?? LEGAL_HEAD; case 'finance': - return POST_APPROVAL_STATUSES.includes(contract.status) ? FINANCE_CONTROLLER : LEGAL_HEAD; + return FINANCE_CONTROLLER; case 'compliance': return LEGAL_HEAD; case 'business': diff --git a/src/flows/finance-review.flow.ts b/src/flows/finance-review.flow.ts new file mode 100644 index 0000000..24e7f45 --- /dev/null +++ b/src/flows/finance-review.flow.ts @@ -0,0 +1,204 @@ +import { expression } from '@objectstack/spec'; +import type { Flow } from '@objectstack/spec/automation'; +import { must, ref } from './_values.js'; + +/** + * 记录财务审查 — finance records its own review of a contract that is still + * in review or in approval (issue #190, ruling D, 2026-11-03). + * + * ## Why finance needs a door at all + * + * DESIGN.md §04 gives `clm_finance` "RCU(stage=finance)" on `clm_review`, and + * `finance.profile.ts` declares it (`review_finance_stage_insert` / + * `_update`). But `clm_review` is `controlled_by_parent`, and the platform + * admits a write to a detail row only from a caller who may EDIT its master + * (ADR-0055 §3, plugin-security's master-write check). Finance edits a + * contract only from `approved` onward (`contract_finance_post_approval`); + * before that it READS the contracts the matrix routed to it + * (`contract_finance_routed`). So on an `in_review` or `in_approval` + * contract — exactly when a finance review is given — finance@'s own POST or + * PATCH of its finance review answers 403 "requires edit access to its master + * record". The ruling keeps that refusal: finance gets no edit on the + * contract, and the direct writes stay 403. This flow is the one door. + * + * ## `runAs: 'system'`, and what that is buying + * + * Exactly one thing: the master-write check, which refuses the caller's own + * write. The run writes one `clm_review` row and nothing on the contract, and + * it takes every value that names WHO and WHERE from the platform, not from + * the caller's input: + * + * - the contract is `record`, the row the action route loaded IN THE + * CALLER'S SCOPE before the run started. A contract finance cannot read + * answers 404 at the route and never reaches this flow. The flow declares + * no input that could name another contract, and a door that hands no + * record (the trigger route, which starts a flow by name) is refused at + * the first decision; + * - `reviewer` is `current_user.id`: elevation is not anonymity, the run + * carries the caller's `userId` beside `isSystem`, so the review is the + * caller's and the audit stamps name them; + * - `stage` is the literal `'finance'`. + * + * So the elevated write is the write `review_finance_stage_insert` / + * `_update` already allow finance, on a contract finance can already read — + * the platform's master-write rule is the only thing stepped around. + * + * ## Who may start it: `type: 'screen'` + `requiredPermissions` + * + * The same two questions `executed-upload.flow.ts` answers. On 18.x a door + * that starts a flow by name refuses every non-system caller of a + * `runAs: 'system'` flow whose type is self-triggered (`autolaunched`, + * `record_change`, `schedule`), whatever they hold, so the type is `screen`: + * an entry a member starts from a button, elevated by the author's choice. It + * has no screen node — the action's dialog collects the three inputs. The + * gate is `requiredPermissions: ['clm_finance.access']`, a token only + * `clm_finance` grants (§04), checked at every door before anything runs. The + * action repeats the list only so the button is hidden from everyone else + * (`src/actions/finance-review.actions.ts`). + * + * ## One review per finance reviewer per contract + * + * The run looks for the caller's own finance-stage review of this contract + * first and UPDATES it when there is one, so a second press restates the + * review instead of filing a second row. A field the dialog leaves empty on + * that second press keeps what the review already says: the dialog cannot + * show the stored values, and an empty box is not a decision to erase the + * comments the requester has read. `decision` is required, so it is always + * written. `decided_at` is `review_decision_stamp`'s (`contract.hook.ts`), + * stamped on the first non-pending decision and never moved. + * + * ## Refusals are refused `end` nodes + * + * As in every flow here: a refused `end` records the run `refused`, the + * action's success sentence is suppressed, and the console shows the + * sentence alone, translated by the + * `flows.record_finance_review.refusals..message` key + * (objectstack#15788, objectstack#22450). Both refusals sit before the first + * read of a review, so a refused run writes nothing. + */ + +/** The two contract states a finance review is recorded in (§03: review, then approval). */ +export const FINANCE_REVIEW_STATUSES = ['in_review', 'in_approval'] as const; + +const HAS_CONTRACT = 'has(vars.record) && vars.record != null && has(vars.record.id) && has(vars.record.status)'; +const IN_WINDOW = `vars.record.status in [${FINANCE_REVIEW_STATUSES.map((s) => JSON.stringify(s)).join(', ')}]`; +const NO_CONTRACT = `!(${HAS_CONTRACT})`; +const OUT_OF_WINDOW = `${HAS_CONTRACT} && !(${IN_WINDOW})`; +const RECORDABLE = `${HAS_CONTRACT} && ${IN_WINDOW}`; +const HAS_EXISTING = 'has(vars.existing) && vars.existing != null && has(vars.existing.id)'; +const NO_EXISTING = `!(${HAS_EXISTING})`; + +/** + * On the update path: the dialog's value when one was given, otherwise what + * the stored review already says (see the header). + */ +const givenOrKept = (input: string) => + expression( + `has(vars.${input}) && vars.${input} != null && vars.${input} != "" ? vars.${input} : (has(vars.existing.${input}) ? vars.existing.${input} : null)`, + ); + +export const FinanceReviewFlow: Flow = { + name: 'record_finance_review', + label: 'Record Finance Review', + description: "Record the caller's finance review of a contract in review or in approval: create it, or update the one they already gave. Nothing on the contract is written.", + // An entry a member starts from the 记录财务审查 button. Not `autolaunched`: + // 18.x refuses every non-system caller at the door of an elevated + // self-triggered flow, whatever they hold (see the header). + type: 'screen', + status: 'active', + // See the header: elevation steps around the master-write check alone. + runAs: 'system', + // The gate: only `clm_finance` holds it (§04). Checked at every door that + // starts this flow by name, before anything runs. + requiredPermissions: ['clm_finance.access'], + successMessage: 'Finance review recorded.', + + // The dialog's three inputs and nothing else: the contract is the record + // the route loaded in the caller's scope, and the reviewer is the caller. + variables: [ + { name: 'decision', type: 'text', isInput: true, isOutput: false }, + { name: 'risk_level_assessed', type: 'text', isInput: true, isOutput: false }, + { name: 'comments', type: 'text', isInput: true, isOutput: false }, + ], + + nodes: [ + { id: 'start', type: 'start', label: 'Start', config: { objectName: 'clm_contract' } }, + + { id: 'decide_contract', type: 'decision', label: 'Contract In Review Or Approval?' }, + { + id: 'refuse_no_contract', + type: 'end', + label: 'Refuse — No Contract', + config: { + outcome: 'refused', + message: 'A finance review is recorded from the contract it reviews. Open the contract and use Record Finance Review there.', + }, + }, + { + id: 'refuse_stage', + type: 'end', + label: 'Refuse — Not In Review Or Approval', + config: { + outcome: 'refused', + message: 'A finance review is recorded while the contract is In Review or In Approval. From Approved onward, finance edits the review on the contract directly.', + }, + }, + + { + id: 'get_existing', + type: 'get_record', + label: "Caller's Finance Review", + config: { + objectName: 'clm_review', + filter: { contract: must('record.id'), stage: 'finance', reviewer: expression('current_user.id') }, + fields: ['id', 'decision', 'risk_level_assessed', 'comments'], + outputVariable: 'existing', + }, + }, + { id: 'decide_existing', type: 'decision', label: 'Already Recorded?' }, + { + id: 'update_review', + type: 'update_record', + label: 'Update The Finance Review', + config: { + objectName: 'clm_review', + filter: { id: must('existing.id') }, + fields: { + decision: ref('decision'), + risk_level_assessed: givenOrKept('risk_level_assessed'), + comments: givenOrKept('comments'), + }, + }, + }, + { + id: 'create_review', + type: 'create_record', + label: 'Record The Finance Review', + config: { + objectName: 'clm_review', + fields: { + contract: must('record.id'), + stage: 'finance', + reviewer: expression('current_user.id'), + decision: ref('decision'), + risk_level_assessed: ref('risk_level_assessed'), + comments: ref('comments'), + }, + outputVariable: 'created', + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + + edges: [ + { id: 'f1', source: 'start', target: 'decide_contract', type: 'default' }, + { id: 'f2', source: 'decide_contract', target: 'refuse_no_contract', type: 'default', label: 'No contract', condition: NO_CONTRACT }, + { id: 'f3', source: 'decide_contract', target: 'refuse_stage', type: 'default', label: 'Outside the window', condition: OUT_OF_WINDOW }, + { id: 'f4', source: 'decide_contract', target: 'get_existing', type: 'default', label: 'In review or approval', condition: RECORDABLE }, + { id: 'f5', source: 'get_existing', target: 'decide_existing', type: 'default' }, + { id: 'f6', source: 'decide_existing', target: 'update_review', type: 'default', label: 'Already recorded', condition: HAS_EXISTING }, + { id: 'f7', source: 'decide_existing', target: 'create_review', type: 'default', label: 'First time', condition: NO_EXISTING }, + { id: 'f8', source: 'update_review', target: 'end', type: 'default' }, + { id: 'f9', source: 'create_review', target: 'end', type: 'default' }, + ], +}; diff --git a/src/flows/index.ts b/src/flows/index.ts index c628109..4652c53 100644 --- a/src/flows/index.ts +++ b/src/flows/index.ts @@ -10,6 +10,7 @@ import { PaymentOverdueFlow } from './payment-overdue.flow.js'; import { RenewalNoticeFlow, RenewalStartFlow, renewalDraft, renewalWindow } from './renewal-notice.flow.js'; import { ExpirationSweepFlow } from './expiration-sweep.flow.js'; import { ExecutedUploadFlow, backfillStamps } from './executed-upload.flow.js'; +import { FinanceReviewFlow } from './finance-review.flow.js'; // F1–F16 of DESIGN.md §06; this barrel is what `defineStack({ flows })` reads. // F8 (e-signature) and F15 (CRM hand-off) are not built yet; both arrive with @@ -24,6 +25,7 @@ export { PaymentOverdueFlow } from './payment-overdue.flow.js'; export { RenewalNoticeFlow, RenewalStartFlow } from './renewal-notice.flow.js'; export { ExpirationSweepFlow } from './expiration-sweep.flow.js'; export { ExecutedUploadFlow } from './executed-upload.flow.js'; +export { FinanceReviewFlow } from './finance-review.flow.js'; export const allFlows: Flow[] = [ ContractIntakeFlow, @@ -41,6 +43,9 @@ export const allFlows: Flow[] = [ ExpirationSweepFlow, RenewalStartFlow, ExecutedUploadFlow, + // Finance's own review before approval (issue #190, ruling D): the door + // the 记录财务审查 button starts. + FinanceReviewFlow, ]; /** diff --git a/src/pages/contract_detail.page.ts b/src/pages/contract_detail.page.ts index 657527c..e1fc78c 100644 --- a/src/pages/contract_detail.page.ts +++ b/src/pages/contract_detail.page.ts @@ -115,6 +115,8 @@ export const ContractDetailPage: Page = { * header, so an action not named here is unreachable from the record — * MEASURED: `start_renewal` was declared, translated and wired to its flow * and still had no button on the contract page until it was named here. + * `record_finance_review` (记录财务审查) is the same: declared on the + * object, reachable only once listed here. */ header: { type: 'page:header', @@ -131,6 +133,10 @@ export const ContractDetailPage: Page = { 'activate_contract', 'terminate_contract', 'start_renewal', + // Finance's own review while the contract is in review or in + // approval (issue #190, ruling D). Not a status write: it files the + // caller's finance-stage review and leaves the contract as it is. + 'record_finance_review', ], }, }, diff --git a/src/translations/en/flows.ts b/src/translations/en/flows.ts index 6c24caa..dd995f2 100644 --- a/src/translations/en/flows.ts +++ b/src/translations/en/flows.ts @@ -92,6 +92,12 @@ export const flowSurface: Pick = { refuse_party: { message: 'That counterparty cannot be used: it is either missing or flagged blocked. A contract with a blocked counterparty is not recorded, even retrospectively.' }, }, }, + record_finance_review: { + refusals: { + refuse_no_contract: { message: 'A finance review is recorded from the contract it reviews. Open the contract and use Record Finance Review there.' }, + refuse_stage: { message: 'A finance review is recorded while the contract is In Review or In Approval. From Approved onward, finance edits the review on the contract directly.' }, + }, + }, renewal_start: { refusals: { refuse_existing: { message: 'This contract already has a renewal draft ({{ existingRenewal.contract_number }}). Open that one instead of starting a second.' }, diff --git a/src/translations/en/objects.contract.ts b/src/translations/en/objects.contract.ts index c6619c4..83e50d0 100644 --- a/src/translations/en/objects.contract.ts +++ b/src/translations/en/objects.contract.ts @@ -4,7 +4,7 @@ * `clm_contract` and its document versions — 234 + 25 of this bundle's keys, * a third of the whole thing, so they get their own file on size alone. * - * The contract carries the only `_actions` group in the bundle: the eight header + * The contract carries the only `_actions` group in the bundle: the nine header * actions of the record page plus the two list-toolbar actions (launch and * backfill), each with its label, its description and — for the actions that * open a dialog — the parameter labels and help that dialog draws. @@ -441,6 +441,16 @@ export const contract: Record = { }, }, }, + record_finance_review: { + label: 'Record Finance Review', + description: 'Record your finance review of this contract. Pressing it again updates the review you already gave; a field left empty keeps what the review says. The contract itself is not changed.', + successMessage: 'Finance review recorded.', + params: { + comments: { + helpText: 'Visible to the requester.', + }, + }, + }, send_for_approval: { label: 'Send for Approval', description: 'Close legal review and start approval. Possible once a legal review is approved and no deviation is still open.', diff --git a/src/translations/zh-CN/flows.ts b/src/translations/zh-CN/flows.ts index 59fb6ba..2ede974 100644 --- a/src/translations/zh-CN/flows.ts +++ b/src/translations/zh-CN/flows.ts @@ -92,6 +92,12 @@ export const flowSurface: Pick = { refuse_party: { message: '这个相对方不能使用:它不存在,或已被标记为「禁止合作」。相对方禁止合作的合同不予登记,补录也不例外。' }, }, }, + record_finance_review: { + refusals: { + refuse_no_contract: { message: '财务审查要从它所审查的合同上记录。请打开合同,在那里使用「记录财务审查」。' }, + refuse_stage: { message: '财务审查在合同处于「审查中」或「审批中」时记录。从「已批准」起,财务直接在合同上修改这条审查。' }, + }, + }, renewal_start: { refusals: { refuse_existing: { message: '这份合同已有续签草稿({{ existingRenewal.contract_number }})。请打开那一份,不要再发起第二份。' }, diff --git a/src/translations/zh-CN/objects.contract.ts b/src/translations/zh-CN/objects.contract.ts index 026cae1..baf752e 100644 --- a/src/translations/zh-CN/objects.contract.ts +++ b/src/translations/zh-CN/objects.contract.ts @@ -4,7 +4,7 @@ * `clm_contract` and its document versions — 234 + 25 of this bundle's keys, * a third of the whole thing, so they get their own file on size alone. * - * The contract carries the only `_actions` group in the bundle: the eight header + * The contract carries the only `_actions` group in the bundle: the nine header * actions of the record page plus the two list-toolbar actions (launch and * backfill), each with its label, its description and — for the actions that * open a dialog — the parameter labels and help that dialog draws. @@ -441,6 +441,16 @@ export const contract: Record = { }, }, }, + record_finance_review: { + label: '记录财务审查', + description: '记录你对这份合同的财务审查。再次点击会更新你已给出的那条审查;留空的字段保留审查里原有的内容。合同本身不会被修改。', + successMessage: '财务审查已记录。', + params: { + comments: { + helpText: '发起人可以看到。', + }, + }, + }, send_for_approval: { label: '送审', description: '结束法务审查并开启审批。须有一条通过的法务审查,且没有未决偏离,才能送审。',