acumatica-cli 0.31.0__tar.gz → 0.32.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.31.0 → acumatica_cli-0.32.0}/PKG-INFO +1 -1
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/pyproject.toml +1 -1
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/pyproject.toml.orig +1 -1
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/client.py +25 -15
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/extract.py +28 -3
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/seed.py +55 -7
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/README.md +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/__init__.py +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/bootstrap.py +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/bootstrap_plugin.cs +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/bootstrap_project.xml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/cli.py +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/config.py +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/firstlogin.py +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/inventory.py +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/models.py +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/output.py +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/reconcile.py +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/run.py +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/seed_catalog.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/snapshot_map.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/state.py +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/README.md +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/baseline/10-subaccounts.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/baseline/20-accounts.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/baseline/40-ledger.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/baseline/50-gl-preferences.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/baseline/60-ledger-company.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/baseline/90-uoms.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/baseline/91-company-packaging.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/bootstrap/company.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/bootstrap/credit-terms.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/bootstrap/features.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/bootstrap/segmented-key.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/05-numbering-sequences.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/10-reason-codes.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/20-in-preferences.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/30-availability-rules.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/40-posting-classes.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/50-warehouse.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/51-warehouse-locations.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/52-warehouse-defaults.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/53-tax-categories.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/54-item-classes.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/56-so-preferences.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/57-po-preferences.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/58-order-types.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/60-ar-preferences.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/61-ap-preferences.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/62-ca-preferences.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/63-cash-account.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/64-payment-methods.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/65-statement-cycles.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/70-vendor-classes.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/71-customer-classes.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/75-vendors.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/76-customers.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/80-stock-items-parts.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/82-stock-items-kits.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/85-kit-specifications.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/90-roles.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/91-users.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/master/92-role-users.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/setup/10-financial-year.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/setup/20-master-calendar.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/setup/30-open-periods.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/config/views/10-trial-balance.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/env +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/gitignore +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/overlays/README.md +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/overlays/default-24.200.001/README.md +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/overlays/default-24.200.001/scenario/30-build.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/scenario/10-seed-capital.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/scenario/20-buy.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/scenario/30-build.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/scenario/40-sell.yaml +0 -0
- {acumatica_cli-0.31.0 → acumatica_cli-0.32.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.32.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"
|
|
@@ -12,6 +12,11 @@ import httpx
|
|
|
12
12
|
|
|
13
13
|
from .config import Instance
|
|
14
14
|
|
|
15
|
+
# Contract control fields travel bare on wrap and stay on unwrap of
|
|
16
|
+
# value-field rows (V54). Files-style rows that unwrap to only these
|
|
17
|
+
# keys are elided so expanded `files` descriptors stay noise.
|
|
18
|
+
_BARE_FIELDS = frozenset({"id", "delete"})
|
|
19
|
+
|
|
15
20
|
|
|
16
21
|
def wrap(record: dict[str, Any]) -> dict[str, Any]:
|
|
17
22
|
"""Plain dict -> contract-API body: {"Field": {"value": ...}}.
|
|
@@ -33,31 +38,35 @@ def wrap(record: dict[str, Any]) -> dict[str, Any]:
|
|
|
33
38
|
return wrap(v)
|
|
34
39
|
return {"value": v}
|
|
35
40
|
|
|
36
|
-
return {k: v if k in
|
|
41
|
+
return {k: v if k in _BARE_FIELDS else _value(v) for k, v in record.items()}
|
|
37
42
|
|
|
38
43
|
|
|
39
44
|
def unwrap(entity: dict[str, Any]) -> dict[str, Any]:
|
|
40
45
|
"""Contract-API entity -> plain dict (value fields + detail arrays).
|
|
41
46
|
|
|
42
|
-
Inverse of wrap (T60/T65): a list of dicts unwraps row by row;
|
|
43
|
-
that are empty or whose rows carry no value fields are elided
|
|
44
|
-
detail array the source never claimed must not surface, and
|
|
45
|
-
`files` descriptors are plain dicts that would unwrap to
|
|
46
|
-
non-value dict is a linked entity — unwrapped recursively,
|
|
47
|
-
when something survives, so bookkeeping dicts (custom,
|
|
48
|
-
stay excluded exactly as before.
|
|
47
|
+
Inverse of wrap (T60/T65/V54): a list of dicts unwraps row by row;
|
|
48
|
+
lists that are empty or whose rows carry no value fields are elided
|
|
49
|
+
(a detail array the source never claimed must not surface, and
|
|
50
|
+
expanded `files` descriptors are plain dicts that would unwrap to
|
|
51
|
+
noise). A non-value dict is a linked entity — unwrapped recursively,
|
|
52
|
+
kept only when something survives, so bookkeeping dicts (custom,
|
|
53
|
+
_links, note) stay excluded exactly as before. `id` and `delete`
|
|
54
|
+
stay on records and on detail rows that also have value fields so a
|
|
55
|
+
later PUT updates the existing line (V54).
|
|
49
56
|
"""
|
|
50
57
|
out: dict[str, Any] = {}
|
|
51
58
|
for k, v in entity.items():
|
|
52
|
-
if isinstance(v, dict)
|
|
59
|
+
if k in _BARE_FIELDS and not isinstance(v, (dict, list)):
|
|
60
|
+
out[k] = v
|
|
61
|
+
elif isinstance(v, dict) and "value" in v:
|
|
53
62
|
out[k] = v["value"]
|
|
54
63
|
elif isinstance(v, dict):
|
|
55
64
|
nested = unwrap(v)
|
|
56
65
|
if nested:
|
|
57
66
|
out[k] = nested
|
|
58
67
|
elif isinstance(v, list) and v and all(isinstance(row, dict) for row in v):
|
|
59
|
-
rows = [unwrap(
|
|
60
|
-
if
|
|
68
|
+
rows = [row for row in (unwrap(r) for r in v) if set(row) - _BARE_FIELDS]
|
|
69
|
+
if rows:
|
|
61
70
|
out[k] = rows
|
|
62
71
|
return out
|
|
63
72
|
|
|
@@ -326,7 +335,7 @@ class AcumaticaClient:
|
|
|
326
335
|
|
|
327
336
|
@staticmethod
|
|
328
337
|
def _checked(r: httpx.Response) -> httpx.Response:
|
|
329
|
-
"""Surface exceptionMessage +
|
|
338
|
+
"""Surface exceptionMessage + innerException + Field.error (V46)."""
|
|
330
339
|
if r.is_error:
|
|
331
340
|
detail = ""
|
|
332
341
|
try:
|
|
@@ -338,12 +347,13 @@ class AcumaticaClient:
|
|
|
338
347
|
or body.get("error")
|
|
339
348
|
or ""
|
|
340
349
|
)
|
|
341
|
-
|
|
342
|
-
if not detail and isinstance(body.get("innerException"), dict):
|
|
350
|
+
if isinstance(body.get("innerException"), dict):
|
|
343
351
|
inner = body["innerException"]
|
|
344
|
-
|
|
352
|
+
inner_msg = (
|
|
345
353
|
inner.get("exceptionMessage") or inner.get("message") or ""
|
|
346
354
|
)
|
|
355
|
+
if inner_msg:
|
|
356
|
+
detail = f"{detail}; {inner_msg}" if detail else inner_msg
|
|
347
357
|
field_errs = AcumaticaClient._field_errors(body)
|
|
348
358
|
if field_errs:
|
|
349
359
|
joined = "; ".join(field_errs)
|
|
@@ -46,6 +46,8 @@ from .models import Model, validation_summary
|
|
|
46
46
|
from .seed import (
|
|
47
47
|
NUMBERING_RUNTIME_FIELDS,
|
|
48
48
|
PASSWORD_FIELDS,
|
|
49
|
+
_calendar_day, # pyright: ignore[reportPrivateUsage]
|
|
50
|
+
_drop_path_ignored, # pyright: ignore[reportPrivateUsage]
|
|
49
51
|
active_bootstrap,
|
|
50
52
|
resolve_endpoint,
|
|
51
53
|
)
|
|
@@ -263,14 +265,16 @@ def _fetch(client: AcumaticaClient, spec: EntitySpec) -> list[dict[str, Any]]:
|
|
|
263
265
|
|
|
264
266
|
# Server-assigned / audit fields that must never enter seed (B11 class).
|
|
265
267
|
# Applied recursively under linked entities and detail rows after expand
|
|
266
|
-
# (T65/T119)
|
|
267
|
-
#
|
|
268
|
+
# (T65/T119). `id`/`delete` stay in unwrap for run-capture PUT (V54);
|
|
269
|
+
# extract still strips them so seed is tenant-portable.
|
|
268
270
|
_SERVER_DERIVED = frozenset(
|
|
269
271
|
{
|
|
270
272
|
"ContactID",
|
|
271
273
|
"CreatedDateTime",
|
|
272
274
|
"LastModifiedDateTime",
|
|
273
275
|
"NoteID",
|
|
276
|
+
"delete",
|
|
277
|
+
"id",
|
|
274
278
|
"tstamp",
|
|
275
279
|
}
|
|
276
280
|
)
|
|
@@ -293,6 +297,26 @@ def _elide_server_derived(value: Any) -> Any:
|
|
|
293
297
|
return value
|
|
294
298
|
|
|
295
299
|
|
|
300
|
+
def _date_strip(value: Any) -> Any:
|
|
301
|
+
"""DateTimeValue seed emit: keep YYYY-MM-DD prefix only (V55)."""
|
|
302
|
+
if isinstance(value, dict):
|
|
303
|
+
return {k: _date_strip(v) for k, v in value.items()}
|
|
304
|
+
if isinstance(value, list):
|
|
305
|
+
return [_date_strip(v) for v in value]
|
|
306
|
+
if isinstance(value, str):
|
|
307
|
+
day = _calendar_day(value)
|
|
308
|
+
if day is not None:
|
|
309
|
+
return day
|
|
310
|
+
return value
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _strip_write_only_get_omit(entity: str, field: str, value: Any) -> Any:
|
|
314
|
+
"""Drop path-qualified write-only GET-omit fields from detail rows (V56)."""
|
|
315
|
+
if not isinstance(value, list):
|
|
316
|
+
return value
|
|
317
|
+
return _drop_path_ignored(entity, field, value)
|
|
318
|
+
|
|
319
|
+
|
|
296
320
|
def _kept_fields(spec: EntitySpec, record: dict[str, Any]) -> dict[str, Any]:
|
|
297
321
|
"""Apply include/strip + nested server-derived elision to one unwrapped row."""
|
|
298
322
|
keep: dict[str, Any] = {}
|
|
@@ -313,6 +337,7 @@ def _kept_fields(spec: EntitySpec, record: dict[str, Any]) -> dict[str, Any]:
|
|
|
313
337
|
continue
|
|
314
338
|
# Nested ContactID / LastModifiedDateTime under expand (T65/T119).
|
|
315
339
|
cleaned = _elide_server_derived(value)
|
|
340
|
+
cleaned = _strip_write_only_get_omit(spec.entity, field, cleaned)
|
|
316
341
|
if cleaned is None or cleaned == "":
|
|
317
342
|
continue
|
|
318
343
|
if isinstance(cleaned, (dict, list)) and not cleaned:
|
|
@@ -320,7 +345,7 @@ def _kept_fields(spec: EntitySpec, record: dict[str, Any]) -> dict[str, Any]:
|
|
|
320
345
|
keep[field] = cleaned
|
|
321
346
|
ordered = {k: keep[k] for k in spec.keys if k in keep}
|
|
322
347
|
ordered |= {k: keep[k] for k in sorted(keep.keys() - set(spec.keys))}
|
|
323
|
-
return ordered
|
|
348
|
+
return _date_strip(ordered)
|
|
324
349
|
|
|
325
350
|
|
|
326
351
|
def _shape(spec: EntitySpec, live: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
@@ -52,6 +52,7 @@ Default Currency = CM201000 list), so an ambiguous file is a hard error,
|
|
|
52
52
|
never a silent Default-endpoint PUT.
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
|
+
import re
|
|
55
56
|
from pathlib import Path
|
|
56
57
|
from typing import Any
|
|
57
58
|
|
|
@@ -88,7 +89,34 @@ PASSWORD_FIELDS = frozenset({"Password", "b64__Password"})
|
|
|
88
89
|
# PUTs them (re-apply must not reset live counters).
|
|
89
90
|
NUMBERING_RUNTIME_FIELDS = frozenset({"LastNbr"})
|
|
90
91
|
# Fields omitted from source↔live compare (V39 write-only + V40 runtime).
|
|
92
|
+
# Field-name deny at every nesting level. Do not add bare ``Value`` here —
|
|
93
|
+
# that would hide real drift on unrelated fields (V56).
|
|
91
94
|
_DIFF_IGNORE_FIELDS = PASSWORD_FIELDS | NUMBERING_RUNTIME_FIELDS
|
|
95
|
+
# Path-qualified write-only GET-omit (V56): (entity, detail, field).
|
|
96
|
+
# Diff ignores; extract strips; apply still PUTs when present in YAML.
|
|
97
|
+
_DIFF_IGNORE_PATHS = frozenset({("LotSerialClass", "Segments", "Value")})
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _diff_ignored(entity: str, *path: str) -> bool:
|
|
101
|
+
"""True when source↔live compare skips this field (V39/V40/V56)."""
|
|
102
|
+
if path and path[-1] in _DIFF_IGNORE_FIELDS:
|
|
103
|
+
return True
|
|
104
|
+
return (entity, *path) in _DIFF_IGNORE_PATHS
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _drop_path_ignored(entity: str, field: str, rows: list[Any]) -> list[Any]:
|
|
108
|
+
"""Strip path-qualified GET-omit fields from detail rows (V56)."""
|
|
109
|
+
drop = {
|
|
110
|
+
sub for (ent, det, sub) in _DIFF_IGNORE_PATHS if ent == entity and det == field
|
|
111
|
+
}
|
|
112
|
+
if not drop:
|
|
113
|
+
return rows
|
|
114
|
+
return [
|
|
115
|
+
{k: v for k, v in row.items() if k not in drop}
|
|
116
|
+
if isinstance(row, dict)
|
|
117
|
+
else row
|
|
118
|
+
for row in rows
|
|
119
|
+
]
|
|
92
120
|
|
|
93
121
|
|
|
94
122
|
def active_bootstrap(root: Path | None = None) -> tuple[str, frozenset[str]]:
|
|
@@ -251,18 +279,33 @@ def load_baseline(path: Path) -> BaselineFile | ActionFile:
|
|
|
251
279
|
return parsed
|
|
252
280
|
|
|
253
281
|
|
|
282
|
+
# ISO date or DateTimeValue: calendar-day prefix only (V55). Random strings
|
|
283
|
+
# that merely contain T stay untouched.
|
|
284
|
+
_ISO_CALENDAR_DAY = re.compile(r"(\d{4}-\d{2}-\d{2})(T.*)?")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _calendar_day(text: str) -> str | None:
|
|
288
|
+
"""YYYY-MM-DD when ``text`` is ISO date/datetime; else None. No TZ convert."""
|
|
289
|
+
m = _ISO_CALENDAR_DAY.fullmatch(text)
|
|
290
|
+
return None if m is None else m.group(1)
|
|
291
|
+
|
|
292
|
+
|
|
254
293
|
def _norm(value: Any) -> str:
|
|
255
|
-
"""Comparable form: bools case-folded, numbers by value,
|
|
294
|
+
"""Comparable form: bools case-folded, numbers by value, dates by day.
|
|
256
295
|
|
|
257
296
|
Numbers compare by value, not spelling - a YAML `0` against the
|
|
258
297
|
endpoint's `0.0` (DecimalValue fields come back as floats) is not
|
|
259
|
-
drift (T13).
|
|
298
|
+
drift (T13). Date-only ``YYYY-MM-DD`` matches a live DateTimeValue on
|
|
299
|
+
the same calendar day (strip ``T...offset``); a different day still
|
|
300
|
+
drifts (V55). No timezone conversion.
|
|
260
301
|
"""
|
|
261
302
|
if isinstance(value, bool):
|
|
262
303
|
return str(value).lower()
|
|
263
304
|
if isinstance(value, int | float):
|
|
264
305
|
return repr(float(value))
|
|
265
|
-
|
|
306
|
+
text = str(value).strip()
|
|
307
|
+
day = _calendar_day(text)
|
|
308
|
+
return text if day is None else day
|
|
266
309
|
|
|
267
310
|
|
|
268
311
|
def _filter_for(record: dict[str, Any], keys: list[str]) -> str:
|
|
@@ -636,13 +679,18 @@ def diff(client: AcumaticaClient, baseline: BaselineFile | ActionFile) -> list[s
|
|
|
636
679
|
drifts.append(f"{label}: missing on tenant")
|
|
637
680
|
continue
|
|
638
681
|
actual = unwrap(live)
|
|
639
|
-
# V39/V40: never compare write-only
|
|
640
|
-
fields = {
|
|
682
|
+
# V39/V40/V56: never compare write-only, runtime, or GET-omit paths
|
|
683
|
+
fields = {
|
|
684
|
+
k: v for k, v in record.items() if not _diff_ignored(baseline.entity, k)
|
|
685
|
+
}
|
|
641
686
|
for field, expected in fields.items():
|
|
642
687
|
if isinstance(expected, list):
|
|
643
688
|
key = (baseline.detail_keys or {})[field] # load-validated
|
|
644
|
-
|
|
645
|
-
|
|
689
|
+
want_rows = _drop_path_ignored(baseline.entity, field, expected)
|
|
690
|
+
live_rows = _drop_path_ignored(
|
|
691
|
+
baseline.entity, field, actual.get(field, [])
|
|
692
|
+
)
|
|
693
|
+
drifts.extend(_diff_details(label, field, key, want_rows, live_rows))
|
|
646
694
|
elif isinstance(expected, dict):
|
|
647
695
|
drifts.extend(
|
|
648
696
|
_diff_nested(f"{label}.{field}", expected, actual.get(field))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/overlays/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/scenario/20-buy.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/scenario/30-build.yaml
RENAMED
|
File without changes
|
{acumatica_cli-0.31.0 → acumatica_cli-0.32.0}/src/acumatica_cli/templates/scenario/40-sell.yaml
RENAMED
|
File without changes
|
|
File without changes
|