acumatica-cli 0.28.0__tar.gz → 0.29.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/PKG-INFO +2 -2
  2. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/README.md +1 -1
  3. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/pyproject.toml +1 -1
  4. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/pyproject.toml.orig +1 -1
  5. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/bootstrap.py +2 -2
  6. acumatica_cli-0.29.0/src/acumatica_cli/bootstrap_plugin.cs +147 -0
  7. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/bootstrap_project.xml +42 -11
  8. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/config.py +5 -0
  9. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/extract.py +7 -0
  10. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/seed.py +59 -1
  11. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/seed_catalog.yaml +37 -12
  12. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/README.md +1 -1
  13. acumatica_cli-0.29.0/src/acumatica_cli/templates/config/baseline/91-company-packaging.yaml +8 -0
  14. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/91-users.yaml +0 -1
  15. acumatica_cli-0.29.0/src/acumatica_cli/templates/config/master/92-role-users.yaml +9 -0
  16. acumatica_cli-0.28.0/src/acumatica_cli/bootstrap_plugin.cs +0 -74
  17. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/__init__.py +0 -0
  18. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/cli.py +0 -0
  19. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/client.py +0 -0
  20. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/firstlogin.py +0 -0
  21. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/inventory.py +0 -0
  22. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/models.py +0 -0
  23. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/output.py +0 -0
  24. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/reconcile.py +0 -0
  25. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/run.py +0 -0
  26. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/snapshot_map.yaml +0 -0
  27. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/state.py +0 -0
  28. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/baseline/10-subaccounts.yaml +0 -0
  29. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/baseline/20-accounts.yaml +0 -0
  30. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/baseline/40-ledger.yaml +0 -0
  31. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/baseline/50-gl-preferences.yaml +0 -0
  32. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/baseline/60-ledger-company.yaml +0 -0
  33. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/baseline/90-uoms.yaml +0 -0
  34. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/bootstrap/company.yaml +0 -0
  35. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/bootstrap/credit-terms.yaml +0 -0
  36. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/bootstrap/features.yaml +0 -0
  37. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/bootstrap/segmented-key.yaml +0 -0
  38. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/05-numbering-sequences.yaml +0 -0
  39. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/10-reason-codes.yaml +0 -0
  40. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/20-in-preferences.yaml +0 -0
  41. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/30-availability-rules.yaml +0 -0
  42. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/40-posting-classes.yaml +0 -0
  43. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/50-warehouse.yaml +0 -0
  44. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/51-warehouse-locations.yaml +0 -0
  45. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/52-warehouse-defaults.yaml +0 -0
  46. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/53-tax-categories.yaml +0 -0
  47. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/54-item-classes.yaml +0 -0
  48. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/56-so-preferences.yaml +0 -0
  49. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/57-po-preferences.yaml +0 -0
  50. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/58-order-types.yaml +0 -0
  51. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/60-ar-preferences.yaml +0 -0
  52. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/61-ap-preferences.yaml +0 -0
  53. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/62-ca-preferences.yaml +0 -0
  54. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/63-cash-account.yaml +0 -0
  55. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/64-payment-methods.yaml +0 -0
  56. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/65-statement-cycles.yaml +0 -0
  57. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/70-vendor-classes.yaml +0 -0
  58. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/71-customer-classes.yaml +0 -0
  59. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/75-vendors.yaml +0 -0
  60. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/76-customers.yaml +0 -0
  61. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/80-stock-items-parts.yaml +0 -0
  62. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/82-stock-items-kits.yaml +0 -0
  63. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/85-kit-specifications.yaml +0 -0
  64. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/master/90-roles.yaml +0 -0
  65. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/setup/10-financial-year.yaml +0 -0
  66. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/setup/20-master-calendar.yaml +0 -0
  67. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/setup/30-open-periods.yaml +0 -0
  68. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/config/views/10-trial-balance.yaml +0 -0
  69. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/env +0 -0
  70. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/gitignore +0 -0
  71. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/overlays/README.md +0 -0
  72. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/overlays/default-24.200.001/README.md +0 -0
  73. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/overlays/default-24.200.001/scenario/30-build.yaml +0 -0
  74. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/scenario/10-seed-capital.yaml +0 -0
  75. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/scenario/20-buy.yaml +0 -0
  76. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/scenario/30-build.yaml +0 -0
  77. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/templates/scenario/40-sell.yaml +0 -0
  78. {acumatica_cli-0.28.0 → acumatica_cli-0.29.0}/src/acumatica_cli/tenant.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acumatica-cli
3
- Version: 0.28.0
3
+ Version: 0.29.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>
@@ -196,7 +196,7 @@ There is no `--flavor`.
196
196
  | `config/bootstrap/` | virgin-tenant config: features, company, credit terms (no `project.xml`) |
197
197
  | `config/baseline/` | reference data: subaccounts, COA, ledger, UOMs |
198
198
  | `config/setup/` | one-time actions: financial year, master calendar, open periods |
199
- | `config/master/` | inventory/distribution masters: numbering (`05-…`) before prefs, warehouse, items, parties + Role/User (`90-roles` then `91-users`) |
199
+ | `config/master/` | inventory/distribution masters: numbering (`05-…`) before prefs, warehouse, items, parties + Role/User (`90-roles` then `91-users` then `92-role-users`) |
200
200
  | `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
201
201
  | `config/views/` | observer views for `acu state` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
202
202
  | `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
@@ -178,7 +178,7 @@ There is no `--flavor`.
178
178
  | `config/bootstrap/` | virgin-tenant config: features, company, credit terms (no `project.xml`) |
179
179
  | `config/baseline/` | reference data: subaccounts, COA, ledger, UOMs |
180
180
  | `config/setup/` | one-time actions: financial year, master calendar, open periods |
181
- | `config/master/` | inventory/distribution masters: numbering (`05-…`) before prefs, warehouse, items, parties + Role/User (`90-roles` then `91-users`) |
181
+ | `config/master/` | inventory/distribution masters: numbering (`05-…`) before prefs, warehouse, items, parties + Role/User (`90-roles` then `91-users` then `92-role-users`) |
182
182
  | `scenario/` | lifecycle txns for `acu run`: once capital, then buy, build, sell |
183
183
  | `config/views/` | observer views for `acu state` (`inquire:` / `entity:` / `gi:`; not SEED_DIRS) |
184
184
  | `state/` | committed derived-state observations (evidence, not seed; money/qty fixed-point) |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acumatica-cli"
3
- version = "0.28.0"
3
+ version = "0.29.0"
4
4
  description = "Acumatica ERP Config-as-Code: tenant provisioning, baseline config, and reference data"
5
5
  license = "PolyForm-Noncommercial-1.0.0"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "acumatica-cli"
3
- version = "0.28.0"
3
+ version = "0.29.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"
@@ -9,7 +9,7 @@ cannot write CS100000 at all (T3 verdict) — and whose Bootstrap contract
9
9
  endpoint exposes the seeding surface (serialization verified T12).
10
10
 
11
11
  Bootstrap endpoint contract is package SoT (V2/V21/T178): always the
12
- packaged full company ``bootstrap_project.xml`` (``Bootstrap/1.7.0``).
12
+ packaged full company ``bootstrap_project.xml`` (``Bootstrap/1.10.0``).
13
13
  Data-repo ``config/bootstrap/project.xml`` or ``bootstrap/project.xml`` is
14
14
  not a seed — present → hard error naming package SoT (no dual contract line).
15
15
 
@@ -68,7 +68,7 @@ def _bootstrap_file(root: Path, name: str) -> Path | None:
68
68
 
69
69
 
70
70
  def packaged_contract_xml() -> bytes:
71
- """The CLI-shipped full company Bootstrap contract (``Bootstrap/1.7.0``)."""
71
+ """The CLI-shipped full company Bootstrap contract (``Bootstrap/1.10.0``)."""
72
72
  return (resources.files("acumatica_cli") / "bootstrap_project.xml").read_bytes()
73
73
 
74
74
 
@@ -0,0 +1,147 @@
1
+ // acu bootstrap CustomizationPlugin (SPEC T11).
2
+ //
3
+ // Runs in-process on customization publish - the one write path to
4
+ // FeaturesSet that works: the contract API cannot persist features no
5
+ // matter what endpoint fronts CS100000 (T3 verdict, docs/rest-api.md).
6
+ //
7
+ // Deliberately writes through PXDatabase, not FeaturesMaint: the graph
8
+ // save collides with the publish pipeline's concurrent plugin invocations
9
+ // ("Another process has added the 'FeaturesSet' record" - observed live,
10
+ // 26.101.0225) and nothing persists. A keyed row write is deterministic
11
+ // and idempotent: update the existing row, insert when absent. All 205
12
+ // NOT NULL bit columns are filled reflectively (only ~136 carry DB
13
+ // defaults); Status = 0 means Validated (PXIntList, verified vs live).
14
+ //
15
+ // The Enabled set is NOT authored here (SPEC V2: feature flags are config
16
+ // "what", never tool source - B6): package_zip() substitutes the
17
+ // ACU_FEATURES sentinel below with the data repo's bootstrap/features.yaml
18
+ // list (built-in six when the file is absent) at package-build time.
19
+ using System;
20
+ using System.Collections;
21
+ using System.Collections.Generic;
22
+ using System.Reflection;
23
+ using PX.Data;
24
+ using PX.Objects.CS;
25
+ using PX.SM;
26
+
27
+ namespace AcuBootstrap
28
+ {
29
+ public class AcuBootstrapPlugin : Customization.CustomizationPlugin
30
+ {
31
+ private static readonly HashSet<string> Enabled = new HashSet<string>
32
+ {
33
+ /*ACU_FEATURES*/
34
+ };
35
+
36
+ public override void UpdateDatabase()
37
+ {
38
+ var flags = new List<PXDataFieldAssign>();
39
+ var known = new HashSet<string>();
40
+ foreach (PropertyInfo prop in typeof(FeaturesSet).GetProperties())
41
+ {
42
+ if (prop.PropertyType == typeof(bool?))
43
+ {
44
+ known.Add(prop.Name);
45
+ flags.Add(new PXDataFieldAssign(
46
+ prop.Name, PXDbType.Bit, Enabled.Contains(prop.Name)));
47
+ }
48
+ }
49
+ foreach (string name in Enabled)
50
+ {
51
+ if (!known.Contains(name))
52
+ {
53
+ // silent-typo guard (T24): a misspelled features.yaml
54
+ // entry enables nothing - say so in the publish log
55
+ WriteLog("AcuBootstrap: unknown feature name '" + name
56
+ + "' - no FeaturesSet property, nothing enabled");
57
+ }
58
+ }
59
+ flags.Add(new PXDataFieldAssign("Status", PXDbType.Int, 0)); // Validated
60
+
61
+ using (var tx = new PXTransactionScope())
62
+ {
63
+ bool updated = PXDatabase.Update<FeaturesSet>(flags.ToArray());
64
+ if (!updated)
65
+ {
66
+ PXDatabase.Insert<FeaturesSet>(flags.ToArray());
67
+ WriteLog("AcuBootstrap: FeaturesSet row inserted (features enabled)");
68
+ }
69
+ else
70
+ {
71
+ WriteLog("AcuBootstrap: FeaturesSet row updated (features enabled)");
72
+ }
73
+ tx.Complete();
74
+ }
75
+ }
76
+
77
+ // V53/B33: mapped Role.Users / User.Roles PUT never writes
78
+ // UsersInRoles. Persist is a keyed PXDatabase write (same
79
+ // FeaturesSet idiom): ApplicationName='/' (ASP.NET membership).
80
+ public static void AssignUserToRole(string username, string rolename)
81
+ {
82
+ if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(rolename))
83
+ {
84
+ throw new PXException(
85
+ "AssignUser requires Username and Rolename");
86
+ }
87
+ Guid userId = PXAccess.GetUserID();
88
+ DateTime now = DateTime.Now;
89
+ var update = new List<PXDataFieldParam>
90
+ {
91
+ new PXDataFieldRestrict("Username", PXDbType.NVarChar, username),
92
+ new PXDataFieldRestrict("Rolename", PXDbType.NVarChar, rolename),
93
+ new PXDataFieldRestrict("ApplicationName", PXDbType.VarChar, "/"),
94
+ new PXDataFieldAssign("LastModifiedByID", PXDbType.UniqueIdentifier, userId),
95
+ new PXDataFieldAssign("LastModifiedByScreenID", PXDbType.Char, "SM201005"),
96
+ new PXDataFieldAssign("LastModifiedDateTime", PXDbType.DateTime, now),
97
+ };
98
+ bool updated = PXDatabase.Update<UsersInRoles>(update.ToArray());
99
+ if (!updated)
100
+ {
101
+ var insert = new List<PXDataFieldAssign>
102
+ {
103
+ new PXDataFieldAssign("Username", PXDbType.NVarChar, username),
104
+ new PXDataFieldAssign("Rolename", PXDbType.NVarChar, rolename),
105
+ new PXDataFieldAssign("ApplicationName", PXDbType.VarChar, "/"),
106
+ new PXDataFieldAssign("CreatedByID", PXDbType.UniqueIdentifier, userId),
107
+ new PXDataFieldAssign("CreatedByScreenID", PXDbType.Char, "SM201005"),
108
+ new PXDataFieldAssign("CreatedDateTime", PXDbType.DateTime, now),
109
+ new PXDataFieldAssign("LastModifiedByID", PXDbType.UniqueIdentifier, userId),
110
+ new PXDataFieldAssign("LastModifiedByScreenID", PXDbType.Char, "SM201005"),
111
+ new PXDataFieldAssign("LastModifiedDateTime", PXDbType.DateTime, now),
112
+ };
113
+ PXDatabase.Insert<UsersInRoles>(insert.ToArray());
114
+ }
115
+ }
116
+ }
117
+
118
+ public class AssignUserFilter : PXBqlTable, IBqlTable
119
+ {
120
+ [PXString(256, IsUnicode = true, InputMask = "")]
121
+ [PXUIField(DisplayName = "Username")]
122
+ public virtual string Username { get; set; }
123
+ public abstract class username : PX.Data.BQL.BqlString.Field<username> { }
124
+ }
125
+
126
+ // Action lives on RoleAccess so Base.Roles.Current is the SM201005 header.
127
+ public class AcuRoleAccessExt : PXGraphExtension<RoleAccess>
128
+ {
129
+ public static bool IsActive() => true;
130
+
131
+ public PXFilter<AssignUserFilter> AssignUserParams;
132
+
133
+ public PXAction<Roles> assignUser;
134
+
135
+ [PXButton(CommitChanges = true)]
136
+ [PXUIField(DisplayName = "Assign User", Visible = false)]
137
+ protected virtual IEnumerable AssignUser(PXAdapter adapter)
138
+ {
139
+ string username = AssignUserParams.Current != null
140
+ ? AssignUserParams.Current.Username : null;
141
+ Roles role = Base.Roles.Current;
142
+ string rolename = role != null ? role.Rolename : null;
143
+ AcuBootstrapPlugin.AssignUserToRole(username, rolename);
144
+ return adapter.Get();
145
+ }
146
+ }
147
+ }
@@ -8,7 +8,12 @@
8
8
  T204 bumps 1.4.0 -> 1.5.0 for SegmentedKey, gh #30 / V50;
9
9
  T207 bumps 1.5.0 -> 1.6.0 for SegmentedKey Detail view (B28);
10
10
  gh #34 bumps 1.6.0 -> 1.7.0 for Company DecPlQty + WeightUOM/VolumeUOM
11
- (CS101500 commonsetup; kit BOM milligram-scale KG needs qty precision 3).
11
+ (CS101500 commonsetup; kit BOM milligram-scale KG needs qty precision 3);
12
+ gh #35 bumps 1.7.0 -> 1.8.0 for User.Roles -> RolesByUser (UsersInRoles)
13
+ and drops Selected (AllowedRoles was EPLoginTypeAllowsRole / EP202500);
14
+ T228 bumps 1.8.0 -> 1.9.0 for Role.Users -> UsersByRole (SM201005
15
+ Users tab; mapped-detail PUT is a no-op — B33) and 1.9.0 -> 1.10.0
16
+ for Role AssignUser action (PXDatabase UsersInRoles persist).
12
17
 
13
18
  package_zip() (bootstrap.py) prefers data-repo bootstrap/project.xml when
14
19
  present; otherwise this packaged template is the fallback so config init,
@@ -48,9 +53,13 @@
48
53
  Username; identity FirstName/LastName/Email + IsApproved +
49
54
  PasswordNeverExpires + PasswordChangeOnNextLogin; Password write-only
50
55
  (extract/diff policy = V39/T147).
51
- - membership = User detail Roles -> AllowedRoles (UsersInRoles/Roles
52
- projection): Rolename + Selected (assign when true). Apply order
53
- Role then User (V22 prefixes; T148).
56
+ - User detail Roles -> RolesByUser (GET/diff shape; PUT never
57
+ writes UsersInRoles — B31/B32/B33). Persist = Role action
58
+ AssignUser -> PXDatabase UsersInRoles (ApplicationName='/').
59
+ Role.Users -> UsersByRole is the mapped-detail fixture (silent
60
+ 200). Apply order Role then User then Role.Users
61
+ (V22 prefixes 90/91/92). Never AllowedRoles
62
+ (EPLoginTypeAllowsRole / EP202500 allow-list; gh #35 / V53).
54
63
 
55
64
  T150 NumberingSequence (gh #25) - Default endpoint has no numbering
56
65
  surface; CS201010 Numbering Sequences (NumberingMaint — NOT the
@@ -112,7 +121,7 @@
112
121
  -->
113
122
  <Customization level="" description="acu bootstrap: CustomizationPlugin enables features on publish; Bootstrap endpoint exposes full company surface (company + credit terms + financial currency + GL chain + module preferences + vendor class + statement cycle + warehouse + order type + cash account + role + user + membership + numbering sequence + segmented key)" product-version="26.101">
114
123
  <EntityEndpoint>
115
- <Endpoint xmlns="http://www.acumatica.com/entity/maintenance/5.31" name="Bootstrap" version="1.7.0" systemContractVersion="4">
124
+ <Endpoint xmlns="http://www.acumatica.com/entity/maintenance/5.31" name="Bootstrap" version="1.10.0" systemContractVersion="4">
116
125
  <TopLevelEntity name="Company" screen="CS101500">
117
126
  <Fields>
118
127
  <Field name="AcctCD" type="StringValue" />
@@ -1019,6 +1028,7 @@
1019
1028
  <Fields>
1020
1029
  <Field name="Rolename" type="StringValue" />
1021
1030
  <Field name="Descr" type="StringValue" />
1031
+ <Field name="Users" type="RoleUser[]" />
1022
1032
  </Fields>
1023
1033
  <Mappings>
1024
1034
  <Mapping field="Rolename">
@@ -1027,8 +1037,33 @@
1027
1037
  <Mapping field="Descr">
1028
1038
  <To object="Roles" field="Descr" />
1029
1039
  </Mapping>
1040
+ <Mapping field="Users">
1041
+ <To object="UsersByRole" field="" />
1042
+ </Mapping>
1030
1043
  </Mappings>
1044
+ <Actions>
1045
+ <Action name="AssignUser" mappedTo="assignUser" isDefault="false">
1046
+ <Fields>
1047
+ <Field name="Username" type="StringValue" />
1048
+ </Fields>
1049
+ <Mappings>
1050
+ <Mapping field="Username">
1051
+ <To object="AssignUserParams" field="Username" />
1052
+ </Mapping>
1053
+ </Mappings>
1054
+ </Action>
1055
+ </Actions>
1031
1056
  </TopLevelEntity>
1057
+ <Detail name="RoleUser">
1058
+ <Fields>
1059
+ <Field name="Username" type="StringValue" />
1060
+ </Fields>
1061
+ <Mappings>
1062
+ <Mapping field="Username">
1063
+ <To object="UsersByRole" field="Username" />
1064
+ </Mapping>
1065
+ </Mappings>
1066
+ </Detail>
1032
1067
  <TopLevelEntity name="User" screen="SM201010">
1033
1068
  <Fields>
1034
1069
  <Field name="Username" type="StringValue" />
@@ -1067,21 +1102,17 @@
1067
1102
  <To object="UserList" field="Password" />
1068
1103
  </Mapping>
1069
1104
  <Mapping field="Roles">
1070
- <To object="AllowedRoles" field="" />
1105
+ <To object="RolesByUser" field="" />
1071
1106
  </Mapping>
1072
1107
  </Mappings>
1073
1108
  </TopLevelEntity>
1074
1109
  <Detail name="UserRole">
1075
1110
  <Fields>
1076
1111
  <Field name="Rolename" type="StringValue" />
1077
- <Field name="Selected" type="BooleanValue" />
1078
1112
  </Fields>
1079
1113
  <Mappings>
1080
1114
  <Mapping field="Rolename">
1081
- <To object="AllowedRoles" field="Rolename" />
1082
- </Mapping>
1083
- <Mapping field="Selected">
1084
- <To object="AllowedRoles" field="Selected" />
1115
+ <To object="RolesByUser" field="Rolename" />
1085
1116
  </Mapping>
1086
1117
  </Mappings>
1087
1118
  </Detail>
@@ -70,6 +70,10 @@ INIT_TEMPLATES = (
70
70
  "config/baseline/60-ledger-company.yaml",
71
71
  ),
72
72
  ("config/baseline/90-uoms.yaml", "config/baseline/90-uoms.yaml"),
73
+ (
74
+ "config/baseline/91-company-packaging.yaml",
75
+ "config/baseline/91-company-packaging.yaml",
76
+ ),
73
77
  ("config/setup/10-financial-year.yaml", "config/setup/10-financial-year.yaml"),
74
78
  ("config/setup/20-master-calendar.yaml", "config/setup/20-master-calendar.yaml"),
75
79
  ("config/setup/30-open-periods.yaml", "config/setup/30-open-periods.yaml"),
@@ -128,6 +132,7 @@ INIT_TEMPLATES = (
128
132
  ),
129
133
  ("config/master/90-roles.yaml", "config/master/90-roles.yaml"),
130
134
  ("config/master/91-users.yaml", "config/master/91-users.yaml"),
135
+ ("config/master/92-role-users.yaml", "config/master/92-role-users.yaml"),
131
136
  (
132
137
  "config/views/10-trial-balance.yaml",
133
138
  "config/views/10-trial-balance.yaml",
@@ -590,6 +590,13 @@ class _Extraction:
590
590
  if not live:
591
591
  self._skip(target, "no records")
592
592
  continue
593
+ # V53: Role.Users GET may stay empty; do not emit identity-only
594
+ # 92-role-users.yaml that would drop authored AssignUser rows.
595
+ if spec.include == ["Users"] and not any(
596
+ isinstance(r.get("Users"), list) and r["Users"] for r in records
597
+ ):
598
+ self._skip(target, "Users GET empty")
599
+ continue
593
600
  self._emit(target, _render(spec, records), len(records))
594
601
  produced.add(spec.file)
595
602
  return produced
@@ -397,6 +397,10 @@ def apply(
397
397
  records in the same file. Returns ``(ok_count, error_messages)`` —
398
398
  never silent partial; caller exits 1 when any errors remain. Exit 2
399
399
  stays drift (``diff`` only).
400
+
401
+ Role.Users mapped-detail PUT is a no-op (V53/B33). After the Role PUT,
402
+ apply invokes contract action ``AssignUser`` per Username so persist
403
+ is PXDatabase UsersInRoles, not the mapped detail.
400
404
  """
401
405
  if isinstance(baseline, ActionFile):
402
406
  return _apply_action(client, baseline, dry_run)
@@ -406,14 +410,23 @@ def apply(
406
410
  label = ", ".join(str(record[k]) for k in baseline.keys)
407
411
  if dry_run:
408
412
  output.data(f" would PUT {baseline.entity} [{label}]")
409
- ok += 1
413
+ try:
414
+ _assign_role_users(client, baseline, record, dry_run=True)
415
+ ok += 1
416
+ except RuntimeError as err:
417
+ msg = f"{baseline.entity} [{label}]: {err}"
418
+ output.error(msg)
419
+ errors.append(msg)
410
420
  continue
411
421
  try:
412
422
  body = _put_body(client, baseline, record)
423
+ if baseline.entity == "Role":
424
+ body.pop("Users", None)
413
425
  _put(client, baseline.entity, body, baseline.endpoint)
414
426
  if baseline.entity == "Company":
415
427
  client.refresh_after_company()
416
428
  output.data(f" PUT {baseline.entity} [{label}]")
429
+ _assign_role_users(client, baseline, record, dry_run=False)
417
430
  ok += 1
418
431
  except RuntimeError as err:
419
432
  msg = f"{baseline.entity} [{label}]: {err}"
@@ -422,6 +435,51 @@ def apply(
422
435
  return ok, errors
423
436
 
424
437
 
438
+ def _assign_role_users(
439
+ client: AcumaticaClient,
440
+ baseline: BaselineFile,
441
+ record: dict[str, Any],
442
+ *,
443
+ dry_run: bool,
444
+ ) -> None:
445
+ """V53/B33: persist Role.Users via AssignUser, not mapped-detail PUT.
446
+
447
+ Additive-only: ``delete: true`` merge rows are skipped. Mapped GET of
448
+ Users may be empty, so extra live members are not a persist delete.
449
+ Role PUT strips ``Users`` so additive-only does not depend on B33.
450
+ """
451
+ if baseline.entity != "Role":
452
+ return
453
+ users = record.get("Users")
454
+ if not isinstance(users, list):
455
+ return
456
+ rolename = record.get("Rolename")
457
+ if not isinstance(rolename, str) or not rolename:
458
+ return
459
+ to_assign: list[str] = []
460
+ for i, row in enumerate(users):
461
+ if isinstance(row, dict) and row.get("delete") is True:
462
+ continue
463
+ if not isinstance(row, dict):
464
+ raise RuntimeError(f"Users[{i}] is not a mapping")
465
+ username = row.get("Username")
466
+ if not isinstance(username, str) or not username:
467
+ raise RuntimeError(f"Users[{i}] missing Username")
468
+ to_assign.append(username)
469
+ for username in to_assign:
470
+ if dry_run:
471
+ output.data(f" would invoke AssignUser [{rolename}/{username}]")
472
+ continue
473
+ client.invoke(
474
+ "Role",
475
+ "AssignUser",
476
+ {"Rolename": rolename},
477
+ {"Username": username},
478
+ baseline.endpoint,
479
+ )
480
+ output.data(f" invoke AssignUser [{rolename}/{username}]")
481
+
482
+
425
483
  def _put(
426
484
  client: AcumaticaClient,
427
485
  entity: str,
@@ -6,7 +6,7 @@
6
6
  # entity contract entity name (the GET source and the emitted `entity:`)
7
7
  # endpoint omitted = the instance Default endpoint; the symbolic name
8
8
  # `bootstrap` stays symbolic in emitted files and resolves to
9
- # the packaged full company Bootstrap/1.7.0 at HTTP/load time
9
+ # the packaged full company Bootstrap/1.10.0 at HTTP/load time
10
10
  # (package SoT - V20/V21/T178; data-repo project.xml forbidden)
11
11
  # keys key fields: the emitted `key:`, the record sort order, and
12
12
  # the B9 fallback's $select projection; the tuple must
@@ -44,10 +44,9 @@ entities:
44
44
 
45
45
  # config/bootstrap/ - the virgin-tenant phase via the custom Bootstrap
46
46
  # endpoint: built-in endpoints cannot create the first organization (C).
47
- # Company packaging: DecPlQty + WeightUOM/VolumeUOM on CS101500 commonsetup
48
- # (gh #34). WeightUOM/VolumeUOM required to persist CommonSetup when
49
- # DistributionModule is on. B26 dropped unmapped packaging claims; mapped
50
- # fields that GET returns stay in include.
47
+ # Company identity first (bootstrap). Packaging qty/UOMs are a second
48
+ # Company PUT after 90-uoms.yaml (see that row). StockItem WeightUOM
49
+ # /VolumeUOM stay GET-omit (B26).
51
50
 
52
51
  - entity: Company
53
52
  endpoint: bootstrap
@@ -58,9 +57,6 @@ entities:
58
57
  - OrganizationType
59
58
  - BaseCuryID
60
59
  - CountryID
61
- - DecPlQty
62
- - WeightUOM
63
- - VolumeUOM
64
60
 
65
61
  - entity: CreditTerms
66
62
  endpoint: bootstrap
@@ -167,6 +163,20 @@ entities:
167
163
  - Description
168
164
  - L3Code
169
165
 
166
+ # Company packaging (gh #34 / V52): second PUT after UOMs so KG/LITER
167
+ # exist for CommonSetup_RowPersisting when DistributionModule is on.
168
+ # Include-partition of the same live Company row (warehouse-phase idiom).
169
+ # Mapped fields GET-return stay in include.
170
+
171
+ - entity: Company
172
+ endpoint: bootstrap
173
+ keys: [AcctCD]
174
+ file: config/baseline/91-company-packaging.yaml
175
+ include:
176
+ - DecPlQty
177
+ - WeightUOM
178
+ - VolumeUOM
179
+
170
180
  # config/master/ - full packaged seed (T117). Multi-file Warehouse uses
171
181
  # include field-partition (same live warehouse row, three apply phases).
172
182
  # Multi-file StockItem uses OData filter-split on ItemClass.
@@ -522,10 +532,12 @@ entities:
522
532
  - Active
523
533
  - StockComponents
524
534
 
525
- # config/master/ Role then User (V22 prefixes; T145/T146/T148). Bootstrap
526
- # only — Default has no Role/User surface. Membership = User detail Roles
527
- # (UserRole/AllowedRoles). Package 90-roles = full pre-build CompanyID=1
528
- # Roles dump (system + module roles; live 26.x); 91-users demo soadmin.
535
+ # config/master/ Role then User then Role.Users (V22 prefixes 90/91/92;
536
+ # T145/T146/T148/T228). Bootstrap only — Default has no Role/User surface.
537
+ # Persist = Role action AssignUser (PXDatabase UsersInRoles; T228/B33).
538
+ # User detail Roles is GET/diff shape (PUT never writes UsersInRoles —
539
+ # B31/B32/B33). Package 90-roles = full pre-build CompanyID=1
540
+ # Roles dump; 91-users demo soadmin identity; 92-role-users membership.
529
541
  # Password is write-only on apply (V39/T147): include omits Password so
530
542
  # extract never emits hashes (b64__Password also out of include).
531
543
 
@@ -551,6 +563,19 @@ entities:
551
563
  - PasswordChangeOnNextLogin
552
564
  - Roles
553
565
 
566
+ # Persist membership (T228/V53): apply Role.Users invokes AssignUser.
567
+ # Filter-split from 90-roles (Descr-only identity dump). Mapped-detail
568
+ # PUT never writes UsersInRoles (B31/B32/B33).
569
+ - entity: Role
570
+ endpoint: bootstrap
571
+ keys: [Rolename]
572
+ file: config/master/92-role-users.yaml
573
+ filter: Rolename eq 'SO Admin'
574
+ detail_keys:
575
+ Users: Username
576
+ include:
577
+ - Users
578
+
554
579
  # config/setup/ - the GL action chain (T36/T37): financial-year setup,
555
580
  # then calendar generation, then period activation; numbered prefixes
556
581
  # mirror the verified apply order.
@@ -48,7 +48,7 @@ Bare `acu apply` / `acu diff` also prefer `config/` when those trees exist.
48
48
  | `config/bootstrap/` | Company, features, credit terms (Bootstrap contract is package SoT — never scaffolded) |
49
49
  | `config/baseline/` | GL foundation (COA, ledger, subaccounts, UOMs) |
50
50
  | `config/setup/` | Financial year, master calendar, open periods |
51
- | `config/master/` | Numbering (`05-numbering-sequences`) before module prefs; inventory, warehouse, items, vendors, customers; roles/users (`90-roles` then `91-users`) |
51
+ | `config/master/` | Numbering (`05-numbering-sequences`) before module prefs; inventory, warehouse, items, vendors, customers; roles/users (`90-roles` then `91-users` then `92-role-users`) |
52
52
  | `scenario/10-seed-capital.yaml` | Once-class owner capital JE (skip-if-present when present); Period = `${current_period}` |
53
53
  | `scenario/20-buy.yaml` | Additive component PO → receipt → bill → AP pay |
54
54
  | `scenario/30-build.yaml` | Additive kit assembly |
@@ -0,0 +1,8 @@
1
+ entity: Company
2
+ key: AcctCD
3
+ endpoint: bootstrap
4
+ records:
5
+ - AcctCD: LAB5
6
+ DecPlQty: 3
7
+ WeightUOM: KG
8
+ VolumeUOM: LITER
@@ -13,4 +13,3 @@ records:
13
13
  PasswordChangeOnNextLogin: false
14
14
  Roles:
15
15
  - Rolename: SO Admin
16
- Selected: true
@@ -0,0 +1,9 @@
1
+ entity: Role
2
+ key: Rolename
3
+ endpoint: bootstrap
4
+ detail_keys:
5
+ Users: Username
6
+ records:
7
+ - Rolename: SO Admin
8
+ Users:
9
+ - Username: soadmin
@@ -1,74 +0,0 @@
1
- // acu bootstrap CustomizationPlugin (SPEC T11).
2
- //
3
- // Runs in-process on customization publish - the one write path to
4
- // FeaturesSet that works: the contract API cannot persist features no
5
- // matter what endpoint fronts CS100000 (T3 verdict, docs/rest-api.md).
6
- //
7
- // Deliberately writes through PXDatabase, not FeaturesMaint: the graph
8
- // save collides with the publish pipeline's concurrent plugin invocations
9
- // ("Another process has added the 'FeaturesSet' record" - observed live,
10
- // 26.101.0225) and nothing persists. A keyed row write is deterministic
11
- // and idempotent: update the existing row, insert when absent. All 205
12
- // NOT NULL bit columns are filled reflectively (only ~136 carry DB
13
- // defaults); Status = 0 means Validated (PXIntList, verified vs live).
14
- //
15
- // The Enabled set is NOT authored here (SPEC V2: feature flags are config
16
- // "what", never tool source - B6): package_zip() substitutes the
17
- // ACU_FEATURES sentinel below with the data repo's bootstrap/features.yaml
18
- // list (built-in six when the file is absent) at package-build time.
19
- using System.Collections.Generic;
20
- using System.Reflection;
21
- using PX.Data;
22
- using PX.Objects.CS;
23
-
24
- namespace AcuBootstrap
25
- {
26
- public class AcuBootstrapPlugin : Customization.CustomizationPlugin
27
- {
28
- private static readonly HashSet<string> Enabled = new HashSet<string>
29
- {
30
- /*ACU_FEATURES*/
31
- };
32
-
33
- public override void UpdateDatabase()
34
- {
35
- var flags = new List<PXDataFieldAssign>();
36
- var known = new HashSet<string>();
37
- foreach (PropertyInfo prop in typeof(FeaturesSet).GetProperties())
38
- {
39
- if (prop.PropertyType == typeof(bool?))
40
- {
41
- known.Add(prop.Name);
42
- flags.Add(new PXDataFieldAssign(
43
- prop.Name, PXDbType.Bit, Enabled.Contains(prop.Name)));
44
- }
45
- }
46
- foreach (string name in Enabled)
47
- {
48
- if (!known.Contains(name))
49
- {
50
- // silent-typo guard (T24): a misspelled features.yaml
51
- // entry enables nothing - say so in the publish log
52
- WriteLog("AcuBootstrap: unknown feature name '" + name
53
- + "' - no FeaturesSet property, nothing enabled");
54
- }
55
- }
56
- flags.Add(new PXDataFieldAssign("Status", PXDbType.Int, 0)); // Validated
57
-
58
- using (var tx = new PXTransactionScope())
59
- {
60
- bool updated = PXDatabase.Update<FeaturesSet>(flags.ToArray());
61
- if (!updated)
62
- {
63
- PXDatabase.Insert<FeaturesSet>(flags.ToArray());
64
- WriteLog("AcuBootstrap: FeaturesSet row inserted (features enabled)");
65
- }
66
- else
67
- {
68
- WriteLog("AcuBootstrap: FeaturesSet row updated (features enabled)");
69
- }
70
- tx.Complete();
71
- }
72
- }
73
- }
74
- }