acumatica-cli 0.7.0__tar.gz → 0.8.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.7.0 → acumatica_cli-0.8.1}/PKG-INFO +13 -22
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/README.md +11 -20
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/pyproject.toml +4 -4
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/bootstrap.py +1 -1
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/bootstrap_project.xml +106 -5
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/cli.py +15 -10
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/client.py +1 -1
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/config.py +16 -13
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/extract.py +2 -2
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/output.py +25 -2
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/run.py +63 -10
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/seed.py +3 -3
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/baseline/50-gl-preferences.yaml +2 -2
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/baseline/60-ledger-company.yaml +2 -2
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/bootstrap/company.yaml +2 -2
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/bootstrap/credit-terms.yaml +2 -2
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/env +3 -2
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/setup/10-financial-year.yaml +1 -1
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/setup/20-master-calendar.yaml +1 -1
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/setup/30-open-periods.yaml +1 -1
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/tenant.py +8 -0
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/extract_manifest.yaml +0 -0
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/gitignore +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: acumatica-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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>
|
|
@@ -10,7 +10,7 @@ Requires-Dist: pydantic>=2.7
|
|
|
10
10
|
Requires-Dist: pydantic-settings>=2.5
|
|
11
11
|
Requires-Dist: pyyaml>=6.0
|
|
12
12
|
Requires-Dist: rich>=13
|
|
13
|
-
Requires-Python: >=3.
|
|
13
|
+
Requires-Python: >=3.14
|
|
14
14
|
Project-URL: Homepage, https://lab5.ca
|
|
15
15
|
Project-URL: Repository, https://github.com/kborovik/acumatica-cli
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
@@ -21,22 +21,6 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
**No UI clicks, no Configuration Wizard.**
|
|
23
23
|
|
|
24
|
-
## How it works
|
|
25
|
-
|
|
26
|
-
1. **Extract** the target Acumatica deployment with `acu extract` — its configuration becomes YAML, config as code.
|
|
27
|
-
2. **Develop** changes in the YAML, in git: edit, review, version.
|
|
28
|
-
3. **Re-deploy** with `acu apply`, then prove the live tenant matches the code with `acu diff`.
|
|
29
|
-
|
|
30
|
-
Four commands do the work, and every one is safe to re-run:
|
|
31
|
-
|
|
32
|
-
- **`acu tenant create`** — creates a tenant and bootstraps it in one step, ready for `apply`.
|
|
33
|
-
Re-running it against an existing tenant republishes the bootstrap package instead of failing.
|
|
34
|
-
- **`acu apply`** — pushes your YAML into the tenant as keyed upserts.
|
|
35
|
-
Running it twice changes nothing.
|
|
36
|
-
- **`acu diff`** — compares your YAML against the live tenant and exits with code 2 on drift.
|
|
37
|
-
- **`acu extract`** — reads a configured tenant and writes the YAML file set back out, the inverse of `apply`.
|
|
38
|
-
Existing files are skipped unless `--force`.
|
|
39
|
-
|
|
40
24
|
> **Tested against** Acumatica ERP **26.101.0225** on Windows Server 2025,
|
|
41
25
|
> contract REST endpoint **25.200.001**. Other versions will likely work,
|
|
42
26
|
> but only this combination is verified.
|
|
@@ -75,6 +59,7 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
75
59
|
│
|
|
76
60
|
├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
|
|
77
61
|
├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
|
|
62
|
+
├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
|
|
78
63
|
├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
|
|
79
64
|
│ dump live tenant state as seed YAML (inverse of apply)
|
|
80
65
|
├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
|
|
@@ -86,25 +71,31 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
86
71
|
```
|
|
87
72
|
|
|
88
73
|
`apply` and `diff` called without FILES default to the scaffolded directories, in order: `bootstrap/`, then `baseline/`, then `setup/`.
|
|
74
|
+
`run` called without FILES defaults to `scenario/`.
|
|
89
75
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
90
76
|
Run `acu <command> --help` for details on any command.
|
|
91
77
|
|
|
92
78
|
## The data repo
|
|
93
79
|
|
|
94
|
-
Your configuration lives in its own git repo
|
|
80
|
+
Your configuration lives in its own git repo. `acu config init` scaffolds everything except `scenario/`, which you author by hand:
|
|
95
81
|
|
|
96
82
|
| Path | What it holds |
|
|
97
83
|
| ------------ | -------------------------------------------------------------------------- |
|
|
98
84
|
| `bootstrap/` | what makes a virgin tenant configurable: features, company, credit terms |
|
|
99
85
|
| `baseline/` | reference data: subaccounts, chart of accounts, ledger, units of measure |
|
|
100
86
|
| `setup/` | one-time actions: financial year, master calendar, open periods |
|
|
87
|
+
| `scenario/` | transaction scenarios for `acu run`: purchase, build, sell flows |
|
|
101
88
|
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
102
89
|
|
|
103
90
|
Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order. The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
|
|
104
91
|
|
|
92
|
+
Seed YAML in `bootstrap/`, `baseline/`, and `setup/` is state: `apply` upserts it, `diff` proves it.
|
|
93
|
+
Scenario YAML is different — it describes transactions that flow forward.
|
|
94
|
+
`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 a scenario re-runs safely on a warm tenant.
|
|
95
|
+
|
|
105
96
|
## Installation
|
|
106
97
|
|
|
107
|
-
Requires Python 3.
|
|
98
|
+
Requires Python 3.14 or newer.
|
|
108
99
|
|
|
109
100
|
```sh
|
|
110
101
|
uv tool install acumatica-cli
|
|
@@ -148,9 +139,9 @@ acu apply --dry-run # show what would be written, write nothing
|
|
|
148
139
|
`acu` talks to an instance over two independent channels:
|
|
149
140
|
|
|
150
141
|
- **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).
|
|
151
|
-
- **Data plane (REST):** `acu apply`, `diff`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
|
|
142
|
+
- **Data plane (REST):** `acu apply`, `diff`, `run`, `extract`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
|
|
152
143
|
|
|
153
|
-
If you
|
|
144
|
+
If you never touch `acu tenant`, you never need SSH — every other command is pure REST.
|
|
154
145
|
|
|
155
146
|
## SSH setup (control plane)
|
|
156
147
|
|
|
@@ -4,22 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
**No UI clicks, no Configuration Wizard.**
|
|
6
6
|
|
|
7
|
-
## How it works
|
|
8
|
-
|
|
9
|
-
1. **Extract** the target Acumatica deployment with `acu extract` — its configuration becomes YAML, config as code.
|
|
10
|
-
2. **Develop** changes in the YAML, in git: edit, review, version.
|
|
11
|
-
3. **Re-deploy** with `acu apply`, then prove the live tenant matches the code with `acu diff`.
|
|
12
|
-
|
|
13
|
-
Four commands do the work, and every one is safe to re-run:
|
|
14
|
-
|
|
15
|
-
- **`acu tenant create`** — creates a tenant and bootstraps it in one step, ready for `apply`.
|
|
16
|
-
Re-running it against an existing tenant republishes the bootstrap package instead of failing.
|
|
17
|
-
- **`acu apply`** — pushes your YAML into the tenant as keyed upserts.
|
|
18
|
-
Running it twice changes nothing.
|
|
19
|
-
- **`acu diff`** — compares your YAML against the live tenant and exits with code 2 on drift.
|
|
20
|
-
- **`acu extract`** — reads a configured tenant and writes the YAML file set back out, the inverse of `apply`.
|
|
21
|
-
Existing files are skipped unless `--force`.
|
|
22
|
-
|
|
23
7
|
> **Tested against** Acumatica ERP **26.101.0225** on Windows Server 2025,
|
|
24
8
|
> contract REST endpoint **25.200.001**. Other versions will likely work,
|
|
25
9
|
> but only this combination is verified.
|
|
@@ -58,6 +42,7 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
58
42
|
│
|
|
59
43
|
├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
|
|
60
44
|
├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
|
|
45
|
+
├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
|
|
61
46
|
├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
|
|
62
47
|
│ dump live tenant state as seed YAML (inverse of apply)
|
|
63
48
|
├── schema [--out DIR] dump the endpoint's OpenAPI schema (swagger.json)
|
|
@@ -69,25 +54,31 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
69
54
|
```
|
|
70
55
|
|
|
71
56
|
`apply` and `diff` called without FILES default to the scaffolded directories, in order: `bootstrap/`, then `baseline/`, then `setup/`.
|
|
57
|
+
`run` called without FILES defaults to `scenario/`.
|
|
72
58
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
73
59
|
Run `acu <command> --help` for details on any command.
|
|
74
60
|
|
|
75
61
|
## The data repo
|
|
76
62
|
|
|
77
|
-
Your configuration lives in its own git repo
|
|
63
|
+
Your configuration lives in its own git repo. `acu config init` scaffolds everything except `scenario/`, which you author by hand:
|
|
78
64
|
|
|
79
65
|
| Path | What it holds |
|
|
80
66
|
| ------------ | -------------------------------------------------------------------------- |
|
|
81
67
|
| `bootstrap/` | what makes a virgin tenant configurable: features, company, credit terms |
|
|
82
68
|
| `baseline/` | reference data: subaccounts, chart of accounts, ledger, units of measure |
|
|
83
69
|
| `setup/` | one-time actions: financial year, master calendar, open periods |
|
|
70
|
+
| `scenario/` | transaction scenarios for `acu run`: purchase, build, sell flows |
|
|
84
71
|
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
85
72
|
|
|
86
73
|
Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order. The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
|
|
87
74
|
|
|
75
|
+
Seed YAML in `bootstrap/`, `baseline/`, and `setup/` is state: `apply` upserts it, `diff` proves it.
|
|
76
|
+
Scenario YAML is different — it describes transactions that flow forward.
|
|
77
|
+
`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 a scenario re-runs safely on a warm tenant.
|
|
78
|
+
|
|
88
79
|
## Installation
|
|
89
80
|
|
|
90
|
-
Requires Python 3.
|
|
81
|
+
Requires Python 3.14 or newer.
|
|
91
82
|
|
|
92
83
|
```sh
|
|
93
84
|
uv tool install acumatica-cli
|
|
@@ -131,9 +122,9 @@ acu apply --dry-run # show what would be written, write nothing
|
|
|
131
122
|
`acu` talks to an instance over two independent channels:
|
|
132
123
|
|
|
133
124
|
- **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).
|
|
134
|
-
- **Data plane (REST):** `acu apply`, `diff`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
|
|
125
|
+
- **Data plane (REST):** `acu apply`, `diff`, `run`, `extract`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
|
|
135
126
|
|
|
136
|
-
If you
|
|
127
|
+
If you never touch `acu tenant`, you never need SSH — every other command is pure REST.
|
|
137
128
|
|
|
138
129
|
## SSH setup (control plane)
|
|
139
130
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "acumatica-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.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
|
readme = "README.md"
|
|
7
|
-
requires-python = ">=3.
|
|
7
|
+
requires-python = ">=3.14"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"click>=8.1",
|
|
10
10
|
"httpx>=0.27",
|
|
@@ -41,7 +41,7 @@ markers = [
|
|
|
41
41
|
|
|
42
42
|
[tool.basedpyright]
|
|
43
43
|
typeCheckingMode = "strict"
|
|
44
|
-
pythonVersion = "3.
|
|
44
|
+
pythonVersion = "3.14"
|
|
45
45
|
include = ["src/acumatica_cli/", "tests/"]
|
|
46
46
|
reportMissingTypeStubs = false
|
|
47
47
|
reportUnknownVariableType = false
|
|
@@ -52,7 +52,7 @@ reportUnknownLambdaType = false
|
|
|
52
52
|
reportUnusedFunction = "warning"
|
|
53
53
|
|
|
54
54
|
[tool.ruff]
|
|
55
|
-
target-version = "
|
|
55
|
+
target-version = "py314"
|
|
56
56
|
src = ["src"]
|
|
57
57
|
|
|
58
58
|
[tool.ruff.format]
|
|
@@ -150,7 +150,7 @@ def _published_description(client: AcumaticaClient) -> str | None:
|
|
|
150
150
|
try:
|
|
151
151
|
with zipfile.ZipFile(io.BytesIO(content)) as zf:
|
|
152
152
|
root = ET.fromstring(zf.read("project.xml"))
|
|
153
|
-
except
|
|
153
|
+
except zipfile.BadZipFile, KeyError, ET.ParseError:
|
|
154
154
|
return None
|
|
155
155
|
return root.get("description")
|
|
156
156
|
|
|
@@ -157,7 +157,12 @@
|
|
|
157
157
|
fields with no server default (read off the LAB5 PUT
|
|
158
158
|
2026-07-13): INProgressAcctID/INProgressSubID (the WIP
|
|
159
159
|
account) + the four reason codes (Issues/Receipt/
|
|
160
|
-
Adjustment/PI) - all exposed; HoldEntry = the echo flag
|
|
160
|
+
Adjustment/PI) - all exposed; HoldEntry = the echo flag;
|
|
161
|
+
INTransitAcctID/INTransitSubID join once the Warehouse
|
|
162
|
+
feature is on (update 500s "'In-Transit Account' cannot
|
|
163
|
+
be empty", P5 probe); UpdateGL defaults OFF - released
|
|
164
|
+
IN documents move quantities but post no GL (COGS and
|
|
165
|
+
PO accrual sat at zero while revenue moved, P5 probe).
|
|
161
166
|
- ReasonCode CS211000 -> ReasonCodeMaint, PrimaryView reasoncode
|
|
162
167
|
(PX.Objects.CS.ReasonCode) - no Default-endpoint entity,
|
|
163
168
|
and the INSetup reason-code fields above hard-require
|
|
@@ -169,11 +174,18 @@
|
|
|
169
174
|
(ARSetup); HoldEntry echo field.
|
|
170
175
|
- SOPreferences SO101000 -> SOSetupMaint, PrimaryView sosetup (SOSetup);
|
|
171
176
|
DefaultOrderType doubles as the echo field and the R1
|
|
172
|
-
probe (does order type SO pre-exist on an empty tenant)
|
|
177
|
+
probe (does order type SO pre-exist on an empty tenant);
|
|
178
|
+
HoldShipments/RequireShipmentTotal off + AutoReleaseIN on
|
|
179
|
+
keep the shipment->invoice chain scriptable (template
|
|
180
|
+
defaults park shipments On Hold and refuse confirmation
|
|
181
|
+
without a control total - P5 probes).
|
|
173
182
|
- POPreferences PO101000 -> POSetupMaint, PrimaryView Setup (POSetup);
|
|
174
183
|
HoldReceipts echo field + RCReturnReasonCodeID (the one
|
|
175
184
|
POSetup field the live insert named as required - a
|
|
176
|
-
Vendor Return usage reason code, 2026-07-13)
|
|
185
|
+
Vendor Return usage reason code, 2026-07-13) +
|
|
186
|
+
AutoReleaseIN (default off: a released PO receipt leaves
|
|
187
|
+
its IN document Balanced and qty never lands on hand -
|
|
188
|
+
P5 probe).
|
|
177
189
|
- AvailabilityCalculationRule IN201500 -> INAvailabilitySchemeMaint,
|
|
178
190
|
PrimaryView Schemes (INAvailabilityScheme) - an empty
|
|
179
191
|
tenant has zero scheme rows and ItemClass 422s
|
|
@@ -209,6 +221,23 @@
|
|
|
209
221
|
fixed-day-of-month choice; the AgeDays00-02 aging bounds
|
|
210
222
|
default inconsistently at raw insert ("end day earlier
|
|
211
223
|
than its start day" 422) so the seed supplies them.
|
|
224
|
+
- Warehouse IN204000 -> INSiteMaint, PrimaryView site (DAC
|
|
225
|
+
PX.Objects.IN.INSite) - the Default Warehouse entity has
|
|
226
|
+
no address surface, and the auto-created warehouse
|
|
227
|
+
Address 422s "'Country' cannot be empty" at insert (P5
|
|
228
|
+
probe 2026-07-13; the T65 vendor class, but Default has
|
|
229
|
+
no path). SiteCD/Descr/Active on the primary view,
|
|
230
|
+
CountryID through the Address view.
|
|
231
|
+
- OrderType SO201000 -> SOOrderTypeMaint, PrimaryView soordertype
|
|
232
|
+
(DAC PX.Objects.SO.SOOrderType) - no Default-endpoint
|
|
233
|
+
entity, and a fresh tenant has ZERO order-type rows
|
|
234
|
+
(they live in the template company, CompanyID 1,
|
|
235
|
+
Active=0 - sqlcmd-verified 2026-07-13): a SalesOrder
|
|
236
|
+
PUT answers 200 with an UNSAVED row (OrderNbr stays
|
|
237
|
+
<SELECT>, CustomerID silently empty) because numbering
|
|
238
|
+
never resolves. Keyed PUT on OrderType materializes the
|
|
239
|
+
template row into the tenant; Active activates it; the
|
|
240
|
+
freight pair is required at persist (422, no default).
|
|
212
241
|
- CashAccount CA202000 -> CashAccountMaint, PrimaryView CashAccount
|
|
213
242
|
(PX.Objects.CA.CashAccount) - Default endpoint answers
|
|
214
243
|
404. CuryID stays out (derived from the GL account,
|
|
@@ -218,9 +247,9 @@
|
|
|
218
247
|
Features (CS100000) stays OUT of the endpoint - writes through any
|
|
219
248
|
contract endpoint do not persist (T3 verdict); the plugin owns features.
|
|
220
249
|
-->
|
|
221
|
-
<Customization level="" description="acu bootstrap: CustomizationPlugin enables features on publish; Bootstrap endpoint exposes company + credit terms + financial currency + GL preferences + ledger-company link + financial year + calendars + period activation + module preferences (IN/AP/AR/SO/PO) + availability rule + posting class + cash account + vendor class + statement cycle" product-version="26.101">
|
|
250
|
+
<Customization level="" description="acu bootstrap: CustomizationPlugin enables features on publish; Bootstrap endpoint exposes company + credit terms + financial currency + GL preferences + ledger-company link + financial year + calendars + period activation + module preferences (IN/AP/AR/SO/PO) + availability rule + posting class + cash account + vendor class + statement cycle + warehouse + order type" product-version="26.101">
|
|
222
251
|
<EntityEndpoint>
|
|
223
|
-
<Endpoint xmlns="http://www.acumatica.com/entity/maintenance/5.31" name="Bootstrap" version="1.
|
|
252
|
+
<Endpoint xmlns="http://www.acumatica.com/entity/maintenance/5.31" name="Bootstrap" version="1.7.0" systemContractVersion="4">
|
|
224
253
|
<TopLevelEntity name="Company" screen="CS101500">
|
|
225
254
|
<Fields>
|
|
226
255
|
<Field name="AcctCD" type="StringValue" />
|
|
@@ -532,6 +561,9 @@
|
|
|
532
561
|
<Field name="ReceiptReasonCode" type="StringValue" />
|
|
533
562
|
<Field name="AdjustmentReasonCode" type="StringValue" />
|
|
534
563
|
<Field name="PIReasonCode" type="StringValue" />
|
|
564
|
+
<Field name="INTransitAcctID" type="StringValue" />
|
|
565
|
+
<Field name="INTransitSubID" type="StringValue" />
|
|
566
|
+
<Field name="UpdateGL" type="BooleanValue" />
|
|
535
567
|
</Fields>
|
|
536
568
|
<Mappings>
|
|
537
569
|
<Mapping field="HoldEntry">
|
|
@@ -555,6 +587,15 @@
|
|
|
555
587
|
<Mapping field="PIReasonCode">
|
|
556
588
|
<To object="setup" field="PIReasonCode" />
|
|
557
589
|
</Mapping>
|
|
590
|
+
<Mapping field="INTransitAcctID">
|
|
591
|
+
<To object="setup" field="INTransitAcctID" />
|
|
592
|
+
</Mapping>
|
|
593
|
+
<Mapping field="INTransitSubID">
|
|
594
|
+
<To object="setup" field="INTransitSubID" />
|
|
595
|
+
</Mapping>
|
|
596
|
+
<Mapping field="UpdateGL">
|
|
597
|
+
<To object="setup" field="UpdateGL" />
|
|
598
|
+
</Mapping>
|
|
558
599
|
</Mappings>
|
|
559
600
|
</TopLevelEntity>
|
|
560
601
|
<TopLevelEntity name="ReasonCode" screen="CS211000">
|
|
@@ -606,22 +647,38 @@
|
|
|
606
647
|
<TopLevelEntity name="SOPreferences" screen="SO101000">
|
|
607
648
|
<Fields>
|
|
608
649
|
<Field name="DefaultOrderType" type="StringValue" />
|
|
650
|
+
<Field name="HoldShipments" type="BooleanValue" />
|
|
651
|
+
<Field name="RequireShipmentTotal" type="BooleanValue" />
|
|
652
|
+
<Field name="AutoReleaseIN" type="BooleanValue" />
|
|
609
653
|
</Fields>
|
|
610
654
|
<Mappings>
|
|
611
655
|
<Mapping field="DefaultOrderType">
|
|
612
656
|
<To object="sosetup" field="DefaultOrderType" />
|
|
613
657
|
</Mapping>
|
|
658
|
+
<Mapping field="HoldShipments">
|
|
659
|
+
<To object="sosetup" field="HoldShipments" />
|
|
660
|
+
</Mapping>
|
|
661
|
+
<Mapping field="RequireShipmentTotal">
|
|
662
|
+
<To object="sosetup" field="RequireShipmentTotal" />
|
|
663
|
+
</Mapping>
|
|
664
|
+
<Mapping field="AutoReleaseIN">
|
|
665
|
+
<To object="sosetup" field="AutoReleaseIN" />
|
|
666
|
+
</Mapping>
|
|
614
667
|
</Mappings>
|
|
615
668
|
</TopLevelEntity>
|
|
616
669
|
<TopLevelEntity name="POPreferences" screen="PO101000">
|
|
617
670
|
<Fields>
|
|
618
671
|
<Field name="HoldReceipts" type="BooleanValue" />
|
|
619
672
|
<Field name="RCReturnReasonCodeID" type="StringValue" />
|
|
673
|
+
<Field name="AutoReleaseIN" type="BooleanValue" />
|
|
620
674
|
</Fields>
|
|
621
675
|
<Mappings>
|
|
622
676
|
<Mapping field="HoldReceipts">
|
|
623
677
|
<To object="Setup" field="HoldReceipts" />
|
|
624
678
|
</Mapping>
|
|
679
|
+
<Mapping field="AutoReleaseIN">
|
|
680
|
+
<To object="Setup" field="AutoReleaseIN" />
|
|
681
|
+
</Mapping>
|
|
625
682
|
<Mapping field="RCReturnReasonCodeID">
|
|
626
683
|
<To object="Setup" field="RCReturnReasonCodeID" />
|
|
627
684
|
</Mapping>
|
|
@@ -861,6 +918,50 @@
|
|
|
861
918
|
</Mapping>
|
|
862
919
|
</Mappings>
|
|
863
920
|
</TopLevelEntity>
|
|
921
|
+
<TopLevelEntity name="Warehouse" screen="IN204000">
|
|
922
|
+
<Fields>
|
|
923
|
+
<Field name="SiteCD" type="StringValue" />
|
|
924
|
+
<Field name="Descr" type="StringValue" />
|
|
925
|
+
<Field name="Active" type="BooleanValue" />
|
|
926
|
+
<Field name="CountryID" type="StringValue" />
|
|
927
|
+
</Fields>
|
|
928
|
+
<Mappings>
|
|
929
|
+
<Mapping field="SiteCD">
|
|
930
|
+
<To object="site" field="SiteCD" />
|
|
931
|
+
</Mapping>
|
|
932
|
+
<Mapping field="Descr">
|
|
933
|
+
<To object="site" field="Descr" />
|
|
934
|
+
</Mapping>
|
|
935
|
+
<Mapping field="Active">
|
|
936
|
+
<To object="site" field="Active" />
|
|
937
|
+
</Mapping>
|
|
938
|
+
<Mapping field="CountryID">
|
|
939
|
+
<To object="Address" field="CountryID" />
|
|
940
|
+
</Mapping>
|
|
941
|
+
</Mappings>
|
|
942
|
+
</TopLevelEntity>
|
|
943
|
+
<TopLevelEntity name="OrderType" screen="SO201000">
|
|
944
|
+
<Fields>
|
|
945
|
+
<Field name="OrderType" type="StringValue" />
|
|
946
|
+
<Field name="Active" type="BooleanValue" />
|
|
947
|
+
<Field name="FreightAcctID" type="StringValue" />
|
|
948
|
+
<Field name="FreightSubID" type="StringValue" />
|
|
949
|
+
</Fields>
|
|
950
|
+
<Mappings>
|
|
951
|
+
<Mapping field="OrderType">
|
|
952
|
+
<To object="soordertype" field="OrderType" />
|
|
953
|
+
</Mapping>
|
|
954
|
+
<Mapping field="Active">
|
|
955
|
+
<To object="soordertype" field="Active" />
|
|
956
|
+
</Mapping>
|
|
957
|
+
<Mapping field="FreightAcctID">
|
|
958
|
+
<To object="soordertype" field="FreightAcctID" />
|
|
959
|
+
</Mapping>
|
|
960
|
+
<Mapping field="FreightSubID">
|
|
961
|
+
<To object="soordertype" field="FreightSubID" />
|
|
962
|
+
</Mapping>
|
|
963
|
+
</Mappings>
|
|
964
|
+
</TopLevelEntity>
|
|
864
965
|
<TopLevelEntity name="CashAccount" screen="CA202000">
|
|
865
966
|
<Fields>
|
|
866
967
|
<Field name="CashAccountCD" type="StringValue" />
|
|
@@ -379,12 +379,13 @@ def config_check(ctx: click.Context) -> None:
|
|
|
379
379
|
|
|
380
380
|
Dependency order: discovery (.env walk-up + parse + ACU_BASE_URL), then
|
|
381
381
|
secrets (ACU_PASSWORD resolved), then REST (login, landed-tenant verify,
|
|
382
|
-
logout) and ssh
|
|
383
|
-
|
|
382
|
+
logout) and ssh probed independently - ssh set → trivial remote; ssh
|
|
383
|
+
unset → skip (ACU_SSH optional, V3 hosted path), never fail. A discovery
|
|
384
|
+
or secrets failure stops; a REST failure still probes ssh when set and
|
|
384
385
|
vice versa. Discovery is lax (V3): no .env passes when --url covers
|
|
385
386
|
base_url, and flags-only runs (no .env anywhere) are valid. Writes
|
|
386
|
-
nothing: no PUTs, no tenant CRUD. Exit 0 when every probe
|
|
387
|
-
any failure.
|
|
387
|
+
nothing: no PUTs, no tenant CRUD. Exit 0 when every non-skipped probe
|
|
388
|
+
passes, 1 on any failure.
|
|
388
389
|
"""
|
|
389
390
|
overrides: dict[str, str] = ctx.obj or {}
|
|
390
391
|
# discovery (V3): lax walk-up + parse; base_url (the primary identity
|
|
@@ -432,12 +433,16 @@ def config_check(ctx: click.Context) -> None:
|
|
|
432
433
|
except (RuntimeError, httpx.HTTPError) as exc:
|
|
433
434
|
output.data(f"fail rest: {exc}")
|
|
434
435
|
failed = True
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
output.data(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
436
|
+
if not inst.ssh:
|
|
437
|
+
# V3/I.cmd: ACU_SSH optional — hosted data-plane path skips, never fails
|
|
438
|
+
output.data("skip ssh (ACU_SSH not set)")
|
|
439
|
+
else:
|
|
440
|
+
try:
|
|
441
|
+
TenantManager(inst).ping()
|
|
442
|
+
output.data(f"ok ssh ({inst.ssh})")
|
|
443
|
+
except RuntimeError as exc:
|
|
444
|
+
output.data(f"fail ssh: {exc}")
|
|
445
|
+
failed = True
|
|
441
446
|
if failed:
|
|
442
447
|
raise SystemExit(1)
|
|
443
448
|
|
|
@@ -94,7 +94,7 @@ class AcumaticaClient:
|
|
|
94
94
|
base_url=instance.base_url, timeout=timeout, transport=transport
|
|
95
95
|
)
|
|
96
96
|
|
|
97
|
-
def __enter__(self) ->
|
|
97
|
+
def __enter__(self) -> AcumaticaClient:
|
|
98
98
|
# tenant guard (V5, docs/rest-api.md): an omitted or empty tenant is
|
|
99
99
|
# the one login the server still routes silently — to the default
|
|
100
100
|
# tenant. Defense-in-depth vs wrong-tenant writes: every data-plane
|
|
@@ -5,9 +5,10 @@ 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. ``base_url``
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
over a found ``.env``), code default. ``base_url`` is the only required
|
|
9
|
+
address (REST data plane); ``ssh`` is optional control-plane address
|
|
10
|
+
(empty = data-plane only; tenant cmds hard-error when unresolved — V1/V3).
|
|
11
|
+
The password must resolve via ``--password`` or ``ACU_PASSWORD``.
|
|
11
12
|
"""
|
|
12
13
|
|
|
13
14
|
from collections.abc import Iterator, Mapping
|
|
@@ -51,11 +52,12 @@ INIT_TEMPLATES = (
|
|
|
51
52
|
class Instance(BaseSettings):
|
|
52
53
|
"""The resolved target: flags over ACU_* vars (.env or process) over defaults.
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
``user@host
|
|
57
|
-
Unknown ``ACU_*`` vars are ignored,
|
|
58
|
-
``.env`` legitimately carry
|
|
55
|
+
Explicit addresses, no derivation (V1): ``base_url`` is the REST root
|
|
56
|
+
(scheme + host + site path); ``ssh`` is the optional control-plane
|
|
57
|
+
``user@host`` (empty = data-plane only). Install-layout values are
|
|
58
|
+
module constants, not fields. Unknown ``ACU_*`` vars are ignored,
|
|
59
|
+
never errors - the environment and ``.env`` legitimately carry
|
|
60
|
+
non-config vars (``ACU_DEBUG``).
|
|
59
61
|
"""
|
|
60
62
|
|
|
61
63
|
model_config = SettingsConfigDict(
|
|
@@ -65,7 +67,7 @@ class Instance(BaseSettings):
|
|
|
65
67
|
)
|
|
66
68
|
|
|
67
69
|
base_url: str # REST root: scheme + host + site path
|
|
68
|
-
ssh: str # control plane: full user@host
|
|
70
|
+
ssh: str = "" # control plane: full user@host; empty = data-plane only
|
|
69
71
|
tenant: str = ""
|
|
70
72
|
api_version: str = "25.200.001" # V11: /entity/Default/<api_version>/
|
|
71
73
|
user: str = "admin" # ACU_USER; the --username flag maps here
|
|
@@ -76,8 +78,8 @@ class Instance(BaseSettings):
|
|
|
76
78
|
@model_validator(mode="before")
|
|
77
79
|
@classmethod
|
|
78
80
|
def _blank_required_is_unset(cls, data: Any) -> Any:
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
+
# blank ACU_BASE_URL= / ACU_SSH= reads as unset (V3): base_url then
|
|
82
|
+
# fails required; ssh falls through to the empty default (optional)
|
|
81
83
|
if isinstance(data, dict):
|
|
82
84
|
for key in ("base_url", "ssh"):
|
|
83
85
|
if data.get(key) == "":
|
|
@@ -161,8 +163,9 @@ def load_instance(overrides: Mapping[str, str | None] | None = None) -> Instance
|
|
|
161
163
|
``overrides`` carries the global flags keyed by Instance field name;
|
|
162
164
|
per key the first set value wins (flag, ACU_* var - process environment
|
|
163
165
|
over a found .env - code default). No .env is fine (V3): the hard error
|
|
164
|
-
comes only when a required value (base_url,
|
|
165
|
-
unresolved after the merge, naming the missing key.
|
|
166
|
+
comes only when a required value (base_url, password) is still
|
|
167
|
+
unresolved after the merge, naming the missing key. ``ssh`` is optional
|
|
168
|
+
(hosted / data-plane-only path); tenant cmds hard-error when it is empty.
|
|
166
169
|
"""
|
|
167
170
|
flags = {k: v for k, v in dict(overrides or {}).items() if v is not None}
|
|
168
171
|
root = find_data_root()
|
|
@@ -66,7 +66,7 @@ class EntitySpec(Model):
|
|
|
66
66
|
return BOOTSTRAP_ENDPOINT if v == "bootstrap" else v
|
|
67
67
|
|
|
68
68
|
@model_validator(mode="after")
|
|
69
|
-
def _strip_include_exclusive(self) ->
|
|
69
|
+
def _strip_include_exclusive(self) -> EntitySpec:
|
|
70
70
|
if self.strip and self.include:
|
|
71
71
|
raise ValueError("strip and include are mutually exclusive")
|
|
72
72
|
return self
|
|
@@ -96,7 +96,7 @@ class Manifest(Model):
|
|
|
96
96
|
setup: list[SetupSynth] = Field(default_factory=list)
|
|
97
97
|
|
|
98
98
|
@model_validator(mode="after")
|
|
99
|
-
def _self_consistent(self) ->
|
|
99
|
+
def _self_consistent(self) -> Manifest:
|
|
100
100
|
# V20 by construction: an emitted file for an entity both endpoints
|
|
101
101
|
# serve must carry an endpoint, or load_baseline rejects it
|
|
102
102
|
for spec in self.entities:
|
|
@@ -6,6 +6,7 @@ and spinners on a TTY and degrades to plain deterministic text when piped
|
|
|
6
6
|
stderr carries status, warnings, and errors.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import os
|
|
9
10
|
from collections.abc import Generator, Iterable
|
|
10
11
|
from contextlib import contextmanager
|
|
11
12
|
|
|
@@ -13,8 +14,30 @@ from rich import box
|
|
|
13
14
|
from rich.console import Console
|
|
14
15
|
from rich.table import Table
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
def _force_terminal() -> bool | None:
|
|
19
|
+
"""Map NO_COLOR / FORCE_COLOR to rich's force_terminal flag.
|
|
20
|
+
|
|
21
|
+
None leaves auto-detect (isatty). False forces plain/piped rendering.
|
|
22
|
+
True forces TTY chrome even when piped.
|
|
23
|
+
|
|
24
|
+
Rich treats any non-empty FORCE_COLOR as "on", so FORCE_COLOR=0 would
|
|
25
|
+
otherwise enable color. We treat empty and "0" as off (force-color.org).
|
|
26
|
+
NO_COLOR (any value) always wins and forces plain output (no-color.org).
|
|
27
|
+
"""
|
|
28
|
+
if "NO_COLOR" in os.environ:
|
|
29
|
+
return False
|
|
30
|
+
force = os.environ.get("FORCE_COLOR")
|
|
31
|
+
if force is not None:
|
|
32
|
+
return force not in ("", "0")
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_force = _force_terminal()
|
|
37
|
+
out = Console(markup=False, emoji=False, highlight=False, force_terminal=_force)
|
|
38
|
+
err = Console(
|
|
39
|
+
stderr=True, markup=False, emoji=False, highlight=False, force_terminal=_force
|
|
40
|
+
)
|
|
18
41
|
|
|
19
42
|
|
|
20
43
|
def data(msg: str) -> None:
|
|
@@ -56,6 +56,7 @@ from .models import Model, validation_summary
|
|
|
56
56
|
from .seed import _norm # pyright: ignore[reportPrivateUsage]
|
|
57
57
|
|
|
58
58
|
_VAR = re.compile(r"\$\{([A-Za-z0-9_]+)\}")
|
|
59
|
+
_PATH = re.compile(r"([A-Za-z0-9_]+)(?:\[(\d+)\])?")
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
class ActionSpec(Model):
|
|
@@ -75,31 +76,41 @@ class WaitSpec(Model):
|
|
|
75
76
|
endpoint: str | None = None
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
class GetOp(Model):
|
|
80
|
+
"""Key-URL fetch a get step performs; expand pulls detail arrays."""
|
|
81
|
+
|
|
82
|
+
entity: str
|
|
83
|
+
keys: list[Any]
|
|
84
|
+
expand: list[str] | None = None
|
|
85
|
+
endpoint: str | None = None
|
|
86
|
+
|
|
87
|
+
|
|
78
88
|
class Step(Model):
|
|
79
|
-
"""One scenario step: exactly one op (put | action | wait-only)."""
|
|
89
|
+
"""One scenario step: exactly one op (put | action | get | wait-only)."""
|
|
80
90
|
|
|
81
91
|
id: str
|
|
82
92
|
put: str | None = None
|
|
83
93
|
action: ActionSpec | None = None
|
|
94
|
+
get: GetOp | None = None
|
|
84
95
|
record: dict[str, Any] | None = None
|
|
85
96
|
parameters: dict[str, Any] | None = None
|
|
86
|
-
capture: dict[str, str] | None = None # {Field: var_name}
|
|
97
|
+
capture: dict[str, str] | None = None # {Field-or-path: var_name}
|
|
87
98
|
wait: WaitSpec | None = None
|
|
88
99
|
endpoint: str | None = None
|
|
89
100
|
|
|
90
101
|
@model_validator(mode="after")
|
|
91
|
-
def _one_op(self) ->
|
|
92
|
-
ops = [op for op in (self.put, self.action) if op is not None]
|
|
102
|
+
def _one_op(self) -> Step:
|
|
103
|
+
ops = [op for op in (self.put, self.action, self.get) if op is not None]
|
|
93
104
|
if len(ops) > 1:
|
|
94
|
-
raise ValueError(f"step '{self.id}': put
|
|
105
|
+
raise ValueError(f"step '{self.id}': put, action, get are exclusive")
|
|
95
106
|
if not ops and self.wait is None:
|
|
96
|
-
raise ValueError(f"step '{self.id}': needs one of put, action, wait")
|
|
107
|
+
raise ValueError(f"step '{self.id}': needs one of put, action, get, wait")
|
|
97
108
|
if self.put is not None and self.record is None:
|
|
98
109
|
raise ValueError(f"step '{self.id}': put needs a record")
|
|
99
110
|
if self.action is not None and self.record is None:
|
|
100
111
|
raise ValueError(f"step '{self.id}': action needs a record")
|
|
101
|
-
if self.capture is not None and self.put is None:
|
|
102
|
-
raise ValueError(f"step '{self.id}': capture rides a put step")
|
|
112
|
+
if self.capture is not None and self.put is None and self.get is None:
|
|
113
|
+
raise ValueError(f"step '{self.id}': capture rides a put or get step")
|
|
103
114
|
return self
|
|
104
115
|
|
|
105
116
|
|
|
@@ -123,7 +134,7 @@ class Expect(Model):
|
|
|
123
134
|
endpoint: str | None = None
|
|
124
135
|
|
|
125
136
|
@model_validator(mode="after")
|
|
126
|
-
def _one_kind(self) ->
|
|
137
|
+
def _one_kind(self) -> Expect:
|
|
127
138
|
if (self.get is None) == (self.inquire is None):
|
|
128
139
|
raise ValueError("expect: exactly one of get, inquire")
|
|
129
140
|
if self.get is not None and self.fields is None:
|
|
@@ -153,7 +164,7 @@ class Scenario(Model):
|
|
|
153
164
|
expect: list[Expect] = Field(default_factory=list)
|
|
154
165
|
|
|
155
166
|
@model_validator(mode="after")
|
|
156
|
-
def _unique_step_ids(self) ->
|
|
167
|
+
def _unique_step_ids(self) -> Scenario:
|
|
157
168
|
seen: set[str] = set()
|
|
158
169
|
for step in self.steps:
|
|
159
170
|
if step.id in seen:
|
|
@@ -200,6 +211,34 @@ def _subst(value: Any, variables: dict[str, Any]) -> Any:
|
|
|
200
211
|
return value
|
|
201
212
|
|
|
202
213
|
|
|
214
|
+
def _resolve_path(record: dict[str, Any], path: str) -> Any:
|
|
215
|
+
"""Resolve a dotted/indexed capture path (`Shipments[0].ShipmentNbr`).
|
|
216
|
+
|
|
217
|
+
Action-created documents (T66) assign numbers readable only off the
|
|
218
|
+
parent's expanded detail rows - the path walks unwrapped nesting:
|
|
219
|
+
a bare segment reads a field, `Seg[i]` indexes a detail array.
|
|
220
|
+
Missing segments raise with the path named.
|
|
221
|
+
"""
|
|
222
|
+
value: Any = record
|
|
223
|
+
for segment in path.split("."):
|
|
224
|
+
m = _PATH.fullmatch(segment)
|
|
225
|
+
if m is None:
|
|
226
|
+
raise RuntimeError(f"capture path {path!r}: bad segment {segment!r}")
|
|
227
|
+
field, index = m.group(1), m.group(2)
|
|
228
|
+
if not isinstance(value, dict) or field not in value:
|
|
229
|
+
raise RuntimeError(
|
|
230
|
+
f"capture path {path!r}: field {field!r} not in the record"
|
|
231
|
+
)
|
|
232
|
+
value = value[field]
|
|
233
|
+
if index is not None:
|
|
234
|
+
if not isinstance(value, list) or int(index) >= len(value):
|
|
235
|
+
raise RuntimeError(
|
|
236
|
+
f"capture path {path!r}: index [{index}] out of range"
|
|
237
|
+
)
|
|
238
|
+
value = value[int(index)]
|
|
239
|
+
return value
|
|
240
|
+
|
|
241
|
+
|
|
203
242
|
def _inquire(client: AcumaticaClient, expect: Expect) -> dict[str, float]:
|
|
204
243
|
"""Probe a contract inquiry; sum each delta field over matching rows."""
|
|
205
244
|
assert expect.inquire is not None
|
|
@@ -258,6 +297,8 @@ def _dry_run(scenario: Scenario) -> None:
|
|
|
258
297
|
if step.put
|
|
259
298
|
else f"invoke {step.action.entity}/{step.action.name}"
|
|
260
299
|
if step.action
|
|
300
|
+
else f"get {step.get.entity}"
|
|
301
|
+
if step.get
|
|
261
302
|
else f"wait {step.wait.entity}" # pyright: ignore[reportOptionalMemberAccess]
|
|
262
303
|
)
|
|
263
304
|
output.data(f" would {op} [{step.id}]")
|
|
@@ -290,6 +331,18 @@ def _run_step(client: AcumaticaClient, step: Step, variables: dict[str, Any]) ->
|
|
|
290
331
|
step.endpoint,
|
|
291
332
|
)
|
|
292
333
|
output.data(f" invoke {step.action.name} [{step.id}]")
|
|
334
|
+
elif step.get is not None:
|
|
335
|
+
keys = _subst(step.get.keys, variables)
|
|
336
|
+
params = (
|
|
337
|
+
{"$expand": ",".join(sorted(step.get.expand))} if step.get.expand else None
|
|
338
|
+
)
|
|
339
|
+
record = client.get_record(step.get.entity, keys, step.get.endpoint, params)
|
|
340
|
+
if record is None:
|
|
341
|
+
raise RuntimeError(f"step '{step.id}': {step.get.entity} {keys} not found")
|
|
342
|
+
fetched = unwrap(record)
|
|
343
|
+
output.data(f" get {step.get.entity} [{step.id}]")
|
|
344
|
+
for path, var in (step.capture or {}).items():
|
|
345
|
+
variables[var] = _resolve_path(fetched, path)
|
|
293
346
|
if step.wait is not None:
|
|
294
347
|
_wait(client, step.wait, variables)
|
|
295
348
|
output.data(f" wait ok [{step.id}]")
|
|
@@ -7,7 +7,7 @@ Baseline file format:
|
|
|
7
7
|
|
|
8
8
|
entity: Currency # entity name in the contract endpoint
|
|
9
9
|
key: CurrencyID # key field(s), string or list
|
|
10
|
-
endpoint: Bootstrap/1.
|
|
10
|
+
endpoint: Bootstrap/1.7.0 # optional: override the instance endpoint
|
|
11
11
|
records:
|
|
12
12
|
- CurrencyID: "CAD"
|
|
13
13
|
Description: Canadian Dollar
|
|
@@ -34,7 +34,7 @@ cannot express (calendar generation and the like):
|
|
|
34
34
|
|
|
35
35
|
action: GenerateCalendar # action name on the endpoint entity
|
|
36
36
|
entity: MasterCalendar # entity the action hangs off
|
|
37
|
-
endpoint: Bootstrap/1.
|
|
37
|
+
endpoint: Bootstrap/1.7.0 # optional: override the instance endpoint
|
|
38
38
|
record: { FinancialYear: 2026 }
|
|
39
39
|
parameters: { FromYear: 2026, ToYear: 2026 } # optional
|
|
40
40
|
done_when: { filter: "FinancialYear eq '2026'" }
|
|
@@ -109,7 +109,7 @@ class BaselineFile(Model):
|
|
|
109
109
|
return v if isinstance(v, list) else [v]
|
|
110
110
|
|
|
111
111
|
@model_validator(mode="after")
|
|
112
|
-
def _keys_identify_records(self) ->
|
|
112
|
+
def _keys_identify_records(self) -> BaselineFile:
|
|
113
113
|
# V25: the declared key tuple must uniquely identify records - a
|
|
114
114
|
# dup-keyed file diffs as permanent false drift and apply collapses
|
|
115
115
|
# the dup records into one PUT target (B21)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# General Ledger preferences (GL102000) via the custom Bootstrap/1.
|
|
1
|
+
# General Ledger preferences (GL102000) via the custom Bootstrap/1.7.0
|
|
2
2
|
# endpoint (the per-file endpoint: override) - the Default endpoint has
|
|
3
3
|
# no GL-setup entity, and every Finance screen throws
|
|
4
4
|
# PXSetupNotEnteredException until the GLSetup singleton row exists. The
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# referenced here must exist.
|
|
13
13
|
entity: GLPreferences
|
|
14
14
|
key: RetEarnAccountID
|
|
15
|
-
endpoint: Bootstrap/1.
|
|
15
|
+
endpoint: Bootstrap/1.7.0
|
|
16
16
|
records:
|
|
17
17
|
- RetEarnAccountID: '32000'
|
|
18
18
|
YtdNetIncAccountID: '33000'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Org-ledger link (GL201500) via the custom Bootstrap/1.
|
|
1
|
+
# Org-ledger link (GL201500) via the custom Bootstrap/1.7.0 endpoint (the
|
|
2
2
|
# per-file endpoint: override). The Default endpoint's Ledger.Companies
|
|
3
3
|
# detail is write-tolerated then silently dropped (PUT answers 200 and
|
|
4
4
|
# echoes the row, the link table stays empty), so the link writes through
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
# (company.yaml).
|
|
22
22
|
entity: LedgerCompany
|
|
23
23
|
key: [LedgerCD, OrganizationID]
|
|
24
|
-
endpoint: Bootstrap/1.
|
|
24
|
+
endpoint: Bootstrap/1.7.0
|
|
25
25
|
records:
|
|
26
26
|
- LedgerCD: ACTUAL
|
|
27
27
|
OrganizationID: COMPANY
|
{acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/bootstrap/company.yaml
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Company (CS101500) via the custom Bootstrap/1.
|
|
1
|
+
# Company (CS101500) via the custom Bootstrap/1.7.0 endpoint (the per-file
|
|
2
2
|
# endpoint: override) - built-in endpoints cannot create the first
|
|
3
3
|
# organization on a virgin tenant. List values are the EXTERNAL labels
|
|
4
4
|
# ('Without Branches', not the DAC's 'WithoutBranches'); CountryID feeds
|
|
5
5
|
# the org's Main Address - required on first save.
|
|
6
6
|
entity: Company
|
|
7
7
|
key: AcctCD
|
|
8
|
-
endpoint: Bootstrap/1.
|
|
8
|
+
endpoint: Bootstrap/1.7.0
|
|
9
9
|
records:
|
|
10
10
|
- AcctCD: COMPANY
|
|
11
11
|
AcctName: Example Company
|
{acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/bootstrap/credit-terms.yaml
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Credit terms (CS206500) via the custom Bootstrap/1.
|
|
1
|
+
# Credit terms (CS206500) via the custom Bootstrap/1.7.0 endpoint (the
|
|
2
2
|
# per-file endpoint: override) - the Default endpoint has no credit-terms
|
|
3
3
|
# entity. List values are the EXTERNAL labels: VisibleTo = All, Vendors,
|
|
4
4
|
# Customers, Disabled; DueType/DiscType = Fixed Number of Days, Day of
|
|
5
5
|
# Next Month, End of Month, ...
|
|
6
6
|
entity: CreditTerms
|
|
7
7
|
key: TermsID
|
|
8
|
-
endpoint: Bootstrap/1.
|
|
8
|
+
endpoint: Bootstrap/1.7.0
|
|
9
9
|
records:
|
|
10
10
|
- TermsID: NET30
|
|
11
11
|
Descr: Net 30 Days
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# scaffolded by `acu config init` - the sole config file: where + secrets,
|
|
2
2
|
# every key an ACU_* var; print the fully resolved document with:
|
|
3
3
|
# acu config show
|
|
4
|
-
# ACU_BASE_URL (REST root)
|
|
5
|
-
#
|
|
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.
|
|
6
7
|
# Keep this file out of git (the scaffolded .gitignore already lists it;
|
|
7
8
|
# encrypt at rest, e.g. .env.gpg).
|
|
8
9
|
ACU_BASE_URL=http://erp.example.com/AcumaticaERP
|
|
@@ -28,6 +28,14 @@ class TenantManager:
|
|
|
28
28
|
"""Tenant CRUD via SSH to the Windows guest."""
|
|
29
29
|
|
|
30
30
|
def __init__(self, instance: Instance):
|
|
31
|
+
# control-plane address is optional on Instance (hosted path, V3);
|
|
32
|
+
# tenant CRUD still needs it - hard-error naming the key before any
|
|
33
|
+
# remote (I.cmd), never an empty ssh argv
|
|
34
|
+
if not instance.ssh:
|
|
35
|
+
raise SystemExit(
|
|
36
|
+
"ACU_SSH not set (pass --ssh, "
|
|
37
|
+
"or put ACU_SSH in .env or the environment)"
|
|
38
|
+
)
|
|
31
39
|
self.instance = instance
|
|
32
40
|
|
|
33
41
|
def _ssh(self, command: str) -> str:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/baseline/10-subaccounts.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/baseline/20-accounts.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/baseline/40-ledger.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/baseline/90-uoms.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.7.0 → acumatica_cli-0.8.1}/src/acumatica_cli/templates/bootstrap/features.yaml
RENAMED
|
File without changes
|
|
File without changes
|