Mikro ERP Alış-Satış Kâr Analizi: Ürün Bazlı Brüt Kâr/Zarar SQL Sorgusu
Mikro ERP'de brüt kâr nasıl garantili hesaplanır? Alış maliyeti ve satış gelirini karşılaştıran CTE analizleri, bedelsiz/promosyon ürünlerinin kârlılık...
İş Problemi: “Sattıkça Neden Para Kaybediyoruz?”
Patronların ERP raporlarında en sık tartışma çıkardığı tablo “Brüt Kâr” raporudur. Yönetim paneli, bir ürünün %35 kârla satıldığını söyler ancak yıl sonu mizanları bambaşka bir tablo çıkartır. Sorun genelde SQL’in yanlış yazılmasıdır.
Neden Standart Kâr Yaklaşımı Çalışmaz? Çünkü basit bir “Ciro - Ortalama Alış” denklemi şu durumları hesaba katmaz:
- Promosyon/Bedelsiz Alım: Tedarikçiden 10 adet telefonu normal fiyata, 1 adedini bedelsiz hediye (
sth_tutar = 0) aldınız. Satışta bu hediyeyi de normal fiyattan satarsınız. Eğer bedelsiz faturayı maliyet ortalamasına dahil ederseniz (sayısal olarak maliyeti sıfıra çekeceğinden) o ürünün birim maliyeti yalan yanlış bir seviyeye düşer. - Müşteri İadeleri: Müşteri ürünü iade ettiğinde (
sth_normal_iade = 1), bunu bir “alış” veya “maliyet” kalemi olarak değil, satış cirosundan düşen bir eksi kalem olarak yorumlamalısınız. - Satış İskontoları: Satır iskontosu mu (Satır toplamı) yoksa evrak altı iskonto mu (Genel Tutar)? Sadece
sth_tutaralanına bakılmazsa iskonto maliyetleri gözden kaçar.
Somut Hata: Kârlılık İllüzyonu
Yanlış yazılmış bir SQL sorgusunun vereceği yanıltıcı çıktı:
Ürün: Kırmızı T-Shirt Satış Cirosu: 10.000 TL Tahmini Maliyet: 1.000 TL Brüt Kâr Marjı: %90 (?) Gerçeklik: Aslında ürünün maliyeti 8.000 TL’ydi. Sadece geçen ay tedarikçi faturayı “sıfır bedelli hediye” kestiği için sistemin maliyet ortalaması dibe çöktü!
Çözüm: Promosyon ve İade Farkındalıklı CTE Sorgusu
Doğru bir ürün bazlı kâr analizi kurmak için, bedelsiz alımları maliyet hesabından (ama stok sayısından değil!) dışlamak şarttır:
;WITH SafAlisMaliyeti AS (
SELECT
sth_stok_kod,
SUM(sth_tutar) AS [GecerliTutar Toplam],
-- SADECE Tutar > 0 olan (bedelsiz OLMAYAN) ürünlerin miktarını alıyoruz!
SUM(CASE WHEN sth_tutar > 0 THEN sth_miktar ELSE 0 END) AS ParaliAlinanMiktar
FROM STOK_HAREKETLERI WITH (NOLOCK)
WHERE sth_tip = 0 AND sth_cins = 0
AND sth_normal_iade = 0
AND sth_iptal = 0
GROUP BY sth_stok_kod
),
GercekSatisCirosu AS (
SELECT
sth_stok_kod,
-- İade edilmeyen net satış cirosu (Eğer iade ise cirodan düş!)
SUM(CASE WHEN sth_normal_iade = 0 THEN sth_tutar ELSE -sth_tutar END) AS NetSatisCirosu,
-- İade edilmeyen net satış miktarı
SUM(CASE WHEN sth_normal_iade = 0 THEN sth_miktar ELSE -sth_miktar END) AS NetSatisMiktar
FROM STOK_HAREKETLERI WITH (NOLOCK)
WHERE sth_tip = 1 AND sth_cins = 0
AND sth_iptal = 0
GROUP BY sth_stok_kod
)
SELECT
SA.sth_stok_kod AS [Stok Kodu],
S.sto_isim AS [Ürün Adı],
SA.NetSatisMiktar AS [Net Satılan Miktar],
SA.NetSatisCirosu AS [Net Satış Cirosu],
-- Gerçekçi (Saf) Ortalama Maliyet Çıkarımı
ROUND(ISNULL(A.[GecerliTutar Toplam], 0) /
NULLIF(A.ParaliAlinanMiktar, 0) * SA.NetSatisMiktar, 2) AS [Kanıtlı Satış Maliyeti],
-- Net Brüt Kâr (Ciro - Kanıtlı Maliyet)
SA.NetSatisCirosu - ROUND(ISNULL(A.[GecerliTutar Toplam], 0) /
NULLIF(A.ParaliAlinanMiktar, 0) * SA.NetSatisMiktar, 2) AS [Net Brüt Kâr],
-- Kâr Marjı (%)
CAST(ROUND(
(SA.NetSatisCirosu - ISNULL(A.[GecerliTutar Toplam], 0) /
NULLIF(A.ParaliAlinanMiktar, 0) * SA.NetSatisMiktar) /
NULLIF(SA.NetSatisCirosu, 0) * 100, 1
) AS VARCHAR) + '%' AS [Kâr Marjı %],
-- Acil Uyarı Sistemi
CASE
WHEN SA.NetSatisCirosu - ISNULL(A.[GecerliTutar Toplam], 0) / NULLIF(A.ParaliAlinanMiktar, 0) * SA.NetSatisMiktar < 0
THEN 'ZARARINA SATIS'
WHEN ((SA.NetSatisCirosu - ISNULL(A.[GecerliTutar Toplam], 0) / NULLIF(A.ParaliAlinanMiktar, 0) * SA.NetSatisMiktar) / NULLIF(SA.NetSatisCirosu, 0)) < 0.15
THEN 'MARJ <%15 (RISK)'
ELSE 'KARLI ISLEM'
END AS [Durum Bildirgesi]
FROM GercekSatisCirosu SA
INNER JOIN STOKLAR S WITH (NOLOCK) ON SA.sth_stok_kod = S.sto_kod
LEFT JOIN SafAlisMaliyeti A ON SA.sth_stok_kod = A.sth_stok_kod
WHERE SA.NetSatisMiktar > 0 -- Hiç satılmayan (veya sadece iade olan) net miktar varsa listeleme
ORDER BY [Net Brüt Kâr] ASC; -- Özellikle zararları en üstte görmek için!
Performans / Ölçekleme Testi
AstaFlow dashboard mimarisinde, kâr analizlerinin anlık çalışması istenir. Bu sorgu son 1 yıl (sth_tarih >= DATEADD(YEAR, -1, GETDATE())) filtresi konulduğunda son derece performanslı çalışır (0.8 - 1.5 saniye). Ancak tarihsiz çalıştırılır ve 10 yıllık hareketler taranırsa, RAM üzerinde (Spooling) ciddi yük oluşturur. Kesinlikle tarih limitleri ile uygulanmalıdır.
Edge Cases (İstisnai Durumlar)
- FIFO (İlk Giren İlk Çıkar) Saplantısı: Yukardaki sorgu Ağırlıklı Ortalama Maliyet (Moving Average) yaklaşımıdır. Enflasyonun yüksek olduğu bizim gibi ekonomilerde ortalama maliyet yerine FIFO zorunludur. Ancak saf FIFO hesaplaması SQL’de son derece karmaşık cursor döngüleri gerektirir. Çözüm: Sorguya maliyet hesabını son 3 ayın alımlarıyla sınırlandıran bağımsız bir CTE eklemektir.
- Dövizli Alım - TL Satış Kur Makası: Kâr raporlarındaki gizli katil kur hareketleridir. Ürünü 1000$‘a alıp (O günkü kurla 30.000 TL), bugün 35.000 TL’ye saatığınızda SQL size “5000 TL Kâr Gözüküyor” diyecektir. Oysa dolar an itibariyla 34 TL olmuştur (1000$ = 34.000 TL). Gerçek kâr zarara dönmüştür! İleri düzey kâr analizi, geçmiş ciro tutarını o günün kuruyla dövize sabitleyip hesaplanmalıdır.
Bu Bilgiyi Nereden Biliyoruz? (Kaynaklar)
- AstaFlow Case Study: Satış Performans ve Ciro Analizi Uygulamaları
- Mikro ERP DB API: STOK_HAREKETLERI Tablosu Detayları
- İlgili Çözüm: Mikro ERP Son Alış Fiyatı Analizi