1
0

Step 7: register-to-BPMN transcoder tool

Adds tools/register-transcoder — a Python tool that reads a published Valsts
Kase accounting-process register (.xlsx/.xlsm) and emits BPMN process
skeletons. For a given sub-process it produces one userTask per register
step, swimlanes from the RACI columns (placing each step in its Responsible
actor's lane), sequence flows reconstructed from the register's own
predecessor/successor step references, and synthesised start/end events per
entry and exit step. Output is an isExecutable=false skeleton — the
deterministic first pass of the transcription pipeline; refinement into a
Level 4 executable package is the human/AI-assisted second pass that produced
the curated FG3-1/FG3-4/FG3-5 packages. Includes a README and sample-output
skeletons emitted from the FG3 register for sub-processes 3.5.2 and 3.5.3.
This commit is contained in:
2026-05-19 21:38:45 +00:00
parent 37000f77f5
commit a608de41ad
4 changed files with 615 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
# register-transcoder
Transcodes a published Valsts Kase accounting-process register
(`.xlsx` / `.xlsm`) into BPMN process skeletons — one deterministic step in
the `vk-gramatvediba` transcription pipeline.
The Valsts Kase / VPC *Grāmatvedības uzskaites procesu apraksts* is published
as a set of function-group spreadsheets (FG1–FG6). Each row of a register is a
process step with explicit predecessor and successor step references, a RACI
split across the responsible actors, the IT system used, an SLA, and the data
the step produces. That structure is already a process graph; this tool reads
it and emits the corresponding BPMN.
## What it produces
For a given sub-process the tool emits one `.bpmn` file containing a single
`bpmn:process` with `isExecutable="false"`:
- one `bpmn:userTask` per register step, named from the register and carrying
the step's description, system, SLA, RACI and cross-references in
`bpmn:documentation`;
- `bpmn:lane`s derived from the RACI columns — a step is placed in the lane of
its **Responsible** actor (Nodarbinātais / Iestāde / VPC);
- `bpmn:sequenceFlow`s reconstructed from the register's own
*No procesa darbības soļa* (predecessor) and *Uz procesa darbības soli*
(successor) columns, restricted to links whose endpoints are both inside the
emitted sub-process;
- synthesised `bpmn:startEvent` / `bpmn:endEvent` nodes — one per entry step
(no in-group predecessor) and one per exit step (no in-group successor) — so
the fragment's real boundary is visible rather than hidden.
## Register format expected
The parser locates the worksheet and header row by content, not by position,
so it tolerates the leading title rows the registers carry. It expects a
header row containing `Nr.p.k.` and the columns *No procesa darbības soļa*
(predecessor, with the FG-group and step-number in adjacent cells),
*Process, apakšprocess*, *Atbildības sadalījums (RACI)* (a three-column block
for Nodarbinātais / Iestāde / VPC), *Darbību apraksts*, *Izmantotā IS*,
*Izpildes termiņš*, *Sagatavotie dati* and *Uz procesa darbības soli*
(successor). Rows that carry a number and a name but no description and no
RACI are treated as sub-process headers; rows with a description or any RACI
entry are treated as steps. Steps are grouped under the most recent header.
## Usage
```
transcode.py list <register.xlsx>
transcode.py emit <register.xlsx> <subprocess> [-o <output.bpmn>]
```
`list` reports the sub-processes that contain steps, with step counts. `emit`
writes (or, without `-o`, prints) the BPMN skeleton for one sub-process.
```
python3 transcode.py list fg3_process.xlsm
python3 transcode.py emit fg3_process.xlsm 3.5.2 -o 3.5.2.skeleton.bpmn
```
The only dependency is `openpyxl`.
## Limitations — a skeleton, not an executable
The output is deliberately a faithful mechanical transcription, not a finished
package. It does **not**:
- detect decisions — every step becomes a `userTask`; branching points are not
promoted to `exclusiveGateway`s and no DMN is extracted;
- repair the register — where the register's predecessor and successor columns
disagree, the skeleton reproduces the result as-is (this can surface as a
reciprocal edge / short cycle, or as a step that reaches the rest of its
sub-process only through a cross-FG excursion);
- carry BPMN diagram interchange (`bpmndi`) — the output is a logical model,
laid out by an editor on import;
- emit a UAPF package — there is no `uapf.yaml`, no resources and no metadata.
The RACI-to-lane rule is a heuristic: the lane is the first actor whose RACI
cell contains `R`. The full RACI is preserved verbatim in each task's
documentation so the heuristic can be checked and corrected.
## Position in the pipeline
A skeleton is the deterministic first pass. Refining one into a Level 4
executable — introducing explicit gateways, extracting decision logic into
DMN, writing resource roles/agents/mappings and the package manifest — is the
human / AI-assisted second pass. The curated `processes/fg3-1`, `fg3-4` and
`fg3-5` packages are what that second pass yields; `docs/methodology.md`
discusses the transcoder skeleton against the curated executable for the same
sub-process.
`sample-output/` holds skeletons emitted from the FG3 register for
sub-processes 3.5.2 (*Saimnieciskie norēķini*) and 3.5.3 (*Komandējuma
norēķini*) — the two that have curated executable counterparts in this
workspace.

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" id="Defs_3_5_2" targetNamespace="https://uapf.dev/vk-gramatvediba/transcoded">
<bpmn:process id="Process_3_5_2" name="Saimnieciskie norēķini un to kustība" isExecutable="false">
<bpmn:laneSet id="LaneSet_3_5_2">
<bpmn:lane id="Lane_Nodarbinatais" name="Nodarbinātais">
<bpmn:flowNodeRef>Task_3_5_2_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Task_3_5_2_2</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Start_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>End_1</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="Lane_VPC" name="VPC (Vienotais pakalpojumu centrs)">
<bpmn:flowNodeRef>Task_3_5_2_3</bpmn:flowNodeRef>
</bpmn:lane>
</bpmn:laneSet>
<bpmn:startEvent id="Start_1" name="Ieeja: 3.5.2.1.">
<bpmn:outgoing>Flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Task_3_5_2_1" name="Avansa pieprasījums saimnieciskajiem izdevumiem (t.sk. uz Iestādei piesaistīto norēķinu karti)">
<bpmn:documentation>Nr.p.k.: 3.5.2.1. | RACI: Nodarbinātais=R; Iestāde=A; VPC=I
Iestādes Nodarbinātais vai Iestāde Nodarbinātā vārdā, ja tas noteikts Iestādes iekšējos noteikumos, iesniedz avansa pieprasījuma pieteikumu Pašapkalpošanās portālā (HoP lietotnē Brīvās formas pieteikumi), norādot pamatojumu un vēlamo summu (finansējumu, ekk).
Ja Nodarbinātajam saimniecisko izdevumu vajadzībām ir piešķirta Iestādei piesaistītā norēķinu karte, izmaksas tiek veiktas uz Iestādei piesaistīto norēķinu karti, pārējos gadījumos - uz Nodarbinātā algas kontu. Ja nodarbinātajam saimniecisko izdevumu vajadzībām ir piešķirts iestādes Mobilly konts. Saskaņo/apstiprina atbilstoši Iestādes iepriekš definētai plūsmai.
Sistēma: Pašapkalpošanās portāls | Izpildes termiņš: pēc nepieciešamības vai Iestādes noteiktā kārtībā | Sagatavotie dati: Avansa pieteikums
Ārējais pēctecis: FG2/2.3.2</bpmn:documentation>
<bpmn:incoming>Flow_1</bpmn:incoming>
<bpmn:outgoing>Flow_4</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="Task_3_5_2_2" name="Izdevumu apliecinošo dokumentu vai avansa norēķina iesniegšana (t.sk. arī par darījumiem ar norēķinu kartēm)">
<bpmn:documentation>Nr.p.k.: 3.5.2.2. | RACI: Nodarbinātais=R; Iestāde=A
Iestādes Nodarbinātais Pašapkalpošanās portālā (HoP lietotnē Mani izdevumi) iesniedz pieteikumu par saimnieciskajiem izdevumiem un medicīniskajiem izdevumiem, tai skaitā obligāto veselības pārbaudi (OVP), pievienojot apliecinošos dokumentus (čekus, kvītis). Nodarbinātais maksājumu apliecinošu dokumentu glabā ne īsāk par pieciem gadiem (Grāmatvedības likums 28.p(5)).
Pieteikuma saskaņošana atbilstoši Iestādes definētajai saskaņošanas plūsmai (norādot Iestādei nepieciešamās dimensijas, finansējumu). Ja iesniegtajā avansa norēķinā ir ietverta ilgtermiņa nefinanšu aktīva vai krājuma iegāde, Iestāde vienlaikus norāda noliktavu, uz kuru attiecīgais aktīvs jāreģistrē.
Izdevumu atlīdzināšana paredzēta uz Nodarbinātā algas kontu, izņemot ārvalstīs Nodarbinātos, kur sākotnēji paredzēts pieteikuma veidā iesniegt informāciju par bankas kontu un turpmāk pastāvīgi to piemērot.
Sistēma: Pašapkalpošanās portāls | Izpildes termiņš: avansa norēķinu personas par tām tieši izmaksātiem finanšu līdzekļiem - ne vēlāk kā 17 kd laikā pēc mēneša beigām, kurā attaisnojuma dokuments (attiecīgais čeks, kas pievienots pie avansa norēķina) ir izsniegts. Avansa norēķinu personas par tām netieši izmaksātiem finanšu līdzekļiem (saņemot līdzekļus Iestādei piesaistītajā maksājumu kartē) - katra nākamā mēneša sākumā līdz 10.datumam par iepriekšējā mēnesī veiktajiem norēķiniem, izņemot par kārtējā gada decembra darījumiem - līdz 27. decembrim | Sagatavotie dati: Avansa norēķins
Ārējais priekštecis: FG2/2.3.4</bpmn:documentation>
<bpmn:incoming>Flow_3</bpmn:incoming>
<bpmn:outgoing>Flow_2</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="Task_3_5_2_3" name="Avansa norēķina apstrāde">
<bpmn:documentation>Nr.p.k.: 3.5.2.3. | RACI: VPC=R
VPC saskaņā ar Horizon projektējumā norādīto ģenerē attiecīgo dokumentu. Par saimnieciskajiem avansiem, kas veikti uz Iestādei piesaistīto norēķinu karti - avansa atlikums tiek saglabāts (izņemot decembra norēķina periodu, kas nepāriet uz nākamā gada janvāri) ar mērķi Iestādei avansa atlikumu izlietot nākamajā norēķina periodā.
Sistēma: RVS Horizon | Izpildes termiņš: *3 dd laikā no avansa norēķina apstiprināšanas, kas apstiprināts līdz darba dienas plkst.15.00 | Sagatavotie dati: Izdevumu/kreditoru dokuments
Ārējais pēctecis: FG2/2.3.2, FG3/3.5.1.5, FG3/3.5.4.1, FG6/6.2.1.1, FG6/6.2.1.2, FG6/6.2.1.3</bpmn:documentation>
<bpmn:incoming>Flow_2</bpmn:incoming>
<bpmn:outgoing>Flow_3</bpmn:outgoing>
</bpmn:userTask>
<bpmn:endEvent id="End_1" name="Izeja: 3.5.2.1.">
<bpmn:incoming>Flow_4</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1" sourceRef="Start_1" targetRef="Task_3_5_2_1"/>
<bpmn:sequenceFlow id="Flow_2" sourceRef="Task_3_5_2_2" targetRef="Task_3_5_2_3"/>
<bpmn:sequenceFlow id="Flow_3" sourceRef="Task_3_5_2_3" targetRef="Task_3_5_2_2"/>
<bpmn:sequenceFlow id="Flow_4" sourceRef="Task_3_5_2_1" targetRef="End_1"/>
</bpmn:process>
</bpmn:definitions>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" id="Defs_3_5_3" targetNamespace="https://uapf.dev/vk-gramatvediba/transcoded">
<bpmn:process id="Process_3_5_3" name="Komandējuma (darba brauciena) dokumenti un to kustība" isExecutable="false">
<bpmn:laneSet id="LaneSet_3_5_3">
<bpmn:lane id="Lane_VPC" name="VPC (Vienotais pakalpojumu centrs)">
<bpmn:flowNodeRef>Task_3_5_3_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Task_3_5_3_2</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Task_3_5_3_4</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Start_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>End_1</bpmn:flowNodeRef>
<bpmn:flowNodeRef>End_2</bpmn:flowNodeRef>
</bpmn:lane>
<bpmn:lane id="Lane_Nodarbinatais" name="Nodarbinātais">
<bpmn:flowNodeRef>Task_3_5_3_3</bpmn:flowNodeRef>
<bpmn:flowNodeRef>Start_2</bpmn:flowNodeRef>
</bpmn:lane>
</bpmn:laneSet>
<bpmn:startEvent id="Start_1" name="Ieeja: 3.5.3.2.">
<bpmn:outgoing>Flow_1</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:startEvent id="Start_2" name="Ieeja: 3.5.3.3.">
<bpmn:outgoing>Flow_2</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:userTask id="Task_3_5_3_1" name="Komandējuma (darba brauciena) vai tā izmaiņu pieteikuma apstrāde">
<bpmn:documentation>Nr.p.k.: 3.5.3.1. | RACI: VPC=R
VPC, saņemot pieteikumu par komandējumu vai tā izmaiņām, veic pieteikuma apstrādi komandējuma dokumentos saskaņā ar Horizon projektējumā norādīto.
Ja Nodarbinātajam komandējuma vajadzībām ir piešķirta Iestādei piesaistītā norēķinu karte, tad izmaksas tiek veiktas uz Iestādei piesaistīto norēķinu karti, pārējos gadījumos - uz darbinieka algas kontu.
Sistēma: RVS Horizon | Izpildes termiņš: ne vēlāk kā 2 dd pirms attiecīgā komandējuma iestāšanās brīža par dienas naudu 3 dd laikā no apstiprinātas pieteikuma saņemšanas, ja avanss pieprasīts citiem komandējuma izdevumiem
Ārējais priekštecis: PP/5.1.2, PP/5.1.3.3
Ārējais pēctecis: FG2/2.3.2</bpmn:documentation>
<bpmn:incoming>Flow_4</bpmn:incoming>
<bpmn:outgoing>Flow_5</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="Task_3_5_3_2" name="Komandējuma (darba brauciena) pieteikuma anulēšana">
<bpmn:documentation>Nr.p.k.: 3.5.3.2. | RACI: VPC=R
VPC saskaņā ar Horizon projektējumā norādīto, saņemot informāciju par komandējuma atcelšanu, anulē pieteikumu un pārbauda veiktās izmaksas. Izmaksu gadījumā tālāk rīkojas atbilstoši komandējuma pieteikumā norādītajam.
Sistēma: RVS Horizon | Izpildes termiņš: 3 dd laikā no informācijas saņemšanas | Sagatavotie dati: Pieteikums Pašapkalpošanās portālā
Ārējais priekštecis: PP/5.1.3.3
Ārējais pēctecis: FG3/3.5.4.1, FG4/4.3.1</bpmn:documentation>
<bpmn:incoming>Flow_1</bpmn:incoming>
<bpmn:outgoing>Flow_6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="Task_3_5_3_3" name="Komandējuma (darba brauciena) izdevumu atskaites iesniegšana">
<bpmn:documentation>Nr.p.k.: 3.5.3.3. | RACI: Nodarbinātais=R; Iestāde=R; VPC=I
Iestādes Nodarbinātais vai Iestāde (tajā gadījumā, ja komandējumā nosūtīta persona, kas nav Iestādes Nodarbinātais) iesniedz komandējuma atskaiti Pašapkalpošanās portālā (HoP lietotnē Komandējumi), audzēkņiem izmanto izdevuma veidu "Mācību mobilitātes projektu izmaksas". Atskaite jāiesniedz arī gadījumos, ja nav radušies papildus izdevumi.Ja tika pieteikts grupas komandējums atskaite ir jāiesniedz par katru komandēto personu.
Atkārtoti dimensijas pieteikumā nenorāda, jo piesakot komandējumu pie "Plānotiem izdevumiem" kā obligāta ir jānorāda dimensija "Finasējums", savukārt pārējos dimensiju laukus sagatavo, ja atbilstoši konkrētās iestādes uzskaites procesiem.
Saskaņošana notiek Iestādes definētā plūsmā.
Atskaite jāiesniedz arī gadījumos, ja nav radušies papildus izdevumi.
Sistēma: Pašapkalpošanās portāls | Izpildes termiņš: 10 dd laikā pēc atgriešanās no komandējuma, ja ir prombūtne, termiņš pagarinās par prombūtnes periodu | Sagatavotie dati: Komandējuma atskaite - avansa izdevumu dokuments
Ārējais priekštecis: FG2/2.3.4</bpmn:documentation>
<bpmn:incoming>Flow_2</bpmn:incoming>
<bpmn:outgoing>Flow_3</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="Task_3_5_3_4" name="Komandējuma (darba brauciena) atskaites apstrāde">
<bpmn:documentation>Nr.p.k.: 3.5.3.4. | RACI: VPC=R
VPC saskaņā ar Horizon projektējumā norādīto, saņemot informāciju par komandējuma izdevumu atskaiti, veic atskaites apstrādi komandējuma dokumentos vai avansa norēķina dokumentos un nepieciešamības gadījumā atzīst prasības pret uzaicinātājpusi.
Pēc izdevumu apliecinošo dokumentu apstrādes VPC pārliecinās vai iesniegtie attaisnojuma izdevumi nosedz iepriekš saņemto avansu un gadījumos, kad izmaksātais avanss pārsniedz iesniegtos attaisnojuma izdevumus un Nodarbinātajam ir apstiprināts nākamais komandējums no tā paša finansējuma, tad avansa atlikums tiek saglabāts uz nākamo apstiprināto komandējumu, bet citos gadījumos VPC tālāk rīkojas atbilstoši komandējuma pieteikumā norādītajam.
Sistēma: RVS Horizon | Izpildes termiņš: *3 dd laikā no atskaites apstiprināšanas, kas apstiprināta līdz plkst.15.00 | Sagatavotie dati: Komandējuma atskaite - avansa izdevumu dokuments
Ārējais pēctecis: FG3/3.5.4.1, FG2/2.3.2, FG6/6.2.1.1, FG6/6.2.1.3</bpmn:documentation>
<bpmn:incoming>Flow_3</bpmn:incoming>
<bpmn:outgoing>Flow_4</bpmn:outgoing>
</bpmn:userTask>
<bpmn:endEvent id="End_1" name="Izeja: 3.5.3.1.">
<bpmn:incoming>Flow_5</bpmn:incoming>
</bpmn:endEvent>
<bpmn:endEvent id="End_2" name="Izeja: 3.5.3.2.">
<bpmn:incoming>Flow_6</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1" sourceRef="Start_1" targetRef="Task_3_5_3_2"/>
<bpmn:sequenceFlow id="Flow_2" sourceRef="Start_2" targetRef="Task_3_5_3_3"/>
<bpmn:sequenceFlow id="Flow_3" sourceRef="Task_3_5_3_3" targetRef="Task_3_5_3_4"/>
<bpmn:sequenceFlow id="Flow_4" sourceRef="Task_3_5_3_4" targetRef="Task_3_5_3_1"/>
<bpmn:sequenceFlow id="Flow_5" sourceRef="Task_3_5_3_1" targetRef="End_1"/>
<bpmn:sequenceFlow id="Flow_6" sourceRef="Task_3_5_3_2" targetRef="End_2"/>
</bpmn:process>
</bpmn:definitions>

View File

@@ -0,0 +1,390 @@
#!/usr/bin/env python3
"""
register-transcoder — Valsts Kase process register (.xlsx/.xlsm) -> BPMN skeleton.
Part of the vk-gramatvediba UAPF workspace. Reads a published Valsts Kase
"Grāmatvedības uzskeites procesu apraksts" function-group register and emits,
for any sub-process in it, a BPMN process skeleton: one task per register
step, swimlanes from the RACI columns, and sequence flows reconstructed from
the register's own predecessor / successor step references.
The output is a *skeleton*, not an executable package. It is the deterministic
first pass of the transcription pipeline; turning a skeleton into a Level 4
executable (explicit gateways, DMN decision extraction, resource mappings,
package manifest) is the human/AI-assisted refinement step — see the curated
FG3-1, FG3-4 and FG3-5 packages and docs/methodology.md.
Usage:
transcode.py list <register.xlsx>
transcode.py emit <register.xlsx> <subprocess> [-o <output.bpmn>]
Examples:
transcode.py list fg3_process.xlsm
transcode.py emit fg3_process.xlsm 3.5.2 -o 3.5.2.skeleton.bpmn
Dependencies: openpyxl.
"""
import sys
import re
from xml.sax.saxutils import escape
try:
import openpyxl
except ImportError:
sys.exit("error: openpyxl is required (pip install openpyxl)")
BPMN_NS = "http://www.omg.org/spec/BPMN/20100524/MODEL"
# RACI actor columns, in register column order, mapped to BPMN lane ids/names.
ACTORS = [
("nodarbinatais", "Lane_Nodarbinatais", "Nodarbinātais"),
("iestade", "Lane_Iestade", "Iestāde"),
("vpc", "Lane_VPC", "VPC (Vienotais pakalpojumu centrs)"),
]
# Header cell texts used to locate columns (substring match, case-insensitive).
H_PRED = "no procesa darbības soļa"
H_NR = "nr.p.k"
H_NAME = "process, apakšprocess"
H_RACI = "atbildības sadalījums"
H_DESC = "darbību apraksts"
H_SYSTEM = "izmantotā is"
H_DEADLINE = "izpildes termiņš"
H_OUTPUTS = "sagatavotie dati"
H_SUCC = "uz procesa darbības soli"
def norm_nr(s):
"""Normalise a step number for matching: trim, drop trailing dots."""
return (s or "").strip().strip(".").strip()
def san(s):
"""Sanitise a string into a BPMN NCName fragment."""
out = re.sub(r"[^A-Za-z0-9]+", "_", (s or "").strip()).strip("_")
return out or "x"
def cell(ws, r, c):
if c is None:
return ""
v = ws.cell(row=r, column=c).value
return "" if v is None else str(v).strip()
def find_sheet_and_header(wb):
"""Locate the function-group worksheet and its header row."""
for ws in wb.worksheets:
for r in range(1, 12):
for c in range(1, 20):
v = ws.cell(row=r, column=c).value
if v and H_NR in str(v).lower():
return ws, r
sys.exit("error: could not find a register sheet (no 'Nr.p.k.' header)")
def map_columns(ws, hrow):
"""Map logical fields to column indices using the header row."""
cols = {}
for c in range(1, ws.max_column + 1):
t = (ws.cell(row=hrow, column=c).value or "")
t = str(t).lower().strip()
if not t:
continue
if H_PRED in t:
cols["pred_fg"] = c # predecessor FG-group column
cols["pred_nr"] = c + 1 # predecessor step-number sub-column
elif H_NR in t:
cols["nr"] = c
elif H_NAME in t:
cols["name"] = c
elif H_RACI in t:
cols["raci"] = c # RACI block spans raci, +1, +2
elif H_DESC in t:
cols["desc"] = c
elif H_SYSTEM in t:
cols["system"] = c
elif H_DEADLINE in t:
cols["deadline"] = c
elif H_OUTPUTS in t:
cols["outputs"] = c
elif H_SUCC in t:
cols["succ_fg"] = c # successor FG-group column
cols["succ_nr"] = c + 1 # successor step-number sub-column
for req in ("nr", "name", "raci"):
if req not in cols:
sys.exit(f"error: register header is missing the '{req}' column")
return cols
def parse_refs(fg_cell, nr_cell):
"""Parse a predecessor/successor cell pair into [(fg, nr_key), ...]."""
fgs = [x.strip() for x in str(fg_cell).splitlines() if x.strip()]
nrs = [x.strip() for x in str(nr_cell).splitlines() if x.strip()]
if not nrs:
return []
if len(fgs) == 1 and len(nrs) > 1:
fgs = fgs * len(nrs)
refs = []
for i, nr in enumerate(nrs):
fg = fgs[i] if i < len(fgs) else (fgs[0] if fgs else "")
key = norm_nr(nr)
if key:
refs.append((fg.upper(), key))
return refs
def parse_register(path):
"""Return (steps, subprocesses). Each step is a dict; subprocesses maps
a sub-process key -> its register name."""
wb = openpyxl.load_workbook(path, data_only=True)
ws, hrow = find_sheet_and_header(wb)
cols = map_columns(ws, hrow)
own_fg = re.sub(r"[^A-Za-z0-9]", "", ws.title).upper() # e.g. FG3
steps = []
subprocesses = {}
current_sub = None
for r in range(hrow + 2, ws.max_row + 1):
nr = cell(ws, r, cols["nr"])
name = cell(ws, r, cols["name"])
if not nr or not name:
continue
raci = [cell(ws, r, cols["raci"] + i) for i in range(3)]
desc = cell(ws, r, cols.get("desc"))
is_step = bool(desc) or any(raci)
if not is_step:
# section / sub-process header row
current_sub = norm_nr(nr)
subprocesses[current_sub] = name
continue
steps.append({
"nr": nr, "key": norm_nr(nr), "name": name,
"sub": current_sub, "raci": raci, "desc": desc,
"system": cell(ws, r, cols.get("system")),
"deadline": cell(ws, r, cols.get("deadline")),
"outputs": cell(ws, r, cols.get("outputs")),
"pred": parse_refs(cell(ws, r, cols.get("pred_fg")),
cell(ws, r, cols.get("pred_nr"))),
"succ": parse_refs(cell(ws, r, cols.get("succ_fg")),
cell(ws, r, cols.get("succ_nr"))),
"own_fg": own_fg,
})
return steps, subprocesses
def primary_lane(raci):
"""Pick the swimlane for a step: the actor that is Responsible ('R')."""
for i, v in enumerate(raci):
if "R" in v.upper():
return ACTORS[i]
for i, v in enumerate(raci):
if "A" in v.upper():
return ACTORS[i]
for i, v in enumerate(raci):
if v:
return ACTORS[i]
return ACTORS[2] # default: VPC
def build_flows(group):
"""Reconstruct in-group sequence flows from predecessor/successor links.
Returns a set of (src_key, dst_key)."""
keys = {s["key"] for s in group}
edges = set()
for s in group:
for fg, nr in s["pred"]:
if nr in keys and nr != s["key"]:
edges.add((nr, s["key"]))
for fg, nr in s["succ"]:
if nr in keys and nr != s["key"]:
edges.add((s["key"], nr))
return edges
def doc_text(s):
"""Assemble the <documentation> body for a step's task."""
parts = []
raci_bits = [f"{ACTORS[i][2].split(' ')[0]}={s['raci'][i]}"
for i in range(3) if s["raci"][i]]
parts.append(f"Nr.p.k.: {s['nr']} | RACI: " + "; ".join(raci_bits))
if s["desc"]:
parts.append(s["desc"])
meta = []
if s["system"]:
meta.append("Sistēma: " + s["system"].replace("\n", " "))
if s["deadline"]:
meta.append("Izpildes termiņš: " + s["deadline"].replace("\n", " "))
if s["outputs"]:
meta.append("Sagatavotie dati: " + s["outputs"].replace("\n", " "))
if meta:
parts.append(" | ".join(meta))
ext_p = [f"{fg}/{nr}" for fg, nr in s["pred"]
if nr not in s["_groupkeys"]]
ext_s = [f"{fg}/{nr}" for fg, nr in s["succ"]
if nr not in s["_groupkeys"]]
if ext_p:
parts.append("Ārējais priekštecis: " + ", ".join(ext_p))
if ext_s:
parts.append("Ārējais pēctecis: " + ", ".join(ext_s))
return "\n".join(parts)
def emit_bpmn(steps, subprocesses, sub):
group = [s for s in steps if s["sub"] == sub]
if not group:
avail = ", ".join(sorted(subprocesses)) or "(none)"
sys.exit(f"error: no steps for sub-process '{sub}'. Available: {avail}")
gkeys = {s["key"] for s in group}
for s in group:
s["_groupkeys"] = gkeys
edges = build_flows(group)
indeg = {s["key"]: 0 for s in group}
outdeg = {s["key"]: 0 for s in group}
for a, b in edges:
outdeg[a] += 1
indeg[b] += 1
entries = [s for s in group if indeg[s["key"]] == 0] or [group[0]]
exits = [s for s in group if outdeg[s["key"]] == 0] or [group[-1]]
tid = {s["key"]: "Task_" + san(s["nr"]) for s in group}
lanes_used = {}
for s in group:
lane = primary_lane(s["raci"])
s["_lane"] = lane[1]
lanes_used.setdefault(lane[1], (lane[1], lane[2]))
name = subprocesses.get(sub, sub)
proc_id = "Process_" + san(sub)
L = []
L.append('<?xml version="1.0" encoding="UTF-8"?>')
L.append('<bpmn:definitions '
'xmlns:bpmn="%s" id="Defs_%s" '
'targetNamespace="https://uapf.dev/vk-gramatvediba/transcoded">'
% (BPMN_NS, san(sub)))
L.append(' <bpmn:process id="%s" name="%s" isExecutable="false">'
% (proc_id, escape(name)))
# --- lanes ---
node_lane = {}
for s in group:
node_lane[tid[s["key"]]] = s["_lane"]
start_ids = ["Start_%d" % (i + 1) for i in range(len(entries))]
end_ids = ["End_%d" % (i + 1) for i in range(len(exits))]
L.append(' <bpmn:laneSet id="LaneSet_%s">' % san(sub))
# start/end events go in the lane of the step they touch
extra = {}
for sid, st in zip(start_ids, entries):
extra.setdefault(st["_lane"], []).append(sid)
for eid, st in zip(end_ids, exits):
extra.setdefault(st["_lane"], []).append(eid)
for lid, lname in lanes_used.values():
L.append(' <bpmn:lane id="%s" name="%s">' % (lid, escape(lname)))
for s in group:
if s["_lane"] == lid:
L.append(' <bpmn:flowNodeRef>%s</bpmn:flowNodeRef>'
% tid[s["key"]])
for nid in extra.get(lid, []):
L.append(' <bpmn:flowNodeRef>%s</bpmn:flowNodeRef>' % nid)
L.append(' </bpmn:lane>')
L.append(' </bpmn:laneSet>')
# --- collect flows: start->entry, edges, exit->end ---
flows = []
fc = 0
incoming = {}
outgoing = {}
def add_flow(src, dst):
nonlocal fc
fc += 1
fid = "Flow_%d" % fc
flows.append((fid, src, dst))
outgoing.setdefault(src, []).append(fid)
incoming.setdefault(dst, []).append(fid)
return fid
for sid, st in zip(start_ids, entries):
add_flow(sid, tid[st["key"]])
for a, b in sorted(edges):
add_flow(tid[a], tid[b])
for eid, st in zip(end_ids, exits):
add_flow(tid[st["key"]], eid)
# --- events + tasks ---
for sid, st in zip(start_ids, entries):
L.append(' <bpmn:startEvent id="%s" name="Ieeja: %s">'
% (sid, escape(st["nr"])))
for f in outgoing.get(sid, []):
L.append(' <bpmn:outgoing>%s</bpmn:outgoing>' % f)
L.append(' </bpmn:startEvent>')
for s in group:
t = tid[s["key"]]
L.append(' <bpmn:userTask id="%s" name="%s">'
% (t, escape(s["name"].replace("\n", " "))))
L.append(' <bpmn:documentation>%s</bpmn:documentation>'
% escape(doc_text(s)))
for f in incoming.get(t, []):
L.append(' <bpmn:incoming>%s</bpmn:incoming>' % f)
for f in outgoing.get(t, []):
L.append(' <bpmn:outgoing>%s</bpmn:outgoing>' % f)
L.append(' </bpmn:userTask>')
for eid, st in zip(end_ids, exits):
L.append(' <bpmn:endEvent id="%s" name="Izeja: %s">'
% (eid, escape(st["nr"])))
for f in incoming.get(eid, []):
L.append(' <bpmn:incoming>%s</bpmn:incoming>' % f)
L.append(' </bpmn:endEvent>')
for fid, src, dst in flows:
L.append(' <bpmn:sequenceFlow id="%s" sourceRef="%s" '
'targetRef="%s"/>' % (fid, src, dst))
L.append(' </bpmn:process>')
L.append('</bpmn:definitions>')
return "\n".join(L) + "\n"
def cmd_list(path):
steps, subs = parse_register(path)
counts = {}
for s in steps:
counts[s["sub"]] = counts.get(s["sub"], 0) + 1
print(f"register: {path}")
print(f"{len(steps)} steps in {len(counts)} sub-process(es) with steps:\n")
for sub in sorted(counts):
print(f" {sub:<10} {counts[sub]:>3} step(s) {subs.get(sub, '')}")
print("\nemit a sub-process: transcode.py emit <register> <subprocess>")
def cmd_emit(path, sub, out):
steps, subs = parse_register(path)
xml = emit_bpmn(steps, subs, sub)
if out:
with open(out, "w", encoding="utf-8") as fh:
fh.write(xml)
n = len([s for s in steps if s["sub"] == sub])
print(f"wrote {out} ({n} step(s), sub-process {sub}{subs.get(sub,'')})")
else:
sys.stdout.write(xml)
def main(argv):
if len(argv) < 3 or argv[1] not in ("list", "emit"):
sys.exit(__doc__.strip())
if argv[1] == "list":
cmd_list(argv[2])
else:
if len(argv) < 4:
sys.exit("usage: transcode.py emit <register> <subprocess> [-o out]")
out = None
if "-o" in argv:
out = argv[argv.index("-o") + 1]
cmd_emit(argv[2], argv[3], out)
if __name__ == "__main__":
main(sys.argv)