Finalize & PDF Generation
Finalizing an invoice locks it for review, generates the PDF, and transitions the status from draft to needs_review.
Finalize Flow
Template Selection
The system uses Go html/template to render invoices:
| Client Type | Template | Language |
|---|---|---|
| Local (UZS) | invoice_local.html | Russian |
| International | invoice_international.html | English |
Templates are stored in internal/services/generator/templates/ and include:
- Company header with logo
- Client details (name, TIN, address)
- Invoice metadata (number, date, period)
- Line items table with descriptions, hours, rates, totals
- Financial summary
- Legal text (dynamic, from per-invoice
legal_textfield) - Signature blocks (local) or payment terms (international)
HTML Override
Invoices have an optional html_override field. When set (non-empty string), it takes precedence over the template-generated HTML:
- Preview:
GET /invoices/:id/preview-htmlreturns the override HTML directly instead of rendering from the template - Finalize: PDF is generated from the override HTML instead of the template
- Edit: In draft status, the frontend provides an "Edit HTML" button that opens a full-page textarea editor where users can modify the raw HTML
- Reset: Users can clear
html_override(set to empty string) to revert to the template-generated HTML - Regenerate: Clears
html_overridewhen regenerating from approved/declined/rejected status
Display Toggles
Each invoice has 4 boolean flags that control what appears in the rendered HTML and PDF:
| Flag | Default | Effect when OFF |
|---|---|---|
show_task_list | true | Hides individual task rows, shows 3 summary rows instead |
show_tracked_time | true | Hides the time/hours column |
show_task_pricing | false | Hides the per-line price column |
show_overtime | true | Hides the overtime row |
These flags are inherited from client_invoice_config defaults when the invoice is generated and can be toggled per-invoice while in draft status.
When show_overtime is enabled and the invoice has overtime, the overtime row displays as: "Переработка (Итого Xч / Лимит Yч)" — showing total hours worked and the monthly limit from monthly_limit_hours (stored on the invoice at generation time from contract_rate_rules). If no monthly limit is available, "—" is shown instead.
Legal Text
Each invoice has a legal_text field that replaces the previously hardcoded legal paragraphs in templates. The text is split into paragraphs using Go's split template function and rendered dynamically.
- Default: Copied from
client_invoice_config.default_legal_textat generation time - Editable: Can be modified per-invoice in the invoice detail page while in draft
- Reset: "Reset to default" button restores the client's configured template
PDF Conversion
The rendered HTML is sent to Gotenberg (hosted at convert.prxm.uz) for PDF conversion:
POST https://convert.prxm.uz/forms/chromium/convert/html
Content-Type: multipart/form-data
- file: index.html (rendered template)
- marginTop: 0.5
- marginBottom: 0.5
- marginLeft: 0.5
- marginRight: 0.5
- paperWidth: 8.27 (A4)
- paperHeight: 11.69 (A4)
Configuration:
- Timeout: 30 seconds for PDF generation
- Paper size: A4 (210mm x 297mm)
- Margins: 0.5 inches on all sides
PDF Storage
The generated PDF is stored directly in the PostgreSQL invoices.pdf_data column as BYTEA. This is a Phase 1 simplification — Phase 3 moves PDF storage to Cloudflare R2.
The html_template_hash (SHA-256) is stored alongside to detect if the template has changed since the last generation.
Retrieving the PDF
curl http://localhost:8081/api/v1/invoices/{id}/pdf \
-H "Authorization: Bearer $TOKEN" \
-o invoice.pdf
Returns Content-Type: application/pdf with the PDF binary. Returns 404 if the invoice has no PDF (not yet finalized).
Finalize Request
curl -X POST http://localhost:8081/api/v1/invoices/{id}/finalize \
-H "Authorization: Bearer $TOKEN"
No request body needed. The endpoint:
- Validates the invoice is in
draftstatus - Loads all line items
- Renders the appropriate HTML template
- Converts to PDF via Gotenberg
- Saves the PDF and updates status
- Returns the updated invoice
Error Cases
| Error | HTTP Status | Cause |
|---|---|---|
| Invoice not in draft | 409 Conflict | Can only finalize draft invoices |
| Template rendering failed | 500 | Missing template data |
| PDF conversion failed | 500 | Gotenberg unreachable or timeout (30s) |
| Invoice not found | 404 | Invalid invoice ID |