Merge branch 'master' into partial-shipment

# Conflicts:
#	InvenTree/build/serializers.py
#	InvenTree/order/templates/order/so_sidebar.html
This commit is contained in:
Oliver 2021-11-26 08:25:51 +11:00
commit 68e2b0850b
129 changed files with 66929 additions and 54543 deletions

View 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

View File

@ -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

View File

@ -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',

View File

@ -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;

View File

@ -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" %}

View File

@ -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"""

View File

@ -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):
""" """

View File

@ -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" %}

View File

@ -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" %}

View File

@ -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" %}

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

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View File

@ -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 %}

View File

@ -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" %}

View File

@ -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" %}

View File

@ -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

View File

@ -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'),

View File

@ -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:

View File

@ -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 """

View File

@ -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

View File

@ -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 """

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 }},
}, },
reload: true, part_2: {
label: '{% trans "Related Part" %}',
filters: {
exclude_related: {{ part.pk }},
}
}
},
title: '{% trans "Add Related Part" %}',
onSuccess: function() {
$('#related-parts-table').bootstrapTable('refresh');
}
}); });
}); });

View File

@ -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 %}

View File

@ -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" %}

View File

@ -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):
""" """

View File

@ -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 """

View File

@ -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)),

View File

@ -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 """

View File

@ -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" %}

View File

@ -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" %}

View File

@ -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',
}, },
{ {

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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: {

View File

@ -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;
} }

View File

@ -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 ? [
{ {

View File

@ -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'],

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -5,6 +5,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree) [![Coverage Status](https://coveralls.io/repos/github/inventree/InvenTree/badge.svg)](https://coveralls.io/github/inventree/InvenTree)
[![Crowdin](https://badges.crowdin.net/inventree/localized.svg)](https://crowdin.com/project/inventree)
![PEP](https://github.com/inventree/inventree/actions/workflows/style.yaml/badge.svg) ![PEP](https://github.com/inventree/inventree/actions/workflows/style.yaml/badge.svg)
![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg) ![SQLite](https://github.com/inventree/inventree/actions/workflows/coverage.yaml/badge.svg)
![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg) ![MySQL](https://github.com/inventree/inventree/actions/workflows/mysql.yaml/badge.svg)

View File

@ -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

View File

@ -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))

View File

@ -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+)+$"

View File

@ -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

View File

@ -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):
""" """