mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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 company.api import company_api_urls
|
||||
from stock.api import stock_api_urls
|
||||
from build.api import build_api_urls
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
@ -31,6 +32,7 @@ apipatterns = [
|
||||
url(r'^part/', include(part_api_urls)),
|
||||
url(r'^company/', include(company_api_urls)),
|
||||
url(r'^stock/', include(stock_api_urls)),
|
||||
url(r'^build/', include(build_api_urls)),
|
||||
|
||||
# User URLs
|
||||
url(r'^user/', include(user_urls)),
|
||||
|
@ -65,9 +65,7 @@ class AjaxMixin(object):
|
||||
else:
|
||||
return self.template_name
|
||||
|
||||
def renderJsonResponse(self, request, form, data={}):
|
||||
|
||||
context = {}
|
||||
def renderJsonResponse(self, request, form=None, data={}, context={}):
|
||||
|
||||
if form:
|
||||
context['form'] = form
|
||||
@ -92,22 +90,46 @@ class AjaxMixin(object):
|
||||
class AjaxView(AjaxMixin, View):
|
||||
""" 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):
|
||||
return JsonResponse('', safe=False)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
return self.renderJsonResponse(request, None)
|
||||
return self.renderJsonResponse(request)
|
||||
|
||||
|
||||
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):
|
||||
form = self.form_class(data=request.POST, files=request.FILES)
|
||||
|
||||
if request.is_ajax():
|
||||
|
||||
data = {'form_valid': form.is_valid()}
|
||||
data = {
|
||||
'form_valid': form.is_valid(),
|
||||
}
|
||||
|
||||
if form.is_valid():
|
||||
obj = form.save()
|
||||
@ -122,20 +144,25 @@ class AjaxCreateView(AjaxMixin, CreateView):
|
||||
else:
|
||||
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):
|
||||
|
||||
response = super(CreateView, self).get(request, *args, **kwargs)
|
||||
html_response = super(UpdateView, self).get(request, *args, **kwargs)
|
||||
|
||||
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)
|
||||
|
||||
else:
|
||||
return response
|
||||
|
||||
|
||||
class AjaxUpdateView(AjaxMixin, UpdateView):
|
||||
return html_response
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
|
||||
@ -154,45 +181,26 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
|
||||
response = self.renderJsonResponse(request, form, data)
|
||||
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:
|
||||
return super(UpdateView, self).post(request, *args, **kwargs)
|
||||
|
||||
|
||||
class AjaxDeleteView(AjaxMixin, DeleteView):
|
||||
|
||||
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, None, data)
|
||||
|
||||
else:
|
||||
return super(DeleteView, self).post(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)
|
||||
- Handles deletion
|
||||
"""
|
||||
|
||||
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():
|
||||
|
||||
data = {'id': self.get_object().id,
|
||||
'title': self.ajax_form_title,
|
||||
'delete': False,
|
||||
'title': self.ajax_form_title,
|
||||
'html_data': render_to_string(self.getAjaxTemplate(),
|
||||
self.get_context_data(),
|
||||
request=request)
|
||||
@ -201,7 +209,23 @@ class AjaxDeleteView(AjaxMixin, DeleteView):
|
||||
return JsonResponse(data)
|
||||
|
||||
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):
|
||||
|
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>
|
||||
|
||||
<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>
|
||||
<tr>
|
||||
<th>Build</th>
|
||||
@ -20,9 +24,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class='container-fluid'>
|
||||
<button class="btn btn-success" id='new-build'>Start New Build</button>
|
||||
</div>
|
||||
|
||||
{% include 'modals.html' %}
|
||||
|
||||
|
@ -4,26 +4,21 @@
|
||||
|
||||
{% include 'company/tabs.html' with tab='parts' %}
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h3>Supplier Parts</h3>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<h3 class='float-right'>
|
||||
<button class="btn btn-success" id='part-create'>New Supplier Part</button>
|
||||
<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">
|
||||
</ul>
|
||||
</div>
|
||||
</h3>
|
||||
<h3>Supplier Parts</h3>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<button class="btn btn-success" id='part-create'>New Supplier Part</button>
|
||||
<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">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -4,12 +4,12 @@
|
||||
|
||||
{% 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>
|
||||
<div id='button-toolbar'>
|
||||
<h3><button style='float: right;' class="btn btn-success" id='new-company'>New Company</button></h3>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped' id='company-table'>
|
||||
<table class='table table-striped' id='company-table' data-toolbar='#button-toolbar'>
|
||||
</table>
|
||||
|
||||
|
||||
|
@ -70,13 +70,14 @@ class PartList(generics.ListCreateAPIView):
|
||||
serializer_class = PartSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
print("Get queryset")
|
||||
|
||||
# Does the user wish to filter by category?
|
||||
cat_id = self.request.query_params.get('category', None)
|
||||
|
||||
# Start with all objects
|
||||
parts_list = Part.objects.all()
|
||||
|
||||
if cat_id:
|
||||
print("Getting category:", cat_id)
|
||||
category = get_object_or_404(PartCategory, pk=cat_id)
|
||||
|
||||
# Filter by the supplied category
|
||||
@ -90,10 +91,10 @@ class PartList(generics.ListCreateAPIView):
|
||||
continue
|
||||
flt |= Q(category=child)
|
||||
|
||||
return Part.objects.filter(flt)
|
||||
parts_list = parts_list.filter(flt)
|
||||
|
||||
# Default - return all parts
|
||||
return Part.objects.all()
|
||||
return parts_list
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticatedOrReadOnly,
|
||||
@ -106,6 +107,11 @@ class PartList(generics.ListCreateAPIView):
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'buildable',
|
||||
'consumable',
|
||||
'trackable',
|
||||
'purchaseable',
|
||||
'salable',
|
||||
]
|
||||
|
||||
ordering_fields = [
|
||||
|
@ -3,6 +3,8 @@ from __future__ import unicode_literals
|
||||
|
||||
from InvenTree.forms import HelperForm
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import Part, PartCategory, BomItem
|
||||
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 Meta:
|
||||
@ -28,8 +51,10 @@ class EditPartForm(HelperForm):
|
||||
'URL',
|
||||
'default_location',
|
||||
'default_supplier',
|
||||
'units',
|
||||
'minimum_stock',
|
||||
'buildable',
|
||||
'consumable',
|
||||
'trackable',
|
||||
'purchaseable',
|
||||
'salable',
|
||||
@ -56,8 +81,10 @@ class EditBomItemForm(HelperForm):
|
||||
fields = [
|
||||
'part',
|
||||
'sub_part',
|
||||
'quantity'
|
||||
'quantity',
|
||||
'note'
|
||||
]
|
||||
widgets = {'part': forms.HiddenInput()}
|
||||
|
||||
|
||||
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 = 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?')
|
||||
|
||||
# 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"?
|
||||
# Trackable parts can have unique instances
|
||||
# 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 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
|
||||
def projects(self):
|
||||
@ -338,11 +408,15 @@ class BomItem(models.Model):
|
||||
|
||||
# A link to the child item (sub-part)
|
||||
# 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 = 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):
|
||||
|
||||
# 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)
|
||||
category = CategorySerializer(many=False, read_only=True)
|
||||
category_name = serializers.CharField(source='category_path', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Part
|
||||
@ -55,11 +55,13 @@ class PartSerializer(serializers.ModelSerializer):
|
||||
'URL', # Link to an external URL (optional)
|
||||
'description',
|
||||
'category',
|
||||
'category_name',
|
||||
'total_stock',
|
||||
'available_stock',
|
||||
'units',
|
||||
'trackable',
|
||||
'buildable',
|
||||
'consumable',
|
||||
'trackable',
|
||||
'salable',
|
||||
]
|
||||
@ -79,7 +81,8 @@ class BomItemSerializer(serializers.ModelSerializer):
|
||||
'url',
|
||||
'part',
|
||||
'sub_part',
|
||||
'quantity'
|
||||
'quantity',
|
||||
'note',
|
||||
]
|
||||
|
||||
|
||||
|
@ -11,109 +11,72 @@
|
||||
|
||||
<h3>Bill of Materials</h3>
|
||||
|
||||
<table class='table table-striped table-condensed' id='bom-table'>
|
||||
</table>
|
||||
|
||||
<div class='container-fluid'>
|
||||
<button type='button' class='btn btn-success' id='new-bom-item'>Add BOM Item</button>
|
||||
<div id='button-toolbar'>
|
||||
{% if editing_enabled %}
|
||||
<div class='btn-group' style='float: right;'>
|
||||
<button class='btn btn-info' type='button' id='bom-item-new'>New 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>
|
||||
|
||||
<table class='table table-striped table-condensed' data-toolbar="#button-toolbar" id='bom-table'>
|
||||
</table>
|
||||
|
||||
{% 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.super }}
|
||||
function reloadBom() {
|
||||
$("#bom-table").bootstrapTable('refresh');
|
||||
}
|
||||
|
||||
$('#bom-table').on('click', '.delete-button', function () {
|
||||
var button = $(this);
|
||||
|
||||
launchDeleteForm(
|
||||
button.attr('url'),
|
||||
{
|
||||
success: reloadBom
|
||||
});
|
||||
// Load the BOM table data
|
||||
loadBomTable($("#bom-table"), {
|
||||
editable: {{ editing_enabled }},
|
||||
bom_url: "{% url 'api-bom-list' %}",
|
||||
part_url: "{% url 'api-part-list' %}",
|
||||
parent_id: {{ part.id }}
|
||||
});
|
||||
|
||||
$("#bom-table").on('click', '.edit-button', function () {
|
||||
var button = $(this);
|
||||
{% if editing_enabled %}
|
||||
$("#editing-finished").click(function() {
|
||||
location.href = "{% url 'part-bom' part.id %}";
|
||||
});
|
||||
|
||||
launchModalForm(
|
||||
button.attr('url'),
|
||||
{
|
||||
success: reloadBom
|
||||
});
|
||||
$("#bom-item-new").click(function () {
|
||||
launchModalForm("{% url 'bom-item-create' %}?parent={{ part.id }}", {});
|
||||
});
|
||||
|
||||
{% 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 () {
|
||||
launchModalForm(
|
||||
"{% url 'bom-item-create' %}",
|
||||
{
|
||||
reload: true,
|
||||
data: {
|
||||
parent: {{ part.id }}
|
||||
}
|
||||
});
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
$("#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 %}
|
@ -6,34 +6,13 @@
|
||||
|
||||
<h3>Part Builds</h3>
|
||||
|
||||
<table class='table table-striped'>
|
||||
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Quantity</th>
|
||||
<th>Status</th>
|
||||
<th>Completion Date</th>
|
||||
</tr>
|
||||
{% if part.active_builds|length > 0 %}
|
||||
<tr>
|
||||
<td colspan="4"><b>Active Builds</b></td>
|
||||
</tr>
|
||||
{% include "part/build_list.html" with builds=part.active_builds %}
|
||||
{% endif %}
|
||||
|
||||
{% if part.inactive_builds|length > 0 %}
|
||||
<tr><td colspan="4"></td></tr>
|
||||
<tr>
|
||||
<td colspan="4"><b>Inactive Builds</b></td>
|
||||
</tr>
|
||||
{% include "part/build_list.html" with builds=part.inactive_builds %}
|
||||
{% endif %}
|
||||
<div id='button-toolbar'>
|
||||
<button class="btn btn-success" id='start-build'>Start New Build</button>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='build-table'>
|
||||
</table>
|
||||
|
||||
<div class='container-fluid'>
|
||||
<button class="btn btn-success" id='start-build'>Start New Build</button>
|
||||
</div>
|
||||
|
||||
{% 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 %}
|
@ -40,13 +40,13 @@
|
||||
{% endif %}
|
||||
<hr>
|
||||
|
||||
<table class='table table-striped table-condensed' id='part-table'>
|
||||
</table>
|
||||
|
||||
<div>
|
||||
<button style='float: right;' class='btn btn-success' id='part-create'>New Part</button>
|
||||
<div id='button-toolbar'>
|
||||
<button style='float: right;' class='btn btn-success' id='part-create'>New Part</button>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
{% block js_load %}
|
||||
{{ block.super }}
|
||||
@ -151,11 +151,11 @@
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'category',
|
||||
field: 'category_name',
|
||||
title: 'Category',
|
||||
formatter: function(value, row, index, field) {
|
||||
if (row.category) {
|
||||
return renderLink(row.category.pathstring, row.category.url);
|
||||
return renderLink(row.category_name, "/part/category/" + row.category + "/");
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
|
@ -32,7 +32,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ part.decription }}</td>
|
||||
<td>{{ part.description }}</td>
|
||||
</tr>
|
||||
{% if part.IPN %}
|
||||
<tr>
|
||||
@ -44,7 +44,7 @@
|
||||
<td>Category</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -70,6 +70,10 @@
|
||||
<td>Buildable</td>
|
||||
<td>{% include "yesnolabel.html" with value=part.buildable %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Consumable</td>
|
||||
<td>{% include "yesnolabel.html" with value=part.consumable %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Trackable</td>
|
||||
<td>{% include "yesnolabel.html" with value=part.trackable %}</td>
|
||||
|
@ -37,7 +37,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<tr>
|
||||
<td>In Stock</td>
|
||||
|
@ -4,32 +4,23 @@
|
||||
|
||||
{% include 'part/tabs.html' with tab='stock' %}
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h3>Part Stock</h3>
|
||||
</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>
|
||||
<div id='opt-dropdown' 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='multi-item-take' title='Take items from stock'>Take items</a></li>
|
||||
<li><a href='#' id='multi-item-give' title='Give items to stock'>Add items</a></li>
|
||||
<li><a href="#" id='multi-item-stocktake' title='Stocktake selected stock items'>Stocktake</a></li>
|
||||
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move items</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
<h3>Part Stock</h3>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<button class='btn btn-success' id='add-stock-item'>New Stock Item</button>
|
||||
<div id='opt-dropdown' 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='multi-item-take' title='Take items from stock'>Take items</a></li>
|
||||
<li><a href='#' id='multi-item-give' title='Give items to stock'>Add items</a></li>
|
||||
<li><a href="#" id='multi-item-stocktake' title='Stocktake selected stock items'>Stocktake</a></li>
|
||||
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move items</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<table class='table table-striped table-condensed' id='stock-table'>
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='stock-table'>
|
||||
</table>
|
||||
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<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>
|
||||
</div>
|
||||
<div id="collapse1" class="panel-collapse collapse">
|
||||
|
@ -4,20 +4,15 @@
|
||||
|
||||
{% include 'part/tabs.html' with tab='suppliers' %}
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h3>Part Suppliers</h3>
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
<h3>
|
||||
<button class="btn btn-success float-right" id='supplier-create'>New Supplier Part</button>
|
||||
</h3>
|
||||
</div>
|
||||
<h3>Part Suppliers</h3>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<button class="btn btn-success float-right" id='supplier-create'>New Supplier Part</button>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -7,7 +7,7 @@
|
||||
<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>
|
||||
{% endif %}
|
||||
{% if part.used_in_count > 0 %}
|
||||
{% if part.consumable or part.used_in_count > 0 %}
|
||||
<li{% ifequal tab 'used' %} class="active"{% endifequal %}>
|
||||
<a href="{% url 'part-used-in' part.id %}">Used In{% if part.used_in_count > 0 %}<span class="badge">{{ part.used_in_count }}</span>{% endif %}</a></li>
|
||||
{% endif %}
|
||||
|
@ -19,6 +19,7 @@ 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'^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'),
|
||||
|
@ -4,7 +4,6 @@ from __future__ import unicode_literals
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from django.views.generic import DetailView, ListView
|
||||
|
||||
from company.models import Company
|
||||
@ -15,10 +14,13 @@ from .forms import PartImageForm
|
||||
from .forms import EditPartForm
|
||||
from .forms import EditCategoryForm
|
||||
from .forms import EditBomItemForm
|
||||
from .forms import BomExportForm
|
||||
|
||||
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):
|
||||
@ -88,6 +90,17 @@ class PartDetail(DetailView):
|
||||
queryset = Part.objects.all()
|
||||
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):
|
||||
|
||||
@ -104,10 +117,88 @@ class PartImage(AjaxUpdateView):
|
||||
|
||||
class PartEdit(AjaxUpdateView):
|
||||
model = Part
|
||||
form_class = EditPartForm
|
||||
template_name = 'part/edit.html'
|
||||
form_class = EditPartForm
|
||||
ajax_template_name = 'modal_form.html'
|
||||
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):
|
||||
@ -115,6 +206,7 @@ class PartDelete(AjaxDeleteView):
|
||||
template_name = 'part/delete.html'
|
||||
ajax_template_name = 'part/partial_delete.html'
|
||||
ajax_form_title = 'Confirm Part Deletion'
|
||||
context_object_name = '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>';
|
||||
}
|
||||
|
||||
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,23 +38,26 @@
|
||||
|
||||
<hr>
|
||||
|
||||
<table class='table table-striped table-condensed' id='stock-table'>
|
||||
</table>
|
||||
|
||||
<div class='container-fluid' style='float: right;'>
|
||||
<button class="btn btn-success" id='item-create'>New Stock Item</span></button>
|
||||
<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='multi-item-add' title='Add to selected stock items'>Add stock</a></li>
|
||||
<li><a href="#" id='multi-item-remove' title='Remove from selected stock items'>Remove stock</a></li>
|
||||
<li><a href="#" id='multi-item-stocktake' title='Stocktake selected stock items'>Stocktake</a></li>
|
||||
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move</a></li>
|
||||
</ul>
|
||||
<div id='button-toolbar'>
|
||||
<div class='container-fluid' style='float: right;'>
|
||||
<button class="btn btn-success" id='item-create'>New Stock Item</span></button>
|
||||
<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='multi-item-add' title='Add to selected stock items'>Add stock</a></li>
|
||||
<li><a href="#" id='multi-item-remove' title='Remove from selected stock items'>Remove stock</a></li>
|
||||
<li><a href="#" id='multi-item-stocktake' title='Stocktake selected stock items'>Stocktake</a></li>
|
||||
<li><a href='#' id='multi-item-move' title='Move selected stock items'>Move</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='stock-table'>
|
||||
</table>
|
||||
|
||||
|
||||
{% include 'modals.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
@ -80,7 +80,7 @@ class StockItemEdit(AjaxUpdateView):
|
||||
|
||||
model = StockItem
|
||||
form_class = EditStockItemForm
|
||||
template_name = 'stock/item_edit.html'
|
||||
# template_name = 'stock/item_edit.html'
|
||||
context_object_name = 'item'
|
||||
ajax_template_name = 'modal_form.html'
|
||||
ajax_form_title = 'Edit Stock Item'
|
||||
|
@ -10,8 +10,9 @@
|
||||
|
||||
<!-- 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/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/select2-bootstrap.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
||||
|
||||
{% block css %}
|
||||
|
Loading…
Reference in New Issue
Block a user