acumatica-cli 0.18.1__tar.gz → 0.19.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 (70) hide show
  1. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/PKG-INFO +3 -2
  2. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/README.md +2 -1
  3. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/pyproject.toml +1 -1
  4. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/pyproject.toml.orig +1 -1
  5. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/cli.py +38 -1
  6. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/client.py +1 -1
  7. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/gitignore +3 -2
  8. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/tenant.py +48 -2
  9. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/__init__.py +0 -0
  10. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/bootstrap.py +0 -0
  11. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
  12. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/bootstrap_project.xml +0 -0
  13. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/config.py +0 -0
  14. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/extract.py +0 -0
  15. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/firstlogin.py +0 -0
  16. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/inventory.py +0 -0
  17. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/models.py +0 -0
  18. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/output.py +0 -0
  19. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/reconcile.py +0 -0
  20. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/run.py +0 -0
  21. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/seed.py +0 -0
  22. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/seed_catalog.yaml +0 -0
  23. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/snapshot_map.yaml +0 -0
  24. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/state.py +0 -0
  25. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/target.py +0 -0
  26. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/README.md +0 -0
  27. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/baseline/10-subaccounts.yaml +0 -0
  28. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/baseline/20-accounts.yaml +0 -0
  29. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/baseline/40-ledger.yaml +0 -0
  30. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/baseline/50-gl-preferences.yaml +0 -0
  31. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/baseline/60-ledger-company.yaml +0 -0
  32. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/baseline/90-uoms.yaml +0 -0
  33. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/bootstrap/company.yaml +0 -0
  34. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/bootstrap/credit-terms.yaml +0 -0
  35. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/bootstrap/features.yaml +0 -0
  36. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/10-reason-codes.yaml +0 -0
  37. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/20-in-preferences.yaml +0 -0
  38. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/30-availability-rules.yaml +0 -0
  39. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/40-posting-classes.yaml +0 -0
  40. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/50-warehouse.yaml +0 -0
  41. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/51-warehouse-locations.yaml +0 -0
  42. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/52-warehouse-defaults.yaml +0 -0
  43. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/53-tax-categories.yaml +0 -0
  44. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/54-item-classes.yaml +0 -0
  45. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/56-so-preferences.yaml +0 -0
  46. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/57-po-preferences.yaml +0 -0
  47. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/58-order-types.yaml +0 -0
  48. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/60-ar-preferences.yaml +0 -0
  49. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/61-ap-preferences.yaml +0 -0
  50. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/62-ca-preferences.yaml +0 -0
  51. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/63-cash-account.yaml +0 -0
  52. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/64-payment-methods.yaml +0 -0
  53. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/65-statement-cycles.yaml +0 -0
  54. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/70-vendor-classes.yaml +0 -0
  55. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/71-customer-classes.yaml +0 -0
  56. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/75-vendors.yaml +0 -0
  57. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/76-customers.yaml +0 -0
  58. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/80-stock-items-parts.yaml +0 -0
  59. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/82-stock-items-kits.yaml +0 -0
  60. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/master/85-kit-specifications.yaml +0 -0
  61. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/setup/10-financial-year.yaml +0 -0
  62. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/setup/20-master-calendar.yaml +0 -0
  63. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/setup/30-open-periods.yaml +0 -0
  64. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/config/views/10-trial-balance.yaml +0 -0
  65. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/env +0 -0
  66. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/scenario/10-seed-capital.yaml +0 -0
  67. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/scenario/20-buy.yaml +0 -0
  68. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/scenario/30-build.yaml +0 -0
  69. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/scenario/40-sell.yaml +0 -0
  70. {acumatica_cli-0.18.1 → acumatica_cli-0.19.0}/src/acumatica_cli/templates/target +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acumatica-cli
3
- Version: 0.18.1
3
+ Version: 0.19.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>
@@ -78,7 +78,8 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
78
78
  │ ├── list CompanyID, sign-in name, internal CD, type
79
79
  │ ├── create --id N --login NAME create + bootstrap; re-run to republish (SSH)
80
80
  │ │ [--type SalesDemo|T100|U100] [--parent N] [--hidden] [--no-init]
81
- │ └── delete --id N [--yes] delete the tenant and its data, recycle app pool
81
+ │ ├── delete --id N [--yes] delete the tenant and its data, recycle app pool
82
+ │ └── recycle [--yes] restart site app pool (tenant map + free API slots)
82
83
  │
83
84
  ├── bootstrap [--export PATH] publish AcuBootstrap (REST); --export = offline zip
84
85
  ├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
@@ -60,7 +60,8 @@ acu [--tenant NAME] [--url URL] [--ssh USER@HOST] [--api-version V]
60
60
  │ ├── list CompanyID, sign-in name, internal CD, type
61
61
  │ ├── create --id N --login NAME create + bootstrap; re-run to republish (SSH)
62
62
  │ │ [--type SalesDemo|T100|U100] [--parent N] [--hidden] [--no-init]
63
- │ └── delete --id N [--yes] delete the tenant and its data, recycle app pool
63
+ │ ├── delete --id N [--yes] delete the tenant and its data, recycle app pool
64
+ │ └── recycle [--yes] restart site app pool (tenant map + free API slots)
64
65
  │
65
66
  ├── bootstrap [--export PATH] publish AcuBootstrap (REST); --export = offline zip
66
67
  ├── apply [--dry-run] [FILES...] push YAML via REST (idempotent PUT upserts)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acumatica-cli"
3
- version = "0.18.1"
3
+ version = "0.19.0"
4
4
  description = "Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data"
5
5
  license = "PolyForm-Noncommercial-1.0.0"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acumatica-cli"
3
- version = "0.18.1"
3
+ version = "0.19.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"
@@ -7,6 +7,7 @@ from collections.abc import Callable
7
7
  from importlib.metadata import distribution
8
8
  from pathlib import Path
9
9
  from typing import Concatenate
10
+ from urllib.parse import urlparse
10
11
 
11
12
  import click
12
13
  import httpx
@@ -239,8 +240,10 @@ def tenant_group() -> None:
239
240
  def tenant_list(inst: Instance) -> None:
240
241
  """List tenants: CompanyID, sign-in name, internal CD, type."""
241
242
  tenants = TenantManager(inst).list()
243
+ # V9/B27: control-plane identity is the host, never scheme+path
244
+ host = urlparse(inst.base_url).hostname or inst.base_url
242
245
  output.table(
243
- f"Tenants on {inst.base_url}",
246
+ f"Tenants on {host}",
244
247
  ("ID", "Login", "CD", "Type"),
245
248
  (
246
249
  (str(t.company_id), t.login_name, t.company_cd, t.company_type)
@@ -306,6 +309,11 @@ def tenant_create(
306
309
  create is skipped and the init + digest-gated publish chain still runs,
307
310
  so re-running create is the republish route for existing tenants. --id
308
311
  must match the existing CompanyID, else hard error naming both.
312
+
313
+ After create (or on exists-skip), CompanyCD is set equal to --login.
314
+ ac.exe only writes LoginName into CompanyKey and auto-generates CD
315
+ (Company2, Company3, …); the align step matches what operators do on
316
+ the Companies screen so ``acu tenant list`` shows Login = CD.
309
317
  """
310
318
  mgr = TenantManager(inst)
311
319
  existing = next((t for t in mgr.list() if t.login_name == login_name), None)
@@ -326,6 +334,14 @@ def tenant_create(
326
334
  company_id, login_name, parent_id, not hidden, company_type or ""
327
335
  )
328
336
  output.data(raw.splitlines()[-1] if raw.strip() else "created")
337
+ # ac.exe only sets CompanyKey from LoginName; CompanyCD stays auto-generated
338
+ # (Company2/Company3/…). Align CD → login so list shows matching columns.
339
+ # Idempotent on re-run / exists-skip (docs/ac-exe.md).
340
+ with output.step(f"aligning CompanyCD to login ({login_name})"):
341
+ if mgr.set_company_cd(company_id, login_name):
342
+ output.data(f"CompanyCD set to {login_name}")
343
+ else:
344
+ output.data(f"CompanyCD already {login_name}")
329
345
  if no_init:
330
346
  output.warn("skipping init: tenant is invisible until an app-pool recycle")
331
347
  return
@@ -454,6 +470,27 @@ def tenant_delete(inst: Instance, company_id: int) -> None:
454
470
  mgr.recycle_app_pool()
455
471
 
456
472
 
473
+ @tenant_group.command("recycle")
474
+ @click.confirmation_option(
475
+ prompt=(
476
+ "Recycle the site app pool? This kills all sessions and frees API-user slots."
477
+ )
478
+ )
479
+ @pass_instance
480
+ def tenant_recycle(inst: Instance) -> None:
481
+ """Recycle the IIS app pool for the instance (site-wide; no tenant --id).
482
+
483
+ Control plane only (SSH): ``Restart-WebAppPool`` for the instance pool.
484
+ Reloads the tenant map (V5) and drops every in-flight session so concurrent
485
+ API-user license slots free up. Hosted / empty ACU_SSH hard-errors before
486
+ any remote (same as other tenant cmds).
487
+ """
488
+ mgr = TenantManager(inst)
489
+ with output.step("recycling app pool (tenant map + free API-user sessions)"):
490
+ mgr.recycle_app_pool()
491
+ output.success("app pool recycled")
492
+
493
+
457
494
  @cli.group("config")
458
495
  def config_group() -> None:
459
496
  """Configuration ops.
@@ -224,7 +224,7 @@ class AcumaticaClient:
224
224
  f"tenant guard: asked for tenant {self.instance.tenant!r} "
225
225
  f"but the session landed on {landed!r} - the instance "
226
226
  "tenant map is stale or the tenant does not exist; check "
227
- "acu tenant list and recycle the app pool"
227
+ "acu tenant list and run acu tenant recycle"
228
228
  )
229
229
 
230
230
  def relogin(self) -> None:
@@ -1,4 +1,5 @@
1
1
  .env
2
- schemas/
3
- inventory/
4
2
  findings/
3
+ inventory/
4
+ schemas/
5
+ state/
@@ -14,8 +14,10 @@ class Tenant(Model):
14
14
  """One row of the instance's Company table.
15
15
 
16
16
  login_name is dbo.Company.CompanyKey — the name on the sign-in page and
17
- the REST login's tenant value. company_cd is the internal auto-generated
18
- code (ac.exe ignores LoginName for it; see docs/ac-exe.md).
17
+ the REST login's tenant value. company_cd is dbo.Company.CompanyCD; ac.exe
18
+ auto-generates it (Company2, Company3, …) and ignores LoginName for it
19
+ (docs/ac-exe.md). ``acu tenant create`` aligns CompanyCD to the login
20
+ after CompanyConfig so list Login and CD match.
19
21
  """
20
22
 
21
23
  company_id: int
@@ -131,6 +133,9 @@ class TenantManager:
131
133
  flags preset the tenant's admin to the instance credentials with no
132
134
  must-change flag, so the tenant is REST-loginable right after the
133
135
  app-pool recycle — no first-login dance (verified, docs/ac-exe.md).
136
+
137
+ Does not set CompanyCD (ac.exe auto-generates it). Call
138
+ :meth:`set_company_cd` after create to align CD with the login.
134
139
  """
135
140
  company = (
136
141
  f"CompanyID={company_id};ParentID={parent_id};"
@@ -142,6 +147,47 @@ class TenantManager:
142
147
  )
143
148
  return self._company_config(company, extra=admin)
144
149
 
150
+ def set_company_cd(self, company_id: int, company_cd: str) -> bool:
151
+ """Set ``dbo.Company.CompanyCD`` for a tenant (sqlcmd over SSH).
152
+
153
+ ac.exe CompanyConfig only writes LoginName into CompanyKey; CompanyCD
154
+ stays auto-generated (``Company2``, ``Company3``, …). Aligning CD to
155
+ the login makes ``acu tenant list`` show matching Login/CD columns and
156
+ matches the Companies-screen rename operators do by hand.
157
+
158
+ Returns True when an UPDATE ran, False when CD already matches.
159
+ Raises if the target id is missing or ``company_cd`` is already used
160
+ by a different CompanyID.
161
+ """
162
+ tenants = self.list()
163
+ target = next((t for t in tenants if t.company_id == company_id), None)
164
+ if target is None:
165
+ raise RuntimeError(f"no tenant with CompanyID={company_id}")
166
+ if target.company_cd == company_cd:
167
+ return False
168
+ conflict = next(
169
+ (
170
+ t
171
+ for t in tenants
172
+ if t.company_cd == company_cd and t.company_id != company_id
173
+ ),
174
+ None,
175
+ )
176
+ if conflict is not None:
177
+ raise RuntimeError(
178
+ f"CompanyCD {company_cd!r} already used by "
179
+ f"CompanyID={conflict.company_id}"
180
+ )
181
+ # company_id is int; company_cd is quoted with doubled single-quotes
182
+ escaped = company_cd.replace("'", "''")
183
+ query = (
184
+ f"UPDATE {DB_NAME}.dbo.Company "
185
+ f"SET CompanyCD = N'{escaped}' "
186
+ f"WHERE CompanyID = {company_id}"
187
+ )
188
+ self._ssh(f'sqlcmd -S "(local)" -E -C -Q "{query}"')
189
+ return True
190
+
145
191
  def delete(self, company_id: int) -> str:
146
192
  """Delete the tenant and all its data.
147
193