Skip to content

Commit

Permalink
feat: working invoice generator
Browse files Browse the repository at this point in the history
  • Loading branch information
KagChi committed Oct 17, 2023
1 parent 91f7cea commit f2237b5
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 2 deletions.
24 changes: 23 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
module golang-template
module github.com/kagchi/invoice-creator

go 1.20

require (
github.com/fogleman/gg v1.3.0
github.com/gofiber/fiber/v2 v2.50.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
golang.org/x/image v0.0.0-20200119044424-58c23975cae1
)

require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/klauspost/compress v1.17.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.50.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0
)
37 changes: 37 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/gofiber/fiber/v2 v2.50.0 h1:ia0JaB+uw3GpNSCR5nvC5dsaxXjRU5OEu36aytx+zGw=
github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g=
github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
160 changes: 159 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,163 @@
package main

import (
"bytes"
"fmt"
"log"

"github.com/fogleman/gg"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/golang/freetype/truetype"
"github.com/kagchi/invoice-creator/structs"
"golang.org/x/image/font/gofont/goregular"
"golang.org/x/text/language"
"golang.org/x/text/message"
)

const (
width = 800 // Width of A4 paper in points (21 cm)
height = 841 // Height of A4 paper in points (29.7 cm)
margin = 20.0 // Margin in points
)

func drawString(dc *gg.Context, fontSize float64, s string, x float64, y float64, ax float64, ay float64, width float64, align gg.Align) {
font, _ := truetype.Parse(goregular.TTF)
face := truetype.NewFace(font, &truetype.Options{
Size: fontSize,
})

dc.SetFontFace(face)
dc.DrawStringWrapped(s, x, y, ax, ay, width, fontSize, align)
}

func formatNumberToLocaleString(num int, locale string) string {
tags, _, err := language.ParseAcceptLanguage(locale)
if err != nil {
return fmt.Sprintf("%d", num)
}

var tag language.Tag

if len(tags) > 0 {
tag = tags[0]
} else {
tag = language.English
}

p := message.NewPrinter(tag)

return p.Sprintf("%d", num)
}

func main() {
print("鬼を捕まえる準備ができました !")
app := fiber.New()

app.Use(logger.New(logger.Config{
Format: "[${ip}]:${port} [${latency}] ${status} - ${method} ${path}\n",
}))

app.Post("/", func(c *fiber.Ctx) error {
b := new(structs.Payload)

if err := c.BodyParser(b); err != nil {
return err
}

dc := gg.NewContext(width, height)
dc.SetRGB(1, 1, 1)
dc.Clear()

dc.SetRGB(0, 0, 0)

drawString(dc, 28.0, "INVOICE", margin, margin, 0, 0, float64(width)-4*margin, gg.AlignRight) // Title
drawString(dc, 16.0, b.Company.Name, margin, 4*margin, 0, 0, float64(width)-4*margin, gg.AlignRight) // Company Name
drawString(dc, 16.0, b.Company.Address.Detail, margin, 5*margin, 0, 0, float64(width)-4*margin, gg.AlignRight) // Address
drawString(dc, 16.0, b.Company.Address.Zip, margin, 6*margin, 0, 0, float64(width)-4*margin, gg.AlignRight) // Zip Address
drawString(dc, 16.0, b.Company.Address.City, margin, 7*margin, 0, 0, float64(width)-4*margin, gg.AlignRight) // Country

dc.SetRGB(0, 0, 0)
lineY := margin + 20.0 + 10
dc.DrawLine(margin+20, lineY*4, width-(margin+20), lineY*4)
dc.SetLineWidth(1.0)
dc.Stroke()

drawString(dc, 16.0, b.Client.Name, margin + 20, 11*margin, 0, 0, float64(width)-4*margin, gg.AlignLeft) // Client Name
drawString(dc, 16.0, b.Client.Address.Detail, margin + 20, 12 * margin, 0, 0, float64(width)-4*margin, gg.AlignLeft) // Client Address
drawString(dc, 16.0, b.Client.Address.Zip, margin + 20, 13 * margin, 0, 0, float64(width)-4*margin, gg.AlignLeft) // Client Zip Address
drawString(dc, 16.0, b.Client.Address.Country, margin + 20, 14 * margin, 0, 0, float64(width)-4*margin, gg.AlignLeft) // Client Country

drawString(dc, 16.0, fmt.Sprintf("Number : %s", b.Invoice.Number), margin + 20, 11 * margin, 0, 0, float64(width) - 4 * margin, gg.AlignRight) // Invoice Number
drawString(dc, 16.0, fmt.Sprintf("Date : %s", b.Invoice.Date), (margin + 20) -87, 12 * margin, 0, 0, float64(width) - 4 * margin, gg.AlignRight) // Invoice Date
drawString(dc, 16.0, fmt.Sprintf("Due Date : %s", b.Invoice.Due), (margin + 20)-87, 13 * margin, 0, 0, float64(width) - 4 * margin, gg.AlignRight) // Invoice Due Date

drawString(dc, 16.0, "Products", margin + 20, 18.5 * margin, 0, 0, float64(width) -4 * margin, gg.AlignLeft)
drawString(dc, 16.0, "Quantity", margin - 210, 18.5 * margin, 0, 0, float64(width) -4 * margin, gg.AlignRight)
drawString(dc, 16.0, "Price", margin - 105, 18.5 * margin, 0, 0, float64(width) - 4 * margin, gg.AlignRight)
drawString(dc, 16.0, "Total", margin + 8, 18.5 * margin, 0, 0, float64(width) - 4 * margin, gg.AlignRight)

dc.DrawLine(margin+20, lineY*8, width-(margin+20), lineY*8)
dc.SetLineWidth(1.0)
dc.Stroke()

// TODO: use for const of

currentMargin := 21.0
currentPrice := 0
for _, product := range b.Products {
drawString(dc, 16.0, product.Name, margin+20, currentMargin * margin, 0, 0, float64(width)-4*margin, gg.AlignLeft)

quantityStr := fmt.Sprintf("%d", product.Quantity)
quantityLen := 0.0

if len(quantityStr) <= 3 {
quantityLen += float64(len(quantityStr)) * 2.7
} else {
quantityLen += float64(len(quantityStr)) * 3.5
}

drawString(dc, 16.0, quantityStr, margin-(237.5 - quantityLen), currentMargin * margin, 0, 0, float64(width) - 4 * margin, gg.AlignRight) // Quantity

priceStr := fmt.Sprintf("%d", product.Price)
priceLen := 0.0

if len(priceStr) <= 1 {
priceLen += float64(len(quantityStr)) * 14.0
} else if len(priceStr) > 1 {
priceLen -= float64(len(quantityStr)) * 10
priceLen -= float64(len(quantityStr)) * (float64(len(quantityStr)) * 10)
}

drawString(dc, 16.0, fmt.Sprintf("Rp %s", formatNumberToLocaleString(product.Price, b.Setting.Locale)), margin-(107 + priceLen), currentMargin * margin, 0, 0, float64(width) - 4 * margin, gg.AlignRight) // Quantity
drawString(dc, 16.0, fmt.Sprintf("Rp %s", formatNumberToLocaleString(product.Price * product.Quantity, b.Setting.Locale)), margin + 20, currentMargin * margin, 0, 0, float64(width) - 4 * margin, gg.AlignRight) // Price
currentPrice += product.Price * product.Quantity
currentMargin += 1.0
}

lineY = currentMargin + 20.0 + 10.0 + float64(len(b.Products)) - 2.0
dc.DrawLine(margin + 20, lineY * 9.5, width - (margin + 20), lineY * 9.5)
dc.SetLineWidth(1.0)
dc.Stroke()

drawString(dc, 16.0, fmt.Sprintf("Sub Total : %s %s", b.Setting.Currency, formatNumberToLocaleString(currentPrice, b.Setting.Locale)), margin+20, (currentMargin + 6.5) * margin, 0, 0, float64(width) - 4 * margin, gg.AlignRight) // Subtotal\
drawString(dc, 16.0, fmt.Sprintf("Vat %.2f%% : %s %s", b.Setting.Vat, b.Setting.Currency, formatNumberToLocaleString(int(float64((currentPrice)) * (b.Setting.Vat / 100)), b.Setting.Locale)), margin + 20, (currentMargin + 7.5) * margin, 0, 0, float64(width)-4*margin, gg.AlignRight) // Subtotal

currentMargin += 1.5
lineY = currentMargin + 20.0 + 10.0 + (float64(len(b.Products)) - 2.0) + 12.5
dc.DrawLine(margin + 440, lineY * 9.5, width - (margin + 20), lineY * 9.5)
dc.SetLineWidth(1.0)
dc.Stroke()


drawString(dc, 16.0, fmt.Sprintf("Total : %s %s", b.Setting.Currency, formatNumberToLocaleString(int(float64(currentPrice) * (b.Setting.Vat / 100)) + currentPrice, b.Setting.Locale)), margin + 20, (currentMargin + 7.5) * margin, 0, 0, float64(width) - 4 * margin, gg.AlignRight) // Subtotal

var buf bytes.Buffer
_ = dc.EncodePNG(&buf)

c.Set("Content-Type", "image/png")

return c.Send(buf.Bytes())
})

log.Fatal(app.Listen(":3000"))
}
39 changes: 39 additions & 0 deletions structs/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package structs

type AddressPayload struct {
Zip string `json:"zip"`
City string `json:"city"`
Country string `json:"country"`
Detail string `json:"detail"`
}

type Profile struct {
Name string `json:"name"`
Address AddressPayload `json:"address"`
}

type Invoice struct {
Number string `json:"number"`
Date string `json:"date"`
Due string `json:"due"`
}

type Setting struct {
Currency string `json:"currency"`
Vat float64 `json:"vat"`
Locale string `json:"locale"`
}

type Product struct {
Name string `json:"name"`
Price int `json:"price"`
Quantity int `json:"quantity"`
}

type Payload struct {
Invoice Invoice `json:"Invoice"`
Company Profile `json:"company"`
Client Profile `json:"client"`
Setting Setting `json:"setting"`
Products []Product `json:"products"`
}

0 comments on commit f2237b5

Please sign in to comment.