DocTypes are the fundamental building blocks in ERPNext. They define data models, business logic, and user interfaces. Every document in ERPNext (Sales Order, Item, Customer, etc.) is a DocType.
# erpnext/selling/doctype/sales_order/sales_order.pyfrom erpnext.controllers.selling_controller import SellingControllerimport frappefrom frappe import _from frappe.utils import flt, getdateclass SalesOrder(SellingController): # Type hints for IDE support (auto-generated) from typing import TYPE_CHECKING if TYPE_CHECKING: from frappe.types import DF customer: DF.Link transaction_date: DF.Date items: DF.Table def validate(self): """Called before save""" super().validate() self.validate_delivery_date() self.calculate_totals() self.check_credit_limit() def validate_delivery_date(self): """Ensure delivery date is after transaction date""" if self.delivery_date and getdate(self.delivery_date) < getdate(self.transaction_date): frappe.throw(_("Delivery Date cannot be before Transaction Date")) def calculate_totals(self): """Calculate order totals""" self.total_qty = sum(flt(item.qty) for item in self.items) self.total = sum(flt(item.amount) for item in self.items) def on_submit(self): """Called after document is submitted""" super().on_submit() self.update_stock_reservation() self.update_blanket_order() def on_cancel(self): """Called when document is cancelled""" super().on_cancel() self.cancel_stock_reservation() def update_stock_reservation(self): """Reserve stock for this order""" if self.reserve_stock: for item in self.items: # Business logic for stock reservation pass# Whitelisted methods (callable from client)@frappe.whitelist()def make_sales_invoice(source_name): """Create Sales Invoice from Sales Order""" from frappe.model.mapper import get_mapped_doc def update_item(source, target, parent): target.qty = source.qty - source.billed_qty doc = get_mapped_doc( "Sales Order", source_name, { "Sales Order": { "doctype": "Sales Invoice" }, "Sales Order Item": { "doctype": "Sales Invoice Item", "postprocess": update_item } } ) return doc
Compute values on-the-fly without storing in database:
class SalesOrder(SellingController): def before_save(self): # Virtual field computed before save self.total_weight = sum( flt(item.qty) * flt(item.weight_per_unit) for item in self.items )
Validation should happen in validate() method. Use before_save() for computations.
from frappe import _from frappe.utils import flt, getdateclass SalesOrder(SellingController): def validate(self): super().validate() self.validate_mandatory_fields() self.validate_dates() self.validate_items() self.validate_credit_limit() def validate_mandatory_fields(self): if not self.customer: frappe.throw(_("Customer is mandatory")) def validate_dates(self): if self.delivery_date and getdate(self.delivery_date) < getdate(self.transaction_date): frappe.throw(_("Delivery Date cannot be before Transaction Date")) def validate_items(self): if not self.items: frappe.throw(_("Please add at least one item")) for item in self.items: if flt(item.qty) <= 0: frappe.throw(_(f"Row {item.idx}: Quantity must be greater than 0")) if flt(item.rate) < 0: frappe.throw(_(f"Row {item.idx}: Rate cannot be negative"))