acumatica-cli 0.12.1__tar.gz → 0.13.1__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.12.1 → acumatica_cli-0.13.1}/PKG-INFO +36 -29
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/README.md +35 -28
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/pyproject.toml +1 -1
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/cli.py +91 -28
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/client.py +24 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/config.py +66 -152
- acumatica_cli-0.13.1/src/acumatica_cli/snapshot.py +573 -0
- acumatica_cli-0.13.1/src/acumatica_cli/templates/README.md +49 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/baseline/20-accounts.yaml +2 -2
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/baseline/90-uoms.yaml +9 -0
- acumatica_cli-0.13.1/src/acumatica_cli/templates/config/baseline/91-company-packaging.yaml +14 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/bootstrap/company.yaml +1 -1
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/bootstrap/features.yaml +1 -1
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/51-warehouse-locations.yaml +8 -6
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/53-tax-categories.yaml +1 -1
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/75-vendors.yaml +1 -1
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/80-stock-items-parts.yaml +22 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/82-stock-items-kits.yaml +10 -0
- acumatica_cli-0.13.1/src/acumatica_cli/templates/config/snapshot/10-trial-balance.yaml +20 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates}/scenario/10-seed-capital.yaml +2 -2
- acumatica_cli-0.13.1/src/acumatica_cli/templates/scenario/20-buy.yaml +361 -0
- acumatica_cli-0.13.1/src/acumatica_cli/templates/scenario/30-build.yaml +136 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates}/scenario/40-sell.yaml +1 -1
- acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution/README.md +0 -59
- acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution/scenario/20-buy-gateways.yaml +0 -112
- acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution/scenario/30-build.yaml +0 -5
- acumatica_cli-0.12.1/src/acumatica_cli/templates/finance/baseline/20-accounts.yaml +0 -94
- acumatica_cli-0.12.1/src/acumatica_cli/templates/finance/baseline/60-ledger-company.yaml +0 -27
- acumatica_cli-0.12.1/src/acumatica_cli/templates/finance/baseline/90-uoms.yaml +0 -16
- acumatica_cli-0.12.1/src/acumatica_cli/templates/finance/bootstrap/company.yaml +0 -15
- acumatica_cli-0.12.1/src/acumatica_cli/templates/finance/bootstrap/features.yaml +0 -16
- acumatica_cli-0.12.1/src/acumatica_cli/templates/finance/setup/30-open-periods.yaml +0 -33
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/bootstrap.py +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/bootstrap_project.xml +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/extract.py +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/extract_manifest.yaml +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/output.py +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/run.py +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/seed.py +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/src/acumatica_cli/target.py +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/finance → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/finance → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/finance → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/baseline/50-gl-preferences.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/baseline/60-ledger-company.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/finance → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/bootstrap/credit-terms.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/10-reason-codes.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/20-in-preferences.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/30-availability-rules.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/40-posting-classes.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/50-warehouse.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/52-warehouse-defaults.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/54-item-classes.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/56-so-preferences.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/57-po-preferences.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/58-order-types.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/60-ar-preferences.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/61-ap-preferences.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/62-ca-preferences.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/63-cash-account.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/64-payment-methods.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/65-statement-cycles.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/70-vendor-classes.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/71-customer-classes.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/76-customers.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/master/85-kit-specifications.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/finance → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/setup/10-financial-year.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/finance → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/setup/20-master-calendar.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/distribution → acumatica_cli-0.13.1/src/acumatica_cli/templates/config}/setup/30-open-periods.yaml +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/finance → acumatica_cli-0.13.1/src/acumatica_cli/templates}/env +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/finance → acumatica_cli-0.13.1/src/acumatica_cli/templates}/gitignore +0 -0
- {acumatica_cli-0.12.1/src/acumatica_cli/templates/finance → acumatica_cli-0.13.1/src/acumatica_cli/templates}/target +0 -0
- {acumatica_cli-0.12.1 → acumatica_cli-0.13.1}/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.13.1
|
|
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>
|
|
@@ -38,27 +38,23 @@ Acumatica configuration normally lives in the web UI: wizards, screens, and manu
|
|
|
38
38
|
uv tool install acumatica-cli
|
|
39
39
|
|
|
40
40
|
acu config init --host erp.example.com my-erp
|
|
41
|
-
cd my-erp # edit .env: set ACU_PASSWORD
|
|
41
|
+
cd my-erp # edit .env: set ACU_PASSWORD, ACU_TENANT
|
|
42
42
|
# keep ACU_API_VERSION in sync with target.yaml
|
|
43
|
+
# start from a brand-new empty tenant
|
|
43
44
|
|
|
44
45
|
acu config check # read-only preflight (incl. target.yaml)
|
|
45
46
|
acu tenant create --id 3 --login DEV # create the tenant + bootstrap it (needs SSH)
|
|
46
|
-
acu --tenant DEV
|
|
47
|
-
acu --tenant DEV
|
|
47
|
+
# or hosted: acu --tenant DEV bootstrap
|
|
48
|
+
acu --tenant DEV apply config/ # seed config/{bootstrap,baseline,setup,master}/
|
|
49
|
+
acu --tenant DEV run scenario/ # once capital → buy → build → sell
|
|
50
|
+
acu --tenant DEV diff config/ # prove zero drift (exit 2 on drift)
|
|
51
|
+
acu --tenant DEV snapshot # capture state/ trial-balance
|
|
52
|
+
# warm: capital once-skips; Owner Capital stays 50000 (not 100000)
|
|
53
|
+
acu --tenant DEV run scenario/
|
|
48
54
|
```
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```sh
|
|
53
|
-
acu config init --flavor distribution --host erp.example.com my-dist
|
|
54
|
-
cd my-dist # edit .env; start from an empty tenant
|
|
55
|
-
acu config check && acu bootstrap \
|
|
56
|
-
&& acu apply config/ && acu run scenario/ && acu diff config/
|
|
57
|
-
# warm re-run: capital once-skips; Owner Capital stays 50000 (not 100000)
|
|
58
|
-
acu run scenario/
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
See [docs/distribution.md](docs/distribution.md) for the entity map, once-guard, and apply-order notes.
|
|
56
|
+
Bare `apply` / `diff` (no path args) also prefer `config/` when those trees exist.
|
|
57
|
+
See [docs/demo-seed.md](docs/demo-seed.md) for the entity map, once-guard, and apply-order notes.
|
|
62
58
|
|
|
63
59
|
**Hosted Acumatica (no SSH):** the tenant already exists; leave `ACU_SSH` blank.
|
|
64
60
|
|
|
@@ -67,8 +63,8 @@ acu config init --host customer.acumatica.com my-erp
|
|
|
67
63
|
cd my-erp # edit .env: ACU_TENANT, ACU_PASSWORD; ACU_SSH=
|
|
68
64
|
acu config check # REST preflight; ssh probe is skipped
|
|
69
65
|
acu --tenant DEV bootstrap # publish AcuBootstrap via REST only
|
|
70
|
-
acu --tenant DEV apply
|
|
71
|
-
acu --tenant DEV diff
|
|
66
|
+
acu --tenant DEV apply config/
|
|
67
|
+
acu --tenant DEV diff config/
|
|
72
68
|
# offline UI fallback when REST publish is blocked:
|
|
73
69
|
acu bootstrap --export AcuBootstrap.zip # import + publish on SM204505
|
|
74
70
|
```
|
|
@@ -89,13 +85,14 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
89
85
|
├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
|
|
90
86
|
├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
|
|
91
87
|
├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
|
|
88
|
+
├── snapshot [--out DIR] [--diff] [--assert-unchanged] [--dry-run] [FILES...]
|
|
89
|
+
│ capture derived state into state/ (not seed)
|
|
92
90
|
├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
|
|
93
91
|
│ dump live tenant state as seed YAML (inverse of apply)
|
|
94
92
|
├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
|
|
95
93
|
│
|
|
96
94
|
└── config configuration ops
|
|
97
|
-
├── init [--host HOST] [
|
|
98
|
-
│ scaffold a data repo (.env, target.yaml, example YAML)
|
|
95
|
+
├── init [--host HOST] [DIR] scaffold full data repo (config/, scenario/, target.yaml)
|
|
99
96
|
├── show print the resolved config as a complete .env
|
|
100
97
|
└── check [--strict] preflight: discovery, secrets, target, REST, endpoints, SSH
|
|
101
98
|
```
|
|
@@ -103,25 +100,29 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
103
100
|
`apply` and `diff` without FILES prefer `config/<name>/` when any seed child exists under `config/`; otherwise root `bootstrap/`, `baseline/`, `setup/`, then `master/` when present.
|
|
104
101
|
A path like `config/` expands nested seed dirs in that fixed order.
|
|
105
102
|
`run` without FILES defaults to `scenario/`.
|
|
103
|
+
`snapshot` without FILES defaults to `config/snapshot/`; writes go to `state/` (`--out`).
|
|
106
104
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
107
105
|
Run `acu <command> --help` for details on any command.
|
|
108
106
|
|
|
109
107
|
## The data repo
|
|
110
108
|
|
|
111
109
|
Your configuration lives in its own git repo.
|
|
112
|
-
`acu config init` scaffolds
|
|
113
|
-
`acu config init --flavor distribution` scaffolds the full demo under `config/` (same contract identity, expanded COA, masters) plus lifecycle `scenario/` and README.
|
|
110
|
+
`acu config init` scaffolds a **single full seed** under `config/` (Bootstrap `project.xml` at `Bootstrap/1.0.0`, expanded COA, masters) plus lifecycle `scenario/`, observer `config/snapshot/`, and README. There is no `--flavor`.
|
|
114
111
|
|
|
115
112
|
| Path | What it holds |
|
|
116
113
|
| ---- | ------------- |
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `config/master/` | distribution masters (prefs, warehouse, items, parties)
|
|
121
|
-
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build
|
|
114
|
+
| `config/bootstrap/` | virgin-tenant config: features, company, credit terms, `project.xml` |
|
|
115
|
+
| `config/baseline/` | reference data: subaccounts, COA, ledger, UOMs, packaging |
|
|
116
|
+
| `config/setup/` | one-time actions: financial year, master calendar, open periods |
|
|
117
|
+
| `config/master/` | inventory/distribution masters (prefs, warehouse, items, parties) |
|
|
118
|
+
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
|
|
119
|
+
| `config/snapshot/` | observer views for `acu snapshot` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
|
|
120
|
+
| `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
|
|
122
121
|
| `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where) |
|
|
123
122
|
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
124
123
|
|
|
124
|
+
Legacy data repos may still keep root `bootstrap/`…`master/`; bare `apply`/`diff` prefer `config/` when present and never merge both trees.
|
|
125
|
+
|
|
125
126
|
Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order.
|
|
126
127
|
The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
|
|
127
128
|
Commit `target.yaml` with the seeds so every clone knows the verified ERP line and Default API generation.
|
|
@@ -131,6 +132,10 @@ Scenario YAML is different — it describes transactions that flow forward.
|
|
|
131
132
|
`acu run` executes each step in order (`put`, `action`, `wait`, `get`), captures server-assigned document numbers into `${var}` references for later steps, and checks `expect:` assertions as deltas against a pre-run snapshot, so additive scenarios re-run safely on a warm tenant.
|
|
132
133
|
`once: true` scenarios declare a `present` inquire-absolute gate; when the probe already holds, the CLI prints `skip <path> (once: already present)` and runs neither steps nor expects (Owner Capital does not restack).
|
|
133
134
|
|
|
135
|
+
`acu snapshot` is the third observation path: it captures live derived state (balances) into `state/` for git review. It is not `extract` (config seed) and not `diff` (desired vs actual config). Packaged golden is trial-balance via contract `inquire:` (`EndingBalance` fixed-point); inventory-summary is not golden this pass. `gi:` stays optional when a GI is V12-verified and **Expose via OData** is on (`params` fail-closed vs `$metadata`). After a cold `acu run scenario/ && acu snapshot`, warm `acu run scenario/10-seed-capital.yaml && acu snapshot --assert-unchanged` is the once-class gate (full scenario re-run is additive and moves cash observations on the TB).
|
|
136
|
+
|
|
137
|
+
**Migration (path hard-cut):** bare defaults are `config/snapshot/` (views) and `state/` (observations). Root `snapshot/` and `snapshots/` are no longer defaulted — move files or pass explicit path args.
|
|
138
|
+
|
|
134
139
|
### Seed `endpoint:` symbols
|
|
135
140
|
|
|
136
141
|
Dual-served entities (on both Bootstrap and Default) need an explicit `endpoint:` line.
|
|
@@ -297,11 +302,13 @@ Configuration is one file: a decrypted `.env` at the repo root names the instanc
|
|
|
297
302
|
|
|
298
303
|
The tier is self-contained.
|
|
299
304
|
Each run scaffolds a synthetic single-org company from the packaged `acu config init` templates into a temporary directory, copies the real `.env` into it, and runs the installed `acu` binary from there — no data repo, no pre-existing fixtures on the instance.
|
|
300
|
-
Scratch tenants (`E2E`, `E2EA`, `E2EB`) are created on the way in and always deleted on the way out, so nothing persists.
|
|
305
|
+
Scratch tenants (`E2E`, `E2EA`, `E2EB`, `E2ESCEN`) are created on the way in and always deleted on the way out, so nothing persists.
|
|
306
|
+
The packaged full `config init` seed (under `config/`) is the only scaffold.
|
|
301
307
|
|
|
302
308
|
```sh
|
|
303
309
|
gmake e2e # whole tier, about 20 minutes
|
|
304
|
-
gmake e2e FILE=test_provision_lifecycle #
|
|
310
|
+
gmake e2e FILE=test_provision_lifecycle # apply/diff focus
|
|
311
|
+
gmake e2e FILE=test_scenario_lifecycle # scenario + snapshot focus
|
|
305
312
|
```
|
|
306
313
|
|
|
307
314
|
## License
|
|
@@ -20,27 +20,23 @@ Acumatica configuration normally lives in the web UI: wizards, screens, and manu
|
|
|
20
20
|
uv tool install acumatica-cli
|
|
21
21
|
|
|
22
22
|
acu config init --host erp.example.com my-erp
|
|
23
|
-
cd my-erp # edit .env: set ACU_PASSWORD
|
|
23
|
+
cd my-erp # edit .env: set ACU_PASSWORD, ACU_TENANT
|
|
24
24
|
# keep ACU_API_VERSION in sync with target.yaml
|
|
25
|
+
# start from a brand-new empty tenant
|
|
25
26
|
|
|
26
27
|
acu config check # read-only preflight (incl. target.yaml)
|
|
27
28
|
acu tenant create --id 3 --login DEV # create the tenant + bootstrap it (needs SSH)
|
|
28
|
-
acu --tenant DEV
|
|
29
|
-
acu --tenant DEV
|
|
29
|
+
# or hosted: acu --tenant DEV bootstrap
|
|
30
|
+
acu --tenant DEV apply config/ # seed config/{bootstrap,baseline,setup,master}/
|
|
31
|
+
acu --tenant DEV run scenario/ # once capital → buy → build → sell
|
|
32
|
+
acu --tenant DEV diff config/ # prove zero drift (exit 2 on drift)
|
|
33
|
+
acu --tenant DEV snapshot # capture state/ trial-balance
|
|
34
|
+
# warm: capital once-skips; Owner Capital stays 50000 (not 100000)
|
|
35
|
+
acu --tenant DEV run scenario/
|
|
30
36
|
```
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```sh
|
|
35
|
-
acu config init --flavor distribution --host erp.example.com my-dist
|
|
36
|
-
cd my-dist # edit .env; start from an empty tenant
|
|
37
|
-
acu config check && acu bootstrap \
|
|
38
|
-
&& acu apply config/ && acu run scenario/ && acu diff config/
|
|
39
|
-
# warm re-run: capital once-skips; Owner Capital stays 50000 (not 100000)
|
|
40
|
-
acu run scenario/
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
See [docs/distribution.md](docs/distribution.md) for the entity map, once-guard, and apply-order notes.
|
|
38
|
+
Bare `apply` / `diff` (no path args) also prefer `config/` when those trees exist.
|
|
39
|
+
See [docs/demo-seed.md](docs/demo-seed.md) for the entity map, once-guard, and apply-order notes.
|
|
44
40
|
|
|
45
41
|
**Hosted Acumatica (no SSH):** the tenant already exists; leave `ACU_SSH` blank.
|
|
46
42
|
|
|
@@ -49,8 +45,8 @@ acu config init --host customer.acumatica.com my-erp
|
|
|
49
45
|
cd my-erp # edit .env: ACU_TENANT, ACU_PASSWORD; ACU_SSH=
|
|
50
46
|
acu config check # REST preflight; ssh probe is skipped
|
|
51
47
|
acu --tenant DEV bootstrap # publish AcuBootstrap via REST only
|
|
52
|
-
acu --tenant DEV apply
|
|
53
|
-
acu --tenant DEV diff
|
|
48
|
+
acu --tenant DEV apply config/
|
|
49
|
+
acu --tenant DEV diff config/
|
|
54
50
|
# offline UI fallback when REST publish is blocked:
|
|
55
51
|
acu bootstrap --export AcuBootstrap.zip # import + publish on SM204505
|
|
56
52
|
```
|
|
@@ -71,13 +67,14 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
71
67
|
├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
|
|
72
68
|
├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
|
|
73
69
|
├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
|
|
70
|
+
├── snapshot [--out DIR] [--diff] [--assert-unchanged] [--dry-run] [FILES...]
|
|
71
|
+
│ capture derived state into state/ (not seed)
|
|
74
72
|
├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
|
|
75
73
|
│ dump live tenant state as seed YAML (inverse of apply)
|
|
76
74
|
├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
|
|
77
75
|
│
|
|
78
76
|
└── config configuration ops
|
|
79
|
-
├── init [--host HOST] [
|
|
80
|
-
│ scaffold a data repo (.env, target.yaml, example YAML)
|
|
77
|
+
├── init [--host HOST] [DIR] scaffold full data repo (config/, scenario/, target.yaml)
|
|
81
78
|
├── show print the resolved config as a complete .env
|
|
82
79
|
└── check [--strict] preflight: discovery, secrets, target, REST, endpoints, SSH
|
|
83
80
|
```
|
|
@@ -85,25 +82,29 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
85
82
|
`apply` and `diff` without FILES prefer `config/<name>/` when any seed child exists under `config/`; otherwise root `bootstrap/`, `baseline/`, `setup/`, then `master/` when present.
|
|
86
83
|
A path like `config/` expands nested seed dirs in that fixed order.
|
|
87
84
|
`run` without FILES defaults to `scenario/`.
|
|
85
|
+
`snapshot` without FILES defaults to `config/snapshot/`; writes go to `state/` (`--out`).
|
|
88
86
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
89
87
|
Run `acu <command> --help` for details on any command.
|
|
90
88
|
|
|
91
89
|
## The data repo
|
|
92
90
|
|
|
93
91
|
Your configuration lives in its own git repo.
|
|
94
|
-
`acu config init` scaffolds
|
|
95
|
-
`acu config init --flavor distribution` scaffolds the full demo under `config/` (same contract identity, expanded COA, masters) plus lifecycle `scenario/` and README.
|
|
92
|
+
`acu config init` scaffolds a **single full seed** under `config/` (Bootstrap `project.xml` at `Bootstrap/1.0.0`, expanded COA, masters) plus lifecycle `scenario/`, observer `config/snapshot/`, and README. There is no `--flavor`.
|
|
96
93
|
|
|
97
94
|
| Path | What it holds |
|
|
98
95
|
| ---- | ------------- |
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `config/master/` | distribution masters (prefs, warehouse, items, parties)
|
|
103
|
-
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build
|
|
96
|
+
| `config/bootstrap/` | virgin-tenant config: features, company, credit terms, `project.xml` |
|
|
97
|
+
| `config/baseline/` | reference data: subaccounts, COA, ledger, UOMs, packaging |
|
|
98
|
+
| `config/setup/` | one-time actions: financial year, master calendar, open periods |
|
|
99
|
+
| `config/master/` | inventory/distribution masters (prefs, warehouse, items, parties) |
|
|
100
|
+
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
|
|
101
|
+
| `config/snapshot/` | observer views for `acu snapshot` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
|
|
102
|
+
| `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
|
|
104
103
|
| `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where) |
|
|
105
104
|
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
106
105
|
|
|
106
|
+
Legacy data repos may still keep root `bootstrap/`…`master/`; bare `apply`/`diff` prefer `config/` when present and never merge both trees.
|
|
107
|
+
|
|
107
108
|
Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order.
|
|
108
109
|
The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
|
|
109
110
|
Commit `target.yaml` with the seeds so every clone knows the verified ERP line and Default API generation.
|
|
@@ -113,6 +114,10 @@ Scenario YAML is different — it describes transactions that flow forward.
|
|
|
113
114
|
`acu run` executes each step in order (`put`, `action`, `wait`, `get`), captures server-assigned document numbers into `${var}` references for later steps, and checks `expect:` assertions as deltas against a pre-run snapshot, so additive scenarios re-run safely on a warm tenant.
|
|
114
115
|
`once: true` scenarios declare a `present` inquire-absolute gate; when the probe already holds, the CLI prints `skip <path> (once: already present)` and runs neither steps nor expects (Owner Capital does not restack).
|
|
115
116
|
|
|
117
|
+
`acu snapshot` is the third observation path: it captures live derived state (balances) into `state/` for git review. It is not `extract` (config seed) and not `diff` (desired vs actual config). Packaged golden is trial-balance via contract `inquire:` (`EndingBalance` fixed-point); inventory-summary is not golden this pass. `gi:` stays optional when a GI is V12-verified and **Expose via OData** is on (`params` fail-closed vs `$metadata`). After a cold `acu run scenario/ && acu snapshot`, warm `acu run scenario/10-seed-capital.yaml && acu snapshot --assert-unchanged` is the once-class gate (full scenario re-run is additive and moves cash observations on the TB).
|
|
118
|
+
|
|
119
|
+
**Migration (path hard-cut):** bare defaults are `config/snapshot/` (views) and `state/` (observations). Root `snapshot/` and `snapshots/` are no longer defaulted — move files or pass explicit path args.
|
|
120
|
+
|
|
116
121
|
### Seed `endpoint:` symbols
|
|
117
122
|
|
|
118
123
|
Dual-served entities (on both Bootstrap and Default) need an explicit `endpoint:` line.
|
|
@@ -279,11 +284,13 @@ Configuration is one file: a decrypted `.env` at the repo root names the instanc
|
|
|
279
284
|
|
|
280
285
|
The tier is self-contained.
|
|
281
286
|
Each run scaffolds a synthetic single-org company from the packaged `acu config init` templates into a temporary directory, copies the real `.env` into it, and runs the installed `acu` binary from there — no data repo, no pre-existing fixtures on the instance.
|
|
282
|
-
Scratch tenants (`E2E`, `E2EA`, `E2EB`) are created on the way in and always deleted on the way out, so nothing persists.
|
|
287
|
+
Scratch tenants (`E2E`, `E2EA`, `E2EB`, `E2ESCEN`) are created on the way in and always deleted on the way out, so nothing persists.
|
|
288
|
+
The packaged full `config init` seed (under `config/`) is the only scaffold.
|
|
283
289
|
|
|
284
290
|
```sh
|
|
285
291
|
gmake e2e # whole tier, about 20 minutes
|
|
286
|
-
gmake e2e FILE=test_provision_lifecycle #
|
|
292
|
+
gmake e2e FILE=test_provision_lifecycle # apply/diff focus
|
|
293
|
+
gmake e2e FILE=test_scenario_lifecycle # scenario + snapshot focus
|
|
287
294
|
```
|
|
288
295
|
|
|
289
296
|
## License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "acumatica-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.13.1"
|
|
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,10 +12,9 @@ import click
|
|
|
12
12
|
import httpx
|
|
13
13
|
from click.shell_completion import get_completion_class
|
|
14
14
|
|
|
15
|
-
from . import bootstrap, extract, firstlogin, output, run, seed
|
|
15
|
+
from . import bootstrap, extract, firstlogin, output, run, seed, snapshot
|
|
16
16
|
from .client import AcumaticaClient
|
|
17
17
|
from .config import (
|
|
18
|
-
INIT_FLAVORS,
|
|
19
18
|
Instance,
|
|
20
19
|
data_root,
|
|
21
20
|
find_data_root,
|
|
@@ -411,45 +410,33 @@ def config_group() -> None:
|
|
|
411
410
|
help="Hostname substituted into the scaffolded .env ACU_BASE_URL/ACU_SSH "
|
|
412
411
|
"values (default: a placeholder)",
|
|
413
412
|
)
|
|
414
|
-
@click.option(
|
|
415
|
-
"--flavor",
|
|
416
|
-
type=click.Choice(sorted(INIT_FLAVORS), case_sensitive=True),
|
|
417
|
-
default=None,
|
|
418
|
-
help="Template set: omit for finance-minimal (default, offline e2e); "
|
|
419
|
-
"distribution = full virgin-tenant demo seed (master/, scenario/, "
|
|
420
|
-
"expanded COA/features)",
|
|
421
|
-
)
|
|
422
413
|
@click.argument(
|
|
423
414
|
"directory", required=False, type=click.Path(file_okay=False, path_type=Path)
|
|
424
415
|
)
|
|
425
|
-
def config_init(host: str | None,
|
|
426
|
-
"""Scaffold a data repo: .env, .
|
|
416
|
+
def config_init(host: str | None, directory: Path | None) -> None:
|
|
417
|
+
"""Scaffold a data repo: .env, target.yaml, config/ seed, scenario/.
|
|
427
418
|
|
|
428
419
|
Templates ship with the package; every value is a placeholder or a
|
|
429
|
-
verified
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
skipped). DIRECTORY defaults to the
|
|
434
|
-
if absent. No git init, no gpg.
|
|
420
|
+
verified example - no secrets. Single full seed under ``config/``
|
|
421
|
+
(bootstrap/baseline/setup/master) + lifecycle ``scenario/`` +
|
|
422
|
+
``config/snapshot/`` + README; full company ``project.xml``
|
|
423
|
+
(Bootstrap/1.0.0). No ``--flavor`` (V28/T108). Existing files are
|
|
424
|
+
never overwritten (reported as skipped). DIRECTORY defaults to the
|
|
425
|
+
current directory and is created if absent. No git init, no gpg.
|
|
435
426
|
"""
|
|
436
427
|
target = directory or Path.cwd()
|
|
437
|
-
for action, path in scaffold(target, host=host
|
|
428
|
+
for action, path in scaffold(target, host=host):
|
|
438
429
|
suffix = " (exists)" if action == "skip" else ""
|
|
439
430
|
output.data(f"{action} {path}{suffix}")
|
|
440
|
-
# next-step cmds
|
|
431
|
+
# next-step cmds: operator rebuild order after scaffold (V28)
|
|
441
432
|
output.data("")
|
|
442
433
|
output.data("next:")
|
|
443
434
|
output.data(" 1. edit .env (set ACU_PASSWORD, ACU_TENANT; keep ACU_API_VERSION)")
|
|
444
435
|
output.data(" 2. acu config check")
|
|
445
436
|
output.data(" 3. acu bootstrap # or: acu tenant create ... (SSH)")
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
output.data(" 6. acu diff config/")
|
|
450
|
-
else:
|
|
451
|
-
output.data(" 4. acu apply")
|
|
452
|
-
output.data(" 5. acu diff")
|
|
437
|
+
output.data(" 4. acu apply config/")
|
|
438
|
+
output.data(" 5. acu run scenario/")
|
|
439
|
+
output.data(" 6. acu diff config/")
|
|
453
440
|
|
|
454
441
|
|
|
455
442
|
@config_group.command("show")
|
|
@@ -695,7 +682,7 @@ def default_seed_dirs() -> tuple[Path, ...]:
|
|
|
695
682
|
When the data-repo ``config/`` has any SEED_DIRS child, only those
|
|
696
683
|
``config/<name>/`` paths are returned (dual layout never merges with
|
|
697
684
|
root). Else root ``bootstrap/``…``master/`` for present names
|
|
698
|
-
(
|
|
685
|
+
(legacy root layout). The data repo is the .env dir (V3 walk-up). None
|
|
699
686
|
existing is an error - an empty default would make a bare run a silent
|
|
700
687
|
no-op. Paths come back relative to cwd so a bare run prints exactly
|
|
701
688
|
what naming the dirs would.
|
|
@@ -946,3 +933,79 @@ def _exit_on_drift(inst: Instance, drifts: list[str], files: int) -> None:
|
|
|
946
933
|
output.data(f" {line}")
|
|
947
934
|
raise SystemExit(2)
|
|
948
935
|
output.success(f"no drift on {inst.tenant} ({inst.base_url}, {files} file(s))")
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
@cli.command("snapshot")
|
|
939
|
+
@click.argument(
|
|
940
|
+
"files", nargs=-1, required=False, type=click.Path(exists=True, path_type=Path)
|
|
941
|
+
)
|
|
942
|
+
@click.option(
|
|
943
|
+
"--out",
|
|
944
|
+
"out_dir",
|
|
945
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
946
|
+
default=None,
|
|
947
|
+
help="Observation output directory (default: state/)",
|
|
948
|
+
)
|
|
949
|
+
@click.option(
|
|
950
|
+
"--diff",
|
|
951
|
+
"do_diff",
|
|
952
|
+
is_flag=True,
|
|
953
|
+
help="Compare live vs disk; write nothing (exit 0 either way)",
|
|
954
|
+
)
|
|
955
|
+
@click.option(
|
|
956
|
+
"--assert-unchanged",
|
|
957
|
+
is_flag=True,
|
|
958
|
+
help="Like --diff, but exit 2 when state moved (idempotence gate)",
|
|
959
|
+
)
|
|
960
|
+
@click.option(
|
|
961
|
+
"--dry-run",
|
|
962
|
+
is_flag=True,
|
|
963
|
+
help="Resolve views and validate sources without HTTP",
|
|
964
|
+
)
|
|
965
|
+
@pass_instance
|
|
966
|
+
def snapshot_cmd(
|
|
967
|
+
inst: Instance,
|
|
968
|
+
files: tuple[Path, ...],
|
|
969
|
+
out_dir: Path | None,
|
|
970
|
+
do_diff: bool,
|
|
971
|
+
assert_unchanged: bool,
|
|
972
|
+
dry_run: bool,
|
|
973
|
+
) -> None:
|
|
974
|
+
"""Capture live derived state into committed observation files.
|
|
975
|
+
|
|
976
|
+
FILES are snapshot view YAML files or directories. Omitted, they default
|
|
977
|
+
to the data repo's config/snapshot/ directory (hard-cut; no root
|
|
978
|
+
snapshot/ fallback). Default write target is state/ (--out; no
|
|
979
|
+
snapshots/ fallback). Bare capture writes observations (change is
|
|
980
|
+
fine). --diff compares live to disk without writing.
|
|
981
|
+
--assert-unchanged is the warm-run idempotence gate (exit 2 when
|
|
982
|
+
moved). Never writes seed trees or endpoint: symbols (V32). Exit 0
|
|
983
|
+
ok, 1 op fail, 2 only under --assert-unchanged when state moved.
|
|
984
|
+
"""
|
|
985
|
+
assert_target_compatible(inst)
|
|
986
|
+
if not files:
|
|
987
|
+
default = data_root() / "config" / "snapshot"
|
|
988
|
+
if not default.is_dir():
|
|
989
|
+
raise SystemExit(f"{default}: snapshot directory does not exist")
|
|
990
|
+
files = (Path(os.path.relpath(default)),)
|
|
991
|
+
paths = snapshot.expand_view_files(files)
|
|
992
|
+
views = [snapshot.load_view(path) for path in paths]
|
|
993
|
+
dest = out_dir if out_dir is not None else Path("state")
|
|
994
|
+
if dry_run:
|
|
995
|
+
code = snapshot.run_views(None, views, out_dir=dest, mode="dry")
|
|
996
|
+
else:
|
|
997
|
+
mode = "assert" if assert_unchanged else "diff" if do_diff else "write"
|
|
998
|
+
with AcumaticaClient(inst) as client:
|
|
999
|
+
code = snapshot.run_views(client, views, out_dir=dest, mode=mode)
|
|
1000
|
+
if code:
|
|
1001
|
+
raise SystemExit(code)
|
|
1002
|
+
if dry_run:
|
|
1003
|
+
return
|
|
1004
|
+
if assert_unchanged:
|
|
1005
|
+
output.success(f"{len(views)} snapshot(s) unchanged on {inst.tenant}")
|
|
1006
|
+
elif do_diff:
|
|
1007
|
+
output.success(f"{len(views)} snapshot(s) compared on {inst.tenant}")
|
|
1008
|
+
else:
|
|
1009
|
+
output.success(
|
|
1010
|
+
f"{len(views)} snapshot(s) written under {dest} on {inst.tenant}"
|
|
1011
|
+
)
|
|
@@ -538,3 +538,27 @@ class AcumaticaClient:
|
|
|
538
538
|
return self._checked(
|
|
539
539
|
self._http.post("/CustomizationApi/publishEnd", json={})
|
|
540
540
|
).json()
|
|
541
|
+
|
|
542
|
+
# -- OData Generic Inquiry (snapshot gi: source; V33) --
|
|
543
|
+
|
|
544
|
+
def _odata_gi_root(self) -> str:
|
|
545
|
+
"""24R2+ OData GI service root ``/t/<tenant>/api/odata/gi`` (V33)."""
|
|
546
|
+
tenant = quote(self.instance.tenant, safe="")
|
|
547
|
+
return f"/t/{tenant}/api/odata/gi"
|
|
548
|
+
|
|
549
|
+
def odata_gi_metadata(self, name: str) -> str:
|
|
550
|
+
"""GET OData GI service ``$metadata`` XML (V33 param validation).
|
|
551
|
+
|
|
552
|
+
``name`` is reserved for future per-GI metadata URLs; the 24R2
|
|
553
|
+
service exposes one EDMX document at the service root.
|
|
554
|
+
"""
|
|
555
|
+
del name # service-level metadata; per-GI filter is validate_gi_params
|
|
556
|
+
r = self._checked(self._http.get(f"{self._odata_gi_root()}/$metadata"))
|
|
557
|
+
return r.text
|
|
558
|
+
|
|
559
|
+
def odata_gi(self, name: str, params: dict[str, str] | None = None) -> Any:
|
|
560
|
+
"""GET OData GI rows as JSON (requires Expose via OData on the GI)."""
|
|
561
|
+
gi = quote(name, safe="")
|
|
562
|
+
return self._checked(
|
|
563
|
+
self._http.get(f"{self._odata_gi_root()}/{gi}", params=params)
|
|
564
|
+
).json()
|