acumatica-cli 0.15.4__tar.gz → 0.16.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.16.0}/PKG-INFO +26 -1
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/README.md +25 -0
- acumatica_cli-0.16.0/pyproject.toml +109 -0
- acumatica_cli-0.15.4/pyproject.toml → acumatica_cli-0.16.0/pyproject.toml.orig +1 -1
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/cli.py +128 -1
- acumatica_cli-0.16.0/src/acumatica_cli/inventory.py +548 -0
- acumatica_cli-0.16.0/src/acumatica_cli/reconcile.py +760 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/bootstrap.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/bootstrap_project.xml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/client.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/config.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/extract.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/output.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/run.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/seed.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/seed_catalog.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/state.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/target.py +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/README.md +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/baseline/50-gl-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/baseline/60-ledger-company.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/bootstrap/company.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/bootstrap/credit-terms.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/10-reason-codes.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/20-in-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/30-availability-rules.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/40-posting-classes.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/50-warehouse.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/51-warehouse-locations.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/52-warehouse-defaults.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/53-tax-categories.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/54-item-classes.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/56-so-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/57-po-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/58-order-types.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/60-ar-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/61-ap-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/62-ca-preferences.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/63-cash-account.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/64-payment-methods.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/65-statement-cycles.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/70-vendor-classes.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/71-customer-classes.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/75-vendors.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/76-customers.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/80-stock-items-parts.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/82-stock-items-kits.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/master/85-kit-specifications.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/setup/10-financial-year.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/setup/20-master-calendar.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/setup/30-open-periods.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/config/views/10-trial-balance.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/env +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/gitignore +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/scenario/10-seed-capital.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/scenario/20-buy.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/scenario/30-build.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/scenario/40-sell.yaml +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.0}/src/acumatica_cli/templates/target +0 -0
- {acumatica_cli-0.15.4 → acumatica_cli-0.16.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.16.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,28 @@ 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).
|
|
104
110
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
105
111
|
Run `acu <command> --help` for details on any command.
|
|
106
112
|
|
|
113
|
+
### Dual readers, one writer
|
|
114
|
+
|
|
115
|
+
Two read paths, one mutator (V35).
|
|
116
|
+
Do not confuse them with each other or with `state`:
|
|
117
|
+
|
|
118
|
+
| Command | Plane | Input | Writes | Role |
|
|
119
|
+
| ------- | ----- | ----- | ------ | ---- |
|
|
120
|
+
| `extract` | REST (live) | tenant via contract API | `config/{bootstrap,baseline,setup,master}/` | Inverse of `apply` — seed YAML |
|
|
121
|
+
| `inventory` | Offline | SM203520 Settings XML ZIP or `ac.exe export xml` folder | `inventory/` (`summary.yaml` + `tables/`) | Full-table snapshot IR — not seed |
|
|
122
|
+
| `reconcile` | Offline | `inventory/` + optional `config/` | `findings/` only | Cross-check gaps/deltas — never mutates seed or tenant |
|
|
123
|
+
| `state` | REST (live) | `config/views/` | `state/` | Derived balances/totals — not seed, not inventory |
|
|
124
|
+
| `apply` | REST (live) | seed YAML under `config/` | tenant | **Sole** tenant writer (keyed PUT) |
|
|
125
|
+
|
|
126
|
+
`inventory/` and `findings/` are engagement outputs: not SEED_DIRS, never loaded by `apply`/`diff`, not scaffolded by `config init`.
|
|
127
|
+
Binary `.adb` snapshots are rejected (XML only).
|
|
128
|
+
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.
|
|
129
|
+
|
|
107
130
|
## The data repo
|
|
108
131
|
|
|
109
132
|
Your configuration lives in its own git repo.
|
|
@@ -119,6 +142,8 @@ There is no `--flavor`.
|
|
|
119
142
|
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
|
|
120
143
|
| `config/views/` | observer views for `acu state` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
|
|
121
144
|
| `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
|
|
145
|
+
| `inventory/` | engagement: offline snapshot tables from `acu inventory` (not seed; not SEED_DIRS) |
|
|
146
|
+
| `findings/` | engagement: `acu reconcile` cross-check output (never apply path) |
|
|
122
147
|
| `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where) |
|
|
123
148
|
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
124
149
|
|
|
@@ -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,28 @@ 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).
|
|
86
92
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
87
93
|
Run `acu <command> --help` for details on any command.
|
|
88
94
|
|
|
95
|
+
### Dual readers, one writer
|
|
96
|
+
|
|
97
|
+
Two read paths, one mutator (V35).
|
|
98
|
+
Do not confuse them with each other or with `state`:
|
|
99
|
+
|
|
100
|
+
| Command | Plane | Input | Writes | Role |
|
|
101
|
+
| ------- | ----- | ----- | ------ | ---- |
|
|
102
|
+
| `extract` | REST (live) | tenant via contract API | `config/{bootstrap,baseline,setup,master}/` | Inverse of `apply` — seed YAML |
|
|
103
|
+
| `inventory` | Offline | SM203520 Settings XML ZIP or `ac.exe export xml` folder | `inventory/` (`summary.yaml` + `tables/`) | Full-table snapshot IR — not seed |
|
|
104
|
+
| `reconcile` | Offline | `inventory/` + optional `config/` | `findings/` only | Cross-check gaps/deltas — never mutates seed or tenant |
|
|
105
|
+
| `state` | REST (live) | `config/views/` | `state/` | Derived balances/totals — not seed, not inventory |
|
|
106
|
+
| `apply` | REST (live) | seed YAML under `config/` | tenant | **Sole** tenant writer (keyed PUT) |
|
|
107
|
+
|
|
108
|
+
`inventory/` and `findings/` are engagement outputs: not SEED_DIRS, never loaded by `apply`/`diff`, not scaffolded by `config init`.
|
|
109
|
+
Binary `.adb` snapshots are rejected (XML only).
|
|
110
|
+
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.
|
|
111
|
+
|
|
89
112
|
## The data repo
|
|
90
113
|
|
|
91
114
|
Your configuration lives in its own git repo.
|
|
@@ -101,6 +124,8 @@ There is no `--flavor`.
|
|
|
101
124
|
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
|
|
102
125
|
| `config/views/` | observer views for `acu state` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
|
|
103
126
|
| `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
|
|
127
|
+
| `inventory/` | engagement: offline snapshot tables from `acu inventory` (not seed; not SEED_DIRS) |
|
|
128
|
+
| `findings/` | engagement: `acu reconcile` cross-check output (never apply path) |
|
|
104
129
|
| `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where) |
|
|
105
130
|
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
106
131
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "acumatica-cli"
|
|
3
|
+
version = "0.16.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.16.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,123 @@ 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.
|
|
1107
|
+
"""
|
|
1108
|
+
inv = (
|
|
1109
|
+
inventory_dir
|
|
1110
|
+
if inventory_dir is not None
|
|
1111
|
+
else Path(reconcile.DEFAULT_INVENTORY)
|
|
1112
|
+
)
|
|
1113
|
+
if config_dir is not None:
|
|
1114
|
+
cfg: Path | None = config_dir
|
|
1115
|
+
else:
|
|
1116
|
+
default_cfg = Path(reconcile.DEFAULT_CONFIG)
|
|
1117
|
+
cfg = default_cfg if default_cfg.is_dir() else None
|
|
1118
|
+
dest = out_dir if out_dir is not None else Path(reconcile.DEFAULT_OUT)
|
|
1119
|
+
cfg_label = str(cfg) if cfg is not None else "(none)"
|
|
1120
|
+
output.data(f"{inv} + {cfg_label} -> {dest}")
|
|
1121
|
+
reconcile.run(
|
|
1122
|
+
inventory_dir=inv,
|
|
1123
|
+
config_dir=cfg,
|
|
1124
|
+
out_dir=dest,
|
|
1125
|
+
force=force,
|
|
1126
|
+
dry_run=dry_run,
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
|
|
1003
1130
|
@cli.command("state")
|
|
1004
1131
|
@click.argument(
|
|
1005
1132
|
"files", nargs=-1, required=False, type=click.Path(exists=True, path_type=Path)
|