diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..9453af6 --- /dev/null +++ b/.air.toml @@ -0,0 +1,51 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] +args_bin = ["serve", "-p", "3001", "--pdf", "prince"] +bin = "./gobl.html" +cmd = "templ generate && go build ./cmd/gobl.html" +delay = 1000 +exclude_dir = ["tmp", "vendor", "testdata"] +exclude_file = ["gobl.html"] +exclude_regex = ["_test.go", "_templ.go"] +exclude_unchanged = false +follow_symlink = false +full_bin = "" +include_dir = [] +include_ext = ["go", "tpl", "templ", "css", "tmpl", "html"] +include_file = [] +kill_delay = "0s" +log = "build-errors.log" +poll = false +poll_interval = 0 +post_cmd = [] +pre_cmd = [] +rerun = false +rerun_delay = 500 +send_interrupt = false +stop_on_error = false + +[color] +app = "" +build = "yellow" +main = "magenta" +runner = "green" +watcher = "cyan" + +[log] +main_only = false +time = false + +[misc] +clean_on_exit = false + +[proxy] +enabled = true +app_port = 3001 +proxy_port = 3000 + +[screen] +clear_on_rebuild = false +keep_scroll = true diff --git a/README.md b/README.md index 9a73d2c..5959c05 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,34 @@ GOBL HTML uses [templ](https://templ.guide/) to define a set of components in Go templ generate ``` -During development, it can help massive to have hot reload to be able to make changes and see them quickly. You can do this using the following example command: +During development, it can help massive to have hot reload to be able to make changes and see them quickly. There are two mechanisms we're currently using: + +#### Air + +Air is a great tool to auto reload potentially any project, but works great with Go. Install with: + +```bash +go install github.com/cosmtrek/air@latest +``` + +The `.toml` is already configured and ready in this repository, so simply run: + +```bash +air +``` + +Air is a bit more reliable at detecting file changes, especially for stylesheets. It's configured to offer a proxy with auto-reload, but of course that will only work at the moment when viewing HTML documents, not PDFs. You'll always need to wait a few seconds before page reloads to give the system chance to recompile. + +#### Templ Watcher + +Templ comes with a watch flag that can also be useful. It has the disadvantage however that it uses `.txt` files for comparisons and the generated code should not be uploaded to git directly. Start the process with: ```bash templ generate --watch --cmd="go run ./cmd/gobl.html serve --pdf prince" ``` +Before uploading changes to git, be sure to re-run the regular `templ generate` command, as the live version makes temporary modifications to files that need to be replaced. + ### Testing Tests are currently pretty limited. To ensure the basics are covered, the contents of the `examples` directory are converted to HTML, pretty printed, and output to the `examples/out` directory. The tests will ensure the output is as expected. To update the output test data run: diff --git a/assets/assets.go b/assets/assets.go new file mode 100644 index 0000000..293b662 --- /dev/null +++ b/assets/assets.go @@ -0,0 +1,9 @@ +// Package assets contains the static resources for things like styles. +package assets + +import "embed" + +//go:embed styles + +// Content stores all the asset contents. +var Content embed.FS diff --git a/assets/styles/envelope.css b/assets/styles/envelope.css new file mode 100644 index 0000000..5d2fc97 --- /dev/null +++ b/assets/styles/envelope.css @@ -0,0 +1,98 @@ +@page { + font-family: "Inter", sans-serif; + size: A4 portrait; + margin: 10mm; + margin-bottom: 20mm; + counter-increment: page; + font-size: 9pt; + line-height: 1.5em; + color: #4B5563; +} +body { + font-family: "Inter", sans-serif; + font-size: 9pt; + line-height: 1.5em; + margin: 0; + margin-bottom: 0mm; + /* color: #030712; */ + /* color: #6B7280; */ + color: #4B5563; +} +@media screen { + body { + margin: 10mm; + } + footer.screen { + display: block; + } + footer.print { + display: none; + } +} +article.envelope { + min-height: 100%; +} +article, section, header, div, h1, h2, h3, h4, table, th, td, p { + font-size: 1em; + margin: 0; +} +h1, h2, h3, h4 { + font-weight: 500; + /* color: #030712; */ +} +a { + color: #4B5563; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +th, td { + text-align: left; +} +section.footer span.page-number:before { + content: counter(page); +} +@media print { + footer.screen { + display: none; + } + footer.print { + display: block; + } +} +@page { + @bottom { + content: element(footer); + } +} +footer { + height: 12mm; + text-align: right; + padding: 4mm 0mm; +} +footer.print { + position: running(footer); +} +footer.screen { + padding: 2mm 0mm; +} +footer .page-number { + content: counter(page); +} +footer .pages-number { + content: counter(pages); +} +footer .gobl-logo { + float: left; +} +footer.print .notes:after { + content: " · "; +} +footer .gobl-logo img { + width: 8mm; + height: 8mm; +} +footer.print .gobl-logo { + margin-top: -7mm; +} \ No newline at end of file diff --git a/assets/styles/invoice.css b/assets/styles/invoice.css new file mode 100644 index 0000000..a2fbe46 --- /dev/null +++ b/assets/styles/invoice.css @@ -0,0 +1,276 @@ +header { + width: 100%; + display: flex; + flex-flow: row wrap; + border-bottom: 0.5px solid #E5E7EB; +} +header div.details { + flex: 1; + border-right: 0.5px solid #E5E7EB; +} +header div.contacts { + flex: 1; + /*display: flex; + flex-flow: column wrap;*/ +} +section { + padding: 0mm; + /* width: 100%; */ + flex: 100%; +} +section.title { + padding-top: 0mm; + padding-bottom: 6mm; +} +section.title .hero { + margin-bottom: 6mm; +} +section.title .alias { + font-size: 16pt; + font-weight: bold; + color: #030712; +} +section.title .hero img { + max-width: 100%; + /* max-height: 10mm; */ +} +section.title h2.code { + font-size: 12pt; + color: #030712; + font-weight: 600; +} +section.summary { + padding-right: 6mm; + padding-top: 0mm; + padding-bottom: 0mm; +} +section.summary h2.title { + display: none; +} +section.summary ul { + list-style-type: none; + margin: 0; + padding: 0; + display: flex; + flex-flow: row wrap; +} +section.summary li { + margin-bottom: 6mm; + flex: 50%; +} +section.summary span.label { + display: block; + font-weight: 500; +} +section.summary span.value { + display: block; + color: #030712; +} +section.supplier { + padding: 6mm; + padding-top: 0mm; +} +section.customer { + padding: 6mm; + border-top: 0.5px solid #E5E7EB; +} +section.lines { + order: 5; + padding-top: 6mm; + padding-bottom: 0mm; +} +section.lines h2 { + display: none; +} +section.lines table { + width: 100%; +} +section.lines th, section.lines td { + text-align: left; + border-bottom: 0.5px solid #E5E7EB; + padding: 1.5mm 2mm; + margin: 0px; +} +section.lines th { + font-weight: 600; + background-color: #F3F4F6; +} +section.lines .i, +section.lines .ref { + width: fit-content; +} +section.lines .price, +section.lines .quantity, +section.lines .tax, +section.lines .discount, +section.lines .charge, +section.lines .total { + text-align: right; + width: fit-content; + white-space: nowrap; +} +section.lines .description, +section.lines .reason { + white-space: wrap; +} +section.lines .label { + font-weight: 500; +} +section.lines .description .extensions { + display: block; +} +section.lines .description .extensions .label { + font-weight: 500; +} +section.lines .alt-price { + display: block; + font-style: italic; +} +section.lines .alt-price:before { + content: '('; +} +section.lines .alt-price:after { + content: ')'; +} +div.totals { + order: 8; + display: flex; + flex-wrap: row; + width: 100%; + padding-top: 6mm; + border-bottom: 0.5px solid #E5E7EB; + padding-bottom: 4mm; +} +div.totals td, div.totals th { + padding: 1.5mm 2mm; +} +section.totals { + order: 2; + flex: 1; +} +section.totals h2 { + display: none; +} +section.totals table { + padding-right: 1cm; + padding-left: 1cm; + width: auto; + margin-right: 0; + margin-left: auto; +} +section.totals th { + text-align: right; + border-bottom: 0.5px solid #E5E7EB; + font-weight: normal; +} +section.totals td { + text-align: right; + border-bottom: 0.5px solid #E5E7EB; +} +section.totals tr:last-child th, +section.totals tr:last-child td { + border-bottom: 0; +} +section.totals tr.strong th, +section.totals tr.strong td { + font-weight: 600; +} +section.totals .exchange-rate { + font-style: italic; +} +section.taxes { + order: 1; + flex: 1; +} +section.taxes h2 { + display: none; +} +section.taxes table { + padding-right: 1cm; + padding-left: 1cm; + margin-left: 0; + width: 90%; +} +section.taxes th, section.taxes td { + text-align: right; + padding: 1.5mm 2mm; +} +section.taxes th { + background-color: #F3F4F6; + font-weight: 600; +} +section.taxes td { + border-bottom: 0.5px solid #E5E7EB; +} +section.taxes tr:last-child td { + border-bottom: 0; +} +section.taxes th.category, +section.taxes td.category { + text-align: left; +} +section.payment { + border-bottom: 0.5px solid #E5E7EB; + padding: 6mm 0mm; + order: 10; + display: flex; + flex-flow: row; + break-inside: avoid; +} +section.payment h2.title { + display: none; +} +section.payment ul { + list-style-type: none; + margin: 0; + padding: 0; +} +section.payment ul.instructions { + margin-right: 6mm; +} +section.payment li { + margin-bottom: 3mm; +} +section.payment li:last-child { + margin-bottom: 0; +} +section.payment span.label { + display: block; + font-weight: 500; +} +section.payment span.value { + display: block; +} +section.payment table td { + padding-right: 3mm; +} +section.payment .due-dates span.percent:before { + content: "(" +} +section.payment .due-dates span.percent:after { + content: ")" +} +section.notes { + padding-top: 6mm; + padding-bottom: 3mm; + border-bottom: 0.5px solid #E5E7EB; + break-inside: avoid; +} +section.notes .note { + margin-bottom: 3mm; +} +.extensions section { + break-inside: avoid; + padding-top: 6mm; + padding-bottom: 6mm; + border-bottom: 0.5px solid #E5E7EB; +} +.extensions:empty { + display: none; +} +.org-party .name { + font-weight: 600; + font-size: 12pt; + line-height: 20pt; + color: #030712; +} \ No newline at end of file diff --git a/cmd/gobl.html/serve.go b/cmd/gobl.html/serve.go index c18eda6..94b6dc9 100644 --- a/cmd/gobl.html/serve.go +++ b/cmd/gobl.html/serve.go @@ -14,6 +14,7 @@ import ( "github.com/invopop/ctxi18n/i18n" "github.com/invopop/gobl" goblhtml "github.com/invopop/gobl.html" + "github.com/invopop/gobl.html/assets" "github.com/invopop/gobl.html/pkg/pdf" "github.com/invopop/gobl/org" "github.com/labstack/echo/v4" @@ -63,6 +64,7 @@ func (s *serveOpts) runE(cmd *cobra.Command, _ []string) error { e := echo.New() + e.StaticFS("/styles", echo.MustSubFS(assets.Content, "styles")) e.GET("/:filename", s.generate) var startErr error @@ -84,12 +86,11 @@ func (s *serveOpts) runE(cmd *cobra.Command, _ []string) error { return startErr } -func (s *serveOpts) render(c echo.Context, req *options, env *gobl.Envelope) ([]byte, error) { +func (s *serveOpts) render(c echo.Context, req *options, env *gobl.Envelope, opts []goblhtml.Option) ([]byte, error) { ctx := c.Request().Context() var err error // Prepare the request options - opts := make([]goblhtml.Option, 0) if req.DateFormat != "" { opts = append(opts, goblhtml.WithCalFormatter(req.DateFormat, "", time.UTC)) } @@ -143,7 +144,11 @@ func (s *serveOpts) generate(c echo.Context) error { return fmt.Errorf("unmarshalling file: %w", err) } - data, err := s.render(c, req, env) + opts := make([]goblhtml.Option, 0) + if ext == ".pdf" { + opts = append(opts, goblhtml.WithEmbeddedStylesheets()) + } + data, err := s.render(c, req, env, opts) if err != nil { return err } diff --git a/components/bill/invoice/invoice.templ b/components/bill/invoice/invoice.templ index d6a9a16..9e265fb 100644 --- a/components/bill/invoice/invoice.templ +++ b/components/bill/invoice/invoice.templ @@ -16,271 +16,6 @@ import ( // Invoice renders a complete GOBL bill.Invoice object. templ Invoice(env *gobl.Envelope, inv *bill.Invoice) { - // Footer needs to be at the top to show on all pages
diff --git a/components/bill/invoice/invoice_templ.go b/components/bill/invoice/invoice_templ.go index aef21de..b344280 100644 --- a/components/bill/invoice/invoice_templ.go +++ b/components/bill/invoice/invoice_templ.go @@ -38,7 +38,7 @@ func Invoice(env *gobl.Envelope, inv *bill.Invoice) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -177,7 +177,7 @@ func title(inv *bill.Invoice) templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(supplierAlias(inv)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/invoice.templ`, Line: 320, Col: 25} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/invoice.templ`, Line: 55, Col: 25} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -203,7 +203,7 @@ func title(inv *bill.Invoice) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(code(inv.Series, inv.Code)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/invoice.templ`, Line: 328, Col: 31} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/invoice.templ`, Line: 63, Col: 31} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { diff --git a/components/bill/invoice/lines.templ b/components/bill/invoice/lines.templ index 950d3d3..99b43e6 100644 --- a/components/bill/invoice/lines.templ +++ b/components/bill/invoice/lines.templ @@ -115,6 +115,7 @@ templ line(_ *bill.Invoice, l *bill.Line, ls *lineSupport) { } @t.LM(l.Item.Price) + @lineItemAltPrices(l) for _, cat := range ls.categories { @@ -145,6 +146,14 @@ templ line(_ *bill.Invoice, l *bill.Line, ls *lineSupport) { } +templ lineItemAltPrices(l *bill.Line) { + for _, a := range l.Item.AltPrices { + + @t.LCD(a.Value, a.Currency) + + } +} + templ lineGroupDiscounts(l *bill.Line) { for _, d := range l.Discounts { if d.Percent != nil { diff --git a/components/bill/invoice/lines_templ.go b/components/bill/invoice/lines_templ.go index 13616d2..d138230 100644 --- a/components/bill/invoice/lines_templ.go +++ b/components/bill/invoice/lines_templ.go @@ -408,6 +408,10 @@ func line(_ *bill.Invoice, l *bill.Line, ls *lineSupport) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + templ_7745c5c3_Err = lineItemAltPrices(l).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -487,7 +491,7 @@ func line(_ *bill.Invoice, l *bill.Line, ls *lineSupport) templ.Component { }) } -func lineGroupDiscounts(l *bill.Line) templ.Component { +func lineItemAltPrices(l *bill.Line) templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) if !templ_7745c5c3_IsBuffer { @@ -500,6 +504,40 @@ func lineGroupDiscounts(l *bill.Line) templ.Component { templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) + for _, a := range l.Item.AltPrices { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = t.LCD(a.Value, a.Currency).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func lineGroupDiscounts(l *bill.Line) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) for _, d := range l.Discounts { if d.Percent != nil { templ_7745c5c3_Err = t.L(*d.Percent).Render(ctx, templ_7745c5c3_Buffer) @@ -528,9 +566,9 @@ func lineGroupCharges(l *bill.Line) templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var13 := templ.GetChildren(ctx) - if templ_7745c5c3_Var13 == nil { - templ_7745c5c3_Var13 = templ.NopComponent + templ_7745c5c3_Var14 := templ.GetChildren(ctx) + if templ_7745c5c3_Var14 == nil { + templ_7745c5c3_Var14 = templ.NopComponent } ctx = templ.ClearChildren(ctx) for _, c := range l.Charges { @@ -561,9 +599,9 @@ func discountsBody(inv *bill.Invoice, ls *lineSupport) templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var14 := templ.GetChildren(ctx) - if templ_7745c5c3_Var14 == nil { - templ_7745c5c3_Var14 = templ.NopComponent + templ_7745c5c3_Var15 := templ.GetChildren(ctx) + if templ_7745c5c3_Var15 == nil { + templ_7745c5c3_Var15 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if len(inv.Discounts) > 0 { @@ -597,21 +635,21 @@ func discountRow(row *bill.Discount, ls *lineSupport) templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var15 := templ.GetChildren(ctx) - if templ_7745c5c3_Var15 == nil { - templ_7745c5c3_Var15 = templ.NopComponent + templ_7745c5c3_Var16 := templ.GetChildren(ctx) + if templ_7745c5c3_Var16 == nil { + templ_7745c5c3_Var16 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("D%d", row.Index)) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("D%d", row.Index)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 180, Col: 34} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 189, Col: 34} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -625,12 +663,12 @@ func discountRow(row *bill.Discount, ls *lineSupport) templ.Component { return templ_7745c5c3_Err } if row.Ref != "" { - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(row.Ref) + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(row.Ref) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 185, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 194, Col: 14} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -649,12 +687,12 @@ func discountRow(row *bill.Discount, ls *lineSupport) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(row.Reason) + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(row.Reason) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 192, Col: 15} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 201, Col: 15} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -747,9 +785,9 @@ func chargesBody(inv *bill.Invoice, ls *lineSupport) templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var19 := templ.GetChildren(ctx) - if templ_7745c5c3_Var19 == nil { - templ_7745c5c3_Var19 = templ.NopComponent + templ_7745c5c3_Var20 := templ.GetChildren(ctx) + if templ_7745c5c3_Var20 == nil { + templ_7745c5c3_Var20 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if len(inv.Charges) > 0 { @@ -783,21 +821,21 @@ func chargeRow(row *bill.Charge, ls *lineSupport) templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var20 := templ.GetChildren(ctx) - if templ_7745c5c3_Var20 == nil { - templ_7745c5c3_Var20 = templ.NopComponent + templ_7745c5c3_Var21 := templ.GetChildren(ctx) + if templ_7745c5c3_Var21 == nil { + templ_7745c5c3_Var21 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("C%d", row.Index)) + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("C%d", row.Index)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 247, Col: 34} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 256, Col: 34} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -811,12 +849,12 @@ func chargeRow(row *bill.Charge, ls *lineSupport) templ.Component { return templ_7745c5c3_Err } if row.Ref != "" { - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(row.Ref) + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(row.Ref) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 252, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 261, Col: 14} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -835,12 +873,12 @@ func chargeRow(row *bill.Charge, ls *lineSupport) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(row.Reason) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(row.Reason) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 259, Col: 15} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/bill/invoice/lines.templ`, Line: 268, Col: 15} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/components/bill/invoice/totals.templ b/components/bill/invoice/totals.templ index fb44365..1091e58 100644 --- a/components/bill/invoice/totals.templ +++ b/components/bill/invoice/totals.templ @@ -4,6 +4,7 @@ import ( "fmt" "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/currency" "github.com/invopop/gobl.html/components/t" "github.com/invopop/ctxi18n/i18n" "github.com/invopop/gobl/pay" @@ -124,6 +125,16 @@ templ totalsPayableRows(inv *bill.Invoice, totals *bill.Totals) { @t.LM(totals.Payable) + for _, er := range totalExchangeRates(inv) { + + + @t.T(".exchange_rate", i18n.M{"to": er.To, "amount": t.Localize(ctx, er.Amount)}) + + + @t.LCD(er.Convert(totals.Payable), er.To) + + + } } templ totalsDueRows(inv *bill.Invoice, totals *bill.Totals) { @@ -159,6 +170,16 @@ templ totalsDueRows(inv *bill.Invoice, totals *bill.Totals) { } } +func totalExchangeRates(inv *bill.Invoice) []*currency.ExchangeRate { + list := make([]*currency.ExchangeRate, 0, len(inv.ExchangeRates)) + for _, er := range inv.ExchangeRates { + if er.From == inv.Currency { + list = append(list, er) + } + } + return list +} + func advanceMap(adv *pay.Advance) i18n.M { txt := adv.Description if adv.Ref != "" { diff --git a/components/bill/invoice/totals_templ.go b/components/bill/invoice/totals_templ.go index 05f14ac..4313669 100644 --- a/components/bill/invoice/totals_templ.go +++ b/components/bill/invoice/totals_templ.go @@ -16,6 +16,7 @@ import ( "github.com/invopop/ctxi18n/i18n" "github.com/invopop/gobl.html/components/t" "github.com/invopop/gobl/bill" + "github.com/invopop/gobl/currency" "github.com/invopop/gobl/pay" ) @@ -330,6 +331,28 @@ func totalsPayableRows(inv *bill.Invoice, totals *bill.Totals) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + for _, er := range totalExchangeRates(inv) { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = t.T(".exchange_rate", i18n.M{"to": er.To, "amount": t.Localize(ctx, er.Amount)}).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = t.LCD(er.Convert(totals.Payable), er.To).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } if !templ_7745c5c3_IsBuffer { _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) } @@ -427,6 +450,16 @@ func totalsDueRows(inv *bill.Invoice, totals *bill.Totals) templ.Component { }) } +func totalExchangeRates(inv *bill.Invoice) []*currency.ExchangeRate { + list := make([]*currency.ExchangeRate, 0, len(inv.ExchangeRates)) + for _, er := range inv.ExchangeRates { + if er.From == inv.Currency { + list = append(list, er) + } + } + return list +} + func advanceMap(adv *pay.Advance) i18n.M { txt := adv.Description if adv.Ref != "" { diff --git a/components/envelope.templ b/components/envelope.templ index 27ab13a..e15a988 100644 --- a/components/envelope.templ +++ b/components/envelope.templ @@ -1,11 +1,17 @@ package components import ( + "fmt" + "io/fs" + "path/filepath" + "github.com/invopop/gobl" "github.com/invopop/gobl/bill" "github.com/invopop/gobl/note" "github.com/invopop/gobl.html/components/bill/invoice" "github.com/invopop/gobl.html/components/notes" + "github.com/invopop/gobl.html/assets" + "github.com/invopop/gobl.html/internal" ) templ Envelope(env *gobl.Envelope) { @@ -17,57 +23,7 @@ templ Envelope(env *gobl.Envelope) { - + @stylesheets()
@@ -86,3 +42,53 @@ templ Envelope(env *gobl.Envelope) { } + +templ stylesheets() { + if opts := internal.Options(ctx); opts != nil { + if opts.EmbedStylesheets { + for _, data := range stylesheetData() { + @templ.Raw(``) + } + } else { + for _, ss := range stylesheetFilenames() { + + } + } + } +} + +// stylesheetFilenames just provides the filenames of all the styles +func stylesheetFilenames() []string { + list := []string{} + err := fs.WalkDir(assets.Content, "styles", func(path string, _ fs.DirEntry, _ error) error { + if filepath.Ext(path) != ".css" { + return nil + } + list = append(list, path) + return nil + }) + if err != nil { + panic(err) + } + return list +} + +// stylesheetData provides a list of data objects of all the stylesheets +func stylesheetData() []string { + list := make([]string, 0) + err := fs.WalkDir(assets.Content, "styles", func(path string, _ fs.DirEntry, _ error) error { + if filepath.Ext(path) != ".css" { + return nil + } + data, err := fs.ReadFile(assets.Content, path) + if err != nil { + return fmt.Errorf("reading file: %w", err) + } + list = append(list, string(data)) + return nil + }) + if err != nil { + panic(err) + } + return list +} diff --git a/components/envelope_templ.go b/components/envelope_templ.go index ddad77e..d0e868d 100644 --- a/components/envelope_templ.go +++ b/components/envelope_templ.go @@ -11,9 +11,15 @@ import "io" import "bytes" import ( + "fmt" + "io/fs" + "path/filepath" + "github.com/invopop/gobl" + "github.com/invopop/gobl.html/assets" "github.com/invopop/gobl.html/components/bill/invoice" "github.com/invopop/gobl.html/components/notes" + "github.com/invopop/gobl.html/internal" "github.com/invopop/gobl/bill" "github.com/invopop/gobl/note" ) @@ -31,7 +37,15 @@ func Envelope(env *gobl.Envelope) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("GOBL HTML Generator
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("GOBL HTML Generator") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = stylesheets().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -70,3 +84,84 @@ func Envelope(env *gobl.Envelope) templ.Component { return templ_7745c5c3_Err }) } + +func stylesheets() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if opts := internal.Options(ctx); opts != nil { + if opts.EmbedStylesheets { + for _, data := range stylesheetData() { + templ_7745c5c3_Err = templ.Raw(``).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } else { + for _, ss := range stylesheetFilenames() { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +// stylesheetFilenames just provides the filenames of all the styles +func stylesheetFilenames() []string { + list := []string{} + err := fs.WalkDir(assets.Content, "styles", func(path string, _ fs.DirEntry, _ error) error { + if filepath.Ext(path) != ".css" { + return nil + } + list = append(list, path) + return nil + }) + if err != nil { + panic(err) + } + return list +} + +// stylesheetData provides a list of data objects of all the stylesheets +func stylesheetData() []string { + list := make([]string, 0) + err := fs.WalkDir(assets.Content, "styles", func(path string, _ fs.DirEntry, _ error) error { + if filepath.Ext(path) != ".css" { + return nil + } + data, err := fs.ReadFile(assets.Content, path) + if err != nil { + return fmt.Errorf("reading file: %w", err) + } + list = append(list, string(data)) + return nil + }) + if err != nil { + panic(err) + } + return list +} diff --git a/components/footer.templ b/components/footer.templ index 729ad73..d87c76e 100644 --- a/components/footer.templ +++ b/components/footer.templ @@ -8,61 +8,6 @@ import ( ) templ footerPrint(env *gobl.Envelope) { -
@footerNotes(env) @footerPage() diff --git a/components/footer_templ.go b/components/footer_templ.go index f4e49b9..f3fb94d 100644 --- a/components/footer_templ.go +++ b/components/footer_templ.go @@ -30,7 +30,7 @@ func footerPrint(env *gobl.Envelope) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/components/t/i18n.templ b/components/t/i18n.templ index 21bbaa0..30b9cab 100644 --- a/components/t/i18n.templ +++ b/components/t/i18n.templ @@ -37,6 +37,12 @@ templ LC(a num.Amount, cur currency.Code) { { localizeCurrency(ctx, a, cur) } } +// LCD localizes and amount using the formatting defined by the specified currency, +// and uses the disembiguate symbol. +templ LCD(a num.Amount, cur currency.Code) { + { localizeCurrency(ctx, a, cur, currency.WithDisambiguateSymbol()) } +} + // Scope helps set a scope around the context templ Scope(key string) { if ctx = i18n.WithScope(ctx, key); true { @@ -76,7 +82,7 @@ func localizeMoney(ctx context.Context, a num.Amount) string { return f.Amount(a) } -func localizeCurrency(_ context.Context, a num.Amount, cur currency.Code) string { - f := cur.Def().Formatter() +func localizeCurrency(_ context.Context, a num.Amount, cur currency.Code, opts ...currency.FormatOption) string { + f := cur.Def().Formatter(opts...) return f.Amount(a) } diff --git a/components/t/i18n_templ.go b/components/t/i18n_templ.go index e1b6972..31d862f 100644 --- a/components/t/i18n_templ.go +++ b/components/t/i18n_templ.go @@ -172,8 +172,9 @@ func LC(a num.Amount, cur currency.Code) templ.Component { }) } -// Scope helps set a scope around the context -func Scope(key string) templ.Component { +// LCD localizes and amount using the formatting defined by the specified currency, +// and uses the disembiguate symbol. +func LCD(a num.Amount, cur currency.Code) templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) if !templ_7745c5c3_IsBuffer { @@ -186,8 +187,38 @@ func Scope(key string) templ.Component { templ_7745c5c3_Var11 = templ.NopComponent } ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(localizeCurrency(ctx, a, cur, currency.WithDisambiguateSymbol())) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/t/i18n.templ`, Line: 42, Col: 67} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +// Scope helps set a scope around the context +func Scope(key string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) if ctx = i18n.WithScope(ctx, key); true { - templ_7745c5c3_Err = templ_7745c5c3_Var11.Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = templ_7745c5c3_Var13.Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -231,7 +262,7 @@ func localizeMoney(ctx context.Context, a num.Amount) string { return f.Amount(a) } -func localizeCurrency(_ context.Context, a num.Amount, cur currency.Code) string { - f := cur.Def().Formatter() +func localizeCurrency(_ context.Context, a num.Amount, cur currency.Code, opts ...currency.FormatOption) string { + f := cur.Def().Formatter(opts...) return f.Amount(a) } diff --git a/examples/invoice-es-usd.json b/examples/invoice-es-usd.json new file mode 100644 index 0000000..4489556 --- /dev/null +++ b/examples/invoice-es-usd.json @@ -0,0 +1,166 @@ +{ + "$schema": "https://gobl.org/draft-0/envelope", + "head": { + "uuid": "8a51fd30-2a27-11ee-be56-0242ac120002", + "dig": { + "alg": "sha256", + "val": "b4e373834c0deb0f03739ff40a38e029bc6f915fde62c44f2f739ebbdfd013ab" + }, + "draft": true + }, + "doc": { + "$schema": "https://gobl.org/draft-0/bill/invoice", + "uuid": "3aea7b56-59d8-4beb-90bd-f8f280d852a0", + "type": "standard", + "series": "EXPORT", + "code": "001", + "issue_date": "2024-05-09", + "currency": "USD", + "exchange_rates": [ + { + "from": "USD", + "to": "EUR", + "amount": "0.875967" + }, + { + "from": "MXN", + "to": "USD", + "amount": "0.059197" + } + ], + "supplier": { + "name": "Provide One S.L.", + "tax_id": { + "country": "ES", + "code": "B98602642" + }, + "addresses": [ + { + "num": "42", + "street": "Calle Pradillo", + "locality": "Madrid", + "region": "Madrid", + "code": "28002", + "country": "ES" + } + ], + "emails": [ + { + "addr": "billing@example.com" + } + ] + }, + "customer": { + "name": "Sample Consumer Inc.", + "tax_id": { + "country": "US" + } + }, + "lines": [ + { + "i": 1, + "quantity": "20", + "item": { + "name": "Development services from Spain", + "currency": "USD", + "price": "100.00", + "alt_prices": [ + { + "currency": "EUR", + "value": "90.00" + } + ], + "unit": "h" + }, + "sum": "2000.00", + "discounts": [ + { + "percent": "10%", + "amount": "200.00", + "reason": "Special discount" + } + ], + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "21.0%" + } + ], + "total": "1800.00" + }, + { + "i": 2, + "quantity": "10", + "item": { + "name": "Development services from Mexico", + "currency": "USD", + "price": "88.80", + "alt_prices": [ + { + "currency": "MXN", + "value": "1500.00" + } + ], + "unit": "h" + }, + "sum": "888.00", + "taxes": [ + { + "cat": "VAT", + "rate": "standard", + "percent": "21.0%" + } + ], + "total": "888.00" + }, + { + "i": 3, + "quantity": "1", + "item": { + "name": "Financial service", + "price": "10.00" + }, + "sum": "10.00", + "taxes": [ + { + "cat": "VAT", + "rate": "zero", + "percent": "0.0%" + } + ], + "total": "10.00" + } + ], + "totals": { + "sum": "2698.00", + "total": "2698.00", + "taxes": { + "categories": [ + { + "code": "VAT", + "rates": [ + { + "key": "standard", + "base": "2688.00", + "percent": "21.0%", + "amount": "564.48" + }, + { + "key": "zero", + "base": "10.00", + "percent": "0.0%", + "amount": "0.00" + } + ], + "amount": "564.48" + } + ], + "sum": "564.48" + }, + "tax": "564.48", + "total_with_tax": "3262.48", + "payable": "3262.48" + } + } +} \ No newline at end of file diff --git a/examples/out/credit-note-es-es-tbai.html b/examples/out/credit-note-es-es-tbai.html index c809955..da34a90 100644 --- a/examples/out/credit-note-es-es-tbai.html +++ b/examples/out/credit-note-es-es-tbai.html @@ -8,115 +8,11 @@ - + +
-
Page @@ -134,271 +30,6 @@
-
diff --git a/examples/out/full-invoice.html b/examples/out/full-invoice.html index fcafc9f..2ee94d2 100644 --- a/examples/out/full-invoice.html +++ b/examples/out/full-invoice.html @@ -8,115 +8,11 @@ - + +
-
Page @@ -134,271 +30,6 @@
-
@@ -477,7 +108,7 @@

LEI: 1010101010

- Identitiy code: ABC1234 + Identity code: ABC1234
diff --git a/examples/out/invoice-es-simplified.html b/examples/out/invoice-es-simplified.html index a864b6a..e54b7c3 100644 --- a/examples/out/invoice-es-simplified.html +++ b/examples/out/invoice-es-simplified.html @@ -8,115 +8,11 @@ - + +
-
Page @@ -134,271 +30,6 @@
-
diff --git a/examples/out/invoice-es-ticketbai.html b/examples/out/invoice-es-ticketbai.html index 8150622..20125e4 100644 --- a/examples/out/invoice-es-ticketbai.html +++ b/examples/out/invoice-es-ticketbai.html @@ -8,115 +8,11 @@ - + +
-
Page @@ -134,271 +30,6 @@
-
diff --git a/examples/out/invoice-es-usd.html b/examples/out/invoice-es-usd.html new file mode 100644 index 0000000..613591a --- /dev/null +++ b/examples/out/invoice-es-usd.html @@ -0,0 +1,334 @@ + + + + GOBL HTML Generator + + + + + + + + + + +
+
+ + Page + + 1 + + of + + 1 + + + +
+
+
+
+
+
+
+ Provide One S.L. +
+
+

+ Invoice +

+

+ EXPORT-001 +

+
+
+

+ Summary +

+
    +
  • + + Issue Date + + + 2024-05-09 + +
  • +
  • + + Currency + + + United States Dollar (USD) + +
  • +
+
+
+
+
+

+ Supplier +

+
+
+ Provide One S.L. +
+
+ + Calle Pradillo 42, Madrid, Madrid, 28002 (Spain) + +
+
+ Email: billing@example.com +
+
+ NIF: (ES) B98602642 +
+
+
+
+

+ Customer +

+
+
+ Sample Consumer Inc. +
+
+
+
+
+
+

+ Lines +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ # + + Description + + Qty. + + Unit + + Price + + VAT + + Disc. + + Total +
+ 1 + + + Development services from Spain + + + 20 + + h + + $100.00 + + €90,00 + + + 21.0% + + 10% + + $1,800.00 +
+ 2 + + + Development services from Mexico + + + 10 + + h + + $88.80 + + MEX$1,500.00 + + + 21.0% + + $888.00 +
+ 3 + + + Financial service + + + 1 + + + + $10.00 + + 0.0% + + $10.00 +
+
+
+
+

+ Totals +

+ + + + + + + + + + + + + + + + + + + +
+ Sum + + $2,698.00 +
+ Tax + + $564.48 +
+ Total to pay + + $3,262.48 +
+ Exchange to EUR (0.875967) + + €2.857,82 +
+
+
+

+ Taxes +

+ + + + + + + + + + + + + + + + + + + + + + +
+ Tax + + Base + + Rate + + Amount +
+ VAT + + $2,688.00 + + 21.0% + + $564.48 +
+ $10.00 + + 0.0% + + $0.00 +
+
+
+
+
+ +
+ + \ No newline at end of file diff --git a/examples/out/invoice-limited-company.html b/examples/out/invoice-limited-company.html index f46076d..e30b84e 100644 --- a/examples/out/invoice-limited-company.html +++ b/examples/out/invoice-limited-company.html @@ -8,115 +8,11 @@ - + +
-
Page @@ -134,271 +30,6 @@
-
diff --git a/examples/out/invoice-mx.html b/examples/out/invoice-mx.html index 220efe6..4b278c6 100644 --- a/examples/out/invoice-mx.html +++ b/examples/out/invoice-mx.html @@ -8,115 +8,11 @@ - + +
-
Page @@ -134,271 +30,6 @@
-
diff --git a/examples/out/invoice-tax-included.html b/examples/out/invoice-tax-included.html index 651d9d1..5b4eae6 100644 --- a/examples/out/invoice-tax-included.html +++ b/examples/out/invoice-tax-included.html @@ -8,115 +8,11 @@ - + +
-
Page @@ -134,271 +30,6 @@
-
diff --git a/examples/out/invoice.env.html b/examples/out/invoice.env.html index f0901e1..137793e 100644 --- a/examples/out/invoice.env.html +++ b/examples/out/invoice.env.html @@ -8,115 +8,11 @@ - + +
-
Page @@ -134,271 +30,6 @@
-
diff --git a/examples/out/mx-food-voucher.html b/examples/out/mx-food-voucher.html index a8f34b1..d31ac44 100644 --- a/examples/out/mx-food-voucher.html +++ b/examples/out/mx-food-voucher.html @@ -8,115 +8,11 @@ - + +
-
Page @@ -134,271 +30,6 @@
-
diff --git a/examples/out/mx-fuel-balance.html b/examples/out/mx-fuel-balance.html index 3982692..f56cee7 100644 --- a/examples/out/mx-fuel-balance.html +++ b/examples/out/mx-fuel-balance.html @@ -8,115 +8,11 @@ - + +
-
Page @@ -134,271 +30,6 @@
-
diff --git a/examples/out/us-invoice.html b/examples/out/us-invoice.html index 35b6f9e..7b4409a 100644 --- a/examples/out/us-invoice.html +++ b/examples/out/us-invoice.html @@ -8,115 +8,11 @@ - + +
-
Page @@ -134,271 +30,6 @@
-
diff --git a/go.mod b/go.mod index 4619102..d9027e9 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/a-h/templ v0.2.598 github.com/go-resty/resty/v2 v2.12.0 github.com/invopop/ctxi18n v0.6.0 - github.com/invopop/gobl v0.75.0 + github.com/invopop/gobl v0.76.0 github.com/invopop/princepdf v0.0.0-20240408123340-585be3cab91a github.com/labstack/echo/v4 v4.11.4 github.com/piglig/go-qr v0.2.4 diff --git a/go.sum b/go.sum index 8279c45..541e25b 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/invopop/ctxi18n v0.6.0 h1:Qm3ZL/kK4EKvmLI3U2ETN2rWrtSTaxXrcA6ZUY9aVGE github.com/invopop/ctxi18n v0.6.0/go.mod h1:1Osw+JGYA+anHt0Z4reF36r5FtGHYjGQ+m1X7keIhPc= github.com/invopop/gobl v0.75.0 h1:QQpFTkraauZoGEGQlYqfHpJmBNayamtjT1+Onw8zVEA= github.com/invopop/gobl v0.75.0/go.mod h1:3ixShxX1jlOKo5Rw22HVQh3jXnK9AZa7Twcw7L92qn0= +github.com/invopop/gobl v0.76.0 h1:WHGJGe+sqljGkcifQMjxaiiYe8kne2bm3Yv4HUlSqOQ= +github.com/invopop/gobl v0.76.0/go.mod h1:3ixShxX1jlOKo5Rw22HVQh3jXnK9AZa7Twcw7L92qn0= github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/invopop/princepdf v0.0.0-20240408123340-585be3cab91a h1:xt18LlIfizLkFgLi+vK/m2SWOsAbQwVwQgbkzxKY0eU= diff --git a/goblhtml.go b/goblhtml.go index 58d70e5..9f5f03b 100644 --- a/goblhtml.go +++ b/goblhtml.go @@ -84,6 +84,14 @@ func WithNumFormatter(nf num.Formatter) Option { } } +// WithEmbeddedStylesheets indicates that the stylesheets should be embedded +// inside the HTML document as opposed to links. +func WithEmbeddedStylesheets() Option { + return func(o *internal.Opts) { + o.EmbedStylesheets = true + } +} + // Render takes the GOBL envelope and attempts to render an HTML document // from it. func Render(ctx context.Context, env *gobl.Envelope, opts ...Option) ([]byte, error) { diff --git a/internal/options.go b/internal/options.go index 6fbc382..9ff6844 100644 --- a/internal/options.go +++ b/internal/options.go @@ -32,6 +32,10 @@ type Opts struct { NumFormatter *num.Formatter // CalFormatter is used to format calendar dates and times. CalFormatter *CalFormatter + // EmbedStylesheets when try ensures that all the stylesheet files + // are contained inside the HTML output. This is useful for PDF + // output or to avoid additional requests. + EmbedStylesheets bool } // WithOptions prepares the context with the options to use. diff --git a/locales/de/app.yml b/locales/de/app.yml index 661947f..d639838 100644 --- a/locales/de/app.yml +++ b/locales/de/app.yml @@ -56,6 +56,7 @@ de: advance: "Vorauszahlung: %{date} %{txt}" advance_sum: "Summe der Vorauszahlungen" due_sum: "Gesamtbetrag fällig" + exchange_rate: "Umtausch in %{to} (%{amount})" taxes: title: "Steuern" diff --git a/locales/en/app.yml b/locales/en/app.yml index 1f0bcb0..5248ee7 100644 --- a/locales/en/app.yml +++ b/locales/en/app.yml @@ -58,6 +58,7 @@ en: advance: "Advance: %{date} %{txt}" advance_sum: "Sum advances" due_sum: "Total due" + exchange_rate: "Exchange to %{to} (%{amount})" taxes: title: "Taxes" @@ -128,7 +129,7 @@ en: other: "Emails: %{addr}" email_label: "%{addr} (%{label})" identity: "%{label}: %{code}" - identity_code: "Identitiy code" + identity_code: "Identity code" po_box: "P.O. Box %{po_box}" labels: default: "Tax Code" diff --git a/locales/es/app.yml b/locales/es/app.yml index 7257430..a8942a3 100644 --- a/locales/es/app.yml +++ b/locales/es/app.yml @@ -58,6 +58,7 @@ es: advance: "Anticipo: %{date} %{txt}" advance_sum: "Suma Anticipos" due_sum: "Total adeudado" + exchange_rate: "Cambio a %{to} (%{amount})" taxes: title: "Impuestos" diff --git a/locales/fr/app.yml b/locales/fr/app.yml index cc637fa..87b6577 100644 --- a/locales/fr/app.yml +++ b/locales/fr/app.yml @@ -50,6 +50,7 @@ fr: advance: "Avance: %{date} %{txt}" advance_sum: "Somme des avances" due_sum: "Total dû" + exchange_rate: "Échange en %{to} (%{amount})" taxes: title: "Taxes" category: "Taxe" diff --git a/locales/gr/app.yml b/locales/gr/app.yml new file mode 100644 index 0000000..c88fadc --- /dev/null +++ b/locales/gr/app.yml @@ -0,0 +1,144 @@ +gr: + page: "Σελίδα %{page} από %{count}" + + billing: + invoice: + title: + standard: "Τιμολόγιο" + standard-simplified: "Απλοποιημένο Τιμολόγιο" + proforma: "Προτιμολόγιο" + corrective: "Διορθωτικό Τιμολόγιο" + credit-note: "Πιστωτικό Σημείωμα" + debit-note: "Χρεωστικό Σημείωμα" + + summary: + title: "Περίληψη" + issue_date: "Ημερομηνία Έκδοσης" + value_date: "Ημερομηνία Αξίας" + operation_date: "Ημερομηνία Ενέργειας" + currency: "Νόμισμα" + currency_value: "%{desc} (%{code})" + preceding_invoice: "Προηγούμενο Τιμολόγιο" + order: "Παραγγελία" + order_period: "Περίοδος" + order_period_label: "%{label}" + order_period_range: "%{start} έως %{end}" + ext_map: + mx-cfdi-issue-place: "Τόπος Έκδοσης" + + supplier: + title: "Προμηθευτής" + + customer: + title: "Πελάτης" + + lines: + title: "Γραμμές" + i: "#" + ref: "Αναφ." + description: "Περιγραφή" + quantity: "Ποσ." + unit: "Μονάδα" + price: "Τιμή" + discount: "Έκπτωση" + charges: "Χρεώσεις" + total: "Σύνολο" + + totals: + sum: "Άθροισμα" + discount: "Έκπτωση" + charge: "Χρέωση" + title: "Σύνολα" + prices_include_tax: "Συμπεριλαμβάνεται φόρος" + total: "Σύνολο" + taxes: "Φόρος" + total_with_tax: "Σύνολο με φόρο" + total_to_pay: "Σύνολο προς πληρωμή" + outlay: "Έξοδος %{i}: %{txt}" + advance: "Προκαταβολή: %{date} %{txt}" + advance_sum: "Άθροισμα προκαταβολών" + due_sum: "Σύνολο οφειλόμενου" + exchange_rate: "Ισοτιμία προς %{to} (%{amount})" + + taxes: + title: "Φόροι" + category: "Φόρος" + base: "Βάση" + rate: "Ποσοστό" + amount: "Ποσό" + + payment: + title: "Πληρωμή" + instructions: + method: "Μέθοδος πληρωμής" + ref: "Αναφορά" + bank_data: "Οδηγίες τραπεζικής μεταφοράς" + card: "Πληρωμή με κάρτα" + card_detail: "Πληρωμή προς κάρτα με τελευταία 4 ψηφία %{last4} που ανήκει σε %{holder}" + observations: "Σημειώσεις πληρωμής" + bank_name: "Τράπεζα: %{name}" + iban: "IBAN: %{iban}" + account_number: "Αριθμός λογαριασμού: %{num}" + bic: "BIC: %{bic}" + methods: + card: "Κάρτα τραπέζης" + credit_transfer: "Τραπεζική μεταφορά" + cash: "Μετρητά" + direct_debit: "Άμεση πίστωση" + online: "Διαδικτυακά" + terms: + key: "Όροι πληρωμής" + notes: "Σημειώσεις όρων πληρωμής" + due_dates: "Ημερομηνίες πληρωμής" + keys: + na: "Δεν έχει καθοριστεί" + end_of_month: "Τέλος μήνα" + due_date: "Στην ημερομηνία πληρωμής" + deferred: "Μετά την ημερομηνία πληρωμής" + proximo: "Επόμενος μήνας" + instant: "Κατά την παράδοση του τιμολογίου" + elective: "Επιλεγμένος από τον πελάτη" + pending: "Να καθοριστεί από τον προμηθευτή" + advanced: "Πληρωμένο" + delivery: "Κατά την παράδοση" + + notes: + title: "Σημειώσεις" + reg: + book: "Βιβλίο %{id}" + volume: "Τόμος %{id}" + sheet: "Φύλλο %{id}" + section: "Ενότητα %{id}" + page: "Σελίδα %{id}" + entry: "Καταχώρηση %{id}" + inscription: "Εγγεγραμμένο σε:" + + regimes: + co: + cufe: "CUFE: %{cufe}" + cude: "CUDE: %{cude}" + preceding_cufe: "CUFE Προηγούμενου τιμολογίου: %{cufe}" + + organizing: + party: + tax_id: "%{label}: (%{country}) %{code}" + tel: "Τηλ: %{num}" + tel_label: "Τηλ: %{num} (%{label})" + email: + one: "Email: %{addr}" + other: "Emails: %{addr}" + email_label: "%{addr} (%{label})" + identity: "%{label}: %{code}" + identity_code: "Κωδικός Ταυτότητας" + po_box: "Τ.Θ. %{po_box}" + labels: + default: "Φορολογικός Κωδικός" + ext: "%{label}: %{value}" + ext_map: + co-dian-municipality: "Δήμος" + mx-cfdi-fiscal-regime: "Φορολογικό Καθεστώς" + mx-cfdi-post-code: "Τοποθεσία" + mx-cfdi-use: "Χρήση CFDI" + address: + label: "%{label}:" + country: "(%{country})" diff --git a/locales/gr/countries.yml b/locales/gr/countries.yml new file mode 100644 index 0000000..0a1f020 --- /dev/null +++ b/locales/gr/countries.yml @@ -0,0 +1,251 @@ +gr: + country_names: + AF: "Αφγανιστάν" + AX: "Νήσοι Ώλαντ" + AL: "Αλβανία" + DZ: "Αλγερία" + AS: "Αμερικανική Σαμόα" + AD: "Ανδόρα" + AO: "Ανγκόλα" + AI: "Ανγκουίλα" + AQ: "Ανταρκτική" + AG: "Αντίγκουα και Μπαρμπούντα" + AR: "Αργεντινή" + AM: "Αρμενία" + AW: "Αρούμπα" + AU: "Αυστραλία" + AT: "Αυστρία" + AZ: "Αζερμπαϊτζάν" + BS: "Μπαχάμες" + BH: "Μπαχρέιν" + BD: "Μπανγκλαντές" + BB: "Μπαρμπάντος" + BY: "Λευκορωσία" + BE: "Βέλγιο" + BZ: "Μπελίζ" + BJ: "Μπενίν" + BM: "Βερμούδες" + BT: "Μπουτάν" + BO: "Βολιβία, Πολυεθνική Κράτος" + BQ: "Μπονέρ, Σιντ Ευστάτιους και Σάμπα" + BA: "Βοσνία και Ερζεγοβίνη" + BW: "Μποτσουάνα" + BV: "Νήσος Μπουβέ" + BR: "Βραζιλία" + IO: "Βρετανική Ινδική Ωκεανία" + BN: "Μπρουνέι Δαρουσαλάμ" + BG: "Βουλγαρία" + BF: "Μπουρκίνα Φάσο" + BI: "Μπουρούντι" + CV: "Πράσινο Ακρωτήριο" + KH: "Καμπότζη" + CM: "Καμερούν" + CA: "Καναδάς" + KY: "Νήσοι Κέιμαν" + CF: "Κεντροαφρικανική Δημοκρατία" + TD: "Τσαντ" + CL: "Χιλή" + CN: "Κίνα" + CX: "Νήσος των Χριστουγέννων" + CC: "Νήσοι Κόκος (Κίλινγκ)" + CO: "Κολομβία" + KM: "Κομόρες" + CG: "Κονγκό" + CD: "Κονγκό, Δημοκρατία του Λαϊκής Δημοκρατίας" + CK: "Νήσοι Κουκ" + CR: "Κόστα Ρίκα" + CI: "Ακτή Ελεφαντοστού" + HR: "Κροατία" + CU: "Κούβα" + CW: "Κουρασάο" + CY: "Κύπρος" + CZ: "Τσεχία" + DK: "Δανία" + DJ: "Τζιμπουτί" + DM: "Δομίνικα" + DO: "Δομινικανή Δημοκρατία" + EC: "Εκουαδόρ" + EG: "Αίγυπτος" + SV: "Ελ Σαλβαδόρ" + GQ: "Ισημερινή Γουινέα" + ER: "Ερυθραία" + EE: "Εσθονία" + SZ: "Εσουατίνι" + ET: "Αιθιοπία" + FK: "Νήσοι Φώκλαντ" + FO: "Νήσοι Φερόες" + FJ: "Φίτζι" + FI: "Φινλανδία" + FR: "Γαλλία" + GF: "Γαλλική Γουιάνα" + PF: "Γαλλική Πολυνησία" + TF: "Γαλλικά Νότια Εδάφη" + GA: "Γκαμπόν" + GM: "Γκάμπια" + GE: "Γεωργία" + DE: "Γερμανία" + GH: "Γκάνα" + GI: "Γιβραλτάρ" + GR: "Ελλάδα" + GL: "Γροιλανδία" + GD: "Γρενάδα" + GP: "Γουαδελούπη" + GU: "Γκουάμ" + GT: "Γουατεμάλα" + GG: "Γκέρνσεϊ" + GN: "Γουινέα" + GW: "Γουινέα-Μπισάου" + GY: "Γουιάνα" + HT: "Αϊτή" + HM: "Νήσοι Χερντ και Μακντόναλντ" + VA: "Αγία Έδρα" + HN: "Ονδούρα" + HK: "Χονγκ Κονγκ" + HU: "Ουγγαρία" + IS: "Ισλανδία" + IN: "Ινδία" + ID: "Ινδονησία" + IR: "Ιράν, Ισλαμική Δημοκρατία του" + IQ: "Ιράκ" + IE: "Ιρλανδία" + IM: "Νήσος του Μαν" + IL: "Ισραήλ" + IT: "Ιταλία" + JM: "Τζαμάικα" + JP: "Ιαπωνία" + JE: "Τζέρσεϊ" + JO: "Ιορδανία" + KZ: "Καζακστάν" + KE: "Κένυα" + KI: "Κιριμπάτι" + KP: "Κορέα, Δημοκρατία του Λαϊκής Δημοκρατίας" + KR: "Κορέα, Δημοκρατία της" + KW: "Κουβέιτ" + KG: "Κιργιστάν" + LA: "Λαϊκή Δημοκρατία του Λάος" + LV: "Λετονία" + LB: "Λίβανος" + LS: "Λεσόθο" + LR: "Λιβερία" + LY: "Λιβύη" + LI: "Λιχτενστάιν" + LT: "Λιθουανία" + LU: "Λουξεμβούργο" + MO: "Μακάο" + MK: "Βόρεια Μακεδονία" + MG: "Μαδαγασκάρη" + MW: "Μαλάουι" + MY: "Μαλαισία" + MV: "Μαλδίβες" + ML: "Μάλι" + MT: "Μάλτα" + MH: "Νήσοι Μάρσαλ" + MQ: "Μαρτινίκα" + MR: "Μαυριτανία" + MU: "Μαυρίκιος" + YT: "Μαγιότ" + MX: "Μεξικό" + FM: "Μικρονησία, Ομόσπονδες Πολιτείες της" + MD: "Μολδαβία, Δημοκρατία της" + MC: "Μονακό" + MN: "Μογγολία" + ME: "Μαυροβούνιο" + MS: "Μονσεράτ" + MA: "Μαρόκο" + MZ: "Μοζαμβίκη" + MM: "Μιανμάρ" + NA: "Ναμίμπια" + NR: "Ναουρού" + NP: "Νεπάλ" + NL: "Ολλανδία" + NC: "Νέα Καληδονία" + NZ: "Νέα Ζηλανδία" + NI: "Νικαράγουα" + NE: "Νίγηρας" + NG: "Νιγηρία" + NU: "Νιούε" + NF: "Νήσος Νόρφολκ" + MP: "Νήσοι Βόρειες Μαριάνες" + NO: "Νορβηγία" + OM: "Ομάν" + PK: "Πακιστάν" + PW: "Παλάου" + PS: "Παλαιστίνη, Κράτος της" + PA: "Παναμάς" + PG: "Παπούα Νέα Γουινέα" + PY: "Παραγουάη" + PE: "Περού" + PH: "Φιλιππίνες" + PN: "Πίτκερν" + PL: "Πολωνία" + PT: "Πορτογαλία" + PR: "Πουέρτο Ρίκο" + QA: "Κατάρ" + RE: "Ρεϋνιόν" + RO: "Ρουμανία" + RU: "Ρωσική Ομοσπονδία" + RW: "Ρουάντα" + BL: "Άγιος Βαρθολομαίος" + SH: "Αγία Ελένη, Αναβασία και Τριστάν ντα Κούνια" + KN: "Άγιος Χριστόφορος και Νέβις" + LC: "Αγία Λουκία" + MF: "Άγιος Μαρτίνος (γαλλικό τμήμα)" + PM: "Άγιος Πέτρος και Μικελόν" + VC: "Άγιος Βικέντιος και Γρεναδίνες" + WS: "Σαμόα" + SM: "Άγιος Μαρίνος" + ST: "Σάο Τομέ και Πρίνσιπε" + SA: "Σαουδική Αραβία" + SN: "Σενεγάλη" + RS: "Σερβία" + SC: "Σεϋχέλλες" + SL: "Σιέρα Λεόνε" + SG: "Σιγκαπούρη" + SX: "Άγιος Μαρτίνος (ολλανδικό τμήμα)" + SK: "Σλοβακία" + SI: "Σλοβενία" + SB: "Νήσοι Σολομώντος" + SO: "Σομαλία" + ZA: "Νότια Αφρική" + GS: "Νότια Γεωργία και Νότιες Νήσοι Σάντουιτς" + SS: "Νότιο Σουδάν" + ES: "Ισπανία" + LK: "Σρι Λάνκα" + SD: "Σουδάν" + SR: "Σουρινάμ" + SJ: "Σβάλμπαρντ και Γιαν Μαγιέν" + SE: "Σουηδία" + CH: "Ελβετία" + SY: "Συριακή Αραβική Δημοκρατία" + TW: "Ταϊβάν (Επαρχία της Κίνας)" + TJ: "Τατζικιστάν" + TZ: "Τανζανία, Ηνωμένη Δημοκρατία της" + TH: "Ταϊλάνδη" + TL: "Τιμόρ-Λέστε" + TG: "Τόγκο" + TK: "Τοκελάου" + TO: "Τόνγκα" + TT: "Τρινιντάντ και Τομπάγκο" + TN: "Τυνησία" + TR: "Τουρκία" + TM: "Τουρκμενιστάν" + TC: "Νήσοι Τερκς και Κάικος" + TV: "Τουβαλού" + UG: "Ουγκάντα" + UA: "Ουκρανία" + AE: "Ηνωμένα Αραβικά Εμιράτα" + GB: "Ηνωμένο Βασίλειο" + US: "Ηνωμένες Πολιτείες Αμερικής" + UM: "Απομακρυσμένες Νησίδες Ηνωμένων Πολιτειών" + UY: "Ουρουγουάη" + UZ: "Ουζμπεκιστάν" + VU: "Βανουάτου" + VE: "Βενεζουέλα, Μπολιβαριανή Δημοκρατία της" + VN: "Βιετνάμ" + VG: "Βρετανικές Παρθένοι Νήσοι" + VI: "Αμερικανικές Παρθένοι Νήσοι" + WF: "Ουαλίς και Φουτουνά" + EH: "Δυτική Σαχάρα" + YE: "Υεμένη" + ZM: "Ζάμπια" + ZW: "Ζιμπάμπουε" diff --git a/locales/it/app.yml b/locales/it/app.yml index 0dbe1d0..8206f8f 100644 --- a/locales/it/app.yml +++ b/locales/it/app.yml @@ -50,6 +50,7 @@ it: advance: "Anticipo: %{date} %{txt}" advance_sum: "Somma anticipi" due_sum: "Totale dovuto" + exchange_rate: "Cambio in %{to} (%{amount})" taxes: title: "Tasse" category: "Tassa" diff --git a/locales/pl/app.yml b/locales/pl/app.yml new file mode 100644 index 0000000..6dee679 --- /dev/null +++ b/locales/pl/app.yml @@ -0,0 +1,138 @@ +pl: + page: "Strona %{page} z %{count}" + + billing: + invoice: + title: + standard: "Faktura" + standard-simplified: "Uproszczona faktura" + proforma: "Faktura proforma" + corrective: "Faktura korygująca" + credit-note: "Nota kredytowa" + debit-note: "Nota debetowa" + + summary: + title: "Podsumowanie" + issue_date: "Data wystawienia" + value_date: "Data wartości" + operation_date: "Data operacji" + currency: "Waluta" + currency_value: "%{desc} (%{code})" + preceding_invoice: "Poprzednia faktura" + order: "Zamówienie" + order_period: "Okres" + order_period_label: "%{label}" + order_period_range: "%{start} do %{end}" + + supplier: + title: "Dostawca" + + customer: + title: "Klient" + + lines: + title: "Linie" + i: "#" + ref: "Ref." + description: "Opis" + quantity: "Ilość" + unit: "Jednostka" + price: "Cena" + discount: "Rabat" + charges: "Opłaty" + total: "Suma" + + totals: + sum: "Suma" + discount: "Rabat" + charge: "Opłata" + title: "Sumy" + prices_include_tax: "Podatek wliczony" + total: "Suma" + taxes: "Podatek" + total_with_tax: "Suma z podatkiem" + total_to_pay: "Suma do zapłaty" + outlay: "Wydatek %{i}: %{txt}" + advance: "Zaliczka: %{date} %{txt}" + advance_sum: "Suma zaliczek" + due_sum: "Suma należności" + exchange_rate: "Kurs wymiany na %{to} (%{amount})" + + taxes: + title: "Podatki" + category: "Podatek" + base: "Podstawa" + rate: "Stawka" + amount: "Kwota" + + payment: + title: "Płatność" + instructions: + method: "Metoda płatności" + ref: "Referencja" + bank_data: "Instrukcje przelewu bankowego" + card: "Płatność kartą" + card_detail: "Płatność dokonana kartą kończącą się na %{last4} należącą do %{holder}" + observations: "Uwagi dotyczące płatności" + bank_name: "Bank: %{name}" + iban: "IBAN: %{iban}" + account_number: "Numer konta: %{num}" + bic: "BIC: %{bic}" + methods: + card: "Karta bankowa" + credit_transfer: "Przelew bankowy" + cash: "Gotówka" + direct_debit: "Polecenie zapłaty" + online: "Online" + terms: + key: "Warunki płatności" + notes: "Uwagi dotyczące warunków płatności" + due_dates: "Terminy płatności" + keys: + na: "Nieokreślone" + end_of_month: "Koniec miesiąca" + due_date: "W dniu płatności" + deferred: "Po terminie płatności" + proximo: "Następny miesiąc" + instant: "Przy dostarczeniu faktury" + elective: "Wybrane przez klienta" + pending: "Do ustalenia przez dostawcę" + advanced: "Opłacone" + delivery: "Przy dostawie" + + notes: + title: "Uwagi" + reg: + book: "Księga %{id}" + volume: "Tom %{id}" + sheet: "Arkusz %{id}" + section: "Sekcja %{id}" + page: "Strona %{id}" + entry: "Wpis %{id}" + inscription: "Zapisane w:" + + regimes: + co: + cufe: "CUFE: %{cufe}" + cude: "CUDE: %{cude}" + preceding_cufe: "Poprzedni CUFE faktury: %{cufe}" + + organizing: + party: + tax_id: "%{label}: (%{country}) %{code}" + tel: "Tel: %{num}" + tel_label: "Tel: %{num} (%{label})" + email: + one: "Email: %{addr}" + other: "Emaile: %{addr}" + email_label: "%{addr} (%{label})" + identity: "%{label}: %{code}" + identity_code: "Kod tożsamości" + po_box: "Skrzynka pocztowa %{po_box}" + labels: + default: "Kod podatkowy" + ext: "%{label}: %{value}" + + address: + label: "%{label}:" + country: "(%{country})" diff --git a/locales/pl/countries.yml b/locales/pl/countries.yml new file mode 100644 index 0000000..b802f93 --- /dev/null +++ b/locales/pl/countries.yml @@ -0,0 +1,250 @@ +pl: + country_names: + AD: Andora + AE: Zjednoczone Emiraty Arabskie + AF: Afganistan + AG: Antigua i Barbuda + AI: Anguilla + AL: Albania + AM: Armenia + AO: Angola + AQ: Antarktyka + AR: Argentyna + AS: Samoa Amerykańskie + AT: Austria + AU: Australia + AW: Aruba + AX: Wyspy Alandzkie + AZ: Azerbejdżan + BA: Bośnia i Hercegowina + BB: Barbados + BD: Bangladesz + BE: Belgia + BF: Burkina Faso + BG: Bułgaria + BH: Bahrajn + BI: Burundi + BJ: Benin + BL: Saint-Barthélemy + BM: Bermudy + BN: Brunei Darussalam + BO: Boliwia + BQ: Niderlandy Karaibskie + BR: Brazylia + BS: Bahamy + BT: Bhutan + BV: Wyspa Bouveta + BW: Botswana + BY: Białoruś + BZ: Belize + CA: Kanada + CC: Wyspy Kokosowe + CD: Demokratyczna Republika Konga + CF: Republika Środkowoafrykańska + CG: Kongo + CH: Szwajcaria + CI: Côte d’Ivoire + CK: Wyspy Cooka + CL: Chile + CM: Kamerun + CN: Chiny + CO: Kolumbia + CR: Kostaryka + CU: Kuba + CV: Republika Zielonego Przylądka + CW: Curaçao + CX: Wyspa Bożego Narodzenia + CY: Cypr + CZ: Czechy + DE: Niemcy + DJ: Dżibuti + DK: Dania + DM: Dominika + DO: Dominikana + DZ: Algieria + EC: Ekwador + EE: Estonia + EG: Egipt + EH: Sahara Zachodnia + ER: Erytrea + ES: Hiszpania + ET: Etiopia + FI: Finlandia + FJ: Fidżi + FK: Falklandy + FM: Mikronezja + FO: Wyspy Owcze + FR: Francja + GA: Gabon + GB: Wielka Brytania + GD: Grenada + GE: Gruzja + GF: Gujana Francuska + GG: Wyspa Guernsey + GH: Ghana + GI: Gibraltar + GL: Grenlandia + GM: Gambia + GN: Gwinea + GP: Gwadelupa + GQ: Gwinea Równikowa + GR: Grecja + GS: Georgia Południowa i Sandwich Południowy + GT: Gwatemala + GU: Guam + GW: Gwinea Bissau + GY: Gujana + HK: Hongkong + HM: Wyspy Heard i McDonalda + HN: Honduras + HR: Chorwacja + HT: Haiti + HU: Węgry + ID: Indonezja + IE: Irlandia + IL: Izrael + IM: Wyspa Man + IN: Indie + IO: Brytyjskie Terytorium Oceanu Indyjskiego + IQ: Irak + IR: Iran + IS: Islandia + IT: Włochy + JE: Wyspa Jersey + JM: Jamajka + JO: Jordania + JP: Japonia + KE: Kenia + KG: Kirgistan + KH: Kambodża + KI: Kiribati + KM: Komory + KN: Saint Kitts i Nevis + KP: Korea Północna + KR: Korea Południowa + KW: Kuwejt + KZ: Kazachstan + LA: Laos + LB: Liban + LC: Saint Lucia + LI: Liechtenstein + LK: Sri Lanka + LR: Liberia + LS: Lesotho + LT: Litwa + LU: Luksemburg + LV: Łotwa + LY: Libia + MA: Maroko + MC: Monako + MD: Mołdawia + ME: Czarnogóra + MF: Saint-Martin + MG: Madagaskar + MH: Wyspy Marshalla + MK: Macedonia + ML: Mali + MM: Mjanma (Birma) + MN: Mongolia + MO: SRA Makau (Chiny) + MP: Mariany Północne + MQ: Martynika + MR: Mauretania + MS: Montserrat + MT: Malta + MU: Mauritius + MV: Malediwy + MW: Malawi + MX: Meksyk + MY: Malezja + MZ: Mozambik + NA: Namibia + NC: Nowa Kaledonia + NE: Niger + NF: Norfolk + NG: Nigeria + NI: Nikaragua + NL: Holandia + NO: Norwegia + NP: Nepal + NR: Nauru + NU: Niue + NZ: Nowa Zelandia + OM: Oman + PA: Panama + PE: Peru + PF: Polinezja Francuska + PG: Papua-Nowa Gwinea + PH: Filipiny + PK: Pakistan + PL: Polska + PM: Saint-Pierre i Miquelon + PN: Pitcairn + PR: Portoryko + PS: Terytoria Palestyńskie + PT: Portugalia + PW: Palau + PY: Paragwaj + QA: Katar + RE: Reunion + RO: Rumunia + RS: Serbia + RU: Rosja + RW: Rwanda + SA: Arabia Saudyjska + SB: Wyspy Salomona + SC: Seszele + SD: Sudan + SE: Szwecja + SG: Singapur + SH: Wyspa Świętej Heleny + SI: Słowenia + SJ: Svalbard i Jan Mayen + SK: Słowacja + SL: Sierra Leone + SM: San Marino + SN: Senegal + SO: Somalia + SR: Surinam + SS: Sudan Południowy + ST: Wyspy Świętego Tomasza i Książęca + SV: Salwador + SX: Sint Maarten + SY: Syria + SZ: Suazi + TC: Turks i Caicos + TD: Czad + TF: Francuskie Terytoria Południowe + TG: Togo + TH: Tajlandia + TJ: Tadżykistan + TK: Tokelau + TL: Timor Wschodni + TM: Turkmenistan + TN: Tunezja + TO: Tonga + TR: Turcja + TT: Trynidad i Tobago + TV: Tuvalu + TW: Tajwan + TZ: Tanzania + UA: Ukraina + UG: Uganda + UM: Dalekie Wyspy Mniejsze Stanów Zjednoczonych + US: Stany Zjednoczone + UY: Urugwaj + UZ: Uzbekistan + VA: Watykan + VC: Saint Vincent i Grenadyny + VE: Wenezuela + VG: Brytyjskie Wyspy Dziewicze + VI: Wyspy Dziewicze Stanów Zjednoczonych + VN: Wietnam + VU: Vanuatu + WF: Wallis i Futuna + WS: Samoa + YE: Jemen + YT: Majotta + ZA: Republika Południowej Afryki + ZM: Zambia + ZW: Zimbabwe diff --git a/locales/pl/currencies.yml b/locales/pl/currencies.yml new file mode 100644 index 0000000..2fe7325 --- /dev/null +++ b/locales/pl/currencies.yml @@ -0,0 +1,154 @@ +pl: + currencies: + AED: Dirham Zjednoczonych Emiratów Arabskich + AFN: Afgani + ALL: Lek albański + AMD: Dram armeński + ANG: Gulden Antyli Holenderskich + AOA: Kwanza + ARS: Peso argentyńskie + AUD: Dolar australijski + AWG: Gulden arubański + AZN: Manat azerski + BAM: Marka zamienna + BBD: Dolar Barbadosu + BDT: Taka + BGN: Lew bułgarski + BHD: Dinar bahrański + BIF: Frank burundyjski + BMD: Dolar Bermudzki + BND: Dolar Brunei + BOB: Boliviano + BOV: Mvdol + BRL: Real brazylijski + BSD: Dolar Bahamów + BTN: Ngultrum + BWP: Pula + BYN: Rubel białoruski + BZD: Dolar belizeński + CAD: Dolar kanadyjski + CDF: Frank kongijski + CHE: Euro WIR + CHF: Frank szwajcarski + CHW: Frank WIR + CLF: Jednostka Rozrachunku Chilijczyków + CLP: Peso chilijskie + CNY: Juan chiński + COP: Peso kolumbijskie + COU: Unidad de Valor Real kolumbijska + CRC: Colon kostarykański + CUC: Peso kubańskie wymienialne + CUP: Peso kubańskie + CVE: Escudo Zielonego Przylądka + CZK: Korona czeska + DJF: Frank Dżibuti + DKK: Korona duńska + DOP: Peso dominikańskie + DZD: Dinar algierski + EGP: Funt egipski + ERN: Nakfa erytrejska + ETB: Birr etiopski + EUR: Euro + FJD: Dolar Fidżi + FKP: Funt Falklandzki + GBP: Funt szterling + GEL: Lari gruziński + GHS: Cedi ghański + GIP: Funt gibraltarski + GMD: Dalasi + GNF: Frank gwinejski + GTQ: Quetzal + GYD: Dolar gujański + HKD: Dolar hongkoński + HNL: Lempira + HRK: Kuna chorwacka + HTG: Gourde haitańskie + HUF: Forint węgierski + IDR: Rupia indonezyjska + ILS: Szekel izraelski + INR: Rupia indyjska + IQD: Dinar iracki + IRR: Rial irański + ISK: Korona islandzka + JMD: Dolar jamajski + JOD: Dinar jordański + JPY: Jen japoński + KES: Szyling kenijski + KGS: Som kirgiski + KHR: Riel kambodżański + KMF: Frank Komorów + KPW: Won północnokoreański + KRW: Won południowokoreański + KWD: Dinar kuwejcki + KYD: Dolar kajmański + KZT: Tenge kazachskie + LAK: Kip laotański + LBP: Funt libański + LKR: Rupia lankijska + LRD: Dolar liberyjski + LSL: Loti Lesoto + LYD: Dinar libijski + MAD: Dirham marokański + MDL: Lej mołdawski + MGA: Ariary madagaskarski + MKD: Denar macedoński + MMK: Kyat birmański + MNT: Tugrik mongolski + MOP: Pataca Makau + MRU: Ouguiya mauretańska + MUR: Rupia Mauritiusa + MVR: Rufiyaa Malediwów + MWK: Kwacha malawijska + MXN: Peso meksykańskie + MXV: Unidad de Inversion (UDI) + MYR: Ringgit malezyjski + MZN: Metical mozambicki + NAD: Dolar namibijski + NGN: Naira nigeryjska + NIO: Córdoba nikaraguańska + NOK: Korona norweska + NPR: Rupia nepalska + NZD: Dolar nowozelandzki + OMR: Rial omański + PAB: Balboa + PEN: Sol peruwiański + PGK: Kina papuaska + PHP: Peso filipińskie + PKR: Rupia pakistańska + PLN: Złoty polski + PYG: Guarani paragwajskie + QAR: Rial katarski + RON: Lej rumuński + RSD: Dinar serbski + RUB: Rubel rosyjski + RWF: Frank rwandyjski + SAR: Rial saudyjski + SBD: Dolar Wysp Salomona + SCR: Rupia seszelska + SDG: Funt sudański + SEK: Korona szwedzka + SGD: Dolar singapurski + SHP: Funt Świętej Heleny + SLL: Leone Sierra Leone + SOS: Szyling somalijski + SRD: Dolar surinamski + SSP: Funt Sudanu Południowego + STN: Dobra Wysp Świętego Tomasza i Książęcej + SVC: Colon salwadorski + SYP: Funt syryjski + SZL: Lilangeni Suazi + THB: Baht tajski + TJS: Somoni tadżyckie + TMT: Manat turkmeński + TND: Dinar tunezyjski + TOP: Pa'anga tongańskie + TRY: Lira turecka + TTD: Dolar Trynidadu i Tobago + TWD: Nowy dolar tajwański + TZS: Szyling tanzański + UAH: Hrywna ukraińska + UGX: Szyling ugandyjski + USD: Dolar amerykański + USN: Dolar amerykański (następny dzień) + UYI: Peso urugwajskie (jednostki indeksowane) + UYU: Peso urugwajskie \ No newline at end of file diff --git a/pkg/pdf/pdf.go b/pkg/pdf/pdf.go index 6e75ea6..6e0f81f 100644 --- a/pkg/pdf/pdf.go +++ b/pkg/pdf/pdf.go @@ -4,6 +4,9 @@ package pdf import ( "context" + "fmt" + "io/fs" + "path/filepath" ) // Config defines options used to configure the PDF convertor. @@ -19,6 +22,7 @@ type Option func(*options) type options struct { metadata *Metadata + styles []*Stylesheet attachments []*Attachment } @@ -31,6 +35,12 @@ type Metadata struct { Creator string } +// Stylesheet descriptions a document to upload with the HTML for styles. +type Stylesheet struct { + Data []byte + Filename string +} + // Attachment is used to embed files inside the PDF when supported by // the implementation. type Attachment struct { @@ -56,6 +66,30 @@ func WithAuthToken(token string) Config { } } +// WithStylesheets prepares the stylesheets to be included in the PDF generation +// request. +func WithStylesheets(src fs.FS) Option { + return func(o *options) { + err := fs.WalkDir(src, "styles", func(path string, _ fs.DirEntry, _ error) error { + if filepath.Ext(path) != ".css" { + return nil + } + data, err := fs.ReadFile(src, path) + if err != nil { + return fmt.Errorf("reading file: %w", err) + } + o.styles = append(o.styles, &Stylesheet{ + Filename: path, + Data: data, + }) + return nil + }) + if err != nil { + panic(err) + } + } +} + // WithMetadata adds the provided metadata to include in the conversion request. func WithMetadata(md *Metadata) Option { return func(o *options) { diff --git a/pkg/pdf/prince.go b/pkg/pdf/prince.go index 27adba9..61f0887 100644 --- a/pkg/pdf/prince.go +++ b/pkg/pdf/prince.go @@ -32,6 +32,12 @@ func (pc *princeConvertor) HTML(_ context.Context, data []byte, opts ...Option) "data.html": data, } + if len(o.styles) > 0 { + for _, ss := range o.styles { + j.Input.Styles = append(j.Input.Styles, ss.Filename) + j.Files[ss.Filename] = ss.Data + } + } if len(o.attachments) > 0 { for _, a := range o.attachments { j.Files[a.Filename] = a.Data diff --git a/tmp/main b/tmp/main new file mode 100755 index 0000000..e80a6d2 Binary files /dev/null and b/tmp/main differ