diff --git a/go/cmd/server/main.go b/go/cmd/server/main.go index d081aa51..284e75e5 100644 --- a/go/cmd/server/main.go +++ b/go/cmd/server/main.go @@ -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 diff --git a/go/go.mod b/go/go.mod index e563a825..ba7aa03e 100644 --- a/go/go.mod +++ b/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 ) diff --git a/go/go.sum b/go/go.sum index cffeb998..1b1703cd 100644 --- a/go/go.sum +++ b/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= diff --git a/go/internal/cmc/handlers/pdf_api.go b/go/internal/cmc/handlers/pdf_api.go new file mode 100644 index 00000000..16d10630 --- /dev/null +++ b/go/internal/cmc/handlers/pdf_api.go @@ -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":".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": } or {"error": ""} +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}) +} diff --git a/go/internal/cmc/pdf/generator.go b/go/internal/cmc/pdf/generator.go index efb65629..05d2d876 100644 --- a/go/internal/cmc/pdf/generator.go +++ b/go/internal/cmc/pdf/generator.go @@ -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("") diff --git a/go/internal/cmc/pdf/templates.go b/go/internal/cmc/pdf/templates.go index 39fb2b9e..a0b8378c 100644 --- a/go/internal/cmc/pdf/templates.go +++ b/go/internal/cmc/pdf/templates.go @@ -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 -} \ No newline at end of file +} diff --git a/php/app/webroot/pdf/CMCIN5986.pdf b/php/app/webroot/pdf/CMCIN5986.pdf new file mode 100644 index 00000000..49a64adf Binary files /dev/null and b/php/app/webroot/pdf/CMCIN5986.pdf differ diff --git a/php/app/webroot/pdf/CMC_terms_and_conditions2006_A4.pdf b/php/app/webroot/pdf/CMC_terms_and_conditions2006_A4.pdf new file mode 100644 index 00000000..bb44ac92 Binary files /dev/null and b/php/app/webroot/pdf/CMC_terms_and_conditions2006_A4.pdf differ