15 KiB
Invoice print setup
This guide creates the invoice tables and Steel calculations through the gRPC API, installs the Typst template used by the print page, inserts example data, and compiles the exported JSON into a PDF.
Run the commands from the repository root. The server address used by the
client is [::1]:50051.
1. Configure grpcurl
PROFILE="fakturacia_2026"
GRPC_ADDR="[::1]:50051"
Log in. Keep -d and its JSON argument on the same logical command line:
grpcurl -plaintext \
-import-path common/proto \
-proto auth.proto \
-d '{"identifier":"YOUR_USERNAME","password":"YOUR_PASSWORD"}' \
"$GRPC_ADDR" \
komp_ac.auth.AuthService/Login
Copy the returned access_token:
TOKEN="PASTE_ACCESS_TOKEN_HERE"
AUTH_HEADER="authorization: Bearer $TOKEN"
The account must have the administrator role for table and script creation.
Table definitions and Steel scripts require an admin token. Row data writes
require an accountant token. Keep the two tokens in separate shell variables,
for example:
ADMIN_TOKEN="PASTE_ADMIN_ACCESS_TOKEN_HERE"
ACCOUNTANT_TOKEN="PASTE_ACCOUNTANT_ACCESS_TOKEN_HERE"
ADMIN_AUTH_HEADER="authorization: Bearer $ADMIN_TOKEN"
ACCOUNTANT_AUTH_HEADER="authorization: Bearer $ACCOUNTANT_TOKEN"
Use ADMIN_AUTH_HEADER for the table-definition and table-script calls. Use
ACCOUNTANT_AUTH_HEADER for the TablesData/PostTableData calls below.
If you do not know an existing password, create a new administrator account on a trusted local development server. The registration endpoint currently accepts the requested role, so do not expose this unauthenticated endpoint to an untrusted network.
grpcurl -plaintext \
-import-path common/proto \
-proto auth.proto \
-d '{
"username":"invoice_admin",
"email":"invoice_admin@example.test",
"password":"CHANGE_THIS_PASSWORD",
"password_confirmation":"CHANGE_THIS_PASSWORD",
"role":"admin",
"timezone":"Europe/Bratislava",
"phone_country":"SK"
}' \
"$GRPC_ADDR" \
komp_ac.auth.AuthService/Register
Then log in with the same credentials and copy the returned token:
grpcurl -plaintext \
-import-path common/proto \
-proto auth.proto \
-d '{"identifier":"invoice_admin","password":"CHANGE_THIS_PASSWORD"}' \
"$GRPC_ADDR" \
komp_ac.auth.AuthService/Login
The Invalid credentials response means the username/email or password is
wrong. Sending only {"identifier":"admin"} cannot work because the login
request still requires a valid password. A temporary connection refused
means the server was not listening at that moment; retry after starting the
server.
2. Create the tables
Create referenced tables first.
Supplier
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto table_definition.proto \
-d '{
"profile_name":"fakturacia_2026",
"table_name":"dodavatel",
"row_display_column":"nazov",
"columns":[
{"name":"nazov","field_type":"TEXT"},
{"name":"ulica","field_type":"TEXT"},
{"name":"mesto","field_type":"TEXT"},
{"name":"ico","field_type":"TEXT"},
{"name":"dic","field_type":"TEXT"},
{"name":"icdph","field_type":"TEXT"},
{"name":"iban","field_type":"TEXT"},
{"name":"banka","field_type":"TEXT"},
{"name":"telefon","field_type":"TEXT"},
{"name":"email","field_type":"TEXT"}
]
}' \
"$GRPC_ADDR" komp_ac.table_definition.TableDefinition/PostTableDefinition
Customer
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto table_definition.proto \
-d '{
"profile_name":"fakturacia_2026",
"table_name":"odberatel",
"row_display_column":"nazov",
"columns":[
{"name":"nazov","field_type":"TEXT"},
{"name":"ulica","field_type":"TEXT"},
{"name":"mesto","field_type":"TEXT"},
{"name":"ico","field_type":"TEXT"},
{"name":"dic","field_type":"TEXT"},
{"name":"icdph","field_type":"TEXT"}
]
}' \
"$GRPC_ADDR" komp_ac.table_definition.TableDefinition/PostTableDefinition
Invoice
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto table_definition.proto \
-d '{
"profile_name":"fakturacia_2026",
"table_name":"faktura",
"row_display_column":"cislo",
"base_currency":"EUR",
"links":[
{"linked_table_name":"dodavatel","required":true},
{"linked_table_name":"odberatel","required":true}
],
"columns":[
{"name":"cislo","field_type":"TEXT"},
{"name":"datum_vystavenia","field_type":"DATE"},
{"name":"datum_dodania","field_type":"DATE"},
{"name":"datum_splatnosti","field_type":"DATE"},
{"name":"variabilny_symbol","field_type":"TEXT"},
{"name":"sposob_uhrady","field_type":"TEXT"},
{"name":"zaklad_dane","field_type":"MONEY","rounding":"MONEY_ROUNDING_HALF_UP","recompute_on_dependency_change":true},
{"name":"dph_celkom","field_type":"MONEY","rounding":"MONEY_ROUNDING_HALF_UP","recompute_on_dependency_change":true},
{"name":"celkom_spolu","field_type":"MONEY","rounding":"MONEY_ROUNDING_HALF_UP","recompute_on_dependency_change":true},
{"name":"poznamka","field_type":"TEXT"}
]
}' \
"$GRPC_ADDR" komp_ac.table_definition.TableDefinition/PostTableDefinition
Invoice item
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto table_definition.proto \
-d '{
"profile_name":"fakturacia_2026",
"table_name":"polozka",
"row_display_column":"nazov",
"base_currency":"EUR",
"links":[{"linked_table_name":"faktura","required":true}],
"columns":[
{"name":"nazov","field_type":"TEXT"},
{"name":"mnozstvo","field_type":"DECIMAL(12,3)"},
{"name":"mj","field_type":"TEXT"},
{"name":"cena","field_type":"MONEY","rounding":"MONEY_ROUNDING_HALF_UP"},
{"name":"dph","field_type":"DECIMAL(5,2)"},
{"name":"bez","field_type":"MONEY","rounding":"MONEY_ROUNDING_HALF_UP","recompute_on_dependency_change":true},
{"name":"dph_suma","field_type":"MONEY","rounding":"MONEY_ROUNDING_HALF_UP","recompute_on_dependency_change":true},
{"name":"spolu","field_type":"MONEY","rounding":"MONEY_ROUNDING_HALF_UP","recompute_on_dependency_change":true}
]
}' \
"$GRPC_ADDR" komp_ac.table_definition.TableDefinition/PostTableDefinition
3. Get table IDs
Use table_definition.proto, not common.proto, for this service call:
TREE=$(grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto table_definition.proto \
-d '{}' "$GRPC_ADDR" \
komp_ac.table_definition.TableDefinition/GetProfileTree)
echo "$TREE" | jq .
FAKTURA_ID=$(echo "$TREE" | jq -r \
'.profiles[] | select(.name=="fakturacia_2026") | .tables[] | select(.name=="faktura") | .id')
POLOZKA_ID=$(echo "$TREE" | jq -r \
'.profiles[] | select(.name=="fakturacia_2026") | .tables[] | select(.name=="polozka") | .id')
4. Add Steel calculations
The script API requires expressions beginning with (. Money values must be
combined using the money functions; do not use ordinary decimal + on MONEY.
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto table_script.proto \
-d "{
\"table_definition_id\":$POLOZKA_ID,
\"target_column\":\"bez\",
\"script\":\"(money-mul (steel_get_column \\\"polozka\\\" \\\"cena\\\") (steel_get_column \\\"polozka\\\" \\\"mnozstvo\\\"))\",
\"description\":\"Quantity multiplied by unit price\"
}" \
"$GRPC_ADDR" komp_ac.table_script.TableScript/PostTableScript
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto table_script.proto \
-d "{
\"table_definition_id\":$POLOZKA_ID,
\"target_column\":\"dph_suma\",
\"script\":\"(money-div (money-mul (steel_get_column \\\"polozka\\\" \\\"bez\\\") (steel_get_column \\\"polozka\\\" \\\"dph\\\")) \\\"100\\\")\",
\"description\":\"VAT amount\"
}" \
"$GRPC_ADDR" komp_ac.table_script.TableScript/PostTableScript
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto table_script.proto \
-d "{
\"table_definition_id\":$POLOZKA_ID,
\"target_column\":\"spolu\",
\"script\":\"(money-add (steel_get_column \\\"polozka\\\" \\\"bez\\\") (steel_get_column \\\"polozka\\\" \\\"dph_suma\\\"))\",
\"description\":\"Total including VAT\"
}" \
"$GRPC_ADDR" komp_ac.table_script.TableScript/PostTableScript
Invoice aggregate scripts:
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto table_script.proto \
-d "{
\"table_definition_id\":$FAKTURA_ID,
\"target_column\":\"zaklad_dane\",
\"script\":\"(money-add (steel_related_aggregate \\\"sum\\\" \\\"polozka\\\" \\\"bez\\\" \\\"faktura\\\") (money-new \\\"EUR\\\" \\\"0\\\"))\"
}" \
"$GRPC_ADDR" komp_ac.table_script.TableScript/PostTableScript
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto table_script.proto \
-d "{
\"table_definition_id\":$FAKTURA_ID,
\"target_column\":\"dph_celkom\",
\"script\":\"(money-add (steel_related_aggregate \\\"sum\\\" \\\"polozka\\\" \\\"dph_suma\\\" \\\"faktura\\\") (money-new \\\"EUR\\\" \\\"0\\\"))\"
}" \
"$GRPC_ADDR" komp_ac.table_script.TableScript/PostTableScript
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto table_script.proto \
-d "{
\"table_definition_id\":$FAKTURA_ID,
\"target_column\":\"celkom_spolu\",
\"script\":\"(money-add (steel_related_aggregate \\\"sum\\\" \\\"polozka\\\" \\\"spolu\\\" \\\"faktura\\\") (money-new \\\"EUR\\\" \\\"0\\\"))\"
}" \
"$GRPC_ADDR" komp_ac.table_script.TableScript/PostTableScript
5. Insert data
Insert supplier and customer rows first. The API returns their IDs.
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto tables_data.proto \
-d '{
"profile_name":"fakturacia_2026",
"table_name":"dodavatel",
"data":{
"nazov":"Kreatív Studio s.r.o.",
"ulica":"Štúrova 8",
"mesto":"040 01 Košice",
"ico":"45612378",
"dic":"2022998877",
"icdph":"SK2022998877",
"iban":"SK89 1100 0000 0026 1234 5678",
"banka":"Tatra banka, a.s.",
"telefon":"+421 911 222 333",
"email":"fakturacia@kreativstudio.sk"
}
}' \
"$GRPC_ADDR" komp_ac.tables_data.TablesData/PostTableData
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto tables_data.proto \
-d '{
"profile_name":"fakturacia_2026",
"table_name":"odberatel",
"data":{
"nazov":"Nová Kaviareň s.r.o.",
"ulica":"Námestie SNP 12",
"mesto":"974 01 Banská Bystrica",
"ico":"51239876",
"dic":"2120654321",
"icdph":"SK2120654321"
}
}' \
"$GRPC_ADDR" komp_ac.tables_data.TablesData/PostTableData
Set the returned IDs:
DODAVATEL_ID="1"
ODBERATEL_ID="1"
Insert the invoice. Scripted totals are initially zero because it has no items yet.
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto tables_data.proto \
-d "{
\"profile_name\":\"fakturacia_2026\",
\"table_name\":\"faktura\",
\"data\":{
\"dodavatel_id\":$DODAVATEL_ID,
\"odberatel_id\":$ODBERATEL_ID,
\"cislo\":\"2026042\",
\"datum_vystavenia\":\"2026-06-15\",
\"datum_dodania\":\"2026-06-12\",
\"datum_splatnosti\":\"2026-06-29\",
\"variabilny_symbol\":\"2026042\",
\"sposob_uhrady\":\"Prevodný príkaz\",
\"zaklad_dane\":\"0\",
\"dph_celkom\":\"0\",
\"celkom_spolu\":\"0\",
\"poznamka\":\"Ďakujeme za Vašu dôveru.\"
}
}" \
"$GRPC_ADDR" komp_ac.tables_data.TablesData/PostTableData
Set the returned invoice ID:
FAKTURA_ROW_ID="PASTE_INVOICE_ID"
Insert invoice items. Because the item columns are scripted, include their calculated values in each request.
grpcurl -plaintext -H "$AUTH_HEADER" \
-import-path common/proto -proto tables_data.proto \
-d "{
\"profile_name\":\"fakturacia_2026\",
\"table_name\":\"polozka\",
\"data\":{
\"faktura_id\":$FAKTURA_ROW_ID,
\"nazov\":\"Návrh loga a vizuálnej identity\",
\"mnozstvo\":\"1\",
\"mj\":\"ks\",
\"cena\":\"850.00\",
\"dph\":\"20\",
\"bez\":\"850.00\",
\"dph_suma\":\"170.00\",
\"spolu\":\"1020.00\"
}
}" \
"$GRPC_ADDR" komp_ac.tables_data.TablesData/PostTableData
Repeat the same request for additional items, changing the values. For the sample invoice, the calculated values are:
| Item | bez |
dph_suma |
spolu |
|---|---|---|---|
| Logo | 850.00 | 170.00 | 1020.00 |
| Menu, quantity 2 | 240.00 | 48.00 | 288.00 |
| Printed menus | 255.00 | 25.50 | 280.50 |
| Interior photography | 260.00 | 52.00 | 312.00 |
The resulting invoice totals are zaklad_dane = 1605.00,
dph_celkom = 295.50, and celkom_spolu = 1900.50.
6. Install the Typst template
The Linux client discovers templates in:
~/.config/komp-ac/komp_ac_client/print_templates/
Place the supplied files there with matching stems:
mkdir -p ~/.config/komp-ac/komp_ac_client/print_templates
cp inv2_generic.typ ~/.config/komp-ac/komp_ac_client/print_templates/faktura.typ
cp inv2_generic.json ~/.config/komp-ac/komp_ac_client/print_templates/faktura.json
The JSON must contain the target paths used by the print mapper:
faktura.*
dodavatel.*
odberatel.*
polozky[].*
Open the invoice row in the client, open the Print page, select faktura.typ,
select the related polozka rows, and export. The client writes the generated
JSON as faktura.export.json beside the template.
7. Compile the PDF
typst compile \
--input data="$HOME/.config/komp-ac/komp_ac_client/print_templates/faktura.export.json" \
"$HOME/.config/komp-ac/komp_ac_client/print_templates/faktura.typ" \
faktura.pdf
The client currently exports the API-backed JSON and saves the document snapshot, but it does not invoke the Typst compiler itself.
Deterministic five-person company simulation
The repository also contains a standalone bootstrap that creates a separate
small_company_2026 profile, installs its Steel calculations, and pushes all
fixture rows through the same gRPC APIs used by the client:
server/scripts/bootstrap_small_company.py
It expects the existing passwordless users admin and filipko. The admin
token is used only for table definitions and scripts. The accountant token is
used for every business-row insert.
Preview the scope without contacting the server:
python3 server/scripts/bootstrap_small_company.py --dry-run
Create and populate the profile:
python3 server/scripts/bootstrap_small_company.py
If a run stops after creating some objects, resume from its checkpoint:
python3 server/scripts/bootstrap_small_company.py --resume
The checkpoint defaults to small_company_2026.bootstrap-state.json. It
contains table IDs, script IDs, and inserted row IDs, but no access tokens or
passwords. The bootstrap does not delete an existing profile. If the profile
already exists without its matching checkpoint, it stops instead of modifying
unknown data.
The fixed scenario includes five employees, customers, suppliers, products, CRM leads, sales orders, invoices and partial payments, purchase orders, supplier invoices, stock movements and positions, expenses, and service time entries. It verifies parent totals, outstanding balances, and stock values by reading the rows back after insertion.