Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-06 10:56:53 +03:00
parent 9097b5af2d
commit 9e534e9a34
18 changed files with 2204 additions and 2015 deletions

View File

@@ -10,7 +10,6 @@ import (
"bssapp-backend/routes"
"database/sql"
"log"
"log/slog"
"net/http"
"os"
"path"
@@ -165,7 +164,6 @@ InitRoutes — FULL V3 (Method-aware) PERMISSION EDITION
func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router {
r := mux.NewRouter()
mountUploads(r)
mountSPA(r)
/*
@@ -450,24 +448,21 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
"finance", "view",
wrapV3(http.HandlerFunc(routes.GetStatementAgingHandler)),
)
bindV3(r, pgDB,
"/api/finance/account-aging-statement/rebuild-cache", "POST",
"finance", "update",
wrapV3(http.HandlerFunc(routes.RebuildStatementAgingCacheHandler)),
)
bindV3(r, pgDB,
"/api/finance/account-aging-statement/export-pdf", "GET",
"finance", "export",
wrapV3(routes.ExportStatementAgingPDFHandler(mssql)),
)
bindV3(r, pgDB,
"/api/finance/account-aging-statement/export-excel", "GET",
"finance", "export",
wrapV3(routes.ExportStatementAgingExcelHandler(mssql)),
)
bindV3(r, pgDB,
"/api/finance/aged-customer-balance-list", "GET",
"finance", "view",
wrapV3(http.HandlerFunc(routes.GetAgedCustomerBalanceListHandler)),
)
// ============================================================
// REPORT (STATEMENTS)
@@ -508,7 +503,6 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
{"/api/orders/close-ready", "GET", "update", routes.OrderCloseReadyListRoute(mssql)},
{"/api/orders/bulk-close", "POST", "update", routes.OrderBulkCloseRoute(mssql)},
{"/api/orders/export", "GET", "export", routes.OrderListExcelRoute(mssql)},
{"/api/orders/export-pdf", "GET", "export", routes.OrderListPDFRoute(mssql)},
{"/api/order/check/{id}", "GET", "view", routes.OrderExistsHandler(mssql)},
{"/api/order/validate", "POST", "insert", routes.ValidateOrderHandler(mssql)},
{"/api/order/pdf/{id}", "GET", "export", routes.OrderPDFHandler(mssql)},
@@ -558,36 +552,6 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
wrapV3(http.HandlerFunc(routes.GetProductSecondColorsHandler)),
)
bindV3(r, pgDB,
"/api/product-stock-query", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductStockQueryHandler)),
)
bindV3(r, pgDB,
"/api/product-stock-attribute-options", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductStockAttributeOptionsHandler)),
)
bindV3(r, pgDB,
"/api/product-stock-query-by-attributes", "GET",
"order", "view",
wrapV3(http.HandlerFunc(routes.GetProductStockQueryByAttributesHandler)),
)
bindV3(r, pgDB,
"/api/product-images", "GET",
"order", "view",
wrapV3(routes.GetProductImagesHandler(pgDB)),
)
bindV3(r, pgDB,
"/api/product-images/{id}/content", "GET",
"order", "view",
http.HandlerFunc(routes.GetProductImageContentHandler(pgDB)),
)
// ============================================================
// ROLE MANAGEMENT
// ============================================================
@@ -645,35 +609,7 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
return r
}
func setupSlog() {
level := new(slog.LevelVar)
switch strings.ToLower(strings.TrimSpace(os.Getenv("LOG_LEVEL"))) {
case "debug":
level.Set(slog.LevelDebug)
case "warn", "warning":
level.Set(slog.LevelWarn)
case "error":
level.Set(slog.LevelError)
default:
level.Set(slog.LevelInfo)
}
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
})
slog.SetDefault(slog.New(handler))
}
func mountUploads(r *mux.Router) {
root := uploadsRootDir()
log.Printf("🖼️ uploads root: %s", root)
r.PathPrefix("/uploads/").Handler(
http.StripPrefix("/uploads/", http.FileServer(http.Dir(root))),
)
}
func main() {
setupSlog()
log.Println("🔥🔥🔥 BSSAPP BACKEND STARTED — LOGIN ROUTE SHOULD EXIST 🔥🔥🔥")
// -------------------------------------------------------
@@ -838,30 +774,3 @@ func uiRootDir() string {
return "../ui/dist/spa"
}
func uploadsRootDir() string {
if root := strings.TrimSpace(os.Getenv("BLOB_ROOT")); root != "" {
candidates := []string{
root,
filepath.Join(root, "uploads"),
}
for _, d := range candidates {
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
return d
}
}
}
candidates := []string{
"./uploads",
"../uploads",
"../../uploads",
}
for _, d := range candidates {
if fi, err := os.Stat(d); err == nil && fi.IsDir() {
return d
}
}
return "./uploads"
}

View File

@@ -1,10 +1,11 @@
package models
type StatementParams struct {
CariKod string `json:"cari_kod"`
StartDate string `json:"startdate"`
EndDate string `json:"enddate"`
AccountCode string `json:"accountcode"`
LangCode string `json:"langcode"`
Parislemler []string `json:"parislemler"` // ✅ slice olmalı
CariKod string `json:"cari_kod"`
StartDate string `json:"startdate"`
EndDate string `json:"enddate"`
AccountCode string `json:"accountcode"`
LangCode string `json:"langcode"`
Parislemler []string `json:"parislemler"`
ExcludeOpening bool `json:"excludeopening"`
}

View File

@@ -7,7 +7,6 @@ import (
"database/sql"
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
@@ -15,59 +14,34 @@ import (
func GetStatementAging(params models.StatementAgingParams) ([]map[string]interface{}, error) {
accountCode := normalizeMasterAccountCode(params.AccountCode)
if strings.TrimSpace(params.EndDate) == "" {
return nil, fmt.Errorf("enddate is required")
}
useType2, useType3 := resolveUseTypes(params.Parislemler)
endDateText := strings.TrimSpace(params.EndDate)
if endDateText == "" {
endDateText = time.Now().Format("2006-01-02")
rateMap, err := loadNearestTryRates(context.Background())
if err != nil {
return nil, err
}
endDate, _ := time.Parse("2006-01-02", endDateText)
cariFilter := ""
if strings.TrimSpace(accountCode) != "" {
cariFilter = strings.TrimSpace(accountCode)
usdTry := rateMap["USD"]
if usdTry <= 0 {
usdTry = 1
}
rows, err := db.MssqlDB.Query(`
SELECT TOP (100)
Cari8 = LEFT(LTRIM(RTRIM(CariKodu)), 8),
CariDetay = LTRIM(RTRIM(CariKodu)),
FaturaCari = LTRIM(RTRIM(CariKodu)),
OdemeCari = LTRIM(RTRIM(CariKodu)),
FaturaRef = CAST(NULL AS NVARCHAR(50)),
OdemeRef = CAST(NULL AS NVARCHAR(50)),
FaturaTarihi = CAST(NULL AS DATE),
OdemeTarihi = CAST(NULL AS DATE),
OdemeDocDate = CAST(NULL AS DATE),
EslesenTutar = CAST(Bakiye AS DECIMAL(18,2)),
GunSayisi = CAST(Vade_Gun AS DECIMAL(18,2)),
GunSayisi_DocDate = CAST(Vade_BelgeTarihi_Gun AS DECIMAL(18,2)),
Aciklama = CAST('AcikKalem' AS NVARCHAR(30)),
DocCurrencyCode = LTRIM(RTRIM(CariDoviz)),
PislemTipi,
SirketKodu,
CurrAccTypeCode,
Bakiye,
Vade_Gun,
Vade_BelgeTarihi_Gun,
SonTarih,
HesaplamaTarihi
FROM dbo.CARI_BAKIYE_GUN_CACHE
WHERE
(
(@UseType2 = 1 AND PislemTipi = '1_2')
OR
(@UseType3 = 1 AND PislemTipi = '1_3')
)
AND (@CariFilter = '' OR LTRIM(RTRIM(CariKodu)) LIKE @CariFilter + '%')
ORDER BY CariKodu, CariDoviz, PislemTipi;
EXEC dbo.SP_FIFO_MATCH_FINAL
@Cari8 = @Cari8,
@SonTarih = @SonTarih,
@UseType2 = @UseType2,
@UseType3 = @UseType3;
`,
sql.Named("Cari8", accountCode),
sql.Named("SonTarih", params.EndDate),
sql.Named("UseType2", useType2),
sql.Named("UseType3", useType3),
sql.Named("CariFilter", cariFilter),
)
if err != nil {
return nil, fmt.Errorf("CARI_BAKIYE_GUN_CACHE query error: %w", err)
return nil, fmt.Errorf("SP_FIFO_MATCH_FINAL query error: %w", err)
}
defer rows.Close()
@@ -78,7 +52,6 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa
result := make([]map[string]interface{}, 0, 2048)
cari8Set := make(map[string]struct{})
currencySet := make(map[string]struct{})
for rows.Next() {
values := make([]interface{}, len(columns))
scanArgs := make([]interface{}, len(columns))
@@ -108,11 +81,6 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa
if cari8 != "" {
cari8Set[cari8] = struct{}{}
}
curr := strings.ToUpper(strings.TrimSpace(asString(row["DocCurrencyCode"])))
if curr != "" && curr != "TRY" {
currencySet[curr] = struct{}{}
}
currencySet["USD"] = struct{}{}
result = append(result, row)
}
@@ -125,10 +93,6 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa
if err != nil {
return nil, err
}
rateSeriesByCurr, err := loadTryRateSeriesByCurrency(context.Background(), currencySet)
if err != nil {
return nil, err
}
for i := range result {
row := result[i]
@@ -137,25 +101,13 @@ func GetStatementAging(params models.StatementAgingParams) ([]map[string]interfa
if curr == "" {
curr = "TRY"
}
aciklama := strings.ToUpper(strings.TrimSpace(asString(row["Aciklama"])))
targetDate := endDate
if aciklama != "ACIKKALEM" {
if odemeTarihi, ok := parseDateOnly(asString(row["OdemeTarihi"])); ok {
targetDate = odemeTarihi
}
}
tutar := asFloat64(row["EslesenTutar"])
currTry := resolveTryRate(curr, targetDate, rateSeriesByCurr)
usdTry := resolveTryRate("USD", targetDate, rateSeriesByCurr)
tryTutar := toTRYByRate(tutar, curr, currTry)
usdTutar := toUSDByRates(tutar, curr, currTry, usdTry)
gunKur := usdRateInCurrency(curr, currTry, usdTry)
usdTutar := toUSD(tutar, curr, usdTry, rateMap)
row["CariDetay"] = cariDetailMap[cari8]
row["UsdTutar"] = round2(usdTutar)
row["TryTutar"] = round2(tryTutar)
row["GunKur"] = round6(gunKur)
row["CurrencyTryRate"] = round6(rateMap[curr])
}
return result, nil
@@ -306,138 +258,3 @@ func round2(v float64) float64 {
func round6(v float64) float64 {
return math.Round(v*1_000_000) / 1_000_000
}
type ratePoint struct {
date time.Time
rate float64
}
func loadTryRateSeriesByCurrency(ctx context.Context, currencies map[string]struct{}) (map[string][]ratePoint, error) {
if len(currencies) == 0 {
return map[string][]ratePoint{}, nil
}
query := fmt.Sprintf(`
SELECT CurrencyCode, Rate, CAST([Date] AS date) AS RateDate
FROM AllExchangeRates
WHERE RelationCurrencyCode = 'TRY'
AND ExchangeTypeCode = 6
AND Rate > 0
AND CurrencyCode IN (%s)
`, quotedInList(currencies))
rows, err := db.MssqlDB.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("aging currency series query error: %w", err)
}
defer rows.Close()
out := make(map[string][]ratePoint, len(currencies))
for rows.Next() {
var code string
var rate float64
var dt time.Time
if err := rows.Scan(&code, &rate, &dt); err != nil {
return nil, err
}
code = strings.ToUpper(strings.TrimSpace(code))
out[code] = append(out[code], ratePoint{date: dt, rate: rate})
}
if err := rows.Err(); err != nil {
return nil, err
}
for c := range out {
sort.Slice(out[c], func(i, j int) bool { return out[c][i].date.Before(out[c][j].date) })
}
return out, nil
}
func resolveTryRate(currency string, target time.Time, series map[string][]ratePoint) float64 {
currency = strings.ToUpper(strings.TrimSpace(currency))
if currency == "" || currency == "TRY" {
return 1
}
points := series[currency]
if len(points) == 0 {
return 0
}
best := points[0]
bestDiff := absDurationDays(points[0].date.Sub(target))
for i := 1; i < len(points); i++ {
diff := absDurationDays(points[i].date.Sub(target))
if diff < bestDiff || (diff == bestDiff && points[i].date.After(best.date)) {
best = points[i]
bestDiff = diff
}
}
return best.rate
}
func absDurationDays(d time.Duration) int64 {
if d < 0 {
d = -d
}
return int64(d.Hours() / 24)
}
func parseDateOnly(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
}
func toTRYByRate(amount float64, currency string, currTry float64) float64 {
currency = strings.ToUpper(strings.TrimSpace(currency))
if currency == "" || currency == "TRY" {
return amount
}
if currTry <= 0 {
return 0
}
return amount * currTry
}
func toUSDByRates(amount float64, currency string, currTry, usdTry float64) float64 {
currency = strings.ToUpper(strings.TrimSpace(currency))
switch currency {
case "USD":
return amount
case "", "TRY":
if usdTry <= 0 {
return 0
}
return amount / usdTry
default:
if currTry <= 0 || usdTry <= 0 {
return 0
}
return (amount * currTry) / usdTry
}
}
// Returns X for "1 USD = X <currency>".
func usdRateInCurrency(currency string, currTry, usdTry float64) float64 {
currency = strings.ToUpper(strings.TrimSpace(currency))
switch currency {
case "", "USD":
return 1
case "TRY":
if usdTry <= 0 {
return 0
}
return usdTry
default:
if currTry <= 0 || usdTry <= 0 {
return 0
}
return usdTry / currTry
}
}

View File

@@ -34,10 +34,38 @@ func GetStatements(ctx context.Context, params models.StatementParams) ([]models
}
}
piyasaScope, err := buildPiyasaExistsForCariCode(ctx, "b.CurrAccCode")
customerPiyasaInClause, err := resolvePiyasaScopeInClause(ctx, "PF.CustomerAtt01")
if err != nil {
return nil, err
}
vendorPiyasaInClause, err := resolvePiyasaScopeInClause(ctx, "VF.VendorAtt01")
if err != nil {
return nil, err
}
piyasaScope := fmt.Sprintf(`
(
(b.CurrAccTypeCode = 3 AND EXISTS (
SELECT 1
FROM CustomerAttributesFilter PF WITH (NOLOCK)
WHERE (PF.CurrAccCode = b.CurrAccCode OR LEFT(PF.CurrAccCode, 8) = LEFT(b.CurrAccCode, 8))
AND %s
))
OR
(b.CurrAccTypeCode = 1 AND EXISTS (
SELECT 1
FROM (
SELECT
CurrAccCode,
VendorAtt01 = MAX(CASE WHEN AttributeTypeCode = 1 THEN AttributeCode END)
FROM prCurrAccAttribute WITH (NOLOCK)
WHERE CurrAccTypeCode = 1
GROUP BY CurrAccCode
) VF
WHERE (VF.CurrAccCode = b.CurrAccCode OR LEFT(VF.CurrAccCode, 8) = LEFT(b.CurrAccCode, 8))
AND %s
))
)`, customerPiyasaInClause, vendorPiyasaInClause)
query := fmt.Sprintf(`
;WITH CurrDesc AS (
@@ -62,7 +90,7 @@ HasMovement AS (
INNER JOIN CurrAccBookATAttributesFilter f
ON f.CurrAccBookID = b.CurrAccBookID
AND f.ATAtt01 IN (%s)
WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = REPLACE(@Carikod, ' ', '')
WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = LEFT(REPLACE(@Carikod, ' ', ''), 7)
AND b.DocumentDate BETWEEN @startdate AND @enddate
AND %s
) THEN 1 ELSE 0 END AS HasMov
@@ -85,7 +113,8 @@ Opening AS (
LEFT JOIN trCurrAccBookCurrency c
ON c.CurrAccBookID = b.CurrAccBookID
AND c.CurrencyCode = b.DocCurrencyCode
WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = REPLACE(@Carikod, ' ', '')
WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = LEFT(REPLACE(@Carikod, ' ', ''), 7)
AND @ExcludeOpening = 0
AND %s
AND (
(hm.HasMov = 1 AND b.DocumentDate < @startdate) -- hareket varsa: klasik devir
@@ -142,7 +171,7 @@ Movements AS (
ON c.CurrAccBookID = b.CurrAccBookID
AND c.CurrencyCode = b.DocCurrencyCode
WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = REPLACE(@Carikod, ' ', '')
WHERE LEFT(REPLACE(b.CurrAccCode, ' ', ''), 7) = LEFT(REPLACE(@Carikod, ' ', ''), 7)
AND %s
AND b.DocumentDate BETWEEN @startdate AND @enddate
)
@@ -223,6 +252,7 @@ ORDER BY
sql.Named("enddate", params.EndDate),
sql.Named("Carikod", params.AccountCode),
sql.Named("LangCode", params.LangCode),
sql.Named("ExcludeOpening", params.ExcludeOpening),
)
if err != nil {
return nil, fmt.Errorf("MSSQL query error: %v", err)

View File

@@ -70,7 +70,7 @@ LEFT JOIN cdItemAttributeDesc KisaKarDesc
ON KisaKar.AttributeTypeCode = KisaKarDesc.AttributeTypeCode
AND KisaKar.AttributeCode = KisaKarDesc.AttributeCode
AND KisaKar.ItemTypeCode = KisaKarDesc.ItemTypeCode
WHERE a.CurrAccCode LIKE @Carikod
WHERE REPLACE(a.CurrAccCode, ' ', '') LIKE REPLACE(@Carikod, ' ', '') + '%%'
AND a.InvoiceDate BETWEEN @StartDate AND @EndDate
AND %s
%s
@@ -91,7 +91,7 @@ ORDER BY Belge_Tarihi, Belge_Ref_Numarasi, Urun_Kodu;`,
)
rows, err := db.MssqlDB.QueryContext(ctx, query,
sql.Named("Carikod", "%"+accountCode+"%"),
sql.Named("Carikod", normalizeMasterAccountCode(accountCode)),
sql.Named("StartDate", startDate),
sql.Named("EndDate", endDate),
)

View File

@@ -7,9 +7,7 @@ import (
"bytes"
"database/sql"
"fmt"
"log"
"net/http"
"runtime/debug"
"sort"
"strconv"
"strings"
@@ -33,18 +31,10 @@ type balanceSummaryPDF struct {
TLBakiye13 float64
VadeGun float64
VadeBelge float64
VadeBase float64
}
func ExportCustomerBalancePDFHandler(_ *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("PANIC ExportCustomerBalancePDFHandler: %v\n%s", rec, string(debug.Stack()))
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
@@ -147,6 +137,9 @@ func filterCustomerBalanceRowsForPDF(rows []models.CustomerBalanceListRow, exclu
func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanceSummaryPDF, map[string][]models.CustomerBalanceListRow) {
summaryMap := make(map[string]*balanceSummaryPDF)
detailsByMaster := make(map[string][]models.CustomerBalanceListRow)
vadeWeightMap := make(map[string]float64)
vadeGunSumMap := make(map[string]float64)
vadeBelgeSumMap := make(map[string]float64)
for _, row := range rows {
master := strings.TrimSpace(row.AnaCariKodu)
@@ -194,11 +187,11 @@ func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanc
s.TLBakiye12 += row.TLBakiye12
s.USDBakiye13 += row.USDBakiye13
s.TLBakiye13 += row.TLBakiye13
w := absFloat(row.Bakiye12) + absFloat(row.Bakiye13)
w := absFloatExcel(row.USDBakiye12) + absFloatExcel(row.TLBakiye12) + absFloatExcel(row.USDBakiye13) + absFloatExcel(row.TLBakiye13)
if w > 0 {
s.VadeBase += w
s.VadeGun += row.VadeGun * w
s.VadeBelge += row.VadeBelgeGun * w
vadeWeightMap[master] += w
vadeGunSumMap[master] += row.VadeGun * w
vadeBelgeSumMap[master] += row.VadeBelgeGun * w
}
detailsByMaster[master] = append(detailsByMaster[master], row)
@@ -212,10 +205,9 @@ func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanc
summaries := make([]balanceSummaryPDF, 0, len(masters))
for _, m := range masters {
s := summaryMap[m]
if s != nil && s.VadeBase > 0 {
s.VadeGun = s.VadeGun / s.VadeBase
s.VadeBelge = s.VadeBelge / s.VadeBase
if base := vadeWeightMap[m]; base > 0 {
summaryMap[m].VadeGun = vadeGunSumMap[m] / base
summaryMap[m].VadeBelge = vadeBelgeSumMap[m] / base
}
summaries = append(summaries, *summaryMap[m])
d := detailsByMaster[m]
@@ -248,11 +240,11 @@ func drawCustomerBalancePDF(
marginL, marginT, marginR, marginB := 8.0, 8.0, 8.0, 12.0
tableW := pageW - marginL - marginR
summaryCols := []string{"Ana Cari Kod", "Ana Cari Detay", "Piyasa", "Temsilci", "Risk", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY", "Vade Gun", "Belge Gun"}
summaryW := normalizeWidths([]float64{18, 46, 14, 18, 12, 20, 20, 12, 12, 12, 12, 10, 10}, tableW)
summaryCols := []string{"Ana Cari Kod", "Ana Cari Detay", "Piyasa", "Temsilci", "Risk", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY"}
summaryW := normalizeWidths([]float64{20, 43, 18, 18, 16, 27, 27, 15, 15, 15, 15}, tableW)
detailCols := []string{"Cari Kod", "Cari Detay", "Sirket", "Muhasebe", "Doviz", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY", "Vade Gun", "Belge Gun"}
detailW := normalizeWidths([]float64{22, 40, 9, 16, 8, 20, 20, 12, 12, 12, 12, 9, 9}, tableW)
detailCols := []string{"Cari Kod", "Cari Detay", "Sirket", "Muhasebe", "Doviz", "1_2 Pr.Br", "1_3 Pr.Br", "1_2 USD", "1_2 TRY", "1_3 USD", "1_3 TRY"}
detailW := normalizeWidths([]float64{26, 46, 10, 20, 10, 24, 24, 15, 15, 15, 15}, tableW)
header := func() {
pdf.AddPage()
@@ -288,44 +280,6 @@ func drawCustomerBalancePDF(
return pdf.GetY()+needH+marginB > 210.0
}
wrappedLines := func(text string, w float64) [][]byte {
t := strings.TrimSpace(sanitizePDFText(text))
if t == "" {
t = "-"
}
return splitLinesSafe(pdf, t, w)
}
calcWrappedRowHeight := func(row []string, widths []float64, wrapIdx map[int]bool, lineH float64, minH float64) float64 {
maxLines := 1
for i, v := range row {
if !wrapIdx[i] {
continue
}
ln := len(wrappedLines(v, widths[i]-2))
if ln > maxLines {
maxLines = ln
}
}
h := float64(maxLines)*lineH + 1.2
if h < minH {
return minH
}
return h
}
drawWrapped := func(text string, x, y, w, rowH, lineH float64, align string) {
lines := wrappedLines(text, w-2)
total := float64(len(lines)) * lineH
startY := y + (rowH-total)/2
cy := startY
for _, ln := range lines {
pdf.SetXY(x+1, cy)
pdf.CellFormat(w-2, lineH, sanitizePDFText(string(ln)), "", 0, align, false, 0, "")
cy += lineH
}
}
drawSummaryHeader := func() {
pdf.SetFont("dejavu", "B", 7.5)
pdf.SetFillColor(149, 113, 22)
@@ -362,7 +316,12 @@ func drawCustomerBalancePDF(
pdf.SetFont("dejavu", "", 7.2)
pdf.SetTextColor(20, 20, 20)
drawSummaryRow := func(s balanceSummaryPDF) {
for _, s := range summaries {
if needPage(6.2) {
header()
drawSummaryHeader()
}
row := []string{
s.AnaCariKodu,
s.AnaCariAdi,
@@ -375,56 +334,31 @@ func drawCustomerBalancePDF(
formatMoneyPDF(s.TLBakiye12),
formatMoneyPDF(s.USDBakiye13),
formatMoneyPDF(s.TLBakiye13),
formatMoneyPDF(s.VadeGun),
formatMoneyPDF(s.VadeBelge),
}
wrapCols := map[int]bool{1: true, 3: true}
rowH := calcWrappedRowHeight(row, summaryW, wrapCols, 3.2, 6.2)
if needPage(rowH) {
header()
drawSummaryHeader()
pdf.SetFont("dejavu", "", 7.2)
pdf.SetTextColor(20, 20, 20)
}
y := pdf.GetY()
x := marginL
for i, v := range row {
if detailed {
pdf.SetFillColor(246, 241, 231)
pdf.Rect(x, y, summaryW[i], rowH, "FD")
} else {
pdf.Rect(x, y, summaryW[i], rowH, "")
}
pdf.Rect(x, y, summaryW[i], 6.2, "")
align := "L"
if i >= 7 {
align = "R"
}
if wrapCols[i] {
drawWrapped(v, x, y, summaryW[i], rowH, 3.2, "L")
} else {
pdf.SetXY(x+1, y+(rowH-4.2)/2)
pdf.CellFormat(summaryW[i]-2, 4.2, sanitizePDFText(v), "", 0, align, false, 0, "")
}
pdf.SetXY(x+1, y+1)
pdf.CellFormat(summaryW[i]-2, 4.2, v, "", 0, align, false, 0, "")
x += summaryW[i]
}
pdf.SetY(y + rowH)
pdf.SetY(y + 6.2)
}
if !detailed {
for _, s := range summaries {
drawSummaryRow(s)
}
return
}
pdf.Ln(1.8)
for _, s := range summaries {
drawSummaryRow(s)
pdf.Ln(1.2)
rows := detailsByMaster[s.AnaCariKodu]
if len(rows) == 0 {
pdf.Ln(1.0)
continue
}
@@ -446,25 +380,7 @@ func drawCustomerBalancePDF(
pdf.SetTextColor(40, 40, 40)
for _, r := range rows {
line := []string{
r.CariKodu,
r.CariDetay,
r.Sirket,
r.MuhasebeKodu,
r.CariDoviz,
formatMoneyPDF(r.Bakiye12),
formatMoneyPDF(r.Bakiye13),
formatMoneyPDF(r.USDBakiye12),
formatMoneyPDF(r.TLBakiye12),
formatMoneyPDF(r.USDBakiye13),
formatMoneyPDF(r.TLBakiye13),
formatMoneyPDF(r.VadeGun),
formatMoneyPDF(r.VadeBelgeGun),
}
detailWrapCols := map[int]bool{1: true}
rowH := calcWrappedRowHeight(line, detailW, detailWrapCols, 3.0, 5.8)
if needPage(rowH) {
if needPage(5.8) {
header()
pdf.SetFont("dejavu", "B", 8)
pdf.SetFillColor(218, 193, 151)
@@ -479,23 +395,33 @@ func drawCustomerBalancePDF(
pdf.SetTextColor(40, 40, 40)
}
line := []string{
r.CariKodu,
r.CariDetay,
r.Sirket,
r.MuhasebeKodu,
r.CariDoviz,
formatMoneyPDF(r.Bakiye12),
formatMoneyPDF(r.Bakiye13),
formatMoneyPDF(r.USDBakiye12),
formatMoneyPDF(r.TLBakiye12),
formatMoneyPDF(r.USDBakiye13),
formatMoneyPDF(r.TLBakiye13),
}
rowY := pdf.GetY()
rowX := marginL
for i, v := range line {
pdf.Rect(rowX, rowY, detailW[i], rowH, "")
pdf.Rect(rowX, rowY, detailW[i], 5.8, "")
align := "L"
if i >= 5 {
align = "R"
}
if detailWrapCols[i] {
drawWrapped(v, rowX, rowY, detailW[i], rowH, 3.0, "L")
} else {
pdf.SetXY(rowX+1, rowY+(rowH-4.0)/2)
pdf.CellFormat(detailW[i]-2, 4.0, sanitizePDFText(v), "", 0, align, false, 0, "")
}
pdf.SetXY(rowX+1, rowY+0.8)
pdf.CellFormat(detailW[i]-2, 4.0, v, "", 0, align, false, 0, "")
rowX += detailW[i]
}
pdf.SetY(rowY + rowH)
pdf.SetY(rowY + 5.8)
}
pdf.Ln(1.2)
}
@@ -548,24 +474,3 @@ func formatMoneyPDF(v float64) string {
return sign + strings.Join(out, ".") + "," + decPart
}
func absFloat(v float64) float64 {
if v < 0 {
return -v
}
return v
}
func sanitizePDFText(s string) string {
s = strings.ToValidUTF8(s, "?")
s = strings.ReplaceAll(s, "\x00", " ")
return strings.Map(func(r rune) rune {
if r == '\n' || r == '\r' || r == '\t' {
return ' '
}
if r < 32 {
return -1
}
return r
}, s)
}

View File

@@ -18,7 +18,51 @@ func GetStatementAgingHandler(w http.ResponseWriter, r *http.Request) {
return
}
selectedDate := time.Now().Format("2006-01-02")
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
}
rows, err := queries.GetStatementAging(params)
if err != nil {
http.Error(w, "Error fetching aging statement: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if err := json.NewEncoder(w).Encode(rows); err != nil {
http.Error(w, "Error encoding response: "+err.Error(), http.StatusInternalServerError)
}
}
// GET /api/finance/aged-customer-balance-list
func GetAgedCustomerBalanceListHandler(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,

View File

@@ -21,7 +21,13 @@ func ExportStatementAgingExcelHandler(_ *sql.DB) http.HandlerFunc {
return
}
selectedDate := time.Now().Format("2006-01-02")
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.CustomerBalanceListParams{
SelectedDate: selectedDate,
CariSearch: strings.TrimSpace(r.URL.Query().Get("cari_search")),
@@ -34,9 +40,17 @@ func ExportStatementAgingExcelHandler(_ *sql.DB) http.HandlerFunc {
Il: strings.TrimSpace(r.URL.Query().Get("il")),
Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")),
}
if accountCode := strings.TrimSpace(r.URL.Query().Get("accountcode")); accountCode != "" {
params.CariSearch = accountCode
}
excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12"))
excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13"))
if err := queries.RebuildStatementAgingCache(r.Context()); err != nil {
http.Error(w, "Error rebuilding aging cache: "+err.Error(), http.StatusInternalServerError)
return
}
rows, err := queries.GetStatementAgingBalanceList(r.Context(), params)
if err != nil {
http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError)

View File

@@ -7,92 +7,30 @@ import (
"bytes"
"database/sql"
"fmt"
"log"
"math"
"net/http"
"runtime/debug"
"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) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("PANIC ExportStatementAgingPDFHandler: %v\n%s", rec, string(debug.Stack()))
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
selectedDate := time.Now().Format("2006-01-02")
listParams := models.CustomerBalanceListParams{
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.CustomerBalanceListParams{
SelectedDate: selectedDate,
CariSearch: strings.TrimSpace(r.URL.Query().Get("cari_search")),
CariIlkGrup: strings.TrimSpace(r.URL.Query().Get("cari_ilk_grup")),
@@ -104,26 +42,31 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc {
Il: strings.TrimSpace(r.URL.Query().Get("il")),
Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")),
}
if accountCode := strings.TrimSpace(r.URL.Query().Get("accountcode")); accountCode != "" {
params.CariSearch = accountCode
}
if err := queries.RebuildStatementAgingCache(r.Context()); err != nil {
http.Error(w, "Error rebuilding aging cache: "+err.Error(), http.StatusInternalServerError)
return
}
detailed := parseBoolQuery(r.URL.Query().Get("detailed"))
excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12"))
excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13"))
if err := queries.RebuildStatementAgingCache(r.Context()); err != nil {
http.Error(w, "cache rebuild error: "+err.Error(), http.StatusInternalServerError)
return
}
rows, err := queries.GetStatementAgingBalanceList(r.Context(), listParams)
rows, err := queries.GetStatementAgingBalanceList(r.Context(), params)
if err != nil {
http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError)
return
}
rows = filterCustomerBalanceRowsForPDF(rows, excludeZero12, excludeZero13)
summaries, detailsByMaster := buildCustomerBalancePDFData(rows)
pdf := gofpdf.New("L", "mm", "A4", "")
pdf.SetMargins(8, 8, 8)
pdf.SetAutoPageBreak(false, 10)
pdf.SetAutoPageBreak(false, 12)
if err := registerDejavuFonts(pdf, "dejavu"); err != nil {
http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError)
return
@@ -132,7 +75,7 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc {
drawCustomerBalancePDF(
pdf,
selectedDate,
listParams.CariSearch,
params.CariSearch,
detailed,
summaries,
detailsByMaster,
@@ -149,457 +92,13 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc {
return
}
w.Header().Set("Content-Type", "application/pdf")
filename := "account-aging-summary.pdf"
if detailed {
filename = "account-aging-detailed.pdf"
}
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename))
_, _ = 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, sanitizePDFText("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, sanitizePDFText("Son Tarih: "+p.EndDate), "", 0, "R", false, 0, "")
pdf.SetXY(pageW-marginR-95, marginT+6)
pdf.CellFormat(95, 5, sanitizePDFText("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, sanitizePDFText("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, sanitizePDFText(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, sanitizePDFText(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
}

View File

@@ -6,6 +6,7 @@ import (
"bssapp-backend/queries"
"encoding/json"
"net/http"
"strconv"
)
// GET /api/statements
@@ -18,11 +19,17 @@ func GetStatementHeadersHandler(w http.ResponseWriter, r *http.Request) {
}
params := models.StatementParams{
StartDate: r.URL.Query().Get("startdate"),
EndDate: r.URL.Query().Get("enddate"),
AccountCode: r.URL.Query().Get("accountcode"),
LangCode: r.URL.Query().Get("langcode"),
Parislemler: r.URL.Query()["parislemler"],
StartDate: r.URL.Query().Get("startdate"),
EndDate: r.URL.Query().Get("enddate"),
AccountCode: r.URL.Query().Get("accountcode"),
LangCode: r.URL.Query().Get("langcode"),
Parislemler: r.URL.Query()["parislemler"],
ExcludeOpening: false,
}
if raw := r.URL.Query().Get("excludeopening"); raw != "" {
if parsed, err := strconv.ParseBool(raw); err == nil {
params.ExcludeOpening = parsed
}
}
statements, err := queries.GetStatements(r.Context(), params)