Setiap Senin pagi kamu harus menutup order yang kadaluarsa di database. Script Python biasa bisa melakukannya, tapi dia tidak kenal ORM Django, settings, atau model-mu. Custom management command mengisi celah itu: kode berjalan penuh di dalam konteks proyek Django, dipanggil lewat python manage.py namacommand — sama seperti migrate atau runserver.
Kenapa bukan view atau script biasa?
View butuh HTTP request, padahal tugas ini tidak ada hubungannya dengan web. Script Python polos bisa jalan, tapi kamu harus set DJANGO_SETTINGS_MODULE dan memanggil django.setup() manual setiap kali. Management command memuat semua itu otomatis, lengkap dengan ORM, settings, dan app registry. Kamu sebenarnya sudah sering memakai hasil kerjanya: startapp, makemigrations, dan createsuperuser semuanya management command bawaan. Kalau kamu belum punya proyek Django sama sekali, mulai dari membuat website pertamamu dengan Django.
Struktur Folder
Command hidup di dalam sebuah app, bukan di root proyek. Django menemukannya lewat konvensi folder management/commands.
blog/
├── management/
│ ├── __init__.py
│ └── commands/
│ ├── __init__.py
│ └── tutup_order.py
├── models.py
└── views.py
Jangan lupa file __init__.py kosong di folder management dan commands. Tanpa keduanya Python tidak menganggap folder itu sebagai package, dan Django tidak akan menemukan command-mu.
Command Pertama: hello
Mulai dari yang paling kecil. Isi tutup_order.py dengan subclass BaseCommand, lalu jalankan.
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Menutup order yang sudah kadaluarsa."
def handle(self, *args, **options):
self.stdout.write("Halo dari command sendiri.")
Jalankan dengan python manage.py tutup_order. Method handle adalah titik masuknya — seluruh logika taruh di sini.
Menambah Argument dan Option
Command tanpa parameter jarang berguna. Django memakai parser argparse di balik add_arguments, jadi sintaksnya sudah kamu kenal.
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Menutup order kadaluarsa, bisa difilter per status."
def add_arguments(self, parser):
parser.add_argument(
"--status",
default="pending",
help="Status order yang mau ditutup (default: pending)",
)
parser.add_argument(
"--hari",
type=int,
default=7,
help="Batas umur order dalam hari (default: 7)",
)
def handle(self, *args, **options):
self.stdout.write(
f"Menutup order '{options['status']}' lebih tua dari {options['hari']} hari."
)
Gunakan self.stdout.write(), bukan print(). Ia menghormati flag --verbosity dan bisa dipanggil dari test lewat call_command.
Command yang Benar-Benar Berguna
Sekarang gabungkan dengan ORM. Contoh nyata: menutup semua order berstatus pending yang dibuat lebih dari 7 hari lalu, memakai timezone.now() dan satu query update().
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils import timezone
from store.models import Order # ganti dengan app & model milikmu
class Command(BaseCommand):
help = "Menutup order pending yang lebih tua dari batas hari."
def add_arguments(self, parser):
parser.add_argument("--hari", type=int, default=7)
def handle(self, *args, **options):
batas = timezone.now() - timedelta(days=options["hari"])
orders = Order.objects.filter(
status="pending",
created_at__lt=batas,
)
jumlah = orders.update(status="closed")
self.stdout.write(self.style.SUCCESS(f"{jumlah} order ditutup."))
Jadwalkan Eksekusi dengan Cron atau systemd Timer
Command jadi berguna maksimal saat berjalan otomatis. Di server Linux, dua opsi paling umum: cron atau systemd timer. Buat file di /etc/cron.d/ untuk menjalankannya tiap Senin pukul 06:00.
# /etc/cron.d/tutup-order
0 6 * * 1 www-data cd /opt/proyek && /opt/proyek/venv/bin/python manage.py tutup_order --hari 14
Kalau kamu lebih suka systemd, pola unit service dan timernya sudah saya bahas di tutorial systemd. Cron lebih sederhana, systemd timer memberi log dan retry yang lebih baik. Kalau yang kamu butuh bukan jadwal tapi antrian tugas terdistribusi, itu ranah Celery.
Setelah tutup_order jalan di terminal, langkah berikutnya yang masuk akal: bungkus logikanya dengan pytest memakai call_command, supaya kamu yakin command tetap benar setelah refactor. Mulai dari situ, tambahkan satu command lagi tiap ada tugas repetitif yang selama ini kamu kerjakan manual lewat shell Django.