feat: generate invoices

Maas Lalani created

Change summary

Inter.ttf   |   0 
README.md   |  33 ++++++++++++++-
currency.go |  12 +++++
go.mod      |  12 +++++
go.sum      |  17 ++++++++
invoice.pdf |   0 
main.go     | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 181 insertions(+), 6 deletions(-)

Detailed changes

README.md 🔗

@@ -1,14 +1,21 @@
 # Invoice
 
-<sub><sub>z</sub></sub><sub>z</sub>z
-
 Generate invoices from the command line.
 
+## Text-based User Interface
+
 ```bash
+invoice
+```
+
+## Command Line Interface
+
+```bash
+# Generate an invoice from information.
 invoice generate --title "Invoice" \
     --id 2 \
     --logo ./images/logo.png \
-    --from "LaLaLabs, Inc." \
+    --from "Dream, Inc." \
     --to "Imagine, Inc." \
     --date "June 10, 2023" \
     --due "June 30, 2023" \
@@ -21,6 +28,25 @@ invoice generate --title "Invoice" \
     --notes "For debugging purposes."
 ```
 
+Save repeated information with environment variables:
+
+```bash
+export INVOICE_LOGO=/path/to/image.png
+export INVOICE_FROM="Dream, Inc."
+export INVOICE_TO="Imagine, Inc."
+export INVOICE_TAX=0.13
+export INVOICE_RATE=25
+```
+
+Generate new invoice:
+
+```bash
+invoice generate \
+    --item "Yellow Rubber Duck" --quantity 5 \
+    --item "Special Edition Plaid Rubber Duck" --quantity 1 \
+    --notes "For debugging purposes."
+```
+
 ## Installation
 
 <!--
@@ -48,7 +74,6 @@ go install github.com/maaslalani/invoice@main
 
 Or download a binary from the [releases](https://github.com/maaslalani/invoice/releases).
 
-
 ## License
 
 [MIT](https://github.com/maaslalani/invoice/blob/master/LICENSE)

currency.go 🔗

@@ -0,0 +1,12 @@
+package main
+
+var currencySymbols = map[string]string{
+	"USD": "$",
+	"EUR": "€",
+	"GBP": "£",
+	"JPY": "¥",
+	"CNY": "¥",
+	"INR": "₹",
+	"RUB": "₽",
+	"KRW": "₩",
+}

go.mod 🔗

@@ -1,3 +1,15 @@
 module github.com/maaslalani/invoice
 
 go 1.19
+
+require (
+	github.com/signintech/gopdf v0.17.1
+	github.com/spf13/cobra v1.7.0
+)
+
+require (
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+)

go.sum 🔗

@@ -0,0 +1,17 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311 h1:zyWXQ6vu27ETMpYsEMAsisQ+GqJ4e1TPvSNfdOPF0no=
+github.com/phpdave11/gofpdi v1.0.14-0.20211212211723-1f10f9844311/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/signintech/gopdf v0.17.1 h1:7g1JCz5XtdyARU3DmGcsyO/eRWM5C2KXGFQ/9+pUCOM=
+github.com/signintech/gopdf v0.17.1/go.mod h1:wrLtZoWaRNrS4hphED0oflFoa6IWkOu6M3nJjm4VbO4=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go 🔗

@@ -1,7 +1,116 @@
 package main
 
-import "fmt"
+import (
+	_ "embed"
+	"flag"
+	"log"
+	"time"
+
+	"github.com/signintech/gopdf"
+	"github.com/spf13/cobra"
+)
+
+//go:embed Inter.ttf
+var interFont []byte
+
+var (
+	id    string
+	title string
+
+	logo string
+	from string
+	to   string
+	date string
+	due  string
+
+	items      []string
+	quantities []int
+	rates      []float64
+
+	tax      float64
+	discount float64
+	currency string
+
+	note   string
+	output string
+)
+
+func init() {
+	generateCmd.Flags().StringVar(&id, "id", time.Now().Format("20060102"), "ID")
+	generateCmd.Flags().StringVar(&title, "title", "INVOICE", "Title")
+
+	generateCmd.Flags().Float64SliceVarP(&rates, "rate", "r", []float64{25}, "Rates")
+	generateCmd.Flags().IntSliceVarP(&quantities, "quantity", "q", []int{2}, "Quantities")
+	generateCmd.Flags().StringSliceVarP(&items, "item", "i", []string{"Paper Cranes"}, "Items")
+
+	generateCmd.Flags().StringVarP(&logo, "logo", "l", "", "Company logo")
+	generateCmd.Flags().StringVarP(&from, "from", "f", "Project Folded, Inc.", "Issuing company")
+	generateCmd.Flags().StringVarP(&to, "to", "t", "Untitled Corporation, Inc.", "Recipient company")
+	generateCmd.Flags().StringVar(&date, "date", time.Now().Format("Jan 02, 2006"), "Date")
+	generateCmd.Flags().StringVar(&due, "due", time.Now().AddDate(0, 0, 14).Format("Jan 02, 2006"), "Payment due date")
+
+	generateCmd.Flags().Float64Var(&tax, "tax", 0.13, "Tax")
+	generateCmd.Flags().Float64VarP(&discount, "discount", "d", 0.0, "Discount")
+	generateCmd.Flags().StringVarP(&currency, "currency", "c", "USD", "Currency")
+
+	generateCmd.Flags().StringVarP(&note, "note", "n", "For debugging purposes.", "Note")
+	generateCmd.Flags().StringVarP(&output, "output", "o", "invoice.pdf", "Output file (.pdf)")
+
+	flag.Parse()
+}
+
+var rootCmd = &cobra.Command{
+	Use:   "invoice",
+	Short: "Invoice generates invoices from the command line.",
+	Long:  `Invoice generates invoices from the command line.`,
+}
+
+var generateCmd = &cobra.Command{
+	Use:   "generate",
+	Short: "Generate an invoice",
+	Long:  `Generate an invoice`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		pdf := gopdf.GoPdf{}
+		pdf.Start(gopdf.Config{
+			PageSize: *gopdf.PageSizeA4,
+		})
+		pdf.SetMargins(40, 40, 40, 40)
+		pdf.AddPage()
+		err := pdf.AddTTFFontData("Inter", interFont)
+		if err != nil {
+			return err
+		}
+
+		writeLogo(&pdf, logo, from)
+		writeTitle(&pdf, title, id)
+		writeBillTo(&pdf, to)
+		writeHeaderRow(&pdf)
+		subtotal := 0.0
+		for i := range items {
+			q := 1
+			if len(quantities) > i {
+				q = quantities[i]
+			}
+
+			r := 0.0
+			if len(rates) > i {
+				r = rates[i]
+			}
+
+			writeRow(&pdf, items[i], q, r)
+			subtotal += float64(q) * r
+		}
+		writeNotes(&pdf, note)
+		writeTotals(&pdf, subtotal, subtotal*tax, subtotal*discount)
+		writeFooter(&pdf, id)
+		return pdf.WritePdf(output)
+	},
+}
 
 func main() {
-	fmt.Println("Hello")
+	rootCmd.AddCommand(generateCmd)
+	err := rootCmd.Execute()
+	if err != nil {
+		log.Fatal(err)
+	}
 }