Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-04-28 01:18:32 +10:00
commit c0e69d7a99
42 changed files with 713 additions and 140 deletions

2
.gitignore vendored
View File

@ -29,8 +29,6 @@ local_settings.py
# Sphinx files # Sphinx files
docs/_build docs/_build
docs/_static
docs/_templates
# Local media storage (only when running in development mode) # Local media storage (only when running in development mode)
InvenTree/media InvenTree/media

View File

@ -0,0 +1,5 @@
"""
The InvenTree module provides high-level management and functionality.
It provides a number of helper functions and generic classes which are used by InvenTree apps.
"""

View File

@ -1,3 +1,7 @@
"""
Helper forms which subclass Django forms to provide additional functionality
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -6,6 +10,7 @@ from crispy_forms.helper import FormHelper
class HelperForm(forms.ModelForm): class HelperForm(forms.ModelForm):
""" Provides simple integration of crispy_forms extension. """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(forms.ModelForm, self).__init__(*args, **kwargs) super(forms.ModelForm, self).__init__(*args, **kwargs)

View File

@ -1,3 +1,7 @@
"""
Provides helper functions used throughout the InvenTree project
"""
import io import io
from wsgiref.util import FileWrapper from wsgiref.util import FileWrapper
@ -5,7 +9,14 @@ from django.http import StreamingHttpResponse
def str2bool(text, test=True): def str2bool(text, test=True):
""" Test if a string 'looks' like a boolean value """ Test if a string 'looks' like a boolean value.
Args:
text: Input text
test (default = True): Set which boolean value to look for
Returns:
True if the text looks like the selected boolean value
""" """
if test: if test:
return str(text).lower() in ['1', 'y', 'yes', 't', 'true', 'ok', ] return str(text).lower() in ['1', 'y', 'yes', 't', 'true', 'ok', ]
@ -13,21 +24,36 @@ def str2bool(text, test=True):
return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', ] return str(text).lower() in ['0', 'n', 'no', 'none', 'f', 'false', ]
def WrapWithQuotes(text): def WrapWithQuotes(text, quote='"'):
# TODO - Make this better """ Wrap the supplied text with quotes
if not text.startswith('"'):
text = '"' + text
if not text.endswith('"'): Args:
text = text + '"' text: Input text to wrap
quote: Quote character to use for wrapping (default = "")
Returns:
Supplied text wrapped in quote char
"""
if not text.startswith(quote):
text = quote + text
if not text.endswith(quote):
text = text + quote
return text return text
def DownloadFile(data, filename, content_type='application/text'): def DownloadFile(data, filename, content_type='application/text'):
""" """ Create a dynamic file for the user to download.
Create a dynamic file for the user to download.
@param data is the raw file data Args:
data: Raw file data (string or bytes)
filename: Filename for the file download
content_type: Content type for the download
Return:
A StreamingHttpResponse object wrapping the supplied data
""" """
filename = WrapWithQuotes(filename) filename = WrapWithQuotes(filename)

View File

@ -1,9 +1,12 @@
"""
Generic models which provide extra functionality over base Django model types.
"""
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models from django.db import models
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from .helpers import str2bool
from django.db.models.signals import pre_delete from django.db.models.signals import pre_delete
from django.dispatch import receiver from django.dispatch import receiver
@ -11,6 +14,7 @@ from django.dispatch import receiver
class InvenTreeTree(models.Model): class InvenTreeTree(models.Model):
""" Provides an abstracted self-referencing tree model for data categories. """ Provides an abstracted self-referencing tree model for data categories.
- Each Category has one parent Category, which can be blank (for a top-level Category). - Each Category has one parent Category, which can be blank (for a top-level Category).
- Each Category can have zero-or-more child Categor(y/ies) - Each Category can have zero-or-more child Categor(y/ies)
""" """
@ -69,10 +73,12 @@ class InvenTreeTree(models.Model):
@property @property
def has_children(self): def has_children(self):
""" True if there are any children under this item """
return self.children.count() > 0 return self.children.count() > 0
@property @property
def children(self): def children(self):
""" Return the children of this item """
contents = ContentType.objects.get_for_model(type(self)) contents = ContentType.objects.get_for_model(type(self))
childs = contents.get_all_objects_for_this_type(parent=self.id) childs = contents.get_all_objects_for_this_type(parent=self.id)
@ -100,11 +106,10 @@ class InvenTreeTree(models.Model):
@property @property
def parentpath(self): def parentpath(self):
""" Return the parent path of this category """ Get the parent path of this category
Todo: Returns:
This function is recursive and expensive. List of category names from the top level to the parent of this category
It should be reworked such that only a single db call is required
""" """
if self.parent: if self.parent:
@ -114,10 +119,21 @@ class InvenTreeTree(models.Model):
@property @property
def path(self): def path(self):
""" Get the complete part of this category.
e.g. ["Top", "Second", "Third", "This"]
Returns:
List of category names from the top level to this category
"""
return self.parentpath + [self] return self.parentpath + [self]
@property @property
def pathstring(self): def pathstring(self):
""" Get a string representation for the path of this item.
e.g. "Top/Second/Third/This"
"""
return '/'.join([item.name for item in self.path]) return '/'.join([item.name for item in self.path])
def __setattr__(self, attrname, val): def __setattr__(self, attrname, val):
@ -157,38 +173,19 @@ class InvenTreeTree(models.Model):
super(InvenTreeTree, self).__setattr__(attrname, val) super(InvenTreeTree, self).__setattr__(attrname, val)
def __str__(self): def __str__(self):
""" String representation of a category is the full path to that category """ String representation of a category is the full path to that category """
Todo:
This is recursive - Make it not so.
"""
return self.pathstring return self.pathstring
@receiver(pre_delete, sender=InvenTreeTree, dispatch_uid='tree_pre_delete_log') @receiver(pre_delete, sender=InvenTreeTree, dispatch_uid='tree_pre_delete_log')
def before_delete_tree_item(sender, instance, using, **kwargs): def before_delete_tree_item(sender, instance, using, **kwargs):
""" Receives pre_delete signal from InvenTreeTree object.
Before an item is deleted, update each child object to point to the parent of the object being deleted.
"""
# Update each tree item below this one # Update each tree item below this one
for child in instance.children.all(): for child in instance.children.all():
child.parent = instance.parent child.parent = instance.parent
child.save() child.save()
def FilterChildren(queryset, parent):
""" Filter a queryset, limit to only objects that are a child of the given parent
Filter is passed in the URL string, e.g. '/?parent=123'
To accommodate for items without a parent, top-level items can be specified as:
none / false / null / top / 0
"""
if not parent:
return queryset
elif str2bool(parent, False):
return queryset.filter(parent=None)
else:
parent_id = int(parent)
if parent_id == 0:
return queryset.filter(parent=None)
else:
return queryset.filter(parent=parent_id)

View File

@ -1,3 +1,8 @@
"""
Serializers used in various InvenTree apps
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -7,6 +12,7 @@ from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
""" Serializer for User - provides all fields """
class Meta: class Meta:
model = User model = User
@ -14,6 +20,7 @@ class UserSerializer(serializers.ModelSerializer):
class UserSerializerBrief(serializers.ModelSerializer): class UserSerializerBrief(serializers.ModelSerializer):
""" Serializer for User - provides limited information """
class Meta: class Meta:
model = User model = User

View File

@ -1,3 +1,10 @@
"""
Top-level URL lookup for InvenTree application.
Passes URL lookup downstream to each app as required.
"""
from django.conf.urls import url, include from django.conf.urls import url, include
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views

View File

@ -1,3 +1,10 @@
"""
Various Views which provide extra functionality over base Django Views.
In particular these views provide base functionality for rendering Django forms
as JSON objects and passing them to modal forms (using jQuery / bootstrap).
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -12,6 +19,8 @@ from rest_framework import views
class TreeSerializer(views.APIView): class TreeSerializer(views.APIView):
""" JSON View for serializing a Tree object.
"""
def itemToJson(self, item): def itemToJson(self, item):
@ -52,20 +61,34 @@ class TreeSerializer(views.APIView):
class AjaxMixin(object): class AjaxMixin(object):
""" AjaxMixin provides basic functionality for rendering a Django form to JSON.
Handles jsonResponse rendering, and adds extra data for the modal forms to process
on the client side.
"""
ajax_form_action = '' ajax_form_action = ''
ajax_form_title = '' ajax_form_title = ''
def get_data(self): def get_data(self):
""" Get extra context data (default implementation is empty dict)
Returns:
dict object (empty)
"""
return {} return {}
def getAjaxTemplate(self):
if hasattr(self, 'ajax_template_name'):
return self.ajax_template_name
else:
return self.template_name
def renderJsonResponse(self, request, form=None, data={}, context={}): def renderJsonResponse(self, request, form=None, data={}, context={}):
""" Render a JSON response based on specific class context.
Args:
request: HTTP request object (e.g. GET / POST)
form: Django form object (may be None)
data: Extra JSON data to pass to client
context: Extra context data to pass to template rendering
Returns:
JSON response object
"""
if form: if form:
context['form'] = form context['form'] = form
@ -73,7 +96,7 @@ class AjaxMixin(object):
data['title'] = self.ajax_form_title data['title'] = self.ajax_form_title
data['html_form'] = render_to_string( data['html_form'] = render_to_string(
self.getAjaxTemplate(), self.ajax_template_name,
context, context,
request=request request=request
) )
@ -88,7 +111,8 @@ class AjaxMixin(object):
class AjaxView(AjaxMixin, View): class AjaxView(AjaxMixin, View):
""" Bare-bones AjaxView """ """ An 'AJAXified' View for displaying an object
"""
# By default, point to the modal_form template # By default, point to the modal_form template
# (this can be overridden by a child class) # (this can be overridden by a child class)
@ -201,7 +225,7 @@ class AjaxDeleteView(AjaxMixin, DeleteView):
data = {'id': self.get_object().id, data = {'id': self.get_object().id,
'delete': False, 'delete': False,
'title': self.ajax_form_title, 'title': self.ajax_form_title,
'html_data': render_to_string(self.getAjaxTemplate(), 'html_data': render_to_string(self.ajax_template_name,
self.get_context_data(), self.get_context_data(),
request=request) request=request)
} }
@ -229,15 +253,24 @@ class AjaxDeleteView(AjaxMixin, DeleteView):
class IndexView(TemplateView): class IndexView(TemplateView):
""" View for InvenTree index page """
template_name = 'InvenTree/index.html' template_name = 'InvenTree/index.html'
class SearchView(TemplateView): class SearchView(TemplateView):
""" View for InvenTree search page.
Displays results of search query
"""
template_name = 'InvenTree/search.html' template_name = 'InvenTree/search.html'
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" Handle POST request (which contains search query).
Pass the search query to the page template
"""
context = self.get_context_data() context = self.get_context_data()

View File

@ -0,0 +1,5 @@
"""
The Build module is responsible for managing "Build" transactions.
A Build consumes parts from stock to create new parts
"""

View File

@ -1,3 +1,7 @@
"""
JSON API for the Build app
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -11,7 +15,12 @@ from .models import Build
from .serializers import BuildSerializer from .serializers import BuildSerializer
class BuildList(generics.ListAPIView): class BuildList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of Build objects.
- GET: Return list of objects (with filters)
- POST: Create a new Build object
"""
queryset = Build.objects.all() queryset = Build.objects.all()
serializer_class = BuildSerializer serializer_class = BuildSerializer

View File

@ -1,3 +1,7 @@
"""
Django Forms for interacting with Build objects
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -7,6 +11,8 @@ from .models import Build
class EditBuildForm(HelperForm): class EditBuildForm(HelperForm):
""" Form for editing a Build object.
"""
class Meta: class Meta:
model = Build model = Build

View File

@ -1,3 +1,7 @@
"""
Build database model definitions
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -9,60 +13,60 @@ from django.core.validators import MinValueValidator
class Build(models.Model): class Build(models.Model):
""" A Build object organises the creation of new parts from the component parts """ A Build object organises the creation of new parts from the component parts.
It uses the part BOM to generate new parts.
Parts are then taken from stock
""" """
def get_absolute_url(self): def get_absolute_url(self):
return reverse('build-detail', kwargs={'pk': self.id}) return reverse('build-detail', kwargs={'pk': self.id})
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='builds',
limit_choices_to={'buildable': True},
)
""" A reference to the part being built - only parts marked as 'buildable' may be selected """
#: Brief title describing the build
title = models.CharField(max_length=100, help_text='Brief description of the build')
#: Number of output parts to build
quantity = models.PositiveIntegerField(default=1,
validators=[MinValueValidator(1)],
help_text='Number of parts to build')
# Build status codes # Build status codes
PENDING = 10 # Build is pending / active PENDING = 10 # Build is pending / active
HOLDING = 20 # Build is currently being held HOLDING = 20 # Build is currently being held
CANCELLED = 30 # Build was cancelled CANCELLED = 30 # Build was cancelled
COMPLETE = 40 # Build is complete COMPLETE = 40 # Build is complete
#: Build status codes
BUILD_STATUS_CODES = {PENDING: _("Pending"), BUILD_STATUS_CODES = {PENDING: _("Pending"),
HOLDING: _("Holding"), HOLDING: _("Holding"),
CANCELLED: _("Cancelled"), CANCELLED: _("Cancelled"),
COMPLETE: _("Complete"), COMPLETE: _("Complete"),
} }
batch = models.CharField(max_length=100, blank=True, null=True, #: Status of the build (ref BUILD_STATUS_CODES)
help_text='Batch code for this build output')
# Status of the build
status = models.PositiveIntegerField(default=PENDING, status = models.PositiveIntegerField(default=PENDING,
choices=BUILD_STATUS_CODES.items(), choices=BUILD_STATUS_CODES.items(),
validators=[MinValueValidator(0)]) validators=[MinValueValidator(0)])
# Date the build model was 'created' #: Batch number for the build (optional)
batch = models.CharField(max_length=100, blank=True, null=True,
help_text='Batch code for this build output')
#: Date the build model was 'created'
creation_date = models.DateField(auto_now=True, editable=False) creation_date = models.DateField(auto_now=True, editable=False)
# Date the build was 'completed' #: Date the build was 'completed' (and parts removed from stock)
completion_date = models.DateField(null=True, blank=True) completion_date = models.DateField(null=True, blank=True)
# Brief build title #: Notes attached to each build output
title = models.CharField(max_length=100, help_text='Brief description of the build')
# A reference to the part being built
# Only 'buildable' parts can be selected
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='builds',
limit_choices_to={'buildable': True},
)
# How many parts to build?
quantity = models.PositiveIntegerField(default=1,
validators=[MinValueValidator(1)],
help_text='Number of parts to build')
# Notes can be attached to each build output
notes = models.TextField(blank=True) notes = models.TextField(blank=True)
@property @property
def required_parts(self): def required_parts(self):
""" Returns a dict of parts required to build this part (BOM) """
parts = [] parts = []
for item in self.part.bom_items.all(): for item in self.part.bom_items.all():
@ -77,8 +81,7 @@ class Build(models.Model):
@property @property
def can_build(self): def can_build(self):
""" Return true if there are enough parts to supply build """ Return true if there are enough parts to supply build """
"""
for item in self.required_parts: for item in self.required_parts:
if item['part'].total_stock < item['quantity']: if item['part'].total_stock < item['quantity']:
@ -88,10 +91,10 @@ class Build(models.Model):
@property @property
def is_active(self): def is_active(self):
""" Is this build active? """ Is this build active? An active build is either:
An active build is either:
- Pending - PENDING
- Holding - HOLDING
""" """
return self.status in [ return self.status in [
@ -101,4 +104,5 @@ class Build(models.Model):
@property @property
def is_complete(self): def is_complete(self):
""" Returns True if the build status is COMPLETE """
return self.status == self.COMPLETE return self.status == self.COMPLETE

View File

@ -1,3 +1,7 @@
"""
JSON serializers for Build API
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -7,6 +11,7 @@ from .models import Build
class BuildSerializer(serializers.ModelSerializer): class BuildSerializer(serializers.ModelSerializer):
""" Serializes a Build object """
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
status_text = serializers.CharField(source='get_status_display', read_only=True) status_text = serializers.CharField(source='get_status_display', read_only=True)

View File

@ -1,3 +1,7 @@
"""
URL lookup for Build app
"""
from django.conf.urls import url, include from django.conf.urls import url, include
from . import views from . import views

View File

@ -1,3 +1,7 @@
"""
Django views for interacting with Build objects
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -13,11 +17,14 @@ from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView
class BuildIndex(ListView): class BuildIndex(ListView):
""" View for displaying list of Builds
"""
model = Build model = Build
template_name = 'build/index.html' template_name = 'build/index.html'
context_object_name = 'builds' context_object_name = 'builds'
def get_queryset(self): def get_queryset(self):
""" Return all Build objects (order by date, newest first) """
return Build.objects.order_by('status', '-completion_date') return Build.objects.order_by('status', '-completion_date')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -35,6 +42,9 @@ class BuildIndex(ListView):
class BuildCancel(AjaxView): class BuildCancel(AjaxView):
""" View to cancel a Build.
Provides a cancellation information dialog
"""
model = Build model = Build
template_name = 'build/cancel.html' template_name = 'build/cancel.html'
ajax_form_title = 'Cancel Build' ajax_form_title = 'Cancel Build'
@ -42,6 +52,7 @@ class BuildCancel(AjaxView):
fields = [] fields = []
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" Handle POST request. Mark the build status as CANCELLED """
build = get_object_or_404(Build, pk=self.kwargs['pk']) build = get_object_or_404(Build, pk=self.kwargs['pk'])
@ -51,24 +62,28 @@ class BuildCancel(AjaxView):
return self.renderJsonResponse(request, None) return self.renderJsonResponse(request, None)
def get_data(self): def get_data(self):
""" Provide JSON context data. """
return { return {
'info': 'Build was cancelled' 'info': 'Build was cancelled'
} }
class BuildDetail(DetailView): class BuildDetail(DetailView):
""" Detail view of a single Build object. """
model = Build model = Build
template_name = 'build/detail.html' template_name = 'build/detail.html'
context_object_name = 'build' context_object_name = 'build'
class BuildAllocate(DetailView): class BuildAllocate(DetailView):
""" View for allocating parts to a Build """
model = Build model = Build
context_object_name = 'build' context_object_name = 'build'
template_name = 'build/allocate.html' template_name = 'build/allocate.html'
class BuildCreate(AjaxCreateView): class BuildCreate(AjaxCreateView):
""" View to create a new Build object """
model = Build model = Build
context_object_name = 'build' context_object_name = 'build'
form_class = EditBuildForm form_class = EditBuildForm
@ -76,6 +91,11 @@ class BuildCreate(AjaxCreateView):
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
def get_initial(self): def get_initial(self):
""" Get initial parameters for Build creation.
If 'part' is specified in the GET query, initialize the Build with the specified Part
"""
initials = super(BuildCreate, self).get_initial().copy() initials = super(BuildCreate, self).get_initial().copy()
part_id = self.request.GET.get('part', None) part_id = self.request.GET.get('part', None)
@ -92,6 +112,8 @@ class BuildCreate(AjaxCreateView):
class BuildUpdate(AjaxUpdateView): class BuildUpdate(AjaxUpdateView):
""" View for editing a Build object """
model = Build model = Build
form_class = EditBuildForm form_class = EditBuildForm
context_object_name = 'build' context_object_name = 'build'

View File

@ -0,0 +1,8 @@
"""
The Company module is responsible for managing Company interactions.
A company can be either (or both):
- Supplier
- Customer
"""

View File

@ -1,3 +1,7 @@
"""
Provides a JSON API for the Company app
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -12,6 +16,13 @@ from .serializers import CompanySerializer
class CompanyList(generics.ListCreateAPIView): class CompanyList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of Company objects
Provides two methods:
- GET: Return list of objects
- POST: Create a new Company object
"""
serializer_class = CompanySerializer serializer_class = CompanySerializer
queryset = Company.objects.all() queryset = Company.objects.all()
@ -44,6 +55,7 @@ class CompanyList(generics.ListCreateAPIView):
class CompanyDetail(generics.RetrieveUpdateDestroyAPIView): class CompanyDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail of a single Company object """
queryset = Company.objects.all() queryset = Company.objects.all()
serializer_class = CompanySerializer serializer_class = CompanySerializer

View File

@ -1,3 +1,7 @@
"""
Django Forms for interacting with Company app
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -7,6 +11,7 @@ from .models import Company
class EditCompanyForm(HelperForm): class EditCompanyForm(HelperForm):
""" Form for editing a Company object """
class Meta: class Meta:
model = Company model = Company
@ -26,6 +31,7 @@ class EditCompanyForm(HelperForm):
class CompanyImageForm(HelperForm): class CompanyImageForm(HelperForm):
""" Form for uploading a Company image """
class Meta: class Meta:
model = Company model = Company

View File

@ -1,3 +1,7 @@
"""
Company database model definitions
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -8,6 +12,16 @@ from django.urls import reverse
def rename_company_image(instance, filename): def rename_company_image(instance, filename):
""" Function to rename a company image after upload
Args:
instance: Company object
filename: uploaded image filename
Returns:
New image filename
"""
base = 'company_images' base = 'company_images'
if filename.count('.') > 0: if filename.count('.') > 0:
@ -24,6 +38,9 @@ def rename_company_image(instance, filename):
class Company(models.Model): class Company(models.Model):
""" A Company object represents an external company.
It may be a supplier or a customer (or both).
"""
name = models.CharField(max_length=100, unique=True, name = models.CharField(max_length=100, unique=True,
help_text='Company name') help_text='Company name')
@ -54,21 +71,28 @@ class Company(models.Model):
is_supplier = models.BooleanField(default=True) is_supplier = models.BooleanField(default=True)
def __str__(self): def __str__(self):
""" Get string representation of a Company """
return "{n} - {d}".format(n=self.name, d=self.description) return "{n} - {d}".format(n=self.name, d=self.description)
def get_absolute_url(self): def get_absolute_url(self):
""" Get the web URL for the detail view for this Company """
return reverse('company-detail', kwargs={'pk': self.id}) return reverse('company-detail', kwargs={'pk': self.id})
@property @property
def part_count(self): def part_count(self):
""" The number of parts supplied by this company """
return self.parts.count() return self.parts.count()
@property @property
def has_parts(self): def has_parts(self):
""" Return True if this company supplies any parts """
return self.part_count > 0 return self.part_count > 0
class Contact(models.Model): class Contact(models.Model):
""" A Contact represents a person who works at a particular company.
A Company may have zero or more associated Contact objects
"""
name = models.CharField(max_length=100) name = models.CharField(max_length=100)

View File

@ -1,9 +1,14 @@
"""
JSON serializers for Company app
"""
from rest_framework import serializers from rest_framework import serializers
from .models import Company from .models import Company
class CompanyBriefSerializer(serializers.ModelSerializer): class CompanyBriefSerializer(serializers.ModelSerializer):
""" Serializer for Company object (limited detail) """
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
@ -17,6 +22,7 @@ class CompanyBriefSerializer(serializers.ModelSerializer):
class CompanySerializer(serializers.ModelSerializer): class CompanySerializer(serializers.ModelSerializer):
""" Serializer for Company object (full detail) """
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)

View File

@ -1,3 +1,8 @@
"""
URL lookup for Company app
"""
from django.conf.urls import url, include from django.conf.urls import url, include
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView

View File

@ -1,3 +1,8 @@
"""
Django views for interacting with Company app
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -12,12 +17,20 @@ from .forms import CompanyImageForm
class CompanyIndex(ListView): class CompanyIndex(ListView):
""" View for displaying list of companies
"""
model = Company model = Company
template_name = 'company/index.html' template_name = 'company/index.html'
context_object_name = 'companies' context_object_name = 'companies'
paginate_by = 50 paginate_by = 50
def get_queryset(self): def get_queryset(self):
""" Retrieve the Company queryset based on HTTP request parameters.
- supplier: Filter by supplier
- customer: Filter by customer
"""
queryset = Company.objects.all().order_by('name') queryset = Company.objects.all().order_by('name')
if self.request.GET.get('supplier', None): if self.request.GET.get('supplier', None):
@ -30,6 +43,7 @@ class CompanyIndex(ListView):
class CompanyDetail(DetailView): class CompanyDetail(DetailView):
""" Detail view for Company object """
context_obect_name = 'company' context_obect_name = 'company'
template_name = 'company/detail.html' template_name = 'company/detail.html'
queryset = Company.objects.all() queryset = Company.objects.all()
@ -37,6 +51,7 @@ class CompanyDetail(DetailView):
class CompanyImage(AjaxUpdateView): class CompanyImage(AjaxUpdateView):
""" View for uploading an image for the Company """
model = Company model = Company
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
ajax_form_title = 'Update Company Image' ajax_form_title = 'Update Company Image'
@ -49,6 +64,7 @@ class CompanyImage(AjaxUpdateView):
class CompanyEdit(AjaxUpdateView): class CompanyEdit(AjaxUpdateView):
""" View for editing a Company object """
model = Company model = Company
form_class = EditCompanyForm form_class = EditCompanyForm
context_object_name = 'company' context_object_name = 'company'
@ -62,6 +78,7 @@ class CompanyEdit(AjaxUpdateView):
class CompanyCreate(AjaxCreateView): class CompanyCreate(AjaxCreateView):
""" View for creating a new Company object """
model = Company model = Company
context_object_name = 'company' context_object_name = 'company'
form_class = EditCompanyForm form_class = EditCompanyForm
@ -75,6 +92,7 @@ class CompanyCreate(AjaxCreateView):
class CompanyDelete(AjaxDeleteView): class CompanyDelete(AjaxDeleteView):
""" View for deleting a Company object """
model = Company model = Company
success_url = '/company/' success_url = '/company/'
ajax_template_name = 'company/delete.html' ajax_template_name = 'company/delete.html'

View File

@ -1,7 +1,5 @@
""" """
Module keygen Generates a Django SECRET_KEY file to be used by manage.py
=============
This module generates a Django SECRET_KEY file to be used by manage.py
""" """
import random import random
@ -15,11 +13,13 @@ KEY_DIR = os.path.dirname(os.path.realpath(__file__))
def generate_key(length=50): def generate_key(length=50):
""" """ Generate a random string
Generate a random string
:param length: Number of characters in returned string (default=50) Args:
:returns: Randomized secret key string length: Number of characters in returned string (default = 50)
Returns:
Randomized secret key string
""" """
options = string.digits + string.ascii_letters + string.punctuation options = string.digits + string.ascii_letters + string.punctuation

View File

@ -0,0 +1,10 @@
"""
The Part module is responsible for Part management.
It includes models for:
- PartCategory
- Part
- SupplierPart
- BomItem
"""

View File

@ -1,3 +1,7 @@
"""
Provides a JSON API for the Part app
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -26,6 +30,12 @@ class PartCategoryTree(TreeSerializer):
class CategoryList(generics.ListCreateAPIView): class CategoryList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartCategory objects.
- GET: Return a list of PartCategory objects
- POST: Create a new PartCategory object
"""
queryset = PartCategory.objects.all() queryset = PartCategory.objects.all()
serializer_class = CategorySerializer serializer_class = CategorySerializer
@ -56,11 +66,13 @@ class CategoryList(generics.ListCreateAPIView):
class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single PartCategory object """
serializer_class = CategorySerializer serializer_class = CategorySerializer
queryset = PartCategory.objects.all() queryset = PartCategory.objects.all()
class PartDetail(generics.RetrieveUpdateDestroyAPIView): class PartDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single Part object """
queryset = Part.objects.all() queryset = Part.objects.all()
serializer_class = PartSerializer serializer_class = PartSerializer
@ -70,6 +82,11 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
class PartList(generics.ListCreateAPIView): class PartList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of Part objects
- GET: Return list of objects
- POST: Create a new Part object
"""
serializer_class = PartSerializer serializer_class = PartSerializer
@ -130,6 +147,11 @@ class PartList(generics.ListCreateAPIView):
class BomList(generics.ListCreateAPIView): class BomList(generics.ListCreateAPIView):
""" API endpoing for accessing a list of BomItem objects
- GET: Return list of BomItem objects
- POST: Create a new BomItem object
"""
queryset = BomItem.objects.all() queryset = BomItem.objects.all()
serializer_class = BomItemSerializer serializer_class = BomItemSerializer
@ -151,6 +173,7 @@ class BomList(generics.ListCreateAPIView):
class BomDetail(generics.RetrieveUpdateDestroyAPIView): class BomDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single BomItem object """
queryset = BomItem.objects.all() queryset = BomItem.objects.all()
serializer_class = BomItemSerializer serializer_class = BomItemSerializer
@ -161,6 +184,11 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView):
class SupplierPartList(generics.ListCreateAPIView): class SupplierPartList(generics.ListCreateAPIView):
""" API endpoint for list view of SupplierPart object
- GET: Return list of SupplierPart objects
- POST: Create a new SupplierPart object
"""
queryset = SupplierPart.objects.all() queryset = SupplierPart.objects.all()
serializer_class = SupplierPartSerializer serializer_class = SupplierPartSerializer
@ -182,6 +210,12 @@ class SupplierPartList(generics.ListCreateAPIView):
class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView): class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of SupplierPart object
- GET: Retrieve detail view
- PATCH: Update object
- DELETE: Delete objec
"""
queryset = SupplierPart.objects.all() queryset = SupplierPart.objects.all()
serializer_class = SupplierPartSerializer serializer_class = SupplierPartSerializer
@ -192,6 +226,11 @@ class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView):
class SupplierPriceBreakList(generics.ListCreateAPIView): class SupplierPriceBreakList(generics.ListCreateAPIView):
""" API endpoint for list view of SupplierPriceBreak object
- GET: Retrieve list of SupplierPriceBreak objects
- POST: Create a new SupplierPriceBreak object
"""
queryset = SupplierPriceBreak.objects.all() queryset = SupplierPriceBreak.objects.all()
serializer_class = SupplierPriceBreakSerializer serializer_class = SupplierPriceBreakSerializer

View File

@ -1,3 +1,7 @@
"""
Django Forms for interacting with Part objects
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -10,6 +14,7 @@ from .models import SupplierPart
class PartImageForm(HelperForm): class PartImageForm(HelperForm):
""" Form for uploading a Part image """
class Meta: class Meta:
model = Part model = Part
@ -40,6 +45,7 @@ class BomExportForm(HelperForm):
class EditPartForm(HelperForm): class EditPartForm(HelperForm):
""" Form for editing a Part object """
class Meta: class Meta:
model = Part model = Part
@ -64,6 +70,7 @@ class EditPartForm(HelperForm):
class EditCategoryForm(HelperForm): class EditCategoryForm(HelperForm):
""" Form for editing a PartCategory object """
class Meta: class Meta:
model = PartCategory model = PartCategory
@ -75,6 +82,7 @@ class EditCategoryForm(HelperForm):
class EditBomItemForm(HelperForm): class EditBomItemForm(HelperForm):
""" Form for editing a BomItem object """
class Meta: class Meta:
model = BomItem model = BomItem
@ -88,6 +96,7 @@ class EditBomItemForm(HelperForm):
class EditSupplierPartForm(HelperForm): class EditSupplierPartForm(HelperForm):
""" Form for editing a SupplierPart object """
class Meta: class Meta:
model = SupplierPart model = SupplierPart

View File

@ -1,3 +1,7 @@
"""
Part database model definitions
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -47,11 +51,19 @@ class PartCategory(InvenTreeTree):
@property @property
def has_parts(self): def has_parts(self):
""" True if there are any parts in this category """
return self.parts.count() > 0 return self.parts.count() > 0
@receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log') @receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log')
def before_delete_part_category(sender, instance, using, **kwargs): def before_delete_part_category(sender, instance, using, **kwargs):
""" Receives before_delete signal for PartCategory object
Before deleting, update child Part and PartCategory objects:
- For each child category, set the parent to the parent of *this* category
- For each part, set the 'category' to the parent of *this* category
"""
# Update each part in this category to point to the parent category # Update each part in this category to point to the parent category
for part in instance.parts.all(): for part in instance.parts.all():
@ -67,6 +79,16 @@ def before_delete_part_category(sender, instance, using, **kwargs):
# Function to automatically rename a part image on upload # Function to automatically rename a part image on upload
# Format: part_pk.<img> # Format: part_pk.<img>
def rename_part_image(instance, filename): def rename_part_image(instance, filename):
""" Function for renaming a part image file
Args:
instance: Instance of a Part object
filename: Name of original uploaded file
Returns:
Cleaned filename in format part_<n>_img
"""
base = 'part_images' base = 'part_images'
if filename.count('.') > 0: if filename.count('.') > 0:
@ -248,7 +270,8 @@ class Part(models.Model):
@property @property
def allocation_count(self): def allocation_count(self):
""" Return true if any of this part is allocated """ Return true if any of this part is allocated:
- To another build - To another build
- To a customer order - To a customer order
""" """
@ -311,6 +334,15 @@ class Part(models.Model):
def attach_file(instance, filename): def attach_file(instance, filename):
""" Function for storing a file for a PartAttachment
Args:
instance: Instance of a PartAttachment object
filename: name of uploaded file
Returns:
path to store file, format: 'part_file_<pk>_filename'
"""
# Construct a path to store a file attachment # Construct a path to store a file attachment
return os.path.join('part_files', str(instance.part.id), filename) return os.path.join('part_files', str(instance.part.id), filename)
@ -356,6 +388,13 @@ class BomItem(models.Model):
note = models.CharField(max_length=100, blank=True, help_text='Item notes') note = models.CharField(max_length=100, blank=True, help_text='Item notes')
def clean(self): def clean(self):
""" Check validity of the BomItem model.
Performs model checks beyond simple field validation.
- A part cannot refer to itself in its BOM
- A part cannot refer to a part which refers to it
"""
# A part cannot refer to itself in its BOM # A part cannot refer to itself in its BOM
if self.part == self.sub_part: if self.part == self.sub_part:
@ -382,8 +421,9 @@ class BomItem(models.Model):
class SupplierPart(models.Model): class SupplierPart(models.Model):
""" Represents a unique part as provided by a Supplier """ Represents a unique part as provided by a Supplier
Each SupplierPart is identified by a MPN (Manufacturer Part Number) Each SupplierPart is identified by a MPN (Manufacturer Part Number)
Each SupplierPart is also linked to a Part object Each SupplierPart is also linked to a Part object.
- A Part may be available from multiple suppliers
A Part may be available from multiple suppliers
""" """
def get_absolute_url(self): def get_absolute_url(self):
@ -453,6 +493,7 @@ class SupplierPart(models.Model):
def get_price(self, quantity, moq=True, multiples=True): def get_price(self, quantity, moq=True, multiples=True):
""" Calculate the supplier price based on quantity price breaks. """ Calculate the supplier price based on quantity price breaks.
- If no price breaks available, use the single_price field - If no price breaks available, use the single_price field
- Don't forget to add in flat-fee cost (base_cost field) - Don't forget to add in flat-fee cost (base_cost field)
- If MOQ (minimum order quantity) is required, bump quantity - If MOQ (minimum order quantity) is required, bump quantity

View File

@ -1,3 +1,7 @@
"""
JSON serializers for Part app
"""
from rest_framework import serializers from rest_framework import serializers
from .models import Part, PartCategory, BomItem from .models import Part, PartCategory, BomItem
@ -7,6 +11,7 @@ from InvenTree.serializers import InvenTreeModelSerializer
class CategorySerializer(serializers.ModelSerializer): class CategorySerializer(serializers.ModelSerializer):
""" Serializer for PartCategory """
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
@ -23,6 +28,7 @@ class CategorySerializer(serializers.ModelSerializer):
class PartBriefSerializer(serializers.ModelSerializer): class PartBriefSerializer(serializers.ModelSerializer):
""" Serializer for Part (brief detail) """
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
@ -68,6 +74,7 @@ class PartSerializer(serializers.ModelSerializer):
class BomItemSerializer(InvenTreeModelSerializer): class BomItemSerializer(InvenTreeModelSerializer):
""" Serializer for BomItem object """
# url = serializers.CharField(source='get_absolute_url', read_only=True) # url = serializers.CharField(source='get_absolute_url', read_only=True)
@ -89,6 +96,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
class SupplierPartSerializer(serializers.ModelSerializer): class SupplierPartSerializer(serializers.ModelSerializer):
""" Serializer for SupplierPart object """
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
@ -112,6 +120,7 @@ class SupplierPartSerializer(serializers.ModelSerializer):
class SupplierPriceBreakSerializer(serializers.ModelSerializer): class SupplierPriceBreakSerializer(serializers.ModelSerializer):
""" Serializer for SupplierPriceBreak object """
class Meta: class Meta:
model = SupplierPriceBreak model = SupplierPriceBreak

View File

@ -1,3 +1,7 @@
"""
URL lookup for Part app
"""
from django.conf.urls import url, include from django.conf.urls import url, include
from . import views from . import views

View File

@ -1,3 +1,7 @@
"""
Django views for interacting with Part app
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -21,10 +25,12 @@ from .forms import EditSupplierPartForm
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
from InvenTree.helpers import DownloadFile from InvenTree.helpers import DownloadFile, str2bool
class PartIndex(ListView): class PartIndex(ListView):
""" View for displaying list of Part objects
"""
model = Part model = Part
template_name = 'part/category.html' template_name = 'part/category.html'
context_object_name = 'parts' context_object_name = 'parts'
@ -45,8 +51,12 @@ class PartIndex(ListView):
class PartCreate(AjaxCreateView): class PartCreate(AjaxCreateView):
""" Create a new part """ View for creating a new Part object.
- Optionally provide a category object as initial data
Options for providing initial conditions:
- Provide a category object as initial data
- Copy an existing Part
""" """
model = Part model = Part
form_class = EditPartForm form_class = EditPartForm
@ -64,6 +74,10 @@ class PartCreate(AjaxCreateView):
# If a category is provided in the URL, pass that to the page context # If a category is provided in the URL, pass that to the page context
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Provide extra context information for the form to display:
- Add category information (if provided)
"""
context = super(PartCreate, self).get_context_data(**kwargs) context = super(PartCreate, self).get_context_data(**kwargs)
# Add category information to the page # Add category information to the page
@ -76,6 +90,11 @@ class PartCreate(AjaxCreateView):
# Pre-fill the category field if a valid category is provided # Pre-fill the category field if a valid category is provided
def get_initial(self): def get_initial(self):
""" Get initial data for the new Part object:
- If a category is provided, pre-fill the Category field
- If 'copy' parameter is provided, copy from referenced Part
"""
# Is the client attempting to copy an existing part? # Is the client attempting to copy an existing part?
part_to_copy = self.request.GET.get('copy', None) part_to_copy = self.request.GET.get('copy', None)
@ -98,15 +117,22 @@ class PartCreate(AjaxCreateView):
class PartDetail(DetailView): class PartDetail(DetailView):
""" Detail view for Part object
"""
context_object_name = 'part' context_object_name = 'part'
queryset = Part.objects.all() queryset = Part.objects.all()
template_name = 'part/detail.html' template_name = 'part/detail.html'
# Add in some extra context information based on query params # Add in some extra context information based on query params
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Provide extra context data to template
- If '?editing=True', set 'editing_enabled' context variable
"""
context = super(PartDetail, self).get_context_data(**kwargs) context = super(PartDetail, self).get_context_data(**kwargs)
if self.request.GET.get('edit', '').lower() in ['true', 'yes', '1']: if str2bool(self.request.GET.get('edit', '')):
context['editing_enabled'] = 1 context['editing_enabled'] = 1
else: else:
context['editing_enabled'] = 0 context['editing_enabled'] = 0
@ -115,6 +141,7 @@ class PartDetail(DetailView):
class PartImage(AjaxUpdateView): class PartImage(AjaxUpdateView):
""" View for uploading Part image """
model = Part model = Part
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
@ -128,6 +155,8 @@ class PartImage(AjaxUpdateView):
class PartEdit(AjaxUpdateView): class PartEdit(AjaxUpdateView):
""" View for editing Part object """
model = Part model = Part
template_name = 'part/edit.html' template_name = 'part/edit.html'
form_class = EditPartForm form_class = EditPartForm
@ -214,6 +243,8 @@ class BomDownload(AjaxView):
class PartDelete(AjaxDeleteView): class PartDelete(AjaxDeleteView):
""" View to delete a Part object """
model = Part model = Part
template_name = 'part/delete.html' template_name = 'part/delete.html'
ajax_template_name = 'part/partial_delete.html' ajax_template_name = 'part/partial_delete.html'
@ -229,6 +260,7 @@ class PartDelete(AjaxDeleteView):
class CategoryDetail(DetailView): class CategoryDetail(DetailView):
""" Detail view for PartCategory """
model = PartCategory model = PartCategory
context_object_name = 'category' context_object_name = 'category'
queryset = PartCategory.objects.all() queryset = PartCategory.objects.all()
@ -236,6 +268,7 @@ class CategoryDetail(DetailView):
class CategoryEdit(AjaxUpdateView): class CategoryEdit(AjaxUpdateView):
""" Update view to edit a PartCategory """
model = PartCategory model = PartCategory
template_name = 'part/category_edit.html' template_name = 'part/category_edit.html'
form_class = EditCategoryForm form_class = EditCategoryForm
@ -251,6 +284,7 @@ class CategoryEdit(AjaxUpdateView):
class CategoryDelete(AjaxDeleteView): class CategoryDelete(AjaxDeleteView):
""" Delete view to delete a PartCategory """
model = PartCategory model = PartCategory
template_name = 'part/category_delete.html' template_name = 'part/category_delete.html'
context_object_name = 'category' context_object_name = 'category'
@ -263,6 +297,7 @@ class CategoryDelete(AjaxDeleteView):
class CategoryCreate(AjaxCreateView): class CategoryCreate(AjaxCreateView):
""" Create view to make a new PartCategory """
model = PartCategory model = PartCategory
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'
@ -271,6 +306,10 @@ class CategoryCreate(AjaxCreateView):
form_class = EditCategoryForm form_class = EditCategoryForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" Add extra context data to template.
- If parent category provided, pass the category details to the template
"""
context = super(CategoryCreate, self).get_context_data(**kwargs).copy() context = super(CategoryCreate, self).get_context_data(**kwargs).copy()
parent_id = self.request.GET.get('category', None) parent_id = self.request.GET.get('category', None)
@ -281,6 +320,10 @@ class CategoryCreate(AjaxCreateView):
return context return context
def get_initial(self): def get_initial(self):
""" Get initial data for new PartCategory
- If parent provided, pre-fill the parent category
"""
initials = super(CategoryCreate, self).get_initial().copy() initials = super(CategoryCreate, self).get_initial().copy()
parent_id = self.request.GET.get('category', None) parent_id = self.request.GET.get('category', None)
@ -292,12 +335,14 @@ class CategoryCreate(AjaxCreateView):
class BomItemDetail(DetailView): class BomItemDetail(DetailView):
""" Detail view for BomItem """
context_object_name = 'item' context_object_name = 'item'
queryset = BomItem.objects.all() queryset = BomItem.objects.all()
template_name = 'part/bom-detail.html' template_name = 'part/bom-detail.html'
class BomItemCreate(AjaxCreateView): class BomItemCreate(AjaxCreateView):
""" Create view for making a new BomItem object """
model = BomItem model = BomItem
form_class = EditBomItemForm form_class = EditBomItemForm
template_name = 'part/bom-create.html' template_name = 'part/bom-create.html'
@ -305,6 +350,11 @@ class BomItemCreate(AjaxCreateView):
ajax_form_title = 'Create BOM item' ajax_form_title = 'Create BOM item'
def get_initial(self): def get_initial(self):
""" Provide initial data for the BomItem:
- If 'parent' provided, set the parent part field
"""
# Look for initial values # Look for initial values
initials = super(BomItemCreate, self).get_initial().copy() initials = super(BomItemCreate, self).get_initial().copy()
@ -318,6 +368,8 @@ class BomItemCreate(AjaxCreateView):
class BomItemEdit(AjaxUpdateView): class BomItemEdit(AjaxUpdateView):
""" Update view for editing BomItem """
model = BomItem model = BomItem
form_class = EditBomItemForm form_class = EditBomItemForm
template_name = 'part/bom-edit.html' template_name = 'part/bom-edit.html'
@ -326,6 +378,7 @@ class BomItemEdit(AjaxUpdateView):
class BomItemDelete(AjaxDeleteView): class BomItemDelete(AjaxDeleteView):
""" Delete view for removing BomItem """
model = BomItem model = BomItem
template_name = 'part/bom-delete.html' template_name = 'part/bom-delete.html'
context_object_name = 'item' context_object_name = 'item'
@ -333,6 +386,7 @@ class BomItemDelete(AjaxDeleteView):
class SupplierPartDetail(DetailView): class SupplierPartDetail(DetailView):
""" Detail view for SupplierPart """
model = SupplierPart model = SupplierPart
template_name = 'company/partdetail.html' template_name = 'company/partdetail.html'
context_object_name = 'part' context_object_name = 'part'
@ -340,6 +394,8 @@ class SupplierPartDetail(DetailView):
class SupplierPartEdit(AjaxUpdateView): class SupplierPartEdit(AjaxUpdateView):
""" Update view for editing SupplierPart """
model = SupplierPart model = SupplierPart
template_name = 'company/partedit.html' template_name = 'company/partedit.html'
context_object_name = 'part' context_object_name = 'part'
@ -349,6 +405,8 @@ class SupplierPartEdit(AjaxUpdateView):
class SupplierPartCreate(AjaxCreateView): class SupplierPartCreate(AjaxCreateView):
""" Create view for making new SupplierPart """
model = SupplierPart model = SupplierPart
form_class = EditSupplierPartForm form_class = EditSupplierPartForm
ajax_template_name = 'modal_form.html' ajax_template_name = 'modal_form.html'
@ -356,6 +414,11 @@ class SupplierPartCreate(AjaxCreateView):
context_object_name = 'part' context_object_name = 'part'
def get_initial(self): def get_initial(self):
""" Provide initial data for new SupplierPart:
- If 'supplier_id' provided, pre-fill supplier field
- If 'part_id' provided, pre-fill part field
"""
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.request.GET.get('supplier', None)
@ -374,6 +437,7 @@ class SupplierPartCreate(AjaxCreateView):
class SupplierPartDelete(AjaxDeleteView): class SupplierPartDelete(AjaxDeleteView):
""" Delete view for removing a SupplierPart """
model = SupplierPart model = SupplierPart
success_url = '/supplier/' success_url = '/supplier/'
template_name = 'company/partdelete.html' template_name = 'company/partdelete.html'

View File

@ -0,0 +1,9 @@
"""
The Stock module is responsible for Stock management.
It includes models for:
- StockLocation
- StockItem
- StockItemTracking
"""

View File

@ -1,3 +1,7 @@
"""
JSON API for the Stock app
"""
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
@ -26,7 +30,7 @@ class StockCategoryTree(TreeSerializer):
class StockDetail(generics.RetrieveUpdateDestroyAPIView): class StockDetail(generics.RetrieveUpdateDestroyAPIView):
""" """ API detail endpoint for Stock object
get: get:
Return a single StockItem object Return a single StockItem object
@ -44,6 +48,11 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
class StockFilter(FilterSet): class StockFilter(FilterSet):
""" FilterSet for advanced stock filtering.
Allows greater-than / less-than filtering for stock quantity
"""
min_stock = NumberFilter(name='quantity', lookup_expr='gte') min_stock = NumberFilter(name='quantity', lookup_expr='gte')
max_stock = NumberFilter(name='quantity', lookup_expr='lte') max_stock = NumberFilter(name='quantity', lookup_expr='lte')
@ -53,12 +62,11 @@ class StockFilter(FilterSet):
class StockStocktake(APIView): class StockStocktake(APIView):
""" """ Stocktake API endpoint provides stock update of multiple items simultaneously.
Stocktake API endpoint provides stock update of multiple items simultaneously
The 'action' field tells the type of stock action to perform: The 'action' field tells the type of stock action to perform:
* 'stocktake' - Count the stock item(s) - stocktake: Count the stock item(s)
* 'remove' - Remove the quantity provided from stock - remove: Remove the quantity provided from stock
* 'add' - Add the quantity provided from stock - add: Add the quantity provided from stock
""" """
permission_classes = [ permission_classes = [
@ -129,6 +137,7 @@ class StockStocktake(APIView):
class StockMove(APIView): class StockMove(APIView):
""" API endpoint for performing stock movements """
permission_classes = [ permission_classes = [
permissions.IsAuthenticatedOrReadOnly, permissions.IsAuthenticatedOrReadOnly,
@ -183,6 +192,11 @@ class StockMove(APIView):
class StockLocationList(generics.ListCreateAPIView): class StockLocationList(generics.ListCreateAPIView):
""" API endpoint for list view of StockLocation objects:
- GET: Return list of StockLocation objects
- POST: Create a new StockLocation
"""
queryset = StockLocation.objects.all() queryset = StockLocation.objects.all()
@ -204,14 +218,10 @@ class StockLocationList(generics.ListCreateAPIView):
class StockList(generics.ListCreateAPIView): class StockList(generics.ListCreateAPIView):
""" """ API endpoint for list view of Stock objects
get: - GET: Return a list of all StockItem objects (with optional query filters)
Return a list of all StockItem objects - POST: Create a new StockItem
(with optional query filters)
post:
Create a new StockItem
""" """
def get_queryset(self): def get_queryset(self):
@ -268,6 +278,7 @@ class StockList(generics.ListCreateAPIView):
class StockStocktakeEndpoint(generics.UpdateAPIView): class StockStocktakeEndpoint(generics.UpdateAPIView):
""" API endpoint for performing stocktake """
queryset = StockItem.objects.all() queryset = StockItem.objects.all()
serializer_class = StockQuantitySerializer serializer_class = StockQuantitySerializer
@ -283,6 +294,13 @@ class StockStocktakeEndpoint(generics.UpdateAPIView):
class StockTrackingList(generics.ListCreateAPIView): class StockTrackingList(generics.ListCreateAPIView):
""" API endpoint for list view of StockItemTracking objects.
StockItemTracking objects are read-only
(they are created by internal model functionality)
- GET: Return list of StockItemTracking objects
"""
queryset = StockItemTracking.objects.all() queryset = StockItemTracking.objects.all()
serializer_class = StockTrackingSerializer serializer_class = StockTrackingSerializer
@ -312,17 +330,11 @@ class StockTrackingList(generics.ListCreateAPIView):
class LocationDetail(generics.RetrieveUpdateDestroyAPIView): class LocationDetail(generics.RetrieveUpdateDestroyAPIView):
""" """ API endpoint for detail view of StockLocation object
get:
Return a single StockLocation object
post:
Update a StockLocation object
delete:
Remove a StockLocation object
- GET: Return a single StockLocation object
- PATCH: Update a StockLocation object
- DELETE: Remove a StockLocation object
""" """
queryset = StockLocation.objects.all() queryset = StockLocation.objects.all()

View File

@ -1,3 +1,7 @@
"""
Django Forms for interacting with Stock app
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -8,6 +12,7 @@ from .models import StockLocation, StockItem
class EditStockLocationForm(HelperForm): class EditStockLocationForm(HelperForm):
""" Form for editing a StockLocation """
class Meta: class Meta:
model = StockLocation model = StockLocation
@ -19,6 +24,7 @@ class EditStockLocationForm(HelperForm):
class CreateStockItemForm(HelperForm): class CreateStockItemForm(HelperForm):
""" Form for creating a new StockItem """
class Meta: class Meta:
model = StockItem model = StockItem
@ -38,6 +44,7 @@ class CreateStockItemForm(HelperForm):
class MoveStockItemForm(forms.ModelForm): class MoveStockItemForm(forms.ModelForm):
""" Form for moving a StockItem to a new location """
note = forms.CharField(label='Notes', required=True, help_text='Add note (required)') note = forms.CharField(label='Notes', required=True, help_text='Add note (required)')
@ -60,6 +67,7 @@ class StocktakeForm(forms.ModelForm):
class EditStockItemForm(HelperForm): class EditStockItemForm(HelperForm):
""" Form for editing a StockItem object """
class Meta: class Meta:
model = StockItem model = StockItem

View File

@ -1,3 +1,8 @@
"""
Stock database model definitions
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals

View File

@ -1,3 +1,7 @@
"""
JSON serializers for Stock app
"""
from rest_framework import serializers from rest_framework import serializers
from .models import StockItem, StockLocation from .models import StockItem, StockLocation
@ -44,8 +48,8 @@ class StockItemSerializerBrief(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 linked part
- Includes serialization for the item location - Includes serialization for the item location
""" """
@ -112,6 +116,7 @@ class LocationSerializer(serializers.ModelSerializer):
class StockTrackingSerializer(serializers.ModelSerializer): class StockTrackingSerializer(serializers.ModelSerializer):
""" Serializer for StockItemTracking model """
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)

View File

@ -1,3 +1,7 @@
"""
URL lookup for Stock app
"""
from django.conf.urls import url, include from django.conf.urls import url, include
from . import views from . import views

View File

@ -1,3 +1,7 @@
"""
Django views for interacting with Stock app
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
@ -19,8 +23,7 @@ from .forms import StocktakeForm
class StockIndex(ListView): class StockIndex(ListView):
""" """ StockIndex view loads all StockLocation and StockItem object
StockIndex view loads all StockLocation and StockItem object
""" """
model = StockItem model = StockItem
template_name = 'stock/location.html' template_name = 'stock/location.html'

View File

@ -28,15 +28,22 @@ copyright = '2019, InvenTree'
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'autoapi.extension' 'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'autoapi.extension',
] ]
napoleon_google_docstring = True
napoleon_numpy_docstring = False
autoapi_dirs = [ autoapi_dirs = [
'../InvenTree', '../InvenTree',
] ]
autoapi_options = [ autoapi_options = [
'members', 'members',
'private-members',
'special-members',
] ]
autoapi_type = 'python' autoapi_type = 'python'
@ -44,11 +51,21 @@ autoapi_type = 'python'
autoapi_ignore = [ autoapi_ignore = [
'*migrations*', '*migrations*',
'**/test*.py', '**/test*.py',
'**/manage.py' '**/manage.py',
'**/apps.py',
'**/admin.py',
'**/middleware.py',
'**/utils.py',
'**/wsgi.py',
'**/templates/',
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] autoapi_template_dir = 'templates'
autoapi_root = 'docs'
autoapi_add_toctree_entry = False
templates_path = ['templates']
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
@ -73,3 +90,11 @@ html_theme = 'sphinx_rtd_theme'
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ['_static']
# Table of contents in sidebar
html_sidebars = {'**': [
'globaltoc.html',
'relations.html',
'sourcelink.html',
'searchbox.html'
]}

View File

@ -1,20 +1,12 @@
.. InvenTree documentation master file, created by InvenTree's Django Documentation
sphinx-quickstart on Sat Apr 27 15:45:39 2019. ================================
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to InvenTree's documentation! This documentation is auto-generated from the `InvenTree codebase <https://github.com/InvenTree/InvenTree>`_
=====================================
.. toctree:: .. toctree::
:titlesonly:
:maxdepth: 2 :maxdepth: 2
:caption: Contents: :caption: Contents:
:hidden:
InvenTree <introduction>
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

18
docs/introduction.rst Normal file
View File

@ -0,0 +1,18 @@
InvenTree
=========
InvenTree is an open source inventory management system which provides powerful low-level part management and stock tracking functionality.
The core of the InvenTree software is a Python/Django database backend whi
Django Apps
===========
The InvenTree Django ecosystem provides the following 'apps' for core functionality:
* `InvenTree </docs/InvenTree/index.html>`_ - High level management functions
* `Build </docs/build/index.html>`_ - Part build projects
* `Company </docs/company/index.html>`_ - Company management (suppliers / customers)
* `Part </docs/part/index.html>`_ - Part management
* `Stock </docs/stock/index.html>`_ - Stock management

7
docs/templates/layout.html vendored Normal file
View File

@ -0,0 +1,7 @@
{% extends "!layout.html" %}
{% block menu %}
{{ super() }}
<a href="/py-modindex.html">Module Index</a>
<a href="/genindex.html">Index</a>
{% endblock %}

97
docs/templates/python/module.rst vendored Normal file
View File

@ -0,0 +1,97 @@
{% if not obj.display %}
:orphan:
{% endif %}
:mod:`{{ obj.name }}`
======={{ "=" * obj.name|length }}
.. py:module:: {{ obj.name }}
{% if obj.docstring %}
.. autoapi-nested-parse::
{{ obj.docstring|prepare_docstring|indent(3) }}
{% endif %}
{% block subpackages %}
{% set visible_subpackages = obj.subpackages|selectattr("display")|list %}
{% if visible_subpackages %}
Subpackages
-----------
.. toctree::
:titlesonly:
:maxdepth: 3
{% for subpackage in visible_subpackages %}
{{ subpackage.short_name }}/index.rst
{% endfor %}
{% endif %}
{% endblock %}
{% block submodules %}
{% set visible_submodules = obj.submodules|selectattr("display")|list %}
{% if visible_submodules %}
Submodules
----------
The {{ obj.name }} module contains the following submodules
.. toctree::
:titlesonly:
:maxdepth: 1
{% for submodule in visible_submodules %}
{{ submodule.short_name }}/index.rst
{% endfor %}
{% endif %}
{% endblock %}
{% block content %}
{% set visible_children = obj.children|selectattr("display")|list %}
{% if visible_children %}
{{ obj.type|title }} Contents
{{ "-" * obj.type|length }}---------
{% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %}
{% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %}
{% if include_summaries and (visible_classes or visible_functions) %}
{% block classes %}
{% if visible_classes %}
Classes
~~~~~~~
.. autoapisummary::
{% for klass in visible_classes %}
{{ klass.id }}
{% endfor %}
{% endif %}
{% endblock %}
{% block functions %}
{% if visible_functions %}
Functions
~~~~~~~~~
.. autoapisummary::
{% for function in visible_functions %}
{{ function.id }}
{% endfor %}
{% endif %}
{% endblock %}
{% endif %}
{% for obj_item in visible_children %}
{% if obj.all is none or obj_item.short_name in obj.all %}
{{ obj_item.rendered|indent(0) }}
{% endif %}
{% endfor %}
{% endif %}
{% endblock %}