Install Frappe Framework
Frappe is a full stack, batteries-included, web framework written in Python and Javascript.
It is the framework which powers ERPNext. It is pretty generic and can be used to build database driven apps.
So Whats in it
Doctypes ( similar to form )
Workflow (enable flow of form from role to role i.e user to authority)
Client Script (Manages ui Interaction in frappe frontend)
Server Script (Manages Backend using python - frappe scripts i.e validation, api servering)
How to work with frappe framework
Using Docker https://github.com/frappe/frappe_docker
Using Wsl (linux)
WSL (linux : Ubuntu)
Setup wsl windows os
https://learn.microsoft.com/en-us/windows/wsl/install
wsl --install --web-downloadwsl --installit will install ubuntuwsl -d ubuntuto login into ubuntu wsl instance (First Login will ask for user-name and password)- after login goto home dir
cd ~
setup frappe bench in ubuntu linux (or Wsl Ubuntu)
Installing Packges
-
sudo apt updateupdate packges -
sudo apt install git python-is-python3 python3-dev python3-pip redis-server libmariadb-dev mariadb-server mariadb-client pkg-configinstall required services -
sudo apt install xvfb libfontconfig -
sudo mariadb-secure-installationit will ask for root password to authorize and ask for new-password for mariadb (if you have not setup this before) -
Install Node and Yarn (https://nodejs.org/en/download)
Get Node.js® (V24…lts) for (Linux) Using (nvm) With (Yarn)
# Download and install nvm: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash # in lieu of restarting the shell \. "$HOME/.nvm/nvm.sh" # Download and install Node.js: nvm install 24 # Verify the Node.js version: node -v # Should print "v24.11.1". # Download and install Yarn: corepack enable yarn # Verify Yarn version: yarn -v
this will install node and yarn both
Setup Python Env
python --versioncheck python versionsudo apt install python3.10-venvif python version is 3.10python -m venv frappe-envcreating python environmentsource frappe-env/bin/activatesourcing the env
Bench Setup
-
pip install frappe-benchinstalling frappe-bench (we will use this to work with frappe-framework) -
bench init probcreating bench folder that contains all the configuration and setup of bench / sites / apps -
cd probdirectory where from where bench commands can be applied -
bench new-site frontendit will asks for super-user and mysql root password -
bench use frontendthis will set site to frontend -
bench startthis will start the site…/2025-12-05-test/prob ✗ bench start 15:03:03 system | redis_queue.1 started (pid=52053) 15:03:03 system | redis_cache.1 started (pid=52050) --------------------------------------------------- --------------------------------------------------- 15:03:04 web.1 | * Running on all addresses (0.0.0.0) 15:03:04 web.1 | * Running on http://127.0.0.1:8000 15:03:04 web.1 | * Running on http://10.120.10.245:8000 15:03:04 web.1 | Press CTRL+C to quit ---------------------------------------------------- ---------------------------------------------------- 15:03:11 watch.1 | Watching for changes...visit http://127.0.0.1:8000
Basic Bench Commands
Run these commands from within the prob directory (you can execute them from a nested subdirectory, as long as one of the parent directories is prob).
bench init bench_name
this commands create bench configuration and folder containing it
bench new-site sitename
creates new site and a site can have multiple selected apps and each site configuration and data does not cross
bench use sitename
to set default site from list of sites
bench start
to start site , it starts frappe backend and frontend
bench build
to generate assets of site , apps that frappe will serve
bench get-app app_name
to get frappe official product application i.e. (erpnext, hrms, wiki)
to get custom application use
bench get-app github_repo_url
bench install-app app_name
to install app into site , use –site site_name to specify site where app will be installed
bench new-app customapp
to create custom app (where you can define , custom doctype and control apis)
bench drop-site sitename
to remove site (be cautious it will delete all data related to site)
bench uninstall-app appname
to uninstall app from site , you can use –site sitename
bench export-fixtures
to export configurations and records for doctypes
you have to define what need to export using frappe-bench/apps/app_name/module_name/hooks.py
running command will generate fixtures folder frappe-bench/apps/app_name/module_name/fixtures
fixtures folder will contain json files for each records and configuration
bench migrate
to restore data and configuration that is inside fixtures folder frappe-bench/apps/app_name/module_name/fixtures
bench set-config config_name config_value
to set config like bench set-config developer_mode 1 or bench set-config server_script_enabled true
you can use -g to set globally
File Folder Structure
Frappe bench
contains all configurations
╭─frappe@custom.inxeoz.com ~/frappe-bench
╰─➤ tree -L 1
.
├── apps
├── config
├── env
├── logs
├── patches.txt
├── Procfile
└── sitesapps
contains all applications
╭─frappe@custom.inxeoz.com ~/frappe-bench/apps ╰─➤ tree -L 1 apps ├── custom_booking └── frappe
config
frappe generated config
env
python environment that frappe uses
logs
all the logs generated frappe
patches.text
all the list of patches that was applied
procfile
Defines how bench runs in production
sites
contains sites global configuration and site it self here
2 directories, 3 files ╭─frappe@custom.inxeoz.com ~/frappe-bench/sites ╰─➤ tree -L 2 sites ├── apps.json ├── apps.txt ├── assets │ ├── assets.json │ ├── assets-rtl.json │ ├── css │ ├── custom_booking -> /workspace/frappe-bench/apps/custom_booking/custom_booking/public │ ├── frappe -> /workspace/frappe-bench/apps/frappe/frappe/public │ ├── js │ └── locale ├── common_site_config.json └── custom.inxeoz.com ├── locks ├── logs ├── private ├── public ├── site_config.json └── touched_tables.json
sites/apps.json
contains application commit hash and github branch if it is from github
{ "frappe": { "resolution": { "commit_hash": null, "branch": null }, "required": [], "idx": 1, "version": "15.87.0" }, "custom_app": { "is_repo": true, "resolution": { "commit_hash": "shdskjfhdsfsddsfshdf", "branch": "develop" }, "required": [], "idx": 2, "version": "0.0.1" } }
sites/apps.txt
list of application
╭─frappe@custom.inxeoz.com ~/frappe-bench/sites ╰─➤ cat apps.txt frappe custom_app
sites/assets
contains frontend build of application (symlink to be exact)
╭─frappe@custom.inxeoz.com ~/frappe-bench/sites/assets ╰─➤ tree -L 1 assets ├── assets.json ├── assets-rtl.json ├── css ├── custom_booking -> /workspace/frappe-bench/apps/custom_booking/custom_booking/public ├── frappe -> /workspace/frappe-bench/apps/frappe/frappe/public ├── js └── locale
sites/common_site_config.json
contains global information for sites like setting
bench set-config -g key valuewith-gupdates here{ "background_workers": 1, "db_host": "global-db", "db_port": 3306, "default_site": "custom.inxeoz.com", "developer_mode": true, "dns_multitenant": true, "file_watcher_port": 6787, "frappe_user": "frappe", "gunicorn_workers": 9, "install_apps": [], "live_reload": true, "rebase_on_pull": false, "redis_cache": "redis://fm__custom_inxeoz_com__redis-cache:6379", "redis_queue": "redis://fm__custom_inxeoz_com__redis-queue:6379", "redis_socketio": "redis://fm__custom_inxeoz_com__redis-cache:6379", "restart_supervisor_on_update": true, "restart_systemd_on_update": false, "serve_default_site": true, "shallow_clone": true, "socketio_port": 80, "use_redis_auth": false, "webserver_port": 80 }
sites/custom.inxeoz.com
contains all information (configuration ) related to site
╭─frappe@custom.inxeoz.com ~/frappe-bench/sites/custom.inxeoz.com ╰─➤ tree -L 1 custom.inxeoz.com ├── locks ├── logs ├── private ├── public ├── site_config.json └── touched_tables.json
sites/custom.inxeoz.com/locks
sites/custom.inxeoz.com/logs
sites/custom.inxeoz.com/private
contains backup file sql file
╭─frappe@custom.inxeoz.com ~/frappe-bench/sites/custom.inxeoz.com/private ╰─➤ tree -L 1 private ├── backups └── files
sites/custom.inxeoz.com/public
sites/custom.inxeoz.com/site_config.json
contains configuration details of site like db credentials
and configs that is set by
bench set-config key valuewithout using-g{ "admin_password": "windwos", "db_host": "global-db", "db_name": "custom-inxeoz-com", "db_password": "windwosBEh", "db_port": 3306, "db_type": "mariadb", "hello": "word" }
sites/custom.inxeoz.com/touched_tables.json
contains list of database tables that is created or modified by frappe for site
[ "tabProperty Setter", "tabSlot Table", "tabWorkflow Action Permitted Role", "tabClient" ]
apps/custom_booking
╭─frappe@custom.inxeoz.com ~/frappe-bench/apps/custom_booking ‹develop*›
╰─➤ tree -L 5
custom_booking/
├── custom_booking/
│ ├── config/
│ ├── custom_booking/
│ │ ├── doctype/
│ │ │ ├── api/
│ │ │ ├── approver/
│ │ │ ├── attender/
│ │ │ ├── companion_table/
│ │ │ ├── devoteee/
│ │ │ │ ├── devoteee.json
│ │ │ │ ├── devoteee.js
│ │ │ │ └── devoteee.py
│ │ │ ├── ... many doctypes ...
│ ├── fixtures/
│ ├── hooks.py
│ ├── modules.txt
│ ├── patches.txt
│ ├── public/
│ └── templates/
├── license.txt
├── pyproject.toml
└── README.md
dont confuse yourself by three custom_booking (each has its own meaning)
frappe-bench/
└── apps/
└── custom_booking/ ← The app folder (bench level)
└── custom_booking/ ← The Python package (custom app name)
└── custom_booking/ ← The module
doctype folder contains all the doctype, a doctype which created with
Module = custom_booking
Custom? = Unchecked (Because Checked would create doctype in database and not map in source code )
custom_booking/
├── custom_booking/
│ ├── config/
│ ├── custom_booking/
│ │ ├── doctype/ ← Doctype folder
│ │ │ ├── api/
│ │ │ ├── approver/
│ │ │ ├── attender/
│ │ │ ├── companion_table/
│ │ │ ├── devoteee/ ← Doctype (Devoteee)
│ │ │ │ ├── devoteee.json ← Json file represent the devoteee doctype schema
│ │ │ │ ├── devoteee.js ← js file that controls that devoteee frappe ui
│ │ │ │ └── devoteee.py ← py file that handles backend control like validation
│ │ │ ├── ... many doctypes ...
│ ├── fixtures/ ← Fixtures folder (bench migrate)
│ ├── hooks.py ← configs about export and app
│ ├── modules.txt ← list of modules
│ ├── patches.txt
│ ├── public/
│ └── templates/
├── license.txt
├── pyproject.toml
└── README.mddevoteee.json
{
"actions": [],
"allow_rename": 1,
"autoname": "DEV-.########",
"creation": "2025-01-01 10:00:00.000000",
"custom": 0,
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"first_name",
"last_name",
"mobile_no",
"email",
"date_of_birth",
"address",
"user",
"is_active"
],
"fields": [
{
"fieldname": "first_name",
"fieldtype": "Data",
"label": "First Name",
"reqd": 1
},
{
"fieldname": "last_name",
"fieldtype": "Data",
"label": "Last Name"
},
{
"fieldname": "mobile_no",
"fieldtype": "Data",
"label": "Mobile Number",
"reqd": 1
},
{
"fieldname": "email",
"fieldtype": "Data",
"label": "Email",
"options": "Email"
},
{
"fieldname": "date_of_birth",
"fieldtype": "Date",
"label": "Date of Birth"
},
{
"fieldname": "address",
"fieldtype": "Small Text",
"label": "Address"
},
{
"fieldname": "user",
"fieldtype": "Link",
"label": "Linked User",
"options": "User"
},
{
"fieldname": "is_active",
"fieldtype": "Check",
"label": "Is Active",
"default": "1"
}
],
"index_web_pages_for_search": 1,
"links": [],
"module": "Custom Booking",
"name": "Devoteee",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"search_fields": "first_name,last_name,mobile_no",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "first_name",
"track_changes": 1,
"track_seen": 1
}devoteee.js
frappe.ui.form.on("Devoteee", {
refresh(frm) {
// Add a custom button
frm.add_custom_button("Get Profile", () => {
frappe.call({
method: "custom_booking.custom_booking.doctype.devoteee.devoteee.get_devotee_profile",
args: { devotee_id: frm.doc.name },
callback: function(r) {
if (r.message) {
frappe.msgprint(`
<b>Full Name:</b> ${r.message.full_name}<br>
<b>Mobile:</b> ${r.message.mobile}<br>
<b>Email:</b> ${r.message.email}<br>
<b>DOB:</b> ${r.message.dob}<br>
<b>Active:</b> ${r.message.is_active}
`);
}
}
});
});
},
// Auto-fill the title when first or last name changes
first_name(frm) {
set_title(frm);
},
last_name(frm) {
set_title(frm);
}
});
function set_title(frm) {
let title = frm.doc.first_name || "";
if (frm.doc.last_name) {
title += " " + frm.doc.last_name;
}
frm.set_value("title", title.trim());
}devoteee.py
import frappe
from frappe.model.document import Document
class Devoteee(Document):
def before_insert(self):
"""Validate mobile number before creating record."""
self.validate_mobile()
def validate(self):
"""Runs on every save."""
self.validate_mobile()
self.set_full_name()
def validate_mobile(self):
"""Ensure mobile number is 10 digits."""
if self.mobile_no and len(self.mobile_no) != 10:
frappe.throw("Mobile number must be 10 digits long")
def set_full_name(self):
"""Auto-set full name for title/representation."""
self.full_name = f"{self.first_name} {self.last_name or ''}".strip()
@frappe.whitelist()
def get_devotee_profile(devotee_id):
"""
API Example:
frappe.call({
method: "custom_booking.custom_booking.doctype.devoteee.devoteee.get_devotee_profile",
args: { devotee_id: "DEV-00000001" }
})
"""
devotee = frappe.get_doc("Devoteee", devotee_id)
return {
"full_name": devotee.full_name,
"mobile": devotee.mobile_no,
"email": devotee.email,
"dob": devotee.date_of_birth,
"is_active": devotee.is_active,
"linked_user": devotee.user
}hooks.py
app_name = "custom_booking"
app_title = "Custom Booking"
app_publisher = "Your Name / Company"
app_description = "Custom booking application for devotee and VIP slot management"
app_email = "you@example.com"
app_license = "MIT"
# Include Fixtures (Optional)
fixtures = [
"Client Script",
"Custom DocPerm"
]
# Document Event Hooks
doc_events = {
"Devoteee": {
"validate": "custom_booking.custom_booking.doctype.devoteee.devoteee.validate",
"before_insert": "custom_booking.custom_booking.doctype.devoteee.devoteee.before_insert"
}
}
# Whitelisted API Methods (Optional but clean)
override_whitelisted_methods = {
# Example:
# "devotee.get_profile": "custom_booking.custom_booking.doctype.devoteee.devoteee.get_devotee_profile"
}
# Home Page (Optional)
# home_page = "login"
# Jinja Options (Optional)
# jinja = {
# "methods": [],
# "filters": []
# }Thats it.