Skip to main content

Understanding Virtual DocType in Frappe Framework v15

What is a Virtual DocType in Frappe v15?

A Virtual DocType in Frappe v15 is a DocType that behaves like a normal document in the Desk UI and API, but does not have a backing database table. Instead of persisting data in MariaDB/PostgreSQL, it sources and writes data from custom logic you implement in Python.

Virtual DocTypes are ideal when:

  • The data already lives in an external system or another datastore.
  • You want to expose computed data as if it were a normal DocType.
  • You need a read-only or partially managed view over non-Frappe data.

From the user’s point of view, a Virtual DocType looks and feels like any other DocType in ERPNext/Frappe: it has forms, list views, DocFields, permissions, and can be accessed from the Desk.

When should you use a Virtual DocType?

Use a Virtual DocType when you:

  • Need to integrate external APIs or services and show their data as documents.
  • Want to avoid data duplication, because the system of record is outside Frappe.
  • Need computed documents – for example, aggregates, analytics, or derived views.
  • Are building connectors (e.g., for a legacy system, data warehouse, or external CRM).

Avoid Virtual DocTypes when:

  • You require normal CRUD persistence inside Frappe’s database.
  • You depend heavily on database-level joins, reports, or heavy filters on that DocType.
  • You could simply use a Query Report, Dashboard Chart, or Custom Script Report instead.

How does a Virtual DocType work internally?

At a high level, Frappe v15 uses the same document model (frappe.model.document.Document) for Virtual DocTypes, but the data access layer is delegated to your own methods on the DocType controller.

Key concepts:

  • The DocType is marked as Virtual in its configuration.
  • Frappe does not create a database table for this DocType.
  • CRUD operations (load, insert, save, delete, get_list) are fulfilled by Python methods you implement.
  • The DocType can still use:
    • DocFields for layout and validation
    • Permissions, Roles, and Workspaces
    • List View / Form View behavior

Technical Prerequisites

To work comfortably with Virtual DocTypes in Frappe v15, you should be familiar with:

  • Creating and editing DocTypes in the Desk or via JSON files.
  • Writing Python DocType controllers in doctype_name.py.
  • Frappe’s Document API (frappe.get_doc, frappe.get_list, etc.).
  • Basic Python OOP patterns and the Frappe request lifecycle.

How to create a Virtual DocType in Frappe v15

Step 1 – Create the DocType

  1. Go to Desk → Developer → DocType.
  2. Click New.
  3. Set:
    • Module: Your app’s module (e.g., Integration).
    • Name: For example, Remote Issue.
    • Custom?: As needed.
  4. In the “Customizations/Advanced” area (or equivalent in v15 UI):
    • Check Virtual (or the “Is Virtual” flag).

This tells Frappe that this DocType is backed by custom logic, not by a database table.

Step 2 – Define DocFields (form structure)

Even though the DocType is virtual, you still define DocFields as usual:

  • Basic fields: Data, Int, Date, Select, Check, etc.
  • Links: Link to other DocTypes (e.g., User, Company).
  • Display fields: Section Break, Column Break, etc.

These fields define how your form and list view appear. The difference is that their values will come from your Python controller, not from a database table.

Step 3 – Implement the Virtual DocType controller

Create the DocType controller file in your app, for example:

your_app/your_app/doctype/remote_issue/remote_issue.py

A basic skeleton for a Virtual DocType might look like:

import frappe
from frappe.model.document import Document
class RemoteIssue(Document):
# Example: load data from an external API instead of DB
@staticmethod
def get_list(args):
"""Return a list of records for List View."""
# You must return a list of dicts with name and other fields
issues = fetch_remote_issues(args)
return issues
def load_from_db(self):
"""Populate document fields when loading a single record."""
data = fetch_remote_issue_by_id(self.name)
if not data:
frappe.throw("Remote Issue not found")
# Map data to DocField names
for field, value in data.items():
setattr(self, field, value)
def db_insert(self):
"""Handle create operation."""
created = create_remote_issue(self.as_dict())
# Ensure that self.name is set to some unique identifier
self.name = created["id"]
def db_update(self):
"""Handle update operation."""
update_remote_issue(self.name, self.as_dict())
def delete(self):
"""Handle delete operation."""
delete_remote_issue(self.name)

Key Methods for Virtual DocTypes

Below are the main controller methods you typically override in a Virtual DocType. Implementation details will depend on your use case.

1. get_list(args)

  • Handles queries for List View and API calls like frappe.get_list.
  • Purpose: Return a list of documents (as dicts) to show in the List View.
  • Input: args – filters, pagination, order by, etc.

Output: [{ “name”: …, “field1”: …, “field2”: … }, …]

Example:

@staticmethod
def get_list(args):
# Map Frappe filters to your external API parameters
filters = args.get("filters") or {}
limit_start = args.get("limit_start") or 0
limit_page_length = args.get("limit_page_length") or 20
data = fetch_remote_issues(filters, limit_start, limit_page_length)
# Return list of dicts with correct keys
return [
{"name": issue["id"],
"title": issue["title"],
"status": issue["status"],
}
for issue in data
]

2. load_from_db(self)

Responsible for filling the document with values when opening Form View or using frappe.get_doc.

Example:

def load_from_db(self):
data = fetch_remote_issue_by_id(self.name)
if not data:
frappe.throw(f"Remote Issue {self.name} not found")
# Map keys from the API to DocType fields
self.title = data.get("title")
self.status = data.get("status")
self.description = data.get("description")
# ...and any other mapped fields

3. db_insert(self)

Handles create operations.

Example:

def db_insert(self):
payload = {
"title": self.title,
"status": self.status,
"description": self.description,
}
created = create_remote_issue(payload)
# Ensure the name is set to a unique identifier
self.name = created["id"]

4. db_update(self)

Handles update operations.

def db_update(self):
payload = {
"title": self.title,
"status": self.status,
"description": self.description,
}
update_remote_issue(self.name, payload)

5. delete(self)

Handles delete operations:

def delete(self):
delete_remote_issue(self.name)

Using Virtual DocType in List and Form Views

How does a Virtual DocType appear in Desk?

Once your Virtual DocType is defined and the controller methods are implemented:

  • You can add it to a Workspace.
  • Users can open List View, filter, and click records.
  • Form View will load by invoking your load_from_db logic.
  • New records created from the form will invoke db_insert.
  • Updates and deletes will use db_update and delete.

From the UX perspective, users see it as a normal DocType. The magic is in the backend implementation.

Best Practices for Virtual DocTypes in Frappe v15

1. Treat external systems as the source of truth

Do not try to partially sync data into Frappe and also use a Virtual DocType for the same dataset. Choose one clear system of record to avoid inconsistencies.

2. Implement robust error handling

Your controller methods should:

  • Handle network timeouts or API failures gracefully.
  • Use frappe.throw with clear messages on failure.
  • Log errors using frappe.log_error where needed.

3. Respect permissions and security

Even though the data is external, you should:

  • Use standard Frappe Role Permissions and User Permissions.
  • Optionally enforce additional access checks inside your controller methods.

4. Keep list responses lean

Virtual DocTypes can incur network/API overhead for each list operation. Keep get_list responses lightweight:

  • Only populate essential fields for list columns.
  • Avoid making multiple nested network calls per row.

5. Cache where appropriate

For relatively static data, consider using:

  • frappe.cache for short-lived caches.
  • Efficient external API filtering to avoid fetching unnecessary data.

Example Use Cases for Virtual DocTypes

Here are some common patterns where Virtual DocType is a strong fit:

External Ticketing System

Map remote “tickets” or “issues” into a Virtual DocType so ERPNext users can view them natively within the Desk.

Cloud Inventory or Pricing Feed

Read live pricing or stock from a third-party platform and show them as documents without persisting in Frappe’s database.

Data Warehouse Views

Expose analytical or aggregated data as documents for quick review or link fields, without duplicating data.

Integration Patterns with Virtual DocTypes

Virtual DocTypes work well combined with:

  • Webhooks / Background Jobs
    Use background jobs to sync key identifiers, but keep the Virtual DocType as the on-demand, fresh view.
  • REST API Integrations
    Controllers call external REST endpoints; optional inbound webhooks can trigger refreshes or caching.
  • Event-driven systems
    Use Virtual DocTypes to inspect state in external event stores or message queues, while keeping ERPNext as the UI layer.

Troubleshooting Virtual DocTypes

Why do I get “TableMissingError” for a Virtual DocType?

If you see an error referring to missing DB tables for a Virtual DocType, it usually means some part of your code is still calling database-oriented functions (frappe.db.get_value, frappe.db.insert, etc.).
Fix: Ensure all persistence and reads are routed through your custom controller methods and external APIs, not direct database utilities.

My List View is empty or very slow

Check:

  • That get_list is returning a proper list of dicts.
  • That your external API integration is not timing out.
  • That you are not fetching unnecessary data for each row.

Form does not load correctly

Ensure:

  • load_from_db correctly populates all required DocFields.
  • self.name is a valid identifier that your external system understands.
Click to rate this post!
[Total: 0 Average: 0]