diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index ba31260..3e75121 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index b6214bf..c1198be 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 7428f1b..dfee85b 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 61533cc..cd54cbe 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 9f2c181..4c75d13 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index ed63395..0d4871d 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Company, Profile, JobStatus, RequiredFolder, Job, JobFolderCompletion, JobFile, Invitation +from .models import Company, Profile, JobStatus, RequiredFolder, Job, JobFolderCompletion, JobFile, Invitation, Client @admin.register(Company) class CompanyAdmin(admin.ModelAdmin): @@ -20,10 +20,16 @@ class RequiredFolderAdmin(admin.ModelAdmin): list_display = ('name', 'company') list_filter = ('company',) +@admin.register(Client) +class ClientAdmin(admin.ModelAdmin): + list_display = ('name', 'company', 'client_job_ref_prefix') + list_filter = ('company',) + search_fields = ('name',) + @admin.register(Job) class JobAdmin(admin.ModelAdmin): - list_display = ('job_ref', 'company', 'status', 'postcode') - list_filter = ('company', 'status') + list_display = ('job_ref', 'company', 'client', 'status', 'postcode') + list_filter = ('company', 'client', 'status') search_fields = ('job_ref', 'uprn', 'address_line_1', 'postcode') @admin.register(JobFolderCompletion) @@ -38,4 +44,4 @@ class JobFileAdmin(admin.ModelAdmin): class InvitationAdmin(admin.ModelAdmin): list_display = ('email', 'company', 'invited_by', 'created_at', 'expires_at', 'is_accepted') list_filter = ('company', 'is_accepted') - search_fields = ('email',) + search_fields = ('email',) \ No newline at end of file diff --git a/core/forms.py b/core/forms.py index df8bcc0..2ebd8b4 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,5 +1,5 @@ from django import forms -from .models import Company, JobStatus, RequiredFolder, Job, JobFile +from .models import Company, JobStatus, RequiredFolder, Job, JobFile, Client class CompanyForm(forms.ModelForm): class Meta: @@ -27,12 +27,22 @@ class RequiredFolderForm(forms.ModelForm): 'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Folder Name (e.g. Photos)'}), } +class ClientForm(forms.ModelForm): + class Meta: + model = Client + fields = ['name', 'client_job_ref_prefix'] + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Client Name'}), + 'client_job_ref_prefix': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Client Job Ref Prefix (Optional)'}), + } + class JobForm(forms.ModelForm): class Meta: model = Job - fields = ['job_ref', 'uprn', 'address_line_1', 'address_line_2', 'address_line_3', 'postcode', 'description', 'action', 'notes', 'status'] + fields = ['job_ref', 'client', 'uprn', 'address_line_1', 'address_line_2', 'address_line_3', 'postcode', 'description', 'action', 'notes', 'status'] widgets = { 'job_ref': forms.TextInput(attrs={'class': 'form-control'}), + 'client': forms.Select(attrs={'class': 'form-control'}), 'uprn': forms.TextInput(attrs={'class': 'form-control'}), 'address_line_1': forms.TextInput(attrs={'class': 'form-control'}), 'address_line_2': forms.TextInput(attrs={'class': 'form-control'}), @@ -49,6 +59,8 @@ class JobForm(forms.ModelForm): super().__init__(*args, **kwargs) if company: self.fields['status'].queryset = JobStatus.objects.filter(company=company) + self.fields['client'].queryset = Client.objects.filter(company=company) + # Set initial status to starting status if creating new job if not self.instance.pk: starting_status = JobStatus.objects.filter(company=company, is_starting_status=True).first() diff --git a/core/migrations/0003_alter_invitation_expires_at_alter_invitation_token_and_more.py b/core/migrations/0003_alter_invitation_expires_at_alter_invitation_token_and_more.py new file mode 100644 index 0000000..60895b1 --- /dev/null +++ b/core/migrations/0003_alter_invitation_expires_at_alter_invitation_token_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.7 on 2026-01-22 08:18 + +import datetime +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_invitation'), + ] + + operations = [ + migrations.AlterField( + model_name='invitation', + name='expires_at', + field=models.DateTimeField(default=datetime.datetime(2026, 1, 29, 8, 18, 26, 436210, tzinfo=datetime.timezone.utc)), + ), + migrations.AlterField( + model_name='invitation', + name='token', + field=models.CharField(default='aae0108e4b4341a0a0125d95d724aa32', max_length=32, unique=True), + ), + migrations.CreateModel( + name='Client', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('client_job_ref_prefix', models.CharField(blank=True, max_length=50, null=True)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='clients', to='core.company')), + ], + options={ + 'unique_together': {('company', 'name')}, + }, + ), + ] diff --git a/core/migrations/0004_job_client_alter_invitation_expires_at_and_more.py b/core/migrations/0004_job_client_alter_invitation_expires_at_and_more.py new file mode 100644 index 0000000..04b9ded --- /dev/null +++ b/core/migrations/0004_job_client_alter_invitation_expires_at_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.2.7 on 2026-01-22 08:28 + +import datetime +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_alter_invitation_expires_at_alter_invitation_token_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='client', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='jobs', to='core.client'), + ), + migrations.AlterField( + model_name='invitation', + name='expires_at', + field=models.DateTimeField(default=datetime.datetime(2026, 1, 29, 8, 28, 36, 55880, tzinfo=datetime.timezone.utc)), + ), + migrations.AlterField( + model_name='invitation', + name='token', + field=models.CharField(default='abd5edeff7af491ca5e08dc0adbbc6b7', max_length=32, unique=True), + ), + ] diff --git a/core/migrations/__pycache__/0003_alter_invitation_expires_at_alter_invitation_token_and_more.cpython-311.pyc b/core/migrations/__pycache__/0003_alter_invitation_expires_at_alter_invitation_token_and_more.cpython-311.pyc new file mode 100644 index 0000000..2f35899 Binary files /dev/null and b/core/migrations/__pycache__/0003_alter_invitation_expires_at_alter_invitation_token_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0004_job_client_alter_invitation_expires_at_and_more.cpython-311.pyc b/core/migrations/__pycache__/0004_job_client_alter_invitation_expires_at_and_more.cpython-311.pyc new file mode 100644 index 0000000..a30f913 Binary files /dev/null and b/core/migrations/__pycache__/0004_job_client_alter_invitation_expires_at_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index ce6efa8..42153c5 100644 --- a/core/models.py +++ b/core/models.py @@ -46,8 +46,20 @@ class RequiredFolder(models.Model): def __str__(self): return f"{self.name} ({self.company.name})" +class Client(models.Model): + company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='clients') + name = models.CharField(max_length=255) + client_job_ref_prefix = models.CharField(max_length=50, blank=True, null=True) + + class Meta: + unique_together = [['company', 'name']] + + def __str__(self): + return self.name + class Job(models.Model): company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name='jobs') + client = models.ForeignKey(Client, on_delete=models.PROTECT, related_name='jobs', null=True, blank=True) job_ref = models.CharField(max_length=100) uprn = models.CharField(max_length=100, null=True, blank=True) address_line_1 = models.CharField(max_length=255) diff --git a/core/templates/core/client_confirm_delete.html b/core/templates/core/client_confirm_delete.html new file mode 100644 index 0000000..5bdc72c --- /dev/null +++ b/core/templates/core/client_confirm_delete.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% block title %}Delete Client{% endblock %} +{% block content %} +
Are you sure you want to delete this client? This action cannot be undone.
+ +| Job Ref | +Address | +Status | +Actions | +
|---|---|---|---|
| {{ job.job_ref }} | +{{ job.address_line_1 }}, {{ job.postcode }} | +{{ job.status.name }} | ++ View + | +
No jobs found or clients assigned.
+ {% endif %} +