{"id":1016,"date":"2025-12-25T13:05:48","date_gmt":"2025-12-25T13:05:48","guid":{"rendered":"https:\/\/invoicesreader.com\/?post_type=documentation&#038;p=1016"},"modified":"2025-12-25T13:25:55","modified_gmt":"2025-12-25T13:25:55","slug":"plugin-developer-manual","status":"publish","type":"documentation","link":"https:\/\/invoicesreader.com\/en\/documentation\/plugin-developer-manual\/","title":{"rendered":"Plugin Developer Manual"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">Invoices Reader &#8211; Plugin Developer Manual<\/h1>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Complete Reference for Plugin Development<\/strong><br>This manual provides everything developers (and AI coding assistants) need to build powerful plugins for the Invoices Reader application.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udccc Application Overview<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"683\" src=\"https:\/\/invoicesreader.com\/wp-content\/uploads\/2025\/12\/main_window_en-1024x683.png\" alt=\"\" class=\"wp-image-955\" srcset=\"https:\/\/invoicesreader.com\/wp-content\/uploads\/2025\/12\/main_window_en-1024x683.png 1024w, https:\/\/invoicesreader.com\/wp-content\/uploads\/2025\/12\/main_window_en-600x400.png 600w, https:\/\/invoicesreader.com\/wp-content\/uploads\/2025\/12\/main_window_en-300x200.png 300w, https:\/\/invoicesreader.com\/wp-content\/uploads\/2025\/12\/main_window_en-768x512.png 768w, https:\/\/invoicesreader.com\/wp-content\/uploads\/2025\/12\/main_window_en.png 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Invoices Reader<\/strong> is a PyQt5 desktop application for OCR-based invoice processing:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>AI Extraction<\/strong>: Uses Gemini, OpenRouter, Ollama, or LMStudio to extract invoice data<\/li>\n\n\n\n<li><strong>Database<\/strong>: SQLite (<code>detections.db<\/code>) stores invoices, line items, vendors, batches<\/li>\n\n\n\n<li><strong>Queue System<\/strong>: Global processing queue for async operations<\/li>\n\n\n\n<li><strong>Plugin System<\/strong>: Declarative architecture for safe extensions<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Key Concepts<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Concept<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td><strong>Invoice<\/strong><\/td><td>A document with vendor, date, amounts, line items<\/td><\/tr><tr><td><strong>Batch<\/strong><\/td><td>Group of invoices processed together<\/td><\/tr><tr><td><strong>Queue<\/strong><\/td><td>Global list of items being processed<\/td><\/tr><tr><td><strong>Connector<\/strong><\/td><td>Background service fetching files from external sources<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\ude80 Quick Start<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Using the Plugin SDK (Recommended)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The Plugin SDK provides everything you need in a single import. It is recommended to use the wildcard import for the best developer experience:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from core.plugins.sdk import *<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">What&#8217;s Included?<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">The SDK re-exports over 120 common items, including:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Standard Library<\/strong>: <code>re<\/code>, <code>time<\/code>, <code>urllib<\/code>, <code>os<\/code>, <code>sys<\/code>, <code>shutil<\/code>, <code>json<\/code>, <code>math<\/code>, <code>random<\/code>, <code>uuid<\/code>, <code>tempfile<\/code>, <code>base64<\/code>, <code>threading<\/code>, <code>sqlite3<\/code>, <code>csv<\/code>, <code>io<\/code>, <code>abc<\/code>, <code>asyncio<\/code>, <code>datetime<\/code>.<\/li>\n\n\n\n<li><strong>Typing<\/strong>: <code>List<\/code>, <code>Dict<\/code>, <code>Any<\/code>, <code>Tuple<\/code>, <code>Optional<\/code>, <code>Union<\/code>, <code>Type<\/code>, <code>Generic<\/code>, <code>Literal<\/code>, <code>Final<\/code>, etc.<\/li>\n\n\n\n<li><strong>Pydantic<\/strong>: <code>BaseModel<\/code> (aliased as <code>ConfigModel<\/code>), <code>Field<\/code>, <code>field_validator<\/code>.<\/li>\n\n\n\n<li><strong>Plugin Framework<\/strong>: <code>DeclarativePlugin<\/code>, <code>Action<\/code>, <code>hook<\/code>, <code>get_logger<\/code>, <code>get_api<\/code>.<\/li>\n\n\n\n<li><strong>UI (PyQt5)<\/strong>: Common widgets like <code>QWidget<\/code>, <code>QLabel<\/code>, <code>QPushButton<\/code>, <code>QImage<\/code>, <code>QApplication<\/code>.<\/li>\n\n\n\n<li><strong>Security<\/strong>: <code>sandbox<\/code>, <code>requires_capability<\/code>, and capability constants (e.g., <code>CAPABILITY_UI_TOAST<\/code>).<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">File Structure<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>plugins\/my_plugin\/\n\u251c\u2500\u2500 manifest.json      # Metadata &amp; Permissions (required)\n\u251c\u2500\u2500 main.py            # Plugin class (required)\n\u2514\u2500\u2500 connector.py       # Optional: Background data fetching<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Multi-file Plugins (Complex)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you split your code into multiple files (e.g., <code>settings.py<\/code>), you must structure your plugin as a package:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>plugins\/complex_plugin\/\n\u251c\u2500\u2500 manifest.json      # Set \"main\": \"__init__.py\"\n\u251c\u2500\u2500 __init__.py        # Plugin class (ENTRY POINT)\n\u2514\u2500\u2500 settings.py        # Helper module<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Crucial<\/strong>: You <strong>MUST<\/strong> use <code>__init__.py<\/code> as the entry point (<code>\"main\": \"__init__.py\"<\/code> in manifest) to enable relative imports like <code>from .settings import Config<\/code>.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">Minimal Plugin (using SDK)<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code># plugins\/hello-world\/main.py\nfrom core.plugins.sdk import (\n    DeclarativePlugin, Action, Field,\n    sandbox, get_logger, Dict, Any\n)\n\nlogger = get_logger(__name__)\n\nclass HelloWorldPlugin(DeclarativePlugin):\n    \"\"\"A sample plugin using the SDK\"\"\"\n\n    # Plugin metadata\n    id = \"hello-world\"\n    name = \"Hello World\"\n    version = \"2.0.0\"\n\n    # Declare a field - app creates UI automatically\n    notes = Field(\n        type=\"text\",\n        label=\"Notes\",\n        section=\"Hello World\",\n        persist=True,\n        tooltip=\"Add your notes here (auto-saved with invoice)\"\n    )\n\n    @Action(label=\"Greet\", icon=\"\ud83d\udc4b\", location=\"toolbar\", tooltip=\"Say hello!\")\n    @sandbox\n    def greet(self, invoice: Dict&#91;str, Any]):\n        \"\"\"Called when toolbar button is clicked\"\"\"\n        vendor = invoice.get('vendor_name', 'Unknown')\n        self.api.ui.toast(f\"Hello, {vendor}!\", \"success\")\n        logger.info(f\"Greeted vendor: {vendor}\")<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">manifest.json<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"id\": \"my-plugin\",\n    \"name\": \"My Plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"description\": \"What this plugin does\",\n    \"main\": \"main.py\",\n    \"plugin_class\": \"MyPlugin\",\n    \"capabilities\": &#91;\"ui:toast\", \"ui:toolbar\"],\n    \"dependencies\": &#91;]\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\u26a0\ufe0f Common Pitfalls (Read this first!)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Before you start, avoid these common mistakes that trip up new developers:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Don&#8217;t use <code>Field<\/code> for Global Settings<\/strong>:<\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Wrong<\/strong>: Using <code>Field(persist=True)<\/code> for plugin API keys or templates. This saves data <em>per invoice<\/em>.<\/li>\n\n\n\n<li><strong>Right<\/strong>: Use <code>QSettings<\/code> and <code>self.api.register_settings_tab<\/code> for global configuration (see &#8220;Unified Settings&#8221; section).<\/li>\n<\/ul>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong><code>@Action<\/code> Decorator<\/strong>:<\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Required<\/strong>: You MUST provide a <code>label<\/code> argument. <code>icon<\/code> alone is not enough.<\/li>\n\n\n\n<li><strong>Icon Names<\/strong>: Use <code>fa5s.name<\/code> or <code>fa5b.name<\/code> (brands). Invalid names will be treated as text.<\/li>\n<\/ul>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Method Signatures<\/strong>:<\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>on_load(self) -> bool<\/code>: Do NOT accept <code>api<\/code> as an argument. Access it via <code>self.api<\/code>.<\/li>\n\n\n\n<li><code>action_method(self, invoice=None)<\/code>: Action callbacks ALWAYS receive the current <code>invoice<\/code> (dict or None) as the second argument. You MUST add <code>invoice=None<\/code> to your signature to prevent <code>TypeError<\/code>.<\/li>\n<\/ul>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>API Access<\/strong>:<\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Use <code>self.api.get_current_invoice()<\/code> to get screen data.<\/li>\n\n\n\n<li>Use <code>self.api.system.open_url(url)<\/code> for opening browsers.<\/li>\n\n\n\n<li>Use <code>self.api.ui.toast(msg)<\/code> (not <code>show_toast<\/code>).<\/li>\n\n\n\n<li><strong>New<\/strong>: Error reporting is now automatic. If your action raises an exception, a detailed dialog with traceback will be shown to the user.<\/li>\n<\/ul>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Use Relative Imports for Plugin-Internal Modules<\/strong>:<\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Wrong<\/strong>: <code>from core.plugins.official.my_plugin.helper import Foo<\/code> (absolute path)<\/li>\n\n\n\n<li><strong>Right<\/strong>: <code>from .helper import Foo<\/code> (relative import)<\/li>\n\n\n\n<li><strong>Why<\/strong>: Plugins are loaded dynamically as source code. Absolute imports to plugin paths may fail in packaged builds because the path structure differs.<\/li>\n\n\n\n<li><strong>Rule<\/strong>: Use relative imports (<code>from .module<\/code>) for files <em>inside<\/em> your plugin. Use absolute imports (<code>from core.plugins import<\/code>) for the <em>framework<\/em> classes.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83c\udfa8 Declarative Framework<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Plugin Lifecycle<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Understanding when your code runs is crucial:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Instantiation<\/strong>: The <code>DeclarativePlugin<\/code> class is instantiated when the app starts (or during hot-reload).<\/li>\n\n\n\n<li><strong><code>on_load()<\/code><\/strong>: Called immediately after instantiation and API connection.<\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li><em>Usage<\/em>: Initialize connects, start schedules, register settings.<\/li>\n\n\n\n<li><em>Return<\/em>: <code>True<\/code> (success) or <code>False<\/code> (failure).<\/li>\n<\/ul>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Active State<\/strong>: The plugin responds to hooks, actions, and API calls.<\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li><em>Note<\/em>: The plugin instance persists across invoice navigation. It is <strong>NOT<\/strong> reloaded when opening a different invoice.<\/li>\n<\/ul>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong><code>on_unload()<\/code><\/strong>: Called when the app closes, the plugin is disabled, or during hot-reload.<\/li>\n<\/ol>\n\n\n\n<ul class=\"wp-block-list\">\n<li><em>Usage<\/em>: Cleanup resources, stop threads, close sockets.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Fields (Auto-Managed UI)<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>from core.plugins import Field\n\nclass MyPlugin(DeclarativePlugin):\n    # Creates text input in sidebar, auto-saves with invoice\n    notes = Field(type=\"text\", label=\"Notes\", section=\"My Section\", persist=True)\n\n    # Other field types\n    enabled = Field(type=\"checkbox\", label=\"Enable Feature\")\n    priority = Field(type=\"number\", label=\"Priority\", default=1)<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Actions (Buttons)<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>from core.plugins import Action\n\nclass MyPlugin(DeclarativePlugin):\n    @Action(label=\"Sync\", icon=\"fa5s.sync\", location=\"toolbar\", tooltip=\"Sync data\")\n    def sync_now(self, invoice: dict):\n        # invoice = current invoice data dict\n        self.api.ui.toast(\"Syncing...\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Location Options:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>\"toolbar\"<\/code> &#8211; Top toolbar (left side, default)<\/li>\n\n\n\n<li><code>\"toolbar:left\"<\/code> &#8211; Explicit left slot<\/li>\n\n\n\n<li><code>\"toolbar:right\"<\/code> &#8211; Right slot (near theme toggle)<\/li>\n\n\n\n<li><code>\"menu:Tools\"<\/code> &#8211; Add to Tools menu under your plugin name<\/li>\n\n\n\n<li><code>\"menu:Plugins\"<\/code> &#8211; Add to Plugins menu (auto-created)<\/li>\n\n\n\n<li><code>\"section\"<\/code> &#8211; Inside the plugin&#8217;s sidebar section. <strong>Note<\/strong>: If your plugin has no fields, a &#8220;Default&#8221; section will be created automatically.<\/li>\n\n\n\n<li><code>\"actions\"<\/code> &#8211; The &#8220;Action Area&#8221; next to the main Save\/Delete buttons (use sparingly).<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83e\udde9 The Invoice Details Panel<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the main form on the right side of the application where invoice data is displayed. It is the primary place for plugins to show data related to the current invoice.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Structure:<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Header<\/strong>: Invoice number and status.<\/li>\n\n\n\n<li><strong>Standard Sections<\/strong>: &#8220;Invoice Details&#8221;, &#8220;Vendor Info&#8221;, &#8220;Payment Info&#8221;.<\/li>\n\n\n\n<li><strong>Plugin Sections<\/strong>: Custom collapsible groups added by plugins.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What you can do here:<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Add Custom Fields<\/strong>: Use <code>Field(..., section=\"My Section\")<\/code> to add persistent inputs (Text, Checkbox, Number) directly into the form. These auto-save with the invoice.<\/li>\n\n\n\n<li><strong>Add Actions<\/strong>: Use <code>@Action(..., location=\"section\")<\/code> to add buttons inside your custom section (e.g., &#8220;Verify Tax ID&#8221;).<\/li>\n\n\n\n<li><strong>Add Custom UI<\/strong>: Use <code>self.api.ui.add_section()<\/code> to add a completely custom widget (graphs, tables, web views) as a collapsible section.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example: Custom Section with Fields and Actions<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class MyPlugin(DeclarativePlugin):\n    # This creates a new section \"Compliance\" with two fields\n    tax_id = Field(type=\"text\", label=\"Tax ID\", section=\"Compliance\", persist=True)\n    verified = Field(type=\"checkbox\", label=\"Verified\", section=\"Compliance\", persist=True)\n\n    @Action(label=\"Check Online\", location=\"section\") # Appears in \"Compliance\" section\n    def check_compliance(self, invoice):\n        # Logic to verify tax_id\n        pass<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udccb Field Types Reference<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>Field<\/code> descriptor supports 10 different widget types:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Type<\/th><th>Widget<\/th><th>Value Type<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td><code>text<\/code><\/td><td>QLineEdit<\/td><td>str<\/td><td>Single-line text input<\/td><\/tr><tr><td><code>number<\/code><\/td><td>QLineEdit<\/td><td>str<\/td><td>Numeric input with validation<\/td><\/tr><tr><td><code>checkbox<\/code><\/td><td>QCheckBox<\/td><td>bool<\/td><td>Boolean on\/off toggle<\/td><\/tr><tr><td><code>textarea<\/code><\/td><td>QTextEdit<\/td><td>str<\/td><td>Multi-line text input<\/td><\/tr><tr><td><code>select<\/code><\/td><td>QComboBox<\/td><td>str<\/td><td>Dropdown selection list<\/td><\/tr><tr><td><code>spinbox<\/code><\/td><td>QSpinBox<\/td><td>int<\/td><td>Numeric spinner with arrows<\/td><\/tr><tr><td><code>slider<\/code><\/td><td>QSlider<\/td><td>int<\/td><td>Value slider with label<\/td><\/tr><tr><td><code>date<\/code><\/td><td>QDateEdit<\/td><td>str (ISO)<\/td><td>Date picker with calendar popup<\/td><\/tr><tr><td><code>password<\/code><\/td><td>QLineEdit<\/td><td>str<\/td><td>Masked text input<\/td><\/tr><tr><td><code>readonly<\/code><\/td><td>QLabel<\/td><td>str<\/td><td>Read-only display label<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Usage Examples:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from core.plugins import DeclarativePlugin, Field\n\nclass MyPlugin(DeclarativePlugin):\n    # Text input\n    notes = Field(type=\"text\", label=\"Notes\", section=\"My Section\", persist=True)\n\n    # Checkbox\n    verified = Field(type=\"checkbox\", label=\"Verified\", section=\"My Section\", persist=True)\n\n    # Dropdown with options\n    status = Field(type=\"select\", label=\"Status\", section=\"My Section\",\n                   options=&#91;\"Pending\", \"Approved\", \"Rejected\"])\n\n    # Number spinner (0-100)\n    confidence = Field(type=\"spinbox\", label=\"Confidence %\", section=\"My Section\",\n                       min_val=0, max_val=100)\n\n    # Slider (0-10)\n    priority = Field(type=\"slider\", label=\"Priority\", section=\"My Section\",\n                     min_val=0, max_val=10)\n\n    # Date picker\n    due_date = Field(type=\"date\", label=\"Due Date\", section=\"My Section\", persist=True)\n\n    # Multi-line text\n    description = Field(type=\"textarea\", label=\"Description\", section=\"My Section\")\n\n    # Password field\n    api_key = Field(type=\"password\", label=\"API Key\", section=\"Settings\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Field Options:<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Option<\/th><th>Type<\/th><th>Default<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td><code>type<\/code><\/td><td>str<\/td><td><code>\"text\"<\/code><\/td><td>Widget type (see table above)<\/td><\/tr><tr><td><code>label<\/code><\/td><td>str<\/td><td>required<\/td><td>Display label<\/td><\/tr><tr><td><code>section<\/code><\/td><td>str<\/td><td>Plugin name<\/td><td>Panel section to place field<\/td><\/tr><tr><td><code>persist<\/code><\/td><td>bool<\/td><td><code>False<\/code><\/td><td>Auto-save with invoice data<\/td><\/tr><tr><td><code>tooltip<\/code><\/td><td>str<\/td><td><code>\"\"<\/code><\/td><td>Hover help text<\/td><\/tr><tr><td><code>options<\/code><\/td><td>list<\/td><td><code>None<\/code><\/td><td>Options for <code>select<\/code> type<\/td><\/tr><tr><td><code>min_val<\/code><\/td><td>float<\/td><td><code>0<\/code><\/td><td>Min for <code>spinbox<\/code>\/<code>slider<\/code><\/td><\/tr><tr><td><code>max_val<\/code><\/td><td>float<\/td><td><code>100<\/code><\/td><td>Max for <code>spinbox<\/code>\/<code>slider<\/code><\/td><\/tr><tr><td><code>readonly<\/code><\/td><td>bool<\/td><td><code>False<\/code><\/td><td>Make field read-only<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Getting\/Setting Values Programmatically:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># In your hook or action method:\nvalue = self.get_field('notes')           # Returns current value\nself.set_field('verified', True)          # Sets checkbox to checked\nself.set_field('status', 'Approved')      # Sets dropdown selection<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udcc4 The Document Viewer<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the large panel on the left\/center of the application that displays the invoice file (PDF or Image).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Key Features:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Zoom\/Pan<\/strong>: Users can inspect the document details.<\/li>\n\n\n\n<li><strong>Highlighting<\/strong>: The app automatically highlights extracted fields (like Total, Date, Vendor) on the document.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Plugin Interactions:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Read-Only<\/strong>: Plugins can read the current file path via <code>invoice_data['file_path']<\/code> in the <code>invoice.loaded<\/code> hook.<\/li>\n\n\n\n<li><strong>No Direct Overlays (Yet)<\/strong>: Currently, plugins cannot draw directly onto the document canvas.<\/li>\n\n\n\n<li><strong>Use Cases<\/strong>: Plugins typically use the file path to:<\/li>\n\n\n\n<li>Send the file to an external API.<\/li>\n\n\n\n<li>Perform custom OCR\/analysis on the raw file.<\/li>\n\n\n\n<li>Open the file in an external viewer.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">\ud83d\udcbe Plugin Data Persistence<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Beyond the <code>Field(persist=True)<\/code> mechanism (which is per-invoice), you can save arbitrary plugin data linked to the current invoice using <code>self.api.save_data<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Smart Serialization<\/strong>: You can pass complex Python objects (dictionaries, lists) directly to <code>save_data<\/code>. The API will automatically serialize them to JSON for storage.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Automatically serialized to JSON\nself.api.save_data(self.id, \"my_complex_meta\", {\n    \"tags\": &#91;\"urgent\", \"utility\"],\n    \"score\": 0.95\n})\n\n# Retrieve and deserialize automatically\ndata = self.api.load_data(self.id, \"my_complex_meta\")\nif data:\n    print(data&#91;\"tags\"]) # &#91;\"urgent\", \"utility\"]<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Hooks (Events)<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>class MyPlugin(DeclarativePlugin):\n    @hook(\"invoice.loaded\")\n    def show_vendor_stats(self, invoice_id: int, invoice: dict):\n        \"\"\"Show stats when invoice is loaded\"\"\"\n        vendor = invoice.get(\"vendor_name\")\n        if not vendor: return\n\n        # Query database for past invoices from this vendor\n        rows = self.api.db.query(\n            \"SELECT invoice_total FROM extracted_data WHERE vendor_name = ?\", \n            (vendor,)\n        )\n\n        if rows:\n            totals = &#91;float(r&#91;0]) for r in rows if r&#91;0]]\n            avg = sum(totals) \/ len(totals)\n            self.api.ui.toast(f\"History: {len(rows)} invoices (Avg: {avg:.2f})\", \"info\")\n\n    @hook(\"invoice.saved\")\n    def on_save(self, invoice_id: int):\n        self.api.system.log(f\"Invoice {invoice_id} saved\", \"info\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Available Hooks:<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Hook<\/th><th>Arguments<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td><code>invoice.loaded<\/code><\/td><td><code>invoice_id, invoice_data<\/code><\/td><td>Invoice displayed<\/td><\/tr><tr><td><code>invoice.saved<\/code><\/td><td><code>invoice_id<\/code><\/td><td>Invoice saved to DB<\/td><\/tr><tr><td><code>navigation.next<\/code><\/td><td>(none)<\/td><td>Next invoice button clicked<\/td><\/tr><tr><td><code>navigation.prev<\/code><\/td><td>(none)<\/td><td>Previous invoice button clicked<\/td><\/tr><tr><td><code>batch.started<\/code><\/td><td><code>batch_id<\/code><\/td><td>Batch processing started<\/td><\/tr><tr><td><code>batch.completed<\/code><\/td><td><code>batch_id<\/code><\/td><td>Batch processing finished<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u26a1 API Reference (<code>self.api<\/code>)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><code>self.api.ui<\/code> &#8211; User Interface<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">Allowed UI Components<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">When creating custom dialogs or docks, you can use standard PyQt5 widgets.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Safe<\/strong>: <code>QLabel<\/code>, <code>QPushButton<\/code>, <code>QLineEdit<\/code>, <code>QTextEdit<\/code>, <code>QCheckBox<\/code>, <code>QComboBox<\/code>, <code>QTableWidget<\/code>, <code>QProgressBar<\/code>.<\/li>\n\n\n\n<li><strong>Layouts<\/strong>: <code>QVBoxLayout<\/code>, <code>QHBoxLayout<\/code>, <code>QGridLayout<\/code>, <code>QFormLayout<\/code>.<\/li>\n\n\n\n<li><strong>Avoid<\/strong>: Top-level <code>QMainWindow<\/code> or <code>QApplication<\/code> instantiation. Always use <code>QWidget<\/code> or <code>QDialog<\/code> with <code>self.api._main_window<\/code> as parent if needed.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">API Methods<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code># Toast notifications\nself.api.ui.toast(\"Message\", \"success\")  # Types: info, success, warning, error\n\n# Confirmation dialog\nif self.api.ui.show_confirm(\"Delete?\", \"Are you sure?\"):\n    delete_item()\n\n# Text input dialog\nname = self.api.ui.show_input(\"Enter Name\", \"Vendor name:\", \"Default\")\nif name:\n    print(f\"User entered: {name}\")\n\n# Custom dialog with any widget\nfrom PyQt5.QtWidgets import QLabel\nwidget = QLabel(\"Custom content here\")\nif self.api.ui.show_dialog(\"My Dialog\", widget, 400, 300):\n    print(\"User clicked OK\")\n\n# Simple modal message (success\/error\/info)\nself.api.ui.message(\"Title\", \"Message text here\", \"success\")  # Types: success, error, info, warning\n\n# File browsing (Simplified)\npath = self.api.ui.browse_file(\"Select Invoice\", \"PDF Files (*.pdf)\")\nif path:\n    print(f\"User picked: {path}\")\n\n# Directory browsing (Simplified)\nfolder = self.api.ui.browse_directory(\"Select Export Folder\")\nif folder:\n    print(f\"User picked: {folder}\")\n\n# Add menu item\nself.api.ui.add_menu_item(\"Tools\", \"My Feature\", self.my_callback, \"fa5s.cog\")\n\n# Add unified settings tab (integrates into main settings dialog)\ndef create_settings_widget():\n    widget = QWidget()\n    # ... build widget ...\n    return widget\n\nself.api.register_settings_tab(self.id, \"My Settings\", create_settings_widget)\n\n# Add sidebar section (empty, with icon)\nsection_id = self.api.ui.add_section(\"My Section\", \"\ud83d\udd0c\", plugin_id=self.id)\n\n# Add sidebar section WITH custom widget content\nfrom PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel\nwidget = QWidget()\nlayout = QVBoxLayout(widget)\nlayout.addWidget(QLabel(\"Custom analytics here\"))\nself.api.ui.add_section(\"Analytics\", widget, plugin_id=self.id)\n\n# Add docking window (separate floating panel)\nfrom PyQt5.QtWidgets import QWidget\nself.api.ui.add_dock(\"My Dock\", QWidget(), \"right\")<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">[!TIP]<br><strong><code>add_section<\/code> Smart Behavior<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Second argument is polymorphic<\/strong>: Pass a string for an icon (e.g., <code>\"\ud83d\udd0c\"<\/code>) OR a <code>QWidget<\/code> for custom content.<\/li>\n\n\n\n<li><strong>Custom icon with widget<\/strong>: Use the <code>icon<\/code> keyword: <code>add_section(\"Title\", widget, icon=\"\ud83d\udcca\", plugin_id=self.id)<\/code><\/li>\n\n\n\n<li><strong>Deduplication<\/strong>: Calling <code>add_section<\/code> with the same <code>plugin_id<\/code> and <code>title<\/code> reuses the existing section.<\/li>\n\n\n\n<li><strong>Always pass <code>plugin_id<\/code><\/strong>: Required for permission checks.<\/li>\n<\/ul>\n<\/blockquote>\n\n\n\n<h4 class=\"wp-block-heading\">\ud83c\udfa8 Icons Reference<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Supported Icon Types:<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Type<\/th><th>Example<\/th><th>Usage<\/th><\/tr><\/thead><tbody><tr><td><strong>Emoji<\/strong><\/td><td><code>\"\ud83d\udcca\"<\/code>, <code>\"\ud83d\udd14\"<\/code>, <code>\"\u2699\ufe0f\"<\/code><\/td><td>Works everywhere, simple to use<\/td><\/tr><tr><td><strong>QtAwesome<\/strong><\/td><td><code>\"fa5s.chart-bar\"<\/code><\/td><td>FontAwesome 5 Solid icons (for buttons\/menus)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Using Emojis (Recommended for Sections):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Simple emoji icons for sections\nself.api.ui.add_section(\"Analytics\", \"\ud83d\udcca\", plugin_id=self.id)\nself.api.ui.add_section(\"Alerts\", widget, icon=\"\ud83d\udd14\", plugin_id=self.id)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Common Section Icons:<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Icon<\/th><th>Meaning<\/th><th>Icon<\/th><th>Meaning<\/th><\/tr><\/thead><tbody><tr><td>\ud83d\udcca<\/td><td>Analytics\/Charts<\/td><td>\ud83d\udd14<\/td><td>Notifications<\/td><\/tr><tr><td>\u2699\ufe0f<\/td><td>Settings<\/td><td>\ud83d\udccb<\/td><td>Reports<\/td><\/tr><tr><td>\ud83d\udd0d<\/td><td>Search<\/td><td>\ud83d\udcc1<\/td><td>Files<\/td><\/tr><tr><td>\ud83d\udcb0<\/td><td>Finance<\/td><td>\ud83c\udfe2<\/td><td>Vendor<\/td><\/tr><tr><td>\u26a0\ufe0f<\/td><td>Warnings<\/td><td>\u2705<\/td><td>Success<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Finding More Emojis:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/emojipedia.org\" target=\"_blank\" rel=\"noopener\">Emojipedia<\/a> &#8211; Full emoji database<\/li>\n\n\n\n<li>Windows: <code>Win + .<\/code> (period) to open emoji picker<\/li>\n\n\n\n<li>Copy-paste directly into your Python code<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Using QtAwesome (For Toolbar\/Menu):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># QtAwesome for toolbar buttons, menu items, and sidebar actions\n@Action(label=\"Sync\", icon=\"fa5s.sync\", location=\"toolbar\")\ndef sync_data(self, invoice):\n    pass\n\n# Note: Invalid icon names will fall back to using prefix text.\n# Always use valid 'fa5s.*', 'fa5b.*', or 'ei.*' names.\nself.api.ui.add_menu_item(\"Tools\", \"Settings\", callback, \"fa5s.cog\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>QtAwesome Icon Prefixes:<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Prefix<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td><code>fa5s.<\/code><\/td><td>FontAwesome 5 Solid<\/td><\/tr><tr><td><code>fa5b.<\/code><\/td><td>FontAwesome 5 Brands<\/td><\/tr><tr><td><code>ei.<\/code><\/td><td>Elusive Icons<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Browse icons at: <a href=\"https:\/\/fontawesome.com\/icons\" target=\"_blank\" rel=\"noopener\">FontAwesome Gallery<\/a><\/p>\n\n\n\n<h4 class=\"wp-block-heading\">\ud83e\ude9f Application Windows<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Plugins can programmatically open core application dialogs. This is useful for creating &#8220;shortcuts&#8221; or guiding users to specific settings.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Method<\/th><th class=\"has-text-align-left\" data-align=\"left\">Window \/ Dialog<\/th><th class=\"has-text-align-left\" data-align=\"left\">Purpose<\/th><\/tr><\/thead><tbody><tr><td class=\"has-text-align-left\" data-align=\"left\"><code>open_history()<\/code><\/td><td class=\"has-text-align-left\" data-align=\"left\"><strong>History &amp; Database<\/strong><\/td><td class=\"has-text-align-left\" data-align=\"left\">Search, filter, and export past invoices.<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\"><code>open_settings()<\/code><\/td><td class=\"has-text-align-left\" data-align=\"left\"><strong>AI Settings<\/strong><\/td><td class=\"has-text-align-left\" data-align=\"left\">API Keys (Gemini\/OpenRouter), Model selection, Ollama setup.<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\"><code>open_integrations()<\/code><\/td><td class=\"has-text-align-left\" data-align=\"left\"><strong>Integrations<\/strong><\/td><td class=\"has-text-align-left\" data-align=\"left\">Configure ERP connections (Odoo) and Webhooks.<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\"><code>open_processing_mode()<\/code><\/td><td class=\"has-text-align-left\" data-align=\"left\"><strong>Processing Mode<\/strong><\/td><td class=\"has-text-align-left\" data-align=\"left\">Switch between Cloud (AI) and Local (Regex\/OCR) modes.<\/td><\/tr><tr><td class=\"has-text-align-left\" data-align=\"left\"><code>open_about()<\/code><\/td><td class=\"has-text-align-left\" data-align=\"left\"><strong>About<\/strong><\/td><td class=\"has-text-align-left\" data-align=\"left\">Version info and credits.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<pre class=\"wp-block-code\"><code># Example: Open AI settings from a custom button\n@Action(label=\"Configure AI\", location=\"toolbar\")\ndef configure_ai(self, invoice):\n    self.api.ui.open_settings()<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><code>self.api.processing<\/code> &#8211; AI Extraction<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code># Synchronous (blocks UI - use in background only)\nresult = self.api.processing.extract(\"C:\/path\/to\/invoice.pdf\")\nif result&#91;'success']:\n    data = result&#91;'data']\n    print(f\"Vendor: {data&#91;'vendor_name']}\")\n    print(f\"Total: {data&#91;'invoice_total']}\")\n    print(f\"Items: {len(data.get('line_items', &#91;]))}\")\n\n# Asynchronous (recommended for UI)\ndef on_result(result):\n    if result&#91;'success']:\n        self.toast(f\"Extracted: {result&#91;'data']&#91;'invoice_total']}\")\n    else:\n        self.toast(f\"Error: {result.get('error_message')}\", \"error\")\n\nself.api.processing.extract_async(\"invoice.pdf\", on_result)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Extraction Result Structure:<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">{<br>&#8220;success&#8221;: True,<br>&#8220;data&#8221;: {<br>&#8220;vendor_name&#8221;: &#8220;Acme Corp&#8221;,<br>&#8220;invoice_number&#8221;: &#8220;INV-001&#8221;,<br>&#8220;invoice_date&#8221;: &#8220;2024-01-15&#8221;,<br>&#8220;date&#8221;: &#8220;2024-01-15&#8221;, \/\/ Alias for date<br>&#8220;invoice_total&#8221;: 1250.00,<br>&#8220;currency&#8221;: &#8220;USD&#8221;,<br>&#8220;line_items&#8221;: [<br>\/\/ Automatically fetched from DB if invoice is saved<br>{&#8220;description&#8221;: &#8220;Widget&#8221;, &#8220;quantity&#8221;: 5, &#8220;unit_price&#8221;: 250.00, &#8220;line_total&#8221;: 1250.00}<br>]<br>},<br>&#8220;error_message&#8221;: None # or error string if success=False<br>}<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Saving Extracted Data<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">After extracting data (especially in Connectors), you <strong>MUST<\/strong> explicitly save the invoice to the database if you want it to appear in the application history.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Save extracted data to database\n# CRITICAL: Pass db_path if running in a background thread (like connectors)\ndb_path = context.get('db_path') if 'context' in locals() else None\ninvoice_id = self.api.processing.save(data, source=\"my_plugin\", db_path=db_path)\n\nif invoice_id:\n    print(f\"Saved invoice #{invoice_id}\")\nelse:\n    print(\"Failed to save invoice\")\n\n### `self.api.files` - File Management\n\nHigh-level helpers for managing physical files related to invoices.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">python<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Move a temporary file to its permanent batch directory and update DB<\/h1>\n\n\n\n<h1 class=\"wp-block-heading\">This handles folder creation, copy\/rename, and updating the image_path in DB.<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">permanent_path = self.api.files.save_invoice_file(invoice_id, local_temp_path)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">if permanent_path:<br>self.api.ui.toast(f&#8221;File saved to batch folder: {permanent_path}&#8221;)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&gt; &#91;!NOTE]\n&gt; `get_current_invoice()` automatically includes `line_items` from the database if the invoice is saved. You do not need to query `invoice_line_items` manually.\n\n### `self.api.queue` - Processing Queue<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">python<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Add item to queue<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">queue_id = self.api.queue.add(<br>{&#8220;id&#8221;: 123, &#8220;file&#8221;: &#8220;invoice.pdf&#8221;, &#8220;source&#8221;: &#8220;email&#8221;},<br>source=&#8221;my_connector&#8221;<br>)<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Update status<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">self.api.queue.update_status(queue_id, &#8220;Downloading&#8221;)<br>self.api.queue.update_status(queue_id, &#8220;Processing&#8221;)<br>self.api.queue.update_status(queue_id, &#8220;Success&#8221;, &#8220;Extracted $1,250&#8221;)<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Get all items<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">items = self.api.queue.get_all()<br>for item in items:<br>print(f&#8221;{item[&#8216;id&#8217;]}: {item[&#8216;status&#8217;]}&#8221;)<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Show queue window<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">self.api.queue.show_window()<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>### `self.api.db` - Database Access\n\n#### Schema Diagram (ERD)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">mermaid<br>erDiagram<br>%% Core Invoices Tables<br>BATCHES ||&#8211;o{ EXTRACTED_DATA : contains<br>EXTRACTED_DATA ||&#8211;o{ INVOICE_LINE_ITEMS : has<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>%% Vendor Trust Center Tables\nVENDOR_PROFILES ||--o{ VENDOR_EXTRACTION_HISTORY : tracks\nVENDOR_PROFILES ||--o{ VENDOR_FIELD_VARIATIONS : has\nVENDOR_PROFILES ||--|{ VENDOR_LEARNING_STATS : metrics\nVENDOR_PROFILES ||--o{ ENRICHMENT_SUGGESTIONS : suggestions\n\n%% Cross-Links\nEXTRACTED_DATA ||--o{ VENDOR_EXTRACTION_HISTORY : extracted_from\nBATCHES ||--o{ VENDOR_EXTRACTION_HISTORY : part_of\n\nEXTRACTED_DATA {\n    int extracted_id PK\n    int id FK \"Refers to Batch ID (main_records)\"\n    string vendor_name\n    string invoice_number\n    string date\n    float invoice_total\n    string currency\n    string image_file\n    string invoice_category\n    string qr_phase \"phase1\/phase2\"\n    boolean is_verified\n}\n\nINVOICE_LINE_ITEMS {\n    int id PK\n    int invoice_id FK\n    int batch_id FK\n    string description\n    int quantity\n    float unit_price\n    float line_total\n    string product_code\n}\n\nVENDOR_PROFILES {\n    int vendor_id PK\n    string vat_id \"Unique\"\n    string vendor_name_primary\n    string vendor_name_ar\n    string vendor_name_en\n    string vendor_address\n    string vendor_phone\n    boolean is_trusted\n    boolean is_verified\n    float data_confidence_score\n}\n\nVENDOR_EXTRACTION_HISTORY {\n    int history_id PK\n    int vendor_id FK\n    int invoice_id FK\n    int batch_id FK\n    datetime extraction_date\n    string extraction_method\n    string vendor_name_extracted\n    float extraction_confidence\n}\n\nVENDOR_FIELD_VARIATIONS {\n    int variation_id PK\n    int vendor_id FK\n    string field_name\n    string field_value\n    int occurrence_count\n}\n\nVENDOR_LEARNING_STATS {\n    int stat_id PK\n    int vendor_id FK\n    string best_extraction_method\n    string field_accuracy_scores \"JSON\"\n    float avg_completeness\n}\n\nENRICHMENT_SUGGESTIONS {\n    int suggestion_id PK\n    int vendor_id FK\n    int invoice_id FK\n    string field_name\n    string suggested_value\n    float confidence\n    string status \"PENDING\/APPROVED\"\n}\n\nBATCHES {\n    int id PK \"Real table name: main_records\"\n    string detection_date\n    int num_invoices\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>&gt; **Note:**\n&gt;\n&gt; - `extracted_data.id` is the Foreign Key to `main_records` (Batch).\n&gt; - `vendor_profiles` is the central table for Vendor Trust Center data.\n&gt; - Tables like `vendor_extraction_history` link extract results to canonical vendor profiles.\n\n&gt; **Note:** In the `extracted_data` table, the column `id` is the Foreign Key linking to the Batch (Table `main_records`). It is NOT called `batch_id`. Be careful when writing SQL queries.\n\n#### Best Practices for Database Access\n\n- **Use `self.api.db.query`**: This method is now backed by a robust, direct connection manager. It is safe to use in plugins and threads.\n- **Do NOT use `sqlite3.connect` directly**: Unless absolutely necessary, avoid raw connections to prevent file lock issues.\n- **Read-Only by Default**: Most plugin operations should be read-only.\n\n#### API Methods<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">python<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Read-only query<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">results = self.api.db.query(<br>&#8220;SELECT extracted_id, vendor_name, invoice_total FROM extracted_data WHERE vendor_name LIKE ?&#8221;,<br>(&#8220;%Acme%&#8221;,)<br>)<br>for row in results:<br>print(f&#8221;Invoice {row[0]}: {row[1]} &#8211; ${row[2]}&#8221;)<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Fetch a single value (Convenience)<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">total_count = self.api.db.get_value(&#8220;SELECT COUNT(*) FROM extracted_data&#8221;)<br>vendor_id = self.api.db.get_value(&#8220;SELECT vendor_id FROM vendor_profiles WHERE vat_id = ?&#8221;, (vat_id,))<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Write operation (requires data:write capability)<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">success = self.api.db.execute(<br>&#8220;UPDATE extracted_data SET notes = ? WHERE extracted_id = ?&#8221;,<br>(&#8220;Updated via plugin&#8221;, invoice_id)<br>)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>### `self.api.system` - System Utilities<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">python<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Logging<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">self.api.system.log(&#8220;Something happened&#8221;, &#8220;info&#8221;) # Levels: debug, info, warning, error<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Schedule recurring task (every 60 seconds)<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">task_id = self.api.system.schedule(self.check_updates, 60)<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Cancel scheduled task<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">self.api.system.cancel_schedule(task_id)<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Get app info<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">info = self.api.system.get_app_info()<br>print(info[&#8216;plugin_dir&#8217;]) # Path to plugins folder<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Get current invoice<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">invoice = self.api.get_current_invoice()<br>if invoice:<br>print(f&#8221;Current: {invoice.get(&#8216;vendor_name&#8217;)}&#8221;)<br>print(f&#8221;Batch ID: {invoice.get(&#8216;batch_id&#8217;)}&#8221;)<br>print(f&#8221;File: {invoice.get(&#8216;file_path&#8217;)}&#8221;)<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Open URL in default browser<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">self.api.system.open_url(&#8220;https:\/\/google.com&#8221;)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>### `self.api.get_plugin_api()` - Plugin Communication<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">python<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">In Plugin A &#8211; expose functions<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">class PluginA(DeclarativePlugin):<br>@property<br>def exports(self):<br>return {<br>&#8216;send_alert&#8217;: self._send_alert,<br>&#8216;get_status&#8217;: lambda: self._status<br>}<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def _send_alert(self, message: str):\n    # Do something\n    pass<\/code><\/pre>\n\n\n\n<h1 class=\"wp-block-heading\">In Plugin B &#8211; call Plugin A<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">telegram = self.api.get_plugin_api(&#8216;telegram_declarative&#8217;)<br>if telegram and &#8216;send_message&#8217; in telegram:<br>telegram[&#8216;send_message&#8217;](&#8220;Invoice processed!&#8221;)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>---\n\n### `self.api.register_settings_tab` - Unified Settings\n\nThe `register_settings_tab` method allows you to add a native page to the application's **Unified Settings Dialog**. This is preferred over creating custom dialogs for configuration.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">python<br>def on_load(self):<br># Register a tab in the &#8220;Integrations&#8221; section (or dynamic list)<br>if hasattr(self.api, &#8216;register_settings_tab&#8217;):<br>self.api.register_settings_tab(<br>plugin_id=self.id,<br>label=&#8221;My Plugin&#8221;,<br>widget_factory=self._create_settings_ui<br>)<br>return True<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def _create_settings_ui(self):<br># Use SchemaWidget for auto-generated forms from Pydantic models<br>from core.plugins.schema_ui import SchemaWidget<br>from pydantic import BaseModel<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class Config(BaseModel):\n    api_key: str\n    enabled: bool = True\n\nreturn SchemaWidget(Config, initial_data=self.config, on_save=self.save_config)<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>---\n\n## \ud83d\udd0c Declarative Connector Framework\n\nFor plugins that fetch data from external sources (like Telegram, Email, Drives), use the **Declarative Connector Framework**. This provides a robust, threaded, and standardized way to build integrations.\n\n### 1. Define Configuration Schema\n\nDefine your configuration using Pydantic. This is used to auto-generate the settings UI.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">python<br>from pydantic import BaseModel, Field<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">class MyConfig(BaseModel):<br>api_key: str = Field(\u2026, title=&#8221;API Key&#8221;, json_schema_extra={&#8220;password&#8221;: True})<br>poll_interval: int = Field(60, title=&#8221;Poll Interval (s)&#8221;, ge=10)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>### 2. Create Connector Class\n\nInherit from `DeclarativeConnector` and define your triggers.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">python<br>from core.plugins.connector import DeclarativeConnector<br>from core.plugins.triggers import PollingTrigger, APIKeyAuth<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">class MyConnector(DeclarativeConnector):<br>display_name = &#8220;My Service&#8221;<br>config_model = MyConfig<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Auto-manages auth validation\nauth = APIKeyAuth(field=\"api_key\")\n\n# Define triggers (polling, webhook, etc)\ntriggers = &#91;\n    PollingTrigger(\n        interval_field=\"poll_interval\",\n        method=\"fetch_items\"\n    )\n]\n\ndef connect(self, config: MyConfig):\n    # Validate connection and return client\n    client = MyClient(config.api_key)\n    if not client.ping():\n        raise ConnectionError(\"Failed to connect\")\n    return client\n\ndef fetch_items(self, client, config: MyConfig, last_state: dict) -&gt; tuple&#91;list, dict]:\n    # Called periodically based on poll_interval\n    last_id = last_state.get('last_id', 0)\n    new_items = client.get_updates(since=last_id)\n\n    results = &#91;]\n    max_id = last_id\n\n    for item in new_items:\n        results.append({\n            'id': item.id,\n            'file_path': item.download_url, # Worker will download this\n            'source': 'my_service',\n            'original_filename': item.filename\n        })\n        max_id = max(max_id, item.id)\n\n    return results, {'last_id': max_id}\n    return results, {'last_id': max_id}\n\n@sandbox\ndef process_item(self, item: dict, context: dict) -&gt; dict:\n    \"\"\"\n    Process a single item (download, extract, SAVE).\n\n    CRITICAL: You must explicitly save the invoice after extraction!\n    \"\"\"\n    api = context.get('api')\n    update_status = context.get('update_status')\n\n    # 1. Download file\n    update_status(\"Downloading\", \"Fetching file...\")\n    local_path = self._download_file(item&#91;'file_path'])\n\n    # 2. Extract Data\n    update_status(\"Processing\", \" extracting data...\")\n    result = api.processing.extract(local_path)\n\n    if result&#91;'success']:\n         data = result&#91;'data']\n\n         # 3. SAVE TO DATABASE (Important!)\n         # Retrieve db_path from context to ensure thread-safe connection\n         db_path = context.get('db_path')\n         saved_id = api.processing.save(data, source=\"my_connector\", db_path=db_path)\n\n         update_status(\"Success\", f\"Saved invoice #{saved_id}\")\n         return {\"status\": \"Success\", \"data\": data}\n    else:\n         update_status(\"Error\", result&#91;'error_message'])\n         return {\"status\": \"Error\", \"message\": result&#91;'error_message']}\n\ndef _download_file(self, url):\n    # Implementation details...\n    return \"\/tmp\/invoice.pdf\"<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>### 3. Integrate in Main Plugin\n\nUse the `GenericConnectorWorker` to manage the lifecycle.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">python<br>from core.plugins.connector_worker import GenericConnectorWorker<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">class MyPlugin(DeclarativePlugin):<br>def on_load(self):<br>self.config = self._load_config() # Load your MyConfig model<br>self.connector = MyConnector()<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    # Start worker thread\n    self.worker = GenericConnectorWorker(self.connector, self.config, db_path=\"...\", api=self.api)\n    self.worker.start()\n\ndef on_unload(self):\n    if self.worker:\n        self.worker.stop()<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>---\n\n## \u2b50 Best Practices\n\n1. **Always use Async for Heavy Tasks**:\n\n   * \u274c BLOCKING: `time.sleep(5)` or `requests.get()` in the main thread. Freezes the UI.\n   * \u2705 ASYNC: Use `self.api.processing.extract_async()` or run code in a separate thread.\n2. **Protect Risky Code with `@sandbox`**:\n\n   * Wrap entry points (actions, hooks) with `@sandbox` so one error doesn't crash the whole app.\n3. **UI Type Safety**:\n\n   * Always use `from core.plugins.sdk import *` for type safety and access to all protocols.\n   * Use `InvoiceData`, `QueueItem`, etc., for proper type hinting.\n4. **Resource Cleanup**:\n\n   * Always implement `on_unload()` to stop timers, close files, and disconnect signals.\n5. **Use Capabilities Explicitly**:\n\n   * Don't just add all capabilities. Follow \"Least Privilege\".\n   * Use `@requires_capability` decorator to enforce checks.\n6. **Database Efficiency**:\n\n   * Use parameterized queries `sql = \"SELECT * FROM x WHERE id=?\"` and `params=(1,)`.\n   * Never concatenate strings into SQL (SQL Injection risk).\n\n---\n\n## \ud83d\udee1\ufe0f Permissions &amp; Capabilities\n\nDefine in `manifest.json` to unlock API features:<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">json<br>{<br>&#8220;capabilities&#8221;: [<br>&#8220;ui:toast&#8221;,<br>&#8220;ui:toolbar&#8221;,<br>&#8220;ui:section&#8221;,<br>&#8220;ui:section&#8221;,<br>&#8220;ui:settings&#8221;,<br>&#8220;data:read&#8221;,<br>&#8220;data:write&#8221;,<br>&#8220;data:persist&#8221;,<br>&#8220;network:access&#8221;,<br>&#8220;file:read&#8221;,<br>&#8220;file:write&#8221;,<br>&#8220;integration:source&#8221;,<br>&#8220;integration:mode&#8221;,<br>&#8220;plugin:communicate&#8221;<br>]<br>}<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Permission Popup &amp; Dynamic Access<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If a plugin requests a capability not listed in its <code>manifest.json<\/code>, the application will <strong>pause and prompt the user<\/strong> to allow or deny the request.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Denied<\/strong>: The API call returns <code>None<\/code> or raises <code>PermissionDeniedError<\/code>, and the plugin logic should handle the failure gracefully.<\/li>\n\n\n\n<li><strong>Allowed<\/strong>: The capability is granted for the current session.<\/li>\n<\/ul>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">[!WARNING]<br>While dynamic requests are possible, strictly declaring capabilities in <code>manifest.json<\/code> is best practice to build user trust.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">Enforcing Permissions<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>from core.plugins.permissions import requires_capability, sandbox\n\nclass MyPlugin(DeclarativePlugin):\n    @requires_capability(\"network:access\")\n    def fetch_external_data(self):\n        # Only runs if plugin has network:access\n        pass\n\n    @sandbox\n    def risky_operation(self):\n        # Errors here won't crash the app\n        pass<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udce6 Dependencies<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Declare Python packages in <code>manifest.json<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"dependencies\": &#91;\"requests&gt;=2.28\", \"pandas\"]\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">1. Standard Library<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">All standard Python 3.12 libraries are available.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Application Packages<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The following packages are pre-installed in the application environment:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>requests<\/code><\/li>\n\n\n\n<li><code>pydantic<\/code><\/li>\n\n\n\n<li><code>PyQt5<\/code><\/li>\n\n\n\n<li><code>sqlite3<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">3. Bundling External Dependencies (Vendoring)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Since the application is distributed as a frozen executable (using Nuitka), users cannot simply <code>pip install<\/code> packages into it. If your plugin requires external libraries (e.g., <code>pandas<\/code>, <code>beautifulsoup4<\/code>, <code>pymongo<\/code>), you must <strong>bundle<\/strong> them with your plugin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How to Bundle Libraries:<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Create a folder named <code>libs<\/code> inside your plugin directory.<\/li>\n\n\n\n<li>Install the package <em>into<\/em> that folder using pip&#8217;s <code>--target<\/code> option.<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code># In your terminal\ncd plugins\/my-plugin\nmkdir libs\npip install pandas --target=.\/libs<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Structure:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>plugins\/my-plugin\/\n\u251c\u2500\u2500 main.py\n\u251c\u2500\u2500 manifest.json\n\u2514\u2500\u2500 libs\/\n    \u251c\u2500\u2500 pandas\/\n    \u251c\u2500\u2500 numpy\/\n    \u2514\u2500\u2500 ...<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Usage:<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Just <code>import<\/code> normally in your code. The Plugin Loader automatically adds the <code>libs<\/code> folder to the system path.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># main.py\nimport pandas as pd  # Works automatically!\n\nclass MyPlugin(DeclarativePlugin):\n    ...<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">[!IMPORTANT]<br><strong>Binary Dependencies<\/strong>: Pure Python packages work best. Packages with compiled C extensions (like <code>numpy<\/code>, <code>pillow<\/code>) <em>may<\/em> work if you bundle the correct binary wheels for Windows, but can be tricky. Test thoroughly.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udd25 Development Tools<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Hot Reload<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>set INVOICES_DEV_MODE=1\npython main.py<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Plugins auto-reload on file save.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Plugin Inspector<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Tools \u2192 Plugin Inspector<\/strong> shows:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Loaded plugins<\/li>\n\n\n\n<li>Fields, actions, hooks<\/li>\n\n\n\n<li>Signal connections<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Type Hints<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>from core.plugins.types import InvoiceData, ExtractionResult, InvoicesAPIProtocol\n\nclass MyPlugin(DeclarativePlugin):\n    api: InvoicesAPIProtocol  # Full IDE autocomplete<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83c\udfaf Common Patterns<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Send Notification on Save<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>@hook(\"invoice.saved\")\ndef notify_on_save(self, invoice_id):\n    invoice = self.api.db.query(\"SELECT vendor_name, invoice_total FROM extracted_data WHERE extracted_id=?\", (invoice_id,))\n    if invoice:\n        self.toast(f\"Saved: {invoice&#91;0]&#91;0]} - ${invoice&#91;0]&#91;1]}\")<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Background Sync<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>def on_load(self):\n    self._sync_task = self.api.system.schedule(self.sync, 300)  # Every 5 min\n\ndef on_unload(self):\n    self.api.system.cancel_schedule(self._sync_task)\n\ndef sync(self):\n    # Sync logic here\n    self.api.system.log(\"Syncing...\", \"info\")<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Export to External Service<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>@Action(label=\"Export\", icon=\"fa5s.upload\", location=\"toolbar\")\ndef export_invoice(self, invoice):\n    if self.api.ui.show_confirm(\"Export?\", f\"Send {invoice&#91;'vendor_name']} to CRM?\"):\n        # Call external API\n        self.toast(\"Exported!\", \"success\")<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udccb Checklist for New Plugins<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>[ ] Create folder: <code>plugins\/my-plugin\/<\/code><\/li>\n\n\n\n<li>[ ] Create <code>manifest.json<\/code> with id, name, version, capabilities<\/li>\n\n\n\n<li>[ ] Create <code>main.py<\/code> with class inheriting <code>DeclarativePlugin<\/code><\/li>\n\n\n\n<li>[ ] Set <code>id<\/code>, <code>name<\/code>, <code>version<\/code> class attributes<\/li>\n\n\n\n<li>[ ] Implement <code>on_load()<\/code> for initialization<\/li>\n\n\n\n<li>[ ] Implement <code>on_unload()<\/code> for cleanup<\/li>\n\n\n\n<li>[ ] Add <code>@Action<\/code> for buttons<\/li>\n\n\n\n<li>[ ] Add <code>@hook<\/code> for event handling<\/li>\n\n\n\n<li>[ ] Add <code>Field<\/code> for persistent data<\/li>\n\n\n\n<li>[ ] Test with Plugin Inspector<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83c\udd98 Troubleshooting<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Issue<\/th><th>Solution<\/th><\/tr><\/thead><tbody><tr><td>Plugin not loading<\/td><td>Check <code>manifest.json<\/code> syntax, verify <code>plugin_class<\/code> matches<\/td><\/tr><tr><td>Action not appearing<\/td><td>Verify <code>location<\/code> is valid, check capabilities<\/td><\/tr><tr><td>Hook not firing<\/td><td>Verify hook name spelling, check signal connection in Inspector<\/td><\/tr><tr><td>Permission denied<\/td><td>Add required capability to manifest<\/td><\/tr><tr><td>Import error<\/td><td>Add package to <code>dependencies<\/code> in manifest<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Last updated: December 2024<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udcc2 Plugin Templates<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Template 1: Export to CSV (using SDK)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Exports the current invoice details to a CSV file.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from core.plugins.sdk import *\n\nclass ExportPlugin(DeclarativePlugin):\n    id = \"csv-exporter\"\n    name = \"Smart Exporter\"\n    version = \"2.0.0\"\n\n    @Action(label='Export CSV', icon='fa5s.file-csv', location='menu:Plugins')\n    @sandbox\n    @requires_capability('file:write')\n    def export_csv(self, invoice: Dict&#91;str, Any]):\n        \"\"\"Export all invoices from current batch to CSV.\"\"\"\n        try:\n            # 1. Get data (from batch or current invoice)\n            batch_id = invoice.get('batch_id')\n            if not batch_id:\n                self.api.ui.toast(\"No batch context\", \"warning\")\n                return\n\n            rows = self.api.db.query(\n                \"SELECT extracted_id, vendor_name, invoice_total, date, currency FROM extracted_data WHERE id = ?\",\n                (batch_id,)\n            )\n\n            if not rows:\n                self.api.ui.toast('No invoices to export', 'warning')\n                return\n\n            # 2. Determine path\n            filename = f\"export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv\"\n            export_dir = os.path.expanduser('~\/Desktop')\n            filepath = os.path.join(export_dir, filename)\n\n            # 3. Write file\n            with open(filepath, 'w', newline='', encoding='utf-8-sig') as f:\n                writer = csv.writer(f)\n                writer.writerow(&#91;'ID', 'Vendor', 'Total', 'Date', 'Currency']) # Header\n                writer.writerows(rows)\n\n            self.api.ui.message(\n                \"Export Complete\",\n                f\"\u2705 Exported {len(rows)} invoices to:\\n{filepath}\",\n                \"success\"\n            )\n        except Exception as e:\n            self.api.system.log(f\"Export failed: {e}\", 'error')\n            self.api.ui.toast(\"Export failed\", \"error\")<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Template 2: Auto-Tag Vendor<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Automatically adds a tag to the vendor profile when an invoice is saved.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from core.plugins import DeclarativePlugin, hook, requires_capability\n\nclass AutoTagger(DeclarativePlugin):\n    id = \"auto-tagger\"\n    name = \"Auto Tagger\"\n    version = \"1.0.0\"\n\n    @hook(\"invoice.saved\")\n    @requires_capability(\"data:write\")\n    def on_save(self, invoice_id):\n        # Get invoice data\n        res = self.api.db.query(\"SELECT vendor_name, invoice_total FROM extracted_data WHERE extracted_id=?\", (invoice_id,))\n        if not res: return\n\n        vendor_name, total = res&#91;0]\n\n        # Logic: Tag high-value vendors\n        if total &gt; 5000:\n            self.api.system.log(f\"Tagging high-value vendor: {vendor_name}\")\n            # (Assume vendor_trust_scores table exists)\n            self.api.db.execute(\n                \"UPDATE vendor_trust_scores SET vendor_tags = 'VIP' WHERE vendor_name = ?\", \n                (vendor_name,)\n            )\n            self.api.ui.toast(f\"Marked {vendor_name} as VIP\", \"info\")<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Template 3: Declarative Fields + Custom Widget<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Combines auto-managed fields with a custom widget in the same section.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel\nfrom core.plugins import DeclarativePlugin, hook, Field\n\nclass VendorAnalytics(DeclarativePlugin):\n    id = \"vendor-analytics\"\n    name = \"Vendor Analytics\"\n    version = \"1.0.0\"\n\n    # Declarative field - creates \"Analytics\" section automatically\n    show_charts = Field(type=\"checkbox\", label=\"Show Charts\", section=\"Analytics\", persist=True)\n\n    def on_load(self):\n        # Custom widget for dynamic content\n        self.summary_widget = QWidget()\n        layout = QVBoxLayout(self.summary_widget)\n        self.lbl_summary = QLabel(\"Loading...\")\n        layout.addWidget(self.lbl_summary)\n\n        # Add widget to the SAME section created by Field above\n        # Deduplication ensures no duplicate sections\n        self.api.ui.add_section(\"Analytics\", self.summary_widget, plugin_id=self.id)\n        return True\n\n    @hook(\"invoice.loaded\")\n    def on_invoice_loaded(self, invoice_id, invoice):\n        vendor = invoice.get(\"vendor_name\", \"Unknown\")\n        self.lbl_summary.setText(f\"&lt;b&gt;Vendor:&lt;\/b&gt; {vendor}\")<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Template 4: Complete UI Test (Cookbook)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A comprehensive plugin demonstrating <strong>every supported UI location<\/strong> and correct icon usage.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>manifest.json<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"id\": \"ui-location-test\",\n    \"name\": \"UI Location Test\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Antigravity\",\n    \"description\": \"Demonstrates all UI action locations\",\n    \"main\": \"main.py\",\n    \"plugin_class\": \"UiTestPlugin\",\n    \"capabilities\": &#91;\"ui:section\", \"ui:toolbar\", \"ui:toast\"]\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>main.py<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from core.plugins.sdk import *\n\nclass UiTestPlugin(DeclarativePlugin):\n    id = \"ui-location-test\"\n    name = \"UI Location Test\"\n\n    def on_load(self) -&gt; bool:\n        return True\n\n    # 1. Sidebar Section (Default or Custom)\n    @Action(label=\"Sidebar Button\", icon=\"fa5s.plug\", location=\"section\")\n    def test_section(self, invoice=None):\n        self.api.show_toast(\"Clicked Sidebar Action!\", \"info\")\n\n    # 2. Main Toolbar (Left Slot)\n    @Action(label=\"Left\", icon=\"fa5s.arrow-left\", location=\"toolbar:left\")\n    def test_toolbar_left(self, invoice=None):\n        self.api.show_toast(\"Clicked Toolbar Left!\", \"info\")\n\n    # 3. Main Toolbar (Right Slot)\n    @Action(label=\"Right\", icon=\"fa5s.arrow-right\", location=\"toolbar:right\")\n    def test_toolbar_right(self, invoice=None):\n        self.api.show_toast(\"Clicked Toolbar Right!\", \"info\")\n\n    # 4. Plugins Menu\n    @Action(label=\"Test Action (Plugins)\", icon=\"fa5s.puzzle-piece\", location=\"menu:Plugins\")\n    def test_menu_plugins(self, invoice=None):\n        self.api.show_toast(\"Clicked Plugins Menu Action!\", \"info\")\n\n    # 5. Tools Menu\n    @Action(label=\"Test Action (Tools)\", icon=\"fa5s.tools\", location=\"menu:Tools\")\n    def test_menu_tools(self, invoice=None):\n        self.api.show_toast(\"Clicked Tools Menu Action!\", \"info\")\n\n    # 6. Action Area (Process Buttons)\n    @Action(label=\"Action Area\", icon=\"fa5s.bolt\", location=\"actions\")\n    def test_actions_area(self, invoice=None):\n        self.api.show_toast(\"Clicked Action Area Button!\", \"info\")<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Invoices Reader \u2013 Plugin Developer Manual Complete Reference for Plugin DevelopmentThis manual provides everything developers (and AI coding assistants) need\u2026<\/p>","protected":false},"featured_media":0,"parent":0,"menu_order":0,"template":"","documentation_category":[123],"documentation_tag":[],"class_list":["post-1016","documentation","type-documentation","status-publish","hentry","documentation_category-developer-resources"],"_links":{"self":[{"href":"https:\/\/invoicesreader.com\/en\/wp-json\/wp\/v2\/documentation\/1016","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/invoicesreader.com\/en\/wp-json\/wp\/v2\/documentation"}],"about":[{"href":"https:\/\/invoicesreader.com\/en\/wp-json\/wp\/v2\/types\/documentation"}],"version-history":[{"count":6,"href":"https:\/\/invoicesreader.com\/en\/wp-json\/wp\/v2\/documentation\/1016\/revisions"}],"predecessor-version":[{"id":1023,"href":"https:\/\/invoicesreader.com\/en\/wp-json\/wp\/v2\/documentation\/1016\/revisions\/1023"}],"wp:attachment":[{"href":"https:\/\/invoicesreader.com\/en\/wp-json\/wp\/v2\/media?parent=1016"}],"wp:term":[{"taxonomy":"documentation_category","embeddable":true,"href":"https:\/\/invoicesreader.com\/en\/wp-json\/wp\/v2\/documentation_category?post=1016"},{"taxonomy":"documentation_tag","embeddable":true,"href":"https:\/\/invoicesreader.com\/en\/wp-json\/wp\/v2\/documentation_tag?post=1016"}],"curies":[{"name":"<p>The digital transformation in the Kingdom of Saudi Arabia is accelerating, and with it, the need for <strong>invoice automation solutions in Saudi Arabia<\/strong> is increasing. As we enter 2025, the implementation of e-invoicing is no longer an option, but a prerequisite for business continuity.<\/p>","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}