acumatica-cli 0.11.0__tar.gz → 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/PKG-INFO +25 -20
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/README.md +24 -19
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/pyproject.toml +1 -1
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/bootstrap.py +32 -21
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/cli.py +56 -30
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/config.py +116 -47
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/run.py +159 -25
- acumatica_cli-0.12.0/src/acumatica_cli/templates/distribution/README.md +59 -0
- acumatica_cli-0.12.0/src/acumatica_cli/templates/distribution/scenario/10-seed-capital.yaml +46 -0
- acumatica_cli-0.12.0/src/acumatica_cli/templates/distribution/scenario/20-buy-gateways.yaml +112 -0
- acumatica_cli-0.12.0/src/acumatica_cli/templates/distribution/scenario/30-build.yaml +5 -0
- acumatica_cli-0.11.0/src/acumatica_cli/templates/distribution/scenario/buy-sell.yaml → acumatica_cli-0.12.0/src/acumatica_cli/templates/distribution/scenario/40-sell.yaml +76 -47
- acumatica_cli-0.11.0/src/acumatica_cli/templates/distribution/README.md +0 -49
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/bootstrap_project.xml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/client.py +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/extract.py +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/extract_manifest.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/output.py +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/seed.py +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/target.py +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/baseline/60-ledger-company.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/bootstrap/company.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/10-reason-codes.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/20-in-preferences.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/30-availability-rules.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/40-posting-classes.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/50-warehouse.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/51-warehouse-locations.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/52-warehouse-defaults.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/53-tax-categories.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/54-item-classes.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/56-so-preferences.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/57-po-preferences.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/58-order-types.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/60-ar-preferences.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/61-ap-preferences.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/62-ca-preferences.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/63-cash-account.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/64-payment-methods.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/65-statement-cycles.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/70-vendor-classes.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/71-customer-classes.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/75-vendors.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/76-customers.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/80-stock-items-parts.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/82-stock-items-kits.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/85-kit-specifications.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/setup/30-open-periods.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/50-gl-preferences.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/60-ledger-company.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/bootstrap/company.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/bootstrap/credit-terms.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/env +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/gitignore +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/setup/10-financial-year.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/setup/20-master-calendar.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/setup/30-open-periods.yaml +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/target +0 -0
- {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/tenant.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: acumatica-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data
|
|
5
5
|
Author: Konstantin Borovik
|
|
6
6
|
Author-email: Konstantin Borovik <kb@lab5.ca>
|
|
@@ -47,15 +47,18 @@ acu --tenant DEV apply # seed bootstrap/, baseline/, setup/ (,
|
|
|
47
47
|
acu --tenant DEV diff # prove zero drift (exit 2 on drift)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
**Distribution demo seed** (inventory, warehouse, items, vendors/customers,
|
|
50
|
+
**Distribution demo seed** (inventory, warehouse, items, vendors/customers, lifecycle scenarios):
|
|
51
51
|
|
|
52
52
|
```sh
|
|
53
53
|
acu config init --flavor distribution --host erp.example.com my-dist
|
|
54
54
|
cd my-dist # edit .env; start from an empty tenant
|
|
55
|
-
acu config check && acu bootstrap
|
|
55
|
+
acu config check && acu bootstrap \
|
|
56
|
+
&& acu apply config/ && acu run scenario/ && acu diff config/
|
|
57
|
+
# warm re-run: capital once-skips; Owner Capital stays 50000 (not 100000)
|
|
58
|
+
acu run scenario/
|
|
56
59
|
```
|
|
57
60
|
|
|
58
|
-
See [docs/distribution.md](docs/distribution.md) for the entity map and apply-order notes.
|
|
61
|
+
See [docs/distribution.md](docs/distribution.md) for the entity map, once-guard, and apply-order notes.
|
|
59
62
|
|
|
60
63
|
**Hosted Acumatica (no SSH):** the tenant already exists; leave `ACU_SSH` blank.
|
|
61
64
|
|
|
@@ -97,34 +100,36 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
97
100
|
└── check [--strict] preflight: discovery, secrets, target, REST, endpoints, SSH
|
|
98
101
|
```
|
|
99
102
|
|
|
100
|
-
`apply` and `diff`
|
|
101
|
-
`
|
|
103
|
+
`apply` and `diff` without FILES prefer `config/<name>/` when any seed child exists under `config/`; otherwise root `bootstrap/`, `baseline/`, `setup/`, then `master/` when present.
|
|
104
|
+
A path like `config/` expands nested seed dirs in that fixed order.
|
|
105
|
+
`run` without FILES defaults to `scenario/`.
|
|
102
106
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
103
107
|
Run `acu <command> --help` for details on any command.
|
|
104
108
|
|
|
105
109
|
## The data repo
|
|
106
110
|
|
|
107
111
|
Your configuration lives in its own git repo.
|
|
108
|
-
`acu config init` scaffolds finance-minimal seeds (Bootstrap `project.xml` at `Bootstrap/1.0.0`, no `master
|
|
109
|
-
`acu config init --flavor distribution`
|
|
110
|
-
|
|
111
|
-
| Path
|
|
112
|
-
|
|
|
113
|
-
| `bootstrap/`
|
|
114
|
-
| `baseline/`
|
|
115
|
-
| `setup/`
|
|
116
|
-
| `master/`
|
|
117
|
-
| `scenario/`
|
|
118
|
-
| `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where)
|
|
119
|
-
| `.env`
|
|
112
|
+
`acu config init` scaffolds finance-minimal seeds at the **root** (Bootstrap `project.xml` at `Bootstrap/1.0.0`, no `master/`, no scenario).
|
|
113
|
+
`acu config init --flavor distribution` scaffolds the full demo under `config/` (same contract identity, expanded COA, masters) plus lifecycle `scenario/` and README.
|
|
114
|
+
|
|
115
|
+
| Path | What it holds |
|
|
116
|
+
| ---- | ------------- |
|
|
117
|
+
| `bootstrap/` / `config/bootstrap/` | virgin-tenant config: features, company, credit terms, `project.xml` |
|
|
118
|
+
| `baseline/` / `config/baseline/` | reference data: subaccounts, COA, ledger, UOMs |
|
|
119
|
+
| `setup/` / `config/setup/` | one-time actions: financial year, master calendar, open periods |
|
|
120
|
+
| `config/master/` | distribution masters (prefs, warehouse, items, parties); flavor only |
|
|
121
|
+
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build stub, sell |
|
|
122
|
+
| `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where) |
|
|
123
|
+
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
120
124
|
|
|
121
125
|
Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order.
|
|
122
126
|
The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
|
|
123
127
|
Commit `target.yaml` with the seeds so every clone knows the verified ERP line and Default API generation.
|
|
124
128
|
|
|
125
|
-
Seed YAML
|
|
129
|
+
Seed YAML is state: `apply` upserts it, `diff` proves it.
|
|
126
130
|
Scenario YAML is different — it describes transactions that flow forward.
|
|
127
|
-
`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
|
|
131
|
+
`acu run` executes each step in order (`put`, `action`, `wait`, `get`), captures server-assigned document numbers into `${var}` references for later steps, and checks `expect:` assertions as deltas against a pre-run snapshot, so additive scenarios re-run safely on a warm tenant.
|
|
132
|
+
`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).
|
|
128
133
|
|
|
129
134
|
### Seed `endpoint:` symbols
|
|
130
135
|
|
|
@@ -29,15 +29,18 @@ acu --tenant DEV apply # seed bootstrap/, baseline/, setup/ (,
|
|
|
29
29
|
acu --tenant DEV diff # prove zero drift (exit 2 on drift)
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
**Distribution demo seed** (inventory, warehouse, items, vendors/customers,
|
|
32
|
+
**Distribution demo seed** (inventory, warehouse, items, vendors/customers, lifecycle scenarios):
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
35
|
acu config init --flavor distribution --host erp.example.com my-dist
|
|
36
36
|
cd my-dist # edit .env; start from an empty tenant
|
|
37
|
-
acu config check && acu bootstrap
|
|
37
|
+
acu config check && acu bootstrap \
|
|
38
|
+
&& acu apply config/ && acu run scenario/ && acu diff config/
|
|
39
|
+
# warm re-run: capital once-skips; Owner Capital stays 50000 (not 100000)
|
|
40
|
+
acu run scenario/
|
|
38
41
|
```
|
|
39
42
|
|
|
40
|
-
See [docs/distribution.md](docs/distribution.md) for the entity map and apply-order notes.
|
|
43
|
+
See [docs/distribution.md](docs/distribution.md) for the entity map, once-guard, and apply-order notes.
|
|
41
44
|
|
|
42
45
|
**Hosted Acumatica (no SSH):** the tenant already exists; leave `ACU_SSH` blank.
|
|
43
46
|
|
|
@@ -79,34 +82,36 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
79
82
|
└── check [--strict] preflight: discovery, secrets, target, REST, endpoints, SSH
|
|
80
83
|
```
|
|
81
84
|
|
|
82
|
-
`apply` and `diff`
|
|
83
|
-
`
|
|
85
|
+
`apply` and `diff` without FILES prefer `config/<name>/` when any seed child exists under `config/`; otherwise root `bootstrap/`, `baseline/`, `setup/`, then `master/` when present.
|
|
86
|
+
A path like `config/` expands nested seed dirs in that fixed order.
|
|
87
|
+
`run` without FILES defaults to `scenario/`.
|
|
84
88
|
`acu --completion` emits a completion script for bash, zsh, or fish — source it from your shell profile.
|
|
85
89
|
Run `acu <command> --help` for details on any command.
|
|
86
90
|
|
|
87
91
|
## The data repo
|
|
88
92
|
|
|
89
93
|
Your configuration lives in its own git repo.
|
|
90
|
-
`acu config init` scaffolds finance-minimal seeds (Bootstrap `project.xml` at `Bootstrap/1.0.0`, no `master
|
|
91
|
-
`acu config init --flavor distribution`
|
|
92
|
-
|
|
93
|
-
| Path
|
|
94
|
-
|
|
|
95
|
-
| `bootstrap/`
|
|
96
|
-
| `baseline/`
|
|
97
|
-
| `setup/`
|
|
98
|
-
| `master/`
|
|
99
|
-
| `scenario/`
|
|
100
|
-
| `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where)
|
|
101
|
-
| `.env`
|
|
94
|
+
`acu config init` scaffolds finance-minimal seeds at the **root** (Bootstrap `project.xml` at `Bootstrap/1.0.0`, no `master/`, no scenario).
|
|
95
|
+
`acu config init --flavor distribution` scaffolds the full demo under `config/` (same contract identity, expanded COA, masters) plus lifecycle `scenario/` and README.
|
|
96
|
+
|
|
97
|
+
| Path | What it holds |
|
|
98
|
+
| ---- | ------------- |
|
|
99
|
+
| `bootstrap/` / `config/bootstrap/` | virgin-tenant config: features, company, credit terms, `project.xml` |
|
|
100
|
+
| `baseline/` / `config/baseline/` | reference data: subaccounts, COA, ledger, UOMs |
|
|
101
|
+
| `setup/` / `config/setup/` | one-time actions: financial year, master calendar, open periods |
|
|
102
|
+
| `config/master/` | distribution masters (prefs, warehouse, items, parties); flavor only |
|
|
103
|
+
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build stub, sell |
|
|
104
|
+
| `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where) |
|
|
105
|
+
| `.env` | where to apply and who signs in, every key an `ACU_*` variable |
|
|
102
106
|
|
|
103
107
|
Files in each directory apply alphabetically; the numbered prefixes (`10-`, `20-`, and so on) encode dependency order.
|
|
104
108
|
The scaffolded `.gitignore` keeps `.env` out of git — store it encrypted (for example as `.env.gpg`) and decrypt once per clone.
|
|
105
109
|
Commit `target.yaml` with the seeds so every clone knows the verified ERP line and Default API generation.
|
|
106
110
|
|
|
107
|
-
Seed YAML
|
|
111
|
+
Seed YAML is state: `apply` upserts it, `diff` proves it.
|
|
108
112
|
Scenario YAML is different — it describes transactions that flow forward.
|
|
109
|
-
`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
|
|
113
|
+
`acu run` executes each step in order (`put`, `action`, `wait`, `get`), captures server-assigned document numbers into `${var}` references for later steps, and checks `expect:` assertions as deltas against a pre-run snapshot, so additive scenarios re-run safely on a warm tenant.
|
|
114
|
+
`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).
|
|
110
115
|
|
|
111
116
|
### Seed `endpoint:` symbols
|
|
112
117
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "acumatica-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.12.0"
|
|
4
4
|
description = "Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data"
|
|
5
5
|
authors = [{ name = "Konstantin Borovik", email = "kb@lab5.ca" }]
|
|
6
6
|
license = "PolyForm-Noncommercial-1.0.0"
|
|
@@ -8,10 +8,11 @@ virgin tenant, so bootstrap = publish a package whose CustomizationPlugin
|
|
|
8
8
|
cannot write CS100000 at all (T3 verdict) — and whose Bootstrap contract
|
|
9
9
|
endpoint exposes the seeding surface (serialization verified T12).
|
|
10
10
|
|
|
11
|
-
Contract ownership is hybrid (T69/V2/T81): the data repo's
|
|
12
|
-
``bootstrap/project.xml`` is preferred
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
Contract ownership is hybrid (T69/V2/T81/T85): the data repo's
|
|
12
|
+
``config/bootstrap/project.xml`` is preferred, then root
|
|
13
|
+
``bootstrap/project.xml``; else the packaged full company
|
|
14
|
+
``bootstrap_project.xml`` (``Bootstrap/1.0.0``) so PyPI-only and offline
|
|
15
|
+
virgin paths still bootstrap the full surface.
|
|
15
16
|
|
|
16
17
|
Customization publishes are tenant-scoped, so the package must be published
|
|
17
18
|
per tenant; publish() is idempotent on content — the skip gate compares the
|
|
@@ -19,8 +20,9 @@ digest embedded in the published package's description against the package
|
|
|
19
20
|
built now, and the plugin's UpdateDatabase is a keyed update on re-run.
|
|
20
21
|
|
|
21
22
|
The feature set the plugin enables is data, not code (V2): load_features()
|
|
22
|
-
reads
|
|
23
|
-
and package_zip() splices it into the plugin
|
|
23
|
+
reads config/bootstrap/features.yaml then root bootstrap/features.yaml
|
|
24
|
+
(absent -> the built-in six) and package_zip() splices it into the plugin
|
|
25
|
+
source at build time.
|
|
24
26
|
"""
|
|
25
27
|
|
|
26
28
|
import hashlib
|
|
@@ -43,11 +45,10 @@ PACKAGE_NAME = "AcuBootstrap"
|
|
|
43
45
|
PLUGIN_CLASS = "AcuBootstrapPlugin"
|
|
44
46
|
_ENDPOINT_NS = "{http://www.acumatica.com/entity/maintenance/5.31}"
|
|
45
47
|
|
|
46
|
-
# Code default when the data repo carries no
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
# source — B6).
|
|
48
|
+
# Code default when the data repo carries no features.yaml (SPEC I.data)
|
|
49
|
+
# — the minimum for company/branch + baseline seeding. The set is injected
|
|
50
|
+
# into the plugin source at package build; it never lives in
|
|
51
|
+
# bootstrap_plugin.cs (V2: feature flags are config, not tool source — B6).
|
|
51
52
|
DEFAULT_FEATURES = (
|
|
52
53
|
"FinancialModule",
|
|
53
54
|
"FinancialStandard",
|
|
@@ -59,6 +60,14 @@ DEFAULT_FEATURES = (
|
|
|
59
60
|
FEATURES_SENTINEL = "/*ACU_FEATURES*/"
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
def _bootstrap_file(root: Path, name: str) -> Path | None:
|
|
64
|
+
"""Resolve a bootstrap package input: config/bootstrap then root (V30)."""
|
|
65
|
+
for path in (root / "config" / "bootstrap" / name, root / "bootstrap" / name):
|
|
66
|
+
if path.is_file():
|
|
67
|
+
return path
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
62
71
|
def packaged_contract_xml() -> bytes:
|
|
63
72
|
"""The CLI-shipped full company Bootstrap contract (``Bootstrap/1.0.0``)."""
|
|
64
73
|
return (resources.files("acumatica_cli") / "bootstrap_project.xml").read_bytes()
|
|
@@ -68,12 +77,13 @@ def load_contract_xml(root: Path | None = None) -> bytes:
|
|
|
68
77
|
"""Active contract bytes: data-repo override when present, else packaged.
|
|
69
78
|
|
|
70
79
|
``root`` is the data-repo discovery root (the dir holding ``.env``).
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
Prefer ``config/bootstrap/project.xml``, then root ``bootstrap/project.xml``
|
|
81
|
+
(V30 dual resolve). Absent root or file falls back to the packaged full
|
|
82
|
+
company contract (V2/T81, same absence pattern as features.yaml).
|
|
73
83
|
"""
|
|
74
84
|
if root is not None:
|
|
75
|
-
path = root
|
|
76
|
-
if path
|
|
85
|
+
path = _bootstrap_file(root, "project.xml")
|
|
86
|
+
if path is not None:
|
|
77
87
|
return path.read_bytes()
|
|
78
88
|
return packaged_contract_xml()
|
|
79
89
|
|
|
@@ -97,15 +107,16 @@ PACKAGE_DESCRIPTION = ET.fromstring(packaged_contract_xml()).get("description",
|
|
|
97
107
|
|
|
98
108
|
|
|
99
109
|
def load_features(root: Path) -> list[str]:
|
|
100
|
-
"""
|
|
110
|
+
"""FeaturesSet property names from config/ then root bootstrap (V30).
|
|
101
111
|
|
|
112
|
+
Prefer ``config/bootstrap/features.yaml``, then ``bootstrap/features.yaml``.
|
|
102
113
|
Absent file -> the built-in six (SPEC I.data). Names are validated as
|
|
103
114
|
plausible property names here (they are spliced into C# string literals);
|
|
104
115
|
whether each matches a real FeaturesSet property only the plugin can
|
|
105
116
|
tell — it logs the strays at publish time (the silent-typo guard).
|
|
106
117
|
"""
|
|
107
|
-
path = root
|
|
108
|
-
if
|
|
118
|
+
path = _bootstrap_file(root, "features.yaml")
|
|
119
|
+
if path is None:
|
|
109
120
|
return list(DEFAULT_FEATURES)
|
|
110
121
|
with open(path) as f:
|
|
111
122
|
data = yaml.safe_load(f)
|
|
@@ -136,9 +147,9 @@ def package_zip(
|
|
|
136
147
|
``Enabled`` set at the ACU_FEATURES sentinel — the one point where the
|
|
137
148
|
data repo's feature list enters the package (V2).
|
|
138
149
|
|
|
139
|
-
Contract XML (V2/T69/T81): ``contract`` when given; else
|
|
140
|
-
``bootstrap/project.xml``
|
|
141
|
-
packaged full company template.
|
|
150
|
+
Contract XML (V2/T69/T81/T85): ``contract`` when given; else
|
|
151
|
+
``config/bootstrap/project.xml`` then ``bootstrap/project.xml`` under
|
|
152
|
+
``root`` when present; else the packaged full company template.
|
|
142
153
|
"""
|
|
143
154
|
pkg = resources.files("acumatica_cli")
|
|
144
155
|
if contract is None:
|
|
@@ -428,26 +428,27 @@ def config_init(host: str | None, flavor: str | None, directory: Path | None) ->
|
|
|
428
428
|
Templates ship with the package; every value is a placeholder or a
|
|
429
429
|
verified minimal example - no secrets. Both flavors scaffold the full
|
|
430
430
|
company ``bootstrap/project.xml`` (Bootstrap/1.0.0). ``--flavor
|
|
431
|
-
distribution``
|
|
432
|
-
Existing files are never overwritten (reported as
|
|
433
|
-
defaults to the current directory and is created
|
|
434
|
-
init, no gpg.
|
|
431
|
+
distribution`` scaffolds under ``config/`` + lifecycle ``scenario/``
|
|
432
|
+
+ README (V28/T87). Existing files are never overwritten (reported as
|
|
433
|
+
skipped). DIRECTORY defaults to the current directory and is created
|
|
434
|
+
if absent. No git init, no gpg.
|
|
435
435
|
"""
|
|
436
436
|
target = directory or Path.cwd()
|
|
437
437
|
for action, path in scaffold(target, host=host, flavor=flavor):
|
|
438
438
|
suffix = " (exists)" if action == "skip" else ""
|
|
439
439
|
output.data(f"{action} {path}{suffix}")
|
|
440
|
-
# next-step cmds (issue #18): operator rebuild order after scaffold
|
|
440
|
+
# next-step cmds (issue #18/#19): operator rebuild order after scaffold
|
|
441
441
|
output.data("")
|
|
442
442
|
output.data("next:")
|
|
443
443
|
output.data(" 1. edit .env (set ACU_PASSWORD, ACU_TENANT; keep ACU_API_VERSION)")
|
|
444
444
|
output.data(" 2. acu config check")
|
|
445
445
|
output.data(" 3. acu bootstrap # or: acu tenant create ... (SSH)")
|
|
446
|
-
output.data(" 4. acu apply")
|
|
447
446
|
if flavor == "distribution":
|
|
447
|
+
output.data(" 4. acu apply config/")
|
|
448
448
|
output.data(" 5. acu run scenario/")
|
|
449
|
-
output.data(" 6. acu diff")
|
|
449
|
+
output.data(" 6. acu diff config/")
|
|
450
450
|
else:
|
|
451
|
+
output.data(" 4. acu apply")
|
|
451
452
|
output.data(" 5. acu diff")
|
|
452
453
|
|
|
453
454
|
|
|
@@ -627,37 +628,60 @@ def _probe_endpoints(client: AcumaticaClient, inst: Instance) -> bool:
|
|
|
627
628
|
SEED_DIRS = ("bootstrap", "baseline", "setup", "master")
|
|
628
629
|
|
|
629
630
|
|
|
631
|
+
def _seed_child_dirs(parent: Path) -> list[Path]:
|
|
632
|
+
"""SEED_DIRS children of ``parent`` that exist, fixed order (V22/V30)."""
|
|
633
|
+
return [parent / name for name in SEED_DIRS if (parent / name).is_dir()]
|
|
634
|
+
|
|
635
|
+
|
|
630
636
|
def default_seed_dirs() -> tuple[Path, ...]:
|
|
631
|
-
"""
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
repo is the .env dir (V3 walk-up). None
|
|
637
|
-
default would make a bare run a silent
|
|
638
|
-
|
|
639
|
-
|
|
637
|
+
"""Default apply/diff dirs: prefer ``config/`` SEED_DIRS, else root (V30).
|
|
638
|
+
|
|
639
|
+
When the data-repo ``config/`` has any SEED_DIRS child, only those
|
|
640
|
+
``config/<name>/`` paths are returned (dual layout never merges with
|
|
641
|
+
root). Else root ``bootstrap/``…``master/`` for present names
|
|
642
|
+
(finance-minimal). The data repo is the .env dir (V3 walk-up). None
|
|
643
|
+
existing is an error - an empty default would make a bare run a silent
|
|
644
|
+
no-op. Paths come back relative to cwd so a bare run prints exactly
|
|
645
|
+
what naming the dirs would.
|
|
640
646
|
"""
|
|
641
647
|
root = data_root()
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
648
|
+
config = root / "config"
|
|
649
|
+
if config.is_dir() and _seed_child_dirs(config):
|
|
650
|
+
parents = _seed_child_dirs(config)
|
|
651
|
+
else:
|
|
652
|
+
parents = _seed_child_dirs(root)
|
|
653
|
+
dirs = tuple(Path(os.path.relpath(d)) for d in parents)
|
|
645
654
|
if not dirs:
|
|
646
655
|
expected = ", ".join(f"{name}/" for name in SEED_DIRS)
|
|
647
|
-
raise SystemExit(
|
|
656
|
+
raise SystemExit(
|
|
657
|
+
f"{root}: none of the seed directories exist (config/<name>/ or {expected})"
|
|
658
|
+
)
|
|
648
659
|
return dirs
|
|
649
660
|
|
|
650
661
|
|
|
662
|
+
def _leaf_yaml(directory: Path) -> list[Path]:
|
|
663
|
+
"""Sorted ``*.yaml`` in a leaf seed dir; skip ``features.yaml`` (I.data)."""
|
|
664
|
+
return sorted(p for p in directory.glob("*.yaml") if p.name != "features.yaml")
|
|
665
|
+
|
|
666
|
+
|
|
651
667
|
def expand_files(files: tuple[Path, ...]) -> list[Path]:
|
|
652
|
-
"""Expand directory arguments into
|
|
668
|
+
"""Expand directory arguments into seed ``*.yaml`` files (V22/V30).
|
|
653
669
|
|
|
654
|
-
|
|
655
|
-
|
|
670
|
+
A dir with any SEED_DIRS child (umbrella e.g. ``config/``) expands those
|
|
671
|
+
nested subdirs in fixed SEED_DIRS order, then leaf ``*.yaml`` per subdir.
|
|
672
|
+
A leaf dir expands its own ``*.yaml`` only. ``features.yaml`` is skipped:
|
|
673
|
+
it configures the bootstrap package build, not an entity/records seed.
|
|
656
674
|
"""
|
|
657
675
|
paths: list[Path] = []
|
|
658
676
|
for path in files:
|
|
659
677
|
if path.is_dir():
|
|
660
|
-
|
|
678
|
+
children = _seed_child_dirs(path)
|
|
679
|
+
if children:
|
|
680
|
+
found: list[Path] = []
|
|
681
|
+
for child in children:
|
|
682
|
+
found += _leaf_yaml(child)
|
|
683
|
+
else:
|
|
684
|
+
found = _leaf_yaml(path)
|
|
661
685
|
if not found:
|
|
662
686
|
raise SystemExit(f"{path}: no seed *.yaml files in directory")
|
|
663
687
|
paths += found
|
|
@@ -675,9 +699,10 @@ def expand_files(files: tuple[Path, ...]) -> list[Path]:
|
|
|
675
699
|
def apply_cmd(inst: Instance, files: tuple[Path, ...], dry_run: bool) -> None:
|
|
676
700
|
"""Seed baseline YAML into the tenant (idempotent PUT upserts).
|
|
677
701
|
|
|
678
|
-
FILES are baseline YAML files or directories containing them.
|
|
679
|
-
|
|
680
|
-
|
|
702
|
+
FILES are baseline YAML files or directories containing them. A dir with
|
|
703
|
+
SEED_DIRS children (e.g. config/) expands nested trees in fixed order.
|
|
704
|
+
Omitted, defaults prefer config/<name>/ when present, else root SEED_DIRS
|
|
705
|
+
(V30).
|
|
681
706
|
"""
|
|
682
707
|
assert_target_compatible(inst)
|
|
683
708
|
with AcumaticaClient(inst) as client:
|
|
@@ -729,9 +754,10 @@ def schema_cmd(inst: Instance, out_dir: Path | None) -> None:
|
|
|
729
754
|
def diff_cmd(inst: Instance, files: tuple[Path, ...]) -> None:
|
|
730
755
|
"""Compare baseline YAML against the live tenant; exit 2 on drift.
|
|
731
756
|
|
|
732
|
-
FILES are baseline YAML files or directories containing them.
|
|
733
|
-
|
|
734
|
-
|
|
757
|
+
FILES are baseline YAML files or directories containing them. A dir with
|
|
758
|
+
SEED_DIRS children (e.g. config/) expands nested trees in fixed order.
|
|
759
|
+
Omitted, defaults prefer config/<name>/ when present, else root SEED_DIRS
|
|
760
|
+
(V30).
|
|
735
761
|
"""
|
|
736
762
|
assert_target_compatible(inst)
|
|
737
763
|
paths = expand_files(files or default_seed_dirs())
|