مكتبة الفاتورة الإلكترونية المصرية (ETA)
طبقة برمجية للتعامل مع منظومة الفاتورة الإلكترونية المصرية — بمعايير مُتحقَّقة من عيّنة الهيئة الرسمية، لا من الاجتهاد.
لماذا
| القياس | الرقم |
|---|---|
| حزم npm لمنظومة ETA | صفر |
| مستودعات ETA على GitHub مقابل ZATCA | 5 مقابل 627 |
المرجع الرسمي EInvoicingSigner |
مهجور منذ 30 يناير 2024 — 26 مشكلة مفتوحة، 63 نسخة مشتقة |
63 نسخة مشتقة من مرجع متروك تعني 63 فريقاً لم يجدوا بديلاً.
والوضع النظامي مفتوح لهذا بالذات: المصلحة تخاطب «مطوري الممولين»
نصاً في أدلتها، وترخيص «مقدم الخدمة» يخصّ طبقة الوساطة لا مورّدي البرمجيات.
التفصيل في research/ETA-LEGAL-CLEARANCE.md.
ما هو مبنيّ
① التسلسل الكنسي والتجزئة — serialization.py
أصعب قطعة في التكامل. مُتحقَّق منه بايتاً ببايت مقابل الملف الرسمي
one-doc-serialized.json.txt المنشور على بوابة الهيئة.
from eta_invoice.serialization import load_document, canonical_hash
doc = load_document(Path("invoice.json").read_text(encoding="utf-8"))
print(canonical_hash(doc)) # D43FAE25B02B7E466FA5AA39E5FA45C7D92661E5B0A0E7964E04783BFF3C2651
الفخّ الذي تقع فيه أغلب النسخ: المواصفة تُلزم بأخذ القيمة «بلا أي
معالجة» — فـ0.0 تبقى 0.0 ولا تصير 0 ولا 0.00. وjson.loads العادي
يحوّلها إلى float فيضيع شكلها اللفظي، فيختلف الهاش عن هاش الهيئة ويُرفض
المستند بلا تشخيص مفيد. الحل هنا: parse_float=str وparse_int=str.
①ب التسلسل الكنسي لصيغة XML — serialization_xml.py
كثير من أنظمة ERP ترسل XML لا JSON، والمواصفة تعرّف لهما خوارزميتين مختلفتين. ومن طبّق خوارزمية JSON على XML حصل على هاش صالح شكلاً خاطئ فعلاً — فرُفض مستنده بلا تشخيص.
| JSON | XML | |
|---|---|---|
| اسم الجذر | لا يُدرَج | يُدرَج — يبدأ الناتج بـ"DOCUMENT" |
| المصفوفات | الاسم يُكرَّر قبل كل عنصر | لا معالجة خاصة — كل وسم يحمل اسمه |
| الاقتباس | لا تهريب | " ⇒ \" — حمايةٌ من تصادم الهاش |
مُتحقَّق بايتاً ببايت من one-doc-serialized.xml.txt الرسمي (3,917 حرفاً).
⚠️ وفخّ موثّق: المسافات بين الوسوم لا أثر لها، والمسافات داخل وسم ورقي لها أثر — لأن القيمة تُؤخذ بلا معالجة. فمن نسّق مستنده بأداة تُضيف مسافات داخل الأوراق، تغيّر هاشه.
② المُحقِّق المحلي — validation.py
يمنع الرفض قبل الإرسال، ويشرح السبب.
from eta_invoice.validation import validate_document, format_report
print(format_report(validate_document(doc)))
[خطأ] invoiceLines[0].salesTotal: إجمالي البيع لا يطابق المحسوب من مكوّناته
المتوقَّع: 947.0 الموجود: 950.0
الإصلاح: salesTotal = quantity × unitValue.amountEGP (الفارق 3.0)
ثماني قواعد، كلها مشتقّة من العيّنة الرسمية ومُختبَرة عليها:
| # | القاعدة |
|---|---|
| 1 | salesTotal = quantity × unitValue.amountEGP |
| 2 | netTotal = salesTotal − discount.amount |
| 3 | totalSalesAmount = Σ salesTotal |
| 4 | totalDiscountAmount = Σ discount.amount |
| 5 | netAmount = totalSalesAmount − totalDiscountAmount |
| 6 | totalItemsDiscountAmount = Σ itemsDiscount |
| 7 | taxTotals[type] = Σ taxableItems لكل نوع |
| 8 | totalAmount = Σ line.total − extraDiscountAmount |
③ بنّاء المستند — builder.py
المُحقِّق يمسك الخطأ بعد وقوعه؛ والبنّاء يمنع وقوعه: يحسب كل حقل مشتقّ
بـDecimal، ويجمّع كل إجماليات المستند، ويفحص ناتجه بنفسه قبل التسليم.
b = InvoiceBuilder(issuer=..., receiver=..., internal_id="INV-1", activity_code="6201")
b.add_line(InvoiceLine(description="خدمة", quantity=1,
unit_price_egp="12000.00", total="13680.00",
taxes=[Tax("T1", rate=14)]))
doc = b.build() # يرمي BuildError لو أخفق فحصه لنفسه
ولا يخترع مالاً: total للبند يأتي من نظامك المحاسبي لأن معادلته
لم تُشتقّ من مواصفة الهيئة. والبنّاء يحسب ما يستطيع إثباته فقط.
ضمانة مُختبَرة: 200 فاتورة عشوائية بمولّد حتمي — كلها تمرّ بالمُحقِّق.
④ كتابة JSON متطابقة مع التجزئة — dump_document
⚠️ الهاش مبنيّ على الشكل النصّي للقيمة. فكاتب JSON يكتب 114 حيث
نجزّئ 114.0 — وكلاهما JSON صحيح — يُغيّر الهاش فيُرفض المستند بلا
تشخيص. فاكتب مستندك بـdump_document، أو اضمن أن كاتبك يطابق تمثيلنا.
⑤ التوقيع الإلكتروني — signing.py
منقولٌ عن نصّ المواصفة الرسمية لا عن مثالٍ ولا عن مكتبة: ITIDA — Digital Signature Format for E-Invoice System، الإصدار 1.1، 10 نوفمبر
- ستّ صفحات، نُزِّلت واستُخرج نصّها.
⚠️ الفخّ الذي يُسقط كل من استعمل مكتبة CMS بإعداداتها:
encapContentInfo.eContentType ليس id-data المعتاد
(1.2.840.113549.1.7.1) بل id-digestedData (1.2.840.113549.1.7.5)،
وكذلك سمة content-type الموقَّعة. وكل مكتبة CMS تختار id-data تلقائياً —
فيخرج توقيعٌ صحيحٌ تشفيرياً ترفضه وحدةُ تحقّق ITIDA. وهو أسوأ إخفاق
ممكن: يمرّ كلَّ فحصٍ عام، ويسقط في الفحص الوحيد الذي يهمّ.
وفخٌّ ثانٍ: التوقيع يقع على ترميز SET OF للسمات (0x31) لا على
الوسم الضمني [0] (0xA0) الذي تُحمل به في الرسالة — RFC 5652 §5.4. وقد
أوقعني بنفسي في أول تشغيل، فأُثبت الفرقُ اختباراً.
from eta_invoice import SignerLike, build_cades_bes, inspect_cades_bes
signer = SignerLike(certificate_der, sign=my_token.sign) # لا يمرّ مفتاحٌ خاص
signature = build_cades_bes(canonical_bytes, signer)
report = inspect_cades_bes(signature, canonical_bytes)
report.conformant # True
report.problems # []
inspect_cades_bes يفكّ أي توقيع ويقارنه بجدول المواصفة حقلاً حقلاً،
فيفيد في فحص توقيعات الغير أيضاً. و26 اختباراً تغطيه، منها اختبارٌ سالب
يبني توقيعاً بإعدادات المكتبة الافتراضية ويُثبت أن الفاحص يرفضه — وإلا كان
الفاحص زينة.
ما ليس مبنيّاً — وأقوله صراحةً
🔴 قاعدة غير محسومة: invoiceLines[].total
جُرّبت netTotal + valueDifference + totalTaxableFees ± الضرائب − itemsDiscount
على العيّنة الرسمية فأعطت فرقاً مقداره 912.00 في البند الأول، ونصف قرش
غير قابل للتمثيل في البند الثاني — أي أن الخلل في الصيغة لا في قطبية
أنواع الضرائب.
ولذلك لا تُفحص هذه القاعدة. مُحقِّقٌ يرفض فاتورة صحيحة أسوأ من مُحقِّق
يفحص أقل: الأول يُفقد الثقة فيُهمَل، فلا يمنع رفضاً أبداً.
مسجَّلة في validation.UNRESOLVED وفي اختبار يفرض ألا تُطبَّق.
🔴 حدُّ التوقيع: مطابقٌ للمواصفة، ولم يُشغَّل على وحدة التحقّق
المصلحة لا تمنح حساب بيئة تجريبية للمطوّر المستقل: الدخول «كمستخدم بصفة مدير مسئول لدى الممول». والختم الإلكتروني «لا يرتبط إلا بكيان قانوني».
فالتوقيع في signing.py مطابقٌ لنصّ المواصفة ومُتحقَّقٌ من كل حقلٍ فيه
بالتفكيك، ويصحّ تشفيرياً — ولم يُشغَّل بعدُ على وحدة تحقّق ITIDA ولا على
شهادة ختمٍ حقيقية. ولا يُدَّعى قبولٌ لم يُرَ.
🔴 الإرسال — لا يُبنى قبل قراءة الشروط
لا يُكتب سطرٌ في إرسال المستندات قبل قراءة شروط البوابة من داخل حسابٍ
مسجَّل. حاجزٌ صلب، واختبارٌ في test_serialization.py يفرض ألا يتسرّب
إرسالٌ غير مبنيّ إلى السطح العام.
التشغيل
python -m pytest eta-lib/tests -q
68 اختباراً، منها المطابقة الحرفية للعيّنتين الرسميتين (JSON وXML) و200 فاتورة عشوائية.
المصادر
- مواصفة التسلسل
- إنشاء التوقيع
- أنواع الضرائب
- العيّنة الرسمية:
fixtures/one-doc.jsonوfixtures/one-doc-serialized.json.txt
yahya@opus-studio.pro