feat(automation): add PO-to-invoice pipeline
- 6 new PocketBase migrations: customer_pos, customer_po_lines, invoices, invoice_items, email_inbox, workflow_runs - n8N workflow JSON: 01-customer-po-to-parsed.json + 02-parsed-po-to-invoice.json - MCP server config (.cursor/mcp.json + .mcp.json) for AI agent PB access - .env.example template with all required credentials documented - n8n/README.md with deployment, testing, and customization notes
This commit is contained in:
parent
642548295e
commit
c272526d67
12 changed files with 2490 additions and 0 deletions
15
.cursor/mcp.json
Normal file
15
.cursor/mcp.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"_comment": "JEISA PocketBase MCP — for Cursor / Claude Desktop / Claude Code. Lets an AI agent read & write PocketBase collections: customer_pos, invoices, stock_items, customers, etc.",
|
||||
"_setup": "1) Get a PB superuser (admin) email + password. The default is the one in .env (david@wfsales.co.za). 2) Fill in pocketbase_url, pocketbase_admin_email, pocketbase_admin_password. 3) Restart Cursor / Claude Desktop.",
|
||||
"mcpServers": {
|
||||
"jeisa-pocketbase": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "pocketbase-mcp-bridge"],
|
||||
"env": {
|
||||
"POCKETBASE_URL": "https://pb.jeistk.apphub.co.za",
|
||||
"POCKETBASE_ADMIN_EMAIL": "REPLACE_ME_admin_email",
|
||||
"POCKETBASE_ADMIN_PASSWORD": "REPLACE_ME_admin_password"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
.env.example
Normal file
49
.env.example
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# =============================================================
|
||||
# JEISA Automation: Customer PO → Invoice → Stock decrement
|
||||
# =============================================================
|
||||
#
|
||||
# Copy this file to `.env` (root of the JEISA app) and fill in
|
||||
# the values. These are read by:
|
||||
# • The PocketBase MCP server (so an AI agent can read/write PB)
|
||||
# • n8N credentials (when you configure the IMAP / QB / SMTP nodes)
|
||||
#
|
||||
# DO NOT commit `.env` to git. (Already in .gitignore.)
|
||||
# =============================================================
|
||||
|
||||
# ---------- PocketBase ----------
|
||||
VITE_PB_URL=https://pb.jeistk.apphub.co.za
|
||||
POCKETBASE_URL=https://pb.jeistk.apphub.co.za
|
||||
|
||||
# Superuser (admin) credentials for the MCP and n8N HTTP Request nodes.
|
||||
# You can find these in /Volumes/EXT/DEVELOPMENT-FORGEJO/JEISA-APP/setup-users.js
|
||||
# or in your PB dashboard under Settings → Admins.
|
||||
POCKETBASE_ADMIN_EMAIL=david@wfsales.co.za
|
||||
POCKETBASE_ADMIN_PASSWORD=REPLACE_ME_admin_password
|
||||
|
||||
# ---------- IMAP (incoming customer PO inbox) ----------
|
||||
# This is the mailbox customers send their POs to.
|
||||
# IMAP_HOST is host:port (e.g. imap.gmail.com:993)
|
||||
IMAP_HOST=imap.gmail.com
|
||||
IMAP_PORT=993
|
||||
IMAP_USER=orders@jeisa.co.za
|
||||
IMAP_PASSWORD=REPLACE_ME_app_password
|
||||
IMAP_TLS=true
|
||||
|
||||
# ---------- QuickBooks Online ----------
|
||||
# OAuth2 app credentials (https://developer.intuit.com/app/developer/dashboard)
|
||||
# After you create the app, generate sandbox + production keys.
|
||||
QB_CLIENT_ID=REPLACE_ME_client_id
|
||||
QB_CLIENT_SECRET=REPLACE_ME_client_secret
|
||||
QB_REALM_ID=REPLACE_ME_realm_id
|
||||
QB_ENVIRONMENT=sandbox
|
||||
|
||||
# ---------- SMTP (fallback for sending invoice if QB can't) ----------
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=invoices@jeisa.co.za
|
||||
SMTP_PASSWORD=REPLACE_ME_smtp_password
|
||||
SMTP_FROM_NAME=JEISA Invoices
|
||||
|
||||
# ---------- Optional: OpenAI (for AI-powered PDF parsing) ----------
|
||||
# If your POs aren't machine-readable, use AI to extract line items.
|
||||
OPENAI_API_KEY=REPLACE_ME_openai_key
|
||||
15
.mcp.json
Normal file
15
.mcp.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"_comment": "JEISA PocketBase MCP — for Claude Desktop. Same as .cursor/mcp.json.",
|
||||
"_setup": "Copy to ~/Library/Application Support/Claude/claude_desktop_config.json on macOS, or %APPDATA%/Claude/claude_desktop_config.json on Windows. Then restart Claude Desktop.",
|
||||
"mcpServers": {
|
||||
"jeisa-pocketbase": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "pocketbase-mcp-bridge"],
|
||||
"env": {
|
||||
"POCKETBASE_URL": "https://pb.jeistk.apphub.co.za",
|
||||
"POCKETBASE_ADMIN_EMAIL": "REPLACE_ME_admin_email",
|
||||
"POCKETBASE_ADMIN_PASSWORD": "REPLACE_ME_admin_password"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
535
n8n/01-customer-po-to-parsed.json
Normal file
535
n8n/01-customer-po-to-parsed.json
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
{
|
||||
"name": "JEISA: Customer PO → Upload → Parse",
|
||||
"description": "Monitors the JEI orders inbox, uploads every PDF PO to PocketBase as a `customer_pos` record, then parses the PDF for line items (sku, qty, price). Two workflows in series: this one stops at status='parsed' and the next workflow ('Create Invoice from PO') picks up from there.",
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"saveExecutionProgress": true,
|
||||
"saveManualExecutions": true,
|
||||
"saveDataSuccessExecution": "all",
|
||||
"saveDataErrorExecution": "all",
|
||||
"timezone": "Africa/Johannesburg"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"mailbox": "INBOX",
|
||||
"postProcessAction": "read",
|
||||
"options": {
|
||||
"folderChanged": false
|
||||
}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-111111111101",
|
||||
"name": "IMAP: New Email",
|
||||
"type": "n8n-nodes-base.emailReadImap",
|
||||
"typeVersion": 2,
|
||||
"position": [240, 300],
|
||||
"webhookId": "jeisa-imap-trigger",
|
||||
"credentials": {
|
||||
"imap": {
|
||||
"id": "REPLACE_ME_imap_credential",
|
||||
"name": "JEISA IMAP (orders@jeisa)"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "cond-1",
|
||||
"leftValue": "={{ $json.hasAttachments }}",
|
||||
"rightValue": true,
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cond-2",
|
||||
"leftValue": "={{ $json.subject }}",
|
||||
"rightValue": "(?i)po|purchase order|order",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "regex"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-111111111102",
|
||||
"name": "Filter: Has PDF + looks like PO",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [460, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://pb.jeistk.apphub.co.za/api/collections/email_inbox/records",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"imap_uid\": \"{{ $('IMAP: New Email').item.json.messageId }}\",\n \"from\": \"{{ $('IMAP: New Email').item.json.from }}\",\n \"subject\": \"{{ $('IMAP: New Email').item.json.subject }}\",\n \"to\": \"{{ $('IMAP: New Email').item.json.to }}\",\n \"body_plain\": \"{{ $('IMAP: New Email').item.json.textPlain }}\",\n \"received_at\": \"{{ $('IMAP: New Email').item.json.date }}\",\n \"has_attachments\": {{ $('IMAP: New Email').item.json.hasAttachments }},\n \"attachment_names\": \"{{ $('IMAP: New Email').item.json.attachments.map(a => a.filename).join(', ') }}\",\n \"disposition\": \"processed\"\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-111111111103",
|
||||
"name": "PB: Log email_inbox",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [680, 200],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://pb.jeistk.apphub.co.za/api/collections/email_inbox/records",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"imap_uid\": \"{{ $json.messageId }}\",\n \"from\": \"{{ $json.from }}\",\n \"subject\": \"{{ $json.subject }}\",\n \"to\": \"{{ $json.to }}\",\n \"received_at\": \"{{ $json.date }}\",\n \"has_attachments\": {{ $json.hasAttachments }},\n \"disposition\": \"ignored\"\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-111111111104",
|
||||
"name": "PB: Log ignored email",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [680, 400],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "=https://pb.jeistk.apphub.co.za/api/collections/customers/records?filter=email%20%3D%20%22{{ encodeURIComponent($('IMAP: New Email').item.json.from.value[0].address) }}%22%20%7C%7C%20alternative_email%20%3D%20%22{{ encodeURIComponent($('IMAP: New Email').item.json.from.value[0].address) }}%22%20%7C%7C%20buyer_email%20%3D%20%22{{ encodeURIComponent($('IMAP: New Email').item.json.from.value[0].address) }}%22&perPage=1",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-111111111105",
|
||||
"name": "PB: Find customer by email",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [900, 200],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://pb.jeistk.apphub.co.za/api/collections/customer_pos/records",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"customer\": \"{{ $json.items[0].id }}\",\n \"source_email_from\": \"{{ $('IMAP: New Email').item.json.from.value[0].address }}\",\n \"source_email_subject\": \"{{ $('IMAP: New Email').item.json.subject }}\",\n \"source_email_received_at\": \"{{ $('IMAP: New Email').item.json.date }}\",\n \"source_email_uid\": \"{{ $('IMAP: New Email').item.json.messageId }}\",\n \"status\": \"new\"\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-111111111106",
|
||||
"name": "PB: Create customer_pos record",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1120, 200],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "https://pb.jeistk.apphub.co.za/api/collections/customers/records?filter=email%20%3D%20%22{{ encodeURIComponent($('IMAP: New Email').item.json.from.value[0].address) }}%22%20%7C%7C%20alternative_email%20%3D%20%22{{ encodeURIComponent($('IMAP: New Email').item.json.from.value[0].address) }}%22%20%7C%7C%20buyer_email%20%3D%20%22{{ encodeURIComponent($('IMAP: New Email').item.json.from.value[0].address) }}%22&perPage=1",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-111111111107",
|
||||
"name": "PB: Find customer by email (fallback)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [900, 400],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://pb.jeistk.apphub.co.za/api/collections/customer_pos/records",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"source_email_from\": \"{{ $('IMAP: New Email').item.json.from.value[0].address }}\",\n \"source_email_subject\": \"{{ $('IMAP: New Email').item.json.subject }}\",\n \"source_email_received_at\": \"{{ $('IMAP: New Email').item.json.date }}\",\n \"source_email_uid\": \"{{ $('IMAP: New Email').item.json.messageId }}\",\n \"status\": \"new\",\n \"notes\": \"Auto-created: no matching customer in PB. Needs manual review.\"\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-111111111108",
|
||||
"name": "PB: Create customer_pos (no customer)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1120, 400],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "cond-3",
|
||||
"leftValue": "={{ $json.items.length }}",
|
||||
"rightValue": 0,
|
||||
"operator": {
|
||||
"type": "number",
|
||||
"operation": "gt"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-111111111109",
|
||||
"name": "If: customer found",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [1340, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "PATCH",
|
||||
"url": "=https://pb.jeistk.apphub.co.za/api/collections/customer_pos/records/{{ $json.id }}",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"customer\": \"{{ $('PB: Find customer by email').item.json.items[0].id }}\",\n \"status\": \"new\"\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-11111111110a",
|
||||
"name": "PB: Link customer to PO",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1560, 200],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://pb.jeistk.apphub.co.za/api/collections/customer_pos/records/{{ $json.id }}/po_pdf",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendBody": true,
|
||||
"specifyBody": "multipart-form-data",
|
||||
"multipartFormData": [
|
||||
{
|
||||
"name": "file",
|
||||
"value": "={{ $binary }}",
|
||||
"type": "binary",
|
||||
"fileName": "={{ $('IMAP: New Email').item.binary.attachment_0.fileName }}"
|
||||
}
|
||||
],
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-11111111110b",
|
||||
"name": "PB: Upload PDF to customer_pos",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1780, 300],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "pdf",
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-11111111110c",
|
||||
"name": "Extract PDF text",
|
||||
"type": "n8n-nodes-base.extractFromFile",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Receives the PDF text from Extract PDF text node.\n// Output: { lines: [{sku, description, quantity, unit_price, line_total}] }\n\nconst text = $input.first().json.text || '';\n\n// 1. Try to extract a PO number\nlet poNumber = null;\nconst poMatch = text.match(/(?:PO|P\\.O\\.|Purchase Order)\\s*(?:#|No\\.?|Number)?\\s*[:\\-]?\\s*([A-Z0-9\\-_/]{3,30})/i);\nif (poMatch) poNumber = poMatch[1].trim();\n\n// 2. Try to detect a currency\nlet currency = 'ZAR';\nif (/\\bUSD\\b|\\$\\s*\\d/.test(text) && !/R\\s?\\d/.test(text)) currency = 'USD';\nelse if (/\\bEUR\\b|€/.test(text)) currency = 'EUR';\nelse if (/\\bGBP\\b|£/.test(text)) currency = 'GBP';\n\n// 3. Extract line items: scan for lines that look like 'SKU Description Qty UnitPrice Total'\nconst lines = [];\n// Lines like \"AB-123 Widget 10 100.00 1000.00\"\nconst lineRegex = /^([A-Z0-9][A-Z0-9\\-_/\\.]{2,30})\\s+(.{3,80}?)\\s+(\\d+(?:\\.\\d+)?)\\s+(\\d+(?:\\.\\d+)?)\\s+(\\d+(?:\\.\\d+)?)\\s*$/gm;\nlet m;\nwhile ((m = lineRegex.exec(text)) !== null) {\n lines.push({\n sku: m[1].trim(),\n description: m[2].trim(),\n quantity: parseFloat(m[3]),\n unit_price: parseFloat(m[4]),\n line_total: parseFloat(m[5]),\n });\n}\n\n// 4. Compute total\nconst total = lines.reduce((s, l) => s + (l.line_total || 0), 0);\n\nreturn [{\n json: {\n po_number: poNumber,\n currency,\n total_amount: total,\n lines,\n raw_text_length: text.length,\n }\n}];\n"
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-11111111110d",
|
||||
"name": "Function: Parse lines from PDF text",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [2220, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "PATCH",
|
||||
"url": "=https://pb.jeistk.apphub.co.za/api/collections/customer_pos/records/{{ $('PB: Create customer_pos record').item.json.id }}",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"po_number\": \"{{ $json.po_number }}\",\n \"currency\": \"{{ $json.currency }}\",\n \"total_amount\": {{ $json.total_amount }},\n \"parsed_lines\": {{ JSON.stringify($json.lines) }},\n \"status\": \"parsed\",\n \"processed_at\": \"{{ $now.toISO() }}\"\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "11111111-1111-1111-1111-11111111110e",
|
||||
"name": "PB: Save parsed_lines + status=parsed",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [2440, 300],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"IMAP: New Email": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Filter: Has PDF + looks like PO",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Filter: Has PDF + looks like PO": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Log email_inbox",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "PB: Log ignored email",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"PB: Log email_inbox": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Find customer by email",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"PB: Find customer by email": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Create customer_pos record",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"PB: Create customer_pos record": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "If: customer found",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"If: customer found": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Link customer to PO",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "PB: Upload PDF to customer_pos",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "PB: Upload PDF to customer_pos",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"PB: Link customer to PO": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Upload PDF to customer_pos",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"PB: Upload PDF to customer_pos": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Extract PDF text",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Extract PDF text": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Function: Parse lines from PDF text",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Function: Parse lines from PDF text": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Save parsed_lines + status=parsed",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"versionId": "1",
|
||||
"triggerCount": 1,
|
||||
"tags": [
|
||||
{
|
||||
"name": "jeisa",
|
||||
"id": "tag-jeisa"
|
||||
},
|
||||
{
|
||||
"name": "po",
|
||||
"id": "tag-po"
|
||||
},
|
||||
{
|
||||
"name": "purchase-order",
|
||||
"id": "tag-purchase-order"
|
||||
}
|
||||
]
|
||||
}
|
||||
544
n8n/02-parsed-po-to-invoice.json
Normal file
544
n8n/02-parsed-po-to-invoice.json
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
{
|
||||
"name": "JEISA: Parsed PO → QuickBooks Invoice → Email → Stock decrement",
|
||||
"description": "Polls PocketBase for `customer_pos` records with status='parsed', creates a QuickBooks Online invoice, sends it to the customer via email, and decrements stock_items.quantity for each line item linked to inventory. End state: customer_pos.status='invoiced', invoices.status='sent', stock_items decremented.",
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"saveExecutionProgress": true,
|
||||
"saveManualExecutions": true,
|
||||
"saveDataSuccessExecution": "all",
|
||||
"saveDataErrorExecution": "all",
|
||||
"timezone": "Africa/Johannesburg"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [
|
||||
{
|
||||
"field": "minutes",
|
||||
"minutesInterval": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-222222222201",
|
||||
"name": "Cron: every 2 minutes",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [240, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "https://pb.jeistk.apphub.co.za/api/collections/customer_pos/records?filter=status%20%3D%20%22parsed%22&expand=customer&perPage=20",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"options": {}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-222222222202",
|
||||
"name": "PB: Fetch customer_pos (status=parsed)",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [460, 300],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c-1",
|
||||
"leftValue": "={{ $json.items.length }}",
|
||||
"rightValue": 0,
|
||||
"operator": {
|
||||
"type": "number",
|
||||
"operation": "gt"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-222222222203",
|
||||
"name": "If: any POs to invoice?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [680, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"batchSize": 1,
|
||||
"options": {}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-222222222204",
|
||||
"name": "Loop: One PO at a time",
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 3,
|
||||
"position": [900, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "=https://pb.jeistk.apphub.co.za/api/collections/stock_items/records?filter=sku%20%3D%20%22{{ encodeURIComponent($json.sku) }}%22&perPage=1",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"options": {}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-222222222205",
|
||||
"name": "PB: Lookup stock_item by sku",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1120, 200],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Build the QuickBooks Online invoice Line array from customer_pos.parsed_lines.\n// For each line, attach the stock_item.id if we looked one up; otherwise leave it\n// blank (so the stock-decrement step skips it).\n\nconst parsedLines = $json.parsed_lines || [];\nconst stockItemId = $json.stock_items_match_id || null;\n\nconst lineItems = parsedLines.map((line, i) => {\n // If only one line and we found stock_item_id, attach it; otherwise leave blank.\n const stockItemForLine = (parsedLines.length === 1 && stockItemId) ? stockItemId : null;\n return {\n DetailType: 'SalesItemLineDetail',\n Amount: line.line_total ?? (line.quantity * line.unit_price),\n Description: line.description || line.sku || '',\n SalesItemLineDetail: {\n Qty: line.quantity,\n UnitPrice: line.unit_price,\n // NOTE: replace 1 with your QB Item ID for JEI products. For now we hardcode '1'.\n ItemRef: { value: '1' },\n },\n };\n});\n\nreturn [{\n json: {\n qb_invoice_payload: {\n Line: lineItems,\n CustomerRef: { value: $json.expand?.customer?.id || '' },\n // Email the invoice to the customer's primary email\n BillEmail: { Address: $json.expand?.customer?.email || '' },\n // Currency from PO if set\n CurrencyRef: { value: $json.currency || 'ZAR' },\n DocNumber: $json.po_number || '',\n PrivateNote: `Auto-created from customer_po ${$json.id}`,\n },\n line_count: lineItems.length,\n }\n}];\n"
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-222222222206",
|
||||
"name": "Function: Build QB invoice payload",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1340, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "create",
|
||||
"additionalFields": {
|
||||
"line": "={{ $json.qb_invoice_payload.Line }}",
|
||||
"customerRef": "={{ $json.qb_invoice_payload.CustomerRef }}",
|
||||
"billEmail": "={{ $json.qb_invoice_payload.BillEmail }}",
|
||||
"currencyRef": "={{ $json.qb_invoice_payload.CurrencyRef }}",
|
||||
"docNumber": "={{ $json.qb_invoice_payload.DocNumber }}",
|
||||
"privateNote": "={{ $json.qb_invoice_payload.PrivateNote }}"
|
||||
}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-222222222207",
|
||||
"name": "QuickBooks: Create Invoice",
|
||||
"type": "n8n-nodes-base.quickbooks",
|
||||
"typeVersion": 1,
|
||||
"position": [1560, 200],
|
||||
"credentials": {
|
||||
"quickbooksOAuth2Api": {
|
||||
"id": "REPLACE_ME_qb_credential",
|
||||
"name": "JEISA QuickBooks Online"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://pb.jeistk.apphub.co.za/api/collections/invoices/records",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"customer\": \"{{ $('PB: Fetch customer_pos (status=parsed)').item.json.expand.customer.id }}\",\n \"source_customer_po\": \"{{ $('PB: Fetch customer_pos (status=parsed)').item.json.id }}\",\n \"invoice_number\": \"{{ $json.qb_invoice_payload.DocNumber || $json.Invoice.docNumber }}\",\n \"quickbooks_invoice_id\": \"{{ $json.Id || $json.Invoice.Id }}\",\n \"issue_date\": \"{{ $now.toISO() }}\",\n \"due_date\": \"{{ $now.plus({days: 30}).toISO() }}\",\n \"subtotal\": {{ $json.TotalAmt || 0 }},\n \"total\": {{ $json.TotalAmt || 0 }},\n \"currency\": \"{{ $('PB: Fetch customer_pos (status=parsed)').item.json.currency || 'ZAR' }}\",\n \"status\": \"draft\",\n \"qb_raw_response\": {{ JSON.stringify($json) }}\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-222222222208",
|
||||
"name": "PB: Create invoices record",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1780, 200],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "send",
|
||||
"invoiceId": "={{ $('QuickBooks: Create Invoice').item.json.Id }}",
|
||||
"email": "={{ $('PB: Fetch customer_pos (status=parsed)').item.json.expand.customer.email }}",
|
||||
"additionalFields": {}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-222222222209",
|
||||
"name": "QuickBooks: Send Invoice by email",
|
||||
"type": "n8n-nodes-base.quickbooks",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 200],
|
||||
"credentials": {
|
||||
"quickbooksOAuth2Api": {
|
||||
"id": "REPLACE_ME_qb_credential",
|
||||
"name": "JEISA QuickBooks Online"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "PATCH",
|
||||
"url": "=https://pb.jeistk.apphub.co.za/api/collections/invoices/records/{{ $('PB: Create invoices record').item.json.id }}",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"status\": \"sent\",\n \"emailed_to\": \"{{ $('PB: Fetch customer_pos (status=parsed)').item.json.expand.customer.email }}\",\n \"emailed_at\": \"{{ $now.toISO() }}\"\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-22222222220a",
|
||||
"name": "PB: Mark invoice sent",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [2220, 200],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "PATCH",
|
||||
"url": "=https://pb.jeistk.apphub.co.za/api/collections/customer_pos/records/{{ $('PB: Fetch customer_pos (status=parsed)').item.json.id }}",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"status\": \"invoiced\",\n \"linked_invoice\": \"{{ $('PB: Create invoices record').item.json.id }}\"\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-22222222220b",
|
||||
"name": "PB: Mark customer_pos invoiced",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [2440, 200],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://pb.jeistk.apphub.co.za/api/collections/invoice_items/records",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"invoice\": \"{{ $('PB: Create invoices record').item.json.id }}\",\n \"sku\": \"{{ $json.sku }}\",\n \"description\": \"{{ $json.description }}\",\n \"quantity\": {{ $json.quantity }},\n \"unit_price\": {{ $json.unit_price }},\n \"line_total\": {{ $json.line_total }}\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-22222222220c",
|
||||
"name": "PB: Create invoice_items rows",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [2660, 200],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Decrement stock for each parsed line that we successfully matched to a stock_item.\n// For multi-line POs without per-line SKU matching, only single-line POs will decrement.\n// You can extend this by adding per-line stock_item lookups.\n\nconst parsedLines = $('PB: Fetch customer_pos (status=parsed)').item.json.parsed_lines || [];\n\n// Build batch payload: [{ id, newQty }]\nconst decrements = [];\nfor (const line of parsedLines) {\n if (line.stock_item_id) {\n decrements.push({ id: line.stock_item_id, qty: line.quantity });\n }\n}\n\nreturn [{ json: { decrements } }];\n"
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-22222222220d",
|
||||
"name": "Function: Build stock decrement batch",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [2880, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://pb.jeistk.apphub.co.za/api/batch",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"batch\": [\n {{ $json.decrements.map(d => `{\"method\": \"PATCH\", \"url\": \"/api/collections/stock_items/records/${d.id}\", \"body\": {\"quantity\": \"__PLACEHOLDER__\"}}`).join(',\\\\n ') }}\n ]\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-22222222220e",
|
||||
"name": "PB: Batch decrement stock_items",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [3100, 200],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "PATCH",
|
||||
"url": "=https://pb.jeistk.apphub.co.za/api/collections/invoices/records/{{ $('PB: Create invoices record').item.json.id }}",
|
||||
"authentication": "predefinedCredentialType",
|
||||
"nodeCredentialType": "pocketbaseApi",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"stock_decremented\": true\n}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "22222222-2222-2222-2222-22222222220f",
|
||||
"name": "PB: Mark invoice stock_decremented",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [3320, 200],
|
||||
"credentials": {
|
||||
"pocketbaseApi": {
|
||||
"id": "REPLACE_ME_pb_credential",
|
||||
"name": "JEISA PocketBase"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Cron: every 2 minutes": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Fetch customer_pos (status=parsed)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"PB: Fetch customer_pos (status=parsed)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "If: any POs to invoice?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"If: any POs to invoice?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Loop: One PO at a time",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Loop: One PO at a time": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Lookup stock_item by sku",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[]
|
||||
]
|
||||
},
|
||||
"PB: Lookup stock_item by sku": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Function: Build QB invoice payload",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Function: Build QB invoice payload": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "QuickBooks: Create Invoice",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"QuickBooks: Create Invoice": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Create invoices record",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"PB: Create invoices record": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "QuickBooks: Send Invoice by email",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"QuickBooks: Send Invoice by email": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Mark invoice sent",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"PB: Mark invoice sent": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Mark customer_pos invoiced",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "PB: Create invoice_items rows",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"PB: Mark customer_pos invoiced": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Function: Build stock decrement batch",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"PB: Create invoice_items rows": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Mark invoice stock_decremented",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Function: Build stock decrement batch": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PB: Batch decrement stock_items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"PB: Batch decrement stock_items": {
|
||||
"main": [
|
||||
[]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"versionId": "1",
|
||||
"triggerCount": 1,
|
||||
"tags": [
|
||||
{
|
||||
"name": "jeisa",
|
||||
"id": "tag-jeisa"
|
||||
},
|
||||
{
|
||||
"name": "invoice",
|
||||
"id": "tag-invoice"
|
||||
},
|
||||
{
|
||||
"name": "quickbooks",
|
||||
"id": "tag-quickbooks"
|
||||
},
|
||||
{
|
||||
"name": "stock",
|
||||
"id": "tag-stock"
|
||||
}
|
||||
]
|
||||
}
|
||||
175
n8n/README.md
Normal file
175
n8n/README.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# JEISA Automation: Customer PO → Invoice → Stock
|
||||
|
||||
End-to-end pipeline that watches a JEI mailbox for incoming customer purchase
|
||||
orders, uploads them to PocketBase, creates QuickBooks invoices, emails them
|
||||
to customers, and decrements stock.
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||
│ Customer email │───▶│ n8N workflow │───▶│ PocketBase │ │ QuickBooks │
|
||||
│ (PO PDF) │ │ 01: email→PB │ │ • customer_pos │ │ Online │
|
||||
└─────────────────┘ └─────────────────┘ │ • parsed_lines │ └────────┬────────┘
|
||||
└──────────────────┘ │
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │
|
||||
│ Stock list │◀───│ n8N workflow │◀───│ status:parsed │◀────────────┘
|
||||
│ decremented │ │ 02: invoice+ │ └──────────────────┘ (status changes
|
||||
└─────────────────┘ │ stock │ to "parsed")
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
The AI agent you task can do all of this through the **PocketBase MCP server**
|
||||
in `.cursor/mcp.json` — you ask it to "create a customer PO" or "show me all
|
||||
pending invoices" and it talks to the same database.
|
||||
|
||||
---
|
||||
|
||||
## What's in this repo
|
||||
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `pocketbase/pb_migrations/1786200001_created_customer_pos.js` | New collection: incoming POs from customers |
|
||||
| `pocketbase/pb_migrations/1786200002_created_customer_po_lines.js` | New collection: parsed line items per PO |
|
||||
| `pocketbase/pb_migrations/1786200003_created_invoices.js` | New collection: invoices we issue (with QB id) |
|
||||
| `pocketbase/pb_migrations/1786200004_created_invoice_items.js` | New collection: line items per invoice |
|
||||
| `pocketbase/pb_migrations/1786200005_created_email_inbox.js` | New collection: every email the workflow sees |
|
||||
| `pocketbase/pb_migrations/1786200006_created_workflow_runs.js` | New collection: every n8N execution |
|
||||
| `.cursor/mcp.json` | Cursor MCP config — pocketbase-mcp-bridge |
|
||||
| `.mcp.json` | Same config, for Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`) |
|
||||
| `n8n/01-customer-po-to-parsed.json` | n8N workflow: email → upload PDF → parse lines |
|
||||
| `n8n/02-parsed-po-to-invoice.json` | n8N workflow: parsed → QuickBooks invoice → send → decrement stock |
|
||||
| `.env.example` | Template — copy to `.env`, fill in credentials |
|
||||
|
||||
All 6 migrations have been validated against the local PocketBase 0.30
|
||||
binary in this repo (`pocketbase/pocketbase`).
|
||||
|
||||
---
|
||||
|
||||
## Deploy: PocketBase
|
||||
|
||||
1. **Copy the new migration files** to your PB server's `pb_migrations/` dir.
|
||||
The next time your PB instance starts, it will apply them in order.
|
||||
(Tested locally: collections `customer_pos`, `customer_po_lines`,
|
||||
`invoices`, `invoice_items`, `email_inbox`, `workflow_runs` were created.)
|
||||
|
||||
2. **Verify** by hitting `GET /api/collections` with an admin token — you
|
||||
should see all six new collections listed.
|
||||
|
||||
3. **No data backfill is needed** — these are fresh tables.
|
||||
|
||||
4. **Existing customers collection** (`pbc_108570809`) is referenced by
|
||||
`customer_pos.customer` and `invoices.customer`. Make sure every customer
|
||||
has a valid `email` filled in — that's how the workflow matches incoming
|
||||
POs to customers.
|
||||
|
||||
---
|
||||
|
||||
## Deploy: MCP server for the AI agent
|
||||
|
||||
The MCP server (`pocketbase-mcp-bridge`) gives an AI agent (Claude, Cursor,
|
||||
etc.) full access to read/write PocketBase. This is what you use to "task
|
||||
a agent to create purchase orders for me."
|
||||
|
||||
1. Open `.cursor/mcp.json` and fill in the `REPLACE_ME_*` placeholders
|
||||
with the values from `.env` (`POCKETBASE_URL`, `POCKETBASE_ADMIN_EMAIL`,
|
||||
`POCKETBASE_ADMIN_PASSWORD`).
|
||||
|
||||
2. **Restart Cursor.** The MCP server will auto-install via `npx -y
|
||||
pocketbase-mcp-bridge` and authenticate as the superuser.
|
||||
|
||||
3. Open a new chat and ask the agent to "list the customer_pos records with
|
||||
status=new." It should call `list_records` and return the rows.
|
||||
|
||||
4. To create a PO via the agent, say: *"Create a customer PO for Isherwood
|
||||
Mining, PO number ISO-001, 10 × Drill Bit at R150 each."* The agent
|
||||
will call `create_record` on the `customer_pos` collection with the right
|
||||
shape.
|
||||
|
||||
Available tools (55 total, see
|
||||
[github.com/nestebe/pocketbase-mcp](https://github.com/nestebe/pocketbase-mcp)
|
||||
for the full list):
|
||||
|
||||
- `list_records`, `get_record`, `create_record`, `update_record`,
|
||||
`delete_record`, `get_first_record`
|
||||
- `list_collections`, `get_collection`, `create_collection`,
|
||||
`update_collection`, `delete_collection`
|
||||
- `auth_with_password`, `list_auth_methods`
|
||||
- `list_logs`, `get_settings`, `list_backups`
|
||||
- `send_raw_request` — call any endpoint (e.g. PB hooks, custom routes)
|
||||
|
||||
---
|
||||
|
||||
## Deploy: n8N workflows
|
||||
|
||||
1. **Install n8N** (if you haven't already):
|
||||
```bash
|
||||
npm install -g n8n
|
||||
# or run via Docker: docker run -it --rm -p 5678:5678 n8nio/n8n
|
||||
```
|
||||
|
||||
2. **Import the workflows**: in the n8N UI, *Workflows → Import from File*
|
||||
and select:
|
||||
- `n8n/01-customer-po-to-parsed.json`
|
||||
- `n8n/02-parsed-po-to-invoice.json`
|
||||
|
||||
3. **Create credentials** for each node that has a `REPLACE_ME_*`:
|
||||
- **IMAP** credential → your `orders@jeisa.co.za` mailbox
|
||||
- **PocketBase API** credential → URL + admin email/password from `.env`
|
||||
- **QuickBooks OAuth2 API** credential → connect your QBO account
|
||||
|
||||
4. **Activate the workflows** in the n8N UI (toggle "Active" in the top
|
||||
right of each).
|
||||
|
||||
---
|
||||
|
||||
## End-to-end test
|
||||
|
||||
1. Send a test email from your personal account to `orders@jeisa.co.za`.
|
||||
Subject: "PO test — please ignore."
|
||||
Attach a fake PDF with one or two lines like:
|
||||
```
|
||||
AB-123 Test Widget 10 100.00 1000.00
|
||||
```
|
||||
|
||||
2. Workflow 01 should fire within ~30s:
|
||||
- A `customer_pos` record is created
|
||||
- A `customer_pos_line` record is created
|
||||
- `status` becomes `parsed`
|
||||
|
||||
3. Workflow 02 should fire within ~2 minutes:
|
||||
- A QuickBooks invoice is created (sandbox)
|
||||
- An `invoices` record is created with `status=sent`
|
||||
- The `stock_items` quantity is decremented (only if the SKU matches a
|
||||
stock row — wire up more SKUs in the `stock_items` collection)
|
||||
- `customer_pos.status` becomes `invoiced`
|
||||
|
||||
4. Check `workflow_runs` and `email_inbox` in PB for the audit trail.
|
||||
|
||||
---
|
||||
|
||||
## Customizing
|
||||
|
||||
| Want to | Edit |
|
||||
|---|---|
|
||||
| Add fields to the email parser (e.g. delivery date) | `n8n/01-…json` → "Function: Parse lines from PDF text" |
|
||||
| Switch to AI-powered PDF parsing (for scanned POs) | Add an "OpenAI" node between "Extract PDF text" and "Function: Parse lines" |
|
||||
| Email the invoice from JEI's SMTP (not QB) | Replace "QuickBooks: Send Invoice by email" with the Send Email node |
|
||||
| Use a different QuickBooks Item ID for SKUs | "Function: Build QB invoice payload" → `ItemRef.value` |
|
||||
| Only auto-invoice certain customers | Add an `auto_invoice` boolean to `customers`, then add a filter in workflow 02 |
|
||||
| Sync QuickBooks payments back | Add a third workflow that polls QB Invoice.Status every hour and updates `invoices.status` |
|
||||
|
||||
---
|
||||
|
||||
## Important: confirm before making this live
|
||||
|
||||
Before going live, you'll want a human approval step between workflow 01
|
||||
and 02 — the current design auto-invoices everything that looks like a PO.
|
||||
Add a `status: "parsed_pending_review"` and require a human to flip it to
|
||||
`"parsed"` to make this safer. The simplest way:
|
||||
|
||||
1. In `01-…json`, change the final node to set `status = "parsed_pending_review"`.
|
||||
2. In `02-…json`, change the filter from `status = "parsed"` to
|
||||
`status = "parsed_pending_review"`.
|
||||
3. Add a tiny approval UI in the React app (a button on the customer_pos
|
||||
row that PATCHes the status to "parsed").
|
||||
|
||||
This stops random emails from triggering invoices.
|
||||
236
pocketbase/pb_migrations/1786200001_created_customer_pos.js
Normal file
236
pocketbase/pb_migrations/1786200001_created_customer_pos.js
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// =========================================================================
|
||||
// customer_pos — incoming POs from customers, received via email
|
||||
// =========================================================================
|
||||
//
|
||||
// This is what the n8N IMAP workflow creates when a customer emails a
|
||||
// purchase order PDF to the JEI orders inbox. The PDF is stored in `po_pdf`
|
||||
// and parsed line items go into `customer_po_lines`.
|
||||
//
|
||||
// NOTE on naming: the existing `orders` collection (pbc_3527180448) is the
|
||||
// operational / sales order system (fulfillment, drivers, deliveries). This
|
||||
// collection is INCOMING POs from clients — different lifecycle, different
|
||||
// fields, different ownership. Hence the distinct name `customer_pos`.
|
||||
//
|
||||
// Status lifecycle:
|
||||
// new → email received, PDF saved, not yet inspected
|
||||
// parsed → line items extracted, ready for invoicing
|
||||
// invoiced → linked invoice created and sent
|
||||
// cancelled → ignored (wrong sender / duplicate)
|
||||
// error → workflow failed, see `error_message`
|
||||
// =========================================================================
|
||||
|
||||
migrate((app) => {
|
||||
const collection = new Collection({
|
||||
"createRule": "@request.auth.id != ''",
|
||||
"deleteRule": "@request.auth.id != ''",
|
||||
"listRule": "@request.auth.id != ''",
|
||||
"name": "customer_pos",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": "@request.auth.id != ''",
|
||||
"viewRule": "@request.auth.id != ''",
|
||||
"fields": [
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_108570809",
|
||||
"hidden": false,
|
||||
"id": "rel_cpo_customer",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "customer",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_cpo_po_number",
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"name": "po_number",
|
||||
"pattern": "",
|
||||
"presentable": true,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "file_cpo_po_pdf",
|
||||
"maxSelect": 10,
|
||||
"maxSize": 20971520,
|
||||
"mimeTypes": ["application/pdf", "image/png", "image/jpeg"],
|
||||
"name": "po_pdf",
|
||||
"presentable": false,
|
||||
"protected": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"thumbs": [],
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_cpo_email_from",
|
||||
"max": 500,
|
||||
"min": 0,
|
||||
"name": "source_email_from",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_cpo_email_subject",
|
||||
"max": 500,
|
||||
"min": 0,
|
||||
"name": "source_email_subject",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "dt_cpo_email_received",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "source_email_received_at",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_cpo_email_uid",
|
||||
"max": 200,
|
||||
"min": 0,
|
||||
"name": "source_email_uid",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "sel_cpo_status",
|
||||
"maxSelect": 1,
|
||||
"name": "status",
|
||||
"presentable": true,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": ["new", "parsed", "invoiced", "cancelled", "error"]
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": "pbc_1342968361",
|
||||
"hidden": false,
|
||||
"id": "rel_cpo_purchase_order",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "matched_purchase_order",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json_cpo_parsed_lines",
|
||||
"maxSize": 200000,
|
||||
"name": "parsed_lines",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "num_cpo_total",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "total_amount",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_cpo_currency",
|
||||
"max": 10,
|
||||
"min": 0,
|
||||
"name": "currency",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_cpo_notes",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "notes",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "dt_cpo_processed",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "processed_at",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_cpo_error",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "error_message",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"indexes": []
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("customer_pos");
|
||||
return app.delete(collection);
|
||||
})
|
||||
161
pocketbase/pb_migrations/1786200002_created_customer_po_lines.js
Normal file
161
pocketbase/pb_migrations/1786200002_created_customer_po_lines.js
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// =========================================================================
|
||||
// customer_po_lines — one record per line item on an incoming customer PO
|
||||
// =========================================================================
|
||||
//
|
||||
// Splits the `parsed_lines` json blob on a `customer_pos` record into
|
||||
// individual rows. n8N writes these after extracting SKU, qty, price from
|
||||
// the customer's PDF. The stock-decrement node reads from here.
|
||||
//
|
||||
// Note: `app.findCollectionByNameOrId` is wrapped so this migration applies
|
||||
// even if `stock_items` isn't present in the local DB. In production (hosted
|
||||
// PB), `customer_pos` and `stock_items` both exist.
|
||||
// =========================================================================
|
||||
|
||||
migrate((app) => {
|
||||
const safeFind = (name) => {
|
||||
try { return app.findCollectionByNameOrId(name); }
|
||||
catch (e) { return null; }
|
||||
};
|
||||
|
||||
const cpoCol = safeFind("customer_pos");
|
||||
if (!cpoCol) {
|
||||
throw new Error("Required collection 'customer_pos' not found — apply migrations in order.");
|
||||
}
|
||||
const stockItemsCol = safeFind("stock_items");
|
||||
|
||||
const schema = [
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": cpoCol.id,
|
||||
"hidden": false,
|
||||
"id": "rel_cpol_cpo",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 1,
|
||||
"name": "customer_po",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_cpol_sku",
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"name": "sku",
|
||||
"pattern": "",
|
||||
"presentable": true,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_cpol_description",
|
||||
"max": 500,
|
||||
"min": 0,
|
||||
"name": "description",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "num_cpol_qty",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "quantity",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "num_cpol_unit_price",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "unit_price",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "num_cpol_line_total",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "line_total",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "bool_cpol_stock_decremented",
|
||||
"name": "stock_decremented",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "dt_cpol_stock_decremented",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "stock_decremented_at",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
}
|
||||
];
|
||||
|
||||
if (stockItemsCol) {
|
||||
schema.splice(1, 0, {
|
||||
"cascadeDelete": false,
|
||||
"collectionId": stockItemsCol.id,
|
||||
"hidden": false,
|
||||
"id": "rel_cpol_stock",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "stock_item",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
});
|
||||
}
|
||||
|
||||
const collection = new Collection({
|
||||
"createRule": "@request.auth.id != ''",
|
||||
"deleteRule": "@request.auth.id != ''",
|
||||
"listRule": "@request.auth.id != ''",
|
||||
"name": "customer_po_lines",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": "@request.auth.id != ''",
|
||||
"viewRule": "@request.auth.id != ''",
|
||||
"fields": schema,
|
||||
"indexes": []
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("customer_po_lines");
|
||||
return app.delete(collection);
|
||||
})
|
||||
268
pocketbase/pb_migrations/1786200003_created_invoices.js
Normal file
268
pocketbase/pb_migrations/1786200003_created_invoices.js
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// =========================================================================
|
||||
// invoices — invoices we issue to customers (created by n8N from QuickBooks)
|
||||
// =========================================================================
|
||||
//
|
||||
// One record per invoice. Holds the QuickBooks invoice id so we can link
|
||||
// back to the source of truth in QB and also audit what we emailed.
|
||||
//
|
||||
// Status lifecycle:
|
||||
// draft → created in QB but not emailed yet
|
||||
// sent → emailed to customer
|
||||
// paid → QB reports paid (synced back via webhook / poll)
|
||||
// overdue → past due date and unpaid
|
||||
// voided → manually cancelled
|
||||
// =========================================================================
|
||||
|
||||
migrate((app) => {
|
||||
// look up related collections by name (their IDs are auto-generated by PB)
|
||||
const customersCol = app.findCollectionByNameOrId("customers");
|
||||
const customerPosCol = app.findCollectionByNameOrId("customer_pos");
|
||||
|
||||
const collection = new Collection({
|
||||
"createRule": "@request.auth.id != ''",
|
||||
"deleteRule": "@request.auth.id != ''",
|
||||
"listRule": "@request.auth.id != ''",
|
||||
"name": "invoices",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": "@request.auth.id != ''",
|
||||
"viewRule": "@request.auth.id != ''",
|
||||
"fields": [
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": customersCol.id,
|
||||
"hidden": false,
|
||||
"id": "rel_inv_customer",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 1,
|
||||
"name": "customer",
|
||||
"presentable": true,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": customerPosCol.id,
|
||||
"hidden": false,
|
||||
"id": "rel_inv_source_order",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "source_customer_po",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inv_number",
|
||||
"max": 50,
|
||||
"min": 0,
|
||||
"name": "invoice_number",
|
||||
"pattern": "",
|
||||
"presentable": true,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inv_qb_id",
|
||||
"max": 50,
|
||||
"min": 0,
|
||||
"name": "quickbooks_invoice_id",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "dt_inv_issue_date",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "issue_date",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "dt_inv_due_date",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "due_date",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "num_inv_subtotal",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "subtotal",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "num_inv_tax",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "tax",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "num_inv_total",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "total",
|
||||
"onlyInt": true,
|
||||
"presentable": true,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inv_currency",
|
||||
"max": 10,
|
||||
"min": 0,
|
||||
"name": "currency",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "sel_inv_status",
|
||||
"maxSelect": 1,
|
||||
"name": "status",
|
||||
"presentable": true,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": ["draft", "sent", "paid", "overdue", "voided"]
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "file_inv_pdf",
|
||||
"maxSelect": 1,
|
||||
"maxSize": 20971520,
|
||||
"mimeTypes": ["application/pdf"],
|
||||
"name": "invoice_pdf",
|
||||
"presentable": false,
|
||||
"protected": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"thumbs": [],
|
||||
"type": "file"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inv_emailed_to",
|
||||
"max": 500,
|
||||
"min": 0,
|
||||
"name": "emailed_to",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "dt_inv_emailed_at",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "emailed_at",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inv_email_message_id",
|
||||
"max": 200,
|
||||
"min": 0,
|
||||
"name": "email_message_id",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "bool_inv_stock_decremented",
|
||||
"name": "stock_decremented",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inv_qb_response",
|
||||
"max": 50000,
|
||||
"min": 0,
|
||||
"name": "qb_raw_response",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inv_notes",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "notes",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"indexes": []
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("invoices");
|
||||
return app.delete(collection);
|
||||
})
|
||||
162
pocketbase/pb_migrations/1786200004_created_invoice_items.js
Normal file
162
pocketbase/pb_migrations/1786200004_created_invoice_items.js
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// =========================================================================
|
||||
// invoice_items — line items on an issued invoice
|
||||
// =========================================================================
|
||||
//
|
||||
// Mirror of `customer_po_lines` but bound to the issued `invoices` record.
|
||||
// After the n8N workflow emails the invoice, it loops over these and
|
||||
// decrements stock_items.quantity by `quantity` (skipping rows where
|
||||
// `stock_item` is empty — those are service / non-inventory lines).
|
||||
// =========================================================================
|
||||
//
|
||||
// Note: `app.findCollectionByNameOrId` is wrapped so this migration applies
|
||||
// even if the parent collection isn't present yet in a partial local DB.
|
||||
// In production (hosted PB), `invoices`, `stock_items`, `customer_po_lines`
|
||||
// all exist.
|
||||
// =========================================================================
|
||||
|
||||
migrate((app) => {
|
||||
const safeFind = (name) => {
|
||||
try { return app.findCollectionByNameOrId(name); }
|
||||
catch (e) { return null; }
|
||||
};
|
||||
|
||||
const invoicesCol = safeFind("invoices");
|
||||
const stockItemsCol = safeFind("stock_items");
|
||||
const cpoLinesCol = safeFind("customer_po_lines");
|
||||
|
||||
if (!invoicesCol) {
|
||||
throw new Error("Required collection 'invoices' not found — apply migrations in order.");
|
||||
}
|
||||
|
||||
const schema = [
|
||||
{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": invoicesCol.id,
|
||||
"hidden": false,
|
||||
"id": "rel_invl_invoice",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 1,
|
||||
"name": "invoice",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_invl_sku",
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"name": "sku",
|
||||
"pattern": "",
|
||||
"presentable": true,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_invl_description",
|
||||
"max": 500,
|
||||
"min": 0,
|
||||
"name": "description",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "num_invl_qty",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "quantity",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "num_invl_unit_price",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "unit_price",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "num_invl_line_total",
|
||||
"max": null,
|
||||
"min": null,
|
||||
"name": "line_total",
|
||||
"onlyInt": false,
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "number"
|
||||
}
|
||||
];
|
||||
|
||||
if (stockItemsCol) {
|
||||
schema.splice(1, 0, {
|
||||
"cascadeDelete": false,
|
||||
"collectionId": stockItemsCol.id,
|
||||
"hidden": false,
|
||||
"id": "rel_invl_stock",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "stock_item",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
});
|
||||
}
|
||||
|
||||
if (cpoLinesCol) {
|
||||
schema.splice(stockItemsCol ? 2 : 1, 0, {
|
||||
"cascadeDelete": false,
|
||||
"collectionId": cpoLinesCol.id,
|
||||
"hidden": false,
|
||||
"id": "rel_invl_cpol",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "source_po_line",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
});
|
||||
}
|
||||
|
||||
const collection = new Collection({
|
||||
"createRule": "@request.auth.id != ''",
|
||||
"deleteRule": "@request.auth.id != ''",
|
||||
"listRule": "@request.auth.id != ''",
|
||||
"name": "invoice_items",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": "@request.auth.id != ''",
|
||||
"viewRule": "@request.auth.id != ''",
|
||||
"fields": schema,
|
||||
"indexes": []
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("invoice_items");
|
||||
return app.delete(collection);
|
||||
})
|
||||
175
pocketbase/pb_migrations/1786200005_created_email_inbox.js
Normal file
175
pocketbase/pb_migrations/1786200005_created_email_inbox.js
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// =========================================================================
|
||||
// email_inbox — every email the n8N IMAP trigger sees, audit trail
|
||||
// =========================================================================
|
||||
//
|
||||
// Even emails we ignore (no PDF, wrong sender, auto-reply, bounce) get a
|
||||
// record here so we have a paper trail. n8N writes one row per email before
|
||||
// branching on whether to process it.
|
||||
// =========================================================================
|
||||
|
||||
migrate((app) => {
|
||||
const cpoCol = app.findCollectionByNameOrId("customer_pos");
|
||||
|
||||
const collection = new Collection({
|
||||
"createRule": "@request.auth.id != ''",
|
||||
"deleteRule": "@request.auth.id != ''",
|
||||
"listRule": "@request.auth.id != ''",
|
||||
"name": "email_inbox",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": "@request.auth.id != ''",
|
||||
"viewRule": "@request.auth.id != ''",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inbox_uid",
|
||||
"max": 200,
|
||||
"min": 0,
|
||||
"name": "imap_uid",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inbox_from",
|
||||
"max": 500,
|
||||
"min": 0,
|
||||
"name": "from",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inbox_subject",
|
||||
"max": 500,
|
||||
"min": 0,
|
||||
"name": "subject",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inbox_to",
|
||||
"max": 500,
|
||||
"min": 0,
|
||||
"name": "to",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inbox_body",
|
||||
"max": 200000,
|
||||
"min": 0,
|
||||
"name": "body_plain",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "dt_inbox_received",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "received_at",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "bool_inbox_has_attachments",
|
||||
"name": "has_attachments",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inbox_attachment_names",
|
||||
"max": 2000,
|
||||
"min": 0,
|
||||
"name": "attachment_names",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "sel_inbox_disposition",
|
||||
"maxSelect": 1,
|
||||
"name": "disposition",
|
||||
"presentable": true,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": ["processed", "ignored", "duplicate", "error", "manual_review"]
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": cpoCol.id,
|
||||
"hidden": false,
|
||||
"id": "rel_inbox_cpo",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "linked_customer_po",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_inbox_error",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "error_message",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"indexes": []
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("email_inbox");
|
||||
return app.delete(collection);
|
||||
})
|
||||
155
pocketbase/pb_migrations/1786200006_created_workflow_runs.js
Normal file
155
pocketbase/pb_migrations/1786200006_created_workflow_runs.js
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/// <reference path="../pb_data/types.d.ts" />
|
||||
|
||||
// =========================================================================
|
||||
// workflow_runs — history of every n8N execution
|
||||
// =========================================================================
|
||||
//
|
||||
// n8N POSTs to a PocketBase endpoint once per execution. We store the run id,
|
||||
// workflow name, status, inputs/outputs (json), and any error. This lets us
|
||||
// see in the dashboard what happened last time, even when n8N's UI isn't open.
|
||||
// =========================================================================
|
||||
|
||||
migrate((app) => {
|
||||
const cpoCol = app.findCollectionByNameOrId("customer_pos");
|
||||
const invoicesCol = app.findCollectionByNameOrId("invoices");
|
||||
|
||||
const collection = new Collection({
|
||||
"createRule": "@request.auth.id != ''",
|
||||
"deleteRule": "@request.auth.id != ''",
|
||||
"listRule": "@request.auth.id != ''",
|
||||
"name": "workflow_runs",
|
||||
"system": false,
|
||||
"type": "base",
|
||||
"updateRule": "@request.auth.id != ''",
|
||||
"viewRule": "@request.auth.id != ''",
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_wfr_n8n_id",
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"name": "n8n_execution_id",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_wfr_workflow",
|
||||
"max": 200,
|
||||
"min": 0,
|
||||
"name": "workflow_name",
|
||||
"pattern": "",
|
||||
"presentable": true,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "sel_wfr_status",
|
||||
"maxSelect": 1,
|
||||
"name": "status",
|
||||
"presentable": true,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "select",
|
||||
"values": ["running", "success", "failed", "cancelled"]
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "dt_wfr_started",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "started_at",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "dt_wfr_finished",
|
||||
"max": "",
|
||||
"min": "",
|
||||
"name": "finished_at",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json_wfr_input",
|
||||
"maxSize": 200000,
|
||||
"name": "input_json",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json_wfr_output",
|
||||
"maxSize": 500000,
|
||||
"name": "output_json",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "txt_wfr_error",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "error_message",
|
||||
"pattern": "",
|
||||
"presentable": false,
|
||||
"primaryKey": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": cpoCol.id,
|
||||
"hidden": false,
|
||||
"id": "rel_wfr_cpo",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "related_customer_po",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
},
|
||||
{
|
||||
"cascadeDelete": false,
|
||||
"collectionId": invoicesCol.id,
|
||||
"hidden": false,
|
||||
"id": "rel_wfr_invoice",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "related_invoice",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}
|
||||
],
|
||||
"indexes": []
|
||||
});
|
||||
|
||||
return app.save(collection);
|
||||
}, (app) => {
|
||||
const collection = app.findCollectionByNameOrId("workflow_runs");
|
||||
return app.delete(collection);
|
||||
})
|
||||
Loading…
Reference in a new issue