From c41e5f99e9a1ffcc9dd97ac5fbb3acb45d34eed1 Mon Sep 17 00:00:00 2001 From: Rose Thatcher <97619538+hopefulTex@users.noreply.github.com> Date: Tue, 27 Jun 2023 04:25:46 -0700 Subject: [PATCH] (feat): JSON/YAML support for input data (#5) * Added JSON support for input data * data from files * Data flags override imported files again * Merge conflicts * Cleaned string parsing --------- Co-authored-by: Maas Lalani --- .gitignore | 3 ++ README.md | 15 ++++++- go.mod | 8 ++-- go.sum | 8 ++-- import.go | 73 ++++++++++++++++++++++++++++++++ main.go | 122 +++++++++++++++++++++++++++++++++-------------------- pdf.go | 6 +-- 7 files changed, 178 insertions(+), 57 deletions(-) create mode 100644 import.go diff --git a/.gitignore b/.gitignore index a1363379944a5745ceb49c0e493d80eb9335c79a..9695e57c1304849570fd8318095e588c2b2d5718 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ *.pdf +*.json +*.yaml +*.yml \ No newline at end of file diff --git a/README.md b/README.md index bc024346f801d506ff8c3c5638c3f5435c4a2a25..c0b6f51e0e3a87a478576bff04a6929157b1bf03 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ open invoice.pdf Example invoice -Save repeated information with environment variables: +Save repeated information with environment variables or in an overridable JSON file: ```bash export INVOICE_LOGO=/path/to/image.png @@ -34,6 +34,16 @@ export INVOICE_TAX=0.13 export INVOICE_RATE=25 ``` +```json +{ + "logo": "/path/to/image.png", + "from": "Dream, Inc.", + "to": "Imagine, Inc.", + "tax": 0.13, + "rates": 25 +} +``` + Generate new invoice: ```bash @@ -43,6 +53,9 @@ invoice generate \ --note "For debugging purposes." \ --output duck-invoice.pdf ``` +```bash +invoice generate --import path/to/data.json --output duck-invoice.pdf +``` ### Custom Templates diff --git a/go.mod b/go.mod index 2d6a6a801f3562b86d149bf438ac895404e67d86..36f91b705ab7e9a1dbe41cef8d9e006419222041 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module github.com/maaslalani/invoice -go 1.19 +go 1.20 require ( github.com/signintech/gopdf v0.18.0 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.16.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -22,8 +23,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 04aa0a222220ffb3b116ecf0df0bdbfec491a650..93c5dc2c3972ee7177db07d11c70240e5406827f 100644 --- a/go.sum +++ b/go.sum @@ -316,8 +316,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -327,8 +327,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/import.go b/import.go new file mode 100644 index 0000000000000000000000000000000000000000..115f402a8e85cb60c5987c69c82c5c95ec788a32 --- /dev/null +++ b/import.go @@ -0,0 +1,73 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + "strings" + + "github.com/spf13/pflag" + "gopkg.in/yaml.v3" +) + +func importData(path string, structure *Invoice, flags *pflag.FlagSet) error { + fileText, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("unable to read file") + } + + var b []byte + var byteBuffer [][]byte + flags.Visit(func(f *pflag.Flag) { + if f.Value.Type() != "string" { + b = []byte(fmt.Sprintf(`{"%s":%s}`, f.Name, f.Value)) + } else { + b = []byte(fmt.Sprintf(`{"%s":"%s"}`, f.Name, f.Value)) + } + byteBuffer = append(byteBuffer, b) + }) + + if strings.HasSuffix(path, ".json") { + err = importJson(fileText, structure) + } else if strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") { + err = importYaml(fileText, structure) + + } else { + return fmt.Errorf("unsupported file type") + } + if err != nil { + log.Fatal(err) + } + + for _, bytes := range byteBuffer { + err = importJson(bytes, structure) + if err != nil { + log.Fatal(err) + } + } + + return err +} + +func importJson(text []byte, structure *Invoice) error { + if !json.Valid(text) { + return fmt.Errorf("json file not correctly formatted") + } + + err := json.Unmarshal(text, structure) + if err != nil { + return fmt.Errorf("json file not correctly formatted") + } + + return nil +} + +func importYaml(text []byte, structure *Invoice) error { + err := yaml.Unmarshal(text, structure) + if err != nil { + return fmt.Errorf("yaml file not correctly formatted") + } + + return nil +} diff --git a/main.go b/main.go index aee74c21bea4f79b835e2a0f559f3dc3a05368ed..5687dc775617f2bae6437d0be6d0de468f6753b9 100644 --- a/main.go +++ b/main.go @@ -19,49 +19,73 @@ var interFont []byte //go:embed Inter-Bold.ttf var interBoldFont []byte -var ( - id string - title string +type Invoice struct { + Id string `json:"id" yaml:"id"` + Title string `json:"title" yaml:"title"` + + Logo string `json:"logo" yaml:"logo"` + From string `json:"from" yaml:"from"` + To string `json:"to" yaml:"to"` + Date string `json:"date" yaml:"date"` + Due string `json:"due" yaml:"due"` + + Items []string `json:"items" yaml:"items"` + Quantities []int `json:"quantities" yaml:"quantities"` + Rates []float64 `json:"rates" yaml:"rates"` - logo string - from string - to string - date string - due string + Tax float64 `json:"tax" yaml:"tax"` + Discount float64 `json:"discount" yaml:"discount"` + Currency string `json:"currency" yaml:"currency"` - items []string - quantities []int - rates []float64 + Note string `json:"note" yaml:"note"` +} - tax float64 - discount float64 - currency string +func DefaultInvoice() Invoice { + return Invoice{ + Id: time.Now().Format("20060102"), + Title: "INVOICE", + Rates: []float64{25}, + Quantities: []int{2}, + Items: []string{"Paper Cranes"}, + From: "Project Folded, Inc.", + To: "Untitled Corporation, Inc.", + Date: time.Now().Format("Jan 02, 2006"), + Due: time.Now().AddDate(0, 0, 14).Format("Jan 02, 2006"), + Tax: 0, + Discount: 0, + Currency: "USD", + } +} - note string - output string +var ( + importPath string + output string + file = Invoice{} + defaultInvoice = DefaultInvoice() ) func init() { viper.AutomaticEnv() - generateCmd.Flags().StringVar(&id, "id", time.Now().Format("20060102"), "ID") - generateCmd.Flags().StringVar(&title, "title", "INVOICE", "Title") + generateCmd.Flags().StringVar(&importPath, "import", "", "Imported file (.json/.yaml)") + generateCmd.Flags().StringVar(&file.Id, "id", time.Now().Format("20060102"), "ID") + generateCmd.Flags().StringVar(&file.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().Float64SliceVarP(&file.Rates, "rate", "r", defaultInvoice.Rates, "Rates") + generateCmd.Flags().IntSliceVarP(&file.Quantities, "quantity", "q", defaultInvoice.Quantities, "Quantities") + generateCmd.Flags().StringSliceVarP(&file.Items, "item", "i", defaultInvoice.Items, "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().StringVarP(&file.Logo, "logo", "l", defaultInvoice.Logo, "Company logo") + generateCmd.Flags().StringVarP(&file.From, "from", "f", defaultInvoice.From, "Issuing company") + generateCmd.Flags().StringVarP(&file.To, "to", "t", defaultInvoice.To, "Recipient company") + generateCmd.Flags().StringVar(&file.Date, "date", defaultInvoice.Date, "Date") + generateCmd.Flags().StringVar(&file.Due, "due", defaultInvoice.Due, "Payment due date") - generateCmd.Flags().Float64Var(&tax, "tax", 0, "Tax") - generateCmd.Flags().Float64VarP(&discount, "discount", "d", 0.0, "Discount") - generateCmd.Flags().StringVarP(¤cy, "currency", "c", "USD", "Currency") + generateCmd.Flags().Float64Var(&file.Tax, "tax", defaultInvoice.Tax, "Tax") + generateCmd.Flags().Float64VarP(&file.Discount, "discount", "d", defaultInvoice.Discount, "Discount") + generateCmd.Flags().StringVarP(&file.Currency, "currency", "c", defaultInvoice.Currency, "Currency") - generateCmd.Flags().StringVarP(¬e, "note", "n", "", "Note") + generateCmd.Flags().StringVarP(&file.Note, "note", "n", "", "Note") generateCmd.Flags().StringVarP(&output, "output", "o", "invoice.pdf", "Output file (.pdf)") flag.Parse() @@ -78,6 +102,14 @@ var generateCmd = &cobra.Command{ Short: "Generate an invoice", Long: `Generate an invoice`, RunE: func(cmd *cobra.Command, args []string) error { + + if importPath != "" { + err := importData(importPath, &file, cmd.Flags()) + if err != nil { + fmt.Println(err) + } + } + pdf := gopdf.GoPdf{} pdf.Start(gopdf.Config{ PageSize: *gopdf.PageSizeA4, @@ -94,33 +126,33 @@ var generateCmd = &cobra.Command{ return err } - writeLogo(&pdf, logo, from) - writeTitle(&pdf, title, id, date) - writeBillTo(&pdf, to) + writeLogo(&pdf, file.Logo, file.From) + writeTitle(&pdf, file.Title, file.Id, file.Date) + writeBillTo(&pdf, file.To) writeHeaderRow(&pdf) subtotal := 0.0 - for i := range items { + for i := range file.Items { q := 1 - if len(quantities) > i { - q = quantities[i] + if len(file.Quantities) > i { + q = file.Quantities[i] } r := 0.0 - if len(rates) > i { - r = rates[i] + if len(file.Rates) > i { + r = file.Rates[i] } - writeRow(&pdf, items[i], q, r) + writeRow(&pdf, file.Items[i], q, r) subtotal += float64(q) * r } - if note != "" { - writeNotes(&pdf, note) + if file.Note != "" { + writeNotes(&pdf, file.Note) } - writeTotals(&pdf, subtotal, subtotal*tax, subtotal*discount) - if due != "" { - writeDueDate(&pdf, due) + writeTotals(&pdf, subtotal, subtotal*file.Tax, subtotal*file.Discount) + if file.Due != "" { + writeDueDate(&pdf, file.Due) } - writeFooter(&pdf, id) + writeFooter(&pdf, file.Id) output = strings.TrimSuffix(output, ".pdf") + ".pdf" err = pdf.WritePdf(output) if err != nil { diff --git a/pdf.go b/pdf.go index 4d0ac20cc099bf8bcb265e265dce6f27c8936a28..441e07c32fa79a6ae4c69ea709348df689350bb9 100644 --- a/pdf.go +++ b/pdf.go @@ -125,9 +125,9 @@ func writeRow(pdf *gopdf.GoPdf, item string, quantity int, rate float64) { pdf.SetX(quantityColumnOffset) _ = pdf.Cell(nil, strconv.Itoa(quantity)) pdf.SetX(rateColumnOffset) - _ = pdf.Cell(nil, currencySymbols[currency]+strconv.FormatFloat(rate, 'f', 2, 64)) + _ = pdf.Cell(nil, currencySymbols[file.Currency]+strconv.FormatFloat(rate, 'f', 2, 64)) pdf.SetX(amountColumnOffset) - _ = pdf.Cell(nil, currencySymbols[currency]+amount) + _ = pdf.Cell(nil, currencySymbols[file.Currency]+amount) pdf.Br(24) } @@ -155,7 +155,7 @@ func writeTotal(pdf *gopdf.GoPdf, label string, total float64) { if label == totalLabel { _ = pdf.SetFont("Inter-Bold", "", 11.5) } - _ = pdf.Cell(nil, currencySymbols[currency]+strconv.FormatFloat(total, 'f', 2, 64)) + _ = pdf.Cell(nil, currencySymbols[file.Currency]+strconv.FormatFloat(total, 'f', 2, 64)) pdf.Br(24) }