~kdsch/invoicer

98e490a32d750a8968649a62021b55bd473cbc3a — Karl Schultheisz 4 years ago
first commit
3 files changed, 349 insertions(+), 0 deletions(-)

A main.go
A notes
A template.html
A  => main.go +154 -0
@@ 1,154 @@
package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"html/template"
	"io"
	"io/ioutil"
	"os"
	"strings"
	"time"

	"github.com/google/uuid"
)

type invoice struct {
	Seller, Buyer contact
	Number        uuid.UUID
	Date          time.Time
	Items         []item
}

type contact struct {
	Company string
	Name    string
	Address string
	City    string
	State   string
	Zip     string
	Phone   string
	Email   string
}

func newInvoice(seller, buyer, items io.Reader) (*invoice, error) {
	var i invoice
	args := []struct {
		src io.Reader
		dst interface{}
		str string
	}{
		{src: seller, dst: &i.Seller, str: "seller"},
		{src: buyer, dst: &i.Buyer, str: "buyer"},
		{src: items, dst: &i.Items, str: "items"},
	}
	for _, arg := range args {
		if err := decode(arg.src, arg.dst); err != nil {
			return nil, fmt.Errorf("%v: %v", arg.str, err)
		}
	}
	i.Number = uuid.New()
	i.Date = time.Now()
	return &i, nil
}

type item struct {
	Description string
	Rate        float64 // $/hr
	Hours       float64
}

func (i invoice) Total() float64 {
	var t float64
	for _, item := range i.Items {
		t += item.Total()
	}
	return t
}

func (i invoice) Proverb() string {
	var proverbs = []string{
		"It's a boon to pay it soon.",
		"It's a boon to pay it soon.<br>What a drag it is to lag.",
		"A speedy check makes us happy as heck.",
		"It takes a minute to win it.",
		"We like it best when we needn't pest.",
		"It's just our luck to make an honest buck.",
		"We deserve a swift check in the book.",
		"A dollar here, a holler there.",
	}
	return proverbs[0]
}

func (i item) Total() float64 { return i.Rate * i.Hours }

func decode(in io.Reader, i interface{}) error {
	data, err := ioutil.ReadAll(in)
	if err != nil {
		return err
	}
	if err := json.Unmarshal(data, i); err != nil {
		return err
	}
	return nil
}

var (
	seller = flag.String("seller", "seller/ks-llc.json", "seller.json")
	buyer  = flag.String("buyer", "customer/oum.json", "buyer.json")
	items  = flag.String("items", "", "items.json")
	out    = flag.String("out", "invoice.html", "out.html")
)

func file(filename string) io.Reader {
	var buf bytes.Buffer
	f, err := os.Open(filename)
	if err != nil {
		panic(err)
	}
	defer f.Close()
	buf.ReadFrom(f)
	return &buf
}

func main() {
	flag.Parse()
	i, err := newInvoice(
		file(*seller),
		file(*buyer),
		file(*items))
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	t, err := template.
		New("template.html").
		Option("missingkey=error").
		Funcs(template.FuncMap{
			"int": func(x float64) int { return int(x) },
			"dec": func(x float64) string {
				if x == float64(int(x)) {
					return ""
				}
				return "." + strings.Split(fmt.Sprintf("%.2f", x), ".")[1]
			},
		}).
		ParseFiles("template.html")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	out, err := os.Create(*out)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	defer out.Close()
	if err := t.Execute(out, i); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

A  => notes +61 -0
@@ 1,61 @@
An invoice contains a lot of information, most of which doesn't change very
much. When writing an invoice, I should only have to focus on the part that
changes: the items, each of which consists of a description, an hourly rate,
and a duration. The tedious parts, such as the customer information, date,
invoice number, my contact info, and the total, should be handled for me
by the system. Of course, I need to specify who the customer is, but that
shouldn't require entering all the details by hand.

It should be possible to modify some of these tedious details if necessary;
for instance, changing the date.

I could use codes to identify customers. BEC Systems, LLC. could be referred
to as BEC. The part of the invoice I write might look like this:

	customer: BEC
	seller: KS
	items:
		-
			description: Preliminary research
			rate: 0
			hours: 5
		-
			description: Embedded Linux development
			rate: 100
			hours: 10

For convenience, a tool could generate incomplete YAML invoices:

	date: 2020-02-22
	number: efeb3056-9924-4a31-ba19-db0163b11767
	seller: KS
	customer:
	items:
		-
			description:
			rate:
			hours:

After I fill in the fields, I'd submit the file to the system, which would
validate and insert it in a database.

Later, I could query the system to produce reports, such as HTML or PDF
invoices to be sent to the customer. There would be no need for me to
save these documents, as the database (such as SQLite) already contains all
of their information.

Because the system is backed by a database, I can run all sorts of queries
that might be useful for accounting or planning.



In summary, the system has the following features:

- generate partially completed invoice.yaml files
- manage invoices and customers through a command-line interface
- generate beautiful HTML or PDF invoices from the database

It should be possible to pipe the invoices through a text editor:

	invoice new | vis - | invoice add


A  => template.html +134 -0
@@ 1,134 @@
<html>
	<head>
		<meta charset="utf-8">
		<style>
		* {
			margin:  0;
			padding: 0;
		}
		/*
		html { background: white; }
		*/
		body {
			width:      8.5in;
			height:     11in;
			/*
			border:     1px dotted gray;
			background: #fdc;
			*/
		}
		.body, table {
			font-family: 'Fira Sans', sans-serif;
			font-size:   10pt;
			line-height: 1.35;
		}
		.body {
			margin:      1.6in;
		}
		p, h1, h2 { margin-bottom: 0.9em; }
		th {
			color: gray;
			font-weight: normal;
		}
		h1, h2, strong {
			font-family: 'Fira Sans Medium', sans-serif;
			font-weight: normal;
		}
		emph, .time { font-style: italic; }
		.time { float: right; }
		h2 {
			margin-top:  2em;
			font-size:   12pt;
		}
		h1 { font-size: 14pt; }
		table {
			width:           100%;
			border-spacing:  0;
			border-collapse: collapse;
		}
		table .left, td.decimal { text-align: left;  }
		th, td {
			text-align: right;
			padding:    0.4em 0;
		}
		td {
			border-top: 1px solid #ccc;
		}
		code { font-family: 'Fira Mono', monospace; }
		.parties {
			display:               grid;
			grid-template-columns: 1fr 1fr;
		}
		</style>
	</head>
	<body>
		<div class="body">
		<h1>Invoice</h1>
		<p class="time">{{.Date.Format "January 2, 2006"}}</p>
		<p><code>{{.Number}}</code></p>

		<div class="parties">
			<div class="seller">
				<h2>Seller</h2>
				<p>
				{{with .Seller}}
					{{.Company}}<br>
					{{.Name}}<br>
					{{.Address}}<br>
					{{.City}}, {{.State}} {{.Zip}}<br><br>
					{{.Phone}}<br>
					{{.Email}}
				{{end}}
				</p>
			</div>

			<div class="customer">
				<h2>Customer</h2>
				<p>
				{{with .Buyer}}
					{{.Company}}<br>
					{{.Name}}<br>
					{{.Address}}<br>
					{{.City}}, {{.State}} {{.Zip}}<br><br>
					{{.Phone}}<br>
					{{.Email}}
				{{end}}
				</p>
			</div>
		</div>

		<h2>Terms</h2>
		<p>
			Write a check for ${{printf "%.2f" .Total}} payable to {{.Seller.Company}}.<br>
			Mail it to the address above.
		</p>

		<h2>Items</h2>
		<table>
			<tr>
				<th class="left">Description</th>
				<th>Rate ($</th><th class="left">/hr)</th>
				<th>Time (</th><th class="left">h)</th>
				<th>Total ($</th><th class="left">)</th>
			</tr>
			{{range .Items}}
			<tr>
				<td class="left">{{.Description}}</td>
				<td>{{.Rate}}</td><td></td>
				<td class="integer">{{int .Hours}}</td>
				<td class="decimal">{{dec .Hours}}</td>
				<td class="integer">{{int .Total}}</td>
				<td class="decimal">{{dec .Total}}</td>
			</tr>
			{{end}}
			<tr>
				<td class="left">(total)</td>
				<td></td><td></td>
				<td></td><td></td>
				<td class="integer"><strong>{{int .Total}}</strong></td>
				<td class="decimal"><strong>{{dec .Total}}</strong></td>
			</tr>
		</table>
		</div>
	</body>
</html>
\ No newline at end of file