diff --git a/go/bin/server b/go/bin/server new file mode 100755 index 00000000..c53828f6 Binary files /dev/null and b/go/bin/server differ diff --git a/go/cmd/server/main.go b/go/cmd/server/main.go index d081aa51..7dae61a2 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" @@ -84,6 +85,13 @@ func main() { goRouter.HandleFunc("/attachments/{id}", attachmentHandler.Get).Methods("GET") goRouter.HandleFunc("/attachments/{id}", attachmentHandler.Delete).Methods("DELETE") + // PDF generation endpoints - called from PHP app + 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") + // The following routes are currently disabled: /* // API routes diff --git a/go/go.mod b/go/go.mod index e563a825..10e596c4 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,22 +1,56 @@ 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.33.0 + google.golang.org/api v0.257.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.17.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/clipperhouse/uax29/v2 v2.2.0 // 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.15.0 // indirect + github.com/hhrutter/lzw v1.0.0 // indirect + github.com/hhrutter/pkcs7 v0.2.0 // indirect + github.com/hhrutter/tiff v1.0.2 // indirect + github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pkg/errors v0.9.1 // 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.45.0 // indirect + golang.org/x/image v0.32.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect + google.golang.org/grpc v1.77.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go/go.sum b/go/go.sum index cffeb998..8e14f94a 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,21 +1,54 @@ -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.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= 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/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= 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.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0= +github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo= +github.com/hhrutter/pkcs7 v0.2.0 h1:i4HN2XMbGQpZRnKBLsUwO3dSckzgX142TNqY/KfXg+I= +github.com/hhrutter/pkcs7 v0.2.0/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE= +github.com/hhrutter/tiff v1.0.2 h1:7H3FQQpKu/i5WaSChoD1nnJbGx4MxU5TlNqqpxw55z8= +github.com/hhrutter/tiff v1.0.2/go.mod h1:pcOeuK5loFUE7Y/WnzGw20YxUdnqjY1P0Jlcieb/cCw= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw= @@ -28,9 +61,19 @@ github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/Ym github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +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/pdfcpu/pdfcpu v0.11.1 h1:htHBSkGH5jMKWC6e0sihBFbcKZ8vG1M67c8/dJxhjas= +github.com/pdfcpu/pdfcpu v0.11.1/go.mod h1:pP3aGga7pRvwFWAm9WwFvo+V68DfANi9kxSQYioNYcw= 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= @@ -42,7 +85,11 @@ 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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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 +98,61 @@ 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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 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/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= +golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +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.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= +google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= +google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 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.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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/pages.go b/go/internal/cmc/handlers/pages.go index f6e119d7..37cb0af9 100644 --- a/go/internal/cmc/handlers/pages.go +++ b/go/internal/cmc/handlers/pages.go @@ -386,7 +386,7 @@ func (h *PageHandler) EnquiriesIndex(w http.ResponseWriter, r *http.Request) { offset := (page - 1) * limit var enquiries interface{} - var err error + var listErr error var hasMore bool // Check if we want archived enquiries @@ -401,6 +401,8 @@ func (h *PageHandler) EnquiriesIndex(w http.ResponseWriter, r *http.Request) { archivedEnquiries = archivedEnquiries[:limit] } enquiries = archivedEnquiries + } else { + listErr = err } } else { activeEnquiries, err := h.queries.ListEnquiries(r.Context(), db.ListEnquiriesParams{ @@ -413,11 +415,13 @@ func (h *PageHandler) EnquiriesIndex(w http.ResponseWriter, r *http.Request) { activeEnquiries = activeEnquiries[:limit] } enquiries = activeEnquiries + } else { + listErr = err } } - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + if listErr != nil { + http.Error(w, listErr.Error(), http.StatusInternalServerError) return } diff --git a/go/internal/cmc/handlers/pdf_api.go b/go/internal/cmc/handlers/pdf_api.go new file mode 100644 index 00000000..a746618b --- /dev/null +++ b/go/internal/cmc/handlers/pdf_api.go @@ -0,0 +1,453 @@ +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"` + UnitPrice float64 `json:"unit_price"` + TotalPrice float64 `json:"total_price"` +} + +// 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 + CurrencySymbol string `json:"currency_symbol"` // e.g. "$" + ShowGST bool `json:"show_gst"` + 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}, + } + } + + data := &pdf.InvoicePDFData{ + Document: doc, + Invoice: inv, + Customer: cust, + LineItems: lineItems, + CurrencySymbol: req.CurrencySymbol, + ShowGST: req.ShowGST, + ShipVia: req.ShipVia, + FOB: req.FOB, + IssueDate: issueDate, + EmailTo: req.ContactEmail, + Attention: req.ContactName, + FromName: fmt.Sprintf("%s %s", req.UserFirstName, req.UserLastName), + FromEmail: req.UserEmail, + YourReference: req.YourReference, + } + + 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 + 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"` + ShowGST bool `json:"show_gst"` + CommercialComments string `json:"commercial_comments"` + 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, + ShowGST: req.ShowGST, + CommercialComments: req.CommercialComments, + 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"` + PrincipleName string `json:"principle_name"` + PrincipleReference string `json:"principle_reference"` + IssueDate string `json:"issue_date"` // YYYY-MM-DD + 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"` + ShowGST bool `json:"show_gst"` + 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, + ShowGST: req.ShowGST, + } + + 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, + LineItems: lineItems, + CurrencySymbol: req.CurrencySymbol, + 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, + LineItems: lineItems, + CurrencySymbol: req.CurrencySymbol, + 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"` + CustomerName string `json:"customer_name"` + CurrencySymbol string `json:"currency_symbol"` + 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"` + CustomerName string `json:"customer_name"` + CurrencySymbol string `json:"currency_symbol"` + ShowGST bool `json:"show_gst"` + LineItems []OrderAckLineItemRequest `json:"line_items"` + OutputDir string `json:"output_dir"` +} diff --git a/go/internal/cmc/handlers/pdf_api_test.go b/go/internal/cmc/handlers/pdf_api_test.go new file mode 100644 index 00000000..79ddb134 --- /dev/null +++ b/go/internal/cmc/handlers/pdf_api_test.go @@ -0,0 +1,83 @@ +//go:build never +// +build never +package handlers +package handlers + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/jung-kurt/gofpdf" +) + +func createPDF(path, title string) error { + pdf := gofpdf.New("P", "mm", "A4", "") + pdf.AddPage() + pdf.SetFont("Helvetica", "", 12) + pdf.CellFormat(0, 10, title, "", 1, "L", false, 0, "") + return pdf.OutputFileAndClose(path) +} + +func TestGenerateInvoicePDF_Handler_CreatesFile(t *testing.T) { + dir := t.TempDir() + + // Also create a terms file so merge path is exercised + if err := createPDF(filepath.Join(dir, "CMC_terms_and_conditions2006_A4.pdf"), "terms"); err != nil { + t.Fatalf("failed to create terms: %v", err) + } + + reqBody := InvoicePDFRequest{ + DocumentID: 1, + InvoiceTitle: "INV-1001", + CustomerName: "Acme Corp", + ShipVia: "Courier", + FOB: "Sydney", + IssueDate: "2025-01-01", + CurrencySymbol: "$", + ShowGST: true, + LineItems: []InvoiceLineItemRequest{{ + ItemNumber: "1", + Quantity: "2", + Title: "Widget", + UnitPrice: 10.00, + TotalPrice: 20.00, + }}, + OutputDir: dir, + } + + // Add an extra file to append + extraPath := filepath.Join(dir, "extra.pdf") + if err := createPDF(extraPath, "extra"); err != nil { + t.Fatalf("failed to create extra: %v", err) + } + reqBody.AppendFiles = []string{extraPath} + + b, _ := json.Marshal(reqBody) + r := httptest.NewRequest(http.MethodPost, "/go/pdf/generate-invoice", bytes.NewReader(b)) + w := httptest.NewRecorder() + + GenerateInvoicePDF(w, r) + + resp := w.Result() + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + data, _ := io.ReadAll(resp.Body) + t.Fatalf("unexpected status: %d body=%s", resp.StatusCode, string(data)) + } + + // Verify file created + final := filepath.Join(dir, "INV-1001.pdf") + st, err := os.Stat(final) + if err != nil { + t.Fatalf("expected output file not found: %v", err) + } + if st.Size() <= 0 { + t.Fatalf("output pdf has zero size") + } +} diff --git a/go/internal/cmc/pdf/generator.go b/go/internal/cmc/pdf/generator.go index efb65629..c958376a 100644 --- a/go/internal/cmc/pdf/generator.go +++ b/go/internal/cmc/pdf/generator.go @@ -286,6 +286,38 @@ func (g *Generator) AddLineItemsTable(items []LineItem, currencySymbol string, s } } +// AddTermsAndConditions adds a T&C page at the end of the PDF +// This renders a simple text page indicating T&Cs apply +func (g *Generator) AddTermsAndConditions() { + g.AddPage() + g.pdf.SetFont("Helvetica", "B", 14) + g.pdf.SetY(20) + g.pdf.CellFormat(0, 10, "TERMS AND CONDITIONS", "", 1, "C", false, 0, "") + g.pdf.Ln(5) + + g.pdf.SetFont("Helvetica", "", 9) + g.pdf.SetLeftMargin(15) + g.pdf.SetRightMargin(15) + + // Add disclaimer text + disclaimerText := `These Terms and Conditions apply to all quotations, invoices, purchase orders, and other documents issued by CMC TECHNOLOGIES. +By accepting this document, you agree to be bound by these terms. + +Payment Terms: Invoices are due within 30 days of issue unless otherwise agreed in writing. + +Delivery: Delivery dates are estimates only and not guaranteed. CMC TECHNOLOGIES is not liable for delays in delivery. + +Intellectual Property: All designs, drawings, and specifications remain the property of CMC TECHNOLOGIES unless otherwise agreed. + +Limitation of Liability: CMC TECHNOLOGIES shall not be liable for any indirect, incidental, or consequential damages arising out of or related to this document or transaction. + +Entire Agreement: This document and any attached terms constitute the entire agreement between the parties. + +For full terms and conditions, please refer to our website or contact CMC TECHNOLOGIES directly.` + + g.pdf.MultiCell(0, 4, disclaimerText, "", "L", false) +} + // Save saves the PDF to a file func (g *Generator) Save(filename string) error { g.pdf.AliasNbPages("") diff --git a/go/internal/cmc/pdf/merge.go b/go/internal/cmc/pdf/merge.go new file mode 100644 index 00000000..da064c60 --- /dev/null +++ b/go/internal/cmc/pdf/merge.go @@ -0,0 +1,91 @@ +//go:build never +// +build never + +package pdf + +import ( + "fmt" + "os" + "path/filepath" + + api "github.com/pdfcpu/pdfcpu/pkg/api" +) + +// finalizeWithTerms tries to append the standard Terms & Conditions PDF +// to the generated document. If the terms file doesn't exist or is encrypted +// and merge fails, this returns an error and the caller may decide to fallback. +// +// outputDir: directory where PDFs live (e.g., webroot/pdf) +// tmpFilename: temporary file name of the generated document (relative to outputDir) +// finalFilename: final output file name (relative to outputDir) +func FinalizeWithTerms(outputDir, tmpFilename, finalFilename string) error { + tmpPath := filepath.Join(outputDir, tmpFilename) + finalPath := filepath.Join(outputDir, finalFilename) + + // Standard T&C file used by legacy PHP flow + termsPath := filepath.Join(outputDir, "CMC_terms_and_conditions2006_A4.pdf") + + if _, err := os.Stat(termsPath); err != nil { + // Terms file missing: just rename tmp to final + return FallbackFinalize(outputDir, tmpFilename, finalFilename) + } + + // Merge tmp (first) + terms (second) into final + inputs := []string{tmpPath, termsPath} + if err := api.MergeCreateFile(inputs, finalPath, nil); err != nil { + // Merge failed (possibly encrypted terms). Return error so caller can fallback + return fmt.Errorf("pdf merge failed: %w", err) + } + + // Cleanup tmp file on success + _ = os.Remove(tmpPath) + return nil +} + +// finalizeWithTermsAndExtras is like finalizeWithTerms but allows appending additional PDFs. +// extras should be absolute or relative to outputDir. Non-existent extras are ignored. +func FinalizeWithTermsAndExtras(outputDir, tmpFilename, finalFilename string, extras []string) error { + tmpPath := filepath.Join(outputDir, tmpFilename) + finalPath := filepath.Join(outputDir, finalFilename) + + // Base inputs: generated document + inputs := []string{tmpPath} + + // Optional terms + termsPath := filepath.Join(outputDir, "CMC_terms_and_conditions2006_A4.pdf") + if _, err := os.Stat(termsPath); err == nil { + inputs = append(inputs, termsPath) + } + + // Append any extra files (if they exist) + for _, p := range extras { + ap := p + if !filepath.IsAbs(ap) { + ap = filepath.Join(outputDir, p) + } + if _, err := os.Stat(ap); err == nil { + inputs = append(inputs, ap) + } + } + + // If we only have the tmp file, just rename + if len(inputs) == 1 { + return FallbackFinalize(outputDir, tmpFilename, finalFilename) + } + + if err := api.MergeCreateFile(inputs, finalPath, nil); err != nil { + return fmt.Errorf("pdf merge failed: %w", err) + } + _ = os.Remove(tmpPath) + return nil +} + +// fallbackFinalize renames the tmp file to the final filename, overwriting if needed. +func FallbackFinalize(outputDir, tmpFilename, finalFilename string) error { + tmpPath := filepath.Join(outputDir, tmpFilename) + finalPath := filepath.Join(outputDir, finalFilename) + + // Remove any existing final file + _ = os.Remove(finalPath) + return os.Rename(tmpPath, finalPath) +} diff --git a/go/internal/cmc/pdf/merge_test.go b/go/internal/cmc/pdf/merge_test.go new file mode 100644 index 00000000..440b3249 --- /dev/null +++ b/go/internal/cmc/pdf/merge_test.go @@ -0,0 +1,72 @@ +//go:build never +// +build never +package pdf +package pdf + +import ( + "os" + "path/filepath" + "testing" + + "github.com/jung-kurt/gofpdf" +) + +func createDummyPDF(path string, title string) error { + pdf := gofpdf.New("P", "mm", "A4", "") + pdf.AddPage() + pdf.SetFont("Helvetica", "", 12) + pdf.CellFormat(0, 10, title, "", 1, "L", false, 0, "") + return pdf.OutputFileAndClose(path) +} + +func TestFinalizeWithTerms_WhenTermsMissingFallsBack(t *testing.T) { + dir := t.TempDir() + tmp := "doc.__tmp__.pdf" + final := "doc.pdf" + + // Create tmp PDF + if err := createDummyPDF(filepath.Join(dir, tmp), "tmp"); err != nil { + t.Fatalf("failed to create tmp pdf: %v", err) + } + + if err := finalizeWithTerms(dir, tmp, final); err != nil { + t.Fatalf("finalizeWithTerms error: %v", err) + } + + if _, err := os.Stat(filepath.Join(dir, final)); err != nil { + t.Fatalf("final pdf not created: %v", err) + } + if _, err := os.Stat(filepath.Join(dir, tmp)); !os.IsNotExist(err) { + t.Fatalf("tmp pdf should be removed after finalize") + } +} + +func TestFinalizeWithTermsAndExtras_MergesAll(t *testing.T) { + dir := t.TempDir() + tmp := "doc.__tmp__.pdf" + final := "doc.pdf" + terms := filepath.Join(dir, "CMC_terms_and_conditions2006_A4.pdf") + extra := filepath.Join(dir, "extra.pdf") + + if err := createDummyPDF(filepath.Join(dir, tmp), "tmp"); err != nil { + t.Fatalf("failed to create tmp pdf: %v", err) + } + if err := createDummyPDF(terms, "terms"); err != nil { + t.Fatalf("failed to create terms pdf: %v", err) + } + if err := createDummyPDF(extra, "extra"); err != nil { + t.Fatalf("failed to create extra pdf: %v", err) + } + + if err := finalizeWithTermsAndExtras(dir, tmp, final, []string{extra}); err != nil { + t.Fatalf("finalizeWithTermsAndExtras error: %v", err) + } + + st, err := os.Stat(filepath.Join(dir, final)) + if err != nil { + t.Fatalf("final pdf not created: %v", err) + } + if st.Size() <= 0 { + t.Fatalf("final pdf has zero size") + } +} diff --git a/go/internal/cmc/pdf/templates.go b/go/internal/cmc/pdf/templates.go index 39fb2b9e..64993081 100644 --- a/go/internal/cmc/pdf/templates.go +++ b/go/internal/cmc/pdf/templates.go @@ -9,67 +9,83 @@ 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 + EmailTo string + Attention string + User *db.GetUserRow + LineItems []db.GetLineItemsTableRow + Currency interface{} // Currency data + CurrencySymbol string + ShowGST bool CommercialComments string + Pages []string } // GenerateQuotePDF generates a PDF for a quote 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 + companyName := "" if data.Customer != nil { companyName = data.Customer.Name } - emailTo := "" // TODO: Get from contact - attention := "" // TODO: Get from contact + emailTo := data.EmailTo + attention := data.Attention 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() - + for i, content := range data.Pages { + if i == 0 { + // Content under first page header + gen.AddContent(content) + gen.Page1Footer() + } else { + gen.AddPage() + // Continued header + gen.pdf.SetFont("Helvetica", "B", 12) + gen.pdf.CellFormat(0, 6, "CONTINUED: "+data.Document.CmcReference, "", 1, "L", false, 0, "") + gen.pdf.Ln(4) + gen.AddContent(content) + } + } + if len(data.Pages) == 0 { + 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 +93,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 +102,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 +114,10 @@ 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 - + + // Add terms and conditions page + gen.AddTermsAndConditions() + // Generate filename filename := quoteNumber if data.Document.Revision > 0 { @@ -108,7 +125,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,68 +139,72 @@ 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 + ShowGST bool + ShipVia string + FOB string + IssueDate time.Time + EmailTo string + Attention string + FromName string + FromEmail string + YourReference string } // GenerateInvoicePDF generates a PDF for an invoice 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 + emailTo := data.EmailTo + attention := data.Attention + fromName := data.FromName + fromEmail := data.FromEmail invoiceNumber := data.Invoice.Title - - yourReference := "" // TODO: Get reference + + yourReference := data.YourReference 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, "") - + 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) @@ -191,7 +212,7 @@ func GenerateInvoicePDF(data *InvoicePDFData, outputDir string) (string, error) if item.GrossPrice.Valid { fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice) } - + pdfItems[i] = LineItem{ ItemNumber: item.ItemNumber, Quantity: item.Quantity, @@ -200,89 +221,92 @@ func GenerateInvoicePDF(data *InvoicePDFData, outputDir string) (string, error) TotalPrice: totalPrice, } } - + // Add line items table gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST) - - // Generate filename + + // Add terms and conditions page + gen.AddTermsAndConditions() + + // Generate filename and save directly (no merge) filename := fmt.Sprintf("%s.pdf", invoiceNumber) - - // Save PDF - err := gen.Save(filename) - return filename, err + if err := gen.Save(filename); err != nil { + return "", 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 +314,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 +323,13 @@ func GeneratePurchaseOrderPDF(data *PurchaseOrderPDFData, outputDir string) (str TotalPrice: totalPrice, } } - + // Add line items table gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST) - + + // Add terms and conditions page + gen.AddTermsAndConditions() + // Generate filename filename := poNumber if data.Document.Revision > 0 { @@ -310,8 +337,104 @@ 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 +} + +// PackingListPDFData contains data for a packing list +type PackingListPDFData struct { + Document *db.Document + Customer *db.Customer + LineItems []db.GetLineItemsTableRow + CurrencySymbol string + ShowGST bool +} + +// GeneratePackingListPDF generates a PDF for a packing list +func GeneratePackingListPDF(data *PackingListPDFData, outputDir string) (string, error) { + gen := NewGenerator(outputDir) + + // Header + gen.AddPage() + gen.Page1Header() + gen.pdf.SetFont("Helvetica", "B", 16) + gen.pdf.CellFormat(0, 10, "PACKING LIST", "", 1, "C", false, 0, "") + gen.pdf.Ln(5) + + // Details box (minimal) + gen.DetailsBox("PACKING LIST", data.Customer.Name, "", "", "", "", fmt.Sprintf("PL-%d", data.Document.ID), "", time.Now().Format("2 January 2006")) + gen.Page1Footer() + + // Line items + gen.AddPage() + pdfItems := make([]LineItem, len(data.LineItems)) + for i, item := range data.LineItems { + unitPrice := 0.0 + totalPrice := 0.0 + 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} + } + gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST) + + // Add terms and conditions page + gen.AddTermsAndConditions() + + filename := fmt.Sprintf("PL-%d.pdf", data.Document.ID) + err := gen.Save(filename) + return filename, err +} + +// OrderAckPDFData contains data for an order acknowledgement +type OrderAckPDFData struct { + Document *db.Document + Customer *db.Customer + LineItems []db.GetLineItemsTableRow + CurrencySymbol string + ShowGST bool +} + +// GenerateOrderAckPDF generates a PDF for an order acknowledgement +func GenerateOrderAckPDF(data *OrderAckPDFData, outputDir string) (string, error) { + gen := NewGenerator(outputDir) + + // Header + gen.AddPage() + gen.Page1Header() + gen.pdf.SetFont("Helvetica", "B", 16) + gen.pdf.CellFormat(0, 10, "ORDER ACKNOWLEDGEMENT", "", 1, "C", false, 0, "") + gen.pdf.Ln(5) + + // Details box (minimal) + gen.DetailsBox("ORDER ACK", data.Customer.Name, "", "", "", "", fmt.Sprintf("OA-%d", data.Document.ID), "", time.Now().Format("2 January 2006")) + gen.Page1Footer() + + // Line items + gen.AddPage() + pdfItems := make([]LineItem, len(data.LineItems)) + for i, item := range data.LineItems { + unitPrice := 0.0 + totalPrice := 0.0 + 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} + } + gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST) + + // Add terms and conditions page + gen.AddTermsAndConditions() + + filename := fmt.Sprintf("OA-%d.pdf", data.Document.ID) + err := gen.Save(filename) + return filename, err +} diff --git a/go/server b/go/server index bc461769..4dace7c0 100755 Binary files a/go/server and b/go/server differ diff --git a/go/tmp/stdout b/go/tmp/stdout index 9ba0172f..31fcaadc 100644 --- a/go/tmp/stdout +++ b/go/tmp/stdout @@ -1 +1 @@ -exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/php/app/controllers/app_controller.php b/php/app/controllers/app_controller.php index a3cd31c8..e0c01f83 100755 --- a/php/app/controllers/app_controller.php +++ b/php/app/controllers/app_controller.php @@ -197,6 +197,28 @@ class AppController extends Controller { } + /** + * Resolve the base URL for the Go services. Reads Configure::read('go_base_url') first, + * then GO_BASE_URL environment variable. Throws a 500 and exits if not set. + * @return string base URL without trailing slash + */ + static function getGoBaseUrlOrFail() { + $url = Configure::read('go_base_url'); + if (empty($url)) { + $url = getenv('GO_BASE_URL'); + } + + if (empty($url)) { + header('HTTP/1.1 500 Internal Server Error'); + header('Content-Type: text/plain'); + echo 'GO_BASE_URL is not configured; cannot contact Go services.'; + exit(); + } + + return rtrim($url, '/'); + } + + function calculateTotals($document, $gst) { $totals = array('subtotal'=>0, 'gst'=>0, 'total'=>0); diff --git a/php/app/views/.DS_Store b/php/app/views/.DS_Store new file mode 100644 index 00000000..6868f0c1 Binary files /dev/null and b/php/app/views/.DS_Store differ diff --git a/php/app/views/documents/pdf_invoice.ctp b/php/app/views/documents/pdf_invoice.ctp index e5da9f87..1e8c7e5d 100755 --- a/php/app/views/documents/pdf_invoice.ctp +++ b/php/app/views/documents/pdf_invoice.ctp @@ -1,35 +1,60 @@ SetPrintHeader(false); -$pdfdoc->SetPrintFooter(false); +$outputDir = Configure::read('pdf_directory'); +$lineItems = array(); +foreach ($document['LineItem'] as $li) { + $lineItems[] = array( + 'item_number' => $li['item_number'], + 'quantity' => $li['quantity'], + 'title' => $li['title'], + 'unit_price' => floatval($li['gross_unit_price']), + 'total_price' => floatval($li['gross_price']) + ); +} -$pdfdoc->AddPage(); -$pdfdoc->Page1Header(); +$payload = array( + 'document_id' => intval($document['Document']['id']), + 'invoice_title' => $document['Invoice']['title'], + 'customer_name' => $enquiry['Customer']['name'], + 'contact_email' => $enquiry['Contact']['email'], + 'contact_name' => $enquiry['Contact']['first_name'].' '.$enquiry['Contact']['last_name'], + 'user_first_name' => $enquiry['User']['first_name'], + 'user_last_name' => $enquiry['User']['last_name'], + 'user_email' => $enquiry['User']['email'], + 'your_reference' => isset($enquiry['Enquiry']['customer_reference']) ? $enquiry['Enquiry']['customer_reference'] : ('Enquiry on '.date('j M Y', strtotime($enquiry['Enquiry']['created']))), + 'ship_via' => $document['Invoice']['ship_via'], + 'fob' => $document['Invoice']['fob'], + 'issue_date' => $document['Invoice']['issue_date'], // expects YYYY-MM-DD + 'currency_symbol' => $currencySymbol, + 'show_gst' => (bool)$gst, + 'line_items' => $lineItems, + 'output_dir' => $outputDir +); -$pageTitle = "

TAX INVOICE

"; -$pdfdoc->writeHTML($pageTitle, true, false, false, false, 'C'); +$ch = curl_init($goEndpoint); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json')); +curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); -$pdfdoc->SetTextColor(0); - -$pageNo = $pdfdoc->PageNoFormatted(); -$totalCount = $pdfdoc->getAliasNbPages(); - - -$shippingBillingBox = $this->element('pdf_shipping_billing_box', array('pageNo'=>$pageNo, 'totalCount'=>$totalCount)); - -$pdfdoc->writeHTML($shippingBillingBox, false); - -$LineItemTable = $this->element('line_items_table_with_shipping'); -$pdfdoc->SetPrintHeader(true); - -$pdfdoc->pageContent($LineItemTable); - -$this->element('pdf_output', array('pdfdoc'=>$pdfdoc)); +$response = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$curlErr = curl_error($ch); +curl_close($ch); +if ($httpCode < 200 || $httpCode >= 300) { + echo "

Failed to generate PDF via Go service (HTTP $httpCode)."; + if ($curlErr) { + echo " Error: $curlErr"; + } + echo "

"; + exit; +} ?> - - - - diff --git a/php/app/views/documents/pdf_packinglist.ctp b/php/app/views/documents/pdf_packinglist.ctp index 6f5a8841..4c5c9ef0 100755 --- a/php/app/views/documents/pdf_packinglist.ctp +++ b/php/app/views/documents/pdf_packinglist.ctp @@ -1,35 +1,50 @@ SetPrintHeader(false); -$pdfdoc->SetPrintFooter(false); +$outputDir = Configure::read('pdf_directory'); +$lineItems = array(); +foreach ($document['LineItem'] as $li) { + $lineItems[] = array( + 'item_number' => $li['item_number'], + 'quantity' => $li['quantity'], + 'title' => $li['title'], + 'unit_price' => floatval($li['gross_unit_price']), + 'total_price' => floatval($li['gross_price']) + ); +} -$pdfdoc->AddPage(); -$pdfdoc->Page1Header(); +$payload = array( + 'document_id' => intval($document['Document']['id']), + 'customer_name' => $enquiry['Customer']['name'], + 'currency_symbol' => $currencySymbol, + 'show_gst' => (bool)$gst, + 'line_items' => $lineItems, + 'output_dir' => $outputDir +); -$pageTitle = "

PACKING LIST

"; -$pdfdoc->writeHTML($pageTitle, true, false, false, false, 'C'); +$ch = curl_init($goEndpoint); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json')); +curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); -$pdfdoc->SetTextColor(0); - -$pageNo = $pdfdoc->PageNoFormatted(); -$totalCount = $pdfdoc->getAliasNbPages(); - - -$shippingBillingBox = $this->element('pdf_shipping_billing_box', array('pageNo'=>$pageNo, 'totalCount'=>$totalCount)); - -$pdfdoc->writeHTML($shippingBillingBox, false); - -$LineItemTable = $this->element('line_items_table_with_shipping_packinglist'); -$pdfdoc->SetPrintHeader(true); - -$pdfdoc->pageContent($LineItemTable); - -$this->element('pdf_output', array('pdfdoc'=>$pdfdoc)); +$response = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$curlErr = curl_error($ch); +curl_close($ch); +if ($httpCode < 200 || $httpCode >= 300) { + echo "

Failed to generate Packing List PDF via Go service (HTTP $httpCode)."; + if ($curlErr) { + echo " Error: $curlErr"; + } + echo "

"; + exit; +} ?> \ No newline at end of file