Frappe Dev Concepts: Documents, Methods & Events
Introduction
A Document is an instance of a DocType in Frappe. It is derived from the frappe.model.Document class and represents a single record in the database table. This guide covers the essential Frappe Python API for working with documents.
Fetching Documents
1. frappe.get_doc
Returns a Document object of the record identified by doctype and name. If no document is found, a DoesNotExistError is raised.
# get an existing document
doc = frappe.get_doc('Task', 'TASK00002')
doc.title = 'Test'
doc.save()
# get a single doctype
doc = frappe.get_doc('System Settings')
doc.timezone # Asia/Kolkata2. frappe.get_doc(dict)
Returns a new Document object in memory which does not exist yet in the database.
# create a new document
doc = frappe.get_doc({
'doctype': 'Task',
'title': 'New Task'
})
doc.insert()
# create with keyword arguments
user = frappe.get_doc(doctype='User', email_id='test@example.com')
user.insert()3. frappe.get_last_doc
Returns the last Document object created under the mentioned doctype. Supports filters and custom ordering.
# get the last Task created
task = frappe.get_last_doc('Task')
# get the last Cancelled Task
task = frappe.get_last_doc('Task', filters={"status": "Cancelled"})
# custom ordering
task = frappe.get_last_doc('Task', filters={"Status": "Cancelled"}, order_by="timestamp desc")4. frappe.new_doc
Alternative way to create a new Document.
doc = frappe.new_doc('Task')
doc.title = 'New Task 2'
doc.insert()5. frappe.get_docs
Returns a list of Document objects. Use as_iterator=True to fetch records in chunks for better memory management.
# Fetch specific documents
tasks = frappe.get_docs('Task', filters={'status': 'Open'}, limit=10)
for task in tasks:
task.status = "Closed"
task.save()
# Efficiently iterate through large datasets
leads = frappe.get_docs('Lead', as_iterator=True, chunk_size=500)
for lead in leads:
lead.process_lead()6. frappe.delete_doc & frappe.rename_doc
# Delete a document
frappe.delete_doc('Task', 'TASK00002')
# Rename a document (Allow Rename must be set in DocType)
frappe.rename_doc('Task', 'TASK00002', 'TASK00003')
# Rename with merge
frappe.rename_doc('Task', 'TASK00002', 'TASK00003', merge=True)7. frappe.get_meta
Returns meta information of a doctype including custom fields and property setters.
meta = frappe.get_meta('Task')
meta.has_field('status') # True
meta.get_custom_fields() # [field1, field2, ..]Document Methods
doc.insert
Inserts a new document into the database. Runs before_insert, validate, on_update, after_insert controller methods.
doc.insert(
ignore_permissions=True,
ignore_links=True,
ignore_if_duplicate=True,
ignore_mandatory=True
)doc.save
Saves changes to an existing document. Executes validate before updating and on_update after.
doc.save(
ignore_permissions=True,
ignore_version=True # do not create a version record
)doc.db_set
Set a field value directly in the database. Does NOT trigger controller validations — use carefully.
# updates value, updates modified timestamp
doc.db_set('price', 2300)
# also triggers notify_update
doc.db_set('price', 2300, notify=True)
# also runs frappe.db.commit()
doc.db_set('price', 2300, commit=True)
# skip modified timestamp update
doc.db_set('price', 2300, update_modified=False)doc.append
Append a new row to a child table.
doc.append("items", {
"item_code": "ITEM-001",
"qty": 5,
"rate": 100
})Other Useful Methods
# Check if a value changed before save
if doc.has_value_changed("price"):
pass
# Get the doc state before current changes
old_doc = doc.get_doc_before_save()
# Reload from database
doc.reload()
# Get desk URL
url = doc.get_url() # /app/task/TASK00002
# Add a comment
doc.add_comment('Comment', text='Test Comment')
# Run a controller method in background
doc.queue_action('send_emails', emails=email_list, message='Howdy')Document Events (doc_events)
Frappe provides doc_events in hooks.py to attach custom Python functions to document lifecycle events — without modifying core code.
# hooks.py
doc_events = {
"Sales Invoice": {
"on_submit": "my_app.invoice.on_submit"
}
}
# The handler function
def on_submit(doc, method):
update_loyalty_points(doc.customer)Event Lifecycle
- before_insert — Document exists only in memory. Set defaults or normalize data.
- after_insert — Document saved for the first time. Send emails, create linked records.
- validate — Runs on every save/submit before DB write. Enforce business rules with
frappe.throw(). - before_save — After validation, before DB write. Compute totals, format fields.
- after_save — After DB write. Logging, notifications, analytics.
- on_update — On every update after creation. Sync with external systems.
- before_submit — Final check before document becomes permanent.
- on_submit — Document is final. Execute real business logic (stock, accounting).
- before_cancel — Prevent cancellation if conditions aren't met.
- on_cancel — Reverse business actions done during submission.
- on_trash — Before permanent deletion. Clean up related records.
- after_delete — Record removed from DB. External cleanup.
Event Examples
def before_insert(doc, method):
if not doc.source:
doc.source = "Web"
def validate(doc, method):
for item in doc.items:
if item.qty <= 0:
frappe.throw("Quantity must be greater than zero")
def before_save(doc, method):
doc.total = sum(item.qty * item.rate for item in doc.items)
def on_submit(doc, method):
update_loyalty_points(doc.customer)
def on_cancel(doc, method):
restore_stock(doc.name)Conclusion
Understanding Frappe's Document API and doc_events system is fundamental to building robust, upgrade-safe ERPNext applications. Use controller methods for per-DocType logic, and hooks for cross-app customizations that don't touch core code.