mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-30 20:46:47 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
ec166c906f
@ -9,7 +9,8 @@ addons:
|
|||||||
-sqlite3
|
-sqlite3
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- make setup
|
- make install
|
||||||
|
- make migrate
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- make coverage
|
- make coverage
|
||||||
|
@ -69,6 +69,22 @@ class AjaxMixin(object):
|
|||||||
ajax_form_action = ''
|
ajax_form_action = ''
|
||||||
ajax_form_title = ''
|
ajax_form_title = ''
|
||||||
|
|
||||||
|
def get_param(self, name, method='GET'):
|
||||||
|
""" Get a request query parameter value from URL e.g. ?part=3
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Variable name e.g. 'part'
|
||||||
|
method: Request type ('GET' or 'POST')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Value of the supplier parameter or None if parameter is not available
|
||||||
|
"""
|
||||||
|
|
||||||
|
if method == 'POST':
|
||||||
|
return self.request.POST.get(name, None)
|
||||||
|
else:
|
||||||
|
return self.request.GET.get(name, None)
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
""" Get extra context data (default implementation is empty dict)
|
""" Get extra context data (default implementation is empty dict)
|
||||||
|
|
||||||
@ -134,79 +150,82 @@ class AjaxCreateView(AjaxMixin, CreateView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
""" Creates form with initial data, and renders JSON response """
|
||||||
|
|
||||||
response = super(CreateView, self).get(request, *args, **kwargs)
|
super(CreateView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
if request.is_ajax():
|
form = self.get_form()
|
||||||
# Initialize a a new form
|
return self.renderJsonResponse(request, 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)
|
""" Responds to form POST. Validates POST data and returns status info.
|
||||||
|
|
||||||
if request.is_ajax():
|
- Validate POST form data
|
||||||
|
- If valid, save form
|
||||||
|
- Return status info (success / failure)
|
||||||
|
"""
|
||||||
|
form = self.get_form()
|
||||||
|
|
||||||
data = {
|
# Extra JSON data sent alongside form
|
||||||
'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()
|
||||||
|
|
||||||
# Return the PK of the newly-created object
|
# Return the PK of the newly-created object
|
||||||
data['pk'] = obj.pk
|
data['pk'] = obj.pk
|
||||||
|
|
||||||
data['url'] = obj.get_absolute_url()
|
data['url'] = obj.get_absolute_url()
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form, data)
|
return self.renderJsonResponse(request, form, data)
|
||||||
|
|
||||||
else:
|
|
||||||
return super(CreateView, self).post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class AjaxUpdateView(AjaxMixin, UpdateView):
|
class AjaxUpdateView(AjaxMixin, UpdateView):
|
||||||
|
|
||||||
""" An 'AJAXified' UpdateView for updating an object in the db
|
""" An 'AJAXified' UpdateView for updating an object in the db
|
||||||
- Returns form in JSON format (for delivery to a modal window)
|
- Returns form in JSON format (for delivery to a modal window)
|
||||||
- Handles repeated form validation (via AJAX) until the form is valid
|
- Handles repeated form validation (via AJAX) until the form is valid
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
""" Respond to GET request.
|
||||||
|
|
||||||
html_response = super(UpdateView, self).get(request, *args, **kwargs)
|
- Populates form with object data
|
||||||
|
- Renders form to JSON and returns to client
|
||||||
|
"""
|
||||||
|
|
||||||
if request.is_ajax():
|
super(UpdateView, self).get(request, *args, **kwargs)
|
||||||
form = self.form_class(instance=self.get_object())
|
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form)
|
form = self.get_form()
|
||||||
|
|
||||||
else:
|
return self.renderJsonResponse(request, form)
|
||||||
return html_response
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
""" Respond to POST request.
|
||||||
|
|
||||||
form = self.form_class(instance=self.get_object(), data=request.POST, files=request.FILES)
|
- Updates model with POST field data
|
||||||
|
- Performs form and object validation
|
||||||
|
- If errors exist, re-render the form
|
||||||
|
- Otherwise, return sucess status
|
||||||
|
"""
|
||||||
|
|
||||||
if request.is_ajax():
|
super(UpdateView, self).post(request, *args, **kwargs)
|
||||||
|
|
||||||
data = {'form_valid': form.is_valid()}
|
form = self.get_form()
|
||||||
|
|
||||||
if form.is_valid():
|
data = {
|
||||||
obj = form.save()
|
'form_valid': form.is_valid()
|
||||||
|
}
|
||||||
|
|
||||||
data['pk'] = obj.id
|
if form.is_valid():
|
||||||
data['url'] = obj.get_absolute_url()
|
obj = form.save()
|
||||||
|
|
||||||
response = self.renderJsonResponse(request, form, data)
|
# Include context data about the updated object
|
||||||
return response
|
data['pk'] = obj.id
|
||||||
|
data['url'] = obj.get_absolute_url()
|
||||||
|
|
||||||
else:
|
return self.renderJsonResponse(request, form, data)
|
||||||
return super(UpdateView, self).post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class AjaxDeleteView(AjaxMixin, DeleteView):
|
class AjaxDeleteView(AjaxMixin, DeleteView):
|
||||||
@ -217,39 +236,43 @@ class AjaxDeleteView(AjaxMixin, DeleteView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
""" Respond to GET request
|
||||||
|
|
||||||
html_response = super(DeleteView, self).get(request, *args, **kwargs)
|
- Render a DELETE confirmation form to JSON
|
||||||
|
- Return rendered form to client
|
||||||
|
"""
|
||||||
|
|
||||||
if request.is_ajax():
|
super(DeleteView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
data = {'id': self.get_object().id,
|
data = {
|
||||||
'delete': False,
|
'id': self.get_object().id,
|
||||||
'title': self.ajax_form_title,
|
'delete': False,
|
||||||
'html_data': render_to_string(self.ajax_template_name,
|
'title': self.ajax_form_title,
|
||||||
self.get_context_data(),
|
'html_data': render_to_string(
|
||||||
request=request)
|
self.ajax_template_name,
|
||||||
}
|
self.get_context_data(),
|
||||||
|
request=request)
|
||||||
|
}
|
||||||
|
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|
||||||
else:
|
|
||||||
return html_response
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
|
""" Respond to POST request
|
||||||
|
|
||||||
if request.is_ajax():
|
- DELETE the object
|
||||||
|
- Render success message to JSON and return to client
|
||||||
|
"""
|
||||||
|
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
pk = obj.id
|
pk = obj.id
|
||||||
obj.delete()
|
obj.delete()
|
||||||
|
|
||||||
data = {'id': pk,
|
data = {
|
||||||
'delete': True}
|
'id': pk,
|
||||||
|
'delete': True
|
||||||
|
}
|
||||||
|
|
||||||
return self.renderJsonResponse(request, data=data)
|
return self.renderJsonResponse(request, data=data)
|
||||||
|
|
||||||
else:
|
|
||||||
return super(DeleteView, self).post(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class IndexView(TemplateView):
|
class IndexView(TemplateView):
|
||||||
|
@ -101,7 +101,10 @@ class BuildCreate(AjaxCreateView):
|
|||||||
part_id = self.request.GET.get('part', None)
|
part_id = self.request.GET.get('part', None)
|
||||||
|
|
||||||
if part_id:
|
if part_id:
|
||||||
initials['part'] = get_object_or_404(Part, pk=part_id)
|
try:
|
||||||
|
initials['part'] = Part.objects.get(pk=part_id)
|
||||||
|
except Part.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ from rest_framework import generics, permissions
|
|||||||
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
|
|
||||||
from .models import Part, PartCategory, BomItem
|
from .models import Part, PartCategory, BomItem
|
||||||
from .models import SupplierPart, SupplierPriceBreak
|
from .models import SupplierPart, SupplierPriceBreak
|
||||||
@ -99,20 +98,24 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
parts_list = Part.objects.all()
|
parts_list = Part.objects.all()
|
||||||
|
|
||||||
if cat_id:
|
if cat_id:
|
||||||
category = get_object_or_404(PartCategory, pk=cat_id)
|
try:
|
||||||
|
category = PartCategory.objects.get(pk=cat_id)
|
||||||
|
|
||||||
# Filter by the supplied category
|
# Filter by the supplied category
|
||||||
flt = Q(category=cat_id)
|
flt = Q(category=cat_id)
|
||||||
|
|
||||||
if self.request.query_params.get('include_child_categories', None):
|
if self.request.query_params.get('include_child_categories', None):
|
||||||
childs = category.getUniqueChildren()
|
childs = category.getUniqueChildren()
|
||||||
for child in childs:
|
for child in childs:
|
||||||
# Ignore the top-level category (already filtered)
|
# Ignore the top-level category (already filtered)
|
||||||
if str(child) == str(cat_id):
|
if str(child) == str(cat_id):
|
||||||
continue
|
continue
|
||||||
flt |= Q(category=child)
|
flt |= Q(category=child)
|
||||||
|
|
||||||
parts_list = parts_list.filter(flt)
|
parts_list = parts_list.filter(flt)
|
||||||
|
|
||||||
|
except PartCategory.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
return parts_list
|
return parts_list
|
||||||
|
|
||||||
|
@ -92,6 +92,8 @@ class EditBomItemForm(HelperForm):
|
|||||||
'quantity',
|
'quantity',
|
||||||
'note'
|
'note'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Prevent editing of the part associated with this BomItem
|
||||||
widgets = {'part': forms.HiddenInput()}
|
widgets = {'part': forms.HiddenInput()}
|
||||||
|
|
||||||
|
|
||||||
@ -101,13 +103,13 @@ class EditSupplierPartForm(HelperForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = SupplierPart
|
model = SupplierPart
|
||||||
fields = [
|
fields = [
|
||||||
|
'part',
|
||||||
'supplier',
|
'supplier',
|
||||||
'SKU',
|
'SKU',
|
||||||
'part',
|
|
||||||
'description',
|
'description',
|
||||||
'URL',
|
|
||||||
'manufacturer',
|
'manufacturer',
|
||||||
'MPN',
|
'MPN',
|
||||||
|
'URL',
|
||||||
'note',
|
'note',
|
||||||
'single_price',
|
'single_price',
|
||||||
'base_cost',
|
'base_cost',
|
||||||
|
19
InvenTree/part/migrations/0011_auto_20190428_0841.py
Normal file
19
InvenTree/part/migrations/0011_auto_20190428_0841.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.2 on 2019-04-27 22:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('part', '0010_auto_20190417_0045'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='supplierpart',
|
||||||
|
name='supplier',
|
||||||
|
field=models.ForeignKey(limit_choices_to={'is_supplier': True}, on_delete=django.db.models.deletion.CASCADE, related_name='parts', to='company.Company'),
|
||||||
|
),
|
||||||
|
]
|
@ -84,7 +84,10 @@ class PartCreate(AjaxCreateView):
|
|||||||
cat_id = self.get_category_id()
|
cat_id = self.get_category_id()
|
||||||
|
|
||||||
if cat_id:
|
if cat_id:
|
||||||
context['category'] = get_object_or_404(PartCategory, pk=cat_id)
|
try:
|
||||||
|
context['category'] = PartCategory.objects.get(pk=cat_id)
|
||||||
|
except PartCategory.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -111,7 +114,10 @@ class PartCreate(AjaxCreateView):
|
|||||||
initials = super(PartCreate, self).get_initial()
|
initials = super(PartCreate, self).get_initial()
|
||||||
|
|
||||||
if self.get_category_id():
|
if self.get_category_id():
|
||||||
initials['category'] = get_object_or_404(PartCategory, pk=self.get_category_id())
|
try:
|
||||||
|
initials['category'] = PartCategory.objects.get(pk=self.get_category_id())
|
||||||
|
except PartCategory.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
@ -158,7 +164,6 @@ class PartEdit(AjaxUpdateView):
|
|||||||
""" View for editing Part object """
|
""" View for editing Part object """
|
||||||
|
|
||||||
model = Part
|
model = Part
|
||||||
template_name = 'part/edit.html'
|
|
||||||
form_class = EditPartForm
|
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'
|
||||||
@ -246,7 +251,6 @@ class PartDelete(AjaxDeleteView):
|
|||||||
""" View to delete a Part object """
|
""" View to delete a Part object """
|
||||||
|
|
||||||
model = Part
|
model = Part
|
||||||
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'
|
context_object_name = 'part'
|
||||||
@ -270,7 +274,6 @@ class CategoryDetail(DetailView):
|
|||||||
class CategoryEdit(AjaxUpdateView):
|
class CategoryEdit(AjaxUpdateView):
|
||||||
""" Update view to edit a PartCategory """
|
""" Update view to edit a PartCategory """
|
||||||
model = PartCategory
|
model = PartCategory
|
||||||
template_name = 'part/category_edit.html'
|
|
||||||
form_class = EditCategoryForm
|
form_class = EditCategoryForm
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit Part Category'
|
ajax_form_title = 'Edit Part Category'
|
||||||
@ -278,7 +281,10 @@ class CategoryEdit(AjaxUpdateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
|
context = super(CategoryEdit, self).get_context_data(**kwargs).copy()
|
||||||
|
|
||||||
context['category'] = get_object_or_404(PartCategory, pk=self.kwargs['pk'])
|
try:
|
||||||
|
context['category'] = PartCategory.objects.get(pk=self.kwargs['pk'])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -286,7 +292,7 @@ class CategoryEdit(AjaxUpdateView):
|
|||||||
class CategoryDelete(AjaxDeleteView):
|
class CategoryDelete(AjaxDeleteView):
|
||||||
""" Delete view to delete a PartCategory """
|
""" Delete view to delete a PartCategory """
|
||||||
model = PartCategory
|
model = PartCategory
|
||||||
template_name = 'part/category_delete.html'
|
ajax_template_name = 'part/category_delete.html'
|
||||||
context_object_name = 'category'
|
context_object_name = 'category'
|
||||||
success_url = '/part/'
|
success_url = '/part/'
|
||||||
|
|
||||||
@ -302,7 +308,6 @@ class CategoryCreate(AjaxCreateView):
|
|||||||
ajax_form_action = reverse_lazy('category-create')
|
ajax_form_action = reverse_lazy('category-create')
|
||||||
ajax_form_title = 'Create new part category'
|
ajax_form_title = 'Create new part category'
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
template_name = 'part/category_new.html'
|
|
||||||
form_class = EditCategoryForm
|
form_class = EditCategoryForm
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -315,7 +320,10 @@ class CategoryCreate(AjaxCreateView):
|
|||||||
parent_id = self.request.GET.get('category', None)
|
parent_id = self.request.GET.get('category', None)
|
||||||
|
|
||||||
if parent_id:
|
if parent_id:
|
||||||
context['category'] = get_object_or_404(PartCategory, pk=parent_id)
|
try:
|
||||||
|
context['category'] = PartCategory.objects.get(pk=parent_id)
|
||||||
|
except PartCategory.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -329,7 +337,10 @@ class CategoryCreate(AjaxCreateView):
|
|||||||
parent_id = self.request.GET.get('category', None)
|
parent_id = self.request.GET.get('category', None)
|
||||||
|
|
||||||
if parent_id:
|
if parent_id:
|
||||||
initials['parent'] = get_object_or_404(PartCategory, pk=parent_id)
|
try:
|
||||||
|
initials['parent'] = PartCategory.objects.get(pk=parent_id)
|
||||||
|
except PartCategory.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
@ -345,7 +356,6 @@ class BomItemCreate(AjaxCreateView):
|
|||||||
""" Create view for making a new BomItem object """
|
""" Create view for making a new BomItem object """
|
||||||
model = BomItem
|
model = BomItem
|
||||||
form_class = EditBomItemForm
|
form_class = EditBomItemForm
|
||||||
template_name = 'part/bom-create.html'
|
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Create BOM item'
|
ajax_form_title = 'Create BOM item'
|
||||||
|
|
||||||
@ -362,7 +372,10 @@ class BomItemCreate(AjaxCreateView):
|
|||||||
parent_id = self.request.GET.get('parent', None)
|
parent_id = self.request.GET.get('parent', None)
|
||||||
|
|
||||||
if parent_id:
|
if parent_id:
|
||||||
initials['part'] = get_object_or_404(Part, pk=parent_id)
|
try:
|
||||||
|
initials['part'] = Part.objects.get(pk=parent_id)
|
||||||
|
except Part.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
@ -372,7 +385,6 @@ class BomItemEdit(AjaxUpdateView):
|
|||||||
|
|
||||||
model = BomItem
|
model = BomItem
|
||||||
form_class = EditBomItemForm
|
form_class = EditBomItemForm
|
||||||
template_name = 'part/bom-edit.html'
|
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Edit BOM item'
|
ajax_form_title = 'Edit BOM item'
|
||||||
|
|
||||||
@ -380,7 +392,7 @@ class BomItemEdit(AjaxUpdateView):
|
|||||||
class BomItemDelete(AjaxDeleteView):
|
class BomItemDelete(AjaxDeleteView):
|
||||||
""" Delete view for removing BomItem """
|
""" Delete view for removing BomItem """
|
||||||
model = BomItem
|
model = BomItem
|
||||||
template_name = 'part/bom-delete.html'
|
ajax_template_name = 'part/bom-delete.html'
|
||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
ajax_form_title = 'Confim BOM item deletion'
|
ajax_form_title = 'Confim BOM item deletion'
|
||||||
|
|
||||||
@ -397,7 +409,6 @@ class SupplierPartEdit(AjaxUpdateView):
|
|||||||
""" Update view for editing SupplierPart """
|
""" Update view for editing SupplierPart """
|
||||||
|
|
||||||
model = SupplierPart
|
model = SupplierPart
|
||||||
template_name = 'company/partedit.html'
|
|
||||||
context_object_name = 'part'
|
context_object_name = 'part'
|
||||||
form_class = EditSupplierPartForm
|
form_class = EditSupplierPartForm
|
||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
@ -413,6 +424,19 @@ class SupplierPartCreate(AjaxCreateView):
|
|||||||
ajax_form_title = 'Create new Supplier Part'
|
ajax_form_title = 'Create new Supplier Part'
|
||||||
context_object_name = 'part'
|
context_object_name = 'part'
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
form = super(AjaxCreateView, self).get_form()
|
||||||
|
|
||||||
|
if form.initial.get('supplier', None):
|
||||||
|
# Hide the supplier field
|
||||||
|
form.fields['supplier'].widget.attrs['disabled'] = True
|
||||||
|
|
||||||
|
if form.initial.get('part', None):
|
||||||
|
# Hide the part field
|
||||||
|
form.fields['part'].widget.attrs['disabled'] = True
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
""" Provide initial data for new SupplierPart:
|
""" Provide initial data for new SupplierPart:
|
||||||
|
|
||||||
@ -421,17 +445,20 @@ class SupplierPartCreate(AjaxCreateView):
|
|||||||
"""
|
"""
|
||||||
initials = super(SupplierPartCreate, self).get_initial().copy()
|
initials = super(SupplierPartCreate, self).get_initial().copy()
|
||||||
|
|
||||||
supplier_id = self.request.GET.get('supplier', None)
|
supplier_id = self.get_param('supplier')
|
||||||
part_id = self.request.GET.get('part', None)
|
part_id = self.get_param('part')
|
||||||
|
|
||||||
if supplier_id:
|
if supplier_id:
|
||||||
initials['supplier'] = get_object_or_404(Company, pk=supplier_id)
|
try:
|
||||||
# TODO
|
initials['supplier'] = Company.objects.get(pk=supplier_id)
|
||||||
# self.fields['supplier'].disabled = True
|
except Company.DoesNotExist:
|
||||||
|
initials['supplier'] = None
|
||||||
|
|
||||||
if part_id:
|
if part_id:
|
||||||
initials['part'] = get_object_or_404(Part, pk=part_id)
|
try:
|
||||||
# TODO
|
initials['part'] = Part.objects.get(pk=part_id)
|
||||||
# self.fields['part'].disabled = True
|
except Part.DoesNotExist:
|
||||||
|
initials['part'] = None
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
@ -440,4 +467,4 @@ class SupplierPartDelete(AjaxDeleteView):
|
|||||||
""" Delete view for removing a SupplierPart """
|
""" Delete view for removing a SupplierPart """
|
||||||
model = SupplierPart
|
model = SupplierPart
|
||||||
success_url = '/supplier/'
|
success_url = '/supplier/'
|
||||||
template_name = 'company/partdelete.html'
|
ajax_template_name = 'company/partdelete.html'
|
||||||
|
@ -7,7 +7,6 @@ from django_filters import NumberFilter
|
|||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
|
|
||||||
from .models import StockLocation, StockItem
|
from .models import StockLocation, StockItem
|
||||||
from .models import StockItemTracking
|
from .models import StockItemTracking
|
||||||
@ -238,20 +237,24 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
stock_list = StockItem.objects.all()
|
stock_list = StockItem.objects.all()
|
||||||
|
|
||||||
if loc_id:
|
if loc_id:
|
||||||
location = get_object_or_404(StockLocation, pk=loc_id)
|
try:
|
||||||
|
location = StockLocation.objects.get(pk=loc_id)
|
||||||
|
|
||||||
# Filter by the supplied category
|
# Filter by the supplied category
|
||||||
flt = Q(location=loc_id)
|
flt = Q(location=loc_id)
|
||||||
|
|
||||||
if self.request.query_params.get('include_child_locations', None):
|
if self.request.query_params.get('include_child_locations', None):
|
||||||
childs = location.getUniqueChildren()
|
childs = location.getUniqueChildren()
|
||||||
for child in childs:
|
for child in childs:
|
||||||
# Ignore the top-level category (already filtered!)
|
# Ignore the top-level category (already filtered!)
|
||||||
if str(child) == str(loc_id):
|
if str(child) == str(loc_id):
|
||||||
continue
|
continue
|
||||||
flt |= Q(location=child)
|
flt |= Q(location=child)
|
||||||
|
|
||||||
stock_list = stock_list.filter(flt)
|
stock_list = stock_list.filter(flt)
|
||||||
|
|
||||||
|
except StockLocation.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
return stock_list
|
return stock_list
|
||||||
|
|
||||||
|
19
InvenTree/stock/migrations/0009_auto_20190428_0841.py
Normal file
19
InvenTree/stock/migrations/0009_auto_20190428_0841.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.2 on 2019-04-27 22:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stock', '0008_auto_20190417_1819'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='stockitem',
|
||||||
|
name='location',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Where is this stock item located?', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='stock_items', to='stock.StockLocation'),
|
||||||
|
),
|
||||||
|
]
|
@ -5,8 +5,6 @@ Django views for interacting with Stock app
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
|
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
from django.forms.models import model_to_dict
|
from django.forms.models import model_to_dict
|
||||||
|
|
||||||
@ -106,7 +104,10 @@ class StockLocationCreate(AjaxCreateView):
|
|||||||
loc_id = self.request.GET.get('location', None)
|
loc_id = self.request.GET.get('location', None)
|
||||||
|
|
||||||
if loc_id:
|
if loc_id:
|
||||||
initials['parent'] = get_object_or_404(StockLocation, pk=loc_id)
|
try:
|
||||||
|
initials['parent'] = StockLocation.objects.get(pk=loc_id)
|
||||||
|
except StockLocation.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
@ -125,7 +126,30 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Create new Stock Item'
|
ajax_form_title = 'Create new Stock Item'
|
||||||
|
|
||||||
|
def get_form(self):
|
||||||
|
""" Get form for StockItem creation.
|
||||||
|
Overrides the default get_form() method to intelligently limit
|
||||||
|
ForeignKey choices based on other selections
|
||||||
|
"""
|
||||||
|
|
||||||
|
form = super(AjaxCreateView, self).get_form()
|
||||||
|
|
||||||
|
# If the user has selected a Part, limit choices for SupplierPart
|
||||||
|
if form['part'].value() is not None:
|
||||||
|
part = form['part'].value()
|
||||||
|
parts = form.fields['supplier_part'].queryset
|
||||||
|
parts = parts.filter(part=part)
|
||||||
|
form.fields['supplier_part'].queryset = parts
|
||||||
|
|
||||||
|
# Otherwise if the user has selected a SupplierPart, we know what Part they meant!
|
||||||
|
elif form['supplier_part'].value() is not None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return form
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
|
""" Provide initial data to create a new StockItem object
|
||||||
|
"""
|
||||||
|
|
||||||
# Is the client attempting to copy an existing stock item?
|
# Is the client attempting to copy an existing stock item?
|
||||||
item_to_copy = self.request.GET.get('copy', None)
|
item_to_copy = self.request.GET.get('copy', None)
|
||||||
@ -144,15 +168,22 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
part_id = self.request.GET.get('part', None)
|
part_id = self.request.GET.get('part', None)
|
||||||
loc_id = self.request.GET.get('location', None)
|
loc_id = self.request.GET.get('location', None)
|
||||||
|
|
||||||
|
# Part field has been specified
|
||||||
if part_id:
|
if part_id:
|
||||||
part = get_object_or_404(Part, pk=part_id)
|
try:
|
||||||
if part:
|
part = Part.objects.get(pk=part_id)
|
||||||
initials['part'] = get_object_or_404(Part, pk=part_id)
|
initials['part'] = part
|
||||||
initials['location'] = part.default_location
|
initials['location'] = part.default_location
|
||||||
initials['supplier_part'] = part.default_supplier
|
initials['supplier_part'] = part.default_supplier
|
||||||
|
except Part.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Location has been specified
|
||||||
if loc_id:
|
if loc_id:
|
||||||
initials['location'] = get_object_or_404(StockLocation, pk=loc_id)
|
try:
|
||||||
|
initials['location'] = StockLocation.objects.get(pk=loc_id)
|
||||||
|
except StockLocation.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
@ -178,7 +209,7 @@ class StockItemDelete(AjaxDeleteView):
|
|||||||
|
|
||||||
model = StockItem
|
model = StockItem
|
||||||
success_url = '/stock/'
|
success_url = '/stock/'
|
||||||
template_name = 'stock/item_delete.html'
|
ajax_template_name = 'stock/item_delete.html'
|
||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
ajax_form_title = 'Delete Stock Item'
|
ajax_form_title = 'Delete Stock Item'
|
||||||
|
|
||||||
@ -190,7 +221,7 @@ class StockItemMove(AjaxUpdateView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
model = StockItem
|
model = StockItem
|
||||||
template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
context_object_name = 'item'
|
context_object_name = 'item'
|
||||||
ajax_form_title = 'Move Stock Item'
|
ajax_form_title = 'Move Stock Item'
|
||||||
form_class = MoveStockItemForm
|
form_class = MoveStockItemForm
|
||||||
|
5
Makefile
5
Makefile
@ -19,7 +19,8 @@ install:
|
|||||||
pip install -U -r requirements.txt
|
pip install -U -r requirements.txt
|
||||||
python InvenTree/keygen.py
|
python InvenTree/keygen.py
|
||||||
|
|
||||||
setup: install migrate
|
superuser:
|
||||||
|
python InvenTree/manage.py createsuperuser
|
||||||
|
|
||||||
style:
|
style:
|
||||||
flake8 InvenTree
|
flake8 InvenTree
|
||||||
@ -37,5 +38,3 @@ documentation:
|
|||||||
pip install -U -r docs/requirements.txt
|
pip install -U -r docs/requirements.txt
|
||||||
cd docs & make html
|
cd docs & make html
|
||||||
|
|
||||||
superuser:
|
|
||||||
python InvenTree/manage.py createsuperuser
|
|
||||||
|
55
README.md
55
README.md
@ -3,23 +3,54 @@
|
|||||||
# InvenTree
|
# InvenTree
|
||||||
InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a JSON API for interaction with external interfaces and applications.
|
InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a JSON API for interaction with external interfaces and applications.
|
||||||
|
|
||||||
## Installation
|
InvenTree is designed to be lightweight and easy to use for SME or hobbyist applications, where many existing stock management solutions are bloated and cumbersome to use. Updating stock is a single-action procses and does not require a complex system of work orders or stock transactions.
|
||||||
|
|
||||||
|
However, complex business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information.
|
||||||
|
|
||||||
|
## User Documentation
|
||||||
|
|
||||||
|
**TODO:** User documentation will be provided on a linked ```.github.io``` site, and will document the various InvenTree functionality
|
||||||
|
|
||||||
|
## Code Documentation
|
||||||
|
|
||||||
|
For project code documentation, refer to the online [documentation](http://inventree.readthedocs.io/en/latest/) (auto-generated)
|
||||||
|
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
It is recommended to set up a clean Python 3.4+ virtual environment first:
|
It is recommended to set up a clean Python 3.4+ virtual environment first:
|
||||||
`mkdir ~/.env && python3 -m venv ~/.env/InvenTree && source ~/.env/InvenTree/bin/activate`
|
`mkdir ~/.env && python3 -m venv ~/.env/InvenTree && source ~/.env/InvenTree/bin/activate`
|
||||||
|
|
||||||
You can then continue running `make setup` (which will be replaced by a proper setup.py soon). This will do the following:
|
A makefile is provided for project configuration:
|
||||||
|
|
||||||
1. Installs required Python dependencies (requires [pip](https://pypi.python.org/pypi/pip), should be part of your virtual environment by default)
|
### Install
|
||||||
1. Performs initial database setup
|
|
||||||
1. Updates database tables for all InvenTree components
|
|
||||||
|
|
||||||
This command can also be used to update the installation if changes have been made to the database configuration.
|
Run `make install` to ensure that all required pip packages are installed (see `requirements.txt`). This step will also generate a `SECRET_KEY.txt` file (unless one already exists) for Django authentication.
|
||||||
|
|
||||||
To create an initial user account, run the command `make superuser`.
|
### Migrate
|
||||||
|
|
||||||
## Documentation
|
Run `make migrate` to perform all pending database migrations to ensure the database schema is up to date.
|
||||||
For project code documentation, refer to the online [documentation](http://inventree.readthedocs.io/en/latest/) (auto-generated)
|
|
||||||
|
|
||||||
## Coding Style
|
**Note:** Run this step once after `make install` to create the initial empty database.
|
||||||
If you'd like to contribute, install our development dependencies using `make develop`.
|
|
||||||
All Python code should conform to the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide. Run `make style` which will compare all source (.py) files against the PEP 8 style. Tests can be run using `make test`.
|
### Superuser
|
||||||
|
|
||||||
|
Run `make superuser` to create an admin account for the database
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
Run `make test` to run all code tests
|
||||||
|
|
||||||
|
### Style
|
||||||
|
|
||||||
|
Run `make style` to check the codebase against PEP coding standards (uses Flake)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Any new functionality should be submitted with matching test cases (using the Django testing framework). Tests should at bare minimum ensure that any new Python code is covered by the integrated coverage testing. Tests can be run using `make test`.
|
||||||
|
|
||||||
|
### Coding Style
|
||||||
|
|
||||||
|
All Python code should conform to the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide. Run `make style` which will compare all source (.py) files against the PEP 8 style.
|
||||||
|
@ -7,6 +7,6 @@ This documentation is auto-generated from the `InvenTree codebase <https://githu
|
|||||||
:titlesonly:
|
:titlesonly:
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
:hidden:
|
|
||||||
|
|
||||||
InvenTree <introduction>
|
InvenTree Modules <introduction>
|
||||||
|
API Reference<reference>
|
@ -6,11 +6,21 @@ InvenTree is an open source inventory management system which provides powerful
|
|||||||
The core of the InvenTree software is a Python/Django database backend whi
|
The core of the InvenTree software is a Python/Django database backend whi
|
||||||
|
|
||||||
|
|
||||||
Django Apps
|
**Django Apps**
|
||||||
===========
|
|
||||||
|
|
||||||
The InvenTree Django ecosystem provides the following 'apps' for core functionality:
|
The InvenTree Django ecosystem provides the following 'apps' for core functionality:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:titlesonly:
|
||||||
|
:maxdepth: 1
|
||||||
|
:caption: App Modules:
|
||||||
|
|
||||||
|
docs/InvenTree/index
|
||||||
|
docs/build/index
|
||||||
|
docs/company/index
|
||||||
|
docs/part/index
|
||||||
|
docs/stock/index
|
||||||
|
|
||||||
* `InvenTree </docs/InvenTree/index.html>`_ - High level management functions
|
* `InvenTree </docs/InvenTree/index.html>`_ - High level management functions
|
||||||
* `Build </docs/build/index.html>`_ - Part build projects
|
* `Build </docs/build/index.html>`_ - Part build projects
|
||||||
* `Company </docs/company/index.html>`_ - Company management (suppliers / customers)
|
* `Company </docs/company/index.html>`_ - Company management (suppliers / customers)
|
||||||
|
7
docs/reference.rst
Normal file
7
docs/reference.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
API Reference Index
|
||||||
|
===================
|
||||||
|
|
||||||
|
The complete reference indexes are found below:
|
||||||
|
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`genindex`
|
7
docs/templates/layout.html
vendored
7
docs/templates/layout.html
vendored
@ -1,7 +0,0 @@
|
|||||||
{% extends "!layout.html" %}
|
|
||||||
|
|
||||||
{% block menu %}
|
|
||||||
{{ super() }}
|
|
||||||
<a href="/py-modindex.html">Module Index</a>
|
|
||||||
<a href="/genindex.html">Index</a>
|
|
||||||
{% endblock %}
|
|
Loading…
x
Reference in New Issue
Block a user