This commit is contained in:
Flatlogic Bot 2025-11-15 19:03:23 +00:00
parent 92ec45d230
commit 873880f7d1
24 changed files with 388 additions and 7 deletions

View File

@ -141,6 +141,9 @@ USE_TZ = True
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
STATICFILES_DIRS = [
BASE_DIR / 'static',

View File

@ -16,8 +16,13 @@ Including another URLconf
"""
from django.contrib import admin
from django.urls import include, path
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("core.urls")),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -1,6 +1,6 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from .models import Company, Tender, Bid
from .models import Company, Tender, Bid, Document, Note, Approval
class SignUpForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
@ -20,3 +20,18 @@ class BidForm(forms.ModelForm):
class Meta:
model = Bid
fields = ['amount']
class DocumentForm(forms.ModelForm):
class Meta:
model = Document
fields = ['file', 'description']
class NoteForm(forms.ModelForm):
class Meta:
model = Note
fields = ['note']
class ApprovalForm(forms.ModelForm):
class Meta:
model = Approval
fields = ['status', 'comments']

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-15 18:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='document',
name='description',
field=models.TextField(blank=True),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 5.2.7 on 2025-11-15 18:58
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_document_description'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='document',
name='uploaded_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -46,7 +46,9 @@ class Bid(models.Model):
class Document(models.Model):
tender = models.ForeignKey(Tender, on_delete=models.CASCADE, null=True, blank=True)
bid = models.ForeignKey(Bid, on_delete=models.CASCADE, null=True, blank=True)
uploaded_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
file = models.FileField(upload_to='documents/')
description = models.TextField(blank=True)
uploaded_at = models.DateTimeField(auto_now_add=True)
def __str__(self):

View File

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Approve Bid</h1>
</div>
<div class="card-body">
<p><strong>Tender:</strong> {{ bid.tender.title }}</p>
<p><strong>Company:</strong> {{ bid.company.name }}</p>
<p><strong>Amount:</strong> ${{ bid.amount }}</p>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Submit Approval</button>
</form>
</div>
<div class="card-footer">
<a href="{% url 'tender_detail' bid.tender.id %}" class="btn btn-secondary">Back to Tender</a>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Delete Bid</h1>
</div>
<div class="card-body">
<p>Are you sure you want to delete this bid of ${{ bid.amount }}?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
<a href="{% url 'tender_detail' bid.tender.id %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Delete Document</h1>
</div>
<div class="card-body">
<p>Are you sure you want to delete the document "{{ document.file.name }}"?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
<a href="{% url 'tender_detail' document.tender.id %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Delete Tender</h1>
</div>
<div class="card-body">
<p>Are you sure you want to delete the tender "{{ tender.title }}"?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
<a href="{% url 'tender_detail' tender.id %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -14,10 +14,12 @@
</div>
<div class="card-footer">
<a href="{% url 'tender_list' tender.company.id %}" class="btn btn-secondary">Back to Tenders</a>
<a href="{% url 'update_tender' tender.id %}" class="btn btn-primary">Update</a>
<a href="{% url 'delete_tender' tender.id %}" class="btn btn-danger">Delete</a>
</div>
</div>
<div class="card">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4">Bids</h2>
<a href="{% url 'create_bid' tender.id %}" class="btn btn-primary">Create Bid</a>
@ -27,7 +29,23 @@
<ul class="list-group">
{% for bid in bids %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<span>{{ bid.company.name }} - ${{ bid.amount }}</span>
<div>
<span>{{ bid.company.name }} - ${{ bid.amount }}</span>
{% with approval=bid.approval_set.first %}
{% if approval %}
<span class="badge bg-{% if approval.status == 'approved' %}success{% elif approval.status == 'rejected' %}danger{% else %}secondary{% endif %}">{{ approval.get_status_display }}</span>
{% else %}
<span class="badge bg-secondary">Pending</span>
{% endif %}
{% endwith %}
</div>
<div>
<a href="{% url 'approve_bid' bid.id %}" class="btn btn-sm btn-outline-primary">Review</a>
{% if request.user.membership_set.first.company == bid.company %}
<a href="{% url 'update_bid' bid.id %}" class="btn btn-sm btn-outline-secondary">Update</a>
<a href="{% url 'delete_bid' bid.id %}" class="btn btn-sm btn-outline-danger">Delete</a>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
@ -36,5 +54,57 @@
{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<h2 class="h4">Documents</h2>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data" class="mb-4">
{% csrf_token %}
{{ doc_form.as_p }}
<button type="submit" name="submit_document" class="btn btn-primary">Upload Document</button>
</form>
{% if documents %}
<ul class="list-group">
{% for doc in documents %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="{{ doc.file.url }}" target="_blank">{{ doc.file.name }}</a>
<small class="text-muted">by {{ doc.uploaded_by.username }} on {{ doc.uploaded_at|date:"Y-m-d" }}</small>
</li>
{% endfor %}
</ul>
{% else %}
<p>No documents yet.</p>
{% endif %}
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="h4">Notes</h2>
</div>
<div class="card-body">
<form method="post" class="mb-4">
{% csrf_token %}
{{ note_form.as_p }}
<button type="submit" name="submit_note" class="btn btn-primary">Add Note</button>
</form>
{% if notes %}
<ul class="list-group">
{% for note in notes %}
<li class="list-group-item">
<p class="mb-1">{{ note.note }}</p>
<small class="text-muted">by {{ note.user.username }} on {{ note.created_at|date:"Y-m-d" }}</small>
</li>
{% endfor %}
</ul>
{% else %}
<p>No notes yet.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -10,8 +10,12 @@
{% if tenders %}
<ul class="list-group">
{% for tender in tenders %}
<li class="list-group-item">
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="{% url 'tender_detail' tender.id %}">{{ tender.title }}</a>
<div>
<a href="{% url 'update_tender' tender.id %}" class="btn btn-sm btn-outline-primary">Update</a>
<a href="{% url 'delete_tender' tender.id %}" class="btn btn-sm btn-outline-danger">Delete</a>
</div>
</li>
{% endfor %}
</ul>

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Update Bid</h1>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Update Bid</button>
</form>
</div>
<div class="card-footer">
<a href="{% url 'tender_detail' bid.tender.id %}" class="btn btn-secondary">Back to Tender</a>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card">
<div class="card-header">
<h1 class="card-title">Update Tender</h1>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Update Tender</button>
</form>
</div>
<div class="card-footer">
<a href="{% url 'tender_detail' tender.id %}" class="btn btn-secondary">Back to Tender</a>
</div>
</div>
</div>
{% endblock %}

View File

@ -11,6 +11,11 @@ urlpatterns = [
path('company/<int:company_id>/tenders/', views.tender_list, name='tender_list'),
path('tender/<int:tender_id>/', views.tender_detail, name='tender_detail'),
path('company/<int:company_id>/create_tender/', views.create_tender, name='create_tender'),
path('tender/<int:tender_id>/update/', views.update_tender, name='update_tender'),
path('tender/<int:tender_id>/delete/', views.delete_tender, name='delete_tender'),
path('tender/<int:tender_id>/create_bid/', views.create_bid, name='create_bid'),
path('bid/<int:bid_id>/update/', views.update_bid, name='update_bid'),
path('bid/<int:bid_id>/delete/', views.delete_bid, name='delete_bid'),
path('bid/<int:bid_id>/approve/', views.approve_bid, name='approve_bid'),
path('', views.home, name='home'),
]

View File

@ -1,8 +1,8 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login, authenticate, logout
from django.contrib.auth.decorators import login_required
from .forms import SignUpForm, CompanyForm, TenderForm, BidForm
from .models import Company, Membership, Tender, Bid
from .forms import SignUpForm, CompanyForm, TenderForm, BidForm, DocumentForm, NoteForm, ApprovalForm
from .models import Company, Membership, Tender, Bid, Document, Note, Approval
def home(request):
return render(request, "core/index.html")
@ -57,9 +57,37 @@ def tender_list(request, company_id):
def tender_detail(request, tender_id):
tender = get_object_or_404(Tender, pk=tender_id)
bids = Bid.objects.filter(tender=tender)
documents = Document.objects.filter(tender=tender)
notes = Note.objects.filter(tender=tender)
if request.method == 'POST':
if 'submit_document' in request.POST:
doc_form = DocumentForm(request.POST, request.FILES)
if doc_form.is_valid():
document = doc_form.save(commit=False)
document.tender = tender
document.uploaded_by = request.user
document.save()
return redirect('tender_detail', tender_id=tender.id)
elif 'submit_note' in request.POST:
note_form = NoteForm(request.POST)
if note_form.is_valid():
note = note_form.save(commit=False)
note.tender = tender
note.user = request.user
note.save()
return redirect('tender_detail', tender_id=tender.id)
doc_form = DocumentForm()
note_form = NoteForm()
context = {
'tender': tender,
'bids': bids
'bids': bids,
'documents': documents,
'notes': notes,
'doc_form': doc_form,
'note_form': note_form
}
return render(request, 'core/tender_detail.html', context)
@ -81,6 +109,34 @@ def create_tender(request, company_id):
}
return render(request, 'core/create_tender.html', context)
@login_required
def update_tender(request, tender_id):
tender = get_object_or_404(Tender, pk=tender_id)
if request.method == 'POST':
form = TenderForm(request.POST, instance=tender)
if form.is_valid():
form.save()
return redirect('tender_detail', tender_id=tender.id)
else:
form = TenderForm(instance=tender)
context = {
'form': form,
'tender': tender
}
return render(request, 'core/update_tender.html', context)
@login_required
def delete_tender(request, tender_id):
tender = get_object_or_404(Tender, pk=tender_id)
if request.method == 'POST':
company_id = tender.company.id
tender.delete()
return redirect('tender_list', company_id=company_id)
context = {
'tender': tender
}
return render(request, 'core/delete_tender.html', context)
@login_required
def create_bid(request, tender_id):
tender = get_object_or_404(Tender, pk=tender_id)
@ -108,3 +164,62 @@ def create_bid(request, tender_id):
}
return render(request, 'core/create_bid.html', context)
@login_required
def update_bid(request, bid_id):
bid = get_object_or_404(Bid, pk=bid_id)
if request.method == 'POST':
form = BidForm(request.POST, instance=bid)
if form.is_valid():
form.save()
return redirect('tender_detail', tender_id=bid.tender.id)
else:
form = BidForm(instance=bid)
context = {
'form': form,
'bid': bid
}
return render(request, 'core/update_bid.html', context)
@login_required
def delete_bid(request, bid_id):
bid = get_object_or_404(Bid, pk=bid_id)
if request.method == 'POST':
tender_id = bid.tender.id
bid.delete()
return redirect('tender_detail', tender_id=tender_id)
context = {
'bid': bid
}
return render(request, 'core/delete_bid.html', context)
@login_required
def approve_bid(request, bid_id):
bid = get_object_or_404(Bid, pk=bid_id)
approval, created = Approval.objects.get_or_create(bid=bid, approver=request.user)
if request.method == 'POST':
form = ApprovalForm(request.POST, instance=approval)
if form.is_valid():
form.save()
return redirect('tender_detail', tender_id=bid.tender.id)
else:
form = ApprovalForm(instance=approval)
context = {
'form': form,
'bid': bid
}
return render(request, 'core/approve_bid.html', context)
@login_required
def delete_document(request, document_id):
document = get_object_or_404(Document, pk=document_id)
if request.method == 'POST':
tender_id = document.tender.id
document.delete()
return redirect('tender_detail', tender_id=tender_id)
context = {
'document': document
}
return render(request, 'core/delete_document.html', context)