Category: Server

  • 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

  • ✅ Layer 1 (RECOMMENDED): Disable IPv6 via sysctl (permanent)

    1️⃣ Create a dedicated sysctl file

    sudo nano /etc/sysctl.d/99-disable-ipv6.conf
    

    Paste exactly this:

    net.ipv6.conf.all.disable_ipv6 = 1
    net.ipv6.conf.default.disable_ipv6 = 1
    net.ipv6.conf.lo.disable_ipv6 = 1
    

    Save and exit.


    2️⃣ Apply immediately (no reboot needed)

    sudo sysctl --system
    

    3️⃣ Verify IPv6 is disabled

    ip a | grep inet6
    

    Expected result: no output
    (or only ::1 disappears as well)

    Also check:

    cat /proc/sys/net/ipv6/conf/all/disable_ipv6
    

    Should return:

    1
Secret Link