mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' into partial-shipment
# Conflicts: # InvenTree/build/serializers.py # InvenTree/order/templates/order/so_sidebar.html
This commit is contained in:
commit
68e2b0850b
37
.github/workflows/check_translations.yaml
vendored
Normal file
37
.github/workflows/check_translations.yaml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: Check Translations
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- l10
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- l10
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
INVENTREE_DB_NAME: './test_db.sqlite'
|
||||||
|
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||||
|
INVENTREE_DEBUG: info
|
||||||
|
INVENTREE_MEDIA_ROOT: ./media
|
||||||
|
INVENTREE_STATIC_ROOT: ./static
|
||||||
|
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install gettext
|
||||||
|
pip3 install invoke
|
||||||
|
invoke install
|
||||||
|
- name: Test Translations
|
||||||
|
run: invoke translate
|
||||||
|
- name: Check Migration Files
|
||||||
|
run: python3 ci/check_migration_files.py
|
@ -46,7 +46,7 @@ class InvenTreeAPITestCase(APITestCase):
|
|||||||
self.user.is_staff = True
|
self.user.is_staff = True
|
||||||
|
|
||||||
self.user.save()
|
self.user.save()
|
||||||
|
|
||||||
for role in self.roles:
|
for role in self.roles:
|
||||||
self.assignRole(role)
|
self.assignRole(role)
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Pull rendered copies of the templated
|
Pull rendered copies of the templated
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.http import response
|
from django.test import TestCase
|
||||||
from django.test import TestCase, testcases
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
@ -53,7 +53,7 @@ class InvenTreeModelMoneyField(ModelMoneyField):
|
|||||||
"""
|
"""
|
||||||
Custom MoneyField for clean migrations while using dynamic currency settings
|
Custom MoneyField for clean migrations while using dynamic currency settings
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
# detect if creating migration
|
# detect if creating migration
|
||||||
if 'migrate' in sys.argv or 'makemigrations' in sys.argv:
|
if 'migrate' in sys.argv or 'makemigrations' in sys.argv:
|
||||||
|
@ -38,7 +38,7 @@ class InvenTreeOrderingFilter(OrderingFilter):
|
|||||||
ordering = []
|
ordering = []
|
||||||
|
|
||||||
for field in ordering_initial:
|
for field in ordering_initial:
|
||||||
|
|
||||||
reverse = field.startswith('-')
|
reverse = field.startswith('-')
|
||||||
|
|
||||||
if reverse:
|
if reverse:
|
||||||
@ -52,7 +52,7 @@ class InvenTreeOrderingFilter(OrderingFilter):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
Potentially, a single field could be "aliased" to multiple field,
|
Potentially, a single field could be "aliased" to multiple field,
|
||||||
|
|
||||||
(For example to enforce a particular ordering sequence)
|
(For example to enforce a particular ordering sequence)
|
||||||
|
|
||||||
e.g. to filter first by the integer value...
|
e.g. to filter first by the integer value...
|
||||||
|
@ -36,7 +36,7 @@ class Command(BaseCommand):
|
|||||||
img = model.image
|
img = model.image
|
||||||
url = img.thumbnail.name
|
url = img.thumbnail.name
|
||||||
loc = os.path.join(settings.MEDIA_ROOT, url)
|
loc = os.path.join(settings.MEDIA_ROOT, url)
|
||||||
|
|
||||||
if not os.path.exists(loc):
|
if not os.path.exists(loc):
|
||||||
logger.info(f"Generating thumbnail image for '{img}'")
|
logger.info(f"Generating thumbnail image for '{img}'")
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def determine_metadata(self, request, view):
|
def determine_metadata(self, request, view):
|
||||||
|
|
||||||
self.request = request
|
self.request = request
|
||||||
self.view = view
|
self.view = view
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
Override get_serializer_info so that we can add 'default' values
|
Override get_serializer_info so that we can add 'default' values
|
||||||
to any fields whose Meta.model specifies a default value
|
to any fields whose Meta.model specifies a default value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_info = super().get_serializer_info(serializer)
|
serializer_info = super().get_serializer_info(serializer)
|
||||||
|
|
||||||
model_class = None
|
model_class = None
|
||||||
@ -174,7 +174,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
# Extract extra information if an instance is available
|
# Extract extra information if an instance is available
|
||||||
if hasattr(serializer, 'instance'):
|
if hasattr(serializer, 'instance'):
|
||||||
instance = serializer.instance
|
instance = serializer.instance
|
||||||
|
|
||||||
if instance is None and model_class is not None:
|
if instance is None and model_class is not None:
|
||||||
# Attempt to find the instance based on kwargs lookup
|
# Attempt to find the instance based on kwargs lookup
|
||||||
kwargs = getattr(self.view, 'kwargs', None)
|
kwargs = getattr(self.view, 'kwargs', None)
|
||||||
@ -240,7 +240,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
|
|
||||||
# Introspect writable related fields
|
# Introspect writable related fields
|
||||||
if field_info['type'] == 'field' and not field_info['read_only']:
|
if field_info['type'] == 'field' and not field_info['read_only']:
|
||||||
|
|
||||||
# If the field is a PrimaryKeyRelatedField, we can extract the model from the queryset
|
# If the field is a PrimaryKeyRelatedField, we can extract the model from the queryset
|
||||||
if isinstance(field, serializers.PrimaryKeyRelatedField):
|
if isinstance(field, serializers.PrimaryKeyRelatedField):
|
||||||
model = field.queryset.model
|
model = field.queryset.model
|
||||||
|
@ -66,7 +66,7 @@ class InvenTreeMoneySerializer(MoneyField):
|
|||||||
|
|
||||||
if currency and amount is not None and not isinstance(amount, MONEY_CLASSES) and amount is not empty:
|
if currency and amount is not None and not isinstance(amount, MONEY_CLASSES) and amount is not empty:
|
||||||
return Money(amount, currency)
|
return Money(amount, currency)
|
||||||
|
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ import moneyed
|
|||||||
import yaml
|
import yaml
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
|
import django.conf.locale
|
||||||
|
|
||||||
|
|
||||||
def _is_true(x):
|
def _is_true(x):
|
||||||
@ -306,7 +307,7 @@ MIDDLEWARE = CONFIG.get('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',
|
||||||
])
|
])
|
||||||
|
|
||||||
# Error reporting middleware
|
# Error reporting middleware
|
||||||
@ -664,6 +665,7 @@ LANGUAGES = [
|
|||||||
('el', _('Greek')),
|
('el', _('Greek')),
|
||||||
('en', _('English')),
|
('en', _('English')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
|
('es-mx', _('Spanish (Mexican)')),
|
||||||
('fr', _('French')),
|
('fr', _('French')),
|
||||||
('he', _('Hebrew')),
|
('he', _('Hebrew')),
|
||||||
('it', _('Italian')),
|
('it', _('Italian')),
|
||||||
@ -681,6 +683,25 @@ LANGUAGES = [
|
|||||||
('zh-cn', _('Chinese')),
|
('zh-cn', _('Chinese')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Testing interface translations
|
||||||
|
if get_setting('TEST_TRANSLATIONS', False):
|
||||||
|
# Set default language
|
||||||
|
LANGUAGE_CODE = 'xx'
|
||||||
|
|
||||||
|
# Add to language catalog
|
||||||
|
LANGUAGES.append(('xx', 'Test'))
|
||||||
|
|
||||||
|
# Add custom languages not provided by Django
|
||||||
|
EXTRA_LANG_INFO = {
|
||||||
|
'xx': {
|
||||||
|
'code': 'xx',
|
||||||
|
'name': 'Test',
|
||||||
|
'name_local': 'Test'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
LANG_INFO = dict(django.conf.locale.LANG_INFO, **EXTRA_LANG_INFO)
|
||||||
|
django.conf.locale.LANG_INFO = LANG_INFO
|
||||||
|
|
||||||
# Currencies available for use
|
# Currencies available for use
|
||||||
CURRENCIES = CONFIG.get(
|
CURRENCIES = CONFIG.get(
|
||||||
'currencies',
|
'currencies',
|
||||||
|
@ -208,7 +208,7 @@ function inventreeDocReady() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Callback for "admin view" button
|
// Callback for "admin view" button
|
||||||
$('#admin-button').click(function() {
|
$('#admin-button, .admin-button').click(function() {
|
||||||
var url = $(this).attr('url');
|
var url = $(this).attr('url');
|
||||||
|
|
||||||
location.href = url;
|
location.href = url;
|
||||||
|
@ -106,7 +106,7 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
|
|||||||
except NameError:
|
except NameError:
|
||||||
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Workers are not running: run it as synchronous task
|
# Workers are not running: run it as synchronous task
|
||||||
_func(*args, **kwargs)
|
_func(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ from base64 import b64encode
|
|||||||
class HTMLAPITests(TestCase):
|
class HTMLAPITests(TestCase):
|
||||||
"""
|
"""
|
||||||
Test that we can access the REST API endpoints via the HTML interface.
|
Test that we can access the REST API endpoints via the HTML interface.
|
||||||
|
|
||||||
History: Discovered on 2021-06-28 a bug in InvenTreeModelSerializer,
|
History: Discovered on 2021-06-28 a bug in InvenTreeModelSerializer,
|
||||||
which raised an AssertionError when using the HTML API interface,
|
which raised an AssertionError when using the HTML API interface,
|
||||||
while the regular JSON interface continued to work as expected.
|
while the regular JSON interface continued to work as expected.
|
||||||
@ -280,7 +280,7 @@ class APITests(InvenTreeAPITestCase):
|
|||||||
"""
|
"""
|
||||||
Tests for detail API endpoint actions
|
Tests for detail API endpoint actions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.basicAuth()
|
self.basicAuth()
|
||||||
|
|
||||||
url = reverse('api-part-detail', kwargs={'pk': 1})
|
url = reverse('api-part-detail', kwargs={'pk': 1})
|
||||||
|
@ -77,7 +77,7 @@ apipatterns = [
|
|||||||
settings_urls = [
|
settings_urls = [
|
||||||
|
|
||||||
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
||||||
|
|
||||||
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||||
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
|
url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'),
|
||||||
|
|
||||||
|
@ -123,10 +123,10 @@ def isInvenTreeDevelopmentVersion():
|
|||||||
def inventreeDocsVersion():
|
def inventreeDocsVersion():
|
||||||
"""
|
"""
|
||||||
Return the version string matching the latest documentation.
|
Return the version string matching the latest documentation.
|
||||||
|
|
||||||
Development -> "latest"
|
Development -> "latest"
|
||||||
Release -> "major.minor.sub" e.g. "0.5.2"
|
Release -> "major.minor.sub" e.g. "0.5.2"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isInvenTreeDevelopmentVersion():
|
if isInvenTreeDevelopmentVersion():
|
||||||
|
@ -198,7 +198,7 @@ class BuildUnallocate(generics.CreateAPIView):
|
|||||||
queryset = Build.objects.none()
|
queryset = Build.objects.none()
|
||||||
|
|
||||||
serializer_class = BuildUnallocationSerializer
|
serializer_class = BuildUnallocationSerializer
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
|
|
||||||
ctx = super().get_serializer_context()
|
ctx = super().get_serializer_context()
|
||||||
@ -231,7 +231,7 @@ class BuildComplete(generics.CreateAPIView):
|
|||||||
ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
|
ctx['build'] = Build.objects.get(pk=self.kwargs.get('pk', None))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
@ -296,7 +296,7 @@ class BuildItemList(generics.ListCreateAPIView):
|
|||||||
kwargs['location_detail'] = str2bool(params.get('location_detail', False))
|
kwargs['location_detail'] = str2bool(params.get('location_detail', False))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -66,7 +66,7 @@ def get_next_build_number():
|
|||||||
attempts.add(reference)
|
attempts.add(reference)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
return reference
|
return reference
|
||||||
|
|
||||||
|
|
||||||
@ -94,13 +94,13 @@ class Build(MPTTModel, ReferenceIndexingMixin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_api_url():
|
def get_api_url():
|
||||||
return reverse('api-build-list')
|
return reverse('api-build-list')
|
||||||
|
|
||||||
def api_instance_filters(self):
|
def api_instance_filters(self):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'parent': {
|
'parent': {
|
||||||
'exclude_tree': self.pk,
|
'exclude_tree': self.pk,
|
||||||
@ -1178,7 +1178,7 @@ class BuildItem(models.Model):
|
|||||||
bom_item = PartModels.BomItem.objects.get(part=self.build.part, sub_part=ancestor)
|
bom_item = PartModels.BomItem.objects.get(part=self.build.part, sub_part=ancestor)
|
||||||
except PartModels.BomItem.DoesNotExist:
|
except PartModels.BomItem.DoesNotExist:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# A matching BOM item has been found!
|
# A matching BOM item has been found!
|
||||||
if idx == 0 or bom_item.allow_variants:
|
if idx == 0 or bom_item.allow_variants:
|
||||||
bom_item_valid = True
|
bom_item_valid = True
|
||||||
@ -1234,7 +1234,7 @@ class BuildItem(models.Model):
|
|||||||
thumb_url = self.stock_item.part.image.thumbnail.url
|
thumb_url = self.stock_item.part.image.thumbnail.url
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if thumb_url is None and self.bom_item and self.bom_item.sub_part:
|
if thumb_url is None and self.bom_item and self.bom_item.sub_part:
|
||||||
try:
|
try:
|
||||||
thumb_url = self.bom_item.sub_part.image.thumbnail.url
|
thumb_url = self.bom_item.sub_part.image.thumbnail.url
|
||||||
|
@ -309,7 +309,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_bom_item(self, bom_item):
|
def validate_bom_item(self, bom_item):
|
||||||
|
|
||||||
# TODO: Fix this validation - allow for variants and substitutes!
|
# TODO: Fix this validation - allow for variants and substitutes!
|
||||||
|
|
||||||
build = self.context['build']
|
build = self.context['build']
|
||||||
@ -332,7 +332,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
if not stock_item.in_stock:
|
if not stock_item.in_stock:
|
||||||
raise ValidationError(_("Item must be in stock"))
|
raise ValidationError(_("Item must be in stock"))
|
||||||
|
|
||||||
return stock_item
|
return stock_item
|
||||||
|
|
||||||
quantity = serializers.DecimalField(
|
quantity = serializers.DecimalField(
|
||||||
@ -398,7 +398,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
# Output *cannot* be set for un-tracked parts
|
# Output *cannot* be set for un-tracked parts
|
||||||
if output is not None and not bom_item.sub_part.trackable:
|
if output is not None and not bom_item.sub_part.trackable:
|
||||||
|
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'output': _('Build output cannot be specified for allocation of untracked parts')
|
'output': _('Build output cannot be specified for allocation of untracked parts')
|
||||||
})
|
})
|
||||||
@ -422,14 +422,14 @@ class BuildAllocationSerializer(serializers.Serializer):
|
|||||||
"""
|
"""
|
||||||
Validation
|
Validation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = super().validate(data)
|
data = super().validate(data)
|
||||||
|
|
||||||
items = data.get('items', [])
|
items = data.get('items', [])
|
||||||
|
|
||||||
if len(items) == 0:
|
if len(items) == 0:
|
||||||
raise ValidationError(_('Allocation items must be provided'))
|
raise ValidationError(_('Allocation items must be provided'))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
@ -2,14 +2,21 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='details' text="Build Order Details" icon="fa-info-circle" %}
|
{% trans "Build Order Details" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='details' text=text icon="fa-info-circle" %}
|
||||||
{% if build.active %}
|
{% if build.active %}
|
||||||
{% include "sidebar_item.html" with label='allocate' text="Allocate Stock" icon="fa-tasks" %}
|
{% trans "Allocate Stock" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='allocate' text=text icon="fa-tasks" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not build.is_complete %}
|
{% if not build.is_complete %}
|
||||||
{% include "sidebar_item.html" with label='outputs' text="Pending Items" icon="fa-tools" %}
|
{% trans "Pending Items" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='outputs' text=text icon="fa-tools" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "sidebar_item.html" with label='completed' text="Completed Items" icon="fa-boxes" %}
|
{% trans "Completed Items" as text %}
|
||||||
{% include "sidebar_item.html" with label='children' text="Child Build Orders" icon="fa-sitemap" %}
|
{% include "sidebar_item.html" with label='completed' text=text icon="fa-boxes" %}
|
||||||
{% include "sidebar_item.html" with label='attachments' text="Attachments" icon="fa-paperclip" %}
|
{% trans "Child Build Orders" as text %}
|
||||||
{% include "sidebar_item.html" with label='notes' text="Notes" icon="fa-clipboard" %}
|
{% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %}
|
||||||
|
{% trans "Attachments" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='attachments' text=text icon="fa-paperclip" %}
|
||||||
|
{% trans "Notes" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='notes' text=text icon="fa-clipboard" %}
|
||||||
|
@ -73,7 +73,7 @@ class GlobalSettingsDetail(generics.RetrieveUpdateAPIView):
|
|||||||
permission_classes = [
|
permission_classes = [
|
||||||
GlobalSettingsPermissions,
|
GlobalSettingsPermissions,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsList(SettingsList):
|
class UserSettingsList(SettingsList):
|
||||||
"""
|
"""
|
||||||
@ -124,7 +124,7 @@ class UserSettingsDetail(generics.RetrieveUpdateAPIView):
|
|||||||
|
|
||||||
queryset = common.models.InvenTreeUserSetting.objects.all()
|
queryset = common.models.InvenTreeUserSetting.objects.all()
|
||||||
serializer_class = common.serializers.UserSettingsSerializer
|
serializer_class = common.serializers.UserSettingsSerializer
|
||||||
|
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
UserSettingsPermissions,
|
UserSettingsPermissions,
|
||||||
]
|
]
|
||||||
|
@ -12,7 +12,7 @@ class CommonConfig(AppConfig):
|
|||||||
name = 'common'
|
name = 'common'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
|
||||||
self.clear_restart_flag()
|
self.clear_restart_flag()
|
||||||
|
|
||||||
def clear_restart_flag(self):
|
def clear_restart_flag(self):
|
||||||
@ -22,7 +22,7 @@ class CommonConfig(AppConfig):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED'):
|
if common.models.InvenTreeSetting.get_setting('SERVER_RESTART_REQUIRED'):
|
||||||
logger.info("Clearing SERVER_RESTART_REQUIRED flag")
|
logger.info("Clearing SERVER_RESTART_REQUIRED flag")
|
||||||
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)
|
common.models.InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', False, None)
|
||||||
|
@ -108,7 +108,9 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
for key, value in settings.items():
|
for key, value in settings.items():
|
||||||
validator = cls.get_setting_validator(key)
|
validator = cls.get_setting_validator(key)
|
||||||
|
|
||||||
if cls.validator_is_bool(validator):
|
if cls.is_protected(key):
|
||||||
|
value = '***'
|
||||||
|
elif cls.validator_is_bool(validator):
|
||||||
value = InvenTree.helpers.str2bool(value)
|
value = InvenTree.helpers.str2bool(value)
|
||||||
elif cls.validator_is_int(validator):
|
elif cls.validator_is_int(validator):
|
||||||
try:
|
try:
|
||||||
@ -485,7 +487,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
elif self.is_int():
|
elif self.is_int():
|
||||||
return 'integer'
|
return 'integer'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return 'string'
|
return 'string'
|
||||||
|
|
||||||
@ -538,6 +540,19 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_protected(cls, key):
|
||||||
|
"""
|
||||||
|
Check if the setting value is protected
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = str(key).strip().upper()
|
||||||
|
|
||||||
|
if key in cls.GLOBAL_SETTINGS:
|
||||||
|
return cls.GLOBAL_SETTINGS[key].get('protected', False)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def settings_group_options():
|
def settings_group_options():
|
||||||
"""build up group tuple for settings based on gour choices"""
|
"""build up group tuple for settings based on gour choices"""
|
||||||
|
@ -45,6 +45,18 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def get_value(self, obj):
|
||||||
|
"""
|
||||||
|
Make sure protected values are not returned
|
||||||
|
"""
|
||||||
|
result = obj.value
|
||||||
|
|
||||||
|
# never return protected values
|
||||||
|
if obj.is_protected:
|
||||||
|
result = '***'
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsSerializer(SettingsSerializer):
|
class GlobalSettingsSerializer(SettingsSerializer):
|
||||||
"""
|
"""
|
||||||
|
@ -170,7 +170,7 @@ class ManufacturerPartParameterList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = ManufacturerPartParameter.objects.all()
|
queryset = ManufacturerPartParameter.objects.all()
|
||||||
serializer_class = ManufacturerPartParameterSerializer
|
serializer_class = ManufacturerPartParameterSerializer
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
|
||||||
# Do we wish to include any extra detail?
|
# Do we wish to include any extra detail?
|
||||||
|
@ -477,7 +477,7 @@ class SupplierPart(models.Model):
|
|||||||
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
def api_instance_filters(self):
|
def api_instance_filters(self):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'manufacturer_part': {
|
'manufacturer_part': {
|
||||||
'part': self.part.pk
|
'part': self.part.pk
|
||||||
|
@ -187,7 +187,7 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
part_detail = kwargs.pop('part_detail', True)
|
part_detail = kwargs.pop('part_detail', True)
|
||||||
supplier_detail = kwargs.pop('supplier_detail', True)
|
supplier_detail = kwargs.pop('supplier_detail', True)
|
||||||
manufacturer_detail = kwargs.pop('manufacturer_detail', True)
|
manufacturer_detail = kwargs.pop('manufacturer_detail', True)
|
||||||
|
|
||||||
prettify = kwargs.pop('pretty', False)
|
prettify = kwargs.pop('pretty', False)
|
||||||
|
|
||||||
super(SupplierPartSerializer, self).__init__(*args, **kwargs)
|
super(SupplierPartSerializer, self).__init__(*args, **kwargs)
|
||||||
|
@ -2,5 +2,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='parameters' text="Parameters" icon="fa-th-list" %}
|
{% trans "Parameters" as text %}
|
||||||
{% include "sidebar_item.html" with label='supplier-parts' text="Supplier Parts" icon="fa-building" %}
|
{% include "sidebar_item.html" with label='parameters' text=text icon="fa-th-list" %}
|
||||||
|
{% trans "Supplier Parts" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}
|
@ -3,17 +3,24 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% if company.is_manufacturer %}
|
{% if company.is_manufacturer %}
|
||||||
{% include "sidebar_item.html" with label='manufacturer-parts' text="Manufactured Parts" icon="fa-industry" %}
|
{% trans "Manufactured Parts" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='manufacturer-parts' text=text icon="fa-industry" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if company.is_supplier %}
|
{% if company.is_supplier %}
|
||||||
{% include "sidebar_item.html" with label='supplier-parts' text="Supplied Parts" icon="fa-building" %}
|
{% trans "Supplied Parts" as text %}
|
||||||
{% include "sidebar_item.html" with label='purchase-orders' text="Purchase Orders" icon="fa-shopping-cart" %}
|
{% include "sidebar_item.html" with label='supplier-parts' text=text icon="fa-building" %}
|
||||||
|
{% trans "Purchase Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='purchase-orders' text=text icon="fa-shopping-cart" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if company.is_manufacturer or company.is_supplier %}
|
{% if company.is_manufacturer or company.is_supplier %}
|
||||||
{% include "sidebar_item.html" with label='company-stock' text="Supplied Stock Items" icon="fa-boxes" %}
|
{% trans "Supplied Stock Items" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='company-stock' text=text icon="fa-boxes" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if company.is_customer %}
|
{% if company.is_customer %}
|
||||||
{% include "sidebar_item.html" with label='sales-orders' text="Sales Orders" icon="fa-truck" %}
|
{% trans "Sales Orders" as text %}
|
||||||
{% include "sidebar_item.html" with label='assigned-stock' text="Assigned Stock Items" icon="fa-sign-out-alt" %}
|
{% include "sidebar_item.html" with label='sales-orders' text=text icon="fa-truck" %}
|
||||||
|
{% trans "Assigned Stock Items" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='assigned-stock' text=text icon="fa-sign-out-alt" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "sidebar_item.html" with label='company-notes' text="Notes" icon="fa-clipboard" %}
|
{% trans "Notes" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='company-notes' text=text icon="fa-clipboard" %}
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='stock' text="Stock Items" icon="fa-boxes" %}
|
{% trans "Stock Items" as text %}
|
||||||
{% include "sidebar_item.html" with label='purchase-orders' text="Purchase Orders" icon="fa-shopping-cart" %}
|
{% include "sidebar_item.html" with label='stock' text=text icon="fa-boxes" %}
|
||||||
{% include "sidebar_item.html" with label='pricing' text="Supplier Part Pricing" icon="fa-dollar-sign" %}
|
{% trans "Purchase Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='purchase-orders' text=text icon="fa-shopping-cart" %}
|
||||||
|
{% trans "Supplier Part Pricing" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='pricing' text=text icon="fa-dollar-sign" %}
|
||||||
|
@ -202,7 +202,7 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
|||||||
data = {
|
data = {
|
||||||
'MPN': 'MPN-TEST-123',
|
'MPN': 'MPN-TEST-123',
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.client.patch(url, data, format='json')
|
response = self.client.patch(url, data, format='json')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
@ -29,7 +29,7 @@ company_urls = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
manufacturer_part_urls = [
|
manufacturer_part_urls = [
|
||||||
|
|
||||||
url(r'^(?P<pk>\d+)/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
|
url(r'^(?P<pk>\d+)/', views.ManufacturerPartDetail.as_view(template_name='company/manufacturer_part.html'), name='manufacturer-part-detail'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -399,7 +399,7 @@ class PartLabelMixin:
|
|||||||
if key in params:
|
if key in params:
|
||||||
parts = params.getlist(key, [])
|
parts = params.getlist(key, [])
|
||||||
break
|
break
|
||||||
|
|
||||||
valid_ids = []
|
valid_ids = []
|
||||||
|
|
||||||
for part in parts:
|
for part in parts:
|
||||||
|
@ -186,7 +186,7 @@ class LabelTemplate(models.Model):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
template_string = Template(self.filename_pattern)
|
template_string = Template(self.filename_pattern)
|
||||||
|
|
||||||
ctx = self.context(request)
|
ctx = self.context(request)
|
||||||
|
|
||||||
context = Context(ctx)
|
context = Context(ctx)
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/el/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/el/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/es_MX/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/es_MX/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
8616
InvenTree/locale/es_MX/LC_MESSAGES/django.po
Normal file
8616
InvenTree/locale/es_MX/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/he/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/he/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/id/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/id/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/ko/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/ko/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/nl/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/nl/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/no/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/no/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/sv/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/sv/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/th/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/th/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/vi/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/vi/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -42,7 +42,7 @@ class POList(generics.ListCreateAPIView):
|
|||||||
"""
|
"""
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
item = serializer.save()
|
item = serializer.save()
|
||||||
item.created_by = request.user
|
item.created_by = request.user
|
||||||
item.save()
|
item.save()
|
||||||
@ -397,7 +397,7 @@ class SOList(generics.ListCreateAPIView):
|
|||||||
"""
|
"""
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
item = serializer.save()
|
item = serializer.save()
|
||||||
item.created_by = request.user
|
item.created_by = request.user
|
||||||
item.save()
|
item.save()
|
||||||
|
@ -743,7 +743,7 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
def get_base_part(self):
|
def get_base_part(self):
|
||||||
"""
|
"""
|
||||||
Return the base part.Part object for the line item
|
Return the base part.Part object for the line item
|
||||||
|
|
||||||
Note: Returns None if the SupplierPart is not set!
|
Note: Returns None if the SupplierPart is not set!
|
||||||
"""
|
"""
|
||||||
if self.part is None:
|
if self.part is None:
|
||||||
|
@ -551,10 +551,10 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
|
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
|
||||||
|
|
||||||
quantity = InvenTreeDecimalField()
|
quantity = InvenTreeDecimalField()
|
||||||
|
|
||||||
allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
|
allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
|
||||||
fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True)
|
fulfilled = serializers.FloatField(source='fulfilled_quantity', read_only=True)
|
||||||
|
|
||||||
sale_price = InvenTreeMoneySerializer(
|
sale_price = InvenTreeMoneySerializer(
|
||||||
allow_null=True
|
allow_null=True
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% url "po-detail" order.id as url %}
|
{% url "po-detail" order.id as url %}
|
||||||
{% include "sidebar_item.html" with url=url text="Return to Orders" icon="fa-undo" %}
|
{% trans "Return to Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with url=url text=text icon="fa-undo" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_content %}
|
{% block page_content %}
|
||||||
|
@ -2,7 +2,11 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='order-items' text="Line Items" icon="fa-list-ol" %}
|
{% trans "Line Items" as text %}
|
||||||
{% include "sidebar_item.html" with label='received-items' text="Received Stock" icon="fa-sign-in-alt" %}
|
{% include "sidebar_item.html" with label='order-items' text=text icon="fa-list-ol" %}
|
||||||
{% include "sidebar_item.html" with label='order-attachments' text="Attachments" icon="fa-paperclip" %}
|
{% trans "Received Stock" as text %}
|
||||||
{% include "sidebar_item.html" with label='order-notes' text="Notes" icon="fa-clipboard" %}
|
{% include "sidebar_item.html" with label='received-items' text=text icon="fa-sign-in-alt" %}
|
||||||
|
{% trans "Attachments" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='order-attachments' text=text icon="fa-paperclip" %}
|
||||||
|
{% trans "Notes" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='order-notes' text=text icon="fa-clipboard" %}
|
@ -2,8 +2,13 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='order-items' text="Line Items" icon="fa-list-ol" %}
|
{% trans "Line Items" as text %}
|
||||||
{% include "sidebar_item.html" with label='order-shipments' text="Shipments" icon="fa-truck" %}
|
{% include "sidebar_item.html" with label='order-items' text=text icon="fa-list-ol" %}
|
||||||
{% include "sidebar_item.html" with label='order-builds' text="Build Orders" icon="fa-tools" %}
|
{% trans "Shipments" as text %}
|
||||||
{% include "sidebar_item.html" with label='order-attachments' text="Attachments" icon="fa-paperclip" %}
|
{% include "sidebar_item.html" with label='order-shipments' text=text icon="fa-truck" %}
|
||||||
{% include "sidebar_item.html" with label='order-notes' text="Notes" icon="fa-clipboard" %}
|
{% trans "Build Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='order-builds' text=text icon="fa-tools" %}
|
||||||
|
{% trans "Attachments" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='order-attachments' text=text icon="fa-paperclip" %}
|
||||||
|
{% trans "Notes" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='order-notes' text=text icon="fa-clipboard" %}
|
||||||
|
@ -229,7 +229,7 @@ class PurchaseOrderReceiveTest(OrderTest):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
self.assignRole('purchase_order.add')
|
self.assignRole('purchase_order.add')
|
||||||
|
|
||||||
self.url = reverse('api-po-receive', kwargs={'pk': 1})
|
self.url = reverse('api-po-receive', kwargs={'pk': 1})
|
||||||
|
@ -406,7 +406,7 @@ class PurchaseOrderUpload(FileManagementFormView):
|
|||||||
|
|
||||||
def done(self, form_list, **kwargs):
|
def done(self, form_list, **kwargs):
|
||||||
""" Once all the data is in, process it to add PurchaseOrderLineItem instances to the order """
|
""" Once all the data is in, process it to add PurchaseOrderLineItem instances to the order """
|
||||||
|
|
||||||
order = self.get_order()
|
order = self.get_order()
|
||||||
items = self.get_clean_items()
|
items = self.get_clean_items()
|
||||||
|
|
||||||
@ -432,7 +432,7 @@ class PurchaseOrderUpload(FileManagementFormView):
|
|||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
# PurchaseOrderLineItem already exists
|
# PurchaseOrderLineItem already exists
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))
|
return HttpResponseRedirect(reverse('po-detail', kwargs={'pk': self.kwargs['pk']}))
|
||||||
|
|
||||||
|
|
||||||
@ -449,7 +449,7 @@ class SalesOrderExport(AjaxView):
|
|||||||
role_required = 'sales_order.view'
|
role_required = 'sales_order.view'
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
order = get_object_or_404(SalesOrder, pk=self.kwargs.get('pk', None))
|
order = get_object_or_404(SalesOrder, pk=self.kwargs.get('pk', None))
|
||||||
|
|
||||||
export_format = request.GET.get('format', 'csv')
|
export_format = request.GET.get('format', 'csv')
|
||||||
|
@ -205,7 +205,7 @@ class BomItemResource(ModelResource):
|
|||||||
|
|
||||||
# If we are not generating an "import" template,
|
# If we are not generating an "import" template,
|
||||||
# just return the complete list of fields
|
# just return the complete list of fields
|
||||||
if not self.is_importing:
|
if not getattr(self, 'is_importing', False):
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
# Otherwise, remove some fields we are not interested in
|
# Otherwise, remove some fields we are not interested in
|
||||||
|
@ -26,7 +26,7 @@ from djmoney.contrib.exchange.exceptions import MissingRate
|
|||||||
|
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
from .models import Part, PartCategory
|
from .models import Part, PartCategory, PartRelated
|
||||||
from .models import BomItem, BomItemSubstitute
|
from .models import BomItem, BomItemSubstitute
|
||||||
from .models import PartParameter, PartParameterTemplate
|
from .models import PartParameter, PartParameterTemplate
|
||||||
from .models import PartAttachment, PartTestTemplate
|
from .models import PartAttachment, PartTestTemplate
|
||||||
@ -169,7 +169,7 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
|||||||
"""
|
"""
|
||||||
API endpoint for detail view of a single PartCategory object
|
API endpoint for detail view of a single PartCategory object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = part_serializers.CategorySerializer
|
serializer_class = part_serializers.CategorySerializer
|
||||||
queryset = PartCategory.objects.all()
|
queryset = PartCategory.objects.all()
|
||||||
|
|
||||||
@ -222,7 +222,7 @@ class CategoryParameterList(generics.ListAPIView):
|
|||||||
|
|
||||||
if category is not None:
|
if category is not None:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
category = PartCategory.objects.get(pk=category)
|
category = PartCategory.objects.get(pk=category)
|
||||||
|
|
||||||
fetch_parent = str2bool(params.get('fetch_parent', True))
|
fetch_parent = str2bool(params.get('fetch_parent', True))
|
||||||
@ -734,7 +734,7 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'initial_stock_quantity': [_('Must be a valid quantity')],
|
'initial_stock_quantity': [_('Must be a valid quantity')],
|
||||||
})
|
})
|
||||||
|
|
||||||
initial_stock_location = request.data.get('initial_stock_location', None)
|
initial_stock_location = request.data.get('initial_stock_location', None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -850,7 +850,7 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
id_values.append(val)
|
id_values.append(val)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
queryset = queryset.exclude(pk__in=id_values)
|
queryset = queryset.exclude(pk__in=id_values)
|
||||||
|
|
||||||
# Exclude part variant tree?
|
# Exclude part variant tree?
|
||||||
@ -901,6 +901,40 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = queryset.filter(pk__in=pks)
|
queryset = queryset.filter(pk__in=pks)
|
||||||
|
|
||||||
|
# Filter by 'related' parts?
|
||||||
|
related = params.get('related', None)
|
||||||
|
exclude_related = params.get('exclude_related', None)
|
||||||
|
|
||||||
|
if related is not None or exclude_related is not None:
|
||||||
|
try:
|
||||||
|
pk = related if related is not None else exclude_related
|
||||||
|
pk = int(pk)
|
||||||
|
|
||||||
|
related_part = Part.objects.get(pk=pk)
|
||||||
|
|
||||||
|
part_ids = set()
|
||||||
|
|
||||||
|
# Return any relationship which points to the part in question
|
||||||
|
relation_filter = Q(part_1=related_part) | Q(part_2=related_part)
|
||||||
|
|
||||||
|
for relation in PartRelated.objects.filter(relation_filter):
|
||||||
|
|
||||||
|
if relation.part_1.pk != pk:
|
||||||
|
part_ids.add(relation.part_1.pk)
|
||||||
|
|
||||||
|
if relation.part_2.pk != pk:
|
||||||
|
part_ids.add(relation.part_2.pk)
|
||||||
|
|
||||||
|
if related is not None:
|
||||||
|
# Only return related results
|
||||||
|
queryset = queryset.filter(pk__in=[pk for pk in part_ids])
|
||||||
|
elif exclude_related is not None:
|
||||||
|
# Exclude related results
|
||||||
|
queryset = queryset.exclude(pk__in=[pk for pk in part_ids])
|
||||||
|
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
# Filter by 'starred' parts?
|
# Filter by 'starred' parts?
|
||||||
starred = params.get('starred', None)
|
starred = params.get('starred', None)
|
||||||
|
|
||||||
@ -1017,6 +1051,44 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartRelatedList(generics.ListCreateAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for accessing a list of PartRelated objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = PartRelated.objects.all()
|
||||||
|
serializer_class = part_serializers.PartRelationSerializer
|
||||||
|
|
||||||
|
def filter_queryset(self, queryset):
|
||||||
|
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
|
params = self.request.query_params
|
||||||
|
|
||||||
|
# Add a filter for "part" - we can filter either part_1 or part_2
|
||||||
|
part = params.get('part', None)
|
||||||
|
|
||||||
|
if part is not None:
|
||||||
|
try:
|
||||||
|
part = Part.objects.get(pk=part)
|
||||||
|
|
||||||
|
queryset = queryset.filter(Q(part_1=part) | Q(part_2=part))
|
||||||
|
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class PartRelatedDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
"""
|
||||||
|
API endpoint for accessing detail view of a PartRelated object
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = PartRelated.objects.all()
|
||||||
|
serializer_class = part_serializers.PartRelationSerializer
|
||||||
|
|
||||||
|
|
||||||
class PartParameterTemplateList(generics.ListCreateAPIView):
|
class PartParameterTemplateList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of PartParameterTemplate objects.
|
""" API endpoint for accessing a list of PartParameterTemplate objects.
|
||||||
|
|
||||||
@ -1081,24 +1153,6 @@ class BomFilter(rest_filters.FilterSet):
|
|||||||
inherited = rest_filters.BooleanFilter(label='BOM line is inherited')
|
inherited = rest_filters.BooleanFilter(label='BOM line is inherited')
|
||||||
allow_variants = rest_filters.BooleanFilter(label='Variants are allowed')
|
allow_variants = rest_filters.BooleanFilter(label='Variants are allowed')
|
||||||
|
|
||||||
validated = rest_filters.BooleanFilter(label='BOM line has been validated', method='filter_validated')
|
|
||||||
|
|
||||||
def filter_validated(self, queryset, name, value):
|
|
||||||
|
|
||||||
# Work out which lines have actually been validated
|
|
||||||
pks = []
|
|
||||||
|
|
||||||
for bom_item in queryset.all():
|
|
||||||
if bom_item.is_line_valid():
|
|
||||||
pks.append(bom_item.pk)
|
|
||||||
|
|
||||||
if str2bool(value):
|
|
||||||
queryset = queryset.filter(pk__in=pks)
|
|
||||||
else:
|
|
||||||
queryset = queryset.exclude(pk__in=pks)
|
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
# Filters for linked 'part'
|
# Filters for linked 'part'
|
||||||
part_active = rest_filters.BooleanFilter(label='Master part is active', field_name='part__active')
|
part_active = rest_filters.BooleanFilter(label='Master part is active', field_name='part__active')
|
||||||
part_trackable = rest_filters.BooleanFilter(label='Master part is trackable', field_name='part__trackable')
|
part_trackable = rest_filters.BooleanFilter(label='Master part is trackable', field_name='part__trackable')
|
||||||
@ -1107,6 +1161,30 @@ class BomFilter(rest_filters.FilterSet):
|
|||||||
sub_part_trackable = rest_filters.BooleanFilter(label='Sub part is trackable', field_name='sub_part__trackable')
|
sub_part_trackable = rest_filters.BooleanFilter(label='Sub part is trackable', field_name='sub_part__trackable')
|
||||||
sub_part_assembly = rest_filters.BooleanFilter(label='Sub part is an assembly', field_name='sub_part__assembly')
|
sub_part_assembly = rest_filters.BooleanFilter(label='Sub part is an assembly', field_name='sub_part__assembly')
|
||||||
|
|
||||||
|
validated = rest_filters.BooleanFilter(label='BOM line has been validated', method='filter_validated')
|
||||||
|
|
||||||
|
def filter_validated(self, queryset, name, value):
|
||||||
|
|
||||||
|
# Work out which lines have actually been validated
|
||||||
|
pks = []
|
||||||
|
|
||||||
|
value = str2bool(value)
|
||||||
|
|
||||||
|
# Shortcut for quicker filtering - BomItem with empty 'checksum' values are not validated
|
||||||
|
if value:
|
||||||
|
queryset = queryset.exclude(checksum=None).exclude(checksum='')
|
||||||
|
|
||||||
|
for bom_item in queryset.all():
|
||||||
|
if bom_item.is_line_valid:
|
||||||
|
pks.append(bom_item.pk)
|
||||||
|
|
||||||
|
if value:
|
||||||
|
queryset = queryset.filter(pk__in=pks)
|
||||||
|
else:
|
||||||
|
queryset = queryset.exclude(pk__in=pks)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class BomList(generics.ListCreateAPIView):
|
class BomList(generics.ListCreateAPIView):
|
||||||
"""
|
"""
|
||||||
@ -1257,7 +1335,7 @@ class BomList(generics.ListCreateAPIView):
|
|||||||
queryset = self.annotate_pricing(queryset)
|
queryset = self.annotate_pricing(queryset)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def include_pricing(self):
|
def include_pricing(self):
|
||||||
"""
|
"""
|
||||||
Determine if pricing information should be included in the response
|
Determine if pricing information should be included in the response
|
||||||
@ -1291,7 +1369,7 @@ class BomList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
# Get default currency from settings
|
# Get default currency from settings
|
||||||
default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||||
|
|
||||||
if price:
|
if price:
|
||||||
if currency and default_currency:
|
if currency and default_currency:
|
||||||
try:
|
try:
|
||||||
@ -1381,7 +1459,7 @@ class BomItemSubstituteList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
serializer_class = part_serializers.BomItemSubstituteSerializer
|
serializer_class = part_serializers.BomItemSubstituteSerializer
|
||||||
queryset = BomItemSubstitute.objects.all()
|
queryset = BomItemSubstitute.objects.all()
|
||||||
|
|
||||||
filter_backends = [
|
filter_backends = [
|
||||||
DjangoFilterBackend,
|
DjangoFilterBackend,
|
||||||
filters.SearchFilter,
|
filters.SearchFilter,
|
||||||
@ -1435,6 +1513,12 @@ part_api_urls = [
|
|||||||
url(r'^.*$', PartInternalPriceList.as_view(), name='api-part-internal-price-list'),
|
url(r'^.*$', PartInternalPriceList.as_view(), name='api-part-internal-price-list'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
# Base URL for PartRelated API endpoints
|
||||||
|
url(r'^related/', include([
|
||||||
|
url(r'^(?P<pk>\d+)/', PartRelatedDetail.as_view(), name='api-part-related-detail'),
|
||||||
|
url(r'^.*$', PartRelatedList.as_view(), name='api-part-related-list'),
|
||||||
|
])),
|
||||||
|
|
||||||
# Base URL for PartParameter API endpoints
|
# Base URL for PartParameter API endpoints
|
||||||
url(r'^parameter/', include([
|
url(r'^parameter/', include([
|
||||||
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'),
|
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'),
|
||||||
|
@ -59,7 +59,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
|
|
||||||
uids = []
|
uids = []
|
||||||
|
|
||||||
def add_items(items, level, cascade):
|
def add_items(items, level, cascade=True):
|
||||||
# Add items at a given layer
|
# Add items at a given layer
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
||||||
@ -172,7 +172,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
|
|
||||||
# Filter manufacturer parts
|
# Filter manufacturer parts
|
||||||
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk).prefetch_related('supplier_parts')
|
manufacturer_parts = ManufacturerPart.objects.filter(part__pk=b_part.pk).prefetch_related('supplier_parts')
|
||||||
|
|
||||||
for mp_idx, mp_part in enumerate(manufacturer_parts):
|
for mp_idx, mp_part in enumerate(manufacturer_parts):
|
||||||
|
|
||||||
# Extract the "name" field of the Manufacturer (Company)
|
# Extract the "name" field of the Manufacturer (Company)
|
||||||
@ -190,7 +190,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
# Generate a column name for this manufacturer
|
# Generate a column name for this manufacturer
|
||||||
k_man = f'{_("Manufacturer")}_{mp_idx}'
|
k_man = f'{_("Manufacturer")}_{mp_idx}'
|
||||||
k_mpn = f'{_("MPN")}_{mp_idx}'
|
k_mpn = f'{_("MPN")}_{mp_idx}'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manufacturer_cols[k_man].update({bom_idx: manufacturer_name})
|
manufacturer_cols[k_man].update({bom_idx: manufacturer_name})
|
||||||
manufacturer_cols[k_mpn].update({bom_idx: manufacturer_mpn})
|
manufacturer_cols[k_mpn].update({bom_idx: manufacturer_mpn})
|
||||||
@ -200,7 +200,7 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
|||||||
|
|
||||||
# We wish to include supplier data for this manufacturer part
|
# We wish to include supplier data for this manufacturer part
|
||||||
if supplier_data:
|
if supplier_data:
|
||||||
|
|
||||||
for sp_idx, sp_part in enumerate(mp_part.supplier_parts.all()):
|
for sp_idx, sp_part in enumerate(mp_part.supplier_parts.all()):
|
||||||
|
|
||||||
supplier_parts_used.add(sp_part)
|
supplier_parts_used.add(sp_part)
|
||||||
|
@ -17,7 +17,7 @@ from InvenTree.fields import RoundingDecimalFormField
|
|||||||
import common.models
|
import common.models
|
||||||
from common.forms import MatchItemForm
|
from common.forms import MatchItemForm
|
||||||
|
|
||||||
from .models import Part, PartCategory, PartRelated
|
from .models import Part, PartCategory
|
||||||
from .models import PartParameterTemplate
|
from .models import PartParameterTemplate
|
||||||
from .models import PartCategoryParameterTemplate
|
from .models import PartCategoryParameterTemplate
|
||||||
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||||
@ -157,20 +157,6 @@ class BomMatchItemForm(MatchItemForm):
|
|||||||
return super().get_special_field(col_guess, row, file_manager)
|
return super().get_special_field(col_guess, row, file_manager)
|
||||||
|
|
||||||
|
|
||||||
class CreatePartRelatedForm(HelperForm):
|
|
||||||
""" Form for creating a PartRelated object """
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = PartRelated
|
|
||||||
fields = [
|
|
||||||
'part_1',
|
|
||||||
'part_2',
|
|
||||||
]
|
|
||||||
labels = {
|
|
||||||
'part_2': _('Related Part'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SetPartCategoryForm(forms.Form):
|
class SetPartCategoryForm(forms.Form):
|
||||||
""" Form for setting the category of multiple Part objects """
|
""" Form for setting the category of multiple Part objects """
|
||||||
|
|
||||||
|
@ -1587,7 +1587,7 @@ class Part(MPTTModel):
|
|||||||
# Exclude any parts that this part is used *in* (to prevent recursive BOMs)
|
# Exclude any parts that this part is used *in* (to prevent recursive BOMs)
|
||||||
used_in = self.get_used_in().all()
|
used_in = self.get_used_in().all()
|
||||||
|
|
||||||
parts = parts.exclude(id__in=[item.part.id for item in used_in])
|
parts = parts.exclude(id__in=[part.id for part in used_in])
|
||||||
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
@ -2118,7 +2118,7 @@ class Part(MPTTModel):
|
|||||||
"""
|
"""
|
||||||
Returns True if the total stock for this part is less than the minimum stock level
|
Returns True if the total stock for this part is less than the minimum stock level
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.get_stock_count() < self.minimum_stock
|
return self.get_stock_count() < self.minimum_stock
|
||||||
|
|
||||||
|
|
||||||
@ -2155,7 +2155,7 @@ class PartSellPriceBreak(common.models.PriceBreak):
|
|||||||
"""
|
"""
|
||||||
Represents a price break for selling this part
|
Represents a price break for selling this part
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_api_url():
|
def get_api_url():
|
||||||
return reverse('api-part-sale-price-list')
|
return reverse('api-part-sale-price-list')
|
||||||
|
@ -25,7 +25,7 @@ from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
|||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
from .models import (BomItem, BomItemSubstitute,
|
from .models import (BomItem, BomItemSubstitute,
|
||||||
Part, PartAttachment, PartCategory,
|
Part, PartAttachment, PartCategory, PartRelated,
|
||||||
PartParameter, PartParameterTemplate, PartSellPriceBreak,
|
PartParameter, PartParameterTemplate, PartSellPriceBreak,
|
||||||
PartStar, PartTestTemplate, PartCategoryParameterTemplate,
|
PartStar, PartTestTemplate, PartCategoryParameterTemplate,
|
||||||
PartInternalPriceBreak)
|
PartInternalPriceBreak)
|
||||||
@ -388,6 +388,25 @@ class PartSerializer(InvenTreeModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PartRelationSerializer(InvenTreeModelSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for a PartRelated model
|
||||||
|
"""
|
||||||
|
|
||||||
|
part_1_detail = PartSerializer(source='part_1', read_only=True, many=False)
|
||||||
|
part_2_detail = PartSerializer(source='part_2', read_only=True, many=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PartRelated
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'part_1',
|
||||||
|
'part_1_detail',
|
||||||
|
'part_2',
|
||||||
|
'part_2_detail',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class PartStarSerializer(InvenTreeModelSerializer):
|
class PartStarSerializer(InvenTreeModelSerializer):
|
||||||
""" Serializer for a PartStar object """
|
""" Serializer for a PartStar object """
|
||||||
|
|
||||||
@ -446,9 +465,9 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
purchase_price_min = MoneyField(max_digits=10, decimal_places=6, read_only=True)
|
purchase_price_min = MoneyField(max_digits=10, decimal_places=6, read_only=True)
|
||||||
|
|
||||||
purchase_price_max = MoneyField(max_digits=10, decimal_places=6, read_only=True)
|
purchase_price_max = MoneyField(max_digits=10, decimal_places=6, read_only=True)
|
||||||
|
|
||||||
purchase_price_avg = serializers.SerializerMethodField()
|
purchase_price_avg = serializers.SerializerMethodField()
|
||||||
|
|
||||||
purchase_price_range = serializers.SerializerMethodField()
|
purchase_price_range = serializers.SerializerMethodField()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -520,7 +539,7 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
def get_purchase_price_avg(self, obj):
|
def get_purchase_price_avg(self, obj):
|
||||||
""" Return purchase price average """
|
""" Return purchase price average """
|
||||||
|
|
||||||
try:
|
try:
|
||||||
purchase_price_avg = obj.purchase_price_avg
|
purchase_price_avg = obj.purchase_price_avg
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -62,7 +62,7 @@ def notify_low_stock(part: part.models.Part):
|
|||||||
def notify_low_stock_if_required(part: part.models.Part):
|
def notify_low_stock_if_required(part: part.models.Part):
|
||||||
"""
|
"""
|
||||||
Check if the stock quantity has fallen below the minimum threshold of part.
|
Check if the stock quantity has fallen below the minimum threshold of part.
|
||||||
|
|
||||||
If true, notify the users who have subscribed to the part
|
If true, notify the users who have subscribed to the part
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% url "part-detail" part.id as url %}
|
{% url "part-detail" part.id as url %}
|
||||||
{% include "sidebar_link.html" with url=url text="Return to BOM" icon="fa-undo" %}
|
{% trans "Return to BOM" as text %}
|
||||||
|
{% include "sidebar_link.html" with url=url text=text icon="fa-undo" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block heading %}
|
||||||
|
@ -4,12 +4,16 @@
|
|||||||
|
|
||||||
{% settings_value 'PART_SHOW_IMPORT' as show_import %}
|
{% settings_value 'PART_SHOW_IMPORT' as show_import %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label="subcategories" text="Subcategories" icon="fa-sitemap" %}
|
{% trans "Subcategories" as text %}
|
||||||
{% include "sidebar_item.html" with label="parts" text="Parts" icon="fa-shapes" %}
|
{% include "sidebar_item.html" with label="subcategories" text=text icon="fa-sitemap" %}
|
||||||
|
{% trans "Parts" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="parts" text=text icon="fa-shapes" %}
|
||||||
{% if show_import and user.is_staff and roles.part.add %}
|
{% if show_import and user.is_staff and roles.part.add %}
|
||||||
{% url "part-import" as url %}
|
{% url "part-import" as url %}
|
||||||
{% include "sidebar_link.html" with url=url text="Import Parts" icon="fa-file-upload" %}
|
{% trans "Import Parts" as text %}
|
||||||
|
{% include "sidebar_link.html" with url=url text=text icon="fa-file-upload" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if category %}
|
{% if category %}
|
||||||
{% include "sidebar_item.html" with label="parameters" text="Parameters" icon="fa-tasks" %}
|
{% trans "Parameters" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="parameters" text=text icon="fa-tasks" %}
|
||||||
{% endif %}
|
{% endif %}
|
@ -329,34 +329,8 @@
|
|||||||
{% include "filter_list.html" with id="related" %}
|
{% include "filter_list.html" with id="related" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table id='table-related-part' class='table table-condensed table-striped' data-toolbar='#related-button-toolbar'>
|
<table id='related-parts-table' class='table table-striped table-condensed' data-toolbar='#related-button-toolbar'></table>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th data-field='part' data-serachable='true'>{% trans "Part" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for item in part.get_related_parts %}
|
|
||||||
{% with part_related=item.0 part=item.1 %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a class='hover-icon'>
|
|
||||||
<img class='hover-img-thumb' src='{{ part.get_thumbnail_url }}'>
|
|
||||||
<img class='hover-img-large' src='{{ part.get_thumbnail_url }}'>
|
|
||||||
</a>
|
|
||||||
<a href='/part/{{ part.id }}/'>{{ part }}</a>
|
|
||||||
<div class='btn-group' style='float: right;'>
|
|
||||||
{% if roles.part.change %}
|
|
||||||
<button title='{% trans "Delete" %}' class='btn btn-outline-secondary delete-related-part' url="{% url 'part-related-delete' part_related.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -771,15 +745,32 @@
|
|||||||
|
|
||||||
// Load the "related parts" tab
|
// Load the "related parts" tab
|
||||||
onPanelLoad("related-parts", function() {
|
onPanelLoad("related-parts", function() {
|
||||||
$('#table-related-part').inventreeTable({
|
|
||||||
});
|
loadRelatedPartsTable(
|
||||||
|
"#related-parts-table",
|
||||||
|
{{ part.pk }}
|
||||||
|
);
|
||||||
|
|
||||||
$("#add-related-part").click(function() {
|
$("#add-related-part").click(function() {
|
||||||
launchModalForm("{% url 'part-related-create' %}", {
|
|
||||||
data: {
|
constructForm('{% url "api-part-related-list" %}', {
|
||||||
part: {{ part.id }},
|
method: 'POST',
|
||||||
|
fields: {
|
||||||
|
part_1: {
|
||||||
|
hidden: true,
|
||||||
|
value: {{ part.pk }},
|
||||||
|
},
|
||||||
|
part_2: {
|
||||||
|
label: '{% trans "Related Part" %}',
|
||||||
|
filters: {
|
||||||
|
exclude_related: {{ part.pk }},
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
reload: true,
|
title: '{% trans "Add Related Part" %}',
|
||||||
|
onSuccess: function() {
|
||||||
|
$('#related-parts-table').bootstrapTable('refresh');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
{% url 'part-index' as url %}
|
{% url 'part-index' as url %}
|
||||||
{% include "sidebar_link.html" with url=url text="Return to Parts" icon="fa-undo" %}
|
{% trans "Return to Parts" as text %}
|
||||||
|
{% include "sidebar_link.html" with url=url text=text icon="fa-undo" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -5,34 +5,49 @@
|
|||||||
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
|
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
|
||||||
{% settings_value 'PART_SHOW_RELATED' as show_related %}
|
{% settings_value 'PART_SHOW_RELATED' as show_related %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label="part-details" text="Details" icon="fa-shapes" %}
|
{% trans "Details" as text %}
|
||||||
{% include "sidebar_item.html" with label="part-parameters" text="Parameters" icon="fa-th-list" %}
|
{% include "sidebar_item.html" with label="part-details" text=text icon="fa-shapes" %}
|
||||||
|
{% trans "Parameters" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="part-parameters" text=text icon="fa-th-list" %}
|
||||||
{% if part.is_template %}
|
{% if part.is_template %}
|
||||||
{% include "sidebar_item.html" with label="variants" text="Variants" icon="fa-shapes" %}
|
{% trans "Variants" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="variants" text=text icon="fa-shapes" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "sidebar_item.html" with label="part-stock" text="Stock" icon="fa-boxes" %}
|
{% trans "Stock" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="part-stock" text=text icon="fa-boxes" %}
|
||||||
{% if part.assembly %}
|
{% if part.assembly %}
|
||||||
{% include "sidebar_item.html" with label="bom" text="Bill of Materials" icon="fa-list" %}
|
{% trans "Bill of Materials" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="bom" text=text icon="fa-list" %}
|
||||||
{% if roles.build.view %}
|
{% if roles.build.view %}
|
||||||
{% include "sidebar_item.html" with label="build-orders" text="Build Orders" icon="fa-tools" %}
|
{% trans "Build Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="build-orders" text=text icon="fa-tools" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.component %}
|
{% if part.component %}
|
||||||
{% include "sidebar_item.html" with label="used-in" text="Used In" icon="fa-layer-group" %}
|
{% trans "Used In" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="used-in" text=text icon="fa-layer-group" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "sidebar_item.html" with label="pricing" text="Pricing" icon="fa-dollar-sign" %}
|
{% trans "Pricing" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="pricing" text=text icon="fa-dollar-sign" %}
|
||||||
{% if part.purchaseable and roles.purchase_order.view %}
|
{% if part.purchaseable and roles.purchase_order.view %}
|
||||||
{% include "sidebar_item.html" with label="suppliers" text="Suppliers" icon="fa-building" %}
|
{% trans "Suppliers" as text %}
|
||||||
{% include "sidebar_item.html" with label="purchase-orders" text="Purchase Orders" icon="fa-shopping-cart" %}
|
{% include "sidebar_item.html" with label="suppliers" text=text icon="fa-building" %}
|
||||||
|
{% trans "Purchase Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="purchase-orders" text=text icon="fa-shopping-cart" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.salable and roles.sales_order.view %}
|
{% if part.salable and roles.sales_order.view %}
|
||||||
{% include "sidebar_item.html" with label="sales-orders" text="Sales Orders" icon="fa-truck" %}
|
{% trans "Sales Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="sales-orders" text=text icon="fa-truck" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if part.trackable %}
|
{% if part.trackable %}
|
||||||
{% include "sidebar_item.html" with label="test-templates" text="Test Templates" icon="fa-vial" %}
|
{% trans "Test Templates" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="test-templates" text=text icon="fa-vial" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if show_related %}
|
{% if show_related %}
|
||||||
{% include "sidebar_item.html" with label="related-parts" text="Related Parts" icon="fa-random" %}
|
{% trans "Related Parts" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="related-parts" text=text icon="fa-random" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "sidebar_item.html" with label="part-attachments" text="Attachments" icon="fa-paperclip" %}
|
{% trans "Attachments" as text %}
|
||||||
{% include "sidebar_item.html" with label="part-notes" text="Notes" icon="fa-clipboard" %}
|
{% include "sidebar_item.html" with label="part-attachments" text=text icon="fa-paperclip" %}
|
||||||
|
{% trans "Notes" as text %}
|
||||||
|
{% include "sidebar_item.html" with label="part-notes" text=text icon="fa-clipboard" %}
|
||||||
|
@ -236,7 +236,7 @@ def settings_value(key, *args, **kwargs):
|
|||||||
|
|
||||||
if 'user' in kwargs:
|
if 'user' in kwargs:
|
||||||
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
|
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
|
||||||
|
|
||||||
return InvenTreeSetting.get_setting(key)
|
return InvenTreeSetting.get_setting(key)
|
||||||
|
|
||||||
|
|
||||||
@ -384,7 +384,7 @@ def keyvalue(dict, key):
|
|||||||
def call_method(obj, method_name, *args):
|
def call_method(obj, method_name, *args):
|
||||||
"""
|
"""
|
||||||
enables calling model methods / functions from templates with arguments
|
enables calling model methods / functions from templates with arguments
|
||||||
|
|
||||||
usage:
|
usage:
|
||||||
{% call_method model_object 'fnc_name' argument1 %}
|
{% call_method model_object 'fnc_name' argument1 %}
|
||||||
"""
|
"""
|
||||||
|
@ -542,7 +542,7 @@ class PartAPITest(InvenTreeAPITestCase):
|
|||||||
# Check that there is a new manufacturer part *and* a new supplier part
|
# Check that there is a new manufacturer part *and* a new supplier part
|
||||||
self.assertEqual(new_part.supplier_parts.count(), 1)
|
self.assertEqual(new_part.supplier_parts.count(), 1)
|
||||||
self.assertEqual(new_part.manufacturer_parts.count(), 1)
|
self.assertEqual(new_part.manufacturer_parts.count(), 1)
|
||||||
|
|
||||||
def test_strange_chars(self):
|
def test_strange_chars(self):
|
||||||
"""
|
"""
|
||||||
Test that non-standard ASCII chars are accepted
|
Test that non-standard ASCII chars are accepted
|
||||||
@ -911,7 +911,7 @@ class BomItemTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
# How many BOM items currently exist in the database?
|
# How many BOM items currently exist in the database?
|
||||||
n = BomItem.objects.count()
|
n = BomItem.objects.count()
|
||||||
|
|
||||||
url = reverse('api-bom-list')
|
url = reverse('api-bom-list')
|
||||||
response = self.get(url, expected_code=200)
|
response = self.get(url, expected_code=200)
|
||||||
self.assertEqual(len(response.data), n)
|
self.assertEqual(len(response.data), n)
|
||||||
@ -925,7 +925,46 @@ class BomItemTest(InvenTreeAPITestCase):
|
|||||||
expected_code=200
|
expected_code=200
|
||||||
)
|
)
|
||||||
|
|
||||||
print("results:", len(response.data))
|
# Filter by "validated"
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
'validated': True,
|
||||||
|
},
|
||||||
|
expected_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should be zero validated results
|
||||||
|
self.assertEqual(len(response.data), 0)
|
||||||
|
|
||||||
|
# Now filter by "not validated"
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
'validated': False,
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
# There should be at least one non-validated item
|
||||||
|
self.assertTrue(len(response.data) > 0)
|
||||||
|
|
||||||
|
# Now, let's validate an item
|
||||||
|
bom_item = BomItem.objects.first()
|
||||||
|
|
||||||
|
bom_item.validate_hash()
|
||||||
|
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
'validated': True,
|
||||||
|
},
|
||||||
|
expected_code=200
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the expected response is returned
|
||||||
|
self.assertEqual(len(response.data), 1)
|
||||||
|
self.assertEqual(response.data[0]['pk'], bom_item.pk)
|
||||||
|
|
||||||
def test_get_bom_detail(self):
|
def test_get_bom_detail(self):
|
||||||
"""
|
"""
|
||||||
@ -962,7 +1001,7 @@ class BomItemTest(InvenTreeAPITestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.post(url, data, expected_code=201)
|
self.post(url, data, expected_code=201)
|
||||||
|
|
||||||
# Now try to create a BomItem which references itself
|
# Now try to create a BomItem which references itself
|
||||||
data['part'] = 100
|
data['part'] = 100
|
||||||
data['sub_part'] = 100
|
data['sub_part'] = 100
|
||||||
@ -1003,7 +1042,7 @@ class BomItemTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
# Now we will create some variant parts and stock
|
# Now we will create some variant parts and stock
|
||||||
for ii in range(5):
|
for ii in range(5):
|
||||||
|
|
||||||
# Create a variant part!
|
# Create a variant part!
|
||||||
variant = Part.objects.create(
|
variant = Part.objects.create(
|
||||||
name=f"Variant_{ii}",
|
name=f"Variant_{ii}",
|
||||||
|
@ -153,7 +153,7 @@ class BomItemTest(TestCase):
|
|||||||
subs = []
|
subs = []
|
||||||
|
|
||||||
for ii in range(5):
|
for ii in range(5):
|
||||||
|
|
||||||
# Create a new part
|
# Create a new part
|
||||||
sub_part = Part.objects.create(
|
sub_part = Part.objects.create(
|
||||||
name=f"Orphan {ii}",
|
name=f"Orphan {ii}",
|
||||||
@ -181,7 +181,7 @@ class BomItemTest(TestCase):
|
|||||||
|
|
||||||
# There should be now 5 substitute parts available
|
# There should be now 5 substitute parts available
|
||||||
self.assertEqual(bom_item.substitutes.count(), 5)
|
self.assertEqual(bom_item.substitutes.count(), 5)
|
||||||
|
|
||||||
# Try to create a substitute which points to the same sub-part (should fail)
|
# Try to create a substitute which points to the same sub-part (should fail)
|
||||||
with self.assertRaises(django_exceptions.ValidationError):
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
BomItemSubstitute.objects.create(
|
BomItemSubstitute.objects.create(
|
||||||
|
@ -370,7 +370,7 @@ class PartSubscriptionTests(TestCase):
|
|||||||
|
|
||||||
# electronics / IC / MCU
|
# electronics / IC / MCU
|
||||||
self.category = PartCategory.objects.get(pk=4)
|
self.category = PartCategory.objects.get(pk=4)
|
||||||
|
|
||||||
self.part = Part.objects.create(
|
self.part = Part.objects.create(
|
||||||
category=self.category,
|
category=self.category,
|
||||||
name='STM32F103',
|
name='STM32F103',
|
||||||
@ -382,7 +382,7 @@ class PartSubscriptionTests(TestCase):
|
|||||||
"""
|
"""
|
||||||
Test basic subscription against a part
|
Test basic subscription against a part
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# First check that the user is *not* subscribed to the part
|
# First check that the user is *not* subscribed to the part
|
||||||
self.assertFalse(self.part.is_starred_by(self.user))
|
self.assertFalse(self.part.is_starred_by(self.user))
|
||||||
|
|
||||||
@ -450,7 +450,7 @@ class PartSubscriptionTests(TestCase):
|
|||||||
"""
|
"""
|
||||||
Check that a parent category can be subscribed to
|
Check that a parent category can be subscribed to
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Top-level "electronics" category
|
# Top-level "electronics" category
|
||||||
cat = PartCategory.objects.get(pk=1)
|
cat = PartCategory.objects.get(pk=1)
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ from django.urls import reverse
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
from .models import Part, PartRelated
|
from .models import Part
|
||||||
|
|
||||||
|
|
||||||
class PartViewTestCase(TestCase):
|
class PartViewTestCase(TestCase):
|
||||||
@ -145,36 +145,6 @@ class PartDetailTest(PartViewTestCase):
|
|||||||
self.assertIn('streaming_content', dir(response))
|
self.assertIn('streaming_content', dir(response))
|
||||||
|
|
||||||
|
|
||||||
class PartRelatedTests(PartViewTestCase):
|
|
||||||
|
|
||||||
def test_valid_create(self):
|
|
||||||
""" test creation of a related part """
|
|
||||||
|
|
||||||
# Test GET view
|
|
||||||
response = self.client.get(reverse('part-related-create'), {'part': 1},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
# Test POST view with valid form data
|
|
||||||
response = self.client.post(reverse('part-related-create'), {'part_1': 1, 'part_2': 2},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertContains(response, '"form_valid": true', status_code=200)
|
|
||||||
|
|
||||||
# Try to create the same relationship with part_1 and part_2 pks reversed
|
|
||||||
response = self.client.post(reverse('part-related-create'), {'part_1': 2, 'part_2': 1},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertContains(response, '"form_valid": false', status_code=200)
|
|
||||||
|
|
||||||
# Try to create part related to itself
|
|
||||||
response = self.client.post(reverse('part-related-create'), {'part_1': 1, 'part_2': 1},
|
|
||||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertContains(response, '"form_valid": false', status_code=200)
|
|
||||||
|
|
||||||
# Check final count
|
|
||||||
n = PartRelated.objects.all().count()
|
|
||||||
self.assertEqual(n, 1)
|
|
||||||
|
|
||||||
|
|
||||||
class PartQRTest(PartViewTestCase):
|
class PartQRTest(PartViewTestCase):
|
||||||
""" Tests for the Part QR Code AJAX view """
|
""" Tests for the Part QR Code AJAX view """
|
||||||
|
|
||||||
|
@ -12,10 +12,6 @@ from django.conf.urls import url, include
|
|||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
part_related_urls = [
|
|
||||||
url(r'^new/?', views.PartRelatedCreate.as_view(), name='part-related-create'),
|
|
||||||
url(r'^(?P<pk>\d+)/delete/?', views.PartRelatedDelete.as_view(), name='part-related-delete'),
|
|
||||||
]
|
|
||||||
|
|
||||||
sale_price_break_urls = [
|
sale_price_break_urls = [
|
||||||
url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
|
url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
|
||||||
@ -40,7 +36,7 @@ part_detail_urls = [
|
|||||||
url(r'^bom-export/?', views.BomExport.as_view(), name='bom-export'),
|
url(r'^bom-export/?', views.BomExport.as_view(), name='bom-export'),
|
||||||
url(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'),
|
url(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'),
|
||||||
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
|
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
|
||||||
|
|
||||||
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
|
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
|
||||||
|
|
||||||
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
|
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
|
||||||
@ -96,9 +92,6 @@ part_urls = [
|
|||||||
# Part category
|
# Part category
|
||||||
url(r'^category/', include(category_urls)),
|
url(r'^category/', include(category_urls)),
|
||||||
|
|
||||||
# Part related
|
|
||||||
url(r'^related-parts/', include(part_related_urls)),
|
|
||||||
|
|
||||||
# Part price breaks
|
# Part price breaks
|
||||||
url(r'^sale-price/', include(sale_price_break_urls)),
|
url(r'^sale-price/', include(sale_price_break_urls)),
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ import io
|
|||||||
from rapidfuzz import fuzz
|
from rapidfuzz import fuzz
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
from .models import PartCategory, Part, PartRelated
|
from .models import PartCategory, Part
|
||||||
from .models import PartParameterTemplate
|
from .models import PartParameterTemplate
|
||||||
from .models import PartCategoryParameterTemplate
|
from .models import PartCategoryParameterTemplate
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
@ -85,75 +85,6 @@ class PartIndex(InvenTreeRoleMixin, ListView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class PartRelatedCreate(AjaxCreateView):
|
|
||||||
""" View for creating a new PartRelated object
|
|
||||||
|
|
||||||
- The view only makes sense if a Part object is passed to it
|
|
||||||
"""
|
|
||||||
model = PartRelated
|
|
||||||
form_class = part_forms.CreatePartRelatedForm
|
|
||||||
ajax_form_title = _("Add Related Part")
|
|
||||||
ajax_template_name = "modal_form.html"
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
""" Set parent part as part_1 field """
|
|
||||||
|
|
||||||
initials = {}
|
|
||||||
|
|
||||||
part_id = self.request.GET.get('part', None)
|
|
||||||
|
|
||||||
if part_id:
|
|
||||||
try:
|
|
||||||
initials['part_1'] = Part.objects.get(pk=part_id)
|
|
||||||
except (Part.DoesNotExist, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return initials
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
""" Create a form to upload a new PartRelated
|
|
||||||
|
|
||||||
- Hide the 'part_1' field (parent part)
|
|
||||||
- Display parts which are not yet related
|
|
||||||
"""
|
|
||||||
|
|
||||||
form = super(AjaxCreateView, self).get_form()
|
|
||||||
|
|
||||||
form.fields['part_1'].widget = HiddenInput()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Get parent part
|
|
||||||
parent_part = self.get_initial()['part_1']
|
|
||||||
# Get existing related parts
|
|
||||||
related_parts = [related_part[1].pk for related_part in parent_part.get_related_parts()]
|
|
||||||
|
|
||||||
# Build updated choice list excluding
|
|
||||||
# - parts already related to parent part
|
|
||||||
# - the parent part itself
|
|
||||||
updated_choices = []
|
|
||||||
for choice in form.fields["part_2"].choices:
|
|
||||||
if (choice[0] not in related_parts) and (choice[0] != parent_part.pk):
|
|
||||||
updated_choices.append(choice)
|
|
||||||
|
|
||||||
# Update choices for related part
|
|
||||||
form.fields['part_2'].choices = updated_choices
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
|
|
||||||
class PartRelatedDelete(AjaxDeleteView):
|
|
||||||
""" View for deleting a PartRelated object """
|
|
||||||
|
|
||||||
model = PartRelated
|
|
||||||
ajax_form_title = _("Delete Related Part")
|
|
||||||
context_object_name = "related"
|
|
||||||
|
|
||||||
# Explicit role requirement
|
|
||||||
role_required = 'part.change'
|
|
||||||
|
|
||||||
|
|
||||||
class PartSetCategory(AjaxUpdateView):
|
class PartSetCategory(AjaxUpdateView):
|
||||||
""" View for settings the part category for multiple parts at once """
|
""" View for settings the part category for multiple parts at once """
|
||||||
|
|
||||||
@ -459,7 +390,7 @@ class PartDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
part = self.get_object()
|
part = self.get_object()
|
||||||
|
|
||||||
ctx = part.get_context_data(self.request)
|
ctx = part.get_context_data(self.request)
|
||||||
|
|
||||||
context.update(**ctx)
|
context.update(**ctx)
|
||||||
|
|
||||||
# Pricing information
|
# Pricing information
|
||||||
@ -1056,7 +987,7 @@ class BomUpload(InvenTreeRoleMixin, FileManagementFormView):
|
|||||||
matches = sorted(matches, key=lambda item: item['match'], reverse=True)
|
matches = sorted(matches, key=lambda item: item['match'], reverse=True)
|
||||||
|
|
||||||
part_options = [m['part'] for m in matches]
|
part_options = [m['part'] for m in matches]
|
||||||
|
|
||||||
# Supply list of part options for each row, sorted by how closely they match the part name
|
# Supply list of part options for each row, sorted by how closely they match the part name
|
||||||
row['item_options'] = part_options
|
row['item_options'] = part_options
|
||||||
|
|
||||||
@ -1520,11 +1451,11 @@ class CategoryDetail(InvenTreeRoleMixin, DetailView):
|
|||||||
|
|
||||||
# Prefetch parts parameters
|
# Prefetch parts parameters
|
||||||
parts_parameters = category.prefetch_parts_parameters(cascade=cascade)
|
parts_parameters = category.prefetch_parts_parameters(cascade=cascade)
|
||||||
|
|
||||||
# Get table headers (unique parameters names)
|
# Get table headers (unique parameters names)
|
||||||
context['headers'] = category.get_unique_parameters(cascade=cascade,
|
context['headers'] = category.get_unique_parameters(cascade=cascade,
|
||||||
prefetch=parts_parameters)
|
prefetch=parts_parameters)
|
||||||
|
|
||||||
# Insert part information
|
# Insert part information
|
||||||
context['headers'].insert(0, 'description')
|
context['headers'].insert(0, 'description')
|
||||||
context['headers'].insert(0, 'part')
|
context['headers'].insert(0, 'part')
|
||||||
|
@ -247,7 +247,7 @@ class ReportTemplateBase(ReportBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
template_string = Template(self.filename_pattern)
|
template_string = Template(self.filename_pattern)
|
||||||
|
|
||||||
ctx = self.context(request)
|
ctx = self.context(request)
|
||||||
|
|
||||||
context = Context(ctx)
|
context = Context(ctx)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user