Author: applegater

  • Workflow: Anthem Network uptime Notifications

    Monitoring Source:
    This workflow is triggered by webhook events sent from Uptime Kuma, a self-hosted monitoring system. When a monitored service changes state (UP or DOWN), Uptime Kuma sends a JSON payload to this webhook endpoint, which then processes the data and forwards formatted notifications to Slack.

    Purpose: Receive Uptime Kuma status webhooks and post Slack alerts for DOWN and UP events.


    Nodes

    1) Webhook

    • Type: Webhook (Trigger)
    • What it’s for: Entry point that receives the POST request from Uptime Kuma.
    • Endpoint path: POST /webhook/Anthemnetworkstatus
    • Expected payload: JSON with body.heartbeat and body.monitor

    2) Code in JavaScript

    • Type: Code
    • What it’s for: Converts the raw Uptime Kuma webhook payload into clean fields used by the rest of the workflow.
    • Key logic:
      • Reads:
    - `body.heartbeat` (status, ping, time, msg)
    - `body.monitor` (name, hostname/url/type)
    
    • Creates:
    - `isDown` → `true` when `heartbeat.status === 0` (Kuma: 0=DOWN, 1=UP)
    - `name` → monitor name
    - `hostnameOrURL` → monitor hostname/url fallback
    - `time` → heartbeat time (fallback: now)
    - `status` → `"Down"` or `"Up"`
    - `msg` → human readable summary string
    
    • Also passes through:
    - `heartbeat`
    - `monitor`
    

    3) If

    • Type: IF (Condition)
    • What it’s for: Routes the workflow based on the status.
    • Condition:{{$json.isDown}} is true
      • True path (DOWN) → goes to Only if go Down
      • False path (UP) → goes to Only if go up1

    4) Only if go Down

    • Type: HTTP Request (POST to Slack Incoming Webhook)
    • What it’s for: Sends a DOWN alert to Slack when isDown=true.
    • Message formatting:
      • Title/header: “🚨 Service is DOWN”
      • Color: #e01e5a (red)
      • Includes fields: Service, Status, Target, Time
      • Includes details from heartbeat message / built msg

    5) Only if go up1

    • Type: HTTP Request (POST to Slack Incoming Webhook)
    • What it’s for: Sends an UP alert to Slack when isDown=false.
    • Message formatting:
      • Title/header: “✅ Service is UP”
      • Color: #2eb886 (green)
      • Includes fields: Service, Status, Target, Time
      • Includes message block with the formatted msg

    Connections (flow)

    Webhook → Code in JavaScript → If →

    • True (DOWN) → Only if go Down
    • False (UP) → Only if go up1
  • Working Setup: Caddy (public HTTPS) → Mailu Front (internal HTTPS) + Mailu SMTP/IMAP uses same Sectigo cert

    Goal

    • https://my.richardapplegate.io/webmail and /admin work
    • Caddy serves the site on public port 443
    • Mailu also serves HTTPS internally on front:443
    • SMTP/IMAP ports (465/993/995/587) use the same Sectigo wildcard cert
    • No port conflicts and no redirect loops

    1) Certificates on the host

    You have these two files (already confirmed they match):

    • fullchain.pem
    • privkey.pem

    Store them in a stable host path, example:

    • /mnt/volumes/certs/fullchain.pem
    • /mnt/volumes/certs/privkey.pem

    ✅ Verified match test:

    openssl x509 -noout -modulus -in fullchain.pem | openssl md5
    openssl pkey -noout -modulus -in privkey.pem  | openssl md5
    

    2) Caddy docker-compose

    Caddy publishes public ports and has the certs mounted:

    services:
      caddy:
        image: caddy:2
        restart: unless-stopped
        networks:
          - caddy
        ports:
          - "80:80"
          - "443:443/tcp"
          - "443:443/udp"
        volumes:
          - ./Caddyfile:/etc/caddy/Caddyfile:ro
          - ./data:/data
          - ./config:/config
          - /mnt/volumes/certs:/certs:ro
    networks:
      caddy:
        external: true
    

    3) Mailu env (mailu.env) — corrected values

    These are the critical “must be right” ones:

    DOMAIN=richardapplegate.io
    HOSTNAMES=my,mail
    
    PORTS=25,465,587,993,995,4190
    
    TLS_FLAVOR=cert
    TLS_CERT_FILENAME=fullchain.pem
    TLS_KEYPAIR_FILENAME=privkey.pem
    
    WEB_ADMIN=/admin
    WEB_WEBMAIL=/webmail
    WEBSITE=https://my.richardapplegate.io
    

    ✅ Important notes:

    • DOMAIN is the apex domain: richardapplegate.io
    • HOSTNAMES are labels only: my,mail (no dots)
    • TLS_FLAVOR is cert (not certs)
    • Do not include 80 or 443 in PORTS (Caddy owns those publicly)

    4) Mailu docker-compose networking + cert mounts

    4.1 Attach front to BOTH networks

    Mailu internal network (for smtp/imap/etc) and the caddy network (so Caddy can reach it):

    services:
      front:
        networks:
          - mailu
          - caddy
    

    4.2 Mount certs into Mailu containers that use them

    At minimum, mount into front, smtp, and imap (names may vary, but these are typical):

    services:
      front:
        volumes:
          - /mnt/volumes/certs/fullchain.pem:/certs/fullchain.pem:ro
          - /mnt/volumes/certs/privkey.pem:/certs/privkey.pem:ro
    
      smtp:
        volumes:
          - /mnt/volumes/certs/fullchain.pem:/certs/fullchain.pem:ro
          - /mnt/volumes/certs/privkey.pem:/certs/privkey.pem:ro
    
      imap:
        volumes:
          - /mnt/volumes/certs/fullchain.pem:/certs/fullchain.pem:ro
          - /mnt/volumes/certs/privkey.pem:/certs/privkey.pem:ro
    

    (These filenames match your TLS_CERT_FILENAME / TLS_KEYPAIR_FILENAME.)


    5) Caddyfile (the working site)

    You terminate TLS at Caddy and proxy to Mailu front over internal HTTPS to avoid redirect loops:

    my.richardapplegate.io {
      tls /certs/fullchain.pem /certs/privkey.pem
    
      # optional: make / go to webmail
      @root path /
      redir @root /webmail 302
    
      reverse_proxy https://front:443 {
        transport http {
          tls_server_name my.richardapplegate.io
          # Only if chain issues ever happen:
          # tls_insecure_skip_verify
        }
    
        header_up Host {host}
        header_up X-Forwarded-Proto {scheme}
        header_up X-Forwarded-Host  {host}
        header_up X-Forwarded-For   {remote_host}
      }
    }
    

    ✅ Why HTTPS upstream (front:443)?
    Because with TLS_FLAVOR=cert, Mailu front enforces HTTPS; proxying to http://front:80 triggers redirect loops. HTTPS upstream avoids that entirely.


    6) Restart order (clean rebuild)

    6.1 Restart Mailu (force recreate so config regenerates)

    cd /mnt/volumes/SamsungSSD970EVOPlus2TB/mailu
    docker compose down
    docker compose up -d --force-recreate
    

    6.2 Reload Caddy

    docker exec caddy caddy reload --config /etc/caddy/Caddyfile
    

    7) Verification tests

    7.1 Web cert (public 443 via Caddy)

    openssl s_client -connect my.richardapplegate.io:443 -servername my.richardapplegate.io </dev/null 2>/dev/null \
    | openssl x509 -noout -subject -issuer -dates
    

    7.2 Mailu front cert (internal, from inside caddy container)

    docker exec caddy sh -lc \
    'openssl s_client -connect front:443 -servername my.richardapplegate.io </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates'
    

    7.3 SMTP TLS cert (465)

    openssl s_client -connect my.richardapplegate.io:465 -servername my.richardapplegate.io </dev/null 2>/dev/null \
    | openssl x509 -noout -subject -issuer -dates
    

    7.4 IMAPS cert (993)

    openssl s_client -connect my.richardapplegate.io:993 -servername my.richardapplegate.io </dev/null 2>/dev/null \
    | openssl x509 -noout -subject -issuer -dates
    

    Expected: all show CN=*.richardapplegate.io and Sectigo issuer.

  • How to Send Slack Notifications for Material Issue, Material Transfer, and Purchase Receipt in ERPNext v15 (With Error Fix)

    If you’re using ERPNext v15 and want Slack notifications when stock movements happen, this guide walks you through:

    • Sending Slack alerts for:
      • Material Issue
      • Material Transfer
      • Purchase Receipt
    • Avoiding the common Jinja error: TypeError: 'builtin_function_or_method' object is not iterable

    This method uses ERPNext’s built-in Webhook DocType and Slack Incoming Webhooks.


    Step 1 — Create a Slack Incoming Webhook

    1. Go to https://api.slack.com/apps
    2. Create a new app (or use an existing one).
    3. Enable Incoming Webhooks.
    4. Click Add New Webhook to Workspace.
    5. Choose your channel.
    6. Copy the generated Webhook URL.

    Important:
    Treat this URL like a password. Do not publish or share it publicly.


    Step 2 — Create Webhook for Stock Entry (Material Issue + Transfer)

    In ERPNext:

    Go to:

    Search → Webhook → New

    Configure:

    Document Type:

    Stock Entry
    

    Doc Event:

    on_submit
    

    Request Method:

    POST
    

    Request URL:
    Paste your Slack webhook URL

    Enabled:
    Checked


    Add Condition (Important)

    Only trigger for Material Issue and Material Transfer:

    doc.stock_entry_type in ("Material Issue", "Material Transfer")
    

    Why this matters:
    Stock Entry is one DocType, but it has multiple types. This prevents Slack spam from other stock movements.


    Add Header

    Add one row:

    KeyValue
    Content-Typeapplication/json

    Step 3 — Correct JSON Payload (Avoid the Common Error)

    Many users encounter this error:

    TypeError: 'builtin_function_or_method' object is not iterable
    

    This happens because in Webhooks, doc is treated as a dictionary.
    Using doc.items conflicts with Python’s dict.items() method.

    ❌ Wrong (causes error)

    {% for i in doc.items %}
    

    ✅ Correct

    {% for i in doc.get('items', []) %}
    

    Use This Working JSON Payload (Stock Entry)

    {
      "text": "📦 *Stock Entry Submitted*\n*Type:* {{ doc.get('stock_entry_type', '—') }}\n*ID:* {{ doc.get('name', '—') }}\n*Company:* {{ doc.get('company', '—') }}\n*Posting:* {{ doc.get('posting_date', '—') }} {{ doc.get('posting_time', '') }}\n*From WH:* {{ doc.get('from_warehouse') or '—' }}\n*To WH:* {{ doc.get('to_warehouse') or '—' }}\n\n*Items:*\n{% for i in doc.get('items', []) %}• {{ i.get('item_code', '—') }} — Qty: {{ i.get('qty', 0) }} {{ i.get('uom', '') }} ({{ i.get('s_warehouse') or i.get('t_warehouse') or '—' }})\n{% endfor %}"
    }
    

    Step 4 — Create Separate Webhook for Purchase Receipt

    Important:
    Purchase Receipt is NOT a Stock Entry Type.
    It is a separate DocType.

    So you must create a second webhook.


    Create New Webhook

    Document Type:

    Purchase Receipt
    

    Doc Event:

    on_submit
    

    Request Method:

    POST
    

    Request URL:
    Slack webhook URL

    Header:

    Content-Type: application/json
    

    No condition required unless you want to filter by supplier or company.


    Working JSON Payload (Purchase Receipt)

    {
      "text": "🧾 *Purchase Receipt Submitted*\n*ID:* {{ doc.get('name', '—') }}\n*Supplier:* {{ doc.get('supplier', '—') }}\n*Company:* {{ doc.get('company', '—') }}\n*Posting:* {{ doc.get('posting_date', '—') }} {{ doc.get('posting_time', '') }}\n\n*Items Received:*\n{% for i in doc.get('items', []) %}• {{ i.get('item_code', '—') }} — Qty: {{ i.get('qty', 0) }} {{ i.get('uom', '') }} ({{ i.get('warehouse') or '—' }})\n{% endfor %}"
    }
    

    Step 5 — Testing

    1. Create a Material Transfer.
    2. Submit it.
    3. Check Slack channel.

    Then test:

    • Material Issue
    • Purchase Receipt

    If nothing appears:

    Check Logs (Docker)

    docker compose logs -f backend
    

    Or inside container:

    bench --site yoursite tail
    

    Optional Enhancement — Add Clickable ERP Link

    If your ERP URL is:

    https://erp.yourdomain.com
    

    You can add this inside your Slack message:

    https://erp.yourdomain.com/app/stock-entry/{{ doc.get('name') }}
    

    Slack will auto-link it.


    Why This Fix Works

    In Webhooks:

    • doc behaves as a Python dictionary.
    • doc.items conflicts with dictionary method .items().
    • Using doc.get('items', []) safely retrieves the child table list.

    This prevents:

    TypeError: 'builtin_function_or_method' object is not iterable
    

    Final Result

    You now have Slack alerts for:

    • Material Issue
    • Material Transfer
    • Purchase Receipt

    Triggered automatically on submit, with item-level detail

  • SOP: Manage “What Link to Order More” (Amazon Link) on Item in ERPNext


    Purpose

    Ensure each Item has a single “order link” that:

    • Can be created/updated by Support Team
    • Can be set once by everyone else, then becomes locked (cannot be changed or removed)

    Field used:

    • Fieldname: custom_what_link_to_order_more

    Scope

    Applies to:

    • Item Master data maintenance
    • Purchasing / replenishment workflow
    • Support Team data governance

    SOP 1 — Add or Update the Amazon Link (Support Team)

    Who can do this

    ✅ Users with role: Support Team

    Steps

    1. Go to Stock → Item
    2. Search and open the Item
    3. Find field: What Link to Order More
    4. Paste the Amazon URL (recommended format):
      • https://www.amazon.com/dp/ASIN
    5. Click Save
    6. Use the Order on Amazon button (if enabled) to verify link opens correctly

    Expected result

    • Link is saved and clickable
    • Support Team can edit again anytime

    SOP 2 — Set Link One Time (Non-Support Users)

    Who this is for

    Warehouse users / staff who can edit Items but are not Support Team.

    Steps

    1. Open Item
    2. If link field is empty:
      • Paste the correct URL
      • Click Save
    3. After saving, do not attempt to change/remove the link

    Expected result

    • First save works
    • Second change will be blocked with message:
      “This link can only be set once. Contact Support Team to modify it.”

    SOP 3 — Admin Setup: Create the Field (One-Time Setup)

    Steps

    1. Go to Settings → Customize Form
    2. Select DocType: Item
    3. Add a field (or confirm existing):

    Field configuration

    • Label: What Link to Order More
    • Field Type: Data
    • Options: URL
    • Length: 500 (or higher)
    • Fieldname: custom_what_link_to_order_more
    1. Click Save
    2. Click Update

    Expected result

    • Field appears on Item form
    • Long URLs allowed
    • URL is clickable

    SOP 4 — Admin Setup: “Order on Amazon” Button (Client Script)

    Steps

    1. Go to Settings → Client Script → New
    2. Set:
    • DocType: Item
    • View: Form
    • Enabled:
    1. Paste:
    frappe.ui.form.on('Item', {
      refresh(frm) {
        if (frm.doc.custom_what_link_to_order_more) {
          frm.add_custom_button(__('Order on Amazon'), () => {
            window.open(frm.doc.custom_what_link_to_order_more, '_blank');
          }, __('Order'));
        }
      }
    });
    
    1. Save
    2. Refresh an Item record

    Expected result

    • Button appears only when link exists
    • Button opens link in a new tab

    SOP 5 — Admin Setup: Enforce “Set Once” (Server Script, Sandbox-Safe)

    Goal

    • Support Team can edit anytime
    • Everyone else: can set once, cannot change/remove after it’s set

    Steps

    1. Go to Settings → Server Script → New
    2. Set:
    • Script Type: DocType Event
    • Reference DocType: Item
    • DocType Event: Before Save (or Validate)
    • Enabled:
    1. Paste:
    FIELDNAME = "custom_what_link_to_order_more"
    ALLOWED_ROLE = "Support Team"
    
    current_user = frappe.session.user
    
    user_roles = frappe.get_all(
        "Has Role",
        filters={"parent": current_user},
        pluck="role"
    )
    
    if ALLOWED_ROLE not in user_roles:
        if not doc.is_new():
            old_value = frappe.db.get_value("Item", doc.name, FIELDNAME) or ""
            new_value = doc.get(FIELDNAME) or ""
    
            if old_value and new_value != old_value:
                frappe.throw(
                    "This link can only be set once. Contact Support Team to modify it."
                )
    
    1. Save

    Validation test

    • As Support Team: edit link → allowed
    • As non-Support: set link once → allowed
    • As non-Support: try change/remove → blocked

    SOP 6 — Troubleshooting

    Issue A: “Method Not Allowed / Login to access”

    Cause: session expired or permissions.
    Fix:

    • Log out/in
    • Ensure you are using /app
    • Confirm role permissions for Server Script/Client Script

    Issue B: “AttributeError: module has no attribute has_role / get_roles”

    Cause: restricted server script sandbox
    Fix: use the Has Role query method (the SOP 5 script)

    Issue C: Field still editable after save

    Cause: server script disabled or wrong DocType event
    Fix:

    • Confirm Server Script is Enabled
    • Confirm DocType = Item
    • Confirm Event = Before Save or Validate

    SOP 7 — Rollback / Undo Changes

    Disable enforcement (keep script for later)

    • Settings → Server Script → open script → uncheck Enabled → Save

    Remove the button

    • Disable or delete the Client Script for Item

    Remove UI lock (if you ever set Read Only Depends On)

    • Customize Form → Item → field → clear Read Only Depends On → Save/Update

Secret Link