acumatica-cli 0.28.0__tar.gz → 0.30.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.28.0 → acumatica_cli-0.30.0}/PKG-INFO +7 -11
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/README.md +6 -10
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/pyproject.toml +1 -1
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/pyproject.toml.orig +1 -1
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/bootstrap.py +2 -2
- acumatica_cli-0.30.0/src/acumatica_cli/bootstrap_plugin.cs +147 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/bootstrap_project.xml +42 -11
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/cli.py +4 -192
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/config.py +5 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/extract.py +7 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/seed.py +59 -1
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/seed_catalog.yaml +37 -12
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/README.md +2 -3
- acumatica_cli-0.30.0/src/acumatica_cli/templates/config/baseline/91-company-packaging.yaml +8 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/91-users.yaml +0 -1
- acumatica_cli-0.30.0/src/acumatica_cli/templates/config/master/92-role-users.yaml +9 -0
- acumatica_cli-0.28.0/src/acumatica_cli/bootstrap_plugin.cs +0 -74
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/client.py +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/inventory.py +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/output.py +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/reconcile.py +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/run.py +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/snapshot_map.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/state.py +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/baseline/50-gl-preferences.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/baseline/60-ledger-company.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/bootstrap/company.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/bootstrap/credit-terms.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/bootstrap/segmented-key.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/05-numbering-sequences.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/10-reason-codes.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/20-in-preferences.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/30-availability-rules.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/40-posting-classes.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/50-warehouse.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/51-warehouse-locations.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/52-warehouse-defaults.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/53-tax-categories.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/54-item-classes.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/56-so-preferences.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/57-po-preferences.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/58-order-types.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/60-ar-preferences.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/61-ap-preferences.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/62-ca-preferences.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/63-cash-account.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/64-payment-methods.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/65-statement-cycles.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/70-vendor-classes.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/71-customer-classes.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/75-vendors.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/76-customers.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/80-stock-items-parts.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/82-stock-items-kits.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/85-kit-specifications.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/master/90-roles.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/setup/10-financial-year.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/setup/20-master-calendar.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/setup/30-open-periods.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/config/views/10-trial-balance.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/env +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/gitignore +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/overlays/README.md +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/overlays/default-24.200.001/README.md +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/overlays/default-24.200.001/scenario/30-build.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/scenario/10-seed-capital.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/scenario/20-buy.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/scenario/30-build.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/scenario/40-sell.yaml +0 -0
- {acumatica_cli-0.28.0 → acumatica_cli-0.30.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.30.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>
|
|
@@ -45,7 +45,7 @@ YAML in git is the source of truth; the live tenant is the target. `apply` is th
|
|
|
45
45
|
| Capture derived balances | `state` | Inquire trial-balance etc. into `state/` (not seed) |
|
|
46
46
|
| Create and destroy tenants | `tenant` | SSH control plane: list / create / delete / recycle |
|
|
47
47
|
| Publish the Bootstrap contract | `bootstrap` | REST publish of AcuBootstrap (or `--export` zip for the UI) |
|
|
48
|
-
| Prove a cold rebuild | `
|
|
48
|
+
| Prove a cold rebuild | `tenant create` then `apply` then `run` then `diff` | Compose on a fresh tenant. No wrap command. |
|
|
49
49
|
| Snapshot a site offline | `inventory` | SM203520 XML ZIP or `ac.exe export xml` writes `inventory/` |
|
|
50
50
|
| Cross-check snapshot vs seed | `reconcile` | `inventory/` plus optional `config/` writes `findings/` only |
|
|
51
51
|
| Dump the contract schema | `schema` | OpenAPI `swagger.json` for the pinned endpoint |
|
|
@@ -76,7 +76,6 @@ acu --tenant DEV apply config/ # seed config/{bootstrap,baseline,setup
|
|
|
76
76
|
acu --tenant DEV run scenario/ # once capital → buy → build → sell
|
|
77
77
|
acu --tenant DEV diff config/ # prove zero drift (exit 2 on drift)
|
|
78
78
|
acu --tenant DEV state # capture state/ trial-balance
|
|
79
|
-
acu check --yes --tenant DEV # cold lifecycle create→apply→run→diff (leave tenant)
|
|
80
79
|
```
|
|
81
80
|
|
|
82
81
|
Bare `apply` / `diff` (no path args) also prefer `config/` when those trees exist.
|
|
@@ -115,7 +114,6 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
115
114
|
├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
|
|
116
115
|
├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
|
|
117
116
|
├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
|
|
118
|
-
├── check [--yes] [--tenant L] cold lifecycle create→apply→run→diff; leave tenant (V47)
|
|
119
117
|
├── state [--out DIR] [--diff] [--assert-unchanged] [--dry-run] [FILES...]
|
|
120
118
|
│ capture derived state into state/ (not seed)
|
|
121
119
|
├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
|
|
@@ -196,7 +194,7 @@ There is no `--flavor`.
|
|
|
196
194
|
| `config/bootstrap/` | virgin-tenant config: features, company, credit terms (no `project.xml`) |
|
|
197
195
|
| `config/baseline/` | reference data: subaccounts, COA, ledger, UOMs |
|
|
198
196
|
| `config/setup/` | one-time actions: financial year, master calendar, open periods |
|
|
199
|
-
| `config/master/` | inventory/distribution masters: numbering (`05-…`) before prefs, warehouse, items, parties + Role/User (`90-roles` then `91-users`) |
|
|
197
|
+
| `config/master/` | inventory/distribution masters: numbering (`05-…`) before prefs, warehouse, items, parties + Role/User (`90-roles` then `91-users` then `92-role-users`) |
|
|
200
198
|
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
|
|
201
199
|
| `config/views/` | observer views for `acu state` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
|
|
202
200
|
| `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
|
|
@@ -285,7 +283,6 @@ Ad-hoc override: `acu --api-version 24.200.001 …` (version half only, never
|
|
|
285
283
|
`base_url` resolves as `--url`, else `ACU_BASE_URL`, else a hard error naming sources.
|
|
286
284
|
|
|
287
285
|
`acu config show` prints the resolved `.env` (password excluded; includes `ACU_API_VERSION`).
|
|
288
|
-
`acu check` (lifecycle) uses the same resolved instance.
|
|
289
286
|
|
|
290
287
|
### Multi-host overlays (V44)
|
|
291
288
|
|
|
@@ -317,9 +314,6 @@ acu diff
|
|
|
317
314
|
# explicit path args (no auto) — later path wins same keys
|
|
318
315
|
acu apply config/ overlays/default-24.200.001/
|
|
319
316
|
acu diff config/ overlays/default-24.200.001/
|
|
320
|
-
|
|
321
|
-
# cold lifecycle vs the resolved .env instance (SSH + tenant required)
|
|
322
|
-
acu check --yes --tenant LAB5
|
|
323
317
|
```
|
|
324
318
|
|
|
325
319
|
Add a future half by creating `overlays/default-<new-half>/` with the
|
|
@@ -335,7 +329,7 @@ Worth knowing:
|
|
|
335
329
|
- Without a `.env`, global flags plus the process environment supply the configuration.
|
|
336
330
|
- When `ACU_SSH` is **absent**, acu defaults to `Administrator@` + the resolved base_url hostname.
|
|
337
331
|
A **present blank** `ACU_SSH=` is the hosted opt-out.
|
|
338
|
-
Only `acu tenant`
|
|
332
|
+
Only `acu tenant` requires a non-empty value post-default.
|
|
339
333
|
- `acu config show` prints the resolved `.env` (password excluded).
|
|
340
334
|
- Redirect it to turn resolved state into a working config: `acu config show > .env`.
|
|
341
335
|
|
|
@@ -383,7 +377,9 @@ gmake release patch # or minor | major
|
|
|
383
377
|
2. Fail if `## Unreleased` has no bullets
|
|
384
378
|
3. Bump `pyproject.toml` version (`major` | `minor` | `patch`)
|
|
385
379
|
4. Promote Unreleased body to `## [vX.Y.Z] - YYYY-MM-DD`, leave an empty `## Unreleased`
|
|
386
|
-
5. Commit `CHANGELOG.md` + `pyproject.toml` (+ lock if bumped) together, tag `vX.Y.Z
|
|
380
|
+
5. Commit `CHANGELOG.md` + `pyproject.toml` (+ lock if bumped) together, tag `vX.Y.Z`
|
|
381
|
+
6. `gmake check` again on the promoted tree
|
|
382
|
+
7. Push tag + branch — GitHub Actions publishes
|
|
387
383
|
|
|
388
384
|
GitHub Actions on tag `v*` re-runs CI, builds sdist+wheel, publishes to PyPI via OIDC trusted publishing, and creates a GitHub Release whose notes are the promoted CHANGELOG section for that tag (plus the artifacts).
|
|
389
385
|
|
|
@@ -27,7 +27,7 @@ YAML in git is the source of truth; the live tenant is the target. `apply` is th
|
|
|
27
27
|
| Capture derived balances | `state` | Inquire trial-balance etc. into `state/` (not seed) |
|
|
28
28
|
| Create and destroy tenants | `tenant` | SSH control plane: list / create / delete / recycle |
|
|
29
29
|
| Publish the Bootstrap contract | `bootstrap` | REST publish of AcuBootstrap (or `--export` zip for the UI) |
|
|
30
|
-
| Prove a cold rebuild | `
|
|
30
|
+
| Prove a cold rebuild | `tenant create` then `apply` then `run` then `diff` | Compose on a fresh tenant. No wrap command. |
|
|
31
31
|
| Snapshot a site offline | `inventory` | SM203520 XML ZIP or `ac.exe export xml` writes `inventory/` |
|
|
32
32
|
| Cross-check snapshot vs seed | `reconcile` | `inventory/` plus optional `config/` writes `findings/` only |
|
|
33
33
|
| Dump the contract schema | `schema` | OpenAPI `swagger.json` for the pinned endpoint |
|
|
@@ -58,7 +58,6 @@ acu --tenant DEV apply config/ # seed config/{bootstrap,baseline,setup
|
|
|
58
58
|
acu --tenant DEV run scenario/ # once capital → buy → build → sell
|
|
59
59
|
acu --tenant DEV diff config/ # prove zero drift (exit 2 on drift)
|
|
60
60
|
acu --tenant DEV state # capture state/ trial-balance
|
|
61
|
-
acu check --yes --tenant DEV # cold lifecycle create→apply→run→diff (leave tenant)
|
|
62
61
|
```
|
|
63
62
|
|
|
64
63
|
Bare `apply` / `diff` (no path args) also prefer `config/` when those trees exist.
|
|
@@ -97,7 +96,6 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
|
|
|
97
96
|
├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
|
|
98
97
|
├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
|
|
99
98
|
├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
|
|
100
|
-
├── check [--yes] [--tenant L] cold lifecycle create→apply→run→diff; leave tenant (V47)
|
|
101
99
|
├── state [--out DIR] [--diff] [--assert-unchanged] [--dry-run] [FILES...]
|
|
102
100
|
│ capture derived state into state/ (not seed)
|
|
103
101
|
├── extract [--out DIR] [--only NAME]... [--force] [--dry-run]
|
|
@@ -178,7 +176,7 @@ There is no `--flavor`.
|
|
|
178
176
|
| `config/bootstrap/` | virgin-tenant config: features, company, credit terms (no `project.xml`) |
|
|
179
177
|
| `config/baseline/` | reference data: subaccounts, COA, ledger, UOMs |
|
|
180
178
|
| `config/setup/` | one-time actions: financial year, master calendar, open periods |
|
|
181
|
-
| `config/master/` | inventory/distribution masters: numbering (`05-…`) before prefs, warehouse, items, parties + Role/User (`90-roles` then `91-users`) |
|
|
179
|
+
| `config/master/` | inventory/distribution masters: numbering (`05-…`) before prefs, warehouse, items, parties + Role/User (`90-roles` then `91-users` then `92-role-users`) |
|
|
182
180
|
| `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
|
|
183
181
|
| `config/views/` | observer views for `acu state` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
|
|
184
182
|
| `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
|
|
@@ -267,7 +265,6 @@ Ad-hoc override: `acu --api-version 24.200.001 …` (version half only, never
|
|
|
267
265
|
`base_url` resolves as `--url`, else `ACU_BASE_URL`, else a hard error naming sources.
|
|
268
266
|
|
|
269
267
|
`acu config show` prints the resolved `.env` (password excluded; includes `ACU_API_VERSION`).
|
|
270
|
-
`acu check` (lifecycle) uses the same resolved instance.
|
|
271
268
|
|
|
272
269
|
### Multi-host overlays (V44)
|
|
273
270
|
|
|
@@ -299,9 +296,6 @@ acu diff
|
|
|
299
296
|
# explicit path args (no auto) — later path wins same keys
|
|
300
297
|
acu apply config/ overlays/default-24.200.001/
|
|
301
298
|
acu diff config/ overlays/default-24.200.001/
|
|
302
|
-
|
|
303
|
-
# cold lifecycle vs the resolved .env instance (SSH + tenant required)
|
|
304
|
-
acu check --yes --tenant LAB5
|
|
305
299
|
```
|
|
306
300
|
|
|
307
301
|
Add a future half by creating `overlays/default-<new-half>/` with the
|
|
@@ -317,7 +311,7 @@ Worth knowing:
|
|
|
317
311
|
- Without a `.env`, global flags plus the process environment supply the configuration.
|
|
318
312
|
- When `ACU_SSH` is **absent**, acu defaults to `Administrator@` + the resolved base_url hostname.
|
|
319
313
|
A **present blank** `ACU_SSH=` is the hosted opt-out.
|
|
320
|
-
Only `acu tenant`
|
|
314
|
+
Only `acu tenant` requires a non-empty value post-default.
|
|
321
315
|
- `acu config show` prints the resolved `.env` (password excluded).
|
|
322
316
|
- Redirect it to turn resolved state into a working config: `acu config show > .env`.
|
|
323
317
|
|
|
@@ -365,7 +359,9 @@ gmake release patch # or minor | major
|
|
|
365
359
|
2. Fail if `## Unreleased` has no bullets
|
|
366
360
|
3. Bump `pyproject.toml` version (`major` | `minor` | `patch`)
|
|
367
361
|
4. Promote Unreleased body to `## [vX.Y.Z] - YYYY-MM-DD`, leave an empty `## Unreleased`
|
|
368
|
-
5. Commit `CHANGELOG.md` + `pyproject.toml` (+ lock if bumped) together, tag `vX.Y.Z
|
|
362
|
+
5. Commit `CHANGELOG.md` + `pyproject.toml` (+ lock if bumped) together, tag `vX.Y.Z`
|
|
363
|
+
6. `gmake check` again on the promoted tree
|
|
364
|
+
7. Push tag + branch — GitHub Actions publishes
|
|
369
365
|
|
|
370
366
|
GitHub Actions on tag `v*` re-runs CI, builds sdist+wheel, publishes to PyPI via OIDC trusted publishing, and creates a GitHub Release whose notes are the promoted CHANGELOG section for that tag (plus the artifacts).
|
|
371
367
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "acumatica-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.30.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"
|
|
@@ -9,7 +9,7 @@ cannot write CS100000 at all (T3 verdict) — and whose Bootstrap contract
|
|
|
9
9
|
endpoint exposes the seeding surface (serialization verified T12).
|
|
10
10
|
|
|
11
11
|
Bootstrap endpoint contract is package SoT (V2/V21/T178): always the
|
|
12
|
-
packaged full company ``bootstrap_project.xml`` (``Bootstrap/1.
|
|
12
|
+
packaged full company ``bootstrap_project.xml`` (``Bootstrap/1.10.0``).
|
|
13
13
|
Data-repo ``config/bootstrap/project.xml`` or ``bootstrap/project.xml`` is
|
|
14
14
|
not a seed — present → hard error naming package SoT (no dual contract line).
|
|
15
15
|
|
|
@@ -68,7 +68,7 @@ def _bootstrap_file(root: Path, name: str) -> Path | None:
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
def packaged_contract_xml() -> bytes:
|
|
71
|
-
"""The CLI-shipped full company Bootstrap contract (``Bootstrap/1.
|
|
71
|
+
"""The CLI-shipped full company Bootstrap contract (``Bootstrap/1.10.0``)."""
|
|
72
72
|
return (resources.files("acumatica_cli") / "bootstrap_project.xml").read_bytes()
|
|
73
73
|
|
|
74
74
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// acu bootstrap CustomizationPlugin (SPEC T11).
|
|
2
|
+
//
|
|
3
|
+
// Runs in-process on customization publish - the one write path to
|
|
4
|
+
// FeaturesSet that works: the contract API cannot persist features no
|
|
5
|
+
// matter what endpoint fronts CS100000 (T3 verdict, docs/rest-api.md).
|
|
6
|
+
//
|
|
7
|
+
// Deliberately writes through PXDatabase, not FeaturesMaint: the graph
|
|
8
|
+
// save collides with the publish pipeline's concurrent plugin invocations
|
|
9
|
+
// ("Another process has added the 'FeaturesSet' record" - observed live,
|
|
10
|
+
// 26.101.0225) and nothing persists. A keyed row write is deterministic
|
|
11
|
+
// and idempotent: update the existing row, insert when absent. All 205
|
|
12
|
+
// NOT NULL bit columns are filled reflectively (only ~136 carry DB
|
|
13
|
+
// defaults); Status = 0 means Validated (PXIntList, verified vs live).
|
|
14
|
+
//
|
|
15
|
+
// The Enabled set is NOT authored here (SPEC V2: feature flags are config
|
|
16
|
+
// "what", never tool source - B6): package_zip() substitutes the
|
|
17
|
+
// ACU_FEATURES sentinel below with the data repo's bootstrap/features.yaml
|
|
18
|
+
// list (built-in six when the file is absent) at package-build time.
|
|
19
|
+
using System;
|
|
20
|
+
using System.Collections;
|
|
21
|
+
using System.Collections.Generic;
|
|
22
|
+
using System.Reflection;
|
|
23
|
+
using PX.Data;
|
|
24
|
+
using PX.Objects.CS;
|
|
25
|
+
using PX.SM;
|
|
26
|
+
|
|
27
|
+
namespace AcuBootstrap
|
|
28
|
+
{
|
|
29
|
+
public class AcuBootstrapPlugin : Customization.CustomizationPlugin
|
|
30
|
+
{
|
|
31
|
+
private static readonly HashSet<string> Enabled = new HashSet<string>
|
|
32
|
+
{
|
|
33
|
+
/*ACU_FEATURES*/
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
public override void UpdateDatabase()
|
|
37
|
+
{
|
|
38
|
+
var flags = new List<PXDataFieldAssign>();
|
|
39
|
+
var known = new HashSet<string>();
|
|
40
|
+
foreach (PropertyInfo prop in typeof(FeaturesSet).GetProperties())
|
|
41
|
+
{
|
|
42
|
+
if (prop.PropertyType == typeof(bool?))
|
|
43
|
+
{
|
|
44
|
+
known.Add(prop.Name);
|
|
45
|
+
flags.Add(new PXDataFieldAssign(
|
|
46
|
+
prop.Name, PXDbType.Bit, Enabled.Contains(prop.Name)));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
foreach (string name in Enabled)
|
|
50
|
+
{
|
|
51
|
+
if (!known.Contains(name))
|
|
52
|
+
{
|
|
53
|
+
// silent-typo guard (T24): a misspelled features.yaml
|
|
54
|
+
// entry enables nothing - say so in the publish log
|
|
55
|
+
WriteLog("AcuBootstrap: unknown feature name '" + name
|
|
56
|
+
+ "' - no FeaturesSet property, nothing enabled");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
flags.Add(new PXDataFieldAssign("Status", PXDbType.Int, 0)); // Validated
|
|
60
|
+
|
|
61
|
+
using (var tx = new PXTransactionScope())
|
|
62
|
+
{
|
|
63
|
+
bool updated = PXDatabase.Update<FeaturesSet>(flags.ToArray());
|
|
64
|
+
if (!updated)
|
|
65
|
+
{
|
|
66
|
+
PXDatabase.Insert<FeaturesSet>(flags.ToArray());
|
|
67
|
+
WriteLog("AcuBootstrap: FeaturesSet row inserted (features enabled)");
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
{
|
|
71
|
+
WriteLog("AcuBootstrap: FeaturesSet row updated (features enabled)");
|
|
72
|
+
}
|
|
73
|
+
tx.Complete();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// V53/B33: mapped Role.Users / User.Roles PUT never writes
|
|
78
|
+
// UsersInRoles. Persist is a keyed PXDatabase write (same
|
|
79
|
+
// FeaturesSet idiom): ApplicationName='/' (ASP.NET membership).
|
|
80
|
+
public static void AssignUserToRole(string username, string rolename)
|
|
81
|
+
{
|
|
82
|
+
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(rolename))
|
|
83
|
+
{
|
|
84
|
+
throw new PXException(
|
|
85
|
+
"AssignUser requires Username and Rolename");
|
|
86
|
+
}
|
|
87
|
+
Guid userId = PXAccess.GetUserID();
|
|
88
|
+
DateTime now = DateTime.Now;
|
|
89
|
+
var update = new List<PXDataFieldParam>
|
|
90
|
+
{
|
|
91
|
+
new PXDataFieldRestrict("Username", PXDbType.NVarChar, username),
|
|
92
|
+
new PXDataFieldRestrict("Rolename", PXDbType.NVarChar, rolename),
|
|
93
|
+
new PXDataFieldRestrict("ApplicationName", PXDbType.VarChar, "/"),
|
|
94
|
+
new PXDataFieldAssign("LastModifiedByID", PXDbType.UniqueIdentifier, userId),
|
|
95
|
+
new PXDataFieldAssign("LastModifiedByScreenID", PXDbType.Char, "SM201005"),
|
|
96
|
+
new PXDataFieldAssign("LastModifiedDateTime", PXDbType.DateTime, now),
|
|
97
|
+
};
|
|
98
|
+
bool updated = PXDatabase.Update<UsersInRoles>(update.ToArray());
|
|
99
|
+
if (!updated)
|
|
100
|
+
{
|
|
101
|
+
var insert = new List<PXDataFieldAssign>
|
|
102
|
+
{
|
|
103
|
+
new PXDataFieldAssign("Username", PXDbType.NVarChar, username),
|
|
104
|
+
new PXDataFieldAssign("Rolename", PXDbType.NVarChar, rolename),
|
|
105
|
+
new PXDataFieldAssign("ApplicationName", PXDbType.VarChar, "/"),
|
|
106
|
+
new PXDataFieldAssign("CreatedByID", PXDbType.UniqueIdentifier, userId),
|
|
107
|
+
new PXDataFieldAssign("CreatedByScreenID", PXDbType.Char, "SM201005"),
|
|
108
|
+
new PXDataFieldAssign("CreatedDateTime", PXDbType.DateTime, now),
|
|
109
|
+
new PXDataFieldAssign("LastModifiedByID", PXDbType.UniqueIdentifier, userId),
|
|
110
|
+
new PXDataFieldAssign("LastModifiedByScreenID", PXDbType.Char, "SM201005"),
|
|
111
|
+
new PXDataFieldAssign("LastModifiedDateTime", PXDbType.DateTime, now),
|
|
112
|
+
};
|
|
113
|
+
PXDatabase.Insert<UsersInRoles>(insert.ToArray());
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public class AssignUserFilter : PXBqlTable, IBqlTable
|
|
119
|
+
{
|
|
120
|
+
[PXString(256, IsUnicode = true, InputMask = "")]
|
|
121
|
+
[PXUIField(DisplayName = "Username")]
|
|
122
|
+
public virtual string Username { get; set; }
|
|
123
|
+
public abstract class username : PX.Data.BQL.BqlString.Field<username> { }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Action lives on RoleAccess so Base.Roles.Current is the SM201005 header.
|
|
127
|
+
public class AcuRoleAccessExt : PXGraphExtension<RoleAccess>
|
|
128
|
+
{
|
|
129
|
+
public static bool IsActive() => true;
|
|
130
|
+
|
|
131
|
+
public PXFilter<AssignUserFilter> AssignUserParams;
|
|
132
|
+
|
|
133
|
+
public PXAction<Roles> assignUser;
|
|
134
|
+
|
|
135
|
+
[PXButton(CommitChanges = true)]
|
|
136
|
+
[PXUIField(DisplayName = "Assign User", Visible = false)]
|
|
137
|
+
protected virtual IEnumerable AssignUser(PXAdapter adapter)
|
|
138
|
+
{
|
|
139
|
+
string username = AssignUserParams.Current != null
|
|
140
|
+
? AssignUserParams.Current.Username : null;
|
|
141
|
+
Roles role = Base.Roles.Current;
|
|
142
|
+
string rolename = role != null ? role.Rolename : null;
|
|
143
|
+
AcuBootstrapPlugin.AssignUserToRole(username, rolename);
|
|
144
|
+
return adapter.Get();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
T204 bumps 1.4.0 -> 1.5.0 for SegmentedKey, gh #30 / V50;
|
|
9
9
|
T207 bumps 1.5.0 -> 1.6.0 for SegmentedKey Detail view (B28);
|
|
10
10
|
gh #34 bumps 1.6.0 -> 1.7.0 for Company DecPlQty + WeightUOM/VolumeUOM
|
|
11
|
-
(CS101500 commonsetup; kit BOM milligram-scale KG needs qty precision 3)
|
|
11
|
+
(CS101500 commonsetup; kit BOM milligram-scale KG needs qty precision 3);
|
|
12
|
+
gh #35 bumps 1.7.0 -> 1.8.0 for User.Roles -> RolesByUser (UsersInRoles)
|
|
13
|
+
and drops Selected (AllowedRoles was EPLoginTypeAllowsRole / EP202500);
|
|
14
|
+
T228 bumps 1.8.0 -> 1.9.0 for Role.Users -> UsersByRole (SM201005
|
|
15
|
+
Users tab; mapped-detail PUT is a no-op — B33) and 1.9.0 -> 1.10.0
|
|
16
|
+
for Role AssignUser action (PXDatabase UsersInRoles persist).
|
|
12
17
|
|
|
13
18
|
package_zip() (bootstrap.py) prefers data-repo bootstrap/project.xml when
|
|
14
19
|
present; otherwise this packaged template is the fallback so config init,
|
|
@@ -48,9 +53,13 @@
|
|
|
48
53
|
Username; identity FirstName/LastName/Email + IsApproved +
|
|
49
54
|
PasswordNeverExpires + PasswordChangeOnNextLogin; Password write-only
|
|
50
55
|
(extract/diff policy = V39/T147).
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
- User detail Roles -> RolesByUser (GET/diff shape; PUT never
|
|
57
|
+
writes UsersInRoles — B31/B32/B33). Persist = Role action
|
|
58
|
+
AssignUser -> PXDatabase UsersInRoles (ApplicationName='/').
|
|
59
|
+
Role.Users -> UsersByRole is the mapped-detail fixture (silent
|
|
60
|
+
200). Apply order Role then User then Role.Users
|
|
61
|
+
(V22 prefixes 90/91/92). Never AllowedRoles
|
|
62
|
+
(EPLoginTypeAllowsRole / EP202500 allow-list; gh #35 / V53).
|
|
54
63
|
|
|
55
64
|
T150 NumberingSequence (gh #25) - Default endpoint has no numbering
|
|
56
65
|
surface; CS201010 Numbering Sequences (NumberingMaint — NOT the
|
|
@@ -112,7 +121,7 @@
|
|
|
112
121
|
-->
|
|
113
122
|
<Customization level="" description="acu bootstrap: CustomizationPlugin enables features on publish; Bootstrap endpoint exposes full company surface (company + credit terms + financial currency + GL chain + module preferences + vendor class + statement cycle + warehouse + order type + cash account + role + user + membership + numbering sequence + segmented key)" product-version="26.101">
|
|
114
123
|
<EntityEndpoint>
|
|
115
|
-
<Endpoint xmlns="http://www.acumatica.com/entity/maintenance/5.31" name="Bootstrap" version="1.
|
|
124
|
+
<Endpoint xmlns="http://www.acumatica.com/entity/maintenance/5.31" name="Bootstrap" version="1.10.0" systemContractVersion="4">
|
|
116
125
|
<TopLevelEntity name="Company" screen="CS101500">
|
|
117
126
|
<Fields>
|
|
118
127
|
<Field name="AcctCD" type="StringValue" />
|
|
@@ -1019,6 +1028,7 @@
|
|
|
1019
1028
|
<Fields>
|
|
1020
1029
|
<Field name="Rolename" type="StringValue" />
|
|
1021
1030
|
<Field name="Descr" type="StringValue" />
|
|
1031
|
+
<Field name="Users" type="RoleUser[]" />
|
|
1022
1032
|
</Fields>
|
|
1023
1033
|
<Mappings>
|
|
1024
1034
|
<Mapping field="Rolename">
|
|
@@ -1027,8 +1037,33 @@
|
|
|
1027
1037
|
<Mapping field="Descr">
|
|
1028
1038
|
<To object="Roles" field="Descr" />
|
|
1029
1039
|
</Mapping>
|
|
1040
|
+
<Mapping field="Users">
|
|
1041
|
+
<To object="UsersByRole" field="" />
|
|
1042
|
+
</Mapping>
|
|
1030
1043
|
</Mappings>
|
|
1044
|
+
<Actions>
|
|
1045
|
+
<Action name="AssignUser" mappedTo="assignUser" isDefault="false">
|
|
1046
|
+
<Fields>
|
|
1047
|
+
<Field name="Username" type="StringValue" />
|
|
1048
|
+
</Fields>
|
|
1049
|
+
<Mappings>
|
|
1050
|
+
<Mapping field="Username">
|
|
1051
|
+
<To object="AssignUserParams" field="Username" />
|
|
1052
|
+
</Mapping>
|
|
1053
|
+
</Mappings>
|
|
1054
|
+
</Action>
|
|
1055
|
+
</Actions>
|
|
1031
1056
|
</TopLevelEntity>
|
|
1057
|
+
<Detail name="RoleUser">
|
|
1058
|
+
<Fields>
|
|
1059
|
+
<Field name="Username" type="StringValue" />
|
|
1060
|
+
</Fields>
|
|
1061
|
+
<Mappings>
|
|
1062
|
+
<Mapping field="Username">
|
|
1063
|
+
<To object="UsersByRole" field="Username" />
|
|
1064
|
+
</Mapping>
|
|
1065
|
+
</Mappings>
|
|
1066
|
+
</Detail>
|
|
1032
1067
|
<TopLevelEntity name="User" screen="SM201010">
|
|
1033
1068
|
<Fields>
|
|
1034
1069
|
<Field name="Username" type="StringValue" />
|
|
@@ -1067,21 +1102,17 @@
|
|
|
1067
1102
|
<To object="UserList" field="Password" />
|
|
1068
1103
|
</Mapping>
|
|
1069
1104
|
<Mapping field="Roles">
|
|
1070
|
-
<To object="
|
|
1105
|
+
<To object="RolesByUser" field="" />
|
|
1071
1106
|
</Mapping>
|
|
1072
1107
|
</Mappings>
|
|
1073
1108
|
</TopLevelEntity>
|
|
1074
1109
|
<Detail name="UserRole">
|
|
1075
1110
|
<Fields>
|
|
1076
1111
|
<Field name="Rolename" type="StringValue" />
|
|
1077
|
-
<Field name="Selected" type="BooleanValue" />
|
|
1078
1112
|
</Fields>
|
|
1079
1113
|
<Mappings>
|
|
1080
1114
|
<Mapping field="Rolename">
|
|
1081
|
-
<To object="
|
|
1082
|
-
</Mapping>
|
|
1083
|
-
<Mapping field="Selected">
|
|
1084
|
-
<To object="AllowedRoles" field="Selected" />
|
|
1115
|
+
<To object="RolesByUser" field="Rolename" />
|
|
1085
1116
|
</Mapping>
|
|
1086
1117
|
</Mappings>
|
|
1087
1118
|
</Detail>
|
|
@@ -55,7 +55,7 @@ COMMAND MAP (pick by intent)
|
|
|
55
55
|
seed write apply [--dry-run] [FILES...] (sole mutator)
|
|
56
56
|
seed check diff [FILES...] (exit 2 on drift)
|
|
57
57
|
txns run [--dry-run] [FILES...]
|
|
58
|
-
cold
|
|
58
|
+
cold rebuild tenant create then apply then run then diff (no wrap cmd)
|
|
59
59
|
pull seed extract [--only ENTITY]... (inverse of apply)
|
|
60
60
|
observe state [--diff|--assert-unchanged]
|
|
61
61
|
offline inventory ARTIFACT | reconcile
|
|
@@ -66,7 +66,6 @@ EXIT CODES (common)
|
|
|
66
66
|
0 success
|
|
67
67
|
1 operational / parse / expectation failure
|
|
68
68
|
2 seed drift (diff) or state moved (state --assert-unchanged)
|
|
69
|
-
check (lifecycle) never exits 2 — drift fails the run as exit 1
|
|
70
69
|
|
|
71
70
|
\b
|
|
72
71
|
DEFAULT PATHS (when FILES omitted)
|
|
@@ -674,8 +673,8 @@ def config_group() -> None:
|
|
|
674
673
|
show print resolved config as a complete .env (password redacted)
|
|
675
674
|
check read-only live preflight (REST + optional SSH)
|
|
676
675
|
|
|
677
|
-
|
|
678
|
-
|
|
676
|
+
Start here on a new machine: init → edit .env → config check →
|
|
677
|
+
tenant create / bootstrap.
|
|
679
678
|
"""
|
|
680
679
|
|
|
681
680
|
|
|
@@ -721,7 +720,6 @@ def config_init(host: str | None, directory: Path | None) -> None:
|
|
|
721
720
|
output.data(" 5. acu run scenario/")
|
|
722
721
|
output.data(" 6. acu diff config/")
|
|
723
722
|
output.data(" 7. acu state")
|
|
724
|
-
output.data(" 8. acu check # cold lifecycle (SSH + tenant)")
|
|
725
723
|
|
|
726
724
|
|
|
727
725
|
@config_group.command("show")
|
|
@@ -751,7 +749,7 @@ def config_check(ctx: click.Context) -> None:
|
|
|
751
749
|
"""Read-only preflight of the resolved target (ok/fail/warn/skip lines).
|
|
752
750
|
|
|
753
751
|
Proves connectivity before apply/run. Writes nothing (no PUT, no tenant
|
|
754
|
-
CRUD).
|
|
752
|
+
CRUD).
|
|
755
753
|
|
|
756
754
|
\b
|
|
757
755
|
Probe order
|
|
@@ -875,192 +873,6 @@ def _probe_endpoints(endpoints: list[tuple[str, str]], inst: Instance) -> bool:
|
|
|
875
873
|
return False
|
|
876
874
|
|
|
877
875
|
|
|
878
|
-
@cli.command("check")
|
|
879
|
-
@click.option(
|
|
880
|
-
"--yes",
|
|
881
|
-
is_flag=True,
|
|
882
|
-
help="Accepted for muscle-memory (pre-clean delete is always unattended)",
|
|
883
|
-
)
|
|
884
|
-
@click.option(
|
|
885
|
-
"--tenant",
|
|
886
|
-
"tenant_login",
|
|
887
|
-
default=None,
|
|
888
|
-
help="Tenant login for pre-clean create (else ACU_TENANT / global --tenant)",
|
|
889
|
-
)
|
|
890
|
-
@click.pass_context
|
|
891
|
-
def check_cmd(ctx: click.Context, yes: bool, tenant_login: str | None) -> None:
|
|
892
|
-
"""Cold lifecycle: delete → create → apply → run → diff (leave tenant).
|
|
893
|
-
|
|
894
|
-
Destructive rebuild for CI against the resolved .env instance. Not
|
|
895
|
-
`acu config check` (read-only preflight). Requires non-empty ACU_SSH
|
|
896
|
-
and a tenant login (--tenant / ACU_TENANT). Pre-clean delete is
|
|
897
|
-
always unattended; rebuilt tenant is left on the host for inspect /
|
|
898
|
-
state. --yes is accepted for muscle-memory only.
|
|
899
|
-
|
|
900
|
-
\b
|
|
901
|
-
Flow
|
|
902
|
-
delete tenant (if present) → create+bootstrap → apply → run → diff
|
|
903
|
-
Diff drift fails the run as exit 1 (never exit 2 from this command).
|
|
904
|
-
|
|
905
|
-
\b
|
|
906
|
-
Examples
|
|
907
|
-
acu check --tenant DEV
|
|
908
|
-
acu check --yes --tenant DEV
|
|
909
|
-
|
|
910
|
-
Prefer `config check` for "can I connect?" Prefer this for "rebuild
|
|
911
|
-
green from empty."
|
|
912
|
-
"""
|
|
913
|
-
_ = yes
|
|
914
|
-
overrides: dict[str, str] = dict(ctx.obj or {})
|
|
915
|
-
tenant = _resolve_check_tenant(tenant_login, overrides)
|
|
916
|
-
try:
|
|
917
|
-
inst = load_instance({**overrides, "tenant": tenant})
|
|
918
|
-
except SystemExit as exc:
|
|
919
|
-
raise SystemExit(str(exc)) from exc
|
|
920
|
-
if not inst.ssh:
|
|
921
|
-
raise SystemExit(
|
|
922
|
-
"ACU_SSH not set (pass --ssh, or put ACU_SSH in .env or the environment)"
|
|
923
|
-
)
|
|
924
|
-
output.data(f"check {inst.base_url} (api={inst.api_version})")
|
|
925
|
-
if _run_lifecycle(inst, tenant) != 0:
|
|
926
|
-
raise SystemExit(1)
|
|
927
|
-
output.success("check: green")
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
def _resolve_check_tenant(tenant_login: str | None, overrides: dict[str, str]) -> str:
|
|
931
|
-
"""Tenant login from flag, global, env, or .env; hard-error if unset."""
|
|
932
|
-
tenant = tenant_login or overrides.get("tenant") or os.environ.get("ACU_TENANT")
|
|
933
|
-
if not tenant and (root := find_data_root()) is not None:
|
|
934
|
-
tenant = read_env_values(root / ".env").get("tenant") or None
|
|
935
|
-
if not tenant:
|
|
936
|
-
raise SystemExit(
|
|
937
|
-
"tenant not set (pass --tenant, "
|
|
938
|
-
"or put ACU_TENANT in .env or the environment)"
|
|
939
|
-
)
|
|
940
|
-
return tenant
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
def _run_lifecycle(inst: Instance, tenant: str) -> int:
|
|
944
|
-
"""pre-clean→create→apply→run→diff; leave tenant (V47); return 0 or 1."""
|
|
945
|
-
mgr = TenantManager(inst)
|
|
946
|
-
_lifecycle_delete_best_effort(mgr, tenant, label="pre-clean")
|
|
947
|
-
try:
|
|
948
|
-
_lifecycle_create_and_bootstrap(inst, mgr, tenant)
|
|
949
|
-
data_inst = inst.model_copy(update={"tenant": tenant})
|
|
950
|
-
_lifecycle_apply(data_inst)
|
|
951
|
-
_lifecycle_run(data_inst)
|
|
952
|
-
_lifecycle_diff(data_inst)
|
|
953
|
-
except SystemExit as exc:
|
|
954
|
-
code = exc.code if isinstance(exc.code, int) else 1
|
|
955
|
-
if code == 2:
|
|
956
|
-
code = 1 # V47: check never exits 2
|
|
957
|
-
output.error(f"lifecycle: {exc}")
|
|
958
|
-
# Leave tenant for manual inspect (no post-clean — V47).
|
|
959
|
-
return 1 if code != 0 else 0
|
|
960
|
-
except (RuntimeError, httpx.HTTPError) as exc:
|
|
961
|
-
output.error(f"lifecycle: {_format_failure(exc, target=inst.base_url)}")
|
|
962
|
-
# Leave tenant for manual inspect (no post-clean — V47).
|
|
963
|
-
return 1
|
|
964
|
-
output.success(
|
|
965
|
-
f"lifecycle green on {inst.base_url} tenant {tenant} (left for inspection)"
|
|
966
|
-
)
|
|
967
|
-
return 0
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
def _lifecycle_create_and_bootstrap(
|
|
971
|
-
inst: Instance, mgr: TenantManager, tenant: str
|
|
972
|
-
) -> None:
|
|
973
|
-
"""Create (or exists-skip) tenant + init + bootstrap chain."""
|
|
974
|
-
tenants = mgr.list()
|
|
975
|
-
existing = next((t for t in tenants if t.login_name == tenant), None)
|
|
976
|
-
if existing is not None:
|
|
977
|
-
company_id = existing.company_id
|
|
978
|
-
output.data(f"skip create: tenant {tenant} exists (id {company_id})")
|
|
979
|
-
else:
|
|
980
|
-
company_id = max((t.company_id for t in tenants), default=0) + 1
|
|
981
|
-
with output.step(f"creating tenant {company_id} ({tenant}) on {inst.base_url}"):
|
|
982
|
-
mgr.create(company_id, tenant, 1, True, "")
|
|
983
|
-
output.data("created")
|
|
984
|
-
with output.step(f"aligning CompanyCD to login ({tenant})"):
|
|
985
|
-
if mgr.set_company_cd(company_id, tenant):
|
|
986
|
-
output.data(f"CompanyCD set to {tenant}")
|
|
987
|
-
else:
|
|
988
|
-
output.data(f"CompanyCD already {tenant}")
|
|
989
|
-
_init_tenant(inst, mgr, tenant)
|
|
990
|
-
_bootstrap_tenant(inst, mgr, tenant)
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
def _lifecycle_delete_best_effort(
|
|
994
|
-
mgr: TenantManager, tenant: str, *, label: str
|
|
995
|
-
) -> None:
|
|
996
|
-
try:
|
|
997
|
-
tenants = mgr.list()
|
|
998
|
-
if any(t.login_name == tenant for t in tenants):
|
|
999
|
-
with output.step(f"{label} delete tenant {tenant}"):
|
|
1000
|
-
mgr.delete(login_name=tenant)
|
|
1001
|
-
with output.step(f"recycling app pool after {label}"):
|
|
1002
|
-
mgr.recycle_app_pool()
|
|
1003
|
-
output.data(f"{label}: deleted {tenant}")
|
|
1004
|
-
else:
|
|
1005
|
-
output.data(f"{label}: tenant {tenant} absent")
|
|
1006
|
-
except (RuntimeError, SystemExit) as exc:
|
|
1007
|
-
output.warn(f"{label}: {exc}")
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
def _lifecycle_apply(inst: Instance) -> None:
|
|
1011
|
-
"""Bare apply (pin overlay auto-compose); SystemExit 1 on failure."""
|
|
1012
|
-
total_ok = 0
|
|
1013
|
-
all_errors: list[str] = []
|
|
1014
|
-
with AcumaticaClient(inst) as client:
|
|
1015
|
-
for path in expand_files(default_apply_dirs(inst)):
|
|
1016
|
-
baseline = seed.load_baseline(path)
|
|
1017
|
-
output.data(
|
|
1018
|
-
f"{path} -> {inst.tenant} on {inst.base_url} ({baseline.entity})"
|
|
1019
|
-
)
|
|
1020
|
-
n, errors = seed.apply(client, baseline, dry_run=False)
|
|
1021
|
-
total_ok += n
|
|
1022
|
-
all_errors.extend(f"{path}: {e}" for e in errors)
|
|
1023
|
-
output.data(f" {n} record(s)")
|
|
1024
|
-
if all_errors:
|
|
1025
|
-
output.error(f"apply: {len(all_errors)} error(s), {total_ok} record(s) applied")
|
|
1026
|
-
for err in all_errors:
|
|
1027
|
-
output.error(err)
|
|
1028
|
-
raise SystemExit(1)
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
def _lifecycle_run(inst: Instance) -> None:
|
|
1032
|
-
"""Bare run (pin overlay); SystemExit 1 on failure."""
|
|
1033
|
-
paths = list(default_scenario_files(inst))
|
|
1034
|
-
scenarios = [run.load_scenario(path) for path in paths]
|
|
1035
|
-
ok = True
|
|
1036
|
-
with AcumaticaClient(inst) as client:
|
|
1037
|
-
for scenario in scenarios:
|
|
1038
|
-
ok = run.run(client, scenario) and ok
|
|
1039
|
-
if not ok:
|
|
1040
|
-
raise SystemExit(1)
|
|
1041
|
-
output.success(f"{len(scenarios)} scenario(s) passed on {inst.tenant}")
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
def _lifecycle_diff(inst: Instance) -> None:
|
|
1045
|
-
"""Bare diff; SystemExit 2 on drift (caller maps to 1 for check)."""
|
|
1046
|
-
paths = expand_files(default_apply_dirs(inst))
|
|
1047
|
-
drifts: list[str] = []
|
|
1048
|
-
with AcumaticaClient(inst) as client:
|
|
1049
|
-
for path in paths:
|
|
1050
|
-
baseline = seed.load_baseline(path)
|
|
1051
|
-
output.data(
|
|
1052
|
-
f"{path} -> {inst.tenant} on {inst.base_url} ({baseline.entity})"
|
|
1053
|
-
)
|
|
1054
|
-
file_drifts = seed.diff(client, baseline)
|
|
1055
|
-
drifts += file_drifts
|
|
1056
|
-
n = 1 if isinstance(baseline, seed.ActionFile) else len(baseline.records)
|
|
1057
|
-
if file_drifts:
|
|
1058
|
-
output.data(f" {n} record(s), {len(file_drifts)} drift(s)")
|
|
1059
|
-
else:
|
|
1060
|
-
output.data(f" {n} record(s) ok")
|
|
1061
|
-
_exit_on_drift(inst, drifts, len(paths))
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
876
|
SEED_DIRS = ("bootstrap", "baseline", "setup", "master")
|
|
1065
877
|
|
|
1066
878
|
|
|
@@ -70,6 +70,10 @@ INIT_TEMPLATES = (
|
|
|
70
70
|
"config/baseline/60-ledger-company.yaml",
|
|
71
71
|
),
|
|
72
72
|
("config/baseline/90-uoms.yaml", "config/baseline/90-uoms.yaml"),
|
|
73
|
+
(
|
|
74
|
+
"config/baseline/91-company-packaging.yaml",
|
|
75
|
+
"config/baseline/91-company-packaging.yaml",
|
|
76
|
+
),
|
|
73
77
|
("config/setup/10-financial-year.yaml", "config/setup/10-financial-year.yaml"),
|
|
74
78
|
("config/setup/20-master-calendar.yaml", "config/setup/20-master-calendar.yaml"),
|
|
75
79
|
("config/setup/30-open-periods.yaml", "config/setup/30-open-periods.yaml"),
|
|
@@ -128,6 +132,7 @@ INIT_TEMPLATES = (
|
|
|
128
132
|
),
|
|
129
133
|
("config/master/90-roles.yaml", "config/master/90-roles.yaml"),
|
|
130
134
|
("config/master/91-users.yaml", "config/master/91-users.yaml"),
|
|
135
|
+
("config/master/92-role-users.yaml", "config/master/92-role-users.yaml"),
|
|
131
136
|
(
|
|
132
137
|
"config/views/10-trial-balance.yaml",
|
|
133
138
|
"config/views/10-trial-balance.yaml",
|
|
@@ -590,6 +590,13 @@ class _Extraction:
|
|
|
590
590
|
if not live:
|
|
591
591
|
self._skip(target, "no records")
|
|
592
592
|
continue
|
|
593
|
+
# V53: Role.Users GET may stay empty; do not emit identity-only
|
|
594
|
+
# 92-role-users.yaml that would drop authored AssignUser rows.
|
|
595
|
+
if spec.include == ["Users"] and not any(
|
|
596
|
+
isinstance(r.get("Users"), list) and r["Users"] for r in records
|
|
597
|
+
):
|
|
598
|
+
self._skip(target, "Users GET empty")
|
|
599
|
+
continue
|
|
593
600
|
self._emit(target, _render(spec, records), len(records))
|
|
594
601
|
produced.add(spec.file)
|
|
595
602
|
return produced
|
|
@@ -397,6 +397,10 @@ def apply(
|
|
|
397
397
|
records in the same file. Returns ``(ok_count, error_messages)`` —
|
|
398
398
|
never silent partial; caller exits 1 when any errors remain. Exit 2
|
|
399
399
|
stays drift (``diff`` only).
|
|
400
|
+
|
|
401
|
+
Role.Users mapped-detail PUT is a no-op (V53/B33). After the Role PUT,
|
|
402
|
+
apply invokes contract action ``AssignUser`` per Username so persist
|
|
403
|
+
is PXDatabase UsersInRoles, not the mapped detail.
|
|
400
404
|
"""
|
|
401
405
|
if isinstance(baseline, ActionFile):
|
|
402
406
|
return _apply_action(client, baseline, dry_run)
|
|
@@ -406,14 +410,23 @@ def apply(
|
|
|
406
410
|
label = ", ".join(str(record[k]) for k in baseline.keys)
|
|
407
411
|
if dry_run:
|
|
408
412
|
output.data(f" would PUT {baseline.entity} [{label}]")
|
|
409
|
-
|
|
413
|
+
try:
|
|
414
|
+
_assign_role_users(client, baseline, record, dry_run=True)
|
|
415
|
+
ok += 1
|
|
416
|
+
except RuntimeError as err:
|
|
417
|
+
msg = f"{baseline.entity} [{label}]: {err}"
|
|
418
|
+
output.error(msg)
|
|
419
|
+
errors.append(msg)
|
|
410
420
|
continue
|
|
411
421
|
try:
|
|
412
422
|
body = _put_body(client, baseline, record)
|
|
423
|
+
if baseline.entity == "Role":
|
|
424
|
+
body.pop("Users", None)
|
|
413
425
|
_put(client, baseline.entity, body, baseline.endpoint)
|
|
414
426
|
if baseline.entity == "Company":
|
|
415
427
|
client.refresh_after_company()
|
|
416
428
|
output.data(f" PUT {baseline.entity} [{label}]")
|
|
429
|
+
_assign_role_users(client, baseline, record, dry_run=False)
|
|
417
430
|
ok += 1
|
|
418
431
|
except RuntimeError as err:
|
|
419
432
|
msg = f"{baseline.entity} [{label}]: {err}"
|
|
@@ -422,6 +435,51 @@ def apply(
|
|
|
422
435
|
return ok, errors
|
|
423
436
|
|
|
424
437
|
|
|
438
|
+
def _assign_role_users(
|
|
439
|
+
client: AcumaticaClient,
|
|
440
|
+
baseline: BaselineFile,
|
|
441
|
+
record: dict[str, Any],
|
|
442
|
+
*,
|
|
443
|
+
dry_run: bool,
|
|
444
|
+
) -> None:
|
|
445
|
+
"""V53/B33: persist Role.Users via AssignUser, not mapped-detail PUT.
|
|
446
|
+
|
|
447
|
+
Additive-only: ``delete: true`` merge rows are skipped. Mapped GET of
|
|
448
|
+
Users may be empty, so extra live members are not a persist delete.
|
|
449
|
+
Role PUT strips ``Users`` so additive-only does not depend on B33.
|
|
450
|
+
"""
|
|
451
|
+
if baseline.entity != "Role":
|
|
452
|
+
return
|
|
453
|
+
users = record.get("Users")
|
|
454
|
+
if not isinstance(users, list):
|
|
455
|
+
return
|
|
456
|
+
rolename = record.get("Rolename")
|
|
457
|
+
if not isinstance(rolename, str) or not rolename:
|
|
458
|
+
return
|
|
459
|
+
to_assign: list[str] = []
|
|
460
|
+
for i, row in enumerate(users):
|
|
461
|
+
if isinstance(row, dict) and row.get("delete") is True:
|
|
462
|
+
continue
|
|
463
|
+
if not isinstance(row, dict):
|
|
464
|
+
raise RuntimeError(f"Users[{i}] is not a mapping")
|
|
465
|
+
username = row.get("Username")
|
|
466
|
+
if not isinstance(username, str) or not username:
|
|
467
|
+
raise RuntimeError(f"Users[{i}] missing Username")
|
|
468
|
+
to_assign.append(username)
|
|
469
|
+
for username in to_assign:
|
|
470
|
+
if dry_run:
|
|
471
|
+
output.data(f" would invoke AssignUser [{rolename}/{username}]")
|
|
472
|
+
continue
|
|
473
|
+
client.invoke(
|
|
474
|
+
"Role",
|
|
475
|
+
"AssignUser",
|
|
476
|
+
{"Rolename": rolename},
|
|
477
|
+
{"Username": username},
|
|
478
|
+
baseline.endpoint,
|
|
479
|
+
)
|
|
480
|
+
output.data(f" invoke AssignUser [{rolename}/{username}]")
|
|
481
|
+
|
|
482
|
+
|
|
425
483
|
def _put(
|
|
426
484
|
client: AcumaticaClient,
|
|
427
485
|
entity: str,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# entity contract entity name (the GET source and the emitted `entity:`)
|
|
7
7
|
# endpoint omitted = the instance Default endpoint; the symbolic name
|
|
8
8
|
# `bootstrap` stays symbolic in emitted files and resolves to
|
|
9
|
-
# the packaged full company Bootstrap/1.
|
|
9
|
+
# the packaged full company Bootstrap/1.10.0 at HTTP/load time
|
|
10
10
|
# (package SoT - V20/V21/T178; data-repo project.xml forbidden)
|
|
11
11
|
# keys key fields: the emitted `key:`, the record sort order, and
|
|
12
12
|
# the B9 fallback's $select projection; the tuple must
|
|
@@ -44,10 +44,9 @@ entities:
|
|
|
44
44
|
|
|
45
45
|
# config/bootstrap/ - the virgin-tenant phase via the custom Bootstrap
|
|
46
46
|
# endpoint: built-in endpoints cannot create the first organization (C).
|
|
47
|
-
# Company
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
# fields that GET returns stay in include.
|
|
47
|
+
# Company identity first (bootstrap). Packaging qty/UOMs are a second
|
|
48
|
+
# Company PUT after 90-uoms.yaml (see that row). StockItem WeightUOM
|
|
49
|
+
# /VolumeUOM stay GET-omit (B26).
|
|
51
50
|
|
|
52
51
|
- entity: Company
|
|
53
52
|
endpoint: bootstrap
|
|
@@ -58,9 +57,6 @@ entities:
|
|
|
58
57
|
- OrganizationType
|
|
59
58
|
- BaseCuryID
|
|
60
59
|
- CountryID
|
|
61
|
-
- DecPlQty
|
|
62
|
-
- WeightUOM
|
|
63
|
-
- VolumeUOM
|
|
64
60
|
|
|
65
61
|
- entity: CreditTerms
|
|
66
62
|
endpoint: bootstrap
|
|
@@ -167,6 +163,20 @@ entities:
|
|
|
167
163
|
- Description
|
|
168
164
|
- L3Code
|
|
169
165
|
|
|
166
|
+
# Company packaging (gh #34 / V52): second PUT after UOMs so KG/LITER
|
|
167
|
+
# exist for CommonSetup_RowPersisting when DistributionModule is on.
|
|
168
|
+
# Include-partition of the same live Company row (warehouse-phase idiom).
|
|
169
|
+
# Mapped fields GET-return stay in include.
|
|
170
|
+
|
|
171
|
+
- entity: Company
|
|
172
|
+
endpoint: bootstrap
|
|
173
|
+
keys: [AcctCD]
|
|
174
|
+
file: config/baseline/91-company-packaging.yaml
|
|
175
|
+
include:
|
|
176
|
+
- DecPlQty
|
|
177
|
+
- WeightUOM
|
|
178
|
+
- VolumeUOM
|
|
179
|
+
|
|
170
180
|
# config/master/ - full packaged seed (T117). Multi-file Warehouse uses
|
|
171
181
|
# include field-partition (same live warehouse row, three apply phases).
|
|
172
182
|
# Multi-file StockItem uses OData filter-split on ItemClass.
|
|
@@ -522,10 +532,12 @@ entities:
|
|
|
522
532
|
- Active
|
|
523
533
|
- StockComponents
|
|
524
534
|
|
|
525
|
-
# config/master/ Role then User (V22 prefixes
|
|
526
|
-
# only — Default has no Role/User surface.
|
|
527
|
-
#
|
|
528
|
-
#
|
|
535
|
+
# config/master/ Role then User then Role.Users (V22 prefixes 90/91/92;
|
|
536
|
+
# T145/T146/T148/T228). Bootstrap only — Default has no Role/User surface.
|
|
537
|
+
# Persist = Role action AssignUser (PXDatabase UsersInRoles; T228/B33).
|
|
538
|
+
# User detail Roles is GET/diff shape (PUT never writes UsersInRoles —
|
|
539
|
+
# B31/B32/B33). Package 90-roles = full pre-build CompanyID=1
|
|
540
|
+
# Roles dump; 91-users demo soadmin identity; 92-role-users membership.
|
|
529
541
|
# Password is write-only on apply (V39/T147): include omits Password so
|
|
530
542
|
# extract never emits hashes (b64__Password also out of include).
|
|
531
543
|
|
|
@@ -551,6 +563,19 @@ entities:
|
|
|
551
563
|
- PasswordChangeOnNextLogin
|
|
552
564
|
- Roles
|
|
553
565
|
|
|
566
|
+
# Persist membership (T228/V53): apply Role.Users invokes AssignUser.
|
|
567
|
+
# Filter-split from 90-roles (Descr-only identity dump). Mapped-detail
|
|
568
|
+
# PUT never writes UsersInRoles (B31/B32/B33).
|
|
569
|
+
- entity: Role
|
|
570
|
+
endpoint: bootstrap
|
|
571
|
+
keys: [Rolename]
|
|
572
|
+
file: config/master/92-role-users.yaml
|
|
573
|
+
filter: Rolename eq 'SO Admin'
|
|
574
|
+
detail_keys:
|
|
575
|
+
Users: Username
|
|
576
|
+
include:
|
|
577
|
+
- Users
|
|
578
|
+
|
|
554
579
|
# config/setup/ - the GL action chain (T36/T37): financial-year setup,
|
|
555
580
|
# then calendar generation, then period activation; numbered prefixes
|
|
556
581
|
# mirror the verified apply order.
|
|
@@ -30,8 +30,7 @@ acu state
|
|
|
30
30
|
# warm gate: once-capital only — additive buy/sell moves numeric observations
|
|
31
31
|
acu run scenario/10-seed-capital.yaml && acu state --assert-unchanged
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
# acu check --yes
|
|
33
|
+
# SSH cold rebuild: acu tenant create --login NAME, then apply, run, diff
|
|
35
34
|
|
|
36
35
|
# Optional: re-seed from live (inverse of apply; always under config/)
|
|
37
36
|
# acu extract --out . --force
|
|
@@ -48,7 +47,7 @@ Bare `acu apply` / `acu diff` also prefer `config/` when those trees exist.
|
|
|
48
47
|
| `config/bootstrap/` | Company, features, credit terms (Bootstrap contract is package SoT — never scaffolded) |
|
|
49
48
|
| `config/baseline/` | GL foundation (COA, ledger, subaccounts, UOMs) |
|
|
50
49
|
| `config/setup/` | Financial year, master calendar, open periods |
|
|
51
|
-
| `config/master/` | Numbering (`05-numbering-sequences`) before module prefs; inventory, warehouse, items, vendors, customers; roles/users (`90-roles` then `91-users`) |
|
|
50
|
+
| `config/master/` | Numbering (`05-numbering-sequences`) before module prefs; inventory, warehouse, items, vendors, customers; roles/users (`90-roles` then `91-users` then `92-role-users`) |
|
|
52
51
|
| `scenario/10-seed-capital.yaml` | Once-class owner capital JE (skip-if-present when present); Period = `${current_period}` |
|
|
53
52
|
| `scenario/20-buy.yaml` | Additive component PO → receipt → bill → AP pay |
|
|
54
53
|
| `scenario/30-build.yaml` | Additive kit assembly |
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// acu bootstrap CustomizationPlugin (SPEC T11).
|
|
2
|
-
//
|
|
3
|
-
// Runs in-process on customization publish - the one write path to
|
|
4
|
-
// FeaturesSet that works: the contract API cannot persist features no
|
|
5
|
-
// matter what endpoint fronts CS100000 (T3 verdict, docs/rest-api.md).
|
|
6
|
-
//
|
|
7
|
-
// Deliberately writes through PXDatabase, not FeaturesMaint: the graph
|
|
8
|
-
// save collides with the publish pipeline's concurrent plugin invocations
|
|
9
|
-
// ("Another process has added the 'FeaturesSet' record" - observed live,
|
|
10
|
-
// 26.101.0225) and nothing persists. A keyed row write is deterministic
|
|
11
|
-
// and idempotent: update the existing row, insert when absent. All 205
|
|
12
|
-
// NOT NULL bit columns are filled reflectively (only ~136 carry DB
|
|
13
|
-
// defaults); Status = 0 means Validated (PXIntList, verified vs live).
|
|
14
|
-
//
|
|
15
|
-
// The Enabled set is NOT authored here (SPEC V2: feature flags are config
|
|
16
|
-
// "what", never tool source - B6): package_zip() substitutes the
|
|
17
|
-
// ACU_FEATURES sentinel below with the data repo's bootstrap/features.yaml
|
|
18
|
-
// list (built-in six when the file is absent) at package-build time.
|
|
19
|
-
using System.Collections.Generic;
|
|
20
|
-
using System.Reflection;
|
|
21
|
-
using PX.Data;
|
|
22
|
-
using PX.Objects.CS;
|
|
23
|
-
|
|
24
|
-
namespace AcuBootstrap
|
|
25
|
-
{
|
|
26
|
-
public class AcuBootstrapPlugin : Customization.CustomizationPlugin
|
|
27
|
-
{
|
|
28
|
-
private static readonly HashSet<string> Enabled = new HashSet<string>
|
|
29
|
-
{
|
|
30
|
-
/*ACU_FEATURES*/
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
public override void UpdateDatabase()
|
|
34
|
-
{
|
|
35
|
-
var flags = new List<PXDataFieldAssign>();
|
|
36
|
-
var known = new HashSet<string>();
|
|
37
|
-
foreach (PropertyInfo prop in typeof(FeaturesSet).GetProperties())
|
|
38
|
-
{
|
|
39
|
-
if (prop.PropertyType == typeof(bool?))
|
|
40
|
-
{
|
|
41
|
-
known.Add(prop.Name);
|
|
42
|
-
flags.Add(new PXDataFieldAssign(
|
|
43
|
-
prop.Name, PXDbType.Bit, Enabled.Contains(prop.Name)));
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
foreach (string name in Enabled)
|
|
47
|
-
{
|
|
48
|
-
if (!known.Contains(name))
|
|
49
|
-
{
|
|
50
|
-
// silent-typo guard (T24): a misspelled features.yaml
|
|
51
|
-
// entry enables nothing - say so in the publish log
|
|
52
|
-
WriteLog("AcuBootstrap: unknown feature name '" + name
|
|
53
|
-
+ "' - no FeaturesSet property, nothing enabled");
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
flags.Add(new PXDataFieldAssign("Status", PXDbType.Int, 0)); // Validated
|
|
57
|
-
|
|
58
|
-
using (var tx = new PXTransactionScope())
|
|
59
|
-
{
|
|
60
|
-
bool updated = PXDatabase.Update<FeaturesSet>(flags.ToArray());
|
|
61
|
-
if (!updated)
|
|
62
|
-
{
|
|
63
|
-
PXDatabase.Insert<FeaturesSet>(flags.ToArray());
|
|
64
|
-
WriteLog("AcuBootstrap: FeaturesSet row inserted (features enabled)");
|
|
65
|
-
}
|
|
66
|
-
else
|
|
67
|
-
{
|
|
68
|
-
WriteLog("AcuBootstrap: FeaturesSet row updated (features enabled)");
|
|
69
|
-
}
|
|
70
|
-
tx.Complete();
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/overlays/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/scenario/20-buy.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/scenario/30-build.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.28.0 → acumatica_cli-0.30.0}/src/acumatica_cli/templates/scenario/40-sell.yaml
RENAMED
|
File without changes
|
|
File without changes
|