File & Photo Uploads & Excel Import/Export

This commit is contained in:
Flatlogic Bot 2026-01-21 23:12:42 +00:00
parent 50d8918174
commit a200f58095
13 changed files with 389 additions and 72 deletions

View File

@ -155,6 +155,10 @@ STATICFILES_DIRS = [
BASE_DIR / 'node_modules', BASE_DIR / 'node_modules',
] ]
# Media files (Uploads)
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Email # Email
EMAIL_BACKEND = os.getenv( EMAIL_BACKEND = os.getenv(
"EMAIL_BACKEND", "EMAIL_BACKEND",

View File

@ -27,3 +27,4 @@ urlpatterns = [
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets")
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -1,5 +1,5 @@
from django import forms from django import forms
from .models import Company, JobStatus, RequiredFolder, Job from .models import Company, JobStatus, RequiredFolder, Job, JobFile
class CompanyForm(forms.ModelForm): class CompanyForm(forms.ModelForm):
class Meta: class Meta:
@ -65,3 +65,14 @@ class JobForm(forms.ModelForm):
if not uprn: if not uprn:
return None return None
return uprn return uprn
class JobFileForm(forms.ModelForm):
class Meta:
model = JobFile
fields = ['file']
widgets = {
'file': forms.ClearableFileInput(attrs={'class': 'form-control'}),
}
class ImportJobsForm(forms.Form):
file = forms.FileField(label="Excel/CSV File", widget=forms.ClearableFileInput(attrs={'class': 'form-control'}))

View File

@ -1,4 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %}
{% block title %}Job: {{ job.job_ref }} - RepairsHub{% endblock %} {% block title %}Job: {{ job.job_ref }} - RepairsHub{% endblock %}
{% block content %} {% block content %}
<div class="container py-5"> <div class="container py-5">
@ -81,24 +82,94 @@
<div class="row g-3"> <div class="row g-3">
{% for completion in folder_completions %} {% for completion in folder_completions %}
<div class="col-md-6"> <div class="col-md-12">
<div class="card h-100 border-0 shadow-sm {% if completion.is_completed %}border-start border-4 border-success{% endif %}"> <div class="card border-0 shadow-sm {% if completion.is_completed %}border-start border-4 border-success{% endif %} mb-3">
<div class="card-body d-flex justify-content-between align-items-center py-4"> <div class="card-body p-4">
<div> <div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-1 fw-bold">{{ completion.folder.name }}</h6> <div>
<p class="text-muted smaller mb-0"> <h6 class="mb-1 fw-bold fs-5">{{ completion.folder.name }}</h6>
<i class="bi bi-file-earmark-arrow-up"></i> 0 Files <p class="text-muted smaller mb-0">
</p> <i class="bi bi-file-earmark-text"></i> {{ completion.files_list.count }} Files uploaded
</p>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3" data-bs-toggle="modal" data-bs-target="#uploadModal{{ completion.folder.id }}">
<i class="bi bi-upload me-1"></i> Upload
</button>
<form action="{% url 'toggle_folder_completion' job.pk completion.folder.id %}" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-sm {% if completion.is_completed %}btn-success{% else %}btn-outline-secondary{% endif %} rounded-pill px-3">
{% if completion.is_completed %}
<i class="bi bi-check2-circle me-1"></i> Done
{% else %}
Mark Ready
{% endif %}
</button>
</form>
</div>
</div> </div>
<form action="{% url 'toggle_folder_completion' job.pk completion.folder.id %}" method="post">
{% if completion.files_list %}
<div class="table-responsive">
<table class="table table-hover table-sm align-middle mb-0">
<thead class="table-light">
<tr>
<th>File Name</th>
<th>Uploaded By</th>
<th>Date</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for job_file in completion.files_list %}
<tr>
<td>
<a href="{{ job_file.file.url }}" target="_blank" class="text-decoration-none">
<i class="bi bi-file-earmark-arrow-down me-2"></i> {{ job_file.file.name|cut:"job_files/" }}
</a>
</td>
<td><small>{{ job_file.uploaded_by.username }}</small></td>
<td><small class="text-muted">{{ job_file.uploaded_at|date:"d/m/y H:i" }}</small></td>
<td class="text-end">
<a href="{% url 'job_delete_file' job.pk job_file.id %}" class="btn btn-link text-danger p-0" onclick="return confirm('Delete this file?')">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-3 bg-light rounded-3">
<p class="text-muted small mb-0">No files uploaded yet.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Upload Modal -->
<div class="modal fade" id="uploadModal{{ completion.folder.id }}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header border-0 bg-light">
<h5 class="modal-title">Upload to {{ completion.folder.name }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'job_upload_file' job.pk completion.folder.id %}" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-sm {% if completion.is_completed %}btn-success{% else %}btn-outline-secondary{% endif %} rounded-pill px-3"> <div class="modal-body p-4">
{% if completion.is_completed %} <div class="mb-3">
<i class="bi bi-check2-circle me-1"></i> Done <label class="form-label fw-bold">Select File</label>
{% else %} {{ file_form.file }}
Mark Ready <div class="form-text">Photos, PDFs, or documents are accepted.</div>
{% endif %} </div>
</button> </div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4">Upload File</button>
</div>
</form> </form>
</div> </div>
</div> </div>
@ -134,13 +205,13 @@
</div> </div>
</div> </div>
<div class="card border-0 shadow-sm bg-navy-dark text-white" style="background-color: var(--navy-dark);"> <div class="card border-0 shadow-sm bg-navy-dark text-white" style="background-color: #0F172A;">
<div class="card-body p-4"> <div class="card-body p-4">
<h6 class="text-uppercase text-muted smaller fw-bold mb-3">Quick Export</h6> <h6 class="text-uppercase text-muted smaller fw-bold mb-3 text-white-50">Quick Export</h6>
<p class="small text-muted mb-4">Download this job details for offline reporting.</p> <p class="small text-white-50 mb-4">Download this job details for offline reporting.</p>
<button class="btn btn-primary w-100 disabled" title="Coming soon"> <a href="{% url 'job_export' %}" class="btn btn-primary w-100 rounded-pill">
<i class="bi bi-file-earmark-pdf me-2"></i> Export to PDF <i class="bi bi-file-earmark-excel me-2"></i> Export to Excel
</button> </a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,72 @@
{% extends 'base.html' %}
{% block title %}Import Jobs - RepairsHub{% endblock %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'job_list' %}" class="text-decoration-none">Jobs</a></li>
<li class="breadcrumb-item active">Import</li>
</ol>
</nav>
<div class="card border-0 shadow-lg overflow-hidden">
<div class="bg-primary p-4 text-white">
<h2 class="h4 mb-1">Import Jobs</h2>
<p class="mb-0 opacity-75">Upload an Excel or CSV file to bulk create jobs.</p>
</div>
<div class="card-body p-5">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show mb-4" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="mb-5">
<h6 class="text-uppercase text-muted smaller fw-bold mb-3">Instructions</h6>
<ul class="small text-muted mb-0">
<li>Supported formats: <strong>.xlsx, .xls, .csv</strong></li>
<li>The following columns are recognized:
<ul class="mt-2">
<li><code>Job Ref</code> (Required, Unique)</li>
<li><code>UPRN</code> (Optional)</li>
<li><code>Address 1</code> (Required)</li>
<li><code>Postcode</code> (Required)</li>
<li><code>Status</code> (Optional, defaults to starting status)</li>
</ul>
</li>
<li>Duplicate <code>Job Ref</code> will update existing jobs.</li>
</ul>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-4">
<label class="form-label fw-bold">{{ form.file.label }}</label>
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger small mt-1">{{ form.file.errors }}</div>
{% endif %}
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary btn-lg rounded-pill">
<i class="bi bi-cloud-upload me-2"></i> Process Import
</button>
<a href="{% url 'job_list' %}" class="btn btn-link text-muted">Cancel</a>
</div>
</form>
</div>
</div>
<div class="mt-4 text-center">
<p class="text-muted small">Need a template? <a href="{% url 'job_export' %}" class="text-decoration-none">Export existing jobs</a> to see the required format.</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -7,9 +7,20 @@
<h1 class="h2 mb-1">Repair Jobs</h1> <h1 class="h2 mb-1">Repair Jobs</h1>
<p class="text-muted">Manage and track all repairs for {{ company.name }}</p> <p class="text-muted">Manage and track all repairs for {{ company.name }}</p>
</div> </div>
<a href="{% url 'job_create' %}" class="btn btn-primary"> <div class="d-flex gap-2">
<i class="bi bi-plus-lg"></i> Create New Job <div class="dropdown">
</a> <button class="btn btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-gear me-1"></i> Bulk Actions
</button>
<ul class="dropdown-menu dropdown-menu-end border-0 shadow" aria-labelledby="bulkActionsDropdown">
<li><a class="dropdown-item" href="{% url 'job_import' %}"><i class="bi bi-cloud-upload me-2 text-primary"></i> Import Jobs</a></li>
<li><a class="dropdown-item" href="{% url 'job_export' %}"><i class="bi bi-file-earmark-excel me-2 text-success"></i> Export All to Excel</a></li>
</ul>
</div>
<a href="{% url 'job_create' %}" class="btn btn-primary">
<i class="bi bi-plus-lg me-1"></i> Create New Job
</a>
</div>
</div> </div>
{% if messages %} {% if messages %}
@ -21,7 +32,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<div class="card overflow-hidden"> <div class="card border-0 shadow-sm overflow-hidden">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover align-middle mb-0"> <table class="table table-hover align-middle mb-0">
<thead class="table-light"> <thead class="table-light">
@ -44,7 +55,7 @@
<td>{{ job.uprn }}</td> <td>{{ job.uprn }}</td>
{% endif %} {% endif %}
<td> <td>
<div class="small">{{ job.address_line_1 }}</div> <div class="small fw-medium">{{ job.address_line_1 }}</div>
<div class="text-muted small">{{ job.postcode }}</div> <div class="text-muted small">{{ job.postcode }}</div>
</td> </td>
<td> <td>
@ -54,17 +65,24 @@
</td> </td>
<td class="text-muted small">{{ job.created_at|date:"M d, Y" }}</td> <td class="text-muted small">{{ job.created_at|date:"M d, Y" }}</td>
<td class="text-end pe-4"> <td class="text-end pe-4">
<a href="{% url 'job_detail' job.pk %}" class="btn btn-sm btn-outline-secondary">View</a> <div class="btn-group">
<a href="{% url 'job_update' job.pk %}" class="btn btn-sm btn-outline-primary">Edit</a> <a href="{% url 'job_detail' job.pk %}" class="btn btn-sm btn-outline-secondary">View</a>
<a href="{% url 'job_update' job.pk %}" class="btn btn-sm btn-outline-primary">Edit</a>
</div>
</td> </td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="6" class="text-center py-5"> <td colspan="{% if company.is_uprn_required %}6{% else %}5{% endif %}" class="text-center py-5">
<div class="py-4"> <div class="py-4">
<i class="bi bi-tools display-4 text-muted mb-3 d-block"></i> <div class="bg-light rounded-circle d-inline-flex p-4 mb-3">
<p class="text-muted">No jobs found. Start by creating your first repair job.</p> <i class="bi bi-tools fs-1 text-muted"></i>
<a href="{% url 'job_create' %}" class="btn btn-primary btn-sm mt-2">Create Job</a> </div>
<p class="text-muted">No jobs found. Start by creating your first repair job or import them from Excel.</p>
<div class="d-flex justify-content-center gap-2 mt-3">
<a href="{% url 'job_create' %}" class="btn btn-primary btn-sm">Create Job</a>
<a href="{% url 'job_import' %}" class="btn btn-outline-secondary btn-sm">Import Excel</a>
</div>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -2,26 +2,31 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path("", views.home, name="home"), path('', views.home, name='home'),
path("register/", views.register_view, name="register"), path('register/', views.register_view, name='register'),
path("login/", views.login_view, name="login"), path('login/', views.login_view, name='login'),
path("logout/", views.logout_view, name="logout"), path('logout/', views.logout_view, name='logout'),
path("setup/", views.company_setup, name="company_setup"),
path("dashboard/", views.dashboard, name="dashboard"),
# Job CRUD path('company-setup/', views.company_setup, name='company_setup'),
path("jobs/", views.job_list, name="job_list"), path('dashboard/', views.dashboard, name='dashboard'),
path("jobs/create/", views.job_create, name="job_create"),
path("jobs/<int:pk>/", views.job_detail, name="job_detail"), # Jobs
path("jobs/<int:pk>/edit/", views.job_update, name="job_update"), path('jobs/', views.job_list, name='job_list'),
path("jobs/<int:pk>/delete/", views.job_delete, name="job_delete"), path('jobs/create/', views.job_create, name='job_create'),
path("jobs/<int:pk>/toggle-folder/<int:folder_id>/", views.toggle_folder_completion, name="toggle_folder_completion"), path('jobs/<int:pk>/', views.job_detail, name='job_detail'),
path('jobs/<int:pk>/edit/', views.job_update, name='job_update'),
path('jobs/<int:pk>/delete/', views.job_delete, name='job_delete'),
path('jobs/<int:pk>/toggle-folder/<int:folder_id>/', views.toggle_folder_completion, name='toggle_folder_completion'),
path('jobs/<int:pk>/upload-file/<int:folder_id>/', views.job_upload_file, name='job_upload_file'),
path('jobs/<int:pk>/delete-file/<int:file_id>/', views.job_delete_file, name='job_delete_file'),
path('jobs/export/', views.job_export, name='job_export'),
path('jobs/import/', views.job_import, name='job_import'),
# Settings # Settings
path("settings/", views.settings_view, name="settings"), path('settings/', views.settings_view, name='settings'),
path("settings/status/create/", views.status_create, name="status_create"), path('settings/status/create/', views.status_create, name='status_create'),
path("settings/status/<int:pk>/edit/", views.status_update, name="status_update"), path('settings/status/<int:pk>/edit/', views.status_update, name='status_update'),
path("settings/status/<int:pk>/delete/", views.status_delete, name="status_delete"), path('settings/status/<int:pk>/delete/', views.status_delete, name='status_delete'),
path("settings/folder/create/", views.folder_create, name="folder_create"), path('settings/folder/create/', views.folder_create, name='folder_create'),
path("settings/folder/<int:pk>/delete/", views.folder_delete, name="folder_delete"), path('settings/folder/<int:pk>/delete/', views.folder_delete, name='folder_delete'),
] ]

View File

@ -1,13 +1,15 @@
import os import os
import platform import io
import pandas as pd
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login, logout, authenticate from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.db import transaction
from .models import Company, Profile, JobStatus, RequiredFolder, Job, JobFolderCompletion from django.http import HttpResponse
from .forms import CompanyForm, JobStatusForm, RequiredFolderForm, JobForm from .models import Company, Profile, JobStatus, RequiredFolder, Job, JobFolderCompletion, JobFile
from .forms import CompanyForm, JobStatusForm, RequiredFolderForm, JobForm, JobFileForm, ImportJobsForm
def home(request): def home(request):
if request.user.is_authenticated: if request.user.is_authenticated:
@ -46,7 +48,6 @@ def logout_view(request):
@login_required @login_required
def company_setup(request): def company_setup(request):
# Check if user already has a company
if request.user.profile.company: if request.user.profile.company:
return redirect('dashboard') return redirect('dashboard')
@ -59,14 +60,11 @@ def company_setup(request):
if company_form.is_valid() and status_names and default_status_idx is not None: if company_form.is_valid() and status_names and default_status_idx is not None:
with transaction.atomic(): with transaction.atomic():
company = company_form.save() company = company_form.save()
# Link user to company as ADMIN
profile = request.user.profile profile = request.user.profile
profile.company = company profile.company = company
profile.role = 'ADMIN' profile.role = 'ADMIN'
profile.save() profile.save()
# Create statuses
for i, name in enumerate(status_names): for i, name in enumerate(status_names):
if name.strip(): if name.strip():
JobStatus.objects.create( JobStatus.objects.create(
@ -76,7 +74,6 @@ def company_setup(request):
order=i order=i
) )
# Create folders
for name in folder_names: for name in folder_names:
if name.strip(): if name.strip():
RequiredFolder.objects.create( RequiredFolder.objects.create(
@ -110,7 +107,7 @@ def dashboard(request):
context = { context = {
'company': company, 'company': company,
'total_jobs': jobs.count(), 'total_jobs': jobs.count(),
'jobs': jobs.order_by('-created_at')[:5], # Recent jobs 'jobs': jobs.order_by('-created_at')[:5],
} }
return render(request, 'core/dashboard.html', context) return render(request, 'core/dashboard.html', context)
@ -143,7 +140,6 @@ def job_create(request):
job.company = company job.company = company
job.save() job.save()
# Initialize folder completions
for folder in company.required_folders.all(): for folder in company.required_folders.all():
JobFolderCompletion.objects.get_or_create(job=job, folder=folder) JobFolderCompletion.objects.get_or_create(job=job, folder=folder)
@ -167,16 +163,22 @@ def job_detail(request, pk):
company = profile.company company = profile.company
job = get_object_or_404(Job, pk=pk, company=company) job = get_object_or_404(Job, pk=pk, company=company)
# Ensure all required folders have completion records (in case new ones were added later)
for folder in company.required_folders.all(): for folder in company.required_folders.all():
JobFolderCompletion.objects.get_or_create(job=job, folder=folder) JobFolderCompletion.objects.get_or_create(job=job, folder=folder)
folder_completions = job.folder_completions.all().select_related('folder') folder_completions = job.folder_completions.all().select_related('folder')
# Get files for each folder
for completion in folder_completions:
completion.files_list = job.files.filter(folder=completion.folder)
file_form = JobFileForm()
return render(request, 'core/job_detail.html', { return render(request, 'core/job_detail.html', {
'job': job, 'job': job,
'folder_completions': folder_completions, 'folder_completions': folder_completions,
'company': company 'company': company,
'file_form': file_form
}) })
@login_required @login_required
@ -238,6 +240,141 @@ def toggle_folder_completion(request, pk, folder_id):
messages.success(request, f"Folder '{completion.folder.name}' status updated.") messages.success(request, f"Folder '{completion.folder.name}' status updated.")
return redirect('job_detail', pk=job.pk) return redirect('job_detail', pk=job.pk)
@login_required
def job_upload_file(request, pk, folder_id):
profile = request.user.profile
company = profile.company
job = get_object_or_404(Job, pk=pk, company=company)
folder = get_object_or_404(RequiredFolder, pk=folder_id, company=company)
if request.method == 'POST':
form = JobFileForm(request.POST, request.FILES)
if form.is_valid():
job_file = form.save(commit=False)
job_file.job = job
job_file.folder = folder
job_file.uploaded_by = request.user
job_file.save()
messages.success(request, f"File uploaded to {folder.name}.")
else:
messages.error(request, "Error uploading file.")
return redirect('job_detail', pk=job.pk)
@login_required
def job_delete_file(request, pk, file_id):
profile = request.user.profile
company = profile.company
job = get_object_or_404(Job, pk=pk, company=company)
job_file = get_object_or_404(JobFile, pk=file_id, job=job)
job_file.file.delete()
job_file.delete()
messages.success(request, "File deleted.")
return redirect('job_detail', pk=job.pk)
@login_required
def job_export(request):
profile = request.user.profile
company = profile.company
jobs = Job.objects.filter(company=company)
data = []
for job in jobs:
data.append({
'Job Ref': job.job_ref,
'UPRN': job.uprn,
'Address 1': job.address_line_1,
'Address 2': job.address_line_2,
'Address 3': job.address_line_3,
'Postcode': job.postcode,
'Status': job.status.name,
'Description': job.description,
'Created At': job.created_at.strftime('%Y-%m-%d %H:%M:%S'),
})
df = pd.DataFrame(data)
# Export as Excel
output = io.BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, index=False, sheet_name='Jobs')
output.seek(0)
response = HttpResponse(output, content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = f'attachment; filename="jobs_export_{company.name}.xlsx"'
return response
@login_required
def job_import(request):
profile = request.user.profile
company = profile.company
if request.method == 'POST':
form = ImportJobsForm(request.POST, request.FILES)
if form.is_valid():
file = request.FILES['file']
try:
if file.name.endswith('.csv'):
df = pd.read_csv(file)
else:
df = pd.read_excel(file)
# Basic mapping: Job Ref, UPRN, Address 1, Postcode, Status
# Expecting columns to match roughly or using a simple mapping
starting_status = company.statuses.filter(is_starting_status=True).first()
imported_count = 0
errors = []
with transaction.atomic():
for index, row in df.iterrows():
job_ref = str(row.get('Job Ref', '')).strip()
if not job_ref: continue
uprn = str(row.get('UPRN', '')).strip() if pd.notna(row.get('UPRN')) else None
addr1 = str(row.get('Address 1', '')).strip()
postcode = str(row.get('Postcode', '')).strip()
# Find status or use default
status_name = str(row.get('Status', '')).strip()
status = company.statuses.filter(name__iexact=status_name).first() or starting_status
try:
job, created = Job.objects.update_or_create(
company=company,
job_ref=job_ref,
defaults={
'uprn': uprn,
'address_line_1': addr1,
'postcode': postcode,
'status': status,
}
)
# Initialize folders
for folder in company.required_folders.all():
JobFolderCompletion.objects.get_or_create(job=job, folder=folder)
imported_count += 1
except Exception as e:
errors.append(f"Row {index+2}: {str(e)}")
if errors:
messages.warning(request, f"Imported {imported_count} jobs with some errors: {', '.join(errors[:5])}")
else:
messages.success(request, f"Successfully imported {imported_count} jobs.")
return redirect('job_list')
except Exception as e:
messages.error(request, f"Error processing file: {str(e)}")
else:
form = ImportJobsForm()
return render(request, 'core/job_import.html', {'form': form, 'company': company})
@login_required @login_required
def settings_view(request): def settings_view(request):
profile = request.user.profile profile = request.user.profile
@ -269,7 +406,6 @@ def status_create(request):
status = form.save(commit=False) status = form.save(commit=False)
status.company = profile.company status.company = profile.company
if status.is_starting_status: if status.is_starting_status:
# Unset other starting statuses
JobStatus.objects.filter(company=profile.company).update(is_starting_status=False) JobStatus.objects.filter(company=profile.company).update(is_starting_status=False)
status.save() status.save()
messages.success(request, "New status created.") messages.success(request, "New status created.")
@ -329,7 +465,6 @@ def folder_create(request):
folder = form.save(commit=False) folder = form.save(commit=False)
folder.company = profile.company folder.company = profile.company
folder.save() folder.save()
# Note: The jobs will automatically see this new folder in job_detail view
messages.success(request, f"Folder '{folder.name}' added company-wide.") messages.success(request, f"Folder '{folder.name}' added company-wide.")
return redirect('settings') return redirect('settings')
else: else: