mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-03-04 02:58:25 +00:00
Merge assetforms -> main assets branch
This commit is contained in:
@@ -39,6 +39,7 @@ class EmbeddedAuthenticationForm(AuthenticationForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['username'].widget.attrs.pop('autofocus', None)
|
||||
|
||||
|
||||
class PasswordReset(PasswordResetForm):
|
||||
captcha = ReCaptchaField(label='Captcha')
|
||||
|
||||
|
||||
@@ -121,8 +121,19 @@ class Command(BaseCommand):
|
||||
self.keyholder_group = Group.objects.create(name='Keyholders')
|
||||
self.finance_group = Group.objects.create(name='Finance')
|
||||
|
||||
keyholderPerms = ["add_event", "change_event", "view_event", "add_eventitem", "change_eventitem", "delete_eventitem", "add_organisation", "change_organisation", "view_organisation", "add_person", "change_person", "view_person", "view_profile", "add_venue", "change_venue", "view_venue"]
|
||||
financePerms = ["change_event", "view_event", "add_eventitem", "change_eventitem", "add_invoice", "change_invoice", "view_invoice", "add_organisation", "change_organisation", "view_organisation", "add_payment", "change_payment", "delete_payment", "add_person", "change_person", "view_person"]
|
||||
keyholderPerms = ["add_event", "change_event", "view_event",
|
||||
"add_eventitem", "change_eventitem", "delete_eventitem",
|
||||
"add_organisation", "change_organisation", "view_organisation",
|
||||
"add_person", "change_person", "view_person", "view_profile",
|
||||
"add_venue", "change_venue", "view_venue",
|
||||
"add_asset", "change_asset", "delete_asset",
|
||||
"asset_finance"]
|
||||
financePerms = ["change_event", "view_event", "add_eventitem",
|
||||
"change_eventitem", "add_invoice", "change_invoice", "view_invoice",
|
||||
"add_organisation", "change_organisation", "view_organisation",
|
||||
"add_payment", "change_payment", "delete_payment",
|
||||
"add_person", "change_person", "view_person",
|
||||
"asset_finance", "change_asset"]
|
||||
|
||||
for permId in keyholderPerms:
|
||||
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
|
||||
|
||||
18
RIGS/migrations/0035_auto_20191008_2148.py
Normal file
18
RIGS/migrations/0035_auto_20191008_2148.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.0.13 on 2019-10-08 20:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('RIGS', '0034_event_risk_assessment_edit_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='risk_assessment_edit_url',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='risk assessment'),
|
||||
),
|
||||
]
|
||||
@@ -83,6 +83,7 @@ class EventEmbed(EventDetail):
|
||||
|
||||
class EventRA(generic.base.RedirectView):
|
||||
permanent = False
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
event = get_object_or_404(models.Event, pk=kwargs['pk'])
|
||||
|
||||
@@ -406,6 +407,7 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView):
|
||||
context['to_name'] = self.request.GET.get('to_name', None)
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class LogRiskAssessment(generic.View):
|
||||
http_method_names = ["post"]
|
||||
|
||||
@@ -32,11 +32,6 @@ class ConnectorAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins']
|
||||
|
||||
|
||||
@admin.register(assets.Cable)
|
||||
class CableAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.AdminSite.site_header = 'PyAssets - TEC\'s Asset System'
|
||||
admin.AdminSite.site_title = 'PyAssets Admin'
|
||||
admin.AdminSite.index_title = 'System Administration'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from assets import models
|
||||
|
||||
@@ -9,7 +10,7 @@ class AssetForm(forms.ModelForm):
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class SupplierForm(forms.ModelForm):
|
||||
class SupplierForm(forms.Form):
|
||||
class Meta:
|
||||
model = models.Supplier
|
||||
fields = '__all__'
|
||||
|
||||
@@ -21,6 +21,7 @@ class Command(BaseCommand):
|
||||
self.create_statuses()
|
||||
self.create_suppliers()
|
||||
self.create_assets()
|
||||
self.create_connectors()
|
||||
|
||||
def create_categories(self):
|
||||
categories = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging']
|
||||
@@ -35,7 +36,8 @@ class Command(BaseCommand):
|
||||
models.AssetStatus.objects.create(name=stat)
|
||||
|
||||
def create_suppliers(self):
|
||||
suppliers = ["Acme, inc.","Widget Corp","123 Warehousing","Demo Company","Smith and Co.","Foo Bars","ABC Telecom","Fake Brothers","QWERTY Logistics","Demo, inc.","Sample Company","Sample, inc","Acme Corp","Allied Biscuit","Ankh-Sto Associates","Extensive Enterprise","Galaxy Corp","Globo-Chem","Mr. Sparkle","Globex Corporation","LexCorp","LuthorCorp","North Central Positronics","Omni Consimer Products","Praxis Corporation","Sombra Corporation","Sto Plains Holdings","Tessier-Ashpool","Wayne Enterprises","Wentworth Industries","ZiffCorp","Bluth Company","Strickland Propane","Thatherton Fuels","Three Waters","Water and Power","Western Gas & Electric","Mammoth Pictures","Mooby Corp","Gringotts","Thrift Bank","Flowers By Irene","The Legitimate Businessmens Club","Osato Chemicals","Transworld Consortium","Universal Export","United Fried Chicken","Virtucon","Kumatsu Motors","Keedsler Motors","Powell Motors","Industrial Automation","Sirius Cybernetics Corporation","U.S. Robotics and Mechanical Men","Colonial Movers","Corellian Engineering Corporation","Incom Corporation","General Products","Leeding Engines Ltd.","Blammo","Input, Inc.","Mainway Toys","Videlectrix","Zevo Toys","Ajax","Axis Chemical Co.","Barrytron","Carrys Candles","Cogswell Cogs","Spacely Sprockets","General Forge and Foundry","Duff Brewing Company","Dunder Mifflin","General Services Corporation","Monarch Playing Card Co.","Krustyco","Initech","Roboto Industries","Primatech","Sonky Rubber Goods","St. Anky Beer","Stay Puft Corporation","Vandelay Industries","Wernham Hogg","Gadgetron","Burleigh and Stronginthearm","BLAND Corporation","Nordyne Defense Dynamics","Petrox Oil Company","Roxxon","McMahon and Tate","Sixty Second Avenue","Charles Townsend Agency","Spade and Archer","Megadodo Publications","Rouster and Sideways","C.H. Lavatory and Sons","Globo Gym American Corp","The New Firm","SpringShield","Compuglobalhypermeganet","Data Systems","Gizmonic Institute","Initrode","Taggart Transcontinental","Atlantic Northern","Niagular","Plow King","Big Kahuna Burger","Big T Burgers and Fries","Chez Quis","Chotchkies","The Frying Dutchman","Klimpys","The Krusty Krab","Monks Diner","Milliways","Minuteman Cafe","Taco Grande","Tip Top Cafe","Moes Tavern","Central Perk","Chasers"]
|
||||
suppliers = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo",
|
||||
"Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"]
|
||||
|
||||
for supplier in suppliers:
|
||||
models.Supplier.objects.create(name=supplier)
|
||||
@@ -60,3 +62,14 @@ class Command(BaseCommand):
|
||||
asset.purchased_from = random.choice(suppliers)
|
||||
|
||||
asset.save()
|
||||
|
||||
def create_connectors(self):
|
||||
connectors = [
|
||||
{"description": "13A UK", "current_rating": 13, "voltage_rating": 230, "num_pins": 3},
|
||||
{"description": "16A", "current_rating": 16, "voltage_rating": 230, "num_pins": 3},
|
||||
{"description": "32/3", "current_rating": 32, "voltage_rating": 400, "num_pins": 5},
|
||||
{"description": "Socapex", "current_rating": 23, "voltage_rating": 600, "num_pins": 19},
|
||||
]
|
||||
for connector in connectors:
|
||||
conn = models.Connector.objects.create(**connector)
|
||||
conn.save()
|
||||
|
||||
@@ -21,7 +21,7 @@ class Command(BaseCommand):
|
||||
self.delete_objects(models.AssetCategory)
|
||||
self.delete_objects(models.AssetStatus)
|
||||
self.delete_objects(models.Supplier)
|
||||
self.delete_objects(models.Collection)
|
||||
self.delete_objects(models.Connector)
|
||||
self.delete_objects(models.Asset)
|
||||
|
||||
def delete_objects(self, model):
|
||||
|
||||
18
assets/migrations/0009_auto_20191008_2148.py
Normal file
18
assets/migrations/0009_auto_20191008_2148.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.0.13 on 2019-10-08 20:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0008_auto_20191002_1931'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='asset_id',
|
||||
field=models.CharField(max_length=10, unique=True),
|
||||
),
|
||||
]
|
||||
67
assets/migrations/0010_auto_20191013_2123.py
Normal file
67
assets/migrations/0010_auto_20191013_2123.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# Generated by Django 2.0.13 on 2019-10-13 20:23
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0009_auto_20191008_2148'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='asset_ptr',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='plug',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='cable',
|
||||
name='socket',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='polymorphic_ctype',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='circuits',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='cores',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='csa',
|
||||
field=models.DecimalField(blank=True, decimal_places=2, help_text='mm^2', max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='length',
|
||||
field=models.DecimalField(blank=True, decimal_places=1, help_text='m', max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='plug',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plug', to='assets.Connector'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='socket',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socket', to='assets.Connector'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Cable',
|
||||
),
|
||||
]
|
||||
24
assets/migrations/0011_auto_20191013_2247.py
Normal file
24
assets/migrations/0011_auto_20191013_2247.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 2.0.13 on 2019-10-13 21:47
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0010_auto_20191013_2123'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='plug',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='plug', to='assets.Connector'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='socket',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='socket', to='assets.Connector'),
|
||||
),
|
||||
]
|
||||
17
assets/migrations/0012_auto_20191014_0012.py
Normal file
17
assets/migrations/0012_auto_20191014_0012.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 2.0.13 on 2019-10-13 23:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0011_auto_20191013_2247'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='asset',
|
||||
options={'permissions': (('asset_finance', 'Can see financial data for assets'),)},
|
||||
),
|
||||
]
|
||||
@@ -1,8 +1,10 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
import datetime
|
||||
import re
|
||||
|
||||
|
||||
class AssetCategory(models.Model):
|
||||
@@ -37,10 +39,25 @@ class Supplier(models.Model):
|
||||
return self.name
|
||||
|
||||
|
||||
class Asset(PolymorphicModel):
|
||||
class Connector(models.Model):
|
||||
description = models.CharField(max_length=80)
|
||||
current_rating = models.DecimalField(decimal_places=2, max_digits=10, help_text='Amps')
|
||||
voltage_rating = models.IntegerField(help_text='Volts')
|
||||
num_pins = models.IntegerField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.description
|
||||
|
||||
|
||||
class Asset(models.Model):
|
||||
class Meta:
|
||||
ordering = ['asset_id']
|
||||
permissions = (
|
||||
('asset_finance', 'Can see financial data for assets'),
|
||||
)
|
||||
|
||||
parent = models.ForeignKey(to='self', related_name='asset_parent', blank=True, null=True, on_delete=models.SET_NULL)
|
||||
asset_id = models.CharField(max_length=10)
|
||||
asset_id = models.CharField(max_length=10, unique=True)
|
||||
description = models.CharField(max_length=120)
|
||||
category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE)
|
||||
status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE)
|
||||
@@ -55,34 +72,55 @@ class Asset(PolymorphicModel):
|
||||
|
||||
# Cable assets
|
||||
is_cable = models.BooleanField(default=False)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('asset_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return str(self.asset_id) + ' - ' + self.description
|
||||
|
||||
class Connector(models.Model):
|
||||
description = models.CharField(max_length=80)
|
||||
current_rating = models.DecimalField(decimal_places=2, max_digits=10, help_text='Amps')
|
||||
voltage_rating = models.IntegerField(help_text='Volts')
|
||||
num_pins = models.IntegerField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.description
|
||||
|
||||
|
||||
class Cable(Asset):
|
||||
plug = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='plug', null=True)
|
||||
socket = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='socket', null=True)
|
||||
plug = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='plug', blank=True, null=True)
|
||||
socket = models.ForeignKey(Connector, on_delete=models.SET_NULL, related_name='socket', blank=True, null=True)
|
||||
length = models.DecimalField(decimal_places=1, max_digits=10, blank=True, null=True, help_text='m')
|
||||
csa = models.DecimalField(decimal_places=2, max_digits=10, blank=True, null=True, help_text='mm^2')
|
||||
circuits = models.IntegerField(blank=True, null=True)
|
||||
cores = models.IntegerField(blank=True, null=True)
|
||||
|
||||
def cable_resistance(self):
|
||||
rho = 0.0000000168
|
||||
return (rho * self.length) / (self.csa * 1000000)
|
||||
def get_absolute_url(self):
|
||||
return reverse('asset_detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return '{} - {}m - {}'.format(self.plug, self.length, self.socket)
|
||||
out = str(self.asset_id) + ' - ' + self.description
|
||||
if self.is_cable:
|
||||
out += '{} - {}m - {}'.format(self.plug, self.length, self.socket)
|
||||
return out
|
||||
|
||||
def clean(self):
|
||||
if self.date_sold and self.date_acquired > self.date_sold:
|
||||
raise ValidationError({"date_sold": "Cannot sell an item before it is acquired"})
|
||||
|
||||
self.asset_id = self.asset_id.upper()
|
||||
if re.search("^[a-zA-Z0-9]+$", self.asset_id) is None:
|
||||
raise ValidationError({"asset_id": "An Asset ID can only consist of letters and numbers"})
|
||||
|
||||
if self.purchase_price and self.purchase_price < 0:
|
||||
raise ValidationError({"purchase_price": "A price cannot be negative"})
|
||||
|
||||
if self.salvage_value and self.salvage_value < 0:
|
||||
raise ValidationError({"purchase_price": "A price cannot be negative"})
|
||||
|
||||
if self.is_cable:
|
||||
if self.length is None:
|
||||
raise ValidationError({"length": "The length of a cable must be a number"})
|
||||
elif self.csa is None:
|
||||
raise ValidationError({"csa": "The csa of a cable must be a number"})
|
||||
elif self.circuits is None:
|
||||
raise ValidationError({"circuits": "The number of circuits in a cable must be a number"})
|
||||
elif self.cores is None:
|
||||
raise ValidationError({"cores": "The number of cores in a cable must be a number"})
|
||||
elif self.socket is None:
|
||||
raise ValidationError({"plug": "A cable must have a plug"})
|
||||
elif self.plug is None:
|
||||
raise ValidationError({"socket": "A cable must have a socket"})
|
||||
|
||||
if self.length <= 0:
|
||||
raise ValidationError({"length": "The length of a cable must be more than 0"})
|
||||
elif self.csa <= 0:
|
||||
raise ValidationError({"csa": "The CSA of a cable must be more than 0"})
|
||||
elif self.circuits <= 0:
|
||||
raise ValidationError({"circuits": "There must be at least one circuit in a cable"})
|
||||
elif self.cores <= 0:
|
||||
raise ValidationError({"cores": "There must be at least one core in a cable"})
|
||||
|
||||
71
assets/templates/asset_create.html
Normal file
71
assets/templates/asset_create.html
Normal file
@@ -0,0 +1,71 @@
|
||||
{% extends 'base_assets.html' %}
|
||||
{% load widget_tweaks %}
|
||||
{% load asset_templatetags %}
|
||||
{% block title %}Asset {{ object.asset_id }}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{% if duplicate %}
|
||||
Duplication of Asset: {{ previous_asset_id }}
|
||||
{% else %}
|
||||
Create Asset
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
{% if duplicate %}
|
||||
<form method="post" id="asset_update_form" action="{% url 'asset_duplicate' pk=previous_asset_pk%}">
|
||||
{% else %}
|
||||
<form method="post" id="asset_update_form" action="{% url 'asset_create'%}">
|
||||
{% endif %}
|
||||
{% include 'form_errors.html' %}
|
||||
<div class="row" style="padding-bottom: 1em">
|
||||
<div class="col-sm-12">
|
||||
<div class="pull-right">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden=true>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{% include 'partials/asset_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
{% include 'partials/purchasedetails_form.html' %}
|
||||
</div>
|
||||
<div class="col-md-6" hidden="true" id="cable-table">
|
||||
{% include 'partials/cable_form.html' %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% include 'partials/parent_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% include 'partials/confirm_delete.html' with object=object %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js%}
|
||||
<script>
|
||||
function checkIfCableHidden() {
|
||||
if (document.getElementById("id_is_cable").checked) {
|
||||
document.getElementById("cable-table").hidden = false;
|
||||
} else {
|
||||
document.getElementById("cable-table").hidden = true;
|
||||
}
|
||||
}
|
||||
checkIfCableHidden();
|
||||
</script>
|
||||
{%endblock%}
|
||||
@@ -8,41 +8,48 @@
|
||||
<h1 class="text-center">Asset List</h1>
|
||||
</div>
|
||||
|
||||
<form id="asset-search-form">
|
||||
<form id="asset-search-form" method="get" class="form-inline pull-right">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<input type="query" name="query" placeholder="Search by Asset ID/Description" class="form-control" value="{{searchName}}">
|
||||
<div class="input-group pull-right" style="width: auto;">
|
||||
<input type="query" name="query" placeholder="Search by Asset ID/Description" class="form-control" value="{{search_name}}" style="width: 250px">
|
||||
<label for="asset_id" class="sr-only">Asset ID/Description:</label>
|
||||
<span class="input-group-btn"><button type="submit" class="btn btn-default">Search</button></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form class="form-inline" id="asset-filter-form">
|
||||
{% csrf_token %}
|
||||
<br>
|
||||
<div style="margin-top: 1em;" class="pull-right">
|
||||
<div class="form-group">
|
||||
<label for="cat">Category:</label>
|
||||
<label for="cat" class="sr-only">Category</label>
|
||||
<select name="cat" class="form-control">
|
||||
<option>None</option>
|
||||
<option value="" class="text-muted">(category)</option>
|
||||
{% for name in categories %}
|
||||
{% if name.name == category_select %}
|
||||
<option selected>
|
||||
{% else %}
|
||||
<option>
|
||||
{% endif %}
|
||||
{{ name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="status">Status:</label>
|
||||
<label for="status" class="sr-only">Status</label>
|
||||
<select name="status" class="form-control">
|
||||
<option>None</option>
|
||||
<option value="">(status)</option>
|
||||
{% for name in statuses %}
|
||||
{% if name.name == status_select %}
|
||||
<option selected>
|
||||
{% else %}
|
||||
<option>
|
||||
{% endif %}
|
||||
{{ name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<!---TODO: Auto filter whenever an option is selected, instead of using a button --->
|
||||
<!---TODO: Auto filter whenever an option is selected, instead of using a button -->
|
||||
<button type="submit" class="btn btn-default">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="table">
|
||||
|
||||
@@ -9,304 +9,64 @@
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
{% if edit and object %}
|
||||
Edit Asset: {{ object.asset_id }} - {{ object.description }}
|
||||
{% elif duplicate %}
|
||||
Duplication of Asset: {{ previous_asset_id }}
|
||||
{% elif not object %}
|
||||
Create Asset
|
||||
Edit Asset: {{ object.asset_id }}
|
||||
{% else %}
|
||||
Asset: {{ object.asset_id }} - {{ object.description }}
|
||||
Asset: {{ object.asset_id }}
|
||||
{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="row" style="padding-bottom: 1em">
|
||||
<form method="post" id="asset_update_form" action="{% url 'asset_update' pk=object.pk%}">
|
||||
{% include 'form_errors.html' %}
|
||||
<div class="row" style="padding-bottom: 1em">
|
||||
<div class="col-sm-12">
|
||||
<div class="pull-right">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" id="asset_update_form">
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ object.id|default:0 }}" hidden=true>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Asset Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if edit or duplicate %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.asset_id.id_for_label }}">Asset ID</label>
|
||||
{% if duplicate %}
|
||||
{% render_field form.asset_id|add_class:'form-control' value=object.asset_id %}
|
||||
{% elif object.asset_id %}
|
||||
{% render_field form.asset_id|attr:'readonly disabled'|add_class:'disabled_input form-control' value=object.asset_id %}
|
||||
{% else %}
|
||||
{% render_field form.asset_id|add_class:'form-control' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.description.id_for_label }}"
|
||||
>Description</label>
|
||||
{% render_field form.description|add_class:'form-control' value=object.description %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.category.id_for_label }}" >Category</label>
|
||||
<select name="{{ form.category.name }}" id="{{ form.category.id_for_label }}"
|
||||
required class="form-control">
|
||||
{% for id, choice in form.category.field.choices %}
|
||||
<option value="{{ id }}"
|
||||
{% if object.category.id == id %}selected{% endif %}>{{ choice }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.status.id_for_label }}" >Status</label>
|
||||
<select class="form-control" name="{{ form.status.name }}" id="{{ form.status.id_for_label }}" required>
|
||||
{% for id, choice in form.status.field.choices %}
|
||||
<option value="{{ id }}"
|
||||
{% if not object.status.id and choice == "Active" or object.status.id == id %}selected{% endif %}>{{ choice }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.serial_number.id_for_label }}">Serial Number</label>
|
||||
{% render_field form.serial_number|add_class:'form-control' value=object.serial_number %}
|
||||
</div>
|
||||
<!---TODO: Lower default number of lines in comments box--->
|
||||
<div class="form-group">
|
||||
<label for="{{ form.comments.id_for_label }}">Comments</label>
|
||||
{% render_field form.comments|add_class:'form-control' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<dt>Asset ID</dt>
|
||||
<dd>{{ object.asset_id }}</dd>
|
||||
|
||||
<dt>Description</dt>
|
||||
<dd>{{ object.description }}</dd>
|
||||
|
||||
<dt>Category</dt>
|
||||
<dd>{{ object.category }}</dd>
|
||||
|
||||
<dt>Status</dt>
|
||||
<dd>{{ object.status }}</dd>
|
||||
|
||||
<dt>Serial Number</dt>
|
||||
<dd>{{ object.serial_number|default:'-' }}</dd>
|
||||
|
||||
<dt>Comments</dt>
|
||||
<dd>{{ object.comments|default:'-'|linebreaksbr }}</dd>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partials/asset_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
{% if perms.asset.asset_financial %}
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Purchase Details
|
||||
{% include 'partials/purchasedetails_form.html' %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if edit or duplicate %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.purchased_from.id_for_label }}">Purchased From</label>
|
||||
<select class="form-control" name="{{ form.purchased_from.name }}"
|
||||
id="{{ form.purchased_from.id_for_label }}">
|
||||
{% for id, choice in form.purchased_from.field.choices %}
|
||||
<option value="{{ id }}"
|
||||
{% if object.purchased_from.id == id %}selected{% endif %}>{{ choice }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{%endif%}
|
||||
<div class="col-md-6"
|
||||
{% if not object.is_cable %} hidden="true" {% endif %} id="cable-table">
|
||||
{% include 'partials/cable_form.html' %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">£</span>
|
||||
{% render_field form.purchase_price|add_class:'form-control' value=object.purchase_price %}
|
||||
<div class="col-md-4">
|
||||
{% include 'partials/parent_form.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.salvage_value.id_for_label }}">Salvage Value</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">£</span>
|
||||
{% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.date_acquired.id_for_label }}" >Date
|
||||
Acquired</label>
|
||||
{% if object.date_acquired %}
|
||||
{% render_field form.date_acquired|add_class:'form-control'|attr:'type="date"' value=object.date_acquired|date %}
|
||||
{% else %}
|
||||
<input type="date" name="date_acquired" value="{% now "DATE_FORMAT" %}"
|
||||
class="form-control" id="id_date_acquired">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.date_sold.id_for_label }}">Date Sold</label>
|
||||
{% render_field form.date_sold|add_class:'form-control'|attr:'type="date"' value=object.date_sold|date %}
|
||||
</div>
|
||||
{% else %}
|
||||
<dl>
|
||||
<dt>Purchased From</dt>
|
||||
<dd>{{ object.purchased_from|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Purchase Price</dt>
|
||||
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Salvage Value</dt>
|
||||
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Date Acquired</dt>
|
||||
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
|
||||
{% if object.date_sold %}
|
||||
<dt>Date Sold</dt>
|
||||
<dd>{{ object.date_sold|default_if_none:'-' }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Collection Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if edit or duplicate %}
|
||||
<div class="form-group">
|
||||
<label for="parent_id">Parent</label>
|
||||
<input type="hidden" name="{{ form.parent.html_name }}" id="hidden_parent_id"
|
||||
value="{{ object.parent.id }}">
|
||||
<div class="input-group">
|
||||
<input type="text" id="parent_id" value="{{ object.parent|default:'' }}"
|
||||
disabled="" class="form-control">
|
||||
<span class="input-group-btn"><button type="button" class="btn btn-default" onclick="clearParent()">Clear
|
||||
</button></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="parent_search">Search for asset</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="parent_search" class="form-control">
|
||||
<span class="input-group-btn"><button type="button" class="btn btn-default" onclick="formAssetSearch()">
|
||||
<i class="glyphicon glyphicon-search"></i> Search
|
||||
</button></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col s2">
|
||||
|
||||
<br>
|
||||
</div>
|
||||
<div class="col s12" id="formAssetSearchResult">
|
||||
<!--Placeholder for search results-->
|
||||
</div>
|
||||
{% else %}
|
||||
<dl>
|
||||
<dt>Parent</dt>
|
||||
<dd>
|
||||
{% if object.parent %}
|
||||
<a href="{% url 'asset_detail' object.parent.pk %}">
|
||||
{{ object.parent.asset_id }} - {{ object.parent.description }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span>-</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>Children</dt>
|
||||
{% if object.asset_parent.all %}
|
||||
{% for child in object.asset_parent.all %}
|
||||
<dd>
|
||||
<a href="{% url 'asset_detail' child.pk %}">
|
||||
{{ child.asset_id }} - {{ child.description }}
|
||||
</a>
|
||||
</dd>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<dd><span>-</span></dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'partials/asset_buttons.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% include 'partials/confirm_delete.html' with object=object %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{% block js%}
|
||||
{% if edit %}
|
||||
<script>
|
||||
function updateAsset() {
|
||||
$.ajax({
|
||||
url: "{% url 'ajax_asset_update' %}", // the endpoint
|
||||
type: "POST", // http method
|
||||
data: {
|
||||
form: $('#asset_update_form').serialize()
|
||||
},
|
||||
traditional: true,
|
||||
headers: {
|
||||
'X-CSRFToken': document.getElementById("asset_update_form").csrfmiddlewaretoken.value
|
||||
},
|
||||
|
||||
success: function(data) {
|
||||
// console.log(data);
|
||||
window.location.href = data['url'];
|
||||
},
|
||||
|
||||
error: function(xhr) {
|
||||
console.log(xhr.status + ": " + xhr.responseText)
|
||||
function checkIfCableHidden() {
|
||||
if (document.getElementById("id_is_cable").checked) {
|
||||
document.getElementById("cable-table").hidden = false;
|
||||
} else {
|
||||
document.getElementById("cable-table").hidden = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
checkIfCableHidden();
|
||||
</script>
|
||||
|
||||
{# <script>#}
|
||||
{# $('#asset_update_form').on('submit', function(event){#}
|
||||
{#console.log($('#asset_update_form').serialize());#}
|
||||
{# event.preventDefault();#}
|
||||
{# updateAsset();#}
|
||||
{# return false;#}
|
||||
{# });#}
|
||||
{# </script>#}
|
||||
|
||||
<script>
|
||||
function clearParent() {
|
||||
$('#hidden_parent_id').val('');
|
||||
$('#parent_id').val('');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
{
|
||||
%
|
||||
if edit or duplicate %
|
||||
}
|
||||
var comments_id = '#{{ form.comments.id_for_label }}';
|
||||
$(comments_id).val('{{ object.comments|linebreaksn }}'); {
|
||||
%
|
||||
endif %
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
{% if edit and object %}
|
||||
<!--edit-->
|
||||
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
|
||||
<a class="btn btn-default" href="{% url 'asset_duplicate' object.pk %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||
<a class="btn btn-danger" data-toggle="modal" data-target="#confirm_delete_modal"><i class="glyphicon glyphicon-trash"></i> Delete</a>
|
||||
{% elif duplicate %}
|
||||
<!--duplicate-->
|
||||
<button type="submit" class="btn btn-default"><i class="glyphicon glyphicon-ok-sign"></i> Create Duplicate</button>
|
||||
{% elif create %}
|
||||
<!--create-->
|
||||
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
|
||||
{% else %}
|
||||
<!--detail view-->
|
||||
<div class="btn-group">
|
||||
{% if edit and object %}
|
||||
<!--edit-->
|
||||
<button type="button" class="btn btn-success" onclick="updateAsset()"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
|
||||
<a class="btn btn-default" href="{% url 'asset_update' object.pk %}?duplicate=true"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||
<a class="btn btn-danger" data-toggle="modal" data-target="#confirm_delete_modal"><i class="glyphicon glyphicon-trash"></i> Delete</a>
|
||||
{% elif duplicate %}
|
||||
<!--duplicate-->
|
||||
<button type="button" class="btn btn-default" onclick="updateAsset()"><i class="glyphicon glyphicon-ok-sign"></i> Create Duplicate</button>
|
||||
<a href="{% url 'asset_detail' previous_asset_pk %}" class="btn btn-warning"><i class="glyphicon glyphicon-remove"></i> Cancel</a>
|
||||
{% elif not object %}
|
||||
<!--create-->
|
||||
<button type="button" class="btn btn-success" onclick="updateAsset()"><i class="glyphicon glyphicon-floppy-disk"></i> Save</button>
|
||||
{% else %}
|
||||
<!--detail view-->
|
||||
<a href="{% url 'asset_update' object.pk %}" class="btn btn-default"><i class="glyphicon glyphicon-edit"></i> Edit</a>
|
||||
<a class="btn btn-default" href="{% url 'asset_update' object.pk %}?duplicate=true"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||
<a class="btn btn-default" href="{% url 'asset_duplicate' object.pk %}"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||
<a class="btn btn-danger" data-toggle="modal" data-target="#confirm_delete_modal"><i class="glyphicon glyphicon-trash"></i> Delete</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if create or edit or duplicate %}
|
||||
<br>
|
||||
<button type="reset" class="btn btn-link" onclick="
|
||||
{%if duplicate%}
|
||||
{% url 'asset_detail' previous_asset_pk %}
|
||||
{%else%}
|
||||
history.back(){%endif%}">Cancel</button>
|
||||
{% endif %}
|
||||
|
||||
75
assets/templates/partials/asset_form.html
Normal file
75
assets/templates/partials/asset_form.html
Normal file
@@ -0,0 +1,75 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load asset_templatetags %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Asset Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.asset_id.id_for_label }}">Asset ID</label>
|
||||
{% if duplicate %}
|
||||
{% render_field form.asset_id|add_class:'form-control' value=object.asset_id %}
|
||||
{% elif object.asset_id %}
|
||||
{% render_field form.asset_id|attr:'readonly'|add_class:'disabled_input form-control' value=object.asset_id %}
|
||||
{% else %}
|
||||
{% render_field form.asset_id|add_class:'form-control' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.description.id_for_label }}">Description</label>
|
||||
{% render_field form.description|add_class:'form-control' value=object.description %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.category.id_for_label }}" >Category</label>
|
||||
<select name="{{ form.category.name }}" id="{{ form.category.id_for_label }}" required class="form-control">
|
||||
{% for id, choice in form.category.field.choices %}
|
||||
<option value="{{ id }}"
|
||||
{% if object.category.id == id %}
|
||||
selected
|
||||
{% endif %}>
|
||||
{{ choice }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% render_field form.is_cable|attr:'onchange=checkIfCableHidden()' %} <label for="{{ form.is_cable.id_for_label }}">Cable?</label>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.status.id_for_label }}" >Status</label>
|
||||
<select class="form-control" name="{{ form.status.name }}" id="{{ form.status.id_for_label }}" required>
|
||||
{% for id, choice in form.status.field.choices %}
|
||||
<option value="{{ id }}"
|
||||
{% if not object.status.id and choice == "Active" or object.status.id == id %}selected{% endif %}>{{ choice }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.serial_number.id_for_label }}">Serial Number</label>
|
||||
{% render_field form.serial_number|add_class:'form-control' value=object.serial_number %}
|
||||
</div>
|
||||
<!---TODO: Lower default number of lines in comments box-->
|
||||
<div class="form-group">
|
||||
<label for="{{ form.comments.id_for_label }}">Comments</label>
|
||||
{% render_field form.comments|add_class:'form-control' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<dt>Asset ID</dt>
|
||||
<dd>{{ object.asset_id }}</dd>
|
||||
|
||||
<dt>Description</dt>
|
||||
<dd style="overflow-wrap: break-word;">{{ object.description }}</dd>
|
||||
|
||||
<dt>Category</dt>
|
||||
<dd>{{ object.category }}</dd>
|
||||
|
||||
<dt>Status</dt>
|
||||
<dd>{{ object.status }}</dd>
|
||||
|
||||
<dt>Serial Number</dt>
|
||||
<dd>{{ object.serial_number|default:'-' }}</dd>
|
||||
|
||||
<dt>Comments</dt>
|
||||
<dd style="overflow-wrap: break-word;">{{ object.comments|default:'-'|linebreaksbr }}</dd>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,6 @@
|
||||
{% for item in object_list %}
|
||||
{# <li><a href="{% url 'asset_detail' item.pk %}">{{ item.asset_id }} - {{ item.description }}</a></li>#}
|
||||
<!---TODO: When the ability to filter the list is added, remove the colours from the filter - specifically, stop greying out sold/binned stuff if it is being searched for--->
|
||||
<tr class="
|
||||
<!---TODO: When the ability to filter the list is added, remove the colours from the filter - specifically, stop greying out sold/binned stuff if it is being searched for--> <tr class="
|
||||
{% if item.status.name == 'Broken' %}
|
||||
danger
|
||||
{% elif item.status.name == 'Lost'%}
|
||||
@@ -12,15 +11,18 @@
|
||||
text-muted
|
||||
{% endif %}
|
||||
">
|
||||
<td><a href="{% url 'asset_detail' item.pk %}">{{ item.asset_id }}</a></td>
|
||||
<td>{{ item.description }}</td>
|
||||
<td>{{ item.category }}</td>
|
||||
<td>{{ item.status }}</td>
|
||||
<td style="vertical-align: middle;"><a href="{% url 'asset_detail' item.pk %}">{{ item.asset_id }}</a></td>
|
||||
<td style="vertical-align: middle; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; max-width: 25vw">{{ item.description }}</td>
|
||||
<td style="vertical-align: middle;">{{ item.category }}</td>
|
||||
<td style="vertical-align: middle;">{{ item.status }}</td>
|
||||
<td>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_detail' item.pk %}"><i class="glyphicon glyphicon-eye-open"></i> View</a>
|
||||
{% if perms.assets.change_asset %}
|
||||
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_update' item.pk %}"><i class="glyphicon glyphicon-edit"></i> Edit</a>
|
||||
<a type="button" class="btn btn-default btn-sm" href="{% url 'asset_update' item.pk %}?duplicate=true"><i class="glyphicon glyphicon-duplicate"></i> Duplicate</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
63
assets/templates/partials/asset_picker.html
Normal file
63
assets/templates/partials/asset_picker.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<select name="parent" id="parent_id" class="selectpicker">
|
||||
{% if object.parent%}
|
||||
<option value="{{object.parent.pk}}" selected>{{object.parent.description}}</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
|
||||
{% load static %}
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="{% static "css/bootstrap-select.min.css" %}"/>
|
||||
<link rel="stylesheet" href="{% static "css/ajax-bootstrap-select.css" %}"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
<script src="{% static "js/bootstrap-select.js" %}"></script>
|
||||
<script src="{% static "js/ajax-bootstrap-select.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
$('#parent_id')
|
||||
.selectpicker({
|
||||
liveSearch: true
|
||||
})
|
||||
.ajaxSelectPicker({
|
||||
ajax: {
|
||||
url: '{% url 'asset_search_json'%}',
|
||||
type: "get",
|
||||
data: function () {
|
||||
var params = {
|
||||
{% verbatim %}query: '{{{q}}}'{% endverbatim %}
|
||||
};
|
||||
return params;
|
||||
}
|
||||
},
|
||||
locale: {
|
||||
emptyTitle: 'Search for item...'
|
||||
},
|
||||
preprocessData: function(data){
|
||||
var assets = [];
|
||||
if(data.length){
|
||||
var len = data.length;
|
||||
for(var i = 0; i < len; i++){
|
||||
var curr = data[i];
|
||||
assets.push(
|
||||
{
|
||||
'value': curr.id,
|
||||
'text': curr.label,
|
||||
'disabled': false
|
||||
}
|
||||
);
|
||||
}
|
||||
assets.push(
|
||||
{
|
||||
'value': null,
|
||||
'text': "No parent"
|
||||
});
|
||||
}
|
||||
return assets;
|
||||
},
|
||||
preserveSelected: false
|
||||
});
|
||||
</script>
|
||||
{% endblock js %}
|
||||
75
assets/templates/partials/cable_form.html
Normal file
75
assets/templates/partials/cable_form.html
Normal file
@@ -0,0 +1,75 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load asset_templatetags %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Cable Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.plug.id_for_label }}">Plug</label>
|
||||
<select name="{{ form.plug.name }}" id="{{ form.plug.id_for_label }}" class="form-control">
|
||||
<option value="">
|
||||
{% for connector in connectors %}
|
||||
<option value="{{ connector.pk }}">
|
||||
{{ connector.description }}
|
||||
</option>
|
||||
{%endfor%}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.socket.id_for_label }}">Socket</label>
|
||||
<select name="{{ form.socket.name }}" id="{{ form.socket.id_for_label }}" class="form-control">
|
||||
<option value="">
|
||||
{% for connector in connectors %}
|
||||
<option value="{{ connector.pk }}">
|
||||
{{ connector.description }}
|
||||
</option>
|
||||
{%endfor%}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.length.id_for_label }}">Length</label>
|
||||
<div class="input-group">
|
||||
{% render_field form.length|add_class:'form-control' %}
|
||||
<span class="input-group-addon">{{ form.length.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.csa.id_for_label }}">Cross Sectional Area</label>
|
||||
<div class="input-group">
|
||||
{% render_field form.csa|add_class:'form-control' value=object.csa %}
|
||||
<span class="input-group-addon">{{ form.csa.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.circuits.id_for_label }}">Circuits</label>
|
||||
{% render_field form.circuits|add_class:'form-control' value=object.circuits %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="{{ form.cores.id_for_label }}">Cores</label>
|
||||
{% render_field form.cores|add_class:'form-control' value=object.cores %}
|
||||
</div>
|
||||
{% else %}
|
||||
<dl>
|
||||
<dt>Socket</dt>
|
||||
<dd>{{ object.socket|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Plug</dt>
|
||||
<dd>{{ object.plug|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Length</dt>
|
||||
<dd>{{ object.length|default_if_none:'-' }}m</dd>
|
||||
|
||||
<dt>Cross Sectional Area</dt>
|
||||
<dd>{{ object.csa|default_if_none:'-' }}m^2</dd>
|
||||
|
||||
<dt>Circuits</dt>
|
||||
<dd>{{ object.circuits|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Cores</dt>
|
||||
<dd>{{ object.cores|default_if_none:'-' }}</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
41
assets/templates/partials/parent_form.html
Normal file
41
assets/templates/partials/parent_form.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load asset_templatetags %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Collection Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group">
|
||||
<label for="selectpicker">Set Parent</label>
|
||||
{% include 'partials/asset_picker.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<dl>
|
||||
<dt>Parent</dt>
|
||||
<dd>
|
||||
{% if object.parent %}
|
||||
<a href="{% url 'asset_detail' object.parent.pk %}">
|
||||
{{ object.parent.asset_id }} - {{ object.parent.description }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span>-</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt>Children</dt>
|
||||
{% if object.asset_parent.all %}
|
||||
{% for child in object.asset_parent.all %}
|
||||
<dd>
|
||||
<a href="{% url 'asset_detail' child.pk %}">
|
||||
{{ child.asset_id }} - {{ child.description }}
|
||||
</a>
|
||||
</dd>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<dd><span>-</span></dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% endif%}
|
||||
</div>
|
||||
</div>
|
||||
76
assets/templates/partials/purchasedetails_form.html
Normal file
76
assets/templates/partials/purchasedetails_form.html
Normal file
@@ -0,0 +1,76 @@
|
||||
{% load widget_tweaks %}
|
||||
{% load asset_templatetags %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Purchase Details
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if create or edit or duplicate %}
|
||||
<div class="form-group">
|
||||
<label for="{{ form.purchased_from.id_for_label }}">Purchased From</label>
|
||||
<select class="form-control" name="{{ form.purchased_from.name }}" id="{{ form.purchased_from.id_for_label }}">
|
||||
{% for id, choice in form.purchased_from.field.choices %}
|
||||
<option value="{{ id }}"
|
||||
{% if object.purchased_from.id == id %}
|
||||
selected
|
||||
{% endif %}>
|
||||
{{ choice }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.purchase_price.id_for_label }}">Purchase Price</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">£</span>
|
||||
{% render_field form.purchase_price|add_class:'form-control' value=object.purchase_price %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.salvage_value.id_for_label }}">Salvage Value</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">£</span>
|
||||
{% render_field form.salvage_value|add_class:'form-control' value=object.salvage_value %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.date_acquired.id_for_label }}" >Date Acquired</label>
|
||||
{% if object.date_acquired%}
|
||||
{% with date_acq=object.date_acquired|date:"Y-m-d" %}
|
||||
{% render_field form.date_acquired|add_class:'form-control'|attr:'type="date"' value=date_acq %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<input type="date" name="date_acquired" value="{% now "Y-m-d" %}"
|
||||
class="form-control" id="id_date_acquired">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.date_sold.id_for_label }}">Date Sold</label>
|
||||
{% with date_sol=object.form.date_sold|date:"Y-m-d" %}
|
||||
{% render_field form.date_sold|add_class:'form-control'|attr:'type="date"' value=date_sol %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% else %}
|
||||
<dl>
|
||||
<dt>Purchased From</dt>
|
||||
<dd>{{ object.purchased_from|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Purchase Price</dt>
|
||||
<dd>£{{ object.purchase_price|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Salvage Value</dt>
|
||||
<dd>£{{ object.salvage_value|default_if_none:'-' }}</dd>
|
||||
|
||||
<dt>Date Acquired</dt>
|
||||
<dd>{{ object.date_acquired|default_if_none:'-' }}</dd>
|
||||
{% if object.date_sold %}
|
||||
<dt>Date Sold</dt>
|
||||
<dd>{{ object.date_sold|default_if_none:'-' }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,24 +2,26 @@ from django.urls import path, include
|
||||
from rest_framework import routers
|
||||
from assets import views, api
|
||||
|
||||
from PyRIGS.decorators import permission_required_with_403
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'api/assets', api.AssetViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
# path('', views.Index.as_view(), name='index'),
|
||||
path('', views.AssetList.as_view(), name='index'),
|
||||
path('asset/list/', views.AssetList.as_view(), name='asset_list'),
|
||||
path('asset/<int:pk>/', views.AssetDetail.as_view(), name='asset_detail'),
|
||||
path('asset/create/', views.AssetEdit.as_view(), name='asset_create'),
|
||||
path('asset/<int:pk>/edit/', views.AssetEdit.as_view(), name='asset_update'),
|
||||
path('asset/delete/', views.asset_delete, name='ajax_asset_delete'),
|
||||
path('asset/update/', views.asset_update, name='ajax_asset_update'),
|
||||
path('asset/create/', permission_required_with_403('assets.create_asset')(views.AssetCreate.as_view()), name='asset_create'),
|
||||
path('asset/<int:pk>/edit/', permission_required_with_403('assets.change_asset')(views.AssetEdit.as_view()), name='asset_update'),
|
||||
path('asset/<int:pk>/duplicate/', permission_required_with_403('assets.create_asset')(views.AssetDuplicate.as_view()), name='asset_duplicate'),
|
||||
path('asset/delete/', permission_required_with_403('assets.delete_asset')(views.asset_delete), name='ajax_asset_delete'),
|
||||
|
||||
path('asset/search/', views.AssetSearch.as_view(), name='asset_search_json'),
|
||||
|
||||
path('supplier/list', views.SupplierList.as_view(), name='supplier_list'),
|
||||
path('supplier/<int:pk>', views.SupplierDetail.as_view(), name='supplier_detail'),
|
||||
path('supplier/create', views.SupplierCreate.as_view(), name='supplier_create'),
|
||||
path('supplier/<int:pk>/edit', views.SupplierUpdate.as_view(), name='supplier_update'),
|
||||
path('supplier/create', permission_required_with_403('assets.create_supplier')(views.SupplierCreate.as_view()), name='supplier_create'),
|
||||
path('supplier/<int:pk>/edit', permission_required_with_403('assets.edit_supplier')(views.SupplierUpdate.as_view()), name='supplier_update'),
|
||||
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
|
||||
|
||||
125
assets/views.py
125
assets/views.py
@@ -1,7 +1,7 @@
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponse, QueryDict
|
||||
from django.http import HttpResponse, QueryDict, JsonResponse
|
||||
from django.core import serializers
|
||||
from django.views import generic
|
||||
from django.contrib.auth import views as auth_views
|
||||
@@ -13,6 +13,7 @@ from dateutil import parser
|
||||
import simplejson as json
|
||||
from assets import models, forms
|
||||
|
||||
|
||||
class AssetList(LoginRequiredMixin, generic.ListView):
|
||||
model = models.Asset
|
||||
template_name = 'asset_list.html'
|
||||
@@ -20,96 +21,101 @@ class AssetList(LoginRequiredMixin, generic.ListView):
|
||||
ordering = ['-pk']
|
||||
|
||||
def get_queryset(self):
|
||||
#TODO Feedback to user when search fails
|
||||
# TODO Feedback to user when search fails
|
||||
query = self.request.GET.get('query', "")
|
||||
if len(query) >= 3:
|
||||
return self.model.objects.filter(Q(asset_id__exact=query) | Q(description__icontains=query))
|
||||
elif query != "":
|
||||
return self.model.objects.filter(Q(asset_id__exact=query))
|
||||
if len(query) == 0:
|
||||
queryset = self.model.objects.all()
|
||||
elif len(query) >= 3:
|
||||
queryset = self.model.objects.filter(Q(asset_id__exact=query) | Q(description__icontains=query))
|
||||
else:
|
||||
queryset = self.model.objects.filter(Q(asset_id__exact=query))
|
||||
|
||||
cat = self.request.GET.get('cat', "")
|
||||
status = self.request.GET.get('status', "")
|
||||
if cat != "None":
|
||||
return self.model.objects.filter(category__name__exact=cat)
|
||||
elif status != "None":
|
||||
return self.model.objects.filter(status__name__exact=status)
|
||||
else:
|
||||
return self.model.objects.all()
|
||||
if cat != "":
|
||||
queryset = queryset.filter(category__name__exact=cat)
|
||||
if status != "":
|
||||
queryset = queryset.filter(status__name__exact=status)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AssetList, self).get_context_data(**kwargs)
|
||||
context["search_name"] = self.request.GET.get('query', "")
|
||||
|
||||
context["categories"] = models.AssetCategory.objects.all()
|
||||
context["category_select"] = self.request.GET.get('cat', "")
|
||||
|
||||
context["statuses"] = models.AssetStatus.objects.all()
|
||||
return context;
|
||||
context["status_select"] = self.request.GET.get('status', "")
|
||||
return context
|
||||
|
||||
|
||||
class AssetSearch(AssetList):
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
result = []
|
||||
|
||||
for asset in context["object_list"]:
|
||||
result.append({"id": asset.pk, "label": (asset.asset_id + " | " + asset.description)})
|
||||
|
||||
return JsonResponse(result, safe=False)
|
||||
|
||||
|
||||
class AssetDetail(LoginRequiredMixin, generic.DetailView):
|
||||
model = models.Asset
|
||||
template_name = 'asset_update.html'
|
||||
|
||||
|
||||
# class AssetCreate(LoginRequiredMixin, generic.TemplateView):
|
||||
# fields = '__all__'
|
||||
# template_name = 'asset_update.html'
|
||||
# # success_url = reverse_lazy('asset_list')
|
||||
|
||||
class AssetEdit(LoginRequiredMixin, generic.TemplateView):
|
||||
class AssetEdit(LoginRequiredMixin, generic.UpdateView):
|
||||
template_name = 'asset_update.html'
|
||||
model = models.Asset
|
||||
form_class = forms.AssetForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AssetEdit, self).get_context_data(**kwargs)
|
||||
if self.kwargs:
|
||||
context['object'] = get_object_or_404(models.Asset, pk=self.kwargs['pk'])
|
||||
context['form'] = forms.AssetForm
|
||||
# context['asset_names'] = models.Asset.objects.values_list('asset_id', 'description').order_by('-date_acquired')[]
|
||||
|
||||
if self.request.GET.get('duplicate'):
|
||||
context['duplicate'] = True
|
||||
context['previous_asset_id'] = context['object'].asset_id
|
||||
context['previous_asset_pk'] = context['object'].pk
|
||||
context['object'].pk = 0
|
||||
context['object'].asset_id = ''
|
||||
context['object'].serial_number = ''
|
||||
else:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['edit'] = True
|
||||
context["connectors"] = models.Connector.objects.all()
|
||||
|
||||
return context
|
||||
|
||||
@login_required()
|
||||
def asset_update(request):
|
||||
context = dict()
|
||||
def get_success_url(self):
|
||||
return reverse("asset_detail", kwargs={"pk": self.object.id})
|
||||
|
||||
if request.method == 'POST' and request.is_ajax():
|
||||
defaults = QueryDict(request.POST['form'].encode('ASCII')).dict()
|
||||
defaults.pop('csrfmiddlewaretoken')
|
||||
|
||||
asset_pk = int(defaults.pop('id'))
|
||||
class AssetCreate(LoginRequiredMixin, generic.CreateView):
|
||||
template_name = 'asset_create.html'
|
||||
model = models.Asset
|
||||
form_class = forms.AssetForm
|
||||
|
||||
if defaults['date_acquired']:
|
||||
defaults['date_acquired'] = parser.parse(defaults.pop('date_acquired'))
|
||||
else:
|
||||
defaults['date_acquired'] = None
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(AssetCreate, self).get_context_data(**kwargs)
|
||||
|
||||
if defaults['date_sold']:
|
||||
defaults['date_sold'] = parser.parse(defaults.pop('date_sold'))
|
||||
else:
|
||||
defaults['date_sold'] = None
|
||||
context["create"] = True
|
||||
context["connectors"] = models.Connector.objects.all()
|
||||
|
||||
# if defaults['parent']:
|
||||
# defaults['parent'] = models.Asset.objects.get(asset_id=defaults.pop('parent'))
|
||||
return context
|
||||
|
||||
form = forms.AssetForm(defaults)
|
||||
context['valid'] = form.is_valid()
|
||||
context['errors'] = form.errors.as_json()
|
||||
def get_success_url(self):
|
||||
return reverse("asset_detail", kwargs={"pk": self.object.id})
|
||||
|
||||
if asset_pk == 0:
|
||||
asset = models.Asset.objects.create(**form.cleaned_data)
|
||||
else:
|
||||
asset, created = models.Asset.objects.update_or_create(pk=asset_pk, defaults=form.cleaned_data)
|
||||
|
||||
context['url'] = reverse('asset_detail', args=[asset.pk])
|
||||
class DuplicateMixin:
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
self.object.pk = None
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
return HttpResponse(json.dumps(context), content_type='application/json')
|
||||
|
||||
class AssetDuplicate(DuplicateMixin, AssetCreate):
|
||||
model = models.Asset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["create"] = None
|
||||
context["duplicate"] = True
|
||||
context['previous_asset_id'] = self.get_object().asset_id
|
||||
context["previous_asset_pk"] = self.kwargs.get(self.pk_url_kwarg)
|
||||
return context
|
||||
|
||||
|
||||
@login_required()
|
||||
@@ -123,6 +129,7 @@ def asset_delete(request):
|
||||
|
||||
return HttpResponse(json.dumps(context), content_type='application/json')
|
||||
|
||||
|
||||
class SupplierList(generic.ListView):
|
||||
model = models.Supplier
|
||||
template_name = 'supplier_list.html'
|
||||
|
||||
Reference in New Issue
Block a user