@dforge-core/dforge-mcp 0.2.42 → 0.2.43

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/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ All notable changes to `@dforge-core/dforge-mcp`. This project uses semver-ish
4
4
  `0.1.0-rc.N` pre-release tags; the published version is set at publish time via
5
5
  the release workflow, so committed `package.json` versions are placeholders.
6
6
 
7
+ ## 0.2.43 — 2026-10-19
8
+
9
+ **Schemas follow metadata 0.0.43.** `dforge://schema/registers` describes `analyticTotals` (an
10
+ accounting register's cache keyed by analytics as well, for the accounts its chart flags), and
11
+ `dforge://schema/entity` describes `lineDateField` (an `A` column dating each line's movement by a
12
+ line column instead of `dateField`).
13
+
14
+ **Print-forms reference.** `references/print-templates.md` now covers what the platform does:
15
+ a child set loads wherever the template names it (`sort`, `.size`, `.first`), with the recipe for
16
+ sorting by one or several columns; formula columns are read from SQL like `data.get`, so `TODAY()`
17
+ works and a reference-navigating formula prints; and an amount or quantity written in words with the
18
+ `common-words` module (`common-words.money` / `common-words.quantity`, languages `uk`, `en`, `de`, the
19
+ inputs, the engine pitfalls). The two mistakes the old text taught (a set loads only from a `for`;
20
+ `TODAY()` is skipped in print) are removed.
21
+
22
+ **Registers reference.** `references/registers.md` describes `analyticTotals` and `lineDateField`.
23
+
24
+ **Needs `@dforge-core/dforge-cli` 0.2.33**, which installs both, adds `register rebuild-totals --cache`
25
+ and `module install --allow-rewrite` (a type change that rewrites a table over 1 GB is refused
26
+ without it).
27
+
7
28
  ## 0.2.41 — 2026-10-15
8
29
 
9
30
  **Schemas follow metadata 0.0.42.** `dforge://schema/entity` describes `periodGuards` (a closed-period
package/dist/server.js CHANGED
@@ -31002,7 +31002,7 @@ var import_node_crypto = require("crypto");
31002
31002
  var fs2 = __toESM(require("fs"));
31003
31003
  var path2 = __toESM(require("path"));
31004
31004
 
31005
- // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.32/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
31005
+ // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.33/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
31006
31006
  function buildManifest(opts, moduleId) {
31007
31007
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31008
31008
  const manifest = {
@@ -31290,7 +31290,7 @@ function buildZedTasks() {
31290
31290
  var fs = __toESM(require("fs"));
31291
31291
  var path = __toESM(require("path"));
31292
31292
 
31293
- // node_modules/.pnpm/@dforge-core+metadata@0.0.42/node_modules/@dforge-core/metadata/dist/index.js
31293
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.43/node_modules/@dforge-core/metadata/dist/index.js
31294
31294
  var AggType = {
31295
31295
  Sum: "sum",
31296
31296
  Count: "count",
@@ -34622,7 +34622,7 @@ var path10 = __toESM(require("path"));
34622
34622
  var fs7 = __toESM(require("fs"));
34623
34623
  var path9 = __toESM(require("path"));
34624
34624
 
34625
- // node_modules/.pnpm/@dforge-core+metadata@0.0.42/node_modules/@dforge-core/metadata/dist/dsl/index.js
34625
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.43/node_modules/@dforge-core/metadata/dist/dsl/index.js
34626
34626
  var PUNCT3 = ["===", "!=="];
34627
34627
  var PUNCT2 = [
34628
34628
  "==",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.2.42",
3
+ "version": "0.2.43",
4
4
  "description": "MCP server for dForge module authoring. Exposes 35 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/iash44/dForge-core",
@@ -26,8 +26,8 @@
26
26
  "node": ">=18"
27
27
  },
28
28
  "dependencies": {
29
- "@dforge-core/dforge-cli": "^0.2.32",
30
- "@dforge-core/metadata": "^0.0.42",
29
+ "@dforge-core/dforge-cli": "^0.2.33",
30
+ "@dforge-core/metadata": "^0.0.43",
31
31
  "@modelcontextprotocol/sdk": "^1.30.0",
32
32
  "zod": "^4.4.3"
33
33
  },
@@ -359,6 +359,7 @@
359
359
  "map": { "type": "object", "additionalProperties": { "oneOf": [ { "type": "string" }, { "type": "object", "required": ["debit", "credit"], "additionalProperties": false, "properties": { "debit": { "type": "string" }, "credit": { "type": "string" } } } ] }, "description": "Register dimension or resource name -> this document's column carrying it. On an accounting (L) column a resource or dimension the register splits by side maps to { \"debit\": \"<line column>\", \"credit\": \"<line column>\" } instead of one column. Every resource the register declares must be mapped (install refuses otherwise); a dimension left out binds to a column of the same name. Only meaningful with 'register'. On an accumulation (A) column a value may be a one-hop reference path, '<reference column>.<target column>', read through the reference at posting time (off the line when the binding posts from lines), or an expression: a string starting with '=' holding an F-column formula over the same row with one-hop reference reads — \"=COALESCE([transit_warehouse_id], [dest_warehouse_id])\"." },
360
360
  "headerMap": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Register dimension or resource name -> this DOCUMENT's column carrying it, for a binding that posts from LINES. A line binding reads every other value off the line, and a dimension that belongs to the document (a dealer, an organisation, a date) cannot be put on the line: a G column is a same-row expression or an aggregate over a child set, so it cannot read a parent. Explicit rather than a fallback, because 'not on the line, try the header' turns a typo into a silent read of an unrelated header column with the same name. Install refuses: a name on neither table, a field also named in 'map', a column also read off the line by 'map', a column that is part of the line's key, a column 'lockedFields' does not freeze (the posting is keyed by it, so it has to hold still while the document is posted), and a RESOURCE unless the register is 'info' (a quantity from the document would be added once per line; an information register's records replace rather than add). A value may be a one-hop reference path off the document, whose reference key column 'lockedFields' must then freeze, or an expression over the document ('=...', see 'map')." },
361
361
  "groupLinesBy": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true, "description": "Line columns whose distinct values each post ONE movement, instead of one per line — a production report posting 'this unit worked today' once per unit with a count of 1. 'map' is then read over a group row: the grouped line columns, every column of the document, and one-hop reads through a reference of either, so an expression may mix the document and the group key. A quoted constant is stamped once per group; a numeric line column mapped to a resource is summed over the group. Install refuses: a binding that posts from the document, an L column, a grouped column the line does not store or 'lockedLineFields' does not freeze, a line column read that is neither grouped nor a summed resource, a non-numeric summed resource, a column both grouped and summed, an ungrouped line resource on an 'info' register, a name both the line and the document carry, and a document column read that 'lockedFields' does not freeze. Accumulation (A) columns only." },
362
+ "lineDateField": { "type": "string", "description": "A line column dating that line's movement instead of 'dateField' — for lines that happen on days of their own under one document date (a limit card issuing through the month and dated at its end). A line that leaves it empty moves on 'dateField'. With 'periodEntity', each line's period must exist and be open, at posting and unposting and at every write to the line, judged by the date the movement is stamped with (the line's or the document's, after 'dateTrunc'). On an information register that records changes only, two lines may not write one key: post one line per key. Install refuses: a register that keeps no date, a binding that posts from the document, a column that is not a stored 'date' or 'timestamp' without time zone, one 'lockedLineFields' does not freeze, one 'groupLinesBy' omits when lines are grouped, and a binding with 'periodDimField'. Accumulation (A) columns only." },
362
363
  "dateField": { "type": "string", "description": "This document's date column, supplying the date the register stamps on each journal row. Required once the register declares a 'journalDate' — one writer that supplied none would make every balance read as of a date quietly short, across all writers. On an accumulation (A) column it may be a one-hop reference path, '<reference column>.<target column>', or an expression over the document ('=...', see 'map')." },
363
364
  "dateTrunc": { "type": "string", "enum": ["day", "month", "year"], "description": "Takes the date 'dateField' supplies back to the start of its day, month or year before the register stamps it — for a document that carries a time while the register's records sit on whole days. The calendar is the value's as stored; no time zone is applied. Needs 'dateField'. Accumulation (A) columns only." },
364
365
  "skipPostingWhenEmpty": { "type": "array", "items": { "type": "string" }, "description": "THE EXCEPTION, not the normal way to model this — read the guide before declaring it. Register dimensions (by the register's names) whose empty value means THIS POSTING records nothing: the line, or the document when the binding posts from the document. One posting is skipped, never the register and never the document. When that is all the document would have recorded it is left unposted to this register, and the flag is per binding, so its other registers post as usual. An empty dimension NOT listed refuses the posting, naming the row and the dimension. It leaves a document fully posted while some of its lines produced no movement, which usually means the document is modelling two kinds of line in one table: since 'lineEntity' is declared per BINDING, prefer giving each register its own line table (goods lines post to stock, service lines do not) so that every line of each table posts. Declare this only when the distinction is not structural but lives in the data — a batch or serial number filled on some rows and not others — so there is no second table to put those rows in. Accumulation (A) columns only. Formerly skipWhenNull: a package using the old name is refused at install, while a tenant's already-stored params keep being read under it." },
@@ -381,6 +382,7 @@
381
382
  "dependencies": {
382
383
  "headerMap": ["lineEntity"],
383
384
  "groupLinesBy": ["lineEntity"],
385
+ "lineDateField": ["lineEntity", "dateField"],
384
386
  "dateTrunc": ["dateField"],
385
387
  "lineEntity": ["lineField", "lockedLineFields"],
386
388
  "lineField": ["lineEntity"],
@@ -24,7 +24,7 @@
24
24
  "account": false,
25
25
  "debitAccount": false, "creditAccount": false,
26
26
  "debitAnalyticFields": false, "creditAnalyticFields": false,
27
- "sideDimensions": false
27
+ "sideDimensions": false, "analyticTotals": false
28
28
  },
29
29
  "description": "A balance register keeps state, so it needs the entity that holds it. It dates its movements on the journal row under 'journalDate'; 'period' belongs to an information register, whose records ARE the dated thing. Its writer is recorded on the journal row, which is what 'recorder' names for an information register that has no journal."
30
30
  }
@@ -41,7 +41,7 @@
41
41
  "account": false,
42
42
  "debitAccount": false, "creditAccount": false,
43
43
  "debitAnalyticFields": false, "creditAnalyticFields": false,
44
- "sideDimensions": false,
44
+ "sideDimensions": false, "analyticTotals": false,
45
45
  "allowNegative": false,
46
46
  "autoCreate": false
47
47
  },
@@ -60,7 +60,7 @@
60
60
  "account": false,
61
61
  "debitAccount": false, "creditAccount": false,
62
62
  "debitAnalyticFields": false, "creditAnalyticFields": false,
63
- "sideDimensions": false,
63
+ "sideDimensions": false, "analyticTotals": false,
64
64
  "totals": false,
65
65
  "allowNegative": false,
66
66
  "autoCreate": false
@@ -241,6 +241,63 @@
241
241
  }
242
242
  }
243
243
  },
244
+ "analyticTotals": {
245
+ "type": "object",
246
+ "description": "An 'accounting' register's per-period cache keyed by the analytics as well: a cached row is keyed by (account, cached dimensions, analytic slots, period), for the accounts the chart flags. It is what makes a balance READ BY AN ANALYTIC cheap - stock by item and warehouse, settlements by counterparty and contract - which 'totals' leaves to the postings because over a whole ledger the analytics are where the rows are. getAccountBalance / getAccountTurnover read whole periods from it and the part-period at the edge from the postings. Beside 'totals', never instead of it, and in an entity of its own. Like 'totals' it caches the turnover within each period, so a backdated posting touches one row; a write to the journal that was not a posting, or a change to an account's flag, marks it stale until it is rebuilt.",
247
+ "required": ["entity", "period", "periodField", "accountField", "accountFlag", "analyticFields", "resources"],
248
+ "additionalProperties": false,
249
+ "properties": {
250
+ "entity": {
251
+ "type": "string",
252
+ "description": "Entity holding the cached rows. Keyed by ONE surrogate column the platform mints (the 'identity' trait), because a slot or a dimension may be empty and a PRIMARY KEY holds no NULL; the platform keeps a unique index over the cache's own key, NULLS NOT DISTINCT, and merges on it. It carries the account column, a column per cached dimension, the jsonb slot columns, the period column and both columns of every resource."
253
+ },
254
+ "period": {
255
+ "type": "string",
256
+ "enum": ["day", "month", "quarter", "year"],
257
+ "description": "How much one cached row covers, as for 'totals'."
258
+ },
259
+ "periodField": {
260
+ "type": "string",
261
+ "pattern": "^[a-z_][a-z0-9_]*$",
262
+ "description": "Column holding the FIRST date of the period a row covers."
263
+ },
264
+ "accountField": {
265
+ "type": "string",
266
+ "pattern": "^[a-z_][a-z0-9_]*$",
267
+ "description": "Column holding the account a cached row belongs to. A compound posting folds into two rows, one per side, each with that side's analytics."
268
+ },
269
+ "accountFlag": {
270
+ "type": "string",
271
+ "pattern": "^[a-z_][a-z0-9_]*$",
272
+ "description": "A bool column of the register's chart: the accounts where it is true are cached, and no other. Changing it on an account marks the cache stale until it is rebuilt (install, or dforge register rebuild-totals --cache analytic)."
273
+ },
274
+ "dimensions": {
275
+ "type": "array",
276
+ "items": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
277
+ "description": "Which of the register's dimensions the cache is grouped by; omitted, it is all of them. Unlike 'totals' a nullable one may be cached: an empty value is a key value of its own."
278
+ },
279
+ "analyticFields": {
280
+ "type": "array",
281
+ "minItems": 1,
282
+ "items": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
283
+ "description": "The cache's jsonb columns holding the analytic slots, slot N in the Nth, one per slot a posting side carries (the wider side's count on a compound register). Each holds the analytic's entity and key, which is what a read filters and groups by."
284
+ },
285
+ "resources": {
286
+ "type": "object",
287
+ "description": "Every resource the register declares, each with its 'debitField' and 'creditField' columns, as for an accounting register's 'totals'.",
288
+ "minProperties": 1,
289
+ "additionalProperties": {
290
+ "type": "object",
291
+ "required": ["debitField", "creditField"],
292
+ "additionalProperties": false,
293
+ "properties": {
294
+ "debitField": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
295
+ "creditField": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" }
296
+ }
297
+ }
298
+ }
299
+ }
300
+ },
244
301
  "period": {
245
302
  "type": "string",
246
303
  "pattern": "^[a-z_][a-z0-9_]*$",
@@ -62,11 +62,23 @@ Every column on the entity is a top-level variable. Display formatting is pre-ap
62
62
  | `{{ _color.<column> }}` | Option color hex for single-value dropdown/radio columns |
63
63
  | `{{ _settings.<cd> }}` | Folder-resolved module setting. Image/file settings come as base64 data URIs (drop into `<img src>`). |
64
64
  | `{{ _today }}` | Typed `DateTimeOffset` (UTC); `{{ _fmt._today }}` is the pre-formatted string |
65
- | `{{ <set_name> }}` | Child set — array. Iterate with `{% for x in <set_name> %}`. Only loaded if the template iterates it. |
65
+ | `{{ <set_name> }}` | Child set — array, in primary-key order. Loaded wherever the template's Liquid code names it (see below) |
66
66
  | `{{ _css }}` | The CSS string attached to the template (rarely needed in template body) |
67
67
 
68
68
  Inside a child-set loop, the same shape applies per row: `line._fmt.<col>`, `line._raw.<col>`, `line._color.<col>`.
69
69
 
70
+ Formula columns (`F`) are read in the same SQL as the record, as `data.get` does, so a formula that navigates references or calls a SQL function prints what the grid shows, and `TODAY()` / `NOW()` work. A formula the SQL translator cannot express prints empty and the response carries a warning naming the column.
71
+
72
+ **Sets: loading and order.** A set is loaded when the template's Liquid code (`{% … %}` or `{{ … }}`) mentions its name anywhere: a `for`, a filter, a condition, `.size`, `.first`. Plain text, `{% comment %}` / `{% raw %}` blocks, string literals and the member of a path (`rec.lines`) do not count, and a snippet's code is not scanned (mention the set in the host template). Rows come in primary-key order; sort in the template:
73
+
74
+ ```liquid
75
+ {% assign sorted = doc_lines | sort: 'doc_date' %} {# ascending #}
76
+ {% assign sorted = doc_lines | sort: 'doc_date' | reverse %} {# descending #}
77
+ {% assign s = doc_lines | sort: 'doc_num' | sort: 'doc_date' %} {# date, then number: sort is stable, last key first #}
78
+ ```
79
+
80
+ `sort` takes a column code and sorts the value as the template sees it (a dropdown or reference sorts by its label). Text order follows the engine, not the database collation. `{# … #}` after an expression in examples is a note for the reader, not syntax: write a real comment as `{% comment %} … {% endcomment %}`.
81
+
70
82
  ## Custom filters
71
83
 
72
84
  | Filter | Example | Output |
@@ -91,6 +103,21 @@ Declare a snippet with `"type": "snippet"`. No `entityCd`. The `template_cd` mus
91
103
 
92
104
  Full mechanics + an EPC SEPA QR worked example: [`docs/ui/print-qr-codes.md`](../../../docs/ui/print-qr-codes.md).
93
105
 
106
+ ### Amount in words
107
+
108
+ The platform has no number-to-words filter. The `common-words` module (`modules/common-words`, a dependency to declare) ships snippets that write an amount or a plain quantity out in Ukrainian, English or German. Set the inputs, then include:
109
+
110
+ ```liquid
111
+ {% assign words_amount = total %}
112
+ {% assign words_currency = currency_cd %}
113
+ {% assign words_lang = 'de' %}
114
+ {% include 'common-words.money' %}
115
+ ```
116
+
117
+ 1234.56 EUR prints «eintausendzweihundertvierunddreißig Euro 56 Cent». `words_amount` must be a numeric field, not text. `words_currency` is an ISO 4217 code in any case; an unlisted currency prints its code. `words_lang` is `uk` (default), `en` or `de`; an unknown code prints `[common-words: unknown language xx]`. `words_cap = true` capitalises the first letter. `common-words.money` only picks the language; `money_uk` / `money_en` / `money_de` can be included directly, and `common-words.quantity` writes a count of things («двадцять одна штука», «twenty-one pieces»): `words_number` (numeric, a fraction rounds half up), optional `words_unit` (forms joined by `|`: three for Ukrainian, two for English and German) and `words_gender` (`m` / `f` / `n`), plus `words_lang` and `words_cap` as above. The text lives in the module, so a fix or a new currency is a module update; a new language is a new `money_xx.html` plus a line in `money.html`.
118
+
119
+ Snippets run on Scriban (print) and LiquidJS (preview), so write them for both: no `floor` on big numbers, no number taken from `split` / `slice`, plain `{% comment %}` without trim dashes.
120
+
94
121
  ## Module package layout
95
122
 
96
123
  ```
@@ -142,10 +169,11 @@ my_module/
142
169
  - **Using `_fmt` in a predicate.** `{% if _fmt.amount_paid > 0 %}` compares strings. Use the bare reference: `{% if amount_paid > 0 %}`.
143
170
  - **Forgetting `_raw` for codes.** Direct `{{ status }}` is the label (`"Partially Paid"`). For CSS classes / wire-format payloads / predicates against codes, use `{{ _raw.status }}` (`"P"`).
144
171
  - **Manually formatting dates/currency.** Don't reach for `format_currency` when the column is already `fieldTypeCd: "currency"` — `_fmt.<col>` already matches the form. Use the filter only for derived values.
145
- - **Referencing a child set outside a `{% for %}`.** The platform only loads sets that appear in a `for` loop. `{{ lines.size }}` won't trigger the load.
172
+ - **Expecting a set to load only from a `for`.** It loads wherever the template names it (`sort`, `.size`, `.first`). A set read only inside a snippet is not seen: mention it in the host template.
146
173
  - **Snippet `template_cd` with a dot.** `{% include 'my_module.foo.bar' %}` — the resolver splits on the last dot only, so `my_module.foo` becomes the module name and breaks. No dots in snippet codes.
147
174
  - **Dropping CSS in `<style>` blocks inline.** The renderer wires `<style>` for you; declare CSS in the separate `.css` file (or inline string via the `css` field).
148
- - **Trying to use `TODAY()` / `NOW()` in a formula column expecting it to render in print.** The print-time formula evaluator skips those. Pre-compute via a formula column at insert time, or use `{{ _today | format_date: '...' }}` directly in Liquid.
175
+ - **Printing an unsorted set.** Rows come in primary-key order; use `| sort: 'col'` (and `| reverse`) in the template.
176
+ - **Writing an amount in words by hand.** Include `common-words.money` instead of a custom filter or lookup table.
149
177
  - **Module-installed template renamed in editor.** The editor preserves the original `template_cd` so `{% include %}` references stay intact — don't try to work around it.
150
178
 
151
179
  Module-installed print-template rows can't be deleted from the editor — uninstall the owning module to remove them.
@@ -155,5 +183,6 @@ Module-installed print-template rows can't be deleted from the editor — uninst
155
183
  - Full developer guide: [`docs/ui/print-templates.md`](../../../docs/ui/print-templates.md)
156
184
  - QR codes + snippets guide: [`docs/ui/print-qr-codes.md`](../../../docs/ui/print-qr-codes.md)
157
185
  - JSON Schema: [`docs/schemas/print_templates.schema.json`](../../../docs/schemas/print_templates.schema.json)
186
+ - Amount in words: [`modules/common-words/`](../../../modules/common-words/) (README lists the inputs)
158
187
  - Reference modules: [`modules/fin/`](../../../modules/fin/), [`modules/fin-ch/`](../../../modules/fin-ch/) (Swiss QR-bill), [`modules/crm/`](../../../modules/crm/), [`modules/wms/`](../../../modules/wms/), [`modules/hr/`](../../../modules/hr/)
159
188
  - Source: `server/src/dForge.Api/Services/PrintTemplateService.cs`, `server/src/dForge.Admin/Services/ModuleInstall/PrintTemplateRegistrar.cs`
@@ -183,6 +183,7 @@ A register column is a `bool` state column. `A` writes `balance`, `turnover` and
183
183
  | `lineEntity` + `lineField` | Post from lines: one movement per line. Declared per binding. Needs `lockedLineFields`. |
184
184
  | `headerMap` | On a line posting, register name to a **document** column. A resource may come from the document only on an `info` register. Every column must be in `lockedFields`. |
185
185
  | `groupLinesBy` | One movement per distinct value of these line columns. `map` is read over a group row; numeric resources are summed. |
186
+ | `lineDateField` | On a line posting, a line column dating that line's movement instead of `dateField`; an empty one falls back to `dateField`. With `periodEntity` each line's period must be open, judged by the movement's stamped date (line's or document's, after `dateTrunc`); lines skipped by `skipPostingWhenEmpty` don't count. On a `recordChangesOnly` info register, one line per key (several dates on a key are refused). A `date` or zoneless `timestamp`, in `lockedLineFields` (and `groupLinesBy` when grouped), not with `periodDimField`. |
186
187
  | `skipPostingWhenEmpty` | Dimensions whose emptiness means "this row posts nothing". The exception, see below. |
187
188
  | `lockedFields` / `lockedLineFields` | Columns frozen while posted. `["*"]` freezes the whole row. |
188
189
  | `movementMapping` | Journal attribute column to document or line column. |
@@ -199,7 +200,7 @@ A register column is a `bool` state column. `A` writes `balance`, `turnover` and
199
200
 
200
201
  The per-side object shape was missing from the entity schema until metadata 0.0.39. Validate with metadata 0.0.39 or later.
201
202
 
202
- Paths, expressions, `dateTrunc`, `headerMap`, `groupLinesBy` and `skipPostingWhenEmpty` are `A`-only. An `L` column refuses them.
203
+ Paths, expressions, `dateTrunc`, `headerMap`, `groupLinesBy`, `lineDateField` and `skipPostingWhenEmpty` are `A`-only. An `L` column refuses them.
203
204
 
204
205
  ### `L` column (`acc-registry.gl_corr`)
205
206
 
@@ -262,6 +263,8 @@ execute:
262
263
 
263
264
  `totals` caches the turnover inside each `day`/`month`/`quarter`/`year`, never a closing balance, so a backdated posting touches one row. Declare an `entity` whose PRIMARY KEY is exactly (dimensions, `periodField`), with `inField`/`outField` per resource (`debitField`/`creditField` and an `accountField` on accounting). Posting maintains it; install rebuilds it when its shape changes. Not available on `info`. Details: registers.md section 8.4.
264
265
 
266
+ An `accounting` register may also declare `analyticTotals`, a second cache keyed by the analytic slots as well: (account, cached dimensions, analytic slots, period). It covers the accounts whose chart flag `accountFlag` is true, lives in an entity of its own beside `totals` (a surrogate key, since a slot or dimension may be empty), and holds `analyticFields` (one jsonb column per slot), `accountField`, `periodField`, `period`, optional `dimensions` and `debitField`/`creditField` per resource. `getAccountBalance` / `getAccountTurnover` read it for a flagged account when every filter and grouping is a cached dimension or an analytic of the account, and take the rest from the postings. A write to the journal around posting, or a changed account flag, marks it stale until `dforge register rebuild-totals --cache analytic` (or a module install) rebuilds it. Not available on `balance`, `turnover` or `info`.
267
+
265
268
  ## Common mistakes
266
269
 
267
270
  - **Undeclared or unqualified register.** `params.register` must be `module_cd.register_cd` and exist. Install fails, not the first posting.