Merge pull request #118 from SchrodingersGat/travis-fix

Updated travis environment
This commit is contained in:
Oliver 2019-04-14 09:41:34 +10:00 committed by GitHub
commit a465e990b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 119 additions and 73 deletions

View File

@ -1,11 +1,18 @@
dist: xenial
language: python language: python
python: python:
- 3.5 - 3.5
- 3.6
addons:
apt-packages:
-sqlite3
before_install: before_install:
- make setup - make setup
- make setup_ci - make setup_ci
script: script:
# - make style
- make test - make test
- make style

View File

@ -11,4 +11,4 @@ class HelperForm(forms.ModelForm):
super(forms.ModelForm, self).__init__(*args, **kwargs) super(forms.ModelForm, self).__init__(*args, **kwargs)
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_tag = False self.helper.form_tag = False

View File

@ -23,4 +23,4 @@ class AuthRequiredMiddleware(object):
# Code to be executed for each request/response after # Code to be executed for each request/response after
# the view is called. # the view is called.
return response return response

View File

@ -3,10 +3,10 @@ from __future__ import unicode_literals
from rest_framework import serializers from rest_framework import serializers
from rest_framework import generics from rest_framework import generics
from rest_framework import mixins
from django.contrib.auth.models import User from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -33,4 +33,4 @@ class DraftRUDView(generics.RetrieveAPIView, generics.UpdateAPIView, generics.De
if ctx_data.get('_is_final', False) in [True, u'true', u'True', 1]: if ctx_data.get('_is_final', False) in [True, u'true', u'True', 1]:
super(generics.UpdateAPIView, self).perform_update(serializer) super(generics.UpdateAPIView, self).perform_update(serializer)
else: else:
pass pass

View File

@ -9,7 +9,6 @@ from django.views.generic import UpdateView, CreateView, DeleteView
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from rest_framework import views from rest_framework import views
from django.http import JsonResponse
class TreeSerializer(views.APIView): class TreeSerializer(views.APIView):
@ -159,9 +158,6 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
return response return response
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
response = super(UpdateView, self).get(request, *args, **kwargs)
if request.is_ajax(): if request.is_ajax():
form = self.form_class(instance=self.get_object()) form = self.form_class(instance=self.get_object())

View File

@ -16,6 +16,6 @@ class EditBuildForm(HelperForm):
'quantity', 'quantity',
'batch', 'batch',
'notes', 'notes',
# 'status', # 'status',
# 'completion_date', # 'completion_date',
] ]

View File

@ -7,11 +7,9 @@ from django.views.generic import DetailView, ListView
from part.models import Part from part.models import Part
from .models import Build from .models import Build
from .forms import EditBuildForm from .forms import EditBuildForm
from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView
from django.http import JsonResponse
class BuildIndex(ListView): class BuildIndex(ListView):

View File

@ -31,4 +31,4 @@ class CompanyImageForm(HelperForm):
model = Company model = Company
fields = [ fields = [
'image' 'image'
] ]

View File

@ -78,4 +78,4 @@ class Contact(models.Model):
role = models.CharField(max_length=100, blank=True) role = models.CharField(max_length=100, blank=True)
company = models.ForeignKey(Company, related_name='contacts', company = models.ForeignKey(Company, related_name='contacts',
on_delete = models.CASCADE) on_delete=models.CASCADE)

View File

@ -15,6 +15,7 @@ class CompanyBriefSerializer(serializers.ModelSerializer):
'name' 'name'
] ]
class CompanySerializer(serializers.ModelSerializer): class CompanySerializer(serializers.ModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters from rest_framework import filters
from rest_framework import generics, permissions, mixins from rest_framework import generics, permissions
from django.conf.urls import url, include from django.conf.urls import url, include
@ -17,6 +17,7 @@ from .serializers import CategorySerializer
from InvenTree.views import TreeSerializer from InvenTree.views import TreeSerializer
from InvenTree.serializers import DraftRUDView from InvenTree.serializers import DraftRUDView
class PartCategoryTree(TreeSerializer): class PartCategoryTree(TreeSerializer):
title = "Parts" title = "Parts"
@ -33,7 +34,7 @@ class CategoryList(generics.ListCreateAPIView):
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
#filters.SearchFilter, # filters.SearchFilter,
filters.OrderingFilter, filters.OrderingFilter,
] ]
@ -61,6 +62,7 @@ class PartDetail(DraftRUDView):
permissions.IsAuthenticatedOrReadOnly, permissions.IsAuthenticatedOrReadOnly,
] ]
class PartList(generics.ListCreateAPIView): class PartList(generics.ListCreateAPIView):
queryset = Part.objects.all() queryset = Part.objects.all()
@ -133,13 +135,12 @@ class SupplierPartList(generics.ListAPIView):
'supplier' 'supplier'
] ]
cat_api_urls = [
cat_api_urls = [
url(r'^$', CategoryList.as_view(), name='api-part-category-list'), url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
] ]
part_api_urls = [ part_api_urls = [
url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'), url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'),
url(r'^category/', include(cat_api_urls)), url(r'^category/', include(cat_api_urls)),

View File

@ -7,7 +7,6 @@ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Sum
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete
@ -250,7 +249,6 @@ class Part(models.Model):
self.allocated_build_count, self.allocated_build_count,
]) ])
@property @property
def stock_entries(self): def stock_entries(self):
return [loc for loc in self.locations.all() if loc.in_stock] return [loc for loc in self.locations.all() if loc.in_stock]
@ -263,7 +261,6 @@ class Part(models.Model):
return sum([loc.quantity for loc in self.stock_entries]) return sum([loc.quantity for loc in self.stock_entries])
@property @property
def has_bom(self): def has_bom(self):
return self.bom_count > 0 return self.bom_count > 0

View File

@ -5,6 +5,7 @@ from .models import SupplierPart
from company.serializers import CompanyBriefSerializer from company.serializers import CompanyBriefSerializer
class CategorySerializer(serializers.ModelSerializer): class CategorySerializer(serializers.ModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
@ -99,4 +100,4 @@ class SupplierPartSerializer(serializers.ModelSerializer):
'SKU', 'SKU',
'manufacturer', 'manufacturer',
'MPN', 'MPN',
] ]

View File

@ -1,11 +1,8 @@
from django_filters.rest_framework import FilterSet, DjangoFilterBackend from django_filters.rest_framework import FilterSet, DjangoFilterBackend
from django_filters import NumberFilter from django_filters import NumberFilter
from rest_framework import generics, permissions, response, filters
from django.conf.urls import url, include from django.conf.urls import url, include
# from InvenTree.models import FilterChildren
from .models import StockLocation, StockItem from .models import StockLocation, StockItem
from .models import StockItemTracking from .models import StockItemTracking
@ -19,8 +16,8 @@ from InvenTree.serializers import DraftRUDView
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import authentication, permissions from rest_framework import generics, response, filters, permissions
from django.contrib.auth.models import User
class StockCategoryTree(TreeSerializer): class StockCategoryTree(TreeSerializer):
title = 'Stock' title = 'Stock'
@ -69,26 +66,26 @@ class StockStocktake(APIView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if not 'action' in request.data: if 'action' not in request.data:
raise ValidationError({'action': 'Stocktake action must be provided'}) raise ValidationError({'action': 'Stocktake action must be provided'})
action = request.data['action'] action = request.data['action']
ACTIONS = ['stocktake', 'remove', 'add'] ACTIONS = ['stocktake', 'remove', 'add']
if not action in ACTIONS: if action not in ACTIONS:
raise ValidationError({'action': 'Action must be one of ' + ','.join(ACTIONS)}) raise ValidationError({'action': 'Action must be one of ' + ','.join(ACTIONS)})
if not 'items[]' in request.data: elif 'items[]' not in request.data:
raise ValidationError({'items[]:' 'Request must contain list of items'}) raise ValidationError({'items[]:' 'Request must contain list of items'})
items = [] items = []
# Ensure each entry is valid # Ensure each entry is valid
for entry in request.data['items[]']: for entry in request.data['items[]']:
if not 'pk' in entry: if 'pk' not in entry:
raise ValidationError({'pk': 'Each entry must contain pk field'}) raise ValidationError({'pk': 'Each entry must contain pk field'})
if not 'quantity' in entry: elif 'quantity' not in entry:
raise ValidationError({'quantity': 'Each entry must contain quantity field'}) raise ValidationError({'quantity': 'Each entry must contain quantity field'})
item = {} item = {}
@ -112,7 +109,6 @@ class StockStocktake(APIView):
if 'notes' in request.data: if 'notes' in request.data:
notes = request.data['notes'] notes = request.data['notes']
for item in items: for item in items:
quantity = int(item['quantity']) quantity = int(item['quantity'])
@ -136,7 +132,7 @@ class StockMove(APIView):
data = request.data data = request.data
if not u'location' in data: if u'location' not in data:
raise ValidationError({'location': 'Destination must be specified'}) raise ValidationError({'location': 'Destination must be specified'})
loc_id = data.get(u'location') loc_id = data.get(u'location')
@ -146,7 +142,7 @@ class StockMove(APIView):
except StockLocation.DoesNotExist: except StockLocation.DoesNotExist:
raise ValidationError({'location': 'Location does not exist'}) raise ValidationError({'location': 'Location does not exist'})
if not u'parts[]' in data: if u'parts[]' not in data:
raise ValidationError({'parts[]': 'Parts list must be specified'}) raise ValidationError({'parts[]': 'Parts list must be specified'})
part_list = data.get(u'parts[]') part_list = data.get(u'parts[]')
@ -160,7 +156,7 @@ class StockMove(APIView):
part = StockItem.objects.get(pk=pid) part = StockItem.objects.get(pk=pid)
parts.append(part) parts.append(part)
except StockItem.DoesNotExist: except StockItem.DoesNotExist:
errors.append({'part': 'Part {id} does not exist'.format(id=part_id)}) errors.append({'part': 'Part {id} does not exist'.format(id=pid)})
if len(errors) > 0: if len(errors) > 0:
raise ValidationError(errors) raise ValidationError(errors)
@ -227,7 +223,7 @@ class StockList(generics.ListCreateAPIView):
'supplier_part', 'supplier_part',
'customer', 'customer',
'belongs_to', 'belongs_to',
#'status', # 'status' TODO - There are some issues filtering based on an enumeration field
] ]
@ -250,7 +246,7 @@ class StockTrackingList(generics.ListCreateAPIView):
queryset = StockItemTracking.objects.all() queryset = StockItemTracking.objects.all()
serializer_class = StockTrackingSerializer serializer_class = StockTrackingSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly,] permission_classes = [permissions.IsAuthenticatedOrReadOnly]
filter_backends = [ filter_backends = [
DjangoFilterBackend, DjangoFilterBackend,
@ -275,7 +271,6 @@ class StockTrackingList(generics.ListCreateAPIView):
] ]
class LocationDetail(generics.RetrieveUpdateDestroyAPIView): class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
@ -303,9 +298,7 @@ location_endpoints = [
url(r'^$', LocationDetail.as_view(), name='stocklocation-detail'), url(r'^$', LocationDetail.as_view(), name='stocklocation-detail'),
] ]
stock_api_urls = [ stock_api_urls = [
url(r'location/?', StockLocationList.as_view(), name='api-location-list'), url(r'location/?', StockLocationList.as_view(), name='api-location-list'),
url(r'location/(?P<pk>\d+)/', include(location_endpoints)), url(r'location/(?P<pk>\d+)/', include(location_endpoints)),
@ -322,4 +315,4 @@ stock_api_urls = [
url(r'^(?P<pk>\d+)/', include(stock_endpoints)), url(r'^(?P<pk>\d+)/', include(stock_endpoints)),
url(r'^.*$', StockList.as_view(), name='api-stock-list'), url(r'^.*$', StockList.as_view(), name='api-stock-list'),
] ]

View File

@ -67,4 +67,4 @@ class EditStockItemForm(HelperForm):
'batch', 'batch',
'status', 'status',
'notes' 'notes'
] ]

View File

@ -69,12 +69,11 @@ class StockItem(models.Model):
if add_note: if add_note:
# This StockItem is being saved for the first time # This StockItem is being saved for the first time
self.add_transaction_note( self.add_transaction_note(
'Created stock item', 'Created stock item',
None, None,
system=True system=True
) )
def clean(self): def clean(self):
# The 'supplier_part' field must point to the same part! # The 'supplier_part' field must point to the same part!
@ -227,8 +226,10 @@ class StockItem(models.Model):
if location.pk == self.location.pk: if location.pk == self.location.pk:
return False # raise forms.ValidationError("Cannot move item to its current location") return False # raise forms.ValidationError("Cannot move item to its current location")
msg = "Moved to {loc} (from {src})".format(loc=location.name, msg = "Moved to {loc} (from {src})".format(
src=self.location.name) loc=location.name,
src=self.location.name
)
self.location = location self.location = location
self.save() self.save()
@ -240,7 +241,6 @@ class StockItem(models.Model):
return True return True
@transaction.atomic @transaction.atomic
def stocktake(self, count, user, notes=''): def stocktake(self, count, user, notes=''):
""" Perform item stocktake. """ Perform item stocktake.

View File

@ -4,9 +4,13 @@ from .models import StockItem, StockLocation
from .models import StockItemTracking from .models import StockItemTracking
from part.serializers import PartBriefSerializer from part.serializers import PartBriefSerializer
from InvenTree.serializers import UserSerializer, UserSerializerBrief from InvenTree.serializers import UserSerializerBrief
class LocationBriefSerializer(serializers.ModelSerializer): class LocationBriefSerializer(serializers.ModelSerializer):
"""
Provides a brief serializer for a StockLocation object
"""
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
@ -49,8 +53,12 @@ class StockTrackingSerializer(serializers.ModelSerializer):
class StockItemSerializer(serializers.ModelSerializer): class StockItemSerializer(serializers.ModelSerializer):
""" Serializer for a StockItem
""" """
Serializer for a StockItem
- Includes serialization for the linked part
- Includes serialization for the item location
"""
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
part = PartBriefSerializer(many=False, read_only=True) part = PartBriefSerializer(many=False, read_only=True)
@ -66,17 +74,11 @@ class StockItemSerializer(serializers.ModelSerializer):
'supplier_part', 'supplier_part',
'location', 'location',
'in_stock', 'in_stock',
#'belongs_to',
#'customer',
'quantity', 'quantity',
'serial', 'serial',
'batch', 'batch',
'status', 'status',
'notes', 'notes',
#'updated',
#'stocktake_date',
#'stocktake_user',
#'review_needed',
] ]
""" These fields are read-only in this context. """ These fields are read-only in this context.
@ -86,7 +88,8 @@ class StockItemSerializer(serializers.ModelSerializer):
'stocktake_date', 'stocktake_date',
'stocktake_user', 'stocktake_user',
'updated', 'updated',
#'quantity', 'quantity',
'in_stock'
] ]
@ -106,10 +109,10 @@ class LocationSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = StockLocation model = StockLocation
fields = [ fields = [
'pk', 'pk',
'url', 'url',
'name', 'name',
'description', 'description',
'parent', 'parent',
'pathstring' 'pathstring'
] ]

View File

@ -10,15 +10,17 @@ from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from part.models import Part from part.models import Part
from .models import StockItem, StockLocation from .models import StockItem, StockLocation
import datetime
from .forms import EditStockLocationForm from .forms import EditStockLocationForm
from .forms import CreateStockItemForm from .forms import CreateStockItemForm
from .forms import EditStockItemForm from .forms import EditStockItemForm
from .forms import MoveStockItemForm from .forms import MoveStockItemForm
from .forms import StocktakeForm from .forms import StocktakeForm
class StockIndex(ListView): class StockIndex(ListView):
"""
StockIndex view loads all StockLocation and StockItem object
"""
model = StockItem model = StockItem
template_name = 'stock/location.html' template_name = 'stock/location.html'
context_obect_name = 'locations' context_obect_name = 'locations'
@ -36,6 +38,10 @@ class StockIndex(ListView):
class StockLocationDetail(DetailView): class StockLocationDetail(DetailView):
"""
Detailed view of a single StockLocation object
"""
context_object_name = 'location' context_object_name = 'location'
template_name = 'stock/location.html' template_name = 'stock/location.html'
queryset = StockLocation.objects.all() queryset = StockLocation.objects.all()
@ -43,6 +49,10 @@ class StockLocationDetail(DetailView):
class StockItemDetail(DetailView): class StockItemDetail(DetailView):
"""
Detailed view of a single StockItem object
"""
context_object_name = 'item' context_object_name = 'item'
template_name = 'stock/item.html' template_name = 'stock/item.html'
queryset = StockItem.objects.all() queryset = StockItem.objects.all()
@ -50,6 +60,11 @@ class StockItemDetail(DetailView):
class StockLocationEdit(AjaxUpdateView): class StockLocationEdit(AjaxUpdateView):
"""
View for editing details of a StockLocation.
This view is used with the EditStockLocationForm to deliver a modal form to the web view
"""
model = StockLocation model = StockLocation
form_class = EditStockLocationForm form_class = EditStockLocationForm
template_name = 'stock/location_edit.html' template_name = 'stock/location_edit.html'
@ -59,6 +74,10 @@ class StockLocationEdit(AjaxUpdateView):
class StockItemEdit(AjaxUpdateView): class StockItemEdit(AjaxUpdateView):
"""
View for editing details of a single StockItem
"""
model = StockItem model = StockItem
form_class = EditStockItemForm form_class = EditStockItemForm
template_name = 'stock/item_edit.html' template_name = 'stock/item_edit.html'
@ -68,6 +87,11 @@ class StockItemEdit(AjaxUpdateView):
class StockLocationCreate(AjaxCreateView): class StockLocationCreate(AjaxCreateView):
"""
View for creating a new StockLocation
A parent location (another StockLocation object) can be passed as a query parameter
"""
model = StockLocation model = StockLocation
form_class = EditStockLocationForm form_class = EditStockLocationForm
template_name = 'stock/location_create.html' template_name = 'stock/location_create.html'
@ -87,6 +111,13 @@ class StockLocationCreate(AjaxCreateView):
class StockItemCreate(AjaxCreateView): class StockItemCreate(AjaxCreateView):
"""
View for creating a new StockItem
Parameters can be pre-filled by passing query items:
- part: The part of which the new StockItem is an instance
- location: The location of the new StockItem
"""
model = StockItem model = StockItem
form_class = CreateStockItemForm form_class = CreateStockItemForm
template_name = 'stock/item_create.html' template_name = 'stock/item_create.html'
@ -114,6 +145,11 @@ class StockItemCreate(AjaxCreateView):
class StockLocationDelete(AjaxDeleteView): class StockLocationDelete(AjaxDeleteView):
"""
View to delete a StockLocation
Presents a deletion confirmation form to the user
"""
model = StockLocation model = StockLocation
success_url = '/stock' success_url = '/stock'
template_name = 'stock/location_delete.html' template_name = 'stock/location_delete.html'
@ -122,6 +158,11 @@ class StockLocationDelete(AjaxDeleteView):
class StockItemDelete(AjaxDeleteView): class StockItemDelete(AjaxDeleteView):
"""
View to delete a StockItem
Presents a deletion confirmation form to the user
"""
model = StockItem model = StockItem
success_url = '/stock/' success_url = '/stock/'
template_name = 'stock/item_delete.html' template_name = 'stock/item_delete.html'
@ -130,6 +171,11 @@ class StockItemDelete(AjaxDeleteView):
class StockItemMove(AjaxUpdateView): class StockItemMove(AjaxUpdateView):
"""
View to move a StockItem from one location to another
Performs some data validation to prevent illogical stock moves
"""
model = StockItem model = StockItem
template_name = 'modal_form.html' template_name = 'modal_form.html'
context_object_name = 'item' context_object_name = 'item'
@ -156,7 +202,6 @@ class StockItemMove(AjaxUpdateView):
form.errors['location'] = ['Cannot move to an empty location'] form.errors['location'] = ['Cannot move to an empty location']
except StockLocation.DoesNotExist: except StockLocation.DoesNotExist:
loc_path = ''
form.errors['location'] = ['Location does not exist'] form.errors['location'] = ['Location does not exist']
data = { data = {
@ -167,6 +212,11 @@ class StockItemMove(AjaxUpdateView):
class StockItemStocktake(AjaxUpdateView): class StockItemStocktake(AjaxUpdateView):
"""
View to perform stocktake on a single StockItem
Updates the quantity, which will also create a new StockItemTracking item
"""
model = StockItem model = StockItem
template_name = 'modal_form.html' template_name = 'modal_form.html'
context_object_name = 'item' context_object_name = 'item'
@ -188,5 +238,3 @@ class StockItemStocktake(AjaxUpdateView):
} }
return self.renderJsonResponse(request, form, data) return self.renderJsonResponse(request, form, data)

View File

@ -8,7 +8,7 @@ clean:
rm -f .coverage rm -f .coverage
style: style:
flake8 InvenTree flake8 InvenTree --ignore=C901,E501
test: test:
python InvenTree/manage.py check python InvenTree/manage.py check

View File

@ -1,4 +1,5 @@
Django==2.2 Django==2.2
psycopg2>=2.8.1
pillow>=5.0.0 pillow>=5.0.0
djangorestframework>=3.6.2 djangorestframework>=3.6.2
django_filter>=1.0.2 django_filter>=1.0.2