Skip to content

[Decision] Currency, all of it: resume multi-currency (#8345) and in what shape, and one rule for an amount whose currency is not known (Studio's USD, the template's USD, fixed's CNY, the example's USD) #27119

Closed

Description

@objectstack-fleet

Ruled: 6464202770 · letter Q2 X · 6465431966 · letter Q1 乙-现在 · 2026-10-28T09:33Z

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Filing gate ③ (maintainer-directed). Maintainer, 2026-10-28, verbatim: 「以上币种的所有相关问题,立一张决策卡」, after the triage seat walked the currency surface: the tenant default currency, how fields and dashboards render it, and the paused multi-currency design #8345. ⛔ Not a claim.

维护者速读

  • 一句话问题: 平台在「不知道这笔钱是什么币种」时,各处的做法互相打架:字段和仪表盘按组织默认币种显示,但有四处会自己猜一个币种(三处猜美元,一处猜人民币),客户看到的是错的货币符号。另外,真正的多币种(每条记录自带币种、按汇率折成本位币)从 10 月 8 日起暂停,要不要恢复、恢复成什么形状,也一并定。
  • 要你裁两件事: Q1 多币种恢复与否、什么形状;Q2 不知道币种时的统一规则。Q3 不用裁,裁后一并做。
  • 我的推荐: Q1 乙(C′ 形状),但时序上保持暂停,等有客户拉动再开工。Q2 X:先用组织默认币种,没有就不显示符号,模板里没有就拒发,任何地方都不猜。

Background (measured on origin/main, 2026-10-28)

What already exists and works:

Where the platform still guesses a currency (each one breaks that rule):

# Where What it does Who sees it
G1 Studio dataset inspector, ui/packages/app-shell/src/views/metadata-admin/inspectors/DatasetDefaultInspector.tsx:215 Picking "currency" format with no currency stores currency: 'USD' on the measure. A measure's own currency outranks the tenant default. A CNY organization's dashboard tile shows $
G2 Template formatter, packages/formula/src/template-engine.ts:82 (arg ?? 'USD'), card #26939 {{ x | currency }} with no argument formats in USD, in mail and in-app copy An email or a flow notice shows $ for a yuan amount
G3 Spec, packages/spec/src/data/field.zod.ts:559 A fixed-mode field authored with no code materializes defaultCurrency: 'CNY'. This is against the ruled principle (#9689): "NEVER materialize a default the schema itself would refuse as authored". A USD or EUR app's fixed field shows ¥
G4 Example, examples/app-crm/src/datasets/opportunity.dataset.ts:32-33 currency: 'USD' over a tenant-currency field Anyone who copies the example, including AI authors, inherits it

Docs that claim what does not exist (#8345's measurement; not a decision; done under Q3):

  • content/docs/protocol/objectql/index.mdx:121: "Money with exchange rates".
  • content/docs/.../types.mdx:30: "Store amount + currency code".

Multi-currency (#8345), paused 2026-10-08: 「8345 多币种暂时不是重点,暂停」. That card's measurement still stands:

  • There is no record currency today (a currency value is one number; dynamic means the tenant default, and "Neither mode is a per-record choice").
  • There is no exchange-rate conversion.
  • Today a mixed sum is meaningless: USD 100 + JPY 15000 answers 15100.
  • The origin was an overseas port-terminal customer (2026-08): billing in the customer's currency, reconciliation at dated rates, and consolidated reporting in one home currency.

Governing text

Premises (each with a re-check)

  1. G1: git grep -n "currency: currency || 'USD'" origin/main -- ui/packages/app-shell/src/views/metadata-admin/inspectors/DatasetDefaultInspector.tsx → 1 hit.
  2. G2: git grep -n "arg.trim() : 'USD'" origin/main -- packages/formula/src/template-engine.ts → 1 hit.
  3. G3: git grep -n "default('CNY')" origin/main -- packages/spec/src/data/field.zod.ts → 1 hit.
  4. G4: git grep -n "currency: 'USD'" origin/main -- examples/app-crm/src/datasets/opportunity.dataset.ts → 2 hits.
  5. The one display rule: git grep -n "tenantDefault ||" origin/main -- ui/packages/i18n/src/currency.ts → 1 hit.
  6. No conversion: git grep -n -i -E "exchange.?rate|fx_rate|convertCurrency" origin/main -- packages/ ':!*.test.ts' → 0 hits.

Q1: Multi-currency, resume #8345 and in what shape?

一句话: 客户要「按客户币种开票、按当日汇率对账、统一折成本位币出报表」,平台今天三件都做不到。

选项 做什么 客户可感知后果 工作量
甲 保持暂停 只用一种币种的客户不受影响;跨币种业务的客户仍做不了,应用只能自己手搓汇率表 0
乙 = #8345 的 C′ 金额字段新增「记录币种」模式(指向记录上的 ISO 4217 币种字段)+ sys_exchange_rate 汇率表 + 写入时按单据日期汇率算好本位币金额并存下(同时记下所用汇率),缺汇率拒绝写入;未按币种分组的跨币种原始求和在构建时拒收 三件事都能做到;现有汇总、报表、表格合计、指标卡不改读取代码就出本位币合计;改汇率需显式重算 spec 1 PR + server 2–3 PR + ui 1 PR · 无先例 · 估 1–2 周
丙 = #8345 的 A′ 只做「记录币种」模式 + 跨币种求和护栏,不做汇率 能按客户币种开票,汇总不再算错,但折不成本位币 spec 1 PR + server 1 PR · 无先例

业务含义直译: 甲:只收一种钱。乙:收银台带换汇柜台,每笔都按当天汇率记下折算值。丙:每张单子写明币种,年底还得手工换算。

推荐: 乙的形状(C′),时序上维持暂停。

  • 两年后的终态是「交易币种 + 本位币」双金额,写入时锁定汇率。这和 Salesforce Advanced Currency Management、Dynamics 365 的 transaction / base currency 是同一个模型。
  • 只看①选乙;②是否翻转:否,只调时序(今天没有在跑的客户拉动,等拉动回来再开工)。
  • 不取丙:它是半量,做完还要再改一次契约。不取甲作终态:跨币种客户会回来。
  • 置信缺口:「1–2 周」是估算,不是实测。

Q2: One rule for an amount whose currency is not known

一句话: 同一笔不知道币种的钱,在字段里显示组织币种,在仪表盘某些格子里显示 $,在邮件里显示 $,在固定币种字段里显示 ¥。

选项 做什么 客户可感知后果 工作量
X 全平台一条规则:先用明确写的币种 → 再用组织默认币种 → 都没有:显示时不带符号,发送(邮件、通知)时拒发。G1 不再存 USD;G2 无参数用组织币种;G3 fixed 不写币种在发布时拒收(不再变成 CNY);G4 删掉 currency: 'USD' 不会再出现错的货币符号;固定币种字段必须写明币种 ui 1 PR(G1)+ formula 1 PR(G2,并入 #26939)+ spec 1 PR(G3,收窄,需 ADR-0087 处置与存量调查)+ example 1 行(G4)· 先例 #25495
Y 一律拒收:任何地方没写币种就在构建时拒收 最严;但把「组织默认币种」这个已有能力废掉,作者每个金额都得写币种 同上加全量调查,量更大

业务含义直译: X:「没写就按公司本位币,公司也没定就不标符号」。Y:「每张单子都必须手写币种」。

推荐: X。

Q3: Not a decision; done after the ruling either way

The two docs over-claims are rewritten to what ships ("one number; currency from the field or the tenant default"). The fix is domain:devx, p3. Under the #8345 hold, this was owed separately and never filed.

Four-axis facets

os-decision-facets

After the ruling

Related

#8345 (multi-currency, on hold), #25495 (new organization default currency, closed), #26939 (template no-argument currency), #26937 / #26660 (currency:@path in mail), #26948 (unknown mail formatter), ADR-0053, ADR-0104.


Generated by Claude Code

Activity

  1. added
    enhancementNew feature or request
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:awaiting-maintainerAwaiting a manual maintainer action — ruling already given; no dispatch, no machine exit
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    on Oct 28, 2026
  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Brief-claimed-by: session_01QSeKYo7qYsX6HsAneHBMP2 · batch #421 · 2026-10-28T07:42Z

    Director seat, summon #53 (ledger objectstack#12708). Presented to the maintainer in batch #421 as item 2 of 3 — two letters: Q1 (multi-currency, #8345) and Q2 (the one rule for an amount whose currency is not known); Q3 is execution after either letter.

    Seat increment (re-read at origin/main e6e41998, 2026-10-28T07:42Z):

    Prior rulings read: defaultCurrency · multi-currency · exchange rate · localization.currency → 97 hits in AGENTS.md / docs/adr / packages/spec/src (field.zod.ts docblock L539, field.test.ts L1686; no ADR rules the fallback); thread: none on this card; #8345 pause 6051408208, #25495 乙, #9689.

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #421 item 2 · letters Q2 X · Q1 held — the maintainer asked for a deeper multi-dimensional analysis before the letter · maintainer 「多币种帮我专题分析,从多个维度深度分析,其他同意。」 2026-10-28T08:22Z

    Recorded by the director seat (summon #53, session_01QSeKYo7qYsX6HsAneHBMP2, ledger objectstack#12708). Brief 6463511172. 「其他同意」 takes Q2 X. Q1 (multi-currency, #8345) is NOT ruled in this act: the maintainer's words ask the seat for a 专题分析 first; it follows on this card as the next comment, and Q1's letter is recorded in a second Ruling: comment when it comes. Q3 is execution and goes with Q2.

    Q2 = X — one rule for an amount whose currency is not known: an explicit currency → the tenant default (localization.currency) → with neither, DISPLAY carries no symbol and SEND (mail, notification) refuses. Nothing guesses, anywhere.

    Workload: as the brief — ui S · formula S (via #26939) · spec S · devx XS × 2. Precedent #25495.

    Execution: this card STAYS needs-user-decision for Q1; pm:awaiting-maintainer (a half-state beside it with no Maintainer-action: line) is removed in this act. Ruling-ref: on each execution card names this comment.

  4. removed
    pm:awaiting-maintainerAwaiting a manual maintainer action — ruling already given; no dispatch, no machine exit
    on Oct 28, 2026
  5. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    多币种专题分析(Q1 / #8345)· 总监席 · 第 53 场 · 2026-10-28T08:36Z

    应维护者「多币种帮我专题分析,从多个维度深度分析」(批 #421 回字)。输入:#8345 的测量轮(6041378776,两个探针跑在真实 ObjectQL 引擎上)、spec 席的四形状分析(6041482631)、本卡正文、origin/main e6e41998 的现读,以及 hotcrm 6c52f22(本席只读克隆)。⛔ 不是裁决;Q1 的字母仍由维护者下。

    0. 结论先行

    • 终态形状只有一个站得住:C′ ——「记录币种(指向同对象上的 ISO 4217 币种字段)+ 平台汇率表(按日期生效)+ 写入时按单据日期折成本位币并存下来(同时存所用汇率)+ 缺汇率拒写 + 未按币种分组的跨币种原始求和在构建时拒收」。这是 Dynamics 365、Odoo、NetSuite、SAP 的公分母,也是本平台「写入时物化、汇总读存储列、公式是虚拟的」架构的自然延伸。
    • 何时开工取决于拉动,不取决于形状:仓内零作者拉动(示例 17 个金额字段全单币种;hotcrm 只有 1 个币种字段,还在流程里),唯一拉动是 8 月的海外港口客户,10-08 已暂停。
    • 不论何时开工,前置都是已裁的 Q2 X:先把四处「猜币种」清掉,否则多币种建在一个会静默猜 $ / ¥ 的底座上。

    1. 今天的平台(实测)

    • 金额字段的值是一个数(ADR-0104 D1);currencyMode: 'dynamic'(默认)= 按组织默认币种显示,'fixed' = 字段有一个币种。没有任何键、列或值承载「这条记录是什么币种」。
    • 组织默认币种 localization.currency:租户级、无平台默认、ISO 4217 值域、可改且无历史——改一下,所有历史金额就换了标签(探针 B)。
    • 零换算实现(exchange.?rate|fx_rate|convertCurrency 在 packages/ 0 命中,对照 95)。CURRENCY_FRACTION_DIGITS 只剩值域键集,小数位的值零读者——换算会是它的第一个读者。
    • 作者今天「凑」多币种的办法 = 旁边放一个币种文本字段。探针实测:USD 100 + EUR 100 的 sum 答 200 "USD";汇总字段 USD 100 + JPY 15000 答 15100;跨币种派生度量相减答 0 无标签。全部静默错数。
    • CEL 标准库禁止回调宿主(stdlib.ts:7),objectstack build 要逐字节稳定 ⇒ 公式里不能有读汇率表的函数。

    2. 业务场景维度:谁要、要什么

    客户类型 需求 今天能否 拉动证据
    国内单币种(大多数) 金额按人民币显示、汇总 能(组织默认币种) 无需求
    出海 / 跨境企业(港口码头客户) ① 按客户币种开票 ② 按单据日汇率对账 ③ 折成本位币合并报表 ①②③ 都不能 2026-08 一个客户;10-08 暂停
    集团多主体 各子公司本位币不同、集团再合并 不能;且超出单租户单本位币 零
    电商 / 支付 展示币种 vs 结算币种 不在平台层(支付网关的事) 零

    港口客户三件事对应的最小能力:① 记录币种;② 按日期的汇率 + 写入时快照;③ 存下来的本位币金额让现有汇总直接出合并数。三件缺一,客户还是得自己手搓(而手搓的写法平台看不见、护栏拦不住)。

    3. 主流平台怎么建模(对照)

    平台 记录币种 本位币 汇率 换算时机 可借鉴 / 教训
    Salesforce 每条记录 CurrencyIsoCode 公司币种,开启多币种后不可撤销 手工汇率表;ACM 加「按日期生效」(只对机会) 报表与公式读时换算(convertCurrency()) 读时换算让历史报表随汇率漂移,ACM 就是为补这个洞而加的
    Dynamics 365 / Dataverse transactioncurrencyid 每条记录 组织创建时定,不可改 手工汇率表 写入时算 _base 影子列并存 与本平台「写入时物化」同构;本位币锁定是硬规则
    Odoo currency_id 字段,金额字段 currency_field 指向它 公司币种 res.currency.rate 按日期 过账日折算,amount_currency + balance 都存 「金额字段指向兄弟币种字段」= 显式组合,多个金额可共用一个币种字段
    NetSuite 交易币种每条记录 子公司本位币 合并汇率表 过账时折算存储 多主体是另一层,不在本阶段
    SAP 单据币种 本币 / 集团币 汇率类型 M 过账时快照 同上
    Airtable / Notion 字段级格式 — 无 无 只是格式,不是多币种;本平台今天就是这一档

    公分母:记录币种 + 固定本位币 + 按日期汇率表 + 写入时快照并存储本位币金额。只有 Salesforce 走读时换算,并为此付了 ACM 的代价。

    4. 数据模型维度:记录币种放哪(Q0)

    • A 兄弟字段(推荐):金额字段 currencyMode: 'record' + currencyField 指向同对象上一个 valueDomain: 'iso_4217_currency' 的文本字段(该值域 No declarative way to constrain an object FIELD to a value domain (iana_time_zone), and neither extension point an app can reach can express it #14168 已落地)。值仍是标量,ADR-0104 D1 不动;多个金额共用一个币种字段;AI 能看见它、构建时能校验它。
    • B 平台注入隐藏列(Salesforce 式):隐式组合,AI 看不见、build 校验不到。
    • C 值对象 {amount, currency}:推翻 ADR-0104 D1,迁移每一个已存金额列,复活刚退役的 CurrencyValueSchema。
    • D 维持现状:每种币种一个 fixed 字段——对开放币种集不可用。

    5. 汇率维度

    • 对象:sys_exchange_rate { currency, rate(相对本位币), effective_date, source },按 org / currency / date 唯一;交叉汇率经本位币推导(⛔ 不做 N×N 矩阵)。
    • 方向只存一种(1 本位币 = rate 外币,或反之),文档写死,避免倒数歧义。
    • 精度:汇率存高精度小数;本位币金额按本位币 ISO 4217 小数位四舍五入——CURRENCY_FRACTION_DIGITS 第一次有读者。
    • 缺汇率 = 拒写,报错点名「哪个币种、哪一天」。⛔ 不按 1:1(Odoo 的静默错数)、⛔ 不取最近一条、⛔ 不写 null(sum 会忽略 null,合计悄悄少算)。
    • 来源:v1 只做手工录入与导入;行情源(ECB 等)= 应用用流程往表里写行,⛔ 平台不内置网络默认。
    • 权限与审计:改汇率等于改历史口径,sys_exchange_rate 走系统对象的权限面,写入留审计。

    6. 换算时机维度:写入 vs 读取(Q3)

    • 写入时(推荐):引擎在写入时查「单据日期」的汇率,算出本位币金额并存成一列,连同所用汇率与日期一起存;汇率更正后用显式重算操作(先例:summary 回填)。
    • 读取时:每一种汇总策略(原生 SQL、ObjectQL aggregate、内存、表格页脚、指标卡、roll-up)都要在读时按行日期 join 汇率,按驱动分叉;历史报表随汇率更正漂移。
    • 「单据日期」必须显式声明(rateDate 指向记录上的日期字段),缺则构建拒收;⛔ 不默认 created_at——那是猜。

    7. 汇总与报表语义维度(Q4)

    • record 模式字段的原始 sum/avg/min/max 在构建时拒收,除非按币种字段分组;分组结果在线上逐行带币种(今天一列一个币种会把分组标错,探针 C)。
    • 合并汇总只走本位币影子列:现有汇总、仪表盘、表格合计、指标卡零改动就出本位币合计——这是写入时存储的全部价值。
    • 今天就能加的护栏(不论 Q1 字母):派生度量跨不同币种相加减、度量 / 指标卡写死的币种与 fixed 字段矛盾 ⇒ 构建拒收。

    8. 公式维度

    9. 防 AI 犯错维度

    • 新键全部在构建时校验:currencyField 必须是 ISO 4217 值域文本字段;rateDate 必须是日期字段;本位币影子列目标必须是存储型、fixed 且币种 = 本位币。
    • 写入时缺汇率响亮拒绝;跨币种原始求和拒收并带处方;无任何静默兜底(无 CNY 默认、无 1:1、无最近汇率)。
    • 对比:B(公式函数)结果可见不可汇总;Salesforce 式隐藏列 AI 看不见;值对象让 AI 写错值形状。C′ 是四种里错误最早、最响的一种。

    10. 迁移与兼容维度

    11. 创业阶段:范围裁剪与工作量

    做(C′ 的最小集):1 个枚举成员(record)· 3 个键(currencyField、rateDate、本位币影子列声明)· 1 个系统对象(sys_exchange_rate)· 1 个重算操作 · 护栏放进既有门(spec 构建校验、analytics 编译腿),⛔ 不新增 check:* 门禁。
    不做:行情源 · 个人显示币种 · 期末重估 / 未实现汇兑损益 · N×N 汇率对 · 读时换算 · 一个组织多本位币(多主体)· 按地区的舍入规则 · 公式换算函数。
    工作量:spec 1 PR(模式 + 键 + 构建校验与拒收用例)· server 2–3 PR(系统对象 + 写入时折算与缺汇率拒写 + 重算操作)· analytics 1 PR(分组拒收与逐行币种)· ui 1 PR(币种字段显示与影子列只读);估 1–2 周 agent 时间 + 契约复核;无先例。

    12. 时序与触发

    13. 风险清单

    14. 四轴小结与推荐

    • ① 长远:C′ 与主流平台终态、与本平台架构同向,保留 ADR-0104 D1;B 破标准库不变量;甲作终态会让跨币种客户再回来。
    • ② 拉动:当前零在跑拉动;一旦出现,三件事只有 C′ 全满足。
    • ③ 防错:C′ 每个键构建时校验、缺汇率写入时拒绝、跨币种求和拒收;今天是静默错数。
    • ④ 不扩散:C′ 已压到最小集,仍是一个模式 + 一个系统对象的永久义务 ⇒ 时序上等拉动。

    推荐:Q1 乙 = C′ 形状,维持暂停(#8345 留 pm:on-hold,Restart-when: 改写为上面三个开工信号之一),并在本裁决同笔把子问题裁清:Q0 A(兄弟币种字段)· 汇率 A(手工 / 导入、缺则拒写)· 本位币 A(沿用 localization.currency,有汇率或影子列即锁定)· 换算时机 A(写入时 + 显式重算)· 汇总 A(未分组拒收、合并走影子列)。

    • 只看①选乙;②③④ 是否翻转:否(④只调时序)。
    • 若维护者认为港口客户的拉动仍在或将回:选 乙-现在 = 同形状,立 epic 按 M0–M3 分期入队;不建议丙(A′ 半量,M0 之后还要再改一次契约)。
    • 置信缺口:主流平台细节来自本席知识与测量轮的搜索摘录,未逐条核对官方文档;「1–2 周」是估算;客户实际币种数与汇率来源无样本;cloud 仓未测。

    15. 裁后执行

  6. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Workload list if C′ starts now (Q1 乙-现在) · director seat · summon #53 · 2026-10-28T09:03Z

    Asked by the maintainer after the analysis 6464438764: 「如果启动这个开发,评估具体的工作量清单」. Read on origin/main e6e41998; precedents named per PR. ⛔ Not a ruling, not a claim. Sizes are the fleet's: S = one subagent run, M = one cloud session (hours), L = a day or more with review loops; Tier = who lands it.

    Prerequisite already filed: #27157 (fixed requires a code; the home field must never fall back to CNY) — on the critical path before M0-1.

    M0 — record currency + guards (= A′; invoicing in the customer's currency works after this)

    PR Lane What (files) Size Tier / review Serial
    M0-1 spec CurrencyConfigSchema gains currencyMode: 'record' + currencyField (a text field on the same object with valueDomain: 'iso_4217_currency'); object-level superRefine in object.zod.ts (precedent L1038/L2706 arms) checks the target exists, is text, carries the domain; formula returnType: 'currency' shares it (#22727); describe / JSDoc / guidance; generated baselines, forms, packages/spec/docs; skills/** authoring guidance (Skills-docs: updated here); pin tests for the three refusals. Metadata-protocol: added, minor. M CONTRACT_REVIEW_TIER (published schema) after #27157
    M0-2 server (objectql) record-validator.ts: a record-mode amount with a blank currency code refuses loudly (ERR_CURRENCY_CODE_REQUIRED); engine-aggregate-field-type-door: sum/avg/min/max over a record-mode field refused unless grouped by its currencyField; summary roll-up over a record-mode child field refused at metadata registration (precedent: the aggregate × type door). The design round's probes become fixtures (fx_invoice: USD 100 + EUR 100 must refuse, not answer 200). M default tier after M0-1
    M0-3 spec + analytics (seam) contracts/analytics-service.ts: grouped results carry a per-row currency; cube-measure-field-type-door.ts (precedent) refuses a raw aggregate over a record-mode field unless the query groups by its code field; the Q6 guards on today's surface: a derived measure across measures whose resolved currencies differ refuses; a measure currency contradicting a fixed field's refuses. M contract half CONTRACT_REVIEW_TIER; Seam: card, spec seat owns the key after M0-1; parallel to M0-2
    M0-4 ui ui/packages/i18n/src/currency.ts resolveFieldCurrency reads the sibling code field's VALUE for record mode (needs the record in scope); callers: fields/src/widgets/CurrencyField.tsx, GridField.tsx, FormulaField.tsx, record drawer, plugin-dashboard metric tile (per-group currency), grid footer sum (refuse / group); Studio field editor: record mode + field picker. 29 files name the resolver (changelogs included); ~8 source files move. M–L default tier after M0-1
    M0-5 devx content/docs/data-modeling currency page (modes, the record mode, what refuses); #27158 lands first. S default after M0-1

    M1 — the rate table (independent of M0; can run in parallel)

    PR Lane What Size Tier Serial
    M1-1 server (platform-objects) sys_exchange_rate object: currency (text, iso_4217_currency), rate (decimal, high precision, "1 home unit = rate units of currency" documented once), effective_date (date), source (select: manual / import / app); unique (currency, effective_date); tenant scope; admin-only permission set; audit on. Precedent files: audit/sys-*.object.ts (142–294 lines each) + tests; Setup nav entry (apps/setup.app.ts) and a list view. M default none
    M1-2 server Import: verify sys_import_job serves an arbitrary object (if yes: 0 code, one docs line; if no: a CSV importer for this object, S). Provider feeds stay out: an app flow writes rows. S (XS if import already serves it) default after M1-1
    M1-3 server (service-settings) Home-currency lock: localization.currency refuses a change once a sys_exchange_rate row or a homeAmount declaration exists. service-settings has no write-side predicate hook today (config-change-audit.ts is after-the-fact) — a new guard with one predicate reading the engine; precedent for the shape: the settings manifest required / value-domain refusals. M default (new mechanism; name it in the PR) after M1-1

    M2 — write-time home projection (the core; consolidation works after this)

    PR Lane What Size Tier Serial
    M2-1 spec Amount field (record mode) declares homeAmount: { field, rateDate, rateField? }: field a stored currency field in dynamic mode (= tenant currency) marked platform-computed; rateDate a date/datetime field on the same object (⛔ no default); rateField optional decimal. superRefine arms for each; no silent default anywhere. Metadata-protocol: added, minor. M CONTRACT_REVIEW_TIER after M0-1
    M2-2 server (objectql) Write path: on create/update where amount, code or rateDate changes, look up the rate (org, currency, latest effective_date ≤ rateDate); home = round(amount × rate, minor units of the home currency) — CURRENCY_FRACTION_DIGITS gets its first reader; same-currency-as-home ⇒ rate 1, no lookup; write home + rateField; missing rate refuses the write (ERR_EXCHANGE_RATE_MISSING, naming currency + date); validator: home and rate fields read-only (precedent: summary, L1173–1180); batch writes; driver-agnostic (engine-computed column, as summary-aggregate.ts); SQLite float-storage rounding pinned. Intra-record, so simpler than the cross-record roll-up machinery. L default; contract review of the refusal envelope after M2-1, M1-1
    M2-3 server (cli + objectql) Recompute: os migrate recompute-home-amounts --object X [--since DATE] [--apply] modelled on summary-backfill.ts and commands/migrate/activity-changed-fields.ts (report-then-apply); rows whose rate is missing are listed, not silently skipped — the apply/skip rule is a parameter for the maintainer (below). M default after M2-2
    M2-4 server Adding a home field to an existing object = the normal add-column path (os migrate); backfill = M2-3. Verify, one test. S default after M2-3

    M3 — reporting, UX, dogfood

    PR Lane What Size Tier Serial
    M3-1 analytics Consolidated measures over the home field work with zero change (a test proves it); a metric / dataset measure over a record-mode field names the home field or groups by code, else refuses with the prescription. S–M default after M2-2, M0-3
    M3-2 ui Home field rendered read-only with "converted at RATE on DATE"; Studio editor for homeAmount; grid footer for record-mode columns groups or refuses. M default after M2-1, M0-4
    M3-3 devx (examples + QA) examples/app-showcase invoice gains currency_code, amount (record mode), amount_home, invoice_date; seeded sys_exchange_rate rows; a docs/qa/platform-checklist item; one browser dogfood run. M default after M2-2, M3-2
    M3-4 skills / governed A new ADR "multi-currency: record currency, dated rates, write-time home projection" recording Q0–Q4 as ruled, superseding the stale sentence in ADR-0104's currency paragraph with a dated note. S Tier H (docs/adr/**, maintainer approves) with M0-1

    Totals and path

    Parameters the maintainer decides (defaults in bold; a letter on each is enough)

    1. Backfill of existing rows when a historical rate is missing: report and skip the row, apply the rest / refuse the whole operation.
    2. Who edits sys_exchange_rate: org admin only / any user with a dedicated permission.
    3. Rate direction convention: 1 home unit = rate units of the foreign currency (Odoo) / the inverse (Dataverse). One line in the ADR either way.
    4. Precision of rate: 12 significant decimals / other.

    What is not in this list (and why)

    Provider feeds, per-user display currency, period-end revaluation / unrealised FX gains, N×N pair rates, read-time conversion, multiple home currencies per organization, locale rounding rules, a CEL convertCurrency() — each a capability with no pull in tree, and the last one conflicts with stdlib.ts:7.

    Confidence gaps

    • sys_import_job serving an arbitrary object is unverified (decides M1-2's size).
    • The settings write guard (M1-3) is a new mechanism; its shape is estimated from the manifest's refusal precedents, not measured.
    • The ui count (29 files naming the resolver) is an upper bound; ~8 source files is the seat's reading.
    • No sample of the customer's currency set or rate source; the showcase seed (M3-3) is the only fixture.
  7. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #421 item 2 (second record, Q1) · letter 乙-现在 · maintainer 「同意立专题卡」 2026-10-28T09:29Z

    Recorded by the director seat (summon #53, session_01QSeKYo7qYsX6HsAneHBMP2, ledger objectstack#12708). First record 6464202770 (Q2 X). The maintainer read the analysis 6464438764, the workload list 6464936321 and the seat's answers in the summon chat (rate sources, per-object opt-in, no platform switch, author-named fields, UI entry, enabled-currency derivation, risks, staged releases, zero impact on single-currency apps), then answered 「同意立专题卡」 — the epic form the seat proposed for 乙-现在.

    Q1 = 乙-现在: multi-currency resumes now, shape C′, as an epic on #8345.

    • Shape (C′): a record-mode amount field points at an ISO 4217 code field on the same object; a platform rate table by effective date; the home-currency amount computed AT WRITE from the document date's rate and STORED with the rate used; a missing rate refuses the write; a raw aggregate over a record-mode field refuses at build unless grouped by its code field; consolidation reads the stored home field. No CEL conversion function.
    • Sub-questions, ruled in the same stroke (the seat's defaults, none contested): Q0 A — the record currency is a sibling text field with valueDomain: 'iso_4217_currency', referenced by currencyField; rates A — sys_exchange_rate { currency, rate, effective_date, source }, manual entry and import in v1, provider feeds by an app flow, a missing rate refuses; home currency A — localization.currency, locked once a rate row or a home-amount declaration exists; conversion timing A — at write, with an explicit recompute operation; aggregation A — ungrouped raw aggregates refused, consolidation through the home field.
    • Parameters (defaults, not contested): 1 backfill with a missing historical rate reports and skips the row, applies the rest; 2 sys_exchange_rate editable by organization admins only; 3 rate direction "1 home unit = rate units of the foreign currency"; 4 rate keeps 12 significant decimals.
    • Design rules recorded from the summon: no platform or tenant switch — the declaration currencyMode: 'record' is the switch; no injected columns, no fixed field names — every currency-related field is declared by the app author and referenced by pointer, the naming convention (currency_code, AMOUNT_home, AMOUNT_rate) is guidance in skills/**, never a contract; "enabled currencies" is derived (home + currencies with a rate row + codes already used), not a setting; the form pre-fills the organization's currency at the renderer, the stored value is always explicit; metadata never names the home currency (the home field is a dynamic-mode currency field).
    • Release pairing (the only half-state risk): R1 = spec(field): fixed-mode currencyConfig with no defaultCurrency materialises 'CNY' — require the code, drop the default (narrowing; #27119 Q2 X, G3) #27157 + the ADR + M0-1..M0-4 in ONE Version Packages release (a published record mode without the refusals and the per-row display is worse than today); R2 = M1 + M2-1..M2-3 (+ M3-2's read-only display) in ONE release (the rate table's only reader is the write-time projection; the homeAmount key never ships ahead of the engine); R3 = the rest of M3. M1 is therefore developed in parallel but released with M2.
    • Commitment: R1 (about 4–5 agent-days) is the delivery the customer can rely on; R2 starts after the customer confirms the consolidation terms (home currency, rate source, which date drives the rate).

    Workload: 16 PRs per 6464936321 plus one XS prerequisite (retire the @deprecated CurrencyValueSchema, still exported at field.zod.ts:581 against ADR-0104 D1's "deleted"); L 1, M 10, S 6; about 9–12 agent-days; ~3 weeks calendar at 2–3 lanes; critical path #27157 → M0-1 → M2-1 → M2-2 → M2-3 → M3-3.

    Execution: #8345 becomes the epic parent (pm:on-hold → pm:epic, title and body rewritten with the shape, the rulings above, the territory and the child tree); the children are filed in this act as Part of #8345 (spec and seam children go to the spec seat's queue with domain:spec; the rest carry pm:epic for the epic PM); the maintainer summons the epic PM with /pm-dispatch epic:#8345. This card closes completed: Q2's execution cards are #27156 / #27157 / #27158 / #26939; Q1's is the epic.

  8. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Closed completed · both letters recorded: Q2 X (6464202770) and Q1 乙-现在 (6465431966, maintainer 「同意立专题卡」) · director seat (summon #53, session_01QSeKYo7qYsX6HsAneHBMP2) · 2026-10-28T09:34Z

    Execution lives elsewhere: Q2 → #27156 / #27157 / #27158 and #26939 (re-scoped); Q1 → the multi-currency epic on #8345 (shape C′, children filed as Part of #8345, the maintainer summons /pm-dispatch epic:#8345). The analysis (6464438764) and the workload list (6464936321) stay on this thread as the epic's design record. Nothing is left on this card.

  9. removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions