Merge remote-tracking branch 'origin/master'
This commit is contained in:
101
svc/main.go
101
svc/main.go
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user