mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
c0e69d7a99
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||
|
@ -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.
|
||||
"""
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
"""
|
||||
The Build module is responsible for managing "Build" transactions.
|
||||
|
||||
A Build consumes parts from stock to create new parts
|
||||
"""
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""
|
||||
URL lookup for Build app
|
||||
"""
|
||||
|
||||
from django.conf.urls import url, include
|
||||
|
||||
from . import views
|
||||
|
@ -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'
|
||||
|
@ -0,0 +1,8 @@
|
||||
"""
|
||||
The Company module is responsible for managing Company interactions.
|
||||
|
||||
A company can be either (or both):
|
||||
|
||||
- Supplier
|
||||
- Customer
|
||||
"""
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -1,3 +1,8 @@
|
||||
"""
|
||||
URL lookup for Company app
|
||||
"""
|
||||
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,10 @@
|
||||
"""
|
||||
The Part module is responsible for Part management.
|
||||
|
||||
It includes models for:
|
||||
|
||||
- PartCategory
|
||||
- Part
|
||||
- SupplierPart
|
||||
- BomItem
|
||||
"""
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.<img>
|
||||
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'
|
||||
|
||||
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_<pk>_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
|
||||
|
@ -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
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""
|
||||
URL lookup for Part app
|
||||
"""
|
||||
|
||||
from django.conf.urls import url, include
|
||||
|
||||
from . import views
|
||||
|
@ -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'
|
||||
|
@ -0,0 +1,9 @@
|
||||
"""
|
||||
The Stock module is responsible for Stock management.
|
||||
|
||||
It includes models for:
|
||||
|
||||
- StockLocation
|
||||
- StockItem
|
||||
- StockItemTracking
|
||||
"""
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -1,3 +1,8 @@
|
||||
"""
|
||||
Stock database model definitions
|
||||
"""
|
||||
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""
|
||||
URL lookup for Stock app
|
||||
"""
|
||||
|
||||
from django.conf.urls import url, include
|
||||
|
||||
from . import views
|
||||
|
@ -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'
|
||||
|
31
docs/conf.py
31
docs/conf.py
@ -28,15 +28,22 @@ 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',
|
||||
]
|
||||
|
||||
autoapi_options = [
|
||||
'members',
|
||||
'private-members',
|
||||
'special-members',
|
||||
]
|
||||
|
||||
autoapi_type = 'python'
|
||||
@ -44,11 +51,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 +90,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'
|
||||
]}
|
@ -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 <https://github.com/InvenTree/InvenTree>`_
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
:hidden:
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
InvenTree <introduction>
|
||||
|
18
docs/introduction.rst
Normal file
18
docs/introduction.rst
Normal 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
7
docs/templates/layout.html
vendored
Normal 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
97
docs/templates/python/module.rst
vendored
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user