Takes JSON output from Time Watcher containing daily work hours per project and generates a markdown invoice. Pipe to pandoc for PDF.
It doesn't need to work with Time Watcher, you could just create the JSON needed and it will create the invoice just fine.
# single project
tw report --json --from 2026-01-01 --to 2026-01-31 | ti --project my_client | pandoc -o invoice.pdf
# multiple projects on one invoice
tw report --json --from 2026-01-01 --to 2026-01-31 | ti -p acme -p widgets | pandoc -o invoice.pdf
# merge overlapping days across projects
tw report --json --from 2026-01-01 --to 2026-01-31 | ti -p acme -p widgets --dedup union | pandoc -o invoice.pdfPass --project multiple times to combine several projects into one invoice.
Each project gets its own section with days, rate, and subtotal.
When projects overlap (same date tracked in both), use --dedup (or -d) to merge:
--dedup union-- sum hours on the same date (projects track separate work)--dedup max-- take the higher hours value (projects double-counted the same work)
Without --dedup, each project is listed separately with its own rate and subtotal.
With --dedup, projects are merged into one using the first project's config for rate and billing details.
nix profile install git+https://codeberg.org/ellyxir/time_invoiceRequires Elixir 1.18+.
git clone https://codeberg.org/ellyxir/time_invoice.git
cd time_invoice
mix deps.get
MIX_ENV=prod mix release time_invoiceThis builds the release at _build/prod/rel/time_invoice/. The ti command lives inside it:
# Run directly from the build
_build/prod/rel/time_invoice/bin/ti --project acme < report.json
# Or symlink it onto your PATH
ln -s "$(pwd)/_build/prod/rel/time_invoice/bin/ti" ~/.local/bin/tiCreate a config file at ~/.config/time_invoice/config.exs (or $XDG_CONFIG_HOME/time_invoice/config.exs):
import Config
config :time_invoice, :projects,
my_client: [
template: :default, # uses built-in template, or path like "~/.config/time_invoice/templates/custom.md.eex"
business_name: "My Consulting LLC",
business_address: "123 Main Street\nSometown, ST 12345",
business_email: "[email protected]",
client_name: "Acme Corporation",
client_address: "456 Corporate Blvd\nBigcity, BC 67890",
hourly_rate: 150.00,
currency: "$"
]| Field | Description |
|---|---|
template |
:default for built-in, or path to custom EEx template |
business_name |
Name of business sending invoice |
client_name |
Name of client receiving invoice |
hourly_rate |
Rate per hour |
currency |
Currency symbol (e.g., "$", "€") |
| Field | Description |
|---|---|
business_address |
Address of business (can include newlines) |
business_email |
Contact email |
client_address |
Address of client |
date_format |
:eu (day-month-year) or :us (month-day-year), defaults to :eu |
Additional custom fields can be added and will be available in templates.
Templates are EEx files. Use @variable_name to access variables.
| Variable | Type | Description |
|---|---|---|
@projects |
list | List of project maps (see below) |
@total_hours |
number | Combined hours across all projects |
@total_amount |
number | Sum of per-project subtotals |
@invoice_number |
string | Generated as INV-YY-MM-DD based on generation date |
@invoice_date |
string | Date invoice was generated (formatted) |
@start_date |
string | Formatted start date of report period |
@end_date |
string | Formatted end date of report period |
All config fields from the first project are also available (e.g., @business_name, @currency).
Each entry in @projects is a map with:
| Field | Type | Description |
|---|---|---|
project |
string | Project name |
days |
list | List of %{date: string, hours: number} |
total_hours |
number | Hours for this project |
hourly_rate |
number | Rate from this project's config |
currency |
string | Currency from this project's config |
subtotal |
number | total_hours * hourly_rate |
For backward compatibility with single-project templates, @project and @days are also set as top-level variables when the result contains a single project entry (including after dedup merging).
Numeric values are rounded to 2 decimal places.
# Invoice <%= @invoice_number %>
**Date:** <%= @invoice_date %>
**From:** <%= @business_name %>
**To:** <%= @client_name %>
**Period:** <%= @start_date %> - <%= @end_date %>
<%= for p <- @projects do %>
### <%= p.project %>
| Date | Hours |
|------|-------|
<%= for day <- p.days do %>| <%= day.date %> | <%= day.hours %> |
<% end %>
| Rate | <%= p.currency %><%= p.hourly_rate %> |
| Subtotal | <%= p.currency %><%= p.subtotal %> |
<% end %>
**Total Hours:** <%= @total_hours %>
**Total Due:** <%= @currency %><%= @total_amount %>Save this to ~/.config/time_invoice/templates/custom.md.eex and reference it in your config:
template: "~/.config/time_invoice/templates/custom.md.eex"Copyright 2026 Ellyse Cedeno
Licensed under the Apache License, Version 2.0. See LICENSE for details.