E-Invoice
PDF faturayı XRechnung e-faturasına dönüştürür ve kural kural neyin yanlış olduğunu açıklar.
- Rol
- Kişisel proje — tasarım, kod, testler
- Yıl
- 2026
- Kategori
- Yapay zekâ · Web
- Teknolojiler
- Python 3.12, FastAPI, Jinja2, Anthropic API

Sorun
1 Ocak 2025'ten beri Almanya'da şirketler arasındaki faturaların e-fatura olması gerekiyor: PDF değil, Avrupa standardı EN 16931'e uyan yapılandırılmış bir format. Kâğıt ve PDF'e 31 Aralık 2026'ya kadar, önceki yıl cirosu 800.000 euroyu geçmeyen şirketler için ise 31 Aralık 2027'ye kadar hâlâ izin var. Almanın geçiş süresi yok: 1 Ocak 2025'ten beri her şirketin e-fatura alabilmesi gerekiyor.
Birçok küçük şirket faturalarını hâlâ Word'de yazıp PDF olarak gönderiyor. Bir PDF insana doğru görünüp makine için yine de yanlış olabilir: tutmayan toplamlar, eksik alıcı referansı, yazım hatalı bir IBAN. Avrupa'da çalışmak istiyorum ve Almanya'da şu anda her şirket bu değişimden geçiyor; bu yüzden muhasebeci olsam kullanmak isteyeceğim dönüştürücüyü yaptım.
Çözüm
- PDF'in metin katmanı, kelimelerin konumlarından satırlara ve tablo hücrelerine yeniden kuruluyor; böylece bir tablo satırı satır olarak kalıyor.
- Claude faturayı sabit bir JSON şemasına okuyor (structured output). Ona basılı olanı kopyalaması ve rakamları düzeltmemesi söyleniyor: "düzeltilmiş" bir toplam, doğrulayıcının bulmak için var olduğu hatayı gizlerdi.
- API anahtarı olmadan kural tabanlı bir okuyucu, hazır Almanca ve İngilizce şablonları çevrimdışı okuyor (demo modu).
- PDF'te basılı olmayan ama standardın istediği değerler hesaplanıyor ve "derived" olarak işaretleniyor; böylece "hesaplandı" asla "kontrol edildi" ile karışmıyor.
- Resmî kimlikleriyle 67 iş kuralı: 51'i EN 16931'den, 15'i XRechnung'dan, 1'i benim. Her ihlal edilen kural kimliğini ve rakamları gösteriyor: "BR-CO-10: the lines add up to 882.30, the invoice says 882.80".
- Sonuç, web uygulamasından ya da komut satırından indirilebilen bir XRechnung 3.0 UBL 2.1 dosyası. SQLite geçmişi her faturayı, durumunu ve XML'ini saklıyor.
Ekran görüntüleri



Teknolojiler
- Python 3.12
- FastAPI
- Jinja2
- Anthropic API
- pdfplumber
- lxml
- SQLite
- pytest
Para her yerde Decimal ve resmî kurallardaki XPath round() gibi yarımlar yukarı
yuvarlanıyor; Python'un round() fonksiyonu 0,125'i 0,12'ye yuvarlar. LLM şeması tutarları metin
olarak döndürüyor, böylece hiçbir zaman float'tan geçmiyorlar. Referans, resmî KoSIT
doğrulayıcısı (Java). Kendi kural alt kümemin ne kadar yakın olduğunu görmek için bir betik, her
örneğin XML'inde resmî UBL şemasını, CEN EN 16931 schematron'unu ve KoSIT XRechnung
schematron'unu çalıştırıyor.
Öne çıkan kod
Her kural, resmî kimliği ve metniyle kaydedilen küçük bir fonksiyon. Fatura geçerse None,
geçmezse gerçek rakamları içeren bir cümle döndürüyor. Bir test her kuralı bilerek bozuyor;
başka bir test de yeni bir kuralın böyle bir durumu yoksa başarısız oluyor:
@rule(
"BR-CO-15",
"Invoice total amount with VAT (BT-112) = Invoice total amount without VAT (BT-109) + "
"Invoice total VAT amount (BT-110).",
)
def br_co_15(inv: Invoice) -> str | None:
t = inv.totals
if t.tax_inclusive is None or t.tax_exclusive is None or t.vat_total is None:
raise NotApplicable
expected = _xpath_round(t.tax_exclusive + t.vat_total)
if t.tax_inclusive == expected:
return None
return (
f"{t.tax_exclusive:f} + {t.vat_total:f} = {expected:f}, "
f"but the total with VAT is {t.tax_inclusive:f}"
)Bulduğum ve düzelttiğim hata
Benim BR-CO-13 kuralım, resmî kuralların görmediği bir hata buluyordu
Ne bozuldu
Ara toplamı yanlış olan örnekte (satırların toplamı 882,30, PDF'te 882,80 yazıyor) doğrulayıcım üç hata gösteriyordu. Aynı XML üzerinde çalışan resmî CEN schematron'u ise iki hata gösterdi: BR-CO-10 ve BR-CO-15, ama BR-CO-13 değil.
Asıl neden
BR-CO-13'ü kural metninden uygulamıştım: "KDV hariç toplam = satır net tutarlarının toplamı − indirimler + ek ücretler". Resmî test satırları toplamıyor. İndirim ve ek ücret yoksa KDV hariç toplamı (BT-109), faturada basılı olan satır toplamıyla (BT-106) karşılaştırıyor. Satırları BT-106 ile karşılaştırmak BR-CO-10'un işi; bu yüzden tek bir yazım hatası raporda iki kez görünüyordu ve raporum alıcının doğrulayıcısının söyleyeceğiyle örtüşmüyordu.
Çözüm
Artık schematron'daki testi izliyorum ve bir regresyon testi bunun böyle kalmasını sağlıyor. O zamandan beri her aritmetik kuralın yalnızca tek satırlık metnini değil, resmî testini de okuyorum. Beş örneğin hepsinde doğrulayıcım ve resmî schematron artık birebir aynı kural kimliklerini bildiriyor.
expected = _xpath_round(_sum(line.net_amount for line in inv.lines))
if inv.totals.tax_exclusive == expected:
return None# The official schematron compares BT-109 with BT-106 (LineExtensionAmount), not with the
# sum of the lines: that sum is BR-CO-10's job.
if t.tax_exclusive == t.line_net_total:
return NoneSonuç
- 192 otomatik test: her örnek şablonda okuma, 67 kuralın her birinin hem geçmesi hem ihlal edilmesi,
Decimalyuvarlama, öğe öğe geri ayrıştırılan XML, web uygulaması ve komut satırı. - Beş örneğin hepsinde resmî CEN ve KoSIT schematron'u doğrulayıcımla birebir aynı kural kimliklerini bildiriyor ve her XML dosyası UBL 2.1 şemasına göre geçerli.
- Bir örneğin çevrimdışı dönüştürülmesi (okuma, çıkarma, 67 kural, XML) 16–29 ms sürüyor.