Merge remote-tracking branch 'origin/master' into bom_dev

This commit is contained in:
eeintech 2020-08-17 12:05:54 -05:00
commit 685a58b807
51 changed files with 2862 additions and 1438 deletions

View File

@ -242,7 +242,7 @@ def WrapWithQuotes(text, quote='"'):
return text
def MakeBarcode(object_name, object_data):
def MakeBarcode(object_name, object_pk, object_data, **kwargs):
""" Generate a string for a barcode. Adds some global InvenTree parameters.
Args:
@ -255,12 +255,20 @@ def MakeBarcode(object_name, object_data):
json string of the supplied data plus some other data
"""
data = {
'tool': 'InvenTree',
'version': inventreeVersion(),
'instance': inventreeInstanceName(),
object_name: object_data
}
brief = kwargs.get('brief', False)
data = {}
if brief:
data[object_name] = object_pk
else:
data['tool'] = 'InvenTree'
data['version'] = inventreeVersion()
data['instance'] = inventreeInstanceName()
# Ensure PK is included
object_data['id'] = object_pk
data[object_name] = object_data
return json.dumps(data, sort_keys=True)
@ -383,3 +391,56 @@ def ExtractSerialNumbers(serials, expected_quantity):
raise ValidationError([_("Number of unique serial number ({s}) must match quantity ({q})".format(s=len(numbers), q=expected_quantity))])
return numbers
def validateFilterString(value):
"""
Validate that a provided filter string looks like a list of comma-separated key=value pairs
These should nominally match to a valid database filter based on the model being filtered.
e.g. "category=6, IPN=12"
e.g. "part__name=widget"
The ReportTemplate class uses the filter string to work out which items a given report applies to.
For example, an acceptance test report template might only apply to stock items with a given IPN,
so the string could be set to:
filters = "IPN = ACME0001"
Returns a map of key:value pairs
"""
# Empty results map
results = {}
value = str(value).strip()
if not value or len(value) == 0:
return results
groups = value.split(',')
for group in groups:
group = group.strip()
pair = group.split('=')
if not len(pair) == 2:
raise ValidationError(
"Invalid group: {g}".format(g=group)
)
k, v = pair
k = k.strip()
v = v.strip()
if not k or not v:
raise ValidationError(
"Invalid group: {g}".format(g=group)
)
results[k] = v
return results

View File

@ -91,6 +91,8 @@ class QueryCountMiddleware(object):
To enable this middleware, set 'log_queries: True' in the local InvenTree config file.
Reference: https://www.dabapps.com/blog/logging-sql-queries-django-13/
Note: 2020-08-15 - This is no longer used, instead we now rely on the django-debug-toolbar addon
"""
def __init__(self, get_response):

View File

@ -130,6 +130,7 @@ INSTALLED_APPS = [
'build.apps.BuildConfig',
'common.apps.CommonConfig',
'company.apps.CompanyConfig',
'label.apps.LabelConfig',
'order.apps.OrderConfig',
'part.apps.PartConfig',
'report.apps.ReportConfig',
@ -172,11 +173,15 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'InvenTree.middleware.AuthRequiredMiddleware',
'InvenTree.middleware.AuthRequiredMiddleware'
]
if CONFIG.get('log_queries', False):
MIDDLEWARE.append('InvenTree.middleware.QueryCountMiddleware')
# If the debug toolbar is enabled, add the modules
if DEBUG and CONFIG.get('debug_toolbar', False):
print("Running with DEBUG_TOOLBAR enabled")
INSTALLED_APPS.append('debug_toolbar')
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
ROOT_URLCONF = 'InvenTree.urls'
@ -377,3 +382,8 @@ DBBACKUP_STORAGE = 'django.core.files.storage.FileSystemStorage'
DBBACKUP_STORAGE_OPTIONS = {
'location': CONFIG.get('backup_dir', tempfile.gettempdir()),
}
# Internal IP addresses allowed to see the debug toolbar
INTERNAL_IPS = [
'127.0.0.1',
]

View File

@ -167,10 +167,6 @@ class StockStatus(StatusCode):
# This can be used as a quick check for filtering
NOT_IN_STOCK = 100
SHIPPED = 110 # Item has been shipped to a customer
ASSIGNED_TO_BUILD = 120
ASSIGNED_TO_OTHER_ITEM = 130
options = {
OK: _("OK"),
ATTENTION: _("Attention needed"),
@ -179,9 +175,6 @@ class StockStatus(StatusCode):
LOST: _("Lost"),
REJECTED: _("Rejected"),
RETURNED: _("Returned"),
SHIPPED: _('Shipped'),
ASSIGNED_TO_BUILD: _("Used for Build"),
ASSIGNED_TO_OTHER_ITEM: _("Installed in Stock Item")
}
colors = {
@ -190,9 +183,6 @@ class StockStatus(StatusCode):
DAMAGED: 'red',
DESTROYED: 'red',
REJECTED: 'red',
SHIPPED: 'green',
ASSIGNED_TO_BUILD: 'blue',
ASSIGNED_TO_OTHER_ITEM: 'blue',
}
# The following codes correspond to parts that are 'available' or 'in stock'
@ -208,9 +198,6 @@ class StockStatus(StatusCode):
DESTROYED,
LOST,
REJECTED,
SHIPPED,
ASSIGNED_TO_BUILD,
ASSIGNED_TO_OTHER_ITEM,
]
# The following codes are available for receiving goods

View File

@ -138,6 +138,7 @@ class TestMakeBarcode(TestCase):
bc = helpers.MakeBarcode(
"part",
3,
{
"id": 3,
"url": "www.google.com",

View File

@ -6,6 +6,7 @@ Passes URL lookup downstream to each app as required.
from django.conf.urls import url, include
from django.urls import path
from django.contrib import admin
from django.contrib.auth import views as auth_views
from qr_code import urls as qr_code_urls
@ -135,5 +136,12 @@ urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# Media file access
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Debug toolbar access (if in DEBUG mode)
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
import debug_toolbar
urlpatterns = [
path('__debug/', include(debug_toolbar.urls)),
] + urlpatterns
# Send any unknown URLs to the parts page
urlpatterns += [url(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')]

View File

@ -6,7 +6,7 @@ import subprocess
from common.models import InvenTreeSetting
import django
INVENTREE_SW_VERSION = "0.1.1 pre"
INVENTREE_SW_VERSION = "0.1.3 pre"
def inventreeInstanceName():

View File

@ -47,12 +47,12 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
else:
return False
for key in ['tool', 'version']:
if key not in self.data.keys():
return False
# If any of the following keys are in the JSON data,
# let's go ahead and assume that the code is a valid InvenTree one...
if not self.data['tool'] == 'InvenTree':
return False
for key in ['tool', 'version', 'InvenTree', 'stockitem', 'location', 'part']:
if key in self.data.keys():
return True
return True
@ -60,10 +60,22 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
for k in self.data.keys():
if k.lower() == 'stockitem':
data = self.data[k]
pk = None
# Initially try casting to an integer
try:
pk = self.data[k]['id']
except (AttributeError, KeyError):
raise ValidationError({k: "id parameter not supplied"})
pk = int(data)
except (TypeError, ValueError):
pk = None
if pk is None:
try:
pk = self.data[k]['id']
except (AttributeError, KeyError):
raise ValidationError({k: "id parameter not supplied"})
try:
item = StockItem.objects.get(pk=pk)
@ -77,10 +89,21 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
for k in self.data.keys():
if k.lower() == 'stocklocation':
pk = None
# First try simple integer lookup
try:
pk = self.data[k]['id']
except (AttributeError, KeyError):
raise ValidationError({k: "id parameter not supplied"})
pk = int(self.data[k])
except (TypeError, ValueError):
pk = None
if pk is None:
# Lookup by 'id' field
try:
pk = self.data[k]['id']
except (AttributeError, KeyError):
raise ValidationError({k: "id parameter not supplied"})
try:
loc = StockLocation.objects.get(pk=pk)
@ -94,10 +117,20 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
for k in self.data.keys():
if k.lower() == 'part':
pk = None
# Try integer lookup first
try:
pk = self.data[k]['id']
except (AttributeError, KeyError):
raise ValidationError({k, 'id parameter not supplied'})
pk = int(self.data[k])
except (TypeError, ValueError):
pk = None
if pk is None:
try:
pk = self.data[k]['id']
except (AttributeError, KeyError):
raise ValidationError({k, 'id parameter not supplied'})
try:
part = Part.objects.get(pk=pk)

View File

@ -21,7 +21,7 @@ from markdownx.models import MarkdownxField
from mptt.models import MPTTModel, TreeForeignKey
from InvenTree.status_codes import BuildStatus, StockStatus
from InvenTree.status_codes import BuildStatus
from InvenTree.fields import InvenTreeURLField
from InvenTree.helpers import decimal2string
@ -501,7 +501,6 @@ class BuildItem(models.Model):
# TODO - If the item__part object is not trackable, delete the stock item here
item.status = StockStatus.ASSIGNED_TO_BUILD
item.build_order = self.build
item.save()

View File

@ -211,15 +211,12 @@ class BuildTest(TestCase):
# New stock items created and assigned to the build
self.assertEqual(StockItem.objects.get(pk=4).quantity, 50)
self.assertEqual(StockItem.objects.get(pk=4).build_order, self.build)
self.assertEqual(StockItem.objects.get(pk=4).status, status.StockStatus.ASSIGNED_TO_BUILD)
self.assertEqual(StockItem.objects.get(pk=5).quantity, 50)
self.assertEqual(StockItem.objects.get(pk=5).build_order, self.build)
self.assertEqual(StockItem.objects.get(pk=5).status, status.StockStatus.ASSIGNED_TO_BUILD)
self.assertEqual(StockItem.objects.get(pk=6).quantity, 250)
self.assertEqual(StockItem.objects.get(pk=6).build_order, self.build)
self.assertEqual(StockItem.objects.get(pk=6).status, status.StockStatus.ASSIGNED_TO_BUILD)
# And a new stock item created for the build output
self.assertEqual(StockItem.objects.get(pk=7).quantity, 1)

View File

@ -0,0 +1,17 @@
# Generated by Django 3.0.7 on 2020-08-08 07:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('company', '0022_auto_20200613_1045'),
]
operations = [
migrations.AlterModelOptions(
name='company',
options={'ordering': ['name']},
),
]

View File

@ -79,6 +79,9 @@ class Company(models.Model):
is_manufacturer: boolean value, is this company a manufacturer
"""
class Meta:
ordering = ['name', ]
name = models.CharField(max_length=100, blank=False, unique=True,
help_text=_('Company name'),
verbose_name=_('Company name'))

View File

@ -99,6 +99,10 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
if manufacturer_detail is not True:
self.fields.pop('manufacturer_detail')
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
manufacturer = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_manufacturer=True))
class Meta:
model = SupplierPart
fields = [

View File

@ -26,7 +26,8 @@
},
buttons: [
'#stock-options',
]
],
filterKey: "companystock",
});
$("#stock-export").click(function() {

View File

@ -58,9 +58,10 @@ static_root: '../inventree_static'
# - git
# - ssh
# Logging options
# If debug mode is enabled, set log_queries to True to show aggregate database queries in the debug console
log_queries: False
# Set debug_toolbar to True to enable a debugging toolbar for InvenTree
# Note: This will only be displayed if DEBUG mode is enabled,
# and only if InvenTree is accessed from a local IP (127.0.0.1)
debug_toolbar: False
# Backup options
# Set the backup_dir parameter to store backup files in a specific location

View File

14
InvenTree/label/admin.py Normal file
View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
from .models import StockItemLabel
class StockItemLabelAdmin(admin.ModelAdmin):
list_display = ('name', 'description', 'label')
admin.site.register(StockItemLabel, StockItemLabelAdmin)

5
InvenTree/label/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class LabelConfig(AppConfig):
name = 'label'

View File

@ -0,0 +1,30 @@
# Generated by Django 3.0.7 on 2020-08-15 23:27
import InvenTree.helpers
import django.core.validators
from django.db import migrations, models
import label.models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='StockItemLabel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Label name', max_length=100, unique=True)),
('description', models.CharField(blank=True, help_text='Label description', max_length=250, null=True)),
('label', models.FileField(help_text='Label template file', upload_to=label.models.rename_label, validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['html'])])),
('filters', models.CharField(blank=True, help_text='Query filters (comma-separated list of key=value pairs', max_length=250, validators=[InvenTree.helpers.validateFilterString])),
],
options={
'abstract': False,
},
),
]

View File

149
InvenTree/label/models.py Normal file
View File

@ -0,0 +1,149 @@
"""
Label printing models
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import io
from blabel import LabelWriter
from django.db import models
from django.core.validators import FileExtensionValidator
from django.utils.translation import gettext_lazy as _
from InvenTree.helpers import validateFilterString, normalize
from stock.models import StockItem
def rename_label(instance, filename):
""" Place the label file into the correct subdirectory """
filename = os.path.basename(filename)
return os.path.join('label', 'template', instance.SUBDIR, filename)
class LabelTemplate(models.Model):
"""
Base class for generic, filterable labels.
"""
class Meta:
abstract = True
# Each class of label files will be stored in a separate subdirectory
SUBDIR = "label"
@property
def template(self):
return self.label.path
def __str__(self):
return "{n} - {d}".format(
n=self.name,
d=self.description
)
name = models.CharField(
unique=True,
blank=False, max_length=100,
help_text=_('Label name'),
)
description = models.CharField(max_length=250, help_text=_('Label description'), blank=True, null=True)
label = models.FileField(
upload_to=rename_label,
blank=False, null=False,
help_text=_('Label template file'),
validators=[FileExtensionValidator(allowed_extensions=['html'])],
)
filters = models.CharField(
blank=True, max_length=250,
help_text=_('Query filters (comma-separated list of key=value pairs'),
validators=[validateFilterString]
)
def get_record_data(self, items):
"""
Return a list of dict objects, one for each item.
"""
return []
def render_to_file(self, filename, items, **kwargs):
"""
Render labels to a PDF file
"""
records = self.get_record_data(items)
writer = LabelWriter(self.template)
writer.write_labels(records, filename)
def render(self, items, **kwargs):
"""
Render labels to an in-memory PDF object, and return it
"""
records = self.get_record_data(items)
writer = LabelWriter(self.template)
buffer = io.BytesIO()
writer.write_labels(records, buffer)
return buffer
class StockItemLabel(LabelTemplate):
"""
Template for printing StockItem labels
"""
SUBDIR = "stockitem"
def matches_stock_item(self, item):
"""
Test if this label template matches a given StockItem object
"""
filters = validateFilterString(self.filters)
items = StockItem.objects.filter(**filters)
items = items.filter(pk=item.pk)
return items.exists()
def get_record_data(self, items):
"""
Generate context data for each provided StockItem
"""
records = []
for item in items:
# Add some basic information
records.append({
'item': item,
'part': item.part,
'name': item.part.name,
'ipn': item.part.IPN,
'quantity': normalize(item.quantity),
'serial': item.serial,
'uid': item.uid,
'pk': item.pk,
'qr_data': item.format_barcode(brief=True),
'tests': item.testResultMap()
})
return records

1
InvenTree/label/tests.py Normal file
View File

@ -0,0 +1 @@
# Create your tests here.

1
InvenTree/label/views.py Normal file
View File

@ -0,0 +1 @@
# Create your views here.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -190,6 +190,21 @@ class PartThumbs(generics.ListAPIView):
return Response(data)
class PartThumbsUpdate(generics.RetrieveUpdateAPIView):
""" API endpoint for updating Part thumbnails"""
queryset = Part.objects.all()
serializer_class = part_serializers.PartThumbSerializerUpdate
permission_classes = [
permissions.IsAuthenticated,
]
filter_backends = [
DjangoFilterBackend
]
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
""" API endpoint for detail view of a single Part object """
@ -589,6 +604,19 @@ class BomList(generics.ListCreateAPIView):
serializer_class = part_serializers.BomItemSerializer
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
data = serializer.data
if request.is_ajax():
return JsonResponse(data, safe=False)
else:
return Response(data)
def get_serializer(self, *args, **kwargs):
# Do we wish to include extra detail?
@ -607,8 +635,10 @@ class BomList(generics.ListCreateAPIView):
return self.serializer_class(*args, **kwargs)
def get_queryset(self):
def get_queryset(self, *args, **kwargs):
queryset = BomItem.objects.all()
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset
@ -716,7 +746,10 @@ part_api_urls = [
url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'),
])),
url(r'^thumbs/', PartThumbs.as_view(), name='api-part-thumbs'),
url(r'^thumbs/', include([
url(r'^$', PartThumbs.as_view(), name='api-part-thumbs'),
url(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'),
])),
url(r'^(?P<pk>\d+)/?', PartDetail.as_view(), name='api-part-detail'),

View File

@ -40,7 +40,7 @@ def MakeBomTemplate(fmt):
return DownloadFile(data, filename)
def ExportBom(part, fmt='csv', cascade=False):
def ExportBom(part, fmt='csv', cascade=False, max_levels=None):
""" Export a BOM (Bill of Materials) for a given part.
Args:
@ -59,7 +59,7 @@ def ExportBom(part, fmt='csv', cascade=False):
# Add items at a given layer
for item in items:
item.level = '-' * level
item.level = str(int(level))
# Avoid circular BOM references
if item.pk in uids:
@ -68,7 +68,8 @@ def ExportBom(part, fmt='csv', cascade=False):
bom_items.append(item)
if item.sub_part.assembly:
add_items(item.sub_part.bom_items.all().order_by('id'), level + 1)
if max_levels is None or level < max_levels:
add_items(item.sub_part.bom_items.all().order_by('id'), level + 1)
if cascade:
# Cascading (multi-level) BOM

View File

@ -56,6 +56,8 @@ class BomExportForm(forms.Form):
cascading = forms.BooleanField(label=_("Cascading"), required=False, initial=False, help_text=_("Download cascading / multi-level BOM"))
levels = forms.IntegerField(label=_("Levels"), required=True, initial=0, help_text=_("Select maximum number of BOM levels to export (0 = all levels)"))
def get_choices(self):
""" BOM export format choices """

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.7 on 2020-08-04 01:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('part', '0045_auto_20200605_0932'),
]
operations = [
migrations.AlterField(
model_name='partcategory',
name='default_keywords',
field=models.CharField(blank=True, help_text='Default keywords for parts in this category', max_length=250, null=True),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.0.7 on 2020-08-08 07:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('part', '0046_auto_20200804_0107'),
]
operations = [
migrations.AlterModelOptions(
name='part',
options={'ordering': ['name'], 'verbose_name': 'Part', 'verbose_name_plural': 'Parts'},
),
]

View File

@ -65,14 +65,14 @@ class PartCategory(InvenTreeTree):
help_text=_('Default location for parts in this category')
)
default_keywords = models.CharField(blank=True, max_length=250, help_text=_('Default keywords for parts in this category'))
default_keywords = models.CharField(null=True, blank=True, max_length=250, help_text=_('Default keywords for parts in this category'))
def get_absolute_url(self):
return reverse('category-detail', kwargs={'pk': self.id})
class Meta:
verbose_name = "Part Category"
verbose_name_plural = "Part Categories"
verbose_name = _("Part Category")
verbose_name_plural = _("Part Categories")
def get_parts(self, cascade=True):
""" Return a queryset for all parts under this category.
@ -239,6 +239,7 @@ class Part(MPTTModel):
class Meta:
verbose_name = _("Part")
verbose_name_plural = _("Parts")
ordering = ['name', ]
class MPTTMeta:
# For legacy reasons the 'variant_of' field is used to indicate the MPTT parent
@ -559,16 +560,17 @@ class Part(MPTTModel):
responsible = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True, related_name='parts_responible')
def format_barcode(self):
def format_barcode(self, **kwargs):
""" Return a JSON string for formatting a barcode for this Part object """
return helpers.MakeBarcode(
"part",
self.id,
{
"id": self.id,
"name": self.full_name,
"url": reverse('api-part-detail', kwargs={'pk': self.id}),
}
},
**kwargs
)
@property
@ -1490,7 +1492,7 @@ class BomItem(models.Model):
pass
class Meta:
verbose_name = "BOM Item"
verbose_name = _("BOM Item")
# Prevent duplication of parent/child rows
unique_together = ('part', 'sub_part')

View File

@ -1,6 +1,7 @@
"""
JSON serializers for Part app
"""
import imghdr
from rest_framework import serializers
@ -92,6 +93,27 @@ class PartThumbSerializer(serializers.Serializer):
count = serializers.IntegerField(read_only=True)
class PartThumbSerializerUpdate(InvenTreeModelSerializer):
""" Serializer for updating Part thumbnail """
def validate_image(self, value):
"""
Check that file is an image.
"""
validate = imghdr.what(value)
if not validate:
raise serializers.ValidationError("File is not an image")
return value
image = InvenTreeAttachmentSerializerField(required=True)
class Meta:
model = Part
fields = [
'image',
]
class PartBriefSerializer(InvenTreeModelSerializer):
""" Serializer for Part (brief detail) """
@ -214,6 +236,9 @@ class PartSerializer(InvenTreeModelSerializer):
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
starred = serializers.SerializerMethodField()
# PrimaryKeyRelated fields (Note: enforcing field type here results in much faster queries, somehow...)
category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all())
# 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)
@ -281,7 +306,12 @@ class BomItemSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField()
part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True))
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
sub_part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(component=True))
sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)
validated = serializers.BooleanField(read_only=True, source='is_line_valid')
@ -306,6 +336,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
queryset = queryset.prefetch_related('part')
queryset = queryset.prefetch_related('part__category')
queryset = queryset.prefetch_related('part__stock_items')
queryset = queryset.prefetch_related('sub_part')
queryset = queryset.prefetch_related('sub_part__category')
queryset = queryset.prefetch_related('sub_part__stock_items')

View File

@ -1392,10 +1392,22 @@ class BomDownload(AjaxView):
cascade = str2bool(request.GET.get('cascade', False))
levels = request.GET.get('levels', None)
if levels is not None:
try:
levels = int(levels)
if levels <= 0:
levels = None
except ValueError:
levels = None
if not IsValidBOMFormat(export_format):
export_format = 'csv'
return ExportBom(part, fmt=export_format, cascade=cascade)
return ExportBom(part, fmt=export_format, cascade=cascade, max_levels=levels)
def get_data(self):
return {
@ -1419,6 +1431,7 @@ class BomExport(AjaxView):
# Extract POSTed form data
fmt = request.POST.get('file_format', 'csv').lower()
cascade = str2bool(request.POST.get('cascading', False))
levels = request.POST.get('levels', None)
try:
part = Part.objects.get(pk=self.kwargs['pk'])
@ -1434,6 +1447,9 @@ class BomExport(AjaxView):
url += '?file_format=' + fmt
url += '&cascade=' + str(cascade)
if levels:
url += '&levels=' + str(levels)
data = {
'form_valid': part is not None,
'url': url,

View File

@ -338,11 +338,6 @@ class StockList(generics.ListCreateAPIView):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
data = serializer.data
@ -363,6 +358,7 @@ class StockList(generics.ListCreateAPIView):
part_ids.add(part)
sp = item['supplier_part']
if sp:
supplier_part_ids.add(sp)
@ -434,6 +430,7 @@ class StockList(generics.ListCreateAPIView):
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
queryset = StockItemSerializer.prefetch_queryset(queryset)
queryset = StockItemSerializer.annotate_queryset(queryset)
@ -477,6 +474,17 @@ class StockList(generics.ListCreateAPIView):
if customer:
queryset = queryset.filter(customer=customer)
# Filter if items have been sent to a customer (any customer)
sent_to_customer = params.get('sent_to_customer', None)
if sent_to_customer is not None:
sent_to_customer = str2bool(sent_to_customer)
if sent_to_customer:
queryset = queryset.exclude(customer=None)
else:
queryset = queryset.filter(customer=None)
# Filter by "serialized" status?
serialized = params.get('serialized', None)
@ -507,6 +515,7 @@ class StockList(generics.ListCreateAPIView):
if serial_number_lte is not None:
queryset = queryset.filter(serial__lte=serial_number_lte)
# Filter by "in_stock" status
in_stock = params.get('in_stock', None)
if in_stock is not None:
@ -539,10 +548,21 @@ class StockList(generics.ListCreateAPIView):
active = str2bool(active)
queryset = queryset.filter(part__active=active)
# Filter by 'depleted' status
depleted = params.get('depleted', None)
if depleted is not None:
depleted = str2bool(depleted)
if depleted:
queryset = queryset.filter(quantity__lte=0)
else:
queryset = queryset.exclude(quantity__lte=0)
# Filter by internal part number
IPN = params.get('IPN', None)
if IPN:
if IPN is not None:
queryset = queryset.filter(part__IPN=IPN)
# Does the client wish to filter by the Part ID?

View File

@ -46,6 +46,18 @@ class AssignStockItemToCustomerForm(HelperForm):
]
class ReturnStockItemForm(HelperForm):
"""
Form for manually returning a StockItem into stock
"""
class Meta:
model = StockItem
fields = [
'location',
]
class EditStockItemTestResultForm(HelperForm):
"""
Form for creating / editing a StockItemTestResult object.
@ -166,6 +178,37 @@ class SerializeStockForm(HelperForm):
]
class StockItemLabelSelectForm(HelperForm):
""" Form for selecting a label template for a StockItem """
label = forms.ChoiceField(
label=_('Label'),
help_text=_('Select test report template')
)
class Meta:
model = StockItem
fields = [
'label',
]
def get_label_choices(self, labels):
choices = []
if len(labels) > 0:
for label in labels:
choices.append((label.pk, label))
return choices
def __init__(self, labels, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['label'].choices = self.get_label_choices(labels)
class TestReportFormatForm(HelperForm):
""" Form for selection a test report template """

View File

@ -0,0 +1,19 @@
# Generated by Django 3.0.7 on 2020-08-07 23:44
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stock', '0047_auto_20200605_0932'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='status',
field=models.PositiveIntegerField(choices=[(10, 'OK'), (50, 'Attention needed'), (55, 'Damaged'), (60, 'Destroyed'), (70, 'Lost'), (65, 'Rejected'), (85, 'Returned')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
),
]

View File

@ -45,16 +45,17 @@ class StockLocation(InvenTreeTree):
def get_absolute_url(self):
return reverse('stock-location-detail', kwargs={'pk': self.id})
def format_barcode(self):
def format_barcode(self, **kwargs):
""" Return a JSON string for formatting a barcode for this StockLocation object """
return helpers.MakeBarcode(
'stocklocation',
self.pk,
{
"id": self.id,
"name": self.name,
"url": reverse('api-location-detail', kwargs={'pk': self.id}),
}
},
**kwargs
)
def get_stock_items(self, cascade=True):
@ -140,6 +141,7 @@ class StockItem(MPTTModel):
sales_order=None,
build_order=None,
belongs_to=None,
customer=None,
status__in=StockStatus.AVAILABLE_CODES
)
@ -219,12 +221,6 @@ class StockItem(MPTTModel):
super().clean()
if self.status == StockStatus.ASSIGNED_TO_OTHER_ITEM and self.belongs_to is None:
raise ValidationError({
'belongs_to': "Belongs_to field must be specified as statis is marked as ASSIGNED_TO_OTHER_ITEM",
'status': 'Status cannot be marked as ASSIGNED_TO_OTHER_ITEM if the belongs_to field is not set',
})
try:
if self.part.trackable:
# Trackable parts must have integer values for quantity field!
@ -288,7 +284,7 @@ class StockItem(MPTTModel):
def get_part_name(self):
return self.part.full_name
def format_barcode(self):
def format_barcode(self, **kwargs):
""" Return a JSON string for formatting a barcode for this StockItem.
Can be used to perform lookup of a stockitem using barcode
@ -301,10 +297,11 @@ class StockItem(MPTTModel):
return helpers.MakeBarcode(
"stockitem",
self.id,
{
"id": self.id,
"url": reverse('api-stock-detail', kwargs={'pk': self.id}),
}
},
**kwargs
)
uid = models.CharField(blank=True, max_length=128, help_text=("Unique identifier field"))
@ -477,7 +474,6 @@ class StockItem(MPTTModel):
# Update StockItem fields with new information
item.sales_order = order
item.status = StockStatus.SHIPPED
item.customer = customer
item.location = None
@ -495,6 +491,23 @@ class StockItem(MPTTModel):
# Return the reference to the stock item
return item
def returnFromCustomer(self, location, user=None):
"""
Return stock item from customer, back into the specified location.
"""
self.addTransactionNote(
_("Returned from customer") + " " + self.customer.name,
user,
notes=_("Returned to location") + " " + location.name,
system=True
)
self.customer = None
self.location = location
self.save()
# If stock item is incoming, an (optional) ETA field
# expected_arrival = models.DateField(null=True, blank=True)
@ -599,6 +612,10 @@ class StockItem(MPTTModel):
if self.build_order is not None:
return False
# Not 'in stock' if it has been assigned to a customer
if self.customer is not None:
return False
# Not 'in stock' if the status code makes it unavailable
if self.status in StockStatus.UNAVAILABLE_CODES:
return False

View File

@ -99,15 +99,34 @@ class StockItemSerializer(InvenTreeModelSerializer):
return queryset
belongs_to = serializers.PrimaryKeyRelatedField(read_only=True)
build_order = serializers.PrimaryKeyRelatedField(read_only=True)
customer = serializers.PrimaryKeyRelatedField(read_only=True)
location = serializers.PrimaryKeyRelatedField(read_only=True)
in_stock = serializers.BooleanField(read_only=True)
sales_order = serializers.PrimaryKeyRelatedField(read_only=True)
status_text = serializers.CharField(source='get_status_display', read_only=True)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
location_detail = LocationBriefSerializer(source='location', many=False, read_only=True)
supplier_part = serializers.PrimaryKeyRelatedField(read_only=True)
supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True)
part = serializers.PrimaryKeyRelatedField(read_only=True)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
location_detail = LocationBriefSerializer(source='location', many=False, read_only=True)
tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True, required=False)
quantity = serializers.FloatField()
allocated = serializers.FloatField(source='allocation_count', required=False)
serial = serializers.IntegerField(required=False)
@ -140,9 +159,9 @@ class StockItemSerializer(InvenTreeModelSerializer):
fields = [
'allocated',
'batch',
'build_order',
'belongs_to',
'customer',
'build_order',
'in_stock',
'link',
'location',
@ -155,10 +174,10 @@ class StockItemSerializer(InvenTreeModelSerializer):
'required_tests',
'sales_order',
'serial',
'supplier_part',
'supplier_part_detail',
'status',
'status_text',
'supplier_part',
'supplier_part_detail',
'tracking_items',
'uid',
]

View File

@ -78,7 +78,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
<button id='barcode-options' title='{% trans "Barcode actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-qrcode'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
<li><a href='#' id='show-qr-code'><span class='fas fa-qrcode'></span> {% trans "Show QR Code" %}</a></li>
<li class='disabled'><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
<li><a href='#' id='print-label'><span class='fas fa-tag'></span> {% trans "Print Label" %}</a></li>
{% if item.uid %}
<li><a href='#' id='unlink-barcode'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
{% else %}
@ -86,43 +86,47 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
{% endif %}
</ul>
</div>
{% if item.in_stock %}
<!-- Stock adjustment menu -->
<div class='dropdown dropdown-buttons'>
<button id='stock-options' title='{% trans "Stock adjustment actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
{% if item.in_stock %}
{% if not item.serialized %}
<li><a href='#' id='stock-count' title='{% trans "Count stock" %}'><span class='fas fa-clipboard-list'></span> {% trans "Count stock" %}</a></li>
<li><a href='#' id='stock-add' title='{% trans "Add stock" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
<li><a href='#' id='stock-remove' title='{% trans "Remove stock" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
{% endif %}
<li><a href='#' id='stock-move' title='{% trans "Transfer stock" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
</ul>
</div>
{% endif %}
<!-- Edit stock item -->
<div class='dropdown dropdown-buttons'>
<button id='stock-edit-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-tools'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
{% if item.part.trackable and not item.serialized %}
<li><a href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
{% endif %}
{% endif %}
{% if item.part.salable and not item.customer %}
<li><a href='#' id='stock-assign-to-customer' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
{% endif %}
{% if item.customer %}
<li><a href='#' id='stock-return-from-customer' title='{% trans "Return to stock" %}'><span class='fas fa-undo'></span> {% trans "Return to stock" %}</a></li>
{% endif %}
</ul>
</div>
<!-- Edit stock item -->
<div class='dropdown dropdown-buttons'>
<button id='stock-edit-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-tools'></span> <span class='caret'></span></button>
<ul class='dropdown-menu' role='menu'>
{% if item.part.has_variants %}
<li><a href='#' id='stock-convert' title='{% trans "Convert to variant" %}'><span class='fas fa-screwdriver'></span> {% trans "Convert to variant" %}</a></li>
{% endif %}
<li><a href='#' id='stock-duplicate' title='{% trans "Duplicate stock item" %}'><span class='fas fa-copy'></span> {% trans "Duplicate stock item" %}</a></li>
<li><a href='#' id='stock-edit' title='{% trans "Edit stock item" %}'><span class='fas fa-edit icon-blue'></span> {% trans "Edit stock item" %}</a></li>
{% if item.can_delete %}
{% if item.can_delete or user.is_staff %}
<li><a href='#' id='stock-delete' title='{% trans "Delete stock item" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete stock item" %}</a></li>
{% endif %}
</ul>
</div>
{% if item.part.has_test_report_templates %}
<button type='button' class='btn btn-default' id='stock-test-report' title='{% trans "Generate test report" %}'>
<span class='fas fa-tasks'/>
<span class='fas fa-file-invoice'/>
</button>
{% endif %}
</div>
@ -157,7 +161,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
<tr>
<td><span class='fas fa-user-tie'></span></td>
<td>{% trans "Customer" %}</td>
<td><a href="{% url 'company-detail' item.customer.id %}">{{ item.customer.name }}</a></td>
<td><a href="{% url 'company-detail-assigned-stock' item.customer.id %}">{{ item.customer.name }}</a></td>
</tr>
{% endif %}
{% if item.belongs_to %}
@ -310,6 +314,15 @@ $("#stock-test-report").click(function() {
});
{% endif %}
$("#print-label").click(function() {
launchModalForm(
"{% url 'stock-item-label-select' item.id %}",
{
follow: true,
}
)
});
$("#stock-duplicate").click(function() {
launchModalForm(
"{% url 'stock-item-create' %}",
@ -349,7 +362,6 @@ $("#unlink-barcode").click(function() {
{% if item.in_stock %}
{% if item.part.salable %}
$("#stock-assign-to-customer").click(function() {
launchModalForm("{% url 'stock-item-assign' item.id %}",
{
@ -357,7 +369,6 @@ $("#stock-assign-to-customer").click(function() {
}
);
});
{% endif %}
function itemAdjust(action) {
launchModalForm("/stock/adjust/",
@ -398,6 +409,16 @@ $('#stock-add').click(function() {
itemAdjust('add');
});
{% else %}
$("#stock-return-from-customer").click(function() {
launchModalForm("{% url 'stock-item-return' item.id %}",
{
reload: true,
}
);
});
{% endif %}
$("#stock-delete").click(function () {

View File

@ -1,11 +1,14 @@
{% extends "modal_delete_form.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block pre_form_content %}
<div class='alert alert-danger alert-block'>
Are you sure you want to delete this stock item?
{% trans "Are you sure you want to delete this stock item?" %}
<br>
This will remove <b>{{ item.quantity }}</b> units of <b>{{ item.part.full_name }}</b> from stock.
This will remove <b>{% decimal item.quantity %}</b> units of <b>{{ item.part.full_name }}</b> from stock.
</div>
{% endblock %}

View File

@ -32,6 +32,7 @@
<input class='numberinput'
min='0'
{% if stock_action == 'take' or stock_action == 'move' %} max='{{ item.quantity }}' {% endif %}
{% if item.serialized %} disabled='true' title='{% trans "Stock item is serialized and quantity cannot be adjusted" %}' {% endif %}
value='{% decimal item.new_quantity %}' type='number' name='stock-id-{{ item.id }}' id='stock-id-{{ item.id }}'/>
{% if item.error %}
<br><span class='help-inline'>{{ item.error }}</span>

View File

@ -24,10 +24,12 @@ stock_item_detail_urls = [
url(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'),
url(r'^delete_test_data/', views.StockItemDeleteTestData.as_view(), name='stock-item-delete-test-data'),
url(r'^assign/', views.StockItemAssignToCustomer.as_view(), name='stock-item-assign'),
url(r'^return/', views.StockItemReturnToStock.as_view(), name='stock-item-return'),
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
url(r'^test-report-select/', views.StockItemTestReportSelect.as_view(), name='stock-item-test-report-select'),
url(r'^label-select/', views.StockItemSelectLabels.as_view(), name='stock-item-label-select'),
url(r'^test/', views.StockItemDetail.as_view(template_name='stock/item_tests.html'), name='stock-item-test-results'),
url(r'^children/', views.StockItemDetail.as_view(template_name='stock/item_childs.html'), name='stock-item-children'),
@ -58,6 +60,7 @@ stock_urls = [
url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'),
url(r'^item/test-report-download/', views.StockItemTestReportDownload.as_view(), name='stock-item-test-report-download'),
url(r'^item/print-stock-labels/', views.StockItemPrintLabels.as_view(), name='stock-item-print-labels'),
# URLs for StockItem attachments
url(r'^item/attachment/', include([

View File

@ -28,6 +28,7 @@ from datetime import datetime
from company.models import Company, SupplierPart
from part.models import Part
from report.models import TestReport
from label.models import StockItemLabel
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
from .admin import StockItemResource
@ -260,6 +261,123 @@ class StockItemAssignToCustomer(AjaxUpdateView):
return self.renderJsonResponse(request, self.get_form(), data)
class StockItemReturnToStock(AjaxUpdateView):
"""
View for returning a stock item (which is assigned to a customer) to stock.
"""
model = StockItem
ajax_form_title = _("Return to Stock")
context_object_name = "item"
form_class = StockForms.ReturnStockItemForm
def post(self, request, *args, **kwargs):
location = request.POST.get('location', None)
if location:
try:
location = StockLocation.objects.get(pk=location)
except (ValueError, StockLocation.DoesNotExist):
location = None
if location:
stock_item = self.get_object()
stock_item.returnFromCustomer(location, request.user)
else:
raise ValidationError({'location': _("Specify a valid location")})
data = {
'form_valid': True,
'success': _("Stock item returned from customer")
}
return self.renderJsonResponse(request, self.get_form(), data)
class StockItemSelectLabels(AjaxView):
"""
View for selecting a template for printing labels for one (or more) StockItem objects
"""
model = StockItem
ajax_form_title = _('Select Label Template')
def get_form(self):
item = StockItem.objects.get(pk=self.kwargs['pk'])
labels = []
for label in StockItemLabel.objects.all():
if label.matches_stock_item(item):
labels.append(label)
return StockForms.StockItemLabelSelectForm(labels)
def post(self, request, *args, **kwargs):
label = request.POST.get('label', None)
try:
label = StockItemLabel.objects.get(pk=label)
except (ValueError, StockItemLabel.DoesNotExist):
raise ValidationError({'label': _("Select valid label")})
stock_item = StockItem.objects.get(pk=self.kwargs['pk'])
url = reverse('stock-item-print-labels')
url += '?label={pk}'.format(pk=label.pk)
url += '&items[]={pk}'.format(pk=stock_item.pk)
data = {
'form_valid': True,
'url': url,
}
return self.renderJsonResponse(request, self.get_form(), data=data)
class StockItemPrintLabels(AjaxView):
"""
View for printing labels and returning a PDF
Requires the following arguments to be passed as URL params:
items: List of valid StockItem pk values
label: Valid pk of a StockItemLabel template
"""
def get(self, request, *args, **kwargs):
label = request.GET.get('label', None)
try:
label = StockItemLabel.objects.get(pk=label)
except (ValueError, StockItemLabel.DoesNotExist):
raise ValidationError({'label': 'Invalid label ID'})
item_pks = request.GET.getlist('items[]')
items = []
for pk in item_pks:
try:
item = StockItem.objects.get(pk=pk)
items.append(item)
except (ValueError, StockItem.DoesNotExist):
pass
if len(items) == 0:
raise ValidationError({'items': 'Must provide valid stockitems'})
pdf = label.render(items).getbuffer()
return DownloadFile(pdf, 'stock_labels.pdf', content_type='application/pdf')
class StockItemDeleteTestData(AjaxUpdateView):
"""
View for deleting all test data
@ -1239,8 +1357,8 @@ class StockItemCreate(AjaxCreateView):
valid = False
form.errors['quantity'] = [_('Invalid quantity')]
if quantity <= 0:
form.errors['quantity'] = [_('Quantity must be greater than zero')]
if quantity < 0:
form.errors['quantity'] = [_('Quantity cannot be less than zero')]
valid = False
if part is None:

View File

@ -1,4 +1,5 @@
{% load i18n %}
{% load status_codes %}
/* Stock API functions
* Requires api.js to be loaded first
@ -425,16 +426,10 @@ function loadStockTable(table, options) {
sortable: true,
formatter: function(value, row, index, field) {
var url = '';
var url = `/stock/item/${row.pk}/`;
var thumb = row.part_detail.thumbnail;
var name = row.part_detail.full_name;
if (row.supplier_part) {
url = `/supplier-part/${row.supplier_part}/`;
} else {
url = `/part/${row.part}/`;
}
html = imageHoverIcon(thumb) + renderLink(name, url);
return html;
@ -466,12 +461,30 @@ function loadStockTable(table, options) {
var html = renderLink(val, `/stock/item/${row.pk}/`);
if (row.allocated) {
html += `<span class='fas fa-bookmark label-right' title='{% trans "StockItem has been allocated" %}'></span>`;
html += `<span class='fas fa-bookmark label-right' title='{% trans "Stock item has been allocated" %}'></span>`;
}
if (row.customer) {
html += `<span class='fas fa-user-tie label-right' title='{% trans "Stock item has been assigned to customer" %}'></span>`;
} else if (row.build_order) {
html += `<span class='fas fa-tools label-right' title='{% trans "Stock item was assigned to a build order" %}'></span>`;
} else if (row.sales_order) {
html += `<span class='fas fa-dollar-sign label-right' title='{% trans "Stock item was assigned to a sales order" %}'></span>`;
}
// Special stock status codes
// 65 = "REJECTED"
if (row.status == 65) {
html += `<span class='fas fa-times-circle label-right' title='{% trans "Stock item has been rejected" %}'></span>`;
}
// 70 = "LOST"
if (row.status == 70) {
html += `<span class='fas fa-question-circle label-right' title='{% trans "StockItem is lost" %}'></span>`;
else if (row.status == 70) {
html += `<span class='fas fa-question-circle label-right' title='{% trans "Stock item is lost" %}'></span>`;
}
if (row.quantity <= 0) {
html += `<span class='label label-right label-danger'>{% trans "Depleted" %}</span>`;
}
return html;

View File

@ -32,30 +32,35 @@ function getAvailableTableFilters(tableKey) {
// Filters for the "Stock" table
if (tableKey == 'stock') {
return {
in_stock: {
active: {
type: 'bool',
title: '{% trans "In Stock" %}',
description: '{% trans "Show items which are in stock" %}',
title: '{% trans "Active parts" %}',
description: '{% trans "Show stock for active parts" %}',
},
allocated: {
type: 'bool',
title: '{% trans "Is allocated" %}',
description: '{% trans "Item has been alloacted" %}',
},
cascade: {
type: 'bool',
title: '{% trans "Include sublocations" %}',
description: '{% trans "Include stock in sublocations" %}',
},
active: {
depleted: {
type: 'bool',
title: '{% trans "Active parts" %}',
description: '{% trans "Show stock for active parts" %}',
title: '{% trans "Depleted" %}',
description: '{% trans "Show stock items which are depleted" %}',
},
status: {
options: stockCodes,
title: '{% trans "Stock status" %}',
description: '{% trans "Stock status" %}',
},
allocated: {
in_stock: {
type: 'bool',
title: '{% trans "Is allocated" %}',
description: '{% trans "Item has been alloacted" %}',
title: '{% trans "In Stock" %}',
description: '{% trans "Show items which are in stock" %}',
},
sent_to_customer: {
type: 'bool',
title: '{% trans "Sent to customer" %}',
description: '{% trans "Show items which have been assigned to a customer" %}',
},
serialized: {
type: 'bool',
@ -69,6 +74,11 @@ function getAvailableTableFilters(tableKey) {
title: "{% trans "Serial number LTE" %}",
description: "{% trans "Serial number less than or equal to" %}",
},
status: {
options: stockCodes,
title: '{% trans "Stock status" %}',
description: '{% trans "Stock status" %}',
},
};
}

View File

@ -18,14 +18,18 @@ function {{ label }}StatusDisplay(key) {
key = String(key);
var value = {{ label }}Codes[key].value;
var value = null;
var label = null;
if (key in {{ label }}Codes) {
value = {{ label }}Codes[key].value;
label = {{ label }}Codes[key].label;
}
if (value == null || value.length == 0) {
value = key;
label = '';
}
// Select the label color
var label = {{ label }}Codes[key].label ?? '';
return `<span class='label ${label}'>${value}</span>`;
}

View File

@ -51,12 +51,12 @@ style:
# Run unit tests
test:
cd InvenTree && python3 manage.py check
cd InvenTree && python3 manage.py test barcode build common company order part report stock InvenTree
cd InvenTree && python3 manage.py test barcode build common company label order part report stock InvenTree
# Run code coverage
coverage:
cd InvenTree && python3 manage.py check
coverage run InvenTree/manage.py test barcode build common company order part report stock InvenTree
coverage run InvenTree/manage.py test barcode build common company label order part report stock InvenTree
coverage html
# Install packages required to generate code docs

View File

@ -1,4 +1,4 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Build Status](https://travis-ci.org/inventree/InvenTree.svg?branch=master)](https://travis-ci.org/inventree/InvenTree) [![Documentation Status](https://readthedocs.org/projects/inventree/badge/?version=latest)](https://inventree.readthedocs.io/en/latest/?badge=latest) [![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Build Status](https://travis-ci.org/inventree/InvenTree.svg?branch=master)](https://travis-ci.org/inventree/InvenTree) [![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree)
<img src="images/logo/inventree.png" alt="InvenTree" width="128"/>
@ -15,7 +15,7 @@ Refer to the [getting started guide](https://inventree.github.io/docs/start/inst
## Documentation
For InvenTree documentation, refer to the [InvenTre documentation website](https://inventree.github.io).
For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.github.io).
## Integration

View File

@ -1,6 +1,7 @@
wheel>=0.34.2 # Wheel
Django==3.0.7 # Django package
pillow==6.2.2 # Image manipulation
pillow==7.1.0 # Image manipulation
blabel==0.1.3 # Simple PDF label printing
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
@ -15,10 +16,11 @@ 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.2.0 # Generate QR codes
flake8==3.3.0 # PEP checking
flake8==3.8.3 # PEP checking
coverage==4.0.3 # Unit test coverage
python-coveralls==2.9.1 # Coveralls linking (for Travis)
rapidfuzz==0.7.6 # Fuzzy string matching
django-stdimage==5.1.1 # Advanced ImageField management
django-tex==1.1.7 # LaTeX PDF export
django-weasyprint==1.0.1 # HTML PDF export
django-debug-toolbar==2.2 # Debug / profiling toolbar