mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-28 11:59:23 +00:00
Merge branch 'master' of github.com:inventree/InvenTree into exchange_rate_task
This commit is contained in:
@@ -13,6 +13,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- name: Check Release tag
|
||||||
|
run: |
|
||||||
|
python3 ci/check_version_number.py ${{ github.event.release.tag_name }}
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
|||||||
@@ -68,3 +68,6 @@ htmlcov/
|
|||||||
|
|
||||||
# Development files
|
# Development files
|
||||||
dev/
|
dev/
|
||||||
|
|
||||||
|
# Locale stats file
|
||||||
|
locale_stats.json
|
||||||
|
|||||||
@@ -344,13 +344,15 @@ def GetExportFormats():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def DownloadFile(data, filename, content_type='application/text'):
|
def DownloadFile(data, filename, content_type='application/text', inline=False):
|
||||||
""" Create a dynamic file for the user to download.
|
"""
|
||||||
|
Create a dynamic file for the user to download.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: Raw file data (string or bytes)
|
data: Raw file data (string or bytes)
|
||||||
filename: Filename for the file download
|
filename: Filename for the file download
|
||||||
content_type: Content type for the download
|
content_type: Content type for the download
|
||||||
|
inline: Download "inline" or as attachment? (Default = attachment)
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
A StreamingHttpResponse object wrapping the supplied data
|
A StreamingHttpResponse object wrapping the supplied data
|
||||||
@@ -365,7 +367,10 @@ def DownloadFile(data, filename, content_type='application/text'):
|
|||||||
|
|
||||||
response = StreamingHttpResponse(wrapper, content_type=content_type)
|
response = StreamingHttpResponse(wrapper, content_type=content_type)
|
||||||
response['Content-Length'] = len(data)
|
response['Content-Length'] = len(data)
|
||||||
response['Content-Disposition'] = 'attachment; filename={f}'.format(f=filename)
|
|
||||||
|
disposition = "inline" if inline else "attachment"
|
||||||
|
|
||||||
|
response['Content-Disposition'] = f'{disposition}; filename={filename}'
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -85,8 +85,10 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, instance=None, data=empty, **kwargs):
|
def __init__(self, instance=None, data=empty, **kwargs):
|
||||||
|
"""
|
||||||
# self.instance = instance
|
Custom __init__ routine to ensure that *default* values (as specified in the ORM)
|
||||||
|
are used by the DRF serializers, *if* the values are not provided by the user.
|
||||||
|
"""
|
||||||
|
|
||||||
# If instance is None, we are creating a new instance
|
# If instance is None, we are creating a new instance
|
||||||
if instance is None and data is not empty:
|
if instance is None and data is not empty:
|
||||||
@@ -193,7 +195,15 @@ class InvenTreeModelSerializer(serializers.ModelSerializer):
|
|||||||
try:
|
try:
|
||||||
instance.full_clean()
|
instance.full_clean()
|
||||||
except (ValidationError, DjangoValidationError) as exc:
|
except (ValidationError, DjangoValidationError) as exc:
|
||||||
raise ValidationError(detail=serializers.as_serializer_error(exc))
|
|
||||||
|
data = exc.message_dict
|
||||||
|
|
||||||
|
# Change '__all__' key (django style) to 'non_field_errors' (DRF style)
|
||||||
|
if '__all__' in data:
|
||||||
|
data['non_field_errors'] = data['__all__']
|
||||||
|
del data['__all__']
|
||||||
|
|
||||||
|
raise ValidationError(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ import common.models
|
|||||||
|
|
||||||
INVENTREE_SW_VERSION = "0.5.0 pre"
|
INVENTREE_SW_VERSION = "0.5.0 pre"
|
||||||
|
|
||||||
INVENTREE_API_VERSION = 8
|
INVENTREE_API_VERSION = 9
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v9 -> 2021-08-09
|
||||||
|
- Adds "price_string" to part pricing serializers
|
||||||
|
|
||||||
v8 -> 2021-07-19
|
v8 -> 2021-07-19
|
||||||
- Refactors the API interface for SupplierPart and ManufacturerPart models
|
- Refactors the API interface for SupplierPart and ManufacturerPart models
|
||||||
- ManufacturerPart objects can no longer be created via the SupplierPart API endpoint
|
- ManufacturerPart objects can no longer be created via the SupplierPart API endpoint
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ as JSON objects and passing them to modal forms (using jQuery / bootstrap).
|
|||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
|
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
|
||||||
@@ -800,6 +803,13 @@ class SettingsView(TemplateView):
|
|||||||
except:
|
except:
|
||||||
ctx["rates_updated"] = None
|
ctx["rates_updated"] = None
|
||||||
|
|
||||||
|
# load locale stats
|
||||||
|
STAT_FILE = os.path.abspath(os.path.join(settings.BASE_DIR, 'InvenTree/locale_stats.json'))
|
||||||
|
try:
|
||||||
|
ctx["locale_stats"] = json.load(open(STAT_FILE, 'r'))
|
||||||
|
except:
|
||||||
|
ctx["locale_stats"] = {}
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -637,7 +637,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'PART_PURCHASEABLE': {
|
'PART_PURCHASEABLE': {
|
||||||
'name': _('Purchaseable'),
|
'name': _('Purchaseable'),
|
||||||
'description': _('Parts are purchaseable by default'),
|
'description': _('Parts are purchaseable by default'),
|
||||||
'default': False,
|
'default': True,
|
||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -662,6 +662,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
# TODO: Remove this setting in future, new API forms make this not useful
|
||||||
'PART_SHOW_QUANTITY_IN_FORMS': {
|
'PART_SHOW_QUANTITY_IN_FORMS': {
|
||||||
'name': _('Show Quantity in Forms'),
|
'name': _('Show Quantity in Forms'),
|
||||||
'description': _('Display available part quantity in some forms'),
|
'description': _('Display available part quantity in some forms'),
|
||||||
@@ -925,6 +926,20 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"LABEL_INLINE": {
|
||||||
|
'name': _('Inline label display'),
|
||||||
|
'description': _('Display PDF labels in the browser, instead of downloading as a file'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
"REPORT_INLINE": {
|
||||||
|
'name': _('Inline report display'),
|
||||||
|
'description': _('Display PDF reports in the browser, instead of downloading as a file'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
'SEARCH_PREVIEW_RESULTS': {
|
'SEARCH_PREVIEW_RESULTS': {
|
||||||
'name': _('Search Preview Results'),
|
'name': _('Search Preview Results'),
|
||||||
'description': _('Number of results to show in search preview window'),
|
'description': _('Number of results to show in search preview window'),
|
||||||
@@ -964,7 +979,10 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_filters(cls, key, **kwargs):
|
def get_filters(cls, key, **kwargs):
|
||||||
return {'key__iexact': key, 'user__id': kwargs['user'].id}
|
return {
|
||||||
|
'key__iexact': key,
|
||||||
|
'user__id': kwargs['user'].id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PriceBreak(models.Model):
|
class PriceBreak(models.Model):
|
||||||
|
|||||||
@@ -109,10 +109,13 @@ class LabelPrintMixin:
|
|||||||
else:
|
else:
|
||||||
pdf = outputs[0].get_document().write_pdf()
|
pdf = outputs[0].get_document().write_pdf()
|
||||||
|
|
||||||
|
inline = common.models.InvenTreeUserSetting.get_setting('LABEL_INLINE', user=request.user)
|
||||||
|
|
||||||
return InvenTree.helpers.DownloadFile(
|
return InvenTree.helpers.DownloadFile(
|
||||||
pdf,
|
pdf,
|
||||||
label_name,
|
label_name,
|
||||||
content_type='application/pdf'
|
content_type='application/pdf',
|
||||||
|
inline=inline
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1860
-1702
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1811
-1653
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1872
-1714
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1939
-1781
File diff suppressed because it is too large
Load Diff
+1892
-1734
File diff suppressed because it is too large
Load Diff
+1827
-1669
File diff suppressed because it is too large
Load Diff
+1841
-1683
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1959
-1801
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+62
-1
@@ -23,6 +23,7 @@ from djmoney.money import Money
|
|||||||
from djmoney.contrib.exchange.models import convert_money
|
from djmoney.contrib.exchange.models import convert_money
|
||||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from .models import Part, PartCategory, BomItem
|
from .models import Part, PartCategory, BomItem
|
||||||
from .models import PartParameter, PartParameterTemplate
|
from .models import PartParameter, PartParameterTemplate
|
||||||
@@ -30,6 +31,7 @@ from .models import PartAttachment, PartTestTemplate
|
|||||||
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||||
from .models import PartCategoryParameterTemplate
|
from .models import PartCategoryParameterTemplate
|
||||||
|
|
||||||
|
from stock.models import StockItem
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from build.models import Build
|
from build.models import Build
|
||||||
|
|
||||||
@@ -628,17 +630,76 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
else:
|
else:
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def create(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
We wish to save the user who created this part!
|
We wish to save the user who created this part!
|
||||||
|
|
||||||
Note: Implementation copied from DRF class CreateModelMixin
|
Note: Implementation copied from DRF class CreateModelMixin
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
part = serializer.save()
|
part = serializer.save()
|
||||||
part.creation_user = self.request.user
|
part.creation_user = self.request.user
|
||||||
|
|
||||||
|
# Optionally copy templates from category or parent category
|
||||||
|
copy_templates = {
|
||||||
|
'main': str2bool(request.data.get('copy_category_templates', False)),
|
||||||
|
'parent': str2bool(request.data.get('copy_parent_templates', False))
|
||||||
|
}
|
||||||
|
|
||||||
|
part.save(**{'add_category_templates': copy_templates})
|
||||||
|
|
||||||
|
# Optionally copy data from another part (e.g. when duplicating)
|
||||||
|
copy_from = request.data.get('copy_from', None)
|
||||||
|
|
||||||
|
if copy_from is not None:
|
||||||
|
|
||||||
|
try:
|
||||||
|
original = Part.objects.get(pk=copy_from)
|
||||||
|
|
||||||
|
copy_bom = str2bool(request.data.get('copy_bom', False))
|
||||||
|
copy_parameters = str2bool(request.data.get('copy_parameters', False))
|
||||||
|
copy_image = str2bool(request.data.get('copy_image', True))
|
||||||
|
|
||||||
|
# Copy image?
|
||||||
|
if copy_image:
|
||||||
|
part.image = original.image
|
||||||
part.save()
|
part.save()
|
||||||
|
|
||||||
|
# Copy BOM?
|
||||||
|
if copy_bom:
|
||||||
|
part.copy_bom_from(original)
|
||||||
|
|
||||||
|
# Copy parameter data?
|
||||||
|
if copy_parameters:
|
||||||
|
part.copy_parameters_from(original)
|
||||||
|
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Optionally create initial stock item
|
||||||
|
try:
|
||||||
|
initial_stock = Decimal(request.data.get('initial_stock', 0))
|
||||||
|
|
||||||
|
if initial_stock > 0 and part.default_location is not None:
|
||||||
|
|
||||||
|
stock_item = StockItem(
|
||||||
|
part=part,
|
||||||
|
quantity=initial_stock,
|
||||||
|
location=part.default_location,
|
||||||
|
)
|
||||||
|
|
||||||
|
stock_item.save(user=request.user)
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
headers = self.get_success_headers(serializer.data)
|
||||||
|
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
def get_queryset(self, *args, **kwargs):
|
def get_queryset(self, *args, **kwargs):
|
||||||
|
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|||||||
@@ -177,82 +177,6 @@ class SetPartCategoryForm(forms.Form):
|
|||||||
part_category = TreeNodeChoiceField(queryset=PartCategory.objects.all(), required=True, help_text=_('Select part category'))
|
part_category = TreeNodeChoiceField(queryset=PartCategory.objects.all(), required=True, help_text=_('Select part category'))
|
||||||
|
|
||||||
|
|
||||||
class EditPartForm(HelperForm):
|
|
||||||
"""
|
|
||||||
Form for editing a Part object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
field_prefix = {
|
|
||||||
'keywords': 'fa-key',
|
|
||||||
'link': 'fa-link',
|
|
||||||
'IPN': 'fa-hashtag',
|
|
||||||
'default_expiry': 'fa-stopwatch',
|
|
||||||
}
|
|
||||||
|
|
||||||
bom_copy = forms.BooleanField(required=False,
|
|
||||||
initial=True,
|
|
||||||
help_text=_("Duplicate all BOM data for this part"),
|
|
||||||
label=_('Copy BOM'),
|
|
||||||
widget=forms.HiddenInput())
|
|
||||||
|
|
||||||
parameters_copy = forms.BooleanField(required=False,
|
|
||||||
initial=True,
|
|
||||||
help_text=_("Duplicate all parameter data for this part"),
|
|
||||||
label=_('Copy Parameters'),
|
|
||||||
widget=forms.HiddenInput())
|
|
||||||
|
|
||||||
confirm_creation = forms.BooleanField(required=False,
|
|
||||||
initial=False,
|
|
||||||
help_text=_('Confirm part creation'),
|
|
||||||
widget=forms.HiddenInput())
|
|
||||||
|
|
||||||
selected_category_templates = forms.BooleanField(required=False,
|
|
||||||
initial=False,
|
|
||||||
label=_('Include category parameter templates'),
|
|
||||||
widget=forms.HiddenInput())
|
|
||||||
|
|
||||||
parent_category_templates = forms.BooleanField(required=False,
|
|
||||||
initial=False,
|
|
||||||
label=_('Include parent categories parameter templates'),
|
|
||||||
widget=forms.HiddenInput())
|
|
||||||
|
|
||||||
initial_stock = forms.IntegerField(required=False,
|
|
||||||
initial=0,
|
|
||||||
label=_('Initial stock amount'),
|
|
||||||
help_text=_('Create stock for this part'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Part
|
|
||||||
fields = [
|
|
||||||
'confirm_creation',
|
|
||||||
'category',
|
|
||||||
'selected_category_templates',
|
|
||||||
'parent_category_templates',
|
|
||||||
'name',
|
|
||||||
'IPN',
|
|
||||||
'description',
|
|
||||||
'revision',
|
|
||||||
'bom_copy',
|
|
||||||
'parameters_copy',
|
|
||||||
'keywords',
|
|
||||||
'variant_of',
|
|
||||||
'link',
|
|
||||||
'default_location',
|
|
||||||
'default_supplier',
|
|
||||||
'default_expiry',
|
|
||||||
'units',
|
|
||||||
'minimum_stock',
|
|
||||||
'initial_stock',
|
|
||||||
'component',
|
|
||||||
'assembly',
|
|
||||||
'is_template',
|
|
||||||
'trackable',
|
|
||||||
'purchaseable',
|
|
||||||
'salable',
|
|
||||||
'virtual',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class EditPartParameterTemplateForm(HelperForm):
|
class EditPartParameterTemplateForm(HelperForm):
|
||||||
""" Form for editing a PartParameterTemplate object """
|
""" Form for editing a PartParameterTemplate object """
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.2.4 on 2021-08-07 11:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import part.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('part', '0070_alter_part_variant_of'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partparametertemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Parameter Name', max_length=100, unique=True, validators=[part.models.validate_template_name], verbose_name='Name'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+30
-74
@@ -34,7 +34,6 @@ from stdimage.models import StdImageField
|
|||||||
|
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from rapidfuzz import fuzz
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from InvenTree import helpers
|
from InvenTree import helpers
|
||||||
@@ -235,57 +234,6 @@ def rename_part_image(instance, filename):
|
|||||||
return os.path.join(base, fname)
|
return os.path.join(base, fname)
|
||||||
|
|
||||||
|
|
||||||
def match_part_names(match, threshold=80, reverse=True, compare_length=False):
|
|
||||||
""" Return a list of parts whose name matches the search term using fuzzy search.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
match: Term to match against
|
|
||||||
threshold: Match percentage that must be exceeded (default = 65)
|
|
||||||
reverse: Ordering for search results (default = True - highest match is first)
|
|
||||||
compare_length: Include string length checks
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A sorted dict where each element contains the following key:value pairs:
|
|
||||||
- 'part' : The matched part
|
|
||||||
- 'ratio' : The matched ratio
|
|
||||||
"""
|
|
||||||
|
|
||||||
match = str(match).strip().lower()
|
|
||||||
|
|
||||||
if len(match) == 0:
|
|
||||||
return []
|
|
||||||
|
|
||||||
parts = Part.objects.all()
|
|
||||||
|
|
||||||
matches = []
|
|
||||||
|
|
||||||
for part in parts:
|
|
||||||
compare = str(part.name).strip().lower()
|
|
||||||
|
|
||||||
if len(compare) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
ratio = fuzz.partial_token_sort_ratio(compare, match)
|
|
||||||
|
|
||||||
if compare_length:
|
|
||||||
# Also employ primitive length comparison
|
|
||||||
# TODO - Improve this somewhat...
|
|
||||||
l_min = min(len(match), len(compare))
|
|
||||||
l_max = max(len(match), len(compare))
|
|
||||||
|
|
||||||
ratio *= (l_min / l_max)
|
|
||||||
|
|
||||||
if ratio >= threshold:
|
|
||||||
matches.append({
|
|
||||||
'part': part,
|
|
||||||
'ratio': round(ratio, 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
matches = sorted(matches, key=lambda item: item['ratio'], reverse=reverse)
|
|
||||||
|
|
||||||
return matches
|
|
||||||
|
|
||||||
|
|
||||||
class PartManager(TreeManager):
|
class PartManager(TreeManager):
|
||||||
"""
|
"""
|
||||||
Defines a custom object manager for the Part model.
|
Defines a custom object manager for the Part model.
|
||||||
@@ -409,7 +357,7 @@ class Part(MPTTModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Get category templates settings
|
# Get category templates settings
|
||||||
add_category_templates = kwargs.pop('add_category_templates', None)
|
add_category_templates = kwargs.pop('add_category_templates', False)
|
||||||
|
|
||||||
if self.pk:
|
if self.pk:
|
||||||
previous = Part.objects.get(pk=self.pk)
|
previous = Part.objects.get(pk=self.pk)
|
||||||
@@ -437,36 +385,26 @@ class Part(MPTTModel):
|
|||||||
# Get part category
|
# Get part category
|
||||||
category = self.category
|
category = self.category
|
||||||
|
|
||||||
if category and add_category_templates:
|
if category is not None:
|
||||||
# Store templates added to part
|
|
||||||
template_list = []
|
template_list = []
|
||||||
|
|
||||||
# Create part parameters for selected category
|
parent_categories = category.get_ancestors(include_self=True)
|
||||||
category_templates = add_category_templates['main']
|
|
||||||
if category_templates:
|
|
||||||
for template in category.get_parameter_templates():
|
|
||||||
parameter = PartParameter.create(part=self,
|
|
||||||
template=template.parameter_template,
|
|
||||||
data=template.default_value,
|
|
||||||
save=True)
|
|
||||||
if parameter:
|
|
||||||
template_list.append(template.parameter_template)
|
|
||||||
|
|
||||||
# Create part parameters for parent category
|
|
||||||
category_templates = add_category_templates['parent']
|
|
||||||
if category_templates:
|
|
||||||
# Get parent categories
|
|
||||||
parent_categories = category.get_ancestors()
|
|
||||||
|
|
||||||
for category in parent_categories:
|
for category in parent_categories:
|
||||||
for template in category.get_parameter_templates():
|
for template in category.get_parameter_templates():
|
||||||
# Check that template wasn't already added
|
# Check that template wasn't already added
|
||||||
if template.parameter_template not in template_list:
|
if template.parameter_template not in template_list:
|
||||||
|
|
||||||
|
template_list.append(template.parameter_template)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
PartParameter.create(part=self,
|
PartParameter.create(
|
||||||
|
part=self,
|
||||||
template=template.parameter_template,
|
template=template.parameter_template,
|
||||||
data=template.default_value,
|
data=template.default_value,
|
||||||
save=True)
|
save=True
|
||||||
|
)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
# PartParameter already exists
|
# PartParameter already exists
|
||||||
pass
|
pass
|
||||||
@@ -2205,6 +2143,16 @@ class PartTestTemplate(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_template_name(name):
|
||||||
|
"""
|
||||||
|
Prevent illegal characters in "name" field for PartParameterTemplate
|
||||||
|
"""
|
||||||
|
|
||||||
|
for c in "!@#$%^&*()<>{}[].,?/\|~`_+-=\'\"":
|
||||||
|
if c in str(name):
|
||||||
|
raise ValidationError(_(f"Illegal character in template name ({c})"))
|
||||||
|
|
||||||
|
|
||||||
class PartParameterTemplate(models.Model):
|
class PartParameterTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
A PartParameterTemplate provides a template for key:value pairs for extra
|
A PartParameterTemplate provides a template for key:value pairs for extra
|
||||||
@@ -2243,7 +2191,15 @@ class PartParameterTemplate(models.Model):
|
|||||||
except PartParameterTemplate.DoesNotExist:
|
except PartParameterTemplate.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
name = models.CharField(max_length=100, verbose_name=_('Name'), help_text=_('Parameter Name'), unique=True)
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
verbose_name=_('Name'),
|
||||||
|
help_text=_('Parameter Name'),
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
validate_template_name,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
units = models.CharField(max_length=25, verbose_name=_('Units'), help_text=_('Parameter Units'), blank=True)
|
units = models.CharField(max_length=25, verbose_name=_('Units'), help_text=_('Parameter Units'), blank=True)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ from djmoney.contrib.django_rest_framework import MoneyField
|
|||||||
|
|
||||||
from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
|
from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
|
||||||
InvenTreeImageSerializerField,
|
InvenTreeImageSerializerField,
|
||||||
InvenTreeModelSerializer)
|
InvenTreeModelSerializer,
|
||||||
|
InvenTreeMoneySerializer)
|
||||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
@@ -102,7 +103,12 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
quantity = serializers.FloatField()
|
quantity = serializers.FloatField()
|
||||||
|
|
||||||
price = serializers.CharField()
|
price = InvenTreeMoneySerializer(
|
||||||
|
max_digits=19, decimal_places=4,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
price_string = serializers.CharField(source='price', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PartSellPriceBreak
|
model = PartSellPriceBreak
|
||||||
@@ -111,6 +117,7 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
|
|||||||
'part',
|
'part',
|
||||||
'quantity',
|
'quantity',
|
||||||
'price',
|
'price',
|
||||||
|
'price_string',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -121,7 +128,12 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
quantity = serializers.FloatField()
|
quantity = serializers.FloatField()
|
||||||
|
|
||||||
price = serializers.CharField()
|
price = InvenTreeMoneySerializer(
|
||||||
|
max_digits=19, decimal_places=4,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
price_string = serializers.CharField(source='price', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PartInternalPriceBreak
|
model = PartInternalPriceBreak
|
||||||
@@ -130,6 +142,7 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer):
|
|||||||
'part',
|
'part',
|
||||||
'quantity',
|
'quantity',
|
||||||
'price',
|
'price',
|
||||||
|
'price_string',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -264,25 +264,25 @@
|
|||||||
|
|
||||||
{% if roles.part.add %}
|
{% if roles.part.add %}
|
||||||
$("#part-create").click(function() {
|
$("#part-create").click(function() {
|
||||||
launchModalForm(
|
|
||||||
"{% url 'part-create' %}",
|
var fields = partFields({
|
||||||
{
|
create: true,
|
||||||
follow: true,
|
});
|
||||||
data: {
|
|
||||||
{% if category %}
|
{% if category %}
|
||||||
category: {{ category.id }}
|
fields.category.value = {{ category.pk }};
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
constructForm('{% url "api-part-list" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
fields: fields,
|
||||||
|
title: '{% trans "Create Part" %}',
|
||||||
|
onSuccess: function(data) {
|
||||||
|
// Follow the new part
|
||||||
|
location.href = `/part/${data.pk}/`;
|
||||||
},
|
},
|
||||||
secondary: [
|
});
|
||||||
{
|
|
||||||
field: 'default_location',
|
|
||||||
label: '{% trans "New Location" %}',
|
|
||||||
title: '{% trans "Create new Stock Location" %}',
|
|
||||||
url: "{% url 'stock-location-create' %}",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -525,10 +525,11 @@
|
|||||||
loadPartVariantTable($('#variants-table'), {{ part.pk }});
|
loadPartVariantTable($('#variants-table'), {{ part.pk }});
|
||||||
|
|
||||||
$('#new-variant').click(function() {
|
$('#new-variant').click(function() {
|
||||||
launchModalForm(
|
|
||||||
"{% url 'make-part-variant' part.id %}",
|
duplicatePart(
|
||||||
|
{{ part.pk}},
|
||||||
{
|
{
|
||||||
follow: true,
|
variant: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -486,12 +486,7 @@
|
|||||||
|
|
||||||
{% if roles.part.add %}
|
{% if roles.part.add %}
|
||||||
$("#part-duplicate").click(function() {
|
$("#part-duplicate").click(function() {
|
||||||
launchModalForm(
|
duplicatePart({{ part.pk }});
|
||||||
"{% url 'part-duplicate' part.id %}",
|
|
||||||
{
|
|
||||||
follow: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ def settings_value(key, *args, **kwargs):
|
|||||||
|
|
||||||
if 'user' in kwargs:
|
if 'user' in kwargs:
|
||||||
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
|
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
|
||||||
|
|
||||||
return InvenTreeSetting.get_setting(key)
|
return InvenTreeSetting.get_setting(key)
|
||||||
|
|
||||||
|
|
||||||
@@ -269,7 +270,7 @@ def keyvalue(dict, key):
|
|||||||
usage:
|
usage:
|
||||||
{% mydict|keyvalue:mykey %}
|
{% mydict|keyvalue:mykey %}
|
||||||
"""
|
"""
|
||||||
return dict[key]
|
return dict.get(key)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
|
|||||||
@@ -434,8 +434,8 @@ class PartAPITest(InvenTreeAPITestCase):
|
|||||||
self.assertTrue(data['active'])
|
self.assertTrue(data['active'])
|
||||||
self.assertFalse(data['virtual'])
|
self.assertFalse(data['virtual'])
|
||||||
|
|
||||||
# By default, parts are not purchaseable
|
# By default, parts are purchaseable
|
||||||
self.assertFalse(data['purchaseable'])
|
self.assertTrue(data['purchaseable'])
|
||||||
|
|
||||||
# Set the default 'purchaseable' status to True
|
# Set the default 'purchaseable' status to True
|
||||||
InvenTreeSetting.set_setting(
|
InvenTreeSetting.set_setting(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from django.core.exceptions import ValidationError
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from .models import Part, PartCategory, PartTestTemplate
|
from .models import Part, PartCategory, PartTestTemplate
|
||||||
from .models import rename_part_image, match_part_names
|
from .models import rename_part_image
|
||||||
from .templatetags import inventree_extras
|
from .templatetags import inventree_extras
|
||||||
|
|
||||||
import part.settings
|
import part.settings
|
||||||
@@ -163,12 +163,6 @@ class PartTest(TestCase):
|
|||||||
def test_copy(self):
|
def test_copy(self):
|
||||||
self.r2.deep_copy(self.r1, image=True, bom=True)
|
self.r2.deep_copy(self.r1, image=True, bom=True)
|
||||||
|
|
||||||
def test_match_names(self):
|
|
||||||
|
|
||||||
matches = match_part_names('M2x5 LPHS')
|
|
||||||
|
|
||||||
self.assertTrue(len(matches) > 0)
|
|
||||||
|
|
||||||
def test_sell_pricing(self):
|
def test_sell_pricing(self):
|
||||||
# check that the sell pricebreaks were loaded
|
# check that the sell pricebreaks were loaded
|
||||||
self.assertTrue(self.r1.has_price_breaks)
|
self.assertTrue(self.r1.has_price_breaks)
|
||||||
@@ -281,7 +275,7 @@ class PartSettingsTest(TestCase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
self.assertTrue(part.settings.part_component_default())
|
self.assertTrue(part.settings.part_component_default())
|
||||||
self.assertFalse(part.settings.part_purchaseable_default())
|
self.assertTrue(part.settings.part_purchaseable_default())
|
||||||
self.assertFalse(part.settings.part_salable_default())
|
self.assertFalse(part.settings.part_salable_default())
|
||||||
self.assertFalse(part.settings.part_trackable_default())
|
self.assertFalse(part.settings.part_trackable_default())
|
||||||
|
|
||||||
@@ -293,7 +287,7 @@ class PartSettingsTest(TestCase):
|
|||||||
part = self.make_part()
|
part = self.make_part()
|
||||||
|
|
||||||
self.assertTrue(part.component)
|
self.assertTrue(part.component)
|
||||||
self.assertFalse(part.purchaseable)
|
self.assertTrue(part.purchaseable)
|
||||||
self.assertFalse(part.salable)
|
self.assertFalse(part.salable)
|
||||||
self.assertFalse(part.trackable)
|
self.assertFalse(part.trackable)
|
||||||
|
|
||||||
|
|||||||
@@ -155,38 +155,6 @@ class PartDetailTest(PartViewTestCase):
|
|||||||
self.assertIn('streaming_content', dir(response))
|
self.assertIn('streaming_content', dir(response))
|
||||||
|
|
||||||
|
|
||||||
class PartTests(PartViewTestCase):
|
|
||||||
""" Tests for Part forms """
|
|
||||||
|
|
||||||
def test_part_create(self):
|
|
||||||
""" Launch form to create a new part """
|
|
||||||
response = self.client.get(reverse('part-create'), {'category': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
# And again, with an invalid category
|
|
||||||
response = self.client.get(reverse('part-create'), {'category': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
# And again, with no category
|
|
||||||
response = self.client.get(reverse('part-create'), {'name': 'Test part'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_part_duplicate(self):
|
|
||||||
""" Launch form to duplicate part """
|
|
||||||
|
|
||||||
# First try with an invalid part
|
|
||||||
response = self.client.get(reverse('part-duplicate', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
response = self.client.get(reverse('part-duplicate', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_make_variant(self):
|
|
||||||
|
|
||||||
response = self.client.get(reverse('make-part-variant', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
|
|
||||||
class PartRelatedTests(PartViewTestCase):
|
class PartRelatedTests(PartViewTestCase):
|
||||||
|
|
||||||
def test_valid_create(self):
|
def test_valid_create(self):
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ part_detail_urls = [
|
|||||||
url(r'^bom-export/?', views.BomExport.as_view(), name='bom-export'),
|
url(r'^bom-export/?', views.BomExport.as_view(), name='bom-export'),
|
||||||
url(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'),
|
url(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'),
|
||||||
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
|
url(r'^validate-bom/', views.BomValidate.as_view(), name='bom-validate'),
|
||||||
url(r'^duplicate/', views.PartDuplicate.as_view(), name='part-duplicate'),
|
|
||||||
url(r'^make-variant/', views.MakePartVariant.as_view(), name='make-part-variant'),
|
|
||||||
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
|
url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'),
|
||||||
|
|
||||||
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
|
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
|
||||||
@@ -81,9 +80,6 @@ category_urls = [
|
|||||||
# URL list for part web interface
|
# URL list for part web interface
|
||||||
part_urls = [
|
part_urls = [
|
||||||
|
|
||||||
# Create a new part
|
|
||||||
url(r'^new/?', views.PartCreate.as_view(), name='part-create'),
|
|
||||||
|
|
||||||
# Upload a part
|
# Upload a part
|
||||||
url(r'^import/', views.PartImport.as_view(), name='part-import'),
|
url(r'^import/', views.PartImport.as_view(), name='part-import'),
|
||||||
url(r'^import-api/', views.PartImportAjax.as_view(), name='api-part-import'),
|
url(r'^import-api/', views.PartImportAjax.as_view(), name='api-part-import'),
|
||||||
|
|||||||
+2
-368
@@ -14,8 +14,7 @@ from django.shortcuts import HttpResponseRedirect
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
from django.forms.models import model_to_dict
|
from django.forms import HiddenInput
|
||||||
from django.forms import HiddenInput, CheckboxInput
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
|
||||||
@@ -35,7 +34,6 @@ from .models import PartCategory, Part, PartRelated
|
|||||||
from .models import PartParameterTemplate
|
from .models import PartParameterTemplate
|
||||||
from .models import PartCategoryParameterTemplate
|
from .models import PartCategoryParameterTemplate
|
||||||
from .models import BomItem
|
from .models import BomItem
|
||||||
from .models import match_part_names
|
|
||||||
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
@@ -44,7 +42,7 @@ from common.files import FileManager
|
|||||||
from common.views import FileManagementFormView, FileManagementAjaxView
|
from common.views import FileManagementFormView, FileManagementAjaxView
|
||||||
from common.forms import UploadFileForm, MatchFieldForm
|
from common.forms import UploadFileForm, MatchFieldForm
|
||||||
|
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockLocation
|
||||||
|
|
||||||
import common.settings as inventree_settings
|
import common.settings as inventree_settings
|
||||||
|
|
||||||
@@ -233,370 +231,6 @@ class PartSetCategory(AjaxUpdateView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class MakePartVariant(AjaxCreateView):
|
|
||||||
""" View for creating a new variant based on an existing template Part
|
|
||||||
|
|
||||||
- Part <pk> is provided in the URL '/part/<pk>/make_variant/'
|
|
||||||
- Automatically copy relevent data (BOM, etc, etc)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Part
|
|
||||||
form_class = part_forms.EditPartForm
|
|
||||||
|
|
||||||
ajax_form_title = _('Create Variant')
|
|
||||||
ajax_template_name = 'part/variant_part.html'
|
|
||||||
|
|
||||||
def get_part_template(self):
|
|
||||||
return get_object_or_404(Part, id=self.kwargs['pk'])
|
|
||||||
|
|
||||||
def get_context_data(self):
|
|
||||||
return {
|
|
||||||
'part': self.get_part_template(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
form = super(AjaxCreateView, self).get_form()
|
|
||||||
|
|
||||||
# Hide some variant-related fields
|
|
||||||
# form.fields['variant_of'].widget = HiddenInput()
|
|
||||||
|
|
||||||
# Force display of the 'bom_copy' widget
|
|
||||||
form.fields['bom_copy'].widget = CheckboxInput()
|
|
||||||
|
|
||||||
# Force display of the 'parameters_copy' widget
|
|
||||||
form.fields['parameters_copy'].widget = CheckboxInput()
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
|
|
||||||
form = self.get_form()
|
|
||||||
context = self.get_context_data()
|
|
||||||
part_template = self.get_part_template()
|
|
||||||
|
|
||||||
valid = form.is_valid()
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'form_valid': valid,
|
|
||||||
}
|
|
||||||
|
|
||||||
if valid:
|
|
||||||
# Create the new part variant
|
|
||||||
part = form.save(commit=False)
|
|
||||||
part.variant_of = part_template
|
|
||||||
part.is_template = False
|
|
||||||
|
|
||||||
part.save()
|
|
||||||
|
|
||||||
data['pk'] = part.pk
|
|
||||||
data['text'] = str(part)
|
|
||||||
data['url'] = part.get_absolute_url()
|
|
||||||
|
|
||||||
bom_copy = str2bool(request.POST.get('bom_copy', False))
|
|
||||||
parameters_copy = str2bool(request.POST.get('parameters_copy', False))
|
|
||||||
|
|
||||||
# Copy relevent information from the template part
|
|
||||||
part.deep_copy(part_template, bom=bom_copy, parameters=parameters_copy)
|
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form, data, context=context)
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
|
|
||||||
part_template = self.get_part_template()
|
|
||||||
|
|
||||||
initials = model_to_dict(part_template)
|
|
||||||
initials['is_template'] = False
|
|
||||||
initials['variant_of'] = part_template
|
|
||||||
initials['bom_copy'] = InvenTreeSetting.get_setting('PART_COPY_BOM')
|
|
||||||
initials['parameters_copy'] = InvenTreeSetting.get_setting('PART_COPY_PARAMETERS')
|
|
||||||
|
|
||||||
return initials
|
|
||||||
|
|
||||||
|
|
||||||
class PartDuplicate(AjaxCreateView):
|
|
||||||
""" View for duplicating an existing Part object.
|
|
||||||
|
|
||||||
- Part <pk> is provided in the URL '/part/<pk>/copy/'
|
|
||||||
- Option for 'deep-copy' which will duplicate all BOM items (default = True)
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = Part
|
|
||||||
form_class = part_forms.EditPartForm
|
|
||||||
|
|
||||||
ajax_form_title = _("Duplicate Part")
|
|
||||||
ajax_template_name = "part/copy_part.html"
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return {
|
|
||||||
'success': _('Copied part')
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_part_to_copy(self):
|
|
||||||
try:
|
|
||||||
return Part.objects.get(id=self.kwargs['pk'])
|
|
||||||
except (Part.DoesNotExist, ValueError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_context_data(self):
|
|
||||||
return {
|
|
||||||
'part': self.get_part_to_copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
form = super(AjaxCreateView, self).get_form()
|
|
||||||
|
|
||||||
# Force display of the 'bom_copy' widget
|
|
||||||
form.fields['bom_copy'].widget = CheckboxInput()
|
|
||||||
|
|
||||||
# Force display of the 'parameters_copy' widget
|
|
||||||
form.fields['parameters_copy'].widget = CheckboxInput()
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
""" Capture the POST request for part duplication
|
|
||||||
|
|
||||||
- If the bom_copy object is set, copy all the BOM items too!
|
|
||||||
- If the parameters_copy object is set, copy all the parameters too!
|
|
||||||
"""
|
|
||||||
|
|
||||||
form = self.get_form()
|
|
||||||
|
|
||||||
context = self.get_context_data()
|
|
||||||
|
|
||||||
valid = form.is_valid()
|
|
||||||
|
|
||||||
name = request.POST.get('name', None)
|
|
||||||
|
|
||||||
if name:
|
|
||||||
matches = match_part_names(name)
|
|
||||||
|
|
||||||
if len(matches) > 0:
|
|
||||||
# Display the first five closest matches
|
|
||||||
context['matches'] = matches[:5]
|
|
||||||
|
|
||||||
# Enforce display of the checkbox
|
|
||||||
form.fields['confirm_creation'].widget = CheckboxInput()
|
|
||||||
|
|
||||||
# Check if the user has checked the 'confirm_creation' input
|
|
||||||
confirmed = str2bool(request.POST.get('confirm_creation', False))
|
|
||||||
|
|
||||||
if not confirmed:
|
|
||||||
msg = _('Possible matches exist - confirm creation of new part')
|
|
||||||
form.add_error('confirm_creation', msg)
|
|
||||||
form.pre_form_warning = msg
|
|
||||||
valid = False
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'form_valid': valid
|
|
||||||
}
|
|
||||||
|
|
||||||
if valid:
|
|
||||||
# Create the new Part
|
|
||||||
part = form.save(commit=False)
|
|
||||||
|
|
||||||
part.creation_user = request.user
|
|
||||||
part.save()
|
|
||||||
|
|
||||||
data['pk'] = part.pk
|
|
||||||
data['text'] = str(part)
|
|
||||||
|
|
||||||
bom_copy = str2bool(request.POST.get('bom_copy', False))
|
|
||||||
parameters_copy = str2bool(request.POST.get('parameters_copy', False))
|
|
||||||
|
|
||||||
original = self.get_part_to_copy()
|
|
||||||
|
|
||||||
if original:
|
|
||||||
part.deep_copy(original, bom=bom_copy, parameters=parameters_copy)
|
|
||||||
|
|
||||||
try:
|
|
||||||
data['url'] = part.get_absolute_url()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if valid:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form, data, context=context)
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
""" Get initial data based on the Part to be copied from.
|
|
||||||
"""
|
|
||||||
|
|
||||||
part = self.get_part_to_copy()
|
|
||||||
|
|
||||||
if part:
|
|
||||||
initials = model_to_dict(part)
|
|
||||||
else:
|
|
||||||
initials = super(AjaxCreateView, self).get_initial()
|
|
||||||
|
|
||||||
initials['bom_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_BOM', True))
|
|
||||||
|
|
||||||
initials['parameters_copy'] = str2bool(InvenTreeSetting.get_setting('PART_COPY_PARAMETERS', True))
|
|
||||||
|
|
||||||
return initials
|
|
||||||
|
|
||||||
|
|
||||||
class PartCreate(AjaxCreateView):
|
|
||||||
""" View for creating a new Part object.
|
|
||||||
|
|
||||||
Options for providing initial conditions:
|
|
||||||
|
|
||||||
- Provide a category object as initial data
|
|
||||||
"""
|
|
||||||
model = Part
|
|
||||||
form_class = part_forms.EditPartForm
|
|
||||||
|
|
||||||
ajax_form_title = _('Create New Part')
|
|
||||||
ajax_template_name = 'part/create_part.html'
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
return {
|
|
||||||
'success': _("Created new part"),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_category_id(self):
|
|
||||||
return self.request.GET.get('category', None)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
""" Provide extra context information for the form to display:
|
|
||||||
|
|
||||||
- Add category information (if provided)
|
|
||||||
"""
|
|
||||||
context = super(PartCreate, self).get_context_data(**kwargs)
|
|
||||||
|
|
||||||
# Add category information to the page
|
|
||||||
cat_id = self.get_category_id()
|
|
||||||
|
|
||||||
if cat_id:
|
|
||||||
try:
|
|
||||||
context['category'] = PartCategory.objects.get(pk=cat_id)
|
|
||||||
except (PartCategory.DoesNotExist, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_form(self):
|
|
||||||
""" Create Form for making new Part object.
|
|
||||||
Remove the 'default_supplier' field as there are not yet any matching SupplierPart objects
|
|
||||||
"""
|
|
||||||
form = super(AjaxCreateView, self).get_form()
|
|
||||||
|
|
||||||
# Hide the "default expiry" field if the feature is not enabled
|
|
||||||
if not inventree_settings.stock_expiry_enabled():
|
|
||||||
form.fields['default_expiry'].widget = HiddenInput()
|
|
||||||
|
|
||||||
# Hide the "initial stock amount" field if the feature is not enabled
|
|
||||||
if not InvenTreeSetting.get_setting('PART_CREATE_INITIAL'):
|
|
||||||
form.fields['initial_stock'].widget = HiddenInput()
|
|
||||||
|
|
||||||
# Hide the default_supplier field (there are no matching supplier parts yet!)
|
|
||||||
form.fields['default_supplier'].widget = HiddenInput()
|
|
||||||
|
|
||||||
# Display category templates widgets
|
|
||||||
form.fields['selected_category_templates'].widget = CheckboxInput()
|
|
||||||
form.fields['parent_category_templates'].widget = CheckboxInput()
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
|
|
||||||
form = self.get_form()
|
|
||||||
|
|
||||||
context = {}
|
|
||||||
|
|
||||||
valid = form.is_valid()
|
|
||||||
|
|
||||||
name = request.POST.get('name', None)
|
|
||||||
|
|
||||||
if name:
|
|
||||||
matches = match_part_names(name)
|
|
||||||
|
|
||||||
if len(matches) > 0:
|
|
||||||
|
|
||||||
# Limit to the top 5 matches (to prevent clutter)
|
|
||||||
context['matches'] = matches[:5]
|
|
||||||
|
|
||||||
# Enforce display of the checkbox
|
|
||||||
form.fields['confirm_creation'].widget = CheckboxInput()
|
|
||||||
|
|
||||||
# Check if the user has checked the 'confirm_creation' input
|
|
||||||
confirmed = str2bool(request.POST.get('confirm_creation', False))
|
|
||||||
|
|
||||||
if not confirmed:
|
|
||||||
msg = _('Possible matches exist - confirm creation of new part')
|
|
||||||
form.add_error('confirm_creation', msg)
|
|
||||||
|
|
||||||
form.pre_form_warning = msg
|
|
||||||
valid = False
|
|
||||||
|
|
||||||
data = {
|
|
||||||
'form_valid': valid
|
|
||||||
}
|
|
||||||
|
|
||||||
if valid:
|
|
||||||
# Create the new Part
|
|
||||||
part = form.save(commit=False)
|
|
||||||
|
|
||||||
# Record the user who created this part
|
|
||||||
part.creation_user = request.user
|
|
||||||
|
|
||||||
# Store category templates settings
|
|
||||||
add_category_templates = {
|
|
||||||
'main': form.cleaned_data['selected_category_templates'],
|
|
||||||
'parent': form.cleaned_data['parent_category_templates'],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Save part and pass category template settings
|
|
||||||
part.save(**{'add_category_templates': add_category_templates})
|
|
||||||
|
|
||||||
# Add stock if set
|
|
||||||
init_stock = int(request.POST.get('initial_stock', 0))
|
|
||||||
if init_stock:
|
|
||||||
stock = StockItem(part=part,
|
|
||||||
quantity=init_stock,
|
|
||||||
location=part.default_location)
|
|
||||||
stock.save()
|
|
||||||
|
|
||||||
data['pk'] = part.pk
|
|
||||||
data['text'] = str(part)
|
|
||||||
|
|
||||||
try:
|
|
||||||
data['url'] = part.get_absolute_url()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return self.renderJsonResponse(request, form, data, context=context)
|
|
||||||
|
|
||||||
def get_initial(self):
|
|
||||||
""" Get initial data for the new Part object:
|
|
||||||
|
|
||||||
- If a category is provided, pre-fill the Category field
|
|
||||||
"""
|
|
||||||
|
|
||||||
initials = super(PartCreate, self).get_initial()
|
|
||||||
|
|
||||||
if self.get_category_id():
|
|
||||||
try:
|
|
||||||
category = PartCategory.objects.get(pk=self.get_category_id())
|
|
||||||
initials['category'] = category
|
|
||||||
initials['keywords'] = category.default_keywords
|
|
||||||
except (PartCategory.DoesNotExist, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Allow initial data to be passed through as arguments
|
|
||||||
for label in ['name', 'IPN', 'description', 'revision', 'keywords']:
|
|
||||||
if label in self.request.GET:
|
|
||||||
initials[label] = self.request.GET.get(label)
|
|
||||||
|
|
||||||
# Automatically create part parameters from category templates
|
|
||||||
initials['selected_category_templates'] = str2bool(InvenTreeSetting.get_setting('PART_CATEGORY_PARAMETERS', False))
|
|
||||||
initials['parent_category_templates'] = initials['selected_category_templates']
|
|
||||||
|
|
||||||
return initials
|
|
||||||
|
|
||||||
|
|
||||||
class PartImport(FileManagementFormView):
|
class PartImport(FileManagementFormView):
|
||||||
''' Part: Upload file, match to fields and import parts(using multi-Step form) '''
|
''' Part: Upload file, match to fields and import parts(using multi-Step form) '''
|
||||||
permission_required = 'part.add'
|
permission_required = 'part.add'
|
||||||
|
|||||||
@@ -254,10 +254,13 @@ class ReportPrintMixin:
|
|||||||
else:
|
else:
|
||||||
pdf = outputs[0].get_document().write_pdf()
|
pdf = outputs[0].get_document().write_pdf()
|
||||||
|
|
||||||
|
inline = common.models.InvenTreeUserSetting.get_setting('REPORT_INLINE', user=request.user)
|
||||||
|
|
||||||
return InvenTree.helpers.DownloadFile(
|
return InvenTree.helpers.DownloadFile(
|
||||||
pdf,
|
pdf,
|
||||||
report_name,
|
report_name,
|
||||||
content_type='application/pdf'
|
content_type='application/pdf',
|
||||||
|
inline=inline,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ content: "v{{report_revision}} - {{ date.isoformat }}";
|
|||||||
|
|
||||||
{% block header_content %}
|
{% block header_content %}
|
||||||
<!-- TODO - Make the company logo asset generic -->
|
<!-- TODO - Make the company logo asset generic -->
|
||||||
<img class='logo' src="{% asset 'company_logo.png' %}" alt="hello" width="150">
|
<img class='logo' src="{% asset 'company_logo.png' %}" alt="logo" width="150">
|
||||||
|
|
||||||
<div class='header-right'>
|
<div class='header-right'>
|
||||||
<h3>
|
<h3>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ This script calculates translation coverage for various languages
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
def calculate_coverage(filename):
|
def calculate_coverage(filename):
|
||||||
@@ -36,8 +37,10 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
MY_DIR = os.path.dirname(os.path.realpath(__file__))
|
MY_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||||
LC_DIR = os.path.abspath(os.path.join(MY_DIR, '..', 'locale'))
|
LC_DIR = os.path.abspath(os.path.join(MY_DIR, '..', 'locale'))
|
||||||
|
STAT_FILE = os.path.abspath(os.path.join(MY_DIR, '..', 'InvenTree/locale_stats.json'))
|
||||||
|
|
||||||
locales = {}
|
locales = {}
|
||||||
|
locales_perc = {}
|
||||||
|
|
||||||
print("InvenTree translation coverage:")
|
print("InvenTree translation coverage:")
|
||||||
|
|
||||||
@@ -64,5 +67,10 @@ if __name__ == '__main__':
|
|||||||
percentage = 0
|
percentage = 0
|
||||||
|
|
||||||
print(f"| {locale.ljust(4, ' ')} : {str(percentage).rjust(4, ' ')}% |")
|
print(f"| {locale.ljust(4, ' ')} : {str(percentage).rjust(4, ' ')}% |")
|
||||||
|
locales_perc[locale] = percentage
|
||||||
|
|
||||||
print("-" * 16)
|
print("-" * 16)
|
||||||
|
|
||||||
|
# write locale stats
|
||||||
|
with open(STAT_FILE, 'w') as target:
|
||||||
|
json.dump(locales_perc, target)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
{% load markdownify %}
|
{% load markdownify %}
|
||||||
|
|
||||||
{% block menubar %}
|
{% block menubar %}
|
||||||
@@ -152,7 +153,7 @@
|
|||||||
{
|
{
|
||||||
stock_item: {{ item.pk }},
|
stock_item: {{ item.pk }},
|
||||||
part: {{ item.part.pk }},
|
part: {{ item.part.pk }},
|
||||||
quantity: {{ item.quantity }},
|
quantity: {{ item.quantity|unlocalize }},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,18 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class='list-group-item' title='{% trans "Labels" %}'>
|
||||||
|
<a href='#' class='nav-toggle' id='select-user-labels'>
|
||||||
|
<span class='fas fa-tag'></span> {% trans "Labels" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class='list-group-item' title='{% trans "Reports" %}'>
|
||||||
|
<a href='#' class='nav-toggle' id='select-user-reports'>
|
||||||
|
<span class='fas fa-file-pdf'></span> {% trans "Reports" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<li class='list-group-item' title='{% trans "Settings" %}'>
|
<li class='list-group-item' title='{% trans "Settings" %}'>
|
||||||
<a href='#' class='nav-toggle' id='select-user-settings'>
|
<a href='#' class='nav-toggle' id='select-user-settings'>
|
||||||
|
|||||||
@@ -68,80 +68,3 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
|
||||||
{{ block.super }}
|
|
||||||
|
|
||||||
$("#param-table").inventreeTable({
|
|
||||||
url: "{% url 'api-part-parameter-template-list' %}",
|
|
||||||
queryParams: {
|
|
||||||
ordering: 'name',
|
|
||||||
},
|
|
||||||
formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; },
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
title: 'ID',
|
|
||||||
visible: false,
|
|
||||||
switchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'name',
|
|
||||||
title: 'Name',
|
|
||||||
sortable: 'true',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'units',
|
|
||||||
title: 'Units',
|
|
||||||
sortable: 'true',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
|
|
||||||
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
|
|
||||||
|
|
||||||
var html = "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#new-param").click(function() {
|
|
||||||
launchModalForm("{% url 'part-param-template-create' %}", {
|
|
||||||
success: function() {
|
|
||||||
$("#param-table").bootstrapTable('refresh');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#param-table").on('click', '.template-edit', function() {
|
|
||||||
var button = $(this);
|
|
||||||
|
|
||||||
var url = "/part/parameter/template/" + button.attr('pk') + "/edit/";
|
|
||||||
|
|
||||||
launchModalForm(url, {
|
|
||||||
success: function() {
|
|
||||||
$("#param-table").bootstrapTable('refresh');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#param-table").on('click', '.template-delete', function() {
|
|
||||||
var button = $(this);
|
|
||||||
|
|
||||||
var url = "/part/parameter/template/" + button.attr('pk') + "/delete/";
|
|
||||||
|
|
||||||
launchModalForm(url, {
|
|
||||||
success: function() {
|
|
||||||
$("#param-table").bootstrapTable('refresh');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#import-part").click(function() {
|
|
||||||
launchModalForm("{% url 'api-part-import' %}?reset", {});
|
|
||||||
});
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -18,6 +18,8 @@
|
|||||||
{% include "InvenTree/settings/user_settings.html" %}
|
{% include "InvenTree/settings/user_settings.html" %}
|
||||||
{% include "InvenTree/settings/user_homepage.html" %}
|
{% include "InvenTree/settings/user_homepage.html" %}
|
||||||
{% include "InvenTree/settings/user_search.html" %}
|
{% include "InvenTree/settings/user_search.html" %}
|
||||||
|
{% include "InvenTree/settings/user_labels.html" %}
|
||||||
|
{% include "InvenTree/settings/user_reports.html" %}
|
||||||
|
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
|
|
||||||
@@ -241,6 +243,79 @@ $("#cat-param-table").on('click', '.template-delete', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#param-table").inventreeTable({
|
||||||
|
url: "{% url 'api-part-parameter-template-list' %}",
|
||||||
|
queryParams: {
|
||||||
|
ordering: 'name',
|
||||||
|
},
|
||||||
|
formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; },
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
title: 'ID',
|
||||||
|
visible: false,
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: 'Name',
|
||||||
|
sortable: 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'units',
|
||||||
|
title: 'Units',
|
||||||
|
sortable: 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
|
||||||
|
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
|
||||||
|
|
||||||
|
var html = "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#new-param").click(function() {
|
||||||
|
launchModalForm("{% url 'part-param-template-create' %}", {
|
||||||
|
success: function() {
|
||||||
|
$("#param-table").bootstrapTable('refresh');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#param-table").on('click', '.template-edit', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var url = "/part/parameter/template/" + button.attr('pk') + "/edit/";
|
||||||
|
|
||||||
|
launchModalForm(url, {
|
||||||
|
success: function() {
|
||||||
|
$("#param-table").bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#param-table").on('click', '.template-delete', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var url = "/part/parameter/template/" + button.attr('pk') + "/delete/";
|
||||||
|
|
||||||
|
launchModalForm(url, {
|
||||||
|
success: function() {
|
||||||
|
$("#param-table").bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#import-part").click(function() {
|
||||||
|
launchModalForm("{% url 'api-part-import' %}?reset", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
enableNavbar({
|
enableNavbar({
|
||||||
label: 'settings',
|
label: 'settings',
|
||||||
toggleId: '#item-menu-toggle',
|
toggleId: '#item-menu-toggle',
|
||||||
|
|||||||
@@ -71,6 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
<form action="{% url 'set_language' %}" method="post">
|
<form action="{% url 'set_language' %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input name="next" type="hidden" value="{% url 'settings' %}">
|
<input name="next" type="hidden" value="{% url 'settings' %}">
|
||||||
@@ -80,8 +81,15 @@
|
|||||||
{% get_available_languages as LANGUAGES %}
|
{% get_available_languages as LANGUAGES %}
|
||||||
{% get_language_info_list for LANGUAGES as languages %}
|
{% get_language_info_list for LANGUAGES as languages %}
|
||||||
{% for language in languages %}
|
{% for language in languages %}
|
||||||
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
|
{% define language.code as lang_code %}
|
||||||
{{ language.name_local }} ({{ language.code }})
|
{% define locale_stats|keyvalue:lang_code as lang_translated %}
|
||||||
|
<option value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
|
||||||
|
{{ language.name_local }} ({{ lang_code }})
|
||||||
|
{% if lang_translated %}
|
||||||
|
{% blocktrans %}{{ lang_translated }}% translated{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% trans 'No translations available' %}
|
||||||
|
{% endif %}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
@@ -91,5 +99,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h4>{% trans "Help the translation efforts!" %}</h4>
|
||||||
|
<p>{% blocktrans with link="https://crowdin.com/project/inventree" %}Native language translation of the InvenTree web application is <a href="{{link}}">community contributed via crowdin</a>. Contributions are welcomed and encouraged.{% endblocktrans %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends "panel.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
{% block label %}user-labels{% endblock %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
{% trans "Label Settings" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
{% include "InvenTree/settings/header.html" %}
|
||||||
|
<tbody>
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="LABEL_INLINE" icon='fa-tag' user_setting=True %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends "panel.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
{% block label %}user-reports{% endblock %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
{% trans "Report Settings" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
{% include "InvenTree/settings/header.html" %}
|
||||||
|
<tbody>
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="REPORT_INLINE" icon='fa-file-pdf' user_setting=True %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -927,7 +927,7 @@ function loadBuildTable(table, options) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'responsible',
|
field: 'responsible',
|
||||||
title: '{% trans "Resposible" %}',
|
title: '{% trans "Responsible" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
if (value)
|
if (value)
|
||||||
|
|||||||
@@ -30,6 +30,17 @@ function createManufacturerPart(options={}) {
|
|||||||
fields.manufacturer.value = options.manufacturer;
|
fields.manufacturer.value = options.manufacturer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fields.manufacturer.secondary = {
|
||||||
|
title: '{% trans "Add Manufacturer" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var company_fields = companyFormFields();
|
||||||
|
|
||||||
|
company_fields.is_manufacturer.value = true;
|
||||||
|
|
||||||
|
return company_fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructForm('{% url "api-manufacturer-part-list" %}', {
|
constructForm('{% url "api-manufacturer-part-list" %}', {
|
||||||
fields: fields,
|
fields: fields,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -72,7 +83,7 @@ function supplierPartFields() {
|
|||||||
filters: {
|
filters: {
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
manufacturer_detail: true,
|
manufacturer_detail: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
description: {},
|
description: {},
|
||||||
link: {
|
link: {
|
||||||
@@ -108,6 +119,33 @@ function createSupplierPart(options={}) {
|
|||||||
fields.manufacturer_part.value = options.manufacturer_part;
|
fields.manufacturer_part.value = options.manufacturer_part;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a secondary modal for the supplier
|
||||||
|
fields.supplier.secondary = {
|
||||||
|
title: '{% trans "Add Supplier" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var company_fields = companyFormFields();
|
||||||
|
|
||||||
|
company_fields.is_supplier.value = true;
|
||||||
|
|
||||||
|
return company_fields;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a secondary modal for the manufacturer part
|
||||||
|
fields.manufacturer_part.secondary = {
|
||||||
|
title: '{% trans "Add Manufacturer Part" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var mp_fields = manufacturerPartFields();
|
||||||
|
|
||||||
|
if (data.part) {
|
||||||
|
mp_fields.part.value = data.part;
|
||||||
|
mp_fields.part.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mp_fields;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
constructForm('{% url "api-supplier-part-list" %}', {
|
constructForm('{% url "api-supplier-part-list" %}', {
|
||||||
fields: fields,
|
fields: fields,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ function constructDeleteForm(fields, options) {
|
|||||||
* - hidden: Set to true to hide the field
|
* - hidden: Set to true to hide the field
|
||||||
* - icon: font-awesome icon to display before the field
|
* - icon: font-awesome icon to display before the field
|
||||||
* - prefix: Custom HTML prefix to display before the field
|
* - prefix: Custom HTML prefix to display before the field
|
||||||
|
* - data: map of data to fill out field values with
|
||||||
* - focus: Name of field to focus on when modal is displayed
|
* - focus: Name of field to focus on when modal is displayed
|
||||||
* - preventClose: Set to true to prevent form from closing on success
|
* - preventClose: Set to true to prevent form from closing on success
|
||||||
* - onSuccess: callback function when form action is successful
|
* - onSuccess: callback function when form action is successful
|
||||||
@@ -263,6 +264,11 @@ function constructForm(url, options) {
|
|||||||
// Default HTTP method
|
// Default HTTP method
|
||||||
options.method = options.method || 'PATCH';
|
options.method = options.method || 'PATCH';
|
||||||
|
|
||||||
|
// Construct an "empty" data object if not provided
|
||||||
|
if (!options.data) {
|
||||||
|
options.data = {};
|
||||||
|
}
|
||||||
|
|
||||||
// Request OPTIONS endpoint from the API
|
// Request OPTIONS endpoint from the API
|
||||||
getApiEndpointOptions(url, function(OPTIONS) {
|
getApiEndpointOptions(url, function(OPTIONS) {
|
||||||
|
|
||||||
@@ -346,11 +352,20 @@ function constructFormBody(fields, options) {
|
|||||||
// otherwise *all* fields will be displayed
|
// otherwise *all* fields will be displayed
|
||||||
var displayed_fields = options.fields || fields;
|
var displayed_fields = options.fields || fields;
|
||||||
|
|
||||||
|
// Handle initial data overrides
|
||||||
|
if (options.data) {
|
||||||
|
for (const field in options.data) {
|
||||||
|
|
||||||
|
if (field in fields) {
|
||||||
|
fields[field].value = options.data[field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Provide each field object with its own name
|
// Provide each field object with its own name
|
||||||
for(field in fields) {
|
for(field in fields) {
|
||||||
fields[field].name = field;
|
fields[field].name = field;
|
||||||
|
|
||||||
|
|
||||||
// If any "instance_filters" are defined for the endpoint, copy them across (overwrite)
|
// If any "instance_filters" are defined for the endpoint, copy them across (overwrite)
|
||||||
if (fields[field].instance_filters) {
|
if (fields[field].instance_filters) {
|
||||||
fields[field].filters = Object.assign(fields[field].filters || {}, fields[field].instance_filters);
|
fields[field].filters = Object.assign(fields[field].filters || {}, fields[field].instance_filters);
|
||||||
@@ -366,6 +381,10 @@ function constructFormBody(fields, options) {
|
|||||||
|
|
||||||
// TODO: Refactor the following code with Object.assign (see above)
|
// TODO: Refactor the following code with Object.assign (see above)
|
||||||
|
|
||||||
|
// "before" and "after" renders
|
||||||
|
fields[field].before = field_options.before;
|
||||||
|
fields[field].after = field_options.after;
|
||||||
|
|
||||||
// Secondary modal options
|
// Secondary modal options
|
||||||
fields[field].secondary = field_options.secondary;
|
fields[field].secondary = field_options.secondary;
|
||||||
|
|
||||||
@@ -545,6 +564,30 @@ function insertConfirmButton(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extract all specified form values as a single object
|
||||||
|
*/
|
||||||
|
function extractFormData(fields, options) {
|
||||||
|
|
||||||
|
var data = {};
|
||||||
|
|
||||||
|
for (var idx = 0; idx < options.field_names.length; idx++) {
|
||||||
|
|
||||||
|
var name = options.field_names[idx];
|
||||||
|
|
||||||
|
var field = fields[name] || null;
|
||||||
|
|
||||||
|
if (!field) continue;
|
||||||
|
|
||||||
|
if (field.type == 'candy') continue;
|
||||||
|
|
||||||
|
data[name] = getFormFieldValue(name, field, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Submit form data to the server.
|
* Submit form data to the server.
|
||||||
*
|
*
|
||||||
@@ -560,10 +603,15 @@ function submitFormData(fields, options) {
|
|||||||
var has_files = false;
|
var has_files = false;
|
||||||
|
|
||||||
// Extract values for each field
|
// Extract values for each field
|
||||||
options.field_names.forEach(function(name) {
|
for (var idx = 0; idx < options.field_names.length; idx++) {
|
||||||
|
|
||||||
|
var name = options.field_names[idx];
|
||||||
|
|
||||||
var field = fields[name] || null;
|
var field = fields[name] || null;
|
||||||
|
|
||||||
|
// Ignore visual fields
|
||||||
|
if (field && field.type == 'candy') continue;
|
||||||
|
|
||||||
if (field) {
|
if (field) {
|
||||||
|
|
||||||
var value = getFormFieldValue(name, field, options);
|
var value = getFormFieldValue(name, field, options);
|
||||||
@@ -593,7 +641,7 @@ function submitFormData(fields, options) {
|
|||||||
} else {
|
} else {
|
||||||
console.log(`WARNING: Could not find field matching '${name}'`);
|
console.log(`WARNING: Could not find field matching '${name}'`);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
var upload_func = inventreePut;
|
var upload_func = inventreePut;
|
||||||
|
|
||||||
@@ -926,10 +974,10 @@ function initializeRelatedFields(fields, options) {
|
|||||||
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'related field':
|
case 'related field':
|
||||||
initializeRelatedField(name, field, options);
|
initializeRelatedField(field, fields, options);
|
||||||
break;
|
break;
|
||||||
case 'choice':
|
case 'choice':
|
||||||
initializeChoiceField(name, field, options);
|
initializeChoiceField(field, fields, options);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -944,7 +992,9 @@ function initializeRelatedFields(fields, options) {
|
|||||||
* - field: The field data object
|
* - field: The field data object
|
||||||
* - options: The options object provided by the client
|
* - options: The options object provided by the client
|
||||||
*/
|
*/
|
||||||
function addSecondaryModal(name, field, options) {
|
function addSecondaryModal(field, fields, options) {
|
||||||
|
|
||||||
|
var name = field.name;
|
||||||
|
|
||||||
var secondary = field.secondary;
|
var secondary = field.secondary;
|
||||||
|
|
||||||
@@ -957,22 +1007,42 @@ function addSecondaryModal(name, field, options) {
|
|||||||
|
|
||||||
$(options.modal).find(`label[for="id_${name}"]`).append(html);
|
$(options.modal).find(`label[for="id_${name}"]`).append(html);
|
||||||
|
|
||||||
// TODO: Launch a callback
|
// Callback function when the secondary button is pressed
|
||||||
$(options.modal).find(`#btn-new-${name}`).click(function() {
|
$(options.modal).find(`#btn-new-${name}`).click(function() {
|
||||||
|
|
||||||
if (secondary.callback) {
|
// Determine the API query URL
|
||||||
// A "custom" callback can be specified for the button
|
var url = secondary.api_url || field.api_url;
|
||||||
secondary.callback(field, options);
|
|
||||||
} else if (secondary.api_url) {
|
|
||||||
// By default, a new modal form is created, with the parameters specified
|
|
||||||
// The parameters match the "normal" form creation parameters
|
|
||||||
|
|
||||||
secondary.onSuccess = function(data, opts) {
|
// If the "fields" attribute is a function, call it with data
|
||||||
setRelatedFieldData(name, data, options);
|
if (secondary.fields instanceof Function) {
|
||||||
};
|
|
||||||
|
|
||||||
constructForm(secondary.api_url, secondary);
|
// Extract form values at time of button press
|
||||||
|
var data = extractFormData(fields, options)
|
||||||
|
|
||||||
|
secondary.fields = secondary.fields(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no onSuccess function is defined, provide a default one
|
||||||
|
if (!secondary.onSuccess) {
|
||||||
|
secondary.onSuccess = function(data, opts) {
|
||||||
|
|
||||||
|
// Force refresh from the API, to get full detail
|
||||||
|
inventreeGet(`${url}${data.pk}/`, {}, {
|
||||||
|
success: function(responseData) {
|
||||||
|
|
||||||
|
setRelatedFieldData(name, responseData, options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method should be "POST" for creation
|
||||||
|
secondary.method = secondary.method || 'POST';
|
||||||
|
|
||||||
|
constructForm(
|
||||||
|
url,
|
||||||
|
secondary
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,7 +1056,9 @@ function addSecondaryModal(name, field, options) {
|
|||||||
* - field: Field definition from the OPTIONS request
|
* - field: Field definition from the OPTIONS request
|
||||||
* - options: Original options object provided by the client
|
* - options: Original options object provided by the client
|
||||||
*/
|
*/
|
||||||
function initializeRelatedField(name, field, options) {
|
function initializeRelatedField(field, fields, options) {
|
||||||
|
|
||||||
|
var name = field.name;
|
||||||
|
|
||||||
if (!field.api_url) {
|
if (!field.api_url) {
|
||||||
// TODO: Provide manual api_url option?
|
// TODO: Provide manual api_url option?
|
||||||
@@ -999,7 +1071,7 @@ function initializeRelatedField(name, field, options) {
|
|||||||
|
|
||||||
// Add a button to launch a 'secondary' modal
|
// Add a button to launch a 'secondary' modal
|
||||||
if (field.secondary != null) {
|
if (field.secondary != null) {
|
||||||
addSecondaryModal(name, field, options);
|
addSecondaryModal(field, fields, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add 'placeholder' support for entry select2 fields
|
// TODO: Add 'placeholder' support for entry select2 fields
|
||||||
@@ -1168,7 +1240,9 @@ function setRelatedFieldData(name, data, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function initializeChoiceField(name, field, options) {
|
function initializeChoiceField(field, fields, options) {
|
||||||
|
|
||||||
|
var name = field.name;
|
||||||
|
|
||||||
var select = $(options.modal).find(`#id_${name}`);
|
var select = $(options.modal).find(`#id_${name}`);
|
||||||
|
|
||||||
@@ -1279,6 +1353,11 @@ function renderModelData(name, model, data, parameters, options) {
|
|||||||
*/
|
*/
|
||||||
function constructField(name, parameters, options) {
|
function constructField(name, parameters, options) {
|
||||||
|
|
||||||
|
// Shortcut for simple visual fields
|
||||||
|
if (parameters.type == 'candy') {
|
||||||
|
return constructCandyInput(name, parameters, options);
|
||||||
|
}
|
||||||
|
|
||||||
var field_name = `id_${name}`;
|
var field_name = `id_${name}`;
|
||||||
|
|
||||||
// Hidden inputs are rendered without label / help text / etc
|
// Hidden inputs are rendered without label / help text / etc
|
||||||
@@ -1292,7 +1371,14 @@ function constructField(name, parameters, options) {
|
|||||||
form_classes += ' has-error';
|
form_classes += ' has-error';
|
||||||
}
|
}
|
||||||
|
|
||||||
var html = `<div id='div_${field_name}' class='${form_classes}'>`;
|
var html = '';
|
||||||
|
|
||||||
|
// Optional content to render before the field
|
||||||
|
if (parameters.before) {
|
||||||
|
html += parameters.before;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `<div id='div_${field_name}' class='${form_classes}'>`;
|
||||||
|
|
||||||
// Add a label
|
// Add a label
|
||||||
html += constructLabel(name, parameters);
|
html += constructLabel(name, parameters);
|
||||||
@@ -1352,6 +1438,10 @@ function constructField(name, parameters, options) {
|
|||||||
html += `</div>`; // controls
|
html += `</div>`; // controls
|
||||||
html += `</div>`; // form-group
|
html += `</div>`; // form-group
|
||||||
|
|
||||||
|
if (parameters.after) {
|
||||||
|
html += parameters.after;
|
||||||
|
}
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1430,6 +1520,9 @@ function constructInput(name, parameters, options) {
|
|||||||
case 'date':
|
case 'date':
|
||||||
func = constructDateInput;
|
func = constructDateInput;
|
||||||
break;
|
break;
|
||||||
|
case 'candy':
|
||||||
|
func = constructCandyInput;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Unsupported field type!
|
// Unsupported field type!
|
||||||
break;
|
break;
|
||||||
@@ -1658,6 +1751,17 @@ function constructDateInput(name, parameters, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a "candy" field input
|
||||||
|
* No actual field data!
|
||||||
|
*/
|
||||||
|
function constructCandyInput(name, parameters, options) {
|
||||||
|
|
||||||
|
return parameters.html;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Construct a 'help text' div based on the field parameters
|
* Construct a 'help text' div based on the field parameters
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ function createSalesOrder(options={}) {
|
|||||||
},
|
},
|
||||||
customer: {
|
customer: {
|
||||||
value: options.customer,
|
value: options.customer,
|
||||||
|
secondary: {
|
||||||
|
title: '{% trans "Add Customer" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var fields = companyFormFields();
|
||||||
|
|
||||||
|
fields.is_customer.value = true;
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
customer_reference: {},
|
customer_reference: {},
|
||||||
description: {},
|
description: {},
|
||||||
@@ -44,6 +54,16 @@ function createPurchaseOrder(options={}) {
|
|||||||
},
|
},
|
||||||
supplier: {
|
supplier: {
|
||||||
value: options.supplier,
|
value: options.supplier,
|
||||||
|
secondary: {
|
||||||
|
title: '{% trans "Add Supplier" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var fields = companyFormFields();
|
||||||
|
|
||||||
|
fields.is_supplier.value = true;
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
supplier_reference: {},
|
supplier_reference: {},
|
||||||
description: {},
|
description: {},
|
||||||
|
|||||||
@@ -13,6 +13,144 @@ function yesNoLabel(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construct fieldset for part forms
|
||||||
|
function partFields(options={}) {
|
||||||
|
|
||||||
|
var fields = {
|
||||||
|
category: {
|
||||||
|
secondary: {
|
||||||
|
title: '{% trans "Add Part Category" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var fields = categoryFields();
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: {},
|
||||||
|
IPN: {},
|
||||||
|
revision: {},
|
||||||
|
description: {},
|
||||||
|
variant_of: {},
|
||||||
|
keywords: {
|
||||||
|
icon: 'fa-key',
|
||||||
|
},
|
||||||
|
units: {},
|
||||||
|
link: {
|
||||||
|
icon: 'fa-link',
|
||||||
|
},
|
||||||
|
default_location: {
|
||||||
|
},
|
||||||
|
default_supplier: {},
|
||||||
|
default_expiry: {
|
||||||
|
icon: 'fa-calendar-alt',
|
||||||
|
},
|
||||||
|
minimum_stock: {
|
||||||
|
icon: 'fa-boxes',
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
type: 'candy',
|
||||||
|
html: `<hr><h4><i>{% trans "Part Attributes" %}</i></h4><hr>`
|
||||||
|
},
|
||||||
|
component: {
|
||||||
|
value: global_settings.PART_COMPONENT,
|
||||||
|
},
|
||||||
|
assembly: {
|
||||||
|
value: global_settings.PART_ASSEMBLY,
|
||||||
|
},
|
||||||
|
is_template: {
|
||||||
|
value: global_settings.PART_TEMPLATE,
|
||||||
|
},
|
||||||
|
trackable: {
|
||||||
|
value: global_settings.PART_TRACKABLE,
|
||||||
|
},
|
||||||
|
purchaseable: {
|
||||||
|
value: global_settings.PART_PURCHASEABLE,
|
||||||
|
},
|
||||||
|
salable: {
|
||||||
|
value: global_settings.PART_SALABLE,
|
||||||
|
},
|
||||||
|
virtual: {
|
||||||
|
value: global_settings.PART_VIRTUAL,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// If editing a part, we can set the "active" status
|
||||||
|
if (options.edit) {
|
||||||
|
fields.active = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop expiry field
|
||||||
|
if (!global_settings.STOCK_ENABLE_EXPIRY) {
|
||||||
|
delete fields["default_expiry"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional fields when "creating" a new part
|
||||||
|
if (options.create) {
|
||||||
|
|
||||||
|
// No supplier parts available yet
|
||||||
|
delete fields["default_supplier"];
|
||||||
|
|
||||||
|
fields.create = {
|
||||||
|
type: 'candy',
|
||||||
|
html: `<hr><h4><i>{% trans "Part Creation Options" %}</i></h4><hr>`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (global_settings.PART_CREATE_INITIAL) {
|
||||||
|
fields.initial_stock = {
|
||||||
|
type: 'decimal',
|
||||||
|
label: '{% trans "Initial Stock Quantity" %}',
|
||||||
|
help_text: '{% trans "Initialize part stock with specified quantity" %}',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.copy_category_parameters = {
|
||||||
|
type: 'boolean',
|
||||||
|
label: '{% trans "Copy Category Parameters" %}',
|
||||||
|
help_text: '{% trans "Copy parameter templates from selected part category" %}',
|
||||||
|
value: global_settings.PART_CATEGORY_PARAMETERS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional fields when "duplicating" a part
|
||||||
|
if (options.duplicate) {
|
||||||
|
|
||||||
|
fields.duplicate = {
|
||||||
|
type: 'candy',
|
||||||
|
html: `<hr><h4><i>{% trans "Part Duplication Options" %}</i></h4><hr>`,
|
||||||
|
};
|
||||||
|
|
||||||
|
fields.copy_from = {
|
||||||
|
type: 'integer',
|
||||||
|
hidden: true,
|
||||||
|
value: options.duplicate,
|
||||||
|
},
|
||||||
|
|
||||||
|
fields.copy_image = {
|
||||||
|
type: 'boolean',
|
||||||
|
label: '{% trans "Copy Image" %}',
|
||||||
|
help_text: '{% trans "Copy image from original part" %}',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
fields.copy_bom = {
|
||||||
|
type: 'boolean',
|
||||||
|
label: '{% trans "Copy BOM" %}',
|
||||||
|
help_text: '{% trans "Copy bill of materials from original part" %}',
|
||||||
|
value: global_settings.PART_COPY_BOM,
|
||||||
|
};
|
||||||
|
|
||||||
|
fields.copy_parameters = {
|
||||||
|
type: 'boolean',
|
||||||
|
label: '{% trans "Copy Parameters" %}',
|
||||||
|
help_text: '{% trans "Copy parameter data from original part" %}',
|
||||||
|
value: global_settings.PART_COPY_PARAMETERS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function categoryFields() {
|
function categoryFields() {
|
||||||
return {
|
return {
|
||||||
@@ -49,86 +187,49 @@ function editPart(pk, options={}) {
|
|||||||
|
|
||||||
var url = `/api/part/${pk}/`;
|
var url = `/api/part/${pk}/`;
|
||||||
|
|
||||||
var fields = {
|
var fields = partFields({
|
||||||
category: {
|
edit: true
|
||||||
/*
|
});
|
||||||
secondary: {
|
|
||||||
label: '{% trans "New Category" %}',
|
|
||||||
title: '{% trans "Create New Part Category" %}',
|
|
||||||
api_url: '{% url "api-part-category-list" %}',
|
|
||||||
method: 'POST',
|
|
||||||
fields: {
|
|
||||||
name: {},
|
|
||||||
description: {},
|
|
||||||
parent: {
|
|
||||||
secondary: {
|
|
||||||
title: '{% trans "New Parent" %}',
|
|
||||||
api_url: '{% url "api-part-category-list" %}',
|
|
||||||
method: 'POST',
|
|
||||||
fields: {
|
|
||||||
name: {},
|
|
||||||
description: {},
|
|
||||||
parent: {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
placeholder: 'part name',
|
|
||||||
},
|
|
||||||
IPN: {},
|
|
||||||
description: {},
|
|
||||||
revision: {},
|
|
||||||
keywords: {
|
|
||||||
icon: 'fa-key',
|
|
||||||
},
|
|
||||||
variant_of: {},
|
|
||||||
link: {
|
|
||||||
icon: 'fa-link',
|
|
||||||
},
|
|
||||||
default_location: {
|
|
||||||
/*
|
|
||||||
secondary: {
|
|
||||||
label: '{% trans "New Location" %}',
|
|
||||||
title: '{% trans "Create new stock location" %}',
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
},
|
|
||||||
default_supplier: {
|
|
||||||
filters: {
|
|
||||||
part: pk,
|
|
||||||
part_detail: true,
|
|
||||||
manufacturer_detail: true,
|
|
||||||
supplier_detail: true,
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
secondary: {
|
|
||||||
label: '{% trans "New Supplier Part" %}',
|
|
||||||
title: '{% trans "Create new supplier part" %}',
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
},
|
|
||||||
units: {},
|
|
||||||
minimum_stock: {},
|
|
||||||
virtual: {},
|
|
||||||
is_template: {},
|
|
||||||
assembly: {},
|
|
||||||
component: {},
|
|
||||||
trackable: {},
|
|
||||||
purchaseable: {},
|
|
||||||
salable: {},
|
|
||||||
active: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
constructForm(url, {
|
constructForm(url, {
|
||||||
fields: fields,
|
fields: fields,
|
||||||
title: '{% trans "Edit Part" %}',
|
title: '{% trans "Edit Part" %}',
|
||||||
reload: true,
|
reload: true,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Launch form to duplicate a part
|
||||||
|
function duplicatePart(pk, options={}) {
|
||||||
|
|
||||||
|
// First we need all the part information
|
||||||
|
inventreeGet(`/api/part/${pk}/`, {}, {
|
||||||
|
|
||||||
|
success: function(data) {
|
||||||
|
|
||||||
|
var fields = partFields({
|
||||||
|
duplicate: pk,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we are making a "variant" part
|
||||||
|
if (options.variant) {
|
||||||
|
|
||||||
|
// Override the "variant_of" field
|
||||||
|
data.variant_of = pk;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructForm('{% url "api-part-list" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
fields: fields,
|
||||||
|
title: '{% trans "Duplicate Part" %}',
|
||||||
|
data: data,
|
||||||
|
onSuccess: function(data) {
|
||||||
|
// Follow the new part
|
||||||
|
location.href = `/part/${data.pk}/`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1005,6 +1106,7 @@ function loadPriceBreakTable(table, options) {
|
|||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
return `{% trans "No ${human_name} information found" %}`;
|
return `{% trans "No ${human_name} information found" %}`;
|
||||||
},
|
},
|
||||||
|
queryParams: {part: options.part},
|
||||||
url: options.url,
|
url: options.url,
|
||||||
onLoadSuccess: function(tableData) {
|
onLoadSuccess: function(tableData) {
|
||||||
if (linkedGraph) {
|
if (linkedGraph) {
|
||||||
@@ -1013,7 +1115,7 @@ function loadPriceBreakTable(table, options) {
|
|||||||
|
|
||||||
// split up for graph definition
|
// split up for graph definition
|
||||||
var graphLabels = Array.from(tableData, x => x.quantity);
|
var graphLabels = Array.from(tableData, x => x.quantity);
|
||||||
var graphData = Array.from(tableData, x => parseFloat(x.price));
|
var graphData = Array.from(tableData, x => x.price);
|
||||||
|
|
||||||
// destroy chart if exists
|
// destroy chart if exists
|
||||||
if (chart){
|
if (chart){
|
||||||
@@ -1100,6 +1202,7 @@ function initPriceBreakSet(table, options) {
|
|||||||
human_name: pb_human_name,
|
human_name: pb_human_name,
|
||||||
url: pb_url,
|
url: pb_url,
|
||||||
linkedGraph: linkedGraph,
|
linkedGraph: linkedGraph,
|
||||||
|
part: part_id,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,18 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load status_codes %}
|
||||||
|
|
||||||
|
|
||||||
|
function locationFields() {
|
||||||
|
return {
|
||||||
|
parent: {
|
||||||
|
help_text: '{% trans "Parent stock location" %}',
|
||||||
|
},
|
||||||
|
name: {},
|
||||||
|
description: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Stock API functions
|
/* Stock API functions
|
||||||
* Requires api.js to be loaded first
|
* Requires api.js to be loaded first
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ However, powerful business logic works in the background to ensure that stock tr
|
|||||||
|
|
||||||
InvenTree is [available via Docker](https://hub.docker.com/r/inventree/inventree). Read the [docker guide](https://inventree.readthedocs.io/en/latest/start/docker/) for full details.
|
InvenTree is [available via Docker](https://hub.docker.com/r/inventree/inventree). Read the [docker guide](https://inventree.readthedocs.io/en/latest/start/docker/) for full details.
|
||||||
|
|
||||||
# Companion App
|
# Mobile App
|
||||||
|
|
||||||
InvenTree is supported by a [companion mobile app](https://inventree.readthedocs.io/en/latest/app/app/) which allows users access to stock control information and functionality.
|
InvenTree is supported by a [companion mobile app](https://inventree.readthedocs.io/en/latest/app/app/) which allows users access to stock control information and functionality.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""
|
||||||
|
On release, ensure that the release tag matches the InvenTree version number!
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
|
||||||
|
|
||||||
|
with open(version_file, 'r') as f:
|
||||||
|
|
||||||
|
results = re.findall(r'INVENTREE_SW_VERSION = "(.*)"', f.read())
|
||||||
|
|
||||||
|
if not len(results) == 1:
|
||||||
|
print(f"Could not find INVENTREE_SW_VERSION in {version_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
version = results[0]
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('tag', help='Version tag', action='store')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.tag == version:
|
||||||
|
print(f"Release tag '{args.tag}' does not match INVENTREE_SW_VERSION '{version}'")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
+2
-1
@@ -21,7 +21,8 @@ coverage==5.3 # Unit test coverage
|
|||||||
coveralls==2.1.2 # Coveralls linking (for Travis)
|
coveralls==2.1.2 # Coveralls linking (for Travis)
|
||||||
rapidfuzz==0.7.6 # Fuzzy string matching
|
rapidfuzz==0.7.6 # Fuzzy string matching
|
||||||
django-stdimage==5.1.1 # Advanced ImageField management
|
django-stdimage==5.1.1 # Advanced ImageField management
|
||||||
django-weasyprint==1.0.1 # HTML PDF export
|
weasyprint==52.5 # PDF generation library (Note: in the future need to update to 53)
|
||||||
|
django-weasyprint==1.0.1 # django weasyprint integration
|
||||||
django-debug-toolbar==2.2 # Debug / profiling toolbar
|
django-debug-toolbar==2.2 # Debug / profiling toolbar
|
||||||
django-admin-shell==0.1.2 # Python shell for the admin interface
|
django-admin-shell==0.1.2 # Python shell for the admin interface
|
||||||
py-moneyed==0.8.0 # Specific version requirement for py-moneyed
|
py-moneyed==0.8.0 # Specific version requirement for py-moneyed
|
||||||
|
|||||||
Reference in New Issue
Block a user