This commit is contained in:
James Newlands 2018-04-17 23:05:59 +10:00
commit 728896be6c
24 changed files with 530 additions and 53 deletions

View File

@ -8,7 +8,14 @@ from .models import Build
class BuildAdmin(admin.ModelAdmin): class BuildAdmin(admin.ModelAdmin):
list_display = ('status', ) list_display = ('part',
'status',
'batch',
'quantity',
'creation_date',
'completion_date',
'title',
'notes',
)
admin.site.register(Build, BuildAdmin) admin.site.register(Build, BuildAdmin)

View File

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 06:57
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0019_auto_20180416_1249'),
('build', '0003_build_part'),
]
operations = [
migrations.CreateModel(
name='BuildOutput',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1, help_text='Number of parts to build', validators=[django.core.validators.MinValueValidator(1)])),
],
),
migrations.RemoveField(
model_name='build',
name='part',
),
migrations.RemoveField(
model_name='build',
name='quantity',
),
migrations.AlterField(
model_name='build',
name='status',
field=models.PositiveIntegerField(choices=[(40, 'Cancelled'), (10, 'Pending'), (20, 'Allocated'), (50, 'Complete'), (30, 'Holding')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
),
migrations.AddField(
model_name='buildoutput',
name='build',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='outputs', to='build.Build'),
),
migrations.AddField(
model_name='buildoutput',
name='part',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'),
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 08:29
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0004_auto_20180417_0657'),
]
operations = [
migrations.AddField(
model_name='buildoutput',
name='batch',
field=models.CharField(blank=True, help_text='Batch code for this build output', max_length=100),
),
]

View File

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 09:33
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0022_auto_20180417_0819'),
('build', '0005_buildoutput_batch'),
]
operations = [
migrations.RemoveField(
model_name='buildoutput',
name='build',
),
migrations.RemoveField(
model_name='buildoutput',
name='part',
),
migrations.AddField(
model_name='build',
name='batch',
field=models.CharField(blank=True, help_text='Batch code for this build output', max_length=100, null=True),
),
migrations.AddField(
model_name='build',
name='completion_date',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='build',
name='creation_date',
field=models.DateField(auto_now=True),
),
migrations.AddField(
model_name='build',
name='notes',
field=models.CharField(blank=True, max_length=500),
),
migrations.AddField(
model_name='build',
name='part',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'),
preserve_default=False,
),
migrations.AddField(
model_name='build',
name='quantity',
field=models.PositiveIntegerField(default=1, help_text='Number of parts to build', validators=[django.core.validators.MinValueValidator(1)]),
),
migrations.AddField(
model_name='build',
name='title',
field=models.CharField(default='Build title', help_text='Brief description of the build', max_length=100),
preserve_default=False,
),
migrations.DeleteModel(
name='BuildOutput',
),
]

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 10:25
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('build', '0006_auto_20180417_0933'),
]
operations = [
migrations.AlterField(
model_name='build',
name='status',
field=models.PositiveIntegerField(choices=[(40, 'Complete'), (10, 'Pending'), (20, 'Holding'), (30, 'Cancelled')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
),
]

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.translation import ugettext as _
from django.db import models from django.db import models
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from InvenTree.helpers import ChoiceEnum
from part.models import Part from part.models import Part
class Build(models.Model): class Build(models.Model):
@ -14,26 +14,65 @@ class Build(models.Model):
Parts are then taken from stock Parts are then taken from stock
""" """
class BUILD_STATUS(ChoiceEnum): # Build status codes
# The build is 'pending' - no action taken yet PENDING = 10 # Build is pending / active
Pending = 10 HOLDING = 20 # Build is currently being held
CANCELLED = 30 # Build was cancelled
COMPLETE = 40 # Build is complete
# The parts required for this build have been allocated BUILD_STATUS_CODES = {
Allocated = 20 PENDING : _("Pending"),
HOLDING : _("Holding"),
CANCELLED : _("Cancelled"),
COMPLETE : _("Complete"),
}
# The build has been cancelled (parts unallocated) batch = models.CharField(max_length=100, blank=True, null=True,
Cancelled = 30 help_text='Batch code for this build output')
# The build is complete!
Complete = 40
# Status of the build # Status of the build
status = models.PositiveIntegerField(default=BUILD_STATUS.Pending.value, status = models.PositiveIntegerField(default=PENDING,
choices=BUILD_STATUS.choices()) choices=BUILD_STATUS_CODES.items(),
validators=[MinValueValidator(0)])
# Date the build model was 'created'
creation_date = models.DateField(auto_now=True, editable=False)
# Date the build was 'completed'
completion_date = models.DateField(null=True, blank=True)
# Brief build title
title = models.CharField(max_length=100, help_text='Brief description of the build')
# A reference to the part being built
# Only 'buildable' parts can be selected
part = models.ForeignKey(Part, on_delete=models.CASCADE, part = models.ForeignKey(Part, on_delete=models.CASCADE,
related_name='builds') related_name='builds',
limit_choices_to={'buildable': True},
)
# How many parts to build?
quantity = models.PositiveIntegerField(default=1, quantity = models.PositiveIntegerField(default=1,
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
help_text='Number of parts to build') help_text='Number of parts to build')
# Notes can be attached to each build output
notes = models.CharField(max_length=500, blank=True)
@property
def is_active(self):
""" Is this build active?
An active build is either:
- Pending
- Holding
"""
return self.status in [
self.PENDING,
self.HOLDING
]
@property
def is_complete(self):
return self.status == self.COMPLETE

View File

@ -24,10 +24,13 @@ class EditPartForm(forms.ModelForm):
'description', 'description',
'IPN', 'IPN',
'URL', 'URL',
'default_location',
'default_supplier',
'minimum_stock', 'minimum_stock',
'buildable', 'buildable',
'trackable', 'trackable',
'purchaseable', 'purchaseable',
'salable',
] ]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 08:06
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0019_auto_20180416_1249'),
]
operations = [
migrations.AddField(
model_name='part',
name='salable',
field=models.BooleanField(default=False, help_text='Can this part be sold to customers?'),
),
]

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 08:12
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0010_stockitem_build'),
('part', '0020_part_salable'),
]
operations = [
migrations.AddField(
model_name='part',
name='default_location',
field=models.ForeignKey(blank=True, help_text='Where is this item normally stored?', null=True, on_delete=django.db.models.deletion.SET_NULL, to='stock.StockLocation'),
),
]

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-17 08:19
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('supplier', '0007_auto_20180416_1253'),
('part', '0021_part_default_location'),
]
operations = [
migrations.AddField(
model_name='part',
name='default_supplier',
field=models.ForeignKey(blank=True, help_text='Default supplier part', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='supplier.SupplierPart'),
),
migrations.AlterField(
model_name='part',
name='default_location',
field=models.ForeignKey(blank=True, help_text='Where is this item normally stored?', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_parts', to='stock.StockLocation'),
),
]

View File

@ -1,15 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import os
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from InvenTree.models import InvenTreeTree
import os
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from InvenTree.models import InvenTreeTree
# from stock.models import StockLocation
class PartCategory(InvenTreeTree): class PartCategory(InvenTreeTree):
""" PartCategory provides hierarchical organization of Part objects. """ PartCategory provides hierarchical organization of Part objects.
@ -103,6 +106,18 @@ class Part(models.Model):
image = models.ImageField(upload_to=rename_part_image, max_length=255, null=True, blank=True) image = models.ImageField(upload_to=rename_part_image, max_length=255, null=True, blank=True)
default_location = models.ForeignKey('stock.StockLocation', on_delete=models.SET_NULL,
blank=True, null=True,
help_text='Where is this item normally stored?',
related_name='default_parts')
# Default supplier part
default_supplier = models.ForeignKey('supplier.SupplierPart',
on_delete=models.SET_NULL,
blank=True, null=True,
help_text='Default supplier part',
related_name='default_parts')
# Minimum "allowed" stock level # Minimum "allowed" stock level
minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)], help_text='Minimum allowed stock level') minimum_stock = models.PositiveIntegerField(default=0, validators=[MinValueValidator(0)], help_text='Minimum allowed stock level')
@ -121,6 +136,9 @@ class Part(models.Model):
# Is this part "purchaseable"? # Is this part "purchaseable"?
purchaseable = models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?') purchaseable = models.BooleanField(default=True, help_text='Can this part be purchased from external suppliers?')
# Can this part be sold to customers?
salable = models.BooleanField(default=False, help_text="Can this part be sold to customers?")
def __str__(self): def __str__(self):
if self.IPN: if self.IPN:
return "{name} ({ipn})".format( return "{name} ({ipn})".format(
@ -140,9 +158,11 @@ class Part(models.Model):
This subtracts stock which is already allocated This subtracts stock which is already allocated
""" """
# TODO - For now, just return total stock count total = self.total_stock
# TODO - In future must take account of allocated stock
return self.total_stock total -= self.allocation_count
return max(total, 0)
@property @property
def can_build(self): def can_build(self):
@ -163,8 +183,68 @@ class Part(models.Model):
if total is None or n < total: if total is None or n < total:
total = n total = n
return max(total, 0)
@property
def active_builds(self):
""" Return a list of outstanding builds.
Builds marked as 'complete' or 'cancelled' are ignored
"""
return [b for b in self.builds.all() if b.is_active]
@property
def inactive_builds(self):
""" Return a list of inactive builds
"""
return [b for b in self.builds.all() if not b.is_active]
@property
def quantity_being_built(self):
""" Return the current number of parts currently being built
"""
return sum([b.quantity for b in self.active_builds])
@property
def allocated_builds(self):
""" Return list of builds to which this part is allocated
"""
builds = []
for item in self.used_in.all():
for build in item.part.active_builds:
builds.append(build)
return builds
@property
def allocated_build_count(self):
""" Return the total number of this that are allocated for builds
"""
total = 0
for item in self.used_in.all():
for build in item.part.active_builds:
n = build.quantity * item.quantity
total += n
return total return total
@property
def allocation_count(self):
""" Return true if any of this part is allocated
- To another build
- To a customer order
"""
return sum([
self.allocated_build_count,
])
@property @property
def total_stock(self): def total_stock(self):
""" Return the total stock quantity for this part. """ Return the total stock quantity for this part.

View File

@ -0,0 +1,29 @@
{% extends "part/part_base.html" %}
{% block details %}
{% include "part/tabs.html" with tab="allocation" %}
<h3>Part Allocation</h3>
{% if part.allocated_build_count > 0 %}
<h4>Allocated to Part Builds</h4>
<table class='table table-striped'>
<tr>
<th>Build</th>
<th>Making</th>
<th>Allocted</th>
<th>Status</th>
</tr>
{% for build in part.allocated_builds %}
<tr>
<td><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></td>
<td><a href="{% url 'part-detail' build.part.id %}">{{ build.part.name }}</a></td>
<td>Quantity</td>
<td>{% include "part/build_status.html" with build=build %}</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}

View File

@ -4,10 +4,31 @@
{% include 'part/tabs.html' with tab='build' %} {% include 'part/tabs.html' with tab='build' %}
<h3>Build Part</h3> <h3>Part Builds</h3>
TODO <table class='table table-striped'>
<br><br>
You can build {{ part.can_build }} of this part with current stock. <tr>
<th>Title</th>
<th>Quantity</th>
<th>Status</th>
<th>Completion Date</th>
</tr>
{% if part.active_builds|length > 0 %}
<tr>
<td colspan="4"><b>Active Builds</b></td>
</tr>
{% include "part/build_list.html" with builds=part.active_builds %}
{% endif %}
{% if part.inactive_builds|length > 0 %}
<tr><td colspan="4"></td></tr>
<tr>
<td colspan="4"><b>Inactive Builds</b></td>
</tr>
{% include "part/build_list.html" with builds=part.inactive_builds %}
{% endif %}
</table>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,10 @@
{% for build in builds %}
<tr>
<td><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></td>
<td>{{ build.quantity }}</td>
<td>
{% include "part/build_status.html" with build=build %}
</td>
<td>{% if build.completion_date %}{{ build.completion_date }}{% endif %}</td>
</tr>
{% endfor %}

View File

@ -0,0 +1,11 @@
{% if build.status == build.PENDING %}
<span class='label label-info'>
{% elif build.status == build.HOLDING %}
<span class='label label-warning'>
{% elif build.status == build.CANCELLED %}
<span class='label label-danger'>
{% elif build.status == build.COMPLETE %}
<span class='label label-success'>
{% endif %}
{{ build.get_status_display }}
</span>

View File

@ -23,21 +23,37 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% if part.default_location %}
<tr>
<td>Default Location</td>
<td>{{ part.default_location.pathstring }}</td>
</tr>
{% endif %}
{% if part.default_supplier %}
<tr>
<td>Default Supplier</td>
<td>{{ part.default_supplier.supplier.name }} | {{ part.default_supplier.SKU }}</td>
</tr>
{% endif %}
<tr> <tr>
<td>Units</td> <td>Units</td>
<td>{{ part.units }}</td> <td>{{ part.units }}</td>
</tr> </tr>
<tr> <tr>
<td>Buildable</td> <td>Buildable</td>
<td>{{ part.buildable }}</td> <td>{% include "yesnolabel.html" with value=part.buildable %}</td>
</tr> </tr>
<tr> <tr>
<td>Trackable</td> <td>Trackable</td>
<td>{{ part.trackable }}</td> <td>{% include "yesnolabel.html" with value=part.trackable %}</td>
</tr> </tr>
<tr> <tr>
<td>Purchaseable</td> <td>Purchaseable</td>
<td>{{ part.purchaseable }}</td> <td>{% include "yesnolabel.html" with value=part.purchaseable %}</td>
</tr>
<tr>
<td>Salable</td>
<td>{% include "yesnolabel.html" with value=part.salable %}</td>
</tr> </tr>
{% if part.minimum_stock > 0 %} {% if part.minimum_stock > 0 %}
<tr> <tr>

View File

@ -22,11 +22,6 @@
{% if part.description %} {% if part.description %}
<p><i>{{ part.description }}</i></p> <p><i>{{ part.description }}</i></p>
{% endif %} {% endif %}
</div>
</div>
</div>
<div class="col-sm-6">
<table class="table table-striped">
{% if part.IPN %} {% if part.IPN %}
<tr> <tr>
<td>IPN</td> <td>IPN</td>
@ -39,15 +34,22 @@
<td><a href="{{ part.URL }}">{{ part.URL }}</a></td> <td><a href="{{ part.URL }}">{{ part.URL }}</a></td>
</tr> </tr>
{% endif %} {% endif %}
</div>
</div>
</div>
<div class="col-sm-6">
<h4>Stock Status - {{ part.available_stock }} available</h4>
<table class="table table-striped">
<tr> <tr>
<td>Available Stock</td> <td>In Stock</td>
<td> <td>
{% if part.available_stock == 0 %} {% if part.stock == 0 %}
<span class='label label-danger'>{{ part.available_stock }}</span> <span class='label label-danger'>{{ part.total_stock }}</span>
{% elif part.available_stock < part.minimum_stock %} {% elif part.stock < part.minimum_stock %}
<span class='label label-warning'>{{ part.available_stock }}</span> <span class='label label-warning'>{{ part.total_stock }}</span>
{% else %} {% else %}
{{ part.available_stock }} {{ part.total_stock }}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
@ -62,6 +64,23 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% if part.quantity_being_built > 0 %}
<tr>
<td>Underway</td>
<td>{{ part.quantity_being_built }}</td>
</tr>
{% endif %}
{% endif %}
{% if part.allocation_count > 0 %}
<tr>
<td>Allocated</td>
{% if part.allocation_count > part.total_stock %}
<td><span class='label label-danger'>{{ part.allocation_count }}</span>
{% else %}
{{ part.allocation_count }} {{ part.total_stock }}
{% endif %}
</td>
</tr>
{% endif %} {% endif %}
</table> </table>
</div> </div>

View File

@ -7,7 +7,10 @@
{% if part.used_in_count > 0 %} {% if part.used_in_count > 0 %}
<li{% ifequal tab 'used' %} class="active"{% endifequal %}><a href="{% url 'part-used-in' part.id %}">Used In{% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li> <li{% ifequal tab 'used' %} class="active"{% endifequal %}><a href="{% url 'part-used-in' part.id %}">Used In{% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li>
{% endif %} {% endif %}
<li{% ifequal tab 'stock' %} class="active"{% endifequal %}><a href="{% url 'part-stock' part.id %}">Stock <span class="badge">{{ part.available_stock }}</span></a></li> <li{% ifequal tab 'stock' %} class="active"{% endifequal %}><a href="{% url 'part-stock' part.id %}">Stock <span class="badge">{{ part.total_stock }}</span></a></li>
{% if part.allocation_count > 0 %}
<li{% ifequal tab 'allocation' %} class="active"{% endifequal %}><a href="{% url 'part-allocation' part.id %}">Allocated <span class="badge">{{ part.allocation_count }}</span></a></li>
{% endif %}
{% if part.purchaseable %} {% if part.purchaseable %}
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}><a href="{% url 'part-suppliers' part.id %}">Suppliers <li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}><a href="{% url 'part-suppliers' part.id %}">Suppliers
<span class="badge">{{ part.supplier_count }}<span> <span class="badge">{{ part.supplier_count }}<span>

View File

@ -9,11 +9,13 @@
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Part</th> <th>Part</th>
<th>Uses</th>
<th>Description</th> <th>Description</th>
</tr> </tr>
{% for item in part.used_in.all %} {% for item in part.used_in.all %}
<tr> <tr>
<td><a href="{% url 'part-bom' item.part.id %}">{{ item.part.name }}</a></td> <td><a href="{% url 'part-bom' item.part.id %}">{{ item.part.name }}</a></td>
<td>{{ item.quantity }}</td>
<td>{{ item.part.description }}</td> <td>{{ item.part.description }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -0,0 +1,5 @@
{% if value %}
<span class='label label-success'>Yes</span>
{% else %}
<span class='label label-warning'>No</span>
{% endif %}

View File

@ -44,6 +44,7 @@ part_detail_urls = [
url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'), url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'), url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'),
url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'), url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'),
url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'),
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'), url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),
# Any other URLs go to the part detail page # Any other URLs go to the part detail page

View File

@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.db import models, transaction from django.db import models, transaction
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.contrib.auth.models import User from django.contrib.auth.models import User

View File

@ -88,7 +88,11 @@ class StockItemCreate(CreateView):
loc_id = self.request.GET.get('location', None) loc_id = self.request.GET.get('location', None)
if part_id: if part_id:
initials['part'] = get_object_or_404(Part, pk=part_id) part = get_object_or_404(Part, pk=part_id)
if part:
initials['part'] = get_object_or_404(Part, pk=part_id)
initials['location'] = part.default_location
initials['supplier_part'] = part.default_supplier
if loc_id: if loc_id:
initials['location'] = get_object_or_404(StockLocation, pk=loc_id) initials['location'] = get_object_or_404(StockLocation, pk=loc_id)

View File

@ -1,9 +1,9 @@
Django==1.11 Django>=1.11
pillow==3.1.2 pillow>=5.0.0
djangorestframework==3.6.2 djangorestframework>=3.6.2
django_filter==1.0.2 django_filter>=1.0.2
django-simple-history==1.8.2 django-simple-history>=1.8.2
coreapi==2.3.0 coreapi>=2.3.0
pygments==2.2.0 pygments>=2.2.0
django-crispy-forms==1.7.2 django-crispy-forms>=1.7.2
django-import-export==1.0.0 django-import-export>=1.0.0