فواتير المبيعات
يُنشئ POST /v1/sales-invoices فاتورة المبيعات ويصدرها في طلب واحد. لا توجد مرحلة مسودة، والفاتورة بعد إصدارها لا تُصحَّح إلا بإشعار دائن.
إذا كان للمنتج حد أدنى لسعر البيع في كسور، فأي unitDiscount ينزل بسعر الوحدة عنه يُخفَّض تلقائياً إلى هذا الحد. والطلب ينجح مع ذلك، دون أي خطأ.
مثال: منتج سعره 100 ريال قبل الضريبة، وحده الأدنى 92 ريالاً شاملة الضريبة (80 ريالاً قبل الضريبة)، أقصى خصم عليه 20 ريالاً. إذا أرسلت unitDiscount: 50 فالفاتورة تطبّق 20 فقط.
اعتمد دائماً على قيمتي lines[].unitDiscount و total في الرد، وليس على ما أرسلته في الطلب. وإذا أرسلت payment.amount فقارنه بقيمة total في الرد.
الحقول المطلوبة
كل طلب يرسل customerId و branchId و pricesIncludeVat و lines، وكل بند يرسل itemId و quantity. وإذا أرسلت payment فيجب أن يحمل paymentMethodId. ويجب أن تحمل كل فاتورة عميلاً محدداً، فلا يدعم الـ API البيع لعميل نقدي غير مسجّل.
فاتورة ضريبية أم مبسطة
نوع العميل هو الذي يحدد نوع الفاتورة:
| العميل | الفاتورة | هيئة الزكاة والضريبة والجمارك |
|---|---|---|
| شركة | فاتورة ضريبية (standard) | تعتمدها قبل الرد |
| فرد | فاتورة مبسطة (simplified) | تُبلَّغ بها الهيئة بعد الرد |
في الفاتورة الضريبية:
- إذا اعتمدتها الهيئة: يرجع
201معzatca.status: "cleared"ورمز QR الذي أرجعته الهيئة. - إذا رفضتها الهيئة: يرجع
422 zatca_rejectedورسائل الهيئة فيzatcaErrors، ولا يُحفظ أي شيء. - إذا لم ترد الهيئة في الوقت المحدد: يرجع
201معzatca.status: "pending"، وتعيد كسور إرسالها لاحقاً، فتصبحcleared، أوrejectedمع رسائل الهيئة فيzatca.errors. وفي الحالتين تبقى الفاتورة صادرة.
الفاتورة الضريبية تحتاج أيضاً عنوان العميل (الشركة) كاملاً مع الرقم الضريبي أو رقم الهوية (الخطأ customer_address_incomplete يوضح الناقص)، وفرعاً مربوطاً بالهيئة (branch_not_onboarded). وملفا PDF و XML يرجعان 409 invoice_not_cleared حتى تعتمدها الهيئة، ودائماً إذا رفضتها. أما الفاتورة المبسطة فملف PDF جاهز فوراً، وملف XML يرجع 409 invoice_not_cleared حتى تقبل الهيئة التبليغ بها، ودائماً إذا رفضتها.
الأسعار
يحدد pricesIncludeVat هل كل قيم unitPrice و unitDiscount في الطلب شاملة الضريبة. البند الذي لا يحمل unitPrice يأخذ سعر المنتج في الكتالوج، أما المنتج الذي يختلف سعره حسب حجم السيارة فليس له سعر واحد في الكتالوج، فيجب إرسال unitPrice في بنده، وإلا يرجع 400 missing_parameter على lines[i].unitPrice.
تحفظ كسور كل سعر قبل الضريبة مقرّباً إلى الهللة، وتقرّب الضريبة مرة واحدة لكل نسبة على مستوى الفاتورة، بنفس طريقة الهيئة في حساب الإجمالي. والسعر الشامل للضريبة يُحوَّل أولاً، لذلك قد يختلف الإجمالي عمّا أرسلته بهللة: 12.00 شاملة الضريبة تصبح 10.43 قبل الضريبة وإجماليها 11.99. والرد يعرض مبالغ البنود دائماً قبل الضريبة. ولدفع فاتورة كهذه بالكامل أرسل payment بدون amount.
الدفع
| ما ترسله | الفاتورة |
|---|---|
بدون payment | تُسجَّل على حساب العميل (للعميل من نوع شركة فقط، والعميل الفرد يرجع له 422 on_account_not_allowed) |
payment بدون amount | مدفوعة بالكامل |
payment.amount أقل من الإجمالي | مدفوعة جزئياً، والباقي على حساب العميل (للعميل من نوع شركة فقط، والعميل الفرد يرجع له 422 on_account_not_allowed) |
payment.amount أكبر من الإجمالي | تُرفض: 400 invalid_parameter |
تقبل payment.amount منزلتين عشريتين على الأكثر. وقيمتا amountPaid و amountDue في الرد توضحان حالة الفاتورة.
قراءة الفواتير
يعرض GET /v1/sales-invoices كل فاتورة صادرة في فروع المفتاح، أياً كانت القناة التي أنشأتها. ويضيف expand=customer بيانات العميل، ويحتاج صلاحية customers:read إلى جانب sales_invoices:read.