ellyxir/time_invoice

mirror of codeberg https://codeberg.org/ellyxir/time_invoice

★ 0Forks 0ElixirGitHub ↗Compare

README

TimeInvoice

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.pdf

Multi-project invoices

Pass --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.

Installation

Nix

nix profile install git+https://codeberg.org/ellyxir/time_invoice

From source

Requires Elixir 1.18+.

git clone https://codeberg.org/ellyxir/time_invoice.git
cd time_invoice
mix deps.get
MIX_ENV=prod mix release time_invoice

This 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/ti

Configuration

Create 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: "$"
  ]

Required fields

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., "$", "€")

Optional fields

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.

Custom Templates

Templates are EEx files. Use @variable_name to access variables.

Template 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.

Example template

# 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"

License

Copyright 2026 Ellyse Cedeno

Licensed under the Apache License, Version 2.0. See LICENSE for details.

Contributors

ellyxir

Issues