acumatica-cli 0.15.2__tar.gz → 0.15.4__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.2 → acumatica_cli-0.15.4}/PKG-INFO +32 -108
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/README.md +31 -107
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/pyproject.toml +1 -1
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/cli.py +85 -22
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/config.py +66 -25
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/seed.py +3 -3
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/target.py +12 -18
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/README.md +2 -1
- acumatica_cli-0.15.4/src/acumatica_cli/templates/env +16 -0
- acumatica_cli-0.15.4/src/acumatica_cli/templates/target +7 -0
- acumatica_cli-0.15.2/src/acumatica_cli/templates/env +0 -17
- acumatica_cli-0.15.2/src/acumatica_cli/templates/target +0 -6
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/bootstrap.py +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/bootstrap_project.xml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/client.py +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/extract.py +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/output.py +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/run.py +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/seed_catalog.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/state.py +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/baseline/50-gl-preferences.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/baseline/60-ledger-company.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/bootstrap/company.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/bootstrap/credit-terms.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/10-reason-codes.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/20-in-preferences.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/30-availability-rules.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/40-posting-classes.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/50-warehouse.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/51-warehouse-locations.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/52-warehouse-defaults.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/53-tax-categories.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/54-item-classes.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/56-so-preferences.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/57-po-preferences.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/58-order-types.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/60-ar-preferences.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/61-ap-preferences.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/62-ca-preferences.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/63-cash-account.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/64-payment-methods.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/65-statement-cycles.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/70-vendor-classes.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/71-customer-classes.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/75-vendors.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/76-customers.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/80-stock-items-parts.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/82-stock-items-kits.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/master/85-kit-specifications.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/setup/10-financial-year.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/setup/20-master-calendar.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/setup/30-open-periods.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/config/views/10-trial-balance.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/gitignore +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/scenario/10-seed-capital.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/scenario/20-buy.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/scenario/30-build.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/scenario/40-sell.yaml +0 -0
- {acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/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.15.
|
|
3
|
+
Version: 0.15.4
|
|
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>
|
|
@@ -39,7 +39,7 @@ uv tool install acumatica-cli
|
|
|
39
39
|
|
|
40
40
|
acu config init --host erp.example.com my-erp
|
|
41
41
|
cd my-erp # edit .env: set ACU_PASSWORD, ACU_TENANT
|
|
42
|
-
#
|
|
42
|
+
# Default API pin = target.yaml default_api
|
|
43
43
|
# start from a brand-new empty tenant
|
|
44
44
|
|
|
45
45
|
acu config check # read-only preflight (incl. target.yaml)
|
|
@@ -49,18 +49,17 @@ acu --tenant DEV apply config/ # seed config/{bootstrap,baseline,setup
|
|
|
49
49
|
acu --tenant DEV run scenario/ # once capital → buy → build → sell
|
|
50
50
|
acu --tenant DEV diff config/ # prove zero drift (exit 2 on drift)
|
|
51
51
|
acu --tenant DEV state # capture state/ trial-balance
|
|
52
|
-
|
|
53
|
-
acu --tenant DEV run scenario/
|
|
52
|
+
acu --tenant DEV run scenario/ # replay transaction scenarios
|
|
54
53
|
```
|
|
55
54
|
|
|
56
55
|
Bare `apply` / `diff` (no path args) also prefer `config/` when those trees exist.
|
|
57
56
|
See [docs/demo-seed.md](docs/demo-seed.md) for the entity map, once-guard, and apply-order notes.
|
|
58
57
|
|
|
59
|
-
**Hosted Acumatica (no SSH):** the tenant already exists;
|
|
58
|
+
**Hosted Acumatica (no SSH):** the tenant already exists; set a blank `ACU_SSH=` in `.env` (scaffold omits the key — without it, acu defaults to `Administrator@<ACU_BASE_URL host>` for SSH boxes).
|
|
60
59
|
|
|
61
60
|
```sh
|
|
62
61
|
acu config init --host customer.acumatica.com my-erp
|
|
63
|
-
cd my-erp # edit .env: ACU_TENANT, ACU_PASSWORD; ACU_SSH=
|
|
62
|
+
cd my-erp # edit .env: ACU_TENANT, ACU_PASSWORD; add ACU_SSH=
|
|
64
63
|
acu config check # REST preflight; ssh probe is skipped
|
|
65
64
|
acu --tenant DEV bootstrap # publish AcuBootstrap via REST only
|
|
66
65
|
acu --tenant DEV apply config/
|
|
@@ -126,7 +125,6 @@ There is no `--flavor`.
|
|
|
126
125
|
Legacy data repos may still keep root `bootstrap/`…`master/`; bare `apply`/`diff` prefer `config/` when present and never merge both trees.
|
|
127
126
|
|
|
128
127
|
Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order.
|
|
129
|
-
The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
|
|
130
128
|
Commit `target.yaml` with the seeds so every clone knows the verified ERP line and Default API generation.
|
|
131
129
|
|
|
132
130
|
Seed YAML is state: `apply` upserts it, `diff` proves it.
|
|
@@ -141,38 +139,19 @@ git diff config/ # review extract delta before commit
|
|
|
141
139
|
acu --tenant DEV apply config/ # replay extracted seed
|
|
142
140
|
acu --tenant DEV diff config/ # expect exit 0
|
|
143
141
|
```
|
|
144
|
-
|
|
145
|
-
**Migration (T115–T120 extract hard-cut):** extract no longer writes root `bootstrap/` / `baseline/` / `setup/` / `master/`.
|
|
146
|
-
Paths are always `config/…`.
|
|
147
|
-
There is no `--layout`.
|
|
148
|
-
Move any root-layout extract output under `config/`, or re-extract into a modern data repo.
|
|
149
|
-
Catalog rename: `extract_manifest.yaml` becomes `seed_catalog.yaml` (package data only; operators do not author it).
|
|
150
|
-
|
|
151
|
-
Scenario YAML is different — it describes transactions that flow forward (not extractable seed).
|
|
152
|
-
`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.
|
|
153
|
-
`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).
|
|
154
|
-
|
|
155
|
-
`acu state` is the third observation path: it captures live derived state (balances) into `state/` for git review.
|
|
156
|
-
It is not `extract` (config seed) and not `diff` (desired vs actual config).
|
|
157
|
-
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`).
|
|
158
|
-
After a cold `acu run scenario/ && acu state`, warm `acu run scenario/10-seed-capital.yaml && acu state --assert-unchanged` is the once-class gate (full scenario re-run is additive and moves cash observations on the TB).
|
|
159
|
-
|
|
160
|
-
**Migration (T112–T114 hard-cut):** CLI verb is `state` only (no `snapshot` alias).
|
|
161
|
-
Bare defaults are `config/views/` (views) and `state/` (observations).
|
|
162
|
-
Prior `config/snapshot/` and root `snapshot/` / `snapshots/` are not fallbacks — move views to `config/views/` or pass explicit path args.
|
|
163
|
-
|
|
164
142
|
### Seed `endpoint:` symbols
|
|
165
143
|
|
|
166
144
|
Dual-served entities (on both Bootstrap and Default) need an explicit `endpoint:` line.
|
|
167
145
|
|
|
168
146
|
| Value | Resolves to |
|
|
169
147
|
| ----- | ----------- |
|
|
170
|
-
| omitted | `Default/<
|
|
148
|
+
| omitted | `Default/<api_version>` for Default-only entities |
|
|
171
149
|
| `bootstrap` | active `Bootstrap/<ver>` from `bootstrap/project.xml` or the packaged contract |
|
|
172
|
-
| `default` | `Default/<
|
|
173
|
-
| `Bootstrap/1.0.0` or `Default/25.200.001` | literal pin (ignores
|
|
150
|
+
| `default` | `Default/<api_version>` — tracks the resolved API version |
|
|
151
|
+
| `Bootstrap/1.0.0` or `Default/25.200.001` | literal pin (ignores the resolved Default version) |
|
|
174
152
|
|
|
175
|
-
|
|
153
|
+
`api_version` resolves as `--api-version` flag, else `target.yaml` `default_api` when present, else code default `25.200.001` (never `ACU_API_VERSION` in `.env`).
|
|
154
|
+
Prefer symbolic `default` over a pinned `Default/25.200.001` so the seed tree travels with the dataset pin.
|
|
176
155
|
|
|
177
156
|
## Installation
|
|
178
157
|
|
|
@@ -193,41 +172,45 @@ Verify with `acu --version`.
|
|
|
193
172
|
|
|
194
173
|
## Configuration
|
|
195
174
|
|
|
196
|
-
Everything lives in one `.env` file: *where* to apply and *who* signs in
|
|
197
|
-
|
|
175
|
+
Everything lives in one `.env` file: *where* to apply and *who* signs in
|
|
176
|
+
(`ACU_*` vars only).
|
|
177
|
+
The Default contract API pin is **not** in `.env` — it
|
|
178
|
+
lives in committed `target.yaml` (`default_api`).
|
|
198
179
|
|
|
199
180
|
```sh
|
|
200
181
|
ACU_BASE_URL=http://acu-dev1.vm.internal/AcumaticaERP # required: REST root
|
|
201
182
|
ACU_TENANT=LAB5 # sign-in name of the tenant API sessions use
|
|
202
|
-
ACU_SSH
|
|
203
|
-
|
|
183
|
+
# ACU_SSH omitted → defaults to Administrator@acu-dev1.vm.internal
|
|
184
|
+
# ACU_SSH= # hosted opt-out (blank key)
|
|
204
185
|
ACU_USER=admin # optional, defaults to admin
|
|
205
186
|
ACU_PASSWORD=... # required for live commands
|
|
206
187
|
```
|
|
207
188
|
|
|
208
|
-
|
|
209
|
-
|
|
189
|
+
There is no `ACU_API_VERSION` env key (unknown `ACU_*` vars are ignored).
|
|
190
|
+
Ad-hoc override: `acu --api-version 24.200.001 …` (version half only, never
|
|
191
|
+
`Default/25.200.001` — a full path would nest as `/entity/Default/Default/...`).
|
|
210
192
|
|
|
211
|
-
The committed `target.yaml` next to `.env` declares the verified matrix (what, not where)
|
|
193
|
+
The committed `target.yaml` next to `.env` declares the verified matrix (what, not where) and is the **sole data-repo Default pin**:
|
|
212
194
|
|
|
213
195
|
```yaml
|
|
214
|
-
erp: "26.101.0225" # claimed product line/build
|
|
215
|
-
default_api: "25.200.001" #
|
|
196
|
+
erp: "26.101.0225" # claimed product line/build
|
|
197
|
+
default_api: "25.200.001" # sources Instance.api_version when --api-version absent
|
|
216
198
|
```
|
|
217
199
|
|
|
218
|
-
When `target.yaml` is present,
|
|
219
|
-
`acu config check` reports
|
|
220
|
-
|
|
200
|
+
When `target.yaml` is present, live commands resolve `api_version` from
|
|
201
|
+
`default_api` (source-merge). `acu config check` reports
|
|
202
|
+
`ok target (api_version from default_api=…; erp=… claimed)`.
|
|
203
|
+
Missing `target.yaml` only warns on check unless you pass `--strict` (then
|
|
204
|
+
the code default `25.200.001` is used).
|
|
221
205
|
|
|
222
206
|
Worth knowing:
|
|
223
207
|
|
|
224
208
|
- The file is found by walking up from the current directory, so any subdirectory of the data repo works.
|
|
225
209
|
- Without a `.env`, global flags plus the process environment supply the full configuration.
|
|
226
|
-
- `ACU_SSH` is
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
- `acu config show` prints the
|
|
230
|
-
- When `target.yaml` is present, `config show` also comments `erp` / `default_api`.
|
|
210
|
+
- When `ACU_SSH` is **absent**, acu defaults to `Administrator@` + the `ACU_BASE_URL` hostname.
|
|
211
|
+
A **present blank** `ACU_SSH=` is the hosted opt-out.
|
|
212
|
+
Only `acu tenant` requires a non-empty value post-default.
|
|
213
|
+
- `acu config show` prints the resolved `.env` (password excluded; never `ACU_API_VERSION`) and comments `erp` / `default_api` plus the `api_version` source when `target.yaml` is present.
|
|
231
214
|
- Redirect it to turn resolved state into a working config: `acu config show > .env`.
|
|
232
215
|
|
|
233
216
|
Verify before touching anything live:
|
|
@@ -238,65 +221,6 @@ acu config check --strict # missing target.yaml becomes fail
|
|
|
238
221
|
acu apply --dry-run # show what would be written, write nothing
|
|
239
222
|
```
|
|
240
223
|
|
|
241
|
-
## Control and Data Planes
|
|
242
|
-
|
|
243
|
-
`acu` talks to an instance over two independent channels:
|
|
244
|
-
|
|
245
|
-
- **Control plane (SSH):** `acu tenant` runs `ac.exe -cm:CompanyConfig` and `sqlcmd` on the Windows guest — see [`docs/ac-exe.md`](docs/ac-exe.md).
|
|
246
|
-
- **Data plane (REST):** `acu bootstrap`, `apply`, `diff`, `run`, `extract`, and `schema` use the contract-based API and CustomizationApi — see [`docs/rest-api.md`](docs/rest-api.md).
|
|
247
|
-
|
|
248
|
-
If you never touch `acu tenant`, you never need SSH.
|
|
249
|
-
On a hosted instance, publish AcuBootstrap with `acu bootstrap`, then seed with `apply`.
|
|
250
|
-
`acu bootstrap --export PATH` writes the package zip for manual import on the Customization Projects screen (SM204505) when you cannot call the API.
|
|
251
|
-
|
|
252
|
-
## SSH setup (control plane)
|
|
253
|
-
|
|
254
|
-
`acu tenant` runs commands on the Windows guest through plain `ssh`.
|
|
255
|
-
Two things about this setup are not obvious, and both are hard requirements.
|
|
256
|
-
|
|
257
|
-
**1.
|
|
258
|
-
The default SSH shell on the Windows guest must be PowerShell.**
|
|
259
|
-
`acu` sends PowerShell syntax over the wire, and every one of those commands fails under `cmd.exe`, the Windows OpenSSH default.
|
|
260
|
-
Switch it once, in an elevated PowerShell on the guest:
|
|
261
|
-
|
|
262
|
-
```powershell
|
|
263
|
-
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
|
|
264
|
-
-Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" `
|
|
265
|
-
-PropertyType String -Force
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
**2.
|
|
269
|
-
Authentication must be key-based and non-interactive.**
|
|
270
|
-
`acu` connects with `BatchMode=yes`, so it will never answer a password prompt.
|
|
271
|
-
Because the default user is `Administrator` (an administrators-group member), Windows OpenSSH reads the key from the *machine-wide* file `C:\ProgramData\ssh\administrators_authorized_keys` — **not** from `~\.ssh\authorized_keys` like on Linux.
|
|
272
|
-
On the guest:
|
|
273
|
-
|
|
274
|
-
Install + start the server (once):
|
|
275
|
-
|
|
276
|
-
```powershell
|
|
277
|
-
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
|
|
278
|
-
Set-Service sshd -StartupType Automatic
|
|
279
|
-
Start-Service sshd
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
Authorize your public key for administrators:
|
|
283
|
-
|
|
284
|
-
```powershell
|
|
285
|
-
Add-Content -Path C:\ProgramData\ssh\administrators_authorized_keys -Value "ssh-ed25519 AAAA... you@laptop"
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
The file must be readable by SYSTEM/Administrators only, or sshd ignores it:
|
|
289
|
-
|
|
290
|
-
```powershell
|
|
291
|
-
icacls C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Then verify from your workstation — this one test proves both requirements at once (key auth works, and the shell is PowerShell):
|
|
295
|
-
|
|
296
|
-
```sh
|
|
297
|
-
ssh -o BatchMode=yes Administrator@acu-dev1.vm.internal '$PSVersionTable.PSVersion'
|
|
298
|
-
```
|
|
299
|
-
|
|
300
224
|
## Development
|
|
301
225
|
|
|
302
226
|
Requires **GNU Make at least 3.82** — the Makefile uses `.ONESHELL`.
|
|
@@ -328,7 +252,7 @@ GitHub Actions re-runs the check on the tag, then publishes the GitHub release a
|
|
|
328
252
|
|
|
329
253
|
`gmake e2e` runs the opt-in live tier against a real Acumatica instance (pytest marker `e2e`, deselected by the default suite).
|
|
330
254
|
|
|
331
|
-
Configuration is one file: a decrypted `.env` at the repo root names the instance — `ACU_BASE_URL`, `
|
|
255
|
+
Configuration is one file: a decrypted `.env` at the repo root names the instance — `ACU_BASE_URL`, `ACU_TENANT`, `ACU_PASSWORD` (and optional `ACU_SSH`; omitted defaults to `Administrator@` + base-url host).
|
|
332
256
|
`gmake e2e` refuses to start without it.
|
|
333
257
|
|
|
334
258
|
The tier is self-contained.
|
|
@@ -21,7 +21,7 @@ uv tool install acumatica-cli
|
|
|
21
21
|
|
|
22
22
|
acu config init --host erp.example.com my-erp
|
|
23
23
|
cd my-erp # edit .env: set ACU_PASSWORD, ACU_TENANT
|
|
24
|
-
#
|
|
24
|
+
# Default API pin = target.yaml default_api
|
|
25
25
|
# start from a brand-new empty tenant
|
|
26
26
|
|
|
27
27
|
acu config check # read-only preflight (incl. target.yaml)
|
|
@@ -31,18 +31,17 @@ acu --tenant DEV apply config/ # seed config/{bootstrap,baseline,setup
|
|
|
31
31
|
acu --tenant DEV run scenario/ # once capital → buy → build → sell
|
|
32
32
|
acu --tenant DEV diff config/ # prove zero drift (exit 2 on drift)
|
|
33
33
|
acu --tenant DEV state # capture state/ trial-balance
|
|
34
|
-
|
|
35
|
-
acu --tenant DEV run scenario/
|
|
34
|
+
acu --tenant DEV run scenario/ # replay transaction scenarios
|
|
36
35
|
```
|
|
37
36
|
|
|
38
37
|
Bare `apply` / `diff` (no path args) also prefer `config/` when those trees exist.
|
|
39
38
|
See [docs/demo-seed.md](docs/demo-seed.md) for the entity map, once-guard, and apply-order notes.
|
|
40
39
|
|
|
41
|
-
**Hosted Acumatica (no SSH):** the tenant already exists;
|
|
40
|
+
**Hosted Acumatica (no SSH):** the tenant already exists; set a blank `ACU_SSH=` in `.env` (scaffold omits the key — without it, acu defaults to `Administrator@<ACU_BASE_URL host>` for SSH boxes).
|
|
42
41
|
|
|
43
42
|
```sh
|
|
44
43
|
acu config init --host customer.acumatica.com my-erp
|
|
45
|
-
cd my-erp # edit .env: ACU_TENANT, ACU_PASSWORD; ACU_SSH=
|
|
44
|
+
cd my-erp # edit .env: ACU_TENANT, ACU_PASSWORD; add ACU_SSH=
|
|
46
45
|
acu config check # REST preflight; ssh probe is skipped
|
|
47
46
|
acu --tenant DEV bootstrap # publish AcuBootstrap via REST only
|
|
48
47
|
acu --tenant DEV apply config/
|
|
@@ -108,7 +107,6 @@ There is no `--flavor`.
|
|
|
108
107
|
Legacy data repos may still keep root `bootstrap/`…`master/`; bare `apply`/`diff` prefer `config/` when present and never merge both trees.
|
|
109
108
|
|
|
110
109
|
Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order.
|
|
111
|
-
The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
|
|
112
110
|
Commit `target.yaml` with the seeds so every clone knows the verified ERP line and Default API generation.
|
|
113
111
|
|
|
114
112
|
Seed YAML is state: `apply` upserts it, `diff` proves it.
|
|
@@ -123,38 +121,19 @@ git diff config/ # review extract delta before commit
|
|
|
123
121
|
acu --tenant DEV apply config/ # replay extracted seed
|
|
124
122
|
acu --tenant DEV diff config/ # expect exit 0
|
|
125
123
|
```
|
|
126
|
-
|
|
127
|
-
**Migration (T115–T120 extract hard-cut):** extract no longer writes root `bootstrap/` / `baseline/` / `setup/` / `master/`.
|
|
128
|
-
Paths are always `config/…`.
|
|
129
|
-
There is no `--layout`.
|
|
130
|
-
Move any root-layout extract output under `config/`, or re-extract into a modern data repo.
|
|
131
|
-
Catalog rename: `extract_manifest.yaml` becomes `seed_catalog.yaml` (package data only; operators do not author it).
|
|
132
|
-
|
|
133
|
-
Scenario YAML is different — it describes transactions that flow forward (not extractable seed).
|
|
134
|
-
`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.
|
|
135
|
-
`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).
|
|
136
|
-
|
|
137
|
-
`acu state` is the third observation path: it captures live derived state (balances) into `state/` for git review.
|
|
138
|
-
It is not `extract` (config seed) and not `diff` (desired vs actual config).
|
|
139
|
-
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`).
|
|
140
|
-
After a cold `acu run scenario/ && acu state`, warm `acu run scenario/10-seed-capital.yaml && acu state --assert-unchanged` is the once-class gate (full scenario re-run is additive and moves cash observations on the TB).
|
|
141
|
-
|
|
142
|
-
**Migration (T112–T114 hard-cut):** CLI verb is `state` only (no `snapshot` alias).
|
|
143
|
-
Bare defaults are `config/views/` (views) and `state/` (observations).
|
|
144
|
-
Prior `config/snapshot/` and root `snapshot/` / `snapshots/` are not fallbacks — move views to `config/views/` or pass explicit path args.
|
|
145
|
-
|
|
146
124
|
### Seed `endpoint:` symbols
|
|
147
125
|
|
|
148
126
|
Dual-served entities (on both Bootstrap and Default) need an explicit `endpoint:` line.
|
|
149
127
|
|
|
150
128
|
| Value | Resolves to |
|
|
151
129
|
| ----- | ----------- |
|
|
152
|
-
| omitted | `Default/<
|
|
130
|
+
| omitted | `Default/<api_version>` for Default-only entities |
|
|
153
131
|
| `bootstrap` | active `Bootstrap/<ver>` from `bootstrap/project.xml` or the packaged contract |
|
|
154
|
-
| `default` | `Default/<
|
|
155
|
-
| `Bootstrap/1.0.0` or `Default/25.200.001` | literal pin (ignores
|
|
132
|
+
| `default` | `Default/<api_version>` — tracks the resolved API version |
|
|
133
|
+
| `Bootstrap/1.0.0` or `Default/25.200.001` | literal pin (ignores the resolved Default version) |
|
|
156
134
|
|
|
157
|
-
|
|
135
|
+
`api_version` resolves as `--api-version` flag, else `target.yaml` `default_api` when present, else code default `25.200.001` (never `ACU_API_VERSION` in `.env`).
|
|
136
|
+
Prefer symbolic `default` over a pinned `Default/25.200.001` so the seed tree travels with the dataset pin.
|
|
158
137
|
|
|
159
138
|
## Installation
|
|
160
139
|
|
|
@@ -175,41 +154,45 @@ Verify with `acu --version`.
|
|
|
175
154
|
|
|
176
155
|
## Configuration
|
|
177
156
|
|
|
178
|
-
Everything lives in one `.env` file: *where* to apply and *who* signs in
|
|
179
|
-
|
|
157
|
+
Everything lives in one `.env` file: *where* to apply and *who* signs in
|
|
158
|
+
(`ACU_*` vars only).
|
|
159
|
+
The Default contract API pin is **not** in `.env` — it
|
|
160
|
+
lives in committed `target.yaml` (`default_api`).
|
|
180
161
|
|
|
181
162
|
```sh
|
|
182
163
|
ACU_BASE_URL=http://acu-dev1.vm.internal/AcumaticaERP # required: REST root
|
|
183
164
|
ACU_TENANT=LAB5 # sign-in name of the tenant API sessions use
|
|
184
|
-
ACU_SSH
|
|
185
|
-
|
|
165
|
+
# ACU_SSH omitted → defaults to Administrator@acu-dev1.vm.internal
|
|
166
|
+
# ACU_SSH= # hosted opt-out (blank key)
|
|
186
167
|
ACU_USER=admin # optional, defaults to admin
|
|
187
168
|
ACU_PASSWORD=... # required for live commands
|
|
188
169
|
```
|
|
189
170
|
|
|
190
|
-
|
|
191
|
-
|
|
171
|
+
There is no `ACU_API_VERSION` env key (unknown `ACU_*` vars are ignored).
|
|
172
|
+
Ad-hoc override: `acu --api-version 24.200.001 …` (version half only, never
|
|
173
|
+
`Default/25.200.001` — a full path would nest as `/entity/Default/Default/...`).
|
|
192
174
|
|
|
193
|
-
The committed `target.yaml` next to `.env` declares the verified matrix (what, not where)
|
|
175
|
+
The committed `target.yaml` next to `.env` declares the verified matrix (what, not where) and is the **sole data-repo Default pin**:
|
|
194
176
|
|
|
195
177
|
```yaml
|
|
196
|
-
erp: "26.101.0225" # claimed product line/build
|
|
197
|
-
default_api: "25.200.001" #
|
|
178
|
+
erp: "26.101.0225" # claimed product line/build
|
|
179
|
+
default_api: "25.200.001" # sources Instance.api_version when --api-version absent
|
|
198
180
|
```
|
|
199
181
|
|
|
200
|
-
When `target.yaml` is present,
|
|
201
|
-
`acu config check` reports
|
|
202
|
-
|
|
182
|
+
When `target.yaml` is present, live commands resolve `api_version` from
|
|
183
|
+
`default_api` (source-merge). `acu config check` reports
|
|
184
|
+
`ok target (api_version from default_api=…; erp=… claimed)`.
|
|
185
|
+
Missing `target.yaml` only warns on check unless you pass `--strict` (then
|
|
186
|
+
the code default `25.200.001` is used).
|
|
203
187
|
|
|
204
188
|
Worth knowing:
|
|
205
189
|
|
|
206
190
|
- The file is found by walking up from the current directory, so any subdirectory of the data repo works.
|
|
207
191
|
- Without a `.env`, global flags plus the process environment supply the full configuration.
|
|
208
|
-
- `ACU_SSH` is
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
- `acu config show` prints the
|
|
212
|
-
- When `target.yaml` is present, `config show` also comments `erp` / `default_api`.
|
|
192
|
+
- When `ACU_SSH` is **absent**, acu defaults to `Administrator@` + the `ACU_BASE_URL` hostname.
|
|
193
|
+
A **present blank** `ACU_SSH=` is the hosted opt-out.
|
|
194
|
+
Only `acu tenant` requires a non-empty value post-default.
|
|
195
|
+
- `acu config show` prints the resolved `.env` (password excluded; never `ACU_API_VERSION`) and comments `erp` / `default_api` plus the `api_version` source when `target.yaml` is present.
|
|
213
196
|
- Redirect it to turn resolved state into a working config: `acu config show > .env`.
|
|
214
197
|
|
|
215
198
|
Verify before touching anything live:
|
|
@@ -220,65 +203,6 @@ acu config check --strict # missing target.yaml becomes fail
|
|
|
220
203
|
acu apply --dry-run # show what would be written, write nothing
|
|
221
204
|
```
|
|
222
205
|
|
|
223
|
-
## Control and Data Planes
|
|
224
|
-
|
|
225
|
-
`acu` talks to an instance over two independent channels:
|
|
226
|
-
|
|
227
|
-
- **Control plane (SSH):** `acu tenant` runs `ac.exe -cm:CompanyConfig` and `sqlcmd` on the Windows guest — see [`docs/ac-exe.md`](docs/ac-exe.md).
|
|
228
|
-
- **Data plane (REST):** `acu bootstrap`, `apply`, `diff`, `run`, `extract`, and `schema` use the contract-based API and CustomizationApi — see [`docs/rest-api.md`](docs/rest-api.md).
|
|
229
|
-
|
|
230
|
-
If you never touch `acu tenant`, you never need SSH.
|
|
231
|
-
On a hosted instance, publish AcuBootstrap with `acu bootstrap`, then seed with `apply`.
|
|
232
|
-
`acu bootstrap --export PATH` writes the package zip for manual import on the Customization Projects screen (SM204505) when you cannot call the API.
|
|
233
|
-
|
|
234
|
-
## SSH setup (control plane)
|
|
235
|
-
|
|
236
|
-
`acu tenant` runs commands on the Windows guest through plain `ssh`.
|
|
237
|
-
Two things about this setup are not obvious, and both are hard requirements.
|
|
238
|
-
|
|
239
|
-
**1.
|
|
240
|
-
The default SSH shell on the Windows guest must be PowerShell.**
|
|
241
|
-
`acu` sends PowerShell syntax over the wire, and every one of those commands fails under `cmd.exe`, the Windows OpenSSH default.
|
|
242
|
-
Switch it once, in an elevated PowerShell on the guest:
|
|
243
|
-
|
|
244
|
-
```powershell
|
|
245
|
-
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
|
|
246
|
-
-Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" `
|
|
247
|
-
-PropertyType String -Force
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
**2.
|
|
251
|
-
Authentication must be key-based and non-interactive.**
|
|
252
|
-
`acu` connects with `BatchMode=yes`, so it will never answer a password prompt.
|
|
253
|
-
Because the default user is `Administrator` (an administrators-group member), Windows OpenSSH reads the key from the *machine-wide* file `C:\ProgramData\ssh\administrators_authorized_keys` — **not** from `~\.ssh\authorized_keys` like on Linux.
|
|
254
|
-
On the guest:
|
|
255
|
-
|
|
256
|
-
Install + start the server (once):
|
|
257
|
-
|
|
258
|
-
```powershell
|
|
259
|
-
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
|
|
260
|
-
Set-Service sshd -StartupType Automatic
|
|
261
|
-
Start-Service sshd
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
Authorize your public key for administrators:
|
|
265
|
-
|
|
266
|
-
```powershell
|
|
267
|
-
Add-Content -Path C:\ProgramData\ssh\administrators_authorized_keys -Value "ssh-ed25519 AAAA... you@laptop"
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
The file must be readable by SYSTEM/Administrators only, or sshd ignores it:
|
|
271
|
-
|
|
272
|
-
```powershell
|
|
273
|
-
icacls C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
Then verify from your workstation — this one test proves both requirements at once (key auth works, and the shell is PowerShell):
|
|
277
|
-
|
|
278
|
-
```sh
|
|
279
|
-
ssh -o BatchMode=yes Administrator@acu-dev1.vm.internal '$PSVersionTable.PSVersion'
|
|
280
|
-
```
|
|
281
|
-
|
|
282
206
|
## Development
|
|
283
207
|
|
|
284
208
|
Requires **GNU Make at least 3.82** — the Makefile uses `.ONESHELL`.
|
|
@@ -310,7 +234,7 @@ GitHub Actions re-runs the check on the tag, then publishes the GitHub release a
|
|
|
310
234
|
|
|
311
235
|
`gmake e2e` runs the opt-in live tier against a real Acumatica instance (pytest marker `e2e`, deselected by the default suite).
|
|
312
236
|
|
|
313
|
-
Configuration is one file: a decrypted `.env` at the repo root names the instance — `ACU_BASE_URL`, `
|
|
237
|
+
Configuration is one file: a decrypted `.env` at the repo root names the instance — `ACU_BASE_URL`, `ACU_TENANT`, `ACU_PASSWORD` (and optional `ACU_SSH`; omitted defaults to `Administrator@` + base-url host).
|
|
314
238
|
`gmake e2e` refuses to start without it.
|
|
315
239
|
|
|
316
240
|
The tier is self-contained.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "acumatica-cli"
|
|
3
|
-
version = "0.15.
|
|
3
|
+
version = "0.15.4"
|
|
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"
|
|
@@ -155,18 +155,67 @@ def pass_instance[**P, R](f: Callable[Concatenate[Instance, P], R]) -> Callable[
|
|
|
155
155
|
return new_func
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
def _transport_target(
|
|
159
|
+
exc: httpx.TransportError, *, target: str | None = None
|
|
160
|
+
) -> str | None:
|
|
161
|
+
"""Prefer explicit target (e.g. Instance.base_url); else request URL."""
|
|
162
|
+
if target:
|
|
163
|
+
return target
|
|
164
|
+
request = getattr(exc, "request", None)
|
|
165
|
+
if request is not None:
|
|
166
|
+
return str(request.url)
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _format_transport_error(
|
|
171
|
+
exc: httpx.TransportError, *, target: str | None = None
|
|
172
|
+
) -> str:
|
|
173
|
+
"""Friendly one-line body for connect/timeout/TLS (V9 / T123).
|
|
174
|
+
|
|
175
|
+
Never surface raw httpx/OS dumps (e.g. ``[Errno 61] Connection refused``).
|
|
176
|
+
"""
|
|
177
|
+
raw = str(exc).lower()
|
|
178
|
+
if "ssl" in raw or "certificate" in raw or "tls" in raw:
|
|
179
|
+
kind = "TLS error"
|
|
180
|
+
elif isinstance(exc, httpx.TimeoutException):
|
|
181
|
+
kind = "request timed out"
|
|
182
|
+
elif isinstance(exc, httpx.ConnectError):
|
|
183
|
+
kind = "cannot connect"
|
|
184
|
+
elif isinstance(exc, httpx.NetworkError):
|
|
185
|
+
kind = "network error"
|
|
186
|
+
else:
|
|
187
|
+
kind = "transport error"
|
|
188
|
+
where = _transport_target(exc, target=target)
|
|
189
|
+
hint = "check ACU_BASE_URL / network / instance up"
|
|
190
|
+
if where:
|
|
191
|
+
return f"{kind} to {where} ({hint})"
|
|
192
|
+
return f"{kind} ({hint})"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _format_failure(exc: BaseException, *, target: str | None = None) -> str:
|
|
196
|
+
"""Map expected failures to one greppable error line body (V9).
|
|
197
|
+
|
|
198
|
+
Transport/network class → friendly rewrite; other RuntimeError / HTTPError
|
|
199
|
+
keep ``str(exc)`` (server ``exceptionMessage`` path unchanged).
|
|
200
|
+
"""
|
|
201
|
+
if isinstance(exc, httpx.TransportError):
|
|
202
|
+
return _format_transport_error(exc, target=target)
|
|
203
|
+
return str(exc)
|
|
204
|
+
|
|
205
|
+
|
|
158
206
|
def main() -> None:
|
|
159
207
|
"""Entry point: run the CLI, mapping expected failures to one-line errors.
|
|
160
208
|
|
|
161
209
|
RuntimeError (SSH/ac.exe, REST, first-login) and httpx transport errors
|
|
162
210
|
print `x message` and exit 1; ACU_DEBUG=1 re-raises for the traceback.
|
|
211
|
+
Transport/network failures use a friendly rewrite (V9), not raw dumps.
|
|
163
212
|
"""
|
|
164
213
|
try:
|
|
165
214
|
cli()
|
|
166
215
|
except (RuntimeError, httpx.HTTPError) as exc:
|
|
167
216
|
if os.environ.get("ACU_DEBUG"):
|
|
168
217
|
raise
|
|
169
|
-
output.error(
|
|
218
|
+
output.error(_format_failure(exc))
|
|
170
219
|
raise SystemExit(1) from exc
|
|
171
220
|
|
|
172
221
|
|
|
@@ -407,8 +456,8 @@ def config_group() -> None:
|
|
|
407
456
|
@click.option(
|
|
408
457
|
"--host",
|
|
409
458
|
default=None,
|
|
410
|
-
help="Hostname substituted into the scaffolded .env ACU_BASE_URL
|
|
411
|
-
"
|
|
459
|
+
help="Hostname substituted into the scaffolded .env ACU_BASE_URL "
|
|
460
|
+
"(default: a placeholder); ACU_SSH is omitted and defaults from that host",
|
|
412
461
|
)
|
|
413
462
|
@click.argument(
|
|
414
463
|
"directory", required=False, type=click.Path(file_okay=False, path_type=Path)
|
|
@@ -431,7 +480,7 @@ def config_init(host: str | None, directory: Path | None) -> None:
|
|
|
431
480
|
# next-step cmds: operator rebuild order after scaffold (V28)
|
|
432
481
|
output.data("")
|
|
433
482
|
output.data("next:")
|
|
434
|
-
output.data(" 1. edit .env (set ACU_PASSWORD, ACU_TENANT
|
|
483
|
+
output.data(" 1. edit .env (set ACU_PASSWORD, ACU_TENANT)")
|
|
435
484
|
output.data(" 2. acu config check")
|
|
436
485
|
output.data(" 3. acu bootstrap # or: acu tenant create ... (SSH)")
|
|
437
486
|
output.data(" 4. acu apply config/")
|
|
@@ -449,23 +498,35 @@ def config_show(inst: Instance) -> None:
|
|
|
449
498
|
so the printed values are exactly what a live command would trust -
|
|
450
499
|
global flag overrides (--url, --ssh, ...) included. The password is
|
|
451
500
|
never emitted in any form (V2): no ACU_PASSWORD key, no value.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
501
|
+
``ACU_API_VERSION`` is never emitted (V27/T125 — api pin is not env;
|
|
502
|
+
source is target.yaml ``default_api`` or ``--api-version``). When
|
|
503
|
+
target.yaml is present, surfaces erp/default_api as comments and notes
|
|
504
|
+
the api_version source (still exit 0 — no hard gate). Redirect to a
|
|
505
|
+
file and edit: the output loads back through load_instance unchanged,
|
|
506
|
+
the password supplied out of band.
|
|
456
507
|
"""
|
|
457
508
|
output.data("# resolved by `acu config show` - a complete .env")
|
|
458
509
|
output.data("# ACU_PASSWORD comes from .env or the environment, never from here")
|
|
459
|
-
|
|
510
|
+
# V27: api pin is not an env key — never emit ACU_API_VERSION
|
|
511
|
+
for field, value in inst.model_dump(exclude={"password", "api_version"}).items():
|
|
460
512
|
output.data(f"ACU_{field.upper()}={value}")
|
|
461
513
|
target = load_target()
|
|
462
514
|
if target is not None:
|
|
463
515
|
output.data(f"# target.yaml: erp={target.erp} default_api={target.default_api}")
|
|
464
|
-
if
|
|
516
|
+
if inst.api_version == target.default_api:
|
|
465
517
|
output.data(
|
|
466
|
-
f"#
|
|
467
|
-
f"ACU_API_VERSION={inst.api_version}"
|
|
518
|
+
f"# api_version={inst.api_version} (from target.yaml default_api)"
|
|
468
519
|
)
|
|
520
|
+
else:
|
|
521
|
+
output.data(
|
|
522
|
+
f"# api_version={inst.api_version} (from --api-version; "
|
|
523
|
+
f"target default_api={target.default_api})"
|
|
524
|
+
)
|
|
525
|
+
else:
|
|
526
|
+
output.data(
|
|
527
|
+
f"# api_version={inst.api_version} "
|
|
528
|
+
f"(code default or --api-version; no target.yaml)"
|
|
529
|
+
)
|
|
469
530
|
|
|
470
531
|
|
|
471
532
|
@config_group.command("check")
|
|
@@ -546,7 +607,7 @@ def _probe_rest(inst: Instance, claimed_erp: str | None) -> bool:
|
|
|
546
607
|
output.data(f"ok rest ({inst.base_url}, tenant {inst.tenant})")
|
|
547
608
|
return _probe_entity_root(client, inst, claimed_erp)
|
|
548
609
|
except (RuntimeError, httpx.HTTPError) as exc:
|
|
549
|
-
output.data(f"fail rest: {exc}")
|
|
610
|
+
output.data(f"fail rest: {_format_failure(exc, target=inst.base_url)}")
|
|
550
611
|
return False
|
|
551
612
|
|
|
552
613
|
|
|
@@ -558,7 +619,7 @@ def _probe_entity_root(
|
|
|
558
619
|
try:
|
|
559
620
|
endpoints, live_build = client.entity_root()
|
|
560
621
|
except (RuntimeError, httpx.HTTPError) as exc:
|
|
561
|
-
output.data(f"fail endpoints: {exc}")
|
|
622
|
+
output.data(f"fail endpoints: {_format_failure(exc, target=inst.base_url)}")
|
|
562
623
|
endpoints, live_build = [], None
|
|
563
624
|
ok = False
|
|
564
625
|
else:
|
|
@@ -609,16 +670,18 @@ def _probe_target(
|
|
|
609
670
|
)
|
|
610
671
|
output.data(f"{'fail' if strict else 'warn'} {msg}")
|
|
611
672
|
return strict, None
|
|
612
|
-
|
|
673
|
+
# V27/T125: source-merge — api_version already from default_api unless
|
|
674
|
+
# --api-version; surface source, never dual-source mismatch fail
|
|
675
|
+
if inst.api_version == target.default_api:
|
|
613
676
|
output.data(
|
|
614
|
-
f"
|
|
615
|
-
f"
|
|
677
|
+
f"ok target (api_version from default_api={target.default_api}; "
|
|
678
|
+
f"erp={target.erp} claimed)"
|
|
679
|
+
)
|
|
680
|
+
else:
|
|
681
|
+
output.data(
|
|
682
|
+
f"ok target (api_version={inst.api_version} from --api-version; "
|
|
683
|
+
f"default_api={target.default_api}; erp={target.erp} claimed)"
|
|
616
684
|
)
|
|
617
|
-
return True, None
|
|
618
|
-
output.data(
|
|
619
|
-
f"ok target (default_api={target.default_api} matches configured; "
|
|
620
|
-
f"erp={target.erp} claimed)"
|
|
621
|
-
)
|
|
622
685
|
return False, target.erp
|
|
623
686
|
|
|
624
687
|
|
|
@@ -5,16 +5,22 @@ env prefix ``ACU_``, and the sole config file is ``.env`` (found by walking
|
|
|
5
5
|
up from cwd) carrying where + secrets as ``ACU_*`` vars. The file is
|
|
6
6
|
optional - flags plus the process environment can supply the full config.
|
|
7
7
|
Per key the first set value wins: flag, ``ACU_*`` var (process environment
|
|
8
|
-
over a found ``.env``), code default
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
over a found ``.env``), code default — exclusion ``api_version`` (V27/T125):
|
|
9
|
+
``--api-version`` flag ? → else data-repo ``target.yaml`` ``default_api``
|
|
10
|
+
when present → else code default ``25.200.001``; never ``ACU_API_VERSION``
|
|
11
|
+
(unknown ``ACU_*`` ignored). ``base_url`` is the only required address
|
|
12
|
+
(REST data plane). ``ssh`` defaults to ``Administrator@`` + the
|
|
13
|
+
``base_url`` hostname when the key is absent (V3/T124); a present blank
|
|
14
|
+
key is the hosted opt-out (empty = data-plane only; tenant cmds hard-error
|
|
15
|
+
when empty post-default — V1/V3). The password must resolve via
|
|
16
|
+
``--password`` or ``ACU_PASSWORD``.
|
|
12
17
|
"""
|
|
13
18
|
|
|
14
19
|
from collections.abc import Iterator, Mapping
|
|
15
20
|
from importlib import resources
|
|
16
21
|
from pathlib import Path
|
|
17
22
|
from typing import Any
|
|
23
|
+
from urllib.parse import urlparse
|
|
18
24
|
|
|
19
25
|
from pydantic import ValidationError, field_validator, model_validator
|
|
20
26
|
from pydantic_settings import BaseSettings, DotEnvSettingsSource, SettingsConfigDict
|
|
@@ -22,6 +28,8 @@ from pydantic_settings import BaseSettings, DotEnvSettingsSource, SettingsConfig
|
|
|
22
28
|
from .models import validation_summary
|
|
23
29
|
|
|
24
30
|
PLACEHOLDER_HOST = "erp.example.com"
|
|
31
|
+
DEFAULT_SSH_USER = "Administrator"
|
|
32
|
+
DEFAULT_API_VERSION = "25.200.001"
|
|
25
33
|
|
|
26
34
|
ACU_INSTANCE_NAME = "AcumaticaERP" # ac.exe -iname; IIS app-pool name
|
|
27
35
|
ACU_INSTANCE_PATH = "C:\\Acumatica\\AcumaticaERP" # ac.exe -h
|
|
@@ -124,13 +132,26 @@ INIT_TEMPLATES = (
|
|
|
124
132
|
)
|
|
125
133
|
|
|
126
134
|
|
|
135
|
+
def default_ssh_for_base_url(base_url: str | None) -> str:
|
|
136
|
+
"""``Administrator@<hostname>`` from a REST root, or empty when unparseable.
|
|
137
|
+
|
|
138
|
+
T124/V3 code default when ``ACU_SSH`` is absent. Blank key stays empty
|
|
139
|
+
(hosted opt-out) and never calls this.
|
|
140
|
+
"""
|
|
141
|
+
if not base_url:
|
|
142
|
+
return ""
|
|
143
|
+
host = urlparse(base_url).hostname
|
|
144
|
+
return f"{DEFAULT_SSH_USER}@{host}" if host else ""
|
|
145
|
+
|
|
146
|
+
|
|
127
147
|
class Instance(BaseSettings):
|
|
128
148
|
"""The resolved target: flags over ACU_* vars (.env or process) over defaults.
|
|
129
149
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
150
|
+
``base_url`` is the REST root (scheme + host + site path). ``ssh`` is the
|
|
151
|
+
control-plane ``user@host``: flag/env when the key is present win
|
|
152
|
+
(including blank = hosted/data-plane only); key absent → code default
|
|
153
|
+
``Administrator@`` + base_url hostname (V3/T124). Install-layout values
|
|
154
|
+
are module constants, not fields. Unknown ``ACU_*`` vars are ignored,
|
|
134
155
|
never errors - the environment and ``.env`` legitimately carry
|
|
135
156
|
non-config vars (``ACU_DEBUG``).
|
|
136
157
|
"""
|
|
@@ -142,9 +163,12 @@ class Instance(BaseSettings):
|
|
|
142
163
|
)
|
|
143
164
|
|
|
144
165
|
base_url: str # REST root: scheme + host + site path
|
|
145
|
-
ssh: str = "" # control plane: full user@host; empty = data-plane only
|
|
166
|
+
ssh: str = "" # control plane: full user@host; empty post-default = data-plane only
|
|
146
167
|
tenant: str = ""
|
|
147
|
-
|
|
168
|
+
# V11/V27: version half only; resolved in load_instance (flag → target →
|
|
169
|
+
# code default), never ACU_API_VERSION env. Default here is the code pin
|
|
170
|
+
# only; env values for this field are discarded post-build (T125).
|
|
171
|
+
api_version: str = DEFAULT_API_VERSION # V11: /entity/Default/<api_version>/
|
|
148
172
|
user: str = "admin" # ACU_USER; the --username flag maps here
|
|
149
173
|
# required, but enforced in load_instance so a blank scaffolded
|
|
150
174
|
# ACU_PASSWORD= placeholder and a missing var raise the same named error
|
|
@@ -152,13 +176,15 @@ class Instance(BaseSettings):
|
|
|
152
176
|
|
|
153
177
|
@model_validator(mode="before")
|
|
154
178
|
@classmethod
|
|
155
|
-
def
|
|
156
|
-
# blank ACU_BASE_URL=
|
|
157
|
-
#
|
|
179
|
+
def _blank_required_and_ssh_default(cls, data: Any) -> Any:
|
|
180
|
+
# blank ACU_BASE_URL= reads as unset (V3): base_url then fails required.
|
|
181
|
+
# ACU_SSH: key absent → Administrator@<base_url host>; key present blank
|
|
182
|
+
# stays empty (hosted opt-out) — do not strip blank ssh (T124).
|
|
158
183
|
if isinstance(data, dict):
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
184
|
+
if data.get("base_url") == "":
|
|
185
|
+
del data["base_url"]
|
|
186
|
+
if "ssh" not in data:
|
|
187
|
+
data["ssh"] = default_ssh_for_base_url(data.get("base_url"))
|
|
162
188
|
return data
|
|
163
189
|
|
|
164
190
|
@field_validator("base_url")
|
|
@@ -188,11 +214,12 @@ def scaffold(directory: Path, host: str | None = None) -> Iterator[tuple[str, Pa
|
|
|
188
214
|
"""Write the data-repo template set into ``directory``, never overwriting.
|
|
189
215
|
|
|
190
216
|
Yields ("write" | "skip", path) per template file. ``host`` replaces the
|
|
191
|
-
placeholder host inside the scaffolded .env ``ACU_BASE_URL
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
217
|
+
placeholder host inside the scaffolded .env ``ACU_BASE_URL`` only
|
|
218
|
+
(``ACU_SSH`` is omitted — defaults from the base_url host at resolve;
|
|
219
|
+
hosted opt-out = present blank ``ACU_SSH=``). Secrets stay placeholders
|
|
220
|
+
(V2). Single full seed under ``config/`` + lifecycle ``scenario/``
|
|
221
|
+
(V28/T108; no flavor). The directory is created if absent. No git init,
|
|
222
|
+
no gpg - version control and secret encryption stay the operator's call.
|
|
196
223
|
"""
|
|
197
224
|
pkg = resources.files("acumatica_cli") / "templates"
|
|
198
225
|
directory.mkdir(parents=True, exist_ok=True)
|
|
@@ -257,13 +284,27 @@ def load_instance(overrides: Mapping[str, str | None] | None = None) -> Instance
|
|
|
257
284
|
|
|
258
285
|
``overrides`` carries the global flags keyed by Instance field name;
|
|
259
286
|
per key the first set value wins (flag, ACU_* var - process environment
|
|
260
|
-
over a found .env - code default).
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
287
|
+
over a found .env - code default). Exclusion ``api_version`` (V27/T125):
|
|
288
|
+
``--api-version`` flag ? → else ``target.yaml`` ``default_api`` when
|
|
289
|
+
present → else ``DEFAULT_API_VERSION``; ``ACU_API_VERSION`` is ignored.
|
|
290
|
+
No .env is fine (V3): the hard error comes only when a required value
|
|
291
|
+
(base_url, password) is still unresolved after the merge, naming the
|
|
292
|
+
missing key. ``ssh`` defaults from the base_url host when the key is
|
|
293
|
+
absent; blank key = hosted path; tenant cmds hard-error when empty
|
|
294
|
+
post-default.
|
|
264
295
|
"""
|
|
265
296
|
flags = {k: v for k, v in dict(overrides or {}).items() if v is not None}
|
|
266
297
|
root = find_data_root()
|
|
298
|
+
# V27/T125: resolve api_version outside env. Init kwargs beat dotenv/env,
|
|
299
|
+
# so always inject — ACU_API_VERSION (valid or invalid) is ignored.
|
|
300
|
+
if "api_version" not in flags:
|
|
301
|
+
# Local import avoids config↔target cycle (target imports Instance).
|
|
302
|
+
from .target import load_target
|
|
303
|
+
|
|
304
|
+
target = load_target(root)
|
|
305
|
+
flags["api_version"] = (
|
|
306
|
+
target.default_api if target is not None else DEFAULT_API_VERSION
|
|
307
|
+
)
|
|
267
308
|
env_file = root / ".env" if root is not None else None
|
|
268
309
|
try:
|
|
269
310
|
# _env_file is a real BaseSettings init override; the synthesized
|
|
@@ -70,7 +70,7 @@ BOOTSTRAP_ENDPOINT, BOOTSTRAP_ENTITIES = bootstrap.parse_endpoint(
|
|
|
70
70
|
bootstrap.packaged_contract_xml()
|
|
71
71
|
)
|
|
72
72
|
# Dual-serve error text prefers the symbolic Default form (V20) so operators
|
|
73
|
-
# do not read the code-default version as their configured
|
|
73
|
+
# do not read the code-default version as their configured api_version.
|
|
74
74
|
_DEFAULT_ENDPOINT_NAME = "Default"
|
|
75
75
|
_SYMBOLIC_BOOTSTRAP = "bootstrap"
|
|
76
76
|
_SYMBOLIC_DEFAULT = "default"
|
|
@@ -113,7 +113,7 @@ def resolve_endpoint(
|
|
|
113
113
|
if not api_version:
|
|
114
114
|
raise SystemExit(
|
|
115
115
|
"endpoint: default requires a configured api_version "
|
|
116
|
-
"(
|
|
116
|
+
"(target.yaml default_api or --api-version)"
|
|
117
117
|
)
|
|
118
118
|
return f"{_DEFAULT_ENDPOINT_NAME}/{api_version}"
|
|
119
119
|
return endpoint
|
|
@@ -233,7 +233,7 @@ def load_baseline(path: Path) -> BaselineFile | ActionFile:
|
|
|
233
233
|
elif parsed.endpoint is None and parsed.entity in entities:
|
|
234
234
|
raise SystemExit(
|
|
235
235
|
f"{path}: entity '{parsed.entity}' is served by both "
|
|
236
|
-
f"Default (use endpoint: default -> Default/<
|
|
236
|
+
f"Default (use endpoint: default -> Default/<api_version>) "
|
|
237
237
|
f"and the active {name} - add an explicit 'endpoint:' line to "
|
|
238
238
|
f"pick one (literal or symbolic 'bootstrap' | 'default')"
|
|
239
239
|
)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Dataset target matrix: committed verified ERP + Default API versions (V27).
|
|
2
2
|
|
|
3
3
|
``target.yaml`` is *what* (V2) — co-located with the data-repo root found by
|
|
4
|
-
``.env`` discovery. Never secrets. Present →
|
|
5
|
-
``Instance.api_version``
|
|
6
|
-
|
|
4
|
+
``.env`` discovery. Never secrets. Present → ``load_instance`` sources
|
|
5
|
+
``Instance.api_version`` from ``default_api`` when ``--api-version`` is
|
|
6
|
+
absent (source-merge; dual-source match gate retired — T125); missing →
|
|
7
|
+
warn on check unless ``--strict``; invalid → hard-fail any loader.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from pathlib import Path
|
|
@@ -81,20 +82,13 @@ def load_target(root: Path | None = None) -> DatasetTarget | None:
|
|
|
81
82
|
|
|
82
83
|
|
|
83
84
|
def assert_target_compatible(inst: Instance, root: Path | None = None) -> None:
|
|
84
|
-
"""
|
|
85
|
+
"""Load target when present; invalid → hard-fail (V27).
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
Version mismatch gate retired (T125): ``load_instance`` source-merges
|
|
88
|
+
``default_api`` into ``api_version`` when the flag is absent; flag
|
|
89
|
+
override is intentional for ad-hoc probes. Missing target is not an
|
|
90
|
+
error here (config check --strict owns that). Call only from the
|
|
91
|
+
allowlisted data-plane commands — never from bare pass_instance.
|
|
89
92
|
"""
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
if target.default_api != inst.api_version:
|
|
94
|
-
raise SystemExit(
|
|
95
|
-
"Default API version mismatch:\n"
|
|
96
|
-
f" dataset target (target.yaml): default_api={target.default_api}\n"
|
|
97
|
-
f" configured (ACU_API_VERSION/--api-version): {inst.api_version}\n"
|
|
98
|
-
f"Fix: set ACU_API_VERSION={target.default_api} to match this "
|
|
99
|
-
f"dataset, or use a dataset verified for {inst.api_version}."
|
|
100
|
-
)
|
|
93
|
+
del inst # kept for call-site stability; version match no longer used
|
|
94
|
+
load_target(root)
|
|
@@ -8,7 +8,8 @@ Tenant Factory class). Single full seed — no `--flavor`.
|
|
|
8
8
|
## Rebuild order
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
# 1. Credentials in .env (ACU_PASSWORD, ACU_TENANT, …)
|
|
11
|
+
# 1. Credentials in .env (ACU_PASSWORD, ACU_TENANT, …)
|
|
12
|
+
# Default API pin = committed target.yaml default_api (not .env)
|
|
12
13
|
acu config check
|
|
13
14
|
|
|
14
15
|
# 2. Publish Bootstrap (features + contract from config/bootstrap/)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# scaffolded by `acu config init` - the sole config file: where + secrets,
|
|
2
|
+
# every key an ACU_* var; print the fully resolved document with:
|
|
3
|
+
# acu config show
|
|
4
|
+
# ACU_BASE_URL (REST root) is required. Control-plane SSH is omitted on
|
|
5
|
+
# purpose: when the key is absent, acu defaults to Administrator@ plus the
|
|
6
|
+
# ACU_BASE_URL hostname (SSH boxes). Hosted / data-plane-only opt-out: add
|
|
7
|
+
# the SSH key with an empty value. Tenant CRUD still needs a non-empty
|
|
8
|
+
# value post-default.
|
|
9
|
+
# Default contract API pin lives in committed target.yaml (default_api),
|
|
10
|
+
# not here — never ACU_API_VERSION (see acu config check / --api-version).
|
|
11
|
+
# Keep this file out of git (the scaffolded .gitignore already lists it;
|
|
12
|
+
# encrypt at rest, e.g. .env.gpg).
|
|
13
|
+
ACU_BASE_URL=http://erp.example.com/AcumaticaERP
|
|
14
|
+
ACU_TENANT=
|
|
15
|
+
ACU_USER=admin
|
|
16
|
+
ACU_PASSWORD=
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# scaffolded by `acu config init` — verified target matrix for this dataset.
|
|
2
|
+
# Edit when you re-verify against a new ERP build or Default API generation.
|
|
3
|
+
# default_api is the sole data-repo Default contract pin (version half only,
|
|
4
|
+
# never Default/<ver>). It sources Instance.api_version when --api-version is
|
|
5
|
+
# absent. There is no ACU_API_VERSION env pin.
|
|
6
|
+
erp: "26.101.0225"
|
|
7
|
+
default_api: "25.200.001"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# scaffolded by `acu config init` - the sole config file: where + secrets,
|
|
2
|
-
# every key an ACU_* var; print the fully resolved document with:
|
|
3
|
-
# acu config show
|
|
4
|
-
# ACU_BASE_URL (REST root) is required; ACU_SSH (control-plane user@host)
|
|
5
|
-
# is optional - leave blank for hosted / data-plane-only (tenant CRUD still
|
|
6
|
-
# needs it). The rest are code defaults you can override here.
|
|
7
|
-
# ACU_API_VERSION is the Default contract version half only (e.g. 25.200.001),
|
|
8
|
-
# never "Default/25.200.001". Must match target.yaml default_api when that
|
|
9
|
-
# file exists (see acu config check).
|
|
10
|
-
# Keep this file out of git (the scaffolded .gitignore already lists it;
|
|
11
|
-
# encrypt at rest, e.g. .env.gpg).
|
|
12
|
-
ACU_BASE_URL=http://erp.example.com/AcumaticaERP
|
|
13
|
-
ACU_SSH=Administrator@erp.example.com
|
|
14
|
-
ACU_TENANT=
|
|
15
|
-
ACU_API_VERSION=25.200.001
|
|
16
|
-
ACU_USER=admin
|
|
17
|
-
ACU_PASSWORD=
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
# scaffolded by `acu config init` — verified target matrix for this dataset.
|
|
2
|
-
# Edit when you re-verify against a new ERP build or Default API generation.
|
|
3
|
-
# default_api is the version half only (ACU_API_VERSION spelling), not Default/<ver>.
|
|
4
|
-
# Must match ACU_API_VERSION / --api-version (default 25.200.001) or live cmds fail.
|
|
5
|
-
erp: "26.101.0225"
|
|
6
|
-
default_api: "25.200.001"
|
|
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.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/scenario/20-buy.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/scenario/30-build.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.15.2 → acumatica_cli-0.15.4}/src/acumatica_cli/templates/scenario/40-sell.yaml
RENAMED
|
File without changes
|
|
File without changes
|