دليل المطور الشامل

كل ما تحتاجه لبناء إضافات احترافية لنظام سيطرة ERP — الأحداث، الصلاحيات، API الكامل، ومثال تطبيقي جاهز للتنزيل.

نظرة عامة — ما هي إضافة سيطرة؟

إضافة سيطرة هي حزمة برمجية (Python + HTML) تُوسّع وظائف النظام. يمكنها:

صفحات جديدة

إضافة صفحات ويب وروابط في القائمة

أحداث (Events)

الاستجابة لـ 65+ حدث في النظام

بيانات آمنة

قراءة/كتابة بصلاحيات محددة

مهام مجدولة

تشغيل كود دوري (ساعة/يوم)

إعدادات

إعدادات قابلة للتخصيص

إشعارات

إرسال إشعارات للمستخدمين

آمنة تماماً: كل إضافة تعمل في بيئة معزولة (Sandbox) — لا تستطيع الوصول لجداول حساسة (users, sessions, passwords). كل عملية مُسجّلة ومحدودة بـ 100 طلب/دقيقة.

بداية سريعة — 3 دقائق!

  1. حمّل القالب الجاهز: saytara_starter_plugin.zip
  2. فك الضغط وعدّل: غيّر id وname في manifest.json، واكتب منطقك في main.py
  3. اضغط كـ ZIP:
    zip -r my_plugin.zip custom_my_plugin/
  4. ارفع وفعّل: ادخل الإعدادات → مدير الإضافات → رفع ZIP → تثبيت → تفعيل ✅
نصيحة: الملف المضغوط يحتوي على إضافة كاملة جاهزة للعمل — بصفحة خاصة، أحداث، إعدادات، وقائمة. ابدأ منه!

1هيكل ملفات الإضافة

custom_my_plugin/
├── manifest.json          ← إجباري — تعريف الإضافة (الاسم، الصلاحيات، الأحداث)
├── main.py                ← إجباري — الكلاس الرئيسي (يرث SaytaraPlugin)
├── __init__.py            ← فارغ (مطلوب لـ Python)
├── templates/
│   └── ext_custom_my_plugin/
│       ├── index.html     ← الصفحة الرئيسية (تستخدم {% extends "base.html" %})
│       └── widget.html    ← ويدجت الداشبورد (اختياري)
├── static/
│   ├── css/style.css      ← أنماط CSS مخصصة
│   └── js/main.js         ← JavaScript مخصص
└── README.md              ← توثيق (اختياري)
مهم: اسم مجلد القوالب يجب أن يكون ext_{plugin_id بدون نقاط} — مثلاً ext_custom_my_plugin. هذا يمنع تعارض الأسماء بين الإضافات.

2ملف manifest.json — التعريف الكامل

{
    "id": "custom.my_report",
    "name": "تقرير الأرباح المتقدم",
    "name_en": "Advanced Profit Report",
    "version": "1.0.0",
    "description": "تقرير أرباح تفصيلي مع رسوم بيانية وتصدير PDF",
    "description_en": "Detailed profit report with charts and PDF export",
    "category": "reports",
    "icon": "chart-line",
    "color": "#28a745",
    "author": {
        "name": "أحمد المطور",
        "email": "ahmed@example.com",
        "url": "https://example.com"
    },
    "entry_point": "main.py",
    "min_system_version": "1.0.0",
    "permissions": [
        "accounting.invoices.read",
        "accounting.reports.read",
        "customers.list.read"
    ],
    "events": {
        "listen": ["accounting.invoice.created", "accounting.payment.received"],
        "emit": ["custom.my_report.generated"]
    },
    "settings": [
        {"key": "show_charts", "label": "إظهار رسوم بيانية", "type": "boolean", "default": true},
        {"key": "currency", "label": "عملة التقرير", "type": "select",
         "options": [{"value":"IQD","label":"دينار عراقي"}, {"value":"USD","label":"دولار"}]}
    ],
    "ui": {
        "menu_items": [
            {"label": "تقرير الأرباح", "icon": "chart-line", "url": "/ext/my-report/", "order": 50}
        ]
    },
    "tags": ["reports", "analytics", "charts"]
}
جميع حقول manifest.json:
الحقلإجباريالنوعالوصف
idstringمعرف فريد: namespace.plugin_name — أحرف صغيرة، نقاط، شرطات
namestringاسم بالعربية
name_enstringاسم بالإنجليزية
versionstringSemantic Versioning: 1.0.0
descriptionstringوصف مختصر بالعربية
description_enstringوصف بالإنجليزية
categorystringالتصنيف: accounting, inventory, pos, crm, hrm, restaurant, reports, integrations, automation, utilities
iconstringأيقونة FontAwesome 6 (بدون fa-): chart-line, robot, rocket...
colorstringلون HEX: #28a745
authorobject{name, email, url}
entry_pointstringملف الدخول (افتراضي: main.py)
min_system_versionstringأدنى إصدار مطلوب من سيطرة
permissionsarrayقائمة الصلاحيات المطلوبة (انظر قسم الصلاحيات)
events.listenarrayالأحداث التي تستمع لها (انظر جدول الأحداث)
events.emitarrayالأحداث التي تطلقها
settingsarrayإعدادات (انظر قسم الإعدادات)
ui.menu_itemsarrayروابط القائمة: [{label, icon, url, order}]
tagsarrayكلمات مفتاحية للبحث
dependenciesobjectإضافات مطلوبة: {"plugin_id": ">=1.0.0"}

3ملف main.py — الكلاس الرئيسي

يجب أن يحتوي على كلاس واحد يرث من SaytaraPlugin:

from extensions.plugin_base import SaytaraPlugin, PluginContext

class MyPlugin(SaytaraPlugin):

    @property
    def plugin_id(self):   return 'custom.my_plugin'   # يطابق id في manifest
    @property
    def plugin_name(self): return 'إضافتي'
    @property
    def plugin_version(self): return '1.0.0'

    # ─── الدوال الإجبارية (Properties) ───
    # plugin_id, plugin_name, plugin_version (أعلاه)

    # ─── دورة الحياة (اختياري) ───
    def on_install(self, ctx):    ctx.log('info', 'تم التثبيت')
    def on_activate(self, ctx):   ctx.log('info', 'تم التفعيل')
    def on_deactivate(self, ctx): ctx.log('info', 'تم التعطيل')
    def on_uninstall(self, ctx):  ctx.log('info', 'تم الحذف')

    # ─── الأحداث ───
    def register_events(self, ctx):
        ctx.on_event('accounting.invoice.created', self._on_invoice)

    def _on_invoice(self, event):
        print(f"فاتورة: {event.data.get('invoice_id')}")

    # ─── الصفحات ───
    def register_routes(self, app):
        from flask import Blueprint, render_template
        bp = Blueprint('ext_my_plugin', __name__,
                       template_folder='templates', url_prefix='/ext/my-plugin')
        @bp.route('/')
        def index(): return render_template('ext_custom_my_plugin/index.html')
        return bp

    # ─── القائمة ───
    def get_menu_items(self):
        return [{'label': 'إضافتي', 'icon': 'rocket', 'url': '/ext/my-plugin/', 'order': 100}]

دورة حياة الإضافة (Lifecycle)

① on_install() ② on_activate() ③ register_events() ④ register_routes() ⑤ on_deactivate() ⑥ on_uninstall()
الدالةمتى تُستدعىاستخدامات شائعة
on_install(ctx)مرة واحدة عند التثبيتإنشاء جداول، تهيئة إعدادات افتراضية
on_activate(ctx)كل مرة عند التفعيلتحميل بيانات مُخَزّنة، بدء خدمات
register_events(ctx)بعد التفعيلتسجيل مستمعي أحداث
register_routes(app)بعد التفعيلإنشاء Blueprint + صفحات
on_deactivate(ctx)عند التعطيلتنظيف موارد، حفظ حالة
on_uninstall(ctx)عند الحذفحذف جداول مخصصة، تنظيف كامل

4إضافة صفحات (Routes & Templates)

register_routes() تُنشئ Flask Blueprint. الصفحات تظهر على /ext/{name}/:

def register_routes(self, app):
    bp = Blueprint('ext_my_report', __name__,
                   template_folder='templates',
                   static_folder='static',
                   url_prefix='/ext/my-report')

    @bp.route('/')
    def index():
        return render_template('ext_my_report/index.html')

    @bp.route('/api/data')
    def api_data():
        return jsonify({'success': True, 'items': [...]})

    return bp  # مهم! أرجع الـ Blueprint

القالب (index.html): استخدم {% extends "base.html" %} لورث تصميم النظام:

{% extends "base.html" %}
{% block title %}تقريري{% endblock %}
{% block content %}
<div class="container-fluid py-4">
    <h2>مرحباً من الإضافة!</h2>
    <!-- محتوى صفحتك هنا -->
</div>
{% endblock %}

القوائم والويدجت

get_menu_items(): يضيف روابط في الشريط العلوي (قائمة الإعدادات):

def get_menu_items(self):
    return [
        {'label': 'تقريري', 'icon': 'chart-line', 'url': '/ext/my-report/', 'order': 50},
    ]

get_dashboard_widgets(): يضيف ويدجت في الداشبورد:

def get_dashboard_widgets(self):
    return [{'title': 'إضافتي', 'icon': 'rocket', 'color': '#667eea',
             'template': 'ext_my_plugin/widget.html', 'order': 100}]

نظام الأحداث — كيف يعمل؟

نظام سيطرة يطلق أحداث عند كل عملية مهمة. إضافتك تسجّل مستمعين لهذه الأحداث:

def register_events(self, ctx):
    # استمع لحدث واحد أو أكثر
    ctx.on_event('accounting.invoice.created', self._on_invoice)
    ctx.on_event('accounting.payment.received', self._on_payment)

def _on_invoice(self, event):
    # event.data يحتوي بيانات الحدث
    invoice_id = event.data.get('invoice_id')
    total = event.data.get('total_amount')
    user_id = event.data.get('user_id')
    print(f"فاتورة #{invoice_id} — المبلغ: {total}")

EventContext يحتوي:

  • event.event_name — اسم الحدث
  • event.data — بيانات الحدث (dict)
  • event.tenant_id — معرف المشترك
  • event.user_id — معرف المستخدم الحالي
  • event.timestamp — وقت الحدث

جميع الأحداث المتاحة — المرجع الكامل (65+ حدث)

محاسبة الفواتير والسندات
اسم الحدثمتى يُطلقالبيانات (data)
accounting.invoice.before_createقبل إنشاء فاتورة (قابل للإلغاء)invoice_type, customer_id, total_amount
accounting.invoice.createdبعد إنشاء فاتورة بيع أو شراءinvoice_id, invoice_type, customer_id, total_amount, status, user_id
accounting.invoice.updatedتعديل أو اعتماد فاتورةinvoice_id, action, user_id
accounting.invoice.deletedحذف أو إلغاء فاتورةinvoice_id, action (deleted/cancelled), user_id
accounting.invoice.paidتسديد فاتورة بالكاملinvoice_id, amount
accounting.invoice.overdueتجاوز فاتورة موعد السدادinvoice_id
accounting.voucher.before_createقبل إنشاء سند (قابل للإلغاء)voucher_type
accounting.voucher.createdإنشاء سند (قبض/صرف/يومية/صرف/تبديل/رصيد افتتاحي/بطاقة)voucher_type, user_id
accounting.voucher.postedترحيل سندvoucher_id
accounting.voucher.deletedحذف سندvoucher_type, voucher_id, user_id
accounting.voucher.reversedعكس قيد يوميةvoucher_id, user_id
accounting.payment.receivedاستلام دفعة على فاتورةinvoice_id, amount, payment_method, user_id
accounting.payment.sentتسجيل سند صرفuser_id
accounting.payment.deletedحذف دفعةpayment_id, user_id
accounting.cost_center.savedحفظ مركز كلفةuser_id
accounting.payment_plan.actionعملية على خطة سداد/أقساطpath, user_id
محاسبة الشيكات
اسم الحدثمتى يُطلقالبيانات
accounting.check.status_changedتغيير حالة شيك (أي حالة)check_id, new_status, user_id
accounting.check.clearedتحصيل شيك بنجاحcheck_id
accounting.check.bouncedارتجاع شيكcheck_id
الزبائن والمجهزين
اسم الحدثمتى يُطلقالبيانات
customers.customer.createdإضافة زبون جديدcustomer_id, customer_name, user_id
customers.customer.updatedتعديل بيانات زبونcustomer_id, customer_name, user_id
customers.customer.deletedحذف زبونcustomer_id, user_id
suppliers.supplier.createdإضافة مجهّز جديدsupplier_name, user_id
suppliers.supplier.updatedتعديل بيانات مجهّزsupplier_name, user_id
suppliers.supplier.deletedحذف مجهّزsupplier_id, user_id
المخزون والمواد
اسم الحدثمتى يُطلقالبيانات
inventory.material.createdإضافة مادة جديدةmaterial_name, user_id
inventory.material.updatedتعديل مادةmaterial_id, user_id
inventory.material.deletedحذف مادةmaterial_id, user_id
inventory.material.price_changedتغيير سعر مادةmaterial_id
inventory.stock.changedتغيّر رصيد مخزونadjustment_type, warehouse_id, user_id
inventory.stock.lowرصيد مخزون منخفض (تحت الحد الأدنى)material_id
inventory.stock.out_of_stockنفاد مخزون مادةmaterial_id
inventory.stock.receivedاستلام بضاعة
inventory.warehouse.savedحفظ مخزنuser_id
inventory.category.savedحفظ تصنيف مادةuser_id
inventory.brand.savedحفظ ماركةuser_id
inventory.stock_take.createdإنشاء جردuser_id
inventory.stock_take.completedإكمال جردtake_id, user_id
inventory.stock_take.approvedاعتماد جردtake_id, user_id
نقاط البيع (POS)
اسم الحدثمتى يُطلقالبيانات
pos.sale.completedإتمام عملية بيع POSsale_id/invoice_id, total, user_id
pos.sale.refundedإلغاء/استرجاع عملية بيعsale_id, amount, user_id
pos.shift.openedفتح ورديةuser_id
pos.shift.closedإغلاق ورديةuser_id
المطاعم
اسم الحدثمتى يُطلقالبيانات
restaurant.order.placedطلب جديدorder_id, table_id, user_id
restaurant.order.readyطلب جاهز من المطبخorder_id, user_id
restaurant.order.deliveredتسليم طلب / دفعorder_id, user_id
restaurant.order.voidedإلغاء طلبorder_id, user_id
restaurant.order.status_changedتغيير حالة طلبorder_id, user_id
restaurant.table.occupiedشَغل طاولةtable_id
restaurant.table.freedتحرير طاولةtable_id
CRM — إدارة علاقات العملاء
اسم الحدثمتى يُطلقالبيانات
crm.lead.createdعميل محتمل جديدlead_id, user_id
crm.lead.updatedتعديل عميل محتملlead_id, user_id
crm.lead.convertedتحويل عميل محتمل لزبونlead_id, user_id
crm.lead.lostفقدان عميل محتملlead_id, action, user_id
crm.opportunity.createdفرصة بيع جديدةopportunity_id/lead_id, user_id
crm.opportunity.updatedتعديل فرصةopportunity_id, user_id
crm.opportunity.stage_changedنقل فرصة بين مراحل Pipelineopportunity_id, user_id
crm.opportunity.wonكسب فرصة بيعopportunity_id
crm.opportunity.lostخسارة فرصة بيعopportunity_id, action, user_id
crm.leads.bulk_actionعملية جماعية على العملاءaction, count, user_id
HRM — الموارد البشرية
اسم الحدثمتى يُطلقالبيانات
hrm.employee.hiredتعيين موظفemployee_id, user_id
hrm.employee.terminatedإنهاء خدمة موظفemployee_id, user_id
hrm.leave.requestedطلب إجازةleave_id, user_id
hrm.leave.approvedالموافقة على إجازةleave_id, user_id
hrm.leave.rejectedرفض إجازةleave_id, user_id
hrm.payroll.createdإنشاء دورة رواتبrun_id, user_id
hrm.payroll.approvedاعتماد رواتبrun_id, user_id
hrm.salary.processedترحيل رواتب (قيد محاسبي)run_id, user_id
hrm.loan.createdإنشاء قرض/سلفةloan_id, user_id
hrm.loan.approvedالموافقة على قرضloan_id, user_id
hrm.recruitment.posting_createdإعلان وظيفةuser_id
hrm.performance.review_createdتقييم أداءuser_id
hrm.eos.calculatedحساب نهاية خدمةuser_id
hrm.department.savedحفظ قسم/إدارةuser_id
النظام والتكاملات
اسم الحدثمتى يُطلقالبيانات
system.user.logged_inتسجيل دخول مستخدمuser_id, username
system.user.logged_outتسجيل خروجuser_id
system.user.createdإنشاء مستخدمuser_id
system.branch.savedحفظ فرعuser_id
system.settings.updatedتحديث إعدادات النظامuser_id
system.backup.createdإنشاء نسخة احتياطيةuser_id
system.data.importedاستيراد بيانات CSVimport_type, user_id
system.currency.updatedتحديث عملاتuser_id
system.plugin.installedتثبيت إضافةplugin_id
system.plugin.enabledتفعيل إضافةplugin_id, tenant_id
system.plugin.disabledتعطيل إضافةplugin_id, tenant_id
notification.sentإرسال إشعارuser_id
أحداث أخرى
اسم الحدثمتى يُطلق
orders.order.createdإنشاء طلب/عرض سعر
assets.asset.savedحفظ أصل ثابت
assets.asset.disposedاستبعاد أصل ثابت
assets.depreciation.runتشغيل استهلاك شهري
fiscal.period.createdإنشاء فترة مالية
fiscal.period.closedإغلاق فترة مالية
fiscal.period.reopenedإعادة فتح فترة
fiscal.year_end.executedإقفال سنة مالية
sales.representative.actionعملية على مندوب مبيعات
supermarket.actionعملية سوبرماركت
integration.woocommerce.syncedمزامنة WooCommerce

أحداث ما قبل التنفيذ (Pre-Events) — إلغاء العمليات

الأحداث التي تبدأ بـ before_ تسمح لإضافتك بـ إلغاء العملية قبل حدوثها:

def register_events(self, ctx):
    ctx.on_event('accounting.invoice.before_create', self._validate, priority=10)

def _validate(self, event):
    total = event.data.get('total_amount', 0)
    if total > 50000000:  # 50 مليون
        event.cancel('المبلغ يتجاوز الحد المسموح!', by=self.plugin_id)

الأحداث المتاحة للإلغاء: accounting.invoice.before_create, accounting.invoice.before_update, accounting.invoice.before_delete, accounting.voucher.before_create

6الوصول للبيانات (Data API)

# قراءة فواتير
invoices = ctx.query('invoices', filters={'payment_status': 'unpaid'}, limit=50)

# قراءة عميل بالمعرف
customer = ctx.get_by_id('customers', 42)

# استعلام SQL مخصص (قراءة فقط!)
results = ctx.execute_query("SELECT * FROM invoices WHERE total_amount > %s", (1000000,))

# إنشاء سجل في جدول الإضافة
ctx.create('ext_plugin_data', {'key': 'sync', 'value': '2026-01-01'})

# تحديث سجل
ctx.update('customers', 5, {'notes': 'VIP'})

# إطلاق حدث مخصص
ctx.emit_event('custom.report.generated', {'report_id': 1})

# إرسال إشعار
ctx.send_notification('تنبيه!', 'تقرير الأرباح جاهز للتحميل')

7الإعدادات (Settings)

def get_settings_schema(self):
    return [
        {'key': 'api_key',   'label': 'مفتاح API',     'type': 'password'},
        {'key': 'auto_sync',  'label': 'مزامنة تلقائية', 'type': 'boolean',  'default': True},
        {'key': 'interval',  'label': 'الفترة (دقائق)', 'type': 'number',   'default': 60},
        {'key': 'currency',  'label': 'العملة',        'type': 'select',
         'options': [{'value':'IQD','label':'دينار'},{'value':'USD','label':'دولار'}]},
    ]

أنواع الإعدادات: text, password, number, boolean, select, textarea

الصلاحيات المتاحة

الصلاحيةالوصف
accounting.invoices.readقراءة الفواتير
accounting.invoices.writeإنشاء/تعديل فواتير
accounting.vouchers.readقراءة السندات
accounting.vouchers.writeإنشاء سندات
accounting.reports.readقراءة التقارير المالية
accounting.checks.readقراءة الشيكات
customers.list.readقراءة بيانات الزبائن
customers.list.writeتعديل بيانات الزبائن
suppliers.list.readقراءة بيانات المجهّزين
inventory.materials.readقراءة المواد والمخزون
inventory.materials.writeتعديل المواد
pos.sales.readقراءة مبيعات POS
crm.leads.readقراءة العملاء المحتملين
hrm.employees.readقراءة بيانات الموظفين
system.settings.readقراءة إعدادات النظام

8التعبئة والرفع

  1. اضغط المجلد كملف ZIP:
    zip -r custom_my_plugin.zip custom_my_plugin/
  2. ارفع عبر مدير الإضافات: الإعدادات → مدير الإضافات → رفع إضافة (ZIP)
  3. ثبّت وفعّل: إضافات متاحة → تثبيتتفعيل

9النشر في متجر سيطرة

  1. أكمل manifest.json (اسم، وصف، أيقونة، tags)
  2. اختبر جيداً على حساب تجريبي
  3. ارفع ZIP عبر مدير الإضافات
  4. فريق سيطرة يراجع الإضافة (أمان + جودة + أداء)
  5. بعد الموافقة: تُنشر في المتجر لجميع المشتركين
مهم: لا تُنشر أي إضافة في المتجر إلا بعد مراجعة وموافقة الإدارة المركزية لضمان الأمان والجودة.

مرجع API الكامل — PluginContext (ctx)

الدالةالوصفمثال
ctx.query(table, filters, fields, order_by, limit)استعلام سجلاتctx.query('invoices', {'status':'active'}, limit=50)
ctx.get_by_id(table, id)جلب سجل واحدctx.get_by_id('customers', 5)
ctx.create(table, data)إنشاء سجلctx.create('ext_data', {'key':'v'})
ctx.update(table, id, data)تحديث سجلctx.update('customers', 5, {'name':'أحمد'})
ctx.execute_query(sql, params)SQL مخصص (SELECT فقط)ctx.execute_query("SELECT ...")
ctx.on_event(name, handler)الاستماع لحدثctx.on_event('invoice.created', fn)
ctx.emit_event(name, data)إطلاق حدث مخصصctx.emit_event('report.ready', {})
ctx.get_setting(key, default)قراءة إعدادctx.get_setting('api_key', '')
ctx.set_setting(key, value)حفظ إعدادctx.set_setting('last_run', '...')
ctx.send_notification(title, msg)إرسال إشعارctx.send_notification('تنبيه', '...')
ctx.log(level, message)تسجيل نشاطctx.log('info', 'تم')

جداول البيانات المتاحة للإضافات

المحاسبة

invoices, invoice_items, receipts, payments, journals, daily_vouchers, checks, accounts

الأطراف

customers, suppliers, sales_representatives

المخزون

materials, warehouses, stock_movements, categories, brands

POS

pos_sales, pos_sale_items, pos_shifts

CRM

crm_leads, crm_opportunities, crm_pipelines, crm_activities

مطاعم

restaurant_orders, restaurant_tables, kitchen_orders

جداول محظورة: users, user_sessions, role_permissions, tenant_config — لا يمكن الوصول إليها مطلقاً.

أسئلة شائعة

س: كيف أختبر إضافتي محلياً؟

ضع مجلد الإضافة في extensions/installed/، أعد تشغيل سيطرة، ثم ثبّت وفعّل من مدير الإضافات.

س: هل يمكن لإضافة الوصول لقاعدة بيانات مشترك آخر؟

لا. كل إضافة تعمل في سياق المشترك الحالي فقط (tenant isolation).

س: هل يمكنني استخدام مكتبات Python خارجية؟

نعم، بشرط أن تكون المكتبة مثبتة على السيرفر. تواصل مع فريق سيطرة لطلب تثبيت مكتبات جديدة.

س: ما حد الطلبات المسموح؟

100 طلب/دقيقة لكل إضافة. للإضافات ذات الحمل العالي، تواصل معنا لرفع الحد.

س: أين يظهر رابط الإضافة بعد التفعيل؟

إذا عرّفت get_menu_items()، يظهر الرابط في الشريط العلوي → قائمة الإعدادات/الأدوات بجانب "مدير الإضافات".