Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-03 00:30:19 +03:00
parent ea27d34336
commit a4f4c2457f
29 changed files with 4522 additions and 752 deletions

View File

@@ -0,0 +1,573 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"bytes"
"database/sql"
"fmt"
"math"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/jung-kurt/gofpdf"
)
type agingDetailPDF struct {
FaturaCari string
OdemeCari string
Doviz string
FaturaRef string
OdemeRef string
FaturaTarihi string
OdemeTarihi string
OdemeDocDate string
EslesenTutar float64
UsdTutar float64
TryTutar float64
Aciklama string
Gun int
GunBelge int
GunKur float64
odemeDateParsed time.Time
odemeDateEmpty bool
}
type agingCurrencyPDF struct {
Key string
Cari8 string
CariDetay string
Doviz string
AcikKalemTutar float64
AcikKalemUSD float64
AcikKalemTRY float64
OrtGun int
OrtBelgeGun int
weightedBase float64
weightedGunSum float64
weightedDocSum float64
Details []agingDetailPDF
}
type agingMasterPDF struct {
Key string
Cari8 string
CariDetay string
AcikKalemUSD float64
AcikKalemTRY float64
AcikKalemOrtVadeGun int
AcikKalemOrtBelge int
NormalUSD float64
NormalTRY float64
OrtalamaVadeGun int
OrtalamaBelgeGun int
weightedAllBase float64
weightedAllGunSum float64
weightedAllDocSum float64
weightedOpenBase float64
weightedOpenGunSum float64
weightedOpenDocSum float64
Currencies []agingCurrencyPDF
}
func ExportStatementAgingPDFHandler(_ *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
}
params := models.StatementAgingParams{
AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")),
EndDate: strings.TrimSpace(r.URL.Query().Get("enddate")),
Parislemler: r.URL.Query()["parislemler"],
}
if params.AccountCode == "" || params.EndDate == "" {
http.Error(w, "accountcode and enddate are required", http.StatusBadRequest)
return
}
rows, err := queries.GetStatementAging(params)
if err != nil {
http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError)
return
}
masters := buildAgingPDFData(rows)
pdf := gofpdf.New("L", "mm", "A4", "")
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
}
drawStatementAgingPDF(pdf, params, masters)
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-detailed.pdf"`)
_, _ = w.Write(buf.Bytes())
}
}
func buildAgingPDFData(rows []map[string]interface{}) []agingMasterPDF {
masterMap := make(map[string]*agingMasterPDF)
currMap := make(map[string]*agingCurrencyPDF)
for _, row := range rows {
cari8 := strings.TrimSpace(readStrAny(row["Cari8"], row["cari8"]))
if cari8 == "" {
continue
}
cariDetay := strings.TrimSpace(readStrAny(row["CariDetay"], row["cari_detay"]))
doviz := strings.ToUpper(strings.TrimSpace(readStrAny(row["DocCurrencyCode"], row["doc_currency_code"])))
if doviz == "" {
doviz = "TRY"
}
aciklama := strings.ToUpper(strings.TrimSpace(readStrAny(row["Aciklama"], row["aciklama"])))
isAcik := aciklama == "ACIKKALEM"
eslesen := readFloatAny(row["EslesenTutar"], row["eslesen_tutar"])
usd := readFloatAny(row["UsdTutar"], row["usd_tutar"])
tryVal := readFloatAny(row["TryTutar"], row["try_tutar"])
gun := readIntAny(row["GunSayisi"], row["gun_sayisi"])
gunBelge := readIntAny(row["GunSayisi_DocDate"], row["gun_sayisi_docdate"])
absTry := math.Abs(tryVal)
m := masterMap[cari8]
if m == nil {
m = &agingMasterPDF{Key: cari8, Cari8: cari8, CariDetay: cariDetay}
masterMap[cari8] = m
}
if m.CariDetay == "" && cariDetay != "" {
m.CariDetay = cariDetay
}
ckey := cari8 + "|" + doviz
c := currMap[ckey]
if c == nil {
c = &agingCurrencyPDF{
Key: ckey,
Cari8: cari8,
CariDetay: cariDetay,
Doviz: doviz,
Details: make([]agingDetailPDF, 0, 128),
}
currMap[ckey] = c
}
if c.CariDetay == "" && cariDetay != "" {
c.CariDetay = cariDetay
}
if isAcik {
m.AcikKalemUSD += usd
m.AcikKalemTRY += tryVal
c.AcikKalemTutar += eslesen
c.AcikKalemUSD += usd
c.AcikKalemTRY += tryVal
} else {
m.NormalUSD += usd
m.NormalTRY += tryVal
}
if absTry > 0 {
m.weightedAllBase += absTry
m.weightedAllGunSum += absTry * float64(gun)
m.weightedAllDocSum += absTry * float64(gunBelge)
if isAcik {
m.weightedOpenBase += absTry
m.weightedOpenGunSum += absTry * float64(gun)
m.weightedOpenDocSum += absTry * float64(gunBelge)
c.weightedBase += absTry
c.weightedGunSum += absTry * float64(gun)
c.weightedDocSum += absTry * float64(gunBelge)
}
}
odemeTar := readStrAny(row["OdemeTarihi"], row["odeme_tarihi"])
odemeParsed, ok := parseYMD(odemeTar)
detail := agingDetailPDF{
FaturaCari: readStrAny(row["FaturaCari"], row["fatura_cari"]),
OdemeCari: readStrAny(row["OdemeCari"], row["odeme_cari"]),
Doviz: doviz,
FaturaRef: readStrAny(row["FaturaRef"], row["fatura_ref"]),
OdemeRef: readStrAny(row["OdemeRef"], row["odeme_ref"]),
FaturaTarihi: readStrAny(row["FaturaTarihi"], row["fatura_tarihi"]),
OdemeTarihi: odemeTar,
OdemeDocDate: readStrAny(row["OdemeDocDate"], row["odeme_doc_date"]),
EslesenTutar: eslesen,
UsdTutar: usd,
TryTutar: tryVal,
Aciklama: readStrAny(row["Aciklama"], row["aciklama"]),
Gun: gun,
GunBelge: gunBelge,
GunKur: readFloatAny(row["GunKur"], row["gun_kur"]),
odemeDateParsed: odemeParsed,
odemeDateEmpty: !ok,
}
c.Details = append(c.Details, detail)
}
masters := make([]agingMasterPDF, 0, len(masterMap))
for _, m := range masterMap {
if m.weightedOpenBase > 0 {
m.AcikKalemOrtVadeGun = int(math.Ceil(m.weightedOpenGunSum / m.weightedOpenBase))
m.AcikKalemOrtBelge = int(math.Ceil(m.weightedOpenDocSum / m.weightedOpenBase))
}
if m.weightedAllBase > 0 {
m.OrtalamaVadeGun = int(math.Ceil(m.weightedAllGunSum / m.weightedAllBase))
m.OrtalamaBelgeGun = int(math.Ceil(m.weightedAllDocSum / m.weightedAllBase))
}
currs := make([]agingCurrencyPDF, 0, len(currMap))
for _, c := range currMap {
if c.Cari8 != m.Cari8 {
continue
}
if c.weightedBase > 0 {
c.OrtGun = int(math.Ceil(c.weightedGunSum / c.weightedBase))
c.OrtBelgeGun = int(math.Ceil(c.weightedDocSum / c.weightedBase))
}
sort.SliceStable(c.Details, func(i, j int) bool {
ai := c.Details[i]
aj := c.Details[j]
if ai.odemeDateEmpty && !aj.odemeDateEmpty {
return true
}
if !ai.odemeDateEmpty && aj.odemeDateEmpty {
return false
}
if ai.odemeDateEmpty && aj.odemeDateEmpty {
return false
}
return ai.odemeDateParsed.After(aj.odemeDateParsed)
})
currs = append(currs, *c)
}
sort.SliceStable(currs, func(i, j int) bool { return currs[i].Doviz < currs[j].Doviz })
m.Currencies = currs
masters = append(masters, *m)
}
sort.SliceStable(masters, func(i, j int) bool { return masters[i].Cari8 < masters[j].Cari8 })
return masters
}
func drawStatementAgingPDF(pdf *gofpdf.Fpdf, p models.StatementAgingParams, masters []agingMasterPDF) {
pageW, pageH := pdf.GetPageSize()
marginL, marginR, marginT, marginB := 8.0, 8.0, 8.0, 10.0
tableW := pageW - marginL - marginR
colorPrimary := [3]int{149, 113, 22}
colorLevel2 := [3]int{76, 95, 122}
colorLevel3 := [3]int{31, 59, 91}
level1Cols := []string{"Ana Cari Kod", "Ana Cari Detay", "Açık Kalem USD", "Açık Kalem TRY", "Açık Kalem Ort Vade", "Açık Kalem Ort Belge", "Normal USD", "Normal TRY", "Ortalama Vade", "Ortalama Belge"}
level1W := normalizeWidths([]float64{20, 46, 18, 18, 15, 15, 16, 16, 15, 15}, tableW)
level2Cols := []string{"Ana Cari Kod", "Ana Cari Detay", "Döviz", "Açık Kalem", "Açık Kalem USD", "Açık Kalem TRY", "Ort Gün", "Ort Belge Gün"}
level2W := normalizeWidths([]float64{20, 52, 12, 24, 24, 24, 16, 18}, tableW)
level3Cols := []string{"Fatura Cari", "Ödeme Cari", "Döviz", "Fatura Ref", "Ödeme Ref", "Fatura Tarihi", "Ödeme Vade", "Ödeme DocDate", "Eşleşen", "USD", "TRY", "Açıklama", "Gün", "Gün Belge", "Gün Kur"}
level3W := normalizeWidths([]float64{15, 15, 10, 15, 15, 13, 13, 13, 14, 12, 14, 14, 8, 10, 10}, tableW)
pageHeader := func() {
pdf.AddPage()
pdf.SetFont("dejavu", "B", 15)
pdf.SetTextColor(colorPrimary[0], colorPrimary[1], colorPrimary[2])
pdf.SetXY(marginL, marginT)
pdf.CellFormat(150, 7, "Cari Yaşlandırmalı Ekstre", "", 0, "L", false, 0, "")
pdf.SetFont("dejavu", "", 9)
pdf.SetTextColor(20, 20, 20)
pdf.SetXY(pageW-marginR-95, marginT+1)
pdf.CellFormat(95, 5, "Son Tarih: "+p.EndDate, "", 0, "R", false, 0, "")
pdf.SetXY(pageW-marginR-95, marginT+6)
pdf.CellFormat(95, 5, "Cari: "+p.AccountCode, "", 0, "R", false, 0, "")
mode := "1_2"
if len(p.Parislemler) > 0 {
mode = strings.Join(p.Parislemler, ",")
}
pdf.SetXY(pageW-marginR-95, marginT+11)
pdf.CellFormat(95, 5, "Parasal İşlem: "+mode, "", 0, "R", false, 0, "")
pdf.SetDrawColor(colorPrimary[0], colorPrimary[1], colorPrimary[2])
pdf.Line(marginL, marginT+16, pageW-marginR, marginT+16)
pdf.SetDrawColor(210, 210, 210)
pdf.SetY(marginT + 19)
}
needPage := func(need float64) bool {
return pdf.GetY()+need+marginB > pageH
}
drawHeaderRow := func(cols []string, widths []float64, rgb [3]int, h float64) {
pdf.SetFont("dejavu", "B", 8.4)
pdf.SetFillColor(rgb[0], rgb[1], rgb[2])
pdf.SetTextColor(255, 255, 255)
y := pdf.GetY()
x := marginL
for i, c := range cols {
pdf.Rect(x, y, widths[i], h, "DF")
pdf.SetXY(x+0.8, y+1.0)
pdf.CellFormat(widths[i]-1.6, h-2.0, c, "", 0, "C", false, 0, "")
x += widths[i]
}
pdf.SetY(y + h)
}
drawRow := func(vals []string, widths []float64, h float64, fill bool, fillRGB [3]int, centerCols map[int]bool, rightCols map[int]bool) {
pdf.SetFont("dejavu", "", 7.6)
pdf.SetTextColor(20, 20, 20)
y := pdf.GetY()
x := marginL
for i, v := range vals {
if fill {
pdf.SetFillColor(fillRGB[0], fillRGB[1], fillRGB[2])
pdf.Rect(x, y, widths[i], h, "DF")
} else {
pdf.Rect(x, y, widths[i], h, "")
}
align := "L"
if rightCols[i] {
align = "R"
} else if centerCols[i] {
align = "C"
}
pdf.SetXY(x+0.8, y+0.8)
pdf.CellFormat(widths[i]-1.6, h-1.6, v, "", 0, align, false, 0, "")
x += widths[i]
}
pdf.SetY(y + h)
}
format2 := func(v float64) string {
return trFormat(v, 2)
}
pageHeader()
for _, m := range masters {
if needPage(7 + 7 + 6) {
pageHeader()
}
drawHeaderRow(level1Cols, level1W, colorPrimary, 7)
drawRow(
[]string{
m.Cari8, m.CariDetay,
format2(m.AcikKalemUSD), format2(m.AcikKalemTRY),
fmt.Sprintf("%d", m.AcikKalemOrtVadeGun), fmt.Sprintf("%d", m.AcikKalemOrtBelge),
format2(m.NormalUSD), format2(m.NormalTRY),
fmt.Sprintf("%d", m.OrtalamaVadeGun), fmt.Sprintf("%d", m.OrtalamaBelgeGun),
},
level1W, 6.4, true, [3]int{250, 246, 238},
map[int]bool{0: true, 4: true, 5: true, 8: true, 9: true},
map[int]bool{2: true, 3: true, 6: true, 7: true},
)
for _, c := range m.Currencies {
if needPage(6 + 6 + 6) {
pageHeader()
drawHeaderRow(level1Cols, level1W, colorPrimary, 7)
drawRow(
[]string{
m.Cari8, m.CariDetay,
format2(m.AcikKalemUSD), format2(m.AcikKalemTRY),
fmt.Sprintf("%d", m.AcikKalemOrtVadeGun), fmt.Sprintf("%d", m.AcikKalemOrtBelge),
format2(m.NormalUSD), format2(m.NormalTRY),
fmt.Sprintf("%d", m.OrtalamaVadeGun), fmt.Sprintf("%d", m.OrtalamaBelgeGun),
},
level1W, 6.4, true, [3]int{250, 246, 238},
map[int]bool{0: true, 4: true, 5: true, 8: true, 9: true},
map[int]bool{2: true, 3: true, 6: true, 7: true},
)
}
drawHeaderRow(level2Cols, level2W, colorLevel2, 6)
drawRow(
[]string{
c.Cari8, c.CariDetay, c.Doviz,
format2(c.AcikKalemTutar), format2(c.AcikKalemUSD), format2(c.AcikKalemTRY),
fmt.Sprintf("%d", c.OrtGun), fmt.Sprintf("%d", c.OrtBelgeGun),
},
level2W, 5.8, true, [3]int{236, 240, 247},
map[int]bool{0: true, 2: true, 6: true, 7: true},
map[int]bool{3: true, 4: true, 5: true},
)
if needPage(5.8) {
pageHeader()
}
drawHeaderRow(level3Cols, level3W, colorLevel3, 5.8)
for _, d := range c.Details {
if needPage(5.2) {
pageHeader()
drawHeaderRow(level3Cols, level3W, colorLevel3, 5.8)
}
drawRow(
[]string{
d.FaturaCari, d.OdemeCari, d.Doviz, d.FaturaRef, d.OdemeRef,
d.FaturaTarihi, d.OdemeTarihi, d.OdemeDocDate,
format2(d.EslesenTutar), format2(d.UsdTutar), format2(d.TryTutar), d.Aciklama,
fmt.Sprintf("%d", d.Gun), fmt.Sprintf("%d", d.GunBelge), trFormat(d.GunKur, 2),
},
level3W, 5.2, false, [3]int{},
map[int]bool{2: true, 5: true, 6: true, 7: true, 11: true, 12: true, 13: true, 14: true},
map[int]bool{8: true, 9: true, 10: true},
)
}
pdf.Ln(1.2)
}
pdf.Ln(1.8)
}
}
func trFormat(v float64, frac int) string {
neg := v < 0
if neg {
v = -v
}
pow := math.Pow(10, float64(frac))
rounded := math.Round(v*pow) / pow
intPart := int64(rounded)
decPart := int64(math.Round((rounded - float64(intPart)) * pow))
intStr := fmt.Sprintf("%d", intPart)
var grouped strings.Builder
for i, r := range intStr {
if i > 0 && (len(intStr)-i)%3 == 0 {
grouped.WriteString(".")
}
grouped.WriteRune(r)
}
out := grouped.String()
if frac > 0 {
decFmt := fmt.Sprintf("%%0%dd", frac)
out += "," + fmt.Sprintf(decFmt, decPart)
}
if neg {
return "-" + out
}
return out
}
func readStrAny(v ...interface{}) string {
for _, x := range v {
switch t := x.(type) {
case nil:
case string:
if strings.TrimSpace(t) != "" {
return t
}
case []byte:
s := strings.TrimSpace(string(t))
if s != "" {
return s
}
case time.Time:
return t.Format("2006-01-02")
default:
s := strings.TrimSpace(fmt.Sprint(t))
if s != "" && s != "<nil>" {
return s
}
}
}
return ""
}
func readFloatAny(v ...interface{}) float64 {
for _, x := range v {
if x == nil {
continue
}
switch t := x.(type) {
case float64:
return t
case float32:
return float64(t)
case int:
return float64(t)
case int32:
return float64(t)
case int64:
return float64(t)
case string:
if n, ok := parseNumberFlexible(t); ok {
return n
}
case []byte:
if n, ok := parseNumberFlexible(string(t)); ok {
return n
}
default:
if n, ok := parseNumberFlexible(fmt.Sprint(t)); ok {
return n
}
}
}
return 0
}
func readIntAny(v ...interface{}) int {
return int(math.Ceil(readFloatAny(v...)))
}
func parseNumberFlexible(s string) (float64, bool) {
s = strings.TrimSpace(s)
if s == "" {
return 0, false
}
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, false
}
return n, true
}
func parseYMD(v string) (time.Time, bool) {
v = strings.TrimSpace(v)
if v == "" {
return time.Time{}, false
}
t, err := time.Parse("2006-01-02", v)
if err != nil {
return time.Time{}, false
}
return t, true
}