acumatica-cli 0.8.2__tar.gz → 0.9.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.8.2 → acumatica_cli-0.9.0}/PKG-INFO +30 -10
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/README.md +29 -9
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/pyproject.toml +1 -1
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/bootstrap.py +73 -16
- acumatica_cli-0.9.0/src/acumatica_cli/bootstrap_project.xml +238 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/cli.py +86 -16
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/extract.py +35 -25
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/extract_manifest.yaml +4 -3
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/seed.py +43 -32
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/baseline/50-gl-preferences.yaml +4 -4
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/baseline/60-ledger-company.yaml +3 -3
- acumatica_cli-0.9.0/src/acumatica_cli/templates/bootstrap/company.yaml +15 -0
- acumatica_cli-0.9.0/src/acumatica_cli/templates/bootstrap/credit-terms.yaml +17 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/setup/10-financial-year.yaml +1 -1
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/setup/20-master-calendar.yaml +1 -1
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/setup/30-open-periods.yaml +1 -1
- acumatica_cli-0.8.2/src/acumatica_cli/bootstrap_project.xml +0 -997
- acumatica_cli-0.8.2/src/acumatica_cli/templates/bootstrap/company.yaml +0 -14
- acumatica_cli-0.8.2/src/acumatica_cli/templates/bootstrap/credit-terms.yaml +0 -16
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/client.py +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/config.py +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/output.py +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/run.py +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/env +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/src/acumatica_cli/templates/gitignore +0 -0
- {acumatica_cli-0.8.2 → acumatica_cli-0.9.0}/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.
|
|
3
|
+
Version: 0.9.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>
|
|
@@ -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 #
|
|
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.
|
|
127
|
-
-
|
|
128
|
-
- `
|
|
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
|
|
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
|
|
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 #
|
|
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.
|
|
110
|
-
-
|
|
111
|
-
- `
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
version = "0.9.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
|
readme = "README.md"
|
|
@@ -6,10 +6,12 @@ docs/rest-api.md). The CustomizationApi is the one door that works on a
|
|
|
6
6
|
virgin tenant, so bootstrap = publish a package whose CustomizationPlugin
|
|
7
7
|
(`bootstrap_plugin.cs`) enables features on publish — the contract API
|
|
8
8
|
cannot write CS100000 at all (T3 verdict) — and whose Bootstrap contract
|
|
9
|
-
endpoint exposes
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
endpoint exposes the seeding surface (serialization verified T12).
|
|
10
|
+
|
|
11
|
+
Contract ownership is hybrid (T69/V2): the data repo's
|
|
12
|
+
``bootstrap/project.xml`` is preferred when present (full company surface);
|
|
13
|
+
else the packaged minimal ``bootstrap_project.xml`` covers config-init +
|
|
14
|
+
the GL chain so PyPI-only and offline virgin paths still bootstrap.
|
|
13
15
|
|
|
14
16
|
Customization publishes are tenant-scoped, so the package must be published
|
|
15
17
|
per tenant; publish() is idempotent on content — the skip gate compares the
|
|
@@ -38,13 +40,8 @@ from .client import AcumaticaClient
|
|
|
38
40
|
# Alphanumeric only: CstDbStorage.ValidatePackageName rejects '-' and '_'
|
|
39
41
|
# (verified vs 26.101.0225 — "Invalid project name")
|
|
40
42
|
PACKAGE_NAME = "AcuBootstrap"
|
|
41
|
-
# Authored once, in the template's root description attribute — read it
|
|
42
|
-
# here rather than hand-syncing a second copy: the XML is digest input,
|
|
43
|
-
# so a divergent XML-only edit would fire a spurious republish (V4).
|
|
44
|
-
PACKAGE_DESCRIPTION = ET.fromstring(
|
|
45
|
-
(resources.files("acumatica_cli") / "bootstrap_project.xml").read_bytes()
|
|
46
|
-
).get("description", "")
|
|
47
43
|
PLUGIN_CLASS = "AcuBootstrapPlugin"
|
|
44
|
+
_ENDPOINT_NS = "{http://www.acumatica.com/entity/maintenance/5.31}"
|
|
48
45
|
|
|
49
46
|
# Code default when the data repo carries no bootstrap/features.yaml
|
|
50
47
|
# (SPEC I.data) — the minimum for company/branch + baseline seeding.
|
|
@@ -62,6 +59,43 @@ DEFAULT_FEATURES = (
|
|
|
62
59
|
FEATURES_SENTINEL = "/*ACU_FEATURES*/"
|
|
63
60
|
|
|
64
61
|
|
|
62
|
+
def packaged_contract_xml() -> bytes:
|
|
63
|
+
"""The CLI-shipped minimal Bootstrap contract (config-init surface)."""
|
|
64
|
+
return (resources.files("acumatica_cli") / "bootstrap_project.xml").read_bytes()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_contract_xml(root: Path | None = None) -> bytes:
|
|
68
|
+
"""Active contract bytes: data-repo override when present, else packaged.
|
|
69
|
+
|
|
70
|
+
``root`` is the data-repo discovery root (the dir holding ``.env``).
|
|
71
|
+
Absent root or absent ``bootstrap/project.xml`` falls back to the
|
|
72
|
+
packaged minimal contract (V2, same absence pattern as features.yaml).
|
|
73
|
+
"""
|
|
74
|
+
if root is not None:
|
|
75
|
+
path = root / "bootstrap" / "project.xml"
|
|
76
|
+
if path.is_file():
|
|
77
|
+
return path.read_bytes()
|
|
78
|
+
return packaged_contract_xml()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def parse_endpoint(xml: bytes) -> tuple[str, frozenset[str]]:
|
|
82
|
+
"""Endpoint ``Name/version`` + entity names from a contract project.xml."""
|
|
83
|
+
root = ET.fromstring(xml)
|
|
84
|
+
endpoint = root.find(f"EntityEndpoint/{_ENDPOINT_NS}Endpoint")
|
|
85
|
+
if endpoint is None:
|
|
86
|
+
raise RuntimeError("bootstrap contract: no EntityEndpoint/Endpoint item")
|
|
87
|
+
name = f"{endpoint.get('name')}/{endpoint.get('version')}"
|
|
88
|
+
entities = frozenset(
|
|
89
|
+
e.get("name", "") for e in endpoint.findall(f"{_ENDPOINT_NS}TopLevelEntity")
|
|
90
|
+
)
|
|
91
|
+
return name, entities
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Authored once on the packaged template's root description — kept for
|
|
95
|
+
# tests that pin a pre-digest stale description (V4 republish path).
|
|
96
|
+
PACKAGE_DESCRIPTION = ET.fromstring(packaged_contract_xml()).get("description", "")
|
|
97
|
+
|
|
98
|
+
|
|
65
99
|
def load_features(root: Path) -> list[str]:
|
|
66
100
|
"""The FeaturesSet property names from <root>/bootstrap/features.yaml.
|
|
67
101
|
|
|
@@ -85,7 +119,12 @@ def load_features(root: Path) -> list[str]:
|
|
|
85
119
|
return list(data)
|
|
86
120
|
|
|
87
121
|
|
|
88
|
-
def package_zip(
|
|
122
|
+
def package_zip(
|
|
123
|
+
features: Sequence[str] | None = None,
|
|
124
|
+
*,
|
|
125
|
+
root: Path | None = None,
|
|
126
|
+
contract: bytes | None = None,
|
|
127
|
+
) -> bytes:
|
|
89
128
|
"""Build the customization package: a zip holding project.xml.
|
|
90
129
|
|
|
91
130
|
The C# plugin travels as a <Graph> item whose Source ATTRIBUTE holds the
|
|
@@ -96,9 +135,21 @@ def package_zip(features: Sequence[str] | None = None) -> bytes:
|
|
|
96
135
|
``features`` (default: the built-in six) is spliced into the plugin's
|
|
97
136
|
``Enabled`` set at the ACU_FEATURES sentinel — the one point where the
|
|
98
137
|
data repo's feature list enters the package (V2).
|
|
138
|
+
|
|
139
|
+
Contract XML (V2/T69): ``contract`` when given; else
|
|
140
|
+
``bootstrap/project.xml`` under ``root`` when present; else the
|
|
141
|
+
packaged minimal template.
|
|
99
142
|
"""
|
|
100
143
|
pkg = resources.files("acumatica_cli")
|
|
101
|
-
|
|
144
|
+
if contract is None:
|
|
145
|
+
if root is None:
|
|
146
|
+
from .config import find_data_root
|
|
147
|
+
|
|
148
|
+
root = find_data_root()
|
|
149
|
+
xml = load_contract_xml(root)
|
|
150
|
+
else:
|
|
151
|
+
xml = contract
|
|
152
|
+
root_el = ET.fromstring(xml)
|
|
102
153
|
source = (pkg / "bootstrap_plugin.cs").read_text(encoding="utf-8")
|
|
103
154
|
if FEATURES_SENTINEL not in source:
|
|
104
155
|
raise RuntimeError(f"bootstrap_plugin.cs: {FEATURES_SENTINEL} sentinel missing")
|
|
@@ -106,13 +157,13 @@ def package_zip(features: Sequence[str] | None = None) -> bytes:
|
|
|
106
157
|
source = source.replace(
|
|
107
158
|
FEATURES_SENTINEL, ", ".join(f'"{name}"' for name in enabled)
|
|
108
159
|
)
|
|
109
|
-
graph = ET.SubElement(
|
|
160
|
+
graph = ET.SubElement(root_el, "Graph")
|
|
110
161
|
graph.set("ClassName", PLUGIN_CLASS)
|
|
111
162
|
graph.set("FileType", "NewFile")
|
|
112
163
|
graph.set("Source", source)
|
|
113
164
|
buf = io.BytesIO()
|
|
114
165
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
115
|
-
zf.writestr("project.xml", ET.tostring(
|
|
166
|
+
zf.writestr("project.xml", ET.tostring(root_el, encoding="utf-8"))
|
|
116
167
|
return buf.getvalue()
|
|
117
168
|
|
|
118
169
|
|
|
@@ -134,8 +185,12 @@ def package_description(zip_bytes: bytes) -> str:
|
|
|
134
185
|
offers (verified live vs 26.101.0225): the import's projectDescription
|
|
135
186
|
comes back only in the root description attribute of getProject's
|
|
136
187
|
re-serialized project.xml — getPublished rows hold names alone.
|
|
188
|
+
Root description is read from the package's own project.xml so a
|
|
189
|
+
data-repo contract carries its own prose (V2).
|
|
137
190
|
"""
|
|
138
|
-
|
|
191
|
+
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
|
192
|
+
desc = ET.fromstring(zf.read("project.xml")).get("description", "")
|
|
193
|
+
return f"{desc} [sha256:{content_digest(zip_bytes)}]"
|
|
139
194
|
|
|
140
195
|
|
|
141
196
|
def _published_description(client: AcumaticaClient) -> str | None:
|
|
@@ -169,6 +224,8 @@ def _log_tail(status: dict[str, object], limit: int = 5) -> str:
|
|
|
169
224
|
def publish(
|
|
170
225
|
client: AcumaticaClient,
|
|
171
226
|
features: Sequence[str] | None = None,
|
|
227
|
+
*,
|
|
228
|
+
root: Path | None = None,
|
|
172
229
|
timeout: float = 600.0,
|
|
173
230
|
poll: float = 5.0,
|
|
174
231
|
) -> str:
|
|
@@ -187,7 +244,7 @@ def publish(
|
|
|
187
244
|
site may briefly drop connections mid-publish. Raises RuntimeError on a
|
|
188
245
|
failed publish or on timeout.
|
|
189
246
|
"""
|
|
190
|
-
zip_bytes = package_zip(features)
|
|
247
|
+
zip_bytes = package_zip(features, root=root)
|
|
191
248
|
description = package_description(zip_bytes)
|
|
192
249
|
if PACKAGE_NAME in client.customization_published() and (
|
|
193
250
|
_published_description(client) == description
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
acu bootstrap customization package template - minimal config-init surface
|
|
4
|
+
(SPEC T69 hybrid A).
|
|
5
|
+
|
|
6
|
+
package_zip() (bootstrap.py) prefers data-repo bootstrap/project.xml when
|
|
7
|
+
present; otherwise this packaged template is the fallback so config init,
|
|
8
|
+
offline tests, and PyPI-only installs still bootstrap.
|
|
9
|
+
|
|
10
|
+
package_zip() injects a
|
|
11
|
+
<Graph ClassName="AcuBootstrapPlugin" FileType="NewFile" Source="..."/>
|
|
12
|
+
item carrying bootstrap_plugin.cs - the CustomizationPlugin that enables
|
|
13
|
+
features on publish (CS100000 is un-writable through the contract API;
|
|
14
|
+
T3 verdict). Item shape verified vs 26.101.0225: Customization.CstCodeFile
|
|
15
|
+
Tag=Graph, source rides in the Source ATTRIBUTE (XML-escaped), not CDATA.
|
|
16
|
+
|
|
17
|
+
Root element shape verified vs the training packages shipped on the live
|
|
18
|
+
box (HelpAndTraining/T*/PhoneRepairShop.zip, same 26.101.0225 build):
|
|
19
|
+
project.xml's root is <Customization level description product-version>;
|
|
20
|
+
the project NAME comes from the import call, not the XML.
|
|
21
|
+
|
|
22
|
+
The <EntityEndpoint> item (T12, verified vs 26.101.0225 by import
|
|
23
|
+
round-trip 2026-07-08): the <Endpoint> child is the XmlSerializer form of
|
|
24
|
+
PX.Api.ContractBased.Common.Model.Endpoint - namespace
|
|
25
|
+
http://www.acumatica.com/entity/maintenance/5.31, systemContractVersion 4.
|
|
26
|
+
|
|
27
|
+
Minimal entity payload (config-init + GL chain only; full company surface
|
|
28
|
+
lives in the data repo's bootstrap/project.xml per T69):
|
|
29
|
+
- Company CS101500 -> OrganizationMaint, PrimaryView BAccount
|
|
30
|
+
- CreditTerms CS206500 -> TermsMaint, PrimaryView TermsDef
|
|
31
|
+
- LedgerCompany GL201500 -> GeneralLedgerMaint (org-ledger link, B12)
|
|
32
|
+
- FinancialYearSettings GL101000 + GeneratePeriods (AutoFill)
|
|
33
|
+
- MasterCalendar GL201000 + GenerateCalendar (GenerateYears)
|
|
34
|
+
- CompanyCalendar / CompanyPeriod GL201100 (done_when probes)
|
|
35
|
+
- ManagePeriods GL503000 + ProcessAll (period activation, B13)
|
|
36
|
+
- GLPreferences GL102000 (YtdNetIncAccountID + RetEarnAccountID)
|
|
37
|
+
|
|
38
|
+
Contract field names = DAC property names verbatim (no rename layer).
|
|
39
|
+
Features (CS100000) stays OUT of the endpoint - the plugin owns features.
|
|
40
|
+
-->
|
|
41
|
+
<Customization level="" description="acu bootstrap: CustomizationPlugin enables features on publish; Bootstrap endpoint exposes company + credit terms + GL preferences + ledger-company link + financial year + calendars + period activation (config-init surface; full company contract lives in data-repo bootstrap/project.xml)" product-version="26.101">
|
|
42
|
+
<EntityEndpoint>
|
|
43
|
+
<Endpoint xmlns="http://www.acumatica.com/entity/maintenance/5.31" name="Bootstrap" version="1.9.0" systemContractVersion="4">
|
|
44
|
+
<TopLevelEntity name="Company" screen="CS101500">
|
|
45
|
+
<Fields>
|
|
46
|
+
<Field name="AcctCD" type="StringValue" />
|
|
47
|
+
<Field name="AcctName" type="StringValue" />
|
|
48
|
+
<Field name="OrganizationType" type="StringValue" />
|
|
49
|
+
<Field name="BaseCuryID" type="StringValue" />
|
|
50
|
+
<Field name="CountryID" type="StringValue" />
|
|
51
|
+
</Fields>
|
|
52
|
+
<Mappings>
|
|
53
|
+
<Mapping field="AcctCD">
|
|
54
|
+
<To object="BAccount" field="AcctCD" />
|
|
55
|
+
</Mapping>
|
|
56
|
+
<Mapping field="AcctName">
|
|
57
|
+
<To object="BAccount" field="AcctName" />
|
|
58
|
+
</Mapping>
|
|
59
|
+
<Mapping field="OrganizationType">
|
|
60
|
+
<To object="OrganizationView" field="OrganizationType" />
|
|
61
|
+
</Mapping>
|
|
62
|
+
<Mapping field="BaseCuryID">
|
|
63
|
+
<To object="OrganizationView" field="BaseCuryID" />
|
|
64
|
+
</Mapping>
|
|
65
|
+
<Mapping field="CountryID">
|
|
66
|
+
<To object="AddressDummy" field="CountryID" />
|
|
67
|
+
</Mapping>
|
|
68
|
+
</Mappings>
|
|
69
|
+
</TopLevelEntity>
|
|
70
|
+
<TopLevelEntity name="CreditTerms" screen="CS206500">
|
|
71
|
+
<Fields>
|
|
72
|
+
<Field name="TermsID" type="StringValue" />
|
|
73
|
+
<Field name="Descr" type="StringValue" />
|
|
74
|
+
<Field name="VisibleTo" type="StringValue" />
|
|
75
|
+
<Field name="DueType" type="StringValue" />
|
|
76
|
+
<Field name="DayDue00" type="ShortValue" />
|
|
77
|
+
<Field name="DiscType" type="StringValue" />
|
|
78
|
+
<Field name="DiscPercent" type="DecimalValue" />
|
|
79
|
+
</Fields>
|
|
80
|
+
<Mappings>
|
|
81
|
+
<Mapping field="TermsID">
|
|
82
|
+
<To object="TermsDef" field="TermsID" />
|
|
83
|
+
</Mapping>
|
|
84
|
+
<Mapping field="Descr">
|
|
85
|
+
<To object="TermsDef" field="Descr" />
|
|
86
|
+
</Mapping>
|
|
87
|
+
<Mapping field="VisibleTo">
|
|
88
|
+
<To object="TermsDef" field="VisibleTo" />
|
|
89
|
+
</Mapping>
|
|
90
|
+
<Mapping field="DueType">
|
|
91
|
+
<To object="TermsDef" field="DueType" />
|
|
92
|
+
</Mapping>
|
|
93
|
+
<Mapping field="DayDue00">
|
|
94
|
+
<To object="TermsDef" field="DayDue00" />
|
|
95
|
+
</Mapping>
|
|
96
|
+
<Mapping field="DiscType">
|
|
97
|
+
<To object="TermsDef" field="DiscType" />
|
|
98
|
+
</Mapping>
|
|
99
|
+
<Mapping field="DiscPercent">
|
|
100
|
+
<To object="TermsDef" field="DiscPercent" />
|
|
101
|
+
</Mapping>
|
|
102
|
+
</Mappings>
|
|
103
|
+
</TopLevelEntity>
|
|
104
|
+
<TopLevelEntity name="LedgerCompany" screen="GL201500">
|
|
105
|
+
<Fields>
|
|
106
|
+
<Field name="LedgerCD" type="StringValue" />
|
|
107
|
+
<Field name="OrganizationID" type="StringValue" />
|
|
108
|
+
</Fields>
|
|
109
|
+
<Mappings>
|
|
110
|
+
<Mapping field="LedgerCD">
|
|
111
|
+
<To object="LedgerRecords" field="LedgerCD" />
|
|
112
|
+
</Mapping>
|
|
113
|
+
<Mapping field="OrganizationID">
|
|
114
|
+
<To object="OrganizationLedgerLinkWithOrganizationSelect" field="OrganizationID" />
|
|
115
|
+
</Mapping>
|
|
116
|
+
</Mappings>
|
|
117
|
+
</TopLevelEntity>
|
|
118
|
+
<TopLevelEntity name="FinancialYearSettings" screen="GL101000">
|
|
119
|
+
<Fields>
|
|
120
|
+
<Field name="BegFinYear" type="DateTimeValue" />
|
|
121
|
+
<Field name="FinPeriods" type="ShortValue" />
|
|
122
|
+
<Field name="PeriodType" type="StringValue" />
|
|
123
|
+
</Fields>
|
|
124
|
+
<Mappings>
|
|
125
|
+
<Mapping field="BegFinYear">
|
|
126
|
+
<To object="FiscalYearSetup" field="BegFinYear" />
|
|
127
|
+
</Mapping>
|
|
128
|
+
<Mapping field="FinPeriods">
|
|
129
|
+
<To object="FiscalYearSetup" field="FinPeriods" />
|
|
130
|
+
</Mapping>
|
|
131
|
+
<Mapping field="PeriodType">
|
|
132
|
+
<To object="FiscalYearSetup" field="PeriodType" />
|
|
133
|
+
</Mapping>
|
|
134
|
+
</Mappings>
|
|
135
|
+
<Actions>
|
|
136
|
+
<Action name="GeneratePeriods" mappedTo="AutoFill" isDefault="false" />
|
|
137
|
+
</Actions>
|
|
138
|
+
</TopLevelEntity>
|
|
139
|
+
<TopLevelEntity name="MasterCalendar" screen="GL201000">
|
|
140
|
+
<Fields>
|
|
141
|
+
<Field name="FinancialYear" type="StringValue" />
|
|
142
|
+
</Fields>
|
|
143
|
+
<Mappings>
|
|
144
|
+
<Mapping field="FinancialYear">
|
|
145
|
+
<To object="FiscalYear" field="Year" />
|
|
146
|
+
</Mapping>
|
|
147
|
+
</Mappings>
|
|
148
|
+
<Actions>
|
|
149
|
+
<Action name="GenerateCalendar" mappedTo="GenerateYears" isDefault="false">
|
|
150
|
+
<Fields>
|
|
151
|
+
<Field name="FromYear" type="StringValue" />
|
|
152
|
+
<Field name="ToYear" type="StringValue" />
|
|
153
|
+
</Fields>
|
|
154
|
+
<Mappings>
|
|
155
|
+
<Mapping field="FromYear">
|
|
156
|
+
<To object="GenerateParams" field="FromYear" />
|
|
157
|
+
</Mapping>
|
|
158
|
+
<Mapping field="ToYear">
|
|
159
|
+
<To object="GenerateParams" field="ToYear" />
|
|
160
|
+
</Mapping>
|
|
161
|
+
</Mappings>
|
|
162
|
+
</Action>
|
|
163
|
+
</Actions>
|
|
164
|
+
</TopLevelEntity>
|
|
165
|
+
<TopLevelEntity name="CompanyCalendar" screen="GL201100">
|
|
166
|
+
<Fields>
|
|
167
|
+
<Field name="FinancialYear" type="StringValue" />
|
|
168
|
+
<Field name="OrganizationID" type="StringValue" />
|
|
169
|
+
</Fields>
|
|
170
|
+
<Mappings>
|
|
171
|
+
<Mapping field="FinancialYear">
|
|
172
|
+
<To object="OrgFinYear" field="Year" />
|
|
173
|
+
</Mapping>
|
|
174
|
+
<Mapping field="OrganizationID">
|
|
175
|
+
<To object="OrgFinYear" field="OrganizationID" />
|
|
176
|
+
</Mapping>
|
|
177
|
+
</Mappings>
|
|
178
|
+
</TopLevelEntity>
|
|
179
|
+
<TopLevelEntity name="CompanyPeriod" screen="GL201100">
|
|
180
|
+
<Fields>
|
|
181
|
+
<Field name="FinancialYear" type="StringValue" />
|
|
182
|
+
<Field name="FinPeriodID" type="StringValue" />
|
|
183
|
+
<Field name="Status" type="StringValue" />
|
|
184
|
+
</Fields>
|
|
185
|
+
<Mappings>
|
|
186
|
+
<Mapping field="FinancialYear">
|
|
187
|
+
<To object="OrgFinPeriods" field="FinYear" />
|
|
188
|
+
</Mapping>
|
|
189
|
+
<Mapping field="FinPeriodID">
|
|
190
|
+
<To object="OrgFinPeriods" field="FinPeriodID" />
|
|
191
|
+
</Mapping>
|
|
192
|
+
<Mapping field="Status">
|
|
193
|
+
<To object="OrgFinPeriods" field="Status" />
|
|
194
|
+
</Mapping>
|
|
195
|
+
</Mappings>
|
|
196
|
+
</TopLevelEntity>
|
|
197
|
+
<TopLevelEntity name="ManagePeriods" screen="GL503000">
|
|
198
|
+
<Fields>
|
|
199
|
+
<Field name="Action" type="StringValue" />
|
|
200
|
+
<Field name="FromYear" type="StringValue" />
|
|
201
|
+
<Field name="ToYear" type="StringValue" />
|
|
202
|
+
<Field name="OrganizationID" type="StringValue" />
|
|
203
|
+
</Fields>
|
|
204
|
+
<Mappings>
|
|
205
|
+
<Mapping field="Action">
|
|
206
|
+
<To object="Filter" field="Action" />
|
|
207
|
+
</Mapping>
|
|
208
|
+
<Mapping field="FromYear">
|
|
209
|
+
<To object="Filter" field="FromYear" />
|
|
210
|
+
</Mapping>
|
|
211
|
+
<Mapping field="ToYear">
|
|
212
|
+
<To object="Filter" field="ToYear" />
|
|
213
|
+
</Mapping>
|
|
214
|
+
<Mapping field="OrganizationID">
|
|
215
|
+
<To object="Filter" field="OrganizationID" />
|
|
216
|
+
</Mapping>
|
|
217
|
+
</Mappings>
|
|
218
|
+
<Actions>
|
|
219
|
+
<Action name="ProcessAll" mappedTo="ProcessAll" isDefault="false" />
|
|
220
|
+
</Actions>
|
|
221
|
+
</TopLevelEntity>
|
|
222
|
+
<TopLevelEntity name="GLPreferences" screen="GL102000">
|
|
223
|
+
<Fields>
|
|
224
|
+
<Field name="YtdNetIncAccountID" type="StringValue" />
|
|
225
|
+
<Field name="RetEarnAccountID" type="StringValue" />
|
|
226
|
+
</Fields>
|
|
227
|
+
<Mappings>
|
|
228
|
+
<Mapping field="YtdNetIncAccountID">
|
|
229
|
+
<To object="GLSetupRecord" field="YtdNetIncAccountID" />
|
|
230
|
+
</Mapping>
|
|
231
|
+
<Mapping field="RetEarnAccountID">
|
|
232
|
+
<To object="GLSetupRecord" field="RetEarnAccountID" />
|
|
233
|
+
</Mapping>
|
|
234
|
+
</Mappings>
|
|
235
|
+
</TopLevelEntity>
|
|
236
|
+
</Endpoint>
|
|
237
|
+
</EntityEndpoint>
|
|
238
|
+
</Customization>
|