---
title: "Mikro ERP Cari Yaşlandırma SQL Sorgusu: Bakiye Dağıtımlı Tam Rehber"
description: "Mikro ERP'de cari yaşlandırma raporunu SQL ile nasıl alırsınız? fn_Aysm fonksiyonları, bakiye dağıtım mantığı (FIFO aging), vade hesabı ve teminat..."
date: 2026-04-12
category: mikro-erp
tags: ["mikro-erp", "sql-server", "cari-yaşlandırma", "fn-aysm", "bakiye-rapor", "muhasebe"]
url: https://mikroerp.dev/blog/mikro-erp-cari-yaslandirma-sql-sorgusu/
---

## Neden Özel SQL Sorgusu Gerekiyor?

Mikro ERP'nin kendi içinde **046921** numaralı "Cari Hesap Bakiye Yaşlandırma Raporu" var. Tek bir müşteri veya küçük bir liste için yeterli. Ama şu durumlarda yetersiz kalır:

- **Tüm carilerin** yaşlandırmasını tek seferde çekmek istiyorsanız
- Sonuçları **web dashboard'a**, Excel'e veya başka bir sisteme aktaracaksanız
- Birden fazla **şube veritabanını** tek bir raporda birleştirmek istiyorsanız
- **Otomatik** günlük/haftalık rapor oluşturmak istiyorsanız

Bu yazıda, Mikro'nun kendi `fn_Aysm` fonksiyonlarını kullanan, **bakiye dağıtımlı** (FIFO aging) profesyonel bir yaşlandırma sorgusu paylaşıyoruz.

## İnternetteki Basit Yaklaşımın Sorunu

İnternette bulacağınız çoğu sorgu şuna benzer:

```sql
-- ❌ Basit ama YANLIŞ yaklaşım
SELECT 
    cari_kodu,
    SUM(CASE WHEN DATEDIFF(day, vade_tarihi, GETDATE()) BETWEEN 0 AND 30 
        THEN borc - alacak ELSE 0 END) AS [0-30 Gün],
    SUM(CASE WHEN DATEDIFF(day, vade_tarihi, GETDATE()) BETWEEN 31 AND 60 
        THEN borc - alacak ELSE 0 END) AS [31-60 Gün]
FROM CARI_HESAP_HAREKETLERI
GROUP BY cari_kodu
```

Bu yaklaşımın **3 kritik sorunu** var:

1. **Kısmi ödemeleri hesaba katmaz** — Müşteri 100.000 TL'lik faturanın 60.000 TL'sini ödediyse, kalan 40.000 TL hangi döneme ait?
2. **Mikro'nun bakiye hesaplama mantığını kullanmaz** — Döviz kuru, KDV, tevkifat, masraf gibi kalemleri atlar
3. **Bakiye dağıtımı yapmaz** — Toplam borcu en eski faturalardan başlayarak dağıtmaz

## Doğru Yaklaşım: Bakiye Dağıtımlı Yaşlandırma (FIFO)

Profesyonel yaşlandırma şöyle çalışır:

```
1. Carinin toplam bakiyesini hesapla (fn_Aysm ile)
2. Tüm hareketleri en yeniden en eskiye sırala
3. Bakiyeyi en yeni faturalardan başlayarak dağıt
4. Her faturanın "yaşı" evrak tarihine göre belirlenir
```

Örnek: Bir carinin 150.000 TL borcu var ve 3 faturası var:

```
Fatura 1 (Mart):   80.000 TL → Bakiyeden dağıt → 80.000 TL
Fatura 2 (Şubat):  60.000 TL → Bakiyeden dağıt → 60.000 TL
Fatura 3 (Ocak):   50.000 TL → Bakiyeden kalan → 10.000 TL
                                                  ─────────
                                          Toplam: 150.000 TL
```

Bu mantık, müşterinin **en son ödediği tutarı en eski faturadan düşer**.

## Tam SQL Sorgusu (7 Adımlı)

Aşağıda Mikro ERP veritabanlarında çalışan, production-ready bir yaşlandırma sorgusu bulunuyor. Kendi yapınıza göre **3 yeri** değiştirmeniz yeterli.

### Değiştirmeniz Gereken Yerler

```
@BakiyeTarih  → Raporun çekileceği tarih
'M.35%'       → Cari kodlarınızın prefix'i (örn: '120%', 'MRK%', '')
Ay aralıkları → Son bölümdeki tarih aralıklarını güncel aya göre ayarlayın
```

### Adım 1: Aktif Carileri Belirleme

Bakiye hesaplama fonksiyonu yavaş çalışır. Önce sadece hareket görmüş carileri süzüyoruz — böylece hiç hareketi olmayan binlerce cari için boşuna fonksiyon çalışmaz:

```sql
SET NOCOUNT ON;

DECLARE @BakiyeTarih datetime = GETDATE(); -- Rapor tarihi
DECLARE @GunTarih date = CAST(@BakiyeTarih AS date);

-- Temp tabloları temizle
IF OBJECT_ID('tempdb..#AktifCariler') IS NOT NULL DROP TABLE #AktifCariler;
IF OBJECT_ID('tempdb..#HamBakiye') IS NOT NULL DROP TABLE #HamBakiye;
IF OBJECT_ID('tempdb..#Bakiye') IS NOT NULL DROP TABLE #Bakiye;
IF OBJECT_ID('tempdb..#Kaynak') IS NOT NULL DROP TABLE #Kaynak;
IF OBJECT_ID('tempdb..#Dagitim') IS NOT NULL DROP TABLE #Dagitim;

-- Sadece hareket görmüş carileri çek
SELECT DISTINCT cha_kod AS cari_kod
INTO #AktifCariler
FROM dbo.CARI_HESAP_HAREKETLERI WITH (NOLOCK)
WHERE cha_tarihi <= @BakiyeTarih;
-- İsteğe bağlı: Belirli bir prefix ile filtrelemek için ekleyin:
-- AND cha_kod LIKE '120%'
```

> **Performans ipucu**: Eğer tüm carileri değil, belirli bir grubu çekmek istiyorsanız `WHERE cha_kod LIKE '120%'` gibi bir filtre ekleyin. Bu, sorgu süresini dramatik şekilde kısaltır.

### Adım 2: Bakiye Hesaplama (fn_Aysm Fonksiyonu)

Mikro'nun kendi bakiye hesaplama fonksiyonunu kullanıyoruz. Bu fonksiyon KDV, döviz kuru, masraf, tevkifat gibi tüm kalemleri hesaba katar:

```sql
SELECT
    a.cari_kod,
    CONVERT(decimal(18,2),
        dbo.fn_Aysm_v2_CariHesapAnaDovizBakiye(
            '', 0, a.cari_kod, '', '',
            NULL, NULL, @BakiyeTarih, 0, 0, 0, 0, 0
        )
    ) AS HamBakiye
INTO #HamBakiye
FROM #AktifCariler a;
```

> **Onemli**: `fn_Aysm_v2_CariHesapAnaDovizBakiye` fonksiyonu Mikro güncellendikçe parametre değişikliğine uğrayabilir. Fonksiyonun veritabanınızda mevcut olduğundan emin olun. Eksikse aşağıdaki [Fonksiyon Kurulumu](#fonksiyon-kodlari) bölümüne bakın.

### Adım 3: Bakiyeleri Ayrıştırma

Sıfır bakiyeli carileri eleyin ve borç/alacak tipini belirleyin:

```sql
SELECT
    cari_kod,
    HamBakiye   AS OrjinalBakiye,   -- Ekranda eksi/artı göstermek için
    ABS(HamBakiye) AS NetBakiye,    -- Dağıtım matematiği için (mutlak değer)
    CASE 
        WHEN HamBakiye > 0 THEN 0   -- Borçlu
        WHEN HamBakiye < 0 THEN 1   -- Alacaklı
        ELSE -1 
    END AS Tip
INTO #Bakiye
FROM #HamBakiye
WHERE HamBakiye <> 0;
```

### Adım 4: Hareketleri Çekme

Bakiye dağıtımı için her carinin hareketlerini, tutar hesaplamasıyla birlikte çekiyoruz. Mikro'nun meblag fonksiyonu tüm vergi ve masraf kalemlerini hesaba katar:

```sql
SELECT
    b.cari_kod AS AnaCariKod,
    b.NetBakiye,
    ch.cha_Guid,
    ch.cha_satici_kodu,
    CAST(ch.cha_tarihi AS date) AS EvrakTarihi,
    -- Vade tarihi hesabı: Mikro'da vade farklı formatlarda tutulur
    CASE
        WHEN ch.cha_vade BETWEEN 19000101 AND 20991231 THEN
            TRY_CONVERT(date, CONVERT(char(8), ch.cha_vade), 112)
        WHEN ch.cha_vade BETWEEN -36500 AND -1 THEN
            DATEADD(DAY, ABS(ch.cha_vade), CAST(ch.cha_tarihi AS date))
        ELSE NULL
    END AS VadeTarihi,
    -- Tutar hesabı (fn_Aysm meblag fonksiyonu)
    CONVERT(decimal(18,2),
        dbo.fn_Aysm_v2_CariHesapMeblag(
            ch.cha_meblag, ch.cha_aratoplam,
            ch.cha_vergi1 + ch.cha_vergi2 + ch.cha_vergi3 + ch.cha_vergi4 + 
            ch.cha_vergi5 + ch.cha_vergi6 + ch.cha_vergi7 + ch.cha_vergi8 + 
            ch.cha_vergi9 + ch.cha_vergi10 +
            ch.cha_ilave_edilecek_kdv1 + ch.cha_ilave_edilecek_kdv2 + 
            ch.cha_ilave_edilecek_kdv3 + ch.cha_ilave_edilecek_kdv4 + 
            ch.cha_ilave_edilecek_kdv5 + ch.cha_ilave_edilecek_kdv6 + 
            ch.cha_ilave_edilecek_kdv7 + ch.cha_ilave_edilecek_kdv8 + 
            ch.cha_ilave_edilecek_kdv9 + ch.cha_ilave_edilecek_kdv10,
            ch.cha_d_kur, ch.cha_altd_kur, ch.cha_karsid_kur, 
            ch.cha_cinsi, ch.cha_evrak_tip, 0,
            ch.cha_cari_cins, ch.cha_meblag_ana_doviz_icin_gecersiz_fl, 
            ch.cha_meblag_alt_doviz_icin_gecersiz_fl,
            ch.cha_meblag_orj_doviz_icin_gecersiz_fl, 
            ch.cha_ft_masraf1, ch.cha_ft_masraf2, ch.cha_ft_masraf3,
            ch.cha_ft_masraf4, ch.cha_otvtutari, ch.cha_oivtutari, 
            ch.cha_yuvarlama, ch.cha_tevkifat_toplam,
            ch.cha_stopaj, ch.cha_savsandesfonu, ch.cha_vergifon_toplam, 
            ch.cha_ticaret_turu, ch.cha_kasa_hizmet, ch.cha_kasa_hizkod
        )
    ) AS Meblag,
    ch.cha_create_date
INTO #Kaynak
FROM dbo.CARI_HESAP_HAREKETLERI ch WITH (NOLOCK)
INNER JOIN #Bakiye b
    ON (ch.cha_kod = b.cari_kod OR ch.cha_ciro_cari_kodu = b.cari_kod)
WHERE ch.cha_tarihi <= @BakiyeTarih
  AND dbo.fn_Aysm_v2_CariharBorcAlacak(
        0, ch.cha_tip, ch.cha_tpoz, ch.cha_cari_cins, ch.cha_cinsi, 0, 0, 0, 0
      ) = b.Tip
  AND ch.cha_meblag * ch.cha_d_kur > 1
  AND ch.cha_evrak_tip <> 59;
```

**Bu adım neden önemli?**

- `cha_ciro_cari_kodu` kontrolü: Cirolu çek/senet gibi işlemleri de yakalar
- `fn_Aysm_v2_CariharBorcAlacak`: Borç mu alacak mı olduğunu Mikro mantığıyla belirler
- `cha_evrak_tip <> 59`: Açılış fişlerini hariç tutar
- Vade hesabı: Mikro'da vade bazen sayısal (gün sayısı) olarak tutulur, bu CASE yapısı her iki formatı da yakalar

### Adım 5: Bakiye Dağıtımı (FIFO)

İşte yaşlandırmanın kalbi — bakiyeyi en yeniden en eskiye doğru dağıtıyoruz:

```sql
;WITH Sira AS (
    SELECT
        k.*,
        SUM(k.Meblag) OVER (
            PARTITION BY k.AnaCariKod 
            ORDER BY k.EvrakTarihi DESC, k.cha_create_date DESC, k.cha_Guid DESC
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ) AS KümülatifTutar
    FROM #Kaynak k
),
Dagitim AS (
    SELECT
        AnaCariKod,
        cha_Guid,
        cha_satici_kodu,
        EvrakTarihi,
        VadeTarihi,
        CASE
            -- Kümülatif tutar bakiyeden küçükse: tamamını al
            WHEN KümülatifTutar < NetBakiye THEN Meblag
            -- Bu satırda bakiyeyi geçtiyse: kalan farkı al
            WHEN KümülatifTutar - Meblag < NetBakiye THEN NetBakiye - (KümülatifTutar - Meblag)
            -- Bakiye zaten dolduysa: 0
            ELSE 0
        END AS DagitilanTutar
    FROM Sira
)
SELECT *
INTO #Dagitim
FROM Dagitim
WHERE DagitilanTutar > 0;
```

**Bu mantık ne yapıyor?**

Window function ile kümülatif toplam hesaplanır. Bakiye dolana kadar en yeni faturalardan başlayarak tutar dağıtılır. Bakiye dolduğunda kalan faturalar 0 alır.

### Adım 6: Teminat Bilgileri

Carinin teminat/kredi limiti bilgisini çekiyoruz:

```sql
;WITH Teminat AS (
    SELECT
        ct_carikodu,
        SUM(ISNULL(ct_tutari, 0)) AS CariLimiti
    FROM dbo.CARI_HESAP_TEMINATLARI WITH (NOLOCK)
    WHERE ISNULL(ct_iptal, 0) = 0
    GROUP BY ct_carikodu
)
```

### Adım 7: Final — Ay Bazlı Yaşlandırma Raporu

Son adımda tüm verileri birleştirip aylık dağılımı gösteriyoruz. **Tarih aralıklarını kendi raporunuzun dönemine göre güncelleyin:**

```sql
SELECT
    c.cari_kod              AS [Hesap Kodu],
    LTRIM(RTRIM(CONCAT(
        ISNULL(c.cari_unvan1,''), ' ', ISNULL(c.cari_unvan2,'')
    )))                     AS [Hesap Adı],
    c.cari_temsilci_kodu    AS [Temsilci],
    ISNULL(a.adr_il,'')     AS [İl],
    ISNULL(a.adr_ilce,'')   AS [İlçe],
    ISNULL(b.OrjinalBakiye, 0) AS [Toplam Bakiye],
    CASE WHEN ISNULL(c.cari_cari_kilitli_flg, 0) = 1 
        THEN N'Kilitli' ELSE N'Aktif' 
    END                     AS [Durum],
    ABS(ISNULL(c.cari_odemeplan_no, 0)) AS [Vade (Gün)],
    CONVERT(decimal(18,2), ISNULL(t.CariLimiti, 0)) AS [Teminat],
    
    -- Vadesi geçen toplam
    CONVERT(decimal(18,2), SUM(
        CASE WHEN d.VadeTarihi IS NOT NULL AND d.VadeTarihi < @GunTarih 
        THEN d.DagitilanTutar ELSE 0 END
    )) AS [Vadesi Geçen],

    -- Aylık dağılım (tarih aralıklarını güncelleyin)
    CONVERT(decimal(18,2), SUM(CASE 
        WHEN d.EvrakTarihi >= DATEADD(MONTH, -1, DATEFROMPARTS(YEAR(@GunTarih), MONTH(@GunTarih), 1))
         AND d.EvrakTarihi <  DATEFROMPARTS(YEAR(@GunTarih), MONTH(@GunTarih), 1)
        THEN d.DagitilanTutar ELSE 0 END)) AS [1 Ay Önce],

    CONVERT(decimal(18,2), SUM(CASE
        WHEN d.EvrakTarihi >= DATEADD(MONTH, -2, DATEFROMPARTS(YEAR(@GunTarih), MONTH(@GunTarih), 1))
         AND d.EvrakTarihi <  DATEADD(MONTH, -1, DATEFROMPARTS(YEAR(@GunTarih), MONTH(@GunTarih), 1))
        THEN d.DagitilanTutar ELSE 0 END)) AS [2 Ay Önce],

    CONVERT(decimal(18,2), SUM(CASE
        WHEN d.EvrakTarihi >= DATEADD(MONTH, -3, DATEFROMPARTS(YEAR(@GunTarih), MONTH(@GunTarih), 1))
         AND d.EvrakTarihi <  DATEADD(MONTH, -2, DATEFROMPARTS(YEAR(@GunTarih), MONTH(@GunTarih), 1))
        THEN d.DagitilanTutar ELSE 0 END)) AS [3 Ay Önce],

    CONVERT(decimal(18,2), SUM(CASE
        WHEN d.EvrakTarihi >= DATEADD(MONTH, -6, DATEFROMPARTS(YEAR(@GunTarih), MONTH(@GunTarih), 1))
         AND d.EvrakTarihi <  DATEADD(MONTH, -3, DATEFROMPARTS(YEAR(@GunTarih), MONTH(@GunTarih), 1))
        THEN d.DagitilanTutar ELSE 0 END)) AS [3-6 Ay Önce],

    CONVERT(decimal(18,2), SUM(CASE
        WHEN d.EvrakTarihi < DATEADD(MONTH, -6, DATEFROMPARTS(YEAR(@GunTarih), MONTH(@GunTarih), 1))
        THEN d.DagitilanTutar ELSE 0 END)) AS [6+ Ay Önce]

FROM dbo.CARI_HESAPLAR c WITH (NOLOCK)
INNER JOIN #Bakiye b ON b.cari_kod = c.cari_kod
LEFT JOIN dbo.CARI_HESAP_ADRESLERI a WITH (NOLOCK)
    ON a.adr_cari_kod = c.cari_kod AND a.adr_adres_no = 1
LEFT JOIN Teminat t ON t.ct_carikodu = c.cari_kod
LEFT JOIN #Dagitim d ON d.AnaCariKod = c.cari_kod
GROUP BY
    c.cari_kod, c.cari_unvan1, c.cari_unvan2,
    c.cari_temsilci_kodu, a.adr_il, a.adr_ilce,
    c.cari_cari_kilitli_flg, c.cari_odemeplan_no,
    t.CariLimiti, b.OrjinalBakiye
ORDER BY b.OrjinalBakiye DESC;
```

## Çıktı Neye Benzer?

```
Hesap Kodu │ Hesap Adı       │ Bakiye     │ Vadesi Geçen │ 1 Ay  │ 2 Ay  │ 3+ Ay
───────────┼─────────────────┼────────────┼──────────────┼───────┼───────┼───────
120.001    │ ABC Tic. Ltd.   │ -245.000   │ 180.000      │ 80K   │ 65K   │ 100K
120.045    │ XYZ İnşaat A.Ş. │ -128.500   │ 45.000       │ 50K   │ 78K   │ 0
120.012    │ Demir Yapı      │  -87.300   │ 87.300       │ 0     │ 40K   │ 47K
```

## Kullandığımız fn_Aysm Fonksiyonları

Bu sorguda 3 Mikro fonksiyonu kullanılıyor:

| Fonksiyon | Ne Yapar |
|-----------|----------|
| `fn_Aysm_v2_CariHesapAnaDovizBakiye` | Carinin toplam bakiyesini ana döviz cinsinden hesaplar |
| `fn_Aysm_v2_CariHesapMeblag` | Tek bir hareketin net tutarını hesaplar (KDV, masraf, tevkifat dahil) |
| `fn_Aysm_v2_CariharBorcAlacak` | Hareketin borç mu alacak mı olduğunu belirler |

Bu fonksiyonlar Mikro'nun kendi iç fonksiyonlarıdır ve tüm veritabanlarında bulunmayabilir. Aşağıda her birinin CREATE FUNCTION kodunu paylaşıyoruz.

> **Bağımlılık Uyarısı:** `fn_Aysm_v2_CariHesapAnaDovizBakiye` fonksiyonu içeride `fn_CariTutarlar` ve `fn_Aysm_v2_CariHesapBakiye` fonksiyonlarını çağırır. `fn_Aysm_v2_CariHesapMeblag` ise `fn_Atik_OTV_Maliyete_eklensin` fonksiyonunu çağırır. Bu bağımlı fonksiyonlar da veritabanınızda mevcut olmalıdır. Bunları ana Mikro veritabanınızdan (genellikle Merkez DB) kopyalayabilirsiniz.

### fn_Aysm_v2_CariharBorcAlacak

Hareketin borç mu alacak mı olduğunu belirler. Teminat mektubu, depozito çeki/senedi gibi özel durumları yönetir.

```sql
CREATE FUNCTION [dbo].[fn_Aysm_v2_CariharBorcAlacak] (
    @NormalKarsi AS tinyint,    -- 0:Normal 1:Karsi
    @Tip AS tinyint,            -- 0:Borc 1:Alacak
    @Poz AS tinyint,            -- 0:Acik 1:Kapali
    @Caricins AS tinyint,
    @Cinsi AS tinyint,
    @MusteriTeminatMektubu_Bakiyeyi_Etkilemesin_fl tinyint,
    @FirmaTeminatMektubu_Bakiyeyi_Etkilemesin_fl tinyint,
    @DepozitoCeki_Bakiyeyi_Etkilemesin_fl tinyint,
    @DepozitoSenedi_Bakiyeyi_Etkilemesin_fl tinyint
)
RETURNS tinyint
AS
BEGIN
    DECLARE @out AS tinyint

    IF (@NormalKarsi = 0)
    BEGIN
        IF @Caricins = 0
        BEGIN
            IF (@Cinsi = 37 AND @MusteriTeminatMektubu_Bakiyeyi_Etkilemesin_fl = 1)
            OR (@Cinsi = 38 AND @FirmaTeminatMektubu_Bakiyeyi_Etkilemesin_fl = 1)
            OR (@Cinsi = 39 AND @DepozitoCeki_Bakiyeyi_Etkilemesin_fl = 1)
            OR (@Cinsi = 40 AND @DepozitoSenedi_Bakiyeyi_Etkilemesin_fl = 1)
                SET @out = 2
            ELSE
                SET @out = @Tip
        END
        ELSE
            SET @out = @Tip
    END
    ELSE
        IF @Tip = 0 SET @out = 1
        ELSE SET @out = 0

    RETURN @out
END
```

### fn_Aysm_v2_CariHesapAnaDovizBakiye

Carinin toplam bakiyesini ana döviz cinsinden hesaplar. Parametrelere göre `fn_CariTutarlar` veya `fn_Aysm_v2_CariHesapBakiye` fonksiyonlarını çağırır.

```sql
CREATE FUNCTION [dbo].[fn_Aysm_v2_CariHesapAnaDovizBakiye] (
    @FIRMALAR AS nvarchar(MAX),
    @CARICINSI AS tinyint,
    @CARIKODU AS nvarchar(25),
    @SORMERKKODU AS nvarchar(25),
    @PROJEKODU AS nvarchar(25),
    @GRUPNO AS tinyint,
    @ILKTARIH AS datetime,
    @SONTARIH AS datetime,
    @ODEMEEMRIDEGERLEMEDOK AS tinyint,
    @MusteriTeminatMektubu_Bakiyeyi_Etkilemesin_fl AS tinyint,
    @FirmaTeminatMektubu_Bakiyeyi_Etkilemesin_fl AS tinyint,
    @DepozitoCeki_Bakiyeyi_Etkilemesin_fl AS tinyint,
    @DepozitoSenedi_Bakiyeyi_Etkilemesin_fl AS tinyint
)
RETURNS float
AS
BEGIN
    DECLARE @Borc AS float
    DECLARE @Alacak AS float

    IF (@ODEMEEMRIDEGERLEMEDOK > 0) OR (@CARICINSI IN (5,9))
       OR (NOT(@ILKTARIH IS NULL)) OR (NOT(@SONTARIH IS NULL))
    BEGIN
        SELECT
            @Borc   = SUM([msg_S_0101\T]),  -- ANA DOVIZ BORC
            @Alacak = SUM([msg_S_0102\T])   -- ANA DOVIZ ALACAK
        FROM dbo.fn_CariTutarlar(
            @FIRMALAR, @CARICINSI, @CARIKODU, @GRUPNO,
            @ILKTARIH, @SONTARIH, @ODEMEEMRIDEGERLEMEDOK,
            @SORMERKKODU, @PROJEKODU
        )
    END
    ELSE
        SET @Borc = dbo.fn_Aysm_v2_CariHesapBakiye(
            @FIRMALAR, @CARICINSI, @CARIKODU,
            @SORMERKKODU, @PROJEKODU, @GRUPNO, 0,
            @MusteriTeminatMektubu_Bakiyeyi_Etkilemesin_fl,
            @FirmaTeminatMektubu_Bakiyeyi_Etkilemesin_fl,
            @DepozitoCeki_Bakiyeyi_Etkilemesin_fl,
            @DepozitoSenedi_Bakiyeyi_Etkilemesin_fl
        )

    RETURN ISNULL(@Borc, 0) - ISNULL(@Alacak, 0)
END
```

### fn_Aysm_v2_CariHesapMeblag

Tek bir hareketin net tutarını hesaplar. KDV, masraf, tevkifat, stopaj, OTV, OIV ve döviz kuru gibi tüm kalemleri hesaba katar.

```sql
CREATE FUNCTION [dbo].[fn_Aysm_v2_CariHesapMeblag] (
    @CariMeblag AS float,
    @AraToplam AS float,
    @Vergitop AS float,
    @HDkur AS float,
    @AltDKur AS float,
    @KarsiDKur AS float,
    @HarCinsi AS tinyint,
    @HarEvrakTipi AS tinyint,
    @IstenenDovizTipi AS tinyint,
    @Caricinsi AS tinyint,
    @AnaDovizGecersiz AS bit,
    @AlternatifDovizGecersiz AS bit,
    @OrjinalDovizGecersiz AS bit,
    @Masraf1 AS float,
    @Masraf2 AS float,
    @Masraf3 AS float,
    @Masraf4 AS float,
    @OtvTutar AS float,
    @OivTutar AS float,
    @Yuvarlama AS float,
    @Tevkifat AS float,
    @Stopaj AS float,
    @SavSanDesFonu AS float,
    @Vergifon AS float,
    @HarTicaretTuru AS tinyint,
    @KarsiCaricinsi AS tinyint,
    @KarsiCarikodu AS nvarchar(25)
)
RETURNS float
AS
BEGIN
    DECLARE @OTV_eklenecek_Fl AS bit

    IF (@Caricinsi = 8) AND (@OtvTutar <> 0)
        SET @OTV_eklenecek_Fl = dbo.fn_Atik_OTV_Maliyete_eklensin(@KarsiCarikodu)
    ELSE
        SET @OTV_eklenecek_Fl = 1

    DECLARE @Result AS float
    SET @Result = 0

    SET @Result = CASE
        WHEN (@HarCinsi IN (1,2,3,4,17,18,19,20,21,22))
         AND (@Caricinsi IN (2,4))
        THEN @AraToplam

        WHEN (@HarCinsi IN (9,27))
         AND (@Caricinsi IN (3,5,9,12))
        THEN @CariMeblag - @Vergitop + @Stopaj + @SavSanDesFonu
             - @Yuvarlama + @Tevkifat - @Vergifon

        WHEN (@HarCinsi IN (26))
         AND (@Caricinsi IN (5,6,8))
        THEN @CariMeblag + @Stopaj + @SavSanDesFonu
             - @Tevkifat - @Yuvarlama

        WHEN (@HarCinsi IN (8,14,11,10,28))
         AND (@Caricinsi IN (3,5,6,8,9,12))
        THEN @CariMeblag - @Vergitop - @OivTutar + @Stopaj
             + @SavSanDesFonu - @Yuvarlama + @Tevkifat - @Vergifon

        WHEN (@HarCinsi IN (13,29))
          OR (@HarTicaretTuru IN (2,4)
              AND (@HarCinsi IN (10,11,14,15)
                   OR (@HarCinsi IN (8) AND @KarsiCaricinsi = 6)))
          OR (NOT (@Caricinsi IN (0,1,2,4,6,10,11,13)))
        THEN @CariMeblag - @Vergitop - @OtvTutar - @OivTutar
             + @Stopaj + @SavSanDesFonu - @Vergifon

        WHEN (@HarCinsi = 33) AND (@Caricinsi IN (0,1,6))
        THEN @AraToplam

        WHEN (@Caricinsi IN (10))
        THEN @AraToplam

        WHEN (@HarEvrakTipi IN (108)) AND (@Caricinsi IN (2))
        THEN @AraToplam

        WHEN (@HarEvrakTipi IN (109)) AND (@Caricinsi IN (11))
        THEN @AraToplam

        ELSE @CariMeblag
    END

    IF @OTV_eklenecek_Fl = 0
        SET @Result = @Result - @OtvTutar

    SET @Result = CASE
        WHEN @IstenenDovizTipi = 0
        THEN @Result * @HDkur
        WHEN (@IstenenDovizTipi = 1) AND (@AltDKur != 0)
        THEN @Result * @HDkur / @AltDKur
        WHEN (@IstenenDovizTipi = 1) AND (@AltDKur = 0)
        THEN 0
        WHEN @IstenenDovizTipi = 2
        THEN @Result
        WHEN (@IstenenDovizTipi = 3) AND (@KarsiDKur != 0)
        THEN @Result * @HDkur / @KarsiDKur
        WHEN (@IstenenDovizTipi = 3) AND (@KarsiDKur = 0)
        THEN 0
        ELSE @Result
    END

    SET @Result = CASE
        WHEN ((@HarCinsi = 11) OR (@HarEvrakTipi = 59))
         AND (@IstenenDovizTipi IN (2,3))
        THEN 0
        WHEN (@AnaDovizGecersiz = 1) AND (@IstenenDovizTipi = 0)
        THEN 0
        WHEN (@AlternatifDovizGecersiz = 1) AND (@IstenenDovizTipi = 1)
        THEN 0
        WHEN (@OrjinalDovizGecersiz = 1) AND (@IstenenDovizTipi IN (2,3))
        THEN 0
        ELSE @Result
    END

    RETURN @Result
END
```

## Performans İpuçları

| Durum | Yaklaşık Süre |
|-------|:---:|
| 500 cari | ~30 saniye |
| 2.000 cari | ~2 dakika |
| 5.000+ cari | 5+ dakika |

Sorguyu hızlandırmak için:

1. **Cari prefix filtresi kullanın** — `WHERE cha_kod LIKE '120%'` ile sadece istediğiniz grubu çekin
2. **Tarih filtresi daraltın** — Çok eski hareketler bakiye dağıtımını yavaşlatır
3. **NOLOCK** — Zaten sorgudaki tüm tablo erişimlerinde `WITH (NOLOCK)` kullanılıyor
4. **Zamanlama** — Mesai saatleri dışında çalıştırın, SQL Server'ı meşgul eder

## Dikkat Edilecekler

### SET LANGUAGE Turkish Tuzağı

Bu sorgu `SET LANGUAGE Turkish` içermiyor — bilerek çıkarıldı. Türkçe dil ayarı tarih hesaplamalarını bozabilir. [Detaylı açıklama](/blog/sql-server-set-language-turkish-tarih-tuzagi/).

### fn_Aysm Fonksiyonu Bulunamadı Hatası

```
Invalid object name 'dbo.fn_Aysm_v2_CariHesapAnaDovizBakiye'
```

Bu fonksiyonlar Mikro güncellendikten sonra bazı şube veritabanlarına eklenmemiş olabilir. Çözüm: Fonksiyonun var olduğu bir veritabanından kopyalayın. [Rehberimiz](/blog/mikro-erp-fn-fonksiyonlari-sube-kopyalama/).

### Bakiye Mikro ile Uyuşmuyor

- Rapor tarihinin Mikro'daki ile aynı olduğundan emin olun
- Döviz cinsini kontrol edin (sorgu ana döviz bakiyesini alır)
- Açılış fişleri (`cha_evrak_tip = 59`) hariç tutulmuştur — gerekirse dahil edin

## Bu Sorguyu Web'e Taşımak

Bu SQL sorgusunu her sabah otomatik çalıştırıp sonuçları web dashboard'a aktarabilirsiniz. [Cari yaşlandırma otomasyon yazımız](/blog/mikro-erp-cari-yaslandirma-otomasyonu/) bu süreci adım adım anlatıyor.

## Tam Sorgu — Kopyala Yapıştır

Yukarıdaki 7 adımın tamamı tek bir blok halinde. SSMS'e yapıştırın, `@BakiyeTarih` değerini ayarlayın, çalıştırın:

```sql
/*
  Mikro ERP — Cari Yaşlandırma (Bakiye Dağıtımlı / FIFO)
  ─────────────────────────────────────────────────────────
  Kullanım:
    1. @BakiyeTarih → Rapor tarihi (varsayılan: bugün)
    2. Cari filtresi → Adım 1'deki WHERE satırını açın
    3. SSMS'de çalıştırın
  
  Gerekli fonksiyonlar:
    - dbo.fn_Aysm_v2_CariHesapAnaDovizBakiye
    - dbo.fn_Aysm_v2_CariHesapMeblag
    - dbo.fn_Aysm_v2_CariharBorcAlacak
*/

SET NOCOUNT ON;

DECLARE @BakiyeTarih datetime = GETDATE();  -- İstediğiniz tarihi yazın: '20240315'
DECLARE @GunTarih date = CAST(@BakiyeTarih AS date);

-- Temp tabloları temizle
IF OBJECT_ID('tempdb..#AktifCariler') IS NOT NULL DROP TABLE #AktifCariler;
IF OBJECT_ID('tempdb..#HamBakiye') IS NOT NULL DROP TABLE #HamBakiye;
IF OBJECT_ID('tempdb..#Bakiye') IS NOT NULL DROP TABLE #Bakiye;
IF OBJECT_ID('tempdb..#Kaynak') IS NOT NULL DROP TABLE #Kaynak;
IF OBJECT_ID('tempdb..#Dagitim') IS NOT NULL DROP TABLE #Dagitim;

----------------------------------------------------------------------
-- 1. Aktif carileri belirle (performans için ön filtre)
----------------------------------------------------------------------
SELECT DISTINCT cha_kod AS cari_kod
INTO #AktifCariler
FROM dbo.CARI_HESAP_HAREKETLERI WITH (NOLOCK)
WHERE cha_tarihi <= @BakiyeTarih;
-- Belirli bir grup icin: AND cha_kod LIKE '120%'

----------------------------------------------------------------------
-- 2. Bakiye hesapla (fn_Aysm fonksiyonu)
----------------------------------------------------------------------
SELECT
    a.cari_kod,
    CONVERT(decimal(18,2),
        dbo.fn_Aysm_v2_CariHesapAnaDovizBakiye(
            '', 0, a.cari_kod, '', '',
            NULL, NULL, @BakiyeTarih, 0, 0, 0, 0, 0
        )
    ) AS HamBakiye
INTO #HamBakiye
FROM #AktifCariler a;

----------------------------------------------------------------------
-- 3. Sıfır bakiyeleri ele, borç/alacak tipini belirle
----------------------------------------------------------------------
SELECT
    cari_kod,
    HamBakiye AS OrjinalBakiye,
    ABS(HamBakiye) AS NetBakiye,
    CASE WHEN HamBakiye > 0 THEN 0
         WHEN HamBakiye < 0 THEN 1
         ELSE -1 END AS Tip
INTO #Bakiye
FROM #HamBakiye
WHERE HamBakiye <> 0;

----------------------------------------------------------------------
-- 4. Hareketleri çek (tutar + vade hesabıyla)
----------------------------------------------------------------------
SELECT
    b.cari_kod AS AnaCariKod,
    b.NetBakiye,
    ch.cha_Guid,
    ch.cha_satici_kodu,
    CAST(ch.cha_tarihi AS date) AS EvrakTarihi,
    CASE
        WHEN ch.cha_vade BETWEEN 19000101 AND 20991231 THEN
            TRY_CONVERT(date, CONVERT(char(8), ch.cha_vade), 112)
        WHEN ch.cha_vade BETWEEN -36500 AND -1 THEN
            DATEADD(DAY, ABS(ch.cha_vade), CAST(ch.cha_tarihi AS date))
        ELSE NULL
    END AS VadeTarihi,
    CONVERT(decimal(18,2),
        dbo.fn_Aysm_v2_CariHesapMeblag(
            ch.cha_meblag, ch.cha_aratoplam,
            ch.cha_vergi1 + ch.cha_vergi2 + ch.cha_vergi3 + ch.cha_vergi4 +
            ch.cha_vergi5 + ch.cha_vergi6 + ch.cha_vergi7 + ch.cha_vergi8 +
            ch.cha_vergi9 + ch.cha_vergi10 +
            ch.cha_ilave_edilecek_kdv1 + ch.cha_ilave_edilecek_kdv2 +
            ch.cha_ilave_edilecek_kdv3 + ch.cha_ilave_edilecek_kdv4 +
            ch.cha_ilave_edilecek_kdv5 + ch.cha_ilave_edilecek_kdv6 +
            ch.cha_ilave_edilecek_kdv7 + ch.cha_ilave_edilecek_kdv8 +
            ch.cha_ilave_edilecek_kdv9 + ch.cha_ilave_edilecek_kdv10,
            ch.cha_d_kur, ch.cha_altd_kur, ch.cha_karsid_kur,
            ch.cha_cinsi, ch.cha_evrak_tip, 0,
            ch.cha_cari_cins, ch.cha_meblag_ana_doviz_icin_gecersiz_fl,
            ch.cha_meblag_alt_doviz_icin_gecersiz_fl,
            ch.cha_meblag_orj_doviz_icin_gecersiz_fl,
            ch.cha_ft_masraf1, ch.cha_ft_masraf2, ch.cha_ft_masraf3,
            ch.cha_ft_masraf4, ch.cha_otvtutari, ch.cha_oivtutari,
            ch.cha_yuvarlama, ch.cha_tevkifat_toplam,
            ch.cha_stopaj, ch.cha_savsandesfonu, ch.cha_vergifon_toplam,
            ch.cha_ticaret_turu, ch.cha_kasa_hizmet, ch.cha_kasa_hizkod
        )
    ) AS Meblag,
    ch.cha_create_date
INTO #Kaynak
FROM dbo.CARI_HESAP_HAREKETLERI ch WITH (NOLOCK)
INNER JOIN #Bakiye b
    ON (ch.cha_kod = b.cari_kod OR ch.cha_ciro_cari_kodu = b.cari_kod)
WHERE ch.cha_tarihi <= @BakiyeTarih
  AND dbo.fn_Aysm_v2_CariharBorcAlacak(
        0, ch.cha_tip, ch.cha_tpoz, ch.cha_cari_cins, ch.cha_cinsi, 0, 0, 0, 0
      ) = b.Tip
  AND ch.cha_meblag * ch.cha_d_kur > 1
  AND ch.cha_evrak_tip <> 59;

----------------------------------------------------------------------
-- 5. Bakiye dağıtımı (FIFO — en yeniden eskiye)
----------------------------------------------------------------------
;WITH Sira AS (
    SELECT k.*,
        SUM(k.Meblag) OVER (
            PARTITION BY k.AnaCariKod
            ORDER BY k.EvrakTarihi DESC, k.cha_create_date DESC, k.cha_Guid DESC
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ) AS CumTutar
    FROM #Kaynak k
),
Dagitim AS (
    SELECT AnaCariKod, cha_Guid, cha_satici_kodu, EvrakTarihi, VadeTarihi,
        CASE
            WHEN CumTutar < NetBakiye THEN Meblag
            WHEN CumTutar - Meblag < NetBakiye THEN NetBakiye - (CumTutar - Meblag)
            ELSE 0
        END AS DagitilanTutar
    FROM Sira
)
SELECT * INTO #Dagitim FROM Dagitim WHERE DagitilanTutar > 0;

----------------------------------------------------------------------
-- 6 + 7. Teminatlar + Final rapor
----------------------------------------------------------------------
;WITH Teminat AS (
    SELECT ct_carikodu,
        SUM(ISNULL(ct_tutari, 0)) AS CariLimiti
    FROM dbo.CARI_HESAP_TEMINATLARI WITH (NOLOCK)
    WHERE ISNULL(ct_iptal, 0) = 0
    GROUP BY ct_carikodu
)
SELECT
    c.cari_kod              AS [Hesap Kodu],
    LTRIM(RTRIM(CONCAT(ISNULL(c.cari_unvan1,''),' ',ISNULL(c.cari_unvan2,'')))) AS [Hesap Adı],
    c.cari_temsilci_kodu    AS [Temsilci],
    ISNULL(a.adr_il,'')     AS [İl],
    ISNULL(a.adr_ilce,'')   AS [İlçe],
    ISNULL(b.OrjinalBakiye, 0) AS [Toplam Bakiye],
    CASE WHEN ISNULL(c.cari_cari_kilitli_flg,0)=1 THEN N'Kilitli' ELSE N'Aktif' END AS [Durum],
    ABS(ISNULL(c.cari_odemeplan_no,0)) AS [Vade (Gün)],
    CONVERT(decimal(18,2), ISNULL(t.CariLimiti,0)) AS [Teminat],

    CONVERT(decimal(18,2), SUM(CASE WHEN d.VadeTarihi IS NOT NULL AND d.VadeTarihi < @GunTarih
        THEN d.DagitilanTutar ELSE 0 END)) AS [Vadesi Geçen],

    CONVERT(decimal(18,2), SUM(CASE
        WHEN d.EvrakTarihi >= DATEADD(MONTH,-1,DATEFROMPARTS(YEAR(@GunTarih),MONTH(@GunTarih),1))
         AND d.EvrakTarihi <  DATEFROMPARTS(YEAR(@GunTarih),MONTH(@GunTarih),1)
        THEN d.DagitilanTutar ELSE 0 END)) AS [1 Ay Önce],

    CONVERT(decimal(18,2), SUM(CASE
        WHEN d.EvrakTarihi >= DATEADD(MONTH,-2,DATEFROMPARTS(YEAR(@GunTarih),MONTH(@GunTarih),1))
         AND d.EvrakTarihi <  DATEADD(MONTH,-1,DATEFROMPARTS(YEAR(@GunTarih),MONTH(@GunTarih),1))
        THEN d.DagitilanTutar ELSE 0 END)) AS [2 Ay Önce],

    CONVERT(decimal(18,2), SUM(CASE
        WHEN d.EvrakTarihi >= DATEADD(MONTH,-3,DATEFROMPARTS(YEAR(@GunTarih),MONTH(@GunTarih),1))
         AND d.EvrakTarihi <  DATEADD(MONTH,-2,DATEFROMPARTS(YEAR(@GunTarih),MONTH(@GunTarih),1))
        THEN d.DagitilanTutar ELSE 0 END)) AS [3 Ay Önce],

    CONVERT(decimal(18,2), SUM(CASE
        WHEN d.EvrakTarihi >= DATEADD(MONTH,-6,DATEFROMPARTS(YEAR(@GunTarih),MONTH(@GunTarih),1))
         AND d.EvrakTarihi <  DATEADD(MONTH,-3,DATEFROMPARTS(YEAR(@GunTarih),MONTH(@GunTarih),1))
        THEN d.DagitilanTutar ELSE 0 END)) AS [3-6 Ay Önce],

    CONVERT(decimal(18,2), SUM(CASE
        WHEN d.EvrakTarihi < DATEADD(MONTH,-6,DATEFROMPARTS(YEAR(@GunTarih),MONTH(@GunTarih),1))
        THEN d.DagitilanTutar ELSE 0 END)) AS [6+ Ay Önce]

FROM dbo.CARI_HESAPLAR c WITH (NOLOCK)
INNER JOIN #Bakiye b ON b.cari_kod = c.cari_kod
LEFT JOIN dbo.CARI_HESAP_ADRESLERI a WITH (NOLOCK)
    ON a.adr_cari_kod = c.cari_kod AND a.adr_adres_no = 1
LEFT JOIN Teminat t ON t.ct_carikodu = c.cari_kod
LEFT JOIN #Dagitim d ON d.AnaCariKod = c.cari_kod
GROUP BY c.cari_kod, c.cari_unvan1, c.cari_unvan2, c.cari_temsilci_kodu,
    a.adr_il, a.adr_ilce, c.cari_cari_kilitli_flg, c.cari_odemeplan_no,
    t.CariLimiti, b.OrjinalBakiye
ORDER BY b.OrjinalBakiye DESC;
```

## İlgili Yazılar

- [Cari Ekstre Raporu — Borç/Alacak Bakiye](/blog/mikro-erp-cari-ekstre-raporu-sql/)
- [Kapanmamış Cari Hareketler](/blog/mikro-erp-kapanmamis-cari-hareket-sql/) — Hangi evrak açık kaldı?
- [fn_ Fonksiyonlarını Şubelere Kopyalama](/blog/mikro-erp-fn-fonksiyonlari-sube-kopyalama/)
- [SET LANGUAGE Turkish Tarih Tuzağı](/blog/sql-server-set-language-turkish-tarih-tuzagi/)
- [Cari Yaşlandırma Web Otomasyonu](/blog/mikro-erp-cari-yaslandirma-otomasyonu/)
- [Belge No ile Hareket Bulma](/blog/mikro-erp-belge-no-cari-hareket-bulma-sql/)

---

*Bu yazı [AstaFlow Case Study](/case-study) serisinin bir parçasıdır — 8 şubeli bir yapıda cari yaşlandırma otomasyonu sırasında geliştirdiğimiz sorgunun genelleştirilmiş halidir.*