acumatica-cli 0.8.2__tar.gz → 0.8.3__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 (32) hide show
  1. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/PKG-INFO +30 -10
  2. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/README.md +29 -9
  3. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/pyproject.toml +1 -1
  4. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/cli.py +86 -16
  5. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/__init__.py +0 -0
  6. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/bootstrap.py +0 -0
  7. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
  8. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/bootstrap_project.xml +0 -0
  9. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/client.py +0 -0
  10. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/config.py +0 -0
  11. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/extract.py +0 -0
  12. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/extract_manifest.yaml +0 -0
  13. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/firstlogin.py +0 -0
  14. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/models.py +0 -0
  15. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/output.py +0 -0
  16. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/run.py +0 -0
  17. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/seed.py +0 -0
  18. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/baseline/10-subaccounts.yaml +0 -0
  19. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/baseline/20-accounts.yaml +0 -0
  20. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/baseline/40-ledger.yaml +0 -0
  21. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/baseline/50-gl-preferences.yaml +0 -0
  22. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/baseline/60-ledger-company.yaml +0 -0
  23. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/baseline/90-uoms.yaml +0 -0
  24. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/bootstrap/company.yaml +0 -0
  25. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/bootstrap/credit-terms.yaml +0 -0
  26. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/bootstrap/features.yaml +0 -0
  27. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/env +0 -0
  28. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/gitignore +0 -0
  29. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/setup/10-financial-year.yaml +0 -0
  30. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/setup/20-master-calendar.yaml +0 -0
  31. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/templates/setup/30-open-periods.yaml +0 -0
  32. {acumatica_cli-0.8.2 → acumatica_cli-0.8.3}/src/acumatica_cli/tenant.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: acumatica-cli
3
- Version: 0.8.2
3
+ Version: 0.8.3
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>
@@ -40,11 +40,24 @@ acu config init --host erp.example.com my-erp
40
40
  cd my-erp # edit .env: set ACU_PASSWORD
41
41
 
42
42
  acu config check # read-only preflight
43
- acu tenant create --id 3 --login DEV # create the tenant + bootstrap it
43
+ acu tenant create --id 3 --login DEV # create the tenant + bootstrap it (needs SSH)
44
44
  acu --tenant DEV apply # seed bootstrap/, baseline/, setup/
45
45
  acu --tenant DEV diff # prove zero drift (exit 2 on drift)
46
46
  ```
47
47
 
48
+ **Hosted Acumatica (no SSH):** the tenant already exists; leave `ACU_SSH` blank.
49
+
50
+ ```sh
51
+ acu config init --host customer.acumatica.com my-erp
52
+ cd my-erp # edit .env: ACU_TENANT, ACU_PASSWORD; ACU_SSH=
53
+ acu config check # REST preflight; ssh probe is skipped
54
+ acu --tenant DEV bootstrap # publish AcuBootstrap via REST only
55
+ acu --tenant DEV apply
56
+ acu --tenant DEV diff
57
+ # offline UI fallback when REST publish is blocked:
58
+ acu bootstrap --export AcuBootstrap.zip # import + publish on SM204505
59
+ ```
60
+
48
61
  ## CLI map
49
62
 
50
63
  ```text
@@ -53,10 +66,11 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
53
66
 
54
67
  ├── tenant tenant CRUD (ac.exe over SSH — control plane)
55
68
  │ ├── list CompanyID, sign-in name, internal CD, type
56
- │ ├── create --id N --login NAME create + bootstrap; re-run to republish
69
+ │ ├── create --id N --login NAME create + bootstrap; re-run to republish (SSH)
57
70
  │ │ [--type SalesDemo|T100|U100] [--parent N] [--hidden] [--no-init]
58
71
  │ └── delete --id N [--yes] delete the tenant and its data, recycle app pool
59
72
 
73
+ ├── bootstrap [--export PATH] publish AcuBootstrap (REST); --export = offline zip
60
74
  ├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
61
75
  ├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
62
76
  ├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
@@ -116,16 +130,20 @@ Everything lives in one `.env` file: *where* to apply and *who* signs in. Three
116
130
  ```sh
117
131
  ACU_BASE_URL=http://acu-dev1.vm.internal/AcumaticaERP # required: REST root
118
132
  ACU_TENANT=LAB5 # sign-in name of the tenant API sessions use
119
- ACU_SSH=Administrator@acu-dev1.vm.internal # required: control-plane user@host
133
+ ACU_SSH=Administrator@acu-dev1.vm.internal # optional: control-plane user@host (tenant CRUD)
120
134
  ACU_USER=admin # optional, defaults to admin
121
- ACU_PASSWORD=... # required
135
+ ACU_PASSWORD=... # required for live commands
122
136
  ```
123
137
 
124
138
  Worth knowing:
125
139
 
126
- - The file is found by walking up from the current directory, so any subdirectory of the data repo works. Without a `.env`, global flags plus the process environment supply the full configuration.
127
- - Nothing is derived: split-horizon DNS, port forwards, and jump hosts are all handled by writing the address you actually want into the two address keys.
128
- - `acu config show` prints the fully resolved configuration as a complete, valid `.env` — every knob visible, the password excluded. Redirect it to turn resolved state into a working config: `acu config show > .env`.
140
+ - The file is found by walking up from the current directory, so any subdirectory of the data repo works.
141
+ - Without a `.env`, global flags plus the process environment supply the full configuration.
142
+ - `ACU_SSH` is optional.
143
+ - Leave it blank on hosted instances; only `acu tenant` needs it.
144
+ - Nothing is derived: split-horizon DNS, port forwards, and jump hosts are all handled by writing the address you actually want into the address keys.
145
+ - `acu config show` prints the fully resolved configuration as a complete, valid `.env` — every knob visible, the password excluded.
146
+ - Redirect it to turn resolved state into a working config: `acu config show > .env`.
129
147
 
130
148
  Verify before touching anything live:
131
149
 
@@ -139,9 +157,11 @@ acu apply --dry-run # show what would be written, write nothing
139
157
  `acu` talks to an instance over two independent channels:
140
158
 
141
159
  - **Control plane (SSH):** `acu tenant` runs `ac.exe -cm:CompanyConfig` and `sqlcmd` on the Windows guest — see [`docs/ac-exe.md`](docs/ac-exe.md).
142
- - **Data plane (REST):** `acu apply`, `diff`, `run`, `extract`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
160
+ - **Data plane (REST):** `acu bootstrap`, `apply`, `diff`, `run`, `extract`, and `schema` use the contract-based API and CustomizationApi — see [`docs/rest-api.md`](docs/rest-api.md).
143
161
 
144
- If you never touch `acu tenant`, you never need SSH — every other command is pure REST.
162
+ If you never touch `acu tenant`, you never need SSH.
163
+ On a hosted instance, publish AcuBootstrap with `acu bootstrap`, then seed with `apply`.
164
+ `acu bootstrap --export PATH` writes the package zip for manual import on the Customization Projects screen (SM204505) when you cannot call the API.
145
165
 
146
166
  ## SSH setup (control plane)
147
167
 
@@ -23,11 +23,24 @@ acu config init --host erp.example.com my-erp
23
23
  cd my-erp # edit .env: set ACU_PASSWORD
24
24
 
25
25
  acu config check # read-only preflight
26
- acu tenant create --id 3 --login DEV # create the tenant + bootstrap it
26
+ acu tenant create --id 3 --login DEV # create the tenant + bootstrap it (needs SSH)
27
27
  acu --tenant DEV apply # seed bootstrap/, baseline/, setup/
28
28
  acu --tenant DEV diff # prove zero drift (exit 2 on drift)
29
29
  ```
30
30
 
31
+ **Hosted Acumatica (no SSH):** the tenant already exists; leave `ACU_SSH` blank.
32
+
33
+ ```sh
34
+ acu config init --host customer.acumatica.com my-erp
35
+ cd my-erp # edit .env: ACU_TENANT, ACU_PASSWORD; ACU_SSH=
36
+ acu config check # REST preflight; ssh probe is skipped
37
+ acu --tenant DEV bootstrap # publish AcuBootstrap via REST only
38
+ acu --tenant DEV apply
39
+ acu --tenant DEV diff
40
+ # offline UI fallback when REST publish is blocked:
41
+ acu bootstrap --export AcuBootstrap.zip # import + publish on SM204505
42
+ ```
43
+
31
44
  ## CLI map
32
45
 
33
46
  ```text
@@ -36,10 +49,11 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
36
49
 
37
50
  ├── tenant tenant CRUD (ac.exe over SSH — control plane)
38
51
  │ ├── list CompanyID, sign-in name, internal CD, type
39
- │ ├── create --id N --login NAME create + bootstrap; re-run to republish
52
+ │ ├── create --id N --login NAME create + bootstrap; re-run to republish (SSH)
40
53
  │ │ [--type SalesDemo|T100|U100] [--parent N] [--hidden] [--no-init]
41
54
  │ └── delete --id N [--yes] delete the tenant and its data, recycle app pool
42
55
 
56
+ ├── bootstrap [--export PATH] publish AcuBootstrap (REST); --export = offline zip
43
57
  ├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
44
58
  ├── diff [FILES...] drift check vs the live tenant (exit 2 on drift)
45
59
  ├── run [--dry-run] [FILES...] execute transaction scenario YAML (exit 1 on any miss)
@@ -99,16 +113,20 @@ Everything lives in one `.env` file: *where* to apply and *who* signs in. Three
99
113
  ```sh
100
114
  ACU_BASE_URL=http://acu-dev1.vm.internal/AcumaticaERP # required: REST root
101
115
  ACU_TENANT=LAB5 # sign-in name of the tenant API sessions use
102
- ACU_SSH=Administrator@acu-dev1.vm.internal # required: control-plane user@host
116
+ ACU_SSH=Administrator@acu-dev1.vm.internal # optional: control-plane user@host (tenant CRUD)
103
117
  ACU_USER=admin # optional, defaults to admin
104
- ACU_PASSWORD=... # required
118
+ ACU_PASSWORD=... # required for live commands
105
119
  ```
106
120
 
107
121
  Worth knowing:
108
122
 
109
- - The file is found by walking up from the current directory, so any subdirectory of the data repo works. Without a `.env`, global flags plus the process environment supply the full configuration.
110
- - Nothing is derived: split-horizon DNS, port forwards, and jump hosts are all handled by writing the address you actually want into the two address keys.
111
- - `acu config show` prints the fully resolved configuration as a complete, valid `.env` — every knob visible, the password excluded. Redirect it to turn resolved state into a working config: `acu config show > .env`.
123
+ - The file is found by walking up from the current directory, so any subdirectory of the data repo works.
124
+ - Without a `.env`, global flags plus the process environment supply the full configuration.
125
+ - `ACU_SSH` is optional.
126
+ - Leave it blank on hosted instances; only `acu tenant` needs it.
127
+ - Nothing is derived: split-horizon DNS, port forwards, and jump hosts are all handled by writing the address you actually want into the address keys.
128
+ - `acu config show` prints the fully resolved configuration as a complete, valid `.env` — every knob visible, the password excluded.
129
+ - Redirect it to turn resolved state into a working config: `acu config show > .env`.
112
130
 
113
131
  Verify before touching anything live:
114
132
 
@@ -122,9 +140,11 @@ acu apply --dry-run # show what would be written, write nothing
122
140
  `acu` talks to an instance over two independent channels:
123
141
 
124
142
  - **Control plane (SSH):** `acu tenant` runs `ac.exe -cm:CompanyConfig` and `sqlcmd` on the Windows guest — see [`docs/ac-exe.md`](docs/ac-exe.md).
125
- - **Data plane (REST):** `acu apply`, `diff`, `run`, `extract`, and `schema` use the contract-based API (`/entity/Default/25.200.001/`), where `PUT` is a keyed upsert — see [`docs/rest-api.md`](docs/rest-api.md).
143
+ - **Data plane (REST):** `acu bootstrap`, `apply`, `diff`, `run`, `extract`, and `schema` use the contract-based API and CustomizationApi — see [`docs/rest-api.md`](docs/rest-api.md).
126
144
 
127
- If you never touch `acu tenant`, you never need SSH — every other command is pure REST.
145
+ If you never touch `acu tenant`, you never need SSH.
146
+ On a hosted instance, publish AcuBootstrap with `acu bootstrap`, then seed with `apply`.
147
+ `acu bootstrap --export PATH` writes the package zip for manual import on the Customization Projects screen (SM204505) when you cannot call the API.
128
148
 
129
149
  ## SSH setup (control plane)
130
150
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acumatica-cli"
3
- version = "0.8.2"
3
+ version = "0.8.3"
4
4
  description = "Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data"
5
5
  authors = [{ name = "Konstantin Borovik", email = "kb@lab5.ca" }]
6
6
  readme = "README.md"
@@ -282,32 +282,102 @@ def _init_tenant(inst: Instance, mgr: TenantManager, login_name: str) -> None:
282
282
  output.success(f"admin {result}; tenant {login_name} is ready")
283
283
 
284
284
 
285
- def _bootstrap_tenant(inst: Instance, mgr: TenantManager, login_name: str) -> None:
286
- """Publish the bootstrap package into the fresh tenant (data plane).
285
+ def _bootstrap_features() -> list[str] | None:
286
+ """Feature list from the data repo, or None package-built-in six.
287
287
 
288
- Idempotent on content, not existence (V4): publish() skips only when the
289
- published package carries the digest of the package built now. The
290
- session targets the new tenant explicitly (V5), never a config default.
291
- The feature set is data (V2): bootstrap/features.yaml in the data repo,
292
- no data repo or no file -> the built-in six.
288
+ features.yaml is package-build config (V2), not a seed file: both the
289
+ live publish path and --export read it the same way.
293
290
  """
294
- inst = inst.model_copy(update={"tenant": login_name})
295
291
  root = find_data_root()
296
- features = bootstrap.load_features(root) if root is not None else None
292
+ return bootstrap.load_features(root) if root is not None else None
293
+
294
+
295
+ def _publish_bootstrap_package(inst: Instance, features: list[str] | None) -> str:
296
+ """Import + publish AcuBootstrap into inst.tenant (data plane only)."""
297
297
  with (
298
298
  output.step(f"publishing {bootstrap.PACKAGE_NAME} to {inst.tenant}"),
299
299
  AcumaticaClient(inst) as client,
300
300
  ):
301
- result = bootstrap.publish(client, features=features)
302
- output.success(f"{bootstrap.PACKAGE_NAME} {result}")
303
- # unconditional: the publish restarts the site BEFORE its DB transaction
304
- # commits, so the restarted domain caches the feature slot pre-plugin
305
- # (verified live: gated screens stay 403 until one more recycle); on the
306
- # skip path a recycle is the cheap way to make a resumed run sound too
301
+ return bootstrap.publish(client, features=features)
302
+
303
+
304
+ def _recycle_after_bootstrap(inst: Instance, mgr: TenantManager | None) -> None:
305
+ """Post-publish app-pool recycle when the control plane is available.
306
+
307
+ The publish restarts the site BEFORE its DB transaction commits, so the
308
+ restarted domain caches the feature slot pre-plugin (verified live:
309
+ gated screens stay 403 until one more recycle). On the skip path a
310
+ recycle is the cheap way to make a resumed run sound too. Hosted
311
+ (no ACU_SSH): warn and continue — the customer must restart the site
312
+ another way before feature-gated apply can succeed.
313
+ """
314
+ if mgr is None and not inst.ssh:
315
+ output.warn(
316
+ "ACU_SSH not set: skipped app-pool recycle; "
317
+ "feature-gated screens may stay 403 until the site restarts"
318
+ )
319
+ return
320
+ if mgr is None:
321
+ mgr = TenantManager(inst)
307
322
  with output.step("recycling app pool (feature set loads at app start)"):
308
323
  mgr.recycle_app_pool()
309
324
  with output.step("waiting for the site to come back"):
310
- firstlogin.initialize_admin_password(inst, tenant=login_name)
325
+ firstlogin.initialize_admin_password(inst, tenant=inst.tenant)
326
+
327
+
328
+ def _bootstrap_tenant(inst: Instance, mgr: TenantManager, login_name: str) -> None:
329
+ """Publish the bootstrap package into the fresh tenant (data plane).
330
+
331
+ Idempotent on content, not existence (V4): publish() skips only when the
332
+ published package carries the digest of the package built now. The
333
+ session targets the new tenant explicitly (V5), never a config default.
334
+ Tenant create always has SSH (TenantManager already constructed), so
335
+ the post-publish recycle always runs on this path.
336
+ """
337
+ inst = inst.model_copy(update={"tenant": login_name})
338
+ features = _bootstrap_features()
339
+ result = _publish_bootstrap_package(inst, features)
340
+ output.success(f"{bootstrap.PACKAGE_NAME} {result}")
341
+ _recycle_after_bootstrap(inst, mgr)
342
+
343
+
344
+ @cli.command("bootstrap")
345
+ @click.option(
346
+ "--export",
347
+ "export_path",
348
+ type=click.Path(path_type=Path, dir_okay=False),
349
+ default=None,
350
+ help="Write the package zip to PATH (offline; no REST, no SSH) "
351
+ "for Customization Projects UI import",
352
+ )
353
+ @click.pass_context
354
+ def bootstrap_cmd(ctx: click.Context, export_path: Path | None) -> None:
355
+ """Publish AcuBootstrap into the session tenant (or export the package zip).
356
+
357
+ Hosted / no-SSH path: CustomizationApi publish is pure REST, so this
358
+ command never requires ACU_SSH. When SSH is set, an app-pool recycle
359
+ follows publish so feature-gated screens load; when unset, a warning
360
+ notes that the site may need another restart before apply.
361
+
362
+ --export writes the same feature-spliced package that publish would
363
+ import, with no HTTP and no password — the SM204505 UI-import fallback.
364
+ """
365
+ features = _bootstrap_features()
366
+ if export_path is not None:
367
+ if not export_path.parent.exists():
368
+ raise SystemExit(f"{export_path.parent}: directory does not exist")
369
+ export_path.write_bytes(bootstrap.package_zip(features))
370
+ output.success(f"wrote {export_path}")
371
+ return
372
+ inst = _resolve_instance(ctx)
373
+ if not inst.tenant:
374
+ raise SystemExit(
375
+ "tenant not set (pass --tenant, "
376
+ "or put ACU_TENANT in .env or the environment)"
377
+ )
378
+ result = _publish_bootstrap_package(inst, features)
379
+ output.success(f"{bootstrap.PACKAGE_NAME} {result}")
380
+ _recycle_after_bootstrap(inst, mgr=None)
311
381
 
312
382
 
313
383
  @tenant_group.command("delete")