@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.
- package/CHANGELOG.md +73 -0
- package/README.md +3 -2
- package/dist/server.js +109 -84
- package/package.json +48 -46
- package/resources/docs/conventions.md +17 -14
- package/resources/docs/dsl-reference.md +15 -5
- package/resources/schemas/entity.schema.json +1 -1
- package/resources/schemas/registers.schema.json +8 -3
- package/scripts/install-skills.mjs +1 -0
- package/skills/dforge-1c-import/SKILL.md +374 -0
- package/skills/dforge-1c-import/references/environment.md +97 -0
- package/skills/dforge-1c-import/references/loading.md +161 -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 +110 -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/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 +741 -0
- package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/docusage.py +105 -0
- package/skills/dforge-1c-import/scripts/lib/dump1c.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
- package/skills/dforge-1c-import/scripts/lib/external.py +201 -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 +315 -0
- package/skills/dforge-1c-import/scripts/lib/migrate.py +1299 -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/paths.py +90 -0
- package/skills/dforge-1c-import/scripts/lib/peek.py +70 -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 +94 -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 +488 -0
- package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -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/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/contours.py +47 -0
- package/skills/dforge-1c-import/scripts/project-template/sources.py +14 -0
- package/skills/dforge-mcp-author/SKILL.md +8 -2
- package/skills/dforge-mcp-author/examples/column-security/entities/product.json +1 -1
- package/skills/dforge-mcp-author/examples/column-security/manifest.json +0 -1
- package/skills/dforge-mcp-author/examples/column-security/security/roles.json +2 -2
- package/skills/dforge-mcp-author/references/action-dsl.md +20 -7
- package/skills/dforge-mcp-author/references/conventions.md +7 -1
- package/skills/dforge-mcp-author/references/field-types.md +3 -2
- package/skills/dforge-mcp-author/references/formulas.md +5 -1
- package/skills/dforge-mcp-author/references/print-templates.md +5 -2
- package/skills/dforge-mcp-author/references/queries.md +1 -1
- package/skills/dforge-mcp-author/references/registers.md +18 -3
- package/skills/dforge-mcp-author/references/reports.md +4 -1
- package/skills/dforge-mcp-author/references/schema-import.md +1 -1
- package/skills/dforge-mcp-author/references/security.md +1 -1
- package/skills/dforge-mcp-author/references/translations.md +38 -15
- package/skills/dforge-mcp-author/references/validation-checklist.md +2 -3
- package/skills/dforge-module-build/SKILL.md +10 -1
- package/skills/dforge-module-ship/SKILL.md +31 -3
- package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +0 -35
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Where the working files live.
|
|
3
|
+
|
|
4
|
+
Every tool used to derive this from its own `__file__`, two directories up, which quietly
|
|
5
|
+
assumed the library sits inside the project it serves. It does not have to: the library
|
|
6
|
+
is meant to be lifted out and pointed at a different infobase, and the project it is
|
|
7
|
+
pointed at is then somewhere else entirely.
|
|
8
|
+
|
|
9
|
+
So the root is asked for, not assumed. `DF1C_ROOT` names it; without that, the directory
|
|
10
|
+
holding `tools/` is used, which is what a project laid out like this one gets for free.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import print_function
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _default_root():
|
|
20
|
+
# tools/lib/paths.py -> tools -> the project root. Only a library sitting in a
|
|
21
|
+
# project's `tools/` has one; a copy anywhere else (an installed skill) would write
|
|
22
|
+
# the dump into its own directory, so it asks instead.
|
|
23
|
+
if os.path.basename(os.path.dirname(HERE)) != "tools":
|
|
24
|
+
raise SystemExit("set DF1C_ROOT to the porting project's directory (and DF1C_PROJECT"
|
|
25
|
+
" to its project package): this library is not inside one")
|
|
26
|
+
return os.path.dirname(os.path.dirname(HERE))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ROOT = os.path.abspath(os.environ.get("DF1C_ROOT") or _default_root())
|
|
30
|
+
|
|
31
|
+
# What the extraction writes and everything downstream reads. Kept separate from ROOT
|
|
32
|
+
# because a second infobase dumped beside the first only differs here.
|
|
33
|
+
OUT = os.path.abspath(os.environ.get("DF1C_OUT") or os.path.join(ROOT, "out"))
|
|
34
|
+
|
|
35
|
+
# Generated dForge modules.
|
|
36
|
+
MODULES = os.path.abspath(os.environ.get("DF1C_MODULES") or os.path.join(ROOT, "modules"))
|
|
37
|
+
|
|
38
|
+
# The project's own package - the contour map, the column map and the authored specs.
|
|
39
|
+
# `DF1C_PROJECT` points the library at a different one without moving anything.
|
|
40
|
+
PROJECT = os.path.abspath(os.environ.get("DF1C_PROJECT")
|
|
41
|
+
or os.path.join(os.path.dirname(HERE), "project"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def required(name, what):
|
|
45
|
+
u"""An environment value the library cannot invent, with a default that would be a lie.
|
|
46
|
+
|
|
47
|
+
`DF1C_DB` used to default to `utp_biscuit` and `DF_TENANT` to `hbf1` - this project's own
|
|
48
|
+
names, sitting in the library. Harmless while there is one infobase and a trap as soon as
|
|
49
|
+
there are two: pointing the tools at a second configuration and forgetting one variable
|
|
50
|
+
would have read or written the FIRST one, quietly and with the right-looking output. So
|
|
51
|
+
the library asks and stops rather than guessing.
|
|
52
|
+
"""
|
|
53
|
+
value = os.environ.get(name)
|
|
54
|
+
if value:
|
|
55
|
+
return value
|
|
56
|
+
raise SystemExit(
|
|
57
|
+
"%s is not set - %s.\n"
|
|
58
|
+
" This is the project's to say, not the library's. Copy .env.example to .env and\n"
|
|
59
|
+
" fill it in, or export the variable for this shell." % (name, what))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def derived(name):
|
|
63
|
+
"""Where a file BUILT FROM THE PROJECT goes, as opposed to one read from the infobase.
|
|
64
|
+
|
|
65
|
+
`out/` holds two different kinds of thing. The extraction - report.json, dbnames.json,
|
|
66
|
+
the configuration dump, the module sources - is about the infobase and is the same
|
|
67
|
+
whoever is porting it. `columnmap.json` and `registers.json` are about the PROJECT:
|
|
68
|
+
which of its entities copies which 1C column, which of its modules declares which
|
|
69
|
+
register. Pointing the library at a second project and rebuilding the first kind is
|
|
70
|
+
free; rebuilding the second kind on top of the first project's copy destroys it.
|
|
71
|
+
|
|
72
|
+
Which is what happened: one `columnmap` run against the standard-configuration project
|
|
73
|
+
overwrote the client port's map with 21 modules the client port has never heard of.
|
|
74
|
+
`migrate` reads that file, so the next load would have copied by a map for other
|
|
75
|
+
entities entirely - and the file is committed, so the damage outlives the session.
|
|
76
|
+
|
|
77
|
+
The default project keeps writing where it always did, so nothing that exists moves.
|
|
78
|
+
Any other project gets a subdirectory named after it.
|
|
79
|
+
"""
|
|
80
|
+
if os.path.basename(PROJECT) == "project":
|
|
81
|
+
return os.path.join(OUT, name)
|
|
82
|
+
folder = os.path.join(OUT, os.path.basename(PROJECT))
|
|
83
|
+
if not os.path.isdir(folder):
|
|
84
|
+
os.makedirs(folder)
|
|
85
|
+
return os.path.join(folder, name)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def show():
|
|
89
|
+
for name in ("ROOT", "OUT", "MODULES", "PROJECT"):
|
|
90
|
+
print("%-8s %s" % (name, globals()[name]))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Show sample rows of a physical table, with 1C's standard columns decoded.
|
|
5
|
+
|
|
6
|
+
Attribute lists from the configuration omit the implicit fields every catalog and
|
|
7
|
+
document carries (_Code, _Description, _Date_Time, _Posted, _Marked), so the only
|
|
8
|
+
way to see what actually lives in them is to look at the data.
|
|
9
|
+
|
|
10
|
+
python tools/peek.py _Reference214 # 5 rows, all columns
|
|
11
|
+
python tools/peek.py _Reference214 10 # 10 rows
|
|
12
|
+
python tools/peek.py --find рзВодители # resolve a metadata name to its table
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import print_function
|
|
15
|
+
|
|
16
|
+
import codecs
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
import paths
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
24
|
+
|
|
25
|
+
import dump1c
|
|
26
|
+
|
|
27
|
+
OUT = paths.OUT
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def resolve(name):
|
|
31
|
+
with codecs.open(os.path.join(OUT, "report.json"), "r", "utf-8") as f:
|
|
32
|
+
for row in json.load(f):
|
|
33
|
+
if row.get("name") == name:
|
|
34
|
+
return row
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def render(value):
|
|
39
|
+
if value is None:
|
|
40
|
+
return "-"
|
|
41
|
+
if isinstance(value, (bytes, bytearray)):
|
|
42
|
+
return value.hex()[:16]
|
|
43
|
+
text = str(value).strip()
|
|
44
|
+
return text[:38] if text else "-"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
if sys.argv[1] == "--find":
|
|
49
|
+
row = resolve(sys.argv[2])
|
|
50
|
+
print(json.dumps(row, ensure_ascii=False, indent="\t") if row else "not found")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
table = sys.argv[1]
|
|
54
|
+
limit = int(sys.argv[2]) if len(sys.argv) > 2 else 5
|
|
55
|
+
cur = dump1c.connect(paths.required("DF1C_SERVER", "which MS SQL server holds it"),
|
|
56
|
+
paths.required("DF1C_DB",
|
|
57
|
+
"which MS SQL infobase to read")).cursor()
|
|
58
|
+
cur.execute("SELECT TOP (%d) * FROM [%s]" % (limit, table))
|
|
59
|
+
columns = [d[0] for d in cur.description]
|
|
60
|
+
rows = cur.fetchall()
|
|
61
|
+
# drop columns that are empty across every sampled row - they only add noise
|
|
62
|
+
keep = [i for i, _ in enumerate(columns)
|
|
63
|
+
if any(render(r[i]) not in ("-", "0", "0.0000") for r in rows)]
|
|
64
|
+
for i in keep:
|
|
65
|
+
print("%-28s | %s" % (columns[i], " | ".join(render(r[i]) for r in rows)))
|
|
66
|
+
print("\n(%d columns, %d hidden as empty)" % (len(columns), len(columns) - len(keep)))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Mark the migrated documents that 1C posted as posted in the source system.
|
|
5
|
+
|
|
6
|
+
The loader brings **both** sides across from the infobase: the documents and the register
|
|
7
|
+
rows they wrote. The platform's posting flag is not among them - it is engine state, and a
|
|
8
|
+
generated trigger refuses any write to it but the engine's (`DF006`) - so every migrated
|
|
9
|
+
document arrives saying "not posted" while its movements already sit in the register. Left
|
|
10
|
+
alone, a user opens a 2018 document, sees it unposted, presses Post, and the engine writes a
|
|
11
|
+
second set of rows on top of the migrated ones.
|
|
12
|
+
|
|
13
|
+
The platform's answer is a distinct state rather than a claim that the engine posted it:
|
|
14
|
+
`register.markImported` sets the hidden marker pair beside a state column
|
|
15
|
+
(`<state>_imported`, `<state>_imported_source`). A marked document is settled - the engine
|
|
16
|
+
refuses to post or unpost it, its locked fields freeze, and the card shows it read-only under
|
|
17
|
+
an "Imported" badge - while its state column stays false, so "posted" keeps meaning "the
|
|
18
|
+
engine posted this".
|
|
19
|
+
|
|
20
|
+
python tools/1c.py posted # measure: what 1C posted, what is marked
|
|
21
|
+
DF_TOKEN=<jwt> python tools/1c.py posted --mark # make the markers match 1C
|
|
22
|
+
DF_TOKEN=<jwt> python tools/1c.py posted --unmark # take every marker off again
|
|
23
|
+
|
|
24
|
+
What gets marked is exactly what 1C asserted: `posted` is the infobase's own `Проведен`, so a
|
|
25
|
+
document 1C left unposted stays postable here, and its movements were never written either.
|
|
26
|
+
`--mark` converges on that: it also takes the marker off a document 1C did not post (an
|
|
27
|
+
earlier run marked without the condition), since a marked draft could never be posted here.
|
|
28
|
+
It is idempotent - the RPC skips a document already marked or already posted - so it is safe
|
|
29
|
+
to run after every load, and it has to be: a reload of a document table brings the rows back
|
|
30
|
+
unmarked.
|
|
31
|
+
|
|
32
|
+
It is deliberately **not** part of `migrate run`. Marking is a statement about history made
|
|
33
|
+
once the history is in place, and it needs a token the loader does not have.
|
|
34
|
+
|
|
35
|
+
Nothing here stamps `source` on the migrated register rows. That column exists so unposting
|
|
36
|
+
can find the rows it wrote, and a marked document is never unposted.
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import print_function
|
|
39
|
+
|
|
40
|
+
import argparse
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
import migrate
|
|
44
|
+
import paths
|
|
45
|
+
import posting
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def source():
|
|
49
|
+
"""The label a marker carries: which infobase the document was posted in."""
|
|
50
|
+
return "1C:%s" % paths.required("DF1C_DB", "which MS SQL infobase the documents came from")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def registry_columns(pg):
|
|
54
|
+
"""Every posting column in the tenant - `A` and ledger `L` alike - with its module and
|
|
55
|
+
the table it sits on.
|
|
56
|
+
|
|
57
|
+
Read from the tenant's own metadata rather than listed here, so a register declared
|
|
58
|
+
tomorrow is covered without editing this file - the failure mode otherwise is a new
|
|
59
|
+
binding quietly missing from the sweep, which is exactly the bug this guards against.
|
|
60
|
+
It was one: the sweep read `A` only, and the first ledger binding would have left every
|
|
61
|
+
document 1C posted offered Post again, doubling its postings in the ledger.
|
|
62
|
+
|
|
63
|
+
A binding may keep its state in a field of its own (`stateField`), and an extension's
|
|
64
|
+
columns sit in a side table (`storage_table`) joined on the document's key: the column
|
|
65
|
+
returned is the state the marker sits beside, and the storage is that side table or None.
|
|
66
|
+
"""
|
|
67
|
+
cur = pg.cursor()
|
|
68
|
+
cur.execute("""
|
|
69
|
+
SELECT m.module_cd, e.entity_cd, e.schema_name, e.db_object,
|
|
70
|
+
coalesce(c.params->>'stateField', c.column_cd), s.storage_table,
|
|
71
|
+
(SELECT k.column_cd FROM "dForge".entity_column k
|
|
72
|
+
WHERE k.entity_id = e.entity_id AND k.is_pk LIMIT 1)
|
|
73
|
+
FROM "dForge".entity_column c
|
|
74
|
+
JOIN "dForge".entity e ON e.entity_id = c.entity_id
|
|
75
|
+
JOIN "dForge".module m ON m.module_id = e.module_id
|
|
76
|
+
LEFT JOIN "dForge".entity_column s
|
|
77
|
+
ON s.entity_id = e.entity_id AND s.column_cd = coalesce(c.params->>'stateField', c.column_cd)
|
|
78
|
+
WHERE c.column_type IN ('A', 'L')
|
|
79
|
+
ORDER BY m.module_cd, e.entity_cd, c.column_cd""")
|
|
80
|
+
return [row for row in cur.fetchall() if row[2] and row[3]]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def storage_of(schema, table, storage):
|
|
84
|
+
"""(schema, table) the state and its marker are kept in."""
|
|
85
|
+
if not storage:
|
|
86
|
+
return schema, table
|
|
87
|
+
side_schema, side_table = storage.split(".", 1)
|
|
88
|
+
return side_schema.strip('"'), side_table.strip('"')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def has_column(pg, schema, table, column):
|
|
92
|
+
cur = pg.cursor()
|
|
93
|
+
cur.execute("""SELECT 1 FROM information_schema.columns
|
|
94
|
+
WHERE table_schema = %s AND table_name = %s AND column_name = %s""",
|
|
95
|
+
(schema, table, column))
|
|
96
|
+
return cur.fetchone() is not None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def counts(pg, schema, table, column, storage=None, key=None):
|
|
100
|
+
"""(1C posted, engine posted, marked, still to mark, marked though 1C did not post).
|
|
101
|
+
An extension's state is read from its side table, where a document it never wrote has no row."""
|
|
102
|
+
marker = column + "_imported"
|
|
103
|
+
side = ' LEFT JOIN %s x ON x."%s" = t."%s"' % (storage, key, key) if storage else ""
|
|
104
|
+
cur = pg.cursor()
|
|
105
|
+
cur.execute("""SELECT count(*) FILTER (WHERE posted),
|
|
106
|
+
count(*) FILTER (WHERE "{c}"),
|
|
107
|
+
count(*) FILTER (WHERE "{m}"),
|
|
108
|
+
count(*) FILTER (WHERE posted AND NOT coalesce("{c}", false) AND NOT coalesce("{m}", false)),
|
|
109
|
+
count(*) FILTER (WHERE "{m}" AND NOT coalesce(posted, false))
|
|
110
|
+
FROM "{s}"."{t}" t{x}""".format(c=column, m=marker, s=schema, t=table, x=side))
|
|
111
|
+
return cur.fetchone()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def folder_of(pg, module):
|
|
115
|
+
# As a string: the RPC declares folderId as one.
|
|
116
|
+
cur = pg.cursor()
|
|
117
|
+
cur.execute('select f.folder_id from "dForge".folder f '
|
|
118
|
+
'join "dForge".module m on m.module_id = f.module_id '
|
|
119
|
+
'where m.module_cd = %s order by f.folder_id limit 1', (module,))
|
|
120
|
+
row = cur.fetchone()
|
|
121
|
+
return str(row[0]) if row else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def survey(pg, module=None, unmark=False, quiet=False):
|
|
125
|
+
"""The table of what 1C posted against what is marked, and the work it implies.
|
|
126
|
+
|
|
127
|
+
Returns (work, total, stale): one (module, label, column) per registry column that can
|
|
128
|
+
be marked, the number of documents a converge would touch, and the modules installed
|
|
129
|
+
before the marker pair existed.
|
|
130
|
+
"""
|
|
131
|
+
columns = [c for c in registry_columns(pg) if not module or c[0] == module]
|
|
132
|
+
out = (lambda *a: None) if quiet else print
|
|
133
|
+
out("%-52s %-22s %9s %9s %9s %9s %9s"
|
|
134
|
+
% ("document", "state", "1C", "posted", "marked", "to mark", "stray"))
|
|
135
|
+
out("-" * 126)
|
|
136
|
+
work, total, stale = [], 0, []
|
|
137
|
+
for module_cd, entity_cd, schema, table, column, storage, key in columns:
|
|
138
|
+
label = "%s.%s" % (module_cd, entity_cd)
|
|
139
|
+
if not has_column(pg, schema, table, "posted"):
|
|
140
|
+
out("%-52s %-22s skipped: no `posted` column to read" % (label[:52], column[:22]))
|
|
141
|
+
continue
|
|
142
|
+
if not has_column(pg, *(storage_of(schema, table, storage) + (column + "_imported",))):
|
|
143
|
+
# Installed before the platform synthesised the marker pair. Marking refuses and
|
|
144
|
+
# says to reinstall; saying so here costs nothing.
|
|
145
|
+
stale.append(label)
|
|
146
|
+
out("%-52s %-22s no marker column: reinstall the module" % (label[:52], column[:22]))
|
|
147
|
+
continue
|
|
148
|
+
in_1c, posted_here, marked, pending, stray = counts(pg, schema, table, column, storage, key)
|
|
149
|
+
total += marked if unmark else pending + stray
|
|
150
|
+
work.append((module_cd, label, column))
|
|
151
|
+
out("%-52s %-22s %9s %9s %9s %9s %9s"
|
|
152
|
+
% (label[:52], column[:22], format(in_1c, ","), format(posted_here, ","),
|
|
153
|
+
format(marked, ","), format(pending, ","), format(stray, ",")))
|
|
154
|
+
out("-" * 126)
|
|
155
|
+
out("%-98s %9s" % ("to unmark" if unmark else "to mark or unmark", format(total, ",")))
|
|
156
|
+
return work, total, stale
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def converge(pg, work, jwt, unmark=False):
|
|
160
|
+
"""Make the markers match 1C (or take every one off): {verb: count}, and whether all went.
|
|
161
|
+
|
|
162
|
+
--mark marks what 1C posted and takes the marker off what it did not. The delta loader
|
|
163
|
+
calls this too, after it has upserted documents: a document it brought in is unmarked,
|
|
164
|
+
and one 1C has since unposted is still marked.
|
|
165
|
+
"""
|
|
166
|
+
steps = [("register.unmarkImported", "true", "unmarked")] if unmark else [
|
|
167
|
+
("register.markImported", "t.posted", "marked"),
|
|
168
|
+
("register.unmarkImported", "NOT coalesce(t.posted, false)", "unmarked")]
|
|
169
|
+
failed = False
|
|
170
|
+
done = {}
|
|
171
|
+
for module, label, column in work:
|
|
172
|
+
for method, where, verb in steps:
|
|
173
|
+
params = {"entity": label, "stateColumn": column, "where": where}
|
|
174
|
+
if method == "register.markImported":
|
|
175
|
+
params["source"] = source()
|
|
176
|
+
result = posting.call(method, params, folder_of(pg, module), jwt)
|
|
177
|
+
count = (result.get("result") or {}).get("count") if isinstance(result, dict) else None
|
|
178
|
+
if count is None:
|
|
179
|
+
failed = True
|
|
180
|
+
print(" %-52s %s" % (label, result), file=sys.stderr)
|
|
181
|
+
continue
|
|
182
|
+
done[verb] = done.get(verb, 0) + count
|
|
183
|
+
if count:
|
|
184
|
+
print(" %-52s %-22s %s %s" % (label, column, verb, format(count, ",")))
|
|
185
|
+
return done, not failed
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def main():
|
|
189
|
+
parser = argparse.ArgumentParser(description=__doc__.strip().splitlines()[0])
|
|
190
|
+
action = parser.add_mutually_exclusive_group()
|
|
191
|
+
action.add_argument("--mark", action="store_true",
|
|
192
|
+
help="mark every document 1C posted (needs a token)")
|
|
193
|
+
action.add_argument("--unmark", action="store_true",
|
|
194
|
+
help="remove every marker (needs a token)")
|
|
195
|
+
parser.add_argument("module", nargs="?", help="one module only")
|
|
196
|
+
args = parser.parse_args()
|
|
197
|
+
|
|
198
|
+
migrate.load_env()
|
|
199
|
+
pg = migrate.pg_connect()
|
|
200
|
+
pg.autocommit = True
|
|
201
|
+
if not any(not args.module or c[0] == args.module for c in registry_columns(pg)):
|
|
202
|
+
print("no registry columns in this tenant")
|
|
203
|
+
return
|
|
204
|
+
work, total, _ = survey(pg, args.module, args.unmark)
|
|
205
|
+
|
|
206
|
+
if not (args.mark or args.unmark):
|
|
207
|
+
if total:
|
|
208
|
+
print("\nnothing written. --mark (with a token) marks them.")
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
done, ok = converge(pg, work, posting.token(), args.unmark)
|
|
212
|
+
print("\n" + ", ".join("%s %s" % (verb, format(n, ",")) for verb, n in done.items())
|
|
213
|
+
+ " document(s).")
|
|
214
|
+
if not ok:
|
|
215
|
+
sys.exit(1)
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
main()
|