diff options
Diffstat (limited to 'procurement')
-rw-r--r-- | procurement/__init__.py | 0 | ||||
-rw-r--r-- | procurement/admin.py | 31 | ||||
-rw-r--r-- | procurement/admin_forms.py | 20 | ||||
-rw-r--r-- | procurement/api.py | 14 | ||||
-rw-r--r-- | procurement/apps.py | 5 | ||||
-rw-r--r-- | procurement/forms.py | 14 | ||||
-rw-r--r-- | procurement/migrations/0001_initial.py | 47 | ||||
-rw-r--r-- | procurement/migrations/__init__.py | 0 | ||||
-rw-r--r-- | procurement/models.py | 83 | ||||
-rw-r--r-- | procurement/serializers.py | 17 | ||||
-rw-r--r-- | procurement/static/procurement/css/suppliers.css | 13 | ||||
-rw-r--r-- | procurement/static/procurement/js/component_search.js | 3 | ||||
-rw-r--r-- | procurement/templates/procurement/admin_templates/source_components.html | 45 | ||||
-rw-r--r-- | procurement/templates/procurement/documentation.html | 76 | ||||
-rw-r--r-- | procurement/templates/procurement/includes/supplier_list.html | 37 | ||||
-rw-r--r-- | procurement/templates/procurement/source_components.html | 61 | ||||
-rw-r--r-- | procurement/urls.py | 12 | ||||
-rw-r--r-- | procurement/views.py | 54 |
18 files changed, 532 insertions, 0 deletions
diff --git a/procurement/__init__.py b/procurement/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/procurement/__init__.py diff --git a/procurement/admin.py b/procurement/admin.py new file mode 100644 index 0000000..993b1b5 --- /dev/null +++ b/procurement/admin.py @@ -0,0 +1,31 @@ +from django.contrib import admin +from django.shortcuts import render_to_response, get_object_or_404 + +from procurement.admin_forms import ComponentAdminForm +from procurement.models import Supplier, Component + + +class SupplierAdmin(admin.ModelAdmin): + list_display = ('name', 'representative_name', 'representative_email', 'is_authorized', 'updated') + filter_horizonal = ('components',) + + +class ComponentAdmin(admin.ModelAdmin): + list_display = ('name', 'sku', 'updated') + form = ComponentAdminForm + source_components_template = 'procurement/admin_templates/source_components.html' + + def source_components(self, request, pk): + component = get_object_or_404(Component, pk=pk) + + return render_to_response(self.source_components_template, { + 'title': 'Source Suppliers for: %s' % component, + 'opts': self.model._meta, + 'component': component, + 'supplier_results': component.suppliers.filter(is_authorized=True), + + }) + + +admin.site.register(Supplier, SupplierAdmin) +admin.site.register(Component, ComponentAdmin) diff --git a/procurement/admin_forms.py b/procurement/admin_forms.py new file mode 100644 index 0000000..b7bbd1c --- /dev/null +++ b/procurement/admin_forms.py @@ -0,0 +1,20 @@ +from django import forms +from django.contrib.admin.widgets import FilteredSelectMultiple + +from procurement.models import Supplier, Component + + +class ComponentAdminForm(forms.ModelForm): + suppliers = forms.ModelMultipleChoiceField( + queryset=Supplier.objects.filter(is_authorized=True), + required=False, + widget=FilteredSelectMultiple( + verbose_name='Suppliers', + is_stacked=False + ) + + ) + + class Meta: + model = Component + fields = ['name', 'sku', 'suppliers']
\ No newline at end of file diff --git a/procurement/api.py b/procurement/api.py new file mode 100644 index 0000000..0419ce8 --- /dev/null +++ b/procurement/api.py @@ -0,0 +1,14 @@ +from rest_framework.generics import ListAPIView, RetrieveAPIView + +from procurement.models import Component +from procurement.serializers import ComponentSerializer + + +class ComponentAPIList(ListAPIView): + queryset = Component.objects.all() + serializer_class = ComponentSerializer + + +class ComponentAPIRetrieve(RetrieveAPIView): + queryset = Component.objects.all() + serializer_class = ComponentSerializer diff --git a/procurement/apps.py b/procurement/apps.py new file mode 100644 index 0000000..94c7e8e --- /dev/null +++ b/procurement/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProcurementConfig(AppConfig): + name = 'procurement' diff --git a/procurement/forms.py b/procurement/forms.py new file mode 100644 index 0000000..4e8b0aa --- /dev/null +++ b/procurement/forms.py @@ -0,0 +1,14 @@ +from django import forms +from procurement.models import Component + + +class ComponentSearchForm(forms.Form): + component = forms.ModelChoiceField( + queryset=Component.objects.all(), + required=False + ) + + def __init__(self, *args, **kwargs): + super(ComponentSearchForm, self).__init__(*args, **kwargs) + self.fields['component'].widget.attrs.update({"class": "form-control"}) + diff --git a/procurement/migrations/0001_initial.py b/procurement/migrations/0001_initial.py new file mode 100644 index 0000000..ffabc0e --- /dev/null +++ b/procurement/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 2.1.2 on 2018-10-02 14:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Component', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255)), + ('sku', models.CharField(max_length=50)), + ], + options={ + 'ordering': ('name',), + }, + ), + migrations.CreateModel( + name='Supplier', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255)), + ('representative_name', models.CharField(blank=True, max_length=255, null=True)), + ('representative_email', models.EmailField(blank=True, max_length=255, null=True)), + ('is_authorized', models.BooleanField()), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='component', + name='suppliers', + field=models.ManyToManyField(blank=True, related_name='components', to='procurement.Supplier'), + ), + ] diff --git a/procurement/migrations/__init__.py b/procurement/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/procurement/migrations/__init__.py diff --git a/procurement/models.py b/procurement/models.py new file mode 100644 index 0000000..1e70736 --- /dev/null +++ b/procurement/models.py @@ -0,0 +1,83 @@ +from django.db import models +from django.utils.timezone import now as timezone_now + +MONTH = 30 * 24 * 60 * 60 +WEEK = 7 * 24 * 60 * 60 +DAY = 24 * 60 * 60 +HOUR = 60 * 60 +MINUTE = 60 + + +class DashboardModel(models.Model): + """ + Abstract base model for things which will be displayed on the dashboard, adds in created and updated fields, + and provides a convenience method which provides a nicely formatted string of the time since update. + """ + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + @property + def time_since_update(self): + update_delta = timezone_now() - self.updated + seconds_since_update = update_delta.seconds + + if seconds_since_update / MONTH >= 1: + quantity = seconds_since_update / MONTH + units = 'months' if quantity > 1 else 'month' + + elif seconds_since_update / WEEK >= 1: + quantity = seconds_since_update / WEEK + units = 'weeks' if quantity > 1 else 'week' + + elif seconds_since_update / DAY >= 1: + quantity = seconds_since_update / DAY + units = 'days' if quantity > 1 else 'day' + + elif seconds_since_update / HOUR >= 1: + quantity = seconds_since_update / HOUR + units = 'hours' if quantity > 1 else 'hour' + + elif seconds_since_update / MINUTE >= 1: + quantity = seconds_since_update / MINUTE + units = 'minutes' if quantity > 1 else 'minute' + + else: + return "updated just now" + + # Ensure the quantity output is rounded to 2 decimal places + base_string = 'updated {quantity:.2f} {units} ago' + return base_string.format(quantity=quantity, units=units) + + +class Supplier(DashboardModel): + """ + Model which represents an individual or organisation which supplies components + """ + name = models.CharField(max_length=255) + representative_name = models.CharField(max_length=255, null=True, blank=True) + representative_email = models.EmailField(max_length=255, null=True, blank=True) + is_authorized = models.BooleanField() + + def __str__(self): + return '{}'.format(self.name) + + +class Component(DashboardModel): + """ + Model which represents items which may be supplied. + """ + name = models.CharField(max_length=255) + sku = models.CharField(max_length=50) + suppliers = models.ManyToManyField(Supplier, related_name='components', blank=True) + + class Meta: + ordering = ("name",) + + def __str__(self): + return '{} ({})'.format( + self.name, + self.sku + ) diff --git a/procurement/serializers.py b/procurement/serializers.py new file mode 100644 index 0000000..1840837 --- /dev/null +++ b/procurement/serializers.py @@ -0,0 +1,17 @@ +from rest_framework import serializers +from procurement.models import Component, Supplier + + +class SupplierSerializer(serializers.ModelSerializer): + class Meta: + model = Supplier + exclude = ('created', 'updated') + + +class ComponentSerializer(serializers.ModelSerializer): + text = serializers.CharField(source='__str__', read_only=True) + suppliers = SupplierSerializer(many=True, read_only=True) + + class Meta: + model = Component + exclude = ('created', 'updated') diff --git a/procurement/static/procurement/css/suppliers.css b/procurement/static/procurement/css/suppliers.css new file mode 100644 index 0000000..396a446 --- /dev/null +++ b/procurement/static/procurement/css/suppliers.css @@ -0,0 +1,13 @@ +/* hide component field on search form */ +#id_component { + display: none; +} +label[for=id_component] { + display: none; +} +.record-list { + display: block; +} +.record-updated{ + display: block; +}
\ No newline at end of file diff --git a/procurement/static/procurement/js/component_search.js b/procurement/static/procurement/js/component_search.js new file mode 100644 index 0000000..cba0a16 --- /dev/null +++ b/procurement/static/procurement/js/component_search.js @@ -0,0 +1,3 @@ +$(document).ready(function(){ + $("#id_component").select2(); +});
\ No newline at end of file diff --git a/procurement/templates/procurement/admin_templates/source_components.html b/procurement/templates/procurement/admin_templates/source_components.html new file mode 100644 index 0000000..2deae1e --- /dev/null +++ b/procurement/templates/procurement/admin_templates/source_components.html @@ -0,0 +1,45 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_modify %} +{% block extrahead %}{{ block.super }} + <!-- Bootstrap Core CSS --> + <link href="/static/prebuilt/bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> + <!-- Lightsource Custom Styles --> + <link href="/static/custom/css/lightsource-styles.css" rel="stylesheet" type="text/css"> + {% for stylesheet in module_stylesheets %} + <link href="/static/{{ stylesheet }}" rel="stylesheet" type="text/css"> + {% endfor %} +{% url 'admin:jsi18n' as jsi18nurl %} +{% endblock %} +{% block extrastyle %}{{ block.super }} +{% endblock %} +{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %} +{% block breadcrumbs %}{% if not is_popup %} +<div class="breadcrumbs"> + <a href="../../">{% trans "Home" %}</a> › + <a href="../">{{ opts.app_label|capfirst|escape }}</a> › + {% trans 'Source Components' %}</div> +{% endif %}{% endblock %} + +{% block content %} + <div class="col-lg-8"> + + <!-- /.panel --> + {% if supplier_results != None %} + {% include "suppliers/includes/supplier_list.html" %} + {% endif %} + </div> + + <!-- jQuery --> + <script src="/static/prebuilt/bower_components/jquery/dist/jquery.min.js"></script> + <script src="//code.jquery.com/ui/1.11.4/jquery-ui.js"></script> + <link rel="stylesheet" href="//code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css"> + + <!-- Bootstrap Core JavaScript --> + <script src="/static/prebuilt/bower_components/bootstrap/dist/js/bootstrap.min.js"></script> + + <!-- Custom Theme JavaScript --> + <script src="/static/prebuilt/dist/js/sb-admin-2.js"></script> + {% for script in module_javascripts %} + <script src="/static/{{ script }}"></script> + {% endfor %} +{% endblock %}
\ No newline at end of file diff --git a/procurement/templates/procurement/documentation.html b/procurement/templates/procurement/documentation.html new file mode 100644 index 0000000..4775890 --- /dev/null +++ b/procurement/templates/procurement/documentation.html @@ -0,0 +1,76 @@ +{% extends 'base.html' %} + +{% block main_content_area %} + <div class="col-lg-4"> + <div class="panel panel-default"> + <div class="panel-heading"> + Documentation + </div> + <!-- /.panel-heading --> + <div class="panel-body"> + <div class="list-group"> + <a href="#introduction" class="list-group-item"> + <i class="fa fa-info-circle fa-fw"></i> Introduction + </a> + <a href="#adding-data" class="list-group-item"> + <i class="fa fa-database fa-fw"></i> Adding Data + </a> + <a href="#supplier-list" class="list-group-item"> + <i class="fa fa-th-list fa-fw"></i> Supplier List + </a> + <a href="#api" class="list-group-item"> + <i class="fa fa-bolt fa-fw"></i> API + </a> + </div> + <!-- /.list-group --> + </div> + <!-- /.panel-body --> + </div> + <!-- /.panel --> + </div> + + <div class="col-lg-8"> + <div class="panel panel-default"> + <div class="panel-body"> + <h2><a name="introduction"></a>Introduction</h2> + <p> + This is a very simple component and supplier management system. The intention function of the system + is to maintain a list of components, and the suppliers who are authorised to supply them. Administrative + staff maintain the list of suppliers, components and their associations, while general staff simply + use the from page to look up a component, and see its list of suppliers. + </p> + <p> + The system is written in Python (v3.6), using the Django framework (v2.1.2). Documentation for Django + is available <a href="https://www.djangoproject.com/">here</a>. + </p> + + + <h2><a name="adding-data"></a>Adding Data</h2> + <p> + You can manage the components and suppliers using the + <a href="{% url 'admin:index' %}">django admin panel</a>. + </p> + + <h2><a name="supplier-list"></a>Supplier List</h2> + <p> + Components and their approved Suppliers can be located under + <a href="{% url 'component-search' %}">source components</a>. From this page you may search for a + component, and then click the Find Suppliers button to display its Suppliers + </p> + + <h2><a name="api"></a>API</h2> + <p> + A simple REST api is integrated into the system, using the <a href="https://www.django-rest-framework.org/">Django Rest Framework</a>. + It provides a means of querying <a href="{% url 'api-component-list' %}">all components</a> or, by providing an id + in the url, <a href="{% url 'api-component-retrieve' pk=1 %}">a specific component</a>. + </p> + </div> + <!-- /.panel-body --> + </div> + <!-- /.panel --> + </div> + + + + +{% endblock %}
\ No newline at end of file diff --git a/procurement/templates/procurement/includes/supplier_list.html b/procurement/templates/procurement/includes/supplier_list.html new file mode 100644 index 0000000..028e62c --- /dev/null +++ b/procurement/templates/procurement/includes/supplier_list.html @@ -0,0 +1,37 @@ + +<!-- Supplier Results --> +<div class="panel panel-default"> + <div class="panel-heading"> + Approved Suppliers for {{ component }} + </div> + <!-- /.panel-heading --> + <div class="panel-body"> + <div class="table-responsive"> + <table class="table"> + <thead> + <tr> + <th>Company</th> + <th>Representative</th> + <th>Email</th> + </tr> + </thead> + <tbody> + {% for supplier in supplier_results %} + <tr> + <td>{{ supplier.name }}</td> + <td>{{ supplier.representative_name }}</td> + <td>{{ supplier.representative_email }}</td> + </tr> + {% empty %} + <tr> + <td colspan="3">No authorized suppliers found.</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + <!-- /.table-responsive --> + </div> + <!-- /.panel-body --> +</div> +<!-- / Supplier Results Panel -->
\ No newline at end of file diff --git a/procurement/templates/procurement/source_components.html b/procurement/templates/procurement/source_components.html new file mode 100644 index 0000000..3eebf84 --- /dev/null +++ b/procurement/templates/procurement/source_components.html @@ -0,0 +1,61 @@ +{% extends 'base.html' %} +{% load static %} + +{% block additional_css %} + <link href="{% static 'procurement/css/suppliers.css' %}" rel="stylesheet" type="text/css"> + <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css" rel="stylesheet" /> +{% endblock %} + +{% block main_content_area %} + <div class="col-lg-8"> + <div class="panel panel-default"> + <div class="panel-body"> + <form id="find-suppliers" method="post"> + <div class="form-group"> + {% csrf_token %} + {{ form }} + </div> + <div class="form-group"> + <input type="submit" value="Find Suppliers" id="find-suppliers-button" class="btn btn-primary"> + </div> + </form> + </div> + <!-- /.panel-body --> + </div> + <!-- /.panel --> + {% if supplier_results != None %} + {% include 'procurement/includes/supplier_list.html' %} + {% endif %} + </div> + + + <div class="col-lg-4"> + <div class="panel panel-default"> + <div class="panel-heading"> + Searching Procurement Records + </div> + <!-- /.panel-heading --> + <div class="panel-body"> + <div class="list-group"> + <span class="list-group-item"> + <span class="record-list"><i class="fa fa-parachute-box fa-fw"></i> {{ supplier_count }} Suppliers</span> + <span class="record-updated text-muted small"><em>{{ suppliers_last_updated }}</em></span> + </span> + <span class="list-group-item"> + <span class="record-list"><i class="fa fa-cog fa-fw"></i> {{ component_count }} Components</span> + <span class="record-updated text-muted small"><em>{{ components_last_updated }}</em></span> + </span> + </div> + <!-- /.list-group --> + </div> + <!-- /.panel-body --> + </div> + <!-- /.panel --> + </div> + +{% endblock %} + +{% block additional_js %} + <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/js/select2.min.js"></script> + <script src="{% static 'procurement/js/component_search.js' %}"></script> +{% endblock %}
\ No newline at end of file diff --git a/procurement/urls.py b/procurement/urls.py new file mode 100644 index 0000000..d629635 --- /dev/null +++ b/procurement/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from procurement.views import ComponentSearchView, DocumentationView +from procurement.api import ComponentAPIList, ComponentAPIRetrieve + +urlpatterns = [ + path('', ComponentSearchView.as_view(), name='component-search'), + path('documentation', DocumentationView.as_view(), name='documentation'), + + # API Urls + path('api/components/', ComponentAPIList.as_view(), name='api-component-list'), + path('api/components/<int:pk>/', ComponentAPIRetrieve.as_view(), name='api-component-retrieve'), +] diff --git a/procurement/views.py b/procurement/views.py new file mode 100644 index 0000000..8891f89 --- /dev/null +++ b/procurement/views.py @@ -0,0 +1,54 @@ +from django.views.generic import FormView, TemplateView + +from procurement.forms import ComponentSearchForm +from procurement.models import Supplier, Component + + +class ComponentSearchView(FormView): + template_name = 'procurement/source_components.html' + form_class = ComponentSearchForm + + component = None + supplier_results = None + + def get_context_data(self): + context = super().get_context_data() + + try: + suppliers_last_updated = Supplier.objects.latest('updated').time_since_update + except Supplier.DoesNotExist: + suppliers_last_updated = '' + + try: + components_last_updated = Component.objects.latest('updated').time_since_update + except Component.DoesNotExist: + components_last_updated = '' + + context.update({ + 'page_name': 'Component Search', + 'component': self.component, + 'supplier_results': self.supplier_results, + 'supplier_count': Supplier.objects.all().count(), + 'suppliers_last_updated': suppliers_last_updated, + 'component_count': Component.objects.all().count(), + 'components_last_updated': components_last_updated, + }) + return context + + def form_valid(self, form): + self.component = form.cleaned_data['component'] + if self.component: + self.supplier_results = self.component.suppliers.filter(is_authorized=True) + + return super(ComponentSearchView, self).get(self.request) + + +class DocumentationView(TemplateView): + template_name = 'procurement/documentation.html' + + def get_context_data(self): + context = super().get_context_data() + context.update({ + 'page_name': 'Documentation', + }) + return context |