# 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) |