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

@@ -431,6 +431,30 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router
wrapV3(http.HandlerFunc(routes.GetCustomerBalanceListHandler)),
)
bindV3(r, pgDB,
"/api/finance/customer-balances/export-pdf", "GET",
"finance", "export",
wrapV3(routes.ExportCustomerBalancePDFHandler(mssql)),
)
bindV3(r, pgDB,
"/api/finance/customer-balances/export-excel", "GET",
"finance", "export",
wrapV3(routes.ExportCustomerBalanceExcelHandler(mssql)),
)
bindV3(r, pgDB,
"/api/finance/account-aging-statement", "GET",
"finance", "view",
wrapV3(http.HandlerFunc(routes.GetStatementAgingHandler)),
)
bindV3(r, pgDB,
"/api/finance/account-aging-statement/export-pdf", "GET",
"finance", "export",
wrapV3(routes.ExportStatementAgingPDFHandler(mssql)),
)
// ============================================================
// REPORT (STATEMENTS)
// ============================================================

View File

@@ -9,28 +9,36 @@ type CustomerBalanceListParams struct {
RiskDurumu string
IslemTipi string
Ulke string
Il string
Ilce string
}
type CustomerBalanceListRow struct {
CariIlkGrup string `json:"cari_ilk_grup"`
Piyasa string `json:"piyasa"`
Temsilci string `json:"temsilci"`
Sirket string `json:"sirket"`
AnaCariKodu string `json:"ana_cari_kodu"`
AnaCariAdi string `json:"ana_cari_adi"`
CariKodu string `json:"cari_kodu"`
CariDetay string `json:"cari_detay"`
Ozellik03 string `json:"ozellik03"`
Ozellik05 string `json:"ozellik05"`
Ozellik06 string `json:"ozellik06"`
Ozellik07 string `json:"ozellik07"`
CariDoviz string `json:"cari_doviz"`
Bakiye12 float64 `json:"bakiye_1_2"`
TLBakiye12 float64 `json:"tl_bakiye_1_2"`
USDBakiye12 float64 `json:"usd_bakiye_1_2"`
Bakiye13 float64 `json:"bakiye_1_3"`
TLBakiye13 float64 `json:"tl_bakiye_1_3"`
USDBakiye13 float64 `json:"usd_bakiye_1_3"`
HesapAlinmayanGun NullInt32 `json:"hesap_alinmayan_gun"`
KalanFaturaOrtalamaVadeTarihi NullString `json:"kalan_fatura_ortalama_vade_tarihi"`
CariIlkGrup string `json:"cari_ilk_grup"`
Piyasa string `json:"piyasa"`
Temsilci string `json:"temsilci"`
Sirket string `json:"sirket"`
AnaCariKodu string `json:"ana_cari_kodu"`
AnaCariAdi string `json:"ana_cari_adi"`
CariKodu string `json:"cari_kodu"`
CariDetay string `json:"cari_detay"`
CariTip string `json:"cari_tip"`
Kanal1 string `json:"kanal_1"`
Ozellik03 string `json:"ozellik03"`
Ozellik05 string `json:"ozellik05"`
Ozellik06 string `json:"ozellik06"`
Ozellik07 string `json:"ozellik07"`
Il string `json:"il"`
Ilce string `json:"ilce"`
MuhasebeKodu string `json:"muhasebe_kodu"`
TC string `json:"tc"`
RiskDurumu string `json:"risk_durumu"`
SirketDetay string `json:"sirket_detay"`
CariDoviz string `json:"cari_doviz"`
Bakiye12 float64 `json:"bakiye_1_2"`
TLBakiye12 float64 `json:"tl_bakiye_1_2"`
USDBakiye12 float64 `json:"usd_bakiye_1_2"`
Bakiye13 float64 `json:"bakiye_1_3"`
TLBakiye13 float64 `json:"tl_bakiye_1_3"`
USDBakiye13 float64 `json:"usd_bakiye_1_3"`
}

View File

@@ -0,0 +1,7 @@
package models
type StatementAgingParams struct {
AccountCode string `json:"accountcode"`
EndDate string `json:"enddate"`
Parislemler []string `json:"parislemler"`
}

View File

@@ -1,91 +0,0 @@
package queries
import (
"bssapp-backend/models"
"context"
)
func GetCustomerBalanceList(
ctx context.Context,
params models.CustomerBalanceListParams,
) ([]models.CustomerBalanceListRow, error) {
//------------------------------------------------
// 1⃣ DATA ÇEK
//------------------------------------------------
balances, err := getFastBalances(ctx, params.SelectedDate)
if err != nil {
return nil, err
}
masterMap, err := getCariMasterMap(ctx)
if err != nil {
return nil, err
}
//------------------------------------------------
// 2⃣ MERGE
//------------------------------------------------
resultMap := make(map[string]*models.CustomerBalanceListRow)
for _, b := range balances {
key := b.CariKodu + "|" + b.CariDoviz
r, ok := resultMap[key]
if !ok {
r = &models.CustomerBalanceListRow{
CariKodu: b.CariKodu,
CariDoviz: b.CariDoviz,
}
// Master
if m, ok := masterMap[b.CariKodu]; ok {
r.CariDetay = m.CariDetay
r.Piyasa = m.Piyasa
r.Temsilci = m.Temsilci
r.Ozellik03 = m.Ozellik03
r.Ozellik05 = m.Ozellik05
r.Ozellik06 = m.Ozellik06
r.Ozellik07 = m.Ozellik07
r.CariIlkGrup = m.Ozellik08
}
resultMap[key] = r
}
//------------------------------------------------
// 3⃣ TOPLA
//------------------------------------------------
if b.PislemTipi == "1_2" {
r.Bakiye12 += b.Bakiye
r.TLBakiye12 += b.KurBakiye
r.USDBakiye12 += b.KurBakiye / b.UsdKur
} else if b.PislemTipi == "1_3" {
r.Bakiye13 += b.Bakiye
r.TLBakiye13 += b.KurBakiye
r.USDBakiye13 += b.KurBakiye / b.UsdKur
}
}
//------------------------------------------------
// 4⃣ SLICE DÖN
//------------------------------------------------
out := make([]models.CustomerBalanceListRow, 0, len(resultMap))
for _, v := range resultMap {
out = append(out, *v)
}
return out, nil
}

View File

@@ -1,79 +0,0 @@
package queries
import (
"bssapp-backend/db"
"context"
)
type CariMasterRow struct {
CariKodu string
CariDetay string
Piyasa string
Temsilci string
Ozellik03 string
Ozellik05 string
Ozellik06 string
Ozellik07 string
Ozellik08 string
}
func getCariMasterMap(
ctx context.Context,
) (map[string]CariMasterRow, error) {
const q = `
WITH CTE AS (
SELECT
*,
rn = ROW_NUMBER() OVER (
PARTITION BY LEFT(CariKodu,8)
ORDER BY CariKodu
)
FROM dbo.MK_CARI_ILETISIM WITH(NOLOCK)
)
SELECT
CariKodu,
CariDetay,
PIYASA,
CARI_TEMSILCI,
Ozellik03,
Ozellik05,
Ozellik06,
Ozellik07,
Ozellik08
FROM CTE
WHERE rn=1
`
rows, err := db.MssqlDB.QueryContext(ctx, q)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[string]CariMasterRow, 4096)
for rows.Next() {
var r CariMasterRow
err := rows.Scan(
&r.CariKodu,
&r.CariDetay,
&r.Piyasa,
&r.Temsilci,
&r.Ozellik03,
&r.Ozellik05,
&r.Ozellik06,
&r.Ozellik07,
&r.Ozellik08,
)
if err != nil {
return nil, err
}
out[r.CariKodu] = r
}
return out, nil
}

View File

@@ -3,62 +3,10 @@ package queries
import (
"bssapp-backend/models"
"database/sql"
"sync"
"time"
"strings"
)
/* ===============================
CACHE STRUCT
================================ */
type currencyCacheItem struct {
data *models.TodayCurrencyV3
expiresAt time.Time
}
var (
currencyCache = make(map[string]currencyCacheItem)
cacheMutex sync.RWMutex
cacheTTL = 5 * time.Minute
)
/* ===============================
MAIN CACHE FUNC
================================ */
func GetCachedCurrencyV3(db *sql.DB, code string) (*models.TodayCurrencyV3, error) {
now := time.Now()
/* ---------- READ CACHE ---------- */
cacheMutex.RLock()
item, ok := currencyCache[code]
if ok && now.Before(item.expiresAt) {
cacheMutex.RUnlock()
return item.data, nil
}
cacheMutex.RUnlock()
/* ---------- FETCH DB ---------- */
data, err := GetTodayCurrencyV3(db, code)
if err != nil {
return nil, err
}
/* ---------- WRITE CACHE ---------- */
cacheMutex.Lock()
currencyCache[code] = currencyCacheItem{
data: data,
expiresAt: now.Add(cacheTTL),
}
cacheMutex.Unlock()
return data, nil
// GetCachedCurrencyV3 keeps compatibility with existing order routes.
func GetCachedCurrencyV3(db *sql.DB, currencyCode string) (*models.TodayCurrencyV3, error) {
return GetTodayCurrencyV3(db, strings.ToUpper(strings.TrimSpace(currencyCode)))
}

View File

@@ -0,0 +1,784 @@
package queries
import (
"bssapp-backend/auth"
"bssapp-backend/db"
"bssapp-backend/internal/authz"
"bssapp-backend/models"
"context"
"database/sql"
"fmt"
"log"
"sort"
"strconv"
"strings"
)
type mkCariBakiyeLine struct {
CurrAccTypeCode int
CariKodu string
CariDoviz string
SirketKodu int
PislemTipi string
YerelBakiye float64
Bakiye float64
}
type cariMeta struct {
CariDetay string
CariTip string
Kanal1 string
Piyasa string
Temsilci string
Ulke string
Il string
Ilce string
TC string
RiskDurumu string
MuhasebeKodu string
SirketDetay string
}
type masterCariMeta struct {
CariDetay string
Kanal1 string
Piyasa string
Temsilci string
Ulke string
Il string
Ilce string
RiskDurumu string
}
type balanceFilters struct {
cariIlkGrup map[string]struct{}
piyasa map[string]struct{}
temsilci map[string]struct{}
riskDurumu map[string]struct{}
islemTipi map[string]struct{}
ulke map[string]struct{}
il map[string]struct{}
ilce map[string]struct{}
}
func GetCustomerBalanceList(ctx context.Context, params models.CustomerBalanceListParams) ([]models.CustomerBalanceListRow, error) {
if strings.TrimSpace(params.SelectedDate) == "" {
return nil, fmt.Errorf("selected_date is required")
}
lines, err := loadBalanceLines(ctx, params.SelectedDate, params.CariSearch)
if err != nil {
return nil, err
}
metaMap, err := loadCariMetaMap(ctx, lines)
if err != nil {
log.Printf("customer_balance_list: cari meta query failed, fallback without meta: %v", err)
metaMap = map[string]cariMeta{}
}
masterMetaMap, err := loadMasterCariMetaMap(ctx, lines)
if err != nil {
log.Printf("customer_balance_list: master cari meta query failed, fallback without master meta: %v", err)
masterMetaMap = map[string]masterCariMeta{}
}
companyMap, err := loadCompanyMap(ctx)
if err != nil {
return nil, err
}
glMap, err := loadGLAccountMap(ctx, lines)
if err != nil {
return nil, err
}
rateMap, err := loadNearestTryRates(ctx)
if err != nil {
return nil, err
}
usdTry := rateMap["USD"]
if usdTry <= 0 {
usdTry = 1
}
filters := buildFilters(params)
agg := make(map[string]*models.CustomerBalanceListRow, len(lines))
for _, ln := range lines {
cari := strings.TrimSpace(ln.CariKodu)
if cari == "" {
continue
}
curr := strings.ToUpper(strings.TrimSpace(ln.CariDoviz))
if curr == "" {
curr = "TRY"
}
meta := metaMap[metaKey(ln.CurrAccTypeCode, cari)]
meta.MuhasebeKodu = glMap[glKey(ln.CurrAccTypeCode, cari, ln.SirketKodu)]
meta.SirketDetay = companyMap[ln.SirketKodu]
master := deriveMasterCari(cari)
mm := masterMetaMap[master]
if strings.TrimSpace(mm.Kanal1) != "" {
meta.Kanal1 = mm.Kanal1
}
if strings.TrimSpace(mm.Piyasa) != "" {
meta.Piyasa = mm.Piyasa
}
if strings.TrimSpace(mm.Temsilci) != "" {
meta.Temsilci = mm.Temsilci
}
if strings.TrimSpace(mm.Ulke) != "" {
meta.Ulke = mm.Ulke
}
if strings.TrimSpace(mm.Il) != "" {
meta.Il = mm.Il
}
if strings.TrimSpace(mm.Ilce) != "" {
meta.Ilce = mm.Ilce
}
if strings.TrimSpace(mm.RiskDurumu) != "" {
meta.RiskDurumu = mm.RiskDurumu
}
if !filters.matchLine(ln.PislemTipi, meta) {
continue
}
key := strconv.Itoa(ln.CurrAccTypeCode) + "|" + cari + "|" + curr + "|" + strconv.Itoa(ln.SirketKodu)
row, ok := agg[key]
if !ok {
row = &models.CustomerBalanceListRow{
CariIlkGrup: meta.Kanal1,
Piyasa: meta.Piyasa,
Temsilci: meta.Temsilci,
Sirket: strconv.Itoa(ln.SirketKodu),
AnaCariKodu: master,
AnaCariAdi: firstNonEmpty(mm.CariDetay, meta.CariDetay),
CariKodu: cari,
CariDetay: meta.CariDetay,
CariTip: meta.CariTip,
Kanal1: meta.Kanal1,
Ozellik03: meta.RiskDurumu,
Ozellik05: meta.Ulke,
Ozellik06: meta.Il,
Ozellik07: meta.Ilce,
Il: meta.Il,
Ilce: meta.Ilce,
MuhasebeKodu: meta.MuhasebeKodu,
TC: meta.TC,
RiskDurumu: meta.RiskDurumu,
SirketDetay: meta.SirketDetay,
CariDoviz: curr,
}
agg[key] = row
}
usd := toUSD(ln.Bakiye, curr, usdTry, rateMap)
switch strings.TrimSpace(ln.PislemTipi) {
case "1_2":
row.Bakiye12 += ln.Bakiye
row.TLBakiye12 += ln.YerelBakiye
row.USDBakiye12 += usd
case "1_3":
row.Bakiye13 += ln.Bakiye
row.TLBakiye13 += ln.YerelBakiye
row.USDBakiye13 += usd
}
}
out := make([]models.CustomerBalanceListRow, 0, len(agg))
for _, v := range agg {
out = append(out, *v)
}
sort.Slice(out, func(i, j int) bool {
if out[i].AnaCariKodu == out[j].AnaCariKodu {
if out[i].CariKodu == out[j].CariKodu {
return out[i].CariDoviz < out[j].CariDoviz
}
return out[i].CariKodu < out[j].CariKodu
}
return out[i].AnaCariKodu < out[j].AnaCariKodu
})
return out, nil
}
func loadMasterCariMetaMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]masterCariMeta, error) {
masters := make(map[string]struct{})
for _, ln := range lines {
m := strings.TrimSpace(deriveMasterCari(ln.CariKodu))
if m != "" {
masters[m] = struct{}{}
}
}
if len(masters) == 0 {
return map[string]masterCariMeta{}, nil
}
query := fmt.Sprintf(`
WITH BaseCari AS
(
SELECT
CB.CurrAccCode,
CB.CurrAccTypeCode,
MasterCari = LEFT(CB.CurrAccCode, 8),
rn = ROW_NUMBER() OVER
(
PARTITION BY LEFT(CB.CurrAccCode, 8)
ORDER BY CB.CurrAccCode
)
FROM cdCurrAcc CB WITH (NOLOCK)
WHERE CB.CurrAccTypeCode IN (1,3)
AND LEFT(CB.CurrAccCode, 8) IN (%s)
),
FirstCari AS
(
SELECT *
FROM BaseCari
WHERE rn = 1
)
SELECT
CariKodu = F.MasterCari,
CariDetay = ISNULL(cd.CurrAccDescription, ''),
KANAL_1 = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt08Desc ELSE CDesc.CustomerAtt08Desc END, ''),
PIYASA = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt01Desc ELSE CDesc.CustomerAtt01Desc END, ''),
CARI_TEMSILCI = ISNULL(
CASE
WHEN ISNULL(CASE WHEN F.CurrAccTypeCode = 1 THEN VDesc.VendorAtt02Desc ELSE CDesc.CustomerAtt02Desc END,'') = ''
THEN ISNULL(CASE WHEN F.CurrAccTypeCode = 1 THEN VAttr.VendorAtt09 ELSE CAttr.CustomerAtt09 END,'')
ELSE CASE WHEN F.CurrAccTypeCode = 1 THEN VDesc.VendorAtt02Desc ELSE CDesc.CustomerAtt02Desc END
END,''
),
ULKE = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt05Desc ELSE CDesc.CustomerAtt05Desc END, ''),
IL = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt06Desc ELSE CDesc.CustomerAtt06Desc END, ''),
ILCE = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt07Desc ELSE CDesc.CustomerAtt07Desc END, ''),
Risk_Durumu = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt03Desc ELSE CDesc.CustomerAtt03Desc END, '')
FROM FirstCari F
LEFT JOIN cdCurrAccDesc cd WITH (NOLOCK)
ON cd.CurrAccTypeCode = F.CurrAccTypeCode
AND cd.CurrAccCode = F.CurrAccCode
AND cd.LangCode = 'TR'
LEFT JOIN VendorAttributeDescriptions('TR') VDesc
ON VDesc.CurrAccCode = F.CurrAccCode
AND VDesc.CurrAccTypeCode = F.CurrAccTypeCode
LEFT JOIN CustomerAttributeDescriptions('TR') CDesc
ON CDesc.CurrAccCode = F.CurrAccCode
AND CDesc.CurrAccTypeCode = F.CurrAccTypeCode
LEFT JOIN VendorAttributes VAttr
ON VAttr.CurrAccCode = F.CurrAccCode
AND VAttr.CurrAccTypeCode = F.CurrAccTypeCode
LEFT JOIN CustomerAttributes CAttr
ON CAttr.CurrAccCode = F.CurrAccCode
AND CAttr.CurrAccTypeCode = F.CurrAccTypeCode
ORDER BY F.MasterCari;
`, quotedInList(masters))
rows, err := db.MssqlDB.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("master cari meta query error: %w", err)
}
defer rows.Close()
out := make(map[string]masterCariMeta, len(masters))
for rows.Next() {
var master string
var m masterCariMeta
if err := rows.Scan(
&master,
&m.CariDetay,
&m.Kanal1,
&m.Piyasa,
&m.Temsilci,
&m.Ulke,
&m.Il,
&m.Ilce,
&m.RiskDurumu,
); err != nil {
return nil, err
}
out[strings.TrimSpace(master)] = m
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func loadBalanceLines(ctx context.Context, selectedDate, cariSearch string) ([]mkCariBakiyeLine, error) {
query := `
SELECT
CurrAccTypeCode,
CariKodu,
CariDoviz,
SirketKodu,
PislemTipi,
YerelBakiye,
Bakiye
FROM dbo.MK_CARI_BAKIYE_LIST(@SonTarih)
WHERE (@CariSearch = '' OR CariKodu LIKE '%' + @CariSearch + '%')
`
rows, err := db.MssqlDB.QueryContext(ctx, query,
sql.Named("SonTarih", selectedDate),
sql.Named("CariSearch", strings.TrimSpace(cariSearch)),
)
if err != nil {
return nil, fmt.Errorf("MK_CARI_BAKIYE_LIST query error: %w", err)
}
defer rows.Close()
out := make([]mkCariBakiyeLine, 0, 4096)
for rows.Next() {
var r mkCariBakiyeLine
if err := rows.Scan(
&r.CurrAccTypeCode,
&r.CariKodu,
&r.CariDoviz,
&r.SirketKodu,
&r.PislemTipi,
&r.YerelBakiye,
&r.Bakiye,
); err != nil {
return nil, err
}
out = append(out, r)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func loadCariMetaMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]cariMeta, error) {
vendorCodes := make(map[string]struct{})
customerCodes := make(map[string]struct{})
for _, ln := range lines {
code := strings.TrimSpace(ln.CariKodu)
if code == "" {
continue
}
if ln.CurrAccTypeCode == 1 {
vendorCodes[code] = struct{}{}
} else if ln.CurrAccTypeCode == 3 {
customerCodes[code] = struct{}{}
}
}
if len(vendorCodes) == 0 && len(customerCodes) == 0 {
return map[string]cariMeta{}, nil
}
whereParts := make([]string, 0, 2)
if len(vendorCodes) > 0 {
whereParts = append(whereParts, fmt.Sprintf("(c.CurrAccTypeCode=1 AND c.CurrAccCode IN (%s))", quotedInList(vendorCodes)))
}
if len(customerCodes) > 0 {
whereParts = append(whereParts, fmt.Sprintf("(c.CurrAccTypeCode=3 AND c.CurrAccCode IN (%s))", quotedInList(customerCodes)))
}
query := fmt.Sprintf(`
SELECT
c.CurrAccTypeCode,
c.CurrAccCode,
CariDetay = ISNULL(d.CurrAccDescription, ''),
CariTip = CASE WHEN c.CurrAccTypeCode = 1 THEN N'Satıcı' ELSE N'Müşteri' END,
KANAL_1 = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt08Desc ELSE cad.CustomerAtt08Desc END, ''),
PIYASA = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt01Desc ELSE cad.CustomerAtt01Desc END, ''),
CARI_TEMSILCI = ISNULL(
CASE
WHEN ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt02Desc ELSE cad.CustomerAtt02Desc END, '') = ''
THEN ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN va.VendorAtt09 ELSE ca.CustomerAtt09 END, '')
ELSE CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt02Desc ELSE cad.CustomerAtt02Desc END
END,
''),
ULKE = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt05Desc ELSE cad.CustomerAtt05Desc END, ''),
IL = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt06Desc ELSE cad.CustomerAtt06Desc END, ''),
ILCE = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt07Desc ELSE cad.CustomerAtt07Desc END, ''),
TC = ISNULL(c.IdentityNum, ''),
Risk_Durumu = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt03Desc ELSE cad.CustomerAtt03Desc END, '')
FROM cdCurrAcc c WITH(NOLOCK)
LEFT JOIN cdCurrAccDesc d WITH(NOLOCK)
ON d.CurrAccTypeCode = c.CurrAccTypeCode
AND d.CurrAccCode = c.CurrAccCode
AND d.LangCode = 'TR'
LEFT JOIN VendorAttributes va WITH(NOLOCK)
ON va.CurrAccTypeCode = c.CurrAccTypeCode
AND va.CurrAccCode = c.CurrAccCode
LEFT JOIN VendorAttributeDescriptions('TR') vad
ON vad.CurrAccTypeCode = c.CurrAccTypeCode
AND vad.CurrAccCode = c.CurrAccCode
LEFT JOIN CustomerAttributes ca WITH(NOLOCK)
ON ca.CurrAccTypeCode = c.CurrAccTypeCode
AND ca.CurrAccCode = c.CurrAccCode
LEFT JOIN CustomerAttributeDescriptions('TR') cad
ON cad.CurrAccTypeCode = c.CurrAccTypeCode
AND cad.CurrAccCode = c.CurrAccCode
WHERE c.CurrAccTypeCode IN (1,3)
AND (%s)
`, strings.Join(whereParts, " OR "))
rows, err := db.MssqlDB.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("cari meta query error: %w", err)
}
defer rows.Close()
out := make(map[string]cariMeta, len(lines))
for rows.Next() {
var t int
var code string
var m cariMeta
if err := rows.Scan(
&t,
&code,
&m.CariDetay,
&m.CariTip,
&m.Kanal1,
&m.Piyasa,
&m.Temsilci,
&m.Ulke,
&m.Il,
&m.Ilce,
&m.TC,
&m.RiskDurumu,
); err != nil {
return nil, err
}
out[metaKey(t, code)] = m
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func loadGLAccountMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]string, error) {
vendorCodes := make(map[string]struct{})
customerCodes := make(map[string]struct{})
companyCodes := make(map[int]struct{})
for _, ln := range lines {
code := strings.TrimSpace(ln.CariKodu)
if code == "" {
continue
}
companyCodes[ln.SirketKodu] = struct{}{}
if ln.CurrAccTypeCode == 1 {
vendorCodes[code] = struct{}{}
} else if ln.CurrAccTypeCode == 3 {
customerCodes[code] = struct{}{}
}
}
if len(companyCodes) == 0 || (len(vendorCodes) == 0 && len(customerCodes) == 0) {
return map[string]string{}, nil
}
whereParts := make([]string, 0, 2)
if len(vendorCodes) > 0 {
whereParts = append(whereParts, fmt.Sprintf("(CurrAccTypeCode=1 AND CurrAccCode IN (%s))", quotedInList(vendorCodes)))
}
if len(customerCodes) > 0 {
whereParts = append(whereParts, fmt.Sprintf("(CurrAccTypeCode=3 AND CurrAccCode IN (%s))", quotedInList(customerCodes)))
}
query := fmt.Sprintf(`
SELECT CurrAccTypeCode, CurrAccCode, CompanyCode, GLAccCode
FROM prCurrAccGLAccount WITH(NOLOCK)
WHERE PostAccTypeCode = 100
AND CompanyCode IN (%s)
AND (%s)
`, intInList(companyCodes), strings.Join(whereParts, " OR "))
rows, err := db.MssqlDB.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("gl account query error: %w", err)
}
defer rows.Close()
out := make(map[string]string)
for rows.Next() {
var t int
var code string
var company int
var gl sql.NullString
if err := rows.Scan(&t, &code, &company, &gl); err != nil {
return nil, err
}
out[glKey(t, code, company)] = strings.TrimSpace(gl.String)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func loadCompanyMap(ctx context.Context) (map[int]string, error) {
rows, err := db.MssqlDB.QueryContext(ctx, `SELECT CompanyCode, CompanyName FROM cdCompany WITH(NOLOCK)`)
if err != nil {
return nil, fmt.Errorf("company map query error: %w", err)
}
defer rows.Close()
out := make(map[int]string)
for rows.Next() {
var code int
var name sql.NullString
if err := rows.Scan(&code, &name); err != nil {
return nil, err
}
out[code] = strings.TrimSpace(name.String)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func loadNearestTryRates(ctx context.Context) (map[string]float64, error) {
query := `
WITH Ranked AS (
SELECT
CurrencyCode,
Rate,
rn = ROW_NUMBER() OVER (
PARTITION BY CurrencyCode
ORDER BY ABS(DATEDIFF(DAY, Date, GETDATE())), Date DESC
)
FROM AllExchangeRates
WHERE RelationCurrencyCode = 'TRY'
AND ExchangeTypeCode = 6
AND Rate > 0
)
SELECT CurrencyCode, Rate
FROM Ranked
WHERE rn = 1
`
rows, err := db.MssqlDB.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("exchange rates query error: %w", err)
}
defer rows.Close()
out := map[string]float64{"TRY": 1}
for rows.Next() {
var code string
var rate float64
if err := rows.Scan(&code, &rate); err != nil {
return nil, err
}
code = strings.ToUpper(strings.TrimSpace(code))
if code != "" && rate > 0 {
out[code] = rate
}
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func toUSD(amount float64, currency string, usdTry float64, rateMap map[string]float64) float64 {
if usdTry <= 0 {
return 0
}
switch currency {
case "USD":
return amount
case "TRY":
return amount / usdTry
default:
currTry := rateMap[currency]
if currTry <= 0 {
return 0
}
return (amount * currTry) / usdTry
}
}
func deriveMasterCari(cari string) string {
cari = strings.TrimSpace(cari)
if cari == "" {
return ""
}
base := cari
if idx := strings.Index(base, "/"); idx > 0 {
base = base[:idx]
}
base = strings.TrimSpace(base)
if len(base) >= 8 {
return strings.TrimSpace(base[:8])
}
return base
}
func buildFilters(params models.CustomerBalanceListParams) balanceFilters {
return balanceFilters{
cariIlkGrup: parseCSVSet(params.CariIlkGrup),
piyasa: parseCSVSet(params.Piyasa),
temsilci: parseCSVSet(params.Temsilci),
riskDurumu: parseCSVSet(params.RiskDurumu),
islemTipi: parseCSVSet(params.IslemTipi),
ulke: parseCSVSet(params.Ulke),
il: parseCSVSet(params.Il),
ilce: parseCSVSet(params.Ilce),
}
}
func (f balanceFilters) matchLine(islemTipi string, m cariMeta) bool {
if !matchSet(f.islemTipi, islemTipi) {
return false
}
if !matchSet(f.cariIlkGrup, m.Kanal1) {
return false
}
if !matchSet(f.piyasa, m.Piyasa) {
return false
}
if !matchSet(f.temsilci, m.Temsilci) {
return false
}
if !matchSet(f.riskDurumu, m.RiskDurumu) {
return false
}
if !matchSet(f.ulke, m.Ulke) {
return false
}
if !matchSet(f.il, m.Il) {
return false
}
if !matchSet(f.ilce, m.Ilce) {
return false
}
return true
}
func matchSet(set map[string]struct{}, value string) bool {
if len(set) == 0 {
return true
}
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return true
}
_, ok := set[trimmed]
return ok
}
func parseCSVSet(v string) map[string]struct{} {
out := make(map[string]struct{})
for _, p := range strings.Split(v, ",") {
t := strings.TrimSpace(p)
if t == "" {
continue
}
out[t] = struct{}{}
}
return out
}
func getAuthorizedPiyasaCodes(ctx context.Context) ([]string, error) {
claims, ok := auth.GetClaimsFromContext(ctx)
if !ok || claims == nil {
return nil, fmt.Errorf("unauthorized: claims not found")
}
if claims.IsAdmin() {
return nil, nil
}
rawCodes := authz.GetPiyasaCodesFromCtx(ctx)
if len(rawCodes) == 0 {
return []string{}, nil
}
unique := make(map[string]struct{}, len(rawCodes))
out := make([]string, 0, len(rawCodes))
for _, code := range rawCodes {
norm := strings.ToUpper(strings.TrimSpace(code))
if norm == "" {
continue
}
if _, exists := unique[norm]; exists {
continue
}
unique[norm] = struct{}{}
out = append(out, norm)
}
if len(out) == 0 {
return []string{}, nil
}
return out, nil
}
func buildPiyasaWhereClause(codes []string, column string) string {
if len(codes) == 0 {
return "1=1"
}
return authz.BuildINClause(column, codes)
}
func metaKey(currType int, code string) string {
return strconv.Itoa(currType) + "|" + strings.TrimSpace(code)
}
func glKey(currType int, code string, company int) string {
return strconv.Itoa(currType) + "|" + strings.TrimSpace(code) + "|" + strconv.Itoa(company)
}
func quotedInList(set map[string]struct{}) string {
vals := make([]string, 0, len(set))
for v := range set {
esc := strings.ReplaceAll(strings.TrimSpace(v), "'", "''")
if esc != "" {
vals = append(vals, "'"+esc+"'")
}
}
if len(vals) == 0 {
return "''"
}
sort.Strings(vals)
return strings.Join(vals, ",")
}
func intInList(set map[int]struct{}) string {
vals := make([]int, 0, len(set))
for v := range set {
vals = append(vals, v)
}
if len(vals) == 0 {
return "0"
}
sort.Ints(vals)
parts := make([]string, 0, len(vals))
for _, v := range vals {
parts = append(parts, strconv.Itoa(v))
}
return strings.Join(parts, ",")
}
func firstNonEmpty(v ...string) string {
for _, s := range v {
if strings.TrimSpace(s) != "" {
return s
}
}
return ""
}

View File

@@ -1,69 +0,0 @@
package queries
import (
"bssapp-backend/db"
"context"
"database/sql"
)
type FastBalanceRow struct {
CariKodu string
CariDoviz string
PislemTipi string
SirketKodu int
YerelBakiye float64
Bakiye float64
IslemTarihi string
IslemKur float64
TLBakiye float64
TLYerel float64
GuncelKur float64
KurBakiye float64
UsdKur float64
}
func getFastBalances(
ctx context.Context,
date string,
) ([]FastBalanceRow, error) {
rows, err := db.MssqlDB.QueryContext(
ctx,
`EXEC dbo.SP_CARI_BAKIYE_API_FAST @Tarih`,
sql.Named("Tarih", date),
)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]FastBalanceRow, 0, 4096)
for rows.Next() {
var r FastBalanceRow
err := rows.Scan(
&r.CariKodu,
&r.CariDoviz,
&r.PislemTipi,
&r.SirketKodu,
&r.YerelBakiye,
&r.Bakiye,
&r.IslemTarihi,
&r.IslemKur,
&r.TLBakiye,
&r.TLYerel,
&r.GuncelKur,
&r.KurBakiye,
&r.UsdKur,
)
if err != nil {
return nil, err
}
out = append(out, r)
}
return out, nil
}

View File

@@ -0,0 +1,414 @@
package queries
import (
"bssapp-backend/db"
"bssapp-backend/models"
"context"
"database/sql"
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
)
func GetStatementAging(params models.StatementAgingParams) ([]map[string]interface{}, error) {
accountCode := normalizeMasterAccountCode(params.AccountCode)
if strings.TrimSpace(accountCode) == "" {
return nil, fmt.Errorf("accountcode is required")
}
if strings.TrimSpace(params.EndDate) == "" {
return nil, fmt.Errorf("enddate is required")
}
useType2, useType3 := resolveUseTypes(params.Parislemler)
endDate, _ := time.Parse("2006-01-02", strings.TrimSpace(params.EndDate))
rows, err := db.MssqlDB.Query(`
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),
)
if err != nil {
return nil, fmt.Errorf("SP_FIFO_MATCH_FINAL query error: %w", err)
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
return nil, fmt.Errorf("columns read error: %w", err)
}
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))
for i := range values {
scanArgs[i] = &values[i]
}
if err := rows.Scan(scanArgs...); err != nil {
return nil, fmt.Errorf("row scan error: %w", err)
}
row := make(map[string]interface{}, len(columns))
for i, col := range columns {
switch v := values[i].(type) {
case nil:
row[col] = nil
case []byte:
row[col] = string(v)
case time.Time:
row[col] = v.Format("2006-01-02")
default:
row[col] = v
}
}
cari8 := strings.TrimSpace(asString(row["Cari8"]))
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)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows error: %w", err)
}
cariDetailMap, err := loadAgingMasterCariDetailMap(context.Background(), cari8Set)
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]
cari8 := strings.TrimSpace(asString(row["Cari8"]))
curr := strings.ToUpper(strings.TrimSpace(asString(row["DocCurrencyCode"])))
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)
row["CariDetay"] = cariDetailMap[cari8]
row["UsdTutar"] = round2(usdTutar)
row["TryTutar"] = round2(tryTutar)
row["GunKur"] = round6(gunKur)
}
return result, nil
}
func resolveUseTypes(parislemler []string) (int, int) {
if len(parislemler) == 0 {
return 1, 0
}
useType2 := 0
useType3 := 0
for _, v := range parislemler {
switch strings.TrimSpace(v) {
case "2":
useType2 = 1
case "3":
useType3 = 1
}
}
if useType2 == 0 && useType3 == 0 {
return 1, 0
}
return useType2, useType3
}
func loadAgingMasterCariDetailMap(ctx context.Context, cari8Set map[string]struct{}) (map[string]string, error) {
if len(cari8Set) == 0 {
return map[string]string{}, nil
}
query := fmt.Sprintf(`
WITH BaseCari AS (
SELECT
CurrAccCode,
CurrAccTypeCode,
MasterCari = LEFT(CurrAccCode, 8),
rn = ROW_NUMBER() OVER (
PARTITION BY LEFT(CurrAccCode, 8)
ORDER BY CurrAccCode
)
FROM cdCurrAcc WITH (NOLOCK)
WHERE CurrAccTypeCode IN (1,3)
AND LEFT(CurrAccCode, 8) IN (%s)
)
SELECT
b.MasterCari,
CariDetay = ISNULL(d.CurrAccDescription, '')
FROM BaseCari b
LEFT JOIN cdCurrAccDesc d WITH (NOLOCK)
ON d.CurrAccTypeCode = b.CurrAccTypeCode
AND d.CurrAccCode = b.CurrAccCode
AND d.LangCode = 'TR'
WHERE b.rn = 1;
`, quotedInList(cari8Set))
rows, err := db.MssqlDB.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("aging cari detail query error: %w", err)
}
defer rows.Close()
out := make(map[string]string, len(cari8Set))
for rows.Next() {
var cari8 string
var detail sql.NullString
if err := rows.Scan(&cari8, &detail); err != nil {
return nil, err
}
out[strings.TrimSpace(cari8)] = strings.TrimSpace(detail.String)
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func asString(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 asFloat64(v interface{}) float64 {
switch x := v.(type) {
case nil:
return 0
case float64:
return x
case float32:
return float64(x)
case int64:
return float64(x)
case int32:
return float64(x)
case int:
return float64(x)
case string:
return parseNumberString(x)
case []byte:
return parseNumberString(string(x))
default:
return parseNumberString(fmt.Sprint(x))
}
}
func parseNumberString(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
}
func round2(v float64) float64 {
return math.Round(v*100) / 100
}
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

@@ -0,0 +1,145 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"database/sql"
"fmt"
"net/http"
"strings"
"time"
"github.com/xuri/excelize/v2"
)
func ExportCustomerBalanceExcelHandler(_ *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("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")),
Piyasa: strings.TrimSpace(r.URL.Query().Get("piyasa")),
Temsilci: strings.TrimSpace(r.URL.Query().Get("temsilci")),
RiskDurumu: strings.TrimSpace(r.URL.Query().Get("risk_durumu")),
IslemTipi: strings.TrimSpace(r.URL.Query().Get("islem_tipi")),
Ulke: strings.TrimSpace(r.URL.Query().Get("ulke")),
Il: strings.TrimSpace(r.URL.Query().Get("il")),
Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")),
}
excludeZero12 := parseBoolQuery(r.URL.Query().Get("exclude_zero_12"))
excludeZero13 := parseBoolQuery(r.URL.Query().Get("exclude_zero_13"))
rows, err := queries.GetCustomerBalanceList(r.Context(), params)
if err != nil {
http.Error(w, "db error: "+err.Error(), http.StatusInternalServerError)
return
}
rows = filterCustomerBalanceRowsForPDF(rows, excludeZero12, excludeZero13)
summaries, _ := buildCustomerBalancePDFData(rows)
f := excelize.NewFile()
sheet := "CariBakiye"
f.SetSheetName("Sheet1", sheet)
headers := []string{
"Ana Cari Kodu",
"Ana Cari Detay",
"Piyasa",
"Temsilci",
"Risk Durumu",
"1_2 Bakiye Pr.Br",
"1_3 Bakiye Pr.Br",
"1_2 USD Bakiye",
"1_2 TRY Bakiye",
"1_3 USD Bakiye",
"1_3 TRY Bakiye",
}
for i, h := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
f.SetCellValue(sheet, cell, h)
}
var totalUSD12, totalTRY12, totalUSD13, totalTRY13 float64
totalPrBr12 := map[string]float64{}
totalPrBr13 := map[string]float64{}
for _, s := range summaries {
totalUSD12 += s.USDBakiye12
totalTRY12 += s.TLBakiye12
totalUSD13 += s.USDBakiye13
totalTRY13 += s.TLBakiye13
for k, v := range s.Bakiye12Map {
totalPrBr12[k] += v
}
for k, v := range s.Bakiye13Map {
totalPrBr13[k] += v
}
}
f.SetSheetRow(sheet, "A2", &[]any{
"TOPLAM",
"",
"",
"",
"",
formatCurrencyMapPDF(totalPrBr12),
formatCurrencyMapPDF(totalPrBr13),
totalUSD12,
totalTRY12,
totalUSD13,
totalTRY13,
})
rowNo := 3
for _, s := range summaries {
f.SetSheetRow(sheet, fmt.Sprintf("A%d", rowNo), &[]any{
s.AnaCariKodu,
s.AnaCariAdi,
s.Piyasa,
s.Temsilci,
s.RiskDurumu,
formatCurrencyMapPDF(s.Bakiye12Map),
formatCurrencyMapPDF(s.Bakiye13Map),
s.USDBakiye12,
s.TLBakiye12,
s.USDBakiye13,
s.TLBakiye13,
})
rowNo++
}
_ = f.SetColWidth(sheet, "A", "A", 16)
_ = f.SetColWidth(sheet, "B", "B", 34)
_ = f.SetColWidth(sheet, "C", "E", 18)
_ = f.SetColWidth(sheet, "F", "G", 34)
_ = f.SetColWidth(sheet, "H", "K", 18)
buf, err := f.WriteToBuffer()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
filename := fmt.Sprintf("cari_bakiye_listesi_%s.xlsx", time.Now().Format("20060102_150405"))
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
w.Header().Set("Content-Length", fmt.Sprint(len(buf.Bytes())))
w.WriteHeader(http.StatusOK)
_, _ = w.Write(buf.Bytes())
}
}

View File

@@ -33,6 +33,8 @@ func GetCustomerBalanceListHandler(w http.ResponseWriter, r *http.Request) {
RiskDurumu: strings.TrimSpace(r.URL.Query().Get("risk_durumu")),
IslemTipi: strings.TrimSpace(r.URL.Query().Get("islem_tipi")),
Ulke: strings.TrimSpace(r.URL.Query().Get("ulke")),
Il: strings.TrimSpace(r.URL.Query().Get("il")),
Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")),
}
rows, err := queries.GetCustomerBalanceList(r.Context(), params)

View File

@@ -0,0 +1,461 @@
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 balanceSummaryPDF struct {
AnaCariKodu string
AnaCariAdi string
Piyasa string
Temsilci string
RiskDurumu string
Bakiye12Map map[string]float64
Bakiye13Map map[string]float64
USDBakiye12 float64
TLBakiye12 float64
USDBakiye13 float64
TLBakiye13 float64
}
func ExportCustomerBalancePDFHandler(_ *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("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")),
Piyasa: strings.TrimSpace(r.URL.Query().Get("piyasa")),
Temsilci: strings.TrimSpace(r.URL.Query().Get("temsilci")),
RiskDurumu: strings.TrimSpace(r.URL.Query().Get("risk_durumu")),
IslemTipi: strings.TrimSpace(r.URL.Query().Get("islem_tipi")),
Ulke: strings.TrimSpace(r.URL.Query().Get("ulke")),
Il: strings.TrimSpace(r.URL.Query().Get("il")),
Ilce: strings.TrimSpace(r.URL.Query().Get("ilce")),
}
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"))
rows, err := queries.GetCustomerBalanceList(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, 12)
if err := registerDejavuFonts(pdf, "dejavu"); err != nil {
http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError)
return
}
drawCustomerBalancePDF(
pdf,
selectedDate,
params.CariSearch,
detailed,
summaries,
detailsByMaster,
)
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
}
filename := "customer-balance-summary.pdf"
if detailed {
filename = "customer-balance-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 parseBoolQuery(v string) bool {
switch strings.ToLower(strings.TrimSpace(v)) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
func filterCustomerBalanceRowsForPDF(rows []models.CustomerBalanceListRow, excludeZero12, excludeZero13 bool) []models.CustomerBalanceListRow {
out := make([]models.CustomerBalanceListRow, 0, len(rows))
for _, row := range rows {
if excludeZero12 && row.Bakiye12 == 0 {
continue
}
if excludeZero13 && row.Bakiye13 == 0 {
continue
}
out = append(out, row)
}
return out
}
func buildCustomerBalancePDFData(rows []models.CustomerBalanceListRow) ([]balanceSummaryPDF, map[string][]models.CustomerBalanceListRow) {
summaryMap := make(map[string]*balanceSummaryPDF)
detailsByMaster := make(map[string][]models.CustomerBalanceListRow)
for _, row := range rows {
master := strings.TrimSpace(row.AnaCariKodu)
if master == "" {
master = strings.TrimSpace(row.CariKodu)
}
if master == "" {
continue
}
s := summaryMap[master]
if s == nil {
s = &balanceSummaryPDF{
AnaCariKodu: master,
AnaCariAdi: strings.TrimSpace(row.AnaCariAdi),
Piyasa: strings.TrimSpace(row.Piyasa),
Temsilci: strings.TrimSpace(row.Temsilci),
RiskDurumu: strings.TrimSpace(row.RiskDurumu),
Bakiye12Map: map[string]float64{},
Bakiye13Map: map[string]float64{},
}
summaryMap[master] = s
}
if s.AnaCariAdi == "" && strings.TrimSpace(row.AnaCariAdi) != "" {
s.AnaCariAdi = strings.TrimSpace(row.AnaCariAdi)
}
if s.Piyasa == "" && strings.TrimSpace(row.Piyasa) != "" {
s.Piyasa = strings.TrimSpace(row.Piyasa)
}
if s.Temsilci == "" && strings.TrimSpace(row.Temsilci) != "" {
s.Temsilci = strings.TrimSpace(row.Temsilci)
}
if s.RiskDurumu == "" && strings.TrimSpace(row.RiskDurumu) != "" {
s.RiskDurumu = strings.TrimSpace(row.RiskDurumu)
}
curr := strings.ToUpper(strings.TrimSpace(row.CariDoviz))
if curr == "" {
curr = "N/A"
}
s.Bakiye12Map[curr] += row.Bakiye12
s.Bakiye13Map[curr] += row.Bakiye13
s.USDBakiye12 += row.USDBakiye12
s.TLBakiye12 += row.TLBakiye12
s.USDBakiye13 += row.USDBakiye13
s.TLBakiye13 += row.TLBakiye13
detailsByMaster[master] = append(detailsByMaster[master], row)
}
masters := make([]string, 0, len(summaryMap))
for m := range summaryMap {
masters = append(masters, m)
}
sort.Strings(masters)
summaries := make([]balanceSummaryPDF, 0, len(masters))
for _, m := range masters {
summaries = append(summaries, *summaryMap[m])
d := detailsByMaster[m]
sort.SliceStable(d, func(i, j int) bool {
if d[i].CariKodu == d[j].CariKodu {
if d[i].CariDoviz == d[j].CariDoviz {
si, _ := strconv.Atoi(d[i].Sirket)
sj, _ := strconv.Atoi(d[j].Sirket)
return si < sj
}
return d[i].CariDoviz < d[j].CariDoviz
}
return d[i].CariKodu < d[j].CariKodu
})
detailsByMaster[m] = d
}
return summaries, detailsByMaster
}
func drawCustomerBalancePDF(
pdf *gofpdf.Fpdf,
selectedDate string,
searchText string,
detailed bool,
summaries []balanceSummaryPDF,
detailsByMaster map[string][]models.CustomerBalanceListRow,
) {
pageW, _ := pdf.GetPageSize()
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"}
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"}
detailW := normalizeWidths([]float64{26, 46, 10, 20, 10, 24, 24, 15, 15, 15, 15}, tableW)
header := func() {
pdf.AddPage()
pdf.SetFont("dejavu", "B", 15)
pdf.SetTextColor(149, 113, 22)
pdf.SetXY(marginL, marginT)
pdf.CellFormat(120, 7, "Cari Bakiye Listesi", "", 0, "L", false, 0, "")
pdf.SetFont("dejavu", "", 9)
pdf.SetTextColor(20, 20, 20)
pdf.SetXY(pageW-marginR-80, marginT+1)
pdf.CellFormat(80, 5, "Tarih: "+selectedDate, "", 0, "R", false, 0, "")
mode := "Detaysiz"
if detailed {
mode = "Detayli"
}
pdf.SetXY(pageW-marginR-80, marginT+6)
pdf.CellFormat(80, 5, "Mod: "+mode, "", 0, "R", false, 0, "")
if strings.TrimSpace(searchText) != "" {
pdf.SetXY(marginL, marginT+8)
pdf.CellFormat(tableW, 5, "Arama: "+searchText, "", 0, "L", false, 0, "")
}
pdf.SetDrawColor(149, 113, 22)
pdf.Line(marginL, marginT+14, pageW-marginR, marginT+14)
pdf.SetDrawColor(210, 210, 210)
pdf.SetY(marginT + 17)
}
needPage := func(needH float64) bool {
return pdf.GetY()+needH+marginB > 210.0
}
drawSummaryHeader := func() {
pdf.SetFont("dejavu", "B", 7.5)
pdf.SetFillColor(149, 113, 22)
pdf.SetTextColor(255, 255, 255)
y := pdf.GetY()
x := marginL
for i, c := range summaryCols {
pdf.Rect(x, y, summaryW[i], 7, "DF")
pdf.SetXY(x+1, y+1.2)
pdf.CellFormat(summaryW[i]-2, 4.6, c, "", 0, "C", false, 0, "")
x += summaryW[i]
}
pdf.SetY(y + 7)
}
drawDetailHeader := func() {
pdf.SetFont("dejavu", "B", 7.2)
pdf.SetFillColor(149, 113, 22)
pdf.SetTextColor(255, 255, 255)
y := pdf.GetY()
x := marginL
for i, c := range detailCols {
pdf.Rect(x, y, detailW[i], 6, "DF")
pdf.SetXY(x+1, y+1)
pdf.CellFormat(detailW[i]-2, 4, c, "", 0, "C", false, 0, "")
x += detailW[i]
}
pdf.SetY(y + 6)
}
header()
drawSummaryHeader()
pdf.SetFont("dejavu", "", 7.2)
pdf.SetTextColor(20, 20, 20)
for _, s := range summaries {
if needPage(6.2) {
header()
drawSummaryHeader()
}
row := []string{
s.AnaCariKodu,
s.AnaCariAdi,
s.Piyasa,
s.Temsilci,
s.RiskDurumu,
formatCurrencyMapPDF(s.Bakiye12Map),
formatCurrencyMapPDF(s.Bakiye13Map),
formatMoneyPDF(s.USDBakiye12),
formatMoneyPDF(s.TLBakiye12),
formatMoneyPDF(s.USDBakiye13),
formatMoneyPDF(s.TLBakiye13),
}
y := pdf.GetY()
x := marginL
for i, v := range row {
pdf.Rect(x, y, summaryW[i], 6.2, "")
align := "L"
if i >= 7 {
align = "R"
}
pdf.SetXY(x+1, y+1)
pdf.CellFormat(summaryW[i]-2, 4.2, v, "", 0, align, false, 0, "")
x += summaryW[i]
}
pdf.SetY(y + 6.2)
}
if !detailed {
return
}
pdf.Ln(1.8)
for _, s := range summaries {
rows := detailsByMaster[s.AnaCariKodu]
if len(rows) == 0 {
continue
}
if needPage(12.4) {
header()
}
pdf.SetFont("dejavu", "B", 8)
pdf.SetFillColor(218, 193, 151)
pdf.SetTextColor(20, 20, 20)
y := pdf.GetY()
pdf.Rect(marginL, y, tableW, 6.2, "DF")
pdf.SetXY(marginL+1.5, y+1)
pdf.CellFormat(tableW-3, 4.2, "Detay: "+s.AnaCariKodu, "", 0, "L", false, 0, "")
pdf.SetY(y + 6.2)
drawDetailHeader()
pdf.SetFont("dejavu", "", 7)
pdf.SetTextColor(40, 40, 40)
for _, r := range rows {
if needPage(5.8) {
header()
pdf.SetFont("dejavu", "B", 8)
pdf.SetFillColor(218, 193, 151)
pdf.SetTextColor(20, 20, 20)
y := pdf.GetY()
pdf.Rect(marginL, y, tableW, 6.2, "DF")
pdf.SetXY(marginL+1.5, y+1)
pdf.CellFormat(tableW-3, 4.2, "Detay: "+s.AnaCariKodu, "", 0, "L", false, 0, "")
pdf.SetY(y + 6.2)
drawDetailHeader()
pdf.SetFont("dejavu", "", 7)
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], 5.8, "")
align := "L"
if i >= 5 {
align = "R"
}
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 + 5.8)
}
pdf.Ln(1.2)
}
}
func formatCurrencyMapPDF(m map[string]float64) string {
if len(m) == 0 {
return "-"
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
if m[k] == 0 {
continue
}
parts = append(parts, k+": "+formatMoneyPDF(m[k]))
}
if len(parts) == 0 {
return "-"
}
return strings.Join(parts, " | ")
}
func formatMoneyPDF(v float64) string {
s := fmt.Sprintf("%.2f", v)
parts := strings.SplitN(s, ".", 2)
intPart, decPart := parts[0], "00"
if len(parts) == 2 {
decPart = parts[1]
}
sign := ""
if strings.HasPrefix(intPart, "-") {
sign = "-"
intPart = strings.TrimPrefix(intPart, "-")
}
var out []string
for len(intPart) > 3 {
out = append([]string{intPart[len(intPart)-3:]}, out...)
intPart = intPart[:len(intPart)-3]
}
if intPart != "" {
out = append([]string{intPart}, out...)
}
return sign + strings.Join(out, ".") + "," + decPart
}

View File

@@ -0,0 +1,41 @@
package routes
import (
"bssapp-backend/auth"
"bssapp-backend/models"
"bssapp-backend/queries"
"encoding/json"
"net/http"
"strings"
)
// GET /api/finance/account-aging-statement
func GetStatementAgingHandler(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, "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)
}
}

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
}