Frappe portal
Skill prilk-consulting/frappe-agent-kit/skills/frappe-portal
Production-grade Frappe Framework & ERPNext skills, agents, and scaffolding commands for Claude Code
npx -y skills add prilk-consulting/frappe-agent-kit --skill frappe-portalAssembled 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
| Pattern | When | Setup |
|---|---|---|
| A: WebsiteGenerator | Publishable 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 document | get_list_context() in controller module; no has_web_view |
Pattern A: WebsiteGenerator
- Set
has_web_view = 1on the DocType, controller extendsWebsiteGenerator - Requires a
routefield; each document gets its own URL - Template file named after the doctype (
.html) inside the doctype folder'stemplatesdirectory - 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:
- The appropriate role (Customer, Supplier, or a custom role)
- Their email in the Customer/Supplier
portal_userstable
Missing either one produces an empty portal list with no error — the most common "portal shows nothing" cause.
Portal gotchas
bench serve --nothreadingdeadlocks 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. Usefrappe.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_itemsonly sync onbench migrate(or Portal Settings save) — adding an entry to hooks.py does nothing until you migrate.website_route_ruleschanges needbench --site <site> clear-website-cache.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.