Logo
Iberis Docs

Aide

Extensions (plugins)

Vue d'ensemble

Le système d'extensions d'Iberis permet à tout développeur de créer des modules complémentaires pour enrichir les fonctionnalités de la plateforme. Les extensions sont entièrement déclaratives : un fichier manifest JSON définit les modèles de données, les pages, les menus, les hooks et les widgets.

Les extensions sont soumises pour vérification par l'équipe Iberis avant d'être publiées sur la marketplace. Une fois approuvées, les entreprises peuvent les installer en un clic depuis la page Intégrations.

Les extensions disposent d'un système complet : modèles de données avec CRUD automatique, champs personnalisés sur les entités Iberis existantes, génération de PDF, workflows d'approbation, et guards pour bloquer certaines actions sous conditions.

Flux de publication : Soumission du manifest → Review par l'équipe Iberis → Approbation → Disponible sur la marketplace → Installation en un clic par les entreprises.

Ce que vous pouvez construire

Voici des exemples concrets de plugins réalisables avec le système :

Gestion de projets — Modèles projet + tâche (master-detail), référence vers contacts, formules de budget (SUM des tâches), hooks pour créer des écritures comptables à la facturation, onglets sur les fiches clients
Allocation de coûts — Répartir les lignes de factures fournisseurs (expense_items) sur plusieurs projets, ventilation par quantité ou montant, suivi des marges par projet
Gestion de stock avancée — Vue matricielle produits × entrepôts avec colonnes dynamiques, valorisation multi-méthodes, alertes de réapprovisionnement via hooks, export CSV enrichi
Édition en masse — Pages de liste avec édition inline, filtres par catégorie/fournisseur, modification simultanée de références et libellés, détection de doublons via guards
CRM / Suivi commercial — Modèles opportunité + activité, pipeline par statuts, onglets sur contacts, actions pour créer un devis depuis une opportunité, widgets dashboard (CA prévisionnel, taux de conversion)
Circuits de validation — Demandes d'achat avec workflow séquentiel (responsable → directeur), guards pour bloquer les paiements sans approbation, PDF de bon de commande interne
Commissions vendeurs — Calcul automatique via hooks sur création de facture, formules basées sur le montant HT, onglet sur la fiche contact avec historique, export CSV mensuel
Champs personnalisés — Extensions sur les factures, dépenses, contacts et articles existants (champs texte, select, booléen). Visibles dans les formulaires et les listes Iberis sans code
Packs de modèles PDF premium — Livrez des modèles HTML/CSS pour les factures, devis, livraisons, etc. Ils s'installent automatiquement dans le sélecteur de modèles existant et se désinstallent proprement. Aucun model ni page nécessaire — un plugin peut être 100% modèles
Limitations : Les extensions sont 100% déclaratives (JSON) — pas d'exécution de code PHP/JS. Les fonctionnalités nécessitant un calendrier interactif drag & drop, des graphiques temps réel, ou des intégrations API bidirectionnelles complexes ne sont pas supportées dans la version actuelle.

Structure du manifest

Le manifest JSON définit tout votre plugin. Les informations de base (titre, description, icône) sont saisies via le formulaire de soumission. Le manifest technique contient les sections suivantes :

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": {}
}

Types de champs

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": "string", "max": 40, "label": { "fr": "Catégorie (A, B, C…)" } },
  "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" }
  }
}
Pas de type "select" : Il n'existe pas de type de champ pour un choix contraint à des valeurs personnalisées (l'exemple ci-dessus utilise string pour category, valeurs documentées dans le label seulement). Le seul champ qui se comporte comme un select géré par la plateforme est le champ réservé status (voir Statuses ci-dessous). Conséquence à anticiper : rien ne valide côté serveur qu'une valeur saisie ou importée appartient à la liste que vous documentez — une faute de frappe passe silencieusement, y compris dans une condition de hook qui compare ce champ à une valeur exacte.
Références aux entités Iberis (champs reference uniquement) : Préfixez avec @ pour référencer un modèle interne au plugin (ex: @task). Voici toutes les entités système disponibles comme cible d'un champ reference :

Documents : invoices, expenses, estimates, credits, deliveries, orders, receipts, services, disbursements, exit_vouchers
Lignes de documents : invoice_items, expense_items, estimate_items, credit_items, delivery_items, order_items, receipt_items, service_items, disbursement_items, exit_voucher_items
Contacts & articles : contacts, items
Finance : payments, withholdings, journals, journal_entries, company_banks, cash_accounts
Stock & projets : warehouses, projects

Cette liste ne s'applique pas à LOOKUP() ni à bulk_create_from_related — ces deux-là n'acceptent qu'une liste beaucoup plus courte d'entités système, détaillée dans leurs sections respectives (Formules, Hooks). Référencer une entité hors de cette liste plus courte n'y déclenche aucune erreur : LOOKUP() retourne silencieusement null, et bulk_create_from_related ne crée simplement rien.

Numérotation

Déclarée via numbering sur un model (voir l'exemple task plus haut). Le champ pattern doit obligatoirement contenir {YYYY}.

Pourquoi : la génération du prochain numéro recherche toujours le dernier numéro existant sur un segment -{année}-, quel que soit le pattern déclaré. Un pattern sans {YYYY} (ex. "{prefix}-{sequence}") ne matche donc jamais rien : la séquence repart de 1 à chaque enregistrement créé, indéfiniment, sans erreur ni avertissement. Utilisez toujours "{prefix}-{YYYY}-{sequence}" (l'ordre des segments après le préfixe n'a pas d'importance, la présence de {YYYY} si).

Statuses (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.


Formules et expressions

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)"

// Lire un paramètre du plugin (voir section Paramètres)
"formula": "SETTING('daily_limit')"

Fonctions disponibles

Agrégation — SUM, COUNT, AVG, MIN, MAX
Math — ROUND, FLOOR, CEIL, ABS
Conditionnel — IF(condition, valeur_si_vrai, valeur_si_faux), COALESCE(a, b)
Texte — CONCAT, UPPER, LOWER, TRIM, LEFT, RIGHT, LEN
Date — NOW(), TODAY(), DATE_DIFF(date1, date2), DATE_ADD(date, days)
Référence — LOOKUP(@model, condition, champ)
Paramètres — SETTING('cle') — la clé doit être une chaîne entre quotes
LOOKUP() — deux limites à connaître :

1. Avec @model, n'importe quel model du plugin. Sans @, seulement 7 entités système : items, contacts, invoices, expenses, journals, company_banks, warehouses — pas la liste plus large des champs reference vue plus haut. Une entité hors de cette liste retourne silencieusement null.

2. LOOKUP() lit uniquement de vraies colonnes en base, jamais un champ computed d'un autre model — même s'il porte un nom qui ressemble à une colonne. Si vous avez besoin d'une valeur calculée sur le model référencé, chaînez plutôt le calcul via un champ computed sur le model appelant lui-même (les champs computed peuvent référencer d'autres champs computed du même enregistrement, y compris entre eux).

Hooks et actions

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()"
        }
      }
    ]
  }
}

Opérateurs de condition disponibles

Liste complète — utilisable dans conditions (hooks) et dans les filter de widgets. Une valeur d'opérande peut être une constante ou une formule (ex. "SETTING('seuil')").

equals, not_equals
greater_than, less_than, greater_or_equal, less_or_equal
in, not_in — valeur = tableau
changed_to — vrai uniquement sur *.updated, quand le champ vient de prendre cette valeur (il était différent juste avant)
is_empty, is_not_empty — valeur = true
Attention aux noms proches mais invalides : il n'y a pas de greater_than_or_equal / less_than_or_equal — ce sont greater_or_equal / less_or_equal. Une clé d'opérateur non reconnue n'est jamais signalée comme une erreur : la condition correspondante est silencieusement ignorée, ce qui peut rendre une condition bien plus permissive que prévu sans qu'aucune alerte n'apparaisse.

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"
      }
    ]
  }
}

Exemple : regrouper des enregistrements liés (bulk_create_from_related)

create_record agit sur l'enregistrement qui a déclenché le hook. bulk_create_from_related fait la même chose pour tout un ensemble : il cherche les lignes de source_model qui correspondent à match, puis crée une ligne de target_model par résultat trouvé.


// Reprend le model order / order_line de la section Master-detail :
// à la création d'un récap mensuel, rattacher les commandes du même
// client tombant dans la période.
"hooks": {
  "regenerer_lignes": {
    "event": "@monthly_recap.created",
    "actions": [
      {
        "type": "bulk_create_from_related",
        "source_model": "@order",
        "match": [
          { "field": "client_id", "equals": "client_id" },
          { "field": "date", "greater_or_equal": "period_start" },
          { "field": "date", "less_or_equal": "period_end" }
        ],
        "target_model": "recap_line",
        "fields": {
          "recap_id": "id",
          "order_id": "source.id",
          "amount": "source.total"
        }
      }
    ]
  }
}
source_model : @model pour un model du plugin, ou une des 7 entités système admises par LOOKUP (voir section Formules) — pas la liste plus large des champs reference.
match : seulement 8 des 11 opérateurs de condition — equals, not_equals, greater_than, less_than, greater_or_equal, less_or_equal, in, not_in. Pas de changed_to ni is_empty / is_not_empty ici. Chaque valeur est une formule évaluée sur l'enregistrement déclencheur — elle peut donc référencer ses champs, ou lire un paramètre via SETTING('cle').
target_model : nom du model du plugin où créer les lignes (sans @, comme pour create_record).
fields : même langage de formules que create_record. La ligne source correspondante est en plus exposée sous source (ex. source.total).
Pour que les lignes créées apparaissent automatiquement dans le formulaire du parent, déclarez target_model avec un parent comme en Master-detail. Limité à 500 lignes créées par exécution.

Exemple : mettre à jour un autre enregistrement (update_record)

Modifie un enregistrement existant plutôt que d'en créer un — typiquement un record lié à celui qui a déclenché le hook.

"hooks": {
  "on_payment_confirmed": {
    "event": "@payment.updated",
    "conditions": [
      { "field": "status", "changed_to": 1 }
    ],
    "actions": [
      {
        "type": "update_record",
        "model": "invoice_tracking",
        "record_id": "LOOKUP(@invoice_tracking, payment_id = id, id)",
        "fields": {
          "status": "1",
          "paid_at": "NOW()"
        }
      }
    ]
  }
}
model : le model du plugin à mettre à jour (sans @).
record_id : une formule évaluée sur l'enregistrement déclencheur — peut utiliser LOOKUP() pour retrouver l'id du record lié. Rien ne se passe si elle s'évalue à vide.
fields : même langage de formules que create_record — seuls les champs listés sont modifiés, le reste de l'enregistrement est inchangé.

Événements disponibles

invoice.created
invoice.updated
invoice.deleted
expense.created
expense.updated
expense.deleted
payment.received
contact.created
contact.updated
contact.deleted
estimate.created
estimate.updated
estimate.deleted
item.created
item.updated
item.deleted
service.created
service.updated
service.deleted
order.created
order.updated
order.deleted
delivery.created
delivery.updated
delivery.deleted
credit.created
credit.updated
credit.deleted
receipt.created
receipt.updated
receipt.deleted
exit_voucher.created
exit_voucher.updated
exit_voucher.deleted
tej.created
tej.updated
tej.deleted

Pour 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

current_user — L'utilisateur qui a déclenché l'action
company_owner — Le propriétaire de l'entreprise
company_users — Tous les collaborateurs de l'entreprise
role — Les utilisateurs ayant un rôle spécifique (paramètre 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
}
email: true — Envoie aussi un email en plus de la notification in-app. La notification apparaît dans la cloche de l'interface et sur l'application mobile (push Firebase).
Rate limiting : Max 5 niveaux de profondeur (anti-boucle), max 10 exécutions par hook, max 50 exécutions totales par requête. Max 5 notifications identiques non lues par utilisateur.

Shortcodes

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")}}

Extensions d'entités

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

invoice
expense
payment
contact
item
estimate
order
service
credit
receipt
delivery

Widgets dashboard

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.


Onglets sur les pages existantes

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"]
  }
}
Résultat : Sur la fiche de chaque client, un onglet "Deals liés" apparaît avec le nombre de deals. Au clic, il affiche la liste des deals où 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

contact — Synthèse client et fournisseur
item — Fiche article
invoice, expense, estimate, order, delivery, credit, receipt, service — Sidebar du document
foreign_key : Le champ de votre model plugin qui référence l'entité parente (ex: client_id de type reference vers contacts). L'onglet filtre automatiquement les records par cette clé.

Paramètres

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": ""
  }
}

Lire un paramètre dans une formule ou une condition

Utilisez SETTING('cle') — la clé entre quotes, comme n'importe quel autre littéral texte du langage de formules. Tant que l'entreprise n'a pas explicitement enregistré de valeur, le default déclaré ci-dessus est utilisé.


// Champ computed
"formula": "IF(quantity > SETTING('daily_limit'), 'Dépassement', 'OK')"

// Condition de hook — le seuil vient du paramètre, pas d'une valeur figée
"conditions": [{ "field": "amount", "greater_than": "SETTING('daily_limit')" }]

Actions sur les documents

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 :

navigate — Redirige vers une page de votre plugin
create_record — Ouvre le formulaire de création avec des champs pré-remplis (ex: l'ID de la facture)
generate_pdf — Génère et ouvre un PDF à partir d'un template du plugin

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"
    }
  }
}
prefill : Mappe les champs du formulaire plugin aux données du document courant. "_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

invoice
expense
estimate
order
delivery
exit_voucher
credit
receipt
service
payment

Couleurs disponibles

primary
success
danger
warning
info

Action guards

Les guards permettent à votre extension de bloquer des actions Iberis si certaines conditions ne sont pas remplies. Par exemple, empêcher un paiement si aucune demande de paiement approuvée n'existe.

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

Liste exhaustive — contrairement aux mutators et aux hooks d'événements, les guards ne sont câblés que sur ces 7 points précis. Un guard déclaré sur un autre événement (ex. order.before_validate) est accepté à la soumission mais ne se déclenchera jamais, sans erreur.

payment.before_create
invoice.before_validate
invoice.before_delete
expense.before_validate
expense.before_delete
estimate.before_validate
estimate.before_delete

Templates PDF

Générez des documents PDF personnalisés à partir des données de vos modèles. Utilisez du HTML/CSS avec des variables et des boucles pour créer des templates professionnels.

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 jour

Formatage


// 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> 150x150px

Conditions


// 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}}
Master-detail automatique : Si votre model a des enfants (relation 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 égal

API


// 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}/download

Modè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.

Plugin 100% modèles autorisé : Un plugin peut ne contenir que des 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

Installation — Chaque entrée de 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.
Désinstallation — Tous les modèles avec ce 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.
Mise à jour — À la réinstallation, les modèles sont mis à jour via updateOrCreate sur (company_id, plugin_slug, name, document_type). Le HTML, le CSS et l'aperçu sont écrasés.
Multi-types en une seule entrée — 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) :

Ventes — invoice, estimate, delivery, credit, order, exitVoucher, paymentSale
Achats — expense, paymentPurchase
Stock & comptabilité — receipt, inventory, entry, disbursement

Structure 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écaissement
Important : Les custom_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.

Tableau — label + valeur sur la même ligne
<tr><td>Remise</td><td>{{document.discount}}</td></tr> — si discount est vide, la ligne entière disparaît (libellé compris).
Tableau — libellé + séparateur + valeur — Le moteur étend la suppression aux cellules adjacentes sans variable (ex. <td>Prix</td><td>:</td><td>{{x}}</td>), afin que le séparateur ne reste pas orphelin.
Plusieurs colonnes dans la même ligne — Si un autre <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.
Listes — Dans un <ul>/<ol>, le <li> contenant la variable vide est retiré ; le reste de la liste est préservé.
Listes de définitions — Dans un <dl>, un <dd>{{x}}</dd> vide fait disparaître aussi son <dt> précédent, sans toucher aux autres paires de la liste.
Hors tableau/liste — Un <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.
Limite : si un même <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> &middot; 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}}"
}
Pièges connus avec mPDF : Évitez 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

Chaque entrée doit avoir 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.
Le HTML est scanné pour détecter <script>, javascript: et les gestionnaires d'événements on*= — rejet immédiat si trouvés.
Compatibilité multi-tenant : Les modèles installés sont liés à company_id + plugin_slug, donc chaque entreprise a sa propre copie. La désinstallation pour une entreprise n'affecte pas les autres.

Workflows d'approbation

Ajoutez un circuit de validation sur vos modèles. Définissez des étapes d'approbation avec des rôles ou des utilisateurs spécifiques. Deux modes sont disponibles : séquentiel (ordre strict) et libre (une seule approbation suffit).

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

sequential — Les étapes sont traitées dans l'ordre. L'étape 2 est bloquée tant que l'étape 1 n'est pas approuvée.
any — N'importe quel approbateur peut valider. Une seule approbation suffit pour débloquer le document.
Le role d'une étape n'est pas vérifié : il est purement informatif — affiché dans l'état du workflow, jamais comparé à l'utilisateur qui appelle l'API d'approbation. Concrètement, l'exemple ci-dessus n'empêche personne : n'importe quel utilisateur ayant le droit d'agir sur le model payment_request peut approuver ou rejeter n'importe quelle étape, y compris "Validation Direction Financière", sans avoir le rôle accountant. Si votre circuit doit réellement restreindre qui approuve quoi, ce contrôle est à faire vous-même côté plugin (ex. un guard, ou vérifier le rôle dans votre propre logique avant d'appeler l'API).

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" }

Mutators (transformation de données)

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"
    }
  }
}
default : Remplit le champ uniquement s'il est vide. Ne modifie pas une valeur existante.

Exemple : calculer et forcer une valeur

"mutators": {
  "auto_reference": {
    "event": "expense.before_save",
    "conditions": [
      { "field": "status", "equals": 0 }
    ],
    "set": {
      "reference": "CONCAT('REF-', order_number)"
    }
  }
}
set : Force la valeur du champ, même s'il est déjà rempli. Supporte les formules.
conditions : Optionnel. Le mutator ne s'applique que si les conditions sont remplies.

Événements disponibles

invoice.before_save
expense.before_save
estimate.before_save
payment.before_save
item.before_save
order.before_save
delivery.before_save
credit.before_save
receipt.before_save
service.before_save
exit_voucher.before_save

Pages tableau de bord

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

type: "dashboard" — Identifie la page comme un tableau de bord multi-model
widgets — Liste des noms de widgets à afficher (définis dans la section widgets du manifest)
sections — Tableaux de données, chacun lié à un model différent
sections[].limit — Nombre de records à afficher (défaut: 10)
sections[].filter — Filtres supplémentaires passés à l'API
Navigation : Les numéros dans les tableaux sont cliquables et redirigent vers le formulaire du record (si une page form existe pour ce model).

Import / export 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

Exporte tous les records du model en CSV (délimiteur ;)
Colonnes automatiques : tous les champs du model sauf id, company_id, deleted_at
Inclut number, created_at, updated_at en lecture seule

Import

Téléchargez le fichier d'export comme modèle, remplissez vos données
Les colonnes number, created_at, updated_at sont ignorées à l'import
Validation automatique : les champs required et les types sont vérifiés
Rapport détaillé : nombre importé + liste des erreurs par ligne

API


// 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"] }
Format CSV : Délimiteur ; (point-virgule), encodage UTF-8, valeurs entre guillemets doubles. La première ligne contient les noms de colonnes.

Relations master-detail

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

Formulaire — Les lignes enfant s'affichent dans un tableau éditable sous le formulaire du parent. Ajout, modification et suppression en ligne.
Sauvegarde — Les lignes sont sauvegardées automatiquement avec le parent (création + mise à jour).
Cascade delete — Quand le parent est supprimé, toutes les lignes enfant sont supprimées automatiquement.
API — L'endpoint GET /record retourne les enfants dans _children. Le CRUD des enfants utilise les mêmes endpoints que les records normaux.

Déclaration

parent.model : Le nom du model parent
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" } }
    }
  }
}

Permissions par rôle

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:canDelete

Permissions personnalisées

"permissions": [
  "canRead",
  "canCreate",
  "canUpdate",
  "canDelete",
  "canExport",
  "canApprove",
  "canManageSettings"
]
Format des clés : Les permissions sont préfixées automatiquement avec 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

Installation — Toutes les permissions sont automatiquement ajoutées au rôle Propriétaire
Sidebar — Les menus plugin sont masqués si l'utilisateur n'a pas canRead
API CRUD — Lecture vérifie canRead, création canCreate, modification canUpdate, suppression canDelete
Gestion des rôles — L'administrateur peut activer/désactiver chaque permission par rôle dans la page Rôles & Permissions

Utiliser 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. Pour un item qui pointe vers une page liée à un modèle (list/form), voir ci-dessous : la permission du modèle prend le pas sur celle déclarée ici.

Restreindre un modèle à une permission dédiée

Par défaut, un modèle est couvert par les permissions standards du plugin (canRead/canCreate/canUpdate/canDelete), au même titre que tous les autres modèles. Si un modèle en particulier doit être verrouillé à part — un écran plus sensible que le reste du plugin — déclarez permission directement sur ce modèle :


{
  "models": {
    "statement": {
      "permission": "canValidateStatement",
      "fields": { ... }
    }
  },
  "permissions": ["canRead", "canCreate", "canUpdate", "canDelete", "canValidateStatement"]
}
La permission déclarée doit obligatoirement figurer dans la liste permissions du plugin (sinon la soumission est rejetée) — c'est ce qui la fait apparaître comme case à cocher dans l'éditeur de rôles.

Une fois déclarée, cette permission remplace canRead/canCreate/canUpdate/canDelete pour toutes les actions sur ce modèle — lecture, création, modification, suppression, export, import, et les étapes d'approbation le cas échéant. Elle ne s'applique qu'à ce modèle : les autres modèles du même plugin continuent d'utiliser les permissions standards, indépendamment. Ce n'est pas plus fin qu'une permission par modèle — il n'y a pas de permission séparée par action au sein d'un même modèle.

Un item de menu qui pointe vers une page liée à ce modèle n'a pas besoin de déclarer sa propre permission : elle est automatiquement dérivée de celle du modèle, pour que le menu ne puisse jamais afficher — ou masquer — une entrée en désaccord avec ce que l'API autorise réellement.


Limites et sécurité

10 models par plugin
50 champs par model
20 hooks par plugin
50 tokens par formule
100ms timeout formule
10 MB upload fichier max
5 niveaux de profondeur hooks
2 plugins par entreprise (gratuit)
Illimité plugins (premium)

Sécurité

Principes :
  • 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