Understanding the Document class and document lifecycle in Frappe
The Document class is the base class for all document instances in Frappe. It provides the core functionality for creating, reading, updating, and deleting documents, along with a rich lifecycle system.
Every record in Frappe is an instance of the Document class or a subclass thereof. Documents inherit behavior from their DocType metadata and can have custom controllers for business logic.
# Get an existing documentuser = frappe.get_doc('User', '[email protected]')# Create a new documenttodo = frappe.get_doc({ 'doctype': 'ToDo', 'description': 'Complete the task', 'priority': 'High'})
# Get by DocType and namedoc = frappe.get_doc('Sales Order', 'SO-0001')# Get Single DocTypesettings = frappe.get_doc('System Settings')# Get with permission checkdoc = frappe.get_doc('Sales Order', 'SO-0001', check_permission='read')# Get for update (with row-level lock)doc = frappe.get_doc('Sales Order', 'SO-0001', for_update=True)
Documents go through a well-defined lifecycle with hooks at each stage:
class MyDocType(Document): def before_insert(self): """Called before document is inserted into database""" self.validate_custom_logic() def validate(self): """Called during insert and save. Validate document data""" if not self.title: frappe.throw('Title is required') def before_save(self): """Called before saving (insert or update)""" self.update_derived_fields() def after_insert(self): """Called after document is inserted""" self.send_notification() def on_update(self): """Called after document is updated (insert or save)""" self.update_related_documents() def on_submit(self): """Called when document is submitted (docstatus = 1)""" self.create_ledger_entries() def on_cancel(self): """Called when document is cancelled (docstatus = 2)""" self.reverse_ledger_entries() def on_trash(self): """Called before document is deleted""" self.cleanup_references() def after_delete(self): """Called after document is deleted""" self.notify_deletion()
# Delete from Document instancedoc = frappe.get_doc('ToDo', 'TODO-0001')doc.delete()# Delete using frappe.delete_docfrappe.delete_doc('ToDo', 'TODO-0001', ignore_permissions=False, force=False # Delete even if linked)
# Check if field changedif doc.has_value_changed('status'): old_value = doc.get_value_before_save('status') new_value = doc.status print(f"Status changed from {old_value} to {new_value}")# Get document before saveold_doc = doc.get_doc_before_save()
print(doc.name) # Document name (primary key)print(doc.owner) # User who created the documentprint(doc.creation) # Creation timestampprint(doc.modified) # Last modified timestampprint(doc.modified_by) # User who last modifiedprint(doc.docstatus) # 0: Draft, 1: Submitted, 2: Cancelledprint(doc.idx) # Index (for child documents)
class SalesOrder(Document): def validate(self): """Validate document before save""" self.validate_customer() self.validate_items() self.calculate_totals() def validate_customer(self): if not self.customer: frappe.throw('Customer is required') # Check if customer is active if frappe.db.get_value('Customer', self.customer, 'disabled'): frappe.throw('Customer is disabled') def validate_items(self): if not self.items: frappe.throw('Please add at least one item') for item in self.items: if item.qty <= 0: frappe.throw(f'Quantity must be positive for {item.item_code}')
# Update without triggering full save cycledoc.db_set('status', 'Completed', update_modified=True)# Update multiple fieldsdoc.db_set({ 'status': 'Completed', 'completion_date': frappe.utils.today()})# Get value from database (without loading full document)status = frappe.db.get_value('ToDo', 'TODO-0001', 'status')
db_set() bypasses validation and controller methods. Use only when you need to skip the full document lifecycle.
# Check if document is lockedif doc.is_locked: frappe.throw('Document is locked for execution')# Lock document for background processingfrom frappe.utils import file_lockwith file_lock(doc.get_signature()): # Document is locked process_document(doc)
# Enable version tracking in DocType# track_changes = 1# Versions are automatically created on savedoc.save()# Disable version tracking for specific savedoc.save(ignore_version=True)# Get document versionsversions = frappe.get_all('Version', filters={'ref_doctype': doc.doctype, 'docname': doc.name}, fields=['*'], order_by='creation desc')
# Get documents linked to this documentfrom frappe.desk.form.linked_with import get_linked_docslinked = get_linked_docs(doc.doctype, doc.name, linkinfo=None)# Check if document has back links (before cancel)from frappe.model.delete_doc import check_if_doc_is_linkedtry: check_if_doc_is_linked(doc)except frappe.LinkExistsError: print("Cannot cancel: Document has dependent links")