~kdsch/invoicer

8a101d1ae218bdc487ad40774354fe38d3e2c086 — Karl Schultheisz 4 years ago 98e490a
make it better
6 files changed, 489 insertions(+), 142 deletions(-)

A data.go
A db.go
A editor.go
M main.go
A report.go
M template.html
A data.go => data.go +68 -0
@@ 0,0 1,68 @@
package main

import (
	"time"

	"github.com/google/uuid"
)

//
// data types

type invoice struct {
	Date     string
	Number   uuid.UUID
	Seller   string
	Customer string
	Summary  string
	Items    []item
}

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

const placeholder = "_"

func newInvoice() invoice {
	return invoice{
		Date:     time.Now().Format(time.RFC3339),
		Number:   uuid.New(),
		Customer: placeholder,
		Seller:   placeholder,
		Summary:  placeholder,
		Items: []item{
			{Description: placeholder},
		},
	}
}

type party struct {
	Id      uuid.UUID
	Code    string
	Company string
	Name    string
	Address string
	City    string
	State   string
	Zip     string
	Phone   string
	Email   string
}

func newParty() party {
	return party{
		Id:      uuid.New(),
		Code:    placeholder,
		Company: placeholder,
		Name:    placeholder,
		Address: placeholder,
		City:    placeholder,
		State:   placeholder,
		Zip:     placeholder,
		Phone:   placeholder,
		Email:   placeholder,
	}
}

A db.go => db.go +142 -0
@@ 0,0 1,142 @@
package main

import (
	"database/sql"
	"fmt"

	"github.com/google/uuid"
)

func initDB(addr string) error {
	db, err := sql.Open("sqlite3", addr)
	if err != nil {
		return err
	}

	queries := []string{
		"parties (id, code, company, name, address, city, state, zip, phone, email)",
		"invoices (id, date, customer_party_id, seller_party_id, summary)",
		"items (id, invoice_id, description, rate, hours)",
	}

	for _, q := range queries {
		_, err := db.Exec("create table " + q)
		if err != nil {
			return err
		}
	}
	return db.Close()
}

type inserter interface {
	insert(*sql.DB) error
}

func insert(addr string, i inserter) error {
		db, err := sql.Open("sqlite3", addr)
		if err != nil {
			return fmt.Errorf("sql.Open: %v", err)
		}
		defer db.Close()

		return i.insert(db)
}

func findPartyIDByCode(code string, db *sql.DB) (id uuid.UUID, err error) {
	const selectParty = "select id from parties where code = ?"
	err = db.QueryRow(selectParty, code).Scan(&id)
	return
}

func findPartyByID(id uuid.UUID, db *sql.DB) (p party, err error) {
	const selectParty = `
	select company, name, address, city, state, zip, phone, email
	from parties
	where id = ?`

	p.Id = id
	err = db.QueryRow(selectParty, id).Scan(
		&p.Company,
		&p.Name,
		&p.Address,
		&p.City,
		&p.State,
		&p.Zip,
		&p.Phone,
		&p.Email,
	)
	return
}

func (p party) insert(db *sql.DB) error {
	const insertParty = `
	insert into parties
				 (id, code, company, name, address, city, state, zip, phone, email)
	values (?,  ?,    ?,       ?,    ?,       ?,    ?,     ?,   ?,     ?    )`

	_, err := db.Exec(insertParty,
		p.Id,
		p.Code,
		p.Company,
		p.Name,
		p.Address,
		p.City,
		p.State,
		p.Zip,
		p.Phone,
		p.Email,
	)
	return err
}

func (i invoice) insert(db *sql.DB) error {
	seller, err := findPartyIDByCode(i.Seller, db)
	if err != nil {
		return fmt.Errorf("could not find seller %q: %v", i.Seller, err)
	}

	customer, err := findPartyIDByCode(i.Customer, db)
	if err != nil {
		return fmt.Errorf("could not find customer %q: %v", i.Customer, err)
	}

	const insertInvoice = `
	insert into invoices
	       (id, date, customer_party_id, seller_party_id, summary)
	values (?,  ?,    ?,              ?,               ?      )`

	_, err = db.Exec(insertInvoice,
		i.Number,
		i.Date,
		customer,
		seller,
		i.Summary,
	)
	if err != nil {
		return fmt.Errorf("could not insert invoice: %v", err)
	}
	for _, item := range i.Items {
		if err := item.insert(i.Number, db); err != nil {
			return err
		}
	}

	return nil
}

func (i item) insert(invoice uuid.UUID, db *sql.DB) error {
	const addItemToInvoice = `
	insert into items
	       (id, invoice_id, description, rate, hours)
	values (?,  ?,          ?,           ?,    ?    )`

	_, err := db.Exec(addItemToInvoice,
		uuid.New(),
		invoice,
		i.Description,
		i.Rate,
		i.Hours,
	)
	return err
}


A editor.go => editor.go +70 -0
@@ 0,0 1,70 @@
package main

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"

	"gopkg.in/yaml.v2"
)

var vis textEditor

type textEditor struct{}

func (textEditor) edit(input io.Reader, output io.Writer) error {
	cmd := exec.Command("vis", "-")
	cmd.Stdin = input
	cmd.Stdout = output
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

type editor interface {
	edit(io.Reader, io.Writer) error
}

func edit(v interface{}, e editor) error {
	var in bytes.Buffer

retry:
	if err := encode(&in, v); err != nil {
		return err
	}

	var out bytes.Buffer
	if err := e.edit(&in, &out); err != nil {
		return err
	}

	if err := decode(&out, v); err != nil {
		// you made a mistake?
		in.Reset()
		in.ReadFrom(&out)
		if _, err := in.WriteString(fmt.Sprintf("# %v\n", err)); err != nil {
			return err
		}
		goto retry
	}
	return nil
}

func encode(out io.Writer, v interface{}) error {
	b, err := yaml.Marshal(v)
	if err != nil {
		return err
	}
	_, err = out.Write(b)
	return err
}

func decode(in io.Reader, v interface{}) error {
	b, err := ioutil.ReadAll(in)
	if err != nil {
		return err
	}
	return yaml.Unmarshal(b, v)
}

M main.go => main.go +47 -127
@@ 1,153 1,73 @@
package main

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

	"github.com/google/uuid"
	_ "rsc.io/sqlite"
)

type invoice struct {
	Seller, Buyer contact
	Number        uuid.UUID
	Date          time.Time
	Items         []item
func usage(msg string) error {
	return fmt.Errorf("%s\nusage: %s init|party|invoice|report id", msg, os.Args[0])
}

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"},
func app() error {
	args := os.Args[1:]
	if len(args) < 1 {
		return usage("Please give me a command.")
	}
	for _, arg := range args {
		if err := decode(arg.src, arg.dst); err != nil {
			return nil, fmt.Errorf("%v: %v", arg.str, err)

	addr := "db"
	switch args[0] {
	case "init":
		return initDB(addr)

	case "party":
		p := newParty()
		if err := edit(&p, vis); err != nil {
			return err
		}
	}
	i.Number = uuid.New()
	i.Date = time.Now()
	return &i, nil
}

type item struct {
	Description string
	Rate        float64 // $/hr
	Hours       float64
}
		return insert(addr, p)

func (i invoice) Total() float64 {
	var t float64
	for _, item := range i.Items {
		t += item.Total()
	}
	return t
}
	case "invoice":
		i := newInvoice()
		if err := edit(&i, vis); err != nil {
			return err
		}

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]
}
		if err := insert(addr, i); err != nil {
			return fmt.Errorf("insert: %v", err)
		}

func (i item) Total() float64 { return i.Rate * i.Hours }
		fmt.Println(i.Number)
		return nil

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
}
	case "report":
		if len(args) < 2 {
			return usage("Please give me an invoice ID.")
		}

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")
)
		db, err := sql.Open("sqlite3", addr)
		if err != nil {
			return fmt.Errorf("sql.Open: %v", err)
		}
		defer db.Close()

		r, err := newReport(args[1], db)
		if err != nil {
			return fmt.Errorf("newReport: %v", err)
		}

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

	return usage("I don't know that command.")
}

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 {
	if err := app(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

A report.go => report.go +148 -0
@@ 0,0 1,148 @@
package main

import (
	"io"
	"database/sql"
	"fmt"
	"html/template"
	"os"
	"strings"
	"time"

	"github.com/google/uuid"
)

//
// document generation

type report struct {
	Number           uuid.UUID
	Date             time.Time
	Seller, Customer party
	Summary          string
	Items            []item
}

func newReport(invoice string, db *sql.DB) (r report, err error) {
	var id uuid.UUID
	id, err = uuid.Parse(invoice)
	if err != nil {
		return
	}

	err = r.load(id, db)
	return
}

func (r *report) load(id uuid.UUID, db *sql.DB) error {
	r.Number = id
	if err := r.loadInvoice(id, db); err != nil {
		return err
	}
	return r.loadItems(id, db)
}

func (r *report) loadInvoice(invoice uuid.UUID, db *sql.DB) error {
	const selectInvoice = `
	select date, customer_party_id, seller_party_id, summary from invoices
	where id = ?`

	var date string
	var customer, seller uuid.UUID
	err := db.QueryRow(selectInvoice, invoice).Scan(
		&date,
		&customer,
		&seller,
		&r.Summary,
	);
	if err != nil {
		return fmt.Errorf("error selecting invoice %q: %v", invoice, err)
	}

	t, err := time.Parse(time.RFC3339, date)
	if err != nil {
		return fmt.Errorf("error parsing date %q: %v", date, err)
	}
	r.Date = t

	return r.loadParties(customer, seller, db)
}

func (r *report) loadParties(customer, seller uuid.UUID, db *sql.DB) error {
	var err error
	r.Customer, err = findPartyByID(customer, db)
	if err != nil {
		return fmt.Errorf("error selecting customer %q: %v", customer, err)
	}

	r.Seller, err = findPartyByID(seller, db)
	if err != nil {
		return fmt.Errorf("error selecting seller %q: %v", seller, err)
	}

	return nil
}

func (r *report) loadItems(invoice uuid.UUID, db *sql.DB) error {
	const selectItems = `
	select description, rate, hours from items
	where invoice_id = ?`
	rows, err := db.Query(selectItems, invoice)
	if err != nil {
		return fmt.Errorf("error selecting items: %v", err)
	}

	for i := 0; rows.Next(); i++ {
		r.Items = append(r.Items, item{})
		err := rows.Scan(
			&r.Items[i].Description,
			&r.Items[i].Rate,
			&r.Items[i].Hours,
		)
		if err != nil {
			return fmt.Errorf("error scanning item: %v", err)
		}
	}

	return nil
}

func (r report) save(filename string) error {
	f, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer f.Close()
	return r.render(f)
}

func (r report) render(out io.Writer) error {
	return templ.Execute(out, r)
}

func round(x float64) int { return int(x) }
func decimal(x float64) string {
	if x == float64(int(x)) {
		return ""
	}
	return "." + strings.Split(fmt.Sprintf("%.2f", x), ".")[1]
}

var templ = template.Must(template.
	New("template.html").
	Option("missingkey=error").
	Funcs(template.FuncMap{"int": round, "dec": decimal}).
	ParseFiles("template.html"))

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

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


M template.html => template.html +14 -15
@@ 25,7 25,7 @@
		.body {
			margin:      1.6in;
		}
		p, h1, h2 { margin-bottom: 0.9em; }
		p, h1, h2 { margin-bottom: 0.8em; }
		th {
			color: gray;
			font-weight: normal;


@@ 37,7 37,7 @@
		emph, .time { font-style: italic; }
		.time { float: right; }
		h2 {
			margin-top:  2em;
			margin-top:  1.9em;
			font-size:   12pt;
		}
		h1 { font-size: 14pt; }


@@ 49,7 49,7 @@
		table .left, td.decimal { text-align: left;  }
		th, td {
			text-align: right;
			padding:    0.4em 0;
			padding:    0.33em 0;
		}
		td {
			border-top: 1px solid #ccc;


@@ 64,13 64,12 @@
	<body>
		<div class="body">
		<h1>Invoice</h1>
		<p class="time">{{.Date.Format "January 2, 2006"}}</p>
		<p><code>{{.Number}}</code></p>
		<div class="time">{{.Date.Format "January 2, 2006"}}</div>
		<code>{{.Number}}</code>

		<div class="parties">
			<div class="seller">
				<h2>Seller</h2>
				<p>
				{{with .Seller}}
					{{.Company}}<br>
					{{.Name}}<br>


@@ 79,13 78,11 @@
					{{.Phone}}<br>
					{{.Email}}
				{{end}}
				</p>
			</div>

			<div class="customer">
				<h2>Customer</h2>
				<p>
				{{with .Buyer}}
				{{with .Customer}}
					{{.Company}}<br>
					{{.Name}}<br>
					{{.Address}}<br>


@@ 93,15 90,11 @@
					{{.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>Summary</h2>
		<p>{{.Summary}}</p>

		<h2>Items</h2>
		<table>


@@ 129,6 122,12 @@
				<td class="decimal"><strong>{{dec .Total}}</strong></td>
			</tr>
		</table>
		<h2>Terms</h2>
		<p>
			Write a check for ${{printf "%.2f" .Total}} payable to {{.Seller.Company}}.<br>
			Mail it to the address above.
		</p>
		</div>

	</body>
</html>
\ No newline at end of file