Initial Commit of the PDM project (ready for DWS migration)
This commit is contained in:
202
BATCH_NOTES.md
Normal file
202
BATCH_NOTES.md
Normal 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 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) |
|
||||
Reference in New Issue
Block a user