diff --git a/.gitignore b/.gitignore index b4aa808107..6229f979df 100644 --- a/.gitignore +++ b/.gitignore @@ -29,8 +29,6 @@ local_settings.py # Sphinx files docs/_build -docs/_static -docs/_templates # Local media storage (only when running in development mode) InvenTree/media diff --git a/InvenTree/InvenTree/__init__.py b/InvenTree/InvenTree/__init__.py index e69de29bb2..521e0a60fc 100644 --- a/InvenTree/InvenTree/__init__.py +++ b/InvenTree/InvenTree/__init__.py @@ -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. +""" diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 002a83e896..ba203b2f1b 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -1,3 +1,7 @@ +""" +Helper forms which subclass Django forms to provide additional functionality +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -6,6 +10,7 @@ from crispy_forms.helper import FormHelper class HelperForm(forms.ModelForm): + """ Provides simple integration of crispy_forms extension. """ def __init__(self, *args, **kwargs): super(forms.ModelForm, self).__init__(*args, **kwargs) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index a492ca353d..295937af08 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -1,3 +1,7 @@ +""" +Provides helper functions used throughout the InvenTree project +""" + import io from wsgiref.util import FileWrapper @@ -5,7 +9,14 @@ from django.http import StreamingHttpResponse 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: 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', ] -def WrapWithQuotes(text): - # TODO - Make this better - if not text.startswith('"'): - text = '"' + text +def WrapWithQuotes(text, quote='"'): + """ Wrap the supplied text with quotes - if not text.endswith('"'): - text = text + '"' + Args: + 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 def DownloadFile(data, filename, content_type='application/text'): - """ - Create a dynamic file for the user to download. - @param data is the raw file data + """ Create a dynamic file for the user to download. + + 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) diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 2b49f7b908..61fb50831b 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -1,9 +1,12 @@ +""" +Generic models which provide extra functionality over base Django model types. +""" + from __future__ import unicode_literals from django.db import models from django.contrib.contenttypes.models import ContentType from rest_framework.exceptions import ValidationError -from .helpers import str2bool from django.db.models.signals import pre_delete from django.dispatch import receiver @@ -11,6 +14,7 @@ from django.dispatch import receiver class InvenTreeTree(models.Model): """ 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 can have zero-or-more child Categor(y/ies) """ @@ -69,10 +73,12 @@ class InvenTreeTree(models.Model): @property def has_children(self): + """ True if there are any children under this item """ return self.children.count() > 0 @property def children(self): + """ Return the children of this item """ contents = ContentType.objects.get_for_model(type(self)) childs = contents.get_all_objects_for_this_type(parent=self.id) @@ -100,11 +106,10 @@ class InvenTreeTree(models.Model): @property def parentpath(self): - """ Return the parent path of this category + """ Get the parent path of this category - Todo: - This function is recursive and expensive. - It should be reworked such that only a single db call is required + Returns: + List of category names from the top level to the parent of this category """ if self.parent: @@ -114,10 +119,21 @@ class InvenTreeTree(models.Model): @property 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] @property 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]) def __setattr__(self, attrname, val): @@ -157,38 +173,19 @@ class InvenTreeTree(models.Model): super(InvenTreeTree, self).__setattr__(attrname, val) def __str__(self): - """ String representation of a category is the full path to that category - - Todo: - This is recursive - Make it not so. - """ + """ String representation of a category is the full path to that category """ return self.pathstring @receiver(pre_delete, sender=InvenTreeTree, dispatch_uid='tree_pre_delete_log') 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 for child in instance.children.all(): child.parent = instance.parent 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) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 4ab1fd694d..7149668cc4 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -1,3 +1,8 @@ +""" +Serializers used in various InvenTree apps +""" + + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -7,6 +12,7 @@ from django.contrib.auth.models import User class UserSerializer(serializers.ModelSerializer): + """ Serializer for User - provides all fields """ class Meta: model = User @@ -14,6 +20,7 @@ class UserSerializer(serializers.ModelSerializer): class UserSerializerBrief(serializers.ModelSerializer): + """ Serializer for User - provides limited information """ class Meta: model = User diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index c33689be61..ccc828de55 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -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.contrib import admin from django.contrib.auth import views as auth_views diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index c160398caf..551f624cb9 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -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 -*- from __future__ import unicode_literals @@ -12,6 +19,8 @@ from rest_framework import views class TreeSerializer(views.APIView): + """ JSON View for serializing a Tree object. + """ def itemToJson(self, item): @@ -52,20 +61,34 @@ class TreeSerializer(views.APIView): 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_title = '' def get_data(self): + """ Get extra context data (default implementation is empty dict) + + Returns: + dict object (empty) + """ 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={}): + """ 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: context['form'] = form @@ -73,7 +96,7 @@ class AjaxMixin(object): data['title'] = self.ajax_form_title data['html_form'] = render_to_string( - self.getAjaxTemplate(), + self.ajax_template_name, context, request=request ) @@ -88,7 +111,8 @@ class AjaxMixin(object): class AjaxView(AjaxMixin, View): - """ Bare-bones AjaxView """ + """ An 'AJAXified' View for displaying an object + """ # By default, point to the modal_form template # (this can be overridden by a child class) @@ -201,7 +225,7 @@ class AjaxDeleteView(AjaxMixin, DeleteView): data = {'id': self.get_object().id, 'delete': False, '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(), request=request) } @@ -229,15 +253,24 @@ class AjaxDeleteView(AjaxMixin, DeleteView): class IndexView(TemplateView): + """ View for InvenTree index page """ template_name = 'InvenTree/index.html' class SearchView(TemplateView): + """ View for InvenTree search page. + + Displays results of search query + """ template_name = 'InvenTree/search.html' 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() diff --git a/InvenTree/build/__init__.py b/InvenTree/build/__init__.py index e69de29bb2..747a20f837 100644 --- a/InvenTree/build/__init__.py +++ b/InvenTree/build/__init__.py @@ -0,0 +1,5 @@ +""" +The Build module is responsible for managing "Build" transactions. + +A Build consumes parts from stock to create new parts +""" diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 41d46bab49..393bd1f073 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -1,3 +1,7 @@ +""" +JSON API for the Build app +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -11,7 +15,12 @@ from .models import Build 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() serializer_class = BuildSerializer diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 1775b5c3b5..6254f0745c 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -1,3 +1,7 @@ +""" +Django Forms for interacting with Build objects +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -7,6 +11,8 @@ from .models import Build class EditBuildForm(HelperForm): + """ Form for editing a Build object. + """ class Meta: model = Build diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index c72aa5209b..880c1b08b1 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -1,3 +1,7 @@ +""" +Build database model definitions +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -9,60 +13,60 @@ from django.core.validators import MinValueValidator class Build(models.Model): - """ 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 + """ A Build object organises the creation of new parts from the component parts. """ def get_absolute_url(self): 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 PENDING = 10 # Build is pending / active HOLDING = 20 # Build is currently being held CANCELLED = 30 # Build was cancelled COMPLETE = 40 # Build is complete + #: Build status codes BUILD_STATUS_CODES = {PENDING: _("Pending"), HOLDING: _("Holding"), CANCELLED: _("Cancelled"), COMPLETE: _("Complete"), } - batch = models.CharField(max_length=100, blank=True, null=True, - help_text='Batch code for this build output') - - # Status of the build + #: Status of the build (ref BUILD_STATUS_CODES) status = models.PositiveIntegerField(default=PENDING, choices=BUILD_STATUS_CODES.items(), 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) - # Date the build was 'completed' + #: Date the build was 'completed' (and parts removed from stock) completion_date = models.DateField(null=True, blank=True) - # Brief build title - 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 attached to each build output notes = models.TextField(blank=True) @property def required_parts(self): + """ Returns a dict of parts required to build this part (BOM) """ parts = [] for item in self.part.bom_items.all(): @@ -77,8 +81,7 @@ class Build(models.Model): @property 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: if item['part'].total_stock < item['quantity']: @@ -88,10 +91,10 @@ class Build(models.Model): @property def is_active(self): - """ Is this build active? - An active build is either: - - Pending - - Holding + """ Is this build active? An active build is either: + + - PENDING + - HOLDING """ return self.status in [ @@ -101,4 +104,5 @@ class Build(models.Model): @property def is_complete(self): + """ Returns True if the build status is COMPLETE """ return self.status == self.COMPLETE diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index ac36fd5b41..955a8d59f5 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -1,3 +1,7 @@ +""" +JSON serializers for Build API +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -7,6 +11,7 @@ from .models import Build class BuildSerializer(serializers.ModelSerializer): + """ Serializes a Build object """ url = serializers.CharField(source='get_absolute_url', read_only=True) status_text = serializers.CharField(source='get_status_display', read_only=True) diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index a8e4435e24..fea3b6596c 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -1,3 +1,7 @@ +""" +URL lookup for Build app +""" + from django.conf.urls import url, include from . import views diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index f3f7bd79f3..c8d40e80e6 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -1,3 +1,7 @@ +""" +Django views for interacting with Build objects +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -13,11 +17,14 @@ from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView class BuildIndex(ListView): + """ View for displaying list of Builds + """ model = Build template_name = 'build/index.html' context_object_name = 'builds' def get_queryset(self): + """ Return all Build objects (order by date, newest first) """ return Build.objects.order_by('status', '-completion_date') def get_context_data(self, **kwargs): @@ -35,6 +42,9 @@ class BuildIndex(ListView): class BuildCancel(AjaxView): + """ View to cancel a Build. + Provides a cancellation information dialog + """ model = Build template_name = 'build/cancel.html' ajax_form_title = 'Cancel Build' @@ -42,6 +52,7 @@ class BuildCancel(AjaxView): fields = [] 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']) @@ -51,24 +62,28 @@ class BuildCancel(AjaxView): return self.renderJsonResponse(request, None) def get_data(self): + """ Provide JSON context data. """ return { 'info': 'Build was cancelled' } class BuildDetail(DetailView): + """ Detail view of a single Build object. """ model = Build template_name = 'build/detail.html' context_object_name = 'build' class BuildAllocate(DetailView): + """ View for allocating parts to a Build """ model = Build context_object_name = 'build' template_name = 'build/allocate.html' class BuildCreate(AjaxCreateView): + """ View to create a new Build object """ model = Build context_object_name = 'build' form_class = EditBuildForm @@ -76,6 +91,11 @@ class BuildCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' 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() part_id = self.request.GET.get('part', None) @@ -92,6 +112,8 @@ class BuildCreate(AjaxCreateView): class BuildUpdate(AjaxUpdateView): + """ View for editing a Build object """ + model = Build form_class = EditBuildForm context_object_name = 'build' diff --git a/InvenTree/company/__init__.py b/InvenTree/company/__init__.py index e69de29bb2..6c89578ac3 100644 --- a/InvenTree/company/__init__.py +++ b/InvenTree/company/__init__.py @@ -0,0 +1,8 @@ +""" +The Company module is responsible for managing Company interactions. + +A company can be either (or both): + +- Supplier +- Customer +""" diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index d90cfdb1ee..dfa3ebfb77 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -1,3 +1,7 @@ +""" +Provides a JSON API for the Company app +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -12,6 +16,13 @@ from .serializers import CompanySerializer 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 queryset = Company.objects.all() @@ -44,6 +55,7 @@ class CompanyList(generics.ListCreateAPIView): class CompanyDetail(generics.RetrieveUpdateDestroyAPIView): + """ API endpoint for detail of a single Company object """ queryset = Company.objects.all() serializer_class = CompanySerializer diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 7fca7c7e72..bf7057a1ea 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -1,3 +1,7 @@ +""" +Django Forms for interacting with Company app +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -7,6 +11,7 @@ from .models import Company class EditCompanyForm(HelperForm): + """ Form for editing a Company object """ class Meta: model = Company @@ -26,6 +31,7 @@ class EditCompanyForm(HelperForm): class CompanyImageForm(HelperForm): + """ Form for uploading a Company image """ class Meta: model = Company diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index fe97d9a90c..dcee37959d 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -1,3 +1,7 @@ +""" +Company database model definitions +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -8,6 +12,16 @@ from django.urls import reverse 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' if filename.count('.') > 0: @@ -24,6 +38,9 @@ def rename_company_image(instance, filename): 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, help_text='Company name') @@ -54,21 +71,28 @@ class Company(models.Model): is_supplier = models.BooleanField(default=True) def __str__(self): + """ Get string representation of a Company """ return "{n} - {d}".format(n=self.name, d=self.description) def get_absolute_url(self): + """ Get the web URL for the detail view for this Company """ return reverse('company-detail', kwargs={'pk': self.id}) @property def part_count(self): + """ The number of parts supplied by this company """ return self.parts.count() @property def has_parts(self): + """ Return True if this company supplies any parts """ return self.part_count > 0 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) diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index a87560a873..88792f7f64 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -1,9 +1,14 @@ +""" +JSON serializers for Company app +""" + from rest_framework import serializers from .models import Company class CompanyBriefSerializer(serializers.ModelSerializer): + """ Serializer for Company object (limited detail) """ url = serializers.CharField(source='get_absolute_url', read_only=True) @@ -17,6 +22,7 @@ class CompanyBriefSerializer(serializers.ModelSerializer): class CompanySerializer(serializers.ModelSerializer): + """ Serializer for Company object (full detail) """ url = serializers.CharField(source='get_absolute_url', read_only=True) diff --git a/InvenTree/company/urls.py b/InvenTree/company/urls.py index 13c478f28a..2c5018ebec 100644 --- a/InvenTree/company/urls.py +++ b/InvenTree/company/urls.py @@ -1,3 +1,8 @@ +""" +URL lookup for Company app +""" + + from django.conf.urls import url, include from django.views.generic.base import RedirectView diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 5a80e5ea15..f61da1fffa 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -1,3 +1,8 @@ +""" +Django views for interacting with Company app +""" + + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -12,12 +17,20 @@ from .forms import CompanyImageForm class CompanyIndex(ListView): + """ View for displaying list of companies + """ + model = Company template_name = 'company/index.html' context_object_name = 'companies' paginate_by = 50 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') if self.request.GET.get('supplier', None): @@ -30,6 +43,7 @@ class CompanyIndex(ListView): class CompanyDetail(DetailView): + """ Detail view for Company object """ context_obect_name = 'company' template_name = 'company/detail.html' queryset = Company.objects.all() @@ -37,6 +51,7 @@ class CompanyDetail(DetailView): class CompanyImage(AjaxUpdateView): + """ View for uploading an image for the Company """ model = Company ajax_template_name = 'modal_form.html' ajax_form_title = 'Update Company Image' @@ -49,6 +64,7 @@ class CompanyImage(AjaxUpdateView): class CompanyEdit(AjaxUpdateView): + """ View for editing a Company object """ model = Company form_class = EditCompanyForm context_object_name = 'company' @@ -62,6 +78,7 @@ class CompanyEdit(AjaxUpdateView): class CompanyCreate(AjaxCreateView): + """ View for creating a new Company object """ model = Company context_object_name = 'company' form_class = EditCompanyForm @@ -75,6 +92,7 @@ class CompanyCreate(AjaxCreateView): class CompanyDelete(AjaxDeleteView): + """ View for deleting a Company object """ model = Company success_url = '/company/' ajax_template_name = 'company/delete.html' diff --git a/InvenTree/keygen.py b/InvenTree/keygen.py index 02909b0d79..c96df22794 100644 --- a/InvenTree/keygen.py +++ b/InvenTree/keygen.py @@ -1,7 +1,5 @@ """ -Module keygen -============= -This module generates a Django SECRET_KEY file to be used by manage.py +Generates a Django SECRET_KEY file to be used by manage.py """ import random @@ -15,11 +13,13 @@ KEY_DIR = os.path.dirname(os.path.realpath(__file__)) def generate_key(length=50): - """ - Generate a random string + """ Generate a random string - :param length: Number of characters in returned string (default=50) - :returns: Randomized secret key string + Args: + length: Number of characters in returned string (default = 50) + + Returns: + Randomized secret key string """ options = string.digits + string.ascii_letters + string.punctuation diff --git a/InvenTree/part/__init__.py b/InvenTree/part/__init__.py index e69de29bb2..da73be7753 100644 --- a/InvenTree/part/__init__.py +++ b/InvenTree/part/__init__.py @@ -0,0 +1,10 @@ +""" +The Part module is responsible for Part management. + +It includes models for: + +- PartCategory +- Part +- SupplierPart +- BomItem +""" diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 9b5aef436a..8f93cede7c 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -1,3 +1,7 @@ +""" +Provides a JSON API for the Part app +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -26,6 +30,12 @@ class PartCategoryTree(TreeSerializer): 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() serializer_class = CategorySerializer @@ -56,11 +66,13 @@ class CategoryList(generics.ListCreateAPIView): class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): + """ API endpoint for detail view of a single PartCategory object """ serializer_class = CategorySerializer queryset = PartCategory.objects.all() class PartDetail(generics.RetrieveUpdateDestroyAPIView): + """ API endpoint for detail view of a single Part object """ queryset = Part.objects.all() serializer_class = PartSerializer @@ -70,6 +82,11 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView): 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 @@ -130,6 +147,11 @@ class PartList(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() serializer_class = BomItemSerializer @@ -151,6 +173,7 @@ class BomList(generics.ListCreateAPIView): class BomDetail(generics.RetrieveUpdateDestroyAPIView): + """ API endpoint for detail view of a single BomItem object """ queryset = BomItem.objects.all() serializer_class = BomItemSerializer @@ -161,6 +184,11 @@ class BomDetail(generics.RetrieveUpdateDestroyAPIView): 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() serializer_class = SupplierPartSerializer @@ -182,6 +210,12 @@ class SupplierPartList(generics.ListCreateAPIView): 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() serializer_class = SupplierPartSerializer @@ -192,6 +226,11 @@ class SupplierPartDetail(generics.RetrieveUpdateDestroyAPIView): 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() serializer_class = SupplierPriceBreakSerializer diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 4d7d05bba1..040444093d 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -1,3 +1,7 @@ +""" +Django Forms for interacting with Part objects +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -10,6 +14,7 @@ from .models import SupplierPart class PartImageForm(HelperForm): + """ Form for uploading a Part image """ class Meta: model = Part @@ -40,6 +45,7 @@ class BomExportForm(HelperForm): class EditPartForm(HelperForm): + """ Form for editing a Part object """ class Meta: model = Part @@ -64,6 +70,7 @@ class EditPartForm(HelperForm): class EditCategoryForm(HelperForm): + """ Form for editing a PartCategory object """ class Meta: model = PartCategory @@ -75,6 +82,7 @@ class EditCategoryForm(HelperForm): class EditBomItemForm(HelperForm): + """ Form for editing a BomItem object """ class Meta: model = BomItem @@ -88,6 +96,7 @@ class EditBomItemForm(HelperForm): class EditSupplierPartForm(HelperForm): + """ Form for editing a SupplierPart object """ class Meta: model = SupplierPart diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index dd30178f39..4022b7ec71 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1,3 +1,7 @@ +""" +Part database model definitions +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -47,11 +51,19 @@ class PartCategory(InvenTreeTree): @property def has_parts(self): + """ True if there are any parts in this category """ return self.parts.count() > 0 @receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log') 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 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 # Format: part_pk. 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__img + """ + base = 'part_images' if filename.count('.') > 0: @@ -248,7 +270,8 @@ class Part(models.Model): @property 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 a customer order """ @@ -311,6 +334,15 @@ class Part(models.Model): 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__filename' + """ # Construct a path to store a file attachment 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') 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 if self.part == self.sub_part: @@ -382,8 +421,9 @@ class BomItem(models.Model): class SupplierPart(models.Model): """ Represents a unique part as provided by a Supplier Each SupplierPart is identified by a MPN (Manufacturer Part Number) - Each SupplierPart is also linked to a Part object - - A Part may be available from multiple suppliers + Each SupplierPart is also linked to a Part object. + + A Part may be available from multiple suppliers """ def get_absolute_url(self): @@ -453,6 +493,7 @@ class SupplierPart(models.Model): def get_price(self, quantity, moq=True, multiples=True): """ Calculate the supplier price based on quantity price breaks. + - If no price breaks available, use the single_price field - Don't forget to add in flat-fee cost (base_cost field) - If MOQ (minimum order quantity) is required, bump quantity diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 68066645bd..0344e46fe7 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -1,3 +1,7 @@ +""" +JSON serializers for Part app +""" + from rest_framework import serializers from .models import Part, PartCategory, BomItem @@ -7,6 +11,7 @@ from InvenTree.serializers import InvenTreeModelSerializer class CategorySerializer(serializers.ModelSerializer): + """ Serializer for PartCategory """ url = serializers.CharField(source='get_absolute_url', read_only=True) @@ -23,6 +28,7 @@ class CategorySerializer(serializers.ModelSerializer): class PartBriefSerializer(serializers.ModelSerializer): + """ Serializer for Part (brief detail) """ url = serializers.CharField(source='get_absolute_url', read_only=True) @@ -68,6 +74,7 @@ class PartSerializer(serializers.ModelSerializer): class BomItemSerializer(InvenTreeModelSerializer): + """ Serializer for BomItem object """ # url = serializers.CharField(source='get_absolute_url', read_only=True) @@ -89,6 +96,7 @@ class BomItemSerializer(InvenTreeModelSerializer): class SupplierPartSerializer(serializers.ModelSerializer): + """ Serializer for SupplierPart object """ url = serializers.CharField(source='get_absolute_url', read_only=True) @@ -112,6 +120,7 @@ class SupplierPartSerializer(serializers.ModelSerializer): class SupplierPriceBreakSerializer(serializers.ModelSerializer): + """ Serializer for SupplierPriceBreak object """ class Meta: model = SupplierPriceBreak diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 14a15625f9..96c830f556 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -1,3 +1,7 @@ +""" +URL lookup for Part app +""" + from django.conf.urls import url, include from . import views diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 4f7175301a..db9f66994c 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1,3 +1,7 @@ +""" +Django views for interacting with Part app +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -21,10 +25,12 @@ from .forms import EditSupplierPartForm from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView -from InvenTree.helpers import DownloadFile +from InvenTree.helpers import DownloadFile, str2bool class PartIndex(ListView): + """ View for displaying list of Part objects + """ model = Part template_name = 'part/category.html' context_object_name = 'parts' @@ -45,8 +51,12 @@ class PartIndex(ListView): class PartCreate(AjaxCreateView): - """ Create a new part - - Optionally provide a category object as initial data + """ View for creating a new Part object. + + Options for providing initial conditions: + + - Provide a category object as initial data + - Copy an existing Part """ model = Part form_class = EditPartForm @@ -64,6 +74,10 @@ class PartCreate(AjaxCreateView): # If a category is provided in the URL, pass that to the page context 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) # Add category information to the page @@ -76,6 +90,11 @@ class PartCreate(AjaxCreateView): # Pre-fill the category field if a valid category is provided 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? part_to_copy = self.request.GET.get('copy', None) @@ -98,15 +117,22 @@ class PartCreate(AjaxCreateView): class PartDetail(DetailView): + """ Detail view for Part object + """ + context_object_name = 'part' queryset = Part.objects.all() template_name = 'part/detail.html' # Add in some extra context information based on query params def get_context_data(self, **kwargs): + """ Provide extra context data to template + + - If '?editing=True', set 'editing_enabled' context variable + """ 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 else: context['editing_enabled'] = 0 @@ -115,6 +141,7 @@ class PartDetail(DetailView): class PartImage(AjaxUpdateView): + """ View for uploading Part image """ model = Part ajax_template_name = 'modal_form.html' @@ -128,6 +155,8 @@ class PartImage(AjaxUpdateView): class PartEdit(AjaxUpdateView): + """ View for editing Part object """ + model = Part template_name = 'part/edit.html' form_class = EditPartForm @@ -214,6 +243,8 @@ class BomDownload(AjaxView): class PartDelete(AjaxDeleteView): + """ View to delete a Part object """ + model = Part template_name = 'part/delete.html' ajax_template_name = 'part/partial_delete.html' @@ -229,6 +260,7 @@ class PartDelete(AjaxDeleteView): class CategoryDetail(DetailView): + """ Detail view for PartCategory """ model = PartCategory context_object_name = 'category' queryset = PartCategory.objects.all() @@ -236,6 +268,7 @@ class CategoryDetail(DetailView): class CategoryEdit(AjaxUpdateView): + """ Update view to edit a PartCategory """ model = PartCategory template_name = 'part/category_edit.html' form_class = EditCategoryForm @@ -251,6 +284,7 @@ class CategoryEdit(AjaxUpdateView): class CategoryDelete(AjaxDeleteView): + """ Delete view to delete a PartCategory """ model = PartCategory template_name = 'part/category_delete.html' context_object_name = 'category' @@ -263,6 +297,7 @@ class CategoryDelete(AjaxDeleteView): class CategoryCreate(AjaxCreateView): + """ Create view to make a new PartCategory """ model = PartCategory ajax_form_action = reverse_lazy('category-create') ajax_form_title = 'Create new part category' @@ -271,6 +306,10 @@ class CategoryCreate(AjaxCreateView): form_class = EditCategoryForm 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() parent_id = self.request.GET.get('category', None) @@ -281,6 +320,10 @@ class CategoryCreate(AjaxCreateView): return context 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() parent_id = self.request.GET.get('category', None) @@ -292,12 +335,14 @@ class CategoryCreate(AjaxCreateView): class BomItemDetail(DetailView): + """ Detail view for BomItem """ context_object_name = 'item' queryset = BomItem.objects.all() template_name = 'part/bom-detail.html' class BomItemCreate(AjaxCreateView): + """ Create view for making a new BomItem object """ model = BomItem form_class = EditBomItemForm template_name = 'part/bom-create.html' @@ -305,6 +350,11 @@ class BomItemCreate(AjaxCreateView): ajax_form_title = 'Create BOM item' def get_initial(self): + """ Provide initial data for the BomItem: + + - If 'parent' provided, set the parent part field + """ + # Look for initial values initials = super(BomItemCreate, self).get_initial().copy() @@ -318,6 +368,8 @@ class BomItemCreate(AjaxCreateView): class BomItemEdit(AjaxUpdateView): + """ Update view for editing BomItem """ + model = BomItem form_class = EditBomItemForm template_name = 'part/bom-edit.html' @@ -326,6 +378,7 @@ class BomItemEdit(AjaxUpdateView): class BomItemDelete(AjaxDeleteView): + """ Delete view for removing BomItem """ model = BomItem template_name = 'part/bom-delete.html' context_object_name = 'item' @@ -333,6 +386,7 @@ class BomItemDelete(AjaxDeleteView): class SupplierPartDetail(DetailView): + """ Detail view for SupplierPart """ model = SupplierPart template_name = 'company/partdetail.html' context_object_name = 'part' @@ -340,6 +394,8 @@ class SupplierPartDetail(DetailView): class SupplierPartEdit(AjaxUpdateView): + """ Update view for editing SupplierPart """ + model = SupplierPart template_name = 'company/partedit.html' context_object_name = 'part' @@ -349,6 +405,8 @@ class SupplierPartEdit(AjaxUpdateView): class SupplierPartCreate(AjaxCreateView): + """ Create view for making new SupplierPart """ + model = SupplierPart form_class = EditSupplierPartForm ajax_template_name = 'modal_form.html' @@ -356,6 +414,11 @@ class SupplierPartCreate(AjaxCreateView): context_object_name = 'part' 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() supplier_id = self.request.GET.get('supplier', None) @@ -374,6 +437,7 @@ class SupplierPartCreate(AjaxCreateView): class SupplierPartDelete(AjaxDeleteView): + """ Delete view for removing a SupplierPart """ model = SupplierPart success_url = '/supplier/' template_name = 'company/partdelete.html' diff --git a/InvenTree/stock/__init__.py b/InvenTree/stock/__init__.py index e69de29bb2..6970329be1 100644 --- a/InvenTree/stock/__init__.py +++ b/InvenTree/stock/__init__.py @@ -0,0 +1,9 @@ +""" +The Stock module is responsible for Stock management. + +It includes models for: + +- StockLocation +- StockItem +- StockItemTracking +""" diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 215a9e311f..c352605ad2 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -1,3 +1,7 @@ +""" +JSON API for the Stock app +""" + from django_filters.rest_framework import FilterSet, DjangoFilterBackend from django_filters import NumberFilter @@ -26,7 +30,7 @@ class StockCategoryTree(TreeSerializer): class StockDetail(generics.RetrieveUpdateDestroyAPIView): - """ + """ API detail endpoint for Stock object get: Return a single StockItem object @@ -44,6 +48,11 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView): 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') max_stock = NumberFilter(name='quantity', lookup_expr='lte') @@ -53,12 +62,11 @@ class StockFilter(FilterSet): 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: - * 'stocktake' - Count the stock item(s) - * 'remove' - Remove the quantity provided from stock - * 'add' - Add the quantity provided from stock + - stocktake: Count the stock item(s) + - remove: Remove the quantity provided from stock + - add: Add the quantity provided from stock """ permission_classes = [ @@ -129,6 +137,7 @@ class StockStocktake(APIView): class StockMove(APIView): + """ API endpoint for performing stock movements """ permission_classes = [ permissions.IsAuthenticatedOrReadOnly, @@ -183,6 +192,11 @@ class StockMove(APIView): 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() @@ -204,14 +218,10 @@ class StockLocationList(generics.ListCreateAPIView): class StockList(generics.ListCreateAPIView): - """ + """ API endpoint for list view of Stock objects - get: - Return a list of all StockItem objects - (with optional query filters) - - post: - Create a new StockItem + - GET: Return a list of all StockItem objects (with optional query filters) + - POST: Create a new StockItem """ def get_queryset(self): @@ -268,6 +278,7 @@ class StockList(generics.ListCreateAPIView): class StockStocktakeEndpoint(generics.UpdateAPIView): + """ API endpoint for performing stocktake """ queryset = StockItem.objects.all() serializer_class = StockQuantitySerializer @@ -283,6 +294,13 @@ class StockStocktakeEndpoint(generics.UpdateAPIView): 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() serializer_class = StockTrackingSerializer @@ -312,17 +330,11 @@ class StockTrackingList(generics.ListCreateAPIView): class LocationDetail(generics.RetrieveUpdateDestroyAPIView): - """ - - get: - Return a single StockLocation object - - post: - Update a StockLocation object - - delete: - Remove a StockLocation object + """ API endpoint for detail view of StockLocation object + - GET: Return a single StockLocation object + - PATCH: Update a StockLocation object + - DELETE: Remove a StockLocation object """ queryset = StockLocation.objects.all() diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index bd0254e720..a94156ad29 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -1,3 +1,7 @@ +""" +Django Forms for interacting with Stock app +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -8,6 +12,7 @@ from .models import StockLocation, StockItem class EditStockLocationForm(HelperForm): + """ Form for editing a StockLocation """ class Meta: model = StockLocation @@ -19,6 +24,7 @@ class EditStockLocationForm(HelperForm): class CreateStockItemForm(HelperForm): + """ Form for creating a new StockItem """ class Meta: model = StockItem @@ -38,6 +44,7 @@ class CreateStockItemForm(HelperForm): 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)') @@ -60,6 +67,7 @@ class StocktakeForm(forms.ModelForm): class EditStockItemForm(HelperForm): + """ Form for editing a StockItem object """ class Meta: model = StockItem diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4b1505d2d0..636efeb293 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -1,3 +1,8 @@ +""" +Stock database model definitions +""" + + # -*- coding: utf-8 -*- from __future__ import unicode_literals diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 64baeb5330..0a863aa7f9 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -1,3 +1,7 @@ +""" +JSON serializers for Stock app +""" + from rest_framework import serializers from .models import StockItem, StockLocation @@ -44,8 +48,8 @@ class StockItemSerializerBrief(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 """ @@ -112,6 +116,7 @@ class LocationSerializer(serializers.ModelSerializer): class StockTrackingSerializer(serializers.ModelSerializer): + """ Serializer for StockItemTracking model """ url = serializers.CharField(source='get_absolute_url', read_only=True) diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index 503c9e809c..6b3fc0ff2d 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -1,3 +1,7 @@ +""" +URL lookup for Stock app +""" + from django.conf.urls import url, include from . import views diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index fc1d2726aa..70d3f7ded3 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -1,3 +1,7 @@ +""" +Django views for interacting with Stock app +""" + # -*- coding: utf-8 -*- from __future__ import unicode_literals @@ -19,8 +23,7 @@ from .forms import StocktakeForm class StockIndex(ListView): - """ - StockIndex view loads all StockLocation and StockItem object + """ StockIndex view loads all StockLocation and StockItem object """ model = StockItem template_name = 'stock/location.html' diff --git a/docs/conf.py b/docs/conf.py index 78bcb74673..d1b8f1c5b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,15 +28,28 @@ copyright = '2019, InvenTree' # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'autoapi.extension' + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'autoapi.extension', ] +napoleon_google_docstring = True +napoleon_numpy_docstring = False + autoapi_dirs = [ '../InvenTree', + '../Inventree/build', + '../InvenTree/company', + '../InvenTree/InvenTree', + '../InvenTree/part', + '../InvenTree/stock', + '../InvenTree/users', ] autoapi_options = [ 'members', + 'private-members', + 'special-members', ] autoapi_type = 'python' @@ -44,11 +57,21 @@ autoapi_type = 'python' autoapi_ignore = [ '*migrations*', '**/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. -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 # directories to ignore when looking for source files. @@ -73,3 +96,11 @@ html_theme = 'sphinx_rtd_theme' # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] + +# Table of contents in sidebar +html_sidebars = {'**': [ + 'globaltoc.html', + 'relations.html', + 'sourcelink.html', + 'searchbox.html' +]} \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 9c43b19929..114ecaf27b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,20 +1,12 @@ -.. InvenTree documentation master file, created by - 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. +InvenTree's Django Documentation +================================ -Welcome to InvenTree's documentation! -===================================== +This documentation is auto-generated from the `InvenTree codebase `_ .. toctree:: + :titlesonly: :maxdepth: 2 :caption: Contents: + :hidden: - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` + InvenTree diff --git a/docs/introduction.rst b/docs/introduction.rst new file mode 100644 index 0000000000..e117010749 --- /dev/null +++ b/docs/introduction.rst @@ -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 `_ - High level management functions +* `Build `_ - Part build projects +* `Company `_ - Company management (suppliers / customers) +* `Part `_ - Part management +* `Stock `_ - Stock management \ No newline at end of file diff --git a/docs/templates/layout.html b/docs/templates/layout.html new file mode 100644 index 0000000000..620814d1b3 --- /dev/null +++ b/docs/templates/layout.html @@ -0,0 +1,7 @@ +{% extends "!layout.html" %} + + {% block menu %} + {{ super() }} + Module Index + Index + {% endblock %} \ No newline at end of file diff --git a/docs/templates/python/module.rst b/docs/templates/python/module.rst new file mode 100644 index 0000000000..ec51763131 --- /dev/null +++ b/docs/templates/python/module.rst @@ -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 %}