مقال تقني

التحقق الأصلي من فواتير EN 16931 في Delphi دون Java

يتحقق 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. والتقرير يوسم نفسه كمجموعة فرعية، وهذا الوسم هو أهم حقل فيه

مسار التحقق المدمج من فواتير EN 16931 في مكتبة PDFlibPas لـ Delphi: كشف Factur-X، ومرحلتا schema وschematron، وتقرير JSON لمجموعة فرعية مغطاة
يعمل المدقق المدمج مرحلتي schema و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 في تحقق فواتير PDFlibPas لـ Delphi: الفاتورة نفسها بفرق تقريب وحدة واحدة تجتاز في CII وتفشل في UBL
وحدة تقريب واحدة تجلس بالضبط على حد BR-CO-17، فالفاتورة نفسها تجتاز في CII وتفشل في UBL

الاختلافات أعمق من 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 منفصلين حتى تميز أي ضمان تملكه فعلًا

مسار سلامة حزمة قواعد فواتير PDFlibPas في Delphi: بصمات SHA-256 للبيان تعاد مراجعتها عند التحميل وقبل كل قراءة، مثبتة من طرف المستدعي
بصمات الملفات تحمي محتويات الحزمة، ولا تحمي البيان نفسه إلا بصمة يثبتها المستدعي

إن مررت 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 والملفات المضمَّنة المستخدمة أعلاه