เรียนรู้วิธีสร้าง ERPNext Custom App ตั้งแต่เริ่มต้น พร้อมตัวอย่างโค้ด Custom Doctype, Hooks และ Fixtures สำหรับ Frappe Framework

การ Customize ERPNext ให้ตรงกับความต้องการของธุรกิจเป็นสิ่งที่หลายองค์กรต้องการ และวิธีที่ดีที่สุดคือการสร้าง ERPNext Custom App ของคุณเอง บทความนี้จะสอนวิธีสร้าง Custom App ตั้งแต่เริ่มต้นจนใช้งานได้จริง พร้อมตัวอย่างโค้ดที่นำไปใช้ได้ทันที
หลายธุรกิจที่ใช้ ERPNext มักพบปัญหาเหล่านี้:
วิธีแก้ไขที่ถูกต้อง คือการสร้าง Custom App ที่แยกออกจาก Core ERPNext ทำให้:
เปิด Terminal แล้วไปที่ directory ของ Frappe Bench:
cd ~/frappe-benchสร้าง Custom App ใหม่ด้วยคำสั่ง:
bench new-app my_custom_appระบบจะถามข้อมูลเบื้องต้น:
App Title (default: My Custom App): My Custom App
App Description: Custom ERPNext modules for my business
App Publisher: Your Company Name
App Email: [email protected]
App Icon (default: octicon octicon-file-directory):
App Color (default: grey): #3498db
App License (default: MIT): MIT
โครงสร้างไฟล์ที่ได้:
apps/my_custom_app/
├── my_custom_app/
│ ├── __init__.py
│ ├── hooks.py # สำคัญ! กำหนดพฤติกรรมของ App
│ ├── modules.txt # รายชื่อ modules
│ ├── patches.txt # database patches
│ ├── templates/
│ └── my_custom_app/ # module หลัก
│ ├── __init__.py
│ └── doctype/
├── setup.py
├── requirements.txt
└── README.md
bench --site your-site.local install-app my_custom_appตรวจสอบว่าติดตั้งสำเร็จ:
bench --site your-site.local list-appsผลลัพธ์ควรแสดง:
frappe
erpnext
my_custom_app
Custom Doctype คือหัวใจของ Custom App สมมติเราต้องการสร้างระบบจัดการ "Project Task" แบบกำหนดเอง
ไปที่ Settings > DocType > New
กรอกข้อมูล:
เพิ่ม Fields:
| Label | Fieldname | Type | Options |
|---|---|---|---|
| Task Name | task_name | Data | - |
| Description | description | Text Editor | - |
| Status | status | Select | Open\nIn Progress\nCompleted\nCancelled |
| Priority | priority | Select | Low\nMedium\nHigh\nCritical |
| Assigned To | assigned_to | Link | User |
| Due Date | due_date | Date | - |
| Project | project | Link | Project |
สร้างไฟล์ custom_project_task.json ใน:
my_custom_app/my_custom_app/doctype/custom_project_task/
สร้างไฟล์ custom_project_task.py:
ไฟล์ hooks.py เป็นหัวใจสำคัญของ Custom App ใช้กำหนดพฤติกรรมต่างๆ
คุณสามารถ hook เข้าไปที่ event ใดก็ได้เพื่อเพิ่ม business logic:
สร้างไฟล์ my_custom_app/events/sales_invoice.py:
สำหรับ logic ที่ทำงานบน browser สร้างไฟล์ .js:
ลงทะเบียน script ใน hooks.py:
# เพิ่มใน hooks.py
doctype_js = {
"Custom Project Task": "public/js/custom_project_task.js"
}Fixtures ช่วยให้คุณ export Custom Fields, Property Setters และข้อมูลอื่นๆ ไปกับ App:
Export fixtures:
bench --site your-site.local export-fixtures --app my_custom_appไฟล์จะถูกสร้างที่ my_custom_app/my_custom_app/fixtures/
cd apps/my_custom_app
git init
git add .
git commit -m "Initial commit: My Custom App"
git remote add origin [email protected]:yourcompany/my_custom_app.git
git push -u origin main# SSH เข้า production server
cd ~/frappe-bench
# ดึง app จาก git
bench get-app https://github.com/yourcompany/my_custom_app.git
# ติดตั้งบน site
bench --site production-site.com install-app my_custom_app
# Migrate database
bench --site production-site.com migrate
# Build assets
bench build --app my_custom_app
# Restart
sudo supervisorctl restart allเพิ่มใน apps.json:
[
{
"url": "https://github.com/yourcompany/my_custom_app.git",
"branch": "main"
}
]Build Docker image ใหม่:
docker build --build-arg=APPS_JSON_BASE64=$(base64 -w 0 apps.json) \
-t my-erpnext:latest .| ประเด็น | Custom App | Customization (UI) |
|---|---|---|
| ความซับซ้อน | สูง - ต้องเขียนโค้ด | ต่ำ - ใช้ UI |
| ความยืดหยุ่น | สูงมาก | จำกัด |
| การย้าย site | ง่าย - export เป็น app | ยาก - ต้อง export ทีละอย่าง |
| Version Control | ได้ - ใช้ Git | ไม่ได้ |
| เหมาะกับ | งาน complex, ใช้หลาย site | งานง่าย, site เดียว |
มี 2 วิธี:
วิธีที่ 1: ผ่าน UI
วิธีที่ 2: ผ่าน Code (แนะนำ)
# ใช้ frappe.log_error() บันทึก error
frappe.log_error(message=str(data), title="Debug Info")
# ใช้ frappe.throw() แสดง error message
frappe.throw("Something went wrong!")
# ใช้ print() แล้วดูที่ bench console
print(f"Debug: {variable}")ดู log:
# Error logs
tail -f ~/frappe-bench/logs/frappe.log
# Scheduler logs
tail -f ~/frappe-bench/logs/scheduler.logbench new-app เพื่อสร้าง App ใหม่การสร้าง Custom App อาจดูซับซ้อนในตอนแรก แต่เมื่อเข้าใจโครงสร้างแล้ว จะช่วยให้คุณ customize ERPNext ได้อย่างมีประสิทธิภาพและยั่งยืน
ต้องการความช่วยเหลือในการสร้าง Custom App หรือ Customize ERPNext? ติดต่อทีม Bunchee เราให้บริการ ERPNext Customization Services สำหรับธุรกิจไทย
{
"name": "Custom Project Task",
"module": "My Custom App",
"doctype": "DocType",
"engine": "InnoDB",
"is_submittable": 1,
"naming_rule": "By fieldname",
"autoname": "field:task_name",
"fields": [
{
"fieldname": "task_name",
"fieldtype": "Data",
"label": "Task Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description"
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Open\nIn Progress\nCompleted\nCancelled",
"default": "Open"
},
{
"fieldname": "priority",
"fieldtype": "Select",
"label": "Priority",
"options": "Low\nMedium\nHigh\nCritical",
"default": "Medium"
},
{
"fieldname": "assigned_to",
"fieldtype": "Link",
"label": "Assigned To",
"options": "User"
},
{
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"permissions": [
{
"role": "System Manager",
"read": 1,
"write": 1,
"create": 1,
"delete": 1,
"submit": 1,
"cancel": 1
}
]
}# my_custom_app/my_custom_app/doctype/custom_project_task/custom_project_task.py
import frappe
from frappe.model.document import Document
from frappe.utils import getdate, nowdate
class CustomProjectTask(Document):
def validate(self):
"""ตรวจสอบข้อมูลก่อนบันทึก"""
self.validate_due_date()
self.set_priority_color()
def validate_due_date(self):
"""ตรวจสอบว่า Due Date ไม่ใช่วันที่ผ่านมาแล้ว"""
if self.due_date and getdate(self.due_date) < getdate(nowdate()):
frappe.throw("Due Date ต้องไม่ใช่วันที่ผ่านมาแล้ว")
def set_priority_color(self):
"""กำหนดสีตาม Priority"""
color_map = {
"Low": "green",
"Medium": "blue",
"High": "orange",
"Critical": "red"
}
self.priority_color = color_map.get(self.priority, "grey")
def on_submit(self):
"""เมื่อ Submit document"""
self.notify_assigned_user()
def notify_assigned_user(self):
"""ส่ง notification ไปยังผู้รับผิดชอบ"""
if self.assigned_to:
frappe.publish_realtime(
event="new_task_assigned",
message={
"task": self.name,
"task_name": self.task_name,
"assigned_by": frappe.session.user
},
user=self.assigned_to
)# my_custom_app/hooks.py
app_name = "my_custom_app"
app_title = "My Custom App"
app_publisher = "Your Company"
app_description = "Custom ERPNext modules"
app_version = "1.0.0"
# Document Events - Override พฤติกรรมของ Doctype อื่น
doc_events = {
"Sales Invoice": {
"on_submit": "my_custom_app.events.sales_invoice.on_submit",
"on_cancel": "my_custom_app.events.sales_invoice.on_cancel"
},
"Customer": {
"after_insert": "my_custom_app.events.customer.after_insert"
}
}
# Scheduled Tasks - งานที่รันอัตโนมัติ
scheduler_events = {
"daily": [
"my_custom_app.tasks.daily.check_overdue_tasks"
],
"hourly": [
"my_custom_app.tasks.hourly.sync_external_data"
]
}
# Override Whitelisted Methods
override_whitelisted_methods = {
"frappe.client.get_count": "my_custom_app.overrides.custom_get_count"
}
# Fixtures - ข้อมูลที่ต้อง export ไปกับ App
fixtures = [
{"dt": "Custom Field", "filters": [["module", "=", "My Custom App"]]},
{"dt": "Property Setter", "filters": [["module", "=", "My Custom App"]]},
{"dt": "Role", "filters": [["name", "in", ["Custom Task Manager"]]]},
]
# Website Route Rules
website_route_rules = [
{"from_route": "/tasks/<task>", "to_route": "task_detail"},
]import frappe
def on_submit(doc, method):
"""ทำงานเมื่อ Sales Invoice ถูก Submit"""
# เพิ่ม loyalty points ให้ลูกค้า
add_loyalty_points(doc)
# ส่ง notification
send_invoice_notification(doc)
def add_loyalty_points(doc):
"""คำนวณและเพิ่ม loyalty points"""
points = doc.grand_total // 100 # 1 point ต่อ 100 บาท
if points > 0:
frappe.get_doc({
"doctype": "Loyalty Point Entry",
"customer": doc.customer,
"points": points,
"invoice": doc.name
}).insert(ignore_permissions=True)
def send_invoice_notification(doc):
"""ส่ง email แจ้งเตือนใบแจ้งหนี้"""
frappe.sendmail(
recipients=[doc.contact_email],
subject=f"ใบแจ้งหนี้ {doc.name}",
template="invoice_notification",
args={"doc": doc}
)
def on_cancel(doc, method):
"""ทำงานเมื่อ Sales Invoice ถูก Cancel"""
# ยกเลิก loyalty points
cancel_loyalty_points(doc)// my_custom_app/public/js/custom_project_task.js
frappe.ui.form.on('Custom Project Task', {
refresh: function(frm) {
// เพิ่มปุ่ม custom
if (frm.doc.status === 'Open') {
frm.add_custom_button(__('Start Task'), function() {
frm.set_value('status', 'In Progress');
frm.save();
}, __('Actions'));
}
if (frm.doc.status === 'In Progress') {
frm.add_custom_button(__('Complete Task'), function() {
frm.set_value('status', 'Completed');
frm.save();
}, __('Actions'));
}
},
priority: function(frm) {
// เปลี่ยนสี indicator ตาม priority
const colors = {
'Critical': 'red',
'High': 'orange',
'Medium': 'blue',
'Low': 'green'
};
frm.set_indicator_formatter('priority', (doc) => colors[doc.priority]);
},
due_date: function(frm) {
// แจ้งเตือนถ้า due date ใกล้จะถึง
if (frm.doc.due_date) {
const due = frappe.datetime.str_to_obj(frm.doc.due_date);
const today = frappe.datetime.str_to_obj(frappe.datetime.nowdate());
const diff = frappe.datetime.get_diff(due, today);
if (diff <= 3 && diff >= 0) {
frappe.show_alert({
message: __('Due date is approaching! Only {0} days left.', [diff]),
indicator: 'orange'
});
}
}
}
});# hooks.py
fixtures = [
# Export Custom Fields ทั้งหมดของ Module นี้
{
"dt": "Custom Field",
"filters": [["module", "=", "My Custom App"]]
},
# Export Property Setters (การแก้ไข properties ของ field เดิม)
{
"dt": "Property Setter",
"filters": [["module", "=", "My Custom App"]]
},
# Export Roles ที่สร้างขึ้น
{
"dt": "Role",
"filters": [["name", "in", ["Custom Task Manager", "Task User"]]]
},
# Export Workflow
{
"dt": "Workflow",
"filters": [["document_type", "=", "Custom Project Task"]]
},
# Export Print Format
{
"dt": "Print Format",
"filters": [["doc_type", "=", "Custom Project Task"]]
}
]# my_custom_app/install.py
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def after_install():
custom_fields = {
"Customer": [
{
"fieldname": "loyalty_tier",
"fieldtype": "Select",
"label": "Loyalty Tier",
"options": "Bronze\nSilver\nGold\nPlatinum",
"insert_after": "customer_group"
}
]
}
create_custom_fields(custom_fields)