agentsclimarketplace

Frappe portal

Skill prilk-consulting/frappe-agent-kit/skills/frappe-portal

Production-grade Frappe Framework & ERPNext skills, agents, and scaffolding commands for Claude Code

Install
npx -y skills add prilk-consulting/frappe-agent-kit --skill frappe-portal

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Frappe/ERPNext portal and website development. Use for exposing DocTypes on the portal (WebsiteGenerator vs portal list patterns), website_route_rules, portal menu items, has_website_permission, detail page templates, and upgrade-safe extension of existing portal pages. Not for React/Vue SPAs mounted inside a Frappe app — use frappe-frontend.

SKILL.md

6.9 KB, ~1.6k tokens by cl100k_base, as published. Nobody here has run it

Portal & Website Development

Usage

Use this skill when:

  • Exposing a DocType on the customer/supplier portal (list + detail pages)
  • Choosing between WebsiteGenerator and the portal-list pattern
  • Portal access control (has_website_permission, portal_users)
  • Extending existing ERPNext portal pages without forking templates
  • Debugging "portal page shows nothing" or PDF-generation hangs

Two patterns for exposing DocTypes

PatternWhenSetup
A: WebsiteGeneratorPublishable content (Blog Post, Web Page)has_web_view=1, controller extends WebsiteGenerator, requires route field
B: Portal List (recommended for transactions)Sales Order, Invoice, any user-owned documentget_list_context() in controller module; no has_web_view

Pattern A: WebsiteGenerator

  • Set has_web_view = 1 on the DocType, controller extends WebsiteGenerator
  • Requires a route field; each document gets its own URL
  • Template file named after the doctype (.html) inside the doctype folder's templates directory
  • Override get_context(context) for template variables

Pattern B: Portal List (recommended for transaction documents)

# In the DocType controller module (my_doctype.py)
def get_list_context(context=None):
    return {
        "show_sidebar": True,
        "show_search": True,
        "no_breadcrumbs": True,
        "title": _("My Documents"),
        "get_list": get_filtered_list,
        "row_template": "templates/includes/my_app/my_row.html",
        "list_template": "templates/includes/list/list.html",
    }

def get_filtered_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by=None):
    """Filter list to only show current user's documents."""
    return frappe.get_all(
        doctype,
        filters={"user": frappe.session.user},
        fields=["name", "status", "creation"],
        limit_start=limit_start,
        limit_page_length=limit_page_length,
        order_by=order_by or "modified desc",
    )

def has_website_permission(doc, ptype, user=None, verbose=False):
    """Only allow users to see their own documents."""
    return doc.user == (user or frappe.session.user)
# hooks.py
website_route_rules = [
    # List page — maps to DocType with get_list_context
    {"from_route": "/my-docs", "to_route": "My DocType"},
    # Detail page — maps to a template
    {
        "from_route": "/my-docs/<path:name>",
        "to_route": "my-doc-detail",
        "defaults": {
            "doctype": "My DocType",
            "parents": [{"label": "My Docs", "route": "my-docs"}],
        },
    },
]

standard_portal_menu_items = [
    {"title": "My Docs", "route": "/my-docs", "reference_doctype": "My DocType", "role": "My Role"},
]

has_website_permission = {
    "My DocType": "my_app.module.doctype.my_doctype.my_doctype.has_website_permission",
}

Detail page template

# templates/pages/my-doc-detail.py
import frappe
from frappe import _

def get_context(context):
    context.no_cache = 1
    context.show_sidebar = True
    context.doc = frappe.get_doc(frappe.form_dict.doctype, frappe.form_dict.name)
    context.parents = frappe.form_dict.parents
    context.title = context.doc.name
    if not frappe.has_website_permission(context.doc):
        frappe.throw(_("Not Permitted"), frappe.PermissionError)
{# templates/pages/my-doc-detail.html #}
{% extends "templates/web.html" %}
{% block title %}{{ doc.name }}{% endblock %}
{% block breadcrumbs %}{% include "templates/includes/breadcrumbs.html" %}{% endblock %}
{% block page_content %}
    <h3>{{ doc.name }}</h3>
    <p>Status: {{ doc.status }}</p>
{% endblock %}

Row template for the list

{# templates/includes/my_app/my_row.html #}
<div class="row py-3 border-bottom">
    <div class="col-sm-4">
        <a href="/my-docs/{{ doc.name }}"><strong>{{ doc.name }}</strong></a>
    </div>
    <div class="col-sm-4">
        <span class="indicator-pill {{ 'green' if doc.status == 'Active' else 'orange' }}">
            {{ doc.status }}
        </span>
    </div>
    <div class="col-sm-4 text-right text-muted small">
        {{ frappe.utils.format_date(doc.creation, 'medium') }}
    </div>
</div>

Extending existing portal pages WITHOUT template overrides

The upgrade-safe way — no ERPNext template gets forked:

# hooks.py
web_include_js = "my_portal.bundle.js"          # JS on all portal pages
web_include_css = "my_portal.bundle.css"        # CSS on all portal pages
update_website_context = ["my_app.portal.context.update_website_context"]
# portal/context.py
def update_website_context(context):
    if frappe.session.user == "Guest":
        return
    if "My Role" not in frappe.get_roles():
        return
    context.my_custom_flag = True
// public/js/my_portal.bundle.js
(function() {
    document.addEventListener("DOMContentLoaded", function() {
        const path = window.location.pathname;
        if (path.match(/^\/orders\/.+/)) {
            // inject custom UI into the order detail page
        }
    });
})();

update_website_context runs on every portal page — keep it lightweight.

Portal access control (ERPNext pattern)

ERPNext scopes portal transactions through the Customer/Supplier portal_users child table:

# website_list_for_contact.py pattern
def get_transaction_list(doctype, ...):
    # 1. Find which customers belong to this portal user
    customers = get_parents_for_user("Customer")
    # 2. Filter transactions by those customers
    return frappe.get_all(doctype, filters={"customer": ["in", customers]}, ...)

Portal users need BOTH:

  1. The appropriate role (Customer, Supplier, or a custom role)
  2. Their email in the Customer/Supplier portal_users table

Missing either one produces an empty portal list with no error — the most common "portal shows nothing" cause.

Portal gotchas

  • bench serve --nothreading deadlocks when wkhtmltopdf loads CSS from the same server (PDF generation hangs forever in single-threaded dev).
  • frm.call() sends form values to the server — Password fields get overwritten on the round-trip. Use frappe.xcall() for methods that set Password fields.
  • Template resolution: last installed app wins. Use {% extends "appname/path" %} prefix to pin a specific app's template.
  • standard_portal_menu_items only sync on bench migrate (or Portal Settings save) — adding an entry to hooks.py does nothing until you migrate.
  • website_route_rules changes need bench --site <site> clear-website-cache.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,970. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.