acumatica-cli 0.17.0__tar.gz → 0.18.0__tar.gz
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.
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/PKG-INFO +2 -2
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/README.md +1 -1
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/pyproject.toml +1 -1
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/pyproject.toml.orig +1 -1
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/cli.py +5 -3
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/reconcile.py +127 -29
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/snapshot_map.yaml +84 -3
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/bootstrap.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/bootstrap_project.xml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/client.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/config.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/extract.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/inventory.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/output.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/run.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/seed.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/seed_catalog.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/state.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/target.py +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/README.md +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/baseline/50-gl-preferences.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/baseline/60-ledger-company.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/bootstrap/company.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/bootstrap/credit-terms.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/10-reason-codes.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/20-in-preferences.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/30-availability-rules.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/40-posting-classes.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/50-warehouse.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/51-warehouse-locations.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/52-warehouse-defaults.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/53-tax-categories.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/54-item-classes.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/56-so-preferences.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/57-po-preferences.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/58-order-types.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/60-ar-preferences.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/61-ap-preferences.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/62-ca-preferences.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/63-cash-account.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/64-payment-methods.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/65-statement-cycles.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/70-vendor-classes.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/71-customer-classes.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/75-vendors.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/76-customers.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/80-stock-items-parts.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/82-stock-items-kits.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/master/85-kit-specifications.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/setup/10-financial-year.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/setup/20-master-calendar.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/setup/30-open-periods.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/config/views/10-trial-balance.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/env +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/gitignore +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/scenario/10-seed-capital.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/scenario/20-buy.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/scenario/30-build.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/scenario/40-sell.yaml +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/target +0 -0
- {acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/tenant.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: acumatica-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.0
|
|
4
4
|
Summary: Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data
|
|
5
5
|
Author: Konstantin Borovik
|
|
6
6
|
Author-email: Konstantin Borovik <kb@lab5.ca>
|
|
@@ -107,7 +107,7 @@ A path like `config/` expands nested seed dirs in that fixed order.
|
|
|
107
107
|
`extract` always writes under `config/{bootstrap,baseline,setup,master}/` (catalog-driven; never root SEED_DIRS).
|
|
108
108
|
`inventory` is offline (no REST/SSH/password): SM203520 Settings XML ZIP or `ac.exe export xml` folder writes to `inventory/`.
|
|
109
109
|
`reconcile` is offline: compare `inventory/` to optional `config/` and write `findings/` only (never writes seed).
|
|
110
|
-
Optional `snapshot_map.yaml` (data-repo root or package defaults) maps DAC tables to catalog entities and normalizes join (pad-trim, key/field aliases, Account/Sub FK CD resolve).
|
|
110
|
+
Optional `snapshot_map.yaml` (data-repo root or package defaults) maps DAC tables to catalog entities and normalizes join (pad-trim, key/field aliases, Account/Sub FK CD resolve, enum label→code).
|
|
111
111
|
See [docs/demo-seed.md](docs/demo-seed.md).
|
|
112
112
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
113
113
|
Run `acu <command> --help` for details on any command.
|
|
@@ -89,7 +89,7 @@ A path like `config/` expands nested seed dirs in that fixed order.
|
|
|
89
89
|
`extract` always writes under `config/{bootstrap,baseline,setup,master}/` (catalog-driven; never root SEED_DIRS).
|
|
90
90
|
`inventory` is offline (no REST/SSH/password): SM203520 Settings XML ZIP or `ac.exe export xml` folder writes to `inventory/`.
|
|
91
91
|
`reconcile` is offline: compare `inventory/` to optional `config/` and write `findings/` only (never writes seed).
|
|
92
|
-
Optional `snapshot_map.yaml` (data-repo root or package defaults) maps DAC tables to catalog entities and normalizes join (pad-trim, key/field aliases, Account/Sub FK CD resolve).
|
|
92
|
+
Optional `snapshot_map.yaml` (data-repo root or package defaults) maps DAC tables to catalog entities and normalizes join (pad-trim, key/field aliases, Account/Sub FK CD resolve, enum label→code).
|
|
93
93
|
See [docs/demo-seed.md](docs/demo-seed.md).
|
|
94
94
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
95
95
|
Run `acu <command> --help` for details on any command.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "acumatica-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.18.0"
|
|
4
4
|
description = "Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data"
|
|
5
5
|
authors = [{ name = "Konstantin Borovik", email = "kb@lab5.ca" }]
|
|
6
6
|
license = "PolyForm-Noncommercial-1.0.0"
|
|
@@ -1104,9 +1104,11 @@ def reconcile_cmd(
|
|
|
1104
1104
|
Exit 0 clean, 1 IO/parse fail; never 2 (conflicts are findings, not
|
|
1105
1105
|
drift). Optional snapshot_map.yaml maps DAC tables to catalog
|
|
1106
1106
|
entities (absent → identity match on entity name) and may declare
|
|
1107
|
-
keys:/fields: seed→inv aliases
|
|
1108
|
-
|
|
1109
|
-
ReasonCode/VendorClass Account+Sub
|
|
1107
|
+
keys:/fields: seed→inv aliases, resolvers:/resolves: for FK int→CD,
|
|
1108
|
+
and enums: label→code (package defaults cover Sub/UOM aliases,
|
|
1109
|
+
ReasonCode/VendorClass/PostingClass/CashAccount Account+Sub,
|
|
1110
|
+
ReasonCode.Usage + Account Type/Active bools, etc.). Compare always
|
|
1111
|
+
pad-trims strings.
|
|
1110
1112
|
"""
|
|
1111
1113
|
inv = (
|
|
1112
1114
|
inventory_dir
|
|
@@ -24,7 +24,9 @@ Per-row optional ``keys:`` / ``fields:`` maps rename seed names →
|
|
|
24
24
|
inventory column names (V38 M3) so e.g. SubaccountCD joins SubCD and
|
|
25
25
|
UnitID joins Unit without false key-skips. Global ``resolvers:`` plus
|
|
26
26
|
per-row ``resolves:`` map inv int FKs → CD strings before compare (V38 M4)
|
|
27
|
-
so seed CD space matches inventory IDs (Account/Sub first).
|
|
27
|
+
so seed CD space matches inventory IDs (Account/Sub first). Global
|
|
28
|
+
``enums:`` plus per-row ``enums:`` map REST labels → DAC codes (V38 M5)
|
|
29
|
+
so e.g. Usage Adjustment vs A and Active true vs 1 do not false-delta.
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
32
|
from __future__ import annotations
|
|
@@ -120,8 +122,10 @@ class TableMapEntry(Model):
|
|
|
120
122
|
|
|
121
123
|
Optional ``keys`` / ``fields`` are seed-name → inventory-column aliases
|
|
122
124
|
(V38 M3). Optional ``resolves`` maps seed field → global resolver name
|
|
123
|
-
(V38 M4): inventory value is looked up id→CD before compare.
|
|
124
|
-
|
|
125
|
+
(V38 M4): inventory value is looked up id→CD before compare. Optional
|
|
126
|
+
``enums`` maps seed field → global enum table name (V38 M5): seed labels
|
|
127
|
+
fold to DAC codes before compare. Absent aliases → identity names. v1
|
|
128
|
+
rows are ``{table, entity}`` only.
|
|
125
129
|
"""
|
|
126
130
|
|
|
127
131
|
table: str
|
|
@@ -129,6 +133,7 @@ class TableMapEntry(Model):
|
|
|
129
133
|
keys: dict[str, str] = Field(default_factory=dict)
|
|
130
134
|
fields: dict[str, str] = Field(default_factory=dict)
|
|
131
135
|
resolves: dict[str, str] = Field(default_factory=dict)
|
|
136
|
+
enums: dict[str, str] = Field(default_factory=dict)
|
|
132
137
|
|
|
133
138
|
@field_validator("table", "entity")
|
|
134
139
|
@classmethod
|
|
@@ -138,25 +143,25 @@ class TableMapEntry(Model):
|
|
|
138
143
|
raise ValueError("table/entity must be non-empty")
|
|
139
144
|
return v
|
|
140
145
|
|
|
141
|
-
@field_validator("keys", "fields", "resolves", mode="before")
|
|
146
|
+
@field_validator("keys", "fields", "resolves", "enums", mode="before")
|
|
142
147
|
@classmethod
|
|
143
148
|
def _str_map(cls, v: object) -> object:
|
|
144
|
-
"""Accept seed→inv
|
|
149
|
+
"""Accept seed→inv/resolver/enum-name string maps; reject empty names."""
|
|
145
150
|
if v is None:
|
|
146
151
|
return {}
|
|
147
152
|
if not isinstance(v, dict):
|
|
148
|
-
raise ValueError("keys/fields/resolves must be a mapping of strings")
|
|
153
|
+
raise ValueError("keys/fields/resolves/enums must be a mapping of strings")
|
|
149
154
|
out: dict[str, str] = {}
|
|
150
155
|
for seed_name, target in v.items():
|
|
151
156
|
s = str(seed_name).strip()
|
|
152
157
|
if isinstance(target, dict):
|
|
153
158
|
raise ValueError(
|
|
154
|
-
f"keys/fields/resolves[{s!r}]: expected string value "
|
|
155
|
-
"(use resolves: for FK names; enums
|
|
159
|
+
f"keys/fields/resolves/enums[{s!r}]: expected string value "
|
|
160
|
+
"(use resolves: for FK names; enums: for label->code tables)"
|
|
156
161
|
)
|
|
157
162
|
i = str(target).strip()
|
|
158
163
|
if not s or not i:
|
|
159
|
-
raise ValueError("keys/fields/resolves names must be non-empty")
|
|
164
|
+
raise ValueError("keys/fields/resolves/enums names must be non-empty")
|
|
160
165
|
out[s] = i
|
|
161
166
|
return out
|
|
162
167
|
|
|
@@ -165,6 +170,7 @@ class SnapshotMap(Model):
|
|
|
165
170
|
"""Optional table→entity map (package or data-repo ``snapshot_map.yaml``)."""
|
|
166
171
|
|
|
167
172
|
resolvers: dict[str, FkResolver] = Field(default_factory=dict)
|
|
173
|
+
enums: dict[str, dict[str, str]] = Field(default_factory=dict)
|
|
168
174
|
tables: list[TableMapEntry] = Field(default_factory=list)
|
|
169
175
|
|
|
170
176
|
@field_validator("resolvers", mode="before")
|
|
@@ -176,6 +182,39 @@ class SnapshotMap(Model):
|
|
|
176
182
|
raise ValueError("resolvers must be a mapping of name -> {table,id,cd}")
|
|
177
183
|
return v
|
|
178
184
|
|
|
185
|
+
@field_validator("enums", mode="before")
|
|
186
|
+
@classmethod
|
|
187
|
+
def _enums_map(cls, v: object) -> object:
|
|
188
|
+
"""Global name → {label: code}; stringify YAML bool keys as true/false."""
|
|
189
|
+
if v is None:
|
|
190
|
+
return {}
|
|
191
|
+
if not isinstance(v, dict):
|
|
192
|
+
raise ValueError("enums must be a mapping of name -> {label: code}")
|
|
193
|
+
out: dict[str, dict[str, str]] = {}
|
|
194
|
+
for name, mapping in v.items():
|
|
195
|
+
n = str(name).strip()
|
|
196
|
+
if not n:
|
|
197
|
+
raise ValueError("enums names must be non-empty")
|
|
198
|
+
if not isinstance(mapping, dict):
|
|
199
|
+
raise ValueError(f"enums[{n!r}]: expected mapping of label -> code")
|
|
200
|
+
inner: dict[str, str] = {}
|
|
201
|
+
for label, code in mapping.items():
|
|
202
|
+
if isinstance(label, bool):
|
|
203
|
+
lab = "true" if label else "false"
|
|
204
|
+
else:
|
|
205
|
+
lab = str(label).strip()
|
|
206
|
+
if isinstance(code, bool):
|
|
207
|
+
cd = "true" if code else "false"
|
|
208
|
+
else:
|
|
209
|
+
cd = str(code).strip()
|
|
210
|
+
if not lab or not cd:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"enums[{n!r}]: label/code must be non-empty strings"
|
|
213
|
+
)
|
|
214
|
+
inner[lab] = cd
|
|
215
|
+
out[n] = inner
|
|
216
|
+
return out
|
|
217
|
+
|
|
179
218
|
def entity_for(self, table: str) -> str | None:
|
|
180
219
|
"""Catalog entity name for a DAC table, or None when unmapped."""
|
|
181
220
|
entry = self.entry_for(table)
|
|
@@ -432,6 +471,7 @@ def reconcile(
|
|
|
432
471
|
catalog_entities = set(by_entity)
|
|
433
472
|
seed_index = _index_seeds(seeds)
|
|
434
473
|
fk_indexes = _build_fk_indexes(inv, smap.resolvers)
|
|
474
|
+
enum_tables = smap.enums
|
|
435
475
|
config_present = config_dir is not None and config_dir.is_dir()
|
|
436
476
|
|
|
437
477
|
unmapped: list[UnmappedTable] = []
|
|
@@ -468,6 +508,7 @@ def reconcile(
|
|
|
468
508
|
seeds=seed_index,
|
|
469
509
|
map_entry=map_entry,
|
|
470
510
|
fk_indexes=fk_indexes,
|
|
511
|
+
enum_tables=enum_tables,
|
|
471
512
|
)
|
|
472
513
|
)
|
|
473
514
|
_append_usr_custom(custom, table.name, entity, col_names)
|
|
@@ -558,7 +599,7 @@ def _rest_gaps_for_table(
|
|
|
558
599
|
return gaps
|
|
559
600
|
|
|
560
601
|
|
|
561
|
-
def _deltas_for_table( # noqa: PLR0913 — keyword-only;
|
|
602
|
+
def _deltas_for_table( # noqa: PLR0913 — keyword-only; map + V38 indexes
|
|
562
603
|
*,
|
|
563
604
|
entity: str,
|
|
564
605
|
table: inventory.TableData,
|
|
@@ -567,11 +608,13 @@ def _deltas_for_table( # noqa: PLR0913 — keyword-only; map_entry + fk_indexes
|
|
|
567
608
|
seeds: _SeedIndex,
|
|
568
609
|
map_entry: TableMapEntry | None = None,
|
|
569
610
|
fk_indexes: dict[str, dict[str, str]] | None = None,
|
|
611
|
+
enum_tables: dict[str, dict[str, str]] | None = None,
|
|
570
612
|
) -> list[FieldDelta]:
|
|
571
613
|
"""Per-spec field deltas for one inventory table (optional map aliases)."""
|
|
572
614
|
if not config_present:
|
|
573
615
|
return []
|
|
574
616
|
indexes = fk_indexes if fk_indexes is not None else {}
|
|
617
|
+
enums = enum_tables if enum_tables is not None else {}
|
|
575
618
|
out: list[FieldDelta] = []
|
|
576
619
|
for spec in specs:
|
|
577
620
|
seed_file = _find_seed_for_spec(spec, seeds)
|
|
@@ -585,6 +628,7 @@ def _deltas_for_table( # noqa: PLR0913 — keyword-only; map_entry + fk_indexes
|
|
|
585
628
|
spec=spec,
|
|
586
629
|
map_entry=map_entry,
|
|
587
630
|
fk_indexes=indexes,
|
|
631
|
+
enum_tables=enums,
|
|
588
632
|
)
|
|
589
633
|
)
|
|
590
634
|
return out
|
|
@@ -657,6 +701,35 @@ def _resolve_inv_value(
|
|
|
657
701
|
return id_to_cd.get(raw, raw)
|
|
658
702
|
|
|
659
703
|
|
|
704
|
+
def _enum_norm(
|
|
705
|
+
raw: str,
|
|
706
|
+
*,
|
|
707
|
+
field: str,
|
|
708
|
+
field_enums: dict[str, str],
|
|
709
|
+
enum_tables: dict[str, dict[str, str]],
|
|
710
|
+
) -> str:
|
|
711
|
+
"""Map REST label → DAC code when field has an enum table (V38 M5).
|
|
712
|
+
|
|
713
|
+
Seed side usually carries labels (Adjustment, Asset, true); inventory
|
|
714
|
+
stores codes (A, A, 1). Labels in the table fold to codes; values that
|
|
715
|
+
already equal a code (or are unmapped) pass through unchanged.
|
|
716
|
+
"""
|
|
717
|
+
enum_name = field_enums.get(field)
|
|
718
|
+
if enum_name is None:
|
|
719
|
+
return raw
|
|
720
|
+
table = enum_tables.get(enum_name)
|
|
721
|
+
if not table:
|
|
722
|
+
return raw
|
|
723
|
+
if raw in table:
|
|
724
|
+
return table[raw]
|
|
725
|
+
# case-insensitive label match (seed YAML sometimes differs in case)
|
|
726
|
+
lower = raw.lower()
|
|
727
|
+
for lab, code in table.items():
|
|
728
|
+
if lab.lower() == lower:
|
|
729
|
+
return code
|
|
730
|
+
return raw
|
|
731
|
+
|
|
732
|
+
|
|
660
733
|
def _compare_seed_table( # noqa: PLR0913 — keyword-only compare bundle
|
|
661
734
|
*,
|
|
662
735
|
entity: str,
|
|
@@ -665,6 +738,7 @@ def _compare_seed_table( # noqa: PLR0913 — keyword-only compare bundle
|
|
|
665
738
|
spec: extract.EntitySpec,
|
|
666
739
|
map_entry: TableMapEntry | None = None,
|
|
667
740
|
fk_indexes: dict[str, dict[str, str]] | None = None,
|
|
741
|
+
enum_tables: dict[str, dict[str, str]] | None = None,
|
|
668
742
|
) -> list[FieldDelta]:
|
|
669
743
|
"""Field-level compare for rows that share a key tuple on both sides.
|
|
670
744
|
|
|
@@ -673,8 +747,9 @@ def _compare_seed_table( # noqa: PLR0913 — keyword-only compare bundle
|
|
|
673
747
|
→ skip that seed row (no false delta). Optional map ``keys`` / ``fields``
|
|
674
748
|
rename seed names → inv columns (V38 M3). Optional ``resolves`` + global
|
|
675
749
|
FK indexes convert inv int IDs → CD before compare (V38 M4; prefer seed
|
|
676
|
-
CD space).
|
|
677
|
-
|
|
750
|
+
CD space). Optional ``enums`` + global enum tables fold seed labels →
|
|
751
|
+
codes (V38 M5). Join keys and field values are pad-trimmed on both sides
|
|
752
|
+
(V38) so fixed-width / trailing-space DAC columns match seed natural keys.
|
|
678
753
|
Detail lists skipped (not comparable to flat snapshot rows). Findings
|
|
679
754
|
always report seed-side field names and resolved inventory values.
|
|
680
755
|
"""
|
|
@@ -682,7 +757,9 @@ def _compare_seed_table( # noqa: PLR0913 — keyword-only compare bundle
|
|
|
682
757
|
key_alias = map_entry.keys if map_entry is not None else {}
|
|
683
758
|
field_alias = map_entry.fields if map_entry is not None else {}
|
|
684
759
|
resolves = map_entry.resolves if map_entry is not None else {}
|
|
760
|
+
field_enums = map_entry.enums if map_entry is not None else {}
|
|
685
761
|
indexes = fk_indexes if fk_indexes is not None else {}
|
|
762
|
+
enums = enum_tables if enum_tables is not None else {}
|
|
686
763
|
inv_keys = [key_alias.get(k, k) for k in keys]
|
|
687
764
|
inv_index = _index_inventory_rows(table.rows, inv_keys)
|
|
688
765
|
deltas: list[FieldDelta] = []
|
|
@@ -701,12 +778,22 @@ def _compare_seed_table( # noqa: PLR0913 — keyword-only compare bundle
|
|
|
701
778
|
inv_field = field_alias.get(field, field)
|
|
702
779
|
if inv_field not in inv_row:
|
|
703
780
|
continue
|
|
704
|
-
left =
|
|
705
|
-
|
|
706
|
-
_norm(inv_row[inv_field]),
|
|
781
|
+
left = _enum_norm(
|
|
782
|
+
_norm(seed_val),
|
|
707
783
|
field=field,
|
|
708
|
-
|
|
709
|
-
|
|
784
|
+
field_enums=field_enums,
|
|
785
|
+
enum_tables=enums,
|
|
786
|
+
)
|
|
787
|
+
right = _enum_norm(
|
|
788
|
+
_resolve_inv_value(
|
|
789
|
+
_norm(inv_row[inv_field]),
|
|
790
|
+
field=field,
|
|
791
|
+
resolves=resolves,
|
|
792
|
+
fk_indexes=indexes,
|
|
793
|
+
),
|
|
794
|
+
field=field,
|
|
795
|
+
field_enums=field_enums,
|
|
796
|
+
enum_tables=enums,
|
|
710
797
|
)
|
|
711
798
|
if left != right:
|
|
712
799
|
deltas.append(
|
|
@@ -742,21 +829,32 @@ def _norm(value: Any) -> str:
|
|
|
742
829
|
|
|
743
830
|
String sides strip leading/trailing whitespace (DAC/NVarChar padding).
|
|
744
831
|
Bools/numbers fold to a stable spelling so seed and inventory agree.
|
|
745
|
-
|
|
746
|
-
|
|
832
|
+
Decimal-looking strings with a point collapse trailing zeros so seed
|
|
833
|
+
``0`` matches inventory ``0.000000`` without treating bare CDs like
|
|
834
|
+
``000000`` as numbers. Sibling of ``seed._norm`` spirit; used on *both*
|
|
835
|
+
key join and field compare so padded inventory never misses a seed row
|
|
836
|
+
or false-deltas.
|
|
747
837
|
"""
|
|
748
838
|
if isinstance(value, bool):
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
839
|
+
s = "true" if value else "false"
|
|
840
|
+
elif value is None:
|
|
841
|
+
s = ""
|
|
842
|
+
elif isinstance(value, int):
|
|
843
|
+
s = str(value)
|
|
844
|
+
elif isinstance(value, float):
|
|
755
845
|
as_int = int(value)
|
|
756
|
-
if value == float(as_int)
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
846
|
+
s = str(as_int) if value == float(as_int) else str(value)
|
|
847
|
+
else:
|
|
848
|
+
s = str(value).strip()
|
|
849
|
+
if "." in s:
|
|
850
|
+
try:
|
|
851
|
+
f = float(s)
|
|
852
|
+
except ValueError:
|
|
853
|
+
return s
|
|
854
|
+
as_int = int(f)
|
|
855
|
+
if f == float(as_int):
|
|
856
|
+
s = str(as_int)
|
|
857
|
+
return s
|
|
760
858
|
|
|
761
859
|
|
|
762
860
|
# ---------------------------------------------------------------------------
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
# table → catalog entity. Optional keys:/fields: rename seed names →
|
|
4
4
|
# inventory columns so join/compare works when DAC names differ (M3).
|
|
5
5
|
# Global resolvers: + per-row resolves: map inv int FKs → CD (M4).
|
|
6
|
+
# Global enums: + per-row enums: map REST labels → DAC codes (M5).
|
|
6
7
|
# Data-repo root snapshot_map.yaml wins over this package file.
|
|
7
|
-
# v1 rows remain {table, entity} only — aliases/resolves optional.
|
|
8
|
+
# v1 rows remain {table, entity} only — aliases/resolves/enums optional.
|
|
8
9
|
|
|
9
10
|
resolvers:
|
|
10
11
|
account_cd:
|
|
@@ -15,6 +16,37 @@ resolvers:
|
|
|
15
16
|
table: Sub
|
|
16
17
|
id: SubID
|
|
17
18
|
cd: SubCD
|
|
19
|
+
branch_cd:
|
|
20
|
+
table: Branch
|
|
21
|
+
id: BranchID
|
|
22
|
+
cd: BranchCD
|
|
23
|
+
|
|
24
|
+
enums:
|
|
25
|
+
reason_usage:
|
|
26
|
+
Adjustment: A
|
|
27
|
+
Assembly/Disassembly: D
|
|
28
|
+
Issue: I
|
|
29
|
+
Receipt: R
|
|
30
|
+
Vendor Return: N
|
|
31
|
+
account_type:
|
|
32
|
+
Asset: A
|
|
33
|
+
Liability: L
|
|
34
|
+
Income: I
|
|
35
|
+
Expense: E
|
|
36
|
+
post_option:
|
|
37
|
+
Summary: S
|
|
38
|
+
Detail: D
|
|
39
|
+
due_type:
|
|
40
|
+
Fixed Number of Days: N
|
|
41
|
+
disc_type:
|
|
42
|
+
Fixed Number of Days: N
|
|
43
|
+
visible_to:
|
|
44
|
+
All: AL
|
|
45
|
+
prepare_on:
|
|
46
|
+
Fixed Day of Month: F
|
|
47
|
+
bool_bit:
|
|
48
|
+
"true": "1"
|
|
49
|
+
"false": "0"
|
|
18
50
|
|
|
19
51
|
tables:
|
|
20
52
|
# Wave 1 renames — key/field aliases where seed ≠ DAC column
|
|
@@ -22,8 +54,14 @@ tables:
|
|
|
22
54
|
entity: Subaccount
|
|
23
55
|
keys:
|
|
24
56
|
SubaccountCD: SubCD
|
|
57
|
+
enums:
|
|
58
|
+
Active: bool_bit
|
|
25
59
|
- table: Terms
|
|
26
60
|
entity: CreditTerms
|
|
61
|
+
enums:
|
|
62
|
+
DueType: due_type
|
|
63
|
+
DiscType: disc_type
|
|
64
|
+
VisibleTo: visible_to
|
|
27
65
|
- table: UnitOfMeasure
|
|
28
66
|
entity: UnitsOfMeasure
|
|
29
67
|
keys:
|
|
@@ -34,6 +72,10 @@ tables:
|
|
|
34
72
|
entity: AvailabilityCalculationRule
|
|
35
73
|
- table: SOSetup
|
|
36
74
|
entity: SOPreferences
|
|
75
|
+
enums:
|
|
76
|
+
AutoReleaseIN: bool_bit
|
|
77
|
+
HoldShipments: bool_bit
|
|
78
|
+
RequireShipmentTotal: bool_bit
|
|
37
79
|
- table: POSetup
|
|
38
80
|
entity: POPreferences
|
|
39
81
|
- table: ARSetup
|
|
@@ -41,13 +83,28 @@ tables:
|
|
|
41
83
|
- table: APSetup
|
|
42
84
|
entity: APPreferences
|
|
43
85
|
|
|
44
|
-
# Wave 2 — table renames
|
|
86
|
+
# Wave 2 — table renames + PostingClass FK resolve
|
|
45
87
|
- table: INPostClass
|
|
46
88
|
entity: PostingClass
|
|
89
|
+
resolves:
|
|
90
|
+
COGSAcctID: account_cd
|
|
91
|
+
COGSSubID: sub_cd
|
|
92
|
+
InvtAcctID: account_cd
|
|
93
|
+
InvtSubID: sub_cd
|
|
94
|
+
LCVarianceAcctID: account_cd
|
|
95
|
+
LCVarianceSubID: sub_cd
|
|
96
|
+
POAccrualAcctID: account_cd
|
|
97
|
+
POAccrualSubID: sub_cd
|
|
98
|
+
PPVAcctID: account_cd
|
|
99
|
+
PPVSubID: sub_cd
|
|
100
|
+
SalesAcctID: account_cd
|
|
101
|
+
SalesSubID: sub_cd
|
|
47
102
|
- table: INItemClass
|
|
48
103
|
entity: ItemClass
|
|
49
104
|
- table: INSite
|
|
50
105
|
entity: Warehouse
|
|
106
|
+
enums:
|
|
107
|
+
Active: bool_bit
|
|
51
108
|
- table: InventoryItem
|
|
52
109
|
entity: StockItem
|
|
53
110
|
- table: INSetup
|
|
@@ -62,17 +119,33 @@ tables:
|
|
|
62
119
|
entity: KitSpecification
|
|
63
120
|
- table: SOOrderType
|
|
64
121
|
entity: OrderType
|
|
122
|
+
resolves:
|
|
123
|
+
FreightAcctID: account_cd
|
|
124
|
+
FreightSubID: sub_cd
|
|
125
|
+
enums:
|
|
126
|
+
Active: bool_bit
|
|
65
127
|
- table: ARStatementCycle
|
|
66
128
|
entity: StatementCycle
|
|
129
|
+
enums:
|
|
130
|
+
PrepareOn: prepare_on
|
|
67
131
|
- table: OrganizationLedgerLink
|
|
68
132
|
entity: LedgerCompany
|
|
69
133
|
|
|
70
|
-
# Identity tables
|
|
134
|
+
# Identity tables — FK resolve (M4) + enums (M5)
|
|
135
|
+
- table: Account
|
|
136
|
+
entity: Account
|
|
137
|
+
enums:
|
|
138
|
+
Type: account_type
|
|
139
|
+
PostOption: post_option
|
|
140
|
+
Active: bool_bit
|
|
141
|
+
RequireUnits: bool_bit
|
|
71
142
|
- table: ReasonCode
|
|
72
143
|
entity: ReasonCode
|
|
73
144
|
resolves:
|
|
74
145
|
AccountID: account_cd
|
|
75
146
|
SubID: sub_cd
|
|
147
|
+
enums:
|
|
148
|
+
Usage: reason_usage
|
|
76
149
|
- table: VendorClass
|
|
77
150
|
entity: VendorClass
|
|
78
151
|
resolves:
|
|
@@ -84,3 +157,11 @@ tables:
|
|
|
84
157
|
ExpenseSubID: sub_cd
|
|
85
158
|
POAccrualAcctID: account_cd
|
|
86
159
|
POAccrualSubID: sub_cd
|
|
160
|
+
- table: CashAccount
|
|
161
|
+
entity: CashAccount
|
|
162
|
+
resolves:
|
|
163
|
+
AccountID: account_cd
|
|
164
|
+
SubID: sub_cd
|
|
165
|
+
BranchID: branch_cd
|
|
166
|
+
enums:
|
|
167
|
+
Active: bool_bit
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/scenario/20-buy.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/scenario/30-build.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.17.0 → acumatica_cli-0.18.0}/src/acumatica_cli/templates/scenario/40-sell.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|