Initial Commit of the PDM project (ready for DWS migration)

This commit is contained in:
will
2026-04-20 08:42:38 -05:00
commit dda7b664e7
2721 changed files with 442772 additions and 0 deletions

202
BATCH_NOTES.md Normal file
View File

@@ -0,0 +1,202 @@
# Batch Workflow Transition — Implementation Notes
## The Problem
`IEdmFile5::ChangeState(toStateID, folderID, comment, flags)` only accepts a **destination state ID**.
When a vault has multiple transitions leading from State A → State B, the API picks one arbitrarily.
In this project, there were 3 transitions from "Under Editing" → "Approved" (IDs 8, 83, 268).
The API consistently selected ID 8 or 83, both of which had folder-location conditions that failed
silently — the call returned no error but the file never moved states.
---
## The Solution: `IEdmFile13::ChangeState3`
The PDM API provides a newer method that accepts **both** a destination state ID and a **specific transition ID**:
```
void ChangeState3(
ref object poStateIdOrName, // destination state ID or name
ref object poTransitionIdOrName, // specific transition ID or name ← key parameter
int lFolderID,
string bsComment,
int lParentWnd,
int lEdmStateFlags,
string bsPasswd
)
```
By passing the specific transition ID (268, the admin "AA" transition with no conditions),
the ambiguous transitions are bypassed entirely.
---
## Why ChangeState3 Was Difficult to Call
`ChangeState3` is marked `[restricted]` in the PDM type library. This means:
- It **has** a dispatch ID (DISPID 48) in the type library.
- The COM object's `IDispatch::Invoke` rejects it with **"Member not found"** regardless of how
it is called through normal IDispatch.
- It can **only** be called via the COM vtable directly — not through Python's win32com/IDispatch path.
### Why `[restricted]` Was Used
The `[restricted]` flag in COM/IDL signals: *"this method is for vtable callers (C++/.NET), not
scripting clients (VBA, VBScript, Python via IDispatch)."*
The primary reason is the parameter types. `ChangeState3` takes `VARIANT*` (by-reference variants),
which are awkward to marshal cleanly through `IDispatch::Invoke`. Rather than risk broken behavior
from scripting clients passing wrong types, the method was restricted to vtable-only access.
### Who the API Was Designed For
SolidWorks PDM's primary API consumers are C++ and .NET:
- In **C++**, vtable methods are called directly with no additional effort.
- In **.NET**, `ref object` parameters map naturally to `VARIANT*` and the interop layer handles
everything transparently.
- **Python via win32com** was never a first-class target. The `[restricted]` flag is invisible to
C++/.NET developers and so was likely never considered a problem.
### Why There Is No Simpler Alternative
`ChangeState` (original, no number) came first and was widely used, so it was kept for
compatibility. `ChangeState3` was added as the correct replacement for cases involving multiple
transitions to the same state, but was never made scripting-accessible. This is a common rough
edge with older Windows COM APIs designed primarily for C++.
---
## How We Called It From Python
### Step 1 — Find the vtable offset
Inspected the win32com gen_py stub file generated by `EnsureDispatch("ConisioLib.EdmVault")`:
```
C:\Users\youngwa\AppData\Local\Temp\gen_py\3.12\
5FA2C692-8393-4F31-9BDB-05E6F807D0D3x0x5x27\IEdmFile13.py
```
That file contains `IEdmFile13_vtables_` which lists:
```python
IEdmFile13_vtables_ = [
(('ChangeState3', ...), 48, (..., 432, ...), )),
]
```
- **DISPID**: 48
- **oVft (vtable byte offset)**: 432 → slot 54
- IUnknown: slots 02 (3 methods)
- IDispatch: slots 36 (4 methods)
- Base interface methods (IEdmObject5 … IEdmFile12): slots 753 (47 methods)
- **ChangeState3**: slot 54
### Step 2 — Find the IEdmFile13 interface IID
Also from `IEdmFile13.py`:
```python
class IEdmFile13(DispatchBaseClass):
CLSID = IID('{DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}')
```
**IEdmFile13 IID: `{DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}`**
### Step 3 — Define the interface in comtypes
Built a `comtypes` class inheriting from `IDispatch`, with 47 placeholder methods to occupy
slots 753, then `ChangeState3` at slot 54:
```python
class _IEdmFile13_CT(CT_IDispatch):
_iid_ = GUID("{DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}")
_methods_ = [COMMETHOD([], HRESULT, f"_ph{i}") for i in range(47)] + [
COMMETHOD(
[], HRESULT, "ChangeState3",
(["in"], POINTER(VARIANT), "poStateIdOrName"),
(["in"], POINTER(VARIANT), "poTransitionIdOrName"),
(["in"], c_long, "lFolderID"),
(["in"], c_wchar_p, "bsComment"),
(["in"], c_long, "lParentWnd"),
(["in"], c_long, "lEdmStateFlags"),
(["in"], c_wchar_p, "bsPasswd"),
),
]
```
### Step 4 — Extract the raw COM pointer
Used `ctypes` to read the raw `IEdmFile13*` out of the pythoncom wrapper's memory.
CPython 64-bit stores the COM pointer at offset 16 in the Python object struct:
```python
py_disp = file_obj._oleobj_.QueryInterface(pythoncom.MakeIID(IID_IEdmFile13))
raw_ptr = ctypes.c_uint64.from_address(id(py_disp) + 16).value
ct_unk = ctypes.cast(raw_ptr, ctypes.POINTER(comtypes.IUnknown))
file13 = ct_unk.QueryInterface(_IEdmFile13_CT) # properly AddRef'd
```
### Step 5 — Call ChangeState3
Constructed `VARIANT` structs (VT_I4) for the state and transition IDs, then called through
the vtable:
```python
v_state = _make_i4_variant(to_state_id)
v_trans = _make_i4_variant(transition_id)
file13.ChangeState3(
ctypes.byref(v_state),
ctypes.byref(v_trans),
ctypes.c_long(folder_id),
comment,
ctypes.c_long(0),
ctypes.c_long(0),
password, # PDM login password — required by this transition
)
```
HRESULT `0x00000000` = success.
---
## How Transition IDs Are Resolved in the Batch Script
No IDs are hardcoded. The script resolves the transition by **name** at runtime:
```python
# In transition_file() — batch_workflows_paths.py
while not trans_pos.IsNull:
transition = current_state.GetNextTransition(trans_pos)
if transition.Name.lower() == transition_name.lower():
target_transition = transition # .ID and .ToState.ID are both available
break
```
The user passes `--transition "AA"` (or whatever the transition name is), and the script passes
`target_transition.ID` directly to `ChangeState3`. This means the same script works for any
transition name, including ones where multiple transitions lead to the same destination state.
---
## Key Files
| File | Purpose |
|------|---------|
| `helpers/batch_workflows_paths.py` | Production batch script — run this |
| `helpers/test_batch_api.py` | Diagnostic/prototype used during development |
| `documentation/API_GB.chm` | PDM API reference (local copy) |
## PDM Type Library Reference
| Item | Value |
|------|-------|
| TypeLib GUID | `{5FA2C692-8393-4F31-9BDB-05E6F807D0D3}` |
| TypeLib version | 5.27 |
| TypeLib name | PDMWorks Enterprise 2024 Type Library |
| IEdmFile13 IID | `{DB0646C9-9E3F-4EA2-93AA-EB6584D268E2}` |
| ChangeState3 DISPID | 48 (restricted — not callable via IDispatch) |
| ChangeState3 vtable offset | 432 bytes (slot 54) |