Get the FREE Ultimate OpenClaw Setup Guide →

erpnext-database

npx machina-cli add skill OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/erpnext-database --openclaw
Files (1)
SKILL.md
6.9 KB

ERPNext Database Operations

Quick Overview

Frappe provides three abstraction levels for database operations:

LevelAPIUsage
High-level ORMfrappe.get_doc, frappe.new_docDocument CRUD with validations
Mid-level Queryfrappe.db.get_list, frappe.db.get_valueReading with filters
Low-level SQLfrappe.db.sql, frappe.qbComplex queries, reports

RULE: Always use the highest abstraction level appropriate for your use case.


Decision Tree

What do you want to do?
│
├─ Create/modify/delete document?
│  └─ frappe.get_doc() + .insert()/.save()/.delete()
│
├─ Get single document?
│  ├─ Changes frequently? → frappe.get_doc()
│  └─ Changes rarely? → frappe.get_cached_doc()
│
├─ List of documents?
│  ├─ With user permissions? → frappe.db.get_list()
│  └─ Without permissions? → frappe.get_all()
│
├─ Single field value?
│  ├─ Regular DocType → frappe.db.get_value()
│  └─ Single DocType → frappe.db.get_single_value()
│
├─ Direct update without triggers?
│  └─ frappe.db.set_value() or doc.db_set()
│
└─ Complex query with JOINs?
   └─ frappe.qb (Query Builder) or frappe.db.sql()

Most Used Patterns

Get Document

# With ORM (triggers validations)
doc = frappe.get_doc('Sales Invoice', 'SINV-00001')

# Cached (faster for frequently accessed docs)
doc = frappe.get_cached_doc('Company', 'My Company')

List Query

# With user permissions
tasks = frappe.db.get_list('Task',
    filters={'status': 'Open'},
    fields=['name', 'subject'],
    order_by='creation desc',
    page_length=50
)

# Without permissions
all_tasks = frappe.get_all('Task', filters={'status': 'Open'})

Single Value

# Single field
status = frappe.db.get_value('Task', 'TASK001', 'status')

# Multiple fields
subject, status = frappe.db.get_value('Task', 'TASK001', ['subject', 'status'])

# As dict
data = frappe.db.get_value('Task', 'TASK001', ['subject', 'status'], as_dict=True)

Create Document

doc = frappe.get_doc({
    'doctype': 'Task',
    'subject': 'New Task',
    'status': 'Open'
})
doc.insert()

Update Document

# Via ORM (with validations)
doc = frappe.get_doc('Task', 'TASK001')
doc.status = 'Completed'
doc.save()

# Direct (without validations) - use carefully!
frappe.db.set_value('Task', 'TASK001', 'status', 'Completed')

Filter Operators

{'status': 'Open'}                          # =
{'status': ['!=', 'Cancelled']}             # !=
{'amount': ['>', 1000]}                     # >
{'amount': ['>=', 1000]}                    # >=
{'status': ['in', ['Open', 'Working']]}     # IN
{'date': ['between', ['2024-01-01', '2024-12-31']]}  # BETWEEN
{'subject': ['like', '%urgent%']}           # LIKE
{'description': ['is', 'set']}              # IS NOT NULL
{'description': ['is', 'not set']}          # IS NULL

Query Builder (frappe.qb)

Task = frappe.qb.DocType('Task')

results = (
    frappe.qb.from_(Task)
    .select(Task.name, Task.subject)
    .where(Task.status == 'Open')
    .orderby(Task.creation, order='desc')
    .limit(10)
).run(as_dict=True)

With JOIN

SI = frappe.qb.DocType('Sales Invoice')
Customer = frappe.qb.DocType('Customer')

results = (
    frappe.qb.from_(SI)
    .inner_join(Customer)
    .on(SI.customer == Customer.name)
    .select(SI.name, Customer.customer_name)
    .where(SI.docstatus == 1)
).run(as_dict=True)

Caching

Basics

# Set/Get
frappe.cache.set_value('key', 'value')
value = frappe.cache.get_value('key')

# With expiry
frappe.cache.set_value('key', 'value', expires_in_sec=3600)

# Delete
frappe.cache.delete_value('key')

@redis_cache Decorator

from frappe.utils.caching import redis_cache

@redis_cache(ttl=300)  # 5 minutes
def get_dashboard_data(user):
    return expensive_calculation(user)

# Invalidate cache
get_dashboard_data.clear_cache()

Transactions

Framework manages transactions automatically:

ContextCommitRollback
POST/PUT requestAfter successOn exception
Background jobAfter successOn exception

Manual (rarely needed)

frappe.db.savepoint('my_savepoint')
try:
    # operations
    frappe.db.commit()
except:
    frappe.db.rollback(save_point='my_savepoint')

Critical Rules

1. NEVER Use String Formatting in SQL

# ❌ SQL Injection risk!
frappe.db.sql(f"SELECT * FROM `tabUser` WHERE name = '{user_input}'")

# ✅ Parameterized
frappe.db.sql("SELECT * FROM `tabUser` WHERE name = %(name)s", {'name': user_input})

2. NEVER Commit in Controller Hooks

# ❌ WRONG
def validate(self):
    frappe.db.commit()  # Never do this!

# ✅ Framework handles commits

3. ALWAYS Paginate

# ✅ Always limit
docs = frappe.get_all('Sales Invoice', page_length=100)

4. Avoid N+1 Queries

# ❌ N+1 problem
for name in names:
    doc = frappe.get_doc('Customer', name)

# ✅ Batch fetch
docs = frappe.get_all('Customer', filters={'name': ['in', names]})

Version Differences

Featurev14v15v16
Transaction hooks
bulk_update
Aggregate syntaxStringStringDict

v16 Aggregate Syntax

# v14/v15
fields=['count(name) as count']

# v16
fields=[{'COUNT': 'name', 'as': 'count'}]

Reference Files

See the references/ folder for detailed documentation:

  • methods-reference.md - All Database and Document API methods
  • query-patterns.md - Filter operators and Query Builder syntax
  • caching-patterns.md - Redis cache patterns and @redis_cache
  • examples.md - Complete working examples
  • anti-patterns.md - Common mistakes and how to avoid them

Quick Reference

ActionMethod
Get documentfrappe.get_doc(doctype, name)
Cached documentfrappe.get_cached_doc(doctype, name)
New documentfrappe.new_doc(doctype) or frappe.get_doc({...})
Save documentdoc.save()
Insert documentdoc.insert()
Delete documentdoc.delete() or frappe.delete_doc()
Get listfrappe.db.get_list() / frappe.get_all()
Single valuefrappe.db.get_value()
Single valuefrappe.db.get_single_value()
Direct updatefrappe.db.set_value() / doc.db_set()
Exists checkfrappe.db.exists()
Count recordsfrappe.db.count()
Raw SQLfrappe.db.sql()
Query Builderfrappe.qb.from_()

Source

git clone https://github.com/OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package/blob/main/skills/source/core/erpnext-database/SKILL.mdView on GitHub

Overview

ERPNext/Frappe offers three database abstraction levels: High-level ORM (frappe.get_doc, frappe.new_doc), Mid-level queries (frappe.db.get_list, frappe.db.get_value), and Low-level SQL (frappe.db.sql, frappe.qb). This skill compiles practical patterns and a decision tree to help you pick the right level for CRUD, reads, and reports in ERPNext v14–v16, while stressing the rule to always use the highest abstraction appropriate for your use case.

How This Skill Works

The skill presents concrete usage patterns across the three abstraction levels: Get Document with ORM (validations on insert/save), List Queries with permissions (frappe.db.get_list) or without (frappe.get_all), Single Value retrieval (get_value) and its as_dict option, Create/Update flows using get_doc and insert/save, and complex queries with frappe.qb or frappe.db.sql. A decision tree guides level selection, backed by practical code examples demonstrating typical ERPNext workflows.

When to Use It

  • To create, modify, or delete ERPNext documents with validations using the ORM
  • To fetch a single cached or frequently accessed document
  • To read lists of records with user permissions or without (get_list vs get_all)
  • To retrieve single or multiple field values efficiently (get_value / as_dict)
  • To build complex queries or reports with joins using frappe.qb or raw SQL

Quick Start

  1. Step 1: Decide the task and pick the API level (ORM for CRUD, get_list/get_value for reads, qb for complex queries)
  2. Step 2: Implement with patterns from the docs (e.g., get_doc/insert, get_list with filters, or qb with joins)
  3. Step 3: Run, verify results, and consider caching or performance optimizations

Best Practices

  • Use the highest abstraction level appropriate for the task to keep code maintainable and future-proof
  • Prefer frappe.db.get_list with filters and fields for list reads to respect permissions and performance
  • Leverage caching for hot reads with frappe.get_cached_doc or frappe.cache where sensible
  • Use get_value for single-field or multi-field retrieval and as_dict when a dict is needed
  • Reserve frappe.qb for complex joins and reports; fall back to frappe.db.sql only for cases QB can't express

Example Use Cases

  • Get a Sales Invoice by ID with ORM: doc = frappe.get_doc('Sales Invoice', 'SINV-00001'); doc.insert()
  • Cache a Company doc: doc = frappe.get_cached_doc('Company', 'My Company')
  • List Open Tasks with user permissions: tasks = frappe.db.get_list('Task', filters={'status': 'Open'}, fields=['name','subject'], order_by='creation desc', page_length=50)
  • Get a single field value: status = frappe.db.get_value('Task', 'TASK001', 'status')
  • Join Invoice and Customer: results = (frappe.qb.from_(SI).inner_join(Customer).on(SI.customer == Customer.name).select(SI.name, Customer.customer_name).where(SI.docstatus == 1)).run(as_dict=True)

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers