الإضافات (Plugins)
نظرة عامة
يتيح نظام إضافات Iberis لأي مطور إنشاء وحدات إضافية لتعزيز إمكانيات المنصة. الإضافات تصريحية بالكامل: ملف manifest بصيغة JSON يحدد نماذج البيانات والصفحات والقوائم والخطافات والأدوات.
تُقدم الإضافات للمراجعة من قبل فريق Iberis قبل نشرها في السوق. بمجرد الموافقة عليها، يمكن للشركات تثبيتها بنقرة واحدة من صفحة التكاملات.
تتميز الإضافات بنظام متكامل: نماذج بيانات مع CRUD تلقائي، حقول مخصصة على كيانات Iberis الحالية، توليد PDF، مسارات موافقة، وحراس لمنع إجراءات معينة بشروط.
Ce que vous pouvez construire
Voici des exemples concrets de plugins réalisables avec le système :
expense_items) sur plusieurs projets, ventilation par quantité ou montant, suivi des marges par projetهيكل Manifest
يحدد ملف manifest بصيغة JSON كل ما يتعلق بالإضافة. المعلومات الأساسية تُدخل عبر نموذج التقديم. يحتوي ملف manifest التقني على الأقسام التالية:
Exemple complet minimal
{
"models": {
"task": {
"numbering": { "prefix": "TSK", "pattern": "{prefix}-{YYYY}-{sequence}" },
"fields": {
"title": { "type": "string", "required": true, "label": { "fr": "Titre", "en": "Title" } },
"description": { "type": "textarea", "label": { "fr": "Description" } },
"status": { "type": "integer", "default": 0 },
"amount": { "type": "decimal", "label": { "fr": "Montant", "en": "Amount" } },
"due_date": { "type": "date", "label": { "fr": "Échéance", "en": "Due date" } },
"client_id": { "type": "reference", "references": "contacts", "label": { "fr": "Client" } },
"attachment": { "type": "file", "label": { "fr": "Pièce jointe" } }
},
"statuses": {
"0": { "fr": "Brouillon", "en": "Draft" },
"1": { "fr": "En cours", "en": "In Progress" },
"2": { "fr": "Terminé", "en": "Done" },
"3": { "fr": "Annulé", "en": "Cancelled" }
}
}
},
"pages": {
"task_list": {
"type": "list",
"model": "task",
"columns": ["_number", "title", "client_id", "amount", "status", "due_date"]
},
"task_form": {
"type": "form",
"model": "task",
"layout": [
{ "row": [{ "field": "title", "col": 8 }, { "field": "status", "col": 4 }] },
{ "row": [{ "field": "client_id", "col": 6 }, { "field": "amount", "col": 6 }] },
{ "row": [{ "field": "due_date", "col": 6 }, { "field": "attachment", "col": 6 }] }
]
}
},
"menus": [
{
"section": {
"title": { "fr": "Gestion de tâches", "en": "Task Management" },
"icon": "fas fa-tasks",
"items": [
{ "label": { "fr": "Taches", "en": "Tasks" }, "page": "task_list" }
]
}
}
],
"widgets": {
"total_tasks": {
"type": "stats_card",
"model": "task",
"aggregate": "COUNT",
"label": { "fr": "Total tâches", "en": "Total tasks" },
"icon": "fas fa-tasks",
"color": "#667eea"
},
"total_amount": {
"type": "stats_card",
"model": "task",
"aggregate": "SUM",
"field": "amount",
"filter": { "status": 2 },
"label": { "fr": "Montant termine", "en": "Completed amount" },
"icon": "fas fa-coins",
"color": "#10b981"
},
"by_status": {
"type": "chart",
"chart_type": "donut",
"model": "task",
"group_by": "status",
"aggregate": "COUNT",
"label": { "fr": "Par statut", "en": "By status" },
"color": "#667eea"
}
},
"hooks": {},
"settings": {},
"extensions": {},
"guards": {}
}أنواع الحقول
Chaque model définit ses champs avec un type. Le moteur crée automatiquement les colonnes en base et le formulaire adapté.
Tous les types
"fields": {
"name": { "type": "string", "required": true, "max": 100, "label": { "fr": "Nom" } },
"notes": { "type": "textarea", "label": { "fr": "Notes" } },
"quantity": { "type": "integer", "default": 1 },
"price": { "type": "decimal", "precision": 15, "scale": 3 },
"is_active": { "type": "boolean", "default": true },
"start_date": { "type": "date" },
"created_at": { "type": "datetime" },
"document": { "type": "file", "label": { "fr": "Document" } },
"photo": { "type": "image", "label": { "fr": "Photo" } },
"category": {
"type": "select",
"options": [
{ "value": "a", "label": { "fr": "Catégorie A" } },
{ "value": "b", "label": { "fr": "Catégorie B" } }
]
},
"client_id": {
"type": "reference",
"references": "contacts",
"label": { "fr": "Client" }
},
"related_task_id": {
"type": "reference",
"references": "@task",
"label": { "fr": "Tâche liée" }
},
"total_computed": {
"type": "computed",
"formula": "quantity * price",
"label": { "fr": "Total" }
}
}@ pour référencer un modèle interne au plugin (ex: @task). Voici toutes les entités système disponibles : Documents :
invoices, expenses, estimates, credits, deliveries, orders, receipts, services, disbursements, exit_vouchersLignes de documents :
invoice_items, expense_items, estimate_items, credit_items, delivery_items, order_items, receipt_items, service_items, disbursement_items, exit_voucher_itemsContacts & articles :
contacts, itemsFinance :
payments, withholdings, journals, journal_entries, company_banks, cash_accountsStock & projets :
warehouses, projectsStatuses (badges automatiques)
"statuses": {
"0": { "fr": "Brouillon", "en": "Draft" },
"1": { "fr": "En attente", "en": "Pending" },
"2": { "fr": "Approuvé", "en": "Approved" },
"3": { "fr": "Terminé", "en": "Completed" },
"4": { "fr": "Rejeté", "en": "Rejected" }
}Le champ status (integer) devient un select dans le formulaire et un badge coloré dans la liste. Couleurs : 0=warning, 1=info, 2=primary, 3=success, 4=danger.
الصيغ والتعبيرات
Utilisées dans les champs computed, les actions de hooks, et les conditions. Le parseur est un AST récursif sécurisé.
Exemples
// Champ computed - total TTC
"formula": "ROUND(quantity * price * (1 + tax_rate / 100), 3)"
// Condition hook - déclencher si montant > 1000
"conditions": [{ "field": "amount", "greater_than": 1000 }]
// Action hook - créer un record avec formule
"fields": {
"title": "CONCAT('Commande #', number)",
"total": "SUM(lines.quantity * lines.price)",
"days_left": "DATE_DIFF(due_date, TODAY())"
}
// Lookup inter-model
"formula": "LOOKUP(@bom, item_id = id, unit_cost)"Fonctions disponibles
الخطافات والإجراءات
Les hooks réagissent aux événements et exécutent des actions. Exemple : à chaque facture créée, créer automatiquement un suivi dans votre plugin.
Exemple : créer un record à chaque facture
"hooks": {
"on_invoice_created": {
"event": "invoice.created",
"conditions": [
{ "field": "status", "not_equals": 0 }
],
"actions": [
{
"type": "create_record",
"model": "invoice_tracking",
"fields": {
"invoice_id": "id",
"invoice_number": "invoice_number",
"amount": "total",
"status": "0",
"tracked_at": "NOW()"
}
}
]
}
}Exemple : mouvement de stock + ecriture comptable
"hooks": {
"on_order_completed": {
"event": "@order.updated",
"conditions": [
{ "field": "status", "changed_to": 3 }
],
"actions": [
{
"type": "stock_movement",
"item": "item_id",
"quantity_formula": "quantity",
"direction": "out"
},
{
"type": "journal_entry",
"debit": "601",
"credit": "401",
"amount_formula": "total",
"label_formula": "CONCAT('Plugin order #', number)"
},
{
"type": "send_email",
"to_formula": "LOOKUP(contacts, id = client_id, email)",
"subject": "Votre commande est prête",
"body": "Bonjour, votre commande a été traitée."
}
]
}
}Exemple : webhook + appel API externe
"hooks": {
"notify_external": {
"event": "payment.received",
"actions": [
{
"type": "webhook",
"url": "https://mon-erp.com/webhooks/iberis",
"event_name": "payment.received"
},
{
"type": "api_call",
"url": "https://api.mon-service.com/sync",
"method": "POST",
"headers": { "Authorization": "Bearer MY_TOKEN" },
"body": { "reference": "payment_number", "amount": "total" },
"response_mapping": { "external_id": "data.id" },
"target_model": "sync_log"
}
]
}
}Événements disponibles
invoice.createdinvoice.updatedinvoice.deletedexpense.createdexpense.updatedexpense.deletedpayment.receivedcontact.createdcontact.updatedcontact.deletedestimate.createdestimate.updatedestimate.deleteditem.createditem.updateditem.deletedservice.createdservice.updatedservice.deletedorder.createdorder.updatedorder.deleteddelivery.createddelivery.updateddelivery.deletedcredit.createdcredit.updatedcredit.deletedreceipt.createdreceipt.updatedreceipt.deletedexit_voucher.createdexit_voucher.updatedexit_voucher.deletedtej.createdtej.updatedtej.deletedPour les événements internes du plugin, préfixez avec @ : @model_name.created, @model_name.updated, @model_name.deleted
Notifications in-app et push
Envoyez des notifications aux utilisateurs depuis vos hooks. Les notifications apparaissent dans la cloche de l'interface, sur l'application mobile (push Firebase), et optionnellement par email.
Exemple : notification à l'approbation
"hooks": {
"notify_on_approval": {
"event": "@payment_request.updated",
"conditions": [
{ "field": "status", "changed_to": 3 }
],
"actions": [
{
"type": "send_notification",
"recipients": "current_user",
"title": { "fr": "Demande approuvée", "en": "Request approved" },
"content_formula": "CONCAT('La demande #', number, ' a été approuvée.')",
"link": {
"route": "pluginPageRecord",
"params": {
"pluginSlug": "mon-plugin",
"pageName": "request_form",
"recordId": "hashed_id"
}
}
}
]
}
}Destinataires
role)Exemple : notifier tous les managers
{
"type": "send_notification",
"recipients": "role",
"role": "Manager",
"title": { "fr": "Nouvelle demande de paiement" },
"content_formula": "CONCAT('Demande #', number, ' - ', total, ' TND')",
"email": true
}الرموز المختصرة
Injectez des données plugin dans les PDFs, emails et templates Iberis.
Exemples
// Nombre total de tâches
{{plugin:task-manager:COUNT(task)}}
// Somme des montants des tâches terminées
{{plugin:task-manager:SUM(task.amount)}}
// Champ custom sur une facture (via extensions)
{{plugin:task-manager:project_code}}
// Expression conditionnelle
{{plugin:task-manager:IF(COUNT(task) > 10, "Beaucoup de tâches", "Peu de tâches")}}امتدادات الكيانات
Ajoutez des champs custom aux entités Iberis existantes. Les champs s'affichent automatiquement dans les formulaires de création/edition.
Exemple : ajouter des champs sur les factures et dépenses
"extensions": {
"invoice": {
"custom_fields": {
"project_code": {
"type": "string",
"label": { "fr": "Code projet", "en": "Project code" }
},
"is_billable": {
"type": "boolean",
"label": { "fr": "Facturable", "en": "Billable" },
"default": true
},
"priority": {
"type": "select",
"label": { "fr": "Priorite" },
"options": [
{ "value": "low", "label": { "fr": "Basse", "en": "Low" } },
{ "value": "medium", "label": { "fr": "Moyenne", "en": "Medium" } },
{ "value": "high", "label": { "fr": "Haute", "en": "High" } }
]
}
}
},
"expense": {
"custom_fields": {
"department": {
"type": "string",
"label": { "fr": "Departement", "en": "Department" }
},
"approved_amount": {
"type": "decimal",
"label": { "fr": "Montant approuvé", "en": "Approved amount" }
}
}
}
}Entités extensibles
invoiceexpensepaymentcontactitemestimateorderservicecreditreceiptdeliveryأدوات لوحة المعلومات
Les widgets s'affichent en haut de la page liste de votre plugin. Ajoutez "dashboard": true sur un widget pour l'afficher aussi sur le tableau de bord principal de l'entreprise.
Exemple complet
"widgets": {
"total": {
"type": "stats_card",
"model": "task",
"aggregate": "COUNT",
"label": { "fr": "Total", "en": "Total" },
"icon": "fas fa-list",
"color": "#667eea",
"dashboard": true
},
"completed": {
"type": "stats_card",
"model": "task",
"aggregate": "COUNT",
"filter": { "status": 2 },
"label": { "fr": "Terminées", "en": "Completed" },
"icon": "fas fa-check",
"color": "#10b981"
},
"total_amount": {
"type": "stats_card",
"model": "task",
"aggregate": "SUM",
"field": "amount",
"label": { "fr": "Montant total", "en": "Total amount" },
"icon": "fas fa-coins",
"color": "#f59e0b"
},
"by_status": {
"type": "chart",
"chart_type": "donut",
"model": "task",
"group_by": "status",
"aggregate": "COUNT",
"label": { "fr": "Répartition", "en": "Distribution" },
"color": "#667eea"
}
}Types de graphiques supportés : bar, donut, pie.
ألسنة على الصفحات الموجودة
Ajoutez un onglet dans les pages de synthèse des clients, fournisseurs, articles, ou dans la sidebar des documents (factures, dépenses, etc.). L'onglet affiche les records de votre plugin liés à l'entité.
Exemple : onglet "Deals" sur la fiche client
Le foreign_key doit correspondre à un champ de type reference déclaré dans votre model. C'est ce champ qui fait le lien entre votre record plugin et l'entité Iberis.
// 1. Déclarer le model avec un champ reference vers contacts
"models": {
"deal": {
"fields": {
"title": { "type": "string", "required": true, "label": { "fr": "Titre" } },
"amount": { "type": "decimal", "label": { "fr": "Montant" } },
"status": { "type": "integer", "default": 0 },
"client_id": {
"type": "reference",
"references": "contacts",
"label": { "fr": "Client", "en": "Client" }
}
},
"statuses": {
"0": { "fr": "Nouveau", "en": "New" },
"1": { "fr": "En cours", "en": "In progress" },
"2": { "fr": "Gagné", "en": "Won" }
}
}
},
// 2. Déclarer l'onglet qui utilise ce champ comme foreign_key
"tabs": {
"contact": {
"label": { "fr": "Deals liés", "en": "Related deals" },
"model": "deal",
"foreign_key": "client_id",
"columns": ["_number", "title", "amount", "status"]
}
}client_id = l'ID du client courant. Autre exemple : suivi sur les factures
"models": {
"invoice_tracking": {
"fields": {
"invoice_id": { "type": "reference", "references": "invoices" },
"status": { "type": "integer", "default": 0 },
"tracked_at": { "type": "date" },
"notes": { "type": "text" }
}
}
},
"tabs": {
"invoice": {
"label": { "fr": "Suivi", "en": "Tracking" },
"model": "invoice_tracking",
"foreign_key": "invoice_id",
"columns": ["status", "tracked_at", "notes"]
}
}Entités supportées
client_id de type reference vers contacts). L'onglet filtre automatiquement les records par cette clé. الإعدادات
Chaque entreprise peut configurer votre plugin via la page Paramètres. Les valeurs sont accessibles dans les hooks et formules.
Exemple
"settings": {
"api_key": {
"type": "string",
"label": { "fr": "Clé API", "en": "API Key" },
"help": { "fr": "Votre clé d'authentification externe", "en": "Your external auth key" },
"default": ""
},
"mode": {
"type": "select",
"label": { "fr": "Mode" },
"default": "production",
"options": [
{ "value": "sandbox", "label": { "fr": "Test", "en": "Sandbox" } },
{ "value": "production", "label": { "fr": "Production" } }
]
},
"auto_sync": {
"type": "boolean",
"label": { "fr": "Synchronisation automatique", "en": "Auto sync" },
"default": true
},
"daily_limit": {
"type": "number",
"label": { "fr": "Limite journalière", "en": "Daily limit" },
"default": 100
},
"notes": {
"type": "textarea",
"label": { "fr": "Notes de configuration", "en": "Config notes" },
"default": ""
}
}إجراءات على المستندات
Ajoutez des boutons dans le menu déroulant "Action" des documents Iberis (factures, dépenses, devis, etc.) et dans la page formulaire. Trois types d'actions sont disponibles :
Exemple : bouton "Créer une demande de paiement" sur les dépenses
// 1. Le model avec une référence vers expenses
"models": {
"payment_request": {
"fields": {
"title": { "type": "string", "required": true },
"expense_id": { "type": "reference", "references": "expenses" },
"amount": { "type": "decimal" },
"status": { "type": "integer", "default": 0 }
}
}
},
// 2. La page formulaire
"pages": {
"request_form": { "type": "form", "model": "payment_request" }
},
// 3. L'action qui apparaît dans le dropdown de chaque dépense
"actions": {
"create_payment_request": {
"entity_type": "expense",
"label": { "fr": "Créer demande de paiement", "en": "Create payment request" },
"icon": "fas fa-file-invoice",
"color": "primary",
"type": "create_record",
"target_page": "request_form",
"prefill": {
"expense_id": "_id",
"amount": "total"
}
}
}"_id" est une valeur spéciale qui injecte l'identifiant du document. Les autres valeurs correspondent aux champs du document (ex: "total", "invoice_number"). Exemple : générer un PDF depuis une facture
"actions": {
"generate_tracking_pdf": {
"entity_type": "invoice",
"label": { "fr": "Fiche de suivi PDF", "en": "Tracking sheet PDF" },
"icon": "fas fa-file-pdf",
"color": "danger",
"type": "generate_pdf",
"pdf_template": "tracking_sheet"
}
}Exemple : naviguer vers une page du plugin
"actions": {
"view_deals": {
"entity_type": "contact",
"label": { "fr": "Voir les deals", "en": "View deals" },
"icon": "fas fa-handshake",
"color": "info",
"type": "navigate",
"target_page": "deals_list"
}
}Entités supportées
invoiceexpenseestimateorderdeliveryexit_vouchercreditreceiptservicepaymentCouleurs disponibles
primarysuccessdangerwarninginfoحراس الإجراءات
تسمح الحراس لإضافتك بمنع إجراءات Iberis إذا لم تتحقق شروط معينة.
Exemple : bloquer un paiement sans demande approuvée
"guards": {
"require_payment_request": {
"event": "payment.before_create",
"conditions": [],
"check": {
"type": "require_record",
"model": "payment_request",
"where": {
"expense_id": "hashed_expense_id"
},
"status": 3
},
"message": {
"fr": "Un paiement ne peut être effectué sans demande de paiement approuvée.",
"en": "Payment cannot be made without an approved payment request."
}
}
}Exemple : bloquer la suppression si champ non vide
"guards": {
"prevent_delete_if_linked": {
"event": "invoice.before_delete",
"check": {
"type": "require_field",
"field": "external_ref"
},
"message": {
"fr": "Cette facture est liée à un système externe et ne peut pas être supprimée."
}
}
}Exemple : bloquer la validation avec expression
"guards": {
"min_amount": {
"event": "expense.before_validate",
"check": {
"type": "expression",
"expression": "total >= 10"
},
"message": {
"fr": "Le montant minimum pour valider une dépense est de 10 TND."
}
}
}Événements guard disponibles
payment.before_createinvoice.before_validateinvoice.before_deleteexpense.before_validateexpense.before_deleteestimate.before_validateestimate.before_deleteorder.before_validateorder.before_deletedelivery.before_validatedelivery.before_deletecredit.before_validatecredit.before_deletereceipt.before_validatereceipt.before_deleteservice.before_validateservice.before_deletecontact.before_deleteقوالب PDF
أنشئ مستندات PDF مخصصة من بيانات نماذجك باستخدام HTML/CSS.
Exemple : demande de paiement PDF
"pdf_templates": {
"demande_paiement": {
"model": "payment_request",
"format": "A4",
"orientation": "P",
"margin_top": 15,
"margin_bottom": 15,
"margin_left": 10,
"margin_right": 10,
"data": {
"lines": {
"model": "@request_line",
"type": "list",
"where": { "request_id": "id" },
"order_by": "created_at"
},
"bank": {
"model": "company_banks",
"type": "single",
"where": { "id": "bank_id" }
}
},
"css": "body { font-family: Arial, sans-serif; font-size: 12px; } .header { text-align: center; margin-bottom: 20px; } .title { font-size: 18px; font-weight: bold; } table { width: 100%; border-collapse: collapse; margin: 15px 0; } th, td { border: 1px solid #ddd; padding: 8px; text-align: left; } th { background: #f5f5f5; } .signatures { display: flex; justify-content: space-between; margin-top: 40px; } .sig-block { text-align: center; width: 30%; } .sig-line { border-top: 1px solid #333; margin-top: 50px; padding-top: 5px; }",
"html": "<div class='header'><div class='title'>DEMANDE DE PAIEMENT</div><div>N: {{number}} - Date: {{date}}</div><div>{{company.title}}</div></div><table><tr><th>#</th><th>Description</th><th>Montant</th></tr>{{#each related.lines}}<tr><td>{{_index}}</td><td>{{item.description}}</td><td>{{item.amount}} TND</td></tr>{{/each}}</table><p><strong>Total: {{total}} TND</strong></p><p>Banque: {{related.bank.title}} - RIB: {{related.bank.rib}}</p><div class='signatures'><div class='sig-block'><div class='sig-line'>Charge des moyens généraux</div></div><div class='sig-block'><div class='sig-line'>Direction Générale</div></div><div class='sig-block'><div class='sig-line'>Direction Financière</div></div></div>"
}
}Variables simples
{{field_name}} — Champ du record principal{{company.title}} — Données entreprise ({{company.address}}, {{company.ttn_fiscal_id}}){{related.alias.field}} — Donnée liée (single){{today}} ou {{today:d/m/Y}} — Date du jourFormatage
// Formater un nombre (3 décimales par défaut)
{{number:amount:2}} → 1 500.00
{{number:item.price:3}} → 25.500
// Formater une date (format PHP)
{{date:created_at:d/m/Y}} → 19/03/2026
{{date:item.due:Y-m-d}} → 2026-03-19
// Somme d'un champ dans une collection
{{sum:related.lines.amount:2}} → 4 250.00
// Nombre d'éléments
{{count:related.lines}} → 5
// Image dynamique (logo, signature)
{{image:company.logo:200}} → <img> 200px de large
{{image:company.stamp:150:150}} → <img> 150x150pxConditions
// Afficher un bloc si la condition est vraie
{{#if status == 3}}
<div class="badge-success">APPROUVÉ</div>
{{#else}}
<div class="badge-warning">EN ATTENTE</div>
{{/if}}
// Masquer si la condition est vraie
{{#unless total == 0}}
<p>Total: {{number:total:3}} TND</p>
{{/unless}}
// Conditions dans les boucles
{{#each related.lines}}
<tr>
<td>{{item.description}}</td>
<td>{{number:item.amount:2}}</td>
{{#if item.status == 1}}
<td class="text-success">Payé</td>
{{#else}}
<td class="text-danger">Impayé</td>
{{/if}}
</tr>
{{/each}}Boucles
{{#each related.lines}}
<tr>
<td>{{_index}}</td>
<td>{{item.description}}</td>
<td>{{number:item.quantity:0}}</td>
<td>{{number:item.unit_price:3}}</td>
<td>{{number:item.line_total:3}}</td>
</tr>
{{/each}}parent), ils sont automatiquement chargés comme données liées sans avoir besoin de les déclarer dans data. Le nom de l'alias est le nom du model enfant. Opérateurs de comparaison
== égal, != différent, > supérieur, < inférieur, >= supérieur ou égal, <= inférieur ou égalAPI
// Générer le PDF (retourne le chemin du fichier)
POST /api/private/company/{companyId}/plugins/{slug}/pdf/{templateName}/{recordId}
// Télécharger directement le PDF
GET /api/private/company/{companyId}/plugins/{slug}/pdf/{templateName}/{recordId}/downloadModèles personnalisés (documents Iberis)
À ne pas confondre avec pdf_templates (qui sert à générer des PDF à partir de vos models de plugin), la section custom_templates permet d'installer des modèles HTML/CSS qui s'ajoutent au sélecteur de modèles natif d'Iberis pour les documents du cœur Iberis (factures, devis, livraisons, etc.). C'est le moyen idéal pour vendre des packs de designs premium.
custom_templates — la section models n'est plus obligatoire dans ce cas. Vous pouvez livrer un pack de modèles sans aucun model, page ou hook. Cycle de vie
custom_templates est insérée dans la table custom_templates de l'entreprise avec plugin_slug = slug du plugin. Les modèles apparaissent immédiatement dans Paramètres → Modèles PDF.plugin_slug sont supprimés. Si l'utilisateur avait sélectionné un de vos modèles comme défaut, le système retombe automatiquement sur le modèle compact.updateOrCreate sur (company_id, plugin_slug, name, document_type). Le HTML, le CSS et l'aperçu sont écrasés.document_type accepte une chaîne OU un tableau de chaînes. Si vous passez un tableau, le moteur d'installation crée automatiquement une ligne par type de document — un seul modèle "Tribune" peut donc apparaître à la fois dans le sélecteur de factures, devis et factures fournisseurs sans dupliquer le HTML/CSS dans le manifest.Types de documents supportés
La valeur de document_type doit être l'une des suivantes (chaîne ou tableau) :
invoice, estimate, delivery, credit, order, exitVoucher, paymentSaleexpense, paymentPurchasereceipt, inventory, entry, disbursementStructure du manifest
{
"plugin": {
"id": "premium-invoice-templates",
"name": { "fr": "Modèles de Factures Premium" },
"description": { "fr": "3 modèles de factures conçus professionnellement." },
"version": "1.0.0",
"category": "finance",
"module": "finance"
},
"custom_templates": [
{
"name": "Tribune",
"document_type": ["invoice", "estimate", "expense"],
"css_content": "body { font-family: Helvetica; ... }",
"html_content": "<div class='wrap'>...</div>",
"preview_image": null
},
{
"name": "Boutique",
"document_type": "invoice",
"css_content": "body { font-family: Arial; ... }",
"html_content": "<div class='bar'>...</div>"
}
]
}Champs d'une entrée
name (requis) — Nom affiché dans le sélecteur. Accepte une chaîne ou un objet i18n (le en sera utilisé comme repli).document_type (requis) — Le type de document Iberis ciblé. Accepte une chaîne ("invoice") ou un tableau (["invoice", "estimate", "expense"]) pour rendre le même modèle disponible sur plusieurs types de documents en une seule entrée.html_content (requis) — Le HTML du modèle. Aucune balise <script>, aucune URL javascript:, aucun gestionnaire d'événement (onclick, etc.) — la validation rejette le manifest sinon.css_content (optionnel) — CSS inline du modèle. Tout doit être autonome — pas de feuilles de style externes.preview_image (optionnel) — Chemin ou data-URI vers une image d'aperçu pour le sélecteur.Variables disponibles dans le HTML/CSS
Le moteur de rendu utilise un remplacement {{variable.path}} simple (pas de Handlebars complet — pas de {{#each}} ni de {{#if}} dans les custom_templates). Les tableaux dynamiques sont insérés via des macros spéciales.
// Entreprise
{{company.name}} {{company.address}} {{company.phone}}
{{company.email}} {{company.fiscal_id}} {{company.website}}
{{company.city}} {{company.postal_code}} {{company.country}}
{{company.logo}} // se rend en <img> auto-dimensionné
{{company.stamp}} // se rend en <img> auto-dimensionné
// Document (facture, devis, etc.)
{{document.number}} {{document.date}} {{document.due_date}}
{{document.object}} {{document.reference}} {{document.currency}}
{{document.subtotal}} {{document.tax_total}} {{document.discount}}
{{document.total}} {{document.paid}} {{document.balance}}
{{document.credits}} {{document.withholding}} {{document.total_words}}
{{document.notes}} {{document.conditions}}
// Contact (client / fournisseur)
{{contact.name}} {{contact.organisation}} {{contact.address}}
{{contact.phone}} {{contact.email}} {{contact.fiscal_id}}
{{contact.city}} {{contact.postal_code}} {{contact.country}}
{{contact.billing_address}} {{contact.delivery_address}}
// Banque (depuis la facture)
{{bank.name}} {{bank.iban}} {{bank.bic}}
{{bank.rib}}
// Date
{{date.today}} {{date.year}}
// Tableaux dynamiques (rendus en <table> complet)
{{items.table}} // lignes du document
{{taxes.table}} // récapitulatif TVA
{{payment.table}} // historique des paiements
{{inventory.table}} // mouvements d'inventaire
{{journal_entry.table}} // écritures comptables
{{disbursement.table}} // détails d'un décaissementcustom_templates n'utilisent pas la même syntaxe que les pdf_templates de plugin. Pas de {{#each}}, pas de {{#if}}, pas de formateurs {{number:...}} ou {{date:...}}. C'est un remplacement de variables simple par str_replace. Les tableaux dynamiques sont des macros ({{items.table}}) et la mise en forme des montants est gérée par le moteur de rendu Iberis selon la devise du document. Masquage automatique des lignes vides
Le moteur de rendu masque automatiquement les lignes ou éléments dont la seule valeur dynamique est vide — aucun besoin de CSS :empty ou de logique conditionnelle. Écrivez simplement votre libellé et sa variable dans la même structure et le moteur fait le ménage.
<tr><td>Remise</td><td>{{document.discount}}</td></tr> — si discount est vide, la ligne entière disparaît (libellé compris).<td>Prix</td><td>:</td><td>{{x}}</td>), afin que le séparateur ne reste pas orphelin.<td> de la ligne contient une variable remplie, la ligne est conservée et seule la cellule vide (+ son libellé adjacent) est vidée — l'alignement des colonnes reste intact.<ul>/<ol>, le <li> contenant la variable vide est retiré ; le reste de la liste est préservé.<dl>, un <dd>{{x}}</dd> vide fait disparaître aussi son <dt> précédent, sans toucher aux autres paires de la liste.<p>Date : {{document.date}}</p> ou <span>{{x}}</span> dont la variable est vide voit l'élément parent direct entier supprimé. Attention : tout contenu statique présent dans ce parent disparaît aussi — gardez une seule variable par élément pour éviter les surprises.<td>, <li> ou <p> contient à la fois une variable vide et une variable remplie (ex. <td>{{a}} / {{b}}</td>), le moteur préserve la cellule pour ne pas perdre la donnée remplie — le libellé associé à la variable vide reste visible. Placez chaque couple libellé/valeur dans sa propre cellule / ligne pour bénéficier du masquage. Exemple complet d'une entrée
Cet exemple utilise un seul document_type en tableau pour cibler facture, devis et facture fournisseur en une seule entrée. Notez l'usage de vraies balises <table> avec des attributs width HTML — mPDF rend les tables natives bien plus fiablement que les divs avec display:table-cell. Aucun CSS :empty n'est nécessaire : les lignes de totaux dont la variable est vide (remise, retenue, crédits…) disparaissent d'elles-mêmes grâce au masquage automatique décrit plus haut.
{
"name": "Tribune",
"document_type": ["invoice", "estimate", "expense"],
"css_content": "body{font-family:Helvetica,Arial,sans-serif;font-size:10pt;color:#0a0a0a} .head{background:#0a0a0a;color:#fff;padding:24px} .num{font-size:32pt;font-weight:bold;letter-spacing:-1px} .row td{padding:8px 0;border-bottom:1px solid #eee} .row .l{color:#737373;text-transform:uppercase;font-size:8pt;letter-spacing:1px;padding-right:14px} .row .v{text-align:right;font-weight:600} .grand td{border-top:2px solid #0a0a0a;font-size:14pt;font-weight:bold}",
"html_content": "<table width='100%' cellpadding='0' cellspacing='0'><tr><td class='head'><strong>{{company.name}}</strong> · MF {{company.fiscal_id}}<br><span class='num'>{{document.number}}</span></td></tr></table><table width='100%' cellpadding='0' cellspacing='0'><tr><td>Émise à</td><td>{{contact.name}}</td></tr><tr><td>Organisation</td><td>{{contact.organisation}}</td></tr><tr><td>Adresse</td><td>{{contact.address}}</td></tr><tr><td>Date</td><td>{{document.date}}</td></tr><tr><td>Échéance</td><td>{{document.due_date}}</td></tr></table>{{items.table}}{{taxes.table}}<table width='100%' cellpadding='0' cellspacing='0'><tr><td width='50%'></td><td width='50%'><table width='100%' cellpadding='0' cellspacing='0'><tr class='row'><td class='l'>Sous-total HT</td><td class='v'>{{document.subtotal}}</td></tr><tr class='row'><td class='l'>Remise</td><td class='v'>{{document.discount}}</td></tr><tr class='row'><td class='l'>TVA</td><td class='v'>{{document.tax_total}}</td></tr><tr class='row'><td class='l'>Retenue à la source</td><td class='v'>{{document.withholding}}</td></tr><tr class='row'><td class='l'>Avoirs appliqués</td><td class='v'>{{document.credits}}</td></tr><tr class='row grand'><td class='l'>Total TTC</td><td class='v'>{{document.total}}</td></tr></table></td></tr></table><p><em>{{document.total_words}}</em></p><table width='100%' cellpadding='0' cellspacing='0'><tr><td>Banque</td><td>{{bank.name}}</td></tr><tr><td>RIB</td><td>{{bank.rib}}</td></tr></table>{{company.stamp}}"
}display:flex, display:grid et display:table-cell sur des <div> — ils ne se rendent pas de manière fiable. Utilisez plutôt de vraies balises <table> avec attributs HTML width="N%", cellpadding, cellspacing. Évitez aussi les pseudo-éléments ::before/::after avec content pour afficher des libellés — mPDF les ignore souvent. Mettez les libellés dans de vrais <td>. Validation à la soumission
name, document_type et html_content.document_type doit être une chaîne ou un tableau de chaînes, et chaque valeur doit être dans la liste autorisée.<script>, javascript: et les gestionnaires d'événements on*= — rejet immédiat si trouvés.company_id + plugin_slug, donc chaque entreprise a sa propre copie. La désinstallation pour une entreprise n'affecte pas les autres. مسارات الموافقة
أضف مسار تحقق على نماذجك مع خطوات موافقة وأدوار محددة.
Exemple : circuit DG → DFC
"models": {
"payment_request": {
"fields": {
"title": { "type": "string", "required": true },
"total": { "type": "decimal" },
"status": { "type": "integer", "default": 0 },
"expense_id": { "type": "reference", "references": "expenses" }
},
"statuses": {
"0": { "fr": "Brouillon", "en": "Draft" },
"1": { "fr": "Soumis", "en": "Submitted" },
"2": { "fr": "Approuvé DG", "en": "CEO Approved" },
"3": { "fr": "Valide DFC", "en": "CFO Validated" },
"4": { "fr": "Rejeté", "en": "Rejected" }
},
"approval_workflow": {
"approval_mode": "sequential",
"steps": [
{
"label": { "fr": "Validation Direction Générale", "en": "CEO Approval" },
"role": "manager"
},
{
"label": { "fr": "Validation Direction Financière", "en": "CFO Approval" },
"role": "accountant"
}
],
"on_approve": { "set_status": 3 },
"on_reject": { "set_status": 4 }
}
}
}Modes d'approbation
API
// Consulter l'état du workflow
GET /api/private/company/{companyId}/plugins/{slug}/{model}/{recordId}/approval
// Approuver (status=1) ou rejeter (status=2) une étape
POST /api/private/company/{companyId}/plugins/{slug}/{model}/{recordId}/approval
Body: { "step_index": 0, "status": 1, "comment": "OK" }محولات (تحويل البيانات)
Les mutators permettent de modifier automatiquement les données d'un document Iberis avant sa sauvegarde. Contrairement aux guards qui bloquent, les mutators transforment.
Exemple : forcer une valeur par défaut
"mutators": {
"default_payment_terms": {
"event": "invoice.before_save",
"default": {
"conditions": "Paiement à 30 jours"
}
}
}Exemple : calculer et forcer une valeur
"mutators": {
"auto_reference": {
"event": "expense.before_save",
"conditions": [
{ "field": "status", "equals": 0 }
],
"set": {
"reference": "CONCAT('REF-', order_number)"
}
}
}conditions : Optionnel. Le mutator ne s'applique que si les conditions sont remplies.
Événements disponibles
invoice.before_saveexpense.before_saveestimate.before_savepayment.before_saveitem.before_saveorder.before_savedelivery.before_savecredit.before_savereceipt.before_saveservice.before_saveexit_voucher.before_saveصفحات لوحة المعلومات
Créez des pages qui affichent des widgets et des tableaux de données de plusieurs models en une seule vue. Idéal pour les tableaux de bord, les rapports, ou les vues d'ensemble.
Exemple
"pages": {
"crm_dashboard": {
"type": "dashboard",
"title": { "fr": "Tableau de bord CRM", "en": "CRM Dashboard" },
"widgets": ["total_deals", "won_deals", "deals_by_status"],
"sections": [
{
"model": "deal",
"title": { "fr": "Deals récents", "en": "Recent deals" },
"columns": ["_number", "title", "amount", "status"],
"limit": 5,
"filter": {}
},
{
"model": "activity",
"title": { "fr": "Dernières activités", "en": "Latest activities" },
"columns": ["_number", "type", "description", "date"],
"limit": 10
}
]
}
}Structure
widgets du manifest)form existe pour ce model). استيراد / تصدير CSV
Chaque page liste d'un plugin dispose automatiquement de boutons d'export et d'import CSV. Aucune configuration n'est nécessaire dans le manifest.
Export
;)id, company_id, deleted_atnumber, created_at, updated_at en lecture seuleImport
number, created_at, updated_at sont ignorées à l'importrequired et les types sont vérifiésAPI
// Export CSV
GET /api/private/company/{companyId}/plugins/{slug}/{model}/export
→ Retourne un fichier CSV en téléchargement
// Import CSV
POST /api/private/company/{companyId}/plugins/{slug}/{model}/import
Body: FormData avec champ "file" (CSV)
→ Retourne: { imported: 15, errors: ["Line 3: column count mismatch"] }; (point-virgule), encodage UTF-8, valeurs entre guillemets doubles. La première ligne contient les noms de colonnes. علاقات رئيسية-تفصيلية
Créez des relations parent-enfant entre vos models. Par exemple, une commande avec des lignes de commande, ou une demande de paiement avec des factures liées. Les lignes enfant sont éditables directement dans le formulaire du parent.
Exemple : commande avec lignes
"models": {
"order": {
"numbering": { "prefix": "CMD", "pattern": "{prefix}-{YYYY}-{sequence}" },
"fields": {
"title": { "type": "string", "required": true, "label": { "fr": "Titre" } },
"client_id": { "type": "reference", "references": "contacts", "label": { "fr": "Client" } },
"date": { "type": "date", "label": { "fr": "Date" } },
"status": { "type": "integer", "default": 0 },
"total": {
"type": "computed",
"formula": "SUM(order_line.quantity * order_line.unit_price)",
"label": { "fr": "Total" }
}
},
"statuses": {
"0": { "fr": "Brouillon" },
"1": { "fr": "Confirmée" },
"2": { "fr": "Livrée" }
}
},
"order_line": {
"label": { "fr": "Lignes de commande", "en": "Order lines" },
"parent": {
"model": "order",
"foreign_key": "order_id"
},
"fields": {
"order_id": { "type": "reference", "references": "@order" },
"description": { "type": "string", "required": true, "label": { "fr": "Description" } },
"quantity": { "type": "integer", "default": 1, "label": { "fr": "Quantité" } },
"unit_price": { "type": "decimal", "label": { "fr": "Prix unitaire" } },
"line_total": {
"type": "computed",
"formula": "quantity * unit_price",
"label": { "fr": "Total ligne" }
}
}
}
}Comportement
GET /record retourne les enfants dans _children. Le CRUD des enfants utilise les mêmes endpoints que les records normaux.Déclaration
parent.foreign_key : Le champ du model enfant qui référence le parent (doit être de type
reference vers @parent_model)label : Le titre affiché au-dessus du tableau des lignes dans le formulaire
Autre exemple : demande de paiement avec factures liées
"models": {
"payment_request": {
"fields": {
"title": { "type": "string", "required": true },
"total": { "type": "computed", "formula": "SUM(request_line.amount)" },
"status": { "type": "integer", "default": 0 }
}
},
"request_line": {
"label": { "fr": "Factures liées", "en": "Linked invoices" },
"parent": { "model": "payment_request", "foreign_key": "request_id" },
"fields": {
"request_id": { "type": "reference", "references": "@payment_request" },
"expense_id": { "type": "reference", "references": "expenses", "label": { "fr": "Facture" } },
"amount": { "type": "decimal", "label": { "fr": "Montant" } },
"description": { "type": "string", "label": { "fr": "Description" } }
}
}
}الصلاحيات حسب الدور
Contrôlez l'accès aux fonctionnalités de votre plugin par rôle d'utilisateur. Par défaut, 4 permissions sont créées automatiquement : canRead, canCreate, canUpdate, canDelete.
Permissions par défaut
Si vous ne déclarez pas de section permissions, les 4 permissions standard sont utilisées. Elles sont automatiquement ajoutées au rôle "Propriétaire" lors de l'installation.
// Implicite (pas besoin de le déclarer) :
"permissions": ["canRead", "canCreate", "canUpdate", "canDelete"]
// Les clés générées seront :
// plugin:mon-plugin:canRead
// plugin:mon-plugin:canCreate
// plugin:mon-plugin:canUpdate
// plugin:mon-plugin:canDeletePermissions personnalisées
"permissions": [
"canRead",
"canCreate",
"canUpdate",
"canDelete",
"canExport",
"canApprove",
"canManageSettings"
]plugin:{slug}:. Par exemple, canApprove devient plugin:mon-plugin:canApprove. C'est ce format qui est stocké dans le rôle de l'utilisateur. Comportement
canReadcanRead, création canCreate, modification canUpdate, suppression canDeleteUtiliser dans les menus
"menus": [{
"section": {
"title": { "fr": "Mon plugin" },
"items": [
{
"label": { "fr": "Liste" },
"page": "list_page",
"permission": "canRead"
},
{
"label": { "fr": "Paramètres" },
"page": "settings_page",
"permission": "canManageSettings"
}
]
}
}]Si permission n'est pas déclaré sur un item de menu, canRead est utilisé par défaut.
الحدود والأمان
Sécurité
- Isolation multi-tenant totale — toutes les requêtes sont scopées par
company_id - Pas de code exécutable — uniquement du JSON déclaratif interprété
- Webhooks et api_call — HTTPS uniquement
- HTML long_description — sanitisé (pas de script, pas d'event handlers)
- Formules — parseur AST sécurisé, pas de raw SQL
- Noms de tables/champs réservés — protégés contre les collisions
- Review manuelle par l'équipe Iberis avant publication















Facebook
Instagram
Linkedin