Skip to main content

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 TypeTemplateLanguage
Local (UZS)invoice_local.htmlRussian
Internationalinvoice_international.htmlEnglish

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_text field)
  • 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-html returns 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_override when regenerating from approved/declined/rejected status

Display Toggles

Each invoice has 4 boolean flags that control what appears in the rendered HTML and PDF:

FlagDefaultEffect when OFF
show_task_listtrueHides individual task rows, shows 3 summary rows instead
show_tracked_timetrueHides the time/hours column
show_task_pricingfalseHides the per-line price column
show_overtimetrueHides 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.

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_text at 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:

  1. Validates the invoice is in draft status
  2. Loads all line items
  3. Renders the appropriate HTML template
  4. Converts to PDF via Gotenberg
  5. Saves the PDF and updates status
  6. Returns the updated invoice

Error Cases

ErrorHTTP StatusCause
Invoice not in draft409 ConflictCan only finalize draft invoices
Template rendering failed500Missing template data
PDF conversion failed500Gotenberg unreachable or timeout (30s)
Invoice not found404Invalid invoice ID