Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-05-02 17:47:17 +10:00
commit 79e0f972e7
13 changed files with 255 additions and 93 deletions

View File

@ -177,7 +177,10 @@ class AjaxCreateView(AjaxMixin, CreateView):
# Return the PK of the newly-created object # Return the PK of the newly-created object
data['pk'] = obj.pk data['pk'] = obj.pk
data['url'] = obj.get_absolute_url() try:
data['url'] = obj.get_absolute_url()
except AttributeError:
pass
return self.renderJsonResponse(request, form, data) return self.renderJsonResponse(request, form, data)
@ -223,7 +226,11 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
# Include context data about the updated object # Include context data about the updated object
data['pk'] = obj.id data['pk'] = obj.id
data['url'] = obj.get_absolute_url()
try:
data['url'] = obj.get_absolute_url()
except AttributeError:
pass
return self.renderJsonResponse(request, form, data) return self.renderJsonResponse(request, form, data)

View File

@ -23,59 +23,66 @@
<hr> <hr>
<table class="table table-striped table-condensed"> <div class='row'>
<tr><td>Supplier</td><td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr> <div class='col-sm-6'>
<tr><td>SKU</td><td>{{ part.SKU }}</tr></tr> <table class="table table-striped table-condensed">
<tr> <tr><td>Supplier</td><td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
<td>Internal Part</td> <tr><td>SKU</td><td>{{ part.SKU }}</tr></tr>
<td> <tr>
{% if part.part %} <td>Internal Part</td>
<a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.name }}</a> <td>
{% if part.part %}
<a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.name }}</a>
{% endif %}
</td>
</tr>
{% if part.URL %}
<tr><td>URL</td><td><a href="{{ part.URL }}">{{ part.URL }}</a></td></tr>
{% endif %} {% endif %}
</td> {% if part.description %}
</tr> <tr><td>Description</td><td>{{ part.description }}</td></tr>
{% if part.URL %} {% endif %}
<tr><td>URL</td><td><a href="{{ part.URL }}">{{ part.URL }}</a></td></tr> {% if part.manufacturer %}
{% endif %} <tr><td>Manufacturer</td><td>{{ part.manufacturer }}</td></tr>
{% if part.description %} <tr><td>MPN</td><td>{{ part.MPN }}</td></tr>
<tr><td>Description</td><td>{{ part.description }}</td></tr> {% endif %}
{% endif %} {% if part.note %}
{% if part.manufacturer %} <tr><td>Note</td><td>{{ part.note }}</td></tr>
<tr><td>Manufacturer</td><td>{{ part.manufacturer }}</td></tr> {% endif %}
<tr><td>MPN</td><td>{{ part.MPN }}</td></tr> </table>
{% endif %} </div>
{% if part.note %}
<tr><td>Note</td><td>{{ part.note }}</td></tr> <div class='col-sm-6'>
{% endif %} <table class="table table-striped table-condensed">
<tr><th colspan='2'>Pricing</th></tr> <tr><th colspan='2'>Pricing</th></tr>
<tr><td>Single Price</td><td>{{ part.single_price }}</td></tr> <tr><td>Single Price</td><td>{{ part.single_price }}</td></tr>
{% if part.multiple > 1 %} {% if part.multiple > 1 %}
<tr><td>Order Multiple</td><td>{{ part.multiple }}</td></tr> <tr><td>Order Multiple</td><td>{{ part.multiple }}</td></tr>
{% endif %} {% endif %}
{% if part.base_cost > 0 %} {% if part.base_cost > 0 %}
<tr><td>Base Price (Flat Fee)</td><td>{{ part.base_cost }}</td></tr> <tr><td>Base Price (Flat Fee)</td><td>{{ part.base_cost }}</td></tr>
{% endif %} {% endif %}
{% if part.minimum > 1 %} {% if part.minimum > 1 %}
<tr><td>Minimum Order Quantity</td><td>{{ part.minimum }}</td></tr> <tr><td>Minimum Order Quantity</td><td>{{ part.minimum }}</td></tr>
{% endif %} {% endif %}
</table> {% if part.price_breaks.all %}
<tr><th colspan='2'>Price Breaks</th></tr>
<br>
<h3>Price Breaks</h3>
<table class="table table-striped table-condensed">
<tr> <tr>
<th>Quantity</th> <th>Quantity</th>
<th>Price</th> <th>Price</th>
</tr> </tr>
{% for pb in part.price_breaks.all %} {% for pb in part.price_breaks.all %}
<tr> <tr>
<td>{{ pb.quantity }}</td> <td>{{ pb.quantity }}</td>
<td>{{ pb.cost }}</td> <td>{{ pb.cost }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% endif %}
</table> </table>
</div>
</div>
<br>
<div> <div>
<button class='btn btn-primary' type='button'>New Price Break</button> <button class='btn btn-primary' type='button'>New Price Break</button>

View File

@ -9,7 +9,8 @@ from InvenTree.forms import HelperForm
from django import forms from django import forms
from .models import Part, PartCategory, BomItem from .models import Part, PartCategory, PartAttachment
from .models import BomItem
from .models import SupplierPart from .models import SupplierPart
@ -44,6 +45,18 @@ class BomExportForm(HelperForm):
] ]
class EditPartAttachmentForm(HelperForm):
""" Form for editing a PartAttachment object """
class Meta:
model = PartAttachment
fields = [
'part',
'attachment',
'comment'
]
class EditPartForm(HelperForm): class EditPartForm(HelperForm):
""" Form for editing a Part object """ """ Form for editing a Part object """

View File

@ -14,5 +14,10 @@ class Migration(migrations.Migration):
model_name='part', model_name='part',
name='active', name='active',
field=models.BooleanField(default=True, help_text='Is this part active?'), field=models.BooleanField(default=True, help_text='Is this part active?'),
),
migrations.AddField(
model_name='partattachment',
name='comment',
field=models.CharField(blank=True, help_text='File comment', max_length=100),
), ),
] ]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2 on 2019-04-30 23:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0013_auto_20190429_2229'),
]
operations = [
migrations.AddField(
model_name='partattachment',
name='comment',
field=models.CharField(blank=True, help_text='Attachment description', max_length=100),
),
]

View File

@ -125,7 +125,7 @@ class Part(models.Model):
IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number') IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
# Provide a URL for an external link # Provide a URL for an external link
URL = models.URLField(blank=True, help_text='Link to external URL') URL = models.URLField(blank=True, help_text='Link to extenal URL')
# Part category - all parts must be assigned to a category # Part category - all parts must be assigned to a category
category = models.ForeignKey(PartCategory, related_name='parts', category = models.ForeignKey(PartCategory, related_name='parts',
@ -307,12 +307,6 @@ class Part(models.Model):
def used_in_count(self): def used_in_count(self):
return self.used_in.count() return self.used_in.count()
def required_parts(self):
parts = []
for bom in self.bom_items.all():
parts.append(bom.sub_part)
return parts
@property @property
def supplier_count(self): def supplier_count(self):
# Return the number of supplier parts available for this part # Return the number of supplier parts available for this part
@ -366,7 +360,7 @@ class PartAttachment(models.Model):
attachment = models.FileField(upload_to=attach_file, null=True, blank=True) attachment = models.FileField(upload_to=attach_file, null=True, blank=True)
comment = models.CharField(max_length=100, blank=True, help_text="Attachment description") comment = models.CharField(max_length=100, blank=True, help_text='File comment')
@property @property
def basename(self): def basename(self):
@ -407,11 +401,8 @@ class BomItem(models.Model):
- A part cannot refer to a part which refers to it - A part cannot refer to a part which refers to it
""" """
if self.part is None or self.sub_part is None:
# Field validation will catch these None values
pass
# A part cannot refer to itself in its BOM # A part cannot refer to itself in its BOM
elif self.part == self.sub_part: if self.part == self.sub_part:
raise ValidationError({'sub_part': _('Part cannot be added to its own Bill of Materials')}) raise ValidationError({'sub_part': _('Part cannot be added to its own Bill of Materials')})
# Test for simple recursion # Test for simple recursion

View File

@ -0,0 +1,3 @@
Are you sure you wish to delete this attachment?
<br>
This will remove the file '{{ attachment.basename }}'.

View File

@ -0,0 +1,62 @@
{% extends "part/part_base.html" %}
{% load static %}
{% block details %}
{% include 'part/tabs.html' with tab='attachments' %}
<h4>Attachments</h4>
<div id='toolbar' class='btn-group'>
<button type='button' class='btn btn-success' id='new-attachment'>Add Attachment</button>
</div>
<table class='table table-striped table-condensed' data-toolbar='#toolbar' id='attachment-table'>
<tr>
<th>File</th>
<th>Comment</th>
<th></th>
</tr>
{% for attachment in part.attachments.all %}
<tr>
<td><a href='/media/{{ attachment.attachment }}'>{{ attachment.basename }}</a></td>
<td>{{ attachment.comment }}</td>
<td>
<div class='btn-group' style='float: right;'>
<button type='button' class='btn btn-primary attachment-edit-button' url="{% url 'part-attachment-edit' attachment.id %}" data-toggle='tooltip' title='Edit attachment ({{ attachment.basename }})'>Edit</button>
<button type='button' class='btn btn-danger attachment-delete-button' url="{% url 'part-attachment-delete' attachment.id %}" data-toggle='tooltip' title='Delete attachment ({{ attachment.basename }})'>Delete</button>
</div>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
$("#new-attachment").click(function() {
launchModalForm("{% url 'part-attachment-create' %}?part={{ part.id }}");
});
$("#attachment-table").on('click', '.attachment-edit-button', function() {
var button = $(this);
launchModalForm(button.attr('url'),
{
success: function() {
}
});
});
$("#attachment-table").on('click', '.attachment-delete-button', function() {
var button = $(this);
launchDeleteForm(button.attr('url'), {
success: function() {
}
});
});
{% endblock %}

View File

@ -66,13 +66,6 @@
</tr> </tr>
{% endif %} {% endif %}
</table> </table>
<h4>Attachments</h4>
<ul>
{% for attachment in part.attachments.all %}
<li><a href="/media/{{ attachment.attachment }}">{{ attachment.basename }}</a></li>
{% endfor %}
</ul>
</div> </div>
</div> </div>

View File

@ -1,6 +1,15 @@
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li{% ifequal tab 'detail' %} class="active"{% endifequal %}> <li{% ifequal tab 'detail' %} class="active"{% endifequal %}>
<a href="{% url 'part-detail' part.id %}">Details</a></li> <a href="{% url 'part-detail' part.id %}">Details</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.buildable %} {% if part.buildable %}
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}> <li{% ifequal tab 'bom' %} class="active"{% endifequal %}>
<a href="{% url 'part-bom' part.id %}">BOM<span class="badge">{{ part.bom_count }}</span></a></li> <a href="{% url 'part-bom' part.id %}">BOM<span class="badge">{{ part.bom_count }}</span></a></li>
@ -11,12 +20,6 @@
<li{% ifequal tab 'used' %} class="active"{% endifequal %}> <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> <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.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 %}> <li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}>
<a href="{% url 'part-suppliers' part.id %}">Suppliers <a href="{% url 'part-suppliers' part.id %}">Suppliers
@ -31,4 +34,7 @@
{% endif %} {% endif %}
</a></li> </a></li>
{% endif %} {% endif %}
<li{% ifequal tab 'attachments' %} class="active"{% endifequal %}>
<a href="{% url 'part-attachments' part.id %}">Attachments {% if part.attachments.all|length > 0 %}<span class="badge">{{ part.attachments.all|length }}</span>{% endif %}</a>
</li>
</ul> </ul>

View File

@ -1,5 +1,11 @@
""" """
URL lookup for Part app URL lookup for Part app. Provides URL endpoints for:
- Display / Create / Edit / Delete PartCategory
- Display / Create / Edit / Delete Part
- Create / Edit / Delete PartAttachment
- Display / Create / Edit / Delete SupplierPart
""" """
from django.conf.urls import url, include from django.conf.urls import url, include
@ -19,11 +25,18 @@ supplier_part_urls = [
url(r'^(?P<pk>\d+)/', include(supplier_part_detail_urls)), url(r'^(?P<pk>\d+)/', include(supplier_part_detail_urls)),
] ]
part_attachment_urls = [
url('^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
url(r'^(?P<pk>\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'),
]
part_detail_urls = [ part_detail_urls = [
url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'), url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'),
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'), url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'), url(r'^bom-export/?', views.BomDownload.as_view(), name='bom-export'),
url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'), url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'),
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'),
@ -69,6 +82,10 @@ part_urls = [
# Part category # Part category
url(r'^category/(?P<pk>\d+)/', include(part_category_urls)), url(r'^category/(?P<pk>\d+)/', include(part_category_urls)),
# Part attachments
url(r'^attachment/', include(part_attachment_urls)),
# Bom Items
url(r'^bom/(?P<pk>\d+)/', include(part_bom_urls)), url(r'^bom/(?P<pk>\d+)/', include(part_bom_urls)),
# Top level part list (display top level parts and categories) # Top level part list (display top level parts and categories)

View File

@ -13,11 +13,13 @@ from django.forms.models import model_to_dict
from django.forms import HiddenInput from django.forms import HiddenInput
from company.models import Company from company.models import Company
from .models import PartCategory, Part, BomItem from .models import PartCategory, Part, PartAttachment
from .models import BomItem
from .models import SupplierPart from .models import SupplierPart
from .forms import PartImageForm from .forms import PartImageForm
from .forms import EditPartForm from .forms import EditPartForm
from .forms import EditPartAttachmentForm
from .forms import EditCategoryForm from .forms import EditCategoryForm
from .forms import EditBomItemForm from .forms import EditBomItemForm
from .forms import BomExportForm from .forms import BomExportForm
@ -51,6 +53,81 @@ class PartIndex(ListView):
return context return context
class PartAttachmentCreate(AjaxCreateView):
""" View for creating a new PartAttachment object
- The view only makes sense if a Part object is passed to it
"""
model = PartAttachment
form_class = EditPartAttachmentForm
ajax_form_title = "Add part attachment"
ajax_template_name = "modal_form.html"
def get_data(self):
return {
'success': 'Added attachment'
}
def get_initial(self):
""" Get initial data for new PartAttachment object.
- Client should have requested this form with a parent part in mind
- e.g. ?part=<pk>
"""
initials = super(AjaxCreateView, self).get_initial()
# TODO - If the proper part was not sent, return an error message
initials['part'] = Part.objects.get(id=self.request.GET.get('part'))
return initials
def get_form(self):
""" Create a form to upload a new PartAttachment
- Hide the 'part' field
"""
form = super(AjaxCreateView, self).get_form()
form.fields['part'].widget = HiddenInput()
return form
class PartAttachmentEdit(AjaxUpdateView):
""" View for editing a PartAttachment object """
model = PartAttachment
form_class = EditPartAttachmentForm
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Edit attachment'
def get_data(self):
return {
'success': 'Part attachment updated'
}
def get_form(self):
form = super(AjaxUpdateView, self).get_form()
form.fields['part'].widget = HiddenInput()
return form
class PartAttachmentDelete(AjaxDeleteView):
""" View for deleting a PartAttachment """
model = PartAttachment
ajax_template_name = "part/attachment_delete.html"
context_object_name = "attachment"
def get_data(self):
return {
'danger': 'Deleted part attachment'
}
class PartCreate(AjaxCreateView): class PartCreate(AjaxCreateView):
""" View for creating a new Part object. """ View for creating a new Part object.
@ -102,7 +179,6 @@ class PartCreate(AjaxCreateView):
return form return form
# Pre-fill the category field if a valid category is provided
def get_initial(self): def get_initial(self):
""" Get initial data for the new Part object: """ Get initial data for the new Part object:

View File

@ -4,5 +4,5 @@ ignore =
W293, W293,
# - E501 - line too long (82 characters) # - E501 - line too long (82 characters)
E501 E501
exclude = .git,__pycache__ exclude = .git,__pycache__,*/migrations/*
max-complexity = 20 max-complexity = 20