@dforge-core/dforge-mcp 0.2.45 → 0.2.47
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 +45 -0
- package/README.md +2 -1
- package/dist/server.js +71 -3
- package/package.json +3 -3
- package/resources/docs/dsl-reference.md +1 -1
- package/resources/schemas/data-views.schema.json +50 -0
- package/scripts/install-skills.mjs +1 -0
- package/skills/dforge-1c-import/SKILL.md +375 -0
- package/skills/dforge-1c-import/references/environment.md +98 -0
- package/skills/dforge-1c-import/references/loading.md +164 -0
- package/skills/dforge-1c-import/references/measurements.md +210 -0
- package/skills/dforge-1c-import/references/print-forms.md +148 -0
- package/skills/dforge-1c-import/scripts/1c.py +178 -0
- package/skills/dforge-1c-import/scripts/README.md +164 -0
- package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
- package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
- package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
- package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
- package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
- package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
- package/skills/dforge-1c-import/scripts/lib/changes.py +326 -0
- package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
- package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
- package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
- package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
- package/skills/dforge-1c-import/scripts/lib/delta.py +770 -0
- package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/docusage.py +106 -0
- package/skills/dforge-1c-import/scripts/lib/dump1c.py +408 -0
- package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
- package/skills/dforge-1c-import/scripts/lib/external.py +202 -0
- package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
- package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
- package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
- package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
- package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
- package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
- package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
- package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
- package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
- package/skills/dforge-1c-import/scripts/lib/meta.py +325 -0
- package/skills/dforge-1c-import/scripts/lib/migrate.py +1318 -0
- package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
- package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/onecd.py +269 -0
- package/skills/dforge-1c-import/scripts/lib/paths.py +111 -0
- package/skills/dforge-1c-import/scripts/lib/peek.py +68 -0
- package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
- package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
- package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
- package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
- package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
- package/skills/dforge-1c-import/scripts/lib/recorders.py +91 -0
- package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
- package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
- package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
- package/skills/dforge-1c-import/scripts/lib/seedstate.py +493 -0
- package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
- package/skills/dforge-1c-import/scripts/lib/source_file.py +227 -0
- package/skills/dforge-1c-import/scripts/lib/source_mssql.py +158 -0
- package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
- package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
- package/skills/dforge-1c-import/scripts/lib/store.py +126 -0
- package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
- package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
- package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
- package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
- package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
- package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
- package/skills/dforge-1c-import/scripts/project-template/.env.example +24 -0
- package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
- package/skills/dforge-1c-import/scripts/project-template/sources.py +24 -0
- package/skills/dforge-mcp-author/SKILL.md +5 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +1 -1
- package/skills/dforge-mcp-author/references/data-views.md +38 -2
- package/skills/dforge-mcp-author/references/formulas.md +32 -1
- package/skills/dforge-mcp-author/references/print-templates.md +30 -5
- package/skills/dforge-mcp-author/references/queries.md +17 -2
- package/skills/dforge-mcp-author/references/registers.md +1 -0
- package/skills/dforge-mcp-author/references/reports.md +1 -1
- package/skills/dforge-mcp-author/references/validation-checklist.md +1 -0
|
@@ -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 the export (DuckDB over `out/data/`, via `store.connect()`) 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.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
One way in, so the split between library and project stays a fact rather than a habit.
|
|
5
|
+
|
|
6
|
+
python tools/1c.py <command> [args...]
|
|
7
|
+
python tools/1c.py --list # what there is
|
|
8
|
+
python tools/1c.py --boundary # does the library still stand on its own?
|
|
9
|
+
|
|
10
|
+
`tools/lib` is 1C and dForge: how a configuration is stored, how its metadata parses,
|
|
11
|
+
what the physical tables mean, and how rows are copied and checked. It is meant to be
|
|
12
|
+
lifted out and pointed at a different infobase.
|
|
13
|
+
|
|
14
|
+
`tools/project` is this infobase: which object belongs to which module, which 1C column
|
|
15
|
+
feeds each ported one, which regulated statements it holds, this company's own proper
|
|
16
|
+
nouns, and the modules written by hand. `DF1C_PROJECT` points the library at a different
|
|
17
|
+
one; `binding.py` lists the whole of what a project has to provide.
|
|
18
|
+
|
|
19
|
+
Both directories go on the path, so the modules import each other by plain name and a
|
|
20
|
+
move between them costs no edits. That convenience is also the hazard, so `--boundary`
|
|
21
|
+
checks two things rather than trusting either: that no library module IMPORTS a project
|
|
22
|
+
one, and that no library module NAMES this project's world - its server, its infobase,
|
|
23
|
+
its tenant, its company's abbreviations. A literal is the quiet way to bind a library to
|
|
24
|
+
one configuration, and it is how `DF1C_DB` came to default to `utp_biscuit` for months.
|
|
25
|
+
|
|
26
|
+
Because both directories are on one path, a project module may not take a name the
|
|
27
|
+
library already uses: `regulated.py` rather than `forms.py`, `vocabulary.py` rather than
|
|
28
|
+
`terms.py`. `--boundary` catches the shadowing as an import crossing.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import print_function
|
|
31
|
+
|
|
32
|
+
import io
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
|
|
36
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
37
|
+
LIB = os.path.join(HERE, "lib")
|
|
38
|
+
PROJECT = os.path.abspath(os.environ.get("DF1C_PROJECT") or os.path.join(HERE, "project"))
|
|
39
|
+
|
|
40
|
+
for entry in (PROJECT, LIB):
|
|
41
|
+
if entry not in sys.path:
|
|
42
|
+
sys.path.insert(0, entry)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def commands():
|
|
46
|
+
"""Every runnable module, library or project, mapped to (path, first docstring line)."""
|
|
47
|
+
found = {}
|
|
48
|
+
for directory in (LIB, PROJECT):
|
|
49
|
+
for name in sorted(os.listdir(directory)):
|
|
50
|
+
if not name.endswith(".py") or name.startswith("_") or name[:-3] in found:
|
|
51
|
+
continue
|
|
52
|
+
path = os.path.join(directory, name)
|
|
53
|
+
summary = _runnable(path)
|
|
54
|
+
if summary is not None:
|
|
55
|
+
found[name[:-3]] = (path, summary)
|
|
56
|
+
return found
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _runnable(path):
|
|
60
|
+
"""The module's first docstring line if it can be run as a script, else None."""
|
|
61
|
+
with io.open(path, encoding="utf-8") as f:
|
|
62
|
+
text = f.read()
|
|
63
|
+
# Anything runnable: most define main(), a few only a __main__ block.
|
|
64
|
+
if "def main(" not in text and "__name__" not in text:
|
|
65
|
+
return None
|
|
66
|
+
if '"""' not in text:
|
|
67
|
+
return ""
|
|
68
|
+
body = text.split('"""', 2)[1].strip().splitlines()
|
|
69
|
+
return (body[0] if body else "").strip()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def boundary():
|
|
73
|
+
"""Two ways the library can stop standing on its own, both read off the source.
|
|
74
|
+
|
|
75
|
+
IMPORTS. The path puts both directories in reach, so nothing stops a library module
|
|
76
|
+
importing `contours` directly and nothing would complain until someone tried to lift
|
|
77
|
+
the library out.
|
|
78
|
+
|
|
79
|
+
NAMES. An import is the loud way to bind to one infobase; a literal is the quiet one.
|
|
80
|
+
`DF1C_DB` defaulting to `utp_biscuit` and `DF_TENANT` to `hbf1` sat in the library for
|
|
81
|
+
a long while, harmless with one infobase and a trap with two - point the tools at a
|
|
82
|
+
second configuration, forget one variable, and they read the first one with
|
|
83
|
+
right-looking output. So the names of THIS project's world are refused in the library,
|
|
84
|
+
including in its comments, since a stale comment is how the next reader learns the
|
|
85
|
+
wrong thing.
|
|
86
|
+
|
|
87
|
+
The list is read from the project rather than written here, so a project that renames
|
|
88
|
+
itself does not have to remember this file.
|
|
89
|
+
"""
|
|
90
|
+
import ast
|
|
91
|
+
import re
|
|
92
|
+
|
|
93
|
+
project_modules = set(n[:-3] for n in os.listdir(PROJECT) if n.endswith(".py"))
|
|
94
|
+
|
|
95
|
+
# What this project calls itself: the infobase, the tenant and its own proper nouns.
|
|
96
|
+
reserved = set()
|
|
97
|
+
for variable in ("DF1C_DB", "DF1C_SERVER", "DF_TENANT"):
|
|
98
|
+
value = os.environ.get(variable)
|
|
99
|
+
if value:
|
|
100
|
+
reserved.add(value.lower())
|
|
101
|
+
env_example = os.path.join(os.path.dirname(os.path.abspath(PROJECT)), os.pardir,
|
|
102
|
+
".env.example")
|
|
103
|
+
if os.path.exists(env_example):
|
|
104
|
+
with io.open(env_example, encoding="utf-8") as f:
|
|
105
|
+
for line in f:
|
|
106
|
+
match = re.match(r"\s*(DF1C_DB|DF1C_SERVER|DF_TENANT)\s*=\s*(\S+)", line)
|
|
107
|
+
if match:
|
|
108
|
+
reserved.add(match.group(2).lower())
|
|
109
|
+
terms = os.path.join(PROJECT, "terms.py")
|
|
110
|
+
if os.path.exists(terms):
|
|
111
|
+
with io.open(terms, encoding="utf-8") as f:
|
|
112
|
+
for value in re.findall(r'u"([^"]+)"\s*:', f.read()):
|
|
113
|
+
if len(value) > 3: # two- and three-letter forms collide with real words
|
|
114
|
+
reserved.add(value.lower())
|
|
115
|
+
|
|
116
|
+
problems = []
|
|
117
|
+
for name in sorted(os.listdir(LIB)):
|
|
118
|
+
if not name.endswith(".py"):
|
|
119
|
+
continue
|
|
120
|
+
with io.open(os.path.join(LIB, name), encoding="utf-8") as f:
|
|
121
|
+
text = f.read()
|
|
122
|
+
tree = ast.parse(text)
|
|
123
|
+
for node in ast.walk(tree):
|
|
124
|
+
named = []
|
|
125
|
+
if isinstance(node, ast.Import):
|
|
126
|
+
named = [a.name.split(".")[0] for a in node.names]
|
|
127
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
128
|
+
named = [node.module.split(".")[0]]
|
|
129
|
+
for target in named:
|
|
130
|
+
if target in project_modules:
|
|
131
|
+
problems.append("lib/%s imports %s - go through binding.py instead"
|
|
132
|
+
% (name, target))
|
|
133
|
+
# paths.py explains why these names are refused, so it is allowed to name them.
|
|
134
|
+
if name == "paths.py":
|
|
135
|
+
continue
|
|
136
|
+
lowered = text.lower()
|
|
137
|
+
for word in sorted(reserved):
|
|
138
|
+
if word in lowered:
|
|
139
|
+
problems.append("lib/%s names %r - that is the project's, not the library's"
|
|
140
|
+
% (name, word))
|
|
141
|
+
return problems
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def main():
|
|
145
|
+
args = sys.argv[1:]
|
|
146
|
+
if not args or args[0] in ("-h", "--help", "--list"):
|
|
147
|
+
import paths
|
|
148
|
+
paths.show()
|
|
149
|
+
print()
|
|
150
|
+
for name, (_, summary) in sorted(commands().items()):
|
|
151
|
+
print(" %-16s %s" % (name, summary[:60]))
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
if args[0] == "--boundary":
|
|
155
|
+
problems = boundary()
|
|
156
|
+
for line in problems:
|
|
157
|
+
print(line)
|
|
158
|
+
print("%d crossing(s) of the library/project boundary" % len(problems))
|
|
159
|
+
return 1 if problems else 0
|
|
160
|
+
|
|
161
|
+
name, rest = args[0], args[1:]
|
|
162
|
+
found = commands()
|
|
163
|
+
if name not in found:
|
|
164
|
+
print("no such command: %s (try --list)" % name)
|
|
165
|
+
return 2
|
|
166
|
+
# run_path rather than calling main(): a few tools only have a __main__
|
|
167
|
+
# block, and this runs every one of them exactly as a script would.
|
|
168
|
+
import runpy
|
|
169
|
+
sys.argv = [name] + rest
|
|
170
|
+
try:
|
|
171
|
+
runpy.run_path(found[name][0], run_name="__main__")
|
|
172
|
+
except SystemExit as stop:
|
|
173
|
+
return stop.code or 0
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
sys.exit(main() or 0)
|