diff --git a/svc/main.go b/svc/main.go index 42f4761..a2dcb11 100644 --- a/svc/main.go +++ b/svc/main.go @@ -453,6 +453,11 @@ func InitRoutes(pgDB *sql.DB, mssql *sql.DB, ml *mailer.GraphMailer) *mux.Router "finance", "export", wrapV3(routes.ExportStatementAgingPDFHandler(mssql)), ) + bindV3(r, pgDB, + "/api/finance/account-aging-statement/export-screen-pdf", "GET", + "finance", "export", + wrapV3(routes.ExportStatementAgingScreenPDFHandler(mssql)), + ) bindV3(r, pgDB, "/api/finance/account-aging-statement/export-excel", "GET", "finance", "export", diff --git a/svc/routes/customer_balance_pdf.go b/svc/routes/customer_balance_pdf.go index e0087ad..3ddb075 100644 --- a/svc/routes/customer_balance_pdf.go +++ b/svc/routes/customer_balance_pdf.go @@ -85,6 +85,7 @@ func ExportCustomerBalancePDFHandler(_ *sql.DB) http.HandlerFunc { selectedDate, params.CariSearch, detailed, + "Cari Bakiye Listesi", summaries, detailsByMaster, ) @@ -233,6 +234,7 @@ func drawCustomerBalancePDF( selectedDate string, searchText string, detailed bool, + reportTitle string, summaries []balanceSummaryPDF, detailsByMaster map[string][]models.CustomerBalanceListRow, ) { @@ -251,7 +253,11 @@ func drawCustomerBalancePDF( 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, "") + title := strings.TrimSpace(reportTitle) + if title == "" { + title = "Cari Bakiye Listesi" + } + pdf.CellFormat(120, 7, title, "", 0, "L", false, 0, "") pdf.SetFont("dejavu", "", 9) pdf.SetTextColor(20, 20, 20) diff --git a/svc/routes/statement_aging_pdf.go b/svc/routes/statement_aging_pdf.go index fc04c67..780d4ad 100644 --- a/svc/routes/statement_aging_pdf.go +++ b/svc/routes/statement_aging_pdf.go @@ -77,6 +77,7 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc { selectedDate, params.CariSearch, detailed, + "Cari Yaslandirmali Ekstre", summaries, detailsByMaster, ) diff --git a/svc/routes/statement_aging_screen_pdf.go b/svc/routes/statement_aging_screen_pdf.go new file mode 100644 index 0000000..d04e48e --- /dev/null +++ b/svc/routes/statement_aging_screen_pdf.go @@ -0,0 +1,302 @@ +package routes + +import ( + "bssapp-backend/auth" + "bssapp-backend/models" + "bssapp-backend/queries" + "bytes" + "database/sql" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/jung-kurt/gofpdf" +) + +type agingScreenPDFRow struct { + Cari8 string + CariDetay string + FaturaCari string + OdemeCari string + FaturaRef string + OdemeRef string + FaturaTarihi string + OdemeTarihi string + OdemeDocDate string + EslesenTutar float64 + UsdTutar float64 + CurrencyTryRate float64 + GunSayisi float64 + GunSayisiDocDate float64 + Aciklama string + DocCurrencyCode string +} + +func ExportStatementAgingScreenPDFHandler(_ *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + claims, ok := auth.GetClaimsFromContext(r.Context()) + if !ok || claims == nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + selectedDate := strings.TrimSpace(r.URL.Query().Get("enddate")) + if selectedDate == "" { + selectedDate = strings.TrimSpace(r.URL.Query().Get("selected_date")) + } + if selectedDate == "" { + selectedDate = time.Now().Format("2006-01-02") + } + + params := models.StatementAgingParams{ + AccountCode: strings.TrimSpace(r.URL.Query().Get("accountcode")), + EndDate: selectedDate, + Parislemler: r.URL.Query()["parislemler"], + } + + if err := queries.RebuildStatementAgingCache(r.Context()); err != nil { + http.Error(w, "Error rebuilding aging cache: "+err.Error(), http.StatusInternalServerError) + return + } + + rawRows, err := queries.GetStatementAging(params) + if err != nil { + http.Error(w, "Error fetching aging statement: "+err.Error(), http.StatusInternalServerError) + return + } + + rows := make([]agingScreenPDFRow, 0, len(rawRows)) + for _, r := range rawRows { + rows = append(rows, agingScreenPDFRow{ + Cari8: pickString(r, "Cari8", "cari8"), + CariDetay: pickString(r, "CariDetay", "cari_detay"), + FaturaCari: pickString(r, "FaturaCari", "fatura_cari"), + OdemeCari: pickString(r, "OdemeCari", "odeme_cari"), + FaturaRef: pickString(r, "FaturaRef", "fatura_ref"), + OdemeRef: pickString(r, "OdemeRef", "odeme_ref"), + FaturaTarihi: pickString(r, "FaturaTarihi", "fatura_tarihi"), + OdemeTarihi: pickString(r, "OdemeTarihi", "odeme_tarihi"), + OdemeDocDate: pickString(r, "OdemeDocDate", "odeme_doc_date"), + EslesenTutar: pickFloat(r, "EslesenTutar", "eslesen_tutar"), + UsdTutar: pickFloat(r, "UsdTutar", "usd_tutar"), + CurrencyTryRate: pickFloat(r, "CurrencyTryRate", "currency_try_rate"), + GunSayisi: pickFloat(r, "GunSayisi", "gun_sayisi"), + GunSayisiDocDate: pickFloat(r, "GunSayisi_DocDate", "gun_sayisi_docdate"), + Aciklama: pickString(r, "Aciklama", "aciklama"), + DocCurrencyCode: pickString(r, "DocCurrencyCode", "doc_currency_code"), + }) + } + + pdf := gofpdf.New("L", "mm", "A3", "") + pdf.SetMargins(8, 8, 8) + pdf.SetAutoPageBreak(false, 10) + if err := registerDejavuFonts(pdf, "dejavu"); err != nil { + http.Error(w, "pdf font error: "+err.Error(), http.StatusInternalServerError) + return + } + + drawStatementAgingScreenPDF(pdf, selectedDate, params.AccountCode, rows) + + if err := pdf.Error(); err != nil { + http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError) + return + } + + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/pdf") + w.Header().Set("Content-Disposition", `inline; filename="account-aging-screen.pdf"`) + _, _ = w.Write(buf.Bytes()) + } +} + +func drawStatementAgingScreenPDF(pdf *gofpdf.Fpdf, selectedDate, accountCode string, rows []agingScreenPDFRow) { + pageW, _ := pdf.GetPageSize() + marginL, marginT, marginR, marginB := 8.0, 8.0, 8.0, 10.0 + tableW := pageW - marginL - marginR + + cols := []string{ + "Ana Cari", "Ana Cari Detay", "Fatura Cari", "Odeme Cari", "Fatura Ref", "Odeme Ref", + "Fatura Tarihi", "Odeme Vade", "Odeme DocDate", "Eslesen Tutar", "USD Tutar", "Kur", + "Gun", "Gun (DocDate)", "Aciklama", "Doviz", + } + widths := normalizeWidths([]float64{ + 18, 34, 18, 18, 22, 22, + 16, 16, 18, 19, 16, 12, + 10, 13, 28, 11, + }, tableW) + + header := func() { + pdf.AddPage() + pdf.SetFont("dejavu", "B", 14) + pdf.SetTextColor(149, 113, 22) + pdf.SetXY(marginL, marginT) + pdf.CellFormat(150, 6, "Cari Yaslandirmali Ekstre", "", 0, "L", false, 0, "") + + pdf.SetFont("dejavu", "", 8.5) + pdf.SetTextColor(30, 30, 30) + pdf.SetXY(pageW-marginR-90, marginT+0.5) + pdf.CellFormat(90, 4.8, "Son Tarih: "+selectedDate, "", 0, "R", false, 0, "") + if strings.TrimSpace(accountCode) != "" { + pdf.SetXY(pageW-marginR-90, marginT+5) + pdf.CellFormat(90, 4.8, "Cari: "+accountCode, "", 0, "R", false, 0, "") + } + + pdf.SetDrawColor(149, 113, 22) + pdf.Line(marginL, marginT+10.5, pageW-marginR, marginT+10.5) + pdf.SetDrawColor(200, 200, 200) + pdf.SetY(marginT + 13) + + pdf.SetFont("dejavu", "B", 7.2) + pdf.SetTextColor(255, 255, 255) + pdf.SetFillColor(149, 113, 22) + y := pdf.GetY() + x := marginL + for i, c := range cols { + pdf.Rect(x, y, widths[i], 6.2, "DF") + pdf.SetXY(x+0.8, y+1) + pdf.CellFormat(widths[i]-1.6, 4.2, c, "", 0, "C", false, 0, "") + x += widths[i] + } + pdf.SetY(y + 6.2) + } + + needPage := func(needH float64) bool { + return pdf.GetY()+needH+marginB > 297.0 + } + + header() + pdf.SetFont("dejavu", "", 6.8) + pdf.SetTextColor(25, 25, 25) + + for _, r := range rows { + if needPage(5.5) { + header() + pdf.SetFont("dejavu", "", 6.8) + pdf.SetTextColor(25, 25, 25) + } + + line := []string{ + r.Cari8, + r.CariDetay, + r.FaturaCari, + r.OdemeCari, + r.FaturaRef, + r.OdemeRef, + r.FaturaTarihi, + r.OdemeTarihi, + r.OdemeDocDate, + formatMoneyPDF(r.EslesenTutar), + formatMoneyPDF(r.UsdTutar), + formatMoneyPDF(r.CurrencyTryRate), + fmt.Sprintf("%.0f", r.GunSayisi), + fmt.Sprintf("%.0f", r.GunSayisiDocDate), + r.Aciklama, + r.DocCurrencyCode, + } + + y := pdf.GetY() + x := marginL + for i, v := range line { + pdf.Rect(x, y, widths[i], 5.5, "") + align := "L" + if i >= 9 && i <= 11 { + align = "R" + } + if i == 12 || i == 13 { + align = "C" + } + pdf.SetXY(x+0.8, y+0.8) + pdf.CellFormat(widths[i]-1.6, 3.8, v, "", 0, align, false, 0, "") + x += widths[i] + } + pdf.SetY(y + 5.5) + } +} + +func pickString(m map[string]interface{}, keys ...string) string { + for _, k := range keys { + if v, ok := m[k]; ok { + return strings.TrimSpace(toStringValue(v)) + } + } + return "" +} + +func pickFloat(m map[string]interface{}, keys ...string) float64 { + for _, k := range keys { + if v, ok := m[k]; ok { + return toFloat64Value(v) + } + } + return 0 +} + +func toStringValue(v interface{}) string { + switch x := v.(type) { + case nil: + return "" + case string: + return x + case []byte: + return string(x) + default: + return fmt.Sprint(x) + } +} + +func toFloat64Value(v interface{}) float64 { + switch x := v.(type) { + case nil: + return 0 + case float64: + return x + case float32: + return float64(x) + case int: + return float64(x) + case int64: + return float64(x) + case int32: + return float64(x) + case string: + return parseFloatValue(x) + case []byte: + return parseFloatValue(string(x)) + default: + return parseFloatValue(fmt.Sprint(x)) + } +} + +func parseFloatValue(s string) float64 { + s = strings.TrimSpace(s) + if s == "" { + return 0 + } + + hasComma := strings.Contains(s, ",") + hasDot := strings.Contains(s, ".") + if hasComma && hasDot { + if strings.LastIndex(s, ",") > strings.LastIndex(s, ".") { + s = strings.ReplaceAll(s, ".", "") + s = strings.Replace(s, ",", ".", 1) + } else { + s = strings.ReplaceAll(s, ",", "") + } + } else if hasComma { + s = strings.ReplaceAll(s, ".", "") + s = strings.Replace(s, ",", ".", 1) + } + + n, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0 + } + return n +} diff --git a/ui/src/pages/AccountAgingStatement.vue b/ui/src/pages/AccountAgingStatement.vue index 2b882f9..9b6fa95 100644 --- a/ui/src/pages/AccountAgingStatement.vue +++ b/ui/src/pages/AccountAgingStatement.vue @@ -64,33 +64,13 @@ :label="filtersCollapsed ? 'Filtreleri Genişlet' : 'Filtreleri Daralt'" @click="toggleFiltersCollapsed" /> - - - - - Detaylı Cari Yaşlandırmalı Bakiye Listesi Yazdır - - - - - Detaysız Cari Yaşlandırmalı Bakiye Listesi Yazdır - - - - - Toplam USD
Normal
Açık Kalem
+
Kur
Ort. Gün
Ort. Gün (DocDate)
@@ -180,6 +161,7 @@
{{ formatAmount(currRow.toplam_usd) }}
{{ formatAmount(currRow.normal_tutar) }}
{{ formatAmount(currRow.acik_kalem_tutar) }}
+
{{ formatAmount(currRow.kur) }}
{{ formatAmount(currRow.ortalama_gun, 0) }}
{{ formatAmount(currRow.ortalama_gun_docdate, 0) }}
@@ -204,6 +186,9 @@ + @@ -267,7 +252,8 @@ const masterColumns = [ { name: 'normal_usd', label: 'Normal USD', field: 'normal_usd', align: 'right', sortable: true }, { name: 'acik_kalem_usd', label: 'Açık Kalem USD', field: 'acik_kalem_usd', align: 'right', sortable: true }, { name: 'ortalama_gun', label: 'Ort. Gün', field: 'ortalama_gun', align: 'center', sortable: true }, - { name: 'ortalama_gun_docdate', label: 'Ort. Gün (DocDate)', field: 'ortalama_gun_docdate', align: 'center', sortable: true } + { name: 'ortalama_gun_docdate', label: 'Ort. Gün (DocDate)', field: 'ortalama_gun_docdate', align: 'center', sortable: true }, + { name: 'kur', label: 'Kur', field: 'kur', align: 'right', sortable: true } ] const detailColumns = [ @@ -280,13 +266,14 @@ const detailColumns = [ { name: 'odeme_doc_date', label: 'Ödeme DocDate', field: 'odeme_doc_date', align: 'left' }, { name: 'eslesen_tutar', label: 'Eşleşen Tutar', field: 'eslesen_tutar', align: 'right' }, { name: 'usd_tutar', label: 'USD Tutar', field: 'usd_tutar', align: 'right' }, + { name: 'currency_try_rate', label: 'Kur', field: 'currency_try_rate', align: 'right' }, { name: 'gun_sayisi', label: 'Gün', field: 'gun_sayisi', align: 'center' }, { name: 'gun_sayisi_docdate', label: 'Gün (DocDate)', field: 'gun_sayisi_docdate', align: 'center' }, { name: 'aciklama', label: 'Açıklama', field: 'aciklama', align: 'left' }, { name: 'doc_currency_code', label: 'Döviz', field: 'doc_currency_code', align: 'left' } ] -const masterNumericCols = ['satir_sayisi', 'toplam_usd', 'normal_usd', 'acik_kalem_usd', 'ortalama_gun', 'ortalama_gun_docdate'] +const masterNumericCols = ['satir_sayisi', 'toplam_usd', 'normal_usd', 'acik_kalem_usd', 'ortalama_gun', 'ortalama_gun_docdate', 'kur'] const masterCenteredCols = ['ortalama_gun', 'ortalama_gun_docdate'] function normalizeText(str) { @@ -401,20 +388,19 @@ function toggleFiltersCollapsed() { filtersCollapsed.value = !filtersCollapsed.value } -function buildExportParams(detailed = false) { +function buildExportParams() { return { accountcode: String(selectedCari.value || '').trim(), cari_search: String(selectedCari.value || '').trim(), enddate: dateTo.value, selected_date: dateTo.value, parislemler: selectedMonType.value, - detailed: detailed ? '1' : '0', exclude_zero_12: '0', exclude_zero_13: '0' } } -async function downloadAgingBalancePDF(detailed) { +async function downloadAgingScreenPDF() { if (!canExportFinance.value) { $q.notify({ type: 'negative', message: 'PDF export yetkiniz yok', position: 'top-right' }) return @@ -425,7 +411,7 @@ async function downloadAgingBalancePDF(detailed) { } try { - const blob = await download('/finance/account-aging-statement/export-pdf', buildExportParams(detailed)) + const blob = await download('/finance/account-aging-statement/export-screen-pdf', buildExportParams()) const pdfUrl = window.URL.createObjectURL(new Blob([blob], { type: 'application/pdf' })) window.open(pdfUrl, '_blank') } catch (err) { @@ -438,37 +424,6 @@ async function downloadAgingBalancePDF(detailed) { } } -async function downloadAgingBalanceExcel() { - if (!canExportFinance.value) { - $q.notify({ type: 'negative', message: 'Excel export yetkiniz yok', position: 'top-right' }) - return - } - if (!selectedCari.value || !dateTo.value) { - $q.notify({ type: 'warning', message: 'Önce cari ve son tarih seçiniz.', position: 'top-right' }) - return - } - - try { - const file = await download('/finance/account-aging-statement/export-excel', buildExportParams(false)) - const blob = new Blob([file], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) - const url = window.URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = 'cari_yaslandirmali_bakiye_listesi.xlsx' - document.body.appendChild(a) - a.click() - a.remove() - window.URL.revokeObjectURL(url) - } catch (err) { - const detail = await extractApiErrorDetail(err?.original || err) - $q.notify({ - type: 'negative', - message: detail || 'Excel oluşturulamadı', - position: 'top-right' - }) - } -} - function formatAmount(value, fraction = 2) { const n = Number(value || 0) return new Intl.NumberFormat('tr-TR', { @@ -586,7 +541,7 @@ function formatAmount(value, fraction = 2) { top: 36px; z-index: 26; display: grid; - grid-template-columns: 48px 100px 80px 1.2fr 1.2fr 1.1fr 1.1fr 110px 140px; + grid-template-columns: 48px 100px 80px 1.2fr 1.2fr 1.1fr 1.1fr 100px 110px 140px; align-items: center; gap: 0; background: var(--q-secondary); @@ -601,7 +556,7 @@ function formatAmount(value, fraction = 2) { top: 72px; z-index: 24; display: grid; - grid-template-columns: 48px 100px 80px 1.2fr 1.2fr 1.1fr 1.1fr 110px 140px; + grid-template-columns: 48px 100px 80px 1.2fr 1.2fr 1.1fr 1.1fr 100px 110px 140px; align-items: center; gap: 0; background: #4c5f7a; @@ -686,7 +641,7 @@ function formatAmount(value, fraction = 2) { } .currency-group-header { - grid-template-columns: 44px 90px 70px 1.1fr 1.1fr 1fr 1fr 90px 120px; + grid-template-columns: 44px 90px 70px 1.1fr 1.1fr 1fr 1fr 84px 90px 120px; } } diff --git a/ui/src/stores/statementAgingStore.js b/ui/src/stores/statementAgingStore.js index 959a3fc..093b874 100644 --- a/ui/src/stores/statementAgingStore.js +++ b/ui/src/stores/statementAgingStore.js @@ -61,9 +61,11 @@ export const useStatementAgingStore = defineStore('statementAging', { cari8: masterKey, cari_detay: String(row?.cari_detay || '').trim(), satir_sayisi: 0, + toplam_tutar: 0, toplam_usd: 0, normal_usd: 0, acik_kalem_usd: 0, + kur: 0, weighted_gun_sum: 0, weighted_gun_doc_sum: 0, weighted_base: 0, @@ -84,6 +86,7 @@ export const useStatementAgingStore = defineStore('statementAging', { toplam_usd: 0, normal_tutar: 0, acik_kalem_tutar: 0, + kur: 0, weighted_gun_sum: 0, weighted_gun_doc_sum: 0, weighted_base: 0, @@ -96,6 +99,7 @@ export const useStatementAgingStore = defineStore('statementAging', { const c = currencyMap[currencyKey] m.satir_sayisi += 1 + m.toplam_tutar += tutar m.toplam_usd += usd if (aciklama === 'ACIKKALEM') { m.acik_kalem_usd += usd @@ -129,6 +133,7 @@ export const useStatementAgingStore = defineStore('statementAging', { this.masterRows = Object.values(masterMap) .map((m) => ({ ...m, + kur: Math.abs(m.toplam_usd) > 0 ? (m.toplam_tutar / m.toplam_usd) : 0, ortalama_gun: m.weighted_base > 0 ? (m.weighted_gun_sum / m.weighted_base) : 0, ortalama_gun_docdate: m.weighted_base > 0 ? (m.weighted_gun_doc_sum / m.weighted_base) : 0 })) @@ -138,6 +143,7 @@ export const useStatementAgingStore = defineStore('statementAging', { for (const c of Object.values(currencyMap)) { const row = { ...c, + kur: Math.abs(c.toplam_usd) > 0 ? (c.toplam_tutar / c.toplam_usd) : 0, ortalama_gun: c.weighted_base > 0 ? (c.weighted_gun_sum / c.weighted_base) : 0, ortalama_gun_docdate: c.weighted_base > 0 ? (c.weighted_gun_doc_sum / c.weighted_base) : 0 } @@ -187,6 +193,7 @@ function normalizeRowKeys(row) { usd_tutar: Number(row.UsdTutar ?? row.usd_tutar ?? 0), gun_sayisi: Number(row.GunSayisi ?? row.gun_sayisi ?? 0), gun_sayisi_docdate: Number(row.GunSayisi_DocDate ?? row.gun_sayisi_docdate ?? 0), + currency_try_rate: Number(row.CurrencyTryRate ?? row.currency_try_rate ?? 0), aciklama: row.Aciklama ?? row.aciklama ?? null, doc_currency_code: row.DocCurrencyCode ?? row.doc_currency_code ?? null }