This commit refactors the models in the 'models' app, specifically 'clients' and 'services' modules. It includes the PhoneNumberField in the Client model and adds a new Credential model to the services module. The Service model has been modified to include new fields and relationships.

This commit is contained in:
sankalp committed 2025-12-09 13:35:09 +05:45
1 parent 27ca456212
commit 8b50ff5412
24 files changed
+1483 -49

No files matched your search

+8 -3
View File
@@ -1,5 +1,10 @@
"""generated with djinit"""
from django.contrib import admin
# Register your models here.
# Import all admin modules to register them with the custom admin site
from src.admin import ( # noqa: F401
clients,
finance,
productivity,
projects,
services,
)
+137
View File
@@ -0,0 +1,137 @@
from django.contrib.admin import AdminSite
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
class DevSuiteAdminSite(AdminSite):
site_header = "DevSuite Admin"
site_title = "DevSuite Admin Portal"
index_title = "Welcome to DevSuite Administration"
def get_app_list(self, request, app_label=None):
"""
Override to organize models into logical groups.
"""
app_list = super().get_app_list(request, app_label)
model_map = {
# Clients
"Client": {
"group": "Clients",
"display_name": "Clients",
"order": 1,
},
# Finance
"Invoice": {
"group": "Finance",
"display_name": "Invoices",
"order": 1,
},
"Expense": {
"group": "Finance",
"display_name": "Expenses",
"order": 2,
},
"Payment": {
"group": "Finance",
"display_name": "Payments",
"order": 3,
},
# Productivity
"Note": {
"group": "Productivity",
"display_name": "Notes",
"order": 1,
},
"TimeEntry": {
"group": "Productivity",
"display_name": "Time Entries",
"order": 2,
},
# Projects
"Project": {
"group": "Projects",
"display_name": "Projects",
"order": 1,
},
"Milestone": {
"group": "Projects",
"display_name": "Milestones",
"order": 2,
},
"Task": {
"group": "Projects",
"display_name": "Tasks",
"order": 3,
},
# Services
"Service": {
"group": "Services",
"display_name": "Services",
"order": 1,
},
"Credential": {
"group": "Services",
"display_name": "Credentials",
"order": 2,
},
# System & Users
"User": {
"group": "System & Users",
"display_name": "Admin Users",
"order": 1,
},
"Group": {
"group": "System & Users",
"display_name": "Groups",
"order": 2,
},
}
# Group models according to the model_map
grouped_models = {}
for app in app_list:
for model in app["models"]:
model_name = model["object_name"]
if model_name in model_map:
config = model_map[model_name]
group = config["group"]
if group not in grouped_models:
grouped_models[group] = {
"name": group,
"app_label": group.lower()
.replace(" ", "_")
.replace("&", "and"),
"models": [],
}
# Use custom display name
model["name"] = config["display_name"]
model["_order"] = config["order"]
grouped_models[group]["models"].append(model)
# Sort models within each group by their order
for group_dict in grouped_models.values():
group_dict["models"].sort(key=lambda m: m.get("_order", 999))
# Define the order of groups (most frequently accessed first)
group_order = [
"Clients",
"Projects",
"Services",
"Finance",
"Productivity",
"System & Users",
]
# Build final app list in the defined order
grouped_app_list = [
grouped_models[g] for g in group_order if g in grouped_models
]
return grouped_app_list
admin_site = DevSuiteAdminSite(name="ssgd_admin")
admin_site.register(User, UserAdmin)
+73
View File
@@ -0,0 +1,73 @@
from django.contrib import admin
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from src.admin.base import admin_site
from src.models.clients import Client
@admin.register(Client, site=admin_site)
class ClientAdmin(admin.ModelAdmin):
list_display = (
"name_display",
"short_code",
"company_name",
"email",
"phone",
"created_at",
"updated_at",
)
list_display_links = ("name_display",)
list_per_page = 25
search_fields = ("name", "short_code", "email", "phone", "company_name")
list_filter = ("created_at", "updated_at")
ordering = ("name",)
fieldsets = (
(
_("Basic Info"),
{
"fields": ("name", "short_code", "company_name"),
"description": "Client’s identifying information. The short code is auto-generated if left blank.",
},
),
(
_("Contact Details"),
{
"fields": ("email", "phone", "address"),
},
),
(
_("Timestamps"),
{
"fields": ("created_at", "updated_at"),
"classes": ("collapse",), # collapsible section for cleaner UI
},
),
)
readonly_fields = ("created_at", "updated_at")
def name_display(self, obj):
"""Display name with subtle emphasis."""
return format_html(
"<strong>{}</strong><br><small style='color: #888;'>{}</small>",
obj.name,
obj.short_code or "—",
)
name_display.short_description = "Client"
def save_model(self, request, obj, form, change):
"""
Ensure short_code generation happens gracefully and show a message.
"""
if not obj.short_code:
generated = obj.generate_short_code()
obj.short_code = generated
self.message_user(
request,
f"Short code '{generated}' was automatically generated for client '{obj.name}'.",
)
super().save_model(request, obj, form, change)
+196
View File
@@ -0,0 +1,196 @@
from django.contrib import admin
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from src.admin.base import admin_site
from src.models.finance import Expense, Invoice, Payment
class PaymentInline(admin.TabularInline):
model = Payment
extra = 1
fields = ("amount", "date", "method", "transaction_id")
readonly_fields = ("transaction_id",)
show_change_link = True
verbose_name_plural = "Payments"
@admin.register(Invoice, site=admin_site)
class InvoiceAdmin(admin.ModelAdmin):
list_display = (
"invoice_number",
"client_display",
"project_display",
"amount_display",
"status_badge",
"date_issued",
"due_date",
)
list_display_links = ("invoice_number",)
list_filter = ("status", "date_issued", "due_date")
search_fields = (
"invoice_number",
"client__name",
"client__short_code",
"project__name",
)
ordering = ("-date_issued",)
list_per_page = 25
inlines = [PaymentInline]
fieldsets = (
(
_("Invoice Details"),
{
"fields": (
"client",
"project",
"invoice_number",
"amount",
"status",
),
"description": "Core invoice information. The <b>invoice number</b> is auto-generated upon creation.",
},
),
(
_("Dates"),
{
"fields": ("date_issued", "due_date"),
"classes": ("collapse",),
},
),
(
_("Metadata"),
{
"fields": ("created_at", "updated_at"),
"classes": ("collapse",),
},
),
)
readonly_fields = (
"invoice_number",
"date_issued",
"due_date",
"created_at",
"updated_at",
)
def client_display(self, obj):
return format_html(
"<strong>{}</strong><br><small>{}</small>",
obj.client.name,
obj.client.short_code,
)
client_display.short_description = "Client"
def project_display(self, obj):
return obj.project.name
project_display.short_description = "Project"
def amount_display(self, obj):
return format_html("<b>${:,.2f}</b>", obj.amount)
amount_display.short_description = "Amount"
def status_badge(self, obj):
color_map = {
"DRAFT": "#999",
"SENT": "#0066cc",
"PAID": "#28a745",
"OVERDUE": "#dc3545",
"CANCELLED": "#6c757d",
}
color = color_map.get(obj.status, "#333")
return format_html(
"<span style='color: white; background: {}; padding: 3px 8px; border-radius: 6px; font-size: 12px;'>{}</span>",
color,
obj.get_status_display(),
)
status_badge.short_description = "Status"
def save_model(self, request, obj, form, change):
if not obj.invoice_number:
obj.save() # triggers auto-generation
self.message_user(
request,
f"Invoice number {obj.invoice_number} generated for {obj.client}.",
)
else:
super().save_model(request, obj, form, change)
@admin.register(Expense, site=admin_site)
class ExpenseAdmin(admin.ModelAdmin):
list_display = (
"description",
"category",
"amount_display",
"service",
"date",
"created_at",
)
list_filter = ("category", "date", "created_at")
search_fields = ("description", "category", "service__name")
ordering = ("-date",)
fieldsets = (
(
_("Expense Info"),
{
"fields": ("service", "description", "category", "amount", "date"),
},
),
(
_("Metadata"),
{
"fields": ("created_at",),
"classes": ("collapse",),
},
),
)
readonly_fields = ("created_at",)
def amount_display(self, obj):
return format_html("<b>${:,.2f}</b>", obj.amount)
amount_display.short_description = "Amount"
@admin.register(Payment, site=admin_site)
class PaymentAdmin(admin.ModelAdmin):
list_display = (
"invoice_display",
"amount_display",
"date",
"method",
"transaction_id",
)
list_filter = ("method", "date")
search_fields = ("invoice__invoice_number", "transaction_id", "method")
ordering = ("-date",)
fieldsets = (
(
_("Payment Info"),
{
"fields": ("invoice", "amount", "date", "method", "transaction_id"),
},
),
)
def invoice_display(self, obj):
return format_html(
"<strong>{}</strong><br><small>{}</small>",
obj.invoice.invoice_number,
obj.invoice.client.name,
)
invoice_display.short_description = "Invoice"
def amount_display(self, obj):
return format_html("<b>${:,.2f}</b>", obj.amount)
amount_display.short_description = "Amount"
+133
View File
@@ -0,0 +1,133 @@
from django.contrib import admin
from django.utils.html import format_html
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
from src.admin.base import admin_site
from src.models.productivity import Note, TimeEntry
@admin.register(Note, site=admin_site)
class NoteAdmin(admin.ModelAdmin):
list_display = ("title_display", "created_at", "updated_at")
search_fields = ("title", "content")
list_filter = ("created_at", "updated_at")
ordering = ("-created_at",)
list_per_page = 25
fieldsets = (
(
_("Note Details"),
{
"fields": ("title", "content"),
},
),
(
_("Timestamps"),
{
"fields": ("created_at", "updated_at"),
"classes": ("collapse",),
},
),
)
readonly_fields = ("created_at", "updated_at")
def title_display(self, obj):
"""Show truncated title for clarity."""
return format_html("<strong>{}</strong>", obj.title[:60])
title_display.short_description = "Title"
@admin.register(TimeEntry, site=admin_site)
class TimeEntryAdmin(admin.ModelAdmin):
list_display = (
"description_display",
"project_display",
"task_display",
"start_time_display",
"end_time_display",
"duration_display",
)
list_filter = (
("project", admin.RelatedOnlyFieldListFilter),
("task", admin.RelatedOnlyFieldListFilter),
"start_time",
"end_time",
)
search_fields = ("description", "project__name", "task__name")
ordering = ("-start_time",)
list_per_page = 25
fieldsets = (
(
_("Work Details"),
{
"fields": ("project", "task", "description"),
},
),
(
_("Timing"),
{
"fields": ("start_time", "end_time", "duration"),
"description": "Duration is automatically calculated when both start and end times are provided.",
},
),
)
readonly_fields = ("duration",)
def description_display(self, obj):
return format_html("<strong>{}</strong>", obj.description[:50])
description_display.short_description = "Description"
def project_display(self, obj):
return obj.project.name if obj.project else "—"
project_display.short_description = "Project"
def task_display(self, obj):
return obj.task.name if obj.task else "—"
task_display.short_description = "Task"
def start_time_display(self, obj):
return (
localtime(obj.start_time).strftime("%Y-%m-%d %H:%M")
if obj.start_time
else "—"
)
start_time_display.short_description = "Start"
def end_time_display(self, obj):
if obj.end_time:
return localtime(obj.end_time).strftime("%Y-%m-%d %H:%M")
return format_html("<span style='color: #999;'>In Progress</span>")
end_time_display.short_description = "End"
def duration_display(self, obj):
if obj.duration:
hours = obj.duration.total_seconds() / 3600
color = "#28a745" if hours < 8 else "#f39c12"
return format_html(
"<span style='color: {}; font-weight: 600;'>{:.2f} hrs</span>",
color,
hours,
)
return format_html("<span style='color: #888;'>—</span>")
duration_display.short_description = "Duration"
def save_model(self, request, obj, form, change):
obj.save() # triggers auto duration calculation
if obj.duration:
hours = round(obj.duration.total_seconds() / 3600, 2)
self.message_user(
request, f"Duration automatically calculated: {hours} hours."
)
else:
self.message_user(
request, "Duration not calculated (in progress or missing end time)."
)
+233
View File
@@ -0,0 +1,233 @@
from django.contrib import admin
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from src.admin.base import admin_site
from src.models.projects import Milestone, Project, Task
class MilestoneInline(admin.TabularInline):
model = Milestone
extra = 1
fields = ("title", "due_date", "is_completed")
show_change_link = True
ordering = ("due_date",)
verbose_name_plural = "Milestones"
class TaskInline(admin.TabularInline):
model = Task
extra = 1
fields = ("title", "status", "due_date")
show_change_link = True
ordering = ("status", "due_date")
verbose_name_plural = "Tasks"
@admin.register(Project, site=admin_site)
class ProjectAdmin(admin.ModelAdmin):
list_display = (
"name_display",
"client_display",
"status_badge",
"start_date",
"deadline",
"budget_display",
)
list_filter = ("status", "client", "start_date", "deadline")
search_fields = ("name", "client__name", "description")
ordering = ("-created_at",)
list_per_page = 25
inlines = [MilestoneInline, TaskInline]
fieldsets = (
(
_("Project Overview"),
{
"fields": ("client", "name", "description", "status"),
},
),
(
_("Timeline"),
{
"fields": ("start_date", "deadline"),
"description": "Start and deadline dates help in tracking project progress.",
},
),
(
_("Financials"),
{
"fields": ("budget",),
},
),
(
_("Timestamps"),
{
"fields": ("created_at", "updated_at"),
"classes": ("collapse",),
},
),
)
readonly_fields = ("created_at", "updated_at")
def name_display(self, obj):
return format_html("<strong>{}</strong>", obj.name)
name_display.short_description = "Project"
def client_display(self, obj):
return obj.client.name
client_display.short_description = "Client"
def status_badge(self, obj):
color_map = {
"PLANNING": "#6c757d",
"IN_PROGRESS": "#007bff",
"ON_HOLD": "#ffc107",
"COMPLETED": "#28a745",
"CANCELLED": "#dc3545",
}
color = color_map.get(obj.status, "#999")
return format_html(
"<span style='background:{}; color:white; padding:3px 8px; border-radius:6px; font-size:12px;'>{}</span>",
color,
obj.get_status_display(),
)
status_badge.short_description = "Status"
def budget_display(self, obj):
return format_html("<b>${:,.2f}</b>", obj.budget) if obj.budget else "—"
budget_display.short_description = "Budget"
def save_model(self, request, obj, form, change):
if obj.deadline and obj.start_date and obj.deadline < obj.start_date:
self.message_user(
request,
"⚠️ Deadline cannot be earlier than start date.",
level="warning",
)
super().save_model(request, obj, form, change)
@admin.register(Milestone, site=admin_site)
class MilestoneAdmin(admin.ModelAdmin):
list_display = ("title_display", "project_display", "due_date", "status_badge")
list_filter = ("is_completed", "due_date", "project")
search_fields = ("title", "project__name")
ordering = ("due_date",)
fieldsets = (
(
_("Milestone Info"),
{
"fields": ("project", "title", "due_date", "is_completed"),
},
),
)
def title_display(self, obj):
return format_html("<strong>{}</strong>", obj.title)
title_display.short_description = "Title"
def project_display(self, obj):
return obj.project.name
project_display.short_description = "Project"
def status_badge(self, obj):
color = "#28a745" if obj.is_completed else "#dc3545"
text = "Completed" if obj.is_completed else "Pending"
return format_html(
"<span style='background:{}; color:white; padding:2px 7px; border-radius:5px; font-size:12px;'>{}</span>",
color,
text,
)
status_badge.short_description = "Status"
@admin.register(Task, site=admin_site)
class TaskAdmin(admin.ModelAdmin):
list_display = (
"title_display",
"project_display",
"status_badge",
"due_date_display",
)
list_filter = ("status", "due_date", "project")
search_fields = ("title", "project__name", "description")
ordering = ("status", "due_date")
fieldsets = (
(
_("Task Info"),
{
"fields": ("project", "title", "description", "status"),
},
),
(
_("Schedule"),
{
"fields": ("due_date",),
"description": "Due date cannot be in the past.",
},
),
(
_("Timestamps"),
{
"fields": ("created_at", "updated_at"),
"classes": ("collapse",),
},
),
)
readonly_fields = ("created_at", "updated_at")
def title_display(self, obj):
return format_html("<strong>{}</strong>", obj.title)
title_display.short_description = "Title"
def project_display(self, obj):
return obj.project.name
project_display.short_description = "Project"
def due_date_display(self, obj):
if not obj.due_date:
return format_html("<span style='color:#999;'>No due date</span>")
elif obj.due_date < timezone.localdate():
return format_html("<span style='color:#dc3545;'>{}</span>", obj.due_date)
elif obj.due_date == timezone.localdate():
return format_html("<span style='color:#f39c12;'>Today</span>")
return obj.due_date
due_date_display.short_description = "Due Date"
def status_badge(self, obj):
color_map = {
"TODO": "#6c757d",
"IN_PROGRESS": "#007bff",
"REVIEW": "#ffc107",
"DONE": "#28a745",
}
color = color_map.get(obj.status, "#333")
return format_html(
"<span style='background:{}; color:white; padding:3px 8px; border-radius:6px; font-size:12px;'>{}</span>",
color,
obj.get_status_display(),
)
status_badge.short_description = "Status"
def save_model(self, request, obj, form, change):
if obj.due_date and obj.due_date < timezone.localdate():
self.message_user(
request, "⚠️ Due date cannot be in the past.", level="warning"
)
super().save_model(request, obj, form, change)
+217
View File
@@ -0,0 +1,217 @@
from django.contrib import admin
from django.utils import timezone
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from src.admin.base import admin_site
from src.models.services import Credential, Service
class CredentialInline(admin.TabularInline):
model = Credential
extra = 1
fields = ("url", "username", "password", "notes")
show_change_link = True
verbose_name_plural = "Credentials"
def get_formset(self, request, obj=None, **kwargs):
formset = super().get_formset(request, obj, **kwargs)
formset.form.base_fields["password"].widget.attrs["type"] = "password"
return formset
@admin.register(Service, site=admin_site)
class ServiceAdmin(admin.ModelAdmin):
list_display = (
"name_display",
"client_display",
"type_badge",
"provider",
"cost_display",
"renewal_price_display",
"expiry_display",
"auto_renew_display",
)
list_filter = (
"service_type",
"auto_renew",
"provider",
"expiry_date",
"client",
)
search_fields = ("name", "client__name", "provider")
ordering = ("-created_at",)
list_per_page = 25
inlines = [CredentialInline]
fieldsets = (
(
_("Service Info"),
{
"fields": ("client", "service_type", "name", "provider"),
},
),
(
_("Billing"),
{
"fields": ("cost", "renewal_price", "auto_renew"),
},
),
(
_("Duration"),
{
"fields": ("start_date", "expiry_date"),
"description": "Start and expiry dates are used for renewal reminders.",
},
),
(
_("Timestamps"),
{
"fields": ("created_at", "updated_at"),
"classes": ("collapse",),
},
),
)
readonly_fields = ("created_at", "updated_at")
def name_display(self, obj):
return format_html("<strong>{}</strong>", obj.name)
name_display.short_description = "Service"
def client_display(self, obj):
return obj.client.name
client_display.short_description = "Client"
def type_badge(self, obj):
color_map = {
"DOMAIN": "#007bff",
"HOSTING": "#28a745",
"VPS": "#6f42c1",
"SSL": "#17a2b8",
"OTHER": "#6c757d",
}
color = color_map.get(obj.service_type, "#333")
return format_html(
"<span style='background:{}; color:white; padding:3px 8px; border-radius:6px; font-size:12px;'>{}</span>",
color,
obj.get_service_type_display(),
)
type_badge.short_description = "Type"
def cost_display(self, obj):
return format_html("<b>${:,.2f}</b>", obj.cost)
cost_display.short_description = "Cost"
def renewal_price_display(self, obj):
return format_html("<b>${:,.2f}</b>", obj.renewal_price)
renewal_price_display.short_description = "Renewal"
def expiry_display(self, obj):
"""Color-coded expiry indicator."""
if not obj.expiry_date:
return format_html("<span style='color:#888;'>—</span>")
today = timezone.localdate()
delta = (obj.expiry_date - today).days
if delta < 0:
color = "#dc3545"
label = "Expired"
elif delta <= 7:
color = "#f39c12"
label = f"Expiring ({delta}d)"
else:
color = "#28a745"
label = f"In {delta}d"
return format_html(
"<span style='color:{}; font-weight:600;'>{}</span><br><small>{}</small>",
color,
label,
obj.expiry_date,
)
expiry_display.short_description = "Expiry"
def auto_renew_display(self, obj):
icon = "✅" if obj.auto_renew else "❌"
color = "#28a745" if obj.auto_renew else "#999"
return format_html(
f"<span style='color:{color}; font-size:14px;'>{icon}</span>"
)
auto_renew_display.short_description = "Auto Renew"
def save_model(self, request, obj, form, change):
if obj.start_date and obj.expiry_date and obj.expiry_date < obj.start_date:
self.message_user(
request,
"⚠️ Expiry date cannot be earlier than start date.",
level="warning",
)
elif obj.expiry_date and obj.expiry_date < timezone.localdate():
self.message_user(
request, "⚠️ Expiry date is already in the past.", level="warning"
)
super().save_model(request, obj, form, change)
@admin.register(Credential, site=admin_site)
class CredentialAdmin(admin.ModelAdmin):
list_display = (
"service_display",
"username_display",
"url_display",
"notes_display",
)
list_filter = ("service__service_type", "service__client")
search_fields = ("username", "service__name", "notes")
ordering = ("service",)
fieldsets = (
(
_("Credential Info"),
{
"fields": ("service", "url", "username", "password", "notes"),
},
),
)
# --- DISPLAY HELPERS ---
def service_display(self, obj):
return format_html(
"<strong>{}</strong><br><small>{}</small>",
obj.service.name,
obj.service.client.name,
)
service_display.short_description = "Service"
def username_display(self, obj):
return obj.username or format_html("<span style='color:#888;'>—</span>")
username_display.short_description = "Username"
def url_display(self, obj):
if obj.url:
return format_html("<a href='{}' target='_blank'>{}</a>", obj.url, obj.url)
return format_html("<span style='color:#888;'>—</span>")
url_display.short_description = "URL"
def notes_display(self, obj):
return (
(obj.notes[:60] + "...")
if obj.notes and len(obj.notes) > 60
else (obj.notes or "—")
)
notes_display.short_description = "Notes"
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related("service", "service__client")