From 4afe6eaa3aea2f7bc7bedd17541b6047e8adebdb Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Tue, 9 Dec 2025 14:28:35 +0545 Subject: [PATCH] - update pyproject.toml and models/clients.py --- pyproject.toml | 1 + src/admin/clients.py | 44 +- src/migrations/0001_initial.py | 521 ++++++++++++++++++++++ src/migrations/0002_alter_client_phone.py | 21 + src/migrations/__init__.py | 0 src/models/clients.py | 2 +- src/settings/base.py | 1 + uv.lock | 11 + 8 files changed, 590 insertions(+), 11 deletions(-) create mode 100644 src/migrations/0001_initial.py create mode 100644 src/migrations/0002_alter_client_phone.py create mode 100644 src/migrations/__init__.py diff --git a/pyproject.toml b/pyproject.toml index 263312e..fe068d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "django-htmx>=1.27.0", "django-nepali-datetime-field>=0.8.0", "django-phonenumber-field[phonenumbers]>=8.4.0", + "babel>=2.17.0", ] [tool.ruff] diff --git a/src/admin/clients.py b/src/admin/clients.py index 22ea5ac..375242a 100644 --- a/src/admin/clients.py +++ b/src/admin/clients.py @@ -1,19 +1,32 @@ from django.contrib import admin +from django.forms import ModelForm from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ +from phonenumber_field.formfields import SplitPhoneNumberField +# Assuming these imports are correct based on your setup from src.admin.base import admin_site from src.models.clients import Client +class ClientAdminForm(ModelForm): + phone = SplitPhoneNumberField(required=True) + + class Meta: + model = Client + fields = "__all__" + + @admin.register(Client, site=admin_site) class ClientAdmin(admin.ModelAdmin): + form = ClientAdminForm + list_display = ( "name_display", "short_code", "company_name", "email", - "phone", + "formatted_phone", "created_at", "updated_at", ) @@ -42,7 +55,7 @@ class ClientAdmin(admin.ModelAdmin): _("Timestamps"), { "fields": ("created_at", "updated_at"), - "classes": ("collapse",), # collapsible section for cleaner UI + "classes": ("collapse",), }, ), ) @@ -59,15 +72,26 @@ class ClientAdmin(admin.ModelAdmin): name_display.short_description = "Client" + def formatted_phone(self, obj): + """Displays the phone number in International format on the list view.""" + if obj.phone: + # Uses the .as_international property from the PhoneNumber wrapper + return obj.phone.as_international + return "—" + + formatted_phone.short_description = "Phone" + 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 + # Check if the short_code was empty before this save operation + short_code_was_empty = not obj.short_code + + # Call the parent save method, which calls obj.save() and triggers + # the short_code generation logic you put in the model. + super().save_model(request, obj, form, change) + + # Check if it was generated and display a message + if short_code_was_empty and obj.short_code: self.message_user( request, - f"Short code '{generated}' was automatically generated for client '{obj.name}'.", + f"Short code '{obj.short_code}' was automatically generated for client '{obj.name}'.", ) - super().save_model(request, obj, form, change) diff --git a/src/migrations/0001_initial.py b/src/migrations/0001_initial.py new file mode 100644 index 0000000..0ce8683 --- /dev/null +++ b/src/migrations/0001_initial.py @@ -0,0 +1,521 @@ +# Generated by Django 6.0 on 2025-12-09 07:58 + +import django.db.models.deletion +import phonenumber_field.modelfields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Note", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(db_index=True, max_length=255)), + ("content", models.TextField()), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "ordering": ["-created_at"], + }, + ), + migrations.CreateModel( + name="Client", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(db_index=True, max_length=255)), + ("short_code", models.CharField(blank=True, max_length=6, unique=True)), + ("email", models.EmailField(max_length=254)), + ( + "phone", + phonenumber_field.modelfields.PhoneNumberField( + max_length=128, region=None + ), + ), + ("address", models.TextField()), + ("company_name", models.CharField(blank=True, max_length=255)), + ], + options={ + "ordering": ["name"], + "indexes": [ + models.Index(fields=["name"], name="src_client_name_ffc910_idx"), + models.Index( + fields=["short_code"], name="src_client_short_c_058134_idx" + ), + ], + }, + ), + migrations.CreateModel( + name="Invoice", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "invoice_number", + models.CharField(editable=False, max_length=20, unique=True), + ), + ("amount", models.DecimalField(decimal_places=2, max_digits=10)), + ("date_issued", models.DateField(auto_now_add=True)), + ("due_date", models.DateField(auto_now=True)), + ( + "status", + models.CharField( + choices=[ + ("DRAFT", "Draft"), + ("SENT", "Sent"), + ("PAID", "Paid"), + ("OVERDUE", "Overdue"), + ("CANCELLED", "Cancelled"), + ], + default="DRAFT", + max_length=10, + ), + ), + ( + "client", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invoices", + to="src.client", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Payment", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("amount", models.DecimalField(decimal_places=2, max_digits=10)), + ("date", models.DateField()), + ("method", models.CharField(blank=True, max_length=50)), + ("transaction_id", models.CharField(blank=True, max_length=100)), + ( + "invoice", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="payments", + to="src.invoice", + ), + ), + ], + ), + migrations.CreateModel( + name="Project", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(db_index=True, max_length=255)), + ("description", models.TextField()), + ("start_date", models.DateField(blank=True, null=True)), + ("deadline", models.DateField(blank=True, null=True)), + ( + "status", + models.CharField( + choices=[ + ("PLANNING", "Planning"), + ("IN_PROGRESS", "In Progress"), + ("ON_HOLD", "On Hold"), + ("COMPLETED", "Completed"), + ("CANCELLED", "Cancelled"), + ], + default="PLANNING", + max_length=20, + ), + ), + ( + "budget", + models.DecimalField( + blank=True, decimal_places=2, max_digits=10, null=True + ), + ), + ( + "client", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="projects", + to="src.client", + ), + ), + ], + options={ + "ordering": ["-created_at"], + }, + ), + migrations.CreateModel( + name="Milestone", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("title", models.CharField(max_length=255)), + ("due_date", models.DateField(blank=True, null=True)), + ("is_completed", models.BooleanField(default=False)), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="milestones", + to="src.project", + ), + ), + ], + options={ + "ordering": ["due_date"], + }, + ), + migrations.AddField( + model_name="invoice", + name="project", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="invoices", + to="src.project", + ), + ), + migrations.CreateModel( + name="Service", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "service_type", + models.CharField( + choices=[ + ("DOMAIN", "Domain"), + ("HOSTING", "Hosting"), + ("VPS", "VPS"), + ("SSL", "SSL"), + ("OTHER", "Other"), + ], + max_length=20, + ), + ), + ( + "name", + models.CharField( + db_index=True, + help_text="e.g. example.com or VPS - 4GB", + max_length=255, + ), + ), + ("provider", models.CharField(blank=True, max_length=255)), + ( + "cost", + models.DecimalField(decimal_places=2, default=0.0, max_digits=10), + ), + ( + "renewal_price", + models.DecimalField(decimal_places=2, default=0.0, max_digits=10), + ), + ("start_date", models.DateField(blank=True, null=True)), + ("expiry_date", models.DateField(blank=True, null=True)), + ("auto_renew", models.BooleanField(default=False)), + ( + "client", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="services", + to="src.client", + ), + ), + ], + options={ + "ordering": ["-created_at"], + }, + ), + migrations.CreateModel( + name="Expense", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(max_length=255)), + ("amount", models.DecimalField(decimal_places=2, max_digits=10)), + ("date", models.DateField()), + ("category", models.CharField(blank=True, max_length=100)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "service", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="expenses", + to="src.service", + ), + ), + ], + ), + migrations.CreateModel( + name="Credential", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("url", models.URLField(blank=True)), + ("username", models.CharField(blank=True, max_length=255)), + ("password", models.CharField(blank=True, max_length=255)), + ("notes", models.TextField(blank=True)), + ( + "service", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="credentials", + to="src.service", + ), + ), + ], + options={ + "ordering": ["service"], + }, + ), + migrations.CreateModel( + name="Task", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("title", models.CharField(db_index=True, max_length=255)), + ("description", models.TextField(blank=True)), + ( + "status", + models.CharField( + choices=[ + ("TODO", "To Do"), + ("IN_PROGRESS", "In Progress"), + ("REVIEW", "Review"), + ("DONE", "Done"), + ], + default="TODO", + max_length=20, + ), + ), + ("due_date", models.DateField(blank=True, null=True)), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tasks", + to="src.project", + ), + ), + ], + options={ + "ordering": ["status", "due_date"], + }, + ), + migrations.CreateModel( + name="TimeEntry", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(max_length=255)), + ("start_time", models.DateTimeField()), + ("end_time", models.DateTimeField(blank=True, null=True)), + ( + "duration", + models.DurationField(blank=True, editable=False, null=True), + ), + ( + "project", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="time_entries", + to="src.project", + ), + ), + ( + "task", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="time_entries", + to="src.task", + ), + ), + ], + options={ + "ordering": ["-start_time"], + }, + ), + migrations.AddIndex( + model_name="project", + index=models.Index(fields=["status"], name="src_project_status_58968d_idx"), + ), + migrations.AddIndex( + model_name="project", + index=models.Index( + fields=["client"], name="src_project_client__f7a74d_idx" + ), + ), + migrations.AddIndex( + model_name="milestone", + index=models.Index( + fields=["project"], name="src_milesto_project_a28344_idx" + ), + ), + migrations.AddIndex( + model_name="milestone", + index=models.Index( + fields=["due_date"], name="src_milesto_due_dat_c4307d_idx" + ), + ), + migrations.AddIndex( + model_name="service", + index=models.Index( + fields=["service_type"], name="src_service_service_a91261_idx" + ), + ), + migrations.AddIndex( + model_name="service", + index=models.Index( + fields=["client"], name="src_service_client__283c36_idx" + ), + ), + migrations.AddIndex( + model_name="service", + index=models.Index( + fields=["expiry_date"], name="src_service_expiry__bec77a_idx" + ), + ), + migrations.AddIndex( + model_name="credential", + index=models.Index( + fields=["service"], name="src_credent_service_0fdfd4_idx" + ), + ), + migrations.AddIndex( + model_name="task", + index=models.Index(fields=["project"], name="src_task_project_1c66bb_idx"), + ), + migrations.AddIndex( + model_name="task", + index=models.Index(fields=["status"], name="src_task_status_4e81ec_idx"), + ), + migrations.AddIndex( + model_name="task", + index=models.Index(fields=["due_date"], name="src_task_due_dat_68af9f_idx"), + ), + migrations.AddIndex( + model_name="timeentry", + index=models.Index( + fields=["start_time"], name="src_timeent_start_t_0e6952_idx" + ), + ), + migrations.AddIndex( + model_name="timeentry", + index=models.Index( + fields=["end_time"], name="src_timeent_end_tim_faabc6_idx" + ), + ), + migrations.AddIndex( + model_name="timeentry", + index=models.Index( + fields=["project"], name="src_timeent_project_142fd7_idx" + ), + ), + migrations.AddIndex( + model_name="timeentry", + index=models.Index(fields=["task"], name="src_timeent_task_id_c75729_idx"), + ), + ] diff --git a/src/migrations/0002_alter_client_phone.py b/src/migrations/0002_alter_client_phone.py new file mode 100644 index 0000000..b6e151e --- /dev/null +++ b/src/migrations/0002_alter_client_phone.py @@ -0,0 +1,21 @@ +# Generated by Django 6.0 on 2025-12-09 08:21 + +import phonenumber_field.modelfields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("src", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="client", + name="phone", + field=phonenumber_field.modelfields.PhoneNumberField( + max_length=128, region="NP" + ), + ), + ] diff --git a/src/migrations/__init__.py b/src/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/models/clients.py b/src/models/clients.py index 24a6d81..9866124 100644 --- a/src/models/clients.py +++ b/src/models/clients.py @@ -9,7 +9,7 @@ class Client(TimeStampedModel): name = models.CharField(max_length=255, db_index=True) short_code = models.CharField(max_length=6, unique=True, blank=True) email = models.EmailField() - phone = PhoneNumberField() + phone = PhoneNumberField(region="NP") address = models.TextField() company_name = models.CharField(max_length=255, blank=True) diff --git a/src/settings/base.py b/src/settings/base.py index d3550c3..5a740fd 100644 --- a/src/settings/base.py +++ b/src/settings/base.py @@ -23,6 +23,7 @@ THIRD_PARTY_APPS = [ "django_htmx", "nepali_datetime_field", "phonenumber_field", + "babel", ] USER_DEFINED_APPS = [ diff --git a/uv.lock b/uv.lock index 955b8e6..e69567f 100644 --- a/uv.lock +++ b/uv.lock @@ -11,11 +11,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, ] +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + [[package]] name = "backend" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "babel" }, { name = "django" }, { name = "django-components" }, { name = "django-environ" }, @@ -36,6 +46,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "babel", specifier = ">=2.17.0" }, { name = "django" }, { name = "django-components", specifier = ">=0.143.2" }, { name = "django-environ" },