> Templates, Notification templates, Portal Pages, and custom Jinja methods. Covers template creation workflows, child table handling, conditional sections, styling, multi-language support, and debugging. Prevents N+1 queries, wrong formatting, and Report Print confusion. template, invoice template, jinja methods, notification template, web page template, print format styling.
npx skills add https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-jinja
Step-by-step workflows for building Jinja templates. For syntax reference, see frappe-syntax-jinja.
Version: v14/v15/v16 (V16 Chrome PDF noted)
WHAT IS YOUR OUTPUT?
│
├─► Printable PDF (invoice, PO, report)?
│ ├─► Standard DocType → Print Format (Jinja)
│ └─► Query/Script Report → Report Print Format (JAVASCRIPT!)
│ ⚠️ Uses {%= %} NOT {{ }}
│
├─► Automated email with dynamic content?
│ └─► Email Template (Jinja, linked to DocType)
│
├─► System notification?
│ └─► Notification (Setup > Notification, uses Jinja)
│
├─► Customer-facing web page?
│ └─► Portal Page (myapp/www/*.html + *.py)
│
└─► Reusable template functions/filters?
└─► Custom jenv methods in hooks.py
Setup > Printing > Print Format > New
- Name: My Invoice Format
- DocType: Sales Invoice
- Module: Accounts
- Standard: No (custom)
- Print Format Type: Jinja
<style>
.print-format { font-family: Arial, sans-serif; font-size: 11px; }
.header { margin-bottom: 20px; }
.table { width: 100%; border-collapse: collapse; margin: 20px 0; }
.table th, .table td { border: 1px solid #ddd; padding: 8px; }
.table th { background: #f0f0f0; }
.text-right { text-align: right; }
</style>
<div class="header">
<h1>{{ doc.select_print_heading or _("Invoice") }}</h1>
<p><strong>{{ doc.name }}</strong> |
{{ doc.get_formatted("posting_date") }}</p>
</div>
<p><strong>{{ doc.customer_name }}</strong></p>
{% if doc.address_display %}
<p>{{ doc.address_display | safe }}</p>
{% endif %}
<table class="table">
<thead>
<tr>
<th>#</th>
<th>{{ _("Item") }}</th>
<th class="text-right">{{ _("Qty") }}</th>
<th class="text-right">{{ _("Rate") }}</th>
<th class="text-right">{{ _("Amount") }}</th>
</tr>
</thead>
<tbody>
{% for row in doc.items %}
<tr>
<td>{{ row.idx }}</td>
<td>{{ row.item_name }}</td>
<td class="text-right">{{ row.qty }}</td>
<td class="text-right">{{ row.get_formatted("rate", doc) }}</td>
<td class="text-right">{{ row.get_formatted("amount", doc) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% for tax in doc.taxes %}
<p class="text-right">{{ tax.description }}: {{ tax.get_formatted("tax_amount", doc) }}</p>
{% endfor %}
<p class="text-right">
<strong>{{ _("Grand Total") }}: {{ doc.get_formatted("grand_total") }}</strong>
</p>
{% if doc.terms %}
<div style="margin-top: 30px; border-top: 1px solid #ddd; padding-top: 10px;">
<strong>{{ _("Terms and Conditions") }}</strong>
{{ doc.terms | safe }}
</div>
{% endif %}
doc.get_formatted("field") for currency, dates, numbersrow.get_formatted("rate", doc)_("text") for translation<style> block at the top (not external files)| safe on user-supplied input — only on trusted system HTMLSetup > Email > Email Template > New
- Name: Payment Reminder
- Subject: Invoice {{ doc.name }} - Payment Reminder
- DocType: Sales Invoice
ALWAYS use inline styles for emails — most clients strip <style> blocks.
<div style="font-family: Arial, sans-serif; max-width: 600px;">
<p>{{ _("Dear") }} {{ doc.customer_name }},</p>
<p>{{ _("Invoice") }} <strong>{{ doc.name }}</strong>
{{ _("for") }} {{ doc.get_formatted("grand_total") }}
{{ _("is due for payment.") }}</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="background: #f5f5f5;">
<td style="padding: 10px; border: 1px solid #ddd;">
<strong>{{ _("Due Date") }}</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">
{{ frappe.format_date(doc.due_date) }}</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;">
<strong>{{ _("Outstanding") }}</strong></td>
<td style="padding: 10px; border: 1px solid #ddd; color: #c00;">
{{ doc.get_formatted("outstanding_amount") }}</td>
</tr>
</table>
{% if doc.items %}
<p><strong>{{ _("Items") }}:</strong></p>
<ul>
{% for item in doc.items[:5] %}
<li>{{ item.item_name }} ({{ item.qty }})</li>
{% endfor %}
{% if doc.items | length > 5 %}
<li style="color: #666;">{{ _("and {0} more...").format(doc.items|length - 5) }}</li>
{% endif %}
</ul>
{% endif %}
<p>{{ _("Best regards") }},<br>
{{ frappe.db.get_value("Company", doc.company, "company_name") }}</p>
</div>
Option A: Auto-triggered Notification
Setup > Notification > New
- Channel: Email
- Document Type: Sales Invoice
- Send Alert On: Days After (7 days after due_date)
- Condition: doc.outstanding_amount > 0
- Email Template: Payment Reminder
Option B: Send from code
template = frappe.get_doc("Email Template", "Payment Reminder")
frappe.sendmail(
recipients=[doc.contact_email],
subject=frappe.render_template(template.subject, {"doc": doc}),
message=frappe.render_template(template.response, {"doc": doc}),
reference_doctype=doc.doctype,
reference_name=doc.name
)
Setup > Notification > New
- Name: Low Stock Alert
- Channel: Email (or Slack, System Notification)
- Document Type: Stock Ledger Entry
- Send Alert On: Method (on change)
- Condition: doc.actual_qty < 10
<h3>{{ _("Low Stock Alert") }}</h3>
<p>{{ _("Item") }}: <strong>{{ doc.item_code }}</strong></p>
<p>{{ _("Warehouse") }}: {{ doc.warehouse }}</p>
<p>{{ _("Current Stock") }}: {{ doc.actual_qty }}</p>
<p>{{ _("Please reorder.") }}</p>
myapp/
└── www/
└── my-orders/
├── index.html # Jinja template
└── index.py # Python context
import frappe
def get_context(context):
if frappe.session.user == "Guest":
frappe.local.flags.redirect_location = "/login"
raise frappe.Redirect
context.title = "My Orders"
context.no_cache = True
customer = frappe.db.get_value("Contact",
{"user": frappe.session.user}, "link_name")
context.orders = frappe.get_all("Sales Order",
filters={"customer": customer, "docstatus": ["!=", 2]},
fields=["name", "transaction_date", "grand_total", "status"],
order_by="transaction_date desc",
limit=50
) if customer else []
return context
{% extends "templates/web.html" %}
{% block title %}{{ _("My Orders") }}{% endblock %}
{% block page_content %}
<div class="container my-4">
<h1>{{ _("My Orders") }}</h1>
{% if orders %}
<table class="table table-hover">
<thead>
<tr>
<th>{{ _("Order") }}</th>
<th>{{ _("Date") }}</th>
<th>{{ _("Status") }}</th>
<th class="text-right">{{ _("Total") }}</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td><a href="/orders/{{ order.name }}">{{ order.name }}</a></td>
<td>{{ frappe.format_date(order.transaction_date) }}</td>
<td>{{ order.status }}</td>
<td class="text-right">
{{ frappe.format(order.grand_total, {"fieldtype": "Currency"}) }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">{{ _("No orders found.") }}</p>
{% endif %}
</div>
{% endblock %}
https://yoursite.com/my-ordersjenv = {
"methods": ["myapp.jinja_utils.methods"],
"filters": ["myapp.jinja_utils.filters"]
}
# myapp/jinja_utils/methods.py
import frappe
def get_company_logo(company):
"""Usage: {{ get_company_logo(doc.company) }}"""
return frappe.db.get_value("Company", company, "company_logo") or ""
def format_address(address_name):
"""Usage: {{ format_address(doc.customer_address) | safe }}"""
if not address_name:
return ""
return frappe.get_doc("Address", address_name).get_display()
# myapp/jinja_utils/filters.py
def phone_format(value):
"""Usage: {{ doc.phone | phone_format }}"""
if not value:
return ""
digits = ''.join(c for c in str(value) if c.isdigit())
if len(digits) == 10:
return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
return value
bench --site sitename migrate
bench --site sitename clear-cache
<!-- Step 1: Check if doc is available -->
<!-- DEBUG: {{ doc.name if doc else 'NO DOC' }} -->
<!-- Step 2: Check child table -->
<!-- DEBUG: items count = {{ doc.items | length if doc.items else 0 }} -->
<!-- Step 3: Check specific field -->
<!-- DEBUG: grand_total = {{ doc.grand_total }} -->
frappe.render_template(template_string, {"doc": doc}) in bench consolefrappe.logger().info(context) in get_context| Symptom | Cause | Fix |
|---------|-------|-----|
| Blank output | Wrong template type (Jinja in Report) | Reports use JS: {%= %} |
| "None" displayed | Field is null | Use \| default('') |
| Wrong currency format | Missing parent doc context | Use row.get_formatted("rate", doc) |
| HTML showing as text | Auto-escaping | Add \| safe (trusted content only) |
| Translations not working | Missing _() wrapper | Wrap all strings: {{ _("text") }} |
{# Child tables — ALWAYS pass parent doc for formatting context #}
{% for row in doc.items %}
{{ row.get_formatted("rate", doc) }} {# Correct: has currency context #}
{% endfor %}
{# Conditional sections #}
{% if doc.shipping_address_name %}
{{ doc.shipping_address | safe }}
{% endif %}
{# Translation — ALWAYS wrap user-facing text #}
{{ _("Invoice") }}
{{ _("Page {0} of {1}").format(page, total_pages) }}
{{ doc.get_formatted("grand_total") }} {# Auto-formats per locale #}
@page { margin: 1.5cm; }
.avoid-break { page-break-inside: avoid; }
thead { display: table-header-group; } /* Repeat header on pages */
.page-break { page-break-before: always; }
/* V14/V15: NO flexbox (wkhtmltopdf). V16 Chrome PDF: flexbox OK */
.layout { display: table; width: 100%; }
.col { display: table-cell; vertical-align: top; }
| Template Type | Available Objects |
|---------------|-------------------|
| Print Format | doc, frappe, _(), frappe.format() |
| Email Template | doc, frappe (limited), _() |
| Notification | doc, frappe, event data |
| Portal Page | frappe.session, frappe.form_dict, custom context |
| Feature | V14 | V15 | V16 |
|---------|:---:|:---:|:---:|
| Jinja templates | Yes | Yes | Yes |
| get_formatted() | Yes | Yes | Yes |
| jenv hooks | Yes | Yes | Yes |
| wkhtmltopdf PDF | Yes | Yes | Deprecated |
| Chrome PDF | No | No | Yes |
> V16 Chrome PDF supports modern CSS (flexbox, grid, CSS variables). See frappe-syntax-jinja for details.
| File | Contents |
|------|----------|
| decision-tree.md | Complete template type selection flowcharts |
| print-format-decision.md | Jinja vs Print Designer vs JS Microtemplate decision tree |
| workflows.md | Step-by-step patterns for all template types |
| examples.md | Production-ready templates (invoice, email, portal) |
Druckenmiller Strategy Synthesizer - Integrates 8 upstream skill outputs (Market Breadth, Uptrend Analysis, Market Top, Macro Regime, FTD Detector, VCP Screener, Theme Detector, CANSLIM Screener) into a unified conviction score (0-100), pattern classification, and allocation recommendation. Use when user asks about overall market conviction, portfolio positioning, asset allocation, strategy synthesis, or Druckenmiller-style analysis. Triggers on queries like "What is my conviction level?", "How should I position?", "Run the strategy synthesizer", "Druckenmiller analysis", "総合的な市場判断", "確信度スコア", "ポートフォリオ配分", "ドラッケンミラー分析".
> 한국 신입 취준생·재직자를 위한 분야별 포트폴리오 구성과 프로젝트 기술서 작성 스킬입니다. "포트폴리오 만들어줘", "프로젝트 정리해줘", "노션 포트폴리오 1시간 만에"처럼 말하면 됩니다. 개발(GitHub·기술 블로그)/디자인(Figma·Behance)/마케팅/기획 분야별 + 검색되는 노션 포트폴리오 + 채용공고 맞춤형 1page 셀링 + 877건 노하우 기반 비중 배분을 지원합니다.
Generate a topic-focused briefing in HTML from public news sources. Use when user asks for a briefing / observation / digest / 简报 / 观察 on any subject — region (Middle East, ASEAN, India), industry (semiconductors, EV supply chain, AI), policy issue (AI regulation, critical minerals, cross-border payments), institution (Fed, ECB, IMF), or theme. Output is a single self-contained HTML file with blue-color "TOPIC BRIEF" branding, optimized for both browser viewing and direct copy-paste into 微信公众号 / WeChat Official Account editor. Walks through 5 steps with one user confirmation midway. Do NOT use this skill for short summaries (<1000 字), single-piece news commentary, or already-defined report templates.
Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.
Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
Take impertio-studio/frappe-impl-jinja from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.