Files
pdm/BATCH_NOTES.md

203 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) |