255 lines
9.4 KiB
Go
255 lines
9.4 KiB
Go
package pkg
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
mimetype = "application/vnd.oasis.opendocument.spreadsheet"
|
|
metaDashInfManifestXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<manifest:manifest
|
|
xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
|
|
manifest:version="1.3">
|
|
<manifest:file-entry manifest:full-path="/" manifest:version="1.3" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>
|
|
<manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
|
|
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
|
|
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
|
|
</manifest:manifest>
|
|
`
|
|
metaXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<office:document-meta
|
|
xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"
|
|
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
|
office:version="1.3">
|
|
<office:meta>
|
|
<meta:creation-date></meta:creation-date>
|
|
<meta:generator>Numerus</meta:generator>
|
|
</office:meta>
|
|
</office:document-meta>
|
|
`
|
|
stylesXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<office:document-styles
|
|
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
|
office:version="1.3">
|
|
</office:document-styles>
|
|
`
|
|
)
|
|
|
|
func extractTaxIDs(taxColumns map[int]string) []int {
|
|
taxIDs := make([]int, len(taxColumns))
|
|
i := 0
|
|
for k := range taxColumns {
|
|
taxIDs[i] = k
|
|
i++
|
|
}
|
|
sort.Ints(taxIDs[:])
|
|
return taxIDs
|
|
}
|
|
|
|
func mustWriteInvoicesOds(invoices []*InvoiceEntry, vatin map[int]string, taxes map[int]taxMap, taxColumns map[int]string, locale *Locale, company *Company) []byte {
|
|
taxIDs := extractTaxIDs(taxColumns)
|
|
columns := make([]string, 7+len(taxIDs))
|
|
columns[0] = "Date"
|
|
columns[1] = "Invoice Num."
|
|
columns[2] = "Customer"
|
|
columns[3] = pgettext("title", "VAT number", locale)
|
|
columns[4] = "Status"
|
|
i := 5
|
|
for _, taxID := range taxIDs {
|
|
columns[i] = taxColumns[taxID]
|
|
i++
|
|
}
|
|
columns[i] = "Amount"
|
|
columns[i+1] = "Tags"
|
|
return mustWriteTableOds(invoices, columns, locale, func(sb *strings.Builder, invoice *InvoiceEntry) {
|
|
writeCellDate(sb, invoice.Date)
|
|
writeCellString(sb, invoice.Number)
|
|
writeCellString(sb, invoice.CustomerName)
|
|
writeCellString(sb, vatin[invoice.ID])
|
|
writeCellString(sb, invoice.StatusLabel)
|
|
writeTaxes(sb, taxes[invoice.ID], taxIDs, locale, company)
|
|
writeCellFloat(sb, invoice.Total, locale, company)
|
|
writeCellString(sb, strings.Join(invoice.Tags, ","))
|
|
})
|
|
}
|
|
|
|
func mustWriteQuotesOds(quotes []*QuoteEntry, locale *Locale, company *Company) []byte {
|
|
columns := []string{
|
|
"Date",
|
|
"Quotation Num.",
|
|
"Customer",
|
|
"Status",
|
|
"Tags",
|
|
"Amount",
|
|
}
|
|
return mustWriteTableOds(quotes, columns, locale, func(sb *strings.Builder, quote *QuoteEntry) {
|
|
writeCellDate(sb, quote.Date)
|
|
writeCellString(sb, quote.Number)
|
|
writeCellString(sb, quote.CustomerName)
|
|
writeCellString(sb, quote.StatusLabel)
|
|
writeCellString(sb, strings.Join(quote.Tags, ","))
|
|
writeCellFloat(sb, quote.Total, locale, company)
|
|
})
|
|
}
|
|
|
|
func mustWriteExpensesOds(expenses []*ExpenseEntry, vatin map[int]string, taxes map[int]taxMap, taxColumns map[int]string, locale *Locale, company *Company) []byte {
|
|
taxIDs := extractTaxIDs(taxColumns)
|
|
columns := make([]string, 8+len(taxIDs))
|
|
columns[0] = "Contact"
|
|
columns[1] = pgettext("title", "VAT number", locale)
|
|
columns[2] = "Invoice Date"
|
|
columns[3] = "Invoice Number"
|
|
columns[4] = "Status"
|
|
columns[5] = "Amount"
|
|
i := 6
|
|
for _, taxID := range taxIDs {
|
|
columns[i] = taxColumns[taxID]
|
|
i++
|
|
}
|
|
columns[i] = "Total"
|
|
columns[i+1] = "Tags"
|
|
return mustWriteTableOds(expenses, columns, locale, func(sb *strings.Builder, expense *ExpenseEntry) {
|
|
|
|
writeCellString(sb, expense.InvoicerName)
|
|
writeCellString(sb, vatin[expense.ID])
|
|
writeCellDate(sb, expense.InvoiceDate)
|
|
writeCellString(sb, expense.InvoiceNumber)
|
|
writeCellString(sb, expense.StatusLabel)
|
|
writeCellFloat(sb, expense.Amount, locale, company)
|
|
writeTaxes(sb, taxes[expense.ID], taxIDs, locale, company)
|
|
writeCellFloat(sb, expense.Total, locale, company)
|
|
writeCellString(sb, strings.Join(expense.Tags, ","))
|
|
})
|
|
}
|
|
|
|
func mustWriteTableOds[K interface{}](rows []*K, columns []string, locale *Locale, writeRow func(*strings.Builder, *K)) []byte {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(`<?xml version="1.0" encoding="UTF-8"?>
|
|
<office:document-content
|
|
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
|
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
|
|
xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0"
|
|
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
|
|
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
|
xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
|
|
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
|
|
xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"
|
|
office:version="1.3">
|
|
<office:scripts/>
|
|
<office:font-face-decls>
|
|
<style:font-face style:name="Liberation Sans" svg:font-family="'Liberation Sans'" style:font-family-generic="swiss" style:font-pitch="variable"/>
|
|
</office:font-face-decls>
|
|
<office:automatic-styles>
|
|
<style:style style:name="co1" style:family="table-column">
|
|
<style:table-column-properties fo:break-before="auto" style:column-width="0.889in"/>
|
|
</style:style>
|
|
<style:style style:name="ro1" style:family="table-row">
|
|
<style:table-row-properties style:row-height="0.178in" fo:break-before="auto" style:use-optimal-row-height="true"/>
|
|
</style:style>
|
|
<style:style style:name="ta1" style:family="table" style:master-page-name="Default">
|
|
<style:table-properties table:display="true" style:writing-mode="lr-tb"/>
|
|
</style:style>
|
|
<number:date-style style:name="N37" number:automatic-order="true">
|
|
<number:day number:style="long"/>
|
|
<number:text>/</number:text>
|
|
<number:month number:style="long"/>
|
|
<number:text>/</number:text>
|
|
<number:year/>
|
|
</number:date-style>
|
|
<style:style style:name="ce1" style:family="table-cell" style:parent-style-name="Default" style:data-style-name="N37"/>
|
|
</office:automatic-styles>
|
|
<office:body>
|
|
<office:spreadsheet>
|
|
<table:calculation-settings table:automatic-find-labels="false" table:use-regular-expressions="false" table:use-wildcards="true"/>
|
|
<table:table table:name="Sheet1" table:style-name="ta1">
|
|
`)
|
|
sb.WriteString(fmt.Sprintf(" <table:table-column table:style-name=\"co1\" table:number-columns-repeated=\"%d\" table:default-cell-style-name=\"Default\"/>\n", len(columns)))
|
|
sb.WriteString(` <table:table-row table:style-name="ro1">
|
|
`)
|
|
for _, t := range columns {
|
|
writeCellString(&sb, locale.GetC(t, "title"))
|
|
}
|
|
sb.WriteString(" </table:table-row>\n")
|
|
for _, row := range rows {
|
|
sb.WriteString(" <table:table-row table:style-name=\"ro1\">\n")
|
|
writeRow(&sb, row)
|
|
sb.WriteString(" </table:table-row>\n")
|
|
}
|
|
sb.WriteString(` </table:table>
|
|
<table:named-expressions/>
|
|
</office:spreadsheet>
|
|
</office:body>
|
|
</office:document-content>
|
|
`)
|
|
return mustWriteOds(sb.String())
|
|
}
|
|
|
|
func mustWriteOds(content string) []byte {
|
|
buf := new(bytes.Buffer)
|
|
ods := zip.NewWriter(buf)
|
|
mustWriteOdsFile(ods, "mimetype", mimetype, zip.Store)
|
|
mustWriteOdsFile(ods, "META-INF/manifest.xml", metaDashInfManifestXml, zip.Deflate)
|
|
mustWriteOdsFile(ods, "meta.xml", metaXml, zip.Deflate)
|
|
mustWriteOdsFile(ods, "styles.xml", stylesXml, zip.Deflate)
|
|
mustWriteOdsFile(ods, "content.xml", content, zip.Deflate)
|
|
mustClose(ods)
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func mustWriteOdsFile(ods *zip.Writer, name string, content string, method uint16) {
|
|
f, err := ods.CreateHeader(&zip.FileHeader{
|
|
Name: name,
|
|
Method: method,
|
|
Modified: time.Now(),
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if _, err = f.Write([]byte(content)); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func writeCellString(sb *strings.Builder, s string) {
|
|
sb.WriteString(` <table:table-cell office:value-type="string" calcext:value-type="string"><text:p>`)
|
|
if err := xml.EscapeText(sb, []byte(s)); err != nil {
|
|
panic(err)
|
|
}
|
|
sb.WriteString("</text:p></table:table-cell>\n")
|
|
}
|
|
|
|
func writeCellDate(sb *strings.Builder, t time.Time) {
|
|
sb.WriteString(fmt.Sprintf(" <table:table-cell table:style-name=\"ce1\" office:value-type=\"date\" office:date-value=\"%s\" calcext:value-type=\"date\"><text:p>%s</text:p></table:table-cell>\n", t.Format("2006-01-02"), t.Format("02/01/06")))
|
|
}
|
|
|
|
func writeCellFloat(sb *strings.Builder, s string, locale *Locale, company *Company) {
|
|
sb.WriteString(fmt.Sprintf(" <table:table-cell office:value-type=\"float\" office:value=\"%s\" calcext:value-type=\"float\"><text:p>%s</text:p></table:table-cell>\n", s, formatPrice(s, locale.Language, "%.[1]*[2]f", company.DecimalDigits, "")))
|
|
}
|
|
|
|
func writeOdsResponse(w http.ResponseWriter, ods []byte, filename string) {
|
|
w.Header().Set("Content-Type", "application/vnd.oasis.opendocument.spreadsheet")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
|
w.WriteHeader(http.StatusOK)
|
|
if _, err := w.Write(ods); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func writeTaxes(sb *strings.Builder, taxes taxMap, taxIDs []int, locale *Locale, company *Company) {
|
|
for _, taxID := range taxIDs {
|
|
var amount string
|
|
if taxes != nil {
|
|
amount = taxes[taxID]
|
|
}
|
|
writeCellFloat(sb, amount, locale, company)
|
|
}
|
|
}
|