1.4
This commit is contained in:
parent
873880f7d1
commit
1955ecf8b3
BIN
core/__pycache__/decorators.cpython-311.pyc
Normal file
BIN
core/__pycache__/decorators.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
12
core/decorators.py
Normal file
12
core/decorators.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django.contrib.auth.models import Group
|
||||
from django.http import HttpResponseForbidden
|
||||
|
||||
def group_required(group_names):
|
||||
def decorator(view_func):
|
||||
def _wrapped_view(request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
if request.user.groups.filter(name__in=group_names).exists() or request.user.is_superuser:
|
||||
return view_func(request, *args, **kwargs)
|
||||
return HttpResponseForbidden("You don't have permission to access this page.")
|
||||
return _wrapped_view
|
||||
return decorator
|
||||
18
core/etenders_api.py
Normal file
18
core/etenders_api.py
Normal file
@ -0,0 +1,18 @@
|
||||
import requests
|
||||
|
||||
ETENDERS_API_URL = "https://www.etenders.gov.za/api/v1/tenders"
|
||||
|
||||
def get_tenders():
|
||||
"""
|
||||
Fetches a list of tenders from the eTenders API.
|
||||
|
||||
Returns:
|
||||
list: A list of tenders as dictionaries, or None if an error occurs.
|
||||
"""
|
||||
try:
|
||||
response = requests.get(ETENDERS_API_URL)
|
||||
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching tenders from eTenders API: {e}")
|
||||
return None
|
||||
0
core/management/__init__.py
Normal file
0
core/management/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
52
core/management/commands/import_tenders.py
Normal file
52
core/management/commands/import_tenders.py
Normal file
@ -0,0 +1,52 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from core.etenders_api import get_tenders
|
||||
from core.models import Tender, Company
|
||||
from datetime import datetime
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Imports tenders from the eTenders API'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write('Importing tenders from eTenders API...')
|
||||
tenders_data = get_tenders()
|
||||
|
||||
if not tenders_data:
|
||||
self.stdout.write(self.style.WARNING('No tenders found or API is unavailable.'))
|
||||
return
|
||||
|
||||
# For now, let's assume a default company exists.
|
||||
# A more robust implementation would be to get the company from the tender data.
|
||||
default_company, created = Company.objects.get_or_create(name='Default Company')
|
||||
|
||||
imported_count = 0
|
||||
for tender_data in tenders_data:
|
||||
# Assuming the API returns these fields. This will need to be adjusted
|
||||
# based on the actual API response.
|
||||
title = tender_data.get('title')
|
||||
description = tender_data.get('description')
|
||||
deadline_str = tender_data.get('deadline')
|
||||
|
||||
if not all([title, description, deadline_str]):
|
||||
self.stdout.write(self.style.WARNING(f'Skipping tender with incomplete data: {tender_data}'))
|
||||
continue
|
||||
|
||||
try:
|
||||
deadline = datetime.fromisoformat(deadline_str)
|
||||
except ValueError:
|
||||
self.stdout.write(self.style.WARNING(f'Skipping tender with invalid deadline format: {deadline_str}'))
|
||||
continue
|
||||
|
||||
# Check if a tender with the same title and company already exists.
|
||||
if Tender.objects.filter(title=title, company=default_company).exists():
|
||||
self.stdout.write(self.style.NOTICE(f'Tender "{title}" already exists. Skipping.'))
|
||||
continue
|
||||
|
||||
Tender.objects.create(
|
||||
company=default_company,
|
||||
title=title,
|
||||
description=description,
|
||||
deadline=deadline,
|
||||
)
|
||||
imported_count += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'Successfully imported {imported_count} new tenders.'))
|
||||
18
core/migrations/0004_tender_status.py
Normal file
18
core/migrations/0004_tender_status.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-15 19:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_document_uploaded_by'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tender',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('opportunity-discovery', 'Opportunity Discovery'), ('qualification', 'Qualification / Go–No Go'), ('tender-registration', 'Tender Registration'), ('bid-planning', 'Bid Planning'), ('team-task-assignment', 'Team & Task Assignment'), ('document-collection-drafting', 'Document Collection & Drafting'), ('internal-review-compliance-check', 'Internal Review & Compliance Check'), ('approvals-sign-off', 'Approvals & Sign‑off'), ('submission-confirmation', 'Submission & Confirmation'), ('post-bid-review-analytics', 'Post‑Bid Review & Analytics')], default='opportunity-discovery', max_length=50),
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0004_tender_status.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0004_tender_status.cpython-311.pyc
Normal file
Binary file not shown.
@ -23,10 +23,23 @@ class Membership(models.Model):
|
||||
return f"{self.user.username} - {self.company.name} ({self.role})"
|
||||
|
||||
class Tender(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('opportunity-discovery', 'Opportunity Discovery'),
|
||||
('qualification', 'Qualification / Go–No Go'),
|
||||
('tender-registration', 'Tender Registration'),
|
||||
('bid-planning', 'Bid Planning'),
|
||||
('team-task-assignment', 'Team & Task Assignment'),
|
||||
('document-collection-drafting', 'Document Collection & Drafting'),
|
||||
('internal-review-compliance-check', 'Internal Review & Compliance Check'),
|
||||
('approvals-sign-off', 'Approvals & Sign‑off'),
|
||||
('submission-confirmation', 'Submission & Confirmation'),
|
||||
('post-bid-review-analytics', 'Post‑Bid Review & Analytics'),
|
||||
]
|
||||
company = models.ForeignKey(Company, on_delete=models.CASCADE)
|
||||
title = models.CharField(max_length=255)
|
||||
description = models.TextField()
|
||||
deadline = models.DateTimeField()
|
||||
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='opportunity-discovery')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
<p><strong>Description:</strong></p>
|
||||
<p>{{ tender.description }}</p>
|
||||
<p><strong>Deadline:</strong> {{ tender.deadline }}</p>
|
||||
<p><strong>Status:</strong> <span class="badge bg-info">{{ tender.get_status_display }}</span></p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="{% url 'tender_list' tender.company.id %}" class="btn btn-secondary">Back to Tenders</a>
|
||||
@ -19,6 +20,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="h4">Workflow</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if request.user.is_authenticated and request.user.groups.all|length > 0 %}
|
||||
{% if 'Head of Bids' in request.user.groups.all.0.name or 'Bid Administrator' in request.user.groups.all.0.name %}
|
||||
{% if next_status %}
|
||||
<p>The current step is complete. Ready to move to the next step.</p>
|
||||
<a href="{% url 'update_tender_status' tender.id next_status.0 %}" class="btn btn-success">Approve and move to: {{ next_status.1 }}</a>
|
||||
{% else %}
|
||||
<p>This tender is in the final workflow step.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>You do not have permission to advance the workflow.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="h4">Bids</h2>
|
||||
|
||||
@ -10,6 +10,7 @@ urlpatterns = [
|
||||
path('create_company/', views.create_company, name='create_company'),
|
||||
path('company/<int:company_id>/tenders/', views.tender_list, name='tender_list'),
|
||||
path('tender/<int:tender_id>/', views.tender_detail, name='tender_detail'),
|
||||
path('tender/<int:tender_id>/update_status/<str:next_status>/', views.update_tender_status, name='update_tender_status'),
|
||||
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'),
|
||||
|
||||
@ -3,6 +3,7 @@ from django.contrib.auth import login, authenticate, logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from .forms import SignUpForm, CompanyForm, TenderForm, BidForm, DocumentForm, NoteForm, ApprovalForm
|
||||
from .models import Company, Membership, Tender, Bid, Document, Note, Approval
|
||||
from .decorators import group_required
|
||||
|
||||
def home(request):
|
||||
return render(request, "core/index.html")
|
||||
@ -81,16 +82,41 @@ def tender_detail(request, tender_id):
|
||||
doc_form = DocumentForm()
|
||||
note_form = NoteForm()
|
||||
|
||||
# Workflow logic
|
||||
current_status_index = [i for i, (value, display) in enumerate(Tender.STATUS_CHOICES) if value == tender.status][0]
|
||||
next_status = Tender.STATUS_CHOICES[current_status_index + 1] if current_status_index + 1 < len(Tender.STATUS_CHOICES) else None
|
||||
|
||||
|
||||
context = {
|
||||
'tender': tender,
|
||||
'bids': bids,
|
||||
'documents': documents,
|
||||
'notes': notes,
|
||||
'doc_form': doc_form,
|
||||
'note_form': note_form
|
||||
'note_form': note_form,
|
||||
'next_status': next_status,
|
||||
}
|
||||
return render(request, 'core/tender_detail.html', context)
|
||||
|
||||
@login_required
|
||||
@group_required(['Head of Bids', 'Bid Administrator'])
|
||||
def update_tender_status(request, tender_id, next_status):
|
||||
tender = get_object_or_404(Tender, pk=tender_id)
|
||||
|
||||
# Find the index of the current status
|
||||
current_status_index = [i for i, (value, display) in enumerate(Tender.STATUS_CHOICES) if value == tender.status][0]
|
||||
|
||||
# Find the index of the next status
|
||||
next_status_index = [i for i, (value, display) in enumerate(Tender.STATUS_CHOICES) if value == next_status][0]
|
||||
|
||||
# Check if the next status is valid
|
||||
if next_status_index == current_status_index + 1:
|
||||
tender.status = next_status
|
||||
tender.save()
|
||||
|
||||
return redirect('tender_detail', tender_id=tender.id)
|
||||
|
||||
|
||||
@login_required
|
||||
def create_tender(request, company_id):
|
||||
company = get_object_or_404(Company, pk=company_id)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
requests==2.31.0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user