يتحقق PDFlibPas، مكتبة PDF لـ Delphi من losLab، من قواعد العمل EN 16931 وXRechnung 3.0.2 وPeppol BIS Billing 3.0 أصليًا في Pascal. يقرأ TPDFlib.ValidateInvoiceBusinessRulesBuiltIn الـ XML المضمَّن في PDF من نوع Factur-X أو ZUGFeRD ويعيد تقرير JSON بمعرفات القواعد الرسمية، دون بيئة تشغيل Java أو Saxon أو سلسلة أدوات Schematron. والتقرير يوسم نفسه كمجموعة فرعية، وهذا الوسم هو أهم حقل فيه
الضغط وراء هذا ألماني في معظمه. فمنذ يناير 2025 صار كل منشأة محلية في ألمانيا يجب أن تقدر على استلام فواتير إلكترونية B2B مهيكلة، ويلتقي التزام الإصدار على مراحل. وفجأة أصبح الطرف المستقبل في تطبيق محاسبة أو ERP مكتوب بـ Delphi مضطرًا للإجابة عن سؤال لم يطرحه قبل ذلك: هل الـ XML داخل هذا PDF فاتورة EN 16931 سليمة فعلًا؟ والجواب الرسمي يأتي من artefacts الرسمية لـ Schematron المترجمة إلى XSLT 2.0، أو من مدقق KoSIT لـ XRechnung، وكلاهما يعمل على JVM. وتوزيع JRE وترقيعها بجوار كل تثبيت مكتبي لمجرد رفض فاتورة سيئة تكلفة يفضل كثير من الفرق ألا يحملوها
ماذا يتحقق منه المدقق المدمج فعلًا؟
يخطط المدقق الأصلي XML الفاتورة على حدود العمل EN 16931 ويشغل مجموعة فرعية موثقة من القواعد الواردة في artefacts التحقق EN 16931 الإصدار 1.3.16، مسجلا كل نتيجة تحت معرف القاعدة الرسمي الخاص بها. وكلا الصياغتين مدعومتان: UN/CEFACT CII D16B التي تضمّنها Factur-X وZUGFeRD، وOASIS UBL 2.1 Invoice وCreditNote. والمجالات المغطاة هي:
- الحدود الإلزامية: معرف المواصفات، ورقم الفاتورة، وتاريخ الإصدار، ورمز النوع، والعملة، وأسماء البائع والمشتري، والعناوين، وإجماليات المستند، وتفصيل VAT والسطور (BR-01 إلى BR-16، وBR-21 إلى BR-28، وBR-CO-04، وBR-CO-18، وBR-CO-26، وBR-45 إلى BR-48)
- الحسابات: إجماليات السطر والخصم والرسم والضريبة والمستحق، بالإضافة إلى المبلغ الخاضع ومبلغ الضريبة لكل فئة VAT (BR-CO-10 إلى BR-CO-17، وBR-xx-08، وBR-xx-09)
- قواعد فئات VAT للرموز S وZ وE وAE وK وG وO وL وM: النسب على السطور، وأسباب الإعفاء، ومعرفات VAT التي تتطلبها كل فئة
- كل قوائم الرموز من BR-CL-01 إلى BR-CL-26، بما فيها رموز الوحدات الأكثر من 2100 في UN/ECE Rec 20/21 خلف BR-CL-23، وقواعد المنزلتين العشريتين BR-DEC-01 و-05 و-09 إلى -20 و-23
- قواعد profile: XRechnung 3.0.2 (من BR-DE-1 إلى BR-DE-31، ومن BR-DEX-01 إلى BR-DEX-14) على CII وUBL، وقواعد النواة والمشتركة وقوائم الرموز والوطنية في Peppol BIS Billing 3.0.20 على UBL
النطاق يصرَّح به لا يُستنتج. وبدون ملف schema تؤكد مرحلة schema حسن التشكيل والعنصر الجذر فقط. كل مرحلة تسرد القواعد خارج المجموعة الفرعية في حقل notCovered، ويعيد GetBuiltInInvoiceRuleCatalog الخريطة كاملة كـ JSON، فيستطيع UI الخاص بك أن يعرض للمستخدم بالضبط ما يغطيه نتيجة خضراء
حد آخر سهل الفقدان: مستندات Factur-X MINIMUM وBASIC WL ليست فواتير EN 16931، لذا لا يطبق عليها المدقق قواعد EN 16931. بل يضيف نتيجة إخبارية BuiltIn.Coverage.Profile تقول ذلك، وهي الجواب الصادق وأيضًا إشارة: فبحسب القواعد الألمانية لا تُحسب تلك الملفات الشخصية (profiles) فواتير إلكترونية على أي حال
التحقق من الـ XML المضمَّن في PDF هجين
يأخذ ValidateInvoiceBusinessRulesBuiltIn وسيطًا واحدًا هو اسم الملف المضمَّن، والسلسلة الفارغة هي الخيار المعتاد. يختار PDFlibPas عندها المرفق الذي يحدده كشف Factur-X؛ وفي PDF بلا بيانات وصفية Factur-X يعود إلى مرفق .xml الوحيد. وعدة مرفقات .xml مرشحة، أو مرفقان بالاسم المطلوب، تنتجان خطأ Invoice.AttachmentAmbiguous بدل تخمين، ويُسقف فك الترميز عند 64 MB، والفاتورة المفقودة تضع LastErrorCode عند 401
uses
System.SysUtils, System.JSON, PDFlibrary;
procedure CheckInboundInvoice(const FileName: string);
var
PDF: TPDFlib;
Report: TJSONObject;
begin
PDF := TPDFlib.Create;
try
if PDF.LoadFromFile(FileName, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s (error %d)',
[FileName, PDF.LastErrorCode]);
// اسم فارغ: تحقق من مرفق Factur-X / ZUGFeRD المكتشف
Report := TJSONObject.ParseJSONValue(
PDF.ValidateInvoiceBusinessRulesBuiltIn('')) as TJSONObject;
try
Writeln('profile: ', Report.GetValue<string>('profile'));
Writeln('valid: ', Report.GetValue<Boolean>('valid'));
Writeln('coverage: ', Report.GetValue<string>('coverage')); // الناتج "subset"
Writeln('full: ', Report.GetValue<Boolean>('fullConformance')); // لا تصير True هنا أبدًا
finally
Report.Free;
end;
finally
PDF.Free;
end;
end;
المستوى الأعلى من التقرير يحمل standard وversion وprofile وcomplete وvalid وfullConformance وcoverage، ومرحلة schema ومرحلة schematron، وكائن package يكون null في الوضع المدمج. وكل مرحلة تسجل حالة، والمحرك، وعدد القواعد المنفذة، وتغطيتها، ومصفوفة issues. وكل نتيجة تحمل ruleID وfieldPath وseverity وcategory وline وmessage
قراءة النتائج بحسب الفئة بدل الرسالة
حقل category هو ما يجعل التقرير قابلًا للاستخدام في صندوق وارد حقيقي، لأنه يخبرك مَن الذي عليه إصلاح المشكلة. القيم هي syntax وschema وcardinality وcalculation وvat وcodelist وdecimal وprofile وcoverage وprocessing وintegrity. نتيجة حسابية تشير عادة إلى كود التقريب عند المرسل، ونتيجة codelist إلى البيانات المرجعية، ونتيجة profile إلى حقل XRechnung مثل مرجع المشتري
procedure ListIssues(Report: TJSONObject);
var
Item: TJSONValue;
Issue: TJSONObject;
begin
for Item in Report.GetValue<TJSONArray>('schematron.issues') do
begin
Issue := Item as TJSONObject;
if Issue.GetValue<string>('severity') = 'info' then
Continue; // ملاحظات تغطية مثل BuiltIn.Coverage.Profile
Writeln(Format('[%s] %s at %s: %s', [
Issue.GetValue<string>('category'),
Issue.GetValue<string>('ruleID'),
Issue.GetValue<string>('fieldPath'),
Issue.GetValue<string>('message')]));
end;
end;
شغّل الحلقة نفسها على schema.issues أيضًا، لأن إخفاقات حسن التشكيل والعنصر الجذر تهبط هناك، ومرحلة قواعد العمل لا ترى مستندًا لم تستطع تحليله أبدًا
لماذا قد تجتاز الفاتورة نفسها في CII وتفشل في UBL؟
artefacts الرسمية لا تطبق السماحية العددية نفسها في الصياغتين، وينسخ PDFlibPas كل قاعدة من XSLT الخاص بصياغتها بدل توحيدها. BR-CO-17 هي الحالة الأوضح: تتحقق من أن مبلغ ضريبة فئة VAT يساوي المبلغ الخاضع مضروبًا في النسبة مقربًا إلى منزلتين عشريتين، وكلا الصياغتين تسمحان بفرق وحدة عملة واحدة. تكتب artefact CII ذلك بالشكل <= و>=؛ بينما تستخدم artefact UBL abs مع < صارمة
خذ مبلغًا خاضعًا 100.00 عند نسبة 19 بالمئة، فيكون الضريبة المتوقعة 19.00، وفاتورة تعلن 20.00. في CII 20.00 ناقص 1 يساوي 19.00 وهو يحقق <=، فتجتاز BR-CO-17. في UBL المقارنة نفسها صارمة، و19.00 ليس أصغر من 19.00، فتفشل BR-CO-17. لا أحد يرسل تلك الفاتورة بقصد، لكن المولدات التي تحمل خطأ تقريب تجلس بالضبط على مثل هذه الحدود، ومجموعة اختبارات تغطي صياغة واحدة لن تنتبه
الاختلافات أعمق من BR-CO-17. تطلب BR-xx-08 تساويًا تامًا بعد التقريب إلى منزلتين في CII، بينما تسمح UBL بـ ±1 ولا تنطلق إلا حين تحوي الفئة فعلًا سطورًا أو خصومات أو رسومًا. وBR-O-08 تامة في الحالتين. وتعرف artefact CII BR-AF-09 وBR-AG-09 كـ true() فلا تفشلان هناك مطلقًا، بينما تتحقق منهما UBL بـ ±1. وحين لا يحمل تفصيل VAT في CII نسبة على الإطلاق، تجتاز قواعد الفئات غير O فراغًا لأن every $rate في XPath على متتالية فارغة تعطي true. ولا بد للكود الأصلي أن يتخطاها بالطريقة نفسها، وإلا سجل أخطاء على ملفات العينات الرسمية
مصيدة ثانية تتعلق بمعرفات القواعد: بادئة فئة القاعدة ليست رمز فئة VAT، فK تقابل BR-IC، وL تقابل BR-AF، وM تقابل BR-AG. اشتقاق المعرف من رمز الفئة ينتج BR-K-09 وBR-K-10، معرفات لا وجود لها في EN 16931. وإن كان تطبيقك يربط معرفات القواعد بنصوص المساعدة، فخذ المعرف من التقرير ولا تبنيه أبدًا من رمز فئة الضريبة
كيف تُشغَّل قواعد XRechnung وPeppol؟
يختار المدقق المدمج قواعد الـ profile من معرف المواصفات (BT-24). ومعرف يحتوي xrechnung يشغل قواعد CIUS الخاصة بـ XRechnung 3.0.2 على CII أو UBL؛ ومعرف الامتداد urn:xeinkauf.de:kosit:extension:xrechnung_3.0 يفعّل قواعد BR-DEX إضافة. وتحققات مبلغ سطر الفاتورة الفرعية وVAT (من BR-DEX-02 و-03 و-09 إلى -14) تعمل على UBL وحده؛ وعلى CII تسجل BR-DEX-15 أن سطور الفاتورة الفرعية غير متاحة. ومعرف يحتوي peppol يشغل قواعد Peppol BIS Billing 3.0.20 التي يعرفها Peppol لـ UBL وحده. ومستند CII يدعي تخصيصًا (customization) خاصًا بـ Peppol يحصل على ملاحظة إخبارية BuiltIn.Coverage.Profile تقول إن تلك القواعد لم تفحص، بدل اجتياز صامت. أما باقي ملفات CIUS الشخصية فاستخدم artefacts الرسمية
تفصيل UBL يستحق المعرفة: لا تقبل BR-06 وBR-07 إلا PartyLegalEntity/RegistrationName كاسم للبائع أو المشتري. والعودة إلى PartyName/Name تبدو تسامحًا لكنها تخفي إخفاقات BR-06 حقيقية، ولهذا يكون التحقق صارمًا
متى ما زلت بحاجة إلى Schematron الرسمي؟
استخدم artefacts الرسمية، أي Schematron الخاص بـ EN 16931 عبر Saxon أو مدقق KoSIT لـ XRechnung، كلما حملت النتيجة وزنًا قانونيًا أو تعاقديًا: رفض فاتورة مورّد، أو توثيق مولدك الخاص، أو حسم خلاف. وتقرير المدمج لا يضع fullConformance عند true أبدًا. فذلك الوسم يتطلب حزمة قواعد تم التحقق منها، واجتياز المرحلتين، وتشغيل المرحلتين بمحرك ينفذ قواعد الحزمة نفسها، ولا شيء متبقيًا في notCovered
يشغل ValidateInvoiceBusinessRulesWithPackage ذلك المسار. حزمة القواعد دليل يحوي ملف بيان rulepackage.xml كتبته CreateInvoiceRulePackage، يسمي المعيار والإصدار وschema وورقة أنماط Schematron ويسجل SHA-256 لكل ملف. ويتحقق PDFlibPas من كل بصمة عند تحميل الحزمة ومرة أخرى قبل أن يقرأ محرك الملف مباشرة، وهو ما يضيق نافذة الاستبدال بعد التحقق. لا يجوز للمسارات أن تغادر دليل الحزمة، ومحمل XSD الأصلي يرفض الاستيرادات التي لا يسردَها البيان. ومع xmllint مضبوطًا تقرأ xmllint بنفسها الـ schemas المستوردة، فيبقى فحص وقت التحميل وحده هو الذي يحميها
const
// بصمة SHA-256 لملف rulepackage.xml الذي راجعته ووافقت عليه
ApprovedManifestSHA256 = '<64 hex digits from your release review>';
var
Report, Pkg: TJSONObject;
begin
// PDF: كائن TPDFlib محمَّل فيه الفاتورة مسبقًا، كما في المثال الأول
Report := TJSONObject.ParseJSONValue(
PDF.ValidateInvoiceBusinessRulesWithPackage('',
'C:\Rules\EN16931-CII-1.3.16', ApprovedManifestSHA256,
'', // فارغ: مرحلة XSD الأصلية
'C:\Java\bin\java.exe', 'C:\Saxon\saxon-he.jar',
0)) as TJSONObject; // 0: 30000 ms لكل أداة
try
if not (Report.FindValue('package') is TJSONObject) then
raise Exception.Create('No rule package in the report');
Pkg := Report.GetValue<TJSONObject>('package');
if not (Pkg.GetValue<Boolean>('pinned') and Pkg.GetValue<Boolean>('verified')) then
raise Exception.Create('Rule package failed its integrity check');
if Report.GetValue<Boolean>('fullConformance') then
Writeln('Conforms to the complete rule package');
finally
Report.Free;
end;
end;
البصمة المثبتة ليست زينة اختيارية. بصمات الملفات تثبت أن الملفات تطابق البيان، لكن لا شيء داخل الحزمة يثبت أن البيان نفسه أصلي: فمهاجم قادر على إعادة كتابة القواعد قادر على إعادة كتابة البيان معها. لا تسد تلك الفجوة إلا بصمة يثبتها المستدعي، في الكود أو في إعدادات موقعة، ويبقي التقرير pinned وverified منفصلين حتى تميز أي ضمان تملكه فعلًا
إن مررت JavaExecutable فارغة، فستبقى مرحلة schema تتحقق أصليًا مقابل XSD الحزمة، بما فيها الوجوه (facets) وقيود الهوية منذ v3.672.0، لكن قواعد العمل تعود إلى المجموعة الفرعية المدمجة ويبقى fullConformance عند false. وبنيات schema التي لا يدعمها المحمل الأصلي، مثل substitution groups، تجعل التحميل يفشل بخطأ XSD.Unsupported بدل تجاوزها
موقع veraPDF وMustang
لا تنظر أي من هذه الدوال إلى حاوية PDF. فمطابقة PDF/A-3 في إطار ISO 19005-3، وschema امتداد XMP الخاص بـ Factur-X، وعلاقة الملف المرافق، أسئلة منفصلة، وتبقى veraPDF وMustang أدواتهما المرجعية، كما في التحقق من الفواتير الإلكترونية بـ veraPDF وMustang. وجهة الإنتاج، من إرفاق الـ XML إلى تصريح الـ profile، مغطاة في بناء فواتير هجينة Factur-X وZUGFeRD في Delphi وschemas امتداد PDF/A-3 لبيانات Factur-X الوصفية
التقسيم العقلاني يشغل التحقق المدمج داخل العملية على كل فاتورة واردة، حيث يكلف ميلي ثوانٍ: فاتورة UBL بحجم نحو 1 MB بمرفقات base64 تتحقق في 15 ms، أو 79 ms مع XSD الخاص بـ UBL 2.1. وتشغيل Saxon المعبأ بحزمة يذهب إلى CI وإلى مسار الرفض، وveraPDF تغطي الحاوية. ومهما كانت نتيجة المدمج فهي تصف المجموعة الفرعية المغطاة لا المعيار، وهذا بالضبط ما يذكرك به وجود حقل coverage
دوال التحقق من الفواتير، وفهرس القواعد، وأدوات حزم القواعد تأتي مع مكتبة PDF لـ Delphi لـ Delphi وC++Builder، مع واجهات Factur-X وPDF/A-3 والملفات المضمَّنة المستخدمة أعلاه