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:
94
tools/register-transcoder/README.md
Normal file
94
tools/register-transcoder/README.md
Normal 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.
|
||||
55
tools/register-transcoder/sample-output/3.5.2.skeleton.bpmn
Normal file
55
tools/register-transcoder/sample-output/3.5.2.skeleton.bpmn
Normal 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>
|
||||
76
tools/register-transcoder/sample-output/3.5.3.skeleton.bpmn
Normal file
76
tools/register-transcoder/sample-output/3.5.3.skeleton.bpmn
Normal 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>
|
||||
390
tools/register-transcoder/transcode.py
Normal file
390
tools/register-transcoder/transcode.py
Normal 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)
|
||||
Reference in New Issue
Block a user