feat: Implement order creation form and fix gitignore

This commit is contained in:
Flatlogic Bot 2025-11-29 02:58:32 +00:00
parent b545d0aaef
commit 87e38f365a
36 changed files with 259 additions and 201 deletions

26
.gitignore vendored
View File

@ -1,3 +1,29 @@
node_modules/
*/node_modules/
*/build/
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
pip-log.txt
pip-delete-this-directory.txt
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.pot
*.mo
# Django
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

7
core/forms.py Normal file
View File

@ -0,0 +1,7 @@
from django import forms
class OrderForm(forms.Form):
client_name = forms.CharField(max_length=100)
client_email = forms.EmailField()
property_address = forms.CharField(widget=forms.Textarea)
description = forms.CharField(widget=forms.Textarea, required=False)

View File

@ -1,9 +1,10 @@
# Generated by Django 5.2.7 on 2025-11-27 23:25
# Generated by Django 5.2.7 on 2025-11-29 01:21
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
@ -39,6 +40,7 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('user_type', models.CharField(choices=[('internal', 'Internal'), ('client', 'Client')], max_length=10)),
('role', models.CharField(choices=[('ORG_ADMIN', 'Org admin'), ('SUPER_ADMIN', 'Super admin'), ('JUNIOR_APPRAISER', 'Junior appraiser'), ('SENIOR_APPRAISER', 'Senior appraiser'), ('DESIGNATED_APPRAISER', 'Designated appraiser'), ('CLIENT_USER', 'Client user'), ('CLIENT_MANAGER', 'Client manager'), ('CLIENT_ADMIN', 'Client admin')], default='CLIENT_USER', max_length=20)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='core_user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='core_user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='users', to='core.organization')),
@ -52,4 +54,78 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Invoice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('invoice_number', models.CharField(max_length=50)),
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
('due_date', models.DateField()),
('status', models.CharField(choices=[('DRAFT', 'Draft'), ('SENT', 'Sent'), ('PAID', 'Paid'), ('CANCELLED', 'Cancelled')], default='DRAFT', max_length=20)),
('client', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order_date', models.DateField(auto_now_add=True)),
('status', models.CharField(choices=[('PENDING', 'Pending'), ('IN_PROGRESS', 'In Progress'), ('COMPLETED', 'Completed'), ('CANCELLED', 'Cancelled')], default='PENDING', max_length=20)),
('description', models.TextField(blank=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Project',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('start_date', models.DateField()),
('end_date', models.DateField(blank=True, null=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to=settings.AUTH_USER_MODEL)),
('invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='core.invoice')),
],
),
migrations.CreateModel(
name='Property',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('address', models.CharField(max_length=255)),
('city', models.CharField(max_length=255)),
('state', models.CharField(max_length=2)),
('zip_code', models.CharField(max_length=10)),
('property_type', models.CharField(choices=[('SINGLE_FAMILY', 'Single Family'), ('MULTI_FAMILY', 'Multi-Family'), ('COMMERCIAL', 'Commercial')], max_length=20)),
('square_footage', models.PositiveIntegerField()),
('bedrooms', models.PositiveIntegerField()),
('bathrooms', models.DecimalField(decimal_places=1, max_digits=3)),
('year_built', models.PositiveIntegerField()),
('description', models.TextField()),
('status', models.CharField(choices=[('FOR_SALE', 'For Sale'), ('FOR_RENT', 'For Rent'), ('SOLD', 'Sold')], max_length=20)),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='properties', to='core.organization')),
],
),
migrations.CreateModel(
name='Appraisal',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('appraisal_date', models.DateField()),
('appraised_value', models.DecimalField(decimal_places=2, max_digits=12)),
('notes', models.TextField(blank=True)),
('appraiser', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='appraisals', to=settings.AUTH_USER_MODEL)),
('invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appraisals', to='core.invoice')),
('order', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appraisals', to='core.order')),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='appraisals', to='core.property')),
],
),
migrations.CreateModel(
name='PropertyPhoto',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='property_photos/')),
('caption', models.CharField(blank=True, max_length=255)),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='core.property')),
],
),
]

View File

@ -0,0 +1,17 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='appraisal',
name='invoice',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appraisals', to='core.invoice'),
),
]

View File

@ -1,29 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-27 23:25
from django.db import migrations
ROLES = [
"Super admin",
"Org admin",
"Senior_appraiser",
"Junior_appraiser",
"Designated_appraiser",
"Client_user",
"Client_manager",
"Client_admin",
]
def create_groups(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
for role in ROLES:
Group.objects.create(name=role)
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.RunPython(create_groups),
]

View File

@ -1,42 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-27 23:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_create_groups'),
]
operations = [
migrations.CreateModel(
name='Property',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('address', models.CharField(max_length=255)),
('city', models.CharField(max_length=255)),
('state', models.CharField(max_length=2)),
('zip_code', models.CharField(max_length=10)),
('property_type', models.CharField(choices=[('SINGLE_FAMILY', 'Single Family'), ('MULTI_FAMILY', 'Multi-Family'), ('COMMERCIAL', 'Commercial')], max_length=20)),
('square_footage', models.PositiveIntegerField()),
('bedrooms', models.PositiveIntegerField()),
('bathrooms', models.DecimalField(decimal_places=1, max_digits=3)),
('year_built', models.PositiveIntegerField()),
('description', models.TextField()),
('status', models.CharField(choices=[('FOR_SALE', 'For Sale'), ('FOR_RENT', 'For Rent'), ('SOLD', 'Sold')], max_length=20)),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='properties', to='core.organization')),
],
),
migrations.CreateModel(
name='PropertyPhoto',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='property_photos/')),
('caption', models.CharField(blank=True, max_length=255)),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='core.property')),
],
),
]

View File

@ -0,0 +1,15 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0002_add_invoice_to_appraisal'),
]
operations = [
migrations.RemoveField(
model_name='appraisal',
name='invoice',
),
]

View File

@ -0,0 +1,17 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('core', '0003_remove_invoice_from_appraisal'),
]
operations = [
migrations.AddField(
model_name='appraisal',
name='invoice',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='appraisals', to='core.invoice'),
),
]

View File

@ -1,47 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-28 02:14
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_property_propertyphoto'),
]
operations = [
migrations.CreateModel(
name='Project',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('description', models.TextField(blank=True)),
('start_date', models.DateField()),
('end_date', models.DateField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='Appraisal',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('appraisal_date', models.DateField()),
('appraised_value', models.DecimalField(decimal_places=2, max_digits=12)),
('notes', models.TextField(blank=True)),
('appraiser', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='appraisals', to=settings.AUTH_USER_MODEL)),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='appraisals', to='core.property')),
],
),
migrations.CreateModel(
name='Invoice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('invoice_number', models.CharField(max_length=50)),
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
('due_date', models.DateField()),
('status', models.CharField(choices=[('DRAFT', 'Draft'), ('SENT', 'Sent'), ('PAID', 'Paid'), ('CANCELLED', 'Cancelled')], default='DRAFT', max_length=20)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to='core.project')),
],
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2025-11-29 01:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0004_add_invoice_to_appraisal'),
]
operations = [
migrations.RemoveField(
model_name='appraisal',
name='invoice',
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-28 04:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_project_appraisal_invoice'),
]
operations = [
migrations.AddField(
model_name='user',
name='user_role_temp',
field=models.CharField(choices=[('ORG_ADMIN', 'Org admin'), ('SUPER_ADMIN', 'Super admin'), ('JUNIOR_APPRAISER', 'Junior appraiser'), ('SENIOR_APPRAISER', 'Senior appraiser'), ('DESIGNATED_APPRAISER', 'Designated appraiser'), ('CLIENT_USER', 'Client user'), ('CLIENT_MANAGER', 'Client manager'), ('CLIENT_ADMIN', 'Client admin')], default='CLIENT_USER', max_length=20),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.3 on 2025-11-29 02:30
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_remove_appraisal_invoice'),
]
operations = [
migrations.AddField(
model_name='project',
name='client',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='projects', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.0.6 on 2025-11-28 20:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0005_user_user_role_temp'),
]
operations = [
migrations.RenameField(
model_name='user',
old_name='user_role_temp',
new_name='role',
),
]

View File

@ -77,25 +77,6 @@ class PropertyPhoto(models.Model):
def __str__(self):
return f"Photo for {self.property.name}"
class Appraisal(models.Model):
property = models.ForeignKey(Property, on_delete=models.CASCADE, related_name='appraisals')
appraiser = models.ForeignKey(User, on_delete=models.CASCADE, related_name='appraisals')
appraisal_date = models.DateField()
appraised_value = models.DecimalField(max_digits=12, decimal_places=2)
notes = models.TextField(blank=True)
def __str__(self):
return f"Appraisal for {self.property.name} on {self.appraisal_date}"
class Project(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
start_date = models.DateField()
end_date = models.DateField(null=True, blank=True)
def __str__(self):
return self.name
class Invoice(models.Model):
STATUS_CHOICES = [
('DRAFT', 'Draft'),
@ -103,11 +84,49 @@ class Invoice(models.Model):
('PAID', 'Paid'),
('CANCELLED', 'Cancelled'),
]
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='invoices')
client = models.ForeignKey('User', on_delete=models.CASCADE, related_name='invoices', null=True, blank=True)
invoice_number = models.CharField(max_length=50)
amount = models.DecimalField(max_digits=10, decimal_places=2)
due_date = models.DateField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='DRAFT')
def __str__(self):
return f"Invoice {self.invoice_number} for {self.project.name}"
return f"Invoice {self.invoice_number}"
class Order(models.Model):
STATUS_CHOICES = [
('PENDING', 'Pending'),
('IN_PROGRESS', 'In Progress'),
('COMPLETED', 'Completed'),
('CANCELLED', 'Cancelled'),
]
client = models.ForeignKey('User', on_delete=models.CASCADE, related_name='orders')
order_date = models.DateField(auto_now_add=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
description = models.TextField(blank=True)
def __str__(self):
return f"Order #{self.id} for {self.client.username}"
class Project(models.Model):
client = models.ForeignKey(User, on_delete=models.CASCADE, related_name='projects')
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
start_date = models.DateField()
end_date = models.DateField(null=True, blank=True)
invoice = models.ForeignKey(Invoice, on_delete=models.SET_NULL, null=True, blank=True, related_name='projects')
def __str__(self):
return self.name
class Appraisal(models.Model):
property = models.ForeignKey(Property, on_delete=models.CASCADE, related_name='appraisals')
appraiser = models.ForeignKey(User, on_delete=models.CASCADE, related_name='appraisals')
appraisal_date = models.DateField()
appraised_value = models.DecimalField(max_digits=12, decimal_places=2)
notes = models.TextField(blank=True)
order = models.ForeignKey(Order, on_delete=models.SET_NULL, related_name='appraisals', null=True, blank=True)
def __str__(self):
return f"Appraisal for {self.property.name} on {self.appraisal_date}"

View File

@ -9,24 +9,9 @@
<div class="card border-0 shadow-sm">
<div class="card-body p-5">
<h2 class="card-title text-center text-primary mb-4">New Appraisal Order</h2>
<form method="post">
<form method="post" action="{% url 'core:order_create' %}">
{% csrf_token %}
<div class="mb-3">
<label for="client_name" class="form-label">Full Name</label>
<input type="text" class="form-control" id="client_name" name="client_name" required>
</div>
<div class="mb-3">
<label for="client_email" class="form-label">Email Address</label>
<input type="email" class="form-control" id="client_email" name="client_email" required>
</div>
<div class="mb-3">
<label for="property_address" class="form-label">Property Address for Appraisal</label>
<textarea class="form-control" id="property_address" name="property_address" rows="3" required></textarea>
</div>
<div class="mb-4">
<label for="description" class="form-label">Additional Information (Optional)</label>
<textarea class="form-control" id="description" name="description" rows="4"></textarea>
</div>
{{ form.as_p }}
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Submit Order</button>
</div>

View File

@ -10,7 +10,7 @@
<p class="lead my-3">Your appraisal order has been successfully submitted.</p>
<p>We will review your request shortly and be in touch. You can now safely close this page.</p>
<div class="mt-4">
<a href="{% url 'home' %}" class="btn btn-secondary">Return to Homepage</a>
<a href="{% url 'core:index' %}" class="btn btn-secondary">Return to Homepage</a>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
from django.urls import path
from django.contrib.auth.views import LoginView, LogoutView
from .views import index, admin_dashboard, health_check, property_list, appraisal_list, user_list, project_list, invoice_list
from .views import index, admin_dashboard, health_check, property_list, user_list, appraisal_list, project_list, invoice_list, order_create, order_success
app_name = 'core'
@ -26,4 +26,6 @@ urlpatterns = [
path("users/", user_list, name='user_list'),
path("projects/", project_list, name='project_list'),
path("invoices/", invoice_list, name='invoice_list'),
path('order/new/', order_create, name='order_create'),
path('order/success/', order_success, name='order_success'),
]

View File

@ -4,7 +4,8 @@ from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required, user_passes_test
from django.http import HttpResponseForbidden
from functools import wraps
from .models import Property, Appraisal, User, Project, Invoice
from .forms import OrderForm
from .models import Property, User, Appraisal, Project, Invoice, Order
def group_required(*group_names):
@ -38,7 +39,6 @@ from django.http import HttpResponse
def health_check(request):
@ -50,7 +50,6 @@ def health_check(request):
def index(request):
@ -274,7 +273,19 @@ def invoice_list(request):
return render(request, 'core/invoice_list.html', context)
@login_required
def order_create(request):
if request.method == 'POST':
form = OrderForm(request.POST)
if form.is_valid():
order = Order.objects.create(
client=request.user,
description=form.cleaned_data['description']
)
return redirect('order_success')
else:
form = OrderForm()
return render(request, 'core/order_form.html', {'form': form})
def order_success(request):
return render(request, 'core/order_success.html')