mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' into partial-shipment
# Conflicts: # InvenTree/build/serializers.py # InvenTree/order/templates/order/so_sidebar.html
This commit is contained in:
commit
68e2b0850b
37
.github/workflows/check_translations.yaml
vendored
Normal file
37
.github/workflows/check_translations.yaml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: Check Translations
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- l10
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- l10
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
INVENTREE_DB_NAME: './test_db.sqlite'
|
||||||
|
INVENTREE_DB_ENGINE: django.db.backends.sqlite3
|
||||||
|
INVENTREE_DEBUG: info
|
||||||
|
INVENTREE_MEDIA_ROOT: ./media
|
||||||
|
INVENTREE_STATIC_ROOT: ./static
|
||||||
|
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install gettext
|
||||||
|
pip3 install invoke
|
||||||
|
invoke install
|
||||||
|
- name: Test Translations
|
||||||
|
run: invoke translate
|
||||||
|
- name: Check Migration Files
|
||||||
|
run: python3 ci/check_migration_files.py
|
@ -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.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/el/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/el/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/es_MX/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/es_MX/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
8616
InvenTree/locale/es_MX/LC_MESSAGES/django.po
Normal file
8616
InvenTree/locale/es_MX/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/he/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/he/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/id/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/id/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/ko/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/ko/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/nl/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/nl/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/no/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/no/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/sv/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/sv/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/th/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/th/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
InvenTree/locale/vi/LC_MESSAGES/django.mo
Normal file
BIN
InvenTree/locale/vi/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
@ -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 }},
|
||||||
},
|
},
|
||||||
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');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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)),
|
||||||
|
|
||||||
|
@ -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 @@
|
|||||||
|
|
||||||
[![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)
|
||||||
|
@ -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+)+$"
|
||||||
|
|
||||||
|
20
setup.cfg
20
setup.cfg
@ -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
|
|
||||||
|
75
tasks.py
75
tasks.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):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user