rechnungskit — فاکتور الکترونیکی آلمان در پایتون

کتابخانه‌ای تایپ‌شده برای ساخت، سریال‌سازی، جاسازی و اعتبارسنجی XRechnung و ZUGFeRD روی مدل معنایی EN 16931، با گزارش خطای ماشین‌خوان.

نتیجه

۲۱۷ قاعده پیاده‌سازی‌شده — ۱۸۵ تای EN 16931 و ۳۲ تای BR-DE — با ۲۱۴ تست و mypy strict؛ و فهرستی صریح از آنچه پوشش داده نشده، به‌جای قبولِ خاموش.

مسئله

از ژانویه‌ی ۲۰۲۵ دریافت فاکتور الکترونیکی ساختاریافته در B2B آلمان اجباری شده و صدورش هم مرحله‌به‌مرحله اجباری می‌شود. عملاً هر فاکتور باید هم‌زمان سه لایه را راضی کند: مدل معنایی EN 16931، تنگ‌ترکردن ملی‌اش (XRechnung 3.0 و قواعد BR-DE-*)، و یک نحو — همان مدل، نوشته‌شده یا به UBL 2.1 یا به CII D16B، با نام عنصر متفاوت برای تک‌تک فیلدها. فاکتور ممکن است XML خام باشد یا PDF هیبرید، به هر نحوی که فرستنده پسندیده. اشتباه‌کردن هم شکست نرمی نیست: فاکتور در پورتال توسط یک اعتبارسنج خودکار رد می‌شود که شناسه‌ی قاعده‌ای مثل BR-CO-13 را جلوی تو می‌گذارد، و باید بدانی یعنی چه و کجا اتفاق افتاده.

یک پکیج لایه‌لایه که وابستگی‌هایش فقط یک‌طرفه‌اند. domain هیچ فایلی نمی‌خواند، هیچ سوکتی باز نمی‌کند، هیچ XMLای پارس نمی‌کند و به هیچ ساعتی نگاه نمی‌کند؛ syntax و validation و pdf رویش می‌نشینند، و CLI و سرویس HTTP پوسته‌های نازکی روی هر دو هستند.

قاعده‌ها روی مدل اجرا می‌شوند، نه روی XPath. بیشتر ابزارهای این حوزه Schematron رسمی را می‌پیچند که به‌ازای هر نحو نوشته شده — یعنی هر قاعده دو بار وجود دارد و فاکتور فقط بعد از سریال‌سازی قابل بررسی است. اینجا هر قاعده یک تابع خالص کوچک است از Invoice به مشکلاتی که پیدا می‌کند. یک قاعده هم UBL را پوشش می‌دهد هم CII، فاکتور را می‌شود پیش از آنکه سندی وجود داشته باشد اعتبارسنجی کرد، و GET /v1/rules از خود قاعده‌ها ساخته می‌شود نه از فهرست دومی که باید هم‌گام نگه داشته شود.

اجباری‌بودن خاصیتِ استاندارد است، نه خاصیتِ سیستم تایپ. Invoice.number از نوع str | None است، با اینکه BR-02 وجودش را لازم می‌داند: برای گزارش BR-02 اول باید بتوانی سندی را نگه داری که شماره‌ی فاکتور ندارد. مدلی که قبولش نکند فقط می‌تواند خطای پارس بدهد، و خطای پارس به تماس‌گیرنده نمی‌گوید کدام قاعده شکسته. به همین دلیل کدها رشته‌ی ساده‌اند — فاکتوری که "XX" را به‌عنوان دسته‌ی مالیاتی آورده باید آن‌قدر زنده بماند که به‌شکل BR-CL-18 گزارش شود.

پول Decimal است و float رد می‌شود، نه تبدیل. فاکتوری که یک سنت اختلاف دارد فاکتور ردشده است. دلیل ظریف‌ترش: Decimal تعداد رقم اعشاری که با آن نوشته شده را به یاد می‌آورد، و دقیقاً همین چیزی است که قواعد BR-DEC-* می‌پرسند؛ پس مبلغ‌ها از شکل لفظی خوانده می‌شوند و هرگز نرمال نمی‌شوند — نرمال‌کردن همان عیبی را پاک می‌کند که این قاعده‌ها برای پیداکردنش هستند.

هر نحو یک ماژول است که هر دو جهت را با هم نگه می‌دارد. نگاشت میان یک اصطلاح کسب‌وکاری و مسیر یک عنصر یک واقعیت است؛ تقسیم نویسنده و خواننده بین دو فایل همان راهی است که این دو از هم می‌افتند. تست‌های رفت‌وبرگشت کل مدل را مقایسه می‌کنند نه چند فیلد را، و همان اول جواب دادند: فیلدی که نویسنده‌ی CII می‌انداخت و فیلد دیگری که روی credit note در UBL گم می‌شد.

هر ورودی دشمن فرض می‌شود: <!DOCTYPE> پیش از پارس رد می‌شود، بدون شبکه و بدون DTD، با سقف برای بایت و عمق و تعداد عنصر، و در کل پکیج فقط یک فراخوانی پیکربندی‌شده‌ی etree.fromstring وجود دارد. خطاها RFC 9457 هستند و دو قرارداد عمدی‌اند: تولید حاضر نیست فاکتور ناسازگار بنویسد، و اعتبارسنجی هرگز امتناع نمی‌کند — پیداکردن پنجاه خطا یک درخواست موفق است، پس 200 با valid: false.

محدودیت‌های شناخته‌شده به‌جای اینکه بماند تا کسی کشفشان کند، اسم برده شده‌اند: خانواده‌های قاعده‌ای که پیاده نشده‌اند، نبودِ اعتبارسنجی XSD، نبودِ Peppol. چون اعتبارسنجی که آنچه را بررسی نکرده بی‌صدا قبول کند، از نبودِ اعتبارسنج بدتر است.