mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
16f3ca589b
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
* text=auto
|
||||
|
||||
*.py text
|
||||
*.md text
|
||||
*.html text
|
||||
*.txt text
|
12
.travis.yml
12
.travis.yml
@ -3,11 +3,9 @@ python:
|
||||
- 3.4
|
||||
|
||||
before_install:
|
||||
- pip install pep8
|
||||
- pip install django
|
||||
- pip install djangorestframework
|
||||
|
||||
- make setup
|
||||
- make setup_ci
|
||||
|
||||
script:
|
||||
- "pep8 --exclude=migrations --ignore=E402,W293,E501 InvenTree"
|
||||
- python InvenTree/manage.py check
|
||||
- python InvenTree/manage.py test --noinput
|
||||
- make style
|
||||
- make test
|
||||
|
@ -1,17 +1,16 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
""" Abstract model representing an external company
|
||||
"""
|
||||
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
name = models.CharField(max_length=100)
|
||||
URL = models.URLField(blank=True)
|
||||
address = models.CharField(max_length=200,
|
||||
@ -33,93 +32,116 @@ class InvenTreeTree(models.Model):
|
||||
- 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)
|
||||
"""
|
||||
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.CharField(max_length=250)
|
||||
description = models.CharField(max_length=250, blank=True)
|
||||
parent = models.ForeignKey('self',
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
null=True)
|
||||
|
||||
null=True,
|
||||
related_name='children')
|
||||
|
||||
def getUniqueParents(self, unique=None):
|
||||
""" Return a flat set of all parent items that exist above this node.
|
||||
If any parents are repeated (which would be very bad!), the process is halted
|
||||
"""
|
||||
|
||||
if unique is None:
|
||||
unique = set()
|
||||
else:
|
||||
unique.add(self.id)
|
||||
|
||||
if self.parent and self.parent.id not in unique:
|
||||
self.parent.getUniqueParents(unique)
|
||||
|
||||
return unique
|
||||
|
||||
def getUniqueChildren(self, unique=None):
|
||||
""" Return a flat set of all child items that exist under this node.
|
||||
If any child items are repeated, the repetitions are omitted.
|
||||
"""
|
||||
|
||||
|
||||
if unique is None:
|
||||
unique = set()
|
||||
|
||||
|
||||
if self.id in unique:
|
||||
return unique
|
||||
|
||||
|
||||
unique.add(self.id)
|
||||
|
||||
|
||||
# Some magic to get around the limitations of abstract models
|
||||
contents = ContentType.objects.get_for_model(type(self))
|
||||
children = contents.get_all_objects_for_this_type(parent=self.id)
|
||||
|
||||
|
||||
for child in children:
|
||||
child.getUniqueChildren(unique)
|
||||
|
||||
|
||||
return unique
|
||||
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
contents = ContentType.objects.get_for_model(type(self))
|
||||
children = contents.get_all_objects_for_this_type(parent=self.id)
|
||||
|
||||
return children
|
||||
|
||||
def getAcceptableParents(self):
|
||||
""" Returns a list of acceptable parent items within this model
|
||||
Acceptable parents are ones which are not underneath this item.
|
||||
Setting the parent of an item to its own child results in recursion.
|
||||
"""
|
||||
contents = ContentType.objects.get_for_model(type(self))
|
||||
|
||||
|
||||
available = contents.get_all_objects_for_this_type()
|
||||
|
||||
|
||||
# List of child IDs
|
||||
childs = getUniqueChildren()
|
||||
|
||||
childs = self.getUniqueChildren()
|
||||
|
||||
acceptable = [None]
|
||||
|
||||
|
||||
for a in available:
|
||||
if a.id not in childs:
|
||||
acceptable.append(a)
|
||||
|
||||
|
||||
return acceptable
|
||||
|
||||
|
||||
@property
|
||||
def parentpath(self):
|
||||
""" Return 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
|
||||
"""
|
||||
|
||||
|
||||
if self.parent:
|
||||
return self.parent.parentpath + [self.parent]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
if self.parent:
|
||||
return "/".join([p.name for p in self.parentpath]) + "/" + self.name
|
||||
else:
|
||||
return self.name
|
||||
|
||||
|
||||
def __setattr__(self, attrname, val):
|
||||
""" Custom Attribute Setting function
|
||||
|
||||
|
||||
Parent:
|
||||
Setting the parent of an item to its own child results in an infinite loop.
|
||||
The parent of an item cannot be set to:
|
||||
a) Its own ID
|
||||
b) The ID of any child items that exist underneath it
|
||||
|
||||
|
||||
Name:
|
||||
Tree node names are limited to a reduced character set
|
||||
"""
|
||||
|
||||
|
||||
if attrname == 'parent_id':
|
||||
# If current ID is None, continue
|
||||
# - This object is just being created
|
||||
@ -140,14 +162,14 @@ class InvenTreeTree(models.Model):
|
||||
# Prohibit certain characters from tree node names
|
||||
elif attrname == 'name':
|
||||
val = val.translate({ord(c): None for c in "!@#$%^&*'\"\\/[]{}<>,|+=~`"})
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
return self.path
|
||||
|
@ -20,6 +20,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
# TODO: remove this
|
||||
SECRET_KEY = 'oc2z%5)lu#jsxi#wpg)700z@v48)2aa_yn(a(3qg!z!fw&tr9f'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
@ -40,7 +41,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
|
||||
# InvenTree apps
|
||||
'part.apps.PartConfig',
|
||||
'project.apps.ProjectConfig',
|
||||
|
@ -1,19 +1,3 @@
|
||||
"""InvenTree URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/1.10/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.conf.urls import url, include
|
||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.contrib import admin
|
||||
|
||||
|
@ -11,7 +11,7 @@ if __name__ == "__main__":
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django
|
||||
import django # NOQA
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
|
@ -4,14 +4,14 @@ from .models import PartCategory, Part, PartParameter, PartParameterTemplate, Ca
|
||||
|
||||
|
||||
class PartAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
list_display = ('name', 'IPN', 'stock', 'category')
|
||||
|
||||
|
||||
class PartCategoryAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
list_display = ('name', 'path', 'description')
|
||||
|
||||
|
||||
|
||||
class ParameterTemplateAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'units', 'format')
|
||||
@ -20,7 +20,7 @@ class ParameterTemplateAdmin(admin.ModelAdmin):
|
||||
class ParameterAdmin(admin.ModelAdmin):
|
||||
list_display = ('part', 'template', 'value')
|
||||
|
||||
|
||||
|
||||
admin.site.register(Part, PartAdmin)
|
||||
admin.site.register(PartCategory, PartCategoryAdmin)
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
|
||||
from InvenTree.models import InvenTreeTree
|
||||
|
||||
@ -10,23 +9,42 @@ from InvenTree.models import InvenTreeTree
|
||||
class PartCategory(InvenTreeTree):
|
||||
""" PartCategory provides hierarchical organization of Part objects.
|
||||
"""
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Part Category"
|
||||
verbose_name_plural = "Part Categories"
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def parts(self):
|
||||
return self.part_set.all()
|
||||
|
||||
|
||||
class Part(models.Model):
|
||||
""" Represents a """
|
||||
|
||||
|
||||
# Short name of the part
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
# Longer description of the part (optional)
|
||||
description = models.CharField(max_length=250, blank=True)
|
||||
|
||||
# Internal Part Number (optional)
|
||||
IPN = models.CharField(max_length=100, blank=True)
|
||||
|
||||
# Part category - all parts must be assigned to a category
|
||||
category = models.ForeignKey(PartCategory, on_delete=models.CASCADE)
|
||||
minimum_stock = models.IntegerField(default=0)
|
||||
|
||||
# Minimum "allowed" stock level
|
||||
minimum_stock = models.PositiveIntegerField(default=0)
|
||||
|
||||
# Units of quantity for this part. Default is "pcs"
|
||||
units = models.CharField(max_length=20, default="pcs", blank=True)
|
||||
|
||||
# Is this part "trackable"?
|
||||
# Trackable parts can have unique instances which are assigned serial numbers
|
||||
# and can have their movements tracked
|
||||
trackable = models.BooleanField(default=False)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
if self.IPN:
|
||||
return "{name} ({ipn})".format(
|
||||
@ -34,46 +52,40 @@ class Part(models.Model):
|
||||
name=self.name)
|
||||
else:
|
||||
return self.name
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Part"
|
||||
verbose_name_plural = "Parts"
|
||||
|
||||
@property
|
||||
def stock_list(self):
|
||||
""" Return a list of all stock objects associated with this part
|
||||
"""
|
||||
|
||||
return self.stockitem_set.all()
|
||||
|
||||
|
||||
@property
|
||||
def stock(self):
|
||||
""" Return the total stock quantity for this part.
|
||||
Part may be stored in multiple locations
|
||||
"""
|
||||
|
||||
stocks = self.stock_list
|
||||
|
||||
stocks = self.locations.all()
|
||||
if len(stocks) == 0:
|
||||
return 0
|
||||
|
||||
|
||||
result = stocks.aggregate(total=Sum('quantity'))
|
||||
return result['total']
|
||||
|
||||
|
||||
@property
|
||||
def projects(self):
|
||||
""" Return a list of unique projects that this part is associated with
|
||||
""" Return a list of unique projects that this part is associated with.
|
||||
A part may be used in zero or more projects.
|
||||
"""
|
||||
|
||||
|
||||
project_ids = set()
|
||||
project_parts = self.projectpart_set.all()
|
||||
|
||||
|
||||
projects = []
|
||||
|
||||
|
||||
for pp in project_parts:
|
||||
if pp.project.id not in project_ids:
|
||||
project_ids.add(pp.project.id)
|
||||
projects.append(pp.project)
|
||||
|
||||
|
||||
return projects
|
||||
|
||||
|
||||
@ -85,28 +97,31 @@ class PartParameterTemplate(models.Model):
|
||||
name = models.CharField(max_length=20)
|
||||
description = models.CharField(max_length=100, blank=True)
|
||||
units = models.CharField(max_length=10, blank=True)
|
||||
|
||||
|
||||
default_value = models.CharField(max_length=50, blank=True)
|
||||
default_min = models.CharField(max_length=50, blank=True)
|
||||
default_max = models.CharField(max_length=50, blank=True)
|
||||
|
||||
|
||||
# Parameter format
|
||||
PARAM_NUMERIC = 10
|
||||
PARAM_TEXT = 20
|
||||
PARAM_BOOL = 30
|
||||
|
||||
format = models.IntegerField(
|
||||
|
||||
PARAM_TYPE_CODES = {
|
||||
PARAM_NUMERIC: _("Numeric"),
|
||||
PARAM_TEXT: _("Text"),
|
||||
PARAM_BOOL: _("Bool")
|
||||
}
|
||||
|
||||
format = models.PositiveIntegerField(
|
||||
default=PARAM_NUMERIC,
|
||||
choices=[
|
||||
(PARAM_NUMERIC, "Numeric"),
|
||||
(PARAM_TEXT, "Text"),
|
||||
(PARAM_BOOL, "Boolean")])
|
||||
|
||||
choices=PARAM_TYPE_CODES.items())
|
||||
|
||||
def __str__(self):
|
||||
return "{name} ({units})".format(
|
||||
name=self.name,
|
||||
units=self.units)
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Parameter Template"
|
||||
verbose_name_plural = "Parameter Templates"
|
||||
@ -117,7 +132,7 @@ class CategoryParameterLink(models.Model):
|
||||
"""
|
||||
category = models.ForeignKey(PartCategory, on_delete=models.CASCADE)
|
||||
template = models.ForeignKey(PartParameterTemplate, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "{name} - {cat}".format(
|
||||
name=self.template.name,
|
||||
@ -126,42 +141,49 @@ class CategoryParameterLink(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Category Parameter"
|
||||
verbose_name_plural = "Category Parameters"
|
||||
|
||||
|
||||
|
||||
class PartParameter(models.Model):
|
||||
""" PartParameter is associated with a single part
|
||||
"""
|
||||
|
||||
part = models.ForeignKey(Part, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
part = models.ForeignKey(Part, on_delete=models.CASCADE, related_name='parameters')
|
||||
template = models.ForeignKey(PartParameterTemplate)
|
||||
|
||||
|
||||
# Value data
|
||||
value = models.CharField(max_length=50, blank=True)
|
||||
min_value = models.CharField(max_length=50, blank=True)
|
||||
max_value = models.CharField(max_length=50, blank=True)
|
||||
|
||||
|
||||
# Prevent multiple parameters of the same template
|
||||
# from being added to the same part
|
||||
def save(self, *args, **kwargs):
|
||||
params = PartParameter.objects.filter(part=self.part, template=self.template)
|
||||
if len(params) > 0:
|
||||
raise ValidationError("Parameter '{param}' already exists for {part}".format(
|
||||
param=self.template.name,
|
||||
part=self.part.name))
|
||||
|
||||
if len(params) > 1:
|
||||
return
|
||||
if len(params) == 1 and params[0].id != self.id:
|
||||
return
|
||||
|
||||
super(PartParameter, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return "{name} : {val}{units}".format(
|
||||
name=self.template.name,
|
||||
val=self.value,
|
||||
units=self.template.units)
|
||||
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return self.template.units
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.template.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Part Parameter"
|
||||
verbose_name_plural = "Part Parameters"
|
||||
|
||||
|
||||
|
||||
class PartRevision(models.Model):
|
||||
""" A PartRevision represents a change-notification to a Part
|
||||
@ -169,12 +191,12 @@ class PartRevision(models.Model):
|
||||
which should be tracked.
|
||||
UniqueParts can have a single associated PartRevision
|
||||
"""
|
||||
|
||||
|
||||
part = models.ForeignKey(Part, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.CharField(max_length=500)
|
||||
revision_date = models.DateField(auto_now_add=True)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -1,22 +1,60 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Part, PartCategory
|
||||
from .models import Part, PartCategory, PartParameter
|
||||
|
||||
|
||||
class PartParameterSerializer(serializers.ModelSerializer):
|
||||
""" Serializer for a PartParameter
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = PartParameter
|
||||
fields = ('pk',
|
||||
'part',
|
||||
'template',
|
||||
'name',
|
||||
'value',
|
||||
'units')
|
||||
|
||||
|
||||
class PartSerializer(serializers.ModelSerializer):
|
||||
""" Serializer for complete detail information of a part.
|
||||
Used when displaying all details of a single component.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Part
|
||||
fields = ('pk',
|
||||
'name',
|
||||
'IPN',
|
||||
'description',
|
||||
'category',
|
||||
'stock')
|
||||
|
||||
|
||||
class PartCategorySerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class PartCategoryBriefSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PartCategory
|
||||
fields = ('pk',
|
||||
'name',
|
||||
'description')
|
||||
|
||||
|
||||
class PartCategoryDetailSerializer(serializers.ModelSerializer):
|
||||
|
||||
# List of parts in this category
|
||||
parts = PartSerializer(many=True)
|
||||
|
||||
# List of child categories under this one
|
||||
children = PartCategoryBriefSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = PartCategory
|
||||
fields = ('pk',
|
||||
'name',
|
||||
'description',
|
||||
'path')
|
||||
'parent',
|
||||
'path',
|
||||
'children',
|
||||
'parts')
|
||||
|
@ -1,3 +1,3 @@
|
||||
from django.test import TestCase
|
||||
# from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
@ -3,15 +3,18 @@ from django.conf.urls import url
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
# Display part detail
|
||||
# Single part detail
|
||||
url(r'^(?P<pk>[0-9]+)/$', views.PartDetail.as_view()),
|
||||
|
||||
# Display a single part category
|
||||
|
||||
# Part parameters list
|
||||
url(r'^(?P<pk>[0-9]+)/parameters/$', views.PartParameters.as_view()),
|
||||
|
||||
# Part category detail
|
||||
url(r'^category/(?P<pk>[0-9]+)/$', views.PartCategoryDetail.as_view()),
|
||||
|
||||
# Display a list of top-level categories
|
||||
|
||||
# List of top-level categories
|
||||
url(r'^category/$', views.PartCategoryList.as_view()),
|
||||
|
||||
# Display list of parts
|
||||
|
||||
# List of all parts
|
||||
url(r'^$', views.PartList.as_view())
|
||||
]
|
||||
|
@ -1,14 +1,9 @@
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse, Http404
|
||||
|
||||
from rest_framework import generics
|
||||
|
||||
from .models import PartCategory, Part
|
||||
from .serializers import PartSerializer, PartCategorySerializer
|
||||
|
||||
|
||||
def index(request):
|
||||
return HttpResponse("Hello world. This is the parts page")
|
||||
from .models import PartCategory, Part, PartParameter
|
||||
from .serializers import PartSerializer
|
||||
from .serializers import PartCategoryDetailSerializer
|
||||
from .serializers import PartParameterSerializer
|
||||
|
||||
|
||||
class PartDetail(generics.RetrieveAPIView):
|
||||
@ -17,6 +12,15 @@ class PartDetail(generics.RetrieveAPIView):
|
||||
serializer_class = PartSerializer
|
||||
|
||||
|
||||
class PartParameters(generics.ListAPIView):
|
||||
|
||||
def get_queryset(self):
|
||||
part_id = self.kwargs['pk']
|
||||
return PartParameter.objects.filter(part=part_id)
|
||||
|
||||
serializer_class = PartParameterSerializer
|
||||
|
||||
|
||||
class PartList(generics.ListAPIView):
|
||||
|
||||
queryset = Part.objects.all()
|
||||
@ -24,12 +28,15 @@ class PartList(generics.ListAPIView):
|
||||
|
||||
|
||||
class PartCategoryDetail(generics.RetrieveAPIView):
|
||||
|
||||
""" Return information on a single PartCategory
|
||||
"""
|
||||
queryset = PartCategory.objects.all()
|
||||
serializer_class = PartCategorySerializer
|
||||
serializer_class = PartCategoryDetailSerializer
|
||||
|
||||
|
||||
class PartCategoryList(generics.ListAPIView):
|
||||
|
||||
queryset = PartCategory.objects.all()
|
||||
serializer_class = PartCategorySerializer
|
||||
""" Return a list of all top-level part categories.
|
||||
Categories are considered "top-level" if they do not have a parent
|
||||
"""
|
||||
queryset = PartCategory.objects.filter(parent=None)
|
||||
serializer_class = PartCategoryDetailSerializer
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import ProjectCategory, Project, ProjectPart
|
||||
from .models import ProjectCategory, Project, ProjectPart, ProjectRun
|
||||
|
||||
|
||||
class ProjectCategoryAdmin(admin.ModelAdmin):
|
||||
@ -12,8 +12,14 @@ class ProjectAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
class ProjectPartAdmin(admin.ModelAdmin):
|
||||
list_display = ('part', 'project', 'quantity')
|
||||
list_display = ('part', 'project', 'quantity', 'output')
|
||||
|
||||
|
||||
class ProjectRunAdmin(admin.ModelAdmin):
|
||||
list_display = ('project', 'quantity', 'run_date')
|
||||
|
||||
|
||||
admin.site.register(ProjectCategory, ProjectCategoryAdmin)
|
||||
admin.site.register(Project, ProjectAdmin)
|
||||
admin.site.register(ProjectPart, ProjectPartAdmin)
|
||||
admin.site.register(ProjectRun, ProjectRunAdmin)
|
||||
|
@ -1,4 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from django.db import models
|
||||
|
||||
@ -11,24 +12,28 @@ class ProjectCategory(InvenTreeTree):
|
||||
Each ProjectCategory can contain zero-or-more child categories,
|
||||
and in turn can have zero-or-one parent category.
|
||||
"""
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Project Category"
|
||||
verbose_name_plural = "Project Categories"
|
||||
|
||||
@property
|
||||
def projects(self):
|
||||
return self.project_set.all()
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
""" A Project takes multiple Part objects.
|
||||
A project can output zero-or-more Part objects
|
||||
"""
|
||||
|
||||
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.CharField(max_length=500, blank=True)
|
||||
category = models.ForeignKey(ProjectCategory, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@property
|
||||
def projectParts(self):
|
||||
""" Return a list of all project parts associated with this project
|
||||
@ -41,23 +46,44 @@ class ProjectPart(models.Model):
|
||||
The quantity of parts required for a single-run of that project is stored.
|
||||
The overage is the number of extra parts that are generally used for a single run.
|
||||
"""
|
||||
|
||||
|
||||
# Overage types
|
||||
OVERAGE_PERCENT = 0
|
||||
OVERAGE_ABSOLUTE = 1
|
||||
|
||||
|
||||
OVARAGE_CODES = {
|
||||
OVERAGE_PERCENT: _("Percent"),
|
||||
OVERAGE_ABSOLUTE: _("Absolute")
|
||||
}
|
||||
|
||||
part = models.ForeignKey(Part, on_delete=models.CASCADE)
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE)
|
||||
quantity = models.IntegerField(default=1)
|
||||
quantity = models.PositiveIntegerField(default=1)
|
||||
overage = models.FloatField(default=0)
|
||||
overage_type = models.IntegerField(
|
||||
default=1,
|
||||
choices=[
|
||||
(OVERAGE_PERCENT, "Percent"),
|
||||
(OVERAGE_ABSOLUTE, "Absolute")
|
||||
])
|
||||
|
||||
overage_type = models.PositiveIntegerField(
|
||||
default=OVERAGE_ABSOLUTE,
|
||||
choices=OVARAGE_CODES.items())
|
||||
|
||||
# Set if the part is generated by the project,
|
||||
# rather than being consumed by the project
|
||||
output = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return "{quan} x {name}".format(
|
||||
name=self.part.name,
|
||||
quan=self.quantity)
|
||||
|
||||
|
||||
class ProjectRun(models.Model):
|
||||
""" A single run of a particular project.
|
||||
Tracks the number of 'units' made in the project.
|
||||
Provides functionality to update stock,
|
||||
based on both:
|
||||
a) Parts used (project inputs)
|
||||
b) Parts produced (project outputs)
|
||||
"""
|
||||
|
||||
project = models.ForeignKey(Project, on_delete=models.CASCADE)
|
||||
quantity = models.PositiveIntegerField(default=1)
|
||||
|
||||
run_date = models.DateField(auto_now_add=True)
|
||||
|
64
InvenTree/project/serializers.py
Normal file
64
InvenTree/project/serializers.py
Normal file
@ -0,0 +1,64 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import ProjectCategory, Project, ProjectPart
|
||||
|
||||
|
||||
class ProjectPartSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProjectPart
|
||||
fields = ('pk',
|
||||
'part',
|
||||
'project',
|
||||
'quantity',
|
||||
'overage',
|
||||
'overage_type',
|
||||
'output')
|
||||
|
||||
|
||||
class ProjectBriefSerializer(serializers.ModelSerializer):
|
||||
""" Serializer for displaying brief overview of a project
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ('pk',
|
||||
'name',
|
||||
'description',
|
||||
'category')
|
||||
|
||||
|
||||
class ProjectDetailSerializer(serializers.ModelSerializer):
|
||||
""" Serializer for detailed project information
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = ('pk',
|
||||
'name',
|
||||
'description',
|
||||
'category')
|
||||
|
||||
|
||||
class ProjectCategoryBriefSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProjectCategory
|
||||
fields = ('pk', 'name', 'description')
|
||||
|
||||
|
||||
class ProjectCategoryDetailSerializer(serializers.ModelSerializer):
|
||||
|
||||
projects = ProjectBriefSerializer(many=True)
|
||||
|
||||
children = ProjectCategoryBriefSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = ProjectCategory
|
||||
fields = ('pk',
|
||||
'name',
|
||||
'description',
|
||||
'parent',
|
||||
'path',
|
||||
'children',
|
||||
'projects')
|
@ -1,3 +1,3 @@
|
||||
from django.test import TestCase
|
||||
# from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
@ -3,5 +3,18 @@ from django.conf.urls import url
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.index, name='index')
|
||||
# Single project detail
|
||||
url(r'^(?P<pk>[0-9]+)/$', views.ProjectDetail.as_view()),
|
||||
|
||||
# Parts associated with a project
|
||||
url(r'^(?P<pk>[0-9]+)/parts$', views.ProjectPartsList.as_view()),
|
||||
|
||||
# List of all projects
|
||||
url(r'^$', views.ProjectList.as_view()),
|
||||
|
||||
# List of top-level project categories
|
||||
url(r'^category/$', views.ProjectCategoryList.as_view()),
|
||||
|
||||
# Detail of a single project category
|
||||
url(r'^category/(?P<pk>[0-9]+)/$', views.ProjectCategoryDetail.as_view())
|
||||
]
|
||||
|
@ -1,6 +1,39 @@
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from rest_framework import generics
|
||||
|
||||
from .models import ProjectCategory, Project, ProjectPart
|
||||
from .serializers import ProjectBriefSerializer, ProjectDetailSerializer
|
||||
from .serializers import ProjectCategoryDetailSerializer
|
||||
from .serializers import ProjectPartSerializer
|
||||
|
||||
|
||||
def index(request):
|
||||
return HttpResponse("This is the Projects page")
|
||||
class ProjectDetail(generics.RetrieveAPIView):
|
||||
|
||||
queryset = Project.objects.all()
|
||||
serializer_class = ProjectDetailSerializer
|
||||
|
||||
|
||||
class ProjectList(generics.ListAPIView):
|
||||
|
||||
queryset = Project.objects.all()
|
||||
serializer_class = ProjectBriefSerializer
|
||||
|
||||
|
||||
class ProjectCategoryDetail(generics.RetrieveAPIView):
|
||||
|
||||
queryset = ProjectCategory.objects.all()
|
||||
serializer_class = ProjectCategoryDetailSerializer
|
||||
|
||||
|
||||
class ProjectCategoryList(generics.ListAPIView):
|
||||
|
||||
queryset = ProjectCategory.objects.filter(parent=None)
|
||||
serializer_class = ProjectCategoryDetailSerializer
|
||||
|
||||
|
||||
class ProjectPartsList(generics.ListAPIView):
|
||||
|
||||
serializer_class = ProjectPartSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
project_id = self.kwargs['pk']
|
||||
return ProjectPart.objects.filter(project=project_id)
|
||||
|
@ -1,14 +1,15 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Warehouse, StockItem
|
||||
from .models import StockLocation, StockItem
|
||||
|
||||
|
||||
class WarehouseAdmin(admin.ModelAdmin):
|
||||
class LocationAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'path', 'description')
|
||||
|
||||
|
||||
class StockItemAdmin(admin.ModelAdmin):
|
||||
list_display = ('part', 'quantity', 'location', 'status', 'updated')
|
||||
|
||||
admin.site.register(Warehouse, WarehouseAdmin)
|
||||
|
||||
admin.site.register(StockLocation, LocationAdmin)
|
||||
admin.site.register(StockItem, StockItemAdmin)
|
||||
|
@ -1,36 +1,60 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db import models
|
||||
|
||||
from part.models import Part
|
||||
from InvenTree.models import InvenTreeTree
|
||||
|
||||
|
||||
class Warehouse(InvenTreeTree):
|
||||
pass
|
||||
|
||||
class StockLocation(InvenTreeTree):
|
||||
""" Organization tree for StockItem objects
|
||||
"""
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
stock_list = self.stockitem_set.all()
|
||||
return stock_list
|
||||
|
||||
|
||||
class StockItem(models.Model):
|
||||
part = models.ForeignKey(Part,
|
||||
on_delete=models.CASCADE)
|
||||
location = models.ForeignKey(Warehouse, on_delete=models.CASCADE)
|
||||
quantity = models.IntegerField()
|
||||
on_delete=models.CASCADE,
|
||||
related_name='locations')
|
||||
location = models.ForeignKey(StockLocation, on_delete=models.CASCADE)
|
||||
quantity = models.PositiveIntegerField()
|
||||
updated = models.DateField(auto_now=True)
|
||||
|
||||
|
||||
# last time the stock was checked / counted
|
||||
last_checked = models.DateField(blank=True, null=True)
|
||||
|
||||
review_needed = models.BooleanField(default=False)
|
||||
|
||||
# Stock status types
|
||||
ITEM_IN_PROGRESS = 0
|
||||
ITEM_DAMAGED = 10
|
||||
ITEM_ATTENTION = 20
|
||||
ITEM_COMPLETE = 50
|
||||
|
||||
status = models.IntegerField(default=ITEM_IN_PROGRESS,
|
||||
choices=[
|
||||
(ITEM_IN_PROGRESS, "In progress"),
|
||||
(ITEM_DAMAGED, "Damaged"),
|
||||
(ITEM_ATTENTION, "Requires attention"),
|
||||
(ITEM_COMPLETE, "Complete")
|
||||
])
|
||||
|
||||
ITEM_IN_STOCK = 10
|
||||
ITEM_INCOMING = 15
|
||||
ITEM_IN_PROGRESS = 20
|
||||
ITEM_COMPLETE = 25
|
||||
ITEM_ATTENTION = 50
|
||||
ITEM_DAMAGED = 55
|
||||
ITEM_DESTROYED = 60
|
||||
|
||||
ITEM_STATUS_CODES = {
|
||||
ITEM_IN_STOCK: _("In stock"),
|
||||
ITEM_INCOMING: _("Incoming"),
|
||||
ITEM_IN_PROGRESS: _("In progress"),
|
||||
ITEM_COMPLETE: _("Complete"),
|
||||
ITEM_ATTENTION: _("Attention needed"),
|
||||
ITEM_DAMAGED: _("Damaged"),
|
||||
ITEM_DESTROYED: _("Destroyed")
|
||||
}
|
||||
|
||||
status = models.PositiveIntegerField(
|
||||
default=ITEM_IN_STOCK,
|
||||
choices=ITEM_STATUS_CODES.items())
|
||||
|
||||
# If stock item is incoming, an (optional) ETA field
|
||||
expected_arrival = models.DateField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return "{n} x {part} @ {loc}".format(
|
||||
n=self.quantity,
|
||||
|
52
InvenTree/stock/serializers.py
Normal file
52
InvenTree/stock/serializers.py
Normal file
@ -0,0 +1,52 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import StockItem, StockLocation
|
||||
|
||||
|
||||
class StockItemSerializer(serializers.ModelSerializer):
|
||||
""" Serializer for a StockItem
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = StockItem
|
||||
fields = ('pk',
|
||||
'part',
|
||||
'location',
|
||||
'quantity',
|
||||
'status',
|
||||
'updated',
|
||||
'last_checked',
|
||||
'review_needed',
|
||||
'expected_arrival')
|
||||
|
||||
|
||||
class LocationBriefSerializer(serializers.ModelSerializer):
|
||||
""" Brief information about a stock location
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = StockLocation
|
||||
fields = ('pk',
|
||||
'name',
|
||||
'description')
|
||||
|
||||
|
||||
class LocationDetailSerializer(serializers.ModelSerializer):
|
||||
""" Detailed information about a stock location
|
||||
"""
|
||||
|
||||
# List of all stock items in this location
|
||||
items = StockItemSerializer(many=True)
|
||||
|
||||
# List of all child locations under this one
|
||||
children = LocationBriefSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = StockLocation
|
||||
fields = ('pk',
|
||||
'name',
|
||||
'description',
|
||||
'parent',
|
||||
'path',
|
||||
'children',
|
||||
'items')
|
@ -1,3 +1,3 @@
|
||||
from django.test import TestCase
|
||||
# from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
@ -3,5 +3,12 @@ from django.conf.urls import url
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.index, name='index')
|
||||
# List all stock quantities for a given part
|
||||
url(r'^part/(?P<part>[0-9]+)$', views.PartStockDetail.as_view()),
|
||||
|
||||
# List all stock items in a given location
|
||||
url(r'^location/(?P<pk>[0-9]+)$', views.LocationDetail.as_view()),
|
||||
|
||||
# List all top-level locations
|
||||
url(r'^location/$', views.LocationList.as_view())
|
||||
]
|
||||
|
@ -1,11 +1,33 @@
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from rest_framework import generics
|
||||
|
||||
from .models import Warehouse, StockItem
|
||||
from .models import StockLocation, StockItem
|
||||
|
||||
from .serializers import StockItemSerializer, LocationDetailSerializer
|
||||
|
||||
|
||||
def index(request):
|
||||
|
||||
warehouses = Warehouse.objects.filter(parent=None)
|
||||
|
||||
return render(request, 'stock/index.html', {'warehouses': warehouses})
|
||||
class PartStockDetail(generics.ListAPIView):
|
||||
""" Return a list of all stockitems for a given part
|
||||
"""
|
||||
|
||||
serializer_class = StockItemSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
part_id = self.kwargs['part']
|
||||
return StockItem.objects.filter(part=part_id)
|
||||
|
||||
|
||||
class LocationDetail(generics.RetrieveAPIView):
|
||||
""" Return information on a specific stock location
|
||||
"""
|
||||
|
||||
queryset = StockLocation.objects.all()
|
||||
serializer_class = LocationDetailSerializer
|
||||
|
||||
|
||||
class LocationList(generics.ListAPIView):
|
||||
""" Return a list of top-level locations
|
||||
Locations are considered "top-level" if they do not have a parent
|
||||
"""
|
||||
|
||||
queryset = StockLocation.objects.filter(parent=None)
|
||||
serializer_class = LocationDetailSerializer
|
||||
|
@ -6,6 +6,7 @@ from .models import Supplier, SupplierPart, Customer, Manufacturer
|
||||
class CompanyAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'URL', 'contact')
|
||||
|
||||
|
||||
admin.site.register(Customer, CompanyAdmin)
|
||||
admin.site.register(Supplier, CompanyAdmin)
|
||||
admin.site.register(Manufacturer, CompanyAdmin)
|
||||
|
@ -9,9 +9,8 @@ from part.models import Part
|
||||
class Supplier(Company):
|
||||
""" Represents a manufacturer or supplier
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Manufacturer(Company):
|
||||
""" Represents a manfufacturer
|
||||
@ -32,21 +31,37 @@ class SupplierPart(models.Model):
|
||||
- A Part may be available from multiple suppliers
|
||||
"""
|
||||
|
||||
part = models.ForeignKey(Part,
|
||||
on_delete=models.CASCADE)
|
||||
supplier = models.ForeignKey(Supplier,
|
||||
on_delete=models.CASCADE)
|
||||
part = models.ForeignKey(Part, null=True, blank=True, on_delete=models.CASCADE)
|
||||
supplier = models.ForeignKey(Supplier, on_delete=models.CASCADE)
|
||||
SKU = models.CharField(max_length=100)
|
||||
|
||||
|
||||
manufacturer = models.ForeignKey(Manufacturer, blank=True, null=True, on_delete=models.CASCADE)
|
||||
MPN = models.CharField(max_length=100, blank=True)
|
||||
|
||||
|
||||
URL = models.URLField(blank=True)
|
||||
description = models.CharField(max_length=250, blank=True)
|
||||
|
||||
# Default price for a single unit
|
||||
single_price = models.DecimalField(max_digits=10, decimal_places=3, default=0)
|
||||
|
||||
# Base charge added to order independent of quantity e.g. "Reeling Fee"
|
||||
base_cost = models.DecimalField(max_digits=10, decimal_places=3, default=0)
|
||||
|
||||
# packaging that the part is supplied in, e.g. "Reel"
|
||||
packaging = models.CharField(max_length=50, blank=True)
|
||||
|
||||
# multiple that the part is provided in
|
||||
multiple = models.PositiveIntegerField(default=1)
|
||||
|
||||
# Mimumum number required to order
|
||||
minimum = models.PositiveIntegerField(default=1)
|
||||
|
||||
# lead time for parts that cannot be delivered immediately
|
||||
lead_time = models.DurationField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return "{mpn} - {supplier}".format(
|
||||
mpn=self.MPN,
|
||||
return "{sku} - {supplier}".format(
|
||||
sku=self.SKU,
|
||||
supplier=self.supplier.name)
|
||||
|
||||
|
||||
@ -56,16 +71,13 @@ class SupplierPriceBreak(models.Model):
|
||||
- SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s)
|
||||
"""
|
||||
|
||||
part = models.ForeignKey(SupplierPart,
|
||||
on_delete=models.CASCADE)
|
||||
quantity = models.IntegerField()
|
||||
part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='price_breaks')
|
||||
quantity = models.PositiveIntegerField()
|
||||
cost = models.DecimalField(max_digits=10, decimal_places=3)
|
||||
currency = models.CharField(max_length=10,
|
||||
blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return "{mpn} - {cost}{currency} @ {quan}".format(
|
||||
mpn=part.MPN,
|
||||
mpn=self.part.MPN,
|
||||
cost=self.cost,
|
||||
currency=self.currency if self.currency else '',
|
||||
quan=self.quantity)
|
||||
|
@ -1,3 +1,3 @@
|
||||
from django.test import TestCase
|
||||
# from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
@ -4,6 +4,7 @@ from .models import UniquePart
|
||||
|
||||
|
||||
class UniquePartAdmin(admin.ModelAdmin):
|
||||
list_display = ('part', 'revision', 'serial', 'creation_date')
|
||||
list_display = ('part', 'revision', 'serial', 'status', 'creation_date')
|
||||
|
||||
|
||||
admin.site.register(UniquePart, UniquePartAdmin)
|
||||
|
@ -1,5 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
@ -36,15 +36,16 @@ class UniquePart(models.Model):
|
||||
PART_DAMAGED = 40
|
||||
PART_DESTROYED = 50
|
||||
|
||||
status = models.IntegerField(default=PART_IN_PROGRESS,
|
||||
choices=[
|
||||
(PART_IN_PROGRESS, "In progress"),
|
||||
(PART_IN_STOCK, "In stock"),
|
||||
(PART_SHIPPED, "Shipped"),
|
||||
(PART_RETURNED, "Returned"),
|
||||
(PART_DAMAGED, "Damaged"),
|
||||
(PART_DESTROYED, "Destroyed"),
|
||||
])
|
||||
PART_STATUS_CODES = {
|
||||
PART_IN_PROGRESS: _("In progress"),
|
||||
PART_IN_STOCK: _("In stock"),
|
||||
PART_SHIPPED: _("Shipped"),
|
||||
PART_RETURNED: _("Returned"),
|
||||
PART_DAMAGED: _("Damaged"),
|
||||
PART_DESTROYED: _("Destroyed")
|
||||
}
|
||||
|
||||
status = models.IntegerField(default=PART_IN_PROGRESS, choices=PART_STATUS_CODES.items())
|
||||
|
||||
def __str__(self):
|
||||
return self.part.name
|
||||
|
@ -1,3 +1,3 @@
|
||||
from django.test import TestCase
|
||||
# from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
@ -1,4 +1,3 @@
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
|
34
Makefile
Normal file
34
Makefile
Normal file
@ -0,0 +1,34 @@
|
||||
clean:
|
||||
find . -path '*/__pycache__/*' -delete
|
||||
find . -type d -name '__pycache__' -empty -delete
|
||||
find . -name *.pyc -o -name *.pyo -delete
|
||||
rm -rf *.egg-info
|
||||
rm -rf .cache
|
||||
rm -rf .tox
|
||||
rm -f .coverage
|
||||
|
||||
style:
|
||||
flake8
|
||||
|
||||
test:
|
||||
python InvenTree/manage.py test --noinput
|
||||
|
||||
migrate:
|
||||
python InvenTree/manage.py makemigrations
|
||||
python InvenTree/manage.py migrate --run-syncdb
|
||||
python InvenTree/manage.py check
|
||||
|
||||
install:
|
||||
# TODO: replace this with a proper setup.py
|
||||
pip install -U -r requirements/base.txt
|
||||
|
||||
setup: install migrate
|
||||
|
||||
setup_ci:
|
||||
pip install -U -r requirements/build.txt
|
||||
|
||||
develop:
|
||||
pip install -U -r requirements/dev.txt
|
||||
|
||||
superuser:
|
||||
python InvenTree/manage.py createsuperuser
|
24
README.md
24
README.md
@ -1,5 +1,25 @@
|
||||
# InvenTree
|
||||
Open Source Inventory Management System
|
||||
[![Build Status](https://travis-ci.org/inventree/InvenTree.svg?branch=master)](https://travis-ci.org/inventree/InvenTree)
|
||||
|
||||
# InvenTree
|
||||
InvenTree is an open-source Inventory Management System which provides powerful low-level stock control and part tracking. The core of the InvenTree system is a Python/Django database backend which provides an admin interface (web-based) and a JSON API for interaction with external interfaces and applications.
|
||||
|
||||
## Installation
|
||||
It is recommended to set up a clean Python 3.4+ virtual environment first:
|
||||
`mkdir ~/.env && python3 -m venv ~/.env/InvenTree && source ~/.env/InvenTree/bin/activate`
|
||||
|
||||
You can then continue running `make setup` (which will be replaced by a proper setup.py soon). This will do the following:
|
||||
|
||||
1. Installs required Python dependencies (requires [pip](https://pypi.python.org/pypi/pip), should be part of your virtual environment by default)
|
||||
1. Performs initial database setup
|
||||
1. Updates database tables for all InvenTree components
|
||||
|
||||
This command can also be used to update the installation if changes have been made to the database configuration.
|
||||
|
||||
To create an initial user account, run the command `make superuser`.
|
||||
|
||||
## Documentation
|
||||
For project code documentation, refer to the online [documentation](http://inventree.readthedocs.io/en/latest/) (auto-generated)
|
||||
|
||||
## Coding Style
|
||||
If you'd like to contribute, install our development dependencies using `make develop`.
|
||||
All Python code should conform to the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide. Run `make style` which will compare all source (.py) files against the PEP 8 style. Tests can be run using `make test`.
|
||||
|
44
install.py
44
install.py
@ -1,44 +0,0 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import subprocess
|
||||
import argparse
|
||||
|
||||
def manage(*arg):
|
||||
args = ["python", "InvenTree/manage.py"]
|
||||
|
||||
for a in arg:
|
||||
args.append(a)
|
||||
|
||||
subprocess.call(args)
|
||||
|
||||
parser = argparse.ArgumentParser(description="Install InvenTree inventory management system")
|
||||
|
||||
parser.add_argument('-u', '--update', help='Update only, do not try to install required components', action='store_true')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# If 'update' is specified, don't perform initial installation
|
||||
if not args.update:
|
||||
# Install django requirements
|
||||
subprocess.call(["pip", "install", "django", "-q"])
|
||||
subprocess.call(["pip", "install", "djangorestframework", "-q"])
|
||||
|
||||
# Initial database setup
|
||||
manage("migrate")
|
||||
|
||||
# Make migrations for all apps
|
||||
manage("makemigrations", "part")
|
||||
manage("makemigrations", "stock")
|
||||
manage("makemigrations", "supplier")
|
||||
manage("makemigrations", "project")
|
||||
manage("makemigrations", "track")
|
||||
|
||||
# Update the database
|
||||
manage("migrate")
|
||||
|
||||
# Check for errors
|
||||
manage("check")
|
||||
|
||||
if not args.update:
|
||||
print("\n\nAdmin account:\nIf a superuser is not already installed,")
|
||||
print("run the command 'python InvenTree/manage.py createsuperuser'")
|
2
requirements/base.txt
Normal file
2
requirements/base.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Django==1.11
|
||||
djangorestframework==3.6.2
|
2
requirements/build.txt
Normal file
2
requirements/build.txt
Normal file
@ -0,0 +1,2 @@
|
||||
-r base.txt
|
||||
flake8==3.3.0
|
4
requirements/dev.txt
Normal file
4
requirements/dev.txt
Normal file
@ -0,0 +1,4 @@
|
||||
-r build.txt
|
||||
django-extensions==1.7.8
|
||||
graphviz==0.6
|
||||
ipython==5.3.0
|
28
roadmap.md
Normal file
28
roadmap.md
Normal file
@ -0,0 +1,28 @@
|
||||
## InvenTree Roadmap
|
||||
|
||||
### Design Goals
|
||||
|
||||
InvenTree is intened to provide a stand-alone stock-management system that runs completely offline.
|
||||
|
||||
It is designed to be run on a local server, and should not require the use of plugins/scripts that phone-home or load external content.
|
||||
|
||||
(This ignores the use of bespoke plugins that may be implemented down the line, e.g. for OctoPart integration, etc)
|
||||
|
||||
### 0.1 Release
|
||||
|
||||
The goals for the initial release should be limited to the following:
|
||||
|
||||
1. Fully implement a JSON API for the various apps and models
|
||||
1. Design an initial front-end for querying data using this API
|
||||
* Single-pase design is preferred, for the sake of responsiveness and intuitive interaction
|
||||
* Investigate JS/AJAX engine - Angular? Bootstrap?
|
||||
1. Allow users to view part category tree
|
||||
1. Allow users to view all parts in a given category
|
||||
1. "" edit parts
|
||||
1. "" add new parts
|
||||
|
||||
### TODO
|
||||
|
||||
Research needed!
|
||||
|
||||
django-restful in combination with angular seems the way to go. Extra tools provided via https://github.com/jrief/django-angular
|
Loading…
Reference in New Issue
Block a user