mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
79e0f972e7
@ -177,7 +177,10 @@ class AjaxCreateView(AjaxMixin, CreateView):
|
||||
# Return the PK of the newly-created object
|
||||
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)
|
||||
|
||||
@ -223,7 +226,11 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
|
||||
|
||||
# Include context data about the updated object
|
||||
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)
|
||||
|
||||
|
@ -23,59 +23,66 @@
|
||||
|
||||
<hr>
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr><td>Supplier</td><td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
|
||||
<tr><td>SKU</td><td>{{ part.SKU }}</tr></tr>
|
||||
<tr>
|
||||
<td>Internal Part</td>
|
||||
<td>
|
||||
{% if part.part %}
|
||||
<a href="{% url 'part-suppliers' part.part.id %}">{{ part.part.name }}</a>
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr><td>Supplier</td><td><a href="{% url 'company-detail-parts' part.supplier.id %}">{{ part.supplier.name }}</a></td></tr>
|
||||
<tr><td>SKU</td><td>{{ part.SKU }}</tr></tr>
|
||||
<tr>
|
||||
<td>Internal Part</td>
|
||||
<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 %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if part.URL %}
|
||||
<tr><td>URL</td><td><a href="{{ part.URL }}">{{ part.URL }}</a></td></tr>
|
||||
{% endif %}
|
||||
{% if part.description %}
|
||||
<tr><td>Description</td><td>{{ part.description }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.manufacturer %}
|
||||
<tr><td>Manufacturer</td><td>{{ part.manufacturer }}</td></tr>
|
||||
<tr><td>MPN</td><td>{{ part.MPN }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.note %}
|
||||
<tr><td>Note</td><td>{{ part.note }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.description %}
|
||||
<tr><td>Description</td><td>{{ part.description }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.manufacturer %}
|
||||
<tr><td>Manufacturer</td><td>{{ part.manufacturer }}</td></tr>
|
||||
<tr><td>MPN</td><td>{{ part.MPN }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.note %}
|
||||
<tr><td>Note</td><td>{{ part.note }}</td></tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='col-sm-6'>
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr><th colspan='2'>Pricing</th></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>
|
||||
{% endif %}
|
||||
{% if part.base_cost > 0 %}
|
||||
{% endif %}
|
||||
{% if part.base_cost > 0 %}
|
||||
<tr><td>Base Price (Flat Fee)</td><td>{{ part.base_cost }}</td></tr>
|
||||
{% endif %}
|
||||
{% if part.minimum > 1 %}
|
||||
{% endif %}
|
||||
{% if part.minimum > 1 %}
|
||||
<tr><td>Minimum Order Quantity</td><td>{{ part.minimum }}</td></tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<h3>Price Breaks</h3>
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
{% endif %}
|
||||
{% if part.price_breaks.all %}
|
||||
<tr><th colspan='2'>Price Breaks</th></tr>
|
||||
<tr>
|
||||
<th>Quantity</th>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
{% for pb in part.price_breaks.all %}
|
||||
<tr>
|
||||
<td>{{ pb.quantity }}</td>
|
||||
<td>{{ pb.cost }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for pb in part.price_breaks.all %}
|
||||
<tr>
|
||||
<td>{{ pb.quantity }}</td>
|
||||
<td>{{ pb.cost }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<button class='btn btn-primary' type='button'>New Price Break</button>
|
||||
|
@ -9,7 +9,8 @@ from InvenTree.forms import HelperForm
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import Part, PartCategory, BomItem
|
||||
from .models import Part, PartCategory, PartAttachment
|
||||
from .models import BomItem
|
||||
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):
|
||||
""" Form for editing a Part object """
|
||||
|
||||
|
@ -14,5 +14,10 @@ class Migration(migrations.Migration):
|
||||
model_name='part',
|
||||
name='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),
|
||||
),
|
||||
]
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
@ -125,7 +125,7 @@ class Part(models.Model):
|
||||
IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
|
||||
|
||||
# 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
|
||||
category = models.ForeignKey(PartCategory, related_name='parts',
|
||||
@ -307,12 +307,6 @@ class Part(models.Model):
|
||||
def used_in_count(self):
|
||||
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
|
||||
def supplier_count(self):
|
||||
# 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)
|
||||
|
||||
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
|
||||
def basename(self):
|
||||
@ -407,11 +401,8 @@ class BomItem(models.Model):
|
||||
- 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
|
||||
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')})
|
||||
|
||||
# Test for simple recursion
|
||||
|
3
InvenTree/part/templates/part/attachment_delete.html
Normal file
3
InvenTree/part/templates/part/attachment_delete.html
Normal file
@ -0,0 +1,3 @@
|
||||
Are you sure you wish to delete this attachment?
|
||||
<br>
|
||||
This will remove the file '{{ attachment.basename }}'.
|
62
InvenTree/part/templates/part/attachments.html
Normal file
62
InvenTree/part/templates/part/attachments.html
Normal 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 %}
|
@ -66,13 +66,6 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
</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>
|
||||
|
||||
|
@ -1,6 +1,15 @@
|
||||
<ul class="nav nav-tabs">
|
||||
<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 %}
|
||||
<li{% ifequal tab 'bom' %} class="active"{% endifequal %}>
|
||||
<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 %}>
|
||||
<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 %}
|
||||
<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 %}
|
||||
<li{% ifequal tab 'suppliers' %} class="active"{% endifequal %}>
|
||||
<a href="{% url 'part-suppliers' part.id %}">Suppliers
|
||||
@ -31,4 +34,7 @@
|
||||
{% endif %}
|
||||
</a></li>
|
||||
{% 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>
|
@ -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
|
||||
@ -19,11 +25,18 @@ supplier_part_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 = [
|
||||
url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'),
|
||||
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'^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'^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'),
|
||||
@ -69,6 +82,10 @@ part_urls = [
|
||||
# Part category
|
||||
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)),
|
||||
|
||||
# Top level part list (display top level parts and categories)
|
||||
|
@ -13,11 +13,13 @@ from django.forms.models import model_to_dict
|
||||
from django.forms import HiddenInput
|
||||
|
||||
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 .forms import PartImageForm
|
||||
from .forms import EditPartForm
|
||||
from .forms import EditPartAttachmentForm
|
||||
from .forms import EditCategoryForm
|
||||
from .forms import EditBomItemForm
|
||||
from .forms import BomExportForm
|
||||
@ -51,6 +53,81 @@ class PartIndex(ListView):
|
||||
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):
|
||||
""" View for creating a new Part object.
|
||||
|
||||
@ -102,7 +179,6 @@ class PartCreate(AjaxCreateView):
|
||||
|
||||
return form
|
||||
|
||||
# Pre-fill the category field if a valid category is provided
|
||||
def get_initial(self):
|
||||
""" Get initial data for the new Part object:
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user