Files
bssapp/svc/routes/statement_aging_screen_pdf.go
2026-03-17 14:11:08 +03:00

680 lines
19 KiB
Go

package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"bytes"
"database/sql"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/jung-kurt/gofpdf"
)
type agingScreenPDFRow struct {
Cari8 string
CariDetay string
FaturaCari string
OdemeCari string
FaturaRef string
OdemeRef string
FaturaTarihi string
OdemeTarihi string
OdemeDocDate string
EslesenTutar float64
UsdTutar float64
CurrencyUsdRate float64
GunSayisi float64
GunSayisiDocDate float64
Aciklama string
DocCurrencyCode string
}
func ExportStatementAgingScreenPDFHandler(_ *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
selectedDate := strings.TrimSpace(r.URL.Query().Get("enddate"))
if selectedDate == "" {
selectedDate = strings.TrimSpace(r.URL.Query().Get("selected_date"))
}
if selectedDate == "" {
selectedDate = time.Now().Format("2006-01-02")
}
params := models.StatementAgingParams{
AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")),
EndDate: selectedDate,
Parislemler: r.URL.Query()["parislemler"],
}
if err := queries.RebuildStatementAgingCache(r.Context()); err != nil {
http.Error(w, "Error rebuilding aging cache: "+err.Error(), http.StatusInternalServerError)
return
}
rawRows, err := queries.GetStatementAging(params)
if err != nil {
http.Error(w, "Error fetching aging statement: "+err.Error(), http.StatusInternalServerError)
return
}
rows := make([]agingScreenPDFRow, 0, len(rawRows))
for _, r := range rawRows {
rows = append(rows, agingScreenPDFRow{
Cari8: pickString(r, "Cari8", "cari8"),
CariDetay: pickString(r, "CariDetay", "cari_detay"),
FaturaCari: pickString(r, "FaturaCari", "fatura_cari"),
OdemeCari: pickString(r, "OdemeCari", "odeme_cari"),
FaturaRef: pickString(r, "FaturaRef", "fatura_ref"),
OdemeRef: pickString(r, "OdemeRef", "odeme_ref"),
FaturaTarihi: pickString(r, "FaturaTarihi", "fatura_tarihi"),
OdemeTarihi: pickString(r, "OdemeTarihi", "odeme_tarihi"),
OdemeDocDate: pickString(r, "OdemeDocDate", "odeme_doc_date"),
EslesenTutar: pickFloat(r, "EslesenTutar", "eslesen_tutar"),
UsdTutar: pickFloat(r, "UsdTutar", "usd_tutar"),
CurrencyUsdRate: pickFloat(r, "CurrencyUsdRate", "currency_usd_rate", "CurrencyTryRate", "currency_try_rate"),
GunSayisi: pickFloat(r, "GunSayisi", "gun_sayisi"),
GunSayisiDocDate: pickFloat(r, "GunSayisi_DocDate", "gun_sayisi_docdate"),
Aciklama: pickString(r, "Aciklama", "aciklama"),
DocCurrencyCode: pickString(r, "DocCurrencyCode", "doc_currency_code"),
})
}
sortBy := strings.TrimSpace(r.URL.Query().Get("sort_by"))
sortDesc := parseBoolQuery(r.URL.Query().Get("sort_desc"))
pdf := gofpdf.New("L", "mm", "A3", "")
pdf.SetMargins(8, 8, 8)
pdf.SetAutoPageBreak(false, 10)
if err := registerDejavuFonts(pdf, "dejavu"); err != nil {
http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError)
return
}
drawStatementAgingScreenPDF(pdf, selectedDate, params.AccountCode, rows, sortBy, sortDesc)
if err := pdf.Error(); err != nil {
http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError)
return
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", `inline; filename="account-aging-screen.pdf"`)
_, _ = w.Write(buf.Bytes())
}
}
type agingScreenMasterPDF struct {
GroupKey string
Cari8 string
CariDetay string
SatirSayisi int
ToplamUSD float64
NormalUSD float64
AcikKalemUSD float64
KurWeightedBase float64
KurWeightedSum float64
KurFallback float64
WeightedBase float64
WeightedGunSum float64
WeightedGunDocSum float64
}
type agingScreenCurrencyPDF struct {
GroupKey string
MasterKey string
DocCurrencyCode string
SatirSayisi int
ToplamTutar float64
ToplamUSD float64
NormalTutar float64
AcikKalemTutar float64
KurWeightedBase float64
KurWeightedSum float64
KurFallback float64
WeightedBase float64
WeightedGunSum float64
WeightedGunDocSum float64
}
func drawStatementAgingScreenPDF(pdf *gofpdf.Fpdf, selectedDate, accountCode string, rows []agingScreenPDFRow, sortBy string, sortDesc bool) {
masters, currenciesByMaster, detailsByCurrency := buildStatementAgingScreenPDFData(rows, sortBy, sortDesc)
pageW, pageH := pdf.GetPageSize()
marginL, marginT, marginR, marginB := 8.0, 8.0, 8.0, 10.0
tableW := pageW - marginL - marginR
masterCols := []string{"Ana Cari Kod", "Ana Cari Detay", "Satir", "Toplam USD", "Normal USD", "Acik Kalem USD", "Ort. Gun", "Ort. Gun (DocDate)", "Kur"}
masterW := normalizeWidths([]float64{22, 40, 12, 18, 18, 20, 14, 18, 12}, tableW)
currencyCols := []string{"Doviz", "Satir", "Toplam Tutar", "Toplam USD", "Normal", "Acik Kalem", "Kur", "Ort. Gun", "Ort. Gun (DocDate)"}
currencyW := normalizeWidths([]float64{16, 12, 20, 18, 18, 18, 12, 14, 18}, tableW)
detailCols := []string{"Fatura Cari", "Odeme Cari", "Fatura Ref", "Odeme Ref", "Fatura Tarihi", "Odeme Vade", "Odeme DocDate", "Eslesen Tutar", "USD Tutar", "Kur", "Gun", "Gun (DocDate)", "Aciklama", "Doviz"}
detailW := normalizeWidths([]float64{18, 18, 22, 22, 16, 16, 18, 18, 16, 12, 10, 13, 30, 11}, tableW)
header := func() {
pdf.AddPage()
pdf.SetFont("dejavu", "B", 14)
pdf.SetTextColor(149, 113, 22)
pdf.SetXY(marginL, marginT)
pdf.CellFormat(150, 6, "Cari Yaslandirmali Ekstre", "", 0, "L", false, 0, "")
pdf.SetFont("dejavu", "", 8.5)
pdf.SetTextColor(30, 30, 30)
pdf.SetXY(pageW-marginR-90, marginT+0.5)
pdf.CellFormat(90, 4.8, "Son Tarih: "+formatDateTR(selectedDate), "", 0, "R", false, 0, "")
if strings.TrimSpace(accountCode) != "" {
pdf.SetXY(pageW-marginR-90, marginT+5)
pdf.CellFormat(90, 4.8, "Cari: "+accountCode, "", 0, "R", false, 0, "")
}
pdf.SetDrawColor(149, 113, 22)
pdf.Line(marginL, marginT+10.5, pageW-marginR, marginT+10.5)
pdf.SetDrawColor(200, 200, 200)
pdf.SetY(marginT + 13)
}
needPage := func(needH float64) bool {
return pdf.GetY()+needH+marginB > pageH
}
drawHeaderRow := func(cols []string, widths []float64, h float64, r, g, b int, fontSize float64) {
pdf.SetFont("dejavu", "B", fontSize)
pdf.SetTextColor(255, 255, 255)
pdf.SetFillColor(r, g, b)
y := pdf.GetY()
x := marginL
for i, c := range cols {
pdf.Rect(x, y, widths[i], h, "DF")
pdf.SetXY(x+0.8, y+0.9)
pdf.CellFormat(widths[i]-1.6, h-1.8, c, "", 0, "C", false, 0, "")
x += widths[i]
}
pdf.SetY(y + h)
}
setDataTextStyle := func(size float64, r, g, b int) {
pdf.SetFont("dejavu", "", size)
pdf.SetTextColor(r, g, b)
}
header()
drawHeaderRow(masterCols, masterW, 6.2, 149, 113, 22, 7.2)
setDataTextStyle(7, 25, 25, 25)
for _, m := range masters {
masterLine := []string{
m.Cari8,
m.CariDetay,
strconv.Itoa(m.SatirSayisi),
formatMoneyPDF(m.ToplamUSD),
formatMoneyPDF(m.NormalUSD),
formatMoneyPDF(m.AcikKalemUSD),
fmt.Sprintf("%.0f", statementAgingAvg(m.WeightedGunSum, m.WeightedBase)),
fmt.Sprintf("%.0f", statementAgingAvg(m.WeightedGunDocSum, m.WeightedBase)),
formatMoneyPDF(statementAgingAvg(m.KurWeightedSum, m.KurWeightedBase, m.KurFallback)),
}
rowH := calcPDFRowHeight(pdf, masterLine, masterW, map[int]bool{1: true}, 5.8, 3.3)
if needPage(rowH) {
header()
drawHeaderRow(masterCols, masterW, 6.2, 149, 113, 22, 7.2)
setDataTextStyle(7, 25, 25, 25)
}
setDataTextStyle(7, 25, 25, 25)
y := pdf.GetY()
x := marginL
for i, v := range masterLine {
pdf.Rect(x, y, masterW[i], rowH, "")
align := "L"
if i >= 2 {
align = "R"
}
if i == 6 || i == 7 {
align = "C"
}
drawPDFCellWrapped(pdf, v, x, y, masterW[i], rowH, align, 3.3)
x += masterW[i]
}
pdf.SetY(y + rowH)
for _, c := range currenciesByMaster[m.GroupKey] {
if needPage(11.2) {
header()
drawHeaderRow(masterCols, masterW, 6.2, 149, 113, 22, 7.2)
setDataTextStyle(7, 25, 25, 25)
}
pdf.SetFont("dejavu", "B", 7)
drawHeaderRow(currencyCols, currencyW, 5.6, 76, 95, 122, 6.8)
setDataTextStyle(6.8, 35, 35, 35)
currencyLine := []string{
c.DocCurrencyCode,
strconv.Itoa(c.SatirSayisi),
formatMoneyPDF(c.ToplamTutar),
formatMoneyPDF(c.ToplamUSD),
formatMoneyPDF(c.NormalTutar),
formatMoneyPDF(c.AcikKalemTutar),
formatMoneyPDF(statementAgingAvg(c.KurWeightedSum, c.KurWeightedBase, c.KurFallback)),
fmt.Sprintf("%.0f", statementAgingAvg(c.WeightedGunSum, c.WeightedBase)),
fmt.Sprintf("%.0f", statementAgingAvg(c.WeightedGunDocSum, c.WeightedBase)),
}
cRowH := 5.4
y := pdf.GetY()
x := marginL
for i, v := range currencyLine {
pdf.Rect(x, y, currencyW[i], cRowH, "")
align := "R"
if i == 0 {
align = "L"
}
if i == 7 || i == 8 {
align = "C"
}
drawPDFCellWrapped(pdf, v, x, y, currencyW[i], cRowH, align, 3.2)
x += currencyW[i]
}
pdf.SetY(y + cRowH)
drawHeaderRow(detailCols, detailW, 5.6, 31, 59, 91, 6.8)
setDataTextStyle(6.6, 30, 30, 30)
for _, d := range detailsByCurrency[c.GroupKey] {
line := []string{
d.FaturaCari,
d.OdemeCari,
d.FaturaRef,
d.OdemeRef,
formatDateTR(d.FaturaTarihi),
formatDateTR(d.OdemeTarihi),
formatDateTR(d.OdemeDocDate),
formatMoneyPDF(d.EslesenTutar),
formatMoneyPDF(d.UsdTutar),
formatMoneyPDF(d.CurrencyUsdRate),
fmt.Sprintf("%.0f", d.GunSayisi),
fmt.Sprintf("%.0f", d.GunSayisiDocDate),
d.Aciklama,
d.DocCurrencyCode,
}
rowH := calcPDFRowHeight(pdf, line, detailW, map[int]bool{0: true, 1: true, 2: true, 3: true, 12: true}, 5.4, 3.1)
if needPage(rowH) {
header()
drawHeaderRow(masterCols, masterW, 6.2, 149, 113, 22, 7.2)
pdf.SetFont("dejavu", "B", 7)
drawHeaderRow(currencyCols, currencyW, 5.6, 76, 95, 122, 6.8)
setDataTextStyle(6.8, 35, 35, 35)
y = pdf.GetY()
x = marginL
for i, v := range currencyLine {
pdf.Rect(x, y, currencyW[i], cRowH, "")
align := "R"
if i == 0 {
align = "L"
}
if i == 7 || i == 8 {
align = "C"
}
drawPDFCellWrapped(pdf, v, x, y, currencyW[i], cRowH, align, 3.2)
x += currencyW[i]
}
pdf.SetY(y + cRowH)
drawHeaderRow(detailCols, detailW, 5.6, 31, 59, 91, 6.8)
setDataTextStyle(6.6, 30, 30, 30)
}
rowY := pdf.GetY()
rowX := marginL
for i, v := range line {
pdf.Rect(rowX, rowY, detailW[i], rowH, "")
align := "L"
if i >= 7 && i <= 9 {
align = "R"
}
if i == 10 || i == 11 {
align = "C"
}
drawPDFCellWrapped(pdf, v, rowX, rowY, detailW[i], rowH, align, 3.1)
rowX += detailW[i]
}
pdf.SetY(rowY + rowH)
}
pdf.Ln(1)
}
pdf.Ln(1.2)
}
}
func statementAgingAvg(sum, base float64, fallback ...float64) float64 {
if base > 0 {
return sum / base
}
if len(fallback) > 0 {
return fallback[0]
}
return 0
}
func buildStatementAgingScreenPDFData(rows []agingScreenPDFRow, sortBy string, sortDesc bool) ([]agingScreenMasterPDF, map[string][]agingScreenCurrencyPDF, map[string][]agingScreenPDFRow) {
masterMap := map[string]*agingScreenMasterPDF{}
currencyMap := map[string]*agingScreenCurrencyPDF{}
detailsByCurrency := map[string][]agingScreenPDFRow{}
for _, row := range rows {
masterKey := strings.TrimSpace(row.Cari8)
if masterKey == "" {
continue
}
curr := strings.ToUpper(strings.TrimSpace(row.DocCurrencyCode))
if curr == "" {
curr = "N/A"
}
currencyKey := masterKey + "|" + curr
aciklama := strings.ToUpper(strings.TrimSpace(row.Aciklama))
absUsd := absFloatExcel(row.UsdTutar)
m := masterMap[masterKey]
if m == nil {
m = &agingScreenMasterPDF{
GroupKey: masterKey,
Cari8: masterKey,
CariDetay: strings.TrimSpace(row.CariDetay),
}
masterMap[masterKey] = m
}
if m.CariDetay == "" {
m.CariDetay = strings.TrimSpace(row.CariDetay)
}
c := currencyMap[currencyKey]
if c == nil {
c = &agingScreenCurrencyPDF{
GroupKey: currencyKey,
MasterKey: masterKey,
DocCurrencyCode: curr,
}
currencyMap[currencyKey] = c
}
m.SatirSayisi++
m.ToplamUSD += row.UsdTutar
if aciklama == "ACIKKALEM" {
m.AcikKalemUSD += row.UsdTutar
} else {
m.NormalUSD += row.UsdTutar
}
if absUsd > 0 {
m.WeightedBase += absUsd
m.WeightedGunSum += absUsd * row.GunSayisi
m.WeightedGunDocSum += absUsd * row.GunSayisiDocDate
if row.CurrencyUsdRate > 0 {
m.KurWeightedBase += absUsd
m.KurWeightedSum += absUsd * row.CurrencyUsdRate
}
}
if row.CurrencyUsdRate > 0 {
m.KurFallback = row.CurrencyUsdRate
}
c.SatirSayisi++
c.ToplamTutar += row.EslesenTutar
c.ToplamUSD += row.UsdTutar
if aciklama == "ACIKKALEM" {
c.AcikKalemTutar += row.EslesenTutar
} else {
c.NormalTutar += row.EslesenTutar
}
if absUsd > 0 {
c.WeightedBase += absUsd
c.WeightedGunSum += absUsd * row.GunSayisi
c.WeightedGunDocSum += absUsd * row.GunSayisiDocDate
if row.CurrencyUsdRate > 0 {
c.KurWeightedBase += absUsd
c.KurWeightedSum += absUsd * row.CurrencyUsdRate
}
}
if row.CurrencyUsdRate > 0 {
c.KurFallback = row.CurrencyUsdRate
}
detailsByCurrency[currencyKey] = append(detailsByCurrency[currencyKey], row)
}
masters := make([]agingScreenMasterPDF, 0, len(masterMap))
currenciesByMaster := make(map[string][]agingScreenCurrencyPDF, len(masterMap))
for _, m := range masterMap {
masters = append(masters, *m)
}
sortAgingScreenMastersForPDF(masters, sortBy, sortDesc)
for _, c := range currencyMap {
currenciesByMaster[c.MasterKey] = append(currenciesByMaster[c.MasterKey], *c)
}
for mk := range currenciesByMaster {
sort.SliceStable(currenciesByMaster[mk], func(i, j int) bool {
return strings.ToUpper(currenciesByMaster[mk][i].DocCurrencyCode) < strings.ToUpper(currenciesByMaster[mk][j].DocCurrencyCode)
})
}
for k := range detailsByCurrency {
sort.SliceStable(detailsByCurrency[k], func(i, j int) bool {
a := detailsByCurrency[k][i]
b := detailsByCurrency[k][j]
aOpen := strings.EqualFold(strings.TrimSpace(a.Aciklama), "ACIKKALEM")
bOpen := strings.EqualFold(strings.TrimSpace(b.Aciklama), "ACIKKALEM")
if aOpen != bOpen {
return aOpen
}
aDate := parseAgingSortDate(a.OdemeDocDate, a.OdemeTarihi, a.FaturaTarihi)
bDate := parseAgingSortDate(b.OdemeDocDate, b.OdemeTarihi, b.FaturaTarihi)
if aDate != bDate {
return aDate.After(bDate)
}
if strings.TrimSpace(a.FaturaCari) == strings.TrimSpace(b.FaturaCari) {
if strings.TrimSpace(a.OdemeCari) == strings.TrimSpace(b.OdemeCari) {
if strings.TrimSpace(a.FaturaRef) == strings.TrimSpace(b.FaturaRef) {
return strings.TrimSpace(a.OdemeRef) < strings.TrimSpace(b.OdemeRef)
}
return strings.TrimSpace(a.FaturaRef) < strings.TrimSpace(b.FaturaRef)
}
return strings.TrimSpace(a.OdemeCari) < strings.TrimSpace(b.OdemeCari)
}
return strings.TrimSpace(a.FaturaCari) < strings.TrimSpace(b.FaturaCari)
})
}
return masters, currenciesByMaster, detailsByCurrency
}
func sortAgingScreenMastersForPDF(masters []agingScreenMasterPDF, sortBy string, descending bool) {
if len(masters) <= 1 {
return
}
key := strings.TrimSpace(sortBy)
if key == "" {
key = "cari8"
}
textCmp := func(a, b string) int {
return strings.Compare(strings.ToUpper(strings.TrimSpace(a)), strings.ToUpper(strings.TrimSpace(b)))
}
numCmp := func(a, b float64) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}
intCmp := func(a, b int) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}
sort.SliceStable(masters, func(i, j int) bool {
a := masters[i]
b := masters[j]
cmp := 0
switch key {
case "cari8":
cmp = textCmp(a.Cari8, b.Cari8)
case "cari_detay":
cmp = textCmp(a.CariDetay, b.CariDetay)
case "satir_sayisi":
cmp = intCmp(a.SatirSayisi, b.SatirSayisi)
case "toplam_usd":
cmp = numCmp(a.ToplamUSD, b.ToplamUSD)
case "normal_usd":
cmp = numCmp(a.NormalUSD, b.NormalUSD)
case "acik_kalem_usd":
cmp = numCmp(a.AcikKalemUSD, b.AcikKalemUSD)
case "ortalama_gun":
cmp = numCmp(statementAgingAvg(a.WeightedGunSum, a.WeightedBase), statementAgingAvg(b.WeightedGunSum, b.WeightedBase))
case "ortalama_gun_docdate":
cmp = numCmp(statementAgingAvg(a.WeightedGunDocSum, a.WeightedBase), statementAgingAvg(b.WeightedGunDocSum, b.WeightedBase))
case "kur":
cmp = numCmp(statementAgingAvg(a.KurWeightedSum, a.KurWeightedBase, a.KurFallback), statementAgingAvg(b.KurWeightedSum, b.KurWeightedBase, b.KurFallback))
default:
cmp = textCmp(a.Cari8, b.Cari8)
}
if cmp == 0 {
cmp = textCmp(a.Cari8, b.Cari8)
}
if descending {
return cmp > 0
}
return cmp < 0
})
}
func parseAgingSortDate(values ...string) time.Time {
layouts := []string{
time.RFC3339,
"2006-01-02",
"2006-01-02 15:04:05",
"02.01.2006",
"02.01.2006 15:04:05",
}
for _, raw := range values {
s := strings.TrimSpace(raw)
if s == "" {
continue
}
for _, l := range layouts {
if t, err := time.Parse(l, s); err == nil {
return t
}
}
}
return time.Time{}
}
func pickString(m map[string]interface{}, keys ...string) string {
for _, k := range keys {
if v, ok := m[k]; ok {
return strings.TrimSpace(toStringValue(v))
}
}
return ""
}
func pickFloat(m map[string]interface{}, keys ...string) float64 {
for _, k := range keys {
if v, ok := m[k]; ok {
return toFloat64Value(v)
}
}
return 0
}
func toStringValue(v interface{}) string {
switch x := v.(type) {
case nil:
return ""
case string:
return x
case []byte:
return string(x)
default:
return fmt.Sprint(x)
}
}
func toFloat64Value(v interface{}) float64 {
switch x := v.(type) {
case nil:
return 0
case float64:
return x
case float32:
return float64(x)
case int:
return float64(x)
case int64:
return float64(x)
case int32:
return float64(x)
case string:
return parseFloatValue(x)
case []byte:
return parseFloatValue(string(x))
default:
return parseFloatValue(fmt.Sprint(x))
}
}
func parseFloatValue(s string) float64 {
s = strings.TrimSpace(s)
if s == "" {
return 0
}
hasComma := strings.Contains(s, ",")
hasDot := strings.Contains(s, ".")
if hasComma && hasDot {
if strings.LastIndex(s, ",") > strings.LastIndex(s, ".") {
s = strings.ReplaceAll(s, ".", "")
s = strings.Replace(s, ",", ".", 1)
} else {
s = strings.ReplaceAll(s, ",", "")
}
} else if hasComma {
s = strings.ReplaceAll(s, ".", "")
s = strings.Replace(s, ",", ".", 1)
}
n, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0
}
return n
}