İçindekiler
Pazartesi sabahı, saat dokuz. Birisi kaynak sistemden veriyi dışa aktarıyor, geçen haftanın Excel şablonuna yapıştırıyor, pivot tabloları yeniliyor, birkaç rakamı elle düzeltiyor ve on buçukta “Haftalık Satış Raporu” konulu e-postayı gönderiyor. Rapor her hafta gidiyor; ta ki o kişi izne çıkana, şablondaki bir formül bir satır kayana ya da iki departman aynı toplantıya aynı KPI için iki farklı rakamla gelene kadar.
Bu tablo neredeyse her şirkette var. Görünen maliyeti zaman. Görünmeyen maliyeti daha büyük: Rakamlar kişiye bağlıdır, KPI tanımları birinin Excel dosyasında saklıdır ve raporun doğruluğu o sabah kimin hazırladığına göre değişir.
Bu yazıda yönetim raporlarını uçtan uca otomatiğe bağlamanın kurgusunu anlatıyorum: raporu tasarlamak, veriyi çekmek, göndermeden önce doğrulamak, HTML özet ve Excel eki üretmek, alıcıları yönetmek ve her gönderimi kayıt altına almak. Örnekler Python ve Prefect ile; ama ilkeler hangi araçla kurarsanız kurun geçerli.
Neden otomatikleştirmeli?
Zaman. Haftada iki saat süren bir rapor yılda yüz saati aşar. Beş rapor, neredeyse üç aylık bir mesai eder. Üstelik bu saatler genelde ekibin en deneyimli, en pahalı kişilerinden çıkar.
Tek tanım. Otomatik bir raporda “net satış”ın tanımı bir kişinin Excel formülünde değil, kodda ve sürüm kontrolünde durur. Herkes aynı rakamı aynı tanımla görür; tanım değişirse ne zaman ve neden değiştiği de kayıtlıdır.
Kişiye bağımlılık. İzin, hastalık ya da ayrılık raporu durdurmaz. Rapor, onu hazırlayan kişinin takvimine değil, iş takvimine bağlı olur.
Zamanlama. Rapor, yönetici masaya oturmadan, sabah sekizde gelen kutusunda olur. Pazartesi toplantısı tahminle değil, rakamla başlar.
Ama bir uyarı: Kötü kurulmuş bir otomasyon, elle hazırlanan rapordan daha tehlikelidir. Elle hazırlayan kişi tuhaf bir rakamı fark eder, durur, sorar. Bir script fark etmez; yanlış rakamı her pazartesi aynı özenle gönderir. Bu yüzden bu yazının önemli bir kısmı “göndermeden önce” ile ilgili.
Önce raporu tasarlayın, sonra kodlayın
KPI sözlüğü
Kod yazmadan önce her metriğin tanımını, kaynağını ve sahibini yazın. Raporlamadaki en pahalı hatalar kod hataları değil, tanım anlaşmazlıklarıdır. “Ciro” finans için KDV hariç net tutarken satış ekibi için brüt sipariş tutarı olabilir.
| Metrik | Tanım | Kaynak | Sahibi |
|---|---|---|---|
| Net satış | İade ve iptaller düşülmüş, KDV hariç tutar | Satış tablosu | Finans |
| Sipariş sayısı | İptal edilmemiş benzersiz sipariş | Sipariş tablosu | Satış operasyon |
| Ortalama sepet | Net satış / sipariş sayısı | Türetilmiş | Finans |
| Yeni müşteri | İlk siparişini bu dönemde veren müşteri | CRM | Pazarlama |
Bu tabloyu metriğin sahibiyle birlikte doldurun. Sahibi olmayan bir metrik, ilk tutarsızlıkta kimsenin savunmadığı bir rakama dönüşür.
Okuyucuyu tanıyın
Bir yönetici rapor e-postasını ortalama yarım dakikada okur. Bu yüzden:
- İlk ekranda üç ila beş rakam olsun. Her birinin yanında bir karşılaştırma bulunsun: geçen hafta, geçen yılın aynı haftası ya da hedef. Karşılaştırması olmayan bir rakam bilgi değildir.
- Özet gövdede, detay ekte. E-posta gövdesinde özet ve en fazla birkaç satırlık “dikkat edilmesi gerekenler” bulunsun; tamamı Excel ekinde. Kimse e-postada beş yüz satırlık bir tabloyu okumaz.
- E-posta ile pano rakip değildir. E-posta ritim ve dikkat sağlar: Bilgiyi kişiye kendisi götürür. Pano ise keşif içindir: Kişi merak ettiğinde gider, kırılımlara iner. İkisi de aynı SQL tanımlarından beslenmeli; e-postanın sonuna panoya giden bir bağlantı koymak, ikisini birbirine bağlamanın en kolay yoludur.
Kapanmış dönemi raporlayın
Raporlanan dönem tamamlanmış olmalı. Pazartesi sabahı çalışan haftalık rapor, “bu hafta”yı değil, bir önceki pazartesi–pazar aralığını raporlar. Ayın ilk günü çalışan aylık rapor da bir önceki ayı. Dönem sınırlarını tek bir yerde hesaplayın:
from datetime import date, timedelta
def onceki_hafta(bugun: date) -> tuple[date, date]:
"""Bugünden önceki son tamamlanmış pazartesi–pazar aralığı."""
bu_haftanin_pazartesisi = bugun - timedelta(days=bugun.weekday())
baslangic = bu_haftanin_pazartesisi - timedelta(days=7)
return baslangic, baslangic + timedelta(days=6)
Bu fonksiyon haftanın hangi günü çalışırsa çalışsın son tamamlanmış haftayı verir; rapor bir gün gecikse bile yanlış dönemi raporlamaz. “Bugün”ü de sunucunun saatine göre değil, iş yerinin saat dilimine göre alın: datetime.now(ZoneInfo("Europe/Istanbul")).date(). Sunucu UTC’de çalışıyorsa pazartesi sabahı 01:00, UTC’de hala pazar gecesidir ve rapor bir önceki haftayı raporlar.
Sorguda da dönemin son gününü eksiksiz almak için yarı açık aralık kullanın:
SELECT
SUM(net_tutar) AS net_satis,
COUNT(DISTINCT siparis_id) AS siparis_sayisi
FROM satislar
WHERE siparis_tarihi >= :baslangic
AND siparis_tarihi < :bitis_ertesi -- pazar gününün tamamı dahil
AND durum <> 'IPTAL';
BETWEEN :baslangic AND :bitis yazmak, tarih alanı saat de içeriyorsa pazar gününün yalnızca gece yarısına kadar olan anını kapsar; pazar günü yapılan satışlar rapordan sessizce düşer.
Akışın iskeleti
Bir rapor otomasyonu beş adımdan oluşur:
çek → doğrula → üret (HTML + Excel) → dağıt (e-posta) → kaydet
│
└─ veri sorunu varsa: rapor bekletilir, veri ekibine alarm gider
Prefect ile her adım bir task, rapor da bir flow olur:
from datetime import date, datetime
from zoneinfo import ZoneInfo
from prefect import flow
RAPOR = "haftalik-satis"
@flow(name="haftalik-satis-raporu", retries=0)
def haftalik_satis_raporu(bugun: date | None = None) -> str:
simdi = datetime.now(ZoneInfo("Europe/Istanbul"))
baslangic, bitis = onceki_hafta(bugun or simdi.date())
donem = baslangic.isoformat()
if daha_once_gonderildi(RAPOR, donem):
return "Bu dönem zaten gönderilmiş; atlandı."
kpi = kpi_cek(baslangic, bitis)
gecmis = gecmis_haftalar_cek(baslangic, hafta_sayisi=8)
sorunlar = dogrula(kpi, gecmis, son_yukleme=kaynak_tazeligi(), simdi=simdi)
if any(s.engelleyici for s in sorunlar):
alarm_gonder(RAPOR, donem, sorunlar) # veri ekibine; yöneticilere değil
gonderim_kaydet(RAPOR, donem, durum="bekletildi")
raise RuntimeError(f"{RAPOR} {donem} bekletildi: {sorunlar}")
html = html_ozet(kpi, gecmis, uyarilar=sorunlar)
excel = excel_uret(detay_cek(baslangic, bitis), "Haftalık Satış Raporu", donem)
message_id = rapor_gonder(alicilar(RAPOR), html, excel, donem)
gonderim_kaydet(RAPOR, donem, durum="gonderildi", message_id=message_id)
return f"{donem} raporu gönderildi."
Flow dosyası okununca ne yapıldığı anlaşılmalı; bağlantı detayları, SMTP ayarları ve HTML şablonları kendi fonksiyonlarında durur. Bu, ilk yazıda anlattığım katmanlı yapının aynısı. Aşağıda adımları tek tek açıyorum.
Göndermeden önce doğrulayın
Bu, otomatik bir raporun en önemli adımı. Üç kontrol yapın:
- Boşluk. Dönemde hiç kayıt yoksa rapor gitmez. “Bu hafta satış sıfır” bir iş gerçeği olabilir; ama çok daha olası açıklama, veri akışının durmuş olmasıdır.
- Tazelik. Kaynak verinin en son ne zaman güncellendiğine bakın. Gece çalışan yükleme işi geciktiyse pazar günü eksik görünür; rapor eksik rakamla değil, gecikmeyle gitmelidir.
- Anomali. Bu haftanın değeri son sekiz haftanın ortalamasından çok saparsa bunu işaretleyin.
Burada kritik bir ayrım var: Veri sorunu ile iş sinyalini ayırın. Veri boşsa ya da bayatsa sorun veridedir; rapor bekletilir ve veri ekibi uyarılır. Ama satış gerçekten yüzde kırk düştüyse bu tam olarak yöneticinin en hızlı görmesi gereken şeydir; rapor bekletilmez, düşüş vurgulanarak gönderilir. Anomaliyi engel yapan bir otomasyon, en önemli haberleri geciktirir.
from dataclasses import dataclass
from datetime import datetime, timedelta
@dataclass
class Sorun:
mesaj: str
engelleyici: bool # True: rapor bekletilir. False: rapora uyarı olarak eklenir.
def dogrula(kpi: dict, gecmis: list[dict], son_yukleme: datetime, simdi: datetime) -> list[Sorun]:
sorunlar = []
if kpi["siparis_sayisi"] == 0:
sorunlar.append(Sorun("Dönemde hiç sipariş yok; veri akışını kontrol edin.", True))
if simdi - son_yukleme > timedelta(hours=12):
sorunlar.append(Sorun(f"Kaynak veri {son_yukleme:%d.%m %H:%M} tarihinden beri güncellenmemiş.", True))
if gecmis:
ortalama = sum(hafta["net_satis"] for hafta in gecmis) / len(gecmis)
if ortalama and abs(kpi["net_satis"] - ortalama) / ortalama > 0.4:
sorunlar.append(Sorun("Net satış son 8 haftanın ortalamasından %40'tan fazla sapıyor.", False))
return sorunlar
Eşikleri (on iki saat, yüzde kırk) koda gömmeyin; ilk yazıda anlattığım gibi ayar dosyasında, herkesin görebileceği bir yerde tutun. Her raporun normal dalgalanması farklıdır.
E-posta gövdesi: özet, karşılaştırma, uyarı
E-posta HTML’i yazarken web sayfası yazmadığınızı unutmayın. E-posta istemcileri CSS’in çok eski bir alt kümesini destekler; Outlook’un masaüstü sürümü e-postayı bir kelime işlemci motoruyla çizer. Kurallar:
- Satır içi stil ve tablo düzeni. Flexbox, grid, harici stil dosyası ve JavaScript kullanmayın.
- Uzak görselleri kullanmayın. Çoğu istemci onları varsayılan olarak engeller. Grafik gerekiyorsa e-postaya gömülü bir PNG kullanın ya da grafiği panoya bırakın.
- Düz metin alternatifi ekleyin. HTML’i göstermeyen istemciler ve önizleme satırı için.
- Rakamları Türkçe biçimleyin. Python varsayılan olarak
1,234,567.89yazar; okuyucunuz1.234.567,89bekler.
Biçimleme için küçük bir yardımcı yeterli:
def tr_sayi(deger: float, ondalik: int = 0) -> str:
"""1234567.891 → '1.234.567,89' (ondalik=2)"""
metin = f"{deger:,.{ondalik}f}" # '1,234,567.89'
return metin.replace(",", "_").replace(".", ",").replace("_", ".")
Karşılaştırmayı gösterirken bir tuzağa dikkat edin: Her artış iyi değildir. Satış artışı yeşil olmalı, ama iade oranı ya da müşteri şikayeti artışı kırmızı. Her metrik için “artış iyi mi?” bilgisini KPI sözlüğünde tutun:
def degisim(bu: float, onceki: float, artis_iyi: bool = True) -> str:
if not onceki:
return '<span style="color:#6b6b73">—</span>'
oran = (bu - onceki) / onceki * 100
iyi = (oran >= 0) == artis_iyi
renk = "#1a7f37" if iyi else "#cf222e"
ok = "▲" if oran >= 0 else "▼"
return f'<span style="color:{renk}">{ok} %{tr_sayi(abs(oran), 1)}</span>'
Doğrulama adımında engelleyici olmayan sorunlar çıktıysa (örneğin sert bir düşüş), bunları e-postanın en üstünde, renkli bir kutuda gösterin. Yönetici “bu rakam doğru mu?” diye sormadan önce cevabı orada görmeli: “Net satış son 8 haftanın ortalamasından %42 düşük; kaynak veri günceldir.”
Excel eki: detay ve analiz
Ek dosya detayın asıl yeridir. Ama aynı zamanda raporun en riskli parçasıdır: Bir e-posta eki, kontrolünüzden çıkan bir veri kopyasıdır. İletilir, indirilir, kişisel bilgisayarlara kaydedilir ve yıllarca gelen kutularında durur. Bu yüzden önce neyin dosyaya gireceğine, sonra nasıl görüneceğine karar verin.
Önce veri güvenliği: ham veri dışarı çıkmaz
Satış verisinde müşteri adı, abone adı, telefon ya da e-posta adresi Excel dosyalarında dolaşmamalı. KVKK’nın temel ilkelerinden biri, kişisel verinin “işlendikleri amaçla bağlantılı, sınırlı ve ölçülü” olmasıdır. Bir yönetim raporunun amacı için müşterinin adına ihtiyaç yoktur. Beş kural:
1. Toplulaştırın. Yönetim raporu müşteri seviyesinde satır içermez; bölge, ürün kategorisi, kanal ya da hafta gibi kırılımlarla gelir. Ham veriye gerçekten ihtiyaç duyan yetkili kişi, onu panodan ya da kaynak sistemden kendi yetkisiyle görür.
2. İzin listesi kullanın, yasak listesi değil. Excel’e yazılacak sütunları tek tek tanımlayın; listede olmayan hiçbir alan dosyaya girmesin. “Hassas sütunları çıkar” yaklaşımı, kaynak tabloya yeni bir sütun eklendiği gün delinir; izin listesi ise yeni sütunu varsayılan olarak dışarıda bırakır. Rapor sorgularında SELECT * kullanmayın.
3. Küçük grupları gizleyin. Toplulaştırılmış veri bile kişiyi ele verebilir: “İzmir – Kurumsal – 2 müşteri” satırı, o iki müşterinin ne kadar alışveriş yaptığını fiilen gösterir. Az sayıda kişiden oluşan grupları rapora yazmayın:
SELECT
bolge,
urun_kategori,
COUNT(DISTINCT musteri_id) AS musteri_sayisi,
COUNT(DISTINCT siparis_id) AS siparis_sayisi,
SUM(net_tutar) AS net_satis
FROM satislar
WHERE siparis_tarihi >= :baslangic
AND siparis_tarihi < :bitis_ertesi
AND durum <> 'IPTAL'
GROUP BY bolge, urun_kategori
HAVING COUNT(DISTINCT musteri_id) >= 5; -- 5'ten az müşteriden oluşan grup yazılmaz
Gizlenen gruplar yüzünden satır toplamı genel toplamı tutmayacaktır. Bunu dosyada tek satırlık bir notla belirtin ya da gizlenen grupları “Diğer” adlı tek bir satırda birleştirin; okuyucu toplamların neden farklı olduğunu merak etmesin.
4. Kişi seviyesi şartsa takma ad ya da maskeleme kullanın. Bazen kişi seviyesine inmek gerçekten gerekir; örneğin bir satış temsilcisine kendi müşterilerinin listesini gönderirken. Bu durumda müşteri numarasının yerine bir takma ad, adın yerine maskelenmiş bir hali kullanın:
import hashlib
import hmac
def takma_ad(musteri_id: str, gizli_anahtar: bytes) -> str:
"""Aynı müşteri her raporda aynı takma adı alır; takma addan kimliğe dönülemez."""
ozet = hmac.new(gizli_anahtar, musteri_id.encode(), hashlib.sha256).hexdigest()
return f"MUS-{ozet[:10].upper()}"
def maskele(ad_soyad: str) -> str:
"""'Ayşe Yılmaz' → 'Ay*** Yı***'"""
return " ".join(parca[:2] + "***" for parca in ad_soyad.split())
İki önemli ayrıntı. Birincisi, müşteri numarasını anahtarsız bir özet fonksiyonundan (örneğin düz SHA-256) geçirmek yeterli değildir: Müşteri numaraları tahmin edilebilir olduğu için tüm olasılıklar denenerek geri çözülebilir. Gizli anahtarlı HMAC bunu engeller; anahtar da kodda değil, gizli bilgi yöneticisinde durur. İkincisi, maskeleme anonimleştirme değildir: İl, yaş grubu ve tutar gibi başka sütunlarla birleşince kişi yine tanınabilir. Takma ad ya da maskeleme ancak toplulaştırma mümkün olmadığında başvurulacak bir yedek çözümdür.
5. Gizlemek silmek değildir. Gizli sütun, beyaz yazı, sayfa koruması ya da Excel parolası veri güvenliği sağlamaz. Veri dosyanın içindedir ve birkaç tıklamayla görünür hale gelir. Güvenli olan tek şey, veriyi dosyaya hiç koymamaktır.
Kaliteli bir Excel çıktısı
Alıcının dosyayı açtığında hiçbir şeyi düzeltmeden kullanabilmesi gerekir:
- Başlık ve bağlam. En üstte rapor adı ve dönem, altında dosyanın ne zaman üretildiği ve “kişisel veri içermez” notu. Dosya iletildiğinde bile ne olduğu anlaşılsın.
- Gerçek bir Excel tablosu. Şeritli satırlar ve hazır filtre gelir; alıcı tek tıkla sıralar, filtreler, pivot yapar.
- Sütun bazında biçim. Tutarlar TL, oranlar yüzde, adetler binlik ayraçlı sayı olarak görünür. Rakamlar metin olarak değil sayı olarak yazılır; Türkçe Excel
#,##0.00biçimini kendiliğinden1.234,56olarak gösterir. Yüzdeleri0,125gibi kesir olarak yazın;0.0%biçimi onu%12,5olarak gösterir. - Okunabilir düzen. Sütun genişlikleri içeriğe göre ayarlanır, başlık satırı sabitlenir, sayfa yatay yazdırılır.
- Bellekte üretim. Diske geçici dosya yazmak, temizlenmediğinde diski doldurur; aynı anda çalışan iki rapor aynı dosya adını kullanırsa birbirinin çıktısını ezer.
İzin listesi ile biçimleri aynı yerde tanımlamak, iki işi birden görür: Hangi sütunun dosyaya gireceği de, nasıl görüneceği de tek bir sözlükte, kod incelemesinde görünür halde durur:
from datetime import datetime
from io import BytesIO
from openpyxl import Workbook
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.table import Table, TableStyleInfo
# İzin listesi: Excel'e sadece burada tanımlı sütunlar, bu sırayla, bu başlık
# ve bu biçimle yazılır. Listede olmayan hiçbir alan dosyaya girmez.
SUTUNLAR = {
"bolge": ("Bölge", None),
"urun_kategori": ("Ürün kategorisi", None),
"musteri_sayisi": ("Müşteri", "#,##0"),
"siparis_sayisi": ("Sipariş", "#,##0"),
"net_satis": ("Net satış", '#,##0.00 "TL"'),
"degisim": ("Geçen haftaya göre", "0.0%"),
}
def excel_uret(satirlar: list[dict], baslik: str, donem: str) -> bytes:
if not satirlar:
raise ValueError("Boş rapor için Excel üretilmez.")
anahtarlar = [anahtar for anahtar in SUTUNLAR if anahtar in satirlar[0]]
wb = Workbook()
wb.properties.title = f"{baslik} – {donem}"
wb.properties.creator = "Raporlama otomasyonu"
ws = wb.active
ws.title = "Detay"
# Başlık ve bağlam satırları
ws.append([f"{baslik} – {donem}"])
ws["A1"].font = Font(bold=True, size=14)
ws.append([f"{datetime.now():%d.%m.%Y %H:%M} tarihinde otomatik üretildi. "
"Kişisel veri içermez; dağıtımı sınırlıdır."])
ws["A2"].font = Font(italic=True, color="6B6B73")
ws.append([])
# Tablo başlığı 4. satırda, veri altında
ws.append([SUTUNLAR[anahtar][0] for anahtar in anahtarlar])
for satir in satirlar:
ws.append([satir[anahtar] for anahtar in anahtarlar])
ilk, son = 4, 4 + len(satirlar)
# Sütun biçimleri ve genişlikleri
for sira, anahtar in enumerate(anahtarlar, start=1):
harf = get_column_letter(sira)
bicim = SUTUNLAR[anahtar][1]
if bicim:
for satir_no in range(ilk + 1, son + 1):
ws[f"{harf}{satir_no}"].number_format = bicim
en_uzun = max(len(str(ws[f"{harf}{satir_no}"].value or "")) for satir_no in range(ilk, son + 1))
ws.column_dimensions[harf].width = min(en_uzun + 4, 40)
# Excel tablosu: şeritli satırlar ve hazır filtre
alan = f"A{ilk}:{get_column_letter(len(anahtarlar))}{son}"
tablo = Table(displayName="DetayTablosu", ref=alan)
tablo.tableStyleInfo = TableStyleInfo(name="TableStyleMedium2", showRowStripes=True)
ws.add_table(tablo)
ws.freeze_panes = f"A{ilk + 1}"
ws.page_setup.orientation = "landscape"
buffer = BytesIO()
wb.save(buffer)
return buffer.getvalue()
satirlar listesi yukarıdaki toplulaştırılmış sorgudan gelir. Sorgu bir gün yanlışlıkla musteri_adi sütununu da döndürse bile, izin listesinde olmadığı için dosyaya yazılmaz.
Dağıtım: alıcılar ve gönderim
Alıcı listeleri iş biriminin elinde olsun
“Bu rapor kime gidiyor?” sorusunun cevabı sık değişir: biri ekibe katılır, biri ayrılır, bir müdür raporu yardımcısına da ister. Alıcıları koda gömerseniz her değişiklik bir geliştirme talebine dönüşür. Basit bir tablo yeterli: rapor adı, e-posta adresi, alıcı tipi (to ya da cc), aktif mi? Bu tabloyu düzenleyebilecekleri basit bir yönetim ekranı (örneğin Directus gibi bir araçla birkaç dakikada kurulabilir) iş birimini sizin iş kuyruğunuzdan çıkarır.
Ayrılan bir çalışanın adresini silmek yerine pasif yapın; kimin hangi raporu ne zamana kadar aldığı kayıtta kalsın. Kaynak sistemden gelen adresleri de göndermeden önce doğrulayın; boş ya da bozuk bir adres tüm gönderimi düşürmesin.
Kişiye özel dağıtım
Herkese giden bir liste, kimsenin sorumluluğunda değildir. Elli satırlık ortak bir “geciken işler” listesi göz ardı edilir; “sizin üç işiniz” başlıklı bir e-posta işlem görür. Aynı akışla, veriyi sorumluya göre gruplayıp her kişiye kendi listesini gönderebilirsiniz: bölge müdürüne kendi bölgesi, satış temsilcisine kendi müşterileri. Bir alıcıya gönderim başarısız olursa diğerleri devam etmeli; hata, döngünün içinde yakalanıp kaydedilmeli.
Gönderim
Python’un standart kütüphanesi bu iş için yeterli. HTML gövde, düz metin alternatifi ve Excel eki tek bir mesajda:
import smtplib
from email.message import EmailMessage
from email.utils import make_msgid
def rapor_gonder(alicilar: list[str], konu: str, html: str, duz_metin: str,
ek: bytes, ek_adi: str, smtp: dict) -> str:
msg = EmailMessage()
msg["From"] = smtp["gonderen"]
msg["To"] = ", ".join(alicilar)
msg["Subject"] = konu
message_id = make_msgid(domain=smtp["alan_adi"])
msg["Message-ID"] = message_id
msg.set_content(duz_metin) # HTML göremeyen istemciler için
msg.add_alternative(html, subtype="html")
msg.add_attachment(
ek,
maintype="application",
subtype="vnd.openxmlformats-officedocument.spreadsheetml.sheet",
filename=ek_adi,
)
with smtplib.SMTP(smtp["host"], smtp["port"]) as sunucu:
sunucu.starttls()
sunucu.login(smtp["kullanici"], smtp["sifre"])
sunucu.send_message(msg)
return message_id
SMTP bilgilerini koda yazmayın; ilk yazıda anlattığım gibi bir Secret block’tan ya da şirketinizin gizli bilgi yöneticisinden okuyun. Konu satırına dönemi yazın (“Haftalık Satış Raporu – 22–28 Eylül”); alıcı aradığında bulsun.
”Rapor gitti mi?” sorusunu kayda bağlayın
Rapor otomasyonunun en sık duyulan cümlesi “rapor gelmedi”dir. Cevabı tahmin değil, kayıt olmalı. Her gönderimi, başarılı ya da başarısız, bir tabloya yazın:
CREATE TABLE rapor_gonderim (
id BIGSERIAL PRIMARY KEY, -- SQL Server'da: BIGINT IDENTITY
rapor TEXT NOT NULL,
donem TEXT NOT NULL,
zaman TIMESTAMPTZ NOT NULL DEFAULT now(),
alici_sayisi INT,
durum TEXT NOT NULL, -- 'gonderildi' | 'hata' | 'bekletildi'
hata TEXT,
message_id TEXT
);
-- Aynı dönemin raporu iki kez "gönderildi" olarak kaydedilemez.
CREATE UNIQUE INDEX rapor_gonderim_tekil
ON rapor_gonderim (rapor, donem)
WHERE durum = 'gonderildi';
Bu tablo iki iş görür:
- Destek diyaloğunu bitirir. “Rapor 08:00’de üç alıcıya gönderilmiş görünüyor, spam klasörünüze bakar mısınız?” Sadece başarılı gönderimleri kaydederseniz, “gelmedi” şikayetinde elinizde sessizlikten başka bir şey olmaz; başarısızlık kaydı sessizlikten çok daha bilgilendiricidir.
- Aynı raporun iki kez gitmesini engeller. Flow’un başındaki
daha_once_gonderildikontrolü bu tabloya bakar. Flow yeniden denense ya da biri elle tetiklese de yönetici aynı raporu iki kez almaz. Koşullu benzersiz indeks (SQL Server’da filtreli indeks) aynı kuralı veritabanı seviyesinde de garanti eder.
Gönderim kaydını e-posta gider gitmez yazın. Gönderim ile kayıt arasında süreç çökerse çok küçük bir ihtimalle aynı rapor iki kez gidebilir; bu, raporun hiç gitmemesinden her zaman daha iyidir.
Zamanlama ve hata yönetimi
Raporu iş yerinin saat dilimiyle zamanlayın. Pazartesi 07:30 için:
from prefect.schedules import Cron
haftalik_satis_raporu.serve(
name="haftalik-satis",
schedule=Cron("30 7 * * 1", timezone="Europe/Istanbul"),
)
Hata durumunda üç kural:
- Rapor ve alarm ayrı sözleşmelerdir. Yöneticiye hata ayrıntısı gönderilmez; iş birimine stack trace göndermek, iki hafta içinde e-postalarınızın filtrelenmesiyle sonuçlanır. Hata alarmı veri ekibine gider. İsterseniz yöneticiye sadece “Bu haftanın raporu veri gecikmesi nedeniyle öğleden sonra gelecek” gibi kısa bir not gönderin.
- Sessizliği yorumlamak zorunda kalmayın. “Hiç e-posta gelmiyor” iki şey anlamına gelebilir: Her şey yolunda ya da sistem hiç çalışmıyor. Raporun 08:00’e kadar gönderilmediğini fark eden bir kural kurun. Prefect’in “beklenen bir olay gelmezse tetiklen” türündeki otomasyonları ya da gönderim tablosuna bakan küçük bir kontrol bu iş için yeterli.
- Retry’ı doğru yere koyun. Veri çekme adımı geçici bağlantı hatalarında yeniden denenebilir. Gönderim adımı ise ancak yukarıdaki tekrar gönderim kontrolü varsa güvenle yeniden denenir.
PDF mi, pano mu, e-posta mı?
- E-posta (HTML + Excel): Haftalık ve aylık yönetim raporlarının çoğu için doğru varsayılan. Hızlı okunur, ek üzerinde analiz yapılabilir.
- PDF: Biçimin birebir korunması gereken, imzalanan, arşivlenen ya da dışarıya gönderilen raporlar için. Maliyeti yüksektir: Genelde bir headless tarayıcıyla çizilir; Türkçe karakterler için doğru fontları kurmanız, sayfa kırılmalarını yönetmeniz gerekir. “Birebir aynı görünsün” isteği gerçekten bir ihtiyaç değilse PDF’e girmeyin.
- Pano (Power BI, Metabase, Superset vb.): Keşif ve derin inceleme için. E-postanın yerine değil, yanına koyun; e-posta dikkat çeker, pano soruyu cevaplar.
Kontrol listesi
- Her KPI’ın tanımı, kaynağı ve sahibi yazılı.
- Rapor kapanmış dönemi raporluyor; dönem iş yerinin saat dilimine göre hesaplanıyor.
- Boşluk, tazelik ve anomali kontrolleri var; veri sorunu raporu bekletiyor, iş sinyali uyarı olarak görünüyor.
- E-posta gövdesi yarım dakikada okunuyor ve her rakamın yanında bir karşılaştırma var.
- Excel’e sadece izin listesindeki sütunlar yazılıyor; rakamlar sayı olarak duruyor.
- Ekte müşteri seviyesinde ham veri ve kişisel veri yok; küçük gruplar gizleniyor.
- Alıcı listeleri koddan bağımsız ve iş birimi tarafından yönetilebiliyor.
- Her gönderim kayıt altında ve aynı dönem iki kez gönderilemiyor.
- Hata alarmı veri ekibine gidiyor, yöneticiye değil.
- Rapor zamanında gitmezse birileri haberdar oluyor.
Bonus: Bir yapay zeka asistanına kurdurmak
Aşağıdaki prompt’u Claude Code, Cursor ya da benzeri bir kodlama asistanına verebilirsiniz. Köşeli parantezli yerleri kendi raporunuza göre doldurun:
Sen kıdemli bir veri mühendisisin. Python ve Prefect 3 ile haftalık bir yönetim
raporu otomasyonu kuracağız. Aşağıdaki kurallara harfiyen uy; bir kuralı
esnetmen gerekirse önce bana sor.
## Rapor
- Ad: [ör. haftalik-satis]
- KPI'lar: [ör. net satış, sipariş sayısı, ortalama sepet, yeni müşteri].
Her biri için tanım, kaynak tablo ve "artış iyi mi?" bilgisi:
[KPI sözlüğünü buraya yapıştır]
- Kaynak veritabanı: [ör. PostgreSQL / SQL Server]
- Zamanlama: her pazartesi 07:30, Europe/Istanbul. Rapor bir önceki
pazartesi–pazar aralığını kapsar.
## Kurallar
1. Dönem sınırları tek bir fonksiyonda hesaplanır; "bugün" Europe/Istanbul
saat dilimine göre alınır. Sorgular yarı açık tarih aralığı kullanır.
2. Göndermeden önce doğrula: dönem boşsa veya kaynak verinin son güncellemesi
[12] saatten eskiyse rapor bekletilir ve veri ekibine alarm gider. Net satış
son 8 haftanın ortalamasından [%40]'tan fazla saparsa rapor gönderilir ama
üstte vurgulu bir uyarı kutusu gösterilir. Eşikler ayar dosyasında durur.
3. E-posta gövdesi: satır içi CSS ve tablo düzeni; üstte 3–5 KPI, her birinin
yanında geçen haftaya göre değişim (artışın iyi ya da kötü olmasına göre
renk). Rakamlar Türkçe biçimlenir (1.234.567,89). Düz metin alternatifi de
eklenir. Panoya bir bağlantı: [URL]
4. Excel eki bellekte üretilir (openpyxl, BytesIO). Dosyaya yazılacak sütunlar
bir izin listesinde (sütun → başlık, sayı biçimi) tanımlanır; listede olmayan
alan asla yazılmaz. Veri toplulaştırılmış gelir; 5'ten az müşteriden oluşan
gruplar HAVING ile elenir. Müşteri adı, telefon, e-posta gibi kişisel veri
eke girmez; kişi seviyesi gerekirse HMAC ile takma ad kullanılır (anahtar
Secret block'ta). Dosyada başlık ve dönem satırı, şeritli bir Excel tablosu,
sütun bazında TL/yüzde/sayı biçimi, otomatik sütun genişliği ve sabit başlık
satırı olur.
5. Alıcılar bir veritabanı tablosundan okunur (rapor, eposta, tip, aktif).
6. Her gönderim rapor_gonderim tablosuna yazılır. (rapor, dönem) için başarılı
bir kayıt varsa rapor tekrar gönderilmez; bunu koşullu benzersiz indeksle
veritabanında da garanti et.
7. SMTP bilgileri Prefect Secret block'tan okunur. Hata alarmı yalnızca veri
ekibine gider; yöneticilere hata ayrıntısı gönderilmez.
## İlk görev
Klasör yapısını, SQL tablolarını, flow'u ve deployment tanımını oluştur. Örnek
veriyle yerelde çalıştırılabilir olsun. Her adımdan sonra ne yaptığını kısaca
özetle ve varsayım yaptığın yerleri açıkça belirt.
Özet
- Otomasyondan önce tanım gelir: KPI sözlüğü, okuyucu ve kapanmış dönem.
- Göndermeden önce doğrulayın: Veri sorunu raporu bekletir, iş sinyali uyarı olarak görünür.
- Özet gövdede, detay ekte: Satır içi stil, Türkçe rakamlar, her rakamın yanında bir karşılaştırma; Excel’de sayılar sayı olarak.
- Ham veri dışarı çıkmaz: Excel’e sadece izin listesindeki toplulaştırılmış sütunlar girer; müşteri adı, abone adı gibi kişisel veriler dosyada dolaşmaz, küçük gruplar gizlenir. Gizlemek silmek değildir.
- Alıcılar iş biriminin elinde, gönderimler kayıt altında ve aynı rapor iki kez gitmiyor.
- Rapor ile alarm ayrı: Yönetici rakam görür, veri ekibi hata.
Otomatik bir raporun asıl değeri zaman kazancı değil, güvendir: Yönetici pazartesi sabahı gelen rakama, o hafta kimin hazırladığını düşünmeden güvenebilmeli.