2024-04-28 18:28:45 +00:00
|
|
|
package ods
|
2024-01-18 20:05:30 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
|
|
|
"bytes"
|
2024-04-28 18:28:45 +00:00
|
|
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
|
|
|
"dev.tandem.ws/tandem/camper/pkg/template"
|
2024-01-18 20:05:30 +00:00
|
|
|
"encoding/xml"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2024-01-31 18:58:46 +00:00
|
|
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
2024-01-18 20:05:30 +00:00
|
|
|
"dev.tandem.ws/tandem/camper/pkg/locale"
|
|
|
|
)
|
|
|
|
|
|
|
|
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>Camper</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>
|
|
|
|
`
|
|
|
|
)
|
|
|
|
|
2024-04-28 18:28:45 +00:00
|
|
|
func WriteTable[K interface{}](rows []*K, columns []string, locale *locale.Locale, writeRow func(*strings.Builder, *K) error) ([]byte, error) {
|
2024-01-18 20:05:30 +00:00
|
|
|
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 {
|
2024-04-28 18:28:45 +00:00
|
|
|
if err := WriteCellString(&sb, locale.GetC(t, "header")); err != nil {
|
2024-01-18 20:05:30 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sb.WriteString(" </table:table-row>\n")
|
|
|
|
for _, row := range rows {
|
|
|
|
sb.WriteString(" <table:table-row table:style-name=\"ro1\">\n")
|
|
|
|
if err := writeRow(&sb, row); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
sb.WriteString(" </table:table-row>\n")
|
|
|
|
}
|
|
|
|
sb.WriteString(` </table:table>
|
|
|
|
<table:named-expressions/>
|
|
|
|
</office:spreadsheet>
|
|
|
|
</office:body>
|
|
|
|
</office:document-content>
|
|
|
|
`)
|
|
|
|
return writeOds(sb.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
func writeOds(content string) ([]byte, error) {
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
ods := zip.NewWriter(buf)
|
|
|
|
if err := writeOdsFile(ods, "mimetype", mimetype, zip.Store); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := writeOdsFile(ods, "META-INF/manifest.xml", metaDashInfManifestXml, zip.Deflate); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := writeOdsFile(ods, "meta.xml", metaXml, zip.Deflate); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := writeOdsFile(ods, "styles.xml", stylesXml, zip.Deflate); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := writeOdsFile(ods, "content.xml", content, zip.Deflate); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := ods.Close(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func writeOdsFile(ods *zip.Writer, name string, content string, method uint16) error {
|
|
|
|
f, err := ods.CreateHeader(&zip.FileHeader{
|
|
|
|
Name: name,
|
|
|
|
Method: method,
|
|
|
|
Modified: time.Now(),
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = f.Write([]byte(content))
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-04-28 18:28:45 +00:00
|
|
|
func WriteCellString(sb *strings.Builder, s string) error {
|
2024-01-18 20:05:30 +00:00
|
|
|
sb.WriteString(` <table:table-cell office:value-type="string" calcext:value-type="string"><text:p>`)
|
|
|
|
if err := xml.EscapeText(sb, []byte(s)); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
sb.WriteString("</text:p></table:table-cell>\n")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-04-28 18:28:45 +00:00
|
|
|
func WriteCellDate(sb *strings.Builder, t time.Time) {
|
2024-01-31 18:58:46 +00:00
|
|
|
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(database.ISODateFormat), t.Format("02/01/06")))
|
2024-01-18 20:05:30 +00:00
|
|
|
}
|
|
|
|
|
2024-04-28 18:28:45 +00:00
|
|
|
func WriteCellFloat(sb *strings.Builder, s string, company *auth.Company, locale *locale.Locale) {
|
|
|
|
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, template.FormatPrice(s, locale.Language, locale.CurrencyPattern, company.DecimalDigits, company.CurrencySymbol)))
|
|
|
|
}
|
|
|
|
|
|
|
|
func MustWriteResponse(w http.ResponseWriter, ods []byte, filename string) {
|
2024-01-18 20:05:30 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|