Why Built This
In most ERPNext setups, users personalize their child tables using the GridView settings — selecting only the columns they care about.
But the Print Format still shows all columns — static, cluttered, and not user-friendly.
So I built a dynamic column print format for Purchase Order
using the saved GridView settings from _user_settings
(with fallback to SQL and Doctype meta).
Here’s how it works
Step 1: Python Method to Get Dynamic Columns
This function:
- Checks the GridView config from cache
- Falls back to the
__UserSettings
table - Falls back to
in_list_view
fields with column width from Doctype meta
Final Code
import json
import frappe
def get_dynamic_columns():
user = frappe.session.user
doctype = "Purchase Order"
child_table = "Purchase Order Item"
meta = frappe.get_meta(child_table)
fields_map = {f.fieldname: f.label for f in meta.fields}
def map_fieldnames(fieldnames):
return [
{"label": fields_map.get(f), "fieldname": f}
for f in fieldnames if f in fields_map
]
# 1. From Cache
user_settings = frappe.cache.hgetall("_user_settings")
for key, data in user_settings.items():
key = frappe.safe_decode(key)
try:
doc, usr = key.split("::")
except ValueError:
continue
if doc == doctype and usr == user:
try:
data = json.loads(data)
gridview_data = data.get("GridView", {})
if child_table in gridview_data:
fieldnames = [f.get("fieldname") for f in gridview_data[child_table]]
return map_fieldnames(fieldnames)
except Exception:
continue
# 2. From SQL
query = """
SELECT data FROM `__UserSettings`
WHERE user = %s AND doctype = %s
"""
sql_data = frappe.db.sql(query, (user, doctype), as_dict=True)
for row in sql_data:
try:
parsed_data = json.loads(row["data"])
gridview_data = parsed_data.get("GridView", {})
if child_table in gridview_data:
fieldnames = [f.get("fieldname") for f in gridview_data[child_table]]
return map_fieldnames(fieldnames)
except Exception:
continue
# 3. Fallback from Meta
return [
{"label": f.label, "fieldname": f.fieldname}
for f in meta.fields
if f.in_list_view and (f.columns or 0) > 0
]
Step 2: Register Method in hooks.py
jinja = {
"methods": "sudhanshu_app.custom_script.utils.utils.get_dynamic_columns"
}
This makes the method callable in Jinja templates like
{{ get_dynamic_columns() }}
.
Step 3: The Jinja Print Format
<!-- Purchase Order Print Format -->
<h2>Purchase Order</h2>
<p><strong>Supplier:</strong> {{ doc.supplier }}</p>
<p><strong>Date:</strong> {{ doc.transaction_date }}</p>
<br>
{% set columns = get_dynamic_columns() %}
<table class="table table-bordered table-condensed">
<thead>
<tr>
{% for col in columns %}
<th>{{ col.label }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for item in doc.items %}
<tr>
{% for col in columns %}
<td>{{ item[col.fieldname] }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
Example Case 1 — Grid View with 4 Fields
User saves the following GridView columns in Purchase Order Item:
- Item Code
- Quantity
- UOM
- Rate (INR)
GridView Settings
Print Output
Example Case 2 — Grid View Changed
User updates the GridView settings:
- Item Code
- Item Name
- Quantity
- UOM
- Rate (INR)
GridView Settings
Print Output
The print format automatically reflects the change without any code update. That’s the power of dynamic print layouts.
Final Thoughts
This pattern keeps the printed documents as clean and personalized as the UI the user is already working in — no more rigid static tables!
Connect with me on: LinkedIn-Sudhanshu Badole