clinvoice-rs
is a command-line tool for generating invoices from timesheet
data. It reads .cli
files, which contain time entries, and uses a Tera
template to generate an invoice. The tool is configured using a
clinvoice.toml
file, which allows you to specify your invoice template, tax
rate, and other settings. This allows for a high degree of customization and
automation in your invoicing workflow.
This is an oxidized π¦ version of clinvoice-zsh, which I was previously using for about 15 years to generate invoices from text timesheets. I tried to stick to the original version so that my old timesheets were still legible, but the template and configuration files are not compatible.
To build you will need rust installed. The following steps should work on Debian based systems, like Ubuntu...
sudo apt install cargo
cargo build
cargo install --path .
This will place the executable in ~/.cargo/bin
which must be added to
your PATH
.
Alternatively you can install cargo-deb
package and run:
cargo-deb -o clinvoice.deb
sudo dpkg -i clinvoice.deb
Timesheet data is stored in .cli
files. These files have a simple format,
with each line representing either a date or a time entry.
Time entries specify hours worked and a description.
2025.07.01
8h = Project A
9-12 = Project B
You can also specify negative hours for discounts:
2025.07.13
-2h = Discount for early payment
Fixed cost entries allow you to add or subtract fixed amounts to the invoice.
2025.07.13
$100 = Some fixed cost
-$50 = Discount for something else
Lines starting with *
or -
are treated as notes and are ignored in
calculations but can be included in templates.
2025.07.13
* This is a note about the work done today.
- Another note.
Lines starting with #
or //
(with optional leading whitespace) are treated
as full-line comments and are ignored by the parser. This is useful for adding
notes to your timesheet files that you don't want to appear on the invoice.
# This is a full-line comment and will be ignored.
// This is also a comment and will be ignored.
2025.07.13
8h = Project A # This is NOT a comment and will be part of the description.
It expected that you have a directory of .cli
files for each client, along
with a clinvoice.toml
configuration file, and a template used to generate the
output.
An directory of example files is provided to illustrate the setup:
β― lt examples
ξΏ examples
βββ ο
2010-11.cli
βββ ο
2010-12.cli
βββ ο
2011-01.cli
βββ ξ bnl-template.tex
βββ ο
bnl-template.txt
βββ ξ² clinvoice.toml
You can view your time entries using the log
command. This command can
display logs in different formats:
full
: Shows all individual time entries.day
: Aggregates entries by day.month
: Aggregates entries by month.year
: Aggregates entries by year.
Example:
clinvoice log --format year
clinvoice log --format month 2025
clinvoice log --format day 2025.07
clinvoice log --format full 2025.07.01
You can also visualize your time entries as a heatmap using the heatmap
command. This command displays a grid of colored squares, where the intensity
of the color corresponds to the amount of work done on a given day.
Example:
clinvoice heatmap 2025
Invoices are generated using Tera templates, which has very similar syntax to Jinja2. The output can be anything, as long as it's text. Some examples would be text, HTML, LaTeX, or Markdown.
Here is a simple example of a LaTeX template, where everything between {{
and
}}
is interpreted by Tera:
\documentclass{article}
\begin{document}
Invoice for {{ period_start | date(format="%B %d, %Y") }} - {{ period_end | date(format="%B %d, %Y") }}
\begin{tabular}{lrr}
Date & Hours & Description \\
\hline
{% for day in days %}
{{ day.date | date(format="%Y-%m-%d") }} & {{ day.hours }} & {{ day.description }} \\
{% endfor %}
\end{tabular}
Total: {{ total_amount }}
\end{document}
This template will generate a simple LaTeX invoice with a table of time entries.
The configuration can point to multiple generators, using multiple template
files. One of the generators can be made default. If the configuration sets up
a build
command for the output file, you have it automatically do post
processing on it. Below, we generate a PDF using pdflatex
.
For example:
[generator]
default = "pdf"
[generator.txt]
template = "template.txt"
output = "output-{{sequence}}.txt"
[generator.pdf]
template = "template.txt"
output = "output-{{sequence}}.tex"
build = "pdflatex {{output}}"
A silly example is provided in the examples directory.
clinvoice generate 201011
clinvoice generate 201012
clinvoice generate 201101
The following variables are available in your templates:
now
: The current date and time in RFC 3339 format.today
: The current date inYYYY-MM-DD
format.invoice_date
: The date of the invoice (same astoday
).due_date
: The invoice due date, calculated based on thecontract.payment-days
in your configuration.period_start
: The first date in the selected time data.period_end
: The last date in the selected time data.subtotal_amount
: The total cost of all time entries before tax.tax_amount
: The calculated tax amount.total_amount
: The total amount of the invoice (subtotal + tax).
Advanced
total_fixed_fees
is a tally of fixed fees (included insubtotal_amount
)total_discounts
is a tally of discounts (included insubtotal_amount
)total_hours_worked
is number of hours in spreadsheettotal_hours_counted
is number of hours aftercontract.cap_hours_per_day
limit is appliedtotal_hours_billed
is number of hours capped tocontract.cap_hours_per_invoice
overage_hours
is number of hours counted, but not billedcounted_amount
israte * total_hours_counted
billed_amount
israte * total_hours_billed
(included insubtotal_amount
)
These variables are available within the {% for day in days %}
loop:
day.index
: The index of the day in the list of entries.day.date
: The date of the entry inYYYY-MM-DD
format.day.hours
: The total hours for the day.day.cost
: The cost for the day (hours * rate).day.description
: A semicolon-separated list of descriptions for the day's entries.
date(format="%Y-%m-%d")
: Formats a date string usingstrftime
syntax.left(width=N)
: Left-justifies a string within the given width, truncating if necessary.right(width=N)
: Right-justifies a string within the given width, truncating if necessary.center(width=N)
: Centers a string within the given width, truncating if necessary.decimal(precision=N)
: Formats a floating-point number to the specified number of decimal places, including trailing zeros.