mirror of
https://github.com/inventree/InvenTree.git
synced 2026-06-01 22:25:35 +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:
@@ -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
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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" %}
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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" %}
|
||||||
|
|||||||
Binary file not shown.
+3081
-2921
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2852
-2692
File diff suppressed because it is too large
Load Diff
+3315
-2982
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3087
-2927
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.
+3031
-2871
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2852
-2692
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2865
-2705
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3744
-3583
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2853
-2693
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2852
-2692
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2864
-2704
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2852
-2692
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2864
-2704
File diff suppressed because it is too large
Load Diff
+1226
-1036
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2859
-2699
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2852
-2692
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2852
-2692
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2916
-2756
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2852
-2692
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2875
-2715
File diff suppressed because it is too large
Load Diff
@@ -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" %}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+103
-19
@@ -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
|
||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
+1
-15
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 """
|
||||||
|
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -330,33 +330,7 @@
|
|||||||
</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" %}
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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'),
|
||||||
@@ -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)),
|
||||||
|
|
||||||
|
|||||||
+1
-70
@@ -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 """
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='sublocations' text="Sublocations" icon="fa-sitemap" %}
|
{% trans "Sublocations" as text %}
|
||||||
{% include "sidebar_item.html" with label='stock' text="Stock Items" icon="fa-boxes" %}
|
{% include "sidebar_item.html" with label='sublocations' text=text icon="fa-sitemap" %}
|
||||||
|
{% trans "Stock Items" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='stock' text=text icon="fa-boxes" %}
|
||||||
|
|||||||
@@ -2,15 +2,21 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='history' text="Stock Tracking" icon="fa-history" %}
|
{% trans "Stock Tracking" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='history' text=text icon="fa-history" %}
|
||||||
{% if item.part.trackable %}
|
{% if item.part.trackable %}
|
||||||
{% include "sidebar_item.html" with label='test-data' text="Test Data" icon="fa-vial" %}
|
{% trans "Test Data" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='test-data' text=text icon="fa-vial" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.part.assembly %}
|
{% if item.part.assembly %}
|
||||||
{% include "sidebar_item.html" with label='installed-items' text="Installed Items" icon="fa-sign-in-alt" %}
|
{% trans "Installed Items" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='installed-items' text=text icon="fa-sign-in-alt" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.child_count > 0 %}
|
{% if item.child_count > 0 %}
|
||||||
{% include "sidebar_item.html" with label='children' text="Child Items" icon="fa-sitemap" %}
|
{% trans "Child Items" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='children' text=text icon="fa-sitemap" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include "sidebar_item.html" with label='attachments' text="Attachments" icon="fa-paperclip" %}
|
{% trans "Attachments" as text %}
|
||||||
{% include "sidebar_item.html" with label='notes' text="Notes" icon="fa-clipboard" %}
|
{% 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" %}
|
||||||
|
|||||||
@@ -250,18 +250,18 @@ $("#param-table").inventreeTable({
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
field: 'pk',
|
field: 'pk',
|
||||||
title: 'ID',
|
title: '{% trans "ID" %}',
|
||||||
visible: false,
|
visible: false,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
title: 'Name',
|
title: '{% trans "Name" %}',
|
||||||
sortable: 'true',
|
sortable: 'true',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'units',
|
field: 'units',
|
||||||
title: 'Units',
|
title: '{% trans "Units" %}',
|
||||||
sortable: 'true',
|
sortable: 'true',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,29 +2,48 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% include "sidebar_header.html" with text="User Settings" icon='fa-user' %}
|
{% trans "User Settings" as text %}
|
||||||
|
{% include "sidebar_header.html" with text=text icon='fa-user' %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='account' text="Account Settings" icon="fa-cog" %}
|
{% trans "Account Settings" as text %}
|
||||||
{% include "sidebar_item.html" with label='user-display' text="Display Settings" icon="fa-desktop" %}
|
{% include "sidebar_item.html" with label='account' text=text icon="fa-cog" %}
|
||||||
{% include "sidebar_item.html" with label='user-home' text="Home Page" icon="fa-home" %}
|
{% trans "Display Settings" as text %}
|
||||||
{% include "sidebar_item.html" with label='user-search' text="Search Settings" icon="fa-search" %}
|
{% include "sidebar_item.html" with label='user-display' text=text icon="fa-desktop" %}
|
||||||
{% include "sidebar_item.html" with label='user-labels' text="Label Printing" icon="fa-tag" %}
|
{% trans "Home Page" as text %}
|
||||||
{% include "sidebar_item.html" with label='user-reports' text="Reporting" icon="fa-file-pdf" %}
|
{% include "sidebar_item.html" with label='user-home' text=text icon="fa-home" %}
|
||||||
|
{% trans "Search Settings" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='user-search' text=text icon="fa-search" %}
|
||||||
|
{% trans "Label Printing" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='user-labels' text=text icon="fa-tag" %}
|
||||||
|
{% trans "Reporting" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='user-reports' text=text icon="fa-file-pdf" %}
|
||||||
|
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
|
|
||||||
{% include "sidebar_header.html" with text="Global Settings" icon='fa-cogs' %}
|
{% trans "Global Settings" as text %}
|
||||||
|
{% include "sidebar_header.html" with text=text icon='fa-cogs' %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='server' text="Server Configuration" icon="fa-server" %}
|
{% trans "Server Configuration" as text %}
|
||||||
{% include "sidebar_item.html" with label='login' text="Login Settings" icon="fa-fingerprint" %}
|
{% include "sidebar_item.html" with label='server' text=text icon="fa-server" %}
|
||||||
{% include "sidebar_item.html" with label='barcodes' text="Barcode Support" icon="fa-qrcode" %}
|
{% trans "Login Settings" as text %}
|
||||||
{% include "sidebar_item.html" with label='currencies' text="Currencies" icon="fa-dollar-sign" %}
|
{% include "sidebar_item.html" with label='login' text=text icon="fa-fingerprint" %}
|
||||||
{% include "sidebar_item.html" with label='reporting' text="Reporting" icon="fa-file-pdf" %}
|
{% trans "Barcode Support" as text %}
|
||||||
{% include "sidebar_item.html" with label='parts' text="Parts" icon="fa-shapes" %}
|
{% include "sidebar_item.html" with label='barcodes' text=text icon="fa-qrcode" %}
|
||||||
{% include "sidebar_item.html" with label='category' text="Categories" icon="fa-sitemap" %}
|
{% trans "Currencies" as text %}
|
||||||
{% include "sidebar_item.html" with label='stock' text="Stock" icon="fa-boxes" %}
|
{% include "sidebar_item.html" with label='currencies' text=text icon="fa-dollar-sign" %}
|
||||||
{% include "sidebar_item.html" with label='build-order' text="Build Orders" icon="fa-tools" %}
|
{% trans "Reporting" as text %}
|
||||||
{% include "sidebar_item.html" with label='purchase-order' text="Purchase Orders" icon="fa-shopping-cart" %}
|
{% include "sidebar_item.html" with label='reporting' text=text icon="fa-file-pdf" %}
|
||||||
{% include "sidebar_item.html" with label='sales-order' text="Sales Orders" icon="fa-truck" %}
|
{% trans "Parts" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='parts' text=text icon="fa-shapes" %}
|
||||||
|
{% trans "Categories" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='category' text=text icon="fa-sitemap" %}
|
||||||
|
{% trans "Stock" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='stock' text=text icon="fa-boxes" %}
|
||||||
|
{% trans "Build Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='build-order' text=text icon="fa-tools" %}
|
||||||
|
{% trans "Purchase Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='purchase-order' text=text icon="fa-shopping-cart" %}
|
||||||
|
{% trans "Sales Orders" as text %}
|
||||||
|
{% include "sidebar_item.html" with label='sales-order' text=text icon="fa-truck" %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
{% if ALL_LANG %}
|
{% if ALL_LANG %}
|
||||||
. <a href="{% url 'settings' %}">{% trans "Show only sufficent" %}</a>
|
. <a href="{% url 'settings' %}">{% trans "Show only sufficent" %}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
and hidden. <a href="?alllang">{% trans "Show them too" %}</a>
|
{% trans "and hidden." %} <a href="?alllang">{% trans "Show them too" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td id='description-${pk}'><em>${part.description}</em></td>
|
<td id='description-${pk}'><em>${part.description}</em></td>
|
||||||
|
<td id='stock-${pk}'><em>${part.stock}</em></td>
|
||||||
<td>${buttons}</td>
|
<td>${buttons}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
@@ -212,6 +213,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Part" %}</th>
|
<th>{% trans "Part" %}</th>
|
||||||
<th>{% trans "Description" %}</th>
|
<th>{% trans "Description" %}</th>
|
||||||
|
<th>{% trans "Stock" %}</th>
|
||||||
<th><!-- Actions --></th>
|
<th><!-- Actions --></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ function supplierPartFields() {
|
|||||||
part_detail: true,
|
part_detail: true,
|
||||||
manufacturer_detail: true,
|
manufacturer_detail: true,
|
||||||
},
|
},
|
||||||
|
auto_fill: true,
|
||||||
},
|
},
|
||||||
description: {},
|
description: {},
|
||||||
link: {
|
link: {
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
|
|
||||||
var element = $(target);
|
var element = $(target);
|
||||||
|
|
||||||
if (!element) {
|
if (!element || !element.exists()) {
|
||||||
console.log(`WARNING: setupFilterList could not find target '${target}'`);
|
console.log(`WARNING: setupFilterList could not find target '${target}'`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
loadPartTable,
|
loadPartTable,
|
||||||
loadPartTestTemplateTable,
|
loadPartTestTemplateTable,
|
||||||
loadPartVariantTable,
|
loadPartVariantTable,
|
||||||
|
loadRelatedPartsTable,
|
||||||
loadSellPricingChart,
|
loadSellPricingChart,
|
||||||
loadSimplePartTable,
|
loadSimplePartTable,
|
||||||
loadStockPricingChart,
|
loadStockPricingChart,
|
||||||
@@ -705,6 +706,97 @@ function loadPartParameterTable(table, url, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadRelatedPartsTable(table, part_id, options={}) {
|
||||||
|
/*
|
||||||
|
* Load table of "related" parts
|
||||||
|
*/
|
||||||
|
|
||||||
|
options.params = options.params || {};
|
||||||
|
|
||||||
|
options.params.part = part_id;
|
||||||
|
|
||||||
|
var filters = {};
|
||||||
|
|
||||||
|
for (var key in options.params) {
|
||||||
|
filters[key] = options.params[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFilterList('related', $(table), options.filterTarget);
|
||||||
|
|
||||||
|
function getPart(row) {
|
||||||
|
if (row.part_1 == part_id) {
|
||||||
|
return row.part_2_detail;
|
||||||
|
} else {
|
||||||
|
return row.part_1_detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var columns = [
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '{% trans "Part" %}',
|
||||||
|
switchable: false,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
|
||||||
|
var part = getPart(row);
|
||||||
|
|
||||||
|
var html = imageHoverIcon(part.thumbnail) + renderLink(part.full_name, `/part/${part.pk}/`);
|
||||||
|
|
||||||
|
html += makePartIcons(part);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
title: '{% trans "Description" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
return getPart(row).description;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
title: '',
|
||||||
|
switchable: false,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
|
||||||
|
var html = `<div class='btn-group float-right' role='group'>`;
|
||||||
|
|
||||||
|
html += makeIconButton('fa-trash-alt icon-red', 'button-related-delete', row.pk, '{% trans "Delete part relationship" %}');
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
$(table).inventreeTable({
|
||||||
|
url: '{% url "api-part-related-list" %}',
|
||||||
|
groupBy: false,
|
||||||
|
name: 'related',
|
||||||
|
original: options.params,
|
||||||
|
queryParams: filters,
|
||||||
|
columns: columns,
|
||||||
|
showColumns: false,
|
||||||
|
search: true,
|
||||||
|
onPostBody: function() {
|
||||||
|
$(table).find('.button-related-delete').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
constructForm(`/api/part/related/${pk}/`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
title: '{% trans "Delete Part Relationship" %}',
|
||||||
|
onSuccess: function() {
|
||||||
|
$(table).bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadParametricPartTable(table, options={}) {
|
function loadParametricPartTable(table, options={}) {
|
||||||
/* Load parametric table for part parameters
|
/* Load parametric table for part parameters
|
||||||
*
|
*
|
||||||
@@ -836,6 +928,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
* query: extra query params for API request
|
* query: extra query params for API request
|
||||||
* buttons: If provided, link buttons to selection status of this table
|
* buttons: If provided, link buttons to selection status of this table
|
||||||
* disableFilters: If true, disable custom filters
|
* disableFilters: If true, disable custom filters
|
||||||
|
* actions: Provide a callback function to construct an "actions" column
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Ensure category detail is included
|
// Ensure category detail is included
|
||||||
@@ -878,7 +971,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
|
|
||||||
col = {
|
col = {
|
||||||
field: 'IPN',
|
field: 'IPN',
|
||||||
title: 'IPN',
|
title: '{% trans "IPN" %}',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!options.params.ordering) {
|
if (!options.params.ordering) {
|
||||||
@@ -895,7 +988,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
|
|
||||||
var name = row.full_name;
|
var name = row.full_name;
|
||||||
|
|
||||||
var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/');
|
var display = imageHoverIcon(row.thumbnail) + renderLink(name, `/part/${row.pk}/`);
|
||||||
|
|
||||||
display += makePartIcons(row);
|
display += makePartIcons(row);
|
||||||
|
|
||||||
@@ -993,6 +1086,21 @@ function loadPartTable(table, url, options={}) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Push an "actions" column
|
||||||
|
if (options.actions) {
|
||||||
|
columns.push({
|
||||||
|
field: 'actions',
|
||||||
|
title: '',
|
||||||
|
switchable: false,
|
||||||
|
visible: true,
|
||||||
|
searchable: false,
|
||||||
|
sortable: false,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
return options.actions(value, row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var grid_view = options.gridView && inventreeLoad('part-grid-view') == 1;
|
var grid_view = options.gridView && inventreeLoad('part-grid-view') == 1;
|
||||||
|
|
||||||
$(table).inventreeTable({
|
$(table).inventreeTable({
|
||||||
@@ -1020,6 +1128,10 @@ function loadPartTable(table, url, options={}) {
|
|||||||
$('#view-part-grid').removeClass('btn-secondary').addClass('btn-outline-secondary');
|
$('#view-part-grid').removeClass('btn-secondary').addClass('btn-outline-secondary');
|
||||||
$('#view-part-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
|
$('#view-part-list').removeClass('btn-outline-secondary').addClass('btn-secondary');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.onPostBody) {
|
||||||
|
options.onPostBody();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
buttons: options.gridView ? [
|
buttons: options.gridView ? [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1101,7 +1101,7 @@ function loadStockTable(table, options) {
|
|||||||
|
|
||||||
col = {
|
col = {
|
||||||
field: 'part_detail.IPN',
|
field: 'part_detail.IPN',
|
||||||
title: 'IPN',
|
title: '{% trans "IPN" %}',
|
||||||
sortName: 'part__IPN',
|
sortName: 'part__IPN',
|
||||||
visible: params['part_detail'],
|
visible: params['part_detail'],
|
||||||
switchable: params['part_detail'],
|
switchable: params['part_detail'],
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filters for the "related parts" table
|
||||||
|
if (tableKey == 'related') {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Filters for the "used in" table
|
// Filters for the "used in" table
|
||||||
if (tableKey == 'usedin') {
|
if (tableKey == 'usedin') {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<span title='{% trans text %}' class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate bg-light" data-bs-parent="#sidebar">
|
<span title='{{ text }}' class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate bg-light" data-bs-parent="#sidebar">
|
||||||
<h6>
|
<h6>
|
||||||
<i class="bi bi-bootstrap"></i>
|
<i class="bi bi-bootstrap"></i>
|
||||||
{% if icon %}<span class='sidebar-item-icon fas {{ icon }}'></span>{% endif %}
|
{% if icon %}<span class='sidebar-item-icon fas {{ icon }}'></span>{% endif %}
|
||||||
{% if text %}<span class='sidebar-item-text' style='display: none;'>{% trans text %}</span>{% endif %}
|
{% if text %}<span class='sidebar-item-text' style='display: none;'>{{ text }}</span>{% endif %}
|
||||||
</h6>
|
</h6>
|
||||||
</span>
|
</span>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<a href="#" id='select-{{ label }}' title='{% trans text %}' class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate sidebar-selector" data-bs-parent="#sidebar">
|
<a href="#" id='select-{{ label }}' title='{{ text }}' class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate sidebar-selector" data-bs-parent="#sidebar">
|
||||||
<i class="bi bi-bootstrap"></i>
|
<i class="bi bi-bootstrap"></i>
|
||||||
<span class='sidebar-item-icon fas {{ icon }}'></span>
|
<span class='sidebar-item-icon fas {{ icon|default:"fa-circle" }}'></span>
|
||||||
<span class='sidebar-item-text' style='display: none;'>{% trans text %}</span>
|
<span class='sidebar-item-text' style='display: none;'>{{ text }}</span>
|
||||||
{% if badge %}
|
{% if badge %}
|
||||||
<span id='sidebar-badge-{{ label }}' class='sidebar-item-badge badge rounded-pill badge-right bg-dark'>
|
<span id='sidebar-badge-{{ label }}' class='sidebar-item-badge badge rounded-pill badge-right bg-dark'>
|
||||||
<span class='fas fa-spin fa-spinner'></span>
|
<span class='fas fa-spin fa-spinner'></span>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<a href="{{ url }}" class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate" data-bs-parent="#sidebar">
|
<a href="{{ url }}" class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate" data-bs-parent="#sidebar">
|
||||||
<i class="bi bi-bootstrap"></i><span class='sidebar-item-icon fas {{ icon }}'></span><span class='sidebar-item-text' style='display: none;'>{% trans text %}</span>
|
<i class="bi bi-bootstrap"></i><span class='sidebar-item-icon fas {{ icon }}'></span><span class='sidebar-item-text' style='display: none;'>{{ text }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://coveralls.io/github/inventree/InvenTree)
|
[](https://coveralls.io/github/inventree/InvenTree)
|
||||||
|
[](https://crowdin.com/project/inventree)
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|||||||
@@ -28,6 +28,7 @@ print("=================================")
|
|||||||
print("Checking static javascript files:")
|
print("Checking static javascript files:")
|
||||||
print("=================================")
|
print("=================================")
|
||||||
|
|
||||||
|
|
||||||
def check_invalid_tag(data):
|
def check_invalid_tag(data):
|
||||||
|
|
||||||
pattern = r"{%(\w+)"
|
pattern = r"{%(\w+)"
|
||||||
@@ -45,6 +46,7 @@ def check_invalid_tag(data):
|
|||||||
|
|
||||||
return err_count
|
return err_count
|
||||||
|
|
||||||
|
|
||||||
def check_prohibited_tags(data):
|
def check_prohibited_tags(data):
|
||||||
|
|
||||||
allowed_tags = [
|
allowed_tags = [
|
||||||
@@ -78,7 +80,7 @@ def check_prohibited_tags(data):
|
|||||||
has_trans = True
|
has_trans = True
|
||||||
|
|
||||||
if not has_trans:
|
if not has_trans:
|
||||||
print(f" > file is missing 'trans' tags")
|
print(" > file is missing 'trans' tags")
|
||||||
err_count += 1
|
err_count += 1
|
||||||
|
|
||||||
return err_count
|
return err_count
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ for line in str(out.decode()).split('\n'):
|
|||||||
if len(locales) > 0:
|
if len(locales) > 0:
|
||||||
print("There are {n} unstaged locale files:".format(n=len(locales)))
|
print("There are {n} unstaged locale files:".format(n=len(locales)))
|
||||||
|
|
||||||
for l in locales:
|
for lang in locales:
|
||||||
print(" - {l}".format(l=l))
|
print(" - {l}".format(l=lang))
|
||||||
|
|
||||||
sys.exit(len(locales))
|
sys.exit(len(locales))
|
||||||
@@ -9,7 +9,6 @@ import sys
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import requests
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
@@ -65,7 +64,7 @@ if __name__ == '__main__':
|
|||||||
e.g. "0.5 dev"
|
e.g. "0.5 dev"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print(f"Checking development branch")
|
print("Checking development branch")
|
||||||
|
|
||||||
pattern = "^\d+(\.\d+)+ dev$"
|
pattern = "^\d+(\.\d+)+ dev$"
|
||||||
|
|
||||||
@@ -81,7 +80,7 @@ if __name__ == '__main__':
|
|||||||
e.g. "0.5.1"
|
e.g. "0.5.1"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print(f"Checking release branch")
|
print("Checking release branch")
|
||||||
|
|
||||||
pattern = "^\d+(\.\d+)+$"
|
pattern = "^\d+(\.\d+)+$"
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,22 @@
|
|||||||
[flake8]
|
[flake8]
|
||||||
ignore =
|
ignore =
|
||||||
# - W293 - blank lines contain whitespace
|
# - W605 - invalid escape sequence
|
||||||
W293,
|
|
||||||
W605,
|
W605,
|
||||||
# - E501 - line too long (82 characters)
|
# - E501 - line too long (82 characters)
|
||||||
E501, E722,
|
E501,
|
||||||
|
# - E722 - do not use bare except
|
||||||
|
E722,
|
||||||
# - C901 - function is too complex
|
# - C901 - function is too complex
|
||||||
C901,
|
C901,
|
||||||
# - N802 - function name should be lowercase (In the future, we should conform to this!)
|
# - N802 - function name should be lowercase
|
||||||
|
# TODO (In the future, we should conform to this!)
|
||||||
N802,
|
N802,
|
||||||
# - N806 - variable should be lowercase
|
# - N806 - variable should be lowercase
|
||||||
N806,
|
N806,
|
||||||
|
# - N812 - lowercase imported as non-lowercase
|
||||||
N812,
|
N812,
|
||||||
exclude = .git,__pycache__,*/migrations/*,*/lib/*,*/bin/*,*/media/*,*/static/*,*ci_*.py*
|
exclude = .git,__pycache__,*/migrations/*,*/lib/*,*/bin/*,*/media/*,*/static/*
|
||||||
max-complexity = 20
|
max-complexity = 20
|
||||||
|
|
||||||
[coverage:run]
|
[coverage:run]
|
||||||
source = ./InvenTree
|
source = ./InvenTree
|
||||||
omit =
|
|
||||||
InvenTree/manage.py
|
|
||||||
InvenTree/setup.py
|
|
||||||
InvenTree/InvenTree/middleware.py
|
|
||||||
InvenTree/InvenTree/utils.py
|
|
||||||
InvenTree/InvenTree/wsgi.py
|
|
||||||
InvenTree/users/apps.py
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from shutil import copyfile
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from invoke import ctask as task
|
from invoke import ctask as task
|
||||||
@@ -134,6 +135,7 @@ def rebuild_models(c):
|
|||||||
|
|
||||||
manage(c, "rebuild_models", pty=True)
|
manage(c, "rebuild_models", pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def rebuild_thumbnails(c):
|
def rebuild_thumbnails(c):
|
||||||
"""
|
"""
|
||||||
@@ -142,6 +144,7 @@ def rebuild_thumbnails(c):
|
|||||||
|
|
||||||
manage(c, "rebuild_thumbnails", pty=True)
|
manage(c, "rebuild_thumbnails", pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def clean_settings(c):
|
def clean_settings(c):
|
||||||
"""
|
"""
|
||||||
@@ -150,6 +153,7 @@ def clean_settings(c):
|
|||||||
|
|
||||||
manage(c, "clean_settings")
|
manage(c, "clean_settings")
|
||||||
|
|
||||||
|
|
||||||
@task(post=[rebuild_models, rebuild_thumbnails])
|
@task(post=[rebuild_models, rebuild_thumbnails])
|
||||||
def migrate(c):
|
def migrate(c):
|
||||||
"""
|
"""
|
||||||
@@ -467,6 +471,75 @@ def server(c, address="127.0.0.1:8000"):
|
|||||||
manage(c, "runserver {address}".format(address=address), pty=True)
|
manage(c, "runserver {address}".format(address=address), pty=True)
|
||||||
|
|
||||||
|
|
||||||
|
@task(post=[translate_stats, static, server])
|
||||||
|
def test_translations(c):
|
||||||
|
"""
|
||||||
|
Add a fictional language to test if each component is ready for translations
|
||||||
|
"""
|
||||||
|
import django
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# setup django
|
||||||
|
base_path = os.getcwd()
|
||||||
|
new_base_path = pathlib.Path('InvenTree').absolute()
|
||||||
|
sys.path.append(str(new_base_path))
|
||||||
|
os.chdir(new_base_path)
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'InvenTree.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
# Add language
|
||||||
|
print("Add dummy language...")
|
||||||
|
print("========================================")
|
||||||
|
manage(c, "makemessages -e py,html,js --no-wrap -l xx")
|
||||||
|
|
||||||
|
# change translation
|
||||||
|
print("Fill in dummy translations...")
|
||||||
|
print("========================================")
|
||||||
|
|
||||||
|
file_path = pathlib.Path(settings.LOCALE_PATHS[0], 'xx', 'LC_MESSAGES', 'django.po')
|
||||||
|
new_file_path = str(file_path) + '_new'
|
||||||
|
|
||||||
|
# complie regex
|
||||||
|
reg = re.compile(
|
||||||
|
r"[a-zA-Z0-9]{1}"+ # match any single letter and number
|
||||||
|
r"(?![^{\(\<]*[}\)\>])"+ # that is not inside curly brackets, brackets or a tag
|
||||||
|
r"(?<![^\%][^\(][)][a-z])"+ # that is not a specially formatted variable with singles
|
||||||
|
r"(?![^\\][\n])" # that is not a newline
|
||||||
|
)
|
||||||
|
last_string = ''
|
||||||
|
|
||||||
|
# loop through input file lines
|
||||||
|
with open(file_path, "rt") as file_org:
|
||||||
|
with open(new_file_path, "wt") as file_new:
|
||||||
|
for line in file_org:
|
||||||
|
if line.startswith('msgstr "'):
|
||||||
|
# write output -> replace regex matches with x in the read in (multi)string
|
||||||
|
file_new.write(f'msgstr "{reg.sub("x", last_string[7:-2])}"\n')
|
||||||
|
last_string = "" # reset (multi)string
|
||||||
|
elif line.startswith('msgid "'):
|
||||||
|
last_string = last_string + line # a new translatable string starts -> start append
|
||||||
|
file_new.write(line)
|
||||||
|
else:
|
||||||
|
if last_string:
|
||||||
|
last_string = last_string + line # a string is beeing read in -> continue appending
|
||||||
|
file_new.write(line)
|
||||||
|
|
||||||
|
# change out translation files
|
||||||
|
os.rename(file_path, str(file_path) + '_old')
|
||||||
|
os.rename(new_file_path, file_path)
|
||||||
|
|
||||||
|
# compile languages
|
||||||
|
print("Compile languages ...")
|
||||||
|
print("========================================")
|
||||||
|
manage(c, "compilemessages")
|
||||||
|
|
||||||
|
# reset cwd
|
||||||
|
os.chdir(base_path)
|
||||||
|
|
||||||
|
# set env flag
|
||||||
|
os.environ['TEST_TRANSLATIONS'] = 'True'
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task
|
||||||
def render_js_files(c):
|
def render_js_files(c):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user