Compare commits

...

33 Commits

Author SHA1 Message Date
Oliver
13c7e2af49 Update version.py
Modify version number for release
2020-04-20 19:28:09 +10:00
Oliver
85e20041c7 Merge pull request #739 from SchrodingersGat/api-consolidation
Api consolidation
2020-04-20 09:55:16 +10:00
Oliver Walters
ff3cc96e0e PEP fixes 2020-04-20 09:50:41 +10:00
Oliver Walters
746e9ab983 Fix rendering of PurchaseOrder table 2020-04-20 09:48:33 +10:00
Oliver Walters
99fcbcc646 Consolidation of PurchaseOrder API 2020-04-20 09:41:21 +10:00
Oliver Walters
fee6246a8f PEP fix 2020-04-20 08:25:24 +10:00
Oliver Walters
4ec5e9a907 Consolidate StockItem serializer 2020-04-20 08:24:43 +10:00
Oliver Walters
ef66a3b8f3 Make PartDetail view same as PartList view 2020-04-20 08:10:59 +10:00
Oliver
654f5d348e Merge pull request #738 from SchrodingersGat/stock-count-fix
Stock count fix
2020-04-20 01:26:42 +10:00
Oliver Walters
f5c86bc457 "Fix" for unit tests
- Not working in travis for some reason?
- But they are working locally...
2020-04-20 01:23:05 +10:00
Oliver Walters
57fa69f6e6 Update version
- Display django version in "about" dialog
2020-04-20 01:20:09 +10:00
Oliver Walters
c72fce0cc5 Add "tracking items" back in 2020-04-20 01:14:19 +10:00
Oliver Walters
b2c40c91b7 PEP fixes 2020-04-20 01:11:25 +10:00
Oliver Walters
0334035e77 Simplify StockItem serializer
- Some more work needed here to cut down on database hits
2020-04-20 01:09:37 +10:00
Oliver Walters
4b1b9df193 Update stock table rendering 2020-04-20 01:02:19 +10:00
Oliver Walters
6a89e0089d Updates for stock serializer 2020-04-20 00:49:13 +10:00
Oliver Walters
5233281a24 Should have checked first :'(
New tablib is broked (at least in the admin interface)
2020-04-20 00:19:04 +10:00
Oliver Walters
468eba1759 Update PIP requirements
- django-mptt required updating to fix a bug where tree was not being rebuild on item save
2020-04-20 00:16:57 +10:00
Oliver Walters
ff91c4ec53 Add a new verision of django-qr-code
Official package does not (yet) support django 3.0
2020-04-20 00:10:16 +10:00
Oliver Walters
3a64d0bc8f Fix display of part table 2020-04-20 00:00:14 +10:00
Oliver Walters
092215918c PEP fixes 2020-04-19 23:56:16 +10:00
Oliver Walters
2621c51a7e Further API cleanup
- Perform a single call to get starred parts for current user and record results
- This provides significant speed improvements
- Remove old manual serializer
- More data prefetching
2020-04-19 23:50:41 +10:00
Oliver Walters
69b8eed028 Fixes for aggregation issues
- Ensure that "distinct=True" is set!
- ARRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
2020-04-19 22:54:46 +10:00
Oliver Walters
85d1c585c0 Update to django3!
- This is required to fix some issues with query aggregations as "distinct=True" cannot be set for Sum operations until django 3.0
- Multiple annotations on the same queryset were returning invalid results
- Ref: https://code.djangoproject.com/ticket/10060
- Ref: https://docs.djangoproject.com/en/3.0/topics/db/aggregation/#combining-multiple-aggregations
- django-dbbackup required updating for django3 support
- django-qr-code for now is disabled, a new solution needs to be worked out
2020-04-19 22:44:16 +10:00
Oliver
9cb1af9587 Merge pull request #737 from SchrodingersGat/doc-updates
Remove some docs pages
2020-04-18 14:55:04 +10:00
Oliver Walters
b580df0d30 Update README.md to point to the new docs 2020-04-18 14:37:51 +10:00
Oliver Walters
d953f1a31e Remove some docs pages
- These are now being consolidated on the inventree.github.io page
2020-04-18 14:09:14 +10:00
Oliver
a28b7df9d4 Merge pull request #736 from SchrodingersGat/config-file-fix
Small fix for config template
2020-04-18 13:26:43 +10:00
Oliver Walters
880655c141 Small fix for config template 2020-04-18 13:22:52 +10:00
Oliver
4f3f78f55a Merge pull request #735 from SchrodingersGat/token-auth-fix
Improvements for token authentication
2020-04-18 12:12:54 +10:00
Oliver Walters
6e3f603413 Improvements for token authentication
- Do not POST username and password data
- This is already encoded in the Authorization: Basic headers
2020-04-18 12:06:50 +10:00
Oliver
2b70b947ee Merge pull request #734 from SchrodingersGat/migration-fix
Add error checking for thumbnail file migration
2020-04-17 21:46:36 +10:00
Oliver Walters
37fcb810e4 Add error checking for thumbnail file migration 2020-04-17 19:23:43 +10:00
27 changed files with 548 additions and 797 deletions

View File

@@ -104,21 +104,23 @@ function removePurchaseOrderLineItem(e) {
function loadPurchaseOrderTable(table, options) {
/* Create a purchase-order table */
var params = options.params || {};
options.params = options.params || {};
options.params['supplier_detail'] = true;
var filters = loadTableFilters("order");
for (var key in params) {
filters[key] = params[key];
for (var key in options.params) {
filters[key] = options.params[key];
}
setupFilterList("order", table);
setupFilterList("order", $(table));
$(table).inventreeTable({
url: options.url,
queryParams: filters,
groupBy: false,
original: params,
original: options.params,
formatNoMatches: function() { return "No purchase orders found"; },
columns: [
{
@@ -131,15 +133,15 @@ function loadPurchaseOrderTable(table, options) {
field: 'reference',
title: 'Purchase Order',
formatter: function(value, row, index, field) {
return renderLink(value, "/order/purchase-order/" + row.pk + "/");
return renderLink(value, `/order/purchase-order/${row.pk}/`);
}
},
{
sortable: true,
field: 'supplier',
field: 'supplier_detail',
title: 'Supplier',
formatter: function(value, row, index, field) {
return imageHoverIcon(row.supplier__image) + renderLink(row.supplier__name, '/company/' + value + '/purchase-orders/');
return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/purchase-orders/`);
}
},
{
@@ -162,7 +164,7 @@ function loadPurchaseOrderTable(table, options) {
},
{
sortable: true,
field: 'lines',
field: 'line_items',
title: 'Items'
},
],

View File

@@ -87,6 +87,9 @@ function loadPartTable(table, url, options={}) {
* disableFilters: If true, disable custom filters
*/
// Ensure category detail is included
options.params['category_detail'] = true;
var params = options.params || {};
var filters = {};
@@ -184,11 +187,11 @@ function loadPartTable(table, url, options={}) {
columns.push({
sortable: true,
field: 'category__name',
field: 'category_detail',
title: 'Category',
formatter: function(value, row, index, field) {
if (row.category) {
return renderLink(row.category__name, "/part/category/" + row.category + "/");
return renderLink(value.pathstring, "/part/category/" + row.category + "/");
}
else {
return 'No category';

View File

@@ -43,8 +43,12 @@ function loadStockTable(table, options) {
* filterList - <ul> element where filters are displayed
* disableFilters: If true, disable custom filters
*/
// List of user-params which override the default filters
options.params['part_detail'] = true;
options.params['location_detail'] = true;
var params = options.params || {};
var filterListElement = options.filterList || "#filter-list-stock";
@@ -83,27 +87,21 @@ function loadStockTable(table, options) {
var row = data[0];
if (field == 'part__name') {
if (field == 'part_name') {
var name = row.part__IPN;
var name = row.part_detail.full_name;
if (name) {
name += ' | ';
}
name += row.part__name;
return imageHoverIcon(row.part__thumbnail) + name + ' <i>(' + data.length + ' items)</i>';
return imageHoverIcon(row.part_detail.thumbnail) + name + ' <i>(' + data.length + ' items)</i>';
}
else if (field == 'part__description') {
return row.part__description;
else if (field == 'part_description') {
return row.part_detail.description;
}
else if (field == 'quantity') {
var stock = 0;
var items = 0;
data.forEach(function(item) {
stock += item.quantity;
stock += parseFloat(item.quantity);
items += 1;
});
@@ -216,25 +214,14 @@ function loadStockTable(table, options) {
visible: false,
},
{
field: 'part__name',
field: 'part_name',
title: 'Part',
sortable: true,
formatter: function(value, row, index, field) {
var name = row.part__IPN;
if (name) {
name += ' | ';
}
name += row.part__name;
if (row.part__revision) {
name += " | ";
name += row.part__revision;
}
var url = '';
var thumb = row.part_detail.thumbnail;
var name = row.part_detail.full_name;
if (row.supplier_part) {
url = `/supplier-part/${row.supplier_part}/`;
@@ -242,13 +229,16 @@ function loadStockTable(table, options) {
url = `/part/${row.part}/`;
}
return imageHoverIcon(row.part__thumbnail) + renderLink(name, url);
return imageHoverIcon(thumb) + renderLink(name, url);
}
},
{
field: 'part__description',
field: 'part_description',
title: 'Description',
sortable: true,
formatter: function(value, row, index, field) {
return row.part_detail.description;
}
},
{
field: 'quantity',
@@ -256,11 +246,13 @@ function loadStockTable(table, options) {
sortable: true,
formatter: function(value, row, index, field) {
var val = value;
var val = parseFloat(value);
// If there is a single unit with a serial number, use the serial number
if (row.serial && row.quantity == 1) {
val = '# ' + row.serial;
} else {
val = +val.toFixed(5);
}
var text = renderLink(val, '/stock/item/' + row.pk + '/');
@@ -282,7 +274,7 @@ function loadStockTable(table, options) {
sortable: true,
},
{
field: 'location__path',
field: 'location_detail.pathstring',
title: 'Location',
sortable: true,
formatter: function(value, row, index, field) {

View File

@@ -4,9 +4,10 @@ from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from django.contrib.auth import get_user_model
from base64 import b64encode
class APITests(APITestCase):
""" Tests for the InvenTree API """
@@ -21,24 +22,48 @@ class APITests(APITestCase):
username = 'test_user'
password = 'test_pass'
token = None
def setUp(self):
# Create a user (but do not log in!)
User = get_user_model()
User.objects.create_user(self.username, 'user@email.com', self.password)
def get_token(self):
def basicAuth(self):
# Use basic authentication
authstring = bytes("{u}:{p}".format(u=self.username, p=self.password), "ascii")
# Use "basic" auth by default
auth = b64encode(authstring).decode("ascii")
self.client.credentials(HTTP_AUTHORIZATION="Basic {auth}".format(auth=auth))
def tokenAuth(self):
self.basicAuth()
token_url = reverse('api-token')
response = self.client.get(token_url, format='json', data={})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('token', response.data)
# POST to retreive a token
response = self.client.post(token_url, format='json', data={'username': self.username, 'password': self.password})
token = response.data['token']
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token)
self.token = token
def token_failure(self):
# Test token endpoint without basic auth
url = reverse('api-token')
response = self.client.get(url, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.assertIsNone(self.token)
def token_success(self):
self.tokenAuth()
self.assertIsNotNone(self.token)
def test_info_view(self):
"""
Test that we can read the 'info-view' endpoint.
@@ -55,51 +80,18 @@ class APITests(APITestCase):
self.assertEquals('InvenTree', data['server'])
def test_get_token_fail(self):
""" Ensure that an invalid user cannot get a token """
token_url = reverse('api-token')
response = self.client.post(token_url, format='json', data={'username': 'bad', 'password': 'also_bad'})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse('token' in response.data)
def test_get_token_pass(self):
""" Ensure that a valid user can request an API token """
token_url = reverse('api-token')
# POST to retreive a token
response = self.client.post(token_url, format='json', data={'username': self.username, 'password': self.password})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue('token' in response.data)
self.assertTrue('pk' in response.data)
self.assertTrue(len(response.data['token']) > 0)
# Now, use the token to access other data
token = response.data['token']
part_url = reverse('api-part-list')
# Try to access without a token
response = self.client.get(part_url, format='json')
def test_barcode_fail(self):
# Test barcode endpoint without auth
response = self.client.post(reverse('api-barcode-plugin'), format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
# Now, with the token
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token)
response = self.client.get(part_url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_barcode(self):
""" Test the barcode endpoint """
url = reverse('api-barcode-plugin')
self.tokenAuth()
self.get_token()
url = reverse('api-barcode-plugin')
data = {
'barcode': {

View File

@@ -4,8 +4,9 @@ Provides information on the current InvenTree version
import subprocess
from common.models import InvenTreeSetting
import django
INVENTREE_SW_VERSION = "0.0.11_pre"
INVENTREE_SW_VERSION = "0.0.12"
def inventreeInstanceName():
@@ -18,6 +19,11 @@ def inventreeVersion():
return INVENTREE_SW_VERSION
def inventreeDjangoVersion():
""" Return the version of Django library """
return django.get_version()
def inventreeCommitHash():
""" Returns the git commit hash for the running codebase """

View File

@@ -31,6 +31,11 @@ class CompanyConfig(AppConfig):
if not os.path.exists(loc):
print("InvenTree: Generating thumbnail for Company '{c}'".format(c=company.name))
company.image.render_variations(replace=False)
try:
company.image.render_variations(replace=False)
except FileNotFoundError:
print("Image file missing")
company.image = None
company.save()
except (OperationalError, ProgrammingError):
print("Could not generate Company thumbnails")

View File

@@ -42,12 +42,12 @@ cors:
# - https://sub.example.com
# MEDIA_ROOT is the local filesystem location for storing uploaded files
# By default, it is stored in a directory named 'media' local to the InvenTree directory
# By default, it is stored in a directory named 'inventree_media' local to the InvenTree directory
# This should be changed for a production installation
media_root: '../inventree_media'
# STATIC_ROOT is the local filesystem location for storing static files
# By default it is stored in a directory named 'static' local to the InvenTree directory
# By default it is stored in a directory named 'inventree_static' local to the InvenTree directory
static_root: '../inventree_static'
# Optional URL schemes to allow in URL fields

View File

@@ -8,14 +8,10 @@ from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions
from rest_framework import filters
from rest_framework.response import Response
from django.conf import settings
from django.conf.urls import url
from InvenTree.status_codes import OrderStatus
import os
from InvenTree.helpers import str2bool
from part.models import Part
from company.models import SupplierPart
@@ -34,66 +30,66 @@ class POList(generics.ListCreateAPIView):
queryset = PurchaseOrder.objects.all()
serializer_class = POSerializer
def list(self, request, *args, **kwargs):
def get_serializer(self, *args, **kwargs):
queryset = self.get_queryset().prefetch_related('supplier', 'lines')
try:
kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', False))
except AttributeError:
pass
queryset = self.filter_queryset(queryset)
# Ensure the request context is passed through
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related(
'supplier',
'lines',
)
queryset = POSerializer.annotate_queryset(queryset)
return queryset
def filter_queryset(self, queryset):
# Perform basic filtering
queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Special filtering for 'status' field
if 'status' in request.GET:
status = request.GET['status']
status = params.get('status', None)
if status is not None:
# First attempt to filter by integer value
try:
status = int(status)
queryset = queryset.filter(status=status)
except ValueError:
try:
value = OrderStatus.value(status)
queryset = queryset.filter(status=value)
except ValueError:
pass
queryset = queryset.filter(status=status)
# Attempt to filter by part
if 'part' in request.GET:
part = params.get('part', None)
if part is not None:
try:
part = Part.objects.get(pk=request.GET['part'])
part = Part.objects.get(pk=part)
queryset = queryset.filter(id__in=[p.id for p in part.purchase_orders()])
except (Part.DoesNotExist, ValueError):
pass
# Attempt to filter by supplier part
if 'supplier_part' in request.GET:
supplier_part = params.get('supplier_part', None)
if supplier_part is not None:
try:
supplier_part = SupplierPart.objects.get(pk=request.GET['supplier_part'])
supplier_part = SupplierPart.objects.get(pk=supplier_part)
queryset = queryset.filter(id__in=[p.id for p in supplier_part.purchase_orders()])
except (ValueError, SupplierPart.DoesNotExist):
pass
data = queryset.values(
'pk',
'supplier',
'supplier_reference',
'supplier__name',
'supplier__image',
'reference',
'description',
'link',
'status',
'notes',
'creation_date',
)
for item in data:
order = queryset.get(pk=item['pk'])
item['supplier__image'] = os.path.join(settings.MEDIA_URL, item['supplier__image'])
item['status_text'] = OrderStatus.label(item['status'])
item['lines'] = order.lines.count()
return Response(data)
return queryset
permission_classes = [
permissions.IsAuthenticated,
@@ -123,6 +119,31 @@ class PODetail(generics.RetrieveUpdateAPIView):
queryset = PurchaseOrder.objects.all()
serializer_class = POSerializer
def get_serializer(self, *args, **kwargs):
try:
kwargs['supplier_detail'] = str2bool(self.request.query_params.get('supplier_detail', False))
except AttributeError:
pass
# Ensure the request context is passed through
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = queryset.prefetch_related(
'supplier',
'lines',
)
queryset = POSerializer.annotate_queryset(queryset)
return queryset
permission_classes = [
permissions.IsAuthenticated
]

View File

@@ -5,7 +5,12 @@ JSON serializers for the Order API
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from rest_framework import serializers
from django.db.models import Count
from InvenTree.serializers import InvenTreeModelSerializer
from company.serializers import CompanyBriefSerializer
from .models import PurchaseOrder, PurchaseOrderLineItem
@@ -13,17 +18,48 @@ from .models import PurchaseOrder, PurchaseOrderLineItem
class POSerializer(InvenTreeModelSerializer):
""" Serializes an Order object """
def __init__(self, *args, **kwargs):
supplier_detail = kwargs.pop('supplier_detail', False)
super().__init__(*args, **kwargs)
if supplier_detail is not True:
self.fields.pop('supplier_detail')
@staticmethod
def annotate_queryset(queryset):
"""
Add extra information to the queryset
"""
return queryset.annotate(
line_items=Count('lines'),
)
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
line_items = serializers.IntegerField(read_only=True)
status_text = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = PurchaseOrder
fields = [
'pk',
'supplier',
'supplier_reference',
'reference',
'issue_date',
'complete_date',
'creation_date',
'description',
'line_items',
'link',
'reference',
'supplier',
'supplier_detail',
'supplier_reference',
'status',
'status_text',
'notes',
]

View File

@@ -6,10 +6,8 @@ Provides a JSON API for the Part app
from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend
from django.conf import settings
from django.db.models import Q, F, Sum, Count
from django.db.models.functions import Coalesce
from django.db.models import Q, F, Count
from rest_framework import status
from rest_framework.response import Response
@@ -19,15 +17,11 @@ from rest_framework import generics, permissions
from django.conf.urls import url, include
from django.urls import reverse
import os
from decimal import Decimal
from .models import Part, PartCategory, BomItem, PartStar
from .models import PartParameter, PartParameterTemplate
from . import serializers as part_serializers
from InvenTree.status_codes import OrderStatus, StockStatus, BuildStatus
from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull
@@ -125,6 +119,8 @@ class PartThumbs(generics.ListAPIView):
# Get all Parts which have an associated image
queryset = Part.objects.all().exclude(image='')
# TODO - We should return the thumbnails here, not the full image!
# Return the most popular parts first
data = queryset.values(
'image',
@@ -138,11 +134,41 @@ class PartDetail(generics.RetrieveUpdateAPIView):
queryset = Part.objects.all()
serializer_class = part_serializers.PartSerializer
starred_parts = None
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
return queryset
permission_classes = [
permissions.IsAuthenticated,
]
def get_serializer(self, *args, **kwargs):
try:
cat_detail = str2bool(self.request.query_params.get('category_detail', False))
except AttributeError:
cat_detail = None
# Ensure the request context is passed through
kwargs['context'] = self.get_serializer_context()
kwargs['category_detail'] = cat_detail
# Pass a list of "starred" parts fo the current user to the serializer
# We do this to reduce the number of database queries required!
if self.starred_parts is None and self.request is not None:
self.starred_parts = [star.part for star in self.request.user.starred_parts.all()]
kwargs['starred_parts'] = self.starred_parts
return self.serializer_class(*args, **kwargs)
class PartList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of Part objects
@@ -166,6 +192,31 @@ class PartList(generics.ListCreateAPIView):
serializer_class = part_serializers.PartSerializer
queryset = Part.objects.all()
starred_parts = None
def get_serializer(self, *args, **kwargs):
try:
cat_detail = str2bool(self.request.query_params.get('category_detail', False))
except AttributeError:
cat_detail = None
# Ensure the request context is passed through
kwargs['context'] = self.get_serializer_context()
kwargs['category_detail'] = cat_detail
# Pass a list of "starred" parts fo the current user to the serializer
# We do this to reduce the number of database queries required!
if self.starred_parts is None and self.request is not None:
self.starred_parts = [star.part for star in self.request.user.starred_parts.all()]
kwargs['starred_parts'] = self.starred_parts
return self.serializer_class(*args, **kwargs)
def create(self, request, *args, **kwargs):
""" Override the default 'create' behaviour:
We wish to save the user who created this part!
@@ -184,129 +235,20 @@ class PartList(generics.ListCreateAPIView):
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def list(self, request, *args, **kwargs):
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = part_serializers.PartSerializer.prefetch_queryset(queryset)
return queryset
def filter_queryset(self, queryset):
"""
Instead of using the DRF serialiser to LIST,
we serialize the objects manually.
This turns out to be significantly faster.
Perform custom filtering of the queryset
"""
queryset = self.filter_queryset(self.get_queryset())
# Filters for annotations
# "in_stock" count should only sum stock items which are "in stock"
stock_filter = Q(stock_items__status__in=StockStatus.AVAILABLE_CODES)
# "on_order" items should only sum orders which are currently outstanding
order_filter = Q(supplier_parts__purchase_order_line_items__order__status__in=OrderStatus.OPEN)
# "building" should only reference builds which are active
build_filter = Q(builds__status__in=BuildStatus.ACTIVE_CODES)
# Set of fields we wish to serialize
data = queryset.values(
'pk',
'category',
'image',
'name',
'IPN',
'revision',
'description',
'keywords',
'is_template',
'link',
'units',
'minimum_stock',
'trackable',
'assembly',
'component',
'salable',
'active',
).annotate(
# Quantity of items which are "in stock"
in_stock=Coalesce(Sum('stock_items__quantity', filter=stock_filter), Decimal(0)),
on_order=Coalesce(Sum('supplier_parts__purchase_order_line_items__quantity', filter=order_filter), Decimal(0)),
building=Coalesce(Sum('builds__quantity', filter=build_filter), Decimal(0)),
)
# If we are filtering by 'has_stock' status
has_stock = self.request.query_params.get('has_stock', None)
if has_stock is not None:
has_stock = str2bool(has_stock)
if has_stock:
# Filter items which have a non-null 'in_stock' quantity above zero
data = data.filter(in_stock__gt=0)
else:
# Filter items which a null or zero 'in_stock' quantity
data = data.filter(Q(in_stock__lte=0))
# If we are filtering by 'low_stock' status
low_stock = self.request.query_params.get('low_stock', None)
if low_stock is not None:
low_stock = str2bool(low_stock)
if low_stock:
# Ignore any parts which do not have a specified 'minimum_stock' level
data = data.exclude(minimum_stock=0)
# Filter items which have an 'in_stock' level lower than 'minimum_stock'
data = data.filter(Q(in_stock__lt=F('minimum_stock')))
else:
# Filter items which have an 'in_stock' level higher than 'minimum_stock'
data = data.filter(Q(in_stock__gte=F('minimum_stock')))
# Get a list of the parts that this user has starred
starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()]
# Reduce the number of lookups we need to do for the part categories
categories = {}
for item in data:
if item['image']:
# Is this part 'starred' for the current user?
item['starred'] = item['pk'] in starred_parts
img = item['image']
# Use the 'thumbnail' image here instead of the full-size image
# Note: The full-size image is used when requesting the /api/part/<x>/ endpoint
if img:
fn, ext = os.path.splitext(img)
thumb = "{fn}.thumbnail{ext}".format(fn=fn, ext=ext)
thumb = os.path.join(settings.MEDIA_URL, thumb)
else:
thumb = ''
item['thumbnail'] = thumb
del item['image']
cat_id = item['category']
if cat_id:
if cat_id not in categories:
categories[cat_id] = PartCategory.objects.get(pk=cat_id).pathstring
item['category__name'] = categories[cat_id]
else:
item['category__name'] = None
return Response(data)
def get_queryset(self):
"""
Implement custom filtering for the Part list API
"""
# Start with all objects
parts_list = Part.objects.all()
# Perform basic filtering
queryset = super().filter_queryset(queryset)
# Filter by 'starred' parts?
starred = str2bool(self.request.query_params.get('starred', None))
@@ -315,12 +257,13 @@ class PartList(generics.ListCreateAPIView):
starred_parts = [star.part.pk for star in self.request.user.starred_parts.all()]
if starred:
parts_list = parts_list.filter(pk__in=starred_parts)
queryset = queryset.filter(pk__in=starred_parts)
else:
parts_list = parts_list.exclude(pk__in=starred_parts)
queryset = queryset.exclude(pk__in=starred_parts)
# Cascade?
cascade = str2bool(self.request.query_params.get('cascade', None))
# Does the user wish to filter by category?
cat_id = self.request.query_params.get('category', None)
@@ -334,7 +277,7 @@ class PartList(generics.ListCreateAPIView):
# A 'null' category is the top-level category
if cascade is False:
# Do not cascade, only list parts in the top-level category
parts_list = parts_list.filter(category=None)
queryset = queryset.filter(category=None)
else:
try:
@@ -342,17 +285,43 @@ class PartList(generics.ListCreateAPIView):
# If '?cascade=true' then include parts which exist in sub-categories
if cascade:
parts_list = parts_list.filter(category__in=category.getUniqueChildren())
queryset = queryset.filter(category__in=category.getUniqueChildren())
# Just return parts directly in the requested category
else:
parts_list = parts_list.filter(category=cat_id)
queryset = queryset.filter(category=cat_id)
except (ValueError, PartCategory.DoesNotExist):
pass
# Ensure that related models are pre-loaded to reduce DB trips
parts_list = self.get_serializer_class().setup_eager_loading(parts_list)
# Annotate calculated data to the queryset
# (This will be used for further filtering)
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
return parts_list
# Filter by whether the part has stock
has_stock = self.request.query_params.get("has_stock", None)
if has_stock is not None:
has_stock = str2bool(has_stock)
if has_stock:
queryset = queryset.filter(Q(in_stock__gt=0))
else:
queryset = queryset.filter(Q(in_stock__lte=0))
# If we are filtering by 'low_stock' status
low_stock = self.request.query_params.get('low_stock', None)
if low_stock is not None:
low_stock = str2bool(low_stock)
if low_stock:
# Ignore any parts which do not have a specified 'minimum_stock' level
queryset = queryset.exclude(minimum_stock=0)
# Filter items which have an 'in_stock' level lower than 'minimum_stock'
queryset = queryset.filter(Q(in_stock__lt=F('minimum_stock')))
else:
# Filter items which have an 'in_stock' level higher than 'minimum_stock'
queryset = queryset.filter(Q(in_stock__gte=F('minimum_stock')))
return queryset
permission_classes = [
permissions.IsAuthenticated,
@@ -379,6 +348,7 @@ class PartList(generics.ListCreateAPIView):
'name',
]
# Default ordering
ordering = 'name'
search_fields = [
@@ -507,7 +477,9 @@ class BomList(generics.ListCreateAPIView):
kwargs['part_detail'] = part_detail
kwargs['sub_part_detail'] = sub_part_detail
# Ensure the request context is passed through!
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def get_queryset(self):

View File

@@ -30,6 +30,11 @@ class PartConfig(AppConfig):
if not os.path.exists(loc):
print("InvenTree: Generating thumbnail for Part '{p}'".format(p=part.name))
part.image.render_variations(replace=False)
try:
part.image.render_variations(replace=False)
except FileNotFoundError:
print("Image file missing")
part.image = None
part.save()
except (OperationalError, ProgrammingError):
print("Could not generate Part thumbnails")

View File

@@ -16,7 +16,14 @@ def create_thumbnails(apps, schema_editor):
for part in Part.objects.all():
# Render thumbnail for each existing Part
if part.image:
part.image.render_variations()
try:
part.image.render_variations()
except FileNotFoundError:
print("Missing image:", part.image())
# The image is missing, so clear the field
part.image = None
part.save()
except (OperationalError, ProgrammingError):
# Migrations have not yet been applied - table does not exist
print("Could not generate Part thumbnails")

View File

@@ -10,6 +10,12 @@ from .models import PartCategory
from .models import BomItem
from .models import PartParameter, PartParameterTemplate
from decimal import Decimal
from django.db.models import Q, Sum
from django.db.models.functions import Coalesce
from InvenTree.status_codes import StockStatus, OrderStatus, BuildStatus
from InvenTree.serializers import InvenTreeModelSerializer
@@ -48,14 +54,6 @@ class PartBriefSerializer(InvenTreeModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
@staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related('category')
queryset = queryset.prefetch_related('stock_items')
queryset = queryset.prefetch_related('bom_items')
queryset = queryset.prefetch_related('builds')
return queryset
class Meta:
model = Part
@@ -64,8 +62,6 @@ class PartBriefSerializer(InvenTreeModelSerializer):
'url',
'full_name',
'description',
'total_stock',
'available_stock',
'thumbnail',
'active',
'assembly',
@@ -78,57 +74,140 @@ class PartSerializer(InvenTreeModelSerializer):
Used when displaying all details of a single component.
"""
allocated_stock = serializers.FloatField(source='allocation_count', read_only=True)
bom_items = serializers.IntegerField(source='bom_count', read_only=True)
building = serializers.FloatField(source='quantity_being_built', read_only=False)
category_name = serializers.CharField(source='category_path', read_only=True)
image = serializers.CharField(source='get_image_url', read_only=True)
on_order = serializers.FloatField(read_only=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
url = serializers.CharField(source='get_absolute_url', read_only=True)
used_in = serializers.IntegerField(source='used_in_count', read_only=True)
def __init__(self, *args, **kwargs):
"""
Custom initialization method for PartSerializer,
so that we can optionally pass extra fields based on the query.
"""
self.starred_parts = kwargs.pop('starred_parts', [])
category_detail = kwargs.pop('category_detail', False)
super().__init__(*args, **kwargs)
if category_detail is not True:
self.fields.pop('category_detail')
@staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related('category')
queryset = queryset.prefetch_related('stock_items')
queryset = queryset.prefetch_related('bom_items')
queryset = queryset.prefetch_related('builds')
def prefetch_queryset(queryset):
"""
Prefetch related database tables,
to reduce database hits.
"""
return queryset.prefetch_related(
'category',
'stock_items',
'bom_items',
'builds',
'supplier_parts',
'supplier_parts__purchase_order_line_items',
'supplier_parts__purchase_order_line_items__order',
)
@staticmethod
def annotate_queryset(queryset):
"""
Add some extra annotations to the queryset,
performing database queries as efficiently as possible,
to reduce database trips.
"""
# Filter to limit stock items to "available"
stock_filter = Q(stock_items__status__in=StockStatus.AVAILABLE_CODES)
# Filter to limit orders to "open"
order_filter = Q(supplier_parts__purchase_order_line_items__order__status__in=OrderStatus.OPEN)
# Filter to limit builds to "active"
build_filter = Q(builds__status__in=BuildStatus.ACTIVE_CODES)
# Annotate the number total stock count
queryset = queryset.annotate(
in_stock=Coalesce(Sum('stock_items__quantity', filter=stock_filter, distinct=True), Decimal(0))
)
# Annotate the number of parts "on order"
# Total "on order" parts = "Quantity" - "Received" for each active purchase order
queryset = queryset.annotate(
ordering=Coalesce(Sum(
'supplier_parts__purchase_order_line_items__quantity',
filter=order_filter,
distinct=True
), Decimal(0)) - Coalesce(Sum(
'supplier_parts__purchase_order_line_items__received',
filter=order_filter,
distinct=True
), Decimal(0))
)
# Annotate number of parts being build
queryset = queryset.annotate(
building=Coalesce(
Sum('builds__quantity', filter=build_filter, distinct=True), Decimal(0)
)
)
return queryset
# TODO - Include a 'category_detail' field which serializers the category object
def get_starred(self, part):
"""
Return "true" if the part is starred by the current user.
"""
return part in self.starred_parts
# Extra detail for the category
category_detail = CategorySerializer(source='category', many=False, read_only=True)
# Calculated fields
in_stock = serializers.FloatField(read_only=True)
ordering = serializers.FloatField(read_only=True)
building = serializers.FloatField(read_only=True)
image = serializers.CharField(source='get_image_url', read_only=True)
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
starred = serializers.SerializerMethodField()
# TODO - Include annotation for the following fields:
# allocated_stock = serializers.FloatField(source='allocation_count', read_only=True)
# bom_items = serializers.IntegerField(source='bom_count', read_only=True)
# used_in = serializers.IntegerField(source='used_in_count', read_only=True)
class Meta:
model = Part
partial = True
fields = [
'active',
'allocated_stock',
# 'allocated_stock',
'assembly',
'bom_items',
'building',
# 'bom_items',
'category',
'category_name',
'category_detail',
'component',
'description',
'full_name',
'image',
'in_stock',
'ordering',
'building',
'IPN',
'is_template',
'keywords',
'link',
'minimum_stock',
'name',
'notes',
'on_order',
'pk',
'purchaseable',
'revision',
'salable',
'starred',
'thumbnail',
'trackable',
'total_stock',
'units',
'used_in',
'url', # Link to the part detail page
# 'used_in',
'variant_of',
'virtual',
]

View File

@@ -55,6 +55,12 @@ def inventree_version(*args, **kwargs):
return version.inventreeVersion()
@register.simple_tag()
def django_version(*args, **kwargs):
""" Return Django version string """
return version.inventreeDjangoVersion()
@register.simple_tag()
def inventree_commit_hash(*args, **kwargs):
""" Return InvenTree git commit hash string """

View File

@@ -5,7 +5,6 @@ JSON API for the Stock app
from django_filters.rest_framework import FilterSet, DjangoFilterBackend
from django_filters import NumberFilter
from django.conf import settings
from django.conf.urls import url, include
from django.urls import reverse
from django.db.models import Q
@@ -21,9 +20,7 @@ from .serializers import StockTrackingSerializer
from InvenTree.views import TreeSerializer
from InvenTree.helpers import str2bool, isNull
from InvenTree.status_codes import StockStatus
import os
from decimal import Decimal, InvalidOperation
from rest_framework.serializers import ValidationError
@@ -61,20 +58,28 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = StockItemSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = StockItemSerializer.prefetch_queryset(queryset)
queryset = StockItemSerializer.annotate_queryset(queryset)
return queryset
def get_serializer(self, *args, **kwargs):
try:
kwargs['part_detail'] = str2bool(self.request.GET.get('part_detail', False))
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', False))
except AttributeError:
pass
try:
kwargs['location_detail'] = str2bool(self.request.GET.get('location_detail', False))
kwargs['location_detail'] = str2bool(self.request.query_params.get('location_detail', False))
except AttributeError:
pass
try:
kwargs['supplier_detail'] = str2bool(self.request.GET.get('supplier_detail', False))
kwargs['supplier_part_detail'] = str2bool(self.request.query_params.get('supplier_detail', False))
except AttributeError:
pass
@@ -317,100 +322,46 @@ class StockList(generics.ListCreateAPIView):
- status: Filter by the StockItem status
"""
serializer_class = StockItemSerializer
queryset = StockItem.objects.all()
def get_serializer(self, *args, **kwargs):
try:
part_detail = str2bool(self.request.GET.get('part_detail', None))
location_detail = str2bool(self.request.GET.get('location_detail', None))
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', None))
except AttributeError:
part_detail = None
location_detail = None
pass
kwargs['part_detail'] = part_detail
kwargs['location_detail'] = location_detail
try:
kwargs['location_detail'] = str2bool(self.request.query_params.get('location_detail', None))
except AttributeError:
pass
try:
kwargs['supplier_part_detail'] = str2bool(self.request.query_params.get('supplier_part_detail', None))
except AttributeError:
pass
# Ensure the request context is passed through
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
def list(self, request, *args, **kwargs):
# TODO - Override the 'create' method for this view,
# to allow the user to be recorded when a new StockItem object is created
queryset = self.filter_queryset(self.get_queryset())
def get_queryset(self, *args, **kwargs):
# Instead of using the DRF serializer to LIST,
# we will serialize the objects manually.
# This is significantly faster
queryset = super().get_queryset(*args, **kwargs)
queryset = StockItemSerializer.prefetch_queryset(queryset)
data = queryset.values(
'pk',
'uid',
'parent',
'quantity',
'serial',
'batch',
'status',
'notes',
'link',
'location',
'location__name',
'location__description',
'part',
'part__IPN',
'part__name',
'part__revision',
'part__description',
'part__image',
'part__category',
'part__category__name',
'part__category__description',
'supplier_part',
)
return queryset
# Reduce the number of lookups we need to do for categories
# Cache location lookups for this query
locations = {}
for item in data:
img = item['part__image']
if img:
# Use the thumbnail image instead
fn, ext = os.path.splitext(img)
thumb = "{fn}.thumbnail{ext}".format(fn=fn, ext=ext)
thumb = os.path.join(settings.MEDIA_URL, thumb)
else:
thumb = ''
item['part__thumbnail'] = thumb
del item['part__image']
loc_id = item['location']
if loc_id:
if loc_id not in locations:
locations[loc_id] = StockLocation.objects.get(pk=loc_id).pathstring
item['location__path'] = locations[loc_id]
else:
item['location__path'] = None
item['status_text'] = StockStatus.label(item['status'])
return Response(data)
def get_queryset(self):
"""
If the query includes a particular location,
we may wish to also request stock items from all child locations.
"""
def filter_queryset(self, queryset):
# Start with all objects
stock_list = super(StockList, self).get_queryset()
stock_list = super().filter_queryset(queryset)
# Filter out parts which are not actually "in stock"
stock_list = stock_list.filter(customer=None, belongs_to=None)

View File

@@ -7,8 +7,8 @@ from rest_framework import serializers
from .models import StockItem, StockLocation
from .models import StockItemTracking
from part.serializers import PartBriefSerializer
from company.serializers import SupplierPartSerializer
from part.serializers import PartBriefSerializer
from InvenTree.serializers import UserSerializerBrief, InvenTreeModelSerializer
@@ -56,24 +56,45 @@ class StockItemSerializer(InvenTreeModelSerializer):
- Includes serialization for the item location
"""
url = serializers.CharField(source='get_absolute_url', read_only=True)
@staticmethod
def prefetch_queryset(queryset):
"""
Prefetch related database tables,
to reduce database hits.
"""
return queryset.prefetch_related(
'supplier_part',
'supplier_part__supplier',
'supplier_part__manufacturer',
'location',
'part',
'tracking_info',
)
@staticmethod
def annotate_queryset(queryset):
"""
Add some extra annotations to the queryset,
performing database queries as efficiently as possible.
"""
# TODO - Add custom annotated fields
return queryset
status_text = serializers.CharField(source='get_status_display', read_only=True)
part_name = serializers.CharField(source='get_part_name', read_only=True)
part_image = serializers.CharField(source='part__image', read_only=True)
tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
location_detail = LocationBriefSerializer(source='location', many=False, read_only=True)
supplier_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True)
supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True)
tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True)
def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False)
location_detail = kwargs.pop('location_detail', False)
supplier_detail = kwargs.pop('supplier_detail', False)
supplier_part_detail = kwargs.pop('supplier_part_detail', False)
super(StockItemSerializer, self).__init__(*args, **kwargs)
@@ -83,8 +104,8 @@ class StockItemSerializer(InvenTreeModelSerializer):
if location_detail is not True:
self.fields.pop('location_detail')
if supplier_detail is not True:
self.fields.pop('supplier_detail')
if supplier_part_detail is not True:
self.fields.pop('supplier_part_detail')
class Meta:
model = StockItem
@@ -97,18 +118,15 @@ class StockItemSerializer(InvenTreeModelSerializer):
'notes',
'part',
'part_detail',
'part_name',
'part_image',
'pk',
'quantity',
'serial',
'supplier_part',
'supplier_detail',
'supplier_part_detail',
'status',
'status_text',
'tracking_items',
'uid',
'url',
]
""" These fields are read-only in this context.

View File

@@ -25,6 +25,10 @@
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "InvenTree Version" %}</td><td><a href="https://github.com/inventree/InvenTree/releases">{% inventree_version %}</a></td>
</tr>
<tr>
<td><span class='fas fa-hashtag'></span></td>
<td>{% trans "Django Version" %}</td><td><a href="https://www.djangoproject.com/">{% django_version %}</a></td>
</tr>
<tr>
<td><span class='fas fa-code-branch'></span></td>
<td>{% trans "Commit Hash" %}</td><td><a href="https://github.com/inventree/InvenTree/commit/{% inventree_commit_hash %}">{% inventree_commit_hash %}</a></td>
@@ -45,7 +49,7 @@
<tr>
<td><span class='fas fa-exclamation-circle'></span></td>
<td>{% trans "Submit Bug Report" %}</td>
<td><a href='{% inventree_github_url %}/issues'>{% inventree_github_url %}/issues</a></td>
<td><a href='{% inventree_github_url %}/issues'>{% inventree_github_url %}issues</a></td>
</tr>
</table>

View File

@@ -3,7 +3,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from .serializers import UserSerializer
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.views import APIView
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from rest_framework import status
@@ -25,28 +25,32 @@ class UserList(generics.ListAPIView):
permission_classes = (permissions.IsAuthenticated,)
class GetAuthToken(ObtainAuthToken):
class GetAuthToken(APIView):
""" Return authentication token for an authenticated user. """
def post(self, request, *args, **kwargs):
permission_classes = [
permissions.IsAuthenticated,
]
def get(self, request, *args, **kwargs):
return self.login(request)
def delete(self, request):
return self.logout(request)
def login(self, request):
serializer = self.serializer_class(data=request.data,
context={'request': request})
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'pk': user.pk,
'username': user.username,
'email': user.email
})
if request.user.is_authenticated:
# Get the user token (or create one if it does not exist)
token, created = Token.objects.get_or_create(user=request.user)
return Response({
'token': token.key,
})
else:
return Response({
'error': 'User not authenticated',
})
def logout(self, request):
try:

View File

@@ -9,18 +9,18 @@ InvenTree is designed to be lightweight and easy to use for SME or hobbyist appl
However, powerful business logic works in the background to ensure that stock tracking history is maintained, and users have ready access to stock level information.
## User Documentation
## Getting Started
For InvenTree documentation, refer to the [user documentation](https://inventree.github.io).
Refer to the [getting started guide](https://inventree.github.io/docs/start/install) for installation and setup instructions.
## Documentation
For InvenTree documentation, refer to the [InvenTre documentation website](https://inventree.github.io).
## Developer Documentation
For site administrator and project code documentation, refer to the [developer documentation](http://inventree.readthedocs.io/en/latest/). This includes auto-generated documentation of the InvenTree python codebase.
For code documentation, refer to the [developer documentation](http://inventree.readthedocs.io/en/latest/).
## Getting Started
## Contributing
Refer to the [getting started guide](https://inventree.readthedocs.io/en/latest/start.html) for installation and setup instructions.
## Third Party Extensions
[InvenTree Docker](https://github.com/Zeigren/inventree-docker) - A docker build for InvenTree by [Zeigren](https://github.com/Zeigren)
Contributions are welcomed and encouraged. Please help to make this project even better! Refer to the [contribution page](https://inventree.github.io/pages/contribute).

View File

@@ -1,14 +0,0 @@
Backup and Restore
==================
.. toctree::
:titlesonly:
:maxdepth: 2
:caption: Backup
:hidden:
InvenTree provides database backup and restore functionality through the `django-dbbackup <https://github.com/django-dbbackup/django-dbbackup>`_ extension.
This extension allows database models and uploaded media files to be backed up (and restored) via the command line.
In the root InvenTre directory, run ``make backup`` to generate backup files for the database models and media files.

View File

@@ -1,79 +0,0 @@
InvenTree Configuration
=======================
.. toctree::
:titlesonly:
:maxdepth: 2
:caption: Configuration
:hidden:
Admin users will need to adjust the InvenTree installation to meet the particular needs of their setup. For example, pointing to the correct database backend, or specifying a list of allowed hosts.
The Django configuration parameters are found in the normal place (*settings.py*). However the settings presented in this file should not be adjusted as they will alter the core behaviour of the InvenTree application.
To support install specific settings, a simple configuration file ``config.yaml`` is provided. This configuration file is loaded by **settings.py** at runtime. Settings specific to a given install should be adjusted in ``config.yaml``.
The default configuration file launches a *DEBUG* configuration with a simple SQLITE database backend. This default configuration file is shown below:
.. literalinclude :: ../InvenTree/config_template.yaml
:language: yaml
:linenos:
Database Options
----------------
InvenTree provides support for multiple database backends - any backend supported natively by Django can be used.
Database options are specified under the *database* heading in the configuration file. Any option available in the Django documentation can be used here - it is passed through transparently to the management scripts.
**SQLite:**
By default, InvenTree uses an sqlite database file : ``inventree_db.sqlite3``. This provides a simple, portable database file that is easy to use for debug and testing purposes.
**MySQL:** MySQL database backend is supported with the native Django implemetation. To run InvenTree with the MySQL backend, a number of extra packages need to be installed:
* mysql-server - *MySQL backend server*
* libmysqlclient-dev - *Required for connecting to the MySQL database in Python*
* (pip) mysqlclient - *Python package for communication with MySQL database*
These requirements can be installed from the base directory with the command ``make mysql``.
It is then up to the database adminstrator to create a new MySQL database to store inventree data, in addition to a username/password to access the data.
.. important:: MySQL Collation:
When creating the MySQL database, the adminstrator must ensure that the collation option is set to *utf8_unicode_520_ci* to ensure that InvenTree features function correctly.
The database options (in the ``config.yaml`` file) then need to be adjusted to communicate the MySQL backend. Refer to the `Django docs <https://docs.djangoproject.com/en/dev/ref/databases/>`_ for further information.
**PostgreSQL:** PostgreSQL database backend is supported with the native Django implementation. Note that to use this backend, the following system packages must be installed:
* postgresql
* postgresql-contrib
* libpq-dev
* (pip3) psycopg2
These requirements can be installed from the base directory with the command ``make postgresql``.
It is then up to the database adminstrator to create a new PostgreSQL database to store inventree data, in addition to a username/password to access the data.
The database options (in the ``config.yaml`` file) then need to be adjusted to communicate the PostgreSQL backend. Refer to the `Django docs <https://docs.djangoproject.com/en/dev/ref/databases/>`_ for further information.
Allowed Hosts / CORS
--------------------
By default, all hosts are allowed, and CORS requests are enabled from any origin. **This is not secure and should be adjusted for your installation**. These options can be changed in the configuration file.
For further information, refer to the following documentation:
* `Django ALLOWED_HOSTS <https://docs.djangoproject.com/en/2.2/ref/settings/#allowed-hosts>`_
* `Django CORS headers <https://github.com/OttoYiu/django-cors-headers>`_
Uploaded File Storage
---------------------
By default, uploaded files are stored in the local direction ``./media``. This directory should be changed based on the particular installation requirements.
Backup Location
---------------
The default behaviour of the database backup is to generate backup files for database tables and media files to the user's temporary directory. The target directory can be overridden by setting the *backup_dir* parameter in the config file.

View File

@@ -1,54 +0,0 @@
Deploying InvenTree
===================
.. toctree::
:titlesonly:
:maxdepth: 2
:caption: Deployment
:hidden:
The development server provided by the Django ecosystem may be fine for a testing environment or small contained setups. However special consideration must be given when deploying InvenTree in a real-world environment.
Django apps provide multiple deployment methods - see the `Django documentation <https://docs.djangoproject.com/en/2.2/howto/deployment/>`_.
There are also numerous online tutorials describing how to deploy a Django application either locally or on an online platform.
Following is a simple tutorial on serving InvenTree using `Gunicorn <https://gunicorn.org/>`_. Gunicorn is a Python WSGI server which provides a multi-worker server which is much better suited to handling multiple simultaneous requests.
Install Gunicorn
----------------
Gunicorn can be installed using PIP:
``pip3 install gunicorn``
Configure Static Directories
----------------------------
Directories for storing *media* files and *static* files should be specified in the ``config.yaml`` configuration file. These directories are the ``MEDIA_ROOT`` and ``STATIC_ROOT`` paths required by the Django app.
Collect Static Files
--------------------
The required static files must be collected into the specified ``STATIC_ROOT`` directory:
``python3 InvenTree/manage.py collectstatic``
Configure Gunicorn
------------------
The Gunicorn server can be configured with a simple configuration file (e.g. python script). An example configuration file is provided in ``InvenTree/gunicorn.conf.py``
.. literalinclude :: ../InvenTree/gunicorn.conf.py
:language: python
:linenos:
This file can be used to configure the Gunicorn server to match particular requirements.
Run Gunicorn
------------
From the directory where ``manage.py`` is located:
Run ``gunicorn -c gunicorn.conf.py InvenTree.wsgi``

View File

@@ -6,14 +6,8 @@ InvenTree Source Documentation
:maxdepth: 2
:caption: Index
:hidden:
Getting Started<start>
Configuration<config>
Deployment<deploy>
Migrate Data<migrate>
Update InvenTree<update>
Translations<translate>
Backup and Restore<backup>
Modal Forms<forms>
Tables<tables>
REST API<rest>

View File

@@ -1,36 +0,0 @@
Migrating Data
==============
.. toctree::
:titlesonly:
:maxdepth: 2
:caption: Migrating Data
:hidden:
In the case that data needs to be migrated from one database installation to another, the following procedure can be used to export data, initialize the new database, and re-import the data.
Export Data
-----------
``python3 InvenTree/manage.py dumpdata --exclude contenttypes --exclude auth.permission --indent 2 > data.json``
This will export all data (including user information) to a json data file.
Initialize Database
-------------------
Configure the new database using the normal processes (see `Getting Started <start.html>`_):
``python3 InvenTree/manage.py makemigrations``
``python3 InvenTree/manage.py migrate --run-syncdb``
Import Data
-----------
The new database should now be correctly initialized with the correct table structures requried to import the data.
``python3 InvenTree/manage.py loaddata data.json``
.. important::
If the character encoding of the data file does not exactly match the target database, the import operation may not succeed. In this case, some manual editing of the data file may be required.

View File

@@ -1,124 +0,0 @@
Getting Started Guide
=====================
.. toctree::
:titlesonly:
:maxdepth: 2
:caption: Getting Started
:hidden:
To install a complete *development* environment for InvenTree, follow the steps presented below. A production environment will require further work as per the particular application requirements.
A makefile in the root directory provides shortcuts for the installation process, and can also be very useful during development.
Requirements
------------
To install InvenTree you will need the following system components installed:
* python3
* python3-dev
* python3-pip (pip3)
* g++
* make
Each of these programs need to be installed (e.g. using apt or similar) before running the ``make install`` script:
```
sudo apt-get install python3 python3-dev python3-pip g++ make
```
Virtual Environment
-------------------
Installing the required Python packages inside a virtual environment allows a local install separate to the system-wide Python installation. While not strictly necessary, using a virtual environment is highly recommended as it prevents conflicts between the different Python installations.
To configure Inventree inside a virtual environment, ``cd`` into the inventree base directory and run the following commands:
``apt-get install python3-venv``
``python3 -m venv inventree-env``
``source inventree-env/bin/activate``
This will place the current shell session inside a virtual environment - the terminal should display the ``(inventree-env)`` prefix.
.. note::
Remember to run ``source inventree-env/bin/activate`` when starting each shell session, before running Inventree commands. This will ensure that the correct environment is being used.
Installation
------------
First, download the latest InvenTree source code:
``git clone https://github.com/inventree/inventree/``
InvenTree is a Python/Django application and relies on the pip package manager. All packages required to develop and test InvenTree can be installed via pip. Package requirements can be found in ``requirements.txt``.
To setup the InvenTree environment, *cd into the inventree directory* and run the command:
``make install``
which installs all required Python packages using pip package manager. It also creates a (default) database configuration file which needs to be edited to meet user needs before proceeding (see next step below).
Additionally, this step creates a *SECRET_KEY* file which is used for the django authentication framework.
.. important::
The *SECRET_KEY* file should never be shared or made public.
Database Configuration
-----------------------
Once the required packages are installed, the database configuration must be adjusted to suit your particular needs. InvenTree provides a simple default setup which should work *out of the box* for testing and debug purposes.
As part of the previous *install* step, a configuration file (``config.yaml``) is created. The configuration file provides administrators control over various setup options without digging into the Django *settings.py* script. The default setup uses a local sqlite database with *DEBUG* mode enabled.
For further information on installation configuration, refer to the `Configuration <config.html>`_ section.
Initialize Database
-------------------
Once install settings are correctly configured (in *config.yaml*) run the initial setup script:
``make migrate``
This performs the initial database migrations, creating the required tables, etc.
The database should now be installed!
Create Admin Account
--------------------
Create an initial superuser (administrator) account for the InvenTree instance:
``make superuser``
Run Development Server
----------------------
Run ``cd InvenTree && python3 manage.py runserver 127.0.0.1:8000`` to launch a development server. This will launch the InvenTree web interface at ``127.0.0.1:8000``. For other options refer to the `django docs <https://docs.djangoproject.com/en/2.2/ref/django-admin/>`_.
Database Migrations
-------------------
Whenever a change is made to the underlying database schema, database migrations must be performed. Call ``make migrate`` to run any outstanding database migrations.
Development and Testing
-----------------------
Other shorthand functions are provided for the development and testing process:
* ``make install`` - Install all required underlying packages using PIP
* ``make update`` - Update InvenTree installation (after database configuration)
* ``make superuser`` - Create a superuser account
* ``make migrate`` - Perform database migrations
* ``make mysql`` - Install packages required for MySQL database backend
* ``make postgresql`` - Install packages required for PostgreSQL database backend
* ``make translate`` - Compile language translation files (requires gettext system package)
* ``make backup`` - Backup database tables and media files
* ``make test`` - Run all unit tests
* ``make coverage`` - Run all unit tests and generate code coverage report
* ``make style`` - Check Python codebase against PEP coding standards (using Flake)
* ``make docreqs`` - Install the packages required to generate documentation
* ``make docs`` - Generate this documentation

View File

@@ -1,42 +0,0 @@
Update InvenTree
================
.. toctree::
:titlesonly:
:maxdepth: 2
:caption: Update
:hidden:
Adminitrators wishing to update InvenTree to the latest version should follow the instructions below. The commands listed below should be run from the InvenTree root directory.
.. important::
It is advisable to backup the InvenTree database before performing these steps. The particular backup procedure may depend on your installation details. To perform a simple database dump, run the command ``make backup``.
Stop Server
-----------
Stop the InvenTree server (e.g. gunicorn)
Update Source
-------------
Update the InvenTree source code to the latest version.
``git pull origin master``
Perform Migrations
------------------
Updating the database is as simple as calling the makefile target:
``make update``
This command performs the following steps:
* Perform required database schema changes
* Collect required static files
Restart Server
--------------
Restart the InvenTree server

View File

@@ -1,11 +1,11 @@
wheel>=0.34.2 # Wheel
Django==2.2.10 # Django package
Django==3.0.5 # Django package
pillow==6.2.0 # Image manipulation
djangorestframework==3.10.3 # DRF framework
django-dbbackup==3.3.0 # Database backup / restore functionality
django-cors-headers==3.2.0 # CORS headers extension for DRF
django_filter==2.2.0 # Extended filtering options
django-mptt==0.10.0 # Modified Preorder Tree Traversal
django-dbbackup==3.2.0 # Database backup / restore functionality
django-mptt==0.11.0 # Modified Preorder Tree Traversal
django-markdownx==3.0.1 # Markdown form fields
django-markdownify==0.8.0 # Markdown rendering
coreapi==2.3.0 # API documentation
@@ -14,9 +14,12 @@ tablib==0.13.0 # Import / export data files
django-crispy-forms==1.8.1 # Form helpers
django-import-export==2.0.0 # Data import / export for admin interface
django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files
django-qr-code==1.1.0 # Generate QR codes
# TODO: Once the official django-qr-code package has been updated with Django3.x support,
# the following line should be removed.
git+git://github.com/chrissam/django-qr-code
# django-qr-code==1.1.0 # Generate QR codes
flake8==3.3.0 # PEP checking
coverage==4.0.3 # Unit test coverage
python-coveralls==2.9.1 # Coveralls linking (for Travis)
rapidfuzz==0.2.1 # Fuzzy string matching
django-stdimage==5.0.3 # Advanced ImageField management
rapidfuzz==0.7.6 # Fuzzy string matching
django-stdimage==5.1.1 # Advanced ImageField management