Mikro Müşteri Risk Raporu SQL: Bakiye ve Son Tahsilat
Mikro'da müşteri risk raporu: tek sorguda güncel bakiye, son tahsilat tarihi, son satış tarihi ve vadesi geçen tutar. CASE + MAX deseniyle T-SQL.
Erp Asistanı
Editör ekibi
11 dk okuma
İçindekiler
Müşteri risk raporu denince çoğu kişi tek bir sayı düşünür: cari ne kadar borçlu. Oysa tek başına bakiye yanıltıcıdır. 300 bin TL borcu olup dün ödeme yapan müşteri ile 80 bin TL borcu olup 7 aydır tek kuruş ödemeyen müşteri aynı sepete girmez. Risk kararı üç verinin birlikte okunmasıyla çıkar: güncel net bakiye, son tahsilat tarihi, son satış tarihi. Bu yazıda bu üçünü tek sorguda üreten T-SQL'i, ardından vadesi geçen tutarı ve risk segmentasyonunu veriyoruz. Tüm sorgular CARI_HESAP_HAREKETLERI ve CARI_HESAPLAR üzerinde çalışır; tablo bağlarını bilmiyorsanız Mikro tablo yapısı rehberi iyi bir başlangıçtır.
Genel kural yine geçerli: her sorguda cha_iptal = 0 ve cari_iptal = 0 filtresi vardır, iade satırları net hesapta düşülür.
Risk raporunun üç ayağı
Raporun doğru olması için üç metriğin de doğru kaynaktan gelmesi gerekir:
- Net bakiye:
SUM(CASE WHEN cha_tip = 0 THEN +tutar WHEN cha_tip = 1 THEN -tutar END).cha_tipborç/alacak yönüdür, tutar değil. - Son tahsilat:
MAX(cha_tarihi)ancakcha_evrak_tip = 1(Tahsilat Makbuzu) koşuluyla. - Son satış:
MAX(cha_tarihi)ancakcha_evrak_tip = 63(Satış Faturası) koşuluyla.
İkinci ve üçüncü maddede klasik tuzak şudur: cha_tip = 1 (alacak) filtresiyle tahsilat aramak. Her alacak satırı tahsilat değildir; iade, kur farkı, virman da alacak tarafına düşer. Tahsilat tarihi için evrak tipini kullanın, cha_tip'i değil.
Ana sorgu: bakiye + son tahsilat + son satış
Aşağıdaki sorgu tek geçişte üç metriği üretir. CASE içindeki MAX deseni, aynı satır kümesinden koşullu tarihleri çıkarmanın en kısa yoludur:
WITH Risk AS (
SELECT
ch.cha_kod,
SUM(CASE
WHEN ch.cha_tip = 0 THEN CASE WHEN ch.cha_normal_Iade = 0 THEN ch.cha_meblag ELSE -ch.cha_meblag END
WHEN ch.cha_tip = 1 THEN CASE WHEN ch.cha_normal_Iade = 0 THEN -ch.cha_meblag ELSE ch.cha_meblag END
ELSE 0
END) AS NetBakiye,
MAX(CASE WHEN ch.cha_evrak_tip = 1 THEN ch.cha_tarihi END) AS SonTahsilat,
MAX(CASE WHEN ch.cha_evrak_tip = 63 THEN ch.cha_tarihi END) AS SonSatis
FROM CARI_HESAP_HAREKETLERI ch
WHERE ch.cha_iptal = 0
GROUP BY ch.cha_kod
)
SELECT
r.cha_kod,
LTRIM(RTRIM(ISNULL(c.cari_unvan1, '') + ' ' + ISNULL(c.cari_unvan2, ''))) AS Unvan,
r.NetBakiye,
CAST(r.SonTahsilat AS date) AS SonTahsilatGunu,
CAST(r.SonSatis AS date) AS SonSatisGunu
FROM Risk r
JOIN CARI_HESAPLAR c ON c.cari_kod = r.cha_kod
WHERE c.cari_iptal = 0
AND c.cari_hidden = 0
AND c.cari_baglanti_tipi = 0
AND r.NetBakiye <> 0
ORDER BY r.NetBakiye DESC;
Bu raporu SQL yazmadan, Türkçe sorarak alın: Mikro raporlama
Çıktıyı okurken neye baktığınızı bilin:
| Kolon | Ne söyler | Karar için kullanımı |
|---|---|---|
| NetBakiye | Bugünkü borç/alacak farkı | Pozitifse müşteri borçlu, negatifse avanslı |
| SonTahsilatGunu | En son ne zaman para girdi | Boşsa hiç tahsilat yok, çok eskiyse risk yüksek |
| SonSatisGunu | En son ne zaman satış yapıldı | Satış durduysa tahsilat da durur |
Adım 2: kendi verinizde tahsilat evrak tiplerini doğrulayın
Yukarıdaki sorgu yalnız cha_evrak_tip = 1 satırlarını tahsilat sayar. Çek, senet, kredi kartı ve dekont tahsilatları ayrı evrak tipleridir ve numaraları kurulumdan kuruluma değişebilir. Tahsilat tanımınızı genişletmeden önce kendi veritabanınızda hangi tiplerin gerçekten kullanıldığına bakın:
SELECT cha_evrak_tip,
COUNT(*) AS SatirSayisi,
MIN(cha_tarihi) AS IlkKayit,
MAX(cha_tarihi) AS SonKayit
FROM CARI_HESAP_HAREKETLERI
WHERE cha_iptal = 0
GROUP BY cha_evrak_tip
ORDER BY SatirSayisi DESC;
Bu listeye bakmadan "çekle tahsilatı da sayıyorum" demek, raporu sessizce yanlış yapar. Liste boş gelen tipleri sorgunuza eklemeyin.
Riskli müşteriyi işaretlemek
Bakiye sıralaması tek başına risk sıralaması değildir. Aşağıdaki sorgu, bakiyesi pozitif olup son 60 gündür tahsilat görmeyen müşterileri çeker. DATEDIFF sonucu NULL ise hiç tahsilat yapılmamış demektir, bunu da listeye dahil ediyoruz:
WITH Risk AS (
SELECT ch.cha_kod,
SUM(CASE
WHEN ch.cha_tip = 0 THEN CASE WHEN ch.cha_normal_Iade = 0 THEN ch.cha_meblag ELSE -ch.cha_meblag END
WHEN ch.cha_tip = 1 THEN CASE WHEN ch.cha_normal_Iade = 0 THEN -ch.cha_meblag ELSE ch.cha_meblag END
ELSE 0
END) AS NetBakiye,
MAX(CASE WHEN ch.cha_evrak_tip = 1 THEN ch.cha_tarihi END) AS SonTahsilat,
MAX(CASE WHEN ch.cha_evrak_tip = 63 THEN ch.cha_tarihi END) AS SonSatis
FROM CARI_HESAP_HAREKETLERI ch
WHERE ch.cha_iptal = 0
GROUP BY ch.cha_kod
)
SELECT r.cha_kod,
LTRIM(RTRIM(ISNULL(c.cari_unvan1, '') + ' ' + ISNULL(c.cari_unvan2, ''))) AS Unvan,
r.NetBakiye,
CAST(r.SonTahsilat AS date) AS SonTahsilatGunu,
DATEDIFF(DAY, r.SonTahsilat, GETDATE()) AS TahsilattanGun
FROM Risk r
JOIN CARI_HESAPLAR c ON c.cari_kod = r.cha_kod
WHERE c.cari_iptal = 0 AND c.cari_hidden = 0 AND c.cari_baglanti_tipi = 0
AND r.NetBakiye > 0
AND (r.SonTahsilat IS NULL OR r.SonTahsilat < DATEADD(DAY, -60, GETDATE()))
ORDER BY r.NetBakiye DESC;
Aynı desene CASE ekleyerek segment üretebilirsiniz. Eşikleri işletmenize göre değiştirin, sorguya gömülü bırakmayın:
WITH Risk AS (
SELECT ch.cha_kod,
SUM(CASE WHEN ch.cha_tip = 0 THEN ch.cha_meblag ELSE -ch.cha_meblag END) AS NetBakiye,
MAX(CASE WHEN ch.cha_evrak_tip = 1 THEN ch.cha_tarihi END) AS SonTahsilat
FROM CARI_HESAP_HAREKETLERI ch
WHERE ch.cha_iptal = 0
GROUP BY ch.cha_kod
)
SELECT cha_kod,
NetBakiye,
CAST(SonTahsilat AS date) AS SonTahsilatGunu,
CASE
WHEN SonTahsilat IS NULL THEN 'Hic tahsilat yok'
WHEN DATEDIFF(DAY, SonTahsilat, GETDATE()) > 180 THEN 'Kritik'
WHEN DATEDIFF(DAY, SonTahsilat, GETDATE()) > 90 THEN 'Yuksek'
WHEN DATEDIFF(DAY, SonTahsilat, GETDATE()) > 30 THEN 'Izle'
ELSE 'Normal'
END AS RiskSegmenti
FROM Risk
WHERE NetBakiye > 0
ORDER BY NetBakiye DESC;
Vadesi geçen tutar: cha_vade tuzağı
Buradaki en pahalı hata cha_vade kolonunu tarih sanmaktır. cha_vade bir vade KODUDUR: 0 peşin, negatif değer hareket tarihinden sonraki gün sayısı, 1..10000 arası ödeme planı numarası, 10000 üzeri ise YYYYMMDD biçiminde tarihtir. Doğrudan DATEADD(DAY, cha_vade, cha_tarihi) yazmak negatif vadelerde tarihi ters yöne kaydırır ve hata vermez; yani rapor sessizce yanlış çıkar. Güvenli çözüm:
WITH Vadeli AS (
SELECT ch.cha_kod,
CASE
WHEN ch.cha_vade > 10000 THEN COALESCE(TRY_CONVERT(date, CAST(ch.cha_vade AS varchar(8)), 112), CAST(ch.cha_tarihi AS date))
WHEN ch.cha_vade < 0 THEN DATEADD(DAY, ABS(ch.cha_vade), CAST(ch.cha_tarihi AS date))
ELSE CAST(ch.cha_tarihi AS date)
END AS VadeTarihi,
CASE
WHEN ch.cha_tip = 0 THEN CASE WHEN ch.cha_normal_Iade = 0 THEN ch.cha_meblag ELSE -ch.cha_meblag END
WHEN ch.cha_tip = 1 THEN CASE WHEN ch.cha_normal_Iade = 0 THEN -ch.cha_meblag ELSE ch.cha_meblag END
ELSE 0
END AS NetTutar
FROM CARI_HESAP_HAREKETLERI ch
WHERE ch.cha_iptal = 0
)
SELECT cha_kod,
SUM(CASE WHEN VadeTarihi < CAST(GETDATE() AS date) AND NetTutar > 0 THEN NetTutar ELSE 0 END) AS VadesiGecen
FROM Vadeli
GROUP BY cha_kod
ORDER BY VadesiGecen DESC;
Bunu dürüstçe çerçeveleyelim: bu hesap, vadesi geçmiş borç satırlarının toplamıdır; tahsilat satırlarıyla satır bazında mahsuplaşmaz. Yani üst sınır verir, kesin yaşlandırma değildir. Vade kırılımlı gerçek yaşlandırma için cari yaşlandırma raporu yazısındaki desenleri kullanın.
Hepsini tek sorguda birleştirmek
Son adım: bakiyeyi, vadesi geçen tutarı, son tahsilatı ve son satışı aynı satırda görmek. İki CTE'yi cari kodu üzerinden birleştiriyoruz:
WITH Risk AS (
SELECT ch.cha_kod,
SUM(CASE
WHEN ch.cha_tip = 0 THEN CASE WHEN ch.cha_normal_Iade = 0 THEN ch.cha_meblag ELSE -ch.cha_meblag END
WHEN ch.cha_tip = 1 THEN CASE WHEN ch.cha_normal_Iade = 0 THEN -ch.cha_meblag ELSE ch.cha_meblag END
ELSE 0
END) AS NetBakiye,
MAX(CASE WHEN ch.cha_evrak_tip = 1 THEN ch.cha_tarihi END) AS SonTahsilat,
MAX(CASE WHEN ch.cha_evrak_tip = 63 THEN ch.cha_tarihi END) AS SonSatis
FROM CARI_HESAP_HAREKETLERI ch
WHERE ch.cha_iptal = 0
GROUP BY ch.cha_kod
),
Vade AS (
SELECT v.cha_kod,
SUM(CASE WHEN v.VadeTarihi < CAST(GETDATE() AS date) AND v.NetTutar > 0 THEN v.NetTutar ELSE 0 END) AS VadesiGecen
FROM (
SELECT ch.cha_kod,
CASE
WHEN ch.cha_vade > 10000 THEN COALESCE(TRY_CONVERT(date, CAST(ch.cha_vade AS varchar(8)), 112), CAST(ch.cha_tarihi AS date))
WHEN ch.cha_vade < 0 THEN DATEADD(DAY, ABS(ch.cha_vade), CAST(ch.cha_tarihi AS date))
ELSE CAST(ch.cha_tarihi AS date)
END AS VadeTarihi,
CASE
WHEN ch.cha_tip = 0 THEN CASE WHEN ch.cha_normal_Iade = 0 THEN ch.cha_meblag ELSE -ch.cha_meblag END
WHEN ch.cha_tip = 1 THEN CASE WHEN ch.cha_normal_Iade = 0 THEN -ch.cha_meblag ELSE ch.cha_meblag END
ELSE 0
END AS NetTutar
FROM CARI_HESAP_HAREKETLERI ch
WHERE ch.cha_iptal = 0
) v
GROUP BY v.cha_kod
)
SELECT r.cha_kod,
LTRIM(RTRIM(ISNULL(c.cari_unvan1, '') + ' ' + ISNULL(c.cari_unvan2, ''))) AS Unvan,
r.NetBakiye,
ISNULL(v.VadesiGecen, 0) AS VadesiGecen,
CAST(r.SonTahsilat AS date) AS SonTahsilatGunu,
CAST(r.SonSatis AS date) AS SonSatisGunu,
DATEDIFF(DAY, r.SonTahsilat, GETDATE()) AS TahsilattanGun
FROM Risk r
JOIN CARI_HESAPLAR c ON c.cari_kod = r.cha_kod
LEFT JOIN Vade v ON v.cha_kod = r.cha_kod
WHERE c.cari_iptal = 0 AND c.cari_hidden = 0 AND c.cari_baglanti_tipi = 0
AND r.NetBakiye <> 0
ORDER BY r.NetBakiye DESC;
Not: cari_baglanti_tipi = 0 müşteri carilerini getirir. Kurulumunuzda bayiler ayrı tipte tutuluyorsa filtreyi genişletmeniz gerekir; yoksa listede bayi müşterileri görünmez.
Döviz uyarısı
cha_meblag hareketin ORİJİNAL döviz cinsindedir. TL ve USD carileri aynı raporda toplarsanız elma ile armutu toplamış olursunuz. Tek kur üzerinden tek para birimine indirmek istiyorsanız cha_d_cins ve cha_d_kur alanlarını hesaba katın; kesin bakiye için Mikro'nun fn_CariFoy / fn_CariHesapAnaDovizBakiye fonksiyonları daha güvenilirdir, çünkü döviz, KDV ve vade mantığını içeride çözerler.
Sık yapılan hatalar
- Tahsilat tarihini
cha_tip = 1ile aramak.cha_tipyön verir; iade ve virman da alacaktır. Evrak tipini (cha_evrak_tip) kullanın. cha_iptal = 0filtresini unutmak. İptal edilmiş bir tahsilat makbuzu, kapanmış müşteriyi "ödeme yapmıyor" gibi gösterir.cha_vadekolonunu tarih sanıpDATEADD(DAY, cha_vade, cha_tarihi)yazmak. Negatif vadede tarih ters yöne kayar ve hata vermez.cha_normal_Iadedüşümünü atlamak. İade faturaları bakiyeyi şişirir.MAX(cha_tarihi)sonucunu gün bazına indirmemek. Saat bileşeni yüzünden aynı gün iki farklı tarih gibi görünür,CAST(... AS date)kullanın.- Kredi limiti kolonunu varmış gibi varsaymak. Cari kartta böyle bir alan standart değildir; limit karşılaştırması yapacaksanız önce kendi şemanızda alanın var olduğunu doğrulayın.
- Bakiye ile ciroyu aynı sorguda karıştırmak. Ciro
STOK_HAREKETLERI, bakiyeCARI_HESAP_HAREKETLERItarafındadır; ikisini aynı toplama koymak çift saymaya yol açar.
Sık sorulanlar
Son tahsilat tarihi boş gelen müşteri ne demek? Hiç tahsilat makbuzu olmayan müşteridir. Ama ödeme havale veya çekle geldiyse evrak tipi farklıdır; bu yüzden önce kendi verinizde cha_evrak_tip dağılımına bakın, sonra tahsilat tanımınızı genişletin.
Rapor neden bazı müşterileri hiç göstermiyor? cari_baglanti_tipi = 0 filtresi yalnız müşteri carilerini getirir, ayrıca cari_hidden = 0 gizli kartları eler. Bayi veya dağıtıcı tipindeki carileriniz varsa filtreyi gevşetin.
Vadesi geçen tutar neden bakiyeden büyük çıkıyor? Çünkü vadesi geçen hesabı brüt borç satırları üzerinden yapıyoruz, tahsilatlarla satır bazında mahsuplaşmıyor. Kesin yaşlandırma için fn_CariFoy veya vade kırılımlı yaşlandırma sorgusunu kullanın.
Bu sorgular v16'da da çalışır mı? Kullanılan tablolar ve kolonlar iki sürümde de aynı isimdedir; ancak cha_Guid gibi bağ alanları sürüme göre farklılık gösterir. Sürüm farkları için Mikro v17 tablo yapısı rehberi ve vadesi geçen alacak takibi yazılarına bakabilirsiniz.
Bakiye, son tahsilat ve son satış üçlüsü tek tabloda durduğunda risk listesi kendiliğinden çıkar; asıl iş bu listeyi her sabah otomatik görmek ve eşikleri işletmenize göre ayarlamaktır. Sorunuzu Türkçe yazın, şemanıza uygun SQL'i saniyeler içinde alın.
Ücretsiz üye olun, müşteri risk sorgunuzu doğal dille üretin →
Bu raporu SQL yazmadan, Türkçe sorarak alın
Türkçe sorunuzu yazın, şemanıza uygun çalışan SQL'i ya da hazır uygulamanızı saniyeler içinde alın.




