Merge remote-tracking branch 'origin/master'

This commit is contained in:
M_Kececi
2026-03-06 13:59:17 +03:00
parent 46f4d15ac7
commit 807bbad0e7
6 changed files with 340 additions and 64 deletions

View File

@@ -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",

View File

@@ -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)

View File

@@ -77,6 +77,7 @@ func ExportStatementAgingPDFHandler(_ *sql.DB) http.HandlerFunc {
selectedDate,
params.CariSearch,
detailed,
"Cari Yaslandirmali Ekstre",
summaries,
detailsByMaster,
)

View File

@@ -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
}

View File

@@ -64,33 +64,13 @@
:label="filtersCollapsed ? 'Filtreleri Genişlet' : 'Filtreleri Daralt'"
@click="toggleFiltersCollapsed"
/>
<q-btn-dropdown
<q-btn
v-if="canExportFinance"
flat
color="red"
icon="picture_as_pdf"
label="Yazdır"
>
<q-list style="min-width: 240px">
<q-item clickable v-close-popup @click="downloadAgingBalancePDF(true)">
<q-item-section class="text-primary">
Detaylı Cari Yaşlandırmalı Bakiye Listesi Yazdır
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="downloadAgingBalancePDF(false)">
<q-item-section class="text-secondary">
Detaysız Cari Yaşlandırmalı Bakiye Listesi Yazdır
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
<q-btn
v-if="canExportFinance"
flat
color="green-8"
icon="table_view"
label="Excel"
@click="downloadAgingBalanceExcel"
label="PDF Yazdır"
@click="downloadAgingScreenPDF"
/>
<q-btn
flat
@@ -155,6 +135,7 @@
<div class="cgh-cell cgh-num">Toplam USD</div>
<div class="cgh-cell cgh-num">Normal</div>
<div class="cgh-cell cgh-num">ık Kalem</div>
<div class="cgh-cell cgh-num">Kur</div>
<div class="cgh-cell cgh-center">Ort. Gün</div>
<div class="cgh-cell cgh-center">Ort. Gün (DocDate)</div>
</div>
@@ -180,6 +161,7 @@
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.toplam_usd) }}</div>
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.normal_tutar) }}</div>
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.acik_kalem_tutar) }}</div>
<div class="cgh-cell cgh-num">{{ formatAmount(currRow.kur) }}</div>
<div class="cgh-cell cgh-center">{{ formatAmount(currRow.ortalama_gun, 0) }}</div>
<div class="cgh-cell cgh-center">{{ formatAmount(currRow.ortalama_gun_docdate, 0) }}</div>
</div>
@@ -204,6 +186,9 @@
<template #body-cell-usd_tutar="d">
<q-td :props="d" class="text-right">{{ formatAmount(d.row.usd_tutar) }}</q-td>
</template>
<template #body-cell-currency_try_rate="d">
<q-td :props="d" class="text-right">{{ formatAmount(d.row.currency_try_rate) }}</q-td>
</template>
<template #body-cell-gun_sayisi="d">
<q-td :props="d" class="text-center">{{ formatAmount(d.row.gun_sayisi, 0) }}</q-td>
</template>
@@ -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;
}
}
</style>

View File

@@ -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
}