Invoice generation working
This commit is contained in:
parent
a33eaa0c3c
commit
2673886033
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/email"
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/handlers"
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/handlers/attachments"
|
||||
quotes "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/handlers/quotes"
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates"
|
||||
|
|
@ -70,9 +71,6 @@ func main() {
|
|||
// Static files
|
||||
goRouter.PathPrefix("/static/").Handler(http.StripPrefix("/go/static/", http.FileServer(http.Dir("static"))))
|
||||
|
||||
// PDF files
|
||||
goRouter.PathPrefix("/pdf/").Handler(http.StripPrefix("/go/pdf/", http.FileServer(http.Dir("webroot/pdf"))))
|
||||
|
||||
// Quote routes
|
||||
goRouter.HandleFunc("/quotes", quoteHandler.QuotesOutstandingView).Methods("GET")
|
||||
goRouter.HandleFunc("/quotes/send-reminder", quoteHandler.SendManualReminder).Methods("POST")
|
||||
|
|
@ -84,6 +82,22 @@ func main() {
|
|||
goRouter.HandleFunc("/attachments/{id}", attachmentHandler.Get).Methods("GET")
|
||||
goRouter.HandleFunc("/attachments/{id}", attachmentHandler.Delete).Methods("DELETE")
|
||||
|
||||
// PDF generation routes
|
||||
goRouter.HandleFunc("/pdf/generate-invoice", handlers.GenerateInvoicePDF).Methods("POST")
|
||||
goRouter.HandleFunc("/pdf/generate-quote", handlers.GenerateQuotePDF).Methods("POST")
|
||||
goRouter.HandleFunc("/pdf/generate-po", handlers.GeneratePurchaseOrderPDF).Methods("POST")
|
||||
goRouter.HandleFunc("/pdf/generate-packinglist", handlers.GeneratePackingListPDF).Methods("POST")
|
||||
goRouter.HandleFunc("/pdf/generate-orderack", handlers.GenerateOrderAckPDF).Methods("POST")
|
||||
goRouter.HandleFunc("/pdf/count-pages", handlers.CountPages).Methods("POST")
|
||||
|
||||
// Serve generated PDFs
|
||||
pdfDir := os.Getenv("PDF_OUTPUT_DIR")
|
||||
if pdfDir == "" {
|
||||
g
|
||||
pdfDir = "webroot/pdf"
|
||||
}
|
||||
goRouter.PathPrefix("/pdf/").Handler(http.StripPrefix("/go/pdf/", http.FileServer(http.Dir(pdfDir))))
|
||||
|
||||
// The following routes are currently disabled:
|
||||
/*
|
||||
// API routes
|
||||
|
|
|
|||
37
go/go.mod
37
go/go.mod
|
|
@ -1,22 +1,51 @@
|
|||
module code.springupsoftware.com/cmc/cmc-sales
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
github.com/go-co-op/gocron v1.37.0
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/jhillyerd/enmime v1.3.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/jung-kurt/gofpdf v1.16.2
|
||||
golang.org/x/text v0.27.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
google.golang.org/api v0.259.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-co-op/gocron v1.37.0 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
google.golang.org/grpc v1.78.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
|
|
|||
98
go/go.sum
98
go/go.sum
|
|
@ -1,19 +1,44 @@
|
|||
cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc=
|
||||
cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
|
||||
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
|
||||
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
|
||||
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
|
||||
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
|
||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y=
|
||||
github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
|
||||
|
|
@ -31,6 +56,11 @@ github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NB
|
|||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
|
@ -38,11 +68,16 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
|
@ -51,23 +86,56 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.259.0 h1:90TaGVIxScrh1Vn/XI2426kRpBqHwWIzVBzJsVZ5XrQ=
|
||||
google.golang.org/api v0.259.0/go.mod h1:LC2ISWGWbRoyQVpxGntWwLWN/vLNxxKBK9KuJRI8Te4=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
602
go/internal/cmc/handlers/pdf_api.go
Normal file
602
go/internal/cmc/handlers/pdf_api.go
Normal file
|
|
@ -0,0 +1,602 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/pdf"
|
||||
)
|
||||
|
||||
// InvoiceLineItemRequest is the JSON shape for a single line item.
|
||||
type InvoiceLineItemRequest struct {
|
||||
ItemNumber string `json:"item_number"`
|
||||
Quantity string `json:"quantity"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
NetUnitPrice float64 `json:"net_unit_price"`
|
||||
NetPrice float64 `json:"net_price"`
|
||||
DiscountPercent float64 `json:"discount_percent"`
|
||||
DiscountAmountUnit float64 `json:"discount_amount_unit"`
|
||||
DiscountAmountTotal float64 `json:"discount_amount_total"`
|
||||
Option int `json:"option"`
|
||||
HasTextPrices bool `json:"has_text_prices"`
|
||||
UnitPriceString string `json:"unit_price_string"`
|
||||
GrossPriceString string `json:"gross_price_string"`
|
||||
}
|
||||
|
||||
// InvoicePDFRequest is the expected payload from the PHP app.
|
||||
type InvoicePDFRequest struct {
|
||||
DocumentID int32 `json:"document_id"`
|
||||
InvoiceTitle string `json:"invoice_title"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
ContactEmail string `json:"contact_email"`
|
||||
ContactName string `json:"contact_name"`
|
||||
UserFirstName string `json:"user_first_name"`
|
||||
UserLastName string `json:"user_last_name"`
|
||||
UserEmail string `json:"user_email"`
|
||||
YourReference string `json:"your_reference"`
|
||||
ShipVia string `json:"ship_via"`
|
||||
FOB string `json:"fob"`
|
||||
IssueDate string `json:"issue_date"` // ISO date: 2006-01-02
|
||||
IssueDateString string `json:"issue_date_string"` // Formatted: "12 January 2026"
|
||||
CurrencySymbol string `json:"currency_symbol"` // e.g. "$"
|
||||
CurrencyCode string `json:"currency_code"` // e.g. "AUD", "USD"
|
||||
ShowGST bool `json:"show_gst"`
|
||||
BillTo string `json:"bill_to"`
|
||||
ShipTo string `json:"ship_to"`
|
||||
ShippingDetails string `json:"shipping_details"`
|
||||
CustomerOrderNumber string `json:"customer_order_number"`
|
||||
JobTitle string `json:"job_title"`
|
||||
PaymentTerms string `json:"payment_terms"`
|
||||
CustomerABN string `json:"customer_abn"`
|
||||
Subtotal interface{} `json:"subtotal"` // Can be float or "TBA"
|
||||
GSTAmount interface{} `json:"gst_amount"`
|
||||
Total interface{} `json:"total"`
|
||||
LineItems []InvoiceLineItemRequest `json:"line_items"`
|
||||
OutputDir string `json:"output_dir"` // optional override
|
||||
}
|
||||
|
||||
// GenerateInvoicePDF handles POST /api/pdf/invoice and writes a PDF to disk.
|
||||
// It returns JSON: {"filename":"<name>.pdf"}
|
||||
func GenerateInvoicePDF(w http.ResponseWriter, r *http.Request) {
|
||||
var req InvoicePDFRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.InvoiceTitle == "" || req.CustomerName == "" {
|
||||
http.Error(w, "invoice_title and customer_name are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
issueDate := time.Now()
|
||||
if req.IssueDate != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", req.IssueDate); err == nil {
|
||||
issueDate = parsed
|
||||
}
|
||||
}
|
||||
|
||||
outputDir := req.OutputDir
|
||||
if outputDir == "" {
|
||||
outputDir = os.Getenv("PDF_OUTPUT_DIR")
|
||||
}
|
||||
if outputDir == "" {
|
||||
outputDir = "../php/app/webroot/pdf"
|
||||
}
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
log.Printf("GenerateInvoicePDF: failed to create output dir: %v", err)
|
||||
http.Error(w, "failed to prepare output directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Map request into the existing PDF generation types.
|
||||
doc := &db.Document{ID: req.DocumentID}
|
||||
inv := &db.Invoice{Title: req.InvoiceTitle}
|
||||
cust := &db.Customer{Name: req.CustomerName}
|
||||
|
||||
lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems))
|
||||
for i, li := range req.LineItems {
|
||||
lineItems[i] = db.GetLineItemsTableRow{
|
||||
ItemNumber: li.ItemNumber,
|
||||
Quantity: li.Quantity,
|
||||
Title: li.Title,
|
||||
GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true},
|
||||
GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true},
|
||||
}
|
||||
}
|
||||
|
||||
// Convert interface{} totals to float64
|
||||
subtotal := 0.0
|
||||
if val, ok := req.Subtotal.(float64); ok {
|
||||
subtotal = val
|
||||
}
|
||||
gstAmount := 0.0
|
||||
if val, ok := req.GSTAmount.(float64); ok {
|
||||
gstAmount = val
|
||||
}
|
||||
total := 0.0
|
||||
if val, ok := req.Total.(float64); ok {
|
||||
total = val
|
||||
}
|
||||
|
||||
data := &pdf.InvoicePDFData{
|
||||
Document: doc,
|
||||
Invoice: inv,
|
||||
Customer: cust,
|
||||
LineItems: lineItems,
|
||||
CurrencySymbol: req.CurrencySymbol,
|
||||
CurrencyCode: req.CurrencyCode,
|
||||
ShowGST: req.ShowGST,
|
||||
ShipVia: req.ShipVia,
|
||||
FOB: req.FOB,
|
||||
IssueDate: issueDate,
|
||||
IssueDateString: req.IssueDateString,
|
||||
EmailTo: req.ContactEmail,
|
||||
Attention: req.ContactName,
|
||||
FromName: fmt.Sprintf("%s %s", req.UserFirstName, req.UserLastName),
|
||||
FromEmail: req.UserEmail,
|
||||
YourReference: req.YourReference,
|
||||
BillTo: req.BillTo,
|
||||
ShipTo: req.ShipTo,
|
||||
ShippingDetails: req.ShippingDetails,
|
||||
CustomerOrderNumber: req.CustomerOrderNumber,
|
||||
JobTitle: req.JobTitle,
|
||||
PaymentTerms: req.PaymentTerms,
|
||||
CustomerABN: req.CustomerABN,
|
||||
Subtotal: subtotal,
|
||||
GSTAmount: gstAmount,
|
||||
Total: total,
|
||||
}
|
||||
|
||||
filename, err := pdf.GenerateInvoicePDF(data, outputDir)
|
||||
if err != nil {
|
||||
log.Printf("GenerateInvoicePDF: failed to generate PDF: %v", err)
|
||||
http.Error(w, "failed to generate PDF", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"filename": filename})
|
||||
}
|
||||
|
||||
// QuoteLineItemRequest reuses the invoice item shape
|
||||
type QuoteLineItemRequest = InvoiceLineItemRequest
|
||||
|
||||
// QuotePDFRequest payload from PHP for quotes
|
||||
type QuotePDFRequest struct {
|
||||
DocumentID int32 `json:"document_id"`
|
||||
CmcReference string `json:"cmc_reference"`
|
||||
Revision int32 `json:"revision"`
|
||||
CreatedDate string `json:"created_date"` // YYYY-MM-DD
|
||||
CreatedDateString string `json:"created_date_string"` // j M Y format
|
||||
DateIssued string `json:"date_issued"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
ContactEmail string `json:"contact_email"`
|
||||
ContactName string `json:"contact_name"`
|
||||
UserFirstName string `json:"user_first_name"`
|
||||
UserLastName string `json:"user_last_name"`
|
||||
UserEmail string `json:"user_email"`
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
CurrencyCode string `json:"currency_code"`
|
||||
ShowGST bool `json:"show_gst"`
|
||||
CommercialComments string `json:"commercial_comments"`
|
||||
DeliveryTime string `json:"delivery_time"`
|
||||
PaymentTerms string `json:"payment_terms"`
|
||||
DaysValid int32 `json:"daysValid"`
|
||||
DeliveryPoint string `json:"delivery_point"`
|
||||
ExchangeRate string `json:"exchange_rate"`
|
||||
CustomsDuty string `json:"customs_duty"`
|
||||
GSTPhrase string `json:"gst_phrase"`
|
||||
SalesEngineer string `json:"sales_engineer"`
|
||||
BillTo string `json:"bill_to"`
|
||||
ShipTo string `json:"ship_to"`
|
||||
LineItems []QuoteLineItemRequest `json:"line_items"`
|
||||
Pages []string `json:"pages"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
}
|
||||
|
||||
// GenerateQuotePDF handles POST /go/pdf/generate-quote
|
||||
func GenerateQuotePDF(w http.ResponseWriter, r *http.Request) {
|
||||
var req QuotePDFRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.CmcReference == "" || req.CustomerName == "" {
|
||||
http.Error(w, "cmc_reference and customer_name are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
created := time.Now()
|
||||
if req.CreatedDate != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", req.CreatedDate); err == nil {
|
||||
created = parsed
|
||||
}
|
||||
}
|
||||
|
||||
outputDir := req.OutputDir
|
||||
if outputDir == "" {
|
||||
outputDir = os.Getenv("PDF_OUTPUT_DIR")
|
||||
}
|
||||
if outputDir == "" {
|
||||
outputDir = "../php/app/webroot/pdf"
|
||||
}
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
log.Printf("GenerateQuotePDF: failed to create output dir: %v", err)
|
||||
http.Error(w, "failed to prepare output directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Map request into PDF data
|
||||
doc := &db.Document{ID: req.DocumentID, CmcReference: req.CmcReference, Revision: req.Revision, Created: created}
|
||||
cust := &db.Customer{Name: req.CustomerName}
|
||||
user := &db.GetUserRow{FirstName: req.UserFirstName, LastName: req.UserLastName, Email: req.UserEmail}
|
||||
|
||||
lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems))
|
||||
for i, li := range req.LineItems {
|
||||
lineItems[i] = db.GetLineItemsTableRow{
|
||||
ItemNumber: li.ItemNumber,
|
||||
Quantity: li.Quantity,
|
||||
Title: li.Title,
|
||||
GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true},
|
||||
GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true},
|
||||
}
|
||||
}
|
||||
|
||||
data := &pdf.QuotePDFData{
|
||||
Document: doc,
|
||||
Customer: cust,
|
||||
EmailTo: req.ContactEmail,
|
||||
Attention: req.ContactName,
|
||||
User: user,
|
||||
LineItems: lineItems,
|
||||
CurrencySymbol: req.CurrencySymbol,
|
||||
CurrencyCode: req.CurrencyCode,
|
||||
ShowGST: req.ShowGST,
|
||||
CommercialComments: req.CommercialComments,
|
||||
DeliveryTime: req.DeliveryTime,
|
||||
PaymentTerms: req.PaymentTerms,
|
||||
DaysValid: int(req.DaysValid),
|
||||
DeliveryPoint: req.DeliveryPoint,
|
||||
ExchangeRate: req.ExchangeRate,
|
||||
CustomsDuty: req.CustomsDuty,
|
||||
GSTPhrase: req.GSTPhrase,
|
||||
SalesEngineer: req.SalesEngineer,
|
||||
BillTo: req.BillTo,
|
||||
ShipTo: req.ShipTo,
|
||||
IssueDateString: req.CreatedDateString,
|
||||
Pages: req.Pages,
|
||||
}
|
||||
|
||||
filename, err := pdf.GenerateQuotePDF(data, outputDir)
|
||||
if err != nil {
|
||||
log.Printf("GenerateQuotePDF: failed to generate PDF: %v", err)
|
||||
http.Error(w, "failed to generate PDF", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"filename": filename})
|
||||
}
|
||||
|
||||
// PurchaseOrderLineItemRequest reuses the invoice item shape
|
||||
type PurchaseOrderLineItemRequest = InvoiceLineItemRequest
|
||||
|
||||
// PurchaseOrderPDFRequest payload from PHP for POs
|
||||
type PurchaseOrderPDFRequest struct {
|
||||
DocumentID int32 `json:"document_id"`
|
||||
Title string `json:"title"`
|
||||
IssueDate string `json:"issue_date"` // YYYY-MM-DD
|
||||
IssueDateString string `json:"issue_date_string"` // formatted date
|
||||
PrincipleName string `json:"principle_name"`
|
||||
PrincipleReference string `json:"principle_reference"`
|
||||
OrderedFrom string `json:"ordered_from"`
|
||||
DispatchBy string `json:"dispatch_by"`
|
||||
DeliverTo string `json:"deliver_to"`
|
||||
ShippingInstructions string `json:"shipping_instructions"`
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
CurrencyCode string `json:"currency_code"`
|
||||
ShowGST bool `json:"show_gst"`
|
||||
Subtotal float64 `json:"subtotal"`
|
||||
GSTAmount float64 `json:"gst_amount"`
|
||||
Total float64 `json:"total"`
|
||||
LineItems []PurchaseOrderLineItemRequest `json:"line_items"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
}
|
||||
|
||||
// GeneratePurchaseOrderPDF handles POST /go/pdf/generate-po
|
||||
func GeneratePurchaseOrderPDF(w http.ResponseWriter, r *http.Request) {
|
||||
var req PurchaseOrderPDFRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Title == "" || req.PrincipleName == "" {
|
||||
http.Error(w, "title and principle_name are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
issueDate := time.Now()
|
||||
if req.IssueDate != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", req.IssueDate); err == nil {
|
||||
issueDate = parsed
|
||||
}
|
||||
}
|
||||
|
||||
outputDir := req.OutputDir
|
||||
if outputDir == "" {
|
||||
outputDir = os.Getenv("PDF_OUTPUT_DIR")
|
||||
}
|
||||
if outputDir == "" {
|
||||
outputDir = "../php/app/webroot/pdf"
|
||||
}
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
log.Printf("GeneratePurchaseOrderPDF: failed to create output dir: %v", err)
|
||||
http.Error(w, "failed to prepare output directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
doc := &db.Document{ID: req.DocumentID}
|
||||
po := &db.PurchaseOrder{
|
||||
Title: req.Title,
|
||||
PrincipleReference: req.PrincipleReference,
|
||||
IssueDate: issueDate,
|
||||
OrderedFrom: req.OrderedFrom,
|
||||
DispatchBy: req.DispatchBy,
|
||||
DeliverTo: req.DeliverTo,
|
||||
ShippingInstructions: req.ShippingInstructions,
|
||||
}
|
||||
principle := &db.Principle{Name: req.PrincipleName}
|
||||
|
||||
lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems))
|
||||
for i, li := range req.LineItems {
|
||||
lineItems[i] = db.GetLineItemsTableRow{
|
||||
ItemNumber: li.ItemNumber,
|
||||
Quantity: li.Quantity,
|
||||
Title: li.Title,
|
||||
GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true},
|
||||
GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true},
|
||||
}
|
||||
}
|
||||
|
||||
data := &pdf.PurchaseOrderPDFData{
|
||||
Document: doc,
|
||||
PurchaseOrder: po,
|
||||
Principle: principle,
|
||||
LineItems: lineItems,
|
||||
CurrencySymbol: req.CurrencySymbol,
|
||||
CurrencyCode: req.CurrencyCode,
|
||||
ShowGST: req.ShowGST,
|
||||
Subtotal: req.Subtotal,
|
||||
GSTAmount: req.GSTAmount,
|
||||
Total: req.Total,
|
||||
IssueDateString: req.IssueDateString,
|
||||
}
|
||||
|
||||
filename, err := pdf.GeneratePurchaseOrderPDF(data, outputDir)
|
||||
if err != nil {
|
||||
log.Printf("GeneratePurchaseOrderPDF: failed to generate PDF: %v", err)
|
||||
http.Error(w, "failed to generate PDF", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"filename": filename})
|
||||
}
|
||||
|
||||
// GeneratePackingListPDF handles POST /go/pdf/generate-packinglist
|
||||
func GeneratePackingListPDF(w http.ResponseWriter, r *http.Request) {
|
||||
var req PackingListPDFRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.CustomerName == "" {
|
||||
http.Error(w, "customer_name is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
outputDir := req.OutputDir
|
||||
if outputDir == "" {
|
||||
outputDir = os.Getenv("PDF_OUTPUT_DIR")
|
||||
}
|
||||
if outputDir == "" {
|
||||
outputDir = "../php/app/webroot/pdf"
|
||||
}
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
log.Printf("GeneratePackingListPDF: failed to create output dir: %v", err)
|
||||
http.Error(w, "failed to prepare output directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Reuse the invoice generator structure but label as PACKING LIST via DetailsBox
|
||||
// Build minimal data shape
|
||||
doc := &db.Document{ID: req.DocumentID}
|
||||
cust := &db.Customer{Name: req.CustomerName}
|
||||
|
||||
lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems))
|
||||
for i, li := range req.LineItems {
|
||||
lineItems[i] = db.GetLineItemsTableRow{
|
||||
ItemNumber: li.ItemNumber,
|
||||
Quantity: li.Quantity,
|
||||
Title: li.Title,
|
||||
GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true},
|
||||
GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true},
|
||||
}
|
||||
}
|
||||
|
||||
data := &pdf.PackingListPDFData{
|
||||
Document: doc,
|
||||
Customer: cust,
|
||||
Title: req.Title,
|
||||
JobTitle: req.JobTitle,
|
||||
IssueDate: req.IssueDate,
|
||||
IssueDateString: req.IssueDateString,
|
||||
ShipVia: req.ShipVia,
|
||||
FOB: req.FOB,
|
||||
LineItems: lineItems,
|
||||
CurrencySymbol: req.CurrencySymbol,
|
||||
CurrencyCode: req.CurrencyCode,
|
||||
ShowGST: req.ShowGST,
|
||||
}
|
||||
|
||||
filename, err := pdf.GeneratePackingListPDF(data, outputDir)
|
||||
if err != nil {
|
||||
log.Printf("GeneratePackingListPDF: failed to generate PDF: %v", err)
|
||||
http.Error(w, "failed to generate PDF", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"filename": filename})
|
||||
}
|
||||
|
||||
// GenerateOrderAckPDF handles POST /go/pdf/generate-orderack
|
||||
func GenerateOrderAckPDF(w http.ResponseWriter, r *http.Request) {
|
||||
var req OrderAckPDFRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.CustomerName == "" {
|
||||
http.Error(w, "customer_name is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
outputDir := req.OutputDir
|
||||
if outputDir == "" {
|
||||
outputDir = os.Getenv("PDF_OUTPUT_DIR")
|
||||
}
|
||||
if outputDir == "" {
|
||||
outputDir = "../php/app/webroot/pdf"
|
||||
}
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
log.Printf("GenerateOrderAckPDF: failed to create output dir: %v", err)
|
||||
http.Error(w, "failed to prepare output directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
doc := &db.Document{ID: req.DocumentID}
|
||||
cust := &db.Customer{Name: req.CustomerName}
|
||||
|
||||
lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems))
|
||||
for i, li := range req.LineItems {
|
||||
lineItems[i] = db.GetLineItemsTableRow{
|
||||
ItemNumber: li.ItemNumber,
|
||||
Quantity: li.Quantity,
|
||||
Title: li.Title,
|
||||
GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true},
|
||||
GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true},
|
||||
}
|
||||
}
|
||||
|
||||
data := &pdf.OrderAckPDFData{
|
||||
Document: doc,
|
||||
Customer: cust,
|
||||
Title: req.Title,
|
||||
JobTitle: req.JobTitle,
|
||||
IssueDate: req.IssueDate,
|
||||
IssueDateString: req.IssueDateString,
|
||||
ShipVia: req.ShipVia,
|
||||
FOB: req.FOB,
|
||||
EstimatedDelivery: req.EstimatedDelivery,
|
||||
LineItems: lineItems,
|
||||
CurrencySymbol: req.CurrencySymbol,
|
||||
CurrencyCode: req.CurrencyCode,
|
||||
ShowGST: req.ShowGST,
|
||||
}
|
||||
|
||||
filename, err := pdf.GenerateOrderAckPDF(data, outputDir)
|
||||
if err != nil {
|
||||
log.Printf("GenerateOrderAckPDF: failed to generate PDF: %v", err)
|
||||
http.Error(w, "failed to generate PDF", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"filename": filename})
|
||||
}
|
||||
|
||||
// PackingListLineItemRequest reuses the invoice item shape
|
||||
type PackingListLineItemRequest = InvoiceLineItemRequest
|
||||
|
||||
// PackingListPDFRequest payload
|
||||
type PackingListPDFRequest struct {
|
||||
DocumentID int32 `json:"document_id"`
|
||||
Title string `json:"title"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
JobTitle string `json:"job_title"`
|
||||
IssueDate string `json:"issue_date"` // YYYY-MM-DD
|
||||
IssueDateString string `json:"issue_date_string"` // formatted date
|
||||
ShipVia string `json:"ship_via"`
|
||||
FOB string `json:"fob"`
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
CurrencyCode string `json:"currency_code"`
|
||||
ShowGST bool `json:"show_gst"`
|
||||
LineItems []PackingListLineItemRequest `json:"line_items"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
}
|
||||
|
||||
// OrderAckLineItemRequest reuses the invoice item shape
|
||||
type OrderAckLineItemRequest = InvoiceLineItemRequest
|
||||
|
||||
// OrderAckPDFRequest payload
|
||||
type OrderAckPDFRequest struct {
|
||||
DocumentID int32 `json:"document_id"`
|
||||
Title string `json:"title"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
JobTitle string `json:"job_title"`
|
||||
IssueDate string `json:"issue_date"` // YYYY-MM-DD
|
||||
IssueDateString string `json:"issue_date_string"` // formatted date
|
||||
ShipVia string `json:"ship_via"`
|
||||
FOB string `json:"fob"`
|
||||
EstimatedDelivery string `json:"estimated_delivery"`
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
CurrencyCode string `json:"currency_code"`
|
||||
ShowGST bool `json:"show_gst"`
|
||||
LineItems []OrderAckLineItemRequest `json:"line_items"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
}
|
||||
|
||||
// CountPagesRequest payload for page counting
|
||||
type CountPagesRequest struct {
|
||||
FilePath string `json:"file_path"`
|
||||
}
|
||||
|
||||
// CountPages handles POST /go/pdf/count-pages
|
||||
// Returns JSON: {"page_count": <number>} or {"error": "<message>"}
|
||||
func CountPages(w http.ResponseWriter, r *http.Request) {
|
||||
var req CountPagesRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.FilePath == "" {
|
||||
http.Error(w, "file_path is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Count pages in the PDF file
|
||||
pageCount, err := pdf.CountPDFPages(req.FilePath)
|
||||
if err != nil {
|
||||
log.Printf("CountPages: error counting pages in %s: %v", req.FilePath, err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"page_count": 0,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]int{"page_count": pageCount})
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package pdf
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jung-kurt/gofpdf"
|
||||
|
|
@ -286,6 +287,316 @@ func (g *Generator) AddLineItemsTable(items []LineItem, currencySymbol string, s
|
|||
}
|
||||
}
|
||||
|
||||
// AddInvoiceShippingBillingBox adds the shipping/billing box with payment details matching old TCPDF layout
|
||||
func (g *Generator) AddInvoiceShippingBillingBox(data interface{}) {
|
||||
invoiceData := data.(*InvoicePDFData)
|
||||
|
||||
// Column widths: 30%, 30%, 3% spacer, 37%
|
||||
pageWidth := 210.0 // A4 width in mm
|
||||
margin := 10.0
|
||||
contentWidth := pageWidth - 2*margin // ~190mm
|
||||
|
||||
soldToWidth := contentWidth * 0.30 // ~57mm
|
||||
deliveryWidth := contentWidth * 0.30 // ~57mm
|
||||
spacerWidth := contentWidth * 0.03 // ~5.7mm
|
||||
invoiceDetailsWidth := contentWidth * 0.37 // ~70mm
|
||||
|
||||
// Row 1: Headers with gray background
|
||||
g.pdf.SetFillColor(242, 242, 242) // Light gray
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.SetDrawColor(0, 0, 0)
|
||||
g.pdf.SetLineWidth(0.5)
|
||||
|
||||
// "Sold To / Invoice Address:"
|
||||
g.pdf.CellFormat(soldToWidth, 8, "Sold To / Invoice Address:", "1", 0, "C", true, 0, "")
|
||||
|
||||
// "Delivery Address:"
|
||||
g.pdf.CellFormat(deliveryWidth, 8, "Delivery Address:", "1", 0, "C", true, 0, "")
|
||||
|
||||
// Spacer (no border)
|
||||
g.pdf.SetDrawColor(255, 255, 255)
|
||||
g.pdf.CellFormat(spacerWidth, 8, "", "0", 0, "C", false, 0, "")
|
||||
g.pdf.SetDrawColor(0, 0, 0)
|
||||
|
||||
// Right column header spanning 2 rows - "MAKE PAYMENT TO:"
|
||||
g.pdf.CellFormat(invoiceDetailsWidth, 8, "MAKE PAYMENT TO:", "1", 1, "C", true, 0, "")
|
||||
|
||||
// Row 2: Address content
|
||||
g.pdf.SetFont("Helvetica", "", 9)
|
||||
g.pdf.SetFillColor(255, 255, 255) // White background
|
||||
|
||||
// Bill To address (left column)
|
||||
billToLines := len(invoiceData.BillTo)
|
||||
maxHeight := float64(billToLines) * 4.5
|
||||
startX := g.pdf.GetX()
|
||||
|
||||
g.pdf.MultiCell(soldToWidth, 4.5, invoiceData.BillTo, "1", "L", false)
|
||||
leftY := g.pdf.GetY()
|
||||
|
||||
// Ship To address (middle column)
|
||||
g.pdf.SetXY(startX+soldToWidth, g.pdf.GetY()-maxHeight)
|
||||
g.pdf.MultiCell(deliveryWidth, 4.5, invoiceData.ShipTo, "1", "L", false)
|
||||
|
||||
// Spacer
|
||||
g.pdf.SetXY(startX+soldToWidth+deliveryWidth, g.pdf.GetY()-maxHeight)
|
||||
g.pdf.SetDrawColor(255, 255, 255)
|
||||
g.pdf.CellFormat(spacerWidth, maxHeight, "", "0", 0, "C", false, 0, "")
|
||||
g.pdf.SetDrawColor(0, 0, 0)
|
||||
|
||||
// Invoice details and payment info (right column)
|
||||
g.pdf.SetXY(startX+soldToWidth+deliveryWidth+spacerWidth, g.pdf.GetY()-maxHeight)
|
||||
g.pdf.SetFont("Helvetica", "B", 9)
|
||||
|
||||
paymentText := "CMC INVOICE#:\n" + invoiceData.Invoice.Title + "\n\n"
|
||||
paymentText += "Date:\n" + invoiceData.IssueDateString + "\n\n"
|
||||
paymentText += "Page:\n1\n\n"
|
||||
|
||||
// Add currency-specific bank details
|
||||
bankDetails := g.getBankDetails(invoiceData.CurrencyCode)
|
||||
for k, v := range bankDetails {
|
||||
paymentText += k + ":\n" + v + "\n\n"
|
||||
}
|
||||
|
||||
g.pdf.SetFont("Helvetica", "", 8)
|
||||
g.pdf.MultiCell(invoiceDetailsWidth, 3.5, paymentText, "1", "L", false)
|
||||
|
||||
// Move to next line after the tallest cell
|
||||
maxY := math.Max(leftY, g.pdf.GetY())
|
||||
g.pdf.SetXY(startX, maxY)
|
||||
g.pdf.Ln(2)
|
||||
}
|
||||
|
||||
// getBankDetails returns currency-specific bank account information
|
||||
func (g *Generator) getBankDetails(currencyCode string) map[string]string {
|
||||
details := make(map[string]string)
|
||||
|
||||
switch currencyCode {
|
||||
case "EUR":
|
||||
details["Account"] = "06200015682004"
|
||||
details["Branch"] = "06200"
|
||||
details["SWIFT"] = "CTBAAU2S"
|
||||
case "GBP":
|
||||
details["Account"] = "06200015642694"
|
||||
details["Branch"] = "06200"
|
||||
details["SWIFT"] = "CTBAAU2S"
|
||||
case "USD":
|
||||
details["Account"] = "06200015681984"
|
||||
details["Branch"] = "06200"
|
||||
details["SWIFT"] = "CTBAAU2S"
|
||||
default: // AUD
|
||||
details["BSB"] = "062-458"
|
||||
details["Account"] = "10067982"
|
||||
details["SWIFT"] = "CTBAAU2S"
|
||||
details["IBAN"] = "06245810067982"
|
||||
}
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
// AddInvoiceDetailsTable adds the order number, job, FOB, payment terms, ABN table
|
||||
func (g *Generator) AddInvoiceDetailsTable(data interface{}) {
|
||||
invoiceData := data.(*InvoicePDFData)
|
||||
|
||||
pageWidth := 210.0
|
||||
margin := 10.0
|
||||
contentWidth := pageWidth - 2*margin
|
||||
colWidth := contentWidth / 5 // 5 equal columns
|
||||
|
||||
g.pdf.Ln(3)
|
||||
|
||||
// Header row with gray background
|
||||
g.pdf.SetFillColor(242, 242, 242)
|
||||
g.pdf.SetFont("Helvetica", "B", 9)
|
||||
g.pdf.SetDrawColor(0, 0, 0)
|
||||
g.pdf.SetLineWidth(0.5)
|
||||
|
||||
headers := []string{
|
||||
"CUSTOMER ORDER NO",
|
||||
"CMC JOB #",
|
||||
"INCOTERMS 2010",
|
||||
"PAYMENT TERMS",
|
||||
"CUSTOMER ABN",
|
||||
}
|
||||
|
||||
for _, header := range headers {
|
||||
g.pdf.CellFormat(colWidth, 6, header, "1", 0, "C", true, 0, "")
|
||||
}
|
||||
g.pdf.Ln()
|
||||
|
||||
// Data row
|
||||
g.pdf.SetFont("Helvetica", "", 9)
|
||||
g.pdf.SetFillColor(255, 255, 255)
|
||||
|
||||
rowData := []string{
|
||||
invoiceData.CustomerOrderNumber,
|
||||
invoiceData.Invoice.Title,
|
||||
invoiceData.FOB,
|
||||
invoiceData.PaymentTerms,
|
||||
invoiceData.CustomerABN,
|
||||
}
|
||||
|
||||
for _, value := range rowData {
|
||||
g.pdf.CellFormat(colWidth, 6, value, "1", 0, "C", false, 0, "")
|
||||
}
|
||||
g.pdf.Ln(2)
|
||||
}
|
||||
|
||||
// AddInvoiceLineItemsTableWithShipping adds line items table with freight and totals
|
||||
func (g *Generator) AddInvoiceLineItemsTableWithShipping(data interface{}) {
|
||||
invoiceData := data.(*InvoicePDFData)
|
||||
|
||||
pageWidth := 210.0
|
||||
margin := 10.0
|
||||
contentWidth := pageWidth - 2*margin
|
||||
|
||||
// Column widths (variable for item no, then equal for others)
|
||||
itemNoWidth := contentWidth * 0.10 // Item number column
|
||||
qtyWidth := contentWidth * 0.08 // Qty column
|
||||
descWidth := contentWidth * 0.42 // Description (larger)
|
||||
unitPriceWidth := contentWidth * 0.20 // Unit Price
|
||||
totalPriceWidth := contentWidth * 0.20 // Total Price
|
||||
|
||||
g.pdf.Ln(2)
|
||||
g.pdf.SetFont("Helvetica", "B", 9)
|
||||
g.pdf.SetFillColor(242, 242, 242)
|
||||
g.pdf.SetDrawColor(0, 0, 0)
|
||||
g.pdf.SetLineWidth(0.5)
|
||||
|
||||
// Table header
|
||||
g.pdf.CellFormat(itemNoWidth, 6, "ITEM NO.", "1", 0, "C", true, 0, "")
|
||||
g.pdf.CellFormat(qtyWidth, 6, "QTY", "1", 0, "C", true, 0, "")
|
||||
g.pdf.CellFormat(descWidth, 6, "DESCRIPTION", "1", 0, "C", true, 0, "")
|
||||
g.pdf.CellFormat(unitPriceWidth, 6, "UNIT PRICE", "1", 0, "C", true, 0, "")
|
||||
g.pdf.CellFormat(totalPriceWidth, 6, "TOTAL PRICE", "1", 1, "C", true, 0, "")
|
||||
|
||||
// Currency row
|
||||
g.pdf.SetFont("Helvetica", "", 8)
|
||||
g.pdf.SetFillColor(255, 255, 255)
|
||||
g.pdf.CellFormat(itemNoWidth, 4, "", "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(qtyWidth, 4, "", "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(descWidth, 4, "", "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(unitPriceWidth, 4, invoiceData.CurrencyCode, "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(totalPriceWidth, 4, invoiceData.CurrencyCode, "1", 1, "C", false, 0, "")
|
||||
|
||||
// Line items
|
||||
g.pdf.SetFont("Helvetica", "", 9)
|
||||
startY := g.pdf.GetY()
|
||||
|
||||
for _, item := range invoiceData.LineItems {
|
||||
// Item number
|
||||
g.pdf.CellFormat(itemNoWidth, 5, item.ItemNumber, "1", 0, "L", false, 0, "")
|
||||
|
||||
// Quantity
|
||||
g.pdf.CellFormat(qtyWidth, 5, item.Quantity, "1", 0, "C", false, 0, "")
|
||||
|
||||
// Title and description (multi-line)
|
||||
descText := item.Title
|
||||
if item.Description != "" {
|
||||
descText += "\n" + item.Description
|
||||
}
|
||||
if item.Option {
|
||||
descText = "Option: " + descText
|
||||
}
|
||||
|
||||
// Calculate description cell height based on content
|
||||
descLines := len([]rune(descText))/50 + 1 // Rough estimation
|
||||
descHeight := float64(descLines) * 4.5
|
||||
|
||||
currentX := g.pdf.GetX()
|
||||
currentY := g.pdf.GetY()
|
||||
|
||||
g.pdf.MultiCell(descWidth, 4.5, descText, "1", "L", false)
|
||||
|
||||
// Back up to same Y as item no to place price cells
|
||||
newY := g.pdf.GetY()
|
||||
g.pdf.SetXY(currentX+descWidth, currentY)
|
||||
|
||||
// Unit Price (with discount breakdown if applicable)
|
||||
unitPrice := item.GrossUnitPrice.String
|
||||
netPrice := item.NetUnitPrice.String
|
||||
discountPercent := item.DiscountPercent.String
|
||||
discountAmount := item.DiscountAmountUnit.String
|
||||
|
||||
priceText := unitPrice
|
||||
if discountPercent != "" && discountPercent != "0" {
|
||||
priceText += "\nless " + discountPercent + "%*"
|
||||
if discountAmount != "" {
|
||||
priceText += "\n(-" + discountAmount + ")"
|
||||
}
|
||||
priceText += "\n=\n" + netPrice
|
||||
}
|
||||
|
||||
g.pdf.MultiCell(unitPriceWidth, 4.5, priceText, "1", "R", false)
|
||||
|
||||
// Back up again for total price
|
||||
g.pdf.SetXY(currentX+descWidth+unitPriceWidth, currentY)
|
||||
|
||||
// Total Price (with discount breakdown if applicable)
|
||||
totalPrice := item.GrossPrice.String
|
||||
netTotalPrice := item.NetPrice.String
|
||||
discountTotal := item.DiscountAmountTotal.String
|
||||
|
||||
totalText := totalPrice
|
||||
if discountPercent != "" && discountPercent != "0" {
|
||||
totalText += "\nless " + discountPercent + "%*"
|
||||
if discountTotal != "" {
|
||||
totalText += "\n(-" + discountTotal + ")"
|
||||
}
|
||||
totalText += "\n=\n" + netTotalPrice
|
||||
}
|
||||
|
||||
g.pdf.MultiCell(totalPriceWidth, 4.5, totalText, "1", "R", false)
|
||||
|
||||
// Move to the bottom of all cells in this row
|
||||
g.pdf.SetXY(margin, newY)
|
||||
}
|
||||
|
||||
currentY := g.pdf.GetY()
|
||||
|
||||
// FREIGHT DETAILS row
|
||||
g.pdf.SetFillColor(242, 242, 242)
|
||||
g.pdf.SetFont("Helvetica", "B", 9)
|
||||
g.pdf.CellFormat(itemNoWidth+qtyWidth+descWidth, 6, "FREIGHT DETAILS:", "1", 0, "C", true, 0, "")
|
||||
g.pdf.CellFormat(unitPriceWidth, 6, "SUBTOTAL", "1", 0, "C", true, 0, "")
|
||||
g.pdf.CellFormat(totalPriceWidth, 6, formatCurrency(invoiceData.Subtotal, invoiceData.CurrencySymbol), "1", 1, "R", true, 0, "")
|
||||
|
||||
// Shipping details row (rowspan=2)
|
||||
g.pdf.SetFont("Helvetica", "", 8)
|
||||
g.pdf.SetFillColor(255, 255, 255)
|
||||
currentX := g.pdf.GetX()
|
||||
currentY = g.pdf.GetY()
|
||||
|
||||
// Shipping details spanning 3 columns and 2 rows
|
||||
shippingText := invoiceData.ShippingDetails
|
||||
if shippingText == "" {
|
||||
shippingText = "TBA"
|
||||
}
|
||||
g.pdf.MultiCell(itemNoWidth+qtyWidth+descWidth, 12, shippingText, "1", "L", false)
|
||||
|
||||
// Move back to add GST row next to shipping details
|
||||
g.pdf.SetXY(currentX+itemNoWidth+qtyWidth+descWidth, currentY)
|
||||
g.pdf.SetFillColor(242, 242, 242)
|
||||
g.pdf.SetFont("Helvetica", "B", 9)
|
||||
g.pdf.CellFormat(unitPriceWidth, 6, "GST", "1", 0, "C", true, 0, "")
|
||||
g.pdf.CellFormat(totalPriceWidth, 6, formatCurrency(invoiceData.GSTAmount, invoiceData.CurrencySymbol), "1", 1, "R", true, 0, "")
|
||||
|
||||
// TOTAL row
|
||||
g.pdf.SetFillColor(242, 242, 242)
|
||||
g.pdf.SetFont("Helvetica", "B", 9)
|
||||
g.pdf.CellFormat(itemNoWidth+qtyWidth+descWidth, 6, "", "0", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(unitPriceWidth, 6, "TOTAL", "1", 0, "C", true, 0, "")
|
||||
|
||||
totalDisplay := formatCurrency(invoiceData.Total, invoiceData.CurrencySymbol)
|
||||
g.pdf.CellFormat(totalPriceWidth, 6, totalDisplay, "1", 1, "R", true, 0, "")
|
||||
|
||||
g.pdf.Ln(2)
|
||||
}
|
||||
|
||||
// formatCurrency formats a float as currency string
|
||||
func formatCurrency(amount float64, symbol string) string {
|
||||
return fmt.Sprintf("%s%.2f", symbol, amount)
|
||||
}
|
||||
|
||||
// Save saves the PDF to a file
|
||||
func (g *Generator) Save(filename string) error {
|
||||
g.pdf.AliasNbPages("")
|
||||
|
|
|
|||
|
|
@ -9,16 +9,16 @@ import (
|
|||
|
||||
// QuotePDFData contains all data needed to generate a quote PDF
|
||||
type QuotePDFData struct {
|
||||
Document *db.Document
|
||||
Quote interface{} // Quote specific data
|
||||
Enquiry *db.Enquiry
|
||||
Customer *db.Customer
|
||||
Contact interface{} // Contact data
|
||||
User *db.GetUserRow
|
||||
LineItems []db.GetLineItemsTableRow
|
||||
Currency interface{} // Currency data
|
||||
CurrencySymbol string
|
||||
ShowGST bool
|
||||
Document *db.Document
|
||||
Quote interface{} // Quote specific data
|
||||
Enquiry *db.Enquiry
|
||||
Customer *db.Customer
|
||||
Contact interface{} // Contact data
|
||||
User *db.GetUserRow
|
||||
LineItems []db.GetLineItemsTableRow
|
||||
Currency interface{} // Currency data
|
||||
CurrencySymbol string
|
||||
ShowGST bool
|
||||
CommercialComments string
|
||||
}
|
||||
|
||||
|
|
@ -26,50 +26,50 @@ type QuotePDFData struct {
|
|||
func GenerateQuotePDF(data *QuotePDFData, outputDir string) (string, error) {
|
||||
fmt.Printf("GenerateQuotePDF called with outputDir: %s\n", outputDir)
|
||||
gen := NewGenerator(outputDir)
|
||||
|
||||
|
||||
// First page with header
|
||||
gen.AddPage()
|
||||
gen.Page1Header()
|
||||
|
||||
|
||||
// Extract data for details box
|
||||
companyName := "" // TODO: Get from customer data
|
||||
if data.Customer != nil {
|
||||
companyName = data.Customer.Name
|
||||
}
|
||||
emailTo := "" // TODO: Get from contact
|
||||
emailTo := "" // TODO: Get from contact
|
||||
attention := "" // TODO: Get from contact
|
||||
fromName := fmt.Sprintf("%s %s", data.User.FirstName, data.User.LastName)
|
||||
fromEmail := data.User.Email
|
||||
|
||||
|
||||
// Use CMC reference as the quote number
|
||||
quoteNumber := data.Document.CmcReference
|
||||
if data.Document.Revision > 0 {
|
||||
quoteNumber = fmt.Sprintf("%s.%d", quoteNumber, data.Document.Revision)
|
||||
}
|
||||
|
||||
|
||||
yourReference := fmt.Sprintf("Enquiry on %s", data.Document.Created.Format("2 Jan 2006"))
|
||||
issueDate := data.Document.Created.Format("2 January 2006")
|
||||
|
||||
|
||||
// Add details box
|
||||
gen.DetailsBox("QUOTE", companyName, emailTo, attention, fromName, fromEmail, quoteNumber, yourReference, issueDate)
|
||||
|
||||
|
||||
// Add page content if any
|
||||
// TODO: Add document pages content
|
||||
|
||||
|
||||
gen.Page1Footer()
|
||||
|
||||
|
||||
// Add pricing page
|
||||
gen.AddPage()
|
||||
gen.pdf.SetFont("Helvetica", "B", 14)
|
||||
gen.pdf.CellFormat(0, 10, "PRICING & SPECIFICATIONS", "", 1, "C", false, 0, "")
|
||||
gen.pdf.Ln(5)
|
||||
|
||||
|
||||
// Convert line items
|
||||
pdfItems := make([]LineItem, len(data.LineItems))
|
||||
for i, item := range data.LineItems {
|
||||
unitPrice := 0.0
|
||||
totalPrice := 0.0
|
||||
|
||||
|
||||
// Parse prices
|
||||
if item.GrossUnitPrice.Valid {
|
||||
fmt.Sscanf(item.GrossUnitPrice.String, "%f", &unitPrice)
|
||||
|
|
@ -77,7 +77,7 @@ func GenerateQuotePDF(data *QuotePDFData, outputDir string) (string, error) {
|
|||
if item.GrossPrice.Valid {
|
||||
fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice)
|
||||
}
|
||||
|
||||
|
||||
pdfItems[i] = LineItem{
|
||||
ItemNumber: item.ItemNumber,
|
||||
Quantity: item.Quantity,
|
||||
|
|
@ -86,10 +86,10 @@ func GenerateQuotePDF(data *QuotePDFData, outputDir string) (string, error) {
|
|||
TotalPrice: totalPrice,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add line items table
|
||||
gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST)
|
||||
|
||||
|
||||
// Add commercial comments if any
|
||||
if data.CommercialComments != "" {
|
||||
gen.pdf.Ln(10)
|
||||
|
|
@ -98,9 +98,9 @@ func GenerateQuotePDF(data *QuotePDFData, outputDir string) (string, error) {
|
|||
gen.pdf.SetFont("Helvetica", "", 9)
|
||||
gen.pdf.MultiCell(0, 5, data.CommercialComments, "", "L", false)
|
||||
}
|
||||
|
||||
|
||||
// TODO: Add terms and conditions page
|
||||
|
||||
|
||||
// Generate filename
|
||||
filename := quoteNumber
|
||||
if data.Document.Revision > 0 {
|
||||
|
|
@ -108,7 +108,7 @@ func GenerateQuotePDF(data *QuotePDFData, outputDir string) (string, error) {
|
|||
} else {
|
||||
filename = fmt.Sprintf("%s.pdf", quoteNumber)
|
||||
}
|
||||
|
||||
|
||||
// Save PDF
|
||||
fmt.Printf("Saving PDF with filename: %s to outputDir: %s\n", filename, outputDir)
|
||||
err := gen.Save(filename)
|
||||
|
|
@ -122,167 +122,142 @@ func GenerateQuotePDF(data *QuotePDFData, outputDir string) (string, error) {
|
|||
|
||||
// InvoicePDFData contains all data needed to generate an invoice PDF
|
||||
type InvoicePDFData struct {
|
||||
Document *db.Document
|
||||
Invoice *db.Invoice
|
||||
Enquiry *db.Enquiry
|
||||
Customer *db.Customer
|
||||
Job interface{} // Job data
|
||||
LineItems []db.GetLineItemsTableRow
|
||||
Currency interface{} // Currency data
|
||||
CurrencySymbol string
|
||||
ShowGST bool
|
||||
ShipVia string
|
||||
FOB string
|
||||
IssueDate time.Time
|
||||
Document *db.Document
|
||||
Invoice *db.Invoice
|
||||
Enquiry *db.Enquiry
|
||||
Customer *db.Customer
|
||||
Job interface{} // Job data
|
||||
LineItems []db.GetLineItemsTableRow
|
||||
Currency interface{} // Currency data
|
||||
CurrencySymbol string
|
||||
CurrencyCode string
|
||||
ShowGST bool
|
||||
ShipVia string
|
||||
FOB string
|
||||
IssueDate time.Time
|
||||
IssueDateString string
|
||||
BillTo string
|
||||
ShipTo string
|
||||
ShippingDetails string
|
||||
CustomerOrderNumber string
|
||||
JobTitle string
|
||||
PaymentTerms string
|
||||
CustomerABN string
|
||||
Subtotal float64
|
||||
GSTAmount float64
|
||||
Total float64
|
||||
}
|
||||
|
||||
// GenerateInvoicePDF generates a PDF for an invoice
|
||||
// GenerateInvoicePDF generates a PDF for an invoice matching old TCPDF layout
|
||||
func GenerateInvoicePDF(data *InvoicePDFData, outputDir string) (string, error) {
|
||||
gen := NewGenerator(outputDir)
|
||||
|
||||
|
||||
// First page with header
|
||||
gen.AddPage()
|
||||
gen.Page1Header()
|
||||
|
||||
// Extract data for details box
|
||||
companyName := data.Customer.Name
|
||||
emailTo := "" // TODO: Get from contact
|
||||
attention := "" // TODO: Get from contact
|
||||
fromName := "" // TODO: Get from user
|
||||
fromEmail := "" // TODO: Get from user
|
||||
invoiceNumber := data.Invoice.Title
|
||||
|
||||
yourReference := "" // TODO: Get reference
|
||||
issueDate := data.IssueDate.Format("2 January 2006")
|
||||
|
||||
// Add details box
|
||||
gen.DetailsBox("INVOICE", companyName, emailTo, attention, fromName, fromEmail, invoiceNumber, yourReference, issueDate)
|
||||
|
||||
// Add shipping details
|
||||
gen.pdf.Ln(5)
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(30, 5, "Ship Via:", "", 0, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.CellFormat(60, 5, data.ShipVia, "", 1, "L", false, 0, "")
|
||||
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(30, 5, "FOB:", "", 0, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.CellFormat(60, 5, data.FOB, "", 1, "L", false, 0, "")
|
||||
|
||||
|
||||
// Title - TAX INVOICE
|
||||
gen.pdf.SetFont("Helvetica", "B", 18)
|
||||
gen.pdf.SetTextColor(0, 0, 0)
|
||||
gen.pdf.CellFormat(0, 10, "TAX INVOICE", "", 1, "C", false, 0, "")
|
||||
gen.pdf.Ln(3)
|
||||
|
||||
// Shipping/Billing Box with payment details (matches old TCPDF layout)
|
||||
gen.AddInvoiceShippingBillingBox(data)
|
||||
|
||||
// Details table - CUSTOMER ORDER NO, JOB #, FOB, PAYMENT TERMS, ABN
|
||||
gen.pdf.Ln(2)
|
||||
gen.AddInvoiceDetailsTable(data)
|
||||
|
||||
gen.pdf.Ln(3)
|
||||
|
||||
// Line items table with freight and totals (matches old TCPDF layout)
|
||||
gen.AddInvoiceLineItemsTableWithShipping(data)
|
||||
|
||||
gen.Page1Footer()
|
||||
|
||||
// Add line items page
|
||||
gen.AddPage()
|
||||
gen.pdf.SetFont("Helvetica", "B", 14)
|
||||
gen.pdf.CellFormat(0, 10, "INVOICE DETAILS", "", 1, "C", false, 0, "")
|
||||
gen.pdf.Ln(5)
|
||||
|
||||
// Convert line items
|
||||
pdfItems := make([]LineItem, len(data.LineItems))
|
||||
for i, item := range data.LineItems {
|
||||
unitPrice := 0.0
|
||||
totalPrice := 0.0
|
||||
|
||||
// Parse prices
|
||||
if item.GrossUnitPrice.Valid {
|
||||
fmt.Sscanf(item.GrossUnitPrice.String, "%f", &unitPrice)
|
||||
}
|
||||
if item.GrossPrice.Valid {
|
||||
fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice)
|
||||
}
|
||||
|
||||
pdfItems[i] = LineItem{
|
||||
ItemNumber: item.ItemNumber,
|
||||
Quantity: item.Quantity,
|
||||
Title: item.Title,
|
||||
UnitPrice: unitPrice,
|
||||
TotalPrice: totalPrice,
|
||||
}
|
||||
|
||||
// Add terms and conditions page
|
||||
gen.AddTermsAndConditions()
|
||||
|
||||
// Generate filename and save
|
||||
filename := fmt.Sprintf("%s.pdf", data.Invoice.Title)
|
||||
if err := gen.Save(filename); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Add line items table
|
||||
gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST)
|
||||
|
||||
// Generate filename
|
||||
filename := fmt.Sprintf("%s.pdf", invoiceNumber)
|
||||
|
||||
// Save PDF
|
||||
err := gen.Save(filename)
|
||||
return filename, err
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
// PurchaseOrderPDFData contains all data needed to generate a purchase order PDF
|
||||
type PurchaseOrderPDFData struct {
|
||||
Document *db.Document
|
||||
PurchaseOrder *db.PurchaseOrder
|
||||
Principle *db.Principle
|
||||
LineItems []db.GetLineItemsTableRow
|
||||
Currency interface{} // Currency data
|
||||
CurrencySymbol string
|
||||
ShowGST bool
|
||||
Document *db.Document
|
||||
PurchaseOrder *db.PurchaseOrder
|
||||
Principle *db.Principle
|
||||
LineItems []db.GetLineItemsTableRow
|
||||
Currency interface{} // Currency data
|
||||
CurrencySymbol string
|
||||
ShowGST bool
|
||||
}
|
||||
|
||||
// GeneratePurchaseOrderPDF generates a PDF for a purchase order
|
||||
func GeneratePurchaseOrderPDF(data *PurchaseOrderPDFData, outputDir string) (string, error) {
|
||||
gen := NewGenerator(outputDir)
|
||||
|
||||
|
||||
// First page with header
|
||||
gen.AddPage()
|
||||
gen.Page1Header()
|
||||
|
||||
|
||||
// Extract data for details box
|
||||
companyName := data.Principle.Name
|
||||
emailTo := "" // TODO: Get from principle contact
|
||||
emailTo := "" // TODO: Get from principle contact
|
||||
attention := "" // TODO: Get from principle contact
|
||||
fromName := "" // TODO: Get from user
|
||||
fromName := "" // TODO: Get from user
|
||||
fromEmail := "" // TODO: Get from user
|
||||
poNumber := data.PurchaseOrder.Title
|
||||
|
||||
|
||||
yourReference := data.PurchaseOrder.PrincipleReference
|
||||
issueDate := data.PurchaseOrder.IssueDate.Format("Monday, 2 January 2006")
|
||||
|
||||
|
||||
// Add details box
|
||||
gen.DetailsBox("PURCHASE ORDER", companyName, emailTo, attention, fromName, fromEmail, poNumber, yourReference, issueDate)
|
||||
|
||||
|
||||
// Add PO specific details
|
||||
gen.pdf.Ln(5)
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(40, 5, "Ordered From:", "", 0, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.MultiCell(0, 5, data.PurchaseOrder.OrderedFrom, "", "L", false)
|
||||
|
||||
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(40, 5, "Dispatch By:", "", 0, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.CellFormat(0, 5, data.PurchaseOrder.DispatchBy, "", 1, "L", false, 0, "")
|
||||
|
||||
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(40, 5, "Deliver To:", "", 0, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.MultiCell(0, 5, data.PurchaseOrder.DeliverTo, "", "L", false)
|
||||
|
||||
|
||||
if data.PurchaseOrder.ShippingInstructions != "" {
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(0, 5, "Shipping Instructions:", "", 1, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.MultiCell(0, 5, data.PurchaseOrder.ShippingInstructions, "", "L", false)
|
||||
}
|
||||
|
||||
|
||||
gen.Page1Footer()
|
||||
|
||||
|
||||
// Add line items page
|
||||
gen.AddPage()
|
||||
gen.pdf.SetFont("Helvetica", "B", 14)
|
||||
gen.pdf.CellFormat(0, 10, "ORDER DETAILS", "", 1, "C", false, 0, "")
|
||||
gen.pdf.Ln(5)
|
||||
|
||||
|
||||
// Convert line items
|
||||
pdfItems := make([]LineItem, len(data.LineItems))
|
||||
for i, item := range data.LineItems {
|
||||
unitPrice := 0.0
|
||||
totalPrice := 0.0
|
||||
|
||||
|
||||
// Parse prices
|
||||
if item.GrossUnitPrice.Valid {
|
||||
fmt.Sscanf(item.GrossUnitPrice.String, "%f", &unitPrice)
|
||||
|
|
@ -290,7 +265,7 @@ func GeneratePurchaseOrderPDF(data *PurchaseOrderPDFData, outputDir string) (str
|
|||
if item.GrossPrice.Valid {
|
||||
fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice)
|
||||
}
|
||||
|
||||
|
||||
pdfItems[i] = LineItem{
|
||||
ItemNumber: item.ItemNumber,
|
||||
Quantity: item.Quantity,
|
||||
|
|
@ -299,10 +274,10 @@ func GeneratePurchaseOrderPDF(data *PurchaseOrderPDFData, outputDir string) (str
|
|||
TotalPrice: totalPrice,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add line items table
|
||||
gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST)
|
||||
|
||||
|
||||
// Generate filename
|
||||
filename := poNumber
|
||||
if data.Document.Revision > 0 {
|
||||
|
|
@ -310,8 +285,8 @@ func GeneratePurchaseOrderPDF(data *PurchaseOrderPDFData, outputDir string) (str
|
|||
} else {
|
||||
filename = fmt.Sprintf("%s.pdf", data.PurchaseOrder.Title)
|
||||
}
|
||||
|
||||
|
||||
// Save PDF
|
||||
err := gen.Save(filename)
|
||||
return filename, err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
php/app/webroot/pdf/CMCIN5986.pdf
Normal file
BIN
php/app/webroot/pdf/CMCIN5986.pdf
Normal file
Binary file not shown.
BIN
php/app/webroot/pdf/CMC_terms_and_conditions2006_A4.pdf
Normal file
BIN
php/app/webroot/pdf/CMC_terms_and_conditions2006_A4.pdf
Normal file
Binary file not shown.
Loading…
Reference in a new issue