acumatica-cli 0.15.4__tar.gz → 0.17.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.15.4 → acumatica_cli-0.17.0}/PKG-INFO +28 -1
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/README.md +27 -0
- acumatica_cli-0.17.0/pyproject.toml +109 -0
- acumatica_cli-0.15.4/pyproject.toml → acumatica_cli-0.17.0/pyproject.toml.orig +1 -1
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/cli.py +131 -1
- acumatica_cli-0.17.0/src/acumatica_cli/inventory.py +548 -0
- acumatica_cli-0.17.0/src/acumatica_cli/reconcile.py +910 -0
- acumatica_cli-0.17.0/src/acumatica_cli/snapshot_map.yaml +86 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/bootstrap.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/bootstrap_project.xml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/client.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/config.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/extract.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/output.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/run.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/seed.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/seed_catalog.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/state.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/target.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/README.md +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/baseline/50-gl-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/baseline/60-ledger-company.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/bootstrap/company.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/bootstrap/credit-terms.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/10-reason-codes.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/20-in-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/30-availability-rules.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/40-posting-classes.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/50-warehouse.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/51-warehouse-locations.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/52-warehouse-defaults.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/53-tax-categories.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/54-item-classes.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/56-so-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/57-po-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/58-order-types.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/60-ar-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/61-ap-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/62-ca-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/63-cash-account.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/64-payment-methods.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/65-statement-cycles.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/70-vendor-classes.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/71-customer-classes.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/75-vendors.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/76-customers.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/80-stock-items-parts.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/82-stock-items-kits.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/master/85-kit-specifications.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/setup/10-financial-year.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/setup/20-master-calendar.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/setup/30-open-periods.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/config/views/10-trial-balance.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/env +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/gitignore +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/scenario/10-seed-capital.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/scenario/20-buy.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/scenario/30-build.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/scenario/40-sell.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.0}/src/acumatica_cli/templates/target +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.17.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.17.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>
|
|
@@ -88,6 +88,10 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
88
88
|
│ capture derived state into state/ (not seed)
|
|
89
89
|
├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
|
|
90
90
|
│ inverse of apply into config/{bootstrap,baseline,setup,master}/
|
|
91
|
+
├── inventory [--out DIR] [--force] [--dry-run] ARTIFACT
|
|
92
|
+
│ offline snapshot artifact → inventory/ (not seed)
|
|
93
|
+
├── reconcile [--inventory DIR] [--config DIR] [--out DIR] [--force] [--dry-run]
|
|
94
|
+
│ inventory/ + optional config/ → findings/ only
|
|
91
95
|
├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
|
|
92
96
|
│
|
|
93
97
|
└── config configuration ops
|
|
@@ -101,9 +105,30 @@ A path like `config/` expands nested seed dirs in that fixed order.
|
|
|
101
105
|
`run` without FILES defaults to `scenario/`.
|
|
102
106
|
`state` without FILES defaults to `config/views/`; writes go to `state/` (`--out`).
|
|
103
107
|
`extract` always writes under `config/{bootstrap,baseline,setup,master}/` (catalog-driven; never root SEED_DIRS).
|
|
108
|
+
`inventory` is offline (no REST/SSH/password): SM203520 Settings XML ZIP or `ac.exe export xml` folder writes to `inventory/`.
|
|
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).
|
|
111
|
+
See [docs/demo-seed.md](docs/demo-seed.md).
|
|
104
112
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
105
113
|
Run `acu <command> --help` for details on any command.
|
|
106
114
|
|
|
115
|
+
### Dual readers, one writer
|
|
116
|
+
|
|
117
|
+
Two read paths, one mutator (V35).
|
|
118
|
+
Do not confuse them with each other or with `state`:
|
|
119
|
+
|
|
120
|
+
| Command | Plane | Input | Writes | Role |
|
|
121
|
+
| ------- | ----- | ----- | ------ | ---- |
|
|
122
|
+
| `extract` | REST (live) | tenant via contract API | `config/{bootstrap,baseline,setup,master}/` | Inverse of `apply` — seed YAML |
|
|
123
|
+
| `inventory` | Offline | SM203520 Settings XML ZIP or `ac.exe export xml` folder | `inventory/` (`summary.yaml` + `tables/`) | Full-table snapshot IR — not seed |
|
|
124
|
+
| `reconcile` | Offline | `inventory/` + optional `config/` | `findings/` only | Cross-check gaps/deltas — never mutates seed or tenant |
|
|
125
|
+
| `state` | REST (live) | `config/views/` | `state/` | Derived balances/totals — not seed, not inventory |
|
|
126
|
+
| `apply` | REST (live) | seed YAML under `config/` | tenant | **Sole** tenant writer (keyed PUT) |
|
|
127
|
+
|
|
128
|
+
`inventory/` and `findings/` are engagement outputs: not SEED_DIRS, never loaded by `apply`/`diff`, not scaffolded by `config init`.
|
|
129
|
+
Binary `.adb` snapshots are rejected (XML only).
|
|
130
|
+
See [docs/ac-exe.md](docs/ac-exe.md) for export / SM203520 notes and [docs/demo-seed.md](docs/demo-seed.md) for the extract/state/inventory map.
|
|
131
|
+
|
|
107
132
|
## The data repo
|
|
108
133
|
|
|
109
134
|
Your configuration lives in its own git repo.
|
|
@@ -119,6 +144,8 @@ There is no `--flavor`.
|
|
|
119
144
|
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
|
|
120
145
|
| `config/views/` | observer views for `acu state` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
|
|
121
146
|
| `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
|
|
147
|
+
| `inventory/` | engagement: offline snapshot tables from `acu inventory` (not seed; not SEED_DIRS) |
|
|
148
|
+
| `findings/` | engagement: `acu reconcile` cross-check output (never apply path) |
|
|
122
149
|
| `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where) |
|
|
123
150
|
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
124
151
|
|
|
@@ -70,6 +70,10 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
70
70
|
│ capture derived state into state/ (not seed)
|
|
71
71
|
├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
|
|
72
72
|
│ inverse of apply into config/{bootstrap,baseline,setup,master}/
|
|
73
|
+
├── inventory [--out DIR] [--force] [--dry-run] ARTIFACT
|
|
74
|
+
│ offline snapshot artifact → inventory/ (not seed)
|
|
75
|
+
├── reconcile [--inventory DIR] [--config DIR] [--out DIR] [--force] [--dry-run]
|
|
76
|
+
│ inventory/ + optional config/ → findings/ only
|
|
73
77
|
├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
|
|
74
78
|
│
|
|
75
79
|
└── config configuration ops
|
|
@@ -83,9 +87,30 @@ A path like `config/` expands nested seed dirs in that fixed order.
|
|
|
83
87
|
`run` without FILES defaults to `scenario/`.
|
|
84
88
|
`state` without FILES defaults to `config/views/`; writes go to `state/` (`--out`).
|
|
85
89
|
`extract` always writes under `config/{bootstrap,baseline,setup,master}/` (catalog-driven; never root SEED_DIRS).
|
|
90
|
+
`inventory` is offline (no REST/SSH/password): SM203520 Settings XML ZIP or `ac.exe export xml` folder writes to `inventory/`.
|
|
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).
|
|
93
|
+
See [docs/demo-seed.md](docs/demo-seed.md).
|
|
86
94
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
87
95
|
Run `acu <command> --help` for details on any command.
|
|
88
96
|
|
|
97
|
+
### Dual readers, one writer
|
|
98
|
+
|
|
99
|
+
Two read paths, one mutator (V35).
|
|
100
|
+
Do not confuse them with each other or with `state`:
|
|
101
|
+
|
|
102
|
+
| Command | Plane | Input | Writes | Role |
|
|
103
|
+
| ------- | ----- | ----- | ------ | ---- |
|
|
104
|
+
| `extract` | REST (live) | tenant via contract API | `config/{bootstrap,baseline,setup,master}/` | Inverse of `apply` — seed YAML |
|
|
105
|
+
| `inventory` | Offline | SM203520 Settings XML ZIP or `ac.exe export xml` folder | `inventory/` (`summary.yaml` + `tables/`) | Full-table snapshot IR — not seed |
|
|
106
|
+
| `reconcile` | Offline | `inventory/` + optional `config/` | `findings/` only | Cross-check gaps/deltas — never mutates seed or tenant |
|
|
107
|
+
| `state` | REST (live) | `config/views/` | `state/` | Derived balances/totals — not seed, not inventory |
|
|
108
|
+
| `apply` | REST (live) | seed YAML under `config/` | tenant | **Sole** tenant writer (keyed PUT) |
|
|
109
|
+
|
|
110
|
+
`inventory/` and `findings/` are engagement outputs: not SEED_DIRS, never loaded by `apply`/`diff`, not scaffolded by `config init`.
|
|
111
|
+
Binary `.adb` snapshots are rejected (XML only).
|
|
112
|
+
See [docs/ac-exe.md](docs/ac-exe.md) for export / SM203520 notes and [docs/demo-seed.md](docs/demo-seed.md) for the extract/state/inventory map.
|
|
113
|
+
|
|
89
114
|
## The data repo
|
|
90
115
|
|
|
91
116
|
Your configuration lives in its own git repo.
|
|
@@ -101,6 +126,8 @@ There is no `--flavor`.
|
|
|
101
126
|
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
|
|
102
127
|
| `config/views/` | observer views for `acu state` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
|
|
103
128
|
| `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
|
|
129
|
+
| `inventory/` | engagement: offline snapshot tables from `acu inventory` (not seed; not SEED_DIRS) |
|
|
130
|
+
| `findings/` | engagement: `acu reconcile` cross-check output (never apply path) |
|
|
104
131
|
| `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where) |
|
|
105
132
|
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
106
133
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "acumatica-cli"
|
|
3
|
+
version = "0.17.0"
|
|
4
|
+
description = "Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data"
|
|
5
|
+
license = "PolyForm-Noncommercial-1.0.0"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.14"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"click>=8.1",
|
|
10
|
+
"httpx>=0.27",
|
|
11
|
+
"pydantic>=2.7",
|
|
12
|
+
"pydantic-settings>=2.5",
|
|
13
|
+
"pyyaml>=6.0",
|
|
14
|
+
"rich>=13",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[[project.authors]]
|
|
18
|
+
name = "Konstantin Borovik"
|
|
19
|
+
email = "kb@lab5.ca"
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://lab5.ca"
|
|
23
|
+
Repository = "https://github.com/kborovik/acumatica-cli"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
acu = "acumatica_cli.cli:main"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["uv_build>=0.11,<0.12"]
|
|
30
|
+
build-backend = "uv_build"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"basedpyright>=1.37.0",
|
|
35
|
+
"pytest>=8",
|
|
36
|
+
"ruff>=0.13.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
addopts = '-q -m "not e2e"'
|
|
42
|
+
markers = ["e2e: live end-to-end tests against a real Acumatica instance (make e2e)"]
|
|
43
|
+
|
|
44
|
+
[tool.basedpyright]
|
|
45
|
+
typeCheckingMode = "strict"
|
|
46
|
+
pythonVersion = "3.14"
|
|
47
|
+
include = [
|
|
48
|
+
"src/acumatica_cli/",
|
|
49
|
+
"tests/",
|
|
50
|
+
]
|
|
51
|
+
reportMissingTypeStubs = false
|
|
52
|
+
reportUnknownVariableType = false
|
|
53
|
+
reportUnknownMemberType = false
|
|
54
|
+
reportUnknownArgumentType = false
|
|
55
|
+
reportUnknownParameterType = false
|
|
56
|
+
reportUnknownLambdaType = false
|
|
57
|
+
reportUnusedFunction = "warning"
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
target-version = "py314"
|
|
61
|
+
src = ["src"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.format]
|
|
64
|
+
docstring-code-format = true
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
fixable = ["ALL"]
|
|
68
|
+
select = [
|
|
69
|
+
"B",
|
|
70
|
+
"C4",
|
|
71
|
+
"C90",
|
|
72
|
+
"COM",
|
|
73
|
+
"D",
|
|
74
|
+
"E",
|
|
75
|
+
"F",
|
|
76
|
+
"I",
|
|
77
|
+
"N",
|
|
78
|
+
"PL",
|
|
79
|
+
"PT",
|
|
80
|
+
"RUF",
|
|
81
|
+
"SIM",
|
|
82
|
+
"T20",
|
|
83
|
+
"TID",
|
|
84
|
+
"UP",
|
|
85
|
+
"W",
|
|
86
|
+
]
|
|
87
|
+
ignore = [
|
|
88
|
+
"PLR2004",
|
|
89
|
+
"PLC0415",
|
|
90
|
+
"D100",
|
|
91
|
+
"D104",
|
|
92
|
+
"D105",
|
|
93
|
+
"D107",
|
|
94
|
+
"COM812",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.pydocstyle]
|
|
98
|
+
convention = "google"
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint.isort]
|
|
101
|
+
known-first-party = ["acumatica_cli"]
|
|
102
|
+
|
|
103
|
+
[tool.ruff.lint.per-file-ignores]
|
|
104
|
+
"src/acumatica_cli/cli.py" = ["PLR0913"]
|
|
105
|
+
"tests/*" = [
|
|
106
|
+
"D101",
|
|
107
|
+
"D102",
|
|
108
|
+
"D103",
|
|
109
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "acumatica-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.17.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"
|
|
@@ -12,7 +12,17 @@ import click
|
|
|
12
12
|
import httpx
|
|
13
13
|
from click.shell_completion import get_completion_class
|
|
14
14
|
|
|
15
|
-
from . import
|
|
15
|
+
from . import (
|
|
16
|
+
bootstrap,
|
|
17
|
+
extract,
|
|
18
|
+
firstlogin,
|
|
19
|
+
inventory,
|
|
20
|
+
output,
|
|
21
|
+
reconcile,
|
|
22
|
+
run,
|
|
23
|
+
seed,
|
|
24
|
+
state,
|
|
25
|
+
)
|
|
16
26
|
from .client import AcumaticaClient
|
|
17
27
|
from .config import (
|
|
18
28
|
Instance,
|
|
@@ -1000,6 +1010,126 @@ def _exit_on_drift(inst: Instance, drifts: list[str], files: int) -> None:
|
|
|
1000
1010
|
output.success(f"no drift on {inst.tenant} ({inst.base_url}, {files} file(s))")
|
|
1001
1011
|
|
|
1002
1012
|
|
|
1013
|
+
@cli.command("inventory")
|
|
1014
|
+
@click.argument(
|
|
1015
|
+
"artifact",
|
|
1016
|
+
type=click.Path(exists=True, path_type=Path),
|
|
1017
|
+
)
|
|
1018
|
+
@click.option(
|
|
1019
|
+
"--out",
|
|
1020
|
+
"out_dir",
|
|
1021
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
1022
|
+
default=None,
|
|
1023
|
+
help="Output directory (default: inventory/)",
|
|
1024
|
+
)
|
|
1025
|
+
@click.option("--force", is_flag=True, help="Overwrite existing files")
|
|
1026
|
+
@click.option(
|
|
1027
|
+
"--dry-run",
|
|
1028
|
+
is_flag=True,
|
|
1029
|
+
help="Show what would be written without writing",
|
|
1030
|
+
)
|
|
1031
|
+
def inventory_cmd(
|
|
1032
|
+
artifact: Path,
|
|
1033
|
+
out_dir: Path | None,
|
|
1034
|
+
force: bool,
|
|
1035
|
+
dry_run: bool,
|
|
1036
|
+
) -> None:
|
|
1037
|
+
"""Parse a snapshot artifact into inventory/ (offline dual-reader).
|
|
1038
|
+
|
|
1039
|
+
ARTIFACT is an SM203520 Settings XML ZIP (manifest.xml + table XML) or
|
|
1040
|
+
an ac.exe export xml folder. Both normalize to one IR (V37). Binary
|
|
1041
|
+
.adb is rejected. No REST, no SSH, no password. Writes summary.yaml
|
|
1042
|
+
and tables/<Table>.yaml under --out (default inventory/). Existing
|
|
1043
|
+
files are skipped unless --force. When data-repo target.yaml is
|
|
1044
|
+
present and the artifact reports a build, erp must match or the run
|
|
1045
|
+
fails. Exit 0 clean, 1 parse/format/version fail; never 2 (drift is
|
|
1046
|
+
not this command). Not extract (REST seed into config/), not state
|
|
1047
|
+
(derived balances) — never writes config/ or state/ (V35).
|
|
1048
|
+
"""
|
|
1049
|
+
art = inventory.parse_artifact(artifact)
|
|
1050
|
+
target = load_target()
|
|
1051
|
+
if target is not None:
|
|
1052
|
+
inventory.assert_erp_matches(art, target.erp)
|
|
1053
|
+
dest = out_dir if out_dir is not None else Path(inventory.DEFAULT_OUT)
|
|
1054
|
+
output.data(
|
|
1055
|
+
f"{artifact} -> {dest} ({len(art.tables)} table(s)"
|
|
1056
|
+
f"{f', erp={art.erp}' if art.erp else ''})"
|
|
1057
|
+
)
|
|
1058
|
+
inventory.emit(art, dest, force=force, dry_run=dry_run)
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
@cli.command("reconcile")
|
|
1062
|
+
@click.option(
|
|
1063
|
+
"--inventory",
|
|
1064
|
+
"inventory_dir",
|
|
1065
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
1066
|
+
default=None,
|
|
1067
|
+
help="Inventory tree directory (default: inventory/)",
|
|
1068
|
+
)
|
|
1069
|
+
@click.option(
|
|
1070
|
+
"--config",
|
|
1071
|
+
"config_dir",
|
|
1072
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
1073
|
+
default=None,
|
|
1074
|
+
help="Optional config/ seed tree (default: config/ when present)",
|
|
1075
|
+
)
|
|
1076
|
+
@click.option(
|
|
1077
|
+
"--out",
|
|
1078
|
+
"out_dir",
|
|
1079
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
1080
|
+
default=None,
|
|
1081
|
+
help="Findings output directory (default: findings/)",
|
|
1082
|
+
)
|
|
1083
|
+
@click.option("--force", is_flag=True, help="Overwrite existing findings files")
|
|
1084
|
+
@click.option(
|
|
1085
|
+
"--dry-run",
|
|
1086
|
+
is_flag=True,
|
|
1087
|
+
help="Show what would be written without writing",
|
|
1088
|
+
)
|
|
1089
|
+
def reconcile_cmd(
|
|
1090
|
+
inventory_dir: Path | None,
|
|
1091
|
+
config_dir: Path | None,
|
|
1092
|
+
out_dir: Path | None,
|
|
1093
|
+
force: bool,
|
|
1094
|
+
dry_run: bool,
|
|
1095
|
+
) -> None:
|
|
1096
|
+
"""Compare inventory/ to optional config/ and write findings/ (offline).
|
|
1097
|
+
|
|
1098
|
+
Dual-reader cross-check (V35/V36): reads an inventory tree from
|
|
1099
|
+
`acu inventory` plus an optional prior-extract config/ seed tree;
|
|
1100
|
+
emits FindingsBundle under --out (default findings/). Never writes
|
|
1101
|
+
config/ (not extract promote) and never captures state/ — unmapped
|
|
1102
|
+
tables, REST gaps, rest-vs-snapshot field deltas, and Usr* custom
|
|
1103
|
+
columns land in findings files only. No REST, no SSH, no password.
|
|
1104
|
+
Exit 0 clean, 1 IO/parse fail; never 2 (conflicts are findings, not
|
|
1105
|
+
drift). Optional snapshot_map.yaml maps DAC tables to catalog
|
|
1106
|
+
entities (absent → identity match on entity name) and may declare
|
|
1107
|
+
keys:/fields: seed→inv aliases plus resolvers:/resolves: for FK
|
|
1108
|
+
int→CD compare (package defaults cover Sub, UnitOfMeasure,
|
|
1109
|
+
ReasonCode/VendorClass Account+Sub). Compare always pad-trims strings.
|
|
1110
|
+
"""
|
|
1111
|
+
inv = (
|
|
1112
|
+
inventory_dir
|
|
1113
|
+
if inventory_dir is not None
|
|
1114
|
+
else Path(reconcile.DEFAULT_INVENTORY)
|
|
1115
|
+
)
|
|
1116
|
+
if config_dir is not None:
|
|
1117
|
+
cfg: Path | None = config_dir
|
|
1118
|
+
else:
|
|
1119
|
+
default_cfg = Path(reconcile.DEFAULT_CONFIG)
|
|
1120
|
+
cfg = default_cfg if default_cfg.is_dir() else None
|
|
1121
|
+
dest = out_dir if out_dir is not None else Path(reconcile.DEFAULT_OUT)
|
|
1122
|
+
cfg_label = str(cfg) if cfg is not None else "(none)"
|
|
1123
|
+
output.data(f"{inv} + {cfg_label} -> {dest}")
|
|
1124
|
+
reconcile.run(
|
|
1125
|
+
inventory_dir=inv,
|
|
1126
|
+
config_dir=cfg,
|
|
1127
|
+
out_dir=dest,
|
|
1128
|
+
force=force,
|
|
1129
|
+
dry_run=dry_run,
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
|
|
1003
1133
|
@cli.command("state")
|
|
1004
1134
|
@click.argument(
|
|
1005
1135
|
"files", nargs=-1, required=False, type=click.Path(exists=True, path_type=Path)
|