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