mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 13:06:45 +00:00
commit
9b0fefb0b4
32
InvenTree/InvenTree/helpers.py
Normal file
32
InvenTree/InvenTree/helpers.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import io
|
||||||
|
|
||||||
|
from wsgiref.util import FileWrapper
|
||||||
|
from django.http import StreamingHttpResponse
|
||||||
|
|
||||||
|
|
||||||
|
def WrapWithQuotes(text):
|
||||||
|
# TODO - Make this better
|
||||||
|
if not text.startswith('"'):
|
||||||
|
text = '"' + text
|
||||||
|
|
||||||
|
if not text.endswith('"'):
|
||||||
|
text = text + '"'
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def DownloadFile(data, filename, content_type='application/text'):
|
||||||
|
"""
|
||||||
|
Create a dynamic file for the user to download.
|
||||||
|
@param data is the raw file data
|
||||||
|
"""
|
||||||
|
|
||||||
|
filename = WrapWithQuotes(filename)
|
||||||
|
|
||||||
|
wrapper = FileWrapper(io.StringIO(data))
|
||||||
|
|
||||||
|
response = StreamingHttpResponse(wrapper, content_type=content_type)
|
||||||
|
response['Content-Length'] = len(data)
|
||||||
|
response['Content-Disposition'] = 'attachment; filename={f}'.format(f=filename)
|
||||||
|
|
||||||
|
return response
|
@ -14,6 +14,7 @@ from build.urls import build_urls
|
|||||||
from part.api import part_api_urls
|
from part.api import part_api_urls
|
||||||
from company.api import company_api_urls
|
from company.api import company_api_urls
|
||||||
from stock.api import stock_api_urls
|
from stock.api import stock_api_urls
|
||||||
|
from build.api import build_api_urls
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
@ -31,6 +32,7 @@ apipatterns = [
|
|||||||
url(r'^part/', include(part_api_urls)),
|
url(r'^part/', include(part_api_urls)),
|
||||||
url(r'^company/', include(company_api_urls)),
|
url(r'^company/', include(company_api_urls)),
|
||||||
url(r'^stock/', include(stock_api_urls)),
|
url(r'^stock/', include(stock_api_urls)),
|
||||||
|
url(r'^build/', include(build_api_urls)),
|
||||||
|
|
||||||
# User URLs
|
# User URLs
|
||||||
url(r'^user/', include(user_urls)),
|
url(r'^user/', include(user_urls)),
|
||||||
|
@ -65,9 +65,7 @@ class AjaxMixin(object):
|
|||||||
else:
|
else:
|
||||||
return self.template_name
|
return self.template_name
|
||||||
|
|
||||||
def renderJsonResponse(self, request, form, data={}):
|
def renderJsonResponse(self, request, form=None, data={}, context={}):
|
||||||
|
|
||||||
context = {}
|
|
||||||
|
|
||||||
if form:
|
if form:
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
@ -92,22 +90,46 @@ class AjaxMixin(object):
|
|||||||
class AjaxView(AjaxMixin, View):
|
class AjaxView(AjaxMixin, View):
|
||||||
""" Bare-bones AjaxView """
|
""" Bare-bones AjaxView """
|
||||||
|
|
||||||
|
# By default, point to the modal_form template
|
||||||
|
# (this can be overridden by a child class)
|
||||||
|
ajax_template_name = 'modal_form.html'
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
return JsonResponse('', safe=False)
|
return JsonResponse('', safe=False)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
return self.renderJsonResponse(request, None)
|
return self.renderJsonResponse(request)
|
||||||
|
|
||||||
|
|
||||||
class AjaxCreateView(AjaxMixin, CreateView):
|
class AjaxCreateView(AjaxMixin, CreateView):
|
||||||
|
|
||||||
|
""" An 'AJAXified' CreateView for creating a new object in the db
|
||||||
|
- Returns a form in JSON format (for delivery to a modal window)
|
||||||
|
- Handles form validation via AJAX POST requests
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
response = super(CreateView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
if request.is_ajax():
|
||||||
|
# Initialize a a new form
|
||||||
|
form = self.form_class(initial=self.get_initial())
|
||||||
|
|
||||||
|
return self.renderJsonResponse(request, form)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return response
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
form = self.form_class(data=request.POST, files=request.FILES)
|
form = self.form_class(data=request.POST, files=request.FILES)
|
||||||
|
|
||||||
if request.is_ajax():
|
if request.is_ajax():
|
||||||
|
|
||||||
data = {'form_valid': form.is_valid()}
|
data = {
|
||||||
|
'form_valid': form.is_valid(),
|
||||||
|
}
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
obj = form.save()
|
obj = form.save()
|
||||||
@ -122,20 +144,25 @@ class AjaxCreateView(AjaxMixin, CreateView):
|
|||||||
else:
|
else:
|
||||||
return super(CreateView, self).post(request, *args, **kwargs)
|
return super(CreateView, self).post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AjaxUpdateView(AjaxMixin, UpdateView):
|
||||||
|
|
||||||
|
""" An 'AJAXified' UpdateView for updating an object in the db
|
||||||
|
- Returns form in JSON format (for delivery to a modal window)
|
||||||
|
- Handles repeated form validation (via AJAX) until the form is valid
|
||||||
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
response = super(CreateView, self).get(request, *args, **kwargs)
|
html_response = super(UpdateView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
if request.is_ajax():
|
if request.is_ajax():
|
||||||
form = self.form_class(initial=self.get_initial())
|
form = self.form_class(instance=self.get_object())
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form)
|
return self.renderJsonResponse(request, form)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return response
|
return html_response
|
||||||
|
|
||||||
|
|
||||||
class AjaxUpdateView(AjaxMixin, UpdateView):
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
@ -154,45 +181,26 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
|
|||||||
response = self.renderJsonResponse(request, form, data)
|
response = self.renderJsonResponse(request, form, data)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
else:
|
|
||||||
return response
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
if request.is_ajax():
|
|
||||||
form = self.form_class(instance=self.get_object())
|
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return super(UpdateView, self).post(request, *args, **kwargs)
|
return super(UpdateView, self).post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class AjaxDeleteView(AjaxMixin, DeleteView):
|
class AjaxDeleteView(AjaxMixin, DeleteView):
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
""" An 'AJAXified DeleteView for removing an object from the DB
|
||||||
|
- Returns a HTML object (not a form!) in JSON format (for delivery to a modal window)
|
||||||
if request.is_ajax():
|
- Handles deletion
|
||||||
obj = self.get_object()
|
"""
|
||||||
pk = obj.id
|
|
||||||
obj.delete()
|
|
||||||
|
|
||||||
data = {'id': pk,
|
|
||||||
'delete': True}
|
|
||||||
|
|
||||||
return self.renderJsonResponse(request, None, data)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return super(DeleteView, self).post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
response = super(DeleteView, self).get(request, *args, **kwargs)
|
html_response = super(DeleteView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
if request.is_ajax():
|
if request.is_ajax():
|
||||||
|
|
||||||
data = {'id': self.get_object().id,
|
data = {'id': self.get_object().id,
|
||||||
'title': self.ajax_form_title,
|
|
||||||
'delete': False,
|
'delete': False,
|
||||||
|
'title': self.ajax_form_title,
|
||||||
'html_data': render_to_string(self.getAjaxTemplate(),
|
'html_data': render_to_string(self.getAjaxTemplate(),
|
||||||
self.get_context_data(),
|
self.get_context_data(),
|
||||||
request=request)
|
request=request)
|
||||||
@ -201,7 +209,23 @@ class AjaxDeleteView(AjaxMixin, DeleteView):
|
|||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return response
|
return html_response
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
if request.is_ajax():
|
||||||
|
|
||||||
|
obj = self.get_object()
|
||||||
|
pk = obj.id
|
||||||
|
obj.delete()
|
||||||
|
|
||||||
|
data = {'id': pk,
|
||||||
|
'delete': True}
|
||||||
|
|
||||||
|
return self.renderJsonResponse(request, data=data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return super(DeleteView, self).post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class IndexView(TemplateView):
|
class IndexView(TemplateView):
|
||||||
|
36
InvenTree/build/api.py
Normal file
36
InvenTree/build/api.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from rest_framework import filters
|
||||||
|
from rest_framework import generics, permissions
|
||||||
|
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from .models import Build
|
||||||
|
from .serializers import BuildSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class BuildList(generics.ListAPIView):
|
||||||
|
|
||||||
|
queryset = Build.objects.all()
|
||||||
|
serializer_class = BuildSerializer
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
permissions.IsAuthenticatedOrReadOnly,
|
||||||
|
]
|
||||||
|
|
||||||
|
filter_backends = [
|
||||||
|
DjangoFilterBackend,
|
||||||
|
filters.SearchFilter,
|
||||||
|
filters.OrderingFilter,
|
||||||
|
]
|
||||||
|
|
||||||
|
filter_fields = [
|
||||||
|
'part',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
build_api_urls = [
|
||||||
|
url(r'^.*$', BuildList.as_view(), name='api-build-list')
|
||||||
|
]
|
23
InvenTree/build/serializers.py
Normal file
23
InvenTree/build/serializers.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .models import Build
|
||||||
|
|
||||||
|
|
||||||
|
class BuildSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Build
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'url',
|
||||||
|
'title',
|
||||||
|
'creation_date',
|
||||||
|
'completion_date',
|
||||||
|
'part',
|
||||||
|
'quantity',
|
||||||
|
'notes']
|
@ -4,7 +4,11 @@
|
|||||||
|
|
||||||
<h3>Part Builds</h3>
|
<h3>Part Builds</h3>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='build-table'>
|
<div id='button-toolbar'>
|
||||||
|
<button class="btn btn-success" id='new-build'>Start New Build</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed' id='build-table' data-toolbar='#button-toolbar'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Build</th>
|
<th>Build</th>
|
||||||
@ -20,9 +24,6 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class='container-fluid'>
|
|
||||||
<button class="btn btn-success" id='new-build'>Start New Build</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include 'modals.html' %}
|
{% include 'modals.html' %}
|
||||||
|
|
||||||
|
@ -4,12 +4,9 @@
|
|||||||
|
|
||||||
{% include 'company/tabs.html' with tab='parts' %}
|
{% include 'company/tabs.html' with tab='parts' %}
|
||||||
|
|
||||||
<div class='row'>
|
<h3>Supplier Parts</h3>
|
||||||
<div class='col-sm-6'>
|
|
||||||
<h3>Supplier Parts</h3>
|
<div id='button-toolbar'>
|
||||||
</div>
|
|
||||||
<div class='col-sm-6'>
|
|
||||||
<h3 class='float-right'>
|
|
||||||
<button class="btn btn-success" id='part-create'>New Supplier Part</button>
|
<button class="btn btn-success" id='part-create'>New Supplier Part</button>
|
||||||
<div class="dropdown" style="float: right;">
|
<div class="dropdown" style="float: right;">
|
||||||
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
||||||
@ -17,13 +14,11 @@
|
|||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<table clas='table table-striped table-condensed' id='part-table'>
|
<table clas='table table-striped table-condensed' id='part-table' data-toolbar='#button-toolbar'>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class='container-fluid'>
|
|
||||||
<h3><button style='float: right;' class="btn btn-success" id='new-company'>New Company</button></h3>
|
|
||||||
<h3>Companies</h3>
|
<h3>Companies</h3>
|
||||||
|
<div id='button-toolbar'>
|
||||||
|
<h3><button style='float: right;' class="btn btn-success" id='new-company'>New Company</button></h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class='table table-striped' id='company-table'>
|
<table class='table table-striped' id='company-table' data-toolbar='#button-toolbar'>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,13 +70,14 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
serializer_class = PartSerializer
|
serializer_class = PartSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
print("Get queryset")
|
|
||||||
|
|
||||||
# Does the user wish to filter by category?
|
# Does the user wish to filter by category?
|
||||||
cat_id = self.request.query_params.get('category', None)
|
cat_id = self.request.query_params.get('category', None)
|
||||||
|
|
||||||
|
# Start with all objects
|
||||||
|
parts_list = Part.objects.all()
|
||||||
|
|
||||||
if cat_id:
|
if cat_id:
|
||||||
print("Getting category:", cat_id)
|
|
||||||
category = get_object_or_404(PartCategory, pk=cat_id)
|
category = get_object_or_404(PartCategory, pk=cat_id)
|
||||||
|
|
||||||
# Filter by the supplied category
|
# Filter by the supplied category
|
||||||
@ -90,10 +91,10 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
continue
|
continue
|
||||||
flt |= Q(category=child)
|
flt |= Q(category=child)
|
||||||
|
|
||||||
return Part.objects.filter(flt)
|
parts_list = parts_list.filter(flt)
|
||||||
|
|
||||||
# Default - return all parts
|
# Default - return all parts
|
||||||
return Part.objects.all()
|
return parts_list
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
permissions.IsAuthenticatedOrReadOnly,
|
permissions.IsAuthenticatedOrReadOnly,
|
||||||
@ -106,6 +107,11 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
filter_fields = [
|
filter_fields = [
|
||||||
|
'buildable',
|
||||||
|
'consumable',
|
||||||
|
'trackable',
|
||||||
|
'purchaseable',
|
||||||
|
'salable',
|
||||||
]
|
]
|
||||||
|
|
||||||
ordering_fields = [
|
ordering_fields = [
|
||||||
|
@ -3,6 +3,8 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
from .models import Part, PartCategory, BomItem
|
from .models import Part, PartCategory, BomItem
|
||||||
from .models import SupplierPart
|
from .models import SupplierPart
|
||||||
|
|
||||||
@ -16,6 +18,27 @@ class PartImageForm(HelperForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BomExportForm(HelperForm):
|
||||||
|
|
||||||
|
# TODO - Define these choices somewhere else, and import them here
|
||||||
|
format_choices = (
|
||||||
|
('csv', 'CSV'),
|
||||||
|
('pdf', 'PDF'),
|
||||||
|
('xml', 'XML'),
|
||||||
|
('xlsx', 'XLSX'),
|
||||||
|
('html', 'HTML')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Select export type
|
||||||
|
format = forms.CharField(label='Format', widget=forms.Select(choices=format_choices), required='true', help_text='Select export format')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Part
|
||||||
|
fields = [
|
||||||
|
'format',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class EditPartForm(HelperForm):
|
class EditPartForm(HelperForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -28,8 +51,10 @@ class EditPartForm(HelperForm):
|
|||||||
'URL',
|
'URL',
|
||||||
'default_location',
|
'default_location',
|
||||||
'default_supplier',
|
'default_supplier',
|
||||||
|
'units',
|
||||||
'minimum_stock',
|
'minimum_stock',
|
||||||
'buildable',
|
'buildable',
|
||||||
|
'consumable',
|
||||||
'trackable',
|
'trackable',
|
||||||
'purchaseable',
|
'purchaseable',
|
||||||
'salable',
|
'salable',
|
||||||
@ -56,8 +81,10 @@ class EditBomItemForm(HelperForm):
|
|||||||
fields = [
|
fields = [
|
||||||
'part',
|
'part',
|
||||||
'sub_part',
|
'sub_part',
|
||||||
'quantity'
|
'quantity',
|
||||||
|
'note'
|
||||||
]
|
]
|
||||||
|
widgets = {'part': forms.HiddenInput()}
|
||||||
|
|
||||||
|
|
||||||
class EditSupplierPartForm(HelperForm):
|
class EditSupplierPartForm(HelperForm):
|
||||||
|
18
InvenTree/part/migrations/0004_bomitem_note.py
Normal file
18
InvenTree/part/migrations/0004_bomitem_note.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2 on 2019-04-14 08:25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('part', '0003_auto_20190412_2030'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='bomitem',
|
||||||
|
name='note',
|
||||||
|
field=models.CharField(blank=True, help_text='Item notes', max_length=100),
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/part/migrations/0005_part_consumable.py
Normal file
18
InvenTree/part/migrations/0005_part_consumable.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2 on 2019-04-15 13:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('part', '0004_bomitem_note'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='part',
|
||||||
|
name='consumable',
|
||||||
|
field=models.BooleanField(default=False, help_text='Can this part be used to build other parts?'),
|
||||||
|
),
|
||||||
|
]
|
@ -126,9 +126,12 @@ class Part(models.Model):
|
|||||||
# Units of quantity for this part. Default is "pcs"
|
# Units of quantity for this part. Default is "pcs"
|
||||||
units = models.CharField(max_length=20, default="pcs", blank=True)
|
units = models.CharField(max_length=20, default="pcs", blank=True)
|
||||||
|
|
||||||
# Can this part be built?
|
# Can this part be built from other parts?
|
||||||
buildable = models.BooleanField(default=False, help_text='Can this part be built from other parts?')
|
buildable = models.BooleanField(default=False, help_text='Can this part be built from other parts?')
|
||||||
|
|
||||||
|
# Can this part be used to make other parts?
|
||||||
|
consumable = models.BooleanField(default=True, help_text='Can this part be used to build other parts?')
|
||||||
|
|
||||||
# Is this part "trackable"?
|
# Is this part "trackable"?
|
||||||
# Trackable parts can have unique instances
|
# Trackable parts can have unique instances
|
||||||
# which are assigned serial numbers (or batch numbers)
|
# which are assigned serial numbers (or batch numbers)
|
||||||
@ -278,6 +281,73 @@ class Part(models.Model):
|
|||||||
# Return the number of supplier parts available for this part
|
# Return the number of supplier parts available for this part
|
||||||
return self.supplier_parts.count()
|
return self.supplier_parts.count()
|
||||||
|
|
||||||
|
def export_bom(self, **kwargs):
|
||||||
|
|
||||||
|
# Construct the export data
|
||||||
|
header = []
|
||||||
|
header.append('Part')
|
||||||
|
header.append('Description')
|
||||||
|
header.append('Quantity')
|
||||||
|
header.append('Note')
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
|
||||||
|
for it in self.bom_items.all():
|
||||||
|
line = []
|
||||||
|
|
||||||
|
line.append(it.sub_part.name)
|
||||||
|
line.append(it.sub_part.description)
|
||||||
|
line.append(it.quantity)
|
||||||
|
line.append(it.note)
|
||||||
|
|
||||||
|
rows.append([str(x) for x in line])
|
||||||
|
|
||||||
|
file_format = kwargs.get('format', 'csv').lower()
|
||||||
|
|
||||||
|
kwargs['header'] = header
|
||||||
|
kwargs['rows'] = rows
|
||||||
|
|
||||||
|
if file_format == 'csv':
|
||||||
|
return self.export_bom_csv(**kwargs)
|
||||||
|
elif file_format in ['xls', 'xlsx']:
|
||||||
|
return self.export_bom_xls(**kwargs)
|
||||||
|
elif file_format == 'xml':
|
||||||
|
return self.export_bom_xml(**kwargs)
|
||||||
|
elif file_format in ['htm', 'html']:
|
||||||
|
return self.export_bom_htm(**kwargs)
|
||||||
|
elif file_format == 'pdf':
|
||||||
|
return self.export_bom_pdf(**kwargs)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def export_bom_csv(self, **kwargs):
|
||||||
|
|
||||||
|
# Construct header line
|
||||||
|
header = kwargs.get('header')
|
||||||
|
rows = kwargs.get('rows')
|
||||||
|
|
||||||
|
# TODO - Choice of formatters goes here?
|
||||||
|
out = ','.join(header)
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
out += '\n'
|
||||||
|
out += ','.join(row)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
def export_bom_xls(self, **kwargs):
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def export_bom_xml(self, **kwargs):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def export_bom_htm(self, **kwargs):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def export_bom_pdf(self, **kwargs):
|
||||||
|
return ''
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@property
|
@property
|
||||||
def projects(self):
|
def projects(self):
|
||||||
@ -338,11 +408,15 @@ class BomItem(models.Model):
|
|||||||
|
|
||||||
# A link to the child item (sub-part)
|
# A link to the child item (sub-part)
|
||||||
# Each part will get a reverse lookup field 'used_in'
|
# Each part will get a reverse lookup field 'used_in'
|
||||||
sub_part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='used_in')
|
sub_part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='used_in',
|
||||||
|
limit_choices_to={'consumable': True})
|
||||||
|
|
||||||
# Quantity required
|
# Quantity required
|
||||||
quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)])
|
quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)])
|
||||||
|
|
||||||
|
# Note attached to this BOM line item
|
||||||
|
note = models.CharField(max_length=100, blank=True, help_text='Item notes')
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
# A part cannot refer to itself in its BOM
|
# A part cannot refer to itself in its BOM
|
||||||
|
@ -43,7 +43,7 @@ class PartSerializer(serializers.ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
category = CategorySerializer(many=False, read_only=True)
|
category_name = serializers.CharField(source='category_path', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Part
|
model = Part
|
||||||
@ -55,11 +55,13 @@ class PartSerializer(serializers.ModelSerializer):
|
|||||||
'URL', # Link to an external URL (optional)
|
'URL', # Link to an external URL (optional)
|
||||||
'description',
|
'description',
|
||||||
'category',
|
'category',
|
||||||
|
'category_name',
|
||||||
'total_stock',
|
'total_stock',
|
||||||
'available_stock',
|
'available_stock',
|
||||||
'units',
|
'units',
|
||||||
'trackable',
|
'trackable',
|
||||||
'buildable',
|
'buildable',
|
||||||
|
'consumable',
|
||||||
'trackable',
|
'trackable',
|
||||||
'salable',
|
'salable',
|
||||||
]
|
]
|
||||||
@ -79,7 +81,8 @@ class BomItemSerializer(serializers.ModelSerializer):
|
|||||||
'url',
|
'url',
|
||||||
'part',
|
'part',
|
||||||
'sub_part',
|
'sub_part',
|
||||||
'quantity'
|
'quantity',
|
||||||
|
'note',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,109 +11,72 @@
|
|||||||
|
|
||||||
<h3>Bill of Materials</h3>
|
<h3>Bill of Materials</h3>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='bom-table'>
|
<div id='button-toolbar'>
|
||||||
</table>
|
{% if editing_enabled %}
|
||||||
|
<div class='btn-group' style='float: right;'>
|
||||||
<div class='container-fluid'>
|
<button class='btn btn-info' type='button' id='bom-item-new'>New BOM Item</button>
|
||||||
<button type='button' class='btn btn-success' id='new-bom-item'>Add BOM Item</button>
|
<button class='btn btn-success' type='button' id='editing-finished'>Finish Editing</button>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class='dropdown' style="float: right;">
|
||||||
|
<button class='btn btn-primary dropdown-toggle' type='button' data-toggle='dropdown'>
|
||||||
|
Options
|
||||||
|
<span class='caret'></span>
|
||||||
|
</button>
|
||||||
|
<ul class='dropdown-menu'>
|
||||||
|
<li><a href='#' id='edit-bom' title='Edit BOM'>Edit BOM</a></li>
|
||||||
|
<li><a href='#' id='export-bom' title='Export BOM'>Export BOM</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed' data-toolbar="#button-toolbar" id='bom-table'>
|
||||||
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_load %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type='text/javascript' src="{% static 'script/inventree/api.js' %}"></script>
|
||||||
|
<script type='text/javascript' src="{% static 'script/inventree/part.js' %}"></script>
|
||||||
|
<script type='text/javascript' src="{% static 'script/inventree/bom.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
function reloadBom() {
|
|
||||||
$("#bom-table").bootstrapTable('refresh');
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#bom-table').on('click', '.delete-button', function () {
|
// Load the BOM table data
|
||||||
var button = $(this);
|
loadBomTable($("#bom-table"), {
|
||||||
|
editable: {{ editing_enabled }},
|
||||||
launchDeleteForm(
|
bom_url: "{% url 'api-bom-list' %}",
|
||||||
button.attr('url'),
|
part_url: "{% url 'api-part-list' %}",
|
||||||
{
|
parent_id: {{ part.id }}
|
||||||
success: reloadBom
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#bom-table").on('click', '.edit-button', function () {
|
{% if editing_enabled %}
|
||||||
var button = $(this);
|
$("#editing-finished").click(function() {
|
||||||
|
location.href = "{% url 'part-bom' part.id %}";
|
||||||
|
});
|
||||||
|
|
||||||
launchModalForm(
|
$("#bom-item-new").click(function () {
|
||||||
button.attr('url'),
|
launchModalForm("{% url 'bom-item-create' %}?parent={{ part.id }}", {});
|
||||||
{
|
});
|
||||||
success: reloadBom
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
$("#edit-bom").click(function () {
|
||||||
|
location.href = "{% url 'part-bom' part.id %}?edit=True";
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#export-bom").click(function () {
|
||||||
|
downloadBom({
|
||||||
|
modal: '#modal-form',
|
||||||
|
url: "{% url 'bom-export' part.id %}"
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#new-bom-item").click(function () {
|
{% endif %}
|
||||||
launchModalForm(
|
|
||||||
"{% url 'bom-item-create' %}",
|
|
||||||
{
|
|
||||||
reload: true,
|
|
||||||
data: {
|
|
||||||
parent: {{ part.id }}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#bom-table").bootstrapTable({
|
|
||||||
sortable: true,
|
|
||||||
search: true,
|
|
||||||
queryParams: function(p) {
|
|
||||||
return {
|
|
||||||
part: {{ part.id }}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
title: 'ID',
|
|
||||||
visible: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'sub_part',
|
|
||||||
title: 'Part',
|
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return renderLink(value.name, value.url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'sub_part.description',
|
|
||||||
title: 'Description',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'quantity',
|
|
||||||
title: 'Required',
|
|
||||||
searchable: false,
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'sub_part.available_stock',
|
|
||||||
title: 'Available',
|
|
||||||
searchable: false,
|
|
||||||
sortable: true,
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
var text = "";
|
|
||||||
if (row.quantity < row.sub_part.available_stock)
|
|
||||||
{
|
|
||||||
text = "<span class='label label-success'>" + value + "</span>";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
text = "<span class='label label-warning'>" + value + "</span>";
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderLink(text, row.sub_part.url + "stock/");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
return editButton(row.url + 'edit') + ' ' + deleteButton(row.url + 'delete');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
url: "{% url 'api-bom-list' %}"
|
|
||||||
});
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -6,34 +6,13 @@
|
|||||||
|
|
||||||
<h3>Part Builds</h3>
|
<h3>Part Builds</h3>
|
||||||
|
|
||||||
<table class='table table-striped'>
|
<div id='button-toolbar'>
|
||||||
|
<button class="btn btn-success" id='start-build'>Start New Build</button>
|
||||||
<tr>
|
</div>
|
||||||
<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 class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='build-table'>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class='container-fluid'>
|
|
||||||
<button class="btn btn-success" id='start-build'>Start New Build</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -49,4 +28,43 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#build-table").bootstrapTable({
|
||||||
|
sortable: true,
|
||||||
|
search: true,
|
||||||
|
pagination: true,
|
||||||
|
queryParams: function(p) {
|
||||||
|
return {
|
||||||
|
part: {{ part.id }},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
title: 'ID',
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'title',
|
||||||
|
title: 'Title',
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
return renderLink(value, row.url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'quantity',
|
||||||
|
title: 'Quantity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: 'Status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'completion_date',
|
||||||
|
title: 'Completed'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
url: "{% url 'api-build-list' %}",
|
||||||
|
});
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -40,13 +40,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='part-table'>
|
<div id='button-toolbar'>
|
||||||
</table>
|
<button style='float: right;' class='btn btn-success' id='part-create'>New Part</button>
|
||||||
|
|
||||||
<div>
|
|
||||||
<button style='float: right;' class='btn btn-success' id='part-create'>New Part</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
|
||||||
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block js_load %}
|
{% block js_load %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@ -151,11 +151,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
field: 'category',
|
field: 'category_name',
|
||||||
title: 'Category',
|
title: 'Category',
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
if (row.category) {
|
if (row.category) {
|
||||||
return renderLink(row.category.pathstring, row.category.url);
|
return renderLink(row.category_name, "/part/category/" + row.category + "/");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return '';
|
return '';
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Description</td>
|
<td>Description</td>
|
||||||
<td>{{ part.decription }}</td>
|
<td>{{ part.description }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if part.IPN %}
|
{% if part.IPN %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -44,7 +44,7 @@
|
|||||||
<td>Category</td>
|
<td>Category</td>
|
||||||
<td>
|
<td>
|
||||||
{% if part.category %}
|
{% if part.category %}
|
||||||
<a href="{% url 'category-detail' part.category.id %}">{{ part.category.name }}</a>
|
<a href="{% url 'category-detail' part.category.id %}">{{ part.category.pathstring }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -70,6 +70,10 @@
|
|||||||
<td>Buildable</td>
|
<td>Buildable</td>
|
||||||
<td>{% include "yesnolabel.html" with value=part.buildable %}</td>
|
<td>{% include "yesnolabel.html" with value=part.buildable %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Consumable</td>
|
||||||
|
<td>{% include "yesnolabel.html" with value=part.consumable %}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Trackable</td>
|
<td>Trackable</td>
|
||||||
<td>{% include "yesnolabel.html" with value=part.trackable %}</td>
|
<td>{% include "yesnolabel.html" with value=part.trackable %}</td>
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<h4>Stock Status - {{ part.available_stock }} available</h4>
|
<h4>Stock Status - {{ part.available_stock }}{% if part.units %} {{ part.units }} {% endif%} available</h4>
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<tr>
|
<tr>
|
||||||
<td>In Stock</td>
|
<td>In Stock</td>
|
||||||
|
@ -4,13 +4,9 @@
|
|||||||
|
|
||||||
{% include 'part/tabs.html' with tab='stock' %}
|
{% include 'part/tabs.html' with tab='stock' %}
|
||||||
|
|
||||||
<div class='row'>
|
<h3>Part Stock</h3>
|
||||||
<div class='col-sm-6'>
|
|
||||||
<h3>Part Stock</h3>
|
<div id='button-toolbar'>
|
||||||
</div>
|
|
||||||
<div class='col-sm-6 float-right'>
|
|
||||||
<h3>
|
|
||||||
<div class='float-right'>
|
|
||||||
<button class='btn btn-success' id='add-stock-item'>New Stock Item</button>
|
<button class='btn btn-success' id='add-stock-item'>New Stock Item</button>
|
||||||
<div id='opt-dropdown' class="dropdown" style='float: right;'>
|
<div id='opt-dropdown' class="dropdown" style='float: right;'>
|
||||||
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
||||||
@ -22,14 +18,9 @@
|
|||||||
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move items</a></li>
|
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move items</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='stock-table'>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='stock-table'>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
<a data-toggle="collapse" href="#collapse1">Child Categories</a><span class='badge'>{{ children|length }}</span>
|
<a data-toggle="collapse" href="#collapse1">{{ children | length }} Child Categories</a>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div id="collapse1" class="panel-collapse collapse">
|
<div id="collapse1" class="panel-collapse collapse">
|
||||||
|
@ -4,20 +4,15 @@
|
|||||||
|
|
||||||
{% include 'part/tabs.html' with tab='suppliers' %}
|
{% include 'part/tabs.html' with tab='suppliers' %}
|
||||||
|
|
||||||
<div class='row'>
|
<h3>Part Suppliers</h3>
|
||||||
<div class='col-sm-6'>
|
|
||||||
<h3>Part Suppliers</h3>
|
<div id='button-toolbar'>
|
||||||
</div>
|
|
||||||
<div class='col-sm-6'>
|
|
||||||
<h3>
|
|
||||||
<button class="btn btn-success float-right" id='supplier-create'>New Supplier Part</button>
|
<button class="btn btn-success float-right" id='supplier-create'>New Supplier Part</button>
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<table class="table table-striped table-condensed" id='supplier-table'>
|
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<li{% ifequal tab 'build' %} class="active"{% endifequal %}>
|
<li{% ifequal tab 'build' %} class="active"{% endifequal %}>
|
||||||
<a href="{% url 'part-build' part.id %}">Build<span class='badge'>{{ part.active_builds|length }}</span></a></li>
|
<a href="{% url 'part-build' part.id %}">Build<span class='badge'>{{ part.active_builds|length }}</span></a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.used_in_count > 0 %}
|
{% if part.consumable or part.used_in_count > 0 %}
|
||||||
<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 %}
|
||||||
|
@ -19,6 +19,7 @@ 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/?', 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'),
|
||||||
|
@ -4,7 +4,6 @@ from __future__ import unicode_literals
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
from company.models import Company
|
from company.models import Company
|
||||||
@ -15,10 +14,13 @@ from .forms import PartImageForm
|
|||||||
from .forms import EditPartForm
|
from .forms import EditPartForm
|
||||||
from .forms import EditCategoryForm
|
from .forms import EditCategoryForm
|
||||||
from .forms import EditBomItemForm
|
from .forms import EditBomItemForm
|
||||||
|
from .forms import BomExportForm
|
||||||
|
|
||||||
from .forms import EditSupplierPartForm
|
from .forms import EditSupplierPartForm
|
||||||
|
|
||||||
from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||||
|
|
||||||
|
from InvenTree.helpers import DownloadFile
|
||||||
|
|
||||||
|
|
||||||
class PartIndex(ListView):
|
class PartIndex(ListView):
|
||||||
@ -88,6 +90,17 @@ class PartDetail(DetailView):
|
|||||||
queryset = Part.objects.all()
|
queryset = Part.objects.all()
|
||||||
template_name = 'part/detail.html'
|
template_name = 'part/detail.html'
|
||||||
|
|
||||||
|
# Add in some extra context information based on query params
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(PartDetail, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
if self.request.GET.get('edit', '').lower() in ['true', 'yes', '1']:
|
||||||
|
context['editing_enabled'] = 1
|
||||||
|
else:
|
||||||
|
context['editing_enabled'] = 0
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class PartImage(AjaxUpdateView):
|
class PartImage(AjaxUpdateView):
|
||||||
|
|
||||||
@ -104,10 +117,88 @@ class PartImage(AjaxUpdateView):
|
|||||||
|
|
||||||
class PartEdit(AjaxUpdateView):
|
class PartEdit(AjaxUpdateView):
|
||||||
model = Part
|
model = Part
|
||||||
form_class = EditPartForm
|
|
||||||
template_name = 'part/edit.html'
|
template_name = 'part/edit.html'
|
||||||
|
form_class = EditPartForm
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit Part Properties'
|
ajax_form_title = 'Edit Part Properties'
|
||||||
|
context_object_name = 'part'
|
||||||
|
|
||||||
|
|
||||||
|
class BomExport(AjaxView):
|
||||||
|
|
||||||
|
model = Part
|
||||||
|
ajax_form_title = 'Export BOM'
|
||||||
|
ajax_template_name = 'part/bom_export.html'
|
||||||
|
context_object_name = 'part'
|
||||||
|
form_class = BomExportForm
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return get_object_or_404(Part, pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
form = self.form_class()
|
||||||
|
|
||||||
|
"""
|
||||||
|
part = self.get_object()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'part': part
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.is_ajax():
|
||||||
|
passs
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.renderJsonResponse(request, form)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
User has now submitted the BOM export data
|
||||||
|
"""
|
||||||
|
|
||||||
|
# part = self.get_object()
|
||||||
|
|
||||||
|
return super(AjaxView, self).post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
return {
|
||||||
|
# 'form_valid': True,
|
||||||
|
# 'redirect': '/'
|
||||||
|
# 'redirect': reverse('bom-download', kwargs={'pk': self.request.GET.get('pk')})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BomDownload(AjaxView):
|
||||||
|
"""
|
||||||
|
Provide raw download of a BOM file.
|
||||||
|
- File format should be passed as a query param e.g. ?format=csv
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO - This should no longer extend an AjaxView!
|
||||||
|
|
||||||
|
model = Part
|
||||||
|
# form_class = BomExportForm
|
||||||
|
# template_name = 'part/bom_export.html'
|
||||||
|
# ajax_form_title = 'Export Bill of Materials'
|
||||||
|
# context_object_name = 'part'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
part = get_object_or_404(Part, pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
export_format = request.GET.get('format', 'csv')
|
||||||
|
|
||||||
|
# Placeholder to test file export
|
||||||
|
filename = '"' + part.name + '_BOM.' + export_format + '"'
|
||||||
|
|
||||||
|
filedata = part.export_bom(format=export_format)
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
|
def get_data(self):
|
||||||
|
return {
|
||||||
|
'info': 'Exported BOM'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PartDelete(AjaxDeleteView):
|
class PartDelete(AjaxDeleteView):
|
||||||
@ -115,6 +206,7 @@ class PartDelete(AjaxDeleteView):
|
|||||||
template_name = 'part/delete.html'
|
template_name = 'part/delete.html'
|
||||||
ajax_template_name = 'part/partial_delete.html'
|
ajax_template_name = 'part/partial_delete.html'
|
||||||
ajax_form_title = 'Confirm Part Deletion'
|
ajax_form_title = 'Confirm Part Deletion'
|
||||||
|
context_object_name = 'part'
|
||||||
|
|
||||||
success_url = '/part/'
|
success_url = '/part/'
|
||||||
|
|
||||||
|
4052
InvenTree/static/css/select2-bootstrap.css
Normal file
4052
InvenTree/static/css/select2-bootstrap.css
Normal file
File diff suppressed because it is too large
Load Diff
192
InvenTree/static/script/inventree/bom.js
Normal file
192
InvenTree/static/script/inventree/bom.js
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
/* BOM management functions.
|
||||||
|
* Requires follwing files to be loaded first:
|
||||||
|
* - api.js
|
||||||
|
* - part.js
|
||||||
|
* - modals.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
function reloadBomTable(table, options) {
|
||||||
|
|
||||||
|
table.bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function downloadBom(options = {}) {
|
||||||
|
|
||||||
|
var modal = options.modal || "#modal-form";
|
||||||
|
|
||||||
|
var content = `
|
||||||
|
<b>Select file format</b><br>
|
||||||
|
<div class='controls'>
|
||||||
|
<select id='bom-format' class='select'>
|
||||||
|
<option value='csv'>CSV</option>
|
||||||
|
<option value='xls'>XLSX</option>
|
||||||
|
<option value='pdf'>PDF</option>
|
||||||
|
<option value='xml'>XML</option>
|
||||||
|
<option value='htm'>HTML</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
openModal({
|
||||||
|
modal: modal,
|
||||||
|
title: "Export Bill of Materials",
|
||||||
|
submit_text: "Download",
|
||||||
|
close_text: "Cancel",
|
||||||
|
});
|
||||||
|
|
||||||
|
modalSetContent(modal, content);
|
||||||
|
|
||||||
|
$(modal).on('click', '#modal-form-submit', function() {
|
||||||
|
$(modal).modal('hide');
|
||||||
|
|
||||||
|
var format = $(modal).find('#bom-format :selected').val();
|
||||||
|
|
||||||
|
if (options.url) {
|
||||||
|
var url = options.url + "?format=" + format;
|
||||||
|
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadBomTable(table, options) {
|
||||||
|
/* Load a BOM table with some configurable options.
|
||||||
|
*
|
||||||
|
* Following options are available:
|
||||||
|
* editable - Should the BOM table be editable?
|
||||||
|
* bom_url - Address to request BOM data from
|
||||||
|
* part_url - Address to request Part data from
|
||||||
|
* parent_id - Parent ID of the owning part
|
||||||
|
*
|
||||||
|
* BOM data are retrieved from the server via AJAX query
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Construct the table columns
|
||||||
|
|
||||||
|
var cols = [
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
title: 'ID',
|
||||||
|
visible: false,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (options.editable) {
|
||||||
|
cols.push({
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var bEdit = "<button class='btn btn-success bom-edit-button btn-sm' type='button' url='" + row.url + "edit'>Edit</button>";
|
||||||
|
var bDelt = "<button class='btn btn-danger bom-delete-button btn-sm' type='button' url='" + row.url + "delete'>Delete</button>";
|
||||||
|
|
||||||
|
return "<div class='btn-group'>" + bEdit + bDelt + "</div>";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part column
|
||||||
|
cols.push(
|
||||||
|
{
|
||||||
|
field: 'sub_part',
|
||||||
|
title: 'Part',
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
return renderLink(value.name, value.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Part description
|
||||||
|
cols.push(
|
||||||
|
{
|
||||||
|
field: 'sub_part.description',
|
||||||
|
title: 'Description',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Part quantity
|
||||||
|
cols.push(
|
||||||
|
{
|
||||||
|
field: 'quantity',
|
||||||
|
title: 'Required',
|
||||||
|
searchable: false,
|
||||||
|
sortable: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Part notes
|
||||||
|
cols.push(
|
||||||
|
{
|
||||||
|
field: 'note',
|
||||||
|
title: 'Notes',
|
||||||
|
searchable: true,
|
||||||
|
sortable: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// If we are NOT editing, display the available stock
|
||||||
|
if (!options.editable) {
|
||||||
|
cols.push(
|
||||||
|
{
|
||||||
|
field: 'sub_part.available_stock',
|
||||||
|
title: 'Available',
|
||||||
|
searchable: false,
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var text = "";
|
||||||
|
|
||||||
|
if (row.quantity < row.sub_part.available_stock)
|
||||||
|
{
|
||||||
|
text = "<span class='label label-success'>" + value + "</span>";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text = "<span class='label label-warning'>" + value + "</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderLink(text, row.sub_part.url + "stock/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the table (bootstrap-table)
|
||||||
|
|
||||||
|
table.bootstrapTable({
|
||||||
|
sortable: true,
|
||||||
|
search: true,
|
||||||
|
clickToSelect: true,
|
||||||
|
queryParams: function(p) {
|
||||||
|
return {
|
||||||
|
part: options.parent_id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
columns: cols,
|
||||||
|
url: options.bom_url
|
||||||
|
});
|
||||||
|
|
||||||
|
// In editing mode, attached editables to the appropriate table elements
|
||||||
|
if (options.editable) {
|
||||||
|
|
||||||
|
table.on('click', '.bom-delete-button', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
launchDeleteForm(button.attr('url'), {
|
||||||
|
success: function() {
|
||||||
|
reloadBomTable(table);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
table.on('click', '.bom-edit-button', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
launchModalForm(button.attr('url'), {
|
||||||
|
success: function() {
|
||||||
|
reloadBomTable(table);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -14,5 +14,49 @@ function renderLink(text, url) {
|
|||||||
return '<a href="' + url + '">' + text + '</a>';
|
return '<a href="' + url + '">' + text + '</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderEditable(text, options) {
|
||||||
|
/* Wrap the text in an 'editable' link
|
||||||
|
* (using bootstrap-editable library)
|
||||||
|
*
|
||||||
|
* Can pass the following parameters in 'options':
|
||||||
|
* _type - parameter for data-type (default = 'text')
|
||||||
|
* _pk - parameter for data-pk (required)
|
||||||
|
* _title - title to show when editing
|
||||||
|
* _empty - placeholder text to show when field is empty
|
||||||
|
* _class - html class (default = 'editable-item')
|
||||||
|
* _id - id
|
||||||
|
* _value - Initial value of the editable (default = blank)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Default values (if not supplied)
|
||||||
|
var _type = options._type || 'text';
|
||||||
|
var _class = options._class || 'editable-item';
|
||||||
|
|
||||||
|
var html = "<a href='#' class='" + _class + "'";
|
||||||
|
|
||||||
|
// Add id parameter if provided
|
||||||
|
if (options._id) {
|
||||||
|
html = html + " id='" + options._id + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
html = html + " data-type='" + _type + "'";
|
||||||
|
html = html + " data-pk='" + options._pk + "'";
|
||||||
|
|
||||||
|
if (options._title) {
|
||||||
|
html = html + " data-title='" + options._title + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options._value) {
|
||||||
|
html = html + " data-value='" + options._value + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options._empty) {
|
||||||
|
html = html + " data-placeholder='" + options._empty + "'";
|
||||||
|
html = html + " data-emptytext='" + options._empty + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
html = html + ">" + text + "</a>";
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -38,10 +38,8 @@
|
|||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='stock-table'>
|
<div id='button-toolbar'>
|
||||||
</table>
|
<div class='container-fluid' style='float: right;'>
|
||||||
|
|
||||||
<div class='container-fluid' style='float: right;'>
|
|
||||||
<button class="btn btn-success" id='item-create'>New Stock Item</span></button>
|
<button class="btn btn-success" id='item-create'>New Stock Item</span></button>
|
||||||
<div class="dropdown" style='float: right;'>
|
<div class="dropdown" style='float: right;'>
|
||||||
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
<button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">Options
|
||||||
@ -53,8 +51,13 @@
|
|||||||
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move</a></li>
|
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='stock-table'>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
{% include 'modals.html' %}
|
{% include 'modals.html' %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -80,7 +80,7 @@ class StockItemEdit(AjaxUpdateView):
|
|||||||
|
|
||||||
model = StockItem
|
model = StockItem
|
||||||
form_class = EditStockItemForm
|
form_class = EditStockItemForm
|
||||||
template_name = 'stock/item_edit.html'
|
# template_name = 'stock/item_edit.html'
|
||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit Stock Item'
|
ajax_form_title = 'Edit Stock Item'
|
||||||
|
@ -10,8 +10,9 @@
|
|||||||
|
|
||||||
<!-- CSS -->
|
<!-- CSS -->
|
||||||
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
|
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
|
|
||||||
<link rel="stylesheet" href="{% static 'css/bootstrap-table.css' %}">
|
<link rel="stylesheet" href="{% static 'css/bootstrap-table.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/select2-bootstrap.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user