mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +00:00
Merge remote-tracking branch 'origin/master' into bom_dev
This commit is contained in:
commit
685a58b807
@ -242,7 +242,7 @@ def WrapWithQuotes(text, quote='"'):
|
|||||||
return text
|
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.
|
""" Generate a string for a barcode. Adds some global InvenTree parameters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -255,12 +255,20 @@ def MakeBarcode(object_name, object_data):
|
|||||||
json string of the supplied data plus some other data
|
json string of the supplied data plus some other data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = {
|
brief = kwargs.get('brief', False)
|
||||||
'tool': 'InvenTree',
|
|
||||||
'version': inventreeVersion(),
|
data = {}
|
||||||
'instance': inventreeInstanceName(),
|
|
||||||
object_name: object_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)
|
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))])
|
raise ValidationError([_("Number of unique serial number ({s}) must match quantity ({q})".format(s=len(numbers), q=expected_quantity))])
|
||||||
|
|
||||||
return numbers
|
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
|
||||||
|
@ -91,6 +91,8 @@ class QueryCountMiddleware(object):
|
|||||||
To enable this middleware, set 'log_queries: True' in the local InvenTree config file.
|
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/
|
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):
|
def __init__(self, get_response):
|
||||||
|
@ -130,6 +130,7 @@ INSTALLED_APPS = [
|
|||||||
'build.apps.BuildConfig',
|
'build.apps.BuildConfig',
|
||||||
'common.apps.CommonConfig',
|
'common.apps.CommonConfig',
|
||||||
'company.apps.CompanyConfig',
|
'company.apps.CompanyConfig',
|
||||||
|
'label.apps.LabelConfig',
|
||||||
'order.apps.OrderConfig',
|
'order.apps.OrderConfig',
|
||||||
'part.apps.PartConfig',
|
'part.apps.PartConfig',
|
||||||
'report.apps.ReportConfig',
|
'report.apps.ReportConfig',
|
||||||
@ -172,11 +173,15 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'InvenTree.middleware.AuthRequiredMiddleware',
|
|
||||||
|
'InvenTree.middleware.AuthRequiredMiddleware'
|
||||||
]
|
]
|
||||||
|
|
||||||
if CONFIG.get('log_queries', False):
|
# If the debug toolbar is enabled, add the modules
|
||||||
MIDDLEWARE.append('InvenTree.middleware.QueryCountMiddleware')
|
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'
|
ROOT_URLCONF = 'InvenTree.urls'
|
||||||
|
|
||||||
@ -377,3 +382,8 @@ DBBACKUP_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
|||||||
DBBACKUP_STORAGE_OPTIONS = {
|
DBBACKUP_STORAGE_OPTIONS = {
|
||||||
'location': CONFIG.get('backup_dir', tempfile.gettempdir()),
|
'location': CONFIG.get('backup_dir', tempfile.gettempdir()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Internal IP addresses allowed to see the debug toolbar
|
||||||
|
INTERNAL_IPS = [
|
||||||
|
'127.0.0.1',
|
||||||
|
]
|
||||||
|
@ -167,10 +167,6 @@ class StockStatus(StatusCode):
|
|||||||
# This can be used as a quick check for filtering
|
# This can be used as a quick check for filtering
|
||||||
NOT_IN_STOCK = 100
|
NOT_IN_STOCK = 100
|
||||||
|
|
||||||
SHIPPED = 110 # Item has been shipped to a customer
|
|
||||||
ASSIGNED_TO_BUILD = 120
|
|
||||||
ASSIGNED_TO_OTHER_ITEM = 130
|
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
OK: _("OK"),
|
OK: _("OK"),
|
||||||
ATTENTION: _("Attention needed"),
|
ATTENTION: _("Attention needed"),
|
||||||
@ -179,9 +175,6 @@ class StockStatus(StatusCode):
|
|||||||
LOST: _("Lost"),
|
LOST: _("Lost"),
|
||||||
REJECTED: _("Rejected"),
|
REJECTED: _("Rejected"),
|
||||||
RETURNED: _("Returned"),
|
RETURNED: _("Returned"),
|
||||||
SHIPPED: _('Shipped'),
|
|
||||||
ASSIGNED_TO_BUILD: _("Used for Build"),
|
|
||||||
ASSIGNED_TO_OTHER_ITEM: _("Installed in Stock Item")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
colors = {
|
colors = {
|
||||||
@ -190,9 +183,6 @@ class StockStatus(StatusCode):
|
|||||||
DAMAGED: 'red',
|
DAMAGED: 'red',
|
||||||
DESTROYED: 'red',
|
DESTROYED: 'red',
|
||||||
REJECTED: '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'
|
# The following codes correspond to parts that are 'available' or 'in stock'
|
||||||
@ -208,9 +198,6 @@ class StockStatus(StatusCode):
|
|||||||
DESTROYED,
|
DESTROYED,
|
||||||
LOST,
|
LOST,
|
||||||
REJECTED,
|
REJECTED,
|
||||||
SHIPPED,
|
|
||||||
ASSIGNED_TO_BUILD,
|
|
||||||
ASSIGNED_TO_OTHER_ITEM,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# The following codes are available for receiving goods
|
# The following codes are available for receiving goods
|
||||||
|
@ -138,6 +138,7 @@ class TestMakeBarcode(TestCase):
|
|||||||
|
|
||||||
bc = helpers.MakeBarcode(
|
bc = helpers.MakeBarcode(
|
||||||
"part",
|
"part",
|
||||||
|
3,
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"url": "www.google.com",
|
"url": "www.google.com",
|
||||||
|
@ -6,6 +6,7 @@ Passes URL lookup downstream to each app as required.
|
|||||||
|
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
from django.urls import path
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth import views as auth_views
|
from django.contrib.auth import views as auth_views
|
||||||
from qr_code import urls as qr_code_urls
|
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
|
# Media file access
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
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
|
# Send any unknown URLs to the parts page
|
||||||
urlpatterns += [url(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')]
|
urlpatterns += [url(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')]
|
||||||
|
@ -6,7 +6,7 @@ import subprocess
|
|||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
import django
|
import django
|
||||||
|
|
||||||
INVENTREE_SW_VERSION = "0.1.1 pre"
|
INVENTREE_SW_VERSION = "0.1.3 pre"
|
||||||
|
|
||||||
|
|
||||||
def inventreeInstanceName():
|
def inventreeInstanceName():
|
||||||
|
@ -47,12 +47,12 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for key in ['tool', 'version']:
|
# If any of the following keys are in the JSON data,
|
||||||
if key not in self.data.keys():
|
# let's go ahead and assume that the code is a valid InvenTree one...
|
||||||
return False
|
|
||||||
|
|
||||||
if not self.data['tool'] == 'InvenTree':
|
for key in ['tool', 'version', 'InvenTree', 'stockitem', 'location', 'part']:
|
||||||
return False
|
if key in self.data.keys():
|
||||||
|
return True
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -60,10 +60,22 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
|
|
||||||
for k in self.data.keys():
|
for k in self.data.keys():
|
||||||
if k.lower() == 'stockitem':
|
if k.lower() == 'stockitem':
|
||||||
|
|
||||||
|
data = self.data[k]
|
||||||
|
|
||||||
|
pk = None
|
||||||
|
|
||||||
|
# Initially try casting to an integer
|
||||||
try:
|
try:
|
||||||
pk = self.data[k]['id']
|
pk = int(data)
|
||||||
except (AttributeError, KeyError):
|
except (TypeError, ValueError):
|
||||||
raise ValidationError({k: "id parameter not supplied"})
|
pk = None
|
||||||
|
|
||||||
|
if pk is None:
|
||||||
|
try:
|
||||||
|
pk = self.data[k]['id']
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
raise ValidationError({k: "id parameter not supplied"})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
item = StockItem.objects.get(pk=pk)
|
item = StockItem.objects.get(pk=pk)
|
||||||
@ -77,10 +89,21 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
|
|
||||||
for k in self.data.keys():
|
for k in self.data.keys():
|
||||||
if k.lower() == 'stocklocation':
|
if k.lower() == 'stocklocation':
|
||||||
|
|
||||||
|
pk = None
|
||||||
|
|
||||||
|
# First try simple integer lookup
|
||||||
try:
|
try:
|
||||||
pk = self.data[k]['id']
|
pk = int(self.data[k])
|
||||||
except (AttributeError, KeyError):
|
except (TypeError, ValueError):
|
||||||
raise ValidationError({k: "id parameter not supplied"})
|
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:
|
try:
|
||||||
loc = StockLocation.objects.get(pk=pk)
|
loc = StockLocation.objects.get(pk=pk)
|
||||||
@ -94,10 +117,20 @@ class InvenTreeBarcodePlugin(BarcodePlugin):
|
|||||||
|
|
||||||
for k in self.data.keys():
|
for k in self.data.keys():
|
||||||
if k.lower() == 'part':
|
if k.lower() == 'part':
|
||||||
|
|
||||||
|
pk = None
|
||||||
|
|
||||||
|
# Try integer lookup first
|
||||||
try:
|
try:
|
||||||
pk = self.data[k]['id']
|
pk = int(self.data[k])
|
||||||
except (AttributeError, KeyError):
|
except (TypeError, ValueError):
|
||||||
raise ValidationError({k, 'id parameter not supplied'})
|
pk = None
|
||||||
|
|
||||||
|
if pk is None:
|
||||||
|
try:
|
||||||
|
pk = self.data[k]['id']
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
raise ValidationError({k, 'id parameter not supplied'})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
part = Part.objects.get(pk=pk)
|
part = Part.objects.get(pk=pk)
|
||||||
|
@ -21,7 +21,7 @@ from markdownx.models import MarkdownxField
|
|||||||
|
|
||||||
from mptt.models import MPTTModel, TreeForeignKey
|
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.fields import InvenTreeURLField
|
||||||
from InvenTree.helpers import decimal2string
|
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
|
# 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.build_order = self.build
|
||||||
item.save()
|
item.save()
|
||||||
|
|
||||||
|
@ -211,15 +211,12 @@ class BuildTest(TestCase):
|
|||||||
# New stock items created and assigned to the build
|
# 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).quantity, 50)
|
||||||
self.assertEqual(StockItem.objects.get(pk=4).build_order, self.build)
|
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).quantity, 50)
|
||||||
self.assertEqual(StockItem.objects.get(pk=5).build_order, self.build)
|
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).quantity, 250)
|
||||||
self.assertEqual(StockItem.objects.get(pk=6).build_order, self.build)
|
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
|
# And a new stock item created for the build output
|
||||||
self.assertEqual(StockItem.objects.get(pk=7).quantity, 1)
|
self.assertEqual(StockItem.objects.get(pk=7).quantity, 1)
|
||||||
|
17
InvenTree/company/migrations/0023_auto_20200808_0715.py
Normal file
17
InvenTree/company/migrations/0023_auto_20200808_0715.py
Normal 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']},
|
||||||
|
),
|
||||||
|
]
|
@ -79,6 +79,9 @@ class Company(models.Model):
|
|||||||
is_manufacturer: boolean value, is this company a manufacturer
|
is_manufacturer: boolean value, is this company a manufacturer
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ['name', ]
|
||||||
|
|
||||||
name = models.CharField(max_length=100, blank=False, unique=True,
|
name = models.CharField(max_length=100, blank=False, unique=True,
|
||||||
help_text=_('Company name'),
|
help_text=_('Company name'),
|
||||||
verbose_name=_('Company name'))
|
verbose_name=_('Company name'))
|
||||||
|
@ -99,6 +99,10 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
if manufacturer_detail is not True:
|
if manufacturer_detail is not True:
|
||||||
self.fields.pop('manufacturer_detail')
|
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:
|
class Meta:
|
||||||
model = SupplierPart
|
model = SupplierPart
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -26,7 +26,8 @@
|
|||||||
},
|
},
|
||||||
buttons: [
|
buttons: [
|
||||||
'#stock-options',
|
'#stock-options',
|
||||||
]
|
],
|
||||||
|
filterKey: "companystock",
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#stock-export").click(function() {
|
$("#stock-export").click(function() {
|
||||||
|
@ -58,9 +58,10 @@ static_root: '../inventree_static'
|
|||||||
# - git
|
# - git
|
||||||
# - ssh
|
# - ssh
|
||||||
|
|
||||||
# Logging options
|
# Set debug_toolbar to True to enable a debugging toolbar for InvenTree
|
||||||
# If debug mode is enabled, set log_queries to True to show aggregate database queries in the debug console
|
# Note: This will only be displayed if DEBUG mode is enabled,
|
||||||
log_queries: False
|
# and only if InvenTree is accessed from a local IP (127.0.0.1)
|
||||||
|
debug_toolbar: False
|
||||||
|
|
||||||
# Backup options
|
# Backup options
|
||||||
# Set the backup_dir parameter to store backup files in a specific location
|
# Set the backup_dir parameter to store backup files in a specific location
|
||||||
|
0
InvenTree/label/__init__.py
Normal file
0
InvenTree/label/__init__.py
Normal file
14
InvenTree/label/admin.py
Normal file
14
InvenTree/label/admin.py
Normal 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
5
InvenTree/label/apps.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class LabelConfig(AppConfig):
|
||||||
|
name = 'label'
|
30
InvenTree/label/migrations/0001_initial.py
Normal file
30
InvenTree/label/migrations/0001_initial.py
Normal 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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
InvenTree/label/migrations/__init__.py
Normal file
0
InvenTree/label/migrations/__init__.py
Normal file
149
InvenTree/label/models.py
Normal file
149
InvenTree/label/models.py
Normal 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
1
InvenTree/label/tests.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Create your tests here.
|
1
InvenTree/label/views.py
Normal file
1
InvenTree/label/views.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Create your views here.
|
Binary file not shown.
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
@ -190,6 +190,21 @@ class PartThumbs(generics.ListAPIView):
|
|||||||
return Response(data)
|
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):
|
class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
""" API endpoint for detail view of a single Part object """
|
""" API endpoint for detail view of a single Part object """
|
||||||
|
|
||||||
@ -588,7 +603,20 @@ class BomList(generics.ListCreateAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = part_serializers.BomItemSerializer
|
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):
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
|
||||||
# Do we wish to include extra detail?
|
# Do we wish to include extra detail?
|
||||||
@ -607,8 +635,10 @@ class BomList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self, *args, **kwargs):
|
||||||
|
|
||||||
queryset = BomItem.objects.all()
|
queryset = BomItem.objects.all()
|
||||||
|
|
||||||
queryset = self.get_serializer_class().setup_eager_loading(queryset)
|
queryset = self.get_serializer_class().setup_eager_loading(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
@ -716,7 +746,10 @@ part_api_urls = [
|
|||||||
url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'),
|
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'),
|
url(r'^(?P<pk>\d+)/?', PartDetail.as_view(), name='api-part-detail'),
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ def MakeBomTemplate(fmt):
|
|||||||
return DownloadFile(data, filename)
|
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.
|
""" Export a BOM (Bill of Materials) for a given part.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -59,8 +59,8 @@ def ExportBom(part, fmt='csv', cascade=False):
|
|||||||
# Add items at a given layer
|
# Add items at a given layer
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
||||||
item.level = '-' * level
|
item.level = str(int(level))
|
||||||
|
|
||||||
# Avoid circular BOM references
|
# Avoid circular BOM references
|
||||||
if item.pk in uids:
|
if item.pk in uids:
|
||||||
continue
|
continue
|
||||||
@ -68,7 +68,8 @@ def ExportBom(part, fmt='csv', cascade=False):
|
|||||||
bom_items.append(item)
|
bom_items.append(item)
|
||||||
|
|
||||||
if item.sub_part.assembly:
|
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:
|
if cascade:
|
||||||
# Cascading (multi-level) BOM
|
# Cascading (multi-level) BOM
|
||||||
|
@ -56,6 +56,8 @@ class BomExportForm(forms.Form):
|
|||||||
|
|
||||||
cascading = forms.BooleanField(label=_("Cascading"), required=False, initial=False, help_text=_("Download cascading / multi-level BOM"))
|
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):
|
def get_choices(self):
|
||||||
""" BOM export format choices """
|
""" BOM export format choices """
|
||||||
|
|
||||||
|
18
InvenTree/part/migrations/0046_auto_20200804_0107.py
Normal file
18
InvenTree/part/migrations/0046_auto_20200804_0107.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
17
InvenTree/part/migrations/0047_auto_20200808_0715.py
Normal file
17
InvenTree/part/migrations/0047_auto_20200808_0715.py
Normal 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'},
|
||||||
|
),
|
||||||
|
]
|
@ -65,14 +65,14 @@ class PartCategory(InvenTreeTree):
|
|||||||
help_text=_('Default location for parts in this category')
|
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):
|
def get_absolute_url(self):
|
||||||
return reverse('category-detail', kwargs={'pk': self.id})
|
return reverse('category-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Part Category"
|
verbose_name = _("Part Category")
|
||||||
verbose_name_plural = "Part Categories"
|
verbose_name_plural = _("Part Categories")
|
||||||
|
|
||||||
def get_parts(self, cascade=True):
|
def get_parts(self, cascade=True):
|
||||||
""" Return a queryset for all parts under this category.
|
""" Return a queryset for all parts under this category.
|
||||||
@ -239,6 +239,7 @@ class Part(MPTTModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Part")
|
verbose_name = _("Part")
|
||||||
verbose_name_plural = _("Parts")
|
verbose_name_plural = _("Parts")
|
||||||
|
ordering = ['name', ]
|
||||||
|
|
||||||
class MPTTMeta:
|
class MPTTMeta:
|
||||||
# For legacy reasons the 'variant_of' field is used to indicate the MPTT parent
|
# 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')
|
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 a JSON string for formatting a barcode for this Part object """
|
||||||
|
|
||||||
return helpers.MakeBarcode(
|
return helpers.MakeBarcode(
|
||||||
"part",
|
"part",
|
||||||
|
self.id,
|
||||||
{
|
{
|
||||||
"id": self.id,
|
|
||||||
"name": self.full_name,
|
"name": self.full_name,
|
||||||
"url": reverse('api-part-detail', kwargs={'pk': self.id}),
|
"url": reverse('api-part-detail', kwargs={'pk': self.id}),
|
||||||
}
|
},
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -1490,7 +1492,7 @@ class BomItem(models.Model):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "BOM Item"
|
verbose_name = _("BOM Item")
|
||||||
|
|
||||||
# Prevent duplication of parent/child rows
|
# Prevent duplication of parent/child rows
|
||||||
unique_together = ('part', 'sub_part')
|
unique_together = ('part', 'sub_part')
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
JSON serializers for Part app
|
JSON serializers for Part app
|
||||||
"""
|
"""
|
||||||
|
import imghdr
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
@ -92,6 +93,27 @@ class PartThumbSerializer(serializers.Serializer):
|
|||||||
count = serializers.IntegerField(read_only=True)
|
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):
|
class PartBriefSerializer(InvenTreeModelSerializer):
|
||||||
""" Serializer for Part (brief detail) """
|
""" Serializer for Part (brief detail) """
|
||||||
|
|
||||||
@ -214,6 +236,9 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
|
thumbnail = serializers.CharField(source='get_thumbnail_url', read_only=True)
|
||||||
starred = serializers.SerializerMethodField()
|
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:
|
# TODO - Include annotation for the following fields:
|
||||||
# allocated_stock = serializers.FloatField(source='allocation_count', read_only=True)
|
# allocated_stock = serializers.FloatField(source='allocation_count', read_only=True)
|
||||||
# bom_items = serializers.IntegerField(source='bom_count', read_only=True)
|
# bom_items = serializers.IntegerField(source='bom_count', read_only=True)
|
||||||
@ -280,8 +305,13 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
price_range = serializers.CharField(read_only=True)
|
price_range = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
quantity = serializers.FloatField()
|
quantity = serializers.FloatField()
|
||||||
|
|
||||||
|
part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True))
|
||||||
|
|
||||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=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)
|
sub_part_detail = PartBriefSerializer(source='sub_part', many=False, read_only=True)
|
||||||
|
|
||||||
validated = serializers.BooleanField(read_only=True, source='is_line_valid')
|
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')
|
||||||
queryset = queryset.prefetch_related('part__category')
|
queryset = queryset.prefetch_related('part__category')
|
||||||
queryset = queryset.prefetch_related('part__stock_items')
|
queryset = queryset.prefetch_related('part__stock_items')
|
||||||
|
|
||||||
queryset = queryset.prefetch_related('sub_part')
|
queryset = queryset.prefetch_related('sub_part')
|
||||||
queryset = queryset.prefetch_related('sub_part__category')
|
queryset = queryset.prefetch_related('sub_part__category')
|
||||||
queryset = queryset.prefetch_related('sub_part__stock_items')
|
queryset = queryset.prefetch_related('sub_part__stock_items')
|
||||||
|
@ -1392,10 +1392,22 @@ class BomDownload(AjaxView):
|
|||||||
|
|
||||||
cascade = str2bool(request.GET.get('cascade', False))
|
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):
|
if not IsValidBOMFormat(export_format):
|
||||||
export_format = 'csv'
|
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):
|
def get_data(self):
|
||||||
return {
|
return {
|
||||||
@ -1419,6 +1431,7 @@ class BomExport(AjaxView):
|
|||||||
# Extract POSTed form data
|
# Extract POSTed form data
|
||||||
fmt = request.POST.get('file_format', 'csv').lower()
|
fmt = request.POST.get('file_format', 'csv').lower()
|
||||||
cascade = str2bool(request.POST.get('cascading', False))
|
cascade = str2bool(request.POST.get('cascading', False))
|
||||||
|
levels = request.POST.get('levels', None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
part = Part.objects.get(pk=self.kwargs['pk'])
|
part = Part.objects.get(pk=self.kwargs['pk'])
|
||||||
@ -1434,6 +1447,9 @@ class BomExport(AjaxView):
|
|||||||
url += '?file_format=' + fmt
|
url += '?file_format=' + fmt
|
||||||
url += '&cascade=' + str(cascade)
|
url += '&cascade=' + str(cascade)
|
||||||
|
|
||||||
|
if levels:
|
||||||
|
url += '&levels=' + str(levels)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'form_valid': part is not None,
|
'form_valid': part is not None,
|
||||||
'url': url,
|
'url': url,
|
||||||
|
@ -338,11 +338,6 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
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)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
|
||||||
data = serializer.data
|
data = serializer.data
|
||||||
@ -363,6 +358,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
part_ids.add(part)
|
part_ids.add(part)
|
||||||
|
|
||||||
sp = item['supplier_part']
|
sp = item['supplier_part']
|
||||||
|
|
||||||
if sp:
|
if sp:
|
||||||
supplier_part_ids.add(sp)
|
supplier_part_ids.add(sp)
|
||||||
|
|
||||||
@ -434,6 +430,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
|
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
queryset = StockItemSerializer.prefetch_queryset(queryset)
|
queryset = StockItemSerializer.prefetch_queryset(queryset)
|
||||||
queryset = StockItemSerializer.annotate_queryset(queryset)
|
queryset = StockItemSerializer.annotate_queryset(queryset)
|
||||||
|
|
||||||
@ -477,6 +474,17 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
if customer:
|
if customer:
|
||||||
queryset = queryset.filter(customer=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?
|
# Filter by "serialized" status?
|
||||||
serialized = params.get('serialized', None)
|
serialized = params.get('serialized', None)
|
||||||
|
|
||||||
@ -507,6 +515,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
if serial_number_lte is not None:
|
if serial_number_lte is not None:
|
||||||
queryset = queryset.filter(serial__lte=serial_number_lte)
|
queryset = queryset.filter(serial__lte=serial_number_lte)
|
||||||
|
|
||||||
|
# Filter by "in_stock" status
|
||||||
in_stock = params.get('in_stock', None)
|
in_stock = params.get('in_stock', None)
|
||||||
|
|
||||||
if in_stock is not None:
|
if in_stock is not None:
|
||||||
@ -539,10 +548,21 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
active = str2bool(active)
|
active = str2bool(active)
|
||||||
queryset = queryset.filter(part__active=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
|
# Filter by internal part number
|
||||||
IPN = params.get('IPN', None)
|
IPN = params.get('IPN', None)
|
||||||
|
|
||||||
if IPN:
|
if IPN is not None:
|
||||||
queryset = queryset.filter(part__IPN=IPN)
|
queryset = queryset.filter(part__IPN=IPN)
|
||||||
|
|
||||||
# Does the client wish to filter by the Part ID?
|
# Does the client wish to filter by the Part ID?
|
||||||
|
@ -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):
|
class EditStockItemTestResultForm(HelperForm):
|
||||||
"""
|
"""
|
||||||
Form for creating / editing a StockItemTestResult object.
|
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):
|
class TestReportFormatForm(HelperForm):
|
||||||
""" Form for selection a test report template """
|
""" Form for selection a test report template """
|
||||||
|
|
||||||
|
19
InvenTree/stock/migrations/0048_auto_20200807_2344.py
Normal file
19
InvenTree/stock/migrations/0048_auto_20200807_2344.py
Normal 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)]),
|
||||||
|
),
|
||||||
|
]
|
@ -45,16 +45,17 @@ class StockLocation(InvenTreeTree):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('stock-location-detail', kwargs={'pk': self.id})
|
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 a JSON string for formatting a barcode for this StockLocation object """
|
||||||
|
|
||||||
return helpers.MakeBarcode(
|
return helpers.MakeBarcode(
|
||||||
'stocklocation',
|
'stocklocation',
|
||||||
|
self.pk,
|
||||||
{
|
{
|
||||||
"id": self.id,
|
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"url": reverse('api-location-detail', kwargs={'pk': self.id}),
|
"url": reverse('api-location-detail', kwargs={'pk': self.id}),
|
||||||
}
|
},
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_stock_items(self, cascade=True):
|
def get_stock_items(self, cascade=True):
|
||||||
@ -140,6 +141,7 @@ class StockItem(MPTTModel):
|
|||||||
sales_order=None,
|
sales_order=None,
|
||||||
build_order=None,
|
build_order=None,
|
||||||
belongs_to=None,
|
belongs_to=None,
|
||||||
|
customer=None,
|
||||||
status__in=StockStatus.AVAILABLE_CODES
|
status__in=StockStatus.AVAILABLE_CODES
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -219,12 +221,6 @@ class StockItem(MPTTModel):
|
|||||||
|
|
||||||
super().clean()
|
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:
|
try:
|
||||||
if self.part.trackable:
|
if self.part.trackable:
|
||||||
# Trackable parts must have integer values for quantity field!
|
# Trackable parts must have integer values for quantity field!
|
||||||
@ -288,7 +284,7 @@ class StockItem(MPTTModel):
|
|||||||
def get_part_name(self):
|
def get_part_name(self):
|
||||||
return self.part.full_name
|
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.
|
""" Return a JSON string for formatting a barcode for this StockItem.
|
||||||
Can be used to perform lookup of a stockitem using barcode
|
Can be used to perform lookup of a stockitem using barcode
|
||||||
|
|
||||||
@ -301,10 +297,11 @@ class StockItem(MPTTModel):
|
|||||||
|
|
||||||
return helpers.MakeBarcode(
|
return helpers.MakeBarcode(
|
||||||
"stockitem",
|
"stockitem",
|
||||||
|
self.id,
|
||||||
{
|
{
|
||||||
"id": self.id,
|
|
||||||
"url": reverse('api-stock-detail', kwargs={'pk': 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"))
|
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
|
# Update StockItem fields with new information
|
||||||
item.sales_order = order
|
item.sales_order = order
|
||||||
item.status = StockStatus.SHIPPED
|
|
||||||
item.customer = customer
|
item.customer = customer
|
||||||
item.location = None
|
item.location = None
|
||||||
|
|
||||||
@ -495,6 +491,23 @@ class StockItem(MPTTModel):
|
|||||||
# Return the reference to the stock item
|
# Return the reference to the stock item
|
||||||
return 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
|
# If stock item is incoming, an (optional) ETA field
|
||||||
# expected_arrival = models.DateField(null=True, blank=True)
|
# expected_arrival = models.DateField(null=True, blank=True)
|
||||||
|
|
||||||
@ -599,6 +612,10 @@ class StockItem(MPTTModel):
|
|||||||
if self.build_order is not None:
|
if self.build_order is not None:
|
||||||
return False
|
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
|
# Not 'in stock' if the status code makes it unavailable
|
||||||
if self.status in StockStatus.UNAVAILABLE_CODES:
|
if self.status in StockStatus.UNAVAILABLE_CODES:
|
||||||
return False
|
return False
|
||||||
|
@ -99,15 +99,34 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
return queryset
|
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)
|
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
||||||
|
|
||||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
supplier_part = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||||
location_detail = LocationBriefSerializer(source='location', many=False, read_only=True)
|
|
||||||
supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, 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)
|
tracking_items = serializers.IntegerField(source='tracking_info_count', read_only=True, required=False)
|
||||||
|
|
||||||
quantity = serializers.FloatField()
|
quantity = serializers.FloatField()
|
||||||
|
|
||||||
allocated = serializers.FloatField(source='allocation_count', required=False)
|
allocated = serializers.FloatField(source='allocation_count', required=False)
|
||||||
|
|
||||||
serial = serializers.IntegerField(required=False)
|
serial = serializers.IntegerField(required=False)
|
||||||
@ -140,9 +159,9 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'allocated',
|
'allocated',
|
||||||
'batch',
|
'batch',
|
||||||
'build_order',
|
|
||||||
'belongs_to',
|
'belongs_to',
|
||||||
'customer',
|
'customer',
|
||||||
|
'build_order',
|
||||||
'in_stock',
|
'in_stock',
|
||||||
'link',
|
'link',
|
||||||
'location',
|
'location',
|
||||||
@ -155,10 +174,10 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
|||||||
'required_tests',
|
'required_tests',
|
||||||
'sales_order',
|
'sales_order',
|
||||||
'serial',
|
'serial',
|
||||||
'supplier_part',
|
|
||||||
'supplier_part_detail',
|
|
||||||
'status',
|
'status',
|
||||||
'status_text',
|
'status_text',
|
||||||
|
'supplier_part',
|
||||||
|
'supplier_part_detail',
|
||||||
'tracking_items',
|
'tracking_items',
|
||||||
'uid',
|
'uid',
|
||||||
]
|
]
|
||||||
|
@ -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>
|
<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'>
|
<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><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 %}
|
{% if item.uid %}
|
||||||
<li><a href='#' id='unlink-barcode'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
|
<li><a href='#' id='unlink-barcode'><span class='fas fa-unlink'></span> {% trans "Unlink Barcode" %}</a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -86,43 +86,47 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% if item.in_stock %}
|
|
||||||
<!-- Stock adjustment menu -->
|
<!-- Stock adjustment menu -->
|
||||||
<div class='dropdown dropdown-buttons'>
|
<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>
|
<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'>
|
<ul class='dropdown-menu' role='menu'>
|
||||||
|
{% if item.in_stock %}
|
||||||
{% if not item.serialized %}
|
{% 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-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-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>
|
<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 %}
|
{% 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>
|
<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 %}
|
{% 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>
|
<li><a href='#' id='stock-serialize' title='{% trans "Serialize stock" %}'><span class='fas fa-hashtag'></span> {% trans "Serialize stock" %}</a> </li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% if item.part.salable and not item.customer %}
|
{% 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>
|
<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 %}
|
{% 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 %}
|
{% 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>
|
<li><a href='#' id='stock-convert' title='{% trans "Convert to variant" %}'><span class='fas fa-screwdriver'></span> {% trans "Convert to variant" %}</a></li>
|
||||||
{% endif %}
|
{% 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-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>
|
<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>
|
<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 %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% if item.part.has_test_report_templates %}
|
{% if item.part.has_test_report_templates %}
|
||||||
<button type='button' class='btn btn-default' id='stock-test-report' title='{% trans "Generate test report" %}'>
|
<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>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -157,7 +161,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
|
|||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-user-tie'></span></td>
|
<td><span class='fas fa-user-tie'></span></td>
|
||||||
<td>{% trans "Customer" %}</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>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.belongs_to %}
|
{% if item.belongs_to %}
|
||||||
@ -310,6 +314,15 @@ $("#stock-test-report").click(function() {
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
$("#print-label").click(function() {
|
||||||
|
launchModalForm(
|
||||||
|
"{% url 'stock-item-label-select' item.id %}",
|
||||||
|
{
|
||||||
|
follow: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
$("#stock-duplicate").click(function() {
|
$("#stock-duplicate").click(function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
"{% url 'stock-item-create' %}",
|
"{% url 'stock-item-create' %}",
|
||||||
@ -349,7 +362,6 @@ $("#unlink-barcode").click(function() {
|
|||||||
|
|
||||||
{% if item.in_stock %}
|
{% if item.in_stock %}
|
||||||
|
|
||||||
{% if item.part.salable %}
|
|
||||||
$("#stock-assign-to-customer").click(function() {
|
$("#stock-assign-to-customer").click(function() {
|
||||||
launchModalForm("{% url 'stock-item-assign' item.id %}",
|
launchModalForm("{% url 'stock-item-assign' item.id %}",
|
||||||
{
|
{
|
||||||
@ -357,7 +369,6 @@ $("#stock-assign-to-customer").click(function() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
function itemAdjust(action) {
|
function itemAdjust(action) {
|
||||||
launchModalForm("/stock/adjust/",
|
launchModalForm("/stock/adjust/",
|
||||||
@ -398,6 +409,16 @@ $('#stock-add').click(function() {
|
|||||||
itemAdjust('add');
|
itemAdjust('add');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
$("#stock-return-from-customer").click(function() {
|
||||||
|
launchModalForm("{% url 'stock-item-return' item.id %}",
|
||||||
|
{
|
||||||
|
reload: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
$("#stock-delete").click(function () {
|
$("#stock-delete").click(function () {
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
{% extends "modal_delete_form.html" %}
|
{% extends "modal_delete_form.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
<div class='alert alert-danger alert-block'>
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -32,6 +32,7 @@
|
|||||||
<input class='numberinput'
|
<input class='numberinput'
|
||||||
min='0'
|
min='0'
|
||||||
{% if stock_action == 'take' or stock_action == 'move' %} max='{{ item.quantity }}' {% endif %}
|
{% 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 }}'/>
|
value='{% decimal item.new_quantity %}' type='number' name='stock-id-{{ item.id }}' id='stock-id-{{ item.id }}'/>
|
||||||
{% if item.error %}
|
{% if item.error %}
|
||||||
<br><span class='help-inline'>{{ item.error }}</span>
|
<br><span class='help-inline'>{{ item.error }}</span>
|
||||||
|
@ -24,10 +24,12 @@ stock_item_detail_urls = [
|
|||||||
url(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'),
|
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'^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'^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'^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'^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'^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'),
|
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/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/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
|
# URLs for StockItem attachments
|
||||||
url(r'^item/attachment/', include([
|
url(r'^item/attachment/', include([
|
||||||
|
@ -28,6 +28,7 @@ from datetime import datetime
|
|||||||
from company.models import Company, SupplierPart
|
from company.models import Company, SupplierPart
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from report.models import TestReport
|
from report.models import TestReport
|
||||||
|
from label.models import StockItemLabel
|
||||||
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
|
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
|
||||||
|
|
||||||
from .admin import StockItemResource
|
from .admin import StockItemResource
|
||||||
@ -260,6 +261,123 @@ class StockItemAssignToCustomer(AjaxUpdateView):
|
|||||||
return self.renderJsonResponse(request, self.get_form(), data)
|
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):
|
class StockItemDeleteTestData(AjaxUpdateView):
|
||||||
"""
|
"""
|
||||||
View for deleting all test data
|
View for deleting all test data
|
||||||
@ -1239,8 +1357,8 @@ class StockItemCreate(AjaxCreateView):
|
|||||||
valid = False
|
valid = False
|
||||||
form.errors['quantity'] = [_('Invalid quantity')]
|
form.errors['quantity'] = [_('Invalid quantity')]
|
||||||
|
|
||||||
if quantity <= 0:
|
if quantity < 0:
|
||||||
form.errors['quantity'] = [_('Quantity must be greater than zero')]
|
form.errors['quantity'] = [_('Quantity cannot be less than zero')]
|
||||||
valid = False
|
valid = False
|
||||||
|
|
||||||
if part is None:
|
if part is None:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load status_codes %}
|
||||||
|
|
||||||
/* Stock API functions
|
/* Stock API functions
|
||||||
* Requires api.js to be loaded first
|
* Requires api.js to be loaded first
|
||||||
@ -425,16 +426,10 @@ function loadStockTable(table, options) {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
var url = '';
|
var url = `/stock/item/${row.pk}/`;
|
||||||
var thumb = row.part_detail.thumbnail;
|
var thumb = row.part_detail.thumbnail;
|
||||||
var name = row.part_detail.full_name;
|
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);
|
html = imageHoverIcon(thumb) + renderLink(name, url);
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
@ -466,12 +461,30 @@ function loadStockTable(table, options) {
|
|||||||
var html = renderLink(val, `/stock/item/${row.pk}/`);
|
var html = renderLink(val, `/stock/item/${row.pk}/`);
|
||||||
|
|
||||||
if (row.allocated) {
|
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"
|
// 70 = "LOST"
|
||||||
if (row.status == 70) {
|
else if (row.status == 70) {
|
||||||
html += `<span class='fas fa-question-circle label-right' title='{% trans "StockItem is lost" %}'></span>`;
|
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;
|
return html;
|
||||||
|
@ -32,30 +32,35 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
// Filters for the "Stock" table
|
// Filters for the "Stock" table
|
||||||
if (tableKey == 'stock') {
|
if (tableKey == 'stock') {
|
||||||
return {
|
return {
|
||||||
in_stock: {
|
active: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "In Stock" %}',
|
title: '{% trans "Active parts" %}',
|
||||||
description: '{% trans "Show items which are in stock" %}',
|
description: '{% trans "Show stock for active parts" %}',
|
||||||
|
},
|
||||||
|
allocated: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Is allocated" %}',
|
||||||
|
description: '{% trans "Item has been alloacted" %}',
|
||||||
},
|
},
|
||||||
cascade: {
|
cascade: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Include sublocations" %}',
|
title: '{% trans "Include sublocations" %}',
|
||||||
description: '{% trans "Include stock in sublocations" %}',
|
description: '{% trans "Include stock in sublocations" %}',
|
||||||
},
|
},
|
||||||
active: {
|
depleted: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Active parts" %}',
|
title: '{% trans "Depleted" %}',
|
||||||
description: '{% trans "Show stock for active parts" %}',
|
description: '{% trans "Show stock items which are depleted" %}',
|
||||||
},
|
},
|
||||||
status: {
|
in_stock: {
|
||||||
options: stockCodes,
|
|
||||||
title: '{% trans "Stock status" %}',
|
|
||||||
description: '{% trans "Stock status" %}',
|
|
||||||
},
|
|
||||||
allocated: {
|
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Is allocated" %}',
|
title: '{% trans "In Stock" %}',
|
||||||
description: '{% trans "Item has been alloacted" %}',
|
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: {
|
serialized: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
@ -69,6 +74,11 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
title: "{% trans "Serial number LTE" %}",
|
title: "{% trans "Serial number LTE" %}",
|
||||||
description: "{% trans "Serial number less than or equal to" %}",
|
description: "{% trans "Serial number less than or equal to" %}",
|
||||||
},
|
},
|
||||||
|
status: {
|
||||||
|
options: stockCodes,
|
||||||
|
title: '{% trans "Stock status" %}',
|
||||||
|
description: '{% trans "Stock status" %}',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,14 +18,18 @@ function {{ label }}StatusDisplay(key) {
|
|||||||
|
|
||||||
key = String(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) {
|
if (value == null || value.length == 0) {
|
||||||
value = key;
|
value = key;
|
||||||
|
label = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select the label color
|
|
||||||
var label = {{ label }}Codes[key].label ?? '';
|
|
||||||
|
|
||||||
return `<span class='label ${label}'>${value}</span>`;
|
return `<span class='label ${label}'>${value}</span>`;
|
||||||
}
|
}
|
||||||
|
4
Makefile
4
Makefile
@ -51,12 +51,12 @@ style:
|
|||||||
# Run unit tests
|
# Run unit tests
|
||||||
test:
|
test:
|
||||||
cd InvenTree && python3 manage.py check
|
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
|
# Run code coverage
|
||||||
coverage:
|
coverage:
|
||||||
cd InvenTree && python3 manage.py check
|
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
|
coverage html
|
||||||
|
|
||||||
# Install packages required to generate code docs
|
# Install packages required to generate code docs
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
[](https://opensource.org/licenses/MIT) [](https://travis-ci.org/inventree/InvenTree) [](https://inventree.readthedocs.io/en/latest/?badge=latest) [](https://coveralls.io/github/inventree/InvenTree)
|
[](https://opensource.org/licenses/MIT) [](https://travis-ci.org/inventree/InvenTree) [](https://coveralls.io/github/inventree/InvenTree)
|
||||||
|
|
||||||
<img src="images/logo/inventree.png" alt="InvenTree" width="128"/>
|
<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
|
## 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
|
## Integration
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
wheel>=0.34.2 # Wheel
|
wheel>=0.34.2 # Wheel
|
||||||
Django==3.0.7 # Django package
|
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
|
djangorestframework==3.10.3 # DRF framework
|
||||||
django-dbbackup==3.3.0 # Database backup / restore functionality
|
django-dbbackup==3.3.0 # Database backup / restore functionality
|
||||||
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
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-import-export==2.0.0 # Data import / export for admin interface
|
||||||
django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files
|
django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files
|
||||||
django-qr-code==1.2.0 # Generate QR codes
|
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
|
coverage==4.0.3 # Unit test coverage
|
||||||
python-coveralls==2.9.1 # Coveralls linking (for Travis)
|
python-coveralls==2.9.1 # Coveralls linking (for Travis)
|
||||||
rapidfuzz==0.7.6 # Fuzzy string matching
|
rapidfuzz==0.7.6 # Fuzzy string matching
|
||||||
django-stdimage==5.1.1 # Advanced ImageField management
|
django-stdimage==5.1.1 # Advanced ImageField management
|
||||||
django-tex==1.1.7 # LaTeX PDF export
|
django-tex==1.1.7 # LaTeX PDF export
|
||||||
django-weasyprint==1.0.1 # HTML PDF export
|
django-weasyprint==1.0.1 # HTML PDF export
|
||||||
|
django-debug-toolbar==2.2 # Debug / profiling toolbar
|
Loading…
x
Reference in New Issue
Block a user