203 lines
7.0 KiB
Markdown
203 lines
7.0 KiB
Markdown
# 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 0–2 (3 methods)
|
||
- IDispatch: slots 3–6 (4 methods)
|
||
- Base interface methods (IEdmObject5 … IEdmFile12): slots 7–53 (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 7–53, 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) |
|