Skip to content

Commit 6bc50ac

Browse files
committed
Runtime PDF engine dispatch instead of build tags
Both engines are always compiled in; the -tags speedata seam is gone. The choice is made per owner via the new settings field pdf_engine (auto | speedata | boxesandglue, migration 007). With "auto", an existing layout.xml switches to the speedata path; layout.xml without a configured publishing server is an error, not a silent fallback. AutoLayoutNote is now a Store method describing the path actually resolved for the owner.
1 parent f7e1915 commit 6bc50ac

19 files changed

Lines changed: 218 additions & 44 deletions

controller/invoicecontroller.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,12 +379,12 @@ func (ctrl *controller) invoiceDetail(c echo.Context) error {
379379
PreviewURL string // optional
380380
}
381381

382-
// The note depends on the PDF engine the binary was built with
383-
// (boxesandglue default vs. -tags speedata).
382+
// The note depends on the PDF engine resolved for the owner (settings
383+
// field pdf_engine, layout.xml, server config).
384384
lh := letterheadVM{
385385
Mode: "auto",
386386
Name: "Automatisch",
387-
Note: model.AutoLayoutNote,
387+
Note: ctrl.model.AutoLayoutNote(ownerID),
388388
}
389389

390390
if i.TemplateID != nil {

controller/settingscontroller.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type settingsForm struct {
3434
CustomerPrefix string `form:"custprefix"` // e.g. "K-"
3535
CustomerWidth int `form:"custwidth"` // e.g. 5
3636
CustomerCounter int64 `form:"custcounter"` // e.g. 1000
37-
37+
PDFEngine string `form:"pdfengine"` // "auto" | "speedata" | "boxesandglue"
3838
}
3939

4040
func (ctrl *controller) settingsInit(e *echo.Echo) {
@@ -71,6 +71,10 @@ func (ctrl *controller) settingslist(c echo.Context) error {
7171
m["submit"] = "Save"
7272
m["cancel"] = "/"
7373
ownerID := c.Get("ownerid").(uint)
74+
// The speedata option is only offered when a publishing server is
75+
// configured in config.toml.
76+
speedataAvailable := ctrl.model.Config.PublishingServerAddress != ""
77+
m["speedataAvailable"] = speedataAvailable
7478

7579
switch c.Request().Method {
7680
case http.MethodGet:
@@ -88,6 +92,20 @@ func (ctrl *controller) settingslist(c echo.Context) error {
8892
return ErrInvalid(err, "Error processing form data")
8993
}
9094

95+
// Normalize the engine choice: reject unknown values and the
96+
// speedata option when no publishing server is configured (the form
97+
// hides it, but the value could still arrive in the POST).
98+
pdfEngine := f.PDFEngine
99+
switch pdfEngine {
100+
case string(model.PDFEngineAuto), string(model.PDFEngineBag):
101+
case string(model.PDFEngineSpeedata):
102+
if !speedataAvailable {
103+
pdfEngine = string(model.PDFEngineAuto)
104+
}
105+
default:
106+
pdfEngine = string(model.PDFEngineAuto)
107+
}
108+
91109
dbSettings := &model.Settings{
92110
OwnerID: ownerID,
93111
CompanyName: f.Companyname,
@@ -108,6 +126,7 @@ func (ctrl *controller) settingslist(c echo.Context) error {
108126
CustomerNumberPrefix: f.CustomerPrefix,
109127
CustomerNumberWidth: f.CustomerWidth,
110128
CustomerNumberCounter: f.CustomerCounter,
129+
PDFEngine: pdfEngine,
111130
}
112131

113132
if err := ctrl.model.SaveSettings(dbSettings); err != nil {

controller/web.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -411,9 +411,9 @@ func NewController(s *model.Store) error {
411411

412412
// Template functions available in views.
413413
var templateFunc = template.FuncMap{
414-
// autolayoutnote depends on the PDF engine the binary was built with
415-
// (boxesandglue default vs. -tags speedata).
416-
"autolayoutnote": func() string { return model.AutoLayoutNote },
414+
// autolayoutnote depends on the PDF engine resolved for the owner
415+
// (settings field pdf_engine, layout.xml, server config).
416+
"autolayoutnote": func(ownerID uint) string { return s.AutoLayoutNote(ownerID) },
417417
"htmldate": func(in time.Time) string { return in.Format("2006-01-02") },
418418
"userdate": func(in time.Time) string { return in.Format("02.01.2006") },
419419
"timeago": func(in time.Time) string { return timeagoGerman.Format(in) },
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE settings DROP COLUMN pdf_engine;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE settings ADD COLUMN pdf_engine text NOT NULL DEFAULT 'auto';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE settings DROP COLUMN pdf_engine;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE settings ADD COLUMN pdf_engine TEXT NOT NULL DEFAULT 'auto';

model/invoice_layout_generic.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
//go:build !speedata
2-
31
package model
42

53
import (

model/invoice_layout_letterhead.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
//go:build !speedata
2-
31
package model
42

53
import (

model/pdfengine.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package model
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"os"
7+
"path/filepath"
8+
)
9+
10+
// PDFEngine selects which engine renders invoice PDFs. Both engines are always
11+
// compiled in; the choice is made at runtime per owner (settings field
12+
// pdf_engine), see docs/pdf-engine-dispatch.md.
13+
type PDFEngine string
14+
15+
const (
16+
// PDFEngineAuto picks speedata when the owner has a layout.xml, otherwise
17+
// boxesandglue. This is the default.
18+
PDFEngineAuto PDFEngine = "auto"
19+
// PDFEngineSpeedata renders on the remote speedata Publishing Server.
20+
PDFEngineSpeedata PDFEngine = "speedata"
21+
// PDFEngineBag renders locally in-process with boxesandglue/bagme.
22+
PDFEngineBag PDFEngine = "boxesandglue"
23+
)
24+
25+
// resolvePDFEngine decides which engine renders the PDF. It is pure so the
26+
// full decision matrix (choice × layout.xml × server config) is testable.
27+
//
28+
// A layout.xml only makes sense with speedata, so "auto" without a configured
29+
// publishing server is an error rather than a silent fallback to boxesandglue:
30+
// whoever maintains a layout.xml must not unknowingly get a different layout
31+
// on their invoices.
32+
func resolvePDFEngine(choice PDFEngine, hasLayoutXML, serverConfigured bool) (PDFEngine, error) {
33+
switch choice {
34+
case PDFEngineBag:
35+
return PDFEngineBag, nil
36+
case PDFEngineSpeedata:
37+
if !serverConfigured {
38+
return "", fmt.Errorf("PDF-Engine speedata gewählt, aber kein Publishing-Server konfiguriert")
39+
}
40+
return PDFEngineSpeedata, nil
41+
case PDFEngineAuto, "":
42+
if !hasLayoutXML {
43+
return PDFEngineBag, nil
44+
}
45+
if !serverConfigured {
46+
return "", fmt.Errorf("layout.xml vorhanden, aber kein Publishing-Server konfiguriert")
47+
}
48+
return PDFEngineSpeedata, nil
49+
default:
50+
return "", fmt.Errorf("unbekannte PDF-Engine %q", choice)
51+
}
52+
}
53+
54+
// hasUserLayoutXML reports whether the owner keeps a layout.xml in their
55+
// userassets directory (the speedata layout file).
56+
func (s *Store) hasUserLayoutXML(ownerID uint) bool {
57+
p := filepath.Join(s.Config.Basedir, "assets", "userassets", fmt.Sprintf("owner%d", ownerID), "layout.xml")
58+
_, err := os.Stat(p)
59+
return err == nil
60+
}
61+
62+
// speedataConfigured reports whether config.toml points to a publishing server.
63+
func (s *Store) speedataConfigured() bool {
64+
return s.Config.PublishingServerAddress != ""
65+
}
66+
67+
// ResolvePDFEngine determines the effective engine for the owner from their
68+
// settings, their userassets and the server configuration.
69+
func (s *Store) ResolvePDFEngine(ownerID uint) (PDFEngine, error) {
70+
settings, err := s.LoadSettings(ownerID)
71+
if err != nil {
72+
return "", fmt.Errorf("load settings: %w", err)
73+
}
74+
return resolvePDFEngine(PDFEngine(settings.PDFEngine), s.hasUserLayoutXML(ownerID), s.speedataConfigured())
75+
}
76+
77+
// CreateZUGFeRDPDF creates a ZUGFeRD PDF file for the invoice with the engine
78+
// resolved for the owner. The CII XML is expected to exist at xmlpath and the
79+
// PDF gets written to pdfpath.
80+
func (s *Store) CreateZUGFeRDPDF(inv *Invoice, ownerID uint, xmlpath string, pdfpath string, logger *slog.Logger) error {
81+
engine, err := s.ResolvePDFEngine(ownerID)
82+
if err != nil {
83+
return err
84+
}
85+
if engine == PDFEngineSpeedata {
86+
return s.createZUGFeRDPDFSpeedata(inv, ownerID, xmlpath, pdfpath, logger)
87+
}
88+
return s.createZUGFeRDPDFBag(inv, ownerID, xmlpath, pdfpath, logger)
89+
}
90+
91+
// AutoLayoutNote describes, for the UI, what the "Automatisch" letterhead
92+
// choice renders for this owner with the engine resolved from their settings.
93+
func (s *Store) AutoLayoutNote(ownerID uint) string {
94+
engine, err := s.ResolvePDFEngine(ownerID)
95+
if err != nil {
96+
return "Achtung: " + err.Error() + " — die PDF-Erzeugung schlägt fehl."
97+
}
98+
if engine == PDFEngineSpeedata {
99+
return `Verwendet "layout.xml" über den speedata Publishing-Server.`
100+
}
101+
return "Verwendet das eingebaute Standard-Layout (DIN 5008) mit den Firmendaten aus den Einstellungen."
102+
}

0 commit comments

Comments
 (0)