Use this file to discover all available pages before exploring further.
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"))