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. 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.

  2. 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.

  3. 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. 裁后执行

  4. 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.
  5. 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.

  6. 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.

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:specenhancementNew 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