@dforge-core/dforge-mcp 0.2.44 → 0.2.46-next

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +3 -2
  3. package/dist/server.js +109 -84
  4. package/package.json +48 -46
  5. package/resources/docs/conventions.md +17 -14
  6. package/resources/docs/dsl-reference.md +15 -5
  7. package/resources/schemas/entity.schema.json +1 -1
  8. package/resources/schemas/registers.schema.json +8 -3
  9. package/scripts/install-skills.mjs +1 -0
  10. package/skills/dforge-1c-import/SKILL.md +374 -0
  11. package/skills/dforge-1c-import/references/environment.md +97 -0
  12. package/skills/dforge-1c-import/references/loading.md +161 -0
  13. package/skills/dforge-1c-import/references/measurements.md +210 -0
  14. package/skills/dforge-1c-import/references/print-forms.md +148 -0
  15. package/skills/dforge-1c-import/scripts/1c.py +178 -0
  16. package/skills/dforge-1c-import/scripts/README.md +110 -0
  17. package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
  18. package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
  19. package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
  20. package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
  21. package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
  22. package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
  23. package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
  24. package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
  25. package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
  26. package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
  27. package/skills/dforge-1c-import/scripts/lib/delta.py +741 -0
  28. package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
  29. package/skills/dforge-1c-import/scripts/lib/docusage.py +105 -0
  30. package/skills/dforge-1c-import/scripts/lib/dump1c.py +251 -0
  31. package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
  32. package/skills/dforge-1c-import/scripts/lib/external.py +201 -0
  33. package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
  34. package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
  35. package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
  36. package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
  37. package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
  38. package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
  39. package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
  40. package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
  41. package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
  42. package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
  43. package/skills/dforge-1c-import/scripts/lib/meta.py +315 -0
  44. package/skills/dforge-1c-import/scripts/lib/migrate.py +1299 -0
  45. package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
  46. package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
  47. package/skills/dforge-1c-import/scripts/lib/paths.py +90 -0
  48. package/skills/dforge-1c-import/scripts/lib/peek.py +70 -0
  49. package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
  50. package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
  51. package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
  52. package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
  53. package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
  54. package/skills/dforge-1c-import/scripts/lib/recorders.py +94 -0
  55. package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
  56. package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
  57. package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
  58. package/skills/dforge-1c-import/scripts/lib/seedstate.py +488 -0
  59. package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
  60. package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
  61. package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
  62. package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
  63. package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
  64. package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
  65. package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
  66. package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
  67. package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
  68. package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
  69. package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
  70. package/skills/dforge-1c-import/scripts/project-template/sources.py +14 -0
  71. package/skills/dforge-mcp-author/SKILL.md +8 -2
  72. package/skills/dforge-mcp-author/examples/column-security/entities/product.json +1 -1
  73. package/skills/dforge-mcp-author/examples/column-security/manifest.json +0 -1
  74. package/skills/dforge-mcp-author/examples/column-security/security/roles.json +2 -2
  75. package/skills/dforge-mcp-author/references/action-dsl.md +20 -7
  76. package/skills/dforge-mcp-author/references/conventions.md +7 -1
  77. package/skills/dforge-mcp-author/references/field-types.md +3 -2
  78. package/skills/dforge-mcp-author/references/formulas.md +5 -1
  79. package/skills/dforge-mcp-author/references/print-templates.md +5 -2
  80. package/skills/dforge-mcp-author/references/queries.md +1 -1
  81. package/skills/dforge-mcp-author/references/registers.md +18 -3
  82. package/skills/dforge-mcp-author/references/reports.md +4 -1
  83. package/skills/dforge-mcp-author/references/schema-import.md +1 -1
  84. package/skills/dforge-mcp-author/references/security.md +1 -1
  85. package/skills/dforge-mcp-author/references/translations.md +38 -15
  86. package/skills/dforge-mcp-author/references/validation-checklist.md +2 -3
  87. package/skills/dforge-module-build/SKILL.md +10 -1
  88. package/skills/dforge-module-ship/SKILL.md +31 -3
  89. package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +0 -35
@@ -0,0 +1,97 @@
1
+ # Environment traps
2
+
3
+ The things that cost hours the first time and minutes every time after. Written down so
4
+ they cost minutes the first time.
5
+
6
+ ## 1C physical storage
7
+
8
+ The infobase's SQL tables carry generated names; the configuration maps GUIDs to them.
9
+
10
+ | prefix | what it is |
11
+ |---|---|
12
+ | `_Reference<N>` | catalog (справочник) |
13
+ | `_Document<N>` | document; its tabular sections are `_Document<N>_VT<M>` |
14
+ | `_InfoRg<N>` | information register; `_InfoRgSL<N>` is its slice-last cache |
15
+ | `_AccumRg<N>` | accumulation register; `_AccumRgT<N>` its totals, `_AccumRgTn<N>` turnovers |
16
+ | `_AccRg<N>` | accounting register |
17
+ | `_Enum<N>` | enumeration; values carry `_EnumOrder`, never their names |
18
+ | `_*ChngR<N>` | change registration for exchange — not the object's own data |
19
+
20
+ Common columns: `_IDRRef` (16-byte GUID key), `_Period`, `_Active`, `_RecordKind`
21
+ (0 receipt / 1 expense), `_RecorderRRef` + `_RecorderTRef` (the writing document, typed),
22
+ `_LineNo`, `_Fld<N>` for every declared field in declaration order — dimensions first,
23
+ then resources, then attributes.
24
+
25
+ A field ending `RRef` is a single-type reference; a `_TYPE`/`_RTRef`/`_RRRef` triple is a
26
+ composite one. `_IDRRef` bytes are stored with the halves swapped relative to the printed
27
+ GUID.
28
+
29
+ **Resolving a `_Fld` number to a name goes through `DBNames`, not through guessing.** The
30
+ configuration keys every attribute by GUID, and `DBNames` holds a `Fld` entry per GUID
31
+ giving its `_FldNNN`; the same file maps `VT` (a tabular section's table) and `LineNo`.
32
+ Parse the object, collect its attributes with their GUIDs, and look each one up. Automate
33
+ it once — everything else depends on it being right.
34
+
35
+ Field numbers do run in declaration order from the object's base number, dimensions first,
36
+ and distinct-value counts do look suggestive. Use that only to **check** a resolution you
37
+ already made: two reference dimensions of similar shape swap silently, every query still
38
+ runs, and every number downstream is confidently wrong.
39
+
40
+ ## The year offset
41
+
42
+ 1C shifts stored dates by the infobase's `_YearOffset` — commonly 2000, so `4012` in the
43
+ column is 2012, and the empty date `0001-01-01` lands on `2001-01-01` and should become
44
+ NULL.
45
+
46
+ It applies to **stored datetime columns only**. A date the configuration formatted into a
47
+ string (`НачалоДня(Дата)` assigned to a `Строка(10)` dimension) carries the real year, and
48
+ shifting it as well moves the value back two millennia.
49
+
50
+ ## An empty string is not NULL
51
+
52
+ 1C writes `''` where a value is absent. So `count(column)` counts it, and a column the
53
+ infobase never filled reads as completely full. Sweep for text columns blank on every row
54
+ before trusting any of them; the results split into stock fields the client does not use
55
+ (fine) and columns the port read from the wrong source (not fine).
56
+
57
+ ## Shell: Cyrillic and backticks
58
+
59
+ When commands are wrapped for execution, Cyrillic characters and backticks in the command
60
+ text can break the wrapper before the command runs — the symptom is `command not found`
61
+ for fragments of your own script, or a syntax error near a Cyrillic word.
62
+
63
+ The fix that always works: write the script to a file with a file-writing tool, then run
64
+ it by an ASCII-only path. Inside the file, Cyrillic is fine; in a pinch, `\uXXXX` escapes
65
+ in the source keep the command itself ASCII.
66
+
67
+ ## Windows: `bash` is not the bash you want
68
+
69
+ On Windows, `bash` on PATH resolves to WSL's, which lives in a different filesystem and
70
+ fails with `execvpe(/bin/bash) failed` before reaching your script. Anything that shells
71
+ out to a `.sh` needs Git's bash by full path:
72
+
73
+ ```
74
+ C:\Program Files\Git\bin\bash.exe
75
+ ```
76
+
77
+ This is invisible when working interactively from Git Bash and appears the moment a script
78
+ does the same thing.
79
+
80
+ ## Python on Windows
81
+
82
+ Output encoding defaults to the system codepage, so Cyrillic output raises
83
+ `UnicodeEncodeError`. Set `PYTHONIOENCODING=utf-8`. When reading and writing files, name
84
+ the encoding explicitly (`io.open(path, encoding="utf-8")`) rather than relying on the
85
+ default.
86
+
87
+ ## Reading the infobase
88
+
89
+ Read-only, always. Porting needs no writes to the source, and a `SELECT`-only credential
90
+ makes that a property of the connection rather than a promise.
91
+
92
+ No 1C licence is engaged by reading the SQL tables directly, which is what makes this
93
+ approach available at all — but it also means nothing validates your reading of the
94
+ metadata except the data itself.
95
+
96
+ Guard the size of exploratory queries: a register of 14 million rows will answer a
97
+ well-scoped question about one month in seconds and an unscoped one in minutes.
@@ -0,0 +1,161 @@
1
+ # Copying the rows
2
+
3
+ How the infobase's data gets into a dForge tenant once the modules are installed. The
4
+ general shape of a migration script is in `dforge://reference/data-migration`. This file
5
+ covers what is specific to 1C.
6
+
7
+ ## Copy, don't replay
8
+
9
+ There are two routes:
10
+
11
+ - **Through the API** (`data.set`, CSV import). Every document is re-posted on the way in.
12
+ - **Straight into Postgres** with `COPY`.
13
+
14
+ Take the second for the history. Re-posting needs the posting logic ported first. Once it
15
+ is ported, its arithmetic has to match 1C's to the kopeck, or the imported balances differ
16
+ from the ones the client reconciles against. It is also slow: millions of documents
17
+ through RPC take days.
18
+
19
+ A direct copy keeps the values 1C computed and runs at disk speed. The API route stays
20
+ useful for one job: catching up the last days at cutover, while 1C is still live.
21
+
22
+ ## Shape the modules so the rows fit
23
+
24
+ A copy is only mechanical if nothing has to be reshaped:
25
+
26
+ - **Use the infobase's key as the key.** Key every entity by `_IDRRef` as a `uuid`. Then no
27
+ old-key → new-key table is needed and no reference has to be re-resolved.
28
+ - **Line tables keep 1C's composite key**, `(<owner>_id, line_no)`, as
29
+ `_Document<N>_VT<M>` is keyed.
30
+ - **Port the reference data as well.** Mirror `Номенклатура`, `Контрагенты`, `Склады`
31
+ rather than mapping them onto platform modules. Then a line's `product_id` is the same
32
+ 16 bytes on both sides.
33
+ - **Build the column map as the entities are generated**, not afterwards, so it cannot
34
+ drift from the schema. The map records which `_FldNNN` feeds each target column.
35
+ Authored entities, whose names were chosen by hand, get a map written by hand. Propose
36
+ it by running each 1C attribute name through the same naming function and declare only
37
+ the leftovers.
38
+ - **Read the target's physical shape from the tenant itself** (`"dForge".entity`,
39
+ `information_schema`). Do not read it from the module files. The loader then cannot
40
+ disagree with what the installer built.
41
+
42
+ ## The conversions
43
+
44
+ Everything else is column-level and mechanical. Write each conversion once, in the loader.
45
+
46
+ | # | 1C storage | becomes | the trap |
47
+ |---|---|---|---|
48
+ | 1 | `_IDRRef binary(16)` | `uuid` | 1C stores the two 8-byte halves swapped relative to the printed GUID. In SQL: `substr(h,17)\|\|substr(h,1,16)` over the hex. Check it once by joining a line to its catalog and seeing the expected name. |
49
+ | 2 | sixteen zero bytes | `NULL` in a link | Left as-is, it is a uuid pointing at nothing, and the FK rejects the load. **In a register dimension it is a value, not an absence:** keep it as the nil uuid in a key column, because NULL ≠ NULL in a balance, a GROUP BY or a unique key. |
50
+ | 3 | `datetime` | date/timestamp minus `_YearOffset` | The offset is commonly 2000. The empty date `0001-01-01` lands on `2001-01-01` and becomes NULL. **On an information register's period it is a value:** "in force since the beginning". Load it as NULL and the slice (`period <= @date`) never finds the row. |
51
+ | 4 | a date 1C formatted into a string (`13.01.2016` in `nvarchar`) | `date` | No offset: nothing stored a datetime there. |
52
+ | 5 | a number in a string column | numeric | Decimal comma, nbsp or space as the thousands separator. |
53
+ | 6 | `binary(1)` flags | `bool` | `_Marked`, `_Posted`, `_Folder`. Check `_Folder`'s polarity against a known group and a known item. Do not trust the name. |
54
+ | 7 | enum reference | option code | The value is a GUID into `_Enum<N>`. `_EnumOrder` is its position in the metadata, so read the options in that order and pair by position. Generate the dropdown from the enum: hand-written option lists come out short. |
55
+ | 8 | composite reference: `_TYPE` + `_RTRef` + `_RRRef` | the key, plus a "which table" column where the port keeps one | The table number in `_RTRef` is the only thing that says what the row on the other end is. Map numbers to meanings via DBNames. |
56
+ | 9 | `ХранилищеЗначения` (`image`) | `bytea`, copied as is | It declares itself a reference. Read as one, it becomes a uuid column that can never be filled. |
57
+ | 10 | `nchar` padding | trimmed text | `_Code nchar(N)` shows trailing spaces. |
58
+ | 11 | `''` | `''` or NULL, decided once | 1C writes `''` for "no value". Every emptiness check must test both. |
59
+
60
+ ### Register keys
61
+
62
+ An information register has no key in 1C. Where the infobase kept one, it is `_SimpleKey`.
63
+ For the rest, derive the key from the row: a uuid5 over the source table and the row's
64
+ values. Then a re-run lands on the same key and a reload is idempotent.
65
+
66
+ ### Movement direction
67
+
68
+ 1C's convention depends on the register kind:
69
+
70
+ - **Balance register.** Resources are unsigned. `_RecordKind` says which way the row went:
71
+ 0 is a receipt (+1), 1 is an expense (−1). Refuse anything else; never default it.
72
+ - **Turnover register.** Some authors kept the direction in the sign of the resource. Some
73
+ registers carry the two sides as separate resources, so every row is additive.
74
+
75
+ Read which convention applies from the writer. Then fill the platform's direction stamp
76
+ in the loader, not through a column default. An upgrade can arrive without the default,
77
+ and every `SUM(value * direction)` silently becomes NULL.
78
+
79
+ ## What goes where for registers
80
+
81
+ In 1C, posting writes movements to `_AccumRg<N>`, and `_AccumRgT<N>` caches period totals.
82
+ In dForge, a register is an `A` column on the document plus a balance entity. In line
83
+ mode the document's lines are the detail. So:
84
+
85
+ - **Copy the documents and their lines.** `_Posted` becomes the register column's state.
86
+ - **Copy the balances** from `_AccumRgT<N>`, or aggregate them from the movements. They are
87
+ 1C's numbers, and those are what the client reconciles against.
88
+ - **Do not copy line-mode movement rows.** They have nowhere to go: the lines are the detail.
89
+ - **Close every period up to cutover after the load.** Unposting an imported document
90
+ recomputes its reversal from the lines. Where 1C's handler did more than sum the lines
91
+ (batch write-off, distribution), that reversal would not match what 1C wrote. The close
92
+ guard then refuses edit, unpost and delete on history.
93
+
94
+ A raw `UPDATE` of a posted flag is refused (`DF006 REGISTRY_STATE_DIRECT`). Posting state
95
+ is the engine's.
96
+
97
+ ## Order and triggers
98
+
99
+ Postgres fires row triggers on `COPY`. The period lock and the posted-document guards
100
+ would reject exactly the rows a migration writes. So:
101
+
102
+ 1. Load with `SET session_replication_role = 'replica'`.
103
+ 2. Load in dependency order: catalogs, then documents, then lines, then balances.
104
+ 3. Load self-referencing catalogs (parent hierarchies) in one pass with the constraint
105
+ suspended. A topological sort buys nothing.
106
+ 4. Re-enable the triggers, close the periods, verify.
107
+
108
+ Keep ad-hoc measurement connections in **autocommit**. A long read transaction left open in
109
+ a console holds a lock that blocks the next install's `ALTER` for as long as it stays open.
110
+
111
+ ## Dangling references
112
+
113
+ 1C does not enforce referential integrity. A copy arrives with references to rows deleted
114
+ years ago. Prove it on the 1C side first: it is the infobase's state, not a load bug.
115
+ There are three ways out:
116
+
117
+ - **Null the reference.** The row stops saying what it said.
118
+ - **Drop the FK.** You give up the integrity the port exists to gain.
119
+ - **Create the missing row and mark it.** Give every ported catalog a read-only
120
+ `is_missing` flag. After a contour loads, walk the FKs and insert one flagged row per
121
+ missing key, its text columns carrying the key so it is traceable. **Recommended:** it
122
+ is the only option that keeps everything.
123
+
124
+ Verification then counts `WHERE NOT is_missing`.
125
+
126
+ ## Verifying the load
127
+
128
+ These checks are all cheap, and all should be automated from the first contour:
129
+
130
+ - **Row counts** per source table against the target.
131
+ - **Control sums** on every resource and every money column, 1C against Postgres. This is
132
+ the check that catches a decimal comma or a missed offset.
133
+ - **Reference integrity.** Ask `pg_constraint`, not your own metadata: count every
134
+ single-column FK against its target, and compare every FK with the reference column's
135
+ declared target. A check that reads the same metadata the generator wrote agrees with
136
+ it by construction. A retargeted reference can keep its old FK for months.
137
+ - **A sample of joins** resolving to the name a 1C report shows.
138
+
139
+ Before the first load, ask the data what the specs claim: that a column is mandatory,
140
+ wide enough, unique, and holds the declared kind. Do it in one pass, not one failed
141
+ `COPY` at a time. The answers are usually the port being tidier than the business. For
142
+ example, a "mandatory" kind may be empty on 95% of rows, and a "unique" plate number
143
+ repeat after years of re-registrations.
144
+
145
+ ## Upgrades leave the physical table behind
146
+
147
+ `module upgrade` can rewrite the metadata and leave the table as it was. Check this after
148
+ any upgrade that changed a column:
149
+
150
+ - a widened `varchar` still narrow;
151
+ - a retargeted reference keeping its old FK;
152
+ - a removed unique constraint still in place;
153
+ - a type change not applied.
154
+
155
+ The repair for widths is one query: join `entity_column` to `information_schema.columns`
156
+ and alter the pairs that disagree. Never narrow one that holds something longer.
157
+
158
+ ## Incremental re-runs
159
+
160
+ 1C's `_Version` (rowversion) gives a cheap delta. Take the high-water mark **before** the
161
+ read, so a row 1C changes during the read is picked up next time rather than lost.
@@ -0,0 +1,210 @@
1
+ # The measurements
2
+
3
+ Each decision in SKILL.md has a query that settles it. They are written against a loaded
4
+ dForge tenant (Postgres) because that is where both sides are comparable — the 1C rows and
5
+ what the platform writes. Where the question can only be asked of the infobase, the query
6
+ is against MS SQL and says so.
7
+
8
+ Adapt names; the shapes are the point.
9
+
10
+ ## Contents
11
+
12
+ 1. Does this register need a collapse?
13
+ 2. Which kind is it?
14
+ 3. Would accumulation ever accumulate?
15
+ 4. Did the collapse actually happen?
16
+ 5. Is a guard in the handler alive?
17
+ 6. Does anything read this?
18
+ 7. Is a column's type a lie?
19
+ 8. Is a text column really full?
20
+ 9. Which costing method does the base use?
21
+
22
+ ---
23
+
24
+ ## 1. Does this register need a collapse?
25
+
26
+ The handler's `Свернуть` is the signal; these two confirm it and size it.
27
+
28
+ ```sql
29
+ -- how often the lines of one document repeat a dimension
30
+ select repeats, count(*) as documents from (
31
+ select <doc_fk>, count(*) - count(distinct <dim>) as repeats
32
+ from <line_table> group by 1) x
33
+ group by 1 order by 1 limit 10;
34
+
35
+ -- lines in, rows 1C actually wrote out, for the same documents
36
+ select sum(lines) as lines, sum(rows_1c) as rows_1c from (
37
+ select d.<pk> id,
38
+ (select count(*) from <line_table> l where l.<doc_fk> = d.<pk>) as lines,
39
+ (select count(*) from <register> r where r.recorder = d.<pk>) as rows_1c
40
+ from <documents> d) y;
41
+ ```
42
+
43
+ `lines > rows_1c` means the collapse is load-bearing.
44
+
45
+ ## 2. Which kind is it?
46
+
47
+ ```sql
48
+ select count(*) as rows,
49
+ count(distinct (<dims>)) as distinct_keys,
50
+ count(distinct recorder) as writing_documents
51
+ from <register>;
52
+ ```
53
+
54
+ `rows = distinct_keys` means one row per key — a table of records, not a journal. Combine
55
+ with question 1: one row per key *and* repeating lines means the writer collapses, which
56
+ is `balance` in dForge regardless of the 1C family.
57
+
58
+ ## 3. Would accumulation ever accumulate?
59
+
60
+ ```sql
61
+ select docs_per_key, count(*) as keys from (
62
+ select <key columns>, count(*) as docs_per_key
63
+ from <documents>
64
+ where coalesce(deletion_mark, false) = false
65
+ group by <key columns>) z
66
+ group by 1 order by 1;
67
+ ```
68
+
69
+ One output row showing `1` retires the objection that `balance` would add two openings
70
+ together. Record the number in the declaration; it is the answer to the question the next
71
+ reader will ask.
72
+
73
+ ## 4. Did the collapse actually happen?
74
+
75
+ Two traps here, and they pull in opposite directions.
76
+
77
+ Resource **totals** match whether the engine collapsed or not, because a sum is a sum. So
78
+ totals alone cannot show a collapse. But row counts alone cannot either: against a
79
+ register that already holds the migrated rows, a posting that did **nothing** adds no rows
80
+ and creates no duplicate keys — and so passes a check built only on counts. A posting that
81
+ updated the *wrong* existing key passes it too.
82
+
83
+ What settles it is the expected delta **per full dimension tuple**, checked in both
84
+ directions. Compute what the lines say each tuple should move, before posting:
85
+
86
+ ```sql
87
+ -- expected, read through the binding's own map and headerMap
88
+ select <dims>, sum(<resource>) as expected
89
+ from <line_table> l join <documents> d on d.<pk> = l.<doc_fk>
90
+ where d.<pk> = '<doc>'
91
+ group by <dims>;
92
+ ```
93
+
94
+ Then post, and compare per tuple rather than in aggregate:
95
+
96
+ ```sql
97
+ select r.<dims>, r.<resource> - b.<resource> as moved
98
+ from <register> r join <baseline snapshot> b using (<dims>);
99
+ ```
100
+
101
+ Three things must hold together, and any one alone is weak:
102
+
103
+ 1. every expected tuple moved by exactly its expected amount;
104
+ 2. no tuple outside the expected set moved at all;
105
+ 3. after unposting, every tuple is back at the baseline.
106
+
107
+ Point 2 is what catches an update to the wrong key; point 3 is what catches a posting that
108
+ silently did nothing, because a no-op also unposts to nothing and the baseline never
109
+ moved. Snapshot the register's rows for the affected tuples before posting so the
110
+ comparison has something to be against.
111
+
112
+ And separately, since it is cheap:
113
+
114
+ ```sql
115
+ select count(*) as keys_with_two_rows from (
116
+ select 1 from <register> group by <dims> having count(*) > 1) x;
117
+ ```
118
+
119
+ ## 5. Is a guard in the handler alive?
120
+
121
+ `Если Стр.Количество <> 0` guards a loop. Has it ever fired?
122
+
123
+ ```sql
124
+ select count(*) as lines,
125
+ count(*) filter (where <col> = 0) as would_be_skipped,
126
+ count(*) filter (where <col> <> 0) as actually_written
127
+ from <line_table>;
128
+ ```
129
+
130
+ Zero skipped over years of data means the guard is dead code. Porting it as a platform
131
+ feature would be paying for nothing.
132
+
133
+ ## 6. Does anything read this?
134
+
135
+ A grep over the extracted configuration modules, not a query. Count the objects naming the
136
+ register, split by whether the object is the client's own (lowercase prefix before a
137
+ capital) or the vendor's, and look at what appears near each mention. If the only mentions
138
+ are the writing document and `УдалитьДвиженияРегистратора`, nothing reads it.
139
+
140
+ Do this before carrying a journal, a recorder link, or any mechanism that exists to
141
+ support a reader.
142
+
143
+ ## 7. Is a column's type a lie?
144
+
145
+ Sweep every text column at once rather than meeting them one at a time. Build the union
146
+ from `information_schema.columns` in the module schemas, then:
147
+
148
+ ```sql
149
+ select '<schema>.<table>.<col>' as col, count(*) as n
150
+ from "<schema>"."<table>"
151
+ where "<col>" ~ '^[0-9]{2}\.[0-9]{2}\.[0-9]{4}( |$)'
152
+ ```
153
+
154
+ A hit is a date the 1C author declared as a string. Expect very few — in the base measured,
155
+ one column out of 611 — which is what makes the sweep worth running once and never
156
+ worrying again.
157
+
158
+ ## 8. Is a text column really full?
159
+
160
+ ```sql
161
+ select count(*) as rows,
162
+ count(*) filter (where <col> is null) as nulls,
163
+ count(*) filter (where <col> = '') as empty_strings,
164
+ count(*) filter (where coalesce(<col>, '') <> '') as actually_filled
165
+ from <table>;
166
+ ```
167
+
168
+ `nulls = 0` while `actually_filled = 0` is the signature of a column read from a field the
169
+ infobase never writes. It is invisible to `count(column)`.
170
+
171
+ ## 9. Which costing method does the base use?
172
+
173
+ The accounting policy register will name an enum position and the configuration text may
174
+ not resolve it. The movements answer it several ways, and agreement across them is the
175
+ point — any one alone can mislead.
176
+
177
+ ```sql
178
+ -- A. batches touched by one write-off. Average costing writes off proportionally across
179
+ -- every batch in stock, so a heavy concentration at 1 rules it out.
180
+ select batches, count(*) as expenses from (
181
+ select _RecorderRRef, <item>, <warehouse>, count(distinct <batch>) as batches
182
+ from <batch_register>
183
+ where _RecordKind = 1 and _Period >= ? and _Period < ?
184
+ group by 1,2,3) x
185
+ group by 1 order by 1;
186
+
187
+ -- B. distinct unit costs per item and warehouse within one month.
188
+ -- A period-weighted average gives exactly one.
189
+ select prices, count(*) from (
190
+ select <item>, <warehouse>,
191
+ count(distinct round(<amount> / nullif(<qty>, 0), 4)) as prices
192
+ from <batch_register>
193
+ where _RecordKind = 1 and _Period >= ? and _Period < ? and <qty> <> 0
194
+ group by 1,2) y
195
+ group by 1 order by 1;
196
+
197
+ -- C. was there a choice at all? (validity check: zero-stock cases mean the
198
+ -- balance arithmetic is wrong and nothing below can be believed)
199
+ -- then, among those with a choice, was the oldest batch in stock taken?
200
+ -- FIFO takes the oldest; LIFO the newest; neither dominating means no rule
201
+ -- is followed and the batch is effectively identified rather than queued.
202
+ ```
203
+
204
+ Read C carefully: "the oldest batch ever received" is a different question from "the oldest
205
+ batch that still had stock at that moment", and only the second is FIFO. The first gives a
206
+ muddle and will mislead you.
207
+
208
+ Whatever the answer, it changes the question put to the client rather than replacing it:
209
+ not *"which method do you use"* — the base answers that — but *"is that what you meant, and
210
+ should it stay that way"*. A rule nobody enforces may be accepted practice or an accident.
@@ -0,0 +1,148 @@
1
+ # Porting 1C print forms
2
+
3
+ The platform's side of this (manifest, context, snippets, Scriban versus LiquidJS) is in
4
+ `dforge://reference/print-templates`; queries are in `dforge://reference/queries`. This
5
+ file covers what a 1C form brings with it.
6
+
7
+ ## What a 1C form is
8
+
9
+ 1C prints in two steps:
10
+
11
+ 1. The document's print procedure (`Печать…`, often in a shared module) computes almost
12
+ every value. It folds lines by product and series, takes places and packs through the
13
+ series' packaging, looks prices up at the date, totals, and assembles the
14
+ organization's requisites into one text.
15
+ 2. A spreadsheet template (макет, `MOXCEL`) is filled area by area with those values.
16
+
17
+ So the template alone is not the form. **Read the procedure first.** It holds the
18
+ requirement. The template is only the layout.
19
+
20
+ The dForge form mirrors the two steps:
21
+
22
+ | 1C | dForge |
23
+ |---|---|
24
+ | the print procedure | the template's datasets: a query, with F columns for per-line arithmetic |
25
+ | the template (макет) | an HTML template with its CSS |
26
+ | the shared printing library (requisites, contact info, amounts in words) | shared datasets and snippets, e.g. the platform's `common-words` for amounts and quantities in words |
27
+
28
+ ## Reading the template
29
+
30
+ A template is the object's metadata group `{3daea016-69b7-4ed4-9453-127911372fe6, n,
31
+ guid…}`. Each template's `<guid>.txt` names it and its body is `<guid>.0.txt`. A spreadsheet
32
+ body starts `MOXCEL` and continues as a brace list:
33
+
34
+ - **Rows** with their cells. A cell is one of:
35
+ - `{0, format}` — empty;
36
+ - `{16, format, {1,1,{lang,text}}}` — text, or a parameter when `lang` is `""`;
37
+ - `{20, …, {"U"}, …}` — a template string whose `[Name]` parts are parameters.
38
+ - **Column sets**, **merges** and **named areas**. An area is rows (kind 1) or columns
39
+ (kind 2), with inclusive bounds.
40
+ - **Formats.** Each is a bit mask followed by its values, low bit first: font index (1),
41
+ left/top/right/bottom border (2/4/8/16), height (64), width (128). The remaining bits
42
+ (alignment, placement) can be carried by number.
43
+ - **Fonts.**
44
+
45
+ Dump each template to HTML and JSON: areas, cells, parameters, merges, widths, fonts,
46
+ borders. **Author the form from the dump; do not convert it.** 1C's layout is a grid of up
47
+ to ~50 narrow columns. A faithful HTML copy of it is unmaintainable. Keep the form's
48
+ logical columns and labels.
49
+
50
+ Which areas the procedure outputs, and in what order, is the form's real structure. Typical
51
+ areas are header, line, subtotal per group, total and signatures.
52
+
53
+ ## Inventory before porting
54
+
55
+ - **List the forms per document from the print command's handler**, not from the
56
+ templates. An object can carry dead templates. Templates the code names only for dates
57
+ before some year print only old documents: record them and skip them.
58
+ - **Count how often each variant fires** on recent documents. A variant is a template
59
+ chosen by a flag on the buyer or the contract, or a "per code" text. A variant used by
60
+ 278 documents since last year is worth a template. One used by none may still be worth a
61
+ column.
62
+ - **External print processings** (`ВнешниеОбработки`) live in the infobase as stored
63
+ binaries, not in the configuration. Inventory them separately.
64
+
65
+ ## Reproduce what 1C prints, including its slips
66
+
67
+ The client compares the new printout to the old one. Where 1C's code has a defect, the
68
+ printout has the defect, and the client knows the printout. Reproduce it, and say so in
69
+ a comment at the place. Common ones:
70
+
71
+ - **A misspelt variable** makes a lookup find nothing. For example, a phone kind read from
72
+ a misnamed variable means no phone ever prints. Print nothing.
73
+ - **A field list with spaces** (`"НомерСчета, Банк, АдресБанка"`) passed where 1C expects
74
+ no spaces fills only the first field.
75
+ - **A test on the wrong attribute**, such as an IBAN check on the account's name instead
76
+ of its number. Do the same test.
77
+ - **Mixed Cyrillic and Latin letters** in a literal (`с` against `c`). Copy them byte for
78
+ byte.
79
+ - **Language codes** in `НСтр("ru = '…'; en = '…'", Lang)` are matched case-insensitively,
80
+ so `"Ru"` finds `ru`.
81
+
82
+ If the client later asks to fix one, it is a one-line change with the comment pointing
83
+ at it.
84
+
85
+ ## Formatting numbers and dates as 1C does
86
+
87
+ | 1C | output | Postgres equivalent |
88
+ |---|---|---|
89
+ | `Строка(n)`, `Формат(n)` without a format | thousands grouped by nbsp, decimal comma, no trailing zeros | `replace(replace(rtrim(rtrim(to_char(n,'FM999G999G999G990D9999999999'),'0'),'.'),',',E' '),'.',',')` (the `G`/`D` result depends on `lc_numeric`; check it) |
90
+ | `Формат(n, "ЧДЦ=2")` | always 2 decimals | `to_char(n, 'FM999G999G990D00')` then the same separators |
91
+ | `Формат(n, "ЧРД=.")` | decimal point | the replacement above with `.` |
92
+ | `Формат(n, "ЧГ=0")` | no grouping | drop `G` |
93
+ | `Формат(d, "ДФ=dd.MM.yyyy")` | `13.01.2016` | `to_char(d, 'DD.MM.YYYY')` |
94
+ | a date parameter output as is | `13.01.2016 9:05:00` (hour without a leading zero) | `to_char(d, 'DD.MM.YYYY FMHH24:MI:SS')` |
95
+
96
+ Before choosing the precision a price prints with, measure how many stored values actually
97
+ have more decimals. Do not trust the declared precision: stored prices can carry a third
98
+ decimal on most rows.
99
+
100
+ Amounts and quantities in words should come from a shared snippet (the platform's
101
+ `common-words`), not per-form code. Verify it on the client's real values against 1C's
102
+ `ЧислоПрописью` output: thousands of values, every case. Expect differences only where 1C
103
+ is ungrammatical, and agree them with the client.
104
+
105
+ ## Datasets: queries, and where a query falls short
106
+
107
+ Prefer a query dataset. It runs under the user's rights and folder filters, which a stored
108
+ procedure bypasses.
109
+
110
+ - **Put per-line arithmetic into F columns on the line entity**: weight = places × place
111
+ weight, packs, the VAT percentage through a `SWITCH`, a group section through `IN`.
112
+ - **Let the query group and sum** (`agg: group | sum`).
113
+ - **Accumulate totals in the template.** The server engine has no `where`, `sum` or
114
+ `group_by` filters, but `plus`, `times`, `round` and `ceil` work.
115
+ - **A dataset is always an array.** Take a header row with
116
+ `{% assign h = header | first %}`.
117
+
118
+ A query cannot (yet) express these, so the dataset stays a procedure until it can:
119
+
120
+ - a value in force at a date (`СрезПоследних`: prices, positions, codes, shelf lives);
121
+ - a filter through a reverse set or a polymorphic reference;
122
+ - string aggregation;
123
+ - a join on columns that are not a reference;
124
+ - formulas over aggregates.
125
+
126
+ 1C rounds after folding (`round(price × SUM(qty))`). A query sums a per-line formula. The two
127
+ differ only where lines fold. Measure how many documents that touches, then agree it.
128
+
129
+ ## Verifying a ported form
130
+
131
+ 1. **Compare the dataset with the procedure's output.** Pick tens of random recent
132
+ documents and compare every field, line by line. Zero differences is the bar. An F
133
+ column rewritten from a procedure gets the same check.
134
+ 2. **Render previews on real documents** for every variant: each language, each flag,
135
+ with and without the optional blocks. Screenshot them against 1C's printout where one
136
+ is available.
137
+ 3. **Measure the data each block depends on.** How many recent documents have the date,
138
+ the indicators, the shelf life? A block empty on every document is either 1C's state
139
+ (record it) or a column read from the wrong source (fix it).
140
+
141
+ ## Generating templates
142
+
143
+ When several forms share blocks (requisites, a lines table, signatures, a certificate
144
+ body), write the templates from one generator. Do not hand-copy HTML. Keep the generator
145
+ in the project, regenerate on every change, and never edit its output.
146
+
147
+ In Python, keep each formatted literal its own expression. Adjacent string literals
148
+ concatenate **before** `%` applies, which produces format errors far from the cause.