acumatica-cli 0.5.0__tar.gz → 0.7.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.5.0 → acumatica_cli-0.7.0}/PKG-INFO +1 -1
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/pyproject.toml +1 -1
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/bootstrap_project.xml +111 -2
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/cli.py +37 -1
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/client.py +29 -9
- acumatica_cli-0.7.0/src/acumatica_cli/run.py +369 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/seed.py +48 -9
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/baseline/50-gl-preferences.yaml +2 -2
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/baseline/60-ledger-company.yaml +2 -2
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/bootstrap/company.yaml +2 -2
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/bootstrap/credit-terms.yaml +2 -2
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/setup/10-financial-year.yaml +1 -1
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/setup/20-master-calendar.yaml +1 -1
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/setup/30-open-periods.yaml +1 -1
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/README.md +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/bootstrap.py +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/config.py +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/extract.py +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/extract_manifest.yaml +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/output.py +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/env +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/gitignore +0 -0
- {acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/tenant.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "acumatica-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.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"
|
|
@@ -192,6 +192,23 @@
|
|
|
192
192
|
(live-read 2026-07-13). TransitAcctId (DAC spelling off
|
|
193
193
|
the aspx - lowercase d) + TransitSubID = the cash-in-
|
|
194
194
|
transit pair; HoldEntry = the echo flag.
|
|
195
|
+
- VendorClass AP201000 -> VendorClassMaint, PrimaryView
|
|
196
|
+
VendorClassRecord (PX.Objects.AP.VendorClass) - the
|
|
197
|
+
Default endpoint's VendorClass is skeletal (ClassID/
|
|
198
|
+
Description/Attributes) and Vendor carries no discount
|
|
199
|
+
fields, so vendor inserts 422 on the cash-discount pair
|
|
200
|
+
the class should default (T64, gh issue #13). Exposes
|
|
201
|
+
id + descr + TermsID + the GL-account pairs off the
|
|
202
|
+
screen's GL Accounts tab (aspx-verified 2026-07-13).
|
|
203
|
+
- StatementCycle AR202800 -> ARStatementMaint, PrimaryView
|
|
204
|
+
ARStatementCycleRecord (PX.Objects.AR.ARStatementCycle) -
|
|
205
|
+
no Default-endpoint entity, and Customer inserts 422
|
|
206
|
+
"'Statement Cycle ID' cannot be empty" (T64 follow-up
|
|
207
|
+
proven live 2026-07-13; the class defaults it). PrepareOn
|
|
208
|
+
takes the stored-word list; Day00 rides the
|
|
209
|
+
fixed-day-of-month choice; the AgeDays00-02 aging bounds
|
|
210
|
+
default inconsistently at raw insert ("end day earlier
|
|
211
|
+
than its start day" 422) so the seed supplies them.
|
|
195
212
|
- CashAccount CA202000 -> CashAccountMaint, PrimaryView CashAccount
|
|
196
213
|
(PX.Objects.CA.CashAccount) - Default endpoint answers
|
|
197
214
|
404. CuryID stays out (derived from the GL account,
|
|
@@ -201,9 +218,9 @@
|
|
|
201
218
|
Features (CS100000) stays OUT of the endpoint - writes through any
|
|
202
219
|
contract endpoint do not persist (T3 verdict); the plugin owns features.
|
|
203
220
|
-->
|
|
204
|
-
<Customization level="" description="acu bootstrap: CustomizationPlugin enables features on publish; Bootstrap endpoint exposes company + credit terms + financial currency + GL preferences + ledger-company link + financial year + calendars + period activation + module preferences (IN/AP/AR/SO/PO) + availability rule + posting class + cash account" product-version="26.101">
|
|
221
|
+
<Customization level="" description="acu bootstrap: CustomizationPlugin enables features on publish; Bootstrap endpoint exposes company + credit terms + financial currency + GL preferences + ledger-company link + financial year + calendars + period activation + module preferences (IN/AP/AR/SO/PO) + availability rule + posting class + cash account + vendor class + statement cycle" product-version="26.101">
|
|
205
222
|
<EntityEndpoint>
|
|
206
|
-
<Endpoint xmlns="http://www.acumatica.com/entity/maintenance/5.31" name="Bootstrap" version="1.
|
|
223
|
+
<Endpoint xmlns="http://www.acumatica.com/entity/maintenance/5.31" name="Bootstrap" version="1.6.0" systemContractVersion="4">
|
|
207
224
|
<TopLevelEntity name="Company" screen="CS101500">
|
|
208
225
|
<Fields>
|
|
209
226
|
<Field name="AcctCD" type="StringValue" />
|
|
@@ -752,6 +769,98 @@
|
|
|
752
769
|
</Mapping>
|
|
753
770
|
</Mappings>
|
|
754
771
|
</TopLevelEntity>
|
|
772
|
+
<TopLevelEntity name="VendorClass" screen="AP201000">
|
|
773
|
+
<Fields>
|
|
774
|
+
<Field name="VendorClassID" type="StringValue" />
|
|
775
|
+
<Field name="Descr" type="StringValue" />
|
|
776
|
+
<Field name="TermsID" type="StringValue" />
|
|
777
|
+
<Field name="APAcctID" type="StringValue" />
|
|
778
|
+
<Field name="APSubID" type="StringValue" />
|
|
779
|
+
<Field name="ExpenseAcctID" type="StringValue" />
|
|
780
|
+
<Field name="ExpenseSubID" type="StringValue" />
|
|
781
|
+
<Field name="DiscTakenAcctID" type="StringValue" />
|
|
782
|
+
<Field name="DiscTakenSubID" type="StringValue" />
|
|
783
|
+
<Field name="POAccrualAcctID" type="StringValue" />
|
|
784
|
+
<Field name="POAccrualSubID" type="StringValue" />
|
|
785
|
+
<Field name="PrepaymentAcctID" type="StringValue" />
|
|
786
|
+
<Field name="PrepaymentSubID" type="StringValue" />
|
|
787
|
+
</Fields>
|
|
788
|
+
<Mappings>
|
|
789
|
+
<Mapping field="VendorClassID">
|
|
790
|
+
<To object="VendorClassRecord" field="VendorClassID" />
|
|
791
|
+
</Mapping>
|
|
792
|
+
<Mapping field="Descr">
|
|
793
|
+
<To object="VendorClassRecord" field="Descr" />
|
|
794
|
+
</Mapping>
|
|
795
|
+
<Mapping field="TermsID">
|
|
796
|
+
<To object="VendorClassRecord" field="TermsID" />
|
|
797
|
+
</Mapping>
|
|
798
|
+
<Mapping field="APAcctID">
|
|
799
|
+
<To object="VendorClassRecord" field="APAcctID" />
|
|
800
|
+
</Mapping>
|
|
801
|
+
<Mapping field="APSubID">
|
|
802
|
+
<To object="VendorClassRecord" field="APSubID" />
|
|
803
|
+
</Mapping>
|
|
804
|
+
<Mapping field="ExpenseAcctID">
|
|
805
|
+
<To object="VendorClassRecord" field="ExpenseAcctID" />
|
|
806
|
+
</Mapping>
|
|
807
|
+
<Mapping field="ExpenseSubID">
|
|
808
|
+
<To object="VendorClassRecord" field="ExpenseSubID" />
|
|
809
|
+
</Mapping>
|
|
810
|
+
<Mapping field="DiscTakenAcctID">
|
|
811
|
+
<To object="VendorClassRecord" field="DiscTakenAcctID" />
|
|
812
|
+
</Mapping>
|
|
813
|
+
<Mapping field="DiscTakenSubID">
|
|
814
|
+
<To object="VendorClassRecord" field="DiscTakenSubID" />
|
|
815
|
+
</Mapping>
|
|
816
|
+
<Mapping field="POAccrualAcctID">
|
|
817
|
+
<To object="VendorClassRecord" field="POAccrualAcctID" />
|
|
818
|
+
</Mapping>
|
|
819
|
+
<Mapping field="POAccrualSubID">
|
|
820
|
+
<To object="VendorClassRecord" field="POAccrualSubID" />
|
|
821
|
+
</Mapping>
|
|
822
|
+
<Mapping field="PrepaymentAcctID">
|
|
823
|
+
<To object="VendorClassRecord" field="PrepaymentAcctID" />
|
|
824
|
+
</Mapping>
|
|
825
|
+
<Mapping field="PrepaymentSubID">
|
|
826
|
+
<To object="VendorClassRecord" field="PrepaymentSubID" />
|
|
827
|
+
</Mapping>
|
|
828
|
+
</Mappings>
|
|
829
|
+
</TopLevelEntity>
|
|
830
|
+
<TopLevelEntity name="StatementCycle" screen="AR202800">
|
|
831
|
+
<Fields>
|
|
832
|
+
<Field name="StatementCycleId" type="StringValue" />
|
|
833
|
+
<Field name="Descr" type="StringValue" />
|
|
834
|
+
<Field name="PrepareOn" type="StringValue" />
|
|
835
|
+
<Field name="Day00" type="ShortValue" />
|
|
836
|
+
<Field name="AgeDays00" type="ShortValue" />
|
|
837
|
+
<Field name="AgeDays01" type="ShortValue" />
|
|
838
|
+
<Field name="AgeDays02" type="ShortValue" />
|
|
839
|
+
</Fields>
|
|
840
|
+
<Mappings>
|
|
841
|
+
<Mapping field="StatementCycleId">
|
|
842
|
+
<To object="ARStatementCycleRecord" field="StatementCycleId" />
|
|
843
|
+
</Mapping>
|
|
844
|
+
<Mapping field="Descr">
|
|
845
|
+
<To object="ARStatementCycleRecord" field="Descr" />
|
|
846
|
+
</Mapping>
|
|
847
|
+
<Mapping field="PrepareOn">
|
|
848
|
+
<To object="ARStatementCycleRecord" field="PrepareOn" />
|
|
849
|
+
</Mapping>
|
|
850
|
+
<Mapping field="Day00">
|
|
851
|
+
<To object="ARStatementCycleRecord" field="Day00" />
|
|
852
|
+
</Mapping>
|
|
853
|
+
<Mapping field="AgeDays00">
|
|
854
|
+
<To object="ARStatementCycleRecord" field="AgeDays00" />
|
|
855
|
+
</Mapping>
|
|
856
|
+
<Mapping field="AgeDays01">
|
|
857
|
+
<To object="ARStatementCycleRecord" field="AgeDays01" />
|
|
858
|
+
</Mapping>
|
|
859
|
+
<Mapping field="AgeDays02">
|
|
860
|
+
<To object="ARStatementCycleRecord" field="AgeDays02" />
|
|
861
|
+
</Mapping>
|
|
862
|
+
</Mappings>
|
|
863
|
+
</TopLevelEntity>
|
|
755
864
|
<TopLevelEntity name="CashAccount" screen="CA202000">
|
|
756
865
|
<Fields>
|
|
757
866
|
<Field name="CashAccountCD" type="StringValue" />
|
|
@@ -12,7 +12,7 @@ import click
|
|
|
12
12
|
import httpx
|
|
13
13
|
from click.shell_completion import get_completion_class
|
|
14
14
|
|
|
15
|
-
from . import bootstrap, extract, firstlogin, output, seed
|
|
15
|
+
from . import bootstrap, extract, firstlogin, output, run, seed
|
|
16
16
|
from .client import AcumaticaClient
|
|
17
17
|
from .config import (
|
|
18
18
|
Instance,
|
|
@@ -557,6 +557,42 @@ def diff_cmd(inst: Instance, files: tuple[Path, ...]) -> None:
|
|
|
557
557
|
_exit_on_drift(inst, drifts, len(paths))
|
|
558
558
|
|
|
559
559
|
|
|
560
|
+
@cli.command("run")
|
|
561
|
+
@click.argument(
|
|
562
|
+
"files", nargs=-1, required=False, type=click.Path(exists=True, path_type=Path)
|
|
563
|
+
)
|
|
564
|
+
@click.option("--dry-run", is_flag=True, help="Parse and list steps without any HTTP")
|
|
565
|
+
@pass_instance
|
|
566
|
+
def run_cmd(inst: Instance, files: tuple[Path, ...], dry_run: bool) -> None:
|
|
567
|
+
"""Execute transaction scenario YAML against the live tenant.
|
|
568
|
+
|
|
569
|
+
FILES are scenario YAML files or directories containing them. Omitted,
|
|
570
|
+
they default to the data repo's scenario/ directory. Transactions are
|
|
571
|
+
executed forward (the server assigns document numbers), never upserted;
|
|
572
|
+
delta expectations snapshot before the first step and compare after the
|
|
573
|
+
last, so a scenario re-runs safely on a warm tenant. Exit 0 when every
|
|
574
|
+
expectation holds, 1 on any step error or expectation miss.
|
|
575
|
+
"""
|
|
576
|
+
if not files:
|
|
577
|
+
default = data_root() / "scenario"
|
|
578
|
+
if not default.is_dir():
|
|
579
|
+
raise SystemExit(f"{default}: scenario directory does not exist")
|
|
580
|
+
files = (Path(os.path.relpath(default)),)
|
|
581
|
+
paths = expand_files(files)
|
|
582
|
+
scenarios = [run.load_scenario(path) for path in paths]
|
|
583
|
+
ok = True
|
|
584
|
+
if dry_run:
|
|
585
|
+
for scenario in scenarios:
|
|
586
|
+
run.run(None, scenario, dry_run=True)
|
|
587
|
+
else:
|
|
588
|
+
with AcumaticaClient(inst) as client:
|
|
589
|
+
for scenario in scenarios:
|
|
590
|
+
ok = run.run(client, scenario) and ok
|
|
591
|
+
if not ok:
|
|
592
|
+
raise SystemExit(1)
|
|
593
|
+
output.success(f"{len(scenarios)} scenario(s) passed on {inst.tenant}")
|
|
594
|
+
|
|
595
|
+
|
|
560
596
|
def _complete_only(
|
|
561
597
|
_ctx: click.Context, _param: click.Parameter, incomplete: str
|
|
562
598
|
) -> list[str]:
|
|
@@ -18,7 +18,9 @@ def wrap(record: dict[str, Any]) -> dict[str, Any]:
|
|
|
18
18
|
|
|
19
19
|
A list value is a detail array (T60): the list itself is NEVER
|
|
20
20
|
value-wrapped, each dict row wraps recursively — the shape the live
|
|
21
|
-
JournalTransaction PUT proved (T50/T37 e2e payload).
|
|
21
|
+
JournalTransaction PUT proved (T50/T37 e2e payload). A dict value is
|
|
22
|
+
a linked entity (T65): a bare nested object, wrapped recursively —
|
|
23
|
+
live-verified Vendor MainContact/Address/Country PUT. The contract's
|
|
22
24
|
OWN control fields travel bare, never value-wrapped: `id` (row GUID —
|
|
23
25
|
the only handle that makes a detail-row PUT an update instead of an
|
|
24
26
|
insert) and `delete` (row-removal marker).
|
|
@@ -27,6 +29,8 @@ def wrap(record: dict[str, Any]) -> dict[str, Any]:
|
|
|
27
29
|
def _value(v: Any) -> Any:
|
|
28
30
|
if isinstance(v, list):
|
|
29
31
|
return [wrap(row) if isinstance(row, dict) else row for row in v]
|
|
32
|
+
if isinstance(v, dict):
|
|
33
|
+
return wrap(v)
|
|
30
34
|
return {"value": v}
|
|
31
35
|
|
|
32
36
|
return {k: v if k in ("id", "delete") else _value(v) for k, v in record.items()}
|
|
@@ -35,16 +39,22 @@ def wrap(record: dict[str, Any]) -> dict[str, Any]:
|
|
|
35
39
|
def unwrap(entity: dict[str, Any]) -> dict[str, Any]:
|
|
36
40
|
"""Contract-API entity -> plain dict (value fields + detail arrays).
|
|
37
41
|
|
|
38
|
-
Inverse of wrap (T60): a list of dicts unwraps row by row; lists
|
|
39
|
-
are empty or whose rows carry no value fields are elided (a
|
|
40
|
-
array the source never claimed must not surface, and expanded
|
|
41
|
-
descriptors are plain dicts that would unwrap to noise).
|
|
42
|
-
|
|
42
|
+
Inverse of wrap (T60/T65): a list of dicts unwraps row by row; lists
|
|
43
|
+
that are empty or whose rows carry no value fields are elided (a
|
|
44
|
+
detail array the source never claimed must not surface, and expanded
|
|
45
|
+
`files` descriptors are plain dicts that would unwrap to noise). A
|
|
46
|
+
non-value dict is a linked entity — unwrapped recursively, kept only
|
|
47
|
+
when something survives, so bookkeeping dicts (custom, _links, note)
|
|
48
|
+
stay excluded exactly as before.
|
|
43
49
|
"""
|
|
44
50
|
out: dict[str, Any] = {}
|
|
45
51
|
for k, v in entity.items():
|
|
46
52
|
if isinstance(v, dict) and "value" in v:
|
|
47
53
|
out[k] = v["value"]
|
|
54
|
+
elif isinstance(v, dict):
|
|
55
|
+
nested = unwrap(v)
|
|
56
|
+
if nested:
|
|
57
|
+
out[k] = nested
|
|
48
58
|
elif isinstance(v, list) and v and all(isinstance(row, dict) for row in v):
|
|
49
59
|
rows = [unwrap(row) for row in v]
|
|
50
60
|
if any(rows):
|
|
@@ -220,11 +230,21 @@ class AcumaticaClient:
|
|
|
220
230
|
return self._checked(self._http.get(self._url("swagger.json"))).content
|
|
221
231
|
|
|
222
232
|
def put(
|
|
223
|
-
self,
|
|
233
|
+
self,
|
|
234
|
+
entity: str,
|
|
235
|
+
record: dict[str, Any],
|
|
236
|
+
endpoint: str | None = None,
|
|
237
|
+
params: dict[str, str] | None = None,
|
|
224
238
|
) -> dict[str, Any]:
|
|
225
|
-
"""Upsert by the entity's key fields — the idempotence primitive.
|
|
239
|
+
"""Upsert by the entity's key fields — the idempotence primitive.
|
|
240
|
+
|
|
241
|
+
`params` carries OData query options — the contract-inquiry idiom
|
|
242
|
+
(T62) is a PUT of the parameter record with `$expand=Results`.
|
|
243
|
+
"""
|
|
226
244
|
return self._checked(
|
|
227
|
-
self._http.put(
|
|
245
|
+
self._http.put(
|
|
246
|
+
self._url(entity, endpoint), json=wrap(record), params=params
|
|
247
|
+
)
|
|
228
248
|
).json()
|
|
229
249
|
|
|
230
250
|
# seconds between polls of a long-running action's status URL; module
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Transaction scenarios: scenario/*.yaml executed against the live tenant.
|
|
2
|
+
|
|
3
|
+
`acu run` is the third data-plane verb (SPEC I.cmd): apply/diff own
|
|
4
|
+
configuration (keyed idempotent upserts), run owns transactions - the
|
|
5
|
+
server assigns document numbers, so a scenario is executed forward,
|
|
6
|
+
never upserted. The scenario file is the contract for the future AI
|
|
7
|
+
scenario generator: declarative steps, checkable expectations.
|
|
8
|
+
|
|
9
|
+
Scenario file format (SPEC I.data):
|
|
10
|
+
|
|
11
|
+
scenario: buy-build-sell
|
|
12
|
+
description: Buy parts -> assemble kits -> sell -> collect
|
|
13
|
+
steps:
|
|
14
|
+
- id: po-shenzhen
|
|
15
|
+
put: PurchaseOrder
|
|
16
|
+
record:
|
|
17
|
+
VendorID: SHENZHEN
|
|
18
|
+
Details:
|
|
19
|
+
- { InventoryID: MB-CM4, OrderQty: 25 }
|
|
20
|
+
capture: { OrderNbr: po_shenzhen } # server-assigned -> ${var}
|
|
21
|
+
- id: release
|
|
22
|
+
action: { entity: PurchaseReceipt, name: ReleasePurchaseReceipt }
|
|
23
|
+
record: { ReceiptNbr: "${rcpt}" }
|
|
24
|
+
wait: # poll until field match
|
|
25
|
+
entity: PurchaseReceipt
|
|
26
|
+
keys: ["${rcpt}"]
|
|
27
|
+
until: { Status: Released }
|
|
28
|
+
expect:
|
|
29
|
+
- get: { entity: Payment, keys: [Payment, "${pmt}"] }
|
|
30
|
+
fields: { Status: Closed }
|
|
31
|
+
- inquire: AccountSummaryInquiry # delta = post - pre
|
|
32
|
+
parameters: { Ledger: ACTUAL, Period: "062026" }
|
|
33
|
+
match: { Account: "40000" }
|
|
34
|
+
delta: { EndingBalance: 4138.00 }
|
|
35
|
+
|
|
36
|
+
Steps run in order; `capture` lifts server-assigned fields into ${var}
|
|
37
|
+
tokens for later steps. `expect` delta assertions snapshot before the
|
|
38
|
+
first step and re-probe after the last, comparing the difference - the
|
|
39
|
+
scenario re-runs safely on a warm tenant (document numbers differ, the
|
|
40
|
+
deltas hold). `get` assertions are absolute (statuses of documents
|
|
41
|
+
created in this run). Exit 0 = every expectation holds; 1 = any step
|
|
42
|
+
error or expectation miss (2 stays diff's drift code).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import re
|
|
46
|
+
import time
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
import yaml
|
|
51
|
+
from pydantic import Field, ValidationError, model_validator
|
|
52
|
+
|
|
53
|
+
from . import output
|
|
54
|
+
from .client import AcumaticaClient, unwrap
|
|
55
|
+
from .models import Model, validation_summary
|
|
56
|
+
from .seed import _norm # pyright: ignore[reportPrivateUsage]
|
|
57
|
+
|
|
58
|
+
_VAR = re.compile(r"\$\{([A-Za-z0-9_]+)\}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ActionSpec(Model):
|
|
62
|
+
"""The contract action a step invokes: entity + action name."""
|
|
63
|
+
|
|
64
|
+
entity: str
|
|
65
|
+
name: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class WaitSpec(Model):
|
|
69
|
+
"""Poll a record by key URL until every `until` field matches."""
|
|
70
|
+
|
|
71
|
+
entity: str
|
|
72
|
+
keys: list[Any]
|
|
73
|
+
until: dict[str, Any]
|
|
74
|
+
timeout: float = 120.0
|
|
75
|
+
endpoint: str | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Step(Model):
|
|
79
|
+
"""One scenario step: exactly one op (put | action | wait-only)."""
|
|
80
|
+
|
|
81
|
+
id: str
|
|
82
|
+
put: str | None = None
|
|
83
|
+
action: ActionSpec | None = None
|
|
84
|
+
record: dict[str, Any] | None = None
|
|
85
|
+
parameters: dict[str, Any] | None = None
|
|
86
|
+
capture: dict[str, str] | None = None # {Field: var_name}
|
|
87
|
+
wait: WaitSpec | None = None
|
|
88
|
+
endpoint: str | None = None
|
|
89
|
+
|
|
90
|
+
@model_validator(mode="after")
|
|
91
|
+
def _one_op(self) -> "Step":
|
|
92
|
+
ops = [op for op in (self.put, self.action) if op is not None]
|
|
93
|
+
if len(ops) > 1:
|
|
94
|
+
raise ValueError(f"step '{self.id}': put and action are exclusive")
|
|
95
|
+
if not ops and self.wait is None:
|
|
96
|
+
raise ValueError(f"step '{self.id}': needs one of put, action, wait")
|
|
97
|
+
if self.put is not None and self.record is None:
|
|
98
|
+
raise ValueError(f"step '{self.id}': put needs a record")
|
|
99
|
+
if self.action is not None and self.record is None:
|
|
100
|
+
raise ValueError(f"step '{self.id}': action needs a record")
|
|
101
|
+
if self.capture is not None and self.put is None:
|
|
102
|
+
raise ValueError(f"step '{self.id}': capture rides a put step")
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class GetSpec(Model):
|
|
107
|
+
"""Key-URL record address for a get expectation."""
|
|
108
|
+
|
|
109
|
+
entity: str
|
|
110
|
+
keys: list[Any]
|
|
111
|
+
endpoint: str | None = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Expect(Model):
|
|
115
|
+
"""One expectation: absolute `get` fields or `inquire` deltas."""
|
|
116
|
+
|
|
117
|
+
get: GetSpec | None = None
|
|
118
|
+
fields: dict[str, Any] | None = None
|
|
119
|
+
inquire: str | None = None
|
|
120
|
+
parameters: dict[str, Any] | None = None
|
|
121
|
+
match: dict[str, Any] | None = None
|
|
122
|
+
delta: dict[str, float] | None = None
|
|
123
|
+
endpoint: str | None = None
|
|
124
|
+
|
|
125
|
+
@model_validator(mode="after")
|
|
126
|
+
def _one_kind(self) -> "Expect":
|
|
127
|
+
if (self.get is None) == (self.inquire is None):
|
|
128
|
+
raise ValueError("expect: exactly one of get, inquire")
|
|
129
|
+
if self.get is not None and self.fields is None:
|
|
130
|
+
raise ValueError("expect: get needs fields")
|
|
131
|
+
if self.inquire is not None and self.delta is None:
|
|
132
|
+
raise ValueError("expect: inquire needs delta")
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
def label(self) -> str:
|
|
136
|
+
"""Human-readable expectation address for report lines."""
|
|
137
|
+
if self.get is not None:
|
|
138
|
+
keys = ", ".join(str(k) for k in self.get.keys)
|
|
139
|
+
return f"{self.get.entity} [{keys}]"
|
|
140
|
+
match = ""
|
|
141
|
+
if self.match:
|
|
142
|
+
match = f" [{', '.join(f'{k}={v}' for k, v in self.match.items())}]"
|
|
143
|
+
return f"{self.inquire}{match}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Scenario(Model):
|
|
147
|
+
"""A parsed scenario YAML: named steps plus expectations."""
|
|
148
|
+
|
|
149
|
+
path: Path
|
|
150
|
+
scenario: str
|
|
151
|
+
description: str | None = None
|
|
152
|
+
steps: list[Step]
|
|
153
|
+
expect: list[Expect] = Field(default_factory=list)
|
|
154
|
+
|
|
155
|
+
@model_validator(mode="after")
|
|
156
|
+
def _unique_step_ids(self) -> "Scenario":
|
|
157
|
+
seen: set[str] = set()
|
|
158
|
+
for step in self.steps:
|
|
159
|
+
if step.id in seen:
|
|
160
|
+
raise ValueError(f"duplicate step id '{step.id}'")
|
|
161
|
+
seen.add(step.id)
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def load_scenario(path: Path) -> Scenario:
|
|
166
|
+
"""Parse and validate one scenario YAML file."""
|
|
167
|
+
with open(path) as f:
|
|
168
|
+
data = yaml.safe_load(f)
|
|
169
|
+
if not isinstance(data, dict):
|
|
170
|
+
raise SystemExit(f"{path}: expected a mapping at the top level")
|
|
171
|
+
try:
|
|
172
|
+
return Scenario.model_validate({"path": path, **data})
|
|
173
|
+
except ValidationError as exc:
|
|
174
|
+
raise SystemExit(f"{path}: {validation_summary(exc)}") from exc
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _subst(value: Any, variables: dict[str, Any]) -> Any:
|
|
178
|
+
"""Interpolate ${var} tokens; unknown names are a hard error."""
|
|
179
|
+
|
|
180
|
+
def _one(text: str) -> Any:
|
|
181
|
+
whole = _VAR.fullmatch(text)
|
|
182
|
+
if whole: # whole-value token keeps the captured value's type
|
|
183
|
+
return _lookup(whole.group(1))
|
|
184
|
+
return _VAR.sub(lambda m: str(_lookup(m.group(1))), text)
|
|
185
|
+
|
|
186
|
+
def _lookup(name: str) -> Any:
|
|
187
|
+
if name not in variables:
|
|
188
|
+
raise SystemExit(
|
|
189
|
+
f"unknown scenario variable '${{{name}}}' - captured so far: "
|
|
190
|
+
f"{', '.join(sorted(variables)) or '(none)'}"
|
|
191
|
+
)
|
|
192
|
+
return variables[name]
|
|
193
|
+
|
|
194
|
+
if isinstance(value, str):
|
|
195
|
+
return _one(value)
|
|
196
|
+
if isinstance(value, dict):
|
|
197
|
+
return {k: _subst(v, variables) for k, v in value.items()}
|
|
198
|
+
if isinstance(value, list):
|
|
199
|
+
return [_subst(v, variables) for v in value]
|
|
200
|
+
return value
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _inquire(client: AcumaticaClient, expect: Expect) -> dict[str, float]:
|
|
204
|
+
"""Probe a contract inquiry; sum each delta field over matching rows."""
|
|
205
|
+
assert expect.inquire is not None
|
|
206
|
+
assert expect.delta is not None
|
|
207
|
+
body = client.put(
|
|
208
|
+
expect.inquire,
|
|
209
|
+
expect.parameters or {},
|
|
210
|
+
endpoint=expect.endpoint,
|
|
211
|
+
params={"$expand": "Results"},
|
|
212
|
+
)
|
|
213
|
+
totals = dict.fromkeys(expect.delta, 0.0)
|
|
214
|
+
for row in body.get("Results") or []:
|
|
215
|
+
values = unwrap(row)
|
|
216
|
+
if expect.match and any(
|
|
217
|
+
field not in values or _norm(values[field]) != _norm(want)
|
|
218
|
+
for field, want in expect.match.items()
|
|
219
|
+
):
|
|
220
|
+
continue
|
|
221
|
+
for field in totals:
|
|
222
|
+
totals[field] += float(values.get(field) or 0.0)
|
|
223
|
+
return totals
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _wait(client: AcumaticaClient, spec: WaitSpec, variables: dict[str, Any]) -> None:
|
|
227
|
+
"""Poll the record until every `until` field matches (live state, V4)."""
|
|
228
|
+
keys = _subst(spec.keys, variables)
|
|
229
|
+
until = _subst(spec.until, variables)
|
|
230
|
+
deadline = time.monotonic() + spec.timeout
|
|
231
|
+
state: dict[str, Any] | None = None
|
|
232
|
+
while time.monotonic() < deadline:
|
|
233
|
+
record = client.get_record(spec.entity, keys, spec.endpoint)
|
|
234
|
+
state = unwrap(record) if record is not None else None
|
|
235
|
+
if state is not None and all(
|
|
236
|
+
field in state and _norm(state[field]) == _norm(want)
|
|
237
|
+
for field, want in until.items()
|
|
238
|
+
):
|
|
239
|
+
return
|
|
240
|
+
time.sleep(client.poll_interval)
|
|
241
|
+
raise RuntimeError(
|
|
242
|
+
f"wait timed out after {spec.timeout:g}s: {spec.entity} "
|
|
243
|
+
f"{keys} never reached {until} (last state: "
|
|
244
|
+
+ (
|
|
245
|
+
", ".join(f"{k}={state.get(k)!r}" for k in until)
|
|
246
|
+
if state is not None
|
|
247
|
+
else "record missing"
|
|
248
|
+
)
|
|
249
|
+
+ ")"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _dry_run(scenario: Scenario) -> None:
|
|
254
|
+
"""List steps and expectations without any HTTP."""
|
|
255
|
+
for step in scenario.steps:
|
|
256
|
+
op = (
|
|
257
|
+
f"PUT {step.put}"
|
|
258
|
+
if step.put
|
|
259
|
+
else f"invoke {step.action.entity}/{step.action.name}"
|
|
260
|
+
if step.action
|
|
261
|
+
else f"wait {step.wait.entity}" # pyright: ignore[reportOptionalMemberAccess]
|
|
262
|
+
)
|
|
263
|
+
output.data(f" would {op} [{step.id}]")
|
|
264
|
+
for expect in scenario.expect:
|
|
265
|
+
output.data(f" would check {expect.label()}")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _run_step(client: AcumaticaClient, step: Step, variables: dict[str, Any]) -> None:
|
|
269
|
+
"""Execute one step, folding captured fields into the variable set."""
|
|
270
|
+
if step.put is not None:
|
|
271
|
+
assert step.record is not None
|
|
272
|
+
body = client.put(
|
|
273
|
+
step.put, _subst(step.record, variables), endpoint=step.endpoint
|
|
274
|
+
)
|
|
275
|
+
output.data(f" put {step.put} [{step.id}]")
|
|
276
|
+
for field, var in (step.capture or {}).items():
|
|
277
|
+
echoed = unwrap(body)
|
|
278
|
+
if field not in echoed:
|
|
279
|
+
raise RuntimeError(
|
|
280
|
+
f"step '{step.id}': capture field '{field}' not in the PUT response"
|
|
281
|
+
)
|
|
282
|
+
variables[var] = echoed[field]
|
|
283
|
+
elif step.action is not None:
|
|
284
|
+
assert step.record is not None
|
|
285
|
+
client.invoke(
|
|
286
|
+
step.action.entity,
|
|
287
|
+
step.action.name,
|
|
288
|
+
_subst(step.record, variables),
|
|
289
|
+
_subst(step.parameters, variables),
|
|
290
|
+
step.endpoint,
|
|
291
|
+
)
|
|
292
|
+
output.data(f" invoke {step.action.name} [{step.id}]")
|
|
293
|
+
if step.wait is not None:
|
|
294
|
+
_wait(client, step.wait, variables)
|
|
295
|
+
output.data(f" wait ok [{step.id}]")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _check_get(
|
|
299
|
+
client: AcumaticaClient, expect: Expect, variables: dict[str, Any]
|
|
300
|
+
) -> bool:
|
|
301
|
+
"""Absolute field assertions on a key-URL record."""
|
|
302
|
+
assert expect.get is not None
|
|
303
|
+
assert expect.fields is not None
|
|
304
|
+
keys = _subst(expect.get.keys, variables)
|
|
305
|
+
# label from the substituted keys - the raw ${var} token is opaque
|
|
306
|
+
label = f"{expect.get.entity} [{', '.join(str(k) for k in keys)}]"
|
|
307
|
+
record = client.get_record(expect.get.entity, keys, expect.get.endpoint)
|
|
308
|
+
state = unwrap(record) if record is not None else {}
|
|
309
|
+
ok = True
|
|
310
|
+
for field, want in _subst(expect.fields, variables).items():
|
|
311
|
+
got = state.get(field)
|
|
312
|
+
if record is None or field not in state:
|
|
313
|
+
ok = False
|
|
314
|
+
output.data(f" x {label}.{field}: not found")
|
|
315
|
+
elif _norm(got) != _norm(want):
|
|
316
|
+
ok = False
|
|
317
|
+
output.data(f" x {label}.{field}: expected {want!r} got {got!r}")
|
|
318
|
+
else:
|
|
319
|
+
output.data(f" + {label}.{field} = {want!r}")
|
|
320
|
+
return ok
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _check_delta(
|
|
324
|
+
client: AcumaticaClient, expect: Expect, snapshot: dict[str, float]
|
|
325
|
+
) -> bool:
|
|
326
|
+
"""Delta assertions: post-run probe minus the pre-run snapshot."""
|
|
327
|
+
assert expect.delta is not None
|
|
328
|
+
after = _inquire(client, expect)
|
|
329
|
+
ok = True
|
|
330
|
+
for field, want in expect.delta.items():
|
|
331
|
+
got = after[field] - snapshot[field]
|
|
332
|
+
if abs(got - want) > 0.005:
|
|
333
|
+
ok = False
|
|
334
|
+
output.data(
|
|
335
|
+
f" x {expect.label()}.{field}: expected delta {want:+g} got {got:+g}"
|
|
336
|
+
)
|
|
337
|
+
else:
|
|
338
|
+
output.data(f" + {expect.label()}.{field} delta {want:+g}")
|
|
339
|
+
return ok
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def run(
|
|
343
|
+
client: AcumaticaClient | None, scenario: Scenario, dry_run: bool = False
|
|
344
|
+
) -> bool:
|
|
345
|
+
"""Execute the scenario; True when every step and expectation held.
|
|
346
|
+
|
|
347
|
+
client may be None only under dry_run - parsing and step listing
|
|
348
|
+
never touch HTTP (a preview costs nothing live). Delta expectations
|
|
349
|
+
snapshot BEFORE the first step (V4): the comparison is post minus
|
|
350
|
+
pre, so a warm tenant re-runs clean.
|
|
351
|
+
"""
|
|
352
|
+
output.data(f"{scenario.path} -> {scenario.scenario}")
|
|
353
|
+
if dry_run:
|
|
354
|
+
_dry_run(scenario)
|
|
355
|
+
return True
|
|
356
|
+
assert client is not None # non-dry-run callers pass a live session
|
|
357
|
+
before = [
|
|
358
|
+
_inquire(client, expect) if expect.inquire else {} for expect in scenario.expect
|
|
359
|
+
]
|
|
360
|
+
variables: dict[str, Any] = {}
|
|
361
|
+
for step in scenario.steps:
|
|
362
|
+
_run_step(client, step, variables)
|
|
363
|
+
ok = True
|
|
364
|
+
for expect, snapshot in zip(scenario.expect, before, strict=True):
|
|
365
|
+
if expect.get is not None:
|
|
366
|
+
ok = _check_get(client, expect, variables) and ok
|
|
367
|
+
else:
|
|
368
|
+
ok = _check_delta(client, expect, snapshot) and ok
|
|
369
|
+
return ok
|
|
@@ -7,7 +7,7 @@ Baseline file format:
|
|
|
7
7
|
|
|
8
8
|
entity: Currency # entity name in the contract endpoint
|
|
9
9
|
key: CurrencyID # key field(s), string or list
|
|
10
|
-
endpoint: Bootstrap/1.
|
|
10
|
+
endpoint: Bootstrap/1.6.0 # optional: override the instance endpoint
|
|
11
11
|
records:
|
|
12
12
|
- CurrencyID: "CAD"
|
|
13
13
|
Description: Canadian Dollar
|
|
@@ -34,7 +34,7 @@ cannot express (calendar generation and the like):
|
|
|
34
34
|
|
|
35
35
|
action: GenerateCalendar # action name on the endpoint entity
|
|
36
36
|
entity: MasterCalendar # entity the action hangs off
|
|
37
|
-
endpoint: Bootstrap/1.
|
|
37
|
+
endpoint: Bootstrap/1.6.0 # optional: override the instance endpoint
|
|
38
38
|
record: { FinancialYear: 2026 }
|
|
39
39
|
parameters: { FromYear: 2026, ToYear: 2026 } # optional
|
|
40
40
|
done_when: { filter: "FinancialYear eq '2026'" }
|
|
@@ -234,6 +234,23 @@ def _filter_for(record: dict[str, Any], keys: list[str]) -> str:
|
|
|
234
234
|
return " and ".join(f"{k} eq {literal(record[k])}" for k in keys)
|
|
235
235
|
|
|
236
236
|
|
|
237
|
+
def _expand_paths(record: dict[str, Any], prefix: str = "") -> list[str]:
|
|
238
|
+
"""$expand paths a record's shape demands (T60/T65).
|
|
239
|
+
|
|
240
|
+
A list field is a detail array - expands by name; a dict field is a
|
|
241
|
+
linked entity - expands by name plus the slash path of every nested
|
|
242
|
+
dict (`MainContact`, `MainContact/Address`).
|
|
243
|
+
"""
|
|
244
|
+
paths: list[str] = []
|
|
245
|
+
for field, value in record.items():
|
|
246
|
+
if isinstance(value, list):
|
|
247
|
+
paths.append(f"{prefix}{field}")
|
|
248
|
+
elif isinstance(value, dict):
|
|
249
|
+
paths.append(f"{prefix}{field}")
|
|
250
|
+
paths.extend(_expand_paths(value, f"{prefix}{field}/"))
|
|
251
|
+
return sorted(paths)
|
|
252
|
+
|
|
253
|
+
|
|
237
254
|
def _probe(client: AcumaticaClient, action: ActionFile) -> bool:
|
|
238
255
|
"""Run the done_when live-state probe; non-empty = action already realized.
|
|
239
256
|
|
|
@@ -345,14 +362,17 @@ def _fetch(
|
|
|
345
362
|
single-record GET skips the optimizer, so diff falls back to it on
|
|
346
363
|
exactly that error (V4: read-back must survive delegate-view entities).
|
|
347
364
|
|
|
348
|
-
Detail arrays only travel under $expand
|
|
349
|
-
answers top-level
|
|
350
|
-
|
|
365
|
+
Detail arrays and linked entities only travel under $expand
|
|
366
|
+
(T60/T65): without it every GET answers top-level scalars alone,
|
|
367
|
+
diff would report each source detail row or nested field missing
|
|
368
|
+
and apply's id-injection would see nothing to match. The expand set
|
|
369
|
+
derives from the record's own shape - a list field expands by name,
|
|
370
|
+
a dict field by its slash path (`MainContact/Address`).
|
|
351
371
|
"""
|
|
352
372
|
params = {"$filter": _filter_for(record, baseline.keys[:1])}
|
|
353
|
-
|
|
354
|
-
if
|
|
355
|
-
params["$expand"] = ",".join(
|
|
373
|
+
expand = _expand_paths(record)
|
|
374
|
+
if expand:
|
|
375
|
+
params["$expand"] = ",".join(expand)
|
|
356
376
|
try:
|
|
357
377
|
live = client.get_list(
|
|
358
378
|
baseline.entity, params=params, endpoint=baseline.endpoint
|
|
@@ -364,7 +384,7 @@ def _fetch(
|
|
|
364
384
|
baseline.entity,
|
|
365
385
|
[record[k] for k in baseline.keys],
|
|
366
386
|
baseline.endpoint,
|
|
367
|
-
params={"$expand": params["$expand"]} if
|
|
387
|
+
params={"$expand": params["$expand"]} if expand else None,
|
|
368
388
|
)
|
|
369
389
|
for row in live:
|
|
370
390
|
actual = unwrap(row)
|
|
@@ -403,6 +423,10 @@ def diff(client: AcumaticaClient, baseline: BaselineFile | ActionFile) -> list[s
|
|
|
403
423
|
key = (baseline.detail_keys or {})[field] # load-validated
|
|
404
424
|
live_rows = actual.get(field, [])
|
|
405
425
|
drifts.extend(_diff_details(label, field, key, expected, live_rows))
|
|
426
|
+
elif isinstance(expected, dict):
|
|
427
|
+
drifts.extend(
|
|
428
|
+
_diff_nested(f"{label}.{field}", expected, actual.get(field))
|
|
429
|
+
)
|
|
406
430
|
elif field not in actual:
|
|
407
431
|
drifts.append(f"{label}.{field}: not returned by endpoint")
|
|
408
432
|
elif _norm(actual[field]) != _norm(expected):
|
|
@@ -412,6 +436,21 @@ def diff(client: AcumaticaClient, baseline: BaselineFile | ActionFile) -> list[s
|
|
|
412
436
|
return drifts
|
|
413
437
|
|
|
414
438
|
|
|
439
|
+
def _diff_nested(path: str, expected: dict[str, Any], live: Any) -> list[str]:
|
|
440
|
+
"""Linked-entity drift (T65): recurse source-side fields only."""
|
|
441
|
+
if not isinstance(live, dict):
|
|
442
|
+
return [f"{path}: not returned by endpoint"]
|
|
443
|
+
drifts: list[str] = []
|
|
444
|
+
for field, want in expected.items():
|
|
445
|
+
if isinstance(want, dict):
|
|
446
|
+
drifts.extend(_diff_nested(f"{path}.{field}", want, live.get(field)))
|
|
447
|
+
elif field not in live:
|
|
448
|
+
drifts.append(f"{path}.{field}: not returned by endpoint")
|
|
449
|
+
elif _norm(live[field]) != _norm(want):
|
|
450
|
+
drifts.append(f"{path}.{field}: source={want!r} live={live[field]!r}")
|
|
451
|
+
return drifts
|
|
452
|
+
|
|
453
|
+
|
|
415
454
|
def _diff_details(
|
|
416
455
|
label: str,
|
|
417
456
|
field: str,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# General Ledger preferences (GL102000) via the custom Bootstrap/1.
|
|
1
|
+
# General Ledger preferences (GL102000) via the custom Bootstrap/1.6.0
|
|
2
2
|
# endpoint (the per-file endpoint: override) - the Default endpoint has
|
|
3
3
|
# no GL-setup entity, and every Finance screen throws
|
|
4
4
|
# PXSetupNotEnteredException until the GLSetup singleton row exists. The
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# referenced here must exist.
|
|
13
13
|
entity: GLPreferences
|
|
14
14
|
key: RetEarnAccountID
|
|
15
|
-
endpoint: Bootstrap/1.
|
|
15
|
+
endpoint: Bootstrap/1.6.0
|
|
16
16
|
records:
|
|
17
17
|
- RetEarnAccountID: '32000'
|
|
18
18
|
YtdNetIncAccountID: '33000'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Org-ledger link (GL201500) via the custom Bootstrap/1.
|
|
1
|
+
# Org-ledger link (GL201500) via the custom Bootstrap/1.6.0 endpoint (the
|
|
2
2
|
# per-file endpoint: override). The Default endpoint's Ledger.Companies
|
|
3
3
|
# detail is write-tolerated then silently dropped (PUT answers 200 and
|
|
4
4
|
# echoes the row, the link table stays empty), so the link writes through
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
# (company.yaml).
|
|
22
22
|
entity: LedgerCompany
|
|
23
23
|
key: [LedgerCD, OrganizationID]
|
|
24
|
-
endpoint: Bootstrap/1.
|
|
24
|
+
endpoint: Bootstrap/1.6.0
|
|
25
25
|
records:
|
|
26
26
|
- LedgerCD: ACTUAL
|
|
27
27
|
OrganizationID: COMPANY
|
{acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/bootstrap/company.yaml
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Company (CS101500) via the custom Bootstrap/1.
|
|
1
|
+
# Company (CS101500) via the custom Bootstrap/1.6.0 endpoint (the per-file
|
|
2
2
|
# endpoint: override) - built-in endpoints cannot create the first
|
|
3
3
|
# organization on a virgin tenant. List values are the EXTERNAL labels
|
|
4
4
|
# ('Without Branches', not the DAC's 'WithoutBranches'); CountryID feeds
|
|
5
5
|
# the org's Main Address - required on first save.
|
|
6
6
|
entity: Company
|
|
7
7
|
key: AcctCD
|
|
8
|
-
endpoint: Bootstrap/1.
|
|
8
|
+
endpoint: Bootstrap/1.6.0
|
|
9
9
|
records:
|
|
10
10
|
- AcctCD: COMPANY
|
|
11
11
|
AcctName: Example Company
|
{acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/bootstrap/credit-terms.yaml
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Credit terms (CS206500) via the custom Bootstrap/1.
|
|
1
|
+
# Credit terms (CS206500) via the custom Bootstrap/1.6.0 endpoint (the
|
|
2
2
|
# per-file endpoint: override) - the Default endpoint has no credit-terms
|
|
3
3
|
# entity. List values are the EXTERNAL labels: VisibleTo = All, Vendors,
|
|
4
4
|
# Customers, Disabled; DueType/DiscType = Fixed Number of Days, Day of
|
|
5
5
|
# Next Month, End of Month, ...
|
|
6
6
|
entity: CreditTerms
|
|
7
7
|
key: TermsID
|
|
8
|
-
endpoint: Bootstrap/1.
|
|
8
|
+
endpoint: Bootstrap/1.6.0
|
|
9
9
|
records:
|
|
10
10
|
- TermsID: NET30
|
|
11
11
|
Descr: Net 30 Days
|
|
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.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/baseline/10-subaccounts.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/baseline/20-accounts.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/baseline/40-ledger.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/baseline/90-uoms.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.5.0 → acumatica_cli-0.7.0}/src/acumatica_cli/templates/bootstrap/features.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|