@datarails-ai/dr-cli 0.0.1-locked → 1.6.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +608 -0
- package/dist/chunk-DWMCBGQH.js +242 -0
- package/dist/cli.js +64468 -0
- package/dist/kb-6UWI7Q32.js +21 -0
- package/dist/scopes-map.json +4412 -0
- package/dist/skills/connector-sandbox/SKILL.md +95 -0
- package/dist/skills/datarails-connect-workato/SKILL.md +140 -0
- package/dist/skills/datarails-connectors-onboard-connector/SKILL.md +813 -0
- package/dist/skills/dr-cli/SKILL.md +659 -0
- package/dist/skills/dr-cli/reference/admin.md +796 -0
- package/dist/skills/dr-cli/reference/budget-builder.md +151 -0
- package/dist/skills/dr-cli/reference/calc-vals.md +73 -0
- package/dist/skills/dr-cli/reference/calcmodels.md +76 -0
- package/dist/skills/dr-cli/reference/cash.md +61 -0
- package/dist/skills/dr-cli/reference/dashboards-widgets.md +85 -0
- package/dist/skills/dr-cli/reference/datamappers.md +150 -0
- package/dist/skills/dr-cli/reference/desk.md +472 -0
- package/dist/skills/dr-cli/reference/filebox-documents.md +373 -0
- package/dist/skills/dr-cli/reference/forecast.md +51 -0
- package/dist/skills/dr-cli/reference/integrations.md +57 -0
- package/dist/skills/dr-cli/reference/lookups.md +168 -0
- package/dist/skills/dr-cli/reference/metrics.md +512 -0
- package/dist/skills/dr-cli/reference/multi-product.md +180 -0
- package/dist/skills/dr-cli/reference/reconciliation-mec.md +76 -0
- package/dist/skills/dr-cli/reference/rh-config.md +84 -0
- package/dist/skills/dr-cli/reference/safety.md +143 -0
- package/dist/skills/dr-cli/reference/spend-control.md +206 -0
- package/dist/skills/dr-cli/reference/templates.md +163 -0
- package/dist/skills/dr-cli/reference/workflows.md +137 -0
- package/dist/skills/dr-error-enrichment/SKILL.md +80 -0
- package/dist/skills/dr-error-enrichment/reference/core.md +257 -0
- package/dist/skills/dr-error-enrichment/reference/groundcover.md +84 -0
- package/dist/skills/dr-error-enrichment/reference/jira-bugs.md +219 -0
- package/dist/skills/dr-error-enrichment/reference/safety-backup.md +122 -0
- package/dist/skills/dr-error-enrichment-author/SKILL.md +219 -0
- package/dist/skills/dr-error-enrichment-improve/SKILL.md +218 -0
- package/dist/skills/dr-error-enrichment-maintain/SKILL.md +237 -0
- package/dist/skills/dr-error-enrichment-reconcile/SKILL.md +167 -0
- package/dist/skills/dr-filebox-api-setup/SKILL.md +170 -0
- package/dist/skills/dr-switch-user/SKILL.md +395 -0
- package/dist/skills/dr-validate/SKILL.md +392 -0
- package/dist/skills/dr-validate/report-template.html +950 -0
- package/dist/skills/fx-conversion-matrix/SKILL.md +763 -0
- package/dist/skills/fx-conversion-matrix/scripts/_args.mjs +52 -0
- package/dist/skills/fx-conversion-matrix/scripts/_xlsx.mjs +241 -0
- package/dist/skills/fx-conversion-matrix/scripts/build-matrix.mjs +366 -0
- package/dist/skills/fx-conversion-matrix/scripts/gen-mappers.mjs +171 -0
- package/dist/skills/fx-conversion-matrix/scripts/rates-to-xlsx.mjs +172 -0
- package/dist/skills/fx-onboard/SKILL.md +325 -0
- package/dist/skills/mec-table-validation/SKILL.md +383 -0
- package/dist/skills/plugins.json +24 -0
- package/dist/skills/validate-workato-migration/SKILL.md +167 -0
- package/dist/skills/validate-workato-migration/reference/gotchas.md +16 -0
- package/dist/skills/validate-workato-migration/reference/staging-orgs.md +26 -0
- package/dist/skills/validate-workato-migration/scripts/compare_files.py +255 -0
- package/dist/zendeskKb-JPSHVOPH.js +243 -0
- package/package.json +56 -9
- package/scripts/postinstall.mjs +91 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fx-conversion-matrix
|
|
3
|
+
description: Use when getting FX rates into a Datarails customer's tables and converting any currency to any other — whether the rates come from a provider feed (XE, quoting everything against USD) or from a file the customer supplies themselves. Builds a monthly-rolling conversion-matrix filebox, lands a rate column per reporting currency and rate type (average, closing/spot, plan), and wires those into the business tables as lookups. Triggers - "import FX rates for <org>", "build an FX conversion matrix", "convert currencies to each other", "cross-rate from USD", "triangulate FX rates", "the customer sent us their own rates", "load their budget/plan rates", "add avg and closing rate columns", "wire FX rates into Financials", "register a custom currency", "roll the FX file forward a month", "add FX to the calc models". Tagging existing Monetary fields currency-aware instead is dr-cli:fx-onboard; installing or updating the CLI is dr-cli:install (or `dr update`); the full dr command reference is the dr-cli skill.
|
|
4
|
+
allowed-tools: Bash, Read, Write
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# FX conversion matrix — get rates in, and cross-rate them onto the tables
|
|
8
|
+
|
|
9
|
+
Takes FX rates from wherever the customer has them — a provider feed, or a
|
|
10
|
+
spreadsheet they maintain themselves — and produces what they actually need: any
|
|
11
|
+
currency converted to any other, as real columns on their tables, refreshed every
|
|
12
|
+
month without anyone editing a formula.
|
|
13
|
+
|
|
14
|
+
This skill teaches the FX-import flow, not the CLI. The `dr-cli` skill is the command
|
|
15
|
+
authority for every command used here, and its **Auth** section covers login and
|
|
16
|
+
identities. It ships both in the Claude Code plugin and in the npm package, so it is
|
|
17
|
+
loaded wherever this skill is.
|
|
18
|
+
|
|
19
|
+
**Scripts live in this skill's own `scripts/` folder,** the base directory Claude Code
|
|
20
|
+
prints when it loads the skill. That is the same whether it came from the `dr-cli`
|
|
21
|
+
plugin or from the npm package (`~/.claude/skills/fx-conversion-matrix/`).
|
|
22
|
+
The commands below write it as `<skill-dir>/scripts/…`.
|
|
23
|
+
|
|
24
|
+
**Division of authority:** the commands own mechanics — ids, payload shapes, what a
|
|
25
|
+
flag does. This file owns *order*, *which shape and wiring apply*, and the failure
|
|
26
|
+
modes the commands cannot see. Where the two disagree, believe the command output
|
|
27
|
+
and fix this file.
|
|
28
|
+
|
|
29
|
+
## What this is not
|
|
30
|
+
|
|
31
|
+
Not `dr-cli:fx-onboard`. That skill makes **existing** Monetary fields currency-aware
|
|
32
|
+
so widgets and DR.GET auto-convert (`fx_role`, `is_fx_aware`). This skill builds a
|
|
33
|
+
**standalone reference table of conversion rates** and lands them as ordinary Float
|
|
34
|
+
columns, usable anywhere.
|
|
35
|
+
|
|
36
|
+
**This one runs first.** The FX selector does not convert — it swaps to a sibling
|
|
37
|
+
column that already holds the amount in the chosen currency
|
|
38
|
+
(`FxReportingFieldResolver`). Those per-currency columns have to exist before
|
|
39
|
+
`fx-onboard` can tag them, and computing them is posting amount times a rate. This
|
|
40
|
+
skill is where the rate comes from. Land the rates here, compute the reporting
|
|
41
|
+
amounts, then run `fx-onboard` to make them selectable.
|
|
42
|
+
|
|
43
|
+
## Inputs to collect before starting
|
|
44
|
+
|
|
45
|
+
| Input | Why | How to confirm |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| Org + data centre | every command needs `--server` / `--account` | `dr whoami --server <S>` |
|
|
48
|
+
| Where the rates come from | feed vs customer file — changes Step 0 only | ask |
|
|
49
|
+
| Rate **shape** | single-base or directed pairs | **detect, Step 0b** |
|
|
50
|
+
| Rate **types** | avg / closing / plan — one column each | ask; most customers need ≥ 2 |
|
|
51
|
+
| Currencies | one tab + columns + mapper each | ask; never infer |
|
|
52
|
+
| Target tables | where the rates must land | ask |
|
|
53
|
+
| Wiring (A or B) | changes the workbook layout | see Step 7 |
|
|
54
|
+
|
|
55
|
+
## Use UDM field names
|
|
56
|
+
|
|
57
|
+
Every field this skill touches or creates follows the UDM convention, so the tables
|
|
58
|
+
stay readable by MEC, reconciliation and everything else that assumes it. Aliases are
|
|
59
|
+
the display name lowercased with spaces as underscores.
|
|
60
|
+
|
|
61
|
+
| Display name | Alias | Type | What it is |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `Posting Amount` | `posting_amount` | Float | the balance in local currency |
|
|
64
|
+
| `Posting Currency` | `posting_currency` | Text | the local currency it was posted in |
|
|
65
|
+
| `Reporting Currency` | `reporting_currency` | Text | the consolidation target |
|
|
66
|
+
| `Reporting Month` | `reporting_month` | Date | **last day of month, 00:00:00 UTC** |
|
|
67
|
+
| `Currency AVG rate` | `currency_avg_rate` | Float | period-average rate, for P&L |
|
|
68
|
+
| `Currency EOP rate` | `currency_eop_rate` | Float | end-of-period rate, for balance sheet |
|
|
69
|
+
| `Reporting Rate` | `reporting_rate` | Float or Text | the rate actually applied to a row |
|
|
70
|
+
|
|
71
|
+
**`Reporting Rate` is derived, not a third source rate.** It picks between the other
|
|
72
|
+
two by statement type, and the reporting amount is `Posting Amount × Reporting Rate`:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Amount = IF([Account Group L0]=="Balance Sheet",[Currency EOP rate],[Currency AVG rate])*[Posting Amount]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Get that condition wrong and it fails silently: a literal that never matches makes
|
|
79
|
+
every row fall through to the AVG branch, so balance-sheet figures are translated at
|
|
80
|
+
the wrong rate and nothing reports it. On a single-currency tenant it is invisible,
|
|
81
|
+
because AVG and EOP are both 1.
|
|
82
|
+
|
|
83
|
+
**`Reporting Month` being last-day-of-month is a UDM rule, not a preference** — and it
|
|
84
|
+
is the same normalisation Step 4 needs anyway, since `DR.MONTH()` cannot match raw
|
|
85
|
+
daily timestamps. `EOMONTH([Posting Date])` satisfies both.
|
|
86
|
+
|
|
87
|
+
### The currency goes in the name only when it is needed
|
|
88
|
+
|
|
89
|
+
UDM models **one** reporting currency per table, so a single-currency table needs no
|
|
90
|
+
currency in its rate column names — and leaving it out is what keeps the plain UDM
|
|
91
|
+
name that MEC and reconciliation look for. Add the currency only once there is more
|
|
92
|
+
than one, and keep the UDM stem when you do. `build-matrix.mjs` climbs that ladder on
|
|
93
|
+
its own:
|
|
94
|
+
|
|
95
|
+
| Reporting currencies | Column name | Set by |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| one | `Currency AVG rate` | default |
|
|
98
|
+
| several | `EUR Currency AVG rate` | default, once `--targets` names more than one |
|
|
99
|
+
| several, source worth stating | `Currency USD to EUR AVG rate` | `--column-pattern "Currency {base} to {ccy} {type} rate"` |
|
|
100
|
+
|
|
101
|
+
All three forms already exist in the reference org, so none of this is a new
|
|
102
|
+
convention — the org has `Currency AVG rate`, `GBP Currency AVG rate` and
|
|
103
|
+
`Currency USD to GBP AVG rate` side by side today. Pattern tokens are `{type}`,
|
|
104
|
+
`{ccy}` and `{base}`; `{base}` is refused in pairs mode, where the source currency
|
|
105
|
+
varies per row and there is no one base to name.
|
|
106
|
+
|
|
107
|
+
## Build on what is there. Never replace it.
|
|
108
|
+
|
|
109
|
+
A customer who already has any FX setup has it for a reason, and someone is
|
|
110
|
+
already reading those numbers. **Inventory before you create anything**, and
|
|
111
|
+
extend what you find rather than standing up a parallel copy beside it:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
dr lut list --table <business_table> --json # existing lookups, and of which kind
|
|
115
|
+
dr templates get <business_table> --json # rate columns they already have
|
|
116
|
+
dr templates get <fx_table> --json # and on the rates table
|
|
117
|
+
dr datamappers list --table <fx_table> # existing mappers
|
|
118
|
+
dr functions list --template <fx_table> # existing rate functions
|
|
119
|
+
dr filebox data-query get <fx_filebox> # an existing query and its schedule
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Read each result as a decision:
|
|
123
|
+
|
|
124
|
+
| What you find | What to do |
|
|
125
|
+
|---|---|
|
|
126
|
+
| A lookup with `document` set (filebox-sourced) | **Extend that one.** Do not add a table lookup beside it. |
|
|
127
|
+
| A lookup with `source_template` set (table-sourced) | Extend it — re-send its existing fields plus the new ones. |
|
|
128
|
+
| Rate columns that already exist | Reuse the names. A second column meaning the same thing is worse than an ugly name. |
|
|
129
|
+
| A function already on the rate field | Reuse it. Functions are cheap to make and confusing to duplicate. |
|
|
130
|
+
| A data-query already syncing | Leave its schedule alone. Widen the query, do not replace the connection. |
|
|
131
|
+
| Nothing | Only then are you choosing, and Step 7's wiring A is the default. |
|
|
132
|
+
|
|
133
|
+
**Naming is part of this.** If their columns read `Avg EUR Rate`, use
|
|
134
|
+
`--column-pattern "{type} {ccy} Rate"` so yours match. Matching an existing
|
|
135
|
+
convention matters more than the convention you would have picked.
|
|
136
|
+
|
|
137
|
+
## Order is not optional
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
rates table exists → register custom currencies → detect shape → functions
|
|
141
|
+
→ filebox → workbook → register functions → upload + tag + calculate
|
|
142
|
+
→ rate columns → lookups → verify types on EVERY table → calc models
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Step 0a — Get a rates table
|
|
148
|
+
|
|
149
|
+
### If a provider feed already lands one
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
dr filebox get <fx_filebox_id> --server <S> --json # → template_status[].table_template
|
|
153
|
+
dr templates get <table_id> --server <S> --json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### If the customer supplies their own file
|
|
157
|
+
|
|
158
|
+
Same destination, one extra hop: their file becomes a filebox, and a mapper lands it
|
|
159
|
+
as a table.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
dr filebox create --name "<Name>_FX_Rates" --scenario Actuals \
|
|
163
|
+
--date-sub-type month --parent <folder_id> --server <S> --json
|
|
164
|
+
dr filebox upload-version <filebox_id> --file their-rates.xlsx --server <S> --json
|
|
165
|
+
dr filebox set-version-date-tag <version_id> --month <YYYY-MM> --server <S> --json
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Then a mapper onto a new (or existing) table, mapping at minimum: the currency
|
|
169
|
+
column(s), the period column, and one column per rate type. `dr datamappers create
|
|
170
|
+
flat` handles the common single-header-row sheet; `from-file` is the power path.
|
|
171
|
+
**`--field` on `flat` keys on the CSV HEADER, not the table field.** Every CSV column
|
|
172
|
+
defaults to `Text` (CSV values are strings) and `--field` overrides that — but the lookup
|
|
173
|
+
uses the column name as it appears in row 1 of the file, *before* any `--rename`. So with
|
|
174
|
+
`--rename "mid=FX Rate"` the override is `--field "mid:Float"`; writing
|
|
175
|
+
`--field "FX Rate:Float"` matches no header, silently does nothing, and the column lands
|
|
176
|
+
`Text`. Nothing warns you, and `datamappers get` omits `fields_to_change` from its
|
|
177
|
+
response — so you cannot confirm what was sent by reading the mapper back. Check the
|
|
178
|
+
column type on the table instead.
|
|
179
|
+
|
|
180
|
+
### First import: 24 months, not one
|
|
181
|
+
|
|
182
|
+
A first import backfills **24 historical months**, so variance against last year
|
|
183
|
+
works from day one and nobody has to come back for it later. On a data-query
|
|
184
|
+
filebox that is one call with one `--date` per period (the flag repeats):
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
dr filebox data-query sync <filebox_id> --date 2026-08-01 --date 2026-07-01 ... --server <S>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Each period lands as its own version, named for its month. Verified: 24 dates in
|
|
191
|
+
a single call produced 24 versions, September 2024 through August 2026. The sync
|
|
192
|
+
is asynchronous — poll `dr filebox data-query get <id>` until `status` leaves
|
|
193
|
+
`SYNCING` before trusting a version count.
|
|
194
|
+
|
|
195
|
+
Afterwards the ongoing schedule takes over; `sync_offset_array: [0, -1]` re-syncs
|
|
196
|
+
the current and previous period on each run, which is what picks up a late
|
|
197
|
+
correction to last month.
|
|
198
|
+
|
|
199
|
+
For a customer file, 24 months means 24 tagged versions, or one file carrying a
|
|
200
|
+
period column. Ask which they have before assuming.
|
|
201
|
+
|
|
202
|
+
Customer-maintained rate files are usually *plan* or *board-approved* rates, not
|
|
203
|
+
market rates. Do not assume a monthly cadence, and do not assume they will be
|
|
204
|
+
updated on time — ask who owns the file and how the new month gets in.
|
|
205
|
+
|
|
206
|
+
### Either way: do not trust field names
|
|
207
|
+
|
|
208
|
+
A rates table that has been iterated on collects plausible-looking dead fields. The
|
|
209
|
+
reference org's FX table carries eight of them — `Currency AVG rate`,
|
|
210
|
+
`Currency EOP rate`, `GBP Currency AVG rate`, `GBP`, `USD to GBP`,
|
|
211
|
+
`Currency USD to GBP AVG rate`, `Rate to GBP` — all either null or hardcoded to one
|
|
212
|
+
currency. The live field was plainly named `FX Rate`.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
dr templates distinct <table_id> --field "Posting Currency" --server <S> --json
|
|
216
|
+
dr templates aggregate <table_id> --group-by "Posting Currency" \
|
|
217
|
+
--group-by "Reporting Month" --metric "<rate field>:AVG" --server <S> --json
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
A field is live when **every** currency moves month to month. An existing function
|
|
221
|
+
pointing at a field is not evidence the field is populated.
|
|
222
|
+
|
|
223
|
+
### The period field must be month-normalised
|
|
224
|
+
|
|
225
|
+
A feed that arrives daily (XE lands one row per currency per day) gives a period column
|
|
226
|
+
full of timestamps like `2026-08-01T00:00:00+00:00`. `DR.MONTH(DR_TAG_DATE)` does **not**
|
|
227
|
+
match those, and the matrix computes **all zeros with no error** — a third silent-zero
|
|
228
|
+
cause on top of the two in Steps 4 and 5. Derive a month column and point the matrix at it:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
calculated_fields: [{ name: "Reporting Month", formula: " EOMONTH( [Posting Date] )" }]
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`EOMONTH` needs its input to already be a `Date`. If the source column is still `Text` the
|
|
235
|
+
calc silently produces no column at all — no error, the field simply never appears. Pin the
|
|
236
|
+
date column's type in the same mapper (`from-file --field "Posting Date:Date"`), not
|
|
237
|
+
afterwards.
|
|
238
|
+
|
|
239
|
+
> **`templates set-field-type` does not survive a rescan.** Retyping a column after the fact
|
|
240
|
+
> works, and is then silently reverted the next time the table is rescanned, because the
|
|
241
|
+
> mapper re-infers from the source. Types must be pinned in the mapper. The one place
|
|
242
|
+
> retyping is the only option is a lookup target (Step 7b), which has no mapper.
|
|
243
|
+
|
|
244
|
+
## Step 0b — Detect the shape, and stop if it is ambiguous
|
|
245
|
+
|
|
246
|
+
Two shapes reach this skill, and they are converted completely differently. Detect;
|
|
247
|
+
do not assume.
|
|
248
|
+
|
|
249
|
+
**The structural signal is how many currency dimensions the table has.**
|
|
250
|
+
|
|
251
|
+
| Currency dimensions | Shape | Meaning |
|
|
252
|
+
|---|---|---|
|
|
253
|
+
| one (e.g. `Posting Currency`) | `single-base` | every rate quoted against one currency |
|
|
254
|
+
| two (e.g. `Posting Currency` + `Reporting Currency`) | `pairs` | directed pairs, supplied as-is |
|
|
255
|
+
|
|
256
|
+
Then confirm against the data, and **stop rather than guess**:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
# single-base: exactly ONE code must be 1.0 in every period -- that is the base
|
|
260
|
+
dr templates aggregate <id> --group-by "Posting Currency" --metric "<rate>:AVG" --server <S> --json
|
|
261
|
+
|
|
262
|
+
# pairs: which (from, to) combinations actually exist
|
|
263
|
+
dr templates aggregate <id> --group-by "Posting Currency" \
|
|
264
|
+
--group-by "Reporting Currency" --metric "<rate>:AVG" --server <S> --json
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Stop conditions — report to the implementor, do not proceed:**
|
|
268
|
+
|
|
269
|
+
- **No currency is always 1.0** in a one-dimension table. The base may not be present
|
|
270
|
+
as a row, or the field is not what you think. Ask.
|
|
271
|
+
- **This is the normal case for a provider feed.** XE quotes every currency *against* the
|
|
272
|
+
base and never against itself, so a real XE extract contains no USD row at all. The
|
|
273
|
+
matrix needs that 1.0 anchor — to convert *from* the base, and to anchor the target
|
|
274
|
+
column when the target *is* the base. Inject it as a **separate document and mapper**
|
|
275
|
+
into the same table (one row per period, rate `1`), which is what the reference org's
|
|
276
|
+
own `fx_integration_usd` mapper does. Do not edit the provider's file to add it: the
|
|
277
|
+
feed should stay exactly as it arrived.
|
|
278
|
+
- **More than one currency is always 1.0.** Either the data is wrong, or these are
|
|
279
|
+
pegged currencies and the base is genuinely ambiguous. Ask which is the base.
|
|
280
|
+
- **Pairs mode with gaps.** Every (source, target) the customer wants must be
|
|
281
|
+
present. List the missing pairs and ask for them. **Never derive a missing pair.**
|
|
282
|
+
Triangulating through a pivot currency invents a rate the customer never agreed
|
|
283
|
+
to, and the result is indistinguishable from one they did. If they explicitly ask
|
|
284
|
+
for it, convert their file to single-base against that pivot instead — then the
|
|
285
|
+
derivation is visible in the workbook rather than hidden in this step.
|
|
286
|
+
|
|
287
|
+
> **Pairs mode cannot detect its own gaps later.** A pair the source lacks returns
|
|
288
|
+
> `0`, with no error. Coverage has to be established here or not at all.
|
|
289
|
+
|
|
290
|
+
## Step 1 — Register any custom currencies
|
|
291
|
+
|
|
292
|
+
A code the ISO list does not carry (`BUSD` for budgeted USD, a plan-rate code, an
|
|
293
|
+
internal unit) must exist as an org custom currency **before** any field references
|
|
294
|
+
it, or the Tables dialog cannot offer it back and the org list and the field drift
|
|
295
|
+
apart.
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
dr fx default-currency --server <S> # lists what the org can already offer
|
|
299
|
+
dr fx add-currency BUSD --symbol "B$" --server <S>
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`add-currency` refuses an ISO code, so it is safe to run over a mixed list. See the
|
|
303
|
+
`dr-cli:fx-onboard` skill for how the org currency list is used downstream.
|
|
304
|
+
|
|
305
|
+
## Step 2 — Create one function per rate type
|
|
306
|
+
|
|
307
|
+
A Datarails function binds exactly **one** value field, so a customer who supplies
|
|
308
|
+
average and closing rates needs two functions. Names take no whitespace.
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
dr functions create --name FX_Avg_Rate --template <table_id> \
|
|
312
|
+
--field <avg_field_id> --agg AVG --server <S> --json
|
|
313
|
+
dr functions create --name FX_EOP_Rate --template <table_id> \
|
|
314
|
+
--field <eop_field_id> --agg AVG --server <S> --json
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
No persisted filters — currency and period are supplied per-cell. These are
|
|
318
|
+
prod-tier writes; confirm first.
|
|
319
|
+
|
|
320
|
+
## Step 3 — Create the filebox
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
dr filebox create --name "<Name>_Conversion_Rates" --scenario Actuals \
|
|
324
|
+
--date-sub-type month --parent <folder_id> --server <S> --json
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Put it next to the source FX filebox (same `--parent`) so it is discoverable.
|
|
328
|
+
|
|
329
|
+
## Step 4 — Build the workbook
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
node <skill-dir>/scripts/build-matrix.mjs \
|
|
333
|
+
--out matrix.xlsx --base USD \
|
|
334
|
+
--rate-types "Avg=FX_Avg_Rate,Spot=FX_EOP_Rate" \
|
|
335
|
+
--currencies USD,EUR,GBP,ILS --targets EUR,GBP,ILS \
|
|
336
|
+
--shape single-base --layout matrix \
|
|
337
|
+
--period-field "Reporting Month" --currency-field "Posting Currency"
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
For `--shape pairs` add `--to-currency-field "Reporting Currency"`. For wiring B use
|
|
341
|
+
`--layout lut` (Step 7). The script writes `matrix.xlsx.layout.json` alongside the
|
|
342
|
+
workbook — Step 6 consumes it so the two never disagree about a cell.
|
|
343
|
+
|
|
344
|
+
Column names follow the ladder above — `Currency AVG rate` for one reporting currency,
|
|
345
|
+
`EUR Currency AVG rate` for several — or whatever `--column-pattern` you pass to match an
|
|
346
|
+
org's existing names.
|
|
347
|
+
|
|
348
|
+
**How single-base converts.** Every row is quoted against the same base, so
|
|
349
|
+
`rate(source→target) = rate(target)/rate(source)`, anchored on the target's own row.
|
|
350
|
+
One `DR.GET` per row per type, not one per pair, and it degenerates correctly when
|
|
351
|
+
the base is itself a row (its rate is 1, and the self-rate comes out exactly 1).
|
|
352
|
+
|
|
353
|
+
**Pairs mode derives nothing** — each cell is a direct lookup of the supplied pair.
|
|
354
|
+
|
|
355
|
+
**Argument order fails silently.** The period pair must come immediately after the
|
|
356
|
+
function name, before any other dimension filter, or `DR.GET` returns `0` — no
|
|
357
|
+
error, no `#NAME?`:
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
=_xll.DR.GET(FX_Avg_Rate,"[Reporting Month]",_xll.DR.MONTH(DR_TAG_DATE),"[Posting Currency]","EUR","[Scenario]","Actuals")
|
|
361
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this pair first
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
`DR_TAG_DATE` is built in — do not declare it; the backend resolves it at calc time
|
|
365
|
+
once the file is a tagged filebox version. Wrap it in `DR.MONTH()` for a Date period
|
|
366
|
+
field. For a human-readable label use `=TEXT(DR_TAG_DATE,"mmmm yyyy")` — `DR.MONTH()`
|
|
367
|
+
returns a range-like pseudo-value and cannot be fed to `TEXT()`.
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
unzip -o matrix.xlsx -d /tmp/m && grep -o '<f>[^<]*</f>' /tmp/m/xl/worksheets/sheet1.xml
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Step 5 — Register the functions, then upload, tag, calculate
|
|
374
|
+
|
|
375
|
+
A brand-new function has no defined name in a freshly built workbook, and `DR.GET`
|
|
376
|
+
against an unregistered name **also returns `0` silently**. Register **every**
|
|
377
|
+
function — repeat `--function` per rate type:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
dr documents add-functions matrix.xlsx --function <avg_fn_id> --function <eop_fn_id> \
|
|
381
|
+
--out matrix_fnreg.xlsx --server <S>
|
|
382
|
+
grep -o '<definedName name="FX_[^"]*">[^<]*</definedName>' /tmp/m/xl/workbook.xml
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
dr filebox upload-version <filebox_id> --file matrix_fnreg.xlsx --server <S> --json
|
|
387
|
+
dr filebox set-version-date-tag <version_id> --doc <filebox_id> --month <YYYY-MM> --server <S> --json
|
|
388
|
+
dr documents calculate <filebox_id> --month <YYYY-MM> --no-roll --server <S>
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**`--no-roll` matters.** `calculate` defaults to `do_roll=true`, advancing the tag to
|
|
392
|
+
the *next* period after computing — correct for the monthly cycle (Step 8), wrong
|
|
393
|
+
here: it would leave the visible version tagged to a month with no source data,
|
|
394
|
+
reading as all zeros.
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
dr filebox download <filebox_id> --server <S> --output result.xlsx --force
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Two cheap, high-signal checks per rate type: each tab's **self-rate is exactly 1**,
|
|
401
|
+
and one **reciprocal pair multiplies to 1**.
|
|
402
|
+
|
|
403
|
+
## Step 6 — Land the rate columns (wiring A)
|
|
404
|
+
|
|
405
|
+
One datamapper per target currency, each writing one column per rate type onto the
|
|
406
|
+
**FX table**:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
node <skill-dir>/scripts/gen-mappers.mjs \
|
|
410
|
+
--layout-file matrix.xlsx.layout.json \
|
|
411
|
+
--filebox <filebox_id> --version <document_version_id> --template <fx_table_id> \
|
|
412
|
+
--out-dir ./mappers --server <S>
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
It prints a ready command per currency. The target table is the **positional**
|
|
416
|
+
argument — `create from-file <table_id>`, not `--table`.
|
|
417
|
+
|
|
418
|
+
**Always pass `--field "<column>:Float"` for every mapped column** (the generator
|
|
419
|
+
does). A `map_to` naming a column the template lacks **bootstraps that field** — that
|
|
420
|
+
is how these columns come into being, there is no add-field API — but the
|
|
421
|
+
bootstrapped **type is inferred from the sample values, often Text**, and a Text rate
|
|
422
|
+
column silently breaks every downstream calculation.
|
|
423
|
+
|
|
424
|
+
**The body must carry no `template_id`.** In power mode that key names the *source*
|
|
425
|
+
template, and the CLI resolves every `map_to` from it and **throws on any name the
|
|
426
|
+
source lacks** — and that throw happens *before* `--field` overrides are merged, so
|
|
427
|
+
`--field` cannot rescue it. The rate columns exist nowhere yet by design, so a body
|
|
428
|
+
carrying `template_id` fails every create. (The thrown message advises `--field`; on
|
|
429
|
+
this path that advice does not hold.) The generator omits it.
|
|
430
|
+
|
|
431
|
+
`document_version_id` is required and is **not** in `dr datamappers get` output —
|
|
432
|
+
read it from the upload response or `dr filebox versions <filebox_id>`.
|
|
433
|
+
|
|
434
|
+
### Changing a mapper that already exists: remap, don't update
|
|
435
|
+
|
|
436
|
+
Widening an existing feed mapper, or adding reporting-amount columns to a business
|
|
437
|
+
table's mappers, means editing mappers that already exist. `dr datamappers update` is
|
|
438
|
+
the obvious tool, and on many orgs **it cannot work.** A mapper created by an org
|
|
439
|
+
import or template push has no source version on record. `PUT
|
|
440
|
+
/api/templates/mappers/<id>` reads the mapper's source file, and for such a mapper it
|
|
441
|
+
fails with a bare HTTP 500. The server log shows `AttributeError: 'NoneType' object has
|
|
442
|
+
no attribute 'get_document_data'`. No request body fixes it, because the endpoint never
|
|
443
|
+
reads a version from the body.
|
|
444
|
+
|
|
445
|
+
Remap instead. `dr datamappers create from-file <table_id>` with the existing
|
|
446
|
+
mapper's id replaces it in place and records a real source version:
|
|
447
|
+
|
|
448
|
+
```json
|
|
449
|
+
{
|
|
450
|
+
"name": "<existing name>",
|
|
451
|
+
"datamapper_id": <existing mapper id>,
|
|
452
|
+
"ordinal": <existing ordinal>,
|
|
453
|
+
"document_version_id": <an ACTIVE version of one of its documents>,
|
|
454
|
+
"selected_document_ids": [<its mapped_documents_ids>],
|
|
455
|
+
"sheet_name": "<a sheet that exists in THAT version>",
|
|
456
|
+
"config": { ... the edited config ... }
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
- **The mapper gets a new id.** The old id stays behind bound to no documents, and
|
|
461
|
+
lookups, which live on the table, are unaffected. Keep an old→new id log for rollback.
|
|
462
|
+
- **`ordinal` must be the existing one.** A lower one is refused as a duplicate mapper.
|
|
463
|
+
- **`sheet_name` must exist in the version you pass**, and a mapper's
|
|
464
|
+
`preview_sheet_name` is often stale. Check with `dr filebox sheets <version_id>`; when
|
|
465
|
+
the preview sheet is gone and the version has one sheet, use that one.
|
|
466
|
+
- **Use an ACTIVE version, not simply the latest.** A version in `ERROR` status cannot be
|
|
467
|
+
read, and the remap 500s with `Cannot read sheet with name '<sheet>'`. Pick the newest
|
|
468
|
+
version whose status is `ACTIVE`.
|
|
469
|
+
- **Skip mappers whose documents are all gone.** A mapper can stay bound only to
|
|
470
|
+
deleted documents (`dr filebox get` returns 404). It feeds no rows, and there is no
|
|
471
|
+
version to remap against. Confirm with `templates aggregate --group-by FileBox_ID`
|
|
472
|
+
before skipping one.
|
|
473
|
+
- **Widen a feed only by appending columns at the end.** A header entry keyed on a
|
|
474
|
+
column's letter keeps working when new columns are added after it. Inserting columns
|
|
475
|
+
in the middle breaks every mapper that reads the file.
|
|
476
|
+
|
|
477
|
+
Pass `--no-rescan` on each remap and let Step 7's lookup rebuild drive the rescans.
|
|
478
|
+
|
|
479
|
+
## Step 7 — Carry the rates into the business tables
|
|
480
|
+
|
|
481
|
+
**The wiring is not yours to pick if they already have one.** Run
|
|
482
|
+
`dr lut list --table <business_table> --json` first and match what is there — a
|
|
483
|
+
lookup with `document` set is filebox-sourced (B), one with `source_template` set is
|
|
484
|
+
table-sourced (A). Only an org with no rate lookup at all is a free choice, and then
|
|
485
|
+
A is the default.
|
|
486
|
+
|
|
487
|
+
**Wiring A — one table-sourced lookup per business table (the default for a fresh
|
|
488
|
+
org).** The columns already live on the FX table, so one lookup carries every
|
|
489
|
+
currency and every rate type:
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
dr lut create-table-lookup --table <business_table> --source-table <fx_table> \
|
|
493
|
+
--keys "Posting Currency,Reporting Month" \
|
|
494
|
+
--fields "EUR Currency AVG rate,EUR Currency EOP rate,GBP Currency AVG rate" --server <S>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**`--keys` and `--fields` take ONE comma-separated list each — they are not repeatable.**
|
|
498
|
+
Repeating the flag silently keeps only the last value, producing a one-key, one-field
|
|
499
|
+
lookup that looks like it succeeded. Check the `KEY_LEN`/`KEYS`/`FIELDS` counts the
|
|
500
|
+
command prints against what you passed.
|
|
501
|
+
|
|
502
|
+
Adding a currency later touches one mapper and one `--fields` entry, not every table.
|
|
503
|
+
|
|
504
|
+
**Wiring B — one filebox-sourced lookup per currency tab.** Skips Step 6 and points
|
|
505
|
+
each business table straight at the matrix file:
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
dr lut create --table <business_table> --payload-file ./luts/eur.json --server <S>
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
> **Wiring B needs `--layout lut` in Step 4.** A filebox-sourced LUT resolves its keys
|
|
512
|
+
> **positionally from the source file's column-header row** — the first `key_len`
|
|
513
|
+
> columns become the keys and the payload's `keys`/`fields` arrays are *silently
|
|
514
|
+
> ignored*. Against the `matrix` layout a 2-key lookup would take `Currency` and the
|
|
515
|
+
> base-rate helper as its keys. The `lut` layout puts the keys leftmost and makes the
|
|
516
|
+
> period a real column. `dr lut create` prints a stderr warning when the stored shape
|
|
517
|
+
> differs from what was sent — **read it**, it is the only signal you get.
|
|
518
|
+
|
|
519
|
+
### Extending a lookup that already exists
|
|
520
|
+
|
|
521
|
+
**Table-sourced.** `update-table-lookup` overlays only what you pass, but `--fields`
|
|
522
|
+
is a whole list, not an append — so send the existing fields **plus** the new ones,
|
|
523
|
+
or the ones you omit are dropped:
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
dr lut list --table <tid> --json # read its current fields first
|
|
527
|
+
dr lut update-table-lookup <lut_id> --fields "<existing>,<existing>,<new>" --server <S>
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
**Filebox-sourced — the important case.** A filebox-sourced lookup resolves its
|
|
531
|
+
fields **positionally from the source file**, so you cannot add a field by editing
|
|
532
|
+
the lookup. Widen the *file*: add the new rate column to the query or workbook that
|
|
533
|
+
feeds it, re-sync, and the lookup picks the column up. Editing the LUT's `fields`
|
|
534
|
+
array alone changes nothing — the engine ignores it.
|
|
535
|
+
|
|
536
|
+
That is the whole reason to check first. An org on wiring B gets its new currency by
|
|
537
|
+
widening their existing feed, which is a one-line change to a query. Standing up a
|
|
538
|
+
table lookup beside it would leave two sources of the same rate disagreeing the
|
|
539
|
+
first time one is re-synced and the other is not.
|
|
540
|
+
|
|
541
|
+
Either wiring returns 202 and computes asynchronously; re-run
|
|
542
|
+
`dr lut list --table <tid>` to confirm.
|
|
543
|
+
|
|
544
|
+
### Build the lookup after the source has finished loading
|
|
545
|
+
|
|
546
|
+
A table-sourced lookup snapshots its source table **when it is created or updated.**
|
|
547
|
+
Point it at an FX table whose new versions are still being processed (a backfill that
|
|
548
|
+
reads `SYNCED` but is still scanning), and it builds **empty**. Every target row then
|
|
549
|
+
gets an empty rate, with no error. The only trace is in the server log:
|
|
550
|
+
`total_luts_size_bytes=0`. `lut list` looks fine, and its key and field counts are
|
|
551
|
+
correct.
|
|
552
|
+
|
|
553
|
+
So order it: sync → wait for `dr scans status <fx_table>` to leave `in_process` →
|
|
554
|
+
create or extend the lookups. If you already built one too early, re-run
|
|
555
|
+
`dr lut update-table-lookup <id> --table <tid>` with any flag, for example
|
|
556
|
+
`--check-misses true`. That rebuilds the lookup from the current source and triggers
|
|
557
|
+
a selective rescan of the affected versions on the target table, which also picks up
|
|
558
|
+
any mapper you remapped in the meantime. Rebuild again after every later backfill.
|
|
559
|
+
|
|
560
|
+
Verify on the target, not the lookup:
|
|
561
|
+
`dr templates aggregate <tid> --group-by "Reporting Month" --metric "EUR Currency AVG rate:AVG"`
|
|
562
|
+
must be non-zero in every month the FX table covers.
|
|
563
|
+
|
|
564
|
+
## Step 7b — Verify the types on EVERY table, not just the source
|
|
565
|
+
|
|
566
|
+
**This is where the reference org is currently broken.** On its FX table the columns
|
|
567
|
+
are Float; on Financials, where the lookup carries them, `Rate to EUR` / `Rate to GBP`
|
|
568
|
+
/ `Rate to ILS` all landed as **Text**. Arithmetic on them is broken or silently
|
|
569
|
+
coercing, and nothing reported it.
|
|
570
|
+
|
|
571
|
+
```bash
|
|
572
|
+
dr templates get <fx_table_id> --server <S> --json # source columns
|
|
573
|
+
dr templates get <business_table> --server <S> --json # lookup targets -- CHECK THESE
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
A correct source column is not evidence of a correct target column, and **the target
|
|
577
|
+
type is not predictable, so check it every time.** Both outcomes have been seen on live
|
|
578
|
+
tenants with Float sources:
|
|
579
|
+
|
|
580
|
+
- **Text:** why the reference org's Financials columns are Text today.
|
|
581
|
+
- **Float:** 54 lookup targets across 9 tables on a DEMO tenant (2026-09). The source
|
|
582
|
+
columns had been pinned `Float` through the mapper's `--field` in Step 6.
|
|
583
|
+
|
|
584
|
+
`lut create-table-lookup` has no `--field` to pin the target type. Retype every target
|
|
585
|
+
that came in Text:
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
dr templates set-field-type <field_id> --type Float --server <S> # plan, then --confirm-token
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
This is the one retype that sticks, because a lookup target has no mapper to re-infer it.
|
|
592
|
+
|
|
593
|
+
## Step 7c — Calc models: the rows no mapper touches
|
|
594
|
+
|
|
595
|
+
A calc model writes its output rows **straight into the table**. They skip the mappers,
|
|
596
|
+
so the per-currency amount columns you add in the mappers never reach them. On a
|
|
597
|
+
Financials table that uses Beginning Balance models, that is every balance-sheet opening
|
|
598
|
+
balance: the currency columns come out empty on exactly the rows a balance-sheet report
|
|
599
|
+
reads first.
|
|
600
|
+
|
|
601
|
+
Find them before you call the reporting amounts done:
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
dr calcmodels list --server <S>
|
|
605
|
+
dr templates aggregate <table> --group-by "Calc_Model_ID" \
|
|
606
|
+
--metric "Amount:SUM" --metric "Amount EUR:SUM" --server <S>
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Rows with a `Calc_Model_ID` and an empty currency column are this step's job.
|
|
610
|
+
|
|
611
|
+
### What a custom column can see
|
|
612
|
+
|
|
613
|
+
Read these before you write a formula (they come from the server's calc-model engine):
|
|
614
|
+
|
|
615
|
+
- **A model rolls up exactly one field**, its `agg_field`. Everything else on an output
|
|
616
|
+
row is the date field, a `group_by` field, the two `Calc_Model_*` fields, or a lookup
|
|
617
|
+
output.
|
|
618
|
+
- **`custom_columns` are evaluated on the output rows, after the roll-up.** A formula
|
|
619
|
+
can only reference columns that exist on the output row. A source column that is not
|
|
620
|
+
in `group_by` is **not available**.
|
|
621
|
+
- **Lookups do run on output rows, but only lookups whose keys are all on the row.** An
|
|
622
|
+
FX lookup keyed on `Reporting Month` and `Posting Currency` works when the model's date
|
|
623
|
+
field is `Reporting Month` and `Posting Currency` is in `group_by`. If either key is
|
|
624
|
+
missing, the rate is empty and the column silently computes nothing.
|
|
625
|
+
- **Only `is_selected: true` columns are computed.** Only `function.formula` matters:
|
|
626
|
+
`function.type` and `function.fields` are frontend metadata that the engine ignores.
|
|
627
|
+
|
|
628
|
+
### The formula
|
|
629
|
+
|
|
630
|
+
Mirror the model's own value field. Most Beginning Balance models roll up `Amount`,
|
|
631
|
+
which is already in the reporting currency (USD, say). Convert that balance back to the
|
|
632
|
+
posting currency at the row's month-end rate, then out to each target currency at that
|
|
633
|
+
currency's month-end rate:
|
|
634
|
+
|
|
635
|
+
```
|
|
636
|
+
Amount EUR = [Amount] / [Currency EOP rate] * [EUR Currency EOP rate]
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
A model that rolls up `Posting Amount` needs one step:
|
|
640
|
+
`[Posting Amount] * [EUR Currency EOP rate]`.
|
|
641
|
+
|
|
642
|
+
**Know what this number is.** It translates the whole balance at that month's closing
|
|
643
|
+
rate (the closing-rate method). It is **not** the sum of each month's activity
|
|
644
|
+
translated at that month's rate, which is what the model's own USD `Amount` column
|
|
645
|
+
holds. The two differ whenever rates move. If the customer needs the historical-rate
|
|
646
|
+
version, the only way is one extra model per currency, each rolling up its own
|
|
647
|
+
`Amount <X>`. That is a separate decision for the owner.
|
|
648
|
+
|
|
649
|
+
### Applying it
|
|
650
|
+
|
|
651
|
+
```bash
|
|
652
|
+
dr calcmodels get <id> --server <S> --json > cm.json
|
|
653
|
+
# append one custom column per currency to config.custom_columns:
|
|
654
|
+
# {"id": <next>, "field": {"id": <Amount EUR field id>}, "is_selected": true,
|
|
655
|
+
# "function": {"type": "custom", "fields": [], "formula": "<formula>"}}
|
|
656
|
+
dr calcmodels update <id> --file cm.json --server <S>
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Two traps on the way:
|
|
660
|
+
|
|
661
|
+
- **Decode the formulas before you PUT.** `calcmodels get` returns existing formulas
|
|
662
|
+
HTML-encoded (`"Beginning Balance"`), and `update` sends them back
|
|
663
|
+
unchanged. The server then fails to parse its own `Data Type` column, and the model
|
|
664
|
+
goes to **ERROR**. Nothing names the cause on the CLI side; the server log shows
|
|
665
|
+
`SyntaxError ... lambda :"Beginning Balance"`. Replace `"`, `&`,
|
|
666
|
+
`<`, `>` and `'` in every `function.formula` first.
|
|
667
|
+
- **The CLI refuses `AUTO` on a model that writes back into its own source table**, and
|
|
668
|
+
Beginning Balance models always do. Models created before that check are often still
|
|
669
|
+
on `AUTO`, so a plain `update` is refused. Do **not** flip them to `DAILY` to get past
|
|
670
|
+
the check. That changes how often the customer's balances refresh, and it is the
|
|
671
|
+
owner's call. Either get the owner's decision, or add the columns in the Tables UI,
|
|
672
|
+
which keeps the flow as it is. Existing `AUTO` models of this kind have been observed
|
|
673
|
+
running stably, so the refusal alone is not evidence of a loop.
|
|
674
|
+
|
|
675
|
+
Every save retriggers a recalculation. Poll `dr calcmodels get <id>` until `status` leaves
|
|
676
|
+
`PROCESSING`. It must come back `ACTIVE`; on `ERROR`, check the decoding trap first.
|
|
677
|
+
|
|
678
|
+
### Verify
|
|
679
|
+
|
|
680
|
+
In a month that has rates, `Amount EUR` should equal `Amount / (EUR→USD rate)`. Check one:
|
|
681
|
+
|
|
682
|
+
```bash
|
|
683
|
+
dr templates aggregate <table> --group-by "Reporting Month" \
|
|
684
|
+
--metric "Amount:SUM" --metric "Amount EUR:SUM" --metric "Currency EOP rate:AVG" \
|
|
685
|
+
--filter "Calc_Model_ID=<id>" --filter "Posting Currency=EUR" --server <S>
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
**Beginning Balance rows run to the table's last month, not the last month with rates.**
|
|
689
|
+
A table that carries budget or forecast rows into future years gets opening balances in
|
|
690
|
+
those years too, and every one of them computes `0` in the currency columns until those
|
|
691
|
+
months have rates (plan rates, usually). Report that as a rate-coverage gap, not a
|
|
692
|
+
formula bug.
|
|
693
|
+
|
|
694
|
+
## Step 8 — Rolling each month
|
|
695
|
+
|
|
696
|
+
No re-upload, no formula edit — the `DR.GET` calls re-resolve against whatever period
|
|
697
|
+
the version is tagged to.
|
|
698
|
+
|
|
699
|
+
- **Recompute the same period:** `set-version-date-tag` + `documents calculate --month <M> --no-roll`
|
|
700
|
+
- **Roll forward:** `documents calculate --month <current>` (default roll)
|
|
701
|
+
- **Roll backward:** same, plus `--backward`
|
|
702
|
+
|
|
703
|
+
A customer-maintained rate file adds a dependency here: rolling forward before they
|
|
704
|
+
have entered next month's rates computes zeros. Confirm the source month exists first.
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
## Handing someone a rate table
|
|
709
|
+
|
|
710
|
+
The chain above lands rates as *columns on tables*, which is what the product
|
|
711
|
+
consumes. When a human needs to read them — a customer asking "what rate did you
|
|
712
|
+
use?", an auditor, a month-end pack — a square matrix is the presentable form:
|
|
713
|
+
|
|
714
|
+
```bash
|
|
715
|
+
node <skill-dir>/scripts/rates-to-xlsx.mjs --in rates.csv --out matrix.xlsx [--date 2026-08-31]
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
It takes a `from_currency, to_currency, rate, timestamp` feed — the shape a
|
|
719
|
+
triangulating SQL data-query produces — and writes a square matrix for one date
|
|
720
|
+
plus a `Daily Rates` tab with every pair. Column names are matched
|
|
721
|
+
case-insensitively with a positional fallback, `--date` defaults to the latest in
|
|
722
|
+
the file, and a pair the feed does not carry is left **blank rather than guessed**.
|
|
723
|
+
|
|
724
|
+
All three scripts share `_xlsx.mjs`, the dependency-free .xlsx writer, so a fix to
|
|
725
|
+
the package layout reaches every producer.
|
|
726
|
+
|
|
727
|
+
## Guard rails
|
|
728
|
+
|
|
729
|
+
- **Every write here is prod-tier.** Functions, filebox, mappers, lookups and custom
|
|
730
|
+
currencies are all mutations. Confirm before running on `US`, `US2`, `UK`, `CA`,
|
|
731
|
+
`EU` — **and on `DEMO`**, which is a customer-facing demo tenant, not a sandbox.
|
|
732
|
+
Prefer a `DEV`/`TEST*` rehearsal when the org is unstated.
|
|
733
|
+
- **Never guess a currency, a base, or a missing pair.** Each is a question for the
|
|
734
|
+
customer. A wrong rate mislabels real money and nobody downstream can tell.
|
|
735
|
+
- **Never guess the rate field.** Sample real rows (Step 0a).
|
|
736
|
+
- **Datamappers bootstrap schema.** A typo in `map_to` does not error — it creates a
|
|
737
|
+
junk column on a production table. Check spelling before, and types after.
|
|
738
|
+
- **Don't leave half-built objects.** An abandoned rehearsal should have its filebox,
|
|
739
|
+
functions and mappers deleted.
|
|
740
|
+
|
|
741
|
+
## Common mistakes
|
|
742
|
+
|
|
743
|
+
| Symptom | Cause | Fix |
|
|
744
|
+
|---|---|---|
|
|
745
|
+
| Every cell is `0`, no error | `DR.GET` argument order — period pair not first | Step 4 |
|
|
746
|
+
| Every cell is `0`, no error | a function not registered into the workbook | Step 5 |
|
|
747
|
+
| One rate type is all `0`, the others fine | only the first `--function` was registered | Step 5 |
|
|
748
|
+
| Some pairs `0` in pairs mode | the source never carried that pair | Step 0b — coverage |
|
|
749
|
+
| Latest version reads all zeros | `calculate` rolled the tag to a month with no data | `--no-roll`, Step 5 |
|
|
750
|
+
| Rates plausible but never change month to month | built on a dead leftover field | Step 0a |
|
|
751
|
+
| Rate column is Text on the FX table | bootstrapped type inferred from samples | `--field "…:Float"`, Step 6 |
|
|
752
|
+
| Rate column is Text on the business table | lookup target type not checked | **Step 7b** |
|
|
753
|
+
| Lookup returns nothing / wrong values | filebox-sourced LUT took the wrong columns as keys | `--layout lut`, Step 7 |
|
|
754
|
+
| `#NAME?` on the label cell | `TEXT()` wrapped around `DR.MONTH()` | Step 4 |
|
|
755
|
+
| Self-rate is not exactly 1 | ratio not anchored to the target row | Step 4 |
|
|
756
|
+
| Custom code rejected by the Tables dialog | never registered as an org currency | Step 1 |
|
|
757
|
+
| `datamappers update` 500s on an unchanged config | mapper has no source version (org import) | remap, Step 6 |
|
|
758
|
+
| Remap 500s: `Cannot read sheet with name …` | chosen version is in `ERROR` status | use the newest ACTIVE version, Step 6 |
|
|
759
|
+
| New lookup columns empty on every target row | lookup built while the FX table was still scanning | rebuild the lookup, Step 7 |
|
|
760
|
+
| Opening balances have no currency amounts | calc-model rows skip the mappers | **Step 7c** |
|
|
761
|
+
| Calc model goes to `ERROR` right after `update` | HTML-encoded formulas sent back unchanged | decode, Step 7c |
|
|
762
|
+
| Calc-model currency column empty in every month | FX lookup key not on the output row | date field + `group_by`, Step 7c |
|
|
763
|
+
| Calc-model currency column `0` only in future months | opening balances run past rate coverage | plan rates, Step 7c |
|