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.
Files changed (71) hide show
  1. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/PKG-INFO +25 -20
  2. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/README.md +24 -19
  3. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/pyproject.toml +1 -1
  4. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/bootstrap.py +32 -21
  5. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/cli.py +56 -30
  6. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/config.py +116 -47
  7. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/run.py +159 -25
  8. acumatica_cli-0.12.0/src/acumatica_cli/templates/distribution/README.md +59 -0
  9. acumatica_cli-0.12.0/src/acumatica_cli/templates/distribution/scenario/10-seed-capital.yaml +46 -0
  10. acumatica_cli-0.12.0/src/acumatica_cli/templates/distribution/scenario/20-buy-gateways.yaml +112 -0
  11. acumatica_cli-0.12.0/src/acumatica_cli/templates/distribution/scenario/30-build.yaml +5 -0
  12. 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
  13. acumatica_cli-0.11.0/src/acumatica_cli/templates/distribution/README.md +0 -49
  14. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/__init__.py +0 -0
  15. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
  16. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/bootstrap_project.xml +0 -0
  17. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/client.py +0 -0
  18. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/extract.py +0 -0
  19. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/extract_manifest.yaml +0 -0
  20. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/firstlogin.py +0 -0
  21. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/models.py +0 -0
  22. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/output.py +0 -0
  23. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/seed.py +0 -0
  24. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/target.py +0 -0
  25. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/baseline/20-accounts.yaml +0 -0
  26. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/baseline/60-ledger-company.yaml +0 -0
  27. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/baseline/90-uoms.yaml +0 -0
  28. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/bootstrap/company.yaml +0 -0
  29. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/bootstrap/features.yaml +0 -0
  30. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/10-reason-codes.yaml +0 -0
  31. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/20-in-preferences.yaml +0 -0
  32. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/30-availability-rules.yaml +0 -0
  33. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/40-posting-classes.yaml +0 -0
  34. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/50-warehouse.yaml +0 -0
  35. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/51-warehouse-locations.yaml +0 -0
  36. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/52-warehouse-defaults.yaml +0 -0
  37. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/53-tax-categories.yaml +0 -0
  38. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/54-item-classes.yaml +0 -0
  39. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/56-so-preferences.yaml +0 -0
  40. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/57-po-preferences.yaml +0 -0
  41. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/58-order-types.yaml +0 -0
  42. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/60-ar-preferences.yaml +0 -0
  43. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/61-ap-preferences.yaml +0 -0
  44. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/62-ca-preferences.yaml +0 -0
  45. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/63-cash-account.yaml +0 -0
  46. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/64-payment-methods.yaml +0 -0
  47. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/65-statement-cycles.yaml +0 -0
  48. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/70-vendor-classes.yaml +0 -0
  49. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/71-customer-classes.yaml +0 -0
  50. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/75-vendors.yaml +0 -0
  51. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/76-customers.yaml +0 -0
  52. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/80-stock-items-parts.yaml +0 -0
  53. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/82-stock-items-kits.yaml +0 -0
  54. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/master/85-kit-specifications.yaml +0 -0
  55. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/distribution/setup/30-open-periods.yaml +0 -0
  56. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/10-subaccounts.yaml +0 -0
  57. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/20-accounts.yaml +0 -0
  58. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/40-ledger.yaml +0 -0
  59. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/50-gl-preferences.yaml +0 -0
  60. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/60-ledger-company.yaml +0 -0
  61. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/baseline/90-uoms.yaml +0 -0
  62. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/bootstrap/company.yaml +0 -0
  63. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/bootstrap/credit-terms.yaml +0 -0
  64. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/bootstrap/features.yaml +0 -0
  65. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/env +0 -0
  66. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/gitignore +0 -0
  67. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/setup/10-financial-year.yaml +0 -0
  68. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/setup/20-master-calendar.yaml +0 -0
  69. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/setup/30-open-periods.yaml +0 -0
  70. {acumatica_cli-0.11.0 → acumatica_cli-0.12.0}/src/acumatica_cli/templates/finance/target +0 -0
  71. {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.11.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, golden scenario):
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 && acu apply && acu run scenario/ && acu diff
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` called without FILES default to existing scaffolded directories, in order: `bootstrap/`, `baseline/`, `setup/`, then `master/` when present.
101
- `run` called without FILES defaults to `scenario/`.
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` adds the full demo set (same contract identity, expanded COA, `master/`, golden `scenario/`, README).
110
-
111
- | Path | What it holds |
112
- | ------------- | -------------------------------------------------------------------------- |
113
- | `bootstrap/` | what makes a virgin tenant configurable: features, company, credit terms |
114
- | `baseline/` | reference data: subaccounts, chart of accounts, ledger, units of measure |
115
- | `setup/` | one-time actions: financial year, master calendar, open periods |
116
- | `master/` | distribution masters (prefs, warehouse, items, vendors, customers); flavor only |
117
- | `scenario/` | transaction scenarios for `acu run`: purchase, build, sell flows |
118
- | `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where) |
119
- | `.env` | where to apply and who signs in, every key an `ACU_*` variable |
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 in `bootstrap/`, `baseline/`, and `setup/` is state: `apply` upserts it, `diff` proves it.
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 a scenario re-runs safely on a warm tenant.
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, golden scenario):
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 && acu apply && acu run scenario/ && acu diff
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` called without FILES default to existing scaffolded directories, in order: `bootstrap/`, `baseline/`, `setup/`, then `master/` when present.
83
- `run` called without FILES defaults to `scenario/`.
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` adds the full demo set (same contract identity, expanded COA, `master/`, golden `scenario/`, README).
92
-
93
- | Path | What it holds |
94
- | ------------- | -------------------------------------------------------------------------- |
95
- | `bootstrap/` | what makes a virgin tenant configurable: features, company, credit terms |
96
- | `baseline/` | reference data: subaccounts, chart of accounts, ledger, units of measure |
97
- | `setup/` | one-time actions: financial year, master calendar, open periods |
98
- | `master/` | distribution masters (prefs, warehouse, items, vendors, customers); flavor only |
99
- | `scenario/` | transaction scenarios for `acu run`: purchase, build, sell flows |
100
- | `target.yaml` | committed verified matrix: `erp` + `default_api` (what, not where) |
101
- | `.env` | where to apply and who signs in, every key an `ACU_*` variable |
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 in `bootstrap/`, `baseline/`, and `setup/` is state: `apply` upserts it, `diff` proves it.
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 a scenario re-runs safely on a warm tenant.
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.11.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 when present; else the packaged
13
- full company ``bootstrap_project.xml`` (``Bootstrap/1.0.0``) so PyPI-only
14
- and offline virgin paths still bootstrap the full surface.
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 the data repo's bootstrap/features.yaml (absent -> the built-in six)
23
- and package_zip() splices it into the plugin source at build time.
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 bootstrap/features.yaml
47
- # (SPEC I.data) — the minimum for company/branch + baseline seeding.
48
- # The set is injected into the plugin source at package build; it never
49
- # lives in bootstrap_plugin.cs (V2: feature flags are config, not tool
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
- Absent root or absent ``bootstrap/project.xml`` falls back to the
72
- packaged full company contract (V2/T81, same absence pattern as features.yaml).
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 / "bootstrap" / "project.xml"
76
- if path.is_file():
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
- """The FeaturesSet property names from <root>/bootstrap/features.yaml.
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 / "bootstrap" / "features.yaml"
108
- if not path.is_file():
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`` under ``root`` when present; else the
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`` adds expanded COA, master/, scenario/, and README (V28).
432
- Existing files are never overwritten (reported as skipped). DIRECTORY
433
- defaults to the current directory and is created if absent. No git
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
- """The init-scaffolded seed dirs that exist at the data-repo root.
632
-
633
- apply and diff default to these when called with no FILES, in fixed
634
- order (bootstrap, baseline, setup, master); only dirs that exist are
635
- included (finance-minimal has no master/; distribution does). The data
636
- repo is the .env dir (V3 walk-up). None existing is an error - an empty
637
- default would make a bare run a silent no-op. Paths come back relative
638
- to cwd (the root is always cwd or an ancestor), so a bare run prints
639
- exactly what naming the dirs would.
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
- dirs = tuple(
643
- Path(os.path.relpath(d)) for name in SEED_DIRS if (d := root / name).is_dir()
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(f"{root}: none of the seed directories exist ({expected})")
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 their *.yaml files, sorted.
668
+ """Expand directory arguments into seed ``*.yaml`` files (V22/V30).
653
669
 
654
- features.yaml is skipped: it configures the bootstrap package build
655
- (a feature-name list, SPEC I.data), not an entity/records seed file.
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
- found = sorted(p for p in path.glob("*.yaml") if p.name != "features.yaml")
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. Omitted,
679
- they default to the data repo's existing init-scaffolded directories in
680
- fixed order: bootstrap/, baseline/, setup/, master/ (when present).
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. Omitted,
733
- they default to the data repo's existing init-scaffolded directories in
734
- fixed order: bootstrap/, baseline/, setup/, master/ (when present).
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())