Bunchee
Back to Blog

วิธีสร้าง ERPNext Custom App ฉบับสมบูรณ์ 2026

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

วิธีสร้าง ERPNext Custom App ฉบับสมบูรณ์ 2026

การ Customize ERPNext ให้ตรงกับความต้องการของธุรกิจเป็นสิ่งที่หลายองค์กรต้องการ และวิธีที่ดีที่สุดคือการสร้าง ERPNext Custom App ของคุณเอง บทความนี้จะสอนวิธีสร้าง Custom App ตั้งแต่เริ่มต้นจนใช้งานได้จริง พร้อมตัวอย่างโค้ดที่นำไปใช้ได้ทันที

ปัญหา (Problem)

หลายธุรกิจที่ใช้ ERPNext มักพบปัญหาเหล่านี้:

  • ฟีเจอร์มาตรฐานไม่ตรงความต้องการ - ERPNext มีฟีเจอร์มากมาย แต่บางครั้งก็ไม่ครอบคลุมทุก workflow ของธุรกิจ
  • การแก้ไข Core Code โดยตรงเป็นอันตราย - เมื่ออัพเดท ERPNext เวอร์ชันใหม่ การแก้ไขจะหายไป
  • ต้องการเพิ่ม Custom Doctype - เช่น ระบบจัดการลูกค้าเฉพาะทาง หรือ Module พิเศษ
  • ต้องการ Override พฤติกรรมของ Doctype เดิม - เช่น เพิ่ม validation หรือ automation

วิธีแก้ไขที่ถูกต้อง คือการสร้าง Custom App ที่แยกออกจาก Core ERPNext ทำให้:

  • อัพเดท ERPNext ได้โดยไม่กระทบ customization
  • จัดการ version control ได้ง่าย
  • ย้ายไปใช้กับ site อื่นได้สะดวก

ภาพรวม Custom App Architecture

graph TB
    subgraph "Frappe Bench"
        FB[frappe-bench]
        FB --> APPS[apps/]
        FB --> SITES[sites/]
    end

    subgraph "Applications"
        APPS --> FRAPPE[frappe]
        APPS --> ERPNEXT[erpnext]
        APPS --> CUSTOM[my_custom_app]
    end

    subgraph "Custom App Structure"
        CUSTOM --> HOOKS[hooks.py]
        CUSTOM --> MODULES[modules/]
        CUSTOM --> FIXTURES[fixtures/]
        MODULES --> DOCTYPE[doctype/]
        MODULES --> EVENTS[events/]
    end

    subgraph "Site"
        SITES --> SITE1[your-site.local]
        SITE1 --> DB[(Database)]
    end

    HOOKS -->|doc_events| ERPNEXT
    DOCTYPE -->|creates tables| DB
    FIXTURES -->|exports to| CUSTOM

    style CUSTOM fill:#3498db,color:#fff
    style HOOKS fill:#e74c3c,color:#fff
    style DOCTYPE fill:#2ecc71,color:#fff

วิธีแก้ไข (Solution)

ขั้นตอนที่ 1: สร้าง Custom App ด้วย Bench CLI

เปิด 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: dev@yourcompany.com
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

ขั้นตอนที่ 2: ติดตั้ง App บน Site

bench --site your-site.local install-app my_custom_app

ตรวจสอบว่าติดตั้งสำเร็จ:

bench --site your-site.local list-apps

ผลลัพธ์ควรแสดง:

frappe
erpnext
my_custom_app

ขั้นตอนที่ 3: สร้าง Custom Doctype

Custom Doctype คือหัวใจของ Custom App สมมติเราต้องการสร้างระบบจัดการ "Project Task" แบบกำหนดเอง

3.1 สร้าง Doctype ผ่าน UI

  1. ไปที่ Settings > DocType > New

  2. กรอกข้อมูล:

    • Name: Custom Project Task
    • Module: My Custom App
    • Is Submittable: ✓ (ถ้าต้องการ workflow Submit/Cancel)
  3. เพิ่ม 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 |

  1. กด Save

3.2 สร้าง Doctype ผ่าน Code (แนะนำ)

สร้างไฟล์ custom_project_task.json ใน:

my_custom_app/my_custom_app/doctype/custom_project_task/
{
  "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
    }
  ]
}

ขั้นตอนที่ 4: เพิ่ม Business Logic ด้วย Controller

สร้างไฟล์ custom_project_task.py:

# 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
            )

ขั้นตอนที่ 5: ใช้ Hooks เพื่อ Override พฤติกรรม

ไฟล์ hooks.py เป็นหัวใจสำคัญของ Custom App ใช้กำหนดพฤติกรรมต่างๆ

Document Lifecycle Events

flowchart LR
    subgraph "Document Events"
        A[New] -->|before_insert| B[Insert]
        B -->|after_insert| C[Draft]
        C -->|before_save| D[Save]
        D -->|validate| D
        D -->|on_update| E[Saved]
        E -->|before_submit| F[Submit]
        F -->|on_submit| G[Submitted]
        G -->|before_cancel| H[Cancel]
        H -->|on_cancel| I[Cancelled]
    end

    style A fill:#3498db,color:#fff
    style G fill:#2ecc71,color:#fff
    style I fill:#e74c3c,color:#fff

คุณสามารถ hook เข้าไปที่ event ใดก็ได้เพื่อเพิ่ม business logic:

# 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"},
]

ตัวอย่าง Event Handler

สร้างไฟล์ my_custom_app/events/sales_invoice.py:

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)

ขั้นตอนที่ 6: สร้าง Custom Script (Client-side)

สำหรับ logic ที่ทำงานบน browser สร้างไฟล์ .js:

// 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'
                });
            }
        }
    }
});

ลงทะเบียน script ใน hooks.py:

# เพิ่มใน hooks.py
doctype_js = {
    "Custom Project Task": "public/js/custom_project_task.js"
}

ขั้นตอนที่ 7: ใช้ Fixtures เพื่อ Export ข้อมูล

Fixtures ช่วยให้คุณ export Custom Fields, Property Setters และข้อมูลอื่นๆ ไปกับ App:

# 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"]]
    }
]

Export fixtures:

bench --site your-site.local export-fixtures --app my_custom_app

ไฟล์จะถูกสร้างที่ my_custom_app/my_custom_app/fixtures/

ขั้นตอนที่ 8: Deploy บน Production

Deployment Flow

flowchart LR
    subgraph "Development"
        DEV[Local Dev] -->|bench new-app| APP[Custom App]
        APP -->|git push| GIT[GitHub]
    end

    subgraph "Production"
        GIT -->|bench get-app| PROD[Production Server]
        PROD -->|install-app| SITE[Site]
        SITE -->|migrate| DB[(Database)]
    end

    subgraph "Docker"
        GIT -->|apps.json| DOCKER[Docker Build]
        DOCKER -->|deploy| CONTAINER[Container]
    end

    subgraph "Frappe Cloud"
        GIT -->|Add App| CLOUD[Frappe Cloud]
        CLOUD -->|install| CLOUDSITE[Cloud Site]
    end

    style APP fill:#3498db,color:#fff
    style GIT fill:#333,color:#fff
    style SITE fill:#2ecc71,color:#fff

8.1 Push to Git

cd apps/my_custom_app
git init
git add .
git commit -m "Initial commit: My Custom App"
git remote add origin git@github.com:yourcompany/my_custom_app.git
git push -u origin main

8.2 ติดตั้งบน Production Server

# 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

8.3 ติดตั้งบน Docker ERPNext

เพิ่มใน 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 .

คำถามที่พบบ่อย (FAQ)

Custom App กับ Customization ต่างกันอย่างไร?

| ประเด็น | Custom App | Customization (UI) | |--------|------------|-------------------| | ความซับซ้อน | สูง - ต้องเขียนโค้ด | ต่ำ - ใช้ UI | | ความยืดหยุ่น | สูงมาก | จำกัด | | การย้าย site | ง่าย - export เป็น app | ยาก - ต้อง export ทีละอย่าง | | Version Control | ได้ - ใช้ Git | ไม่ได้ | | เหมาะกับ | งาน complex, ใช้หลาย site | งานง่าย, site เดียว |

จะเพิ่ม Custom Field ให้ Doctype ของ ERPNext ยังไง?

มี 2 วิธี:

วิธีที่ 1: ผ่าน UI

  1. ไปที่ Customize Form
  2. เลือก Doctype ที่ต้องการ
  3. เพิ่ม Custom Field
  4. Export เป็น Fixture

วิธีที่ 2: ผ่าน Code (แนะนำ)

# 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)

ติดตั้ง Custom App บน Frappe Cloud ยังไง?

  1. Push app ไป GitHub (public หรือ private)
  2. ไปที่ Frappe Cloud > Benches > Apps
  3. กด "Add App" แล้วใส่ URL ของ Git repository
  4. รอ build เสร็จแล้ว install บน site

จะ debug Custom App ยังไง?

# ใช้ 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.log

สรุป (Summary)

  • ERPNext Custom App คือวิธีที่ถูกต้องในการ customize ERPNext โดยไม่แก้ไข Core Code
  • ใช้คำสั่ง bench new-app เพื่อสร้าง App ใหม่
  • Custom Doctype ใช้สร้างตารางข้อมูลใหม่ตามความต้องการ
  • Hooks ใช้ override พฤติกรรมของ Doctype อื่นและกำหนด scheduled tasks
  • Fixtures ช่วย export Custom Fields และ configurations ไปกับ App
  • ควรใช้ Git ในการจัดการ version และ deploy บน production

การสร้าง Custom App อาจดูซับซ้อนในตอนแรก แต่เมื่อเข้าใจโครงสร้างแล้ว จะช่วยให้คุณ customize ERPNext ได้อย่างมีประสิทธิภาพและยั่งยืน

อ้างอิง (References)


ต้องการความช่วยเหลือในการสร้าง Custom App หรือ Customize ERPNext? ติดต่อทีม Bunchee เราให้บริการ ERPNext Customization Services สำหรับธุรกิจไทย