mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-20 05:46:34 +00:00
Merge branch 'inventree:master' into bom-internal
This commit is contained in:
@ -5,7 +5,8 @@ Provides a JSON API for the Part app
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import reverse
|
||||
from django.http import JsonResponse
|
||||
from django.db.models import Q, F, Count, Min, Max, Avg
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@ -15,12 +16,13 @@ from rest_framework.response import Response
|
||||
from rest_framework import filters, serializers
|
||||
from rest_framework import generics
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters import rest_framework as rest_filters
|
||||
|
||||
from djmoney.money import Money
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import reverse
|
||||
|
||||
from .models import Part, PartCategory, BomItem
|
||||
from .models import PartParameter, PartParameterTemplate
|
||||
@ -116,9 +118,17 @@ class CategoryList(generics.ListCreateAPIView):
|
||||
|
||||
ordering_fields = [
|
||||
'name',
|
||||
'level',
|
||||
'tree_id',
|
||||
'lft',
|
||||
]
|
||||
|
||||
ordering = 'name'
|
||||
# Use hierarchical ordering by default
|
||||
ordering = [
|
||||
'tree_id',
|
||||
'lft',
|
||||
'name'
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'name',
|
||||
@ -127,7 +137,10 @@ class CategoryList(generics.ListCreateAPIView):
|
||||
|
||||
|
||||
class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
""" API endpoint for detail view of a single PartCategory object """
|
||||
"""
|
||||
API endpoint for detail view of a single PartCategory object
|
||||
"""
|
||||
|
||||
serializer_class = part_serializers.CategorySerializer
|
||||
queryset = PartCategory.objects.all()
|
||||
|
||||
@ -229,6 +242,24 @@ class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
|
||||
]
|
||||
|
||||
|
||||
class PartAttachmentDetail(generics.RetrieveUpdateDestroyAPIView, AttachmentMixin):
|
||||
"""
|
||||
Detail endpoint for PartAttachment model
|
||||
"""
|
||||
|
||||
queryset = PartAttachment.objects.all()
|
||||
serializer_class = part_serializers.PartAttachmentSerializer
|
||||
|
||||
|
||||
class PartTestTemplateDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""
|
||||
Detail endpoint for PartTestTemplate model
|
||||
"""
|
||||
|
||||
queryset = PartTestTemplate.objects.all()
|
||||
serializer_class = part_serializers.PartTestTemplateSerializer
|
||||
|
||||
|
||||
class PartTestTemplateList(generics.ListCreateAPIView):
|
||||
"""
|
||||
API endpoint for listing (and creating) a PartTestTemplate.
|
||||
@ -337,8 +368,9 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
|
||||
# By default, include 'category_detail' information in the detail view
|
||||
try:
|
||||
kwargs['category_detail'] = str2bool(self.request.query_params.get('category_detail', False))
|
||||
kwargs['category_detail'] = str2bool(self.request.query_params.get('category_detail', True))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@ -383,8 +415,90 @@ class PartDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
return response
|
||||
|
||||
|
||||
class PartFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filters for the PartList endpoint.
|
||||
Uses the django_filters extension framework
|
||||
"""
|
||||
|
||||
# Filter by parts which have (or not) an IPN value
|
||||
has_ipn = rest_filters.BooleanFilter(label='Has IPN', method='filter_has_ipn')
|
||||
|
||||
def filter_has_ipn(self, queryset, name, value):
|
||||
|
||||
value = str2bool(value)
|
||||
|
||||
if value:
|
||||
queryset = queryset.exclude(IPN='')
|
||||
else:
|
||||
queryset = queryset.filter(IPN='')
|
||||
|
||||
# Regex filter for name
|
||||
name_regex = rest_filters.CharFilter(label='Filter by name (regex)', field_name='name', lookup_expr='iregex')
|
||||
|
||||
# Exact match for IPN
|
||||
IPN = rest_filters.CharFilter(
|
||||
label='Filter by exact IPN (internal part number)',
|
||||
field_name='IPN',
|
||||
lookup_expr="iexact"
|
||||
)
|
||||
|
||||
# Regex match for IPN
|
||||
IPN_regex = rest_filters.CharFilter(label='Filter by regex on IPN (internal part number)', field_name='IPN', lookup_expr='iregex')
|
||||
|
||||
# low_stock filter
|
||||
low_stock = rest_filters.BooleanFilter(label='Low stock', method='filter_low_stock')
|
||||
|
||||
def filter_low_stock(self, queryset, name, value):
|
||||
"""
|
||||
Filter by "low stock" status
|
||||
"""
|
||||
|
||||
value = str2bool(value)
|
||||
|
||||
if value:
|
||||
# Ignore any parts which do not have a specified 'minimum_stock' level
|
||||
queryset = queryset.exclude(minimum_stock=0)
|
||||
# Filter items which have an 'in_stock' level lower than 'minimum_stock'
|
||||
queryset = queryset.filter(Q(in_stock__lt=F('minimum_stock')))
|
||||
else:
|
||||
# Filter items which have an 'in_stock' level higher than 'minimum_stock'
|
||||
queryset = queryset.filter(Q(in_stock__gte=F('minimum_stock')))
|
||||
|
||||
return queryset
|
||||
|
||||
# has_stock filter
|
||||
has_stock = rest_filters.BooleanFilter(label='Has stock', method='filter_has_stock')
|
||||
|
||||
def filter_has_stock(self, queryset, name, value):
|
||||
|
||||
value = str2bool(value)
|
||||
|
||||
if value:
|
||||
queryset = queryset.filter(Q(in_stock__gt=0))
|
||||
else:
|
||||
queryset = queryset.filter(Q(in_stock__lte=0))
|
||||
|
||||
return queryset
|
||||
|
||||
is_template = rest_filters.BooleanFilter()
|
||||
|
||||
assembly = rest_filters.BooleanFilter()
|
||||
|
||||
component = rest_filters.BooleanFilter()
|
||||
|
||||
trackable = rest_filters.BooleanFilter()
|
||||
|
||||
purchaseable = rest_filters.BooleanFilter()
|
||||
|
||||
salable = rest_filters.BooleanFilter()
|
||||
|
||||
active = rest_filters.BooleanFilter()
|
||||
|
||||
|
||||
class PartList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of Part objects
|
||||
"""
|
||||
API endpoint for accessing a list of Part objects
|
||||
|
||||
- GET: Return list of objects
|
||||
- POST: Create a new Part object
|
||||
@ -405,8 +519,8 @@ class PartList(generics.ListCreateAPIView):
|
||||
"""
|
||||
|
||||
serializer_class = part_serializers.PartSerializer
|
||||
|
||||
queryset = Part.objects.all()
|
||||
filterset_class = PartFilter
|
||||
|
||||
starred_parts = None
|
||||
|
||||
@ -447,7 +561,7 @@ class PartList(generics.ListCreateAPIView):
|
||||
# Do we wish to include PartCategory detail?
|
||||
if str2bool(request.query_params.get('category_detail', False)):
|
||||
|
||||
# Work out which part categorie we need to query
|
||||
# Work out which part categories we need to query
|
||||
category_ids = set()
|
||||
|
||||
for part in data:
|
||||
@ -519,6 +633,10 @@ class PartList(generics.ListCreateAPIView):
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
# Annotate calculated data to the queryset
|
||||
# (This will be used for further filtering)
|
||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
# Filter by "uses" query - Limit to parts which use the provided part
|
||||
@ -545,17 +663,6 @@ class PartList(generics.ListCreateAPIView):
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by whether the part has an IPN (internal part number) defined
|
||||
has_ipn = params.get('has_ipn', None)
|
||||
|
||||
if has_ipn is not None:
|
||||
has_ipn = str2bool(has_ipn)
|
||||
|
||||
if has_ipn:
|
||||
queryset = queryset.exclude(IPN='')
|
||||
else:
|
||||
queryset = queryset.filter(IPN='')
|
||||
|
||||
# Filter by whether the BOM has been validated (or not)
|
||||
bom_valid = params.get('bom_valid', None)
|
||||
|
||||
@ -621,35 +728,14 @@ class PartList(generics.ListCreateAPIView):
|
||||
except (ValueError, PartCategory.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Annotate calculated data to the queryset
|
||||
# (This will be used for further filtering)
|
||||
queryset = part_serializers.PartSerializer.annotate_queryset(queryset)
|
||||
# Filer by 'depleted_stock' status -> has no stock and stock items
|
||||
depleted_stock = params.get('depleted_stock', None)
|
||||
|
||||
# Filter by whether the part has stock
|
||||
has_stock = params.get("has_stock", None)
|
||||
if depleted_stock is not None:
|
||||
depleted_stock = str2bool(depleted_stock)
|
||||
|
||||
if has_stock is not None:
|
||||
has_stock = str2bool(has_stock)
|
||||
|
||||
if has_stock:
|
||||
queryset = queryset.filter(Q(in_stock__gt=0))
|
||||
else:
|
||||
queryset = queryset.filter(Q(in_stock__lte=0))
|
||||
|
||||
# If we are filtering by 'low_stock' status
|
||||
low_stock = params.get('low_stock', None)
|
||||
|
||||
if low_stock is not None:
|
||||
low_stock = str2bool(low_stock)
|
||||
|
||||
if low_stock:
|
||||
# Ignore any parts which do not have a specified 'minimum_stock' level
|
||||
queryset = queryset.exclude(minimum_stock=0)
|
||||
# Filter items which have an 'in_stock' level lower than 'minimum_stock'
|
||||
queryset = queryset.filter(Q(in_stock__lt=F('minimum_stock')))
|
||||
else:
|
||||
# Filter items which have an 'in_stock' level higher than 'minimum_stock'
|
||||
queryset = queryset.filter(Q(in_stock__gte=F('minimum_stock')))
|
||||
if depleted_stock:
|
||||
queryset = queryset.filter(Q(in_stock=0) & ~Q(stock_item_count=0))
|
||||
|
||||
# Filter by "parts which need stock to complete build"
|
||||
stock_to_build = params.get('stock_to_build', None)
|
||||
@ -691,14 +777,7 @@ class PartList(generics.ListCreateAPIView):
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'is_template',
|
||||
'variant_of',
|
||||
'assembly',
|
||||
'component',
|
||||
'trackable',
|
||||
'purchaseable',
|
||||
'salable',
|
||||
'active',
|
||||
]
|
||||
|
||||
ordering_fields = [
|
||||
@ -770,14 +849,54 @@ class PartParameterDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = part_serializers.PartParameterSerializer
|
||||
|
||||
|
||||
class BomFilter(rest_filters.FilterSet):
|
||||
"""
|
||||
Custom filters for the BOM list
|
||||
"""
|
||||
|
||||
# Boolean filters for BOM item
|
||||
optional = rest_filters.BooleanFilter(label='BOM line is optional')
|
||||
inherited = rest_filters.BooleanFilter(label='BOM line is inherited')
|
||||
allow_variants = rest_filters.BooleanFilter(label='Variants are allowed')
|
||||
|
||||
validated = rest_filters.BooleanFilter(label='BOM line has been validated', method='filter_validated')
|
||||
|
||||
def filter_validated(self, queryset, name, value):
|
||||
|
||||
# Work out which lines have actually been validated
|
||||
pks = []
|
||||
|
||||
for bom_item in queryset.all():
|
||||
if bom_item.is_line_valid():
|
||||
pks.append(bom_item.pk)
|
||||
|
||||
if str2bool(value):
|
||||
queryset = queryset.filter(pk__in=pks)
|
||||
else:
|
||||
queryset = queryset.exclude(pk__in=pks)
|
||||
|
||||
return queryset
|
||||
|
||||
# Filters for linked 'part'
|
||||
part_active = rest_filters.BooleanFilter(label='Master part is active', field_name='part__active')
|
||||
part_trackable = rest_filters.BooleanFilter(label='Master part is trackable', field_name='part__trackable')
|
||||
|
||||
# Filters for linked 'sub_part'
|
||||
sub_part_trackable = rest_filters.BooleanFilter(label='Sub part is trackable', field_name='sub_part__trackable')
|
||||
sub_part_assembly = rest_filters.BooleanFilter(label='Sub part is an assembly', field_name='sub_part__assembly')
|
||||
|
||||
|
||||
class BomList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of BomItem objects.
|
||||
"""
|
||||
API endpoint for accessing a list of BomItem objects.
|
||||
|
||||
- GET: Return list of BomItem objects
|
||||
- POST: Create a new BomItem object
|
||||
"""
|
||||
|
||||
serializer_class = part_serializers.BomItemSerializer
|
||||
queryset = BomItem.objects.all()
|
||||
filterset_class = BomFilter
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
@ -824,30 +943,6 @@ class BomList(generics.ListCreateAPIView):
|
||||
|
||||
params = self.request.query_params
|
||||
|
||||
# Filter by "optional" status?
|
||||
optional = params.get('optional', None)
|
||||
|
||||
if optional is not None:
|
||||
optional = str2bool(optional)
|
||||
|
||||
queryset = queryset.filter(optional=optional)
|
||||
|
||||
# Filter by "inherited" status
|
||||
inherited = params.get('inherited', None)
|
||||
|
||||
if inherited is not None:
|
||||
inherited = str2bool(inherited)
|
||||
|
||||
queryset = queryset.filter(inherited=inherited)
|
||||
|
||||
# Filter by "allow_variants"
|
||||
variants = params.get("allow_variants", None)
|
||||
|
||||
if variants is not None:
|
||||
variants = str2bool(variants)
|
||||
|
||||
queryset = queryset.filter(allow_variants=variants)
|
||||
|
||||
# Filter by part?
|
||||
part = params.get('part', None)
|
||||
|
||||
@ -870,45 +965,6 @@ class BomList(generics.ListCreateAPIView):
|
||||
except (ValueError, Part.DoesNotExist):
|
||||
pass
|
||||
|
||||
# Filter by "active" status of the part
|
||||
part_active = params.get('part_active', None)
|
||||
|
||||
if part_active is not None:
|
||||
part_active = str2bool(part_active)
|
||||
queryset = queryset.filter(part__active=part_active)
|
||||
|
||||
# Filter by "trackable" status of the part
|
||||
part_trackable = params.get('part_trackable', None)
|
||||
|
||||
if part_trackable is not None:
|
||||
part_trackable = str2bool(part_trackable)
|
||||
queryset = queryset.filter(part__trackable=part_trackable)
|
||||
|
||||
# Filter by "trackable" status of the sub-part
|
||||
sub_part_trackable = params.get('sub_part_trackable', None)
|
||||
|
||||
if sub_part_trackable is not None:
|
||||
sub_part_trackable = str2bool(sub_part_trackable)
|
||||
queryset = queryset.filter(sub_part__trackable=sub_part_trackable)
|
||||
|
||||
# Filter by whether the BOM line has been validated
|
||||
validated = params.get('validated', None)
|
||||
|
||||
if validated is not None:
|
||||
validated = str2bool(validated)
|
||||
|
||||
# Work out which lines have actually been validated
|
||||
pks = []
|
||||
|
||||
for bom_item in queryset.all():
|
||||
if bom_item.is_line_valid:
|
||||
pks.append(bom_item.pk)
|
||||
|
||||
if validated:
|
||||
queryset = queryset.filter(pk__in=pks)
|
||||
else:
|
||||
queryset = queryset.exclude(pk__in=pks)
|
||||
|
||||
# Annotate with purchase prices
|
||||
queryset = queryset.annotate(
|
||||
purchase_price_min=Min('sub_part__stock_items__purchase_price'),
|
||||
@ -1023,11 +1079,13 @@ part_api_urls = [
|
||||
|
||||
# Base URL for PartTestTemplate API endpoints
|
||||
url(r'^test-template/', include([
|
||||
url(r'^(?P<pk>\d+)/', PartTestTemplateDetail.as_view(), name='api-part-test-template-detail'),
|
||||
url(r'^$', PartTestTemplateList.as_view(), name='api-part-test-template-list'),
|
||||
])),
|
||||
|
||||
# Base URL for PartAttachment API endpoints
|
||||
url(r'^attachment/', include([
|
||||
url(r'^(?P<pk>\d+)/', PartAttachmentDetail.as_view(), name='api-part-attachment-detail'),
|
||||
url(r'^$', PartAttachmentList.as_view(), name='api-part-attachment-list'),
|
||||
])),
|
||||
|
||||
@ -1043,10 +1101,10 @@ part_api_urls = [
|
||||
|
||||
# Base URL for PartParameter API endpoints
|
||||
url(r'^parameter/', include([
|
||||
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),
|
||||
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'),
|
||||
|
||||
url(r'^(?P<pk>\d+)/', PartParameterDetail.as_view(), name='api-part-param-detail'),
|
||||
url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'),
|
||||
url(r'^(?P<pk>\d+)/', PartParameterDetail.as_view(), name='api-part-parameter-detail'),
|
||||
url(r'^.*$', PartParameterList.as_view(), name='api-part-parameter-list'),
|
||||
])),
|
||||
|
||||
url(r'^thumbs/', include([
|
||||
@ -1054,7 +1112,7 @@ part_api_urls = [
|
||||
url(r'^(?P<pk>\d+)/?', PartThumbsUpdate.as_view(), name='api-part-thumbs-update'),
|
||||
])),
|
||||
|
||||
url(r'^(?P<pk>\d+)/?', PartDetail.as_view(), name='api-part-detail'),
|
||||
url(r'^(?P<pk>\d+)/', PartDetail.as_view(), name='api-part-detail'),
|
||||
|
||||
url(r'^.*$', PartList.as_view(), name='api-part-list'),
|
||||
]
|
||||
|
@ -3,14 +3,9 @@ Functionality for Bill of Material (BOM) management.
|
||||
Primarily BOM upload tools.
|
||||
"""
|
||||
|
||||
from rapidfuzz import fuzz
|
||||
import tablib
|
||||
import os
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from InvenTree.helpers import DownloadFile, GetExportFormats
|
||||
|
||||
@ -145,11 +140,16 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
||||
stock_data = []
|
||||
# Get part default location
|
||||
try:
|
||||
stock_data.append(bom_item.sub_part.get_default_location().name)
|
||||
loc = bom_item.sub_part.get_default_location()
|
||||
|
||||
if loc is not None:
|
||||
stock_data.append(str(loc.name))
|
||||
else:
|
||||
stock_data.append('')
|
||||
except AttributeError:
|
||||
stock_data.append('')
|
||||
# Get part current stock
|
||||
stock_data.append(bom_item.sub_part.available_stock)
|
||||
stock_data.append(str(bom_item.sub_part.available_stock))
|
||||
|
||||
for s_idx, header in enumerate(stock_headers):
|
||||
try:
|
||||
@ -323,177 +323,6 @@ def ExportBom(part, fmt='csv', cascade=False, max_levels=None, parameter_data=Fa
|
||||
|
||||
data = dataset.export(fmt)
|
||||
|
||||
filename = '{n}_BOM.{fmt}'.format(n=part.full_name, fmt=fmt)
|
||||
filename = f"{part.full_name}_BOM.{fmt}"
|
||||
|
||||
return DownloadFile(data, filename)
|
||||
|
||||
|
||||
class BomUploadManager:
|
||||
""" Class for managing an uploaded BOM file """
|
||||
|
||||
# Fields which are absolutely necessary for valid upload
|
||||
REQUIRED_HEADERS = [
|
||||
'Quantity'
|
||||
]
|
||||
|
||||
# Fields which are used for part matching (only one of them is needed)
|
||||
PART_MATCH_HEADERS = [
|
||||
'Part_Name',
|
||||
'Part_IPN',
|
||||
'Part_ID',
|
||||
]
|
||||
|
||||
# Fields which would be helpful but are not required
|
||||
OPTIONAL_HEADERS = [
|
||||
'Reference',
|
||||
'Note',
|
||||
'Overage',
|
||||
]
|
||||
|
||||
EDITABLE_HEADERS = [
|
||||
'Reference',
|
||||
'Note',
|
||||
'Overage'
|
||||
]
|
||||
|
||||
HEADERS = REQUIRED_HEADERS + PART_MATCH_HEADERS + OPTIONAL_HEADERS
|
||||
|
||||
def __init__(self, bom_file):
|
||||
""" Initialize the BomUpload class with a user-uploaded file object """
|
||||
|
||||
self.process(bom_file)
|
||||
|
||||
def process(self, bom_file):
|
||||
""" Process a BOM file """
|
||||
|
||||
self.data = None
|
||||
|
||||
ext = os.path.splitext(bom_file.name)[-1].lower()
|
||||
|
||||
if ext in ['.csv', '.tsv', ]:
|
||||
# These file formats need string decoding
|
||||
raw_data = bom_file.read().decode('utf-8')
|
||||
elif ext in ['.xls', '.xlsx']:
|
||||
raw_data = bom_file.read()
|
||||
else:
|
||||
raise ValidationError({'bom_file': _('Unsupported file format: {f}').format(f=ext)})
|
||||
|
||||
try:
|
||||
self.data = tablib.Dataset().load(raw_data)
|
||||
except tablib.UnsupportedFormat:
|
||||
raise ValidationError({'bom_file': _('Error reading BOM file (invalid data)')})
|
||||
except tablib.core.InvalidDimensions:
|
||||
raise ValidationError({'bom_file': _('Error reading BOM file (incorrect row size)')})
|
||||
|
||||
def guess_header(self, header, threshold=80):
|
||||
""" Try to match a header (from the file) to a list of known headers
|
||||
|
||||
Args:
|
||||
header - Header name to look for
|
||||
threshold - Match threshold for fuzzy search
|
||||
"""
|
||||
|
||||
# Try for an exact match
|
||||
for h in self.HEADERS:
|
||||
if h == header:
|
||||
return h
|
||||
|
||||
# Try for a case-insensitive match
|
||||
for h in self.HEADERS:
|
||||
if h.lower() == header.lower():
|
||||
return h
|
||||
|
||||
# Try for a case-insensitive match with space replacement
|
||||
for h in self.HEADERS:
|
||||
if h.lower() == header.lower().replace(' ', '_'):
|
||||
return h
|
||||
|
||||
# Finally, look for a close match using fuzzy matching
|
||||
matches = []
|
||||
|
||||
for h in self.HEADERS:
|
||||
ratio = fuzz.partial_ratio(header, h)
|
||||
if ratio > threshold:
|
||||
matches.append({'header': h, 'match': ratio})
|
||||
|
||||
if len(matches) > 0:
|
||||
matches = sorted(matches, key=lambda item: item['match'], reverse=True)
|
||||
return matches[0]['header']
|
||||
|
||||
return None
|
||||
|
||||
def columns(self):
|
||||
""" Return a list of headers for the thingy """
|
||||
headers = []
|
||||
|
||||
for header in self.data.headers:
|
||||
headers.append({
|
||||
'name': header,
|
||||
'guess': self.guess_header(header)
|
||||
})
|
||||
|
||||
return headers
|
||||
|
||||
def col_count(self):
|
||||
if self.data is None:
|
||||
return 0
|
||||
|
||||
return len(self.data.headers)
|
||||
|
||||
def row_count(self):
|
||||
""" Return the number of rows in the file. """
|
||||
|
||||
if self.data is None:
|
||||
return 0
|
||||
|
||||
return len(self.data)
|
||||
|
||||
def rows(self):
|
||||
""" Return a list of all rows """
|
||||
rows = []
|
||||
|
||||
for i in range(self.row_count()):
|
||||
|
||||
data = [item for item in self.get_row_data(i)]
|
||||
|
||||
# Is the row completely empty? Skip!
|
||||
empty = True
|
||||
|
||||
for idx, item in enumerate(data):
|
||||
if len(str(item).strip()) > 0:
|
||||
empty = False
|
||||
|
||||
try:
|
||||
# Excel import casts number-looking-items into floats, which is annoying
|
||||
if item == int(item) and not str(item) == str(int(item)):
|
||||
data[idx] = int(item)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Skip empty rows
|
||||
if empty:
|
||||
continue
|
||||
|
||||
row = {
|
||||
'data': data,
|
||||
'index': i
|
||||
}
|
||||
|
||||
rows.append(row)
|
||||
|
||||
return rows
|
||||
|
||||
def get_row_data(self, index):
|
||||
""" Retrieve row data at a particular index """
|
||||
if self.data is None or index >= len(self.data):
|
||||
return None
|
||||
|
||||
return self.data[index]
|
||||
|
||||
def get_row_dict(self, index):
|
||||
""" Retrieve a dict object representing the data row at a particular offset """
|
||||
|
||||
if self.data is None or index >= len(self.data):
|
||||
return None
|
||||
|
||||
return self.data.dict[index]
|
||||
|
@ -5,21 +5,22 @@ Django Forms for interacting with Part objects
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.helpers import GetExportFormats
|
||||
from InvenTree.fields import RoundingDecimalFormField
|
||||
|
||||
from mptt.fields import TreeNodeChoiceField
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import common.models
|
||||
from mptt.fields import TreeNodeChoiceField
|
||||
|
||||
from .models import Part, PartCategory, PartAttachment, PartRelated
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.helpers import GetExportFormats, clean_decimal
|
||||
from InvenTree.fields import RoundingDecimalFormField
|
||||
|
||||
import common.models
|
||||
from common.forms import MatchItemForm
|
||||
|
||||
from .models import Part, PartCategory, PartRelated
|
||||
from .models import BomItem
|
||||
from .models import PartParameterTemplate, PartParameter
|
||||
from .models import PartCategoryParameterTemplate
|
||||
from .models import PartTestTemplate
|
||||
from .models import PartSellPriceBreak, PartInternalPriceBreak
|
||||
|
||||
|
||||
@ -55,32 +56,6 @@ class PartImageDownloadForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class PartImageForm(HelperForm):
|
||||
""" Form for uploading a Part image """
|
||||
|
||||
class Meta:
|
||||
model = Part
|
||||
fields = [
|
||||
'image',
|
||||
]
|
||||
|
||||
|
||||
class EditPartTestTemplateForm(HelperForm):
|
||||
""" Class for creating / editing a PartTestTemplate object """
|
||||
|
||||
class Meta:
|
||||
model = PartTestTemplate
|
||||
|
||||
fields = [
|
||||
'part',
|
||||
'test_name',
|
||||
'description',
|
||||
'required',
|
||||
'requires_value',
|
||||
'requires_attachment',
|
||||
]
|
||||
|
||||
|
||||
class BomExportForm(forms.Form):
|
||||
""" Simple form to let user set BOM export options,
|
||||
before exporting a BOM (bill of materials) file.
|
||||
@ -159,16 +134,28 @@ class BomValidateForm(HelperForm):
|
||||
]
|
||||
|
||||
|
||||
class BomUploadSelectFile(HelperForm):
|
||||
""" Form for importing a BOM. Provides a file input box for upload """
|
||||
class BomMatchItemForm(MatchItemForm):
|
||||
""" Override MatchItemForm fields """
|
||||
|
||||
bom_file = forms.FileField(label=_('BOM file'), required=True, help_text=_("Select BOM file to upload"))
|
||||
def get_special_field(self, col_guess, row, file_manager):
|
||||
""" Set special fields """
|
||||
|
||||
class Meta:
|
||||
model = Part
|
||||
fields = [
|
||||
'bom_file',
|
||||
]
|
||||
# set quantity field
|
||||
if 'quantity' in col_guess.lower():
|
||||
return forms.CharField(
|
||||
required=False,
|
||||
widget=forms.NumberInput(attrs={
|
||||
'name': 'quantity' + str(row['index']),
|
||||
'class': 'numberinput',
|
||||
'type': 'number',
|
||||
'min': '0',
|
||||
'step': 'any',
|
||||
'value': clean_decimal(row.get('quantity', '')),
|
||||
})
|
||||
)
|
||||
|
||||
# return default
|
||||
return super().get_special_field(col_guess, row, file_manager)
|
||||
|
||||
|
||||
class CreatePartRelatedForm(HelperForm):
|
||||
@ -185,18 +172,6 @@ class CreatePartRelatedForm(HelperForm):
|
||||
}
|
||||
|
||||
|
||||
class EditPartAttachmentForm(HelperForm):
|
||||
""" Form for editing a PartAttachment object """
|
||||
|
||||
class Meta:
|
||||
model = PartAttachment
|
||||
fields = [
|
||||
'part',
|
||||
'attachment',
|
||||
'comment'
|
||||
]
|
||||
|
||||
|
||||
class SetPartCategoryForm(forms.Form):
|
||||
""" Form for setting the category of multiple Part objects """
|
||||
|
||||
@ -242,6 +217,11 @@ class EditPartForm(HelperForm):
|
||||
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 = [
|
||||
@ -263,6 +243,7 @@ class EditPartForm(HelperForm):
|
||||
'default_expiry',
|
||||
'units',
|
||||
'minimum_stock',
|
||||
'initial_stock',
|
||||
'component',
|
||||
'assembly',
|
||||
'is_template',
|
||||
|
35
InvenTree/part/migrations/0069_auto_20210701_0509.py
Normal file
35
InvenTree/part/migrations/0069_auto_20210701_0509.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-01 05:09
|
||||
|
||||
import InvenTree.fields
|
||||
from django.db import migrations
|
||||
import djmoney.models.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0068_part_unique_part'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='partinternalpricebreak',
|
||||
name='price',
|
||||
field=InvenTree.fields.InvenTreeModelMoneyField(currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='partinternalpricebreak',
|
||||
name='price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='partsellpricebreak',
|
||||
name='price',
|
||||
field=InvenTree.fields.InvenTreeModelMoneyField(currency_choices=[], decimal_places=4, default_currency='', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='partsellpricebreak',
|
||||
name='price_currency',
|
||||
field=djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3),
|
||||
),
|
||||
]
|
19
InvenTree/part/migrations/0070_alter_part_variant_of.py
Normal file
19
InvenTree/part/migrations/0070_alter_part_variant_of.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-08 07:02
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('part', '0069_auto_20210701_0509'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='part',
|
||||
name='variant_of',
|
||||
field=models.ForeignKey(blank=True, help_text='Is this part a variant of another part?', limit_choices_to={'is_template': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='part.part', verbose_name='Variant Of'),
|
||||
),
|
||||
]
|
@ -30,7 +30,7 @@ from mptt.models import TreeForeignKey, MPTTModel
|
||||
|
||||
from stdimage.models import StdImageField
|
||||
|
||||
from decimal import Decimal
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from datetime import datetime
|
||||
from rapidfuzz import fuzz
|
||||
import hashlib
|
||||
@ -39,7 +39,7 @@ from InvenTree import helpers
|
||||
from InvenTree import validators
|
||||
from InvenTree.models import InvenTreeTree, InvenTreeAttachment
|
||||
from InvenTree.fields import InvenTreeURLField
|
||||
from InvenTree.helpers import decimal2string, normalize
|
||||
from InvenTree.helpers import decimal2string, normalize, decimal2money
|
||||
|
||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
|
||||
|
||||
@ -75,6 +75,10 @@ class PartCategory(InvenTreeTree):
|
||||
|
||||
default_keywords = models.CharField(null=True, blank=True, max_length=250, verbose_name=_('Default keywords'), help_text=_('Default keywords for parts in this category'))
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-category-list')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('category-detail', kwargs={'pk': self.id})
|
||||
|
||||
@ -329,6 +333,11 @@ class Part(MPTTModel):
|
||||
# For legacy reasons the 'variant_of' field is used to indicate the MPTT parent
|
||||
parent_attr = 'variant_of'
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
|
||||
return reverse('api-part-list')
|
||||
|
||||
def get_context_data(self, request, **kwargs):
|
||||
"""
|
||||
Return some useful context data about this part for template rendering
|
||||
@ -683,7 +692,6 @@ class Part(MPTTModel):
|
||||
null=True, blank=True,
|
||||
limit_choices_to={
|
||||
'is_template': True,
|
||||
'active': True,
|
||||
},
|
||||
on_delete=models.SET_NULL,
|
||||
help_text=_('Is this part a variant of another part?'),
|
||||
@ -1479,16 +1487,17 @@ class Part(MPTTModel):
|
||||
|
||||
return True
|
||||
|
||||
def get_price_info(self, quantity=1, buy=True, bom=True):
|
||||
def get_price_info(self, quantity=1, buy=True, bom=True, internal=False):
|
||||
""" Return a simplified pricing string for this part
|
||||
|
||||
Args:
|
||||
quantity: Number of units to calculate price for
|
||||
buy: Include supplier pricing (default = True)
|
||||
bom: Include BOM pricing (default = True)
|
||||
internal: Include internal pricing (default = False)
|
||||
"""
|
||||
|
||||
price_range = self.get_price_range(quantity, buy, bom)
|
||||
price_range = self.get_price_range(quantity, buy, bom, internal)
|
||||
|
||||
if price_range is None:
|
||||
return None
|
||||
@ -1576,9 +1585,10 @@ class Part(MPTTModel):
|
||||
|
||||
- Supplier price (if purchased from suppliers)
|
||||
- BOM price (if built from other parts)
|
||||
- Internal price (if set for the part)
|
||||
|
||||
Returns:
|
||||
Minimum of the supplier price or BOM price. If no pricing available, returns None
|
||||
Minimum of the supplier, BOM or internal price. If no pricing available, returns None
|
||||
"""
|
||||
|
||||
# only get internal price if set and should be used
|
||||
@ -1966,6 +1976,10 @@ class PartAttachment(InvenTreeAttachment):
|
||||
Model for storing file attachments against a Part object
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-attachment-list')
|
||||
|
||||
def getSubdir(self):
|
||||
return os.path.join("part_files", str(self.part.id))
|
||||
|
||||
@ -1977,6 +1991,10 @@ class PartSellPriceBreak(common.models.PriceBreak):
|
||||
"""
|
||||
Represents a price break for selling this part
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-sale-price-list')
|
||||
|
||||
part = models.ForeignKey(
|
||||
Part, on_delete=models.CASCADE,
|
||||
@ -1994,6 +2012,10 @@ class PartInternalPriceBreak(common.models.PriceBreak):
|
||||
Represents a price break for internally selling this part
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-internal-price-list')
|
||||
|
||||
part = models.ForeignKey(
|
||||
Part, on_delete=models.CASCADE,
|
||||
related_name='internalpricebreaks',
|
||||
@ -2038,6 +2060,10 @@ class PartTestTemplate(models.Model):
|
||||
run on the model (refer to the validate_unique function).
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-test-template-list')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
self.clean()
|
||||
@ -2136,6 +2162,10 @@ class PartParameterTemplate(models.Model):
|
||||
units: The units of the Parameter [string]
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-parameter-template-list')
|
||||
|
||||
def __str__(self):
|
||||
s = str(self.name)
|
||||
if self.units:
|
||||
@ -2173,6 +2203,10 @@ class PartParameter(models.Model):
|
||||
data: The data (value) of the Parameter [string]
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-part-parameter-list')
|
||||
|
||||
def __str__(self):
|
||||
# String representation of a PartParameter (used in the admin interface)
|
||||
return "{part} : {param} = {data}{units}".format(
|
||||
@ -2264,6 +2298,10 @@ class BomItem(models.Model):
|
||||
allow_variants: Stock for part variants can be substituted for this BomItem
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_api_url():
|
||||
return reverse('api-bom-list')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
self.clean()
|
||||
@ -2380,6 +2418,15 @@ class BomItem(models.Model):
|
||||
- If the "sub_part" is trackable, then the "part" must be trackable too!
|
||||
"""
|
||||
|
||||
super().clean()
|
||||
|
||||
try:
|
||||
self.quantity = Decimal(self.quantity)
|
||||
except InvalidOperation:
|
||||
raise ValidationError({
|
||||
'quantity': _('Must be a valid number')
|
||||
})
|
||||
|
||||
try:
|
||||
# Check for circular BOM references
|
||||
if self.sub_part:
|
||||
@ -2412,7 +2459,7 @@ class BomItem(models.Model):
|
||||
return "{n} x {child} to make {parent}".format(
|
||||
parent=self.part.full_name,
|
||||
child=self.sub_part.full_name,
|
||||
n=helpers.decimal2string(self.quantity))
|
||||
n=decimal2string(self.quantity))
|
||||
|
||||
def available_stock(self):
|
||||
"""
|
||||
@ -2496,10 +2543,12 @@ class BomItem(models.Model):
|
||||
return required
|
||||
|
||||
@property
|
||||
def price_range(self):
|
||||
def price_range(self, internal=False):
|
||||
""" Return the price-range for this BOM item. """
|
||||
|
||||
prange = self.sub_part.get_price_range(self.quantity)
|
||||
# get internal price setting
|
||||
use_internal = common.models.InvenTreeSetting.get_setting('PART_BOM_USE_INTERNAL_PRICE', False)
|
||||
prange = self.sub_part.get_price_range(self.quantity, internal=use_internal and internal)
|
||||
|
||||
if prange is None:
|
||||
return prange
|
||||
@ -2507,11 +2556,11 @@ class BomItem(models.Model):
|
||||
pmin, pmax = prange
|
||||
|
||||
if pmin == pmax:
|
||||
return decimal2string(pmin)
|
||||
return decimal2money(pmin)
|
||||
|
||||
# Convert to better string representation
|
||||
pmin = decimal2string(pmin)
|
||||
pmax = decimal2string(pmax)
|
||||
pmin = decimal2money(pmin)
|
||||
pmax = decimal2money(pmax)
|
||||
|
||||
return "{pmin} to {pmax}".format(pmin=pmin, pmax=pmax)
|
||||
|
||||
|
@ -4,6 +4,7 @@ JSON serializers for Part app
|
||||
import imghdr
|
||||
from decimal import Decimal
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Coalesce
|
||||
@ -31,6 +32,8 @@ class CategorySerializer(InvenTreeModelSerializer):
|
||||
|
||||
parts = serializers.IntegerField(source='item_count', read_only=True)
|
||||
|
||||
level = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PartCategory
|
||||
fields = [
|
||||
@ -38,10 +41,12 @@ class CategorySerializer(InvenTreeModelSerializer):
|
||||
'name',
|
||||
'description',
|
||||
'default_location',
|
||||
'pathstring',
|
||||
'url',
|
||||
'default_keywords',
|
||||
'level',
|
||||
'parent',
|
||||
'parts',
|
||||
'pathstring',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
@ -59,7 +64,12 @@ class PartAttachmentSerializer(InvenTreeModelSerializer):
|
||||
'pk',
|
||||
'part',
|
||||
'attachment',
|
||||
'comment'
|
||||
'comment',
|
||||
'upload_date',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'upload_date',
|
||||
]
|
||||
|
||||
|
||||
@ -187,6 +197,9 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
Used when displaying all details of a single component.
|
||||
"""
|
||||
|
||||
def get_api_url(self):
|
||||
return reverse_lazy('api-part-list')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Custom initialization method for PartSerializer,
|
||||
@ -326,9 +339,10 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
'category',
|
||||
'category_detail',
|
||||
'component',
|
||||
'description',
|
||||
'default_location',
|
||||
'default_expiry',
|
||||
'default_location',
|
||||
'default_supplier',
|
||||
'description',
|
||||
'full_name',
|
||||
'image',
|
||||
'in_stock',
|
||||
@ -377,7 +391,7 @@ class PartStarSerializer(InvenTreeModelSerializer):
|
||||
class BomItemSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for BomItem object """
|
||||
|
||||
# price_range = serializers.CharField(read_only=True)
|
||||
price_range = serializers.CharField(read_only=True)
|
||||
|
||||
quantity = serializers.FloatField()
|
||||
|
||||
@ -492,24 +506,11 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
||||
'reference',
|
||||
'sub_part',
|
||||
'sub_part_detail',
|
||||
# 'price_range',
|
||||
'price_range',
|
||||
'validated',
|
||||
]
|
||||
|
||||
|
||||
class PartParameterSerializer(InvenTreeModelSerializer):
|
||||
""" JSON serializers for the PartParameter model """
|
||||
|
||||
class Meta:
|
||||
model = PartParameter
|
||||
fields = [
|
||||
'pk',
|
||||
'part',
|
||||
'template',
|
||||
'data'
|
||||
]
|
||||
|
||||
|
||||
class PartParameterTemplateSerializer(InvenTreeModelSerializer):
|
||||
""" JSON serializer for the PartParameterTemplate model """
|
||||
|
||||
@ -522,6 +523,22 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class PartParameterSerializer(InvenTreeModelSerializer):
|
||||
""" JSON serializers for the PartParameter model """
|
||||
|
||||
template_detail = PartParameterTemplateSerializer(source='template', many=False, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PartParameter
|
||||
fields = [
|
||||
'pk',
|
||||
'part',
|
||||
'template',
|
||||
'template_detail',
|
||||
'data'
|
||||
]
|
||||
|
||||
|
||||
class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
|
||||
""" Serializer for PartCategoryParameterTemplate """
|
||||
|
||||
|
@ -1,50 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load status_codes %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "part/navbar.html" with tab="allocation" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Build Order Allocations" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<table class='table table-striped table-condensed' id='build-order-table'></table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pre_content_panel %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Sales Order Allocations" %}</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
<table class='table table-striped table-condensed' id='sales-order-table'></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadSalesOrderAllocationTable("#sales-order-table", {
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
}
|
||||
});
|
||||
|
||||
loadBuildOrderAllocationTable("#build-order-table", {
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
}
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -1,69 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='attachments' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Part Attachments" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
{% include "attachment_table.html" with attachments=part.part_attachments %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
enableDragAndDrop(
|
||||
'#attachment-dropzone',
|
||||
"{% url 'part-attachment-create' %}",
|
||||
{
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
label: 'attachment',
|
||||
success: function(data, status, xhr) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$("#new-attachment").click(function() {
|
||||
launchModalForm("{% url 'part-attachment-create' %}?part={{ part.id }}",
|
||||
{
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$("#attachment-table").on('click', '.attachment-edit-button', function() {
|
||||
var button = $(this);
|
||||
|
||||
var url = `/part/attachment/${button.attr('pk')}/edit/`;
|
||||
|
||||
launchModalForm(url,
|
||||
{
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$("#attachment-table").on('click', '.attachment-delete-button', function() {
|
||||
var button = $(this);
|
||||
|
||||
var url = `/part/attachment/${button.attr('pk')}/delete/`;
|
||||
|
||||
launchModalForm(url, {
|
||||
success: function() {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#attachment-table").inventreeTable({
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -1,16 +0,0 @@
|
||||
{% extends "modal_delete_form.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
|
||||
{% trans "Are you sure you want to delete this BOM item?" %}
|
||||
<br>
|
||||
{% trans "Deleting this entry will remove the BOM row from the following part" %}:
|
||||
|
||||
<ul class='list-group'>
|
||||
<li class='list-group-item'>
|
||||
<b>{{ item.part.full_name }}</b> - <i>{{ item.part.description }}</i>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
@ -1,16 +1,11 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='bom' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Bill of Materials" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% if roles.part.change != True and editing_enabled %}
|
||||
<div class='alert alert-danger alert-block'>
|
||||
{% trans "You do not have permission to edit the BOM." %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if part.bom_checked_date %}
|
||||
{% if part.is_bom_valid %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
@ -26,7 +21,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div id='bom-button-toolbar'>
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
{% if editing_enabled %}
|
||||
<button class='btn btn-default' type='button' title='{% trans "Remove selected BOM items" %}' id='bom-item-delete'>
|
||||
@ -70,130 +65,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-bom table-condensed' data-toolbar="#button-toolbar" id='bom-table'>
|
||||
<table class='table table-bom table-condensed' data-toolbar="#bom-button-toolbar" id='bom-table'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
// Load the BOM table data
|
||||
loadBomTable($("#bom-table"), {
|
||||
editable: {{ editing_enabled }},
|
||||
bom_url: "{% url 'api-bom-list' %}",
|
||||
part_url: "{% url 'api-part-list' %}",
|
||||
parent_id: {{ part.id }} ,
|
||||
sub_part_detail: true,
|
||||
});
|
||||
|
||||
linkButtonsToSelection($("#bom-table"),
|
||||
[
|
||||
"#bom-item-delete",
|
||||
]
|
||||
);
|
||||
|
||||
{% if editing_enabled %}
|
||||
$("#editing-finished").click(function() {
|
||||
location.href = "{% url 'part-bom' part.id %}";
|
||||
});
|
||||
|
||||
$('#bom-item-delete').click(function() {
|
||||
|
||||
// Get a list of the selected BOM items
|
||||
var rows = $("#bom-table").bootstrapTable('getSelections');
|
||||
|
||||
// TODO - In the future, display (in the dialog) which items are going to be deleted
|
||||
|
||||
showQuestionDialog(
|
||||
'{% trans "Delete selected BOM items?" %}',
|
||||
'{% trans "All selected BOM items will be deleted" %}',
|
||||
{
|
||||
accept: function() {
|
||||
|
||||
// Keep track of each DELETE request
|
||||
var requests = [];
|
||||
|
||||
rows.forEach(function(row) {
|
||||
requests.push(
|
||||
inventreeDelete(
|
||||
`/api/bom/${row.pk}/`,
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// Wait for *all* the requests to complete
|
||||
$.when.apply($, requests).then(function() {
|
||||
$('#bom-table').bootstrapTable('refresh');
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#bom-upload').click(function() {
|
||||
location.href = "{% url 'upload-bom' part.id %}";
|
||||
});
|
||||
|
||||
$('#bom-duplicate').click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'duplicate-bom' part.id %}",
|
||||
{
|
||||
success: function() {
|
||||
$('#bom-table').bootstrapTable('refresh');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#bom-item-new").click(function () {
|
||||
launchModalForm(
|
||||
"{% url 'bom-item-create' %}?parent={{ part.id }}",
|
||||
{
|
||||
success: function() {
|
||||
$("#bom-table").bootstrapTable('refresh');
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
field: 'sub_part',
|
||||
label: '{% trans "New Part" %}',
|
||||
title: '{% trans "Create New Part" %}',
|
||||
url: "{% url 'part-create' %}",
|
||||
},
|
||||
]
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
{% else %}
|
||||
|
||||
$("#validate-bom").click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'bom-validate' part.id %}",
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#edit-bom").click(function () {
|
||||
location.href = "{% url 'part-bom' part.id %}?edit=1";
|
||||
});
|
||||
|
||||
$("#download-bom").click(function () {
|
||||
launchModalForm("{% url 'bom-export' part.id %}",
|
||||
{
|
||||
success: function(response) {
|
||||
location.href = response.url;
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
{% endif %}
|
||||
|
||||
$("#print-bom-report").click(function() {
|
||||
printBomReports([{{ part.pk }}]);
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
{% endif %}
|
99
InvenTree/part/templates/part/bom_upload/match_fields.html
Normal file
99
InvenTree/part/templates/part/bom_upload/match_fields.html
Normal file
@ -0,0 +1,99 @@
|
||||
{% extends "part/bom_upload/upload_file.html" %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block form_alert %}
|
||||
{% if missing_columns and missing_columns|length > 0 %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Missing selections for the following required columns" %}:
|
||||
<br>
|
||||
<ul>
|
||||
{% for col in missing_columns %}
|
||||
<li>{{ col }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if duplicates and duplicates|length > 0 %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Duplicate selections found, see below. Fix them then retry submitting." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock form_alert %}
|
||||
|
||||
{% block form_buttons_top %}
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="save btn btn-default">{% trans "Submit Selections" %}</button>
|
||||
{% endblock form_buttons_top %}
|
||||
|
||||
{% block form_content %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "File Fields" %}</th>
|
||||
<th></th>
|
||||
{% for col in form %}
|
||||
<th>
|
||||
<div>
|
||||
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
|
||||
{{ col.name }}
|
||||
<button class='btn btn-default btn-remove' onClick='removeColFromBomWizard()' id='del_col_{{ forloop.counter0 }}' style='display: inline; float: right;' title='{% trans "Remove column" %}'>
|
||||
<span col_id='{{ forloop.counter0 }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{% trans "Match Fields" %}</td>
|
||||
<td></td>
|
||||
{% for col in form %}
|
||||
<td>
|
||||
{{ col }}
|
||||
{% for duplicate in duplicates %}
|
||||
{% if duplicate == col.value %}
|
||||
<div class='alert alert-danger alert-block text-center' role='alert' style='padding:2px; margin-top:6px; margin-bottom:2px'>
|
||||
<b>{% trans "Duplicate selection" %}</b>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for row in rows %}
|
||||
{% with forloop.counter as row_index %}
|
||||
<tr>
|
||||
<td style='width: 32px;'>
|
||||
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ row_index }}' style='display: inline; float: left;' title='{% trans "Remove row" %}'>
|
||||
<span row_id='{{ row_index }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</td>
|
||||
<td style='text-align: left;'>{{ row_index }}</td>
|
||||
{% for item in row.data %}
|
||||
<td>
|
||||
<input type='hidden' name='row_{{ row_index }}_col_{{ forloop.counter0 }}' value='{{ item }}'/>
|
||||
{{ item }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endblock form_content %}
|
||||
|
||||
{% block form_buttons_bottom %}
|
||||
{% endblock form_buttons_bottom %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('.fieldselect').select2({
|
||||
width: '100%',
|
||||
matcher: partialMatcher,
|
||||
});
|
||||
|
||||
{% endblock %}
|
127
InvenTree/part/templates/part/bom_upload/match_parts.html
Normal file
127
InvenTree/part/templates/part/bom_upload/match_parts.html
Normal file
@ -0,0 +1,127 @@
|
||||
{% extends "part/bom_upload/upload_file.html" %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block form_alert %}
|
||||
{% if form.errors %}
|
||||
{% endif %}
|
||||
{% if form_errors %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Errors exist in the submitted data" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock form_alert %}
|
||||
|
||||
{% block form_buttons_top %}
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="save btn btn-default">{% trans "Submit Selections" %}</button>
|
||||
{% endblock form_buttons_top %}
|
||||
|
||||
{% block form_content %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans "Row" %}</th>
|
||||
<th>{% trans "Select Part" %}</th>
|
||||
<th>{% trans "Reference" %}</th>
|
||||
<th>{% trans "Quantity" %}</th>
|
||||
{% for col in columns %}
|
||||
{% if col.guess != 'Quantity' %}
|
||||
<th>
|
||||
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
|
||||
<input type='hidden' name='col_guess_{{ forloop.counter0 }}' value='{{ col.guess }}'/>
|
||||
{% if col.guess %}
|
||||
{{ col.guess }}
|
||||
{% else %}
|
||||
{{ col.name }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr></tr> {% comment %} Dummy row for javascript del_row method {% endcomment %}
|
||||
{% for row in rows %}
|
||||
<tr {% if row.errors %} style='background: #ffeaea;'{% endif %} part-select='#select_part_{{ row.index }}'>
|
||||
<td>
|
||||
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ row.index }}' style='display: inline; float: right;' title='{% trans "Remove row" %}'>
|
||||
<span row_id='{{ row.index }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
{% add row.index 1 %}
|
||||
</td>
|
||||
<td>
|
||||
{% for field in form.visible_fields %}
|
||||
{% if field.name == row.item_select %}
|
||||
{{ field }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if row.errors.part %}
|
||||
<p class='help-inline'>{{ row.errors.part }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% for field in form.visible_fields %}
|
||||
{% if field.name == row.reference %}
|
||||
{{ field|as_crispy_field }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if row.errors.reference %}
|
||||
<p class='help-inline'>{{ row.errors.reference }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% for field in form.visible_fields %}
|
||||
{% if field.name == row.quantity %}
|
||||
{{ field|as_crispy_field }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if row.errors.quantity %}
|
||||
<p class='help-inline'>{{ row.errors.quantity }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% for item in row.data %}
|
||||
{% if item.column.guess != 'Quantity' %}
|
||||
<td>
|
||||
{% if item.column.guess == 'Overage' %}
|
||||
{% for field in form.visible_fields %}
|
||||
{% if field.name == row.overage %}
|
||||
{{ field|as_crispy_field }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% elif item.column.guess == 'Note' %}
|
||||
{% for field in form.visible_fields %}
|
||||
{% if field.name == row.note %}
|
||||
{{ field|as_crispy_field }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{{ item.cell }}
|
||||
{% endif %}
|
||||
<input type='hidden' name='row_{{ row.index }}_col_{{ forloop.counter0 }}' value='{{ item.cell }}'/>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endblock form_content %}
|
||||
|
||||
{% block form_buttons_bottom %}
|
||||
{% endblock form_buttons_bottom %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('.bomselect').select2({
|
||||
dropdownAutoWidth: true,
|
||||
matcher: partialMatcher,
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -1,94 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "part/navbar.html" with tab='bom' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Upload Bill of Materials" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<p>{% trans "Step 2 - Select Fields" %}</p>
|
||||
<hr>
|
||||
|
||||
{% if missing_columns and missing_columns|length > 0 %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Missing selections for the following required columns" %}:
|
||||
<br>
|
||||
<ul>
|
||||
{% for col in missing_columns %}
|
||||
<li>{{ col }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
||||
<button type="submit" class="save btn btn-default">{% trans "Submit Selections" %}</button>
|
||||
{% csrf_token %}
|
||||
|
||||
<input type='hidden' name='form_step' value='select_fields'/>
|
||||
|
||||
<table class='table table-striped'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans "File Fields" %}</th>
|
||||
{% for col in bom_columns %}
|
||||
<th>
|
||||
<div>
|
||||
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
|
||||
{{ col.name }}
|
||||
<button class='btn btn-default btn-remove' onClick='removeColFromBomWizard()' id='del_col_{{ forloop.counter0 }}' style='display: inline; float: right;' title='{% trans "Remove column" %}'>
|
||||
<span col_id='{{ forloop.counter0 }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "Match Fields" %}</td>
|
||||
{% for col in bom_columns %}
|
||||
<td>
|
||||
<select class='select' id='id_col_{{ forloop.counter0 }}' name='col_guess_{{ forloop.counter0 }}'>
|
||||
<option value=''>---------</option>
|
||||
{% for req in bom_headers %}
|
||||
<option value='{{ req }}'{% if req == col.guess %}selected='selected'{% endif %}>{{ req }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if col.duplicate %}
|
||||
<p class='help-inline'>{% trans "Duplicate column selection" %}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for row in bom_rows %}
|
||||
<tr>
|
||||
<td>
|
||||
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ forloop.counter }}' style='display: inline; float: right;' title='{% trans "Remove row" %}'>
|
||||
<span row_id='{{ forloop.counter }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ forloop.counter }}</td>
|
||||
{% for item in row.data %}
|
||||
<td>
|
||||
<input type='hidden' name='row_{{ row.index }}_col_{{ forloop.counter0 }}' value='{{ item.cell }}'/>
|
||||
{{ item.cell }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -1,121 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "part/navbar.html" with tab="bom" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Upload Bill of Materials" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<p>{% trans "Step 3 - Select Parts" %}</p>
|
||||
<hr>
|
||||
|
||||
{% if form_errors %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Errors exist in the submitted data" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
||||
|
||||
<button type="submit" class="save btn btn-default">{% trans "Submit BOM" %}</button>
|
||||
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<input type='hidden' name='form_step' value='select_parts'/>
|
||||
|
||||
<table class='table table-striped'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th>{% trans "Row" %}</th>
|
||||
<th>{% trans "Select Part" %}</th>
|
||||
{% for col in bom_columns %}
|
||||
<th>
|
||||
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
|
||||
<input type='hidden' name='col_guess_{{ forloop.counter0 }}' value='{{ col.guess }}'/>
|
||||
{% if col.guess %}
|
||||
{{ col.guess }}
|
||||
{% else %}
|
||||
{{ col.name }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in bom_rows %}
|
||||
<tr {% if row.errors %} style='background: #ffeaea;'{% endif %} part-name='{{ row.part_name }}' part-description='{{ row.description }}' part-select='#select_part_{{ row.index }}'>
|
||||
<td>
|
||||
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ forloop.counter }}' style='display: inline; float: right;' title='{% trans "Remove row" %}'>
|
||||
<span row_id='{{ forloop.counter }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</td>
|
||||
<td></td>
|
||||
<td>{% add row.index 1 %}</td>
|
||||
<td>
|
||||
<button class='btn btn-default btn-create' onClick='newPartFromBomWizard()' id='new_part_row_{{ row.index }}' title='{% trans "Create new part" %}' type='button'>
|
||||
<span row_id='{{ row.index }}' class='fas fa-plus icon-green'/>
|
||||
</button>
|
||||
<select class='select bomselect' id='select_part_{{ row.index }}' name='part_{{ row.index }}'>
|
||||
<option value=''>--- {% trans "Select Part" %} ---</option>
|
||||
{% for part in row.part_options %}
|
||||
<option value='{{ part.id }}' {% if part.id == row.part.id %} selected='selected' {% elif part.id == row.part_match.id %} selected='selected' {% endif %}>
|
||||
{{ part }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if row.errors.part %}
|
||||
<p class='help-inline'>{{ row.errors.part }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% for item in row.data %}
|
||||
<td>
|
||||
{% if item.column.guess == 'Part' %}
|
||||
<i>{{ item.cell }}</i>
|
||||
{% if row.errors.part %}
|
||||
<p class='help-inline'>{{ row.errors.part }}</p>
|
||||
{% endif %}
|
||||
{% elif item.column.guess == 'Quantity' %}
|
||||
<input name='quantity_{{ row.index }}' class='numberinput' type='number' min='1' step='any' value='{% decimal row.quantity %}'/>
|
||||
{% if row.errors.quantity %}
|
||||
<p class='help-inline'>{{ row.errors.quantity }}</p>
|
||||
{% endif %}
|
||||
{% elif item.column.guess == 'Reference' %}
|
||||
<input name='reference_{{ row.index }}' value='{{ row.reference }}'/>
|
||||
{% elif item.column.guess == 'Note' %}
|
||||
<input name='notes_{{ row.index }}' value='{{ row.notes }}'/>
|
||||
{% elif item.column.guess == 'Overage' %}
|
||||
<input name='overage_{{ row.index }}' value='{{ row.overage }}'/>
|
||||
{% else %}
|
||||
{{ item.cell }}
|
||||
{% endif %}
|
||||
<input type='hidden' name='row_{{ row.index }}_col_{{ forloop.counter0 }}' value='{{ item.cell }}'/>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('.bomselect').select2({
|
||||
dropdownAutoWidth: true,
|
||||
matcher: partialMatcher,
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -3,18 +3,15 @@
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "part/navbar.html" with tab='bom' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Upload Bill of Materials" %}
|
||||
{% trans "Upload BOM File" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% block page_content %}
|
||||
|
||||
<p>{% trans "Step 1 - Select BOM File" %}</p>
|
||||
<h4>{% trans "Upload Bill of Materials" %}</h4>
|
||||
|
||||
{% block form_alert %}
|
||||
<div class='alert alert-info alert-block'>
|
||||
<b>{% trans "Requirements for BOM upload" %}:</b>
|
||||
<ul>
|
||||
@ -22,16 +19,31 @@
|
||||
<li>{% trans "Each part must already exist in the database" %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<form method="post" action='' class='js-modal-form' enctype="multipart/form-data">
|
||||
<button type="submit" class="save btn btn-default">{% trans 'Upload File' %}</button>
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
|
||||
{% if description %}- {{ description }}{% endif %}</p>
|
||||
|
||||
<input type='hidden' name='form_step' value='select_file'/>
|
||||
<form action="" method="post" class='js-modal-form' enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% crispy form %}
|
||||
{% block form_buttons_top %}
|
||||
{% endblock form_buttons_top %}
|
||||
|
||||
<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
|
||||
{{ wizard.management_form }}
|
||||
{% block form_content %}
|
||||
{% crispy wizard.form %}
|
||||
{% endblock form_content %}
|
||||
</table>
|
||||
|
||||
{% block form_buttons_bottom %}
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="save btn btn-default">{% trans "Upload File" %}</button>
|
||||
</form>
|
||||
{% endblock form_buttons_bottom %}
|
||||
|
||||
{% endblock %}
|
@ -1,50 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='build' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Part Builds" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right';>
|
||||
{% if part.active %}
|
||||
{% if roles.build.add %}
|
||||
<button class="btn btn-success" id='start-build'><span class='fas fa-tools'></span> {% trans "Start New Build" %}</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class='filter-list' id='filter-list-build'>
|
||||
<!-- Empty div for filters -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='build-table'>
|
||||
</table>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
$("#start-build").click(function() {
|
||||
newBuildOrder({
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
loadBuildTable($("#build-table"), {
|
||||
url: "{% url 'api-build-list' %}",
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
}
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -3,11 +3,20 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/category_navbar.html' with tab='parts' %}
|
||||
{% include 'part/category_navbar.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class='{{ message.tags }}'>
|
||||
{{ message|safe }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
@ -106,63 +115,81 @@
|
||||
|
||||
</div>
|
||||
|
||||
{% block category_content %}
|
||||
{% block page_content %}
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>
|
||||
<span class='fas fa-file-download'></span> {% trans "Export" %}
|
||||
</button>
|
||||
{% if roles.part.add %}
|
||||
<button class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Part" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-parts'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Parts" %}</h4>
|
||||
</div>
|
||||
<div id='part-button-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown">{% trans "Options" %}<span class='caret'></span></button>
|
||||
<ul class='dropdown-menu'>
|
||||
{% if roles.part.change %}
|
||||
<li><a href='#' id='multi-part-category' title='{% trans "Set category" %}'>{% trans "Set Category" %}</a></li>
|
||||
{% endif %}
|
||||
<li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li>
|
||||
<li><a href='#' id='multi-part-export' title='{% trans "Export" %}'>{% trans "Export Data" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Buttons to toggle between grid and table view -->
|
||||
<button id='view-list' class='btn btn-default' type='button' title='{% trans "View list display" %}'>
|
||||
<span class='fas fa-th-list'></span>
|
||||
</button>
|
||||
<button id='view-grid' class='btn btn-default' type='button' title='{% trans "View grid display" %}'>
|
||||
<span class='fas fa-th'></span>
|
||||
</button>
|
||||
<div class='filter-list' id='filter-list-parts'>
|
||||
<!-- Empty div -->
|
||||
<button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>
|
||||
<span class='fas fa-file-download'></span> {% trans "Export" %}
|
||||
</button>
|
||||
{% if roles.part.add %}
|
||||
<button class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Part" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class='btn-group'>
|
||||
<button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown">{% trans "Options" %}<span class='caret'></span></button>
|
||||
<ul class='dropdown-menu'>
|
||||
{% if roles.part.change %}
|
||||
<li><a href='#' id='multi-part-category' title='{% trans "Set category" %}'>{% trans "Set Category" %}</a></li>
|
||||
{% endif %}
|
||||
<li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li>
|
||||
<li><a href='#' id='multi-part-export' title='{% trans "Export" %}'>{% trans "Export Data" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Buttons to toggle between grid and table view -->
|
||||
<button id='view-list' class='btn btn-default' type='button' title='{% trans "View list display" %}'>
|
||||
<span class='fas fa-th-list'></span>
|
||||
</button>
|
||||
<button id='view-grid' class='btn btn-default' type='button' title='{% trans "View grid display" %}'>
|
||||
<span class='fas fa-th'></span>
|
||||
</button>
|
||||
<div class='filter-list' id='filter-list-parts'>
|
||||
<!-- Empty div -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<table class='table table-striped table-condensed' data-toolbar='#part-button-toolbar' id='part-table'>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-parameters'>
|
||||
<div class='panel-heading'>
|
||||
<h4>
|
||||
{% block heading %}
|
||||
{% trans "Parts" %}
|
||||
{% endblock %}
|
||||
</h4>
|
||||
<h4>{% trans "Part Parameters" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% block details %}
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
|
||||
</table>
|
||||
{% endblock %}
|
||||
<div class='panel-content'>
|
||||
<table class='table table-striped table-condensed' data-toolbar='#param-button-toolbar' id='parametric-part-table'>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='panel panel-default panel-inventree panel-hidden' id='panel-subcategories'>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Subcategories" %}</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<div id='subcategory-button-toolbar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
|
||||
<div class='filter-list' id='filter-list-category'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='subcategory-table' data-toolbar='#subcategory-button-toolbar'></table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block category_tables %}
|
||||
{% endblock category_tables %}
|
||||
|
||||
{% endblock %}
|
||||
{% block js_load %}
|
||||
{{ block.super }}
|
||||
@ -171,6 +198,26 @@
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadPartCategoryTable($('#subcategory-table'), {
|
||||
params: {
|
||||
{% if category %}
|
||||
parent: {{ category.pk }}
|
||||
{% else %}
|
||||
parent: 'null'
|
||||
{% endif %}
|
||||
}
|
||||
});
|
||||
|
||||
{% if category %}
|
||||
loadParametricPartTable(
|
||||
"#parametric-part-table",
|
||||
{
|
||||
headers: {{ headers|safe }},
|
||||
data: {{ parameters|safe }},
|
||||
}
|
||||
);
|
||||
{% endif %}
|
||||
|
||||
enableNavbar({
|
||||
label: 'category',
|
||||
toggleId: '#category-menu-toggle',
|
||||
@ -197,11 +244,11 @@
|
||||
"{% url 'category-create' %}",
|
||||
{
|
||||
follow: true,
|
||||
{% if category %}
|
||||
data: {
|
||||
{% if category %}
|
||||
category: {{ category.id }}
|
||||
{% endif %}
|
||||
},
|
||||
{% endif %}
|
||||
secondary: [
|
||||
{
|
||||
field: 'default_location',
|
||||
@ -259,13 +306,25 @@
|
||||
|
||||
{% if category %}
|
||||
$("#cat-edit").click(function () {
|
||||
launchModalForm(
|
||||
"{% url 'category-edit' category.id %}",
|
||||
|
||||
constructForm(
|
||||
'{% url "api-part-category-detail" category.pk %}',
|
||||
{
|
||||
fields: {
|
||||
name: {},
|
||||
description: {},
|
||||
parent: {
|
||||
help_text: '{% trans "Select parent category" %}',
|
||||
},
|
||||
default_location: {},
|
||||
default_keywords: {
|
||||
icon: 'fa-key',
|
||||
}
|
||||
},
|
||||
title: '{% trans "Edit Part Category" %}',
|
||||
reload: true
|
||||
},
|
||||
}
|
||||
);
|
||||
return false;
|
||||
});
|
||||
|
||||
{% if category.parent %}
|
||||
@ -307,4 +366,9 @@
|
||||
$('#view-list').hide();
|
||||
}
|
||||
|
||||
attachNavCallbacks({
|
||||
name: 'partcategory',
|
||||
default: 'part-stock'
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -1,4 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% settings_value 'PART_SHOW_IMPORT' as show_import %}
|
||||
|
||||
<ul class='list-group'>
|
||||
|
||||
@ -8,31 +11,32 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "subcategories" %}active{% endif %}' title='{% trans "Subcategories" %}'>
|
||||
{% if category %}
|
||||
<a href='{% url "category-subcategory" category.id %}'>
|
||||
{% else %}
|
||||
<a href='{% url "category-index-subcategory" %}'>
|
||||
{% endif %}
|
||||
<li class='list-group-item' title='{% trans "Subcategories" %}'>
|
||||
<a href='#' id='select-subcategories' class='nav-toggle'>
|
||||
<span class='fas fa-sitemap sidebar-icon'></span>
|
||||
{% trans "Subcategories" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "parts" %}active{% endif %}' title='{% trans "Parts" %}'>
|
||||
{% if category %}
|
||||
<a href='{% url "category-detail" category.id %}'>
|
||||
{% else %}
|
||||
<a href='{% url "part-index" %}'>
|
||||
{% endif %}
|
||||
<li class='list-group-item' title='{% trans "Parts" %}'>
|
||||
<a href='#' id='select-parts' class='nav-toggle'>
|
||||
<span class='fas fa-shapes sidebar-icon'></span>
|
||||
{% trans "Parts" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if show_import and user.is_staff and roles.part.add %}
|
||||
<li class='list-group-item' title='{% trans "Import Parts" %}'>
|
||||
<a href='{% url "part-import" %}'>
|
||||
<span class='fas fa-file-upload sidebar-icon'></span>
|
||||
{% trans "Import Parts" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if category %}
|
||||
<li class='list-group-item {% if tab == "parameters" %}active{% endif %}' title='{% trans "Parameters" %}'>
|
||||
<a href='{% url "category-parametric" category.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Parameters" %}'>
|
||||
<a href='#' id='select-parameters' class='nav-toggle'>
|
||||
<span class='fas fa-tasks sidebar-icon'></span>
|
||||
{% trans "Parameters" %}
|
||||
</a>
|
||||
|
@ -1,37 +0,0 @@
|
||||
{% extends "part/category.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/category_navbar.html' with tab='parameters' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Part Parameters" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='parametric-part-table'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
/* Hide Button Toolbar */
|
||||
window.onload = function hideButtonToolbar() {
|
||||
var toolbar = document.getElementById("button-toolbar");
|
||||
toolbar.style.display = "none";
|
||||
};
|
||||
|
||||
loadParametricPartTable(
|
||||
"#parametric-part-table",
|
||||
{
|
||||
headers: {{ headers|safe }},
|
||||
data: {{ parameters|safe }},
|
||||
}
|
||||
);
|
||||
|
||||
{% endblock %}
|
@ -1,16 +0,0 @@
|
||||
{% extends "part/category.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/category_navbar.html' with tab='parts' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Parts" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
|
||||
</table>
|
||||
{% endblock %}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,89 @@
|
||||
{% extends "part/import_wizard/ajax_part_upload.html" %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block form_alert %}
|
||||
{% if missing_columns and missing_columns|length > 0 %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Missing selections for the following required columns" %}:
|
||||
<br>
|
||||
<ul>
|
||||
{% for col in missing_columns %}
|
||||
<li>{{ col }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if duplicates and duplicates|length > 0 %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Duplicate selections found, see below. Fix them then retry submitting." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock form_alert %}
|
||||
|
||||
{% block form_content %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "File Fields" %}</th>
|
||||
<th></th>
|
||||
{% for col in form %}
|
||||
<th>
|
||||
<div>
|
||||
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
|
||||
{{ col.name }}
|
||||
<button class='btn btn-default btn-remove' onClick='removeColFromBomWizard()' id='del_col_{{ forloop.counter0 }}' style='display: inline; float: right;' title='{% trans "Remove column" %}'>
|
||||
<span col_id='{{ forloop.counter0 }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{% trans "Match Fields" %}</td>
|
||||
<td></td>
|
||||
{% for col in form %}
|
||||
<td>
|
||||
{{ col }}
|
||||
{% for duplicate in duplicates %}
|
||||
{% if duplicate == col.value %}
|
||||
<div class='alert alert-danger alert-block text-center' role='alert' style='padding:2px; margin-top:6px; margin-bottom:2px'>
|
||||
<b>{% trans "Duplicate selection" %}</b>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for row in rows %}
|
||||
{% with forloop.counter as row_index %}
|
||||
<tr>
|
||||
<td style='width: 32px;'>
|
||||
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ row_index }}' style='display: inline; float: left;' title='{% trans "Remove row" %}'>
|
||||
<span row_id='{{ row_index }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</td>
|
||||
<td style='text-align: left;'>{{ row_index }}</td>
|
||||
{% for item in row.data %}
|
||||
<td>
|
||||
<input type='hidden' name='row_{{ row_index }}_col_{{ forloop.counter0 }}' value='{{ item }}'/>
|
||||
{{ item }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endblock form_content %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('.fieldselect').select2({
|
||||
width: '100%',
|
||||
matcher: partialMatcher,
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,84 @@
|
||||
{% extends "part/import_wizard/ajax_part_upload.html" %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block form_alert %}
|
||||
{% if form.errors %}
|
||||
{% endif %}
|
||||
{% if form_errors %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Errors exist in the submitted data" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock form_alert %}
|
||||
|
||||
{% block form_content %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans "Row" %}</th>
|
||||
{% for col in columns %}
|
||||
|
||||
<th>
|
||||
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
|
||||
<input type='hidden' name='col_guess_{{ forloop.counter0 }}' value='{{ col.guess }}'/>
|
||||
{% if col.guess %}
|
||||
{{ col.guess }}
|
||||
{% else %}
|
||||
{{ col.name }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr></tr> {% comment %} Dummy row for javascript del_row method {% endcomment %}
|
||||
{% for row in rows %}
|
||||
<tr {% if row.errors %} style='background: #ffeaea;'{% endif %} part-select='#select_part_{{ row.index }}'>
|
||||
<td>
|
||||
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ row.index }}' style='display: inline; float: right;' title='{% trans "Remove row" %}'>
|
||||
<span row_id='{{ row.index }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
{% add row.index 1 %}
|
||||
</td>
|
||||
{% for item in row.data %}
|
||||
<td>
|
||||
{% if item.column.guess %}
|
||||
{% with row_name=item.column.guess|lower %}
|
||||
{% for field in form.visible_fields %}
|
||||
{% if field.name == row|keyvalue:row_name %}
|
||||
{{ field|as_crispy_field }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{{ item.cell }}
|
||||
{% endif %}
|
||||
<input type='hidden' name='row_{{ row.index }}_col_{{ forloop.counter0 }}' value='{{ item.cell }}'/>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endblock form_content %}
|
||||
|
||||
{% block form_buttons_bottom %}
|
||||
{% endblock form_buttons_bottom %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('.bomselect').select2({
|
||||
dropdownAutoWidth: true,
|
||||
matcher: partialMatcher,
|
||||
});
|
||||
|
||||
$('.currencyselect').select2({
|
||||
dropdownAutoWidth: true,
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,33 @@
|
||||
{% extends "modal_form.html" %}
|
||||
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block form %}
|
||||
|
||||
{% if roles.part.change %}
|
||||
|
||||
<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
|
||||
{% if description %}- {{ description }}{% endif %}</p>
|
||||
|
||||
{% block form_alert %}
|
||||
{% endblock form_alert %}
|
||||
|
||||
<form action="" method="post" class='js-modal-form' enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
|
||||
{{ wizard.management_form }}
|
||||
{% block form_content %}
|
||||
{% crispy wizard.form %}
|
||||
{% endblock form_content %}
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Unsuffitient privileges." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,99 @@
|
||||
{% extends "part/import_wizard/part_upload.html" %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block form_alert %}
|
||||
{% if missing_columns and missing_columns|length > 0 %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Missing selections for the following required columns" %}:
|
||||
<br>
|
||||
<ul>
|
||||
{% for col in missing_columns %}
|
||||
<li>{{ col }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if duplicates and duplicates|length > 0 %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Duplicate selections found, see below. Fix them then retry submitting." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock form_alert %}
|
||||
|
||||
{% block form_buttons_top %}
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="save btn btn-default">{% trans "Submit Selections" %}</button>
|
||||
{% endblock form_buttons_top %}
|
||||
|
||||
{% block form_content %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "File Fields" %}</th>
|
||||
<th></th>
|
||||
{% for col in form %}
|
||||
<th>
|
||||
<div>
|
||||
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
|
||||
{{ col.name }}
|
||||
<button class='btn btn-default btn-remove' onClick='removeColFromBomWizard()' id='del_col_{{ forloop.counter0 }}' style='display: inline; float: right;' title='{% trans "Remove column" %}'>
|
||||
<span col_id='{{ forloop.counter0 }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{% trans "Match Fields" %}</td>
|
||||
<td></td>
|
||||
{% for col in form %}
|
||||
<td>
|
||||
{{ col }}
|
||||
{% for duplicate in duplicates %}
|
||||
{% if duplicate == col.value %}
|
||||
<div class='alert alert-danger alert-block text-center' role='alert' style='padding:2px; margin-top:6px; margin-bottom:2px'>
|
||||
<b>{% trans "Duplicate selection" %}</b>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for row in rows %}
|
||||
{% with forloop.counter as row_index %}
|
||||
<tr>
|
||||
<td style='width: 32px;'>
|
||||
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ row_index }}' style='display: inline; float: left;' title='{% trans "Remove row" %}'>
|
||||
<span row_id='{{ row_index }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</td>
|
||||
<td style='text-align: left;'>{{ row_index }}</td>
|
||||
{% for item in row.data %}
|
||||
<td>
|
||||
<input type='hidden' name='row_{{ row_index }}_col_{{ forloop.counter0 }}' value='{{ item }}'/>
|
||||
{{ item }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endblock form_content %}
|
||||
|
||||
{% block form_buttons_bottom %}
|
||||
{% endblock form_buttons_bottom %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('.fieldselect').select2({
|
||||
width: '100%',
|
||||
matcher: partialMatcher,
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,91 @@
|
||||
{% extends "part/import_wizard/part_upload.html" %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block form_alert %}
|
||||
{% if form.errors %}
|
||||
{% endif %}
|
||||
{% if form_errors %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Errors exist in the submitted data" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock form_alert %}
|
||||
|
||||
{% block form_buttons_top %}
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="save btn btn-default">{% trans "Submit Selections" %}</button>
|
||||
{% endblock form_buttons_top %}
|
||||
|
||||
{% block form_content %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans "Row" %}</th>
|
||||
{% for col in columns %}
|
||||
|
||||
<th>
|
||||
<input type='hidden' name='col_name_{{ forloop.counter0 }}' value='{{ col.name }}'/>
|
||||
<input type='hidden' name='col_guess_{{ forloop.counter0 }}' value='{{ col.guess }}'/>
|
||||
{% if col.guess %}
|
||||
{{ col.guess }}
|
||||
{% else %}
|
||||
{{ col.name }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr></tr> {% comment %} Dummy row for javascript del_row method {% endcomment %}
|
||||
{% for row in rows %}
|
||||
<tr {% if row.errors %} style='background: #ffeaea;'{% endif %} part-select='#select_part_{{ row.index }}'>
|
||||
<td>
|
||||
<button class='btn btn-default btn-remove' onClick='removeRowFromBomWizard()' id='del_row_{{ row.index }}' style='display: inline; float: right;' title='{% trans "Remove row" %}'>
|
||||
<span row_id='{{ row.index }}' class='fas fa-trash-alt icon-red'></span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
{% add row.index 1 %}
|
||||
</td>
|
||||
{% for item in row.data %}
|
||||
<td>
|
||||
{% if item.column.guess %}
|
||||
{% with row_name=item.column.guess|lower %}
|
||||
{% for field in form.visible_fields %}
|
||||
{% if field.name == row|keyvalue:row_name %}
|
||||
{{ field|as_crispy_field }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{{ item.cell }}
|
||||
{% endif %}
|
||||
<input type='hidden' name='row_{{ row.index }}_col_{{ forloop.counter0 }}' value='{{ item.cell }}'/>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endblock form_content %}
|
||||
|
||||
{% block form_buttons_bottom %}
|
||||
{% endblock form_buttons_bottom %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('.bomselect').select2({
|
||||
dropdownAutoWidth: true,
|
||||
matcher: partialMatcher,
|
||||
});
|
||||
|
||||
$('.currencyselect').select2({
|
||||
dropdownAutoWidth: true,
|
||||
});
|
||||
|
||||
{% endblock %}
|
57
InvenTree/part/templates/part/import_wizard/part_upload.html
Normal file
57
InvenTree/part/templates/part/import_wizard/part_upload.html
Normal file
@ -0,0 +1,57 @@
|
||||
{% extends "base.html" %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
<div class='panel-heading'>
|
||||
<h4>
|
||||
{% trans "Import Parts from File" %}
|
||||
{{ wizard.form.media }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% if roles.part.change %}
|
||||
|
||||
<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
|
||||
{% if description %}- {{ description }}{% endif %}</p>
|
||||
|
||||
{% block form_alert %}
|
||||
{% endblock form_alert %}
|
||||
|
||||
<form action="" method="post" class='js-modal-form' enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block form_buttons_top %}
|
||||
{% endblock form_buttons_top %}
|
||||
|
||||
<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
|
||||
{{ wizard.management_form }}
|
||||
{% block form_content %}
|
||||
{% crispy wizard.form %}
|
||||
{% endblock form_content %}
|
||||
</table>
|
||||
|
||||
{% block form_buttons_bottom %}
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="save btn btn-default">{% trans "Upload File" %}</button>
|
||||
</form>
|
||||
{% endblock form_buttons_bottom %}
|
||||
|
||||
{% else %}
|
||||
<div class='alert alert-danger alert-block' role='alert'>
|
||||
{% trans "Unsuffitient privileges." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
{% endblock %}
|
@ -1,122 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='internal-prices' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Internal Price Information" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
|
||||
{% if show_internal_price and roles.sales_order.view %}
|
||||
<div id='internal-price-break-toolbar' class='btn-group'>
|
||||
<button class='btn btn-primary' id='new-internal-price-break' type='button'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Add Internal Price Break" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='internal-price-break-table' data-toolbar='#internal-price-break-toolbar'>
|
||||
</table>
|
||||
|
||||
{% else %}
|
||||
<div class='container-fluid'>
|
||||
<h3>{% trans "Permission Denied" %}</h3>
|
||||
|
||||
<div class='alert alert-danger alert-block'>
|
||||
{% trans "You do not have permission to view this page." %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
|
||||
{% if show_internal_price and roles.sales_order.view %}
|
||||
function reloadPriceBreaks() {
|
||||
$("#internal-price-break-table").bootstrapTable("refresh");
|
||||
}
|
||||
|
||||
$('#new-internal-price-break').click(function() {
|
||||
launchModalForm("{% url 'internal-price-break-create' %}",
|
||||
{
|
||||
success: reloadPriceBreaks,
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#internal-price-break-table').inventreeTable({
|
||||
name: 'internalprice',
|
||||
formatNoMatches: function() { return "{% trans 'No internal price break information found' %}"; },
|
||||
queryParams: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
url: "{% url 'api-part-internal-price-list' %}",
|
||||
onPostBody: function() {
|
||||
var table = $('#internal-price-break-table');
|
||||
|
||||
table.find('.button-internal-price-break-delete').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(
|
||||
`/part/internal-price/${pk}/delete/`,
|
||||
{
|
||||
success: reloadPriceBreaks
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
table.find('.button-internal-price-break-edit').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(
|
||||
`/part/internal-price/${pk}/edit/`,
|
||||
{
|
||||
success: reloadPriceBreaks
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
title: 'ID',
|
||||
visible: false,
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '{% trans "Price" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row, index) {
|
||||
var html = value;
|
||||
|
||||
html += `<div class='btn-group float-right' role='group'>`
|
||||
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-internal-price-break-edit', row.pk, '{% trans "Edit internal price break" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-internal-price-break-delete', row.pk, '{% trans "Delete internal price break" %}');
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -1,92 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='manufacturers' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Part Manufacturers" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button class="btn btn-success" id='manufacturer-create'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Manufacturer Part" %}
|
||||
</button>
|
||||
<div id='opt-dropdown' class="btn-group">
|
||||
<button id='manufacturer-part-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href='#' id='manufacturer-part-delete' title='{% trans "Delete manufacturer parts" %}'>{% trans "Delete" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-condensed" id='manufacturer-table' data-toolbar='#button-toolbar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_load %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('#manufacturer-create').click(function () {
|
||||
launchModalForm(
|
||||
"{% url 'manufacturer-part-create' %}",
|
||||
{
|
||||
reload: true,
|
||||
data: {
|
||||
part: {{ part.id }}
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
field: 'manufacturer',
|
||||
label: '{% trans "New Manufacturer" %}',
|
||||
title: '{% trans "Create new manufacturer" %}',
|
||||
url: "{% url 'manufacturer-create' %}",
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
$("#manufacturer-part-delete").click(function() {
|
||||
|
||||
var selections = $("#manufacturer-table").bootstrapTable("getSelections");
|
||||
|
||||
var parts = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts.push(item.pk);
|
||||
});
|
||||
|
||||
launchModalForm("{% url 'manufacturer-part-delete' %}", {
|
||||
data: {
|
||||
parts: parts,
|
||||
},
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
loadManufacturerPartTable(
|
||||
"#manufacturer-table",
|
||||
"{% url 'api-manufacturer-part-list' %}",
|
||||
{
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
part_detail: false,
|
||||
manufacturer_detail: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
linkButtonsToSelection($("#manufacturer-table"), ['#manufacturer-part-options'])
|
||||
|
||||
{% endblock %}
|
@ -3,6 +3,7 @@
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
|
||||
{% settings_value 'PART_SHOW_RELATED' as show_related %}
|
||||
|
||||
<ul class='list-group'>
|
||||
<li class='list-group-item'>
|
||||
@ -10,52 +11,36 @@
|
||||
<span class='menu-tab-icon fas fa-expand-arrows-alt'></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class='list-group-item {% if tab == "details" %}active{% endif %}' title='{% trans "Part Details" %}'>
|
||||
<a href='{% url "part-detail" part.id %}'>
|
||||
<span class='menu-tab-icon fas fa-info-circle sidebar-icon'></span>
|
||||
<span class='tab-text'>
|
||||
{% trans "Details" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class='list-group-item {% if tab == "params" %}active{% endif %}' title='{% trans "Part Parameters" %}'>
|
||||
<a href='{% url "part-params" part.id %}'>
|
||||
<span class='menu-tab-icon fas fa-tasks sidebar-icon'></span>
|
||||
<li class='list-group-item' title='{% trans "Parameters" %}'>
|
||||
<a href='#' id='select-part-parameters' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-th-list sidebar-icon'></span>
|
||||
{% trans "Parameters" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if part.is_template %}
|
||||
<li class='list-group-item {% if tab == "variants" %}active{% endif %}' title='{% trans "Part Variants" %}'>
|
||||
<a href='{% url "part-variants" part.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Part Variants" %}'>
|
||||
<a href='#' id='select-variants' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-shapes sidebar-icon'></span>
|
||||
{% trans "Variants" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Stock Items" %}'>
|
||||
<a href='{% url "part-stock" part.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Stock Items" %}'>
|
||||
<a href='#' id='select-part-stock' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-boxes sidebar-icon'></span>
|
||||
{% trans "Stock" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if part.component or part.salable %}
|
||||
<li class='list-group-item {% if tab == "allocation" %}active{% endif %}' title='{% trans "Allocated Stock" %}'>
|
||||
<a href='{% url "part-allocation" part.id %}'>
|
||||
<span class='menu-tab-icon fas fa-sign-out-alt sidebar-icon'></span>
|
||||
{% trans "Allocations" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if part.assembly %}
|
||||
<li class='list-group-item {% if tab == "bom" %}active{% endif %}' title='{% trans "Bill of Materials" %}'>
|
||||
<a href='{% url "part-bom" part.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Bill of Materials" %}'>
|
||||
<a href='#' id='select-bom' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-list sidebar-icon'></span>
|
||||
{% trans "Bill of Materials" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if roles.build.view %}
|
||||
<li class='list-group-item {% if tab == "build" %}active{% endif %}' title='{% trans "Build Orders" %}'>
|
||||
<a href='{% url "part-build" part.id %}'>
|
||||
<li class='list-group-item ' title='{% trans "Build Orders" %}'>
|
||||
<a href='#' id='select-build-orders' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-tools sidebar-icon'></span>
|
||||
{% trans "Build Orders" %}
|
||||
</a>
|
||||
@ -63,82 +48,66 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if part.component %}
|
||||
<li class='list-group-item {% if tab == "used" %}active{% endif %}' title='{% trans "Used In" %}'>
|
||||
<a href='{% url "part-used-in" part.id %}'>
|
||||
<li class='list-group-item ' title='{% trans "Used In" %}'>
|
||||
<a href='#' id='select-used-in' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-layer-group sidebar-icon'></span>
|
||||
{% trans "Used In" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if part.purchaseable and roles.purchase_order.view %}
|
||||
<li class='list-group-item {% if tab == "order-prices" %}active{% endif %}' title='{% trans "Order Price Information" %}'>
|
||||
<a href='{% url "part-order-prices" part.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Pricing Information" %}'>
|
||||
<a href='#' id='select-pricing' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-dollar-sign sidebar-icon'></span>
|
||||
{% trans "Order Price" %}
|
||||
{% trans "Prices" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class='list-group-item {% if tab == "manufacturers" %}active{% endif %}' title='{% trans "Manufacturers" %}'>
|
||||
<a href='{% url "part-manufacturers" part.id %}'>
|
||||
<span class='menu-tab-icon fas fa-industry sidebar-icon'></span>
|
||||
{% trans "Manufacturers" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class='list-group-item {% if tab == "suppliers" %}active{% endif %}' title='{% trans "Suppliers" %}'>
|
||||
<a href='{% url "part-suppliers" part.id %}'>
|
||||
{% if part.purchaseable and roles.purchase_order.view %}
|
||||
<li class='list-group-item' title='{% trans "Suppliers" %}'>
|
||||
<a href='#' id='select-suppliers' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-building sidebar-icon'></span>
|
||||
{% trans "Suppliers" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class='list-group-item {% if tab == "orders" %}active{% endif %}' title='{% trans "Purchase Orders" %}'>
|
||||
<a href='{% url "part-orders" part.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Purchase Orders" %}'>
|
||||
<a href='#' id='select-purchase-orders' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-shopping-cart sidebar-icon'></span>
|
||||
{% trans "Purchase Orders" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if show_internal_price and roles.sales_order.view %}
|
||||
<li class='list-group-item {% if tab == "internal-prices" %}active{% endif %}' title='{% trans "Internal Price Information" %}'>
|
||||
<a href='{% url "part-internal-prices" part.id %}'>
|
||||
<span class='menu-tab-icon fas fa-dollar-sign' style='width: 20px;'></span>
|
||||
{% trans "Internal Price" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class='list-group-item {% if tab == "sales-prices" %}active{% endif %}' title='{% trans "Sales Price Information" %}'>
|
||||
<a href='{% url "part-sale-prices" part.id %}'>
|
||||
<span class='menu-tab-icon fas fa-dollar-sign sidebar-icon'></span>
|
||||
{% trans "Sale Price" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class='list-group-item {% if tab == "sales-orders" %}active{% endif %}' title='{% trans "Sales Orders" %}'>
|
||||
<a href='{% url "part-sales-orders" part.id %}'>
|
||||
{% if roles.sales_order.view %}
|
||||
<li class='list-group-item' title='{% trans "Sales Orders" %}'>
|
||||
<a href='#' id='select-sales-orders' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-truck sidebar-icon'></span>
|
||||
{% trans "Sales Orders" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if part.trackable %}
|
||||
<li class='list-group-item {% if tab == "tests" %}active{% endif %}' title='{% trans "Part Test Templates" %}'>
|
||||
<a href='{% url "part-test-templates" part.id %}'>
|
||||
<li class='list-group-item' title='{% trans "Part Test Templates" %}'>
|
||||
<a href='#' id='select-test-templates' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-vial sidebar-icon'></span>
|
||||
{% trans "Tests" %}
|
||||
{% trans "Test Templates" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class='list-group-item {% if tab == "related" %}active{% endif %}' title='{% trans "Related Parts" %}'>
|
||||
<a href='{% url "part-related" part.id %}'>
|
||||
{% if show_related %}
|
||||
<li class='list-group-item' title='{% trans "Related Parts" %}'>
|
||||
<a href='#' id='select-related-parts' class='nav-toggle'>
|
||||
<span class='menu-tab-icon fas fa-random sidebar-icon'></span>
|
||||
{% trans "Related Parts" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class='list-group-item {% if tab == "attachments" %}active{% endif %}' title='{% trans "Attachments" %}'>
|
||||
<a href='{% url "part-attachments" part.id %}'>
|
||||
<span class='menu-tab-icon fas fa-paperclip sidebar-icon'></span>
|
||||
{% endif %}
|
||||
<li class='list-group-item' title='{% trans "Attachments" %}'>
|
||||
<a href='#' id='select-part-attachments' class='nav-toggle'>
|
||||
<span class='fas fa-paperclip side-icon'></span>
|
||||
{% trans "Attachments" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class='list-group-item {% if tab == "notes" %}active{% endif %}' title='{% trans "Part Notes" %}'>
|
||||
<a href='{% url "part-notes" part.id %}'>
|
||||
<span class='menu-tab-icon fas fa-clipboard sidebar-icon'></span>
|
||||
<li class='list-group-item' title='{% trans "Notes" %}'>
|
||||
<a href='#' id='select-part-notes' class='nav-toggle'>
|
||||
<span class='fas fa-clipboard side-icon'></span>
|
||||
{% trans "Notes" %}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -1,57 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='notes' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Part Notes" %}
|
||||
{% if roles.part.change and not editing %}
|
||||
<button title='{% trans "Edit notes" %}' class='btn btn-default' id='edit-notes'><span class='fas fa-edit'></span></button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
{% if editing %}
|
||||
<form method='POST'>
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form }}
|
||||
<hr>
|
||||
|
||||
<button type="submit" class='btn btn-default'>{% trans "Save" %}</button>
|
||||
|
||||
</form>
|
||||
|
||||
{{ form.media }}
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class='panel panel-default'>
|
||||
{% if part.notes %}
|
||||
<div class='panel-content'>
|
||||
{{ part.notes | markdownify }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if editing %}
|
||||
{% else %}
|
||||
$("#edit-notes").click(function() {
|
||||
location.href = "{% url 'part-notes' part.id %}?edit=1";
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -1,235 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='order-prices' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Order Price Information" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% default_currency as currency %}
|
||||
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
|
||||
|
||||
<form method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-sm-9">{{ form|crispy }}</div>
|
||||
<div class="col-sm-3">
|
||||
<input type="submit" value="{% trans 'Calculate' %}" class="btn btn-primary btn-block">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
|
||||
<div class="row"><div class="col col-md-6">
|
||||
<h4>{% trans "Pricing ranges" %}</h4>
|
||||
<table class='table table-striped table-condensed'>
|
||||
{% if part.supplier_count > 0 %}
|
||||
{% if min_total_buy_price %}
|
||||
<tr>
|
||||
<td><b>{% trans 'Supplier Pricing' %}</b></td>
|
||||
<td>{% trans 'Unit Cost' %}</td>
|
||||
<td>Min: {% include "price.html" with price=min_unit_buy_price %}</td>
|
||||
<td>Max: {% include "price.html" with price=max_unit_buy_price %}</td>
|
||||
</tr>
|
||||
{% if quantity > 1 %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans 'Total Cost' %}</td>
|
||||
<td>Min: {% include "price.html" with price=min_total_buy_price %}</td>
|
||||
<td>Max: {% include "price.html" with price=max_total_buy_price %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan='4'>
|
||||
<span class='warning-msg'><i>{% trans 'No supplier pricing available' %}</i></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if part.bom_count > 0 %}
|
||||
{% if min_total_bom_price %}
|
||||
<tr>
|
||||
<td><b>{% trans 'BOM Pricing' %}</b></td>
|
||||
<td>{% trans 'Unit Cost' %}</td>
|
||||
<td>Min: {% include "price.html" with price=min_unit_bom_price %}</td>
|
||||
<td>Max: {% include "price.html" with price=max_unit_bom_price %}</td>
|
||||
</tr>
|
||||
{% if quantity > 1 %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans 'Total Cost' %}</td>
|
||||
<td>Min: {% include "price.html" with price=min_total_bom_price %}</td>
|
||||
<td>Max: {% include "price.html" with price=max_total_bom_price %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.has_complete_bom_pricing == False %}
|
||||
<tr>
|
||||
<td colspan='4'>
|
||||
<span class='warning-msg'><i>{% trans 'Note: BOM pricing is incomplete for this part' %}</i></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan='4'>
|
||||
<span class='warning-msg'><i>{% trans 'No BOM pricing available' %}</i></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if show_internal_price and roles.sales_order.view %}
|
||||
{% if total_internal_part_price %}
|
||||
<tr>
|
||||
<td><b>{% trans 'Internal Price' %}</b></td>
|
||||
<td>{% trans 'Unit Cost' %}</td>
|
||||
<td colspan='2'>{% include "price.html" with price=unit_internal_part_price %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans 'Total Cost' %}</td>
|
||||
<td colspan='2'>{% include "price.html" with price=total_internal_part_price %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if total_part_price %}
|
||||
<tr>
|
||||
<td><b>{% trans 'Sale Price' %}</b></td>
|
||||
<td>{% trans 'Unit Cost' %}</td>
|
||||
<td colspan='2'>{% include "price.html" with price=unit_part_price %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans 'Total Cost' %}</td>
|
||||
<td colspan='2'>{% include "price.html" with price=total_part_price %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
{% if min_unit_buy_price or min_unit_bom_price %}
|
||||
{% else %}
|
||||
<div class='alert alert-danger alert-block'>
|
||||
{% trans 'No pricing information is available for this part.' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if part.bom_count > 0 %}
|
||||
<div class="col col-md-6">
|
||||
<h4>{% trans 'BOM Pricing' %}</h4>
|
||||
<div style="max-width: 99%;">
|
||||
<canvas id="BomChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if price_history %}
|
||||
<hr>
|
||||
<h4>{% trans 'Stock Pricing' %}<i class="fas fa-info-circle" title="Shows the purchase prices of stock for this part.
|
||||
The part single price is the current purchase price for that supplier part."></i></h4>
|
||||
{% if price_history|length > 0 %}
|
||||
<div style="max-width: 99%; min-height: 300px">
|
||||
<canvas id="StockPriceChart"></canvas>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-danger alert-block'>
|
||||
{% trans 'No stock pricing history is available for this part.' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
{% default_currency as currency %}
|
||||
{% if price_history %}
|
||||
var pricedata = {
|
||||
labels: [
|
||||
{% for line in price_history %}'{{ line.date }}',{% endfor %}
|
||||
],
|
||||
datasets: [{
|
||||
label: '{% blocktrans %}Single Price - {{currency}}{% endblocktrans %}',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
borderColor: 'rgb(255, 99, 132)',
|
||||
yAxisID: 'y',
|
||||
data: [
|
||||
{% for line in price_history %}{{ line.price|stringformat:".2f" }},{% endfor %}
|
||||
],
|
||||
borderWidth: 1,
|
||||
type: 'line'
|
||||
},
|
||||
{% if 'price_diff' in price_history.0 %}
|
||||
{
|
||||
label: '{% blocktrans %}Single Price Difference - {{currency}}{% endblocktrans %}',
|
||||
backgroundColor: 'rgba(68, 157, 68, 0.2)',
|
||||
borderColor: 'rgb(68, 157, 68)',
|
||||
yAxisID: 'y2',
|
||||
data: [
|
||||
{% for line in price_history %}{{ line.price_diff|stringformat:".2f" }},{% endfor %}
|
||||
],
|
||||
borderWidth: 1,
|
||||
type: 'line',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
label: '{% blocktrans %}Part Single Price - {{currency}}{% endblocktrans %}',
|
||||
backgroundColor: 'rgba(70, 127, 155, 0.2)',
|
||||
borderColor: 'rgb(70, 127, 155)',
|
||||
yAxisID: 'y',
|
||||
data: [
|
||||
{% for line in price_history %}{{ line.price_part|stringformat:".2f" }},{% endfor %}
|
||||
],
|
||||
borderWidth: 1,
|
||||
type: 'line',
|
||||
hidden: true,
|
||||
},
|
||||
{% endif %}
|
||||
{
|
||||
label: '{% trans "Quantity" %}',
|
||||
backgroundColor: 'rgba(255, 206, 86, 0.2)',
|
||||
borderColor: 'rgb(255, 206, 86)',
|
||||
yAxisID: 'y1',
|
||||
data: [
|
||||
{% for line in price_history %}{{ line.qty|stringformat:"f" }},{% endfor %}
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
}
|
||||
var StockPriceChart = loadStockPricingChart(document.getElementById('StockPriceChart'), pricedata)
|
||||
var bom_colors = randomColor({hue: 'green', count: {{ bom_parts|length }} })
|
||||
var bomdata = {
|
||||
labels: [{% for line in bom_parts %}'{{ line.name }}',{% endfor %}],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Price',
|
||||
data: [{% for line in bom_parts %}{{ line.min_price }},{% endfor %}],
|
||||
backgroundColor: bom_colors,
|
||||
},
|
||||
{% if bom_pie_max %}
|
||||
{
|
||||
label: 'Max Price',
|
||||
data: [{% for line in bom_parts %}{{ line.max_price }},{% endfor %}],
|
||||
backgroundColor: bom_colors,
|
||||
},
|
||||
{% endif %}
|
||||
]
|
||||
};
|
||||
var BomChart = loadBomChart(document.getElementById('BomChart'), bomdata)
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -1,51 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='orders' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Purchase Orders" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<div id='button-bar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
<button class='btn btn-primary' type='button' id='part-order2' title='{% trans "Order part" %}'>
|
||||
<span class='fas fa-shopping-cart'></span> {% trans "Order Part" %}
|
||||
</button>
|
||||
<div class='filter-list' id='filter-list-purchaseorder'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
|
||||
</table>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadPurchaseOrderTable($("#purchase-order-table"), {
|
||||
url: "{% url 'api-po-list' %}",
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
});
|
||||
|
||||
$("#part-order2").click(function() {
|
||||
launchModalForm("{% url 'order-parts' %}", {
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -1,5 +0,0 @@
|
||||
{% extends "modal_delete_form.html" %}
|
||||
|
||||
{% block pre_form_content %}
|
||||
Are you sure you want to remove this parameter?
|
||||
{% endblock %}
|
@ -1,92 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "part/navbar.html" with tab='params' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Part Parameters" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
{% if roles.part.add %}
|
||||
<button title='{% trans "Add new parameter" %}' class='btn btn-success' id='param-create'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Parameter" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id='param-table' class='table table-condensed table-striped' data-toolbar='#button-toolbar'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-field='name' data-serachable='true'>{% trans "Name" %}</th>
|
||||
<th data-field='value' data-searchable='true'>{% trans "Value" %}</th>
|
||||
<th data-field='units' data-searchable='true'>{% trans "Units" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for param in part.get_parameters %}
|
||||
<tr>
|
||||
<td>{{ param.template.name }}</td>
|
||||
<td>{{ param.data }}</td>
|
||||
<td>
|
||||
{{ param.template.units }}
|
||||
<div class='btn-group' style='float: right;'>
|
||||
{% if roles.part.change %}
|
||||
<button title='{% trans "Edit" %}' class='btn btn-default btn-glyph param-edit' url="{% url 'part-param-edit' param.id %}" type='button'><span class='fas fa-edit'/></button>
|
||||
{% endif %}
|
||||
{% if roles.part.change %}
|
||||
<button title='{% trans "Delete" %}' class='btn btn-default btn-glyph param-delete' url="{% url 'part-param-delete' param.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('#param-table').inventreeTable({
|
||||
});
|
||||
|
||||
{% if roles.part.add %}
|
||||
$('#param-create').click(function() {
|
||||
launchModalForm("{% url 'part-param-create' %}?part={{ part.id }}", {
|
||||
reload: true,
|
||||
secondary: [{
|
||||
field: 'template',
|
||||
label: '{% trans "New Template" %}',
|
||||
title: '{% trans "Create New Parameter Template" %}',
|
||||
url: "{% url 'part-param-template-create' %}"
|
||||
}],
|
||||
});
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
$('.param-edit').click(function() {
|
||||
var button = $(this);
|
||||
|
||||
launchModalForm(button.attr('url'), {
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$('.param-delete').click(function() {
|
||||
var button = $(this);
|
||||
|
||||
launchModalForm(button.attr('url'), {
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -27,7 +27,34 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</h3>
|
||||
{% if part.description %}
|
||||
<p><em>{{ part.description }}</em></p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<div id='part-properties' class='btn-group' role='group'>
|
||||
{% if part.virtual %}
|
||||
<span class='fas fa-ghost' title='{% trans "Part is virtual (not a physical part)" %}'></span>
|
||||
{% endif %}
|
||||
{% if part.is_template %}
|
||||
<span class='fas fa-clone' title='{% trans "Part is a template part (variants can be made from this part)" %}'></span>
|
||||
{% endif %}
|
||||
{% if part.assembly %}
|
||||
<span class='fas fa-tools' title='{% trans "Part can be assembled from other parts" %}'></span>
|
||||
{% endif %}
|
||||
{% if part.component %}
|
||||
<span class='fas fa-th' title='{% trans "Part can be used in assemblies" %}'></span>
|
||||
{% endif %}
|
||||
{% if part.trackable %}
|
||||
<span class='fas fa-directions' title='{% trans "Part stock is tracked by serial number" %}'></span>
|
||||
{% endif %}
|
||||
{% if part.purchaseable %}
|
||||
<span class='fas fa-shopping-cart' title='{% trans "Part can be purchased from external suppliers" %}'></span>
|
||||
{% endif %}
|
||||
{% if part.salable %}
|
||||
<span class='fas fa-dollar-sign' title='{% trans "Part can be sold to customers" %}'></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div class='btn-group action-buttons' role='group'>
|
||||
<button type='button' class='btn btn-default' id='toggle-starred' title='{% trans "Star this part" %}'>
|
||||
@ -49,9 +76,25 @@
|
||||
<span id='part-price-icon' class='fas fa-dollar-sign'/>
|
||||
</button>
|
||||
{% if roles.stock.change %}
|
||||
<button type='button' class='btn btn-default' id='part-count' title='{% trans "Count part stock" %}'>
|
||||
<span class='fas fa-clipboard-list'/>
|
||||
</button>
|
||||
<div class='btn-group'>
|
||||
<button id='stock-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'>
|
||||
<span class='fas fa-boxes'></span> <span class='caret'></span>
|
||||
</button>
|
||||
<ul class='dropdown-menu'>
|
||||
<li>
|
||||
<a href='#' id='part-count'>
|
||||
<span class='fas fa-clipboard-list'></span>
|
||||
{% trans "Count part stock" %}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='#' id='part-move'>
|
||||
<span class='fas fa-exchange-alt'></span>
|
||||
{% trans "Transfer part stock" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if part.purchaseable %}
|
||||
{% if roles.purchase_order.add %}
|
||||
@ -81,11 +124,11 @@
|
||||
</div>
|
||||
<table class='table table-condensed'>
|
||||
<col width='25'>
|
||||
{% if part.IPN %}
|
||||
{% if part.keywords %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans "IPN" %}</td>
|
||||
<td>{{ part.IPN }}</td>
|
||||
<td><span class='fas fa-key'></span></td>
|
||||
<td>{% trans "Keywords" %}</td>
|
||||
<td>{{ part.keywords }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.link %}
|
||||
@ -96,7 +139,22 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td><span class='fas fa-calendar-alt'></span></td>
|
||||
<td>{% trans "Creation Date" %}</td>
|
||||
<td>
|
||||
{{ part.creation_date }}
|
||||
{% if part.creation_user %}
|
||||
<span class='badge'>{{ part.creation_user }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if part.trackable and part.getLatestSerialNumber %}
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "Latest Serial Number" %}</td>
|
||||
<td>{{ part.getLatestSerialNumber }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -109,7 +167,7 @@
|
||||
{% endif %}
|
||||
{% if part.variant_of %}
|
||||
<div class='alert alert-info alert-block'>
|
||||
{% object_link 'part-variants' part.variant_of.id part.variant_of.full_name as link %}
|
||||
{% object_link 'part-detail' part.variant_of.id part.variant_of.full_name as link %}
|
||||
{% blocktrans %}This part is a variant of {{link}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -181,44 +239,13 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if part.trackable and part.getLatestSerialNumber %}
|
||||
<tr><td colspan="3"></td></tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "Latest Serial Number" %}</td>
|
||||
<td>{{ part.getLatestSerialNumber }}{% include "clip.html"%}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% block pre_content_panel %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
|
||||
|
||||
<div class='panel-heading'>
|
||||
<h4>
|
||||
{% block heading %}
|
||||
<!-- Heading goes here -->
|
||||
{% endblock %}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
{% block details %}
|
||||
<!-- Specific part details go here... -->
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% block post_content_panel %}
|
||||
|
||||
{% block page_content %}
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
@ -226,17 +253,22 @@
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
enableNavbar({
|
||||
label: 'part',
|
||||
toggleId: '#part-menu-toggle',
|
||||
});
|
||||
|
||||
{% if part.image %}
|
||||
$('#part-thumb').click(function() {
|
||||
showModalImage('{{ part.image.url }}');
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
function reloadImage(data) {
|
||||
// If image / thumbnail data present, live update
|
||||
if (data.image) {
|
||||
$('#part-image').attr('src', data.image);
|
||||
} else {
|
||||
// Otherwise, reload the page
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
enableDragAndDrop(
|
||||
'#part-thumb',
|
||||
"{% url 'api-part-detail' part.id %}",
|
||||
@ -244,14 +276,7 @@
|
||||
label: 'image',
|
||||
method: 'PATCH',
|
||||
success: function(data, status, xhr) {
|
||||
|
||||
// If image / thumbnail data present, live update
|
||||
if (data.image) {
|
||||
$('#part-image').attr('src', data.image);
|
||||
} else {
|
||||
// Otherwise, reload the page
|
||||
location.reload();
|
||||
}
|
||||
reloadImage(data);
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -265,14 +290,38 @@
|
||||
);
|
||||
});
|
||||
|
||||
$("#part-count").click(function() {
|
||||
launchModalForm("/stock/adjust/", {
|
||||
data: {
|
||||
action: "count",
|
||||
$('#print-label').click(function() {
|
||||
printPartLabels([{{ part.pk }}]);
|
||||
});
|
||||
|
||||
function adjustPartStock(action) {
|
||||
inventreeGet(
|
||||
'{% url "api-stock-list" %}',
|
||||
{
|
||||
part: {{ part.id }},
|
||||
in_stock: true,
|
||||
allow_variants: true,
|
||||
part_detail: true,
|
||||
location_detail: true,
|
||||
},
|
||||
reload: true,
|
||||
});
|
||||
{
|
||||
success: function(items) {
|
||||
adjustStock(action, items, {
|
||||
onSuccess: function() {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$("#part-move").click(function() {
|
||||
adjustPartStock('move');
|
||||
});
|
||||
|
||||
$("#part-count").click(function() {
|
||||
adjustPartStock('count');
|
||||
});
|
||||
|
||||
$("#price-button").click(function() {
|
||||
@ -293,11 +342,20 @@
|
||||
});
|
||||
|
||||
$("#part-image-upload").click(function() {
|
||||
launchModalForm("{% url 'part-image-upload' part.id %}",
|
||||
|
||||
constructForm(
|
||||
'{% url "api-part-detail" part.pk %}',
|
||||
{
|
||||
reload: true
|
||||
method: 'PATCH',
|
||||
fields: {
|
||||
image: {},
|
||||
},
|
||||
title: '{% trans "Upload Image" %}',
|
||||
onSuccess: function(data) {
|
||||
reloadImage(data);
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@ -357,12 +415,8 @@
|
||||
});
|
||||
|
||||
$("#part-edit").click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'part-edit' part.id %}",
|
||||
{
|
||||
reload: true,
|
||||
}
|
||||
);
|
||||
|
||||
editPart({{ part.pk }});
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='tests' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Part Test Templates" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid' style="float: right;">
|
||||
<div class='btn-group' role='group'>
|
||||
<button type='button' class='btn btn-success' id='add-test-template'>{% trans "Add Test Template" %}</button>
|
||||
</div>
|
||||
<div class='filter-list' id='filter-list-parttests'>
|
||||
<!-- Empty div -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='test-template-table'></table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadPartTestTemplateTable(
|
||||
$("#test-template-table"),
|
||||
{
|
||||
part: {{ part.pk }},
|
||||
params: {
|
||||
part: {{ part.pk }},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function reloadTable() {
|
||||
$("#test-template-table").bootstrapTable("refresh");
|
||||
}
|
||||
|
||||
$("#add-test-template").click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'part-test-template-create' %}",
|
||||
{
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
success: reloadTable,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#test-template-table").on('click', '.button-test-edit', function() {
|
||||
var button = $(this);
|
||||
|
||||
var url = `/part/test-template/${button.attr('pk')}/edit/`;
|
||||
|
||||
launchModalForm(url, {
|
||||
success: reloadTable,
|
||||
});
|
||||
});
|
||||
|
||||
$("#test-template-table").on('click', '.button-test-delete', function() {
|
||||
var button = $(this);
|
||||
|
||||
var url = `/part/test-template/${button.attr('pk')}/delete/`;
|
||||
|
||||
launchModalForm(url, {
|
||||
success: reloadTable,
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
282
InvenTree/part/templates/part/prices.html
Normal file
282
InvenTree/part/templates/part/prices.html
Normal file
@ -0,0 +1,282 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Pricing Information" %}</h4>
|
||||
</div>
|
||||
|
||||
{% default_currency as currency %}
|
||||
<div class='panel-content'>
|
||||
|
||||
<div class="row">
|
||||
<a class="anchor" id="overview"></a>
|
||||
<div class="col col-md-6">
|
||||
<h4>{% trans "Pricing ranges" %}</h4>
|
||||
<table class='table table-striped table-condensed'>
|
||||
{% if part.supplier_count > 0 %}
|
||||
{% if min_total_buy_price %}
|
||||
<tr>
|
||||
<td><b>{% trans 'Supplier Pricing' %}</b>
|
||||
<a href="#supplier-cost" title='{% trans "Show supplier cost" %}'><span class="fas fa-search-dollar"></span></a>
|
||||
<a href="#purchase-price" title='{% trans "Show purchase price" %}'><span class="fas fa-chart-bar"></span></a>
|
||||
</td>
|
||||
<td>{% trans 'Unit Cost' %}</td>
|
||||
<td>Min: {% include "price.html" with price=min_unit_buy_price %}</td>
|
||||
<td>Max: {% include "price.html" with price=max_unit_buy_price %}</td>
|
||||
</tr>
|
||||
{% if quantity > 1 %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans 'Total Cost' %}</td>
|
||||
<td>Min: {% include "price.html" with price=min_total_buy_price %}</td>
|
||||
<td>Max: {% include "price.html" with price=max_total_buy_price %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan='4'>
|
||||
<span class='warning-msg'><i>{% trans 'No supplier pricing available' %}</i></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if part.bom_count > 0 %}
|
||||
{% if min_total_bom_price %}
|
||||
<tr>
|
||||
<td><b>{% trans 'BOM Pricing' %}</b>
|
||||
<a href="#bom-cost" title='{% trans "Show BOM cost" %}'><span class="fas fa-search-dollar"></span></a>
|
||||
</td>
|
||||
<td>{% trans 'Unit Cost' %}</td>
|
||||
<td>Min: {% include "price.html" with price=min_unit_bom_price %}</td>
|
||||
<td>Max: {% include "price.html" with price=max_unit_bom_price %}</td>
|
||||
</tr>
|
||||
{% if quantity > 1 %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans 'Total Cost' %}</td>
|
||||
<td>Min: {% include "price.html" with price=min_total_bom_price %}</td>
|
||||
<td>Max: {% include "price.html" with price=max_total_bom_price %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if part.has_complete_bom_pricing == False %}
|
||||
<tr>
|
||||
<td colspan='4'>
|
||||
<span class='warning-msg'><i>{% trans 'Note: BOM pricing is incomplete for this part' %}</i></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan='4'>
|
||||
<span class='warning-msg'><i>{% trans 'No BOM pricing available' %}</i></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if show_internal_price and roles.sales_order.view %}
|
||||
{% if total_internal_part_price %}
|
||||
<tr>
|
||||
<td><b>{% trans 'Internal Price' %}</b></td>
|
||||
<td>{% trans 'Unit Cost' %}</td>
|
||||
<td colspan='2'>{% include "price.html" with price=unit_internal_part_price %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans 'Total Cost' %}</td>
|
||||
<td colspan='2'>{% include "price.html" with price=total_internal_part_price %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if total_part_price %}
|
||||
<tr>
|
||||
<td><b>{% trans 'Sale Price' %}</b>
|
||||
<a href="#sale-cost" title='{% trans "Show sale cost" %}'><span class="fas fa-search-dollar"></span></a>
|
||||
<a href="#sale-price" title='{% trans "Show sale price" %}'><span class="fas fa-chart-bar"></span></a>
|
||||
</td>
|
||||
<td>{% trans 'Unit Cost' %}</td>
|
||||
<td colspan='2'>{% include "price.html" with price=unit_part_price %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>{% trans 'Total Cost' %}</td>
|
||||
<td colspan='2'>{% include "price.html" with price=total_part_price %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
{% if min_unit_buy_price or min_unit_bom_price %}
|
||||
{% else %}
|
||||
<div class='alert alert-danger alert-block'>
|
||||
{% trans 'No pricing information is available for this part.' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col col-md-6">
|
||||
<h4>{% trans "Calculation parameters" %}</h4>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<input type="submit" value="{% trans 'Calculate' %}" class="btn btn-primary btn-block">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% settings_value "PART_INTERNAL_PRICE" as show_internal_price %}
|
||||
|
||||
{% if part.purchaseable and roles.purchase_order.view %}
|
||||
<a class="anchor" id="supplier-cost"></a>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Supplier Cost" %}
|
||||
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<h4>{% trans "Suppliers" %}</h4>
|
||||
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'></table>
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
<h4>{% trans "Manufacturers" %}</h4>
|
||||
<table class="table table-striped table-condensed" id='manufacturer-table' data-toolbar='#button-toolbar'></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if price_history %}
|
||||
<a class="anchor" id="purchase-price"></a>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Purchase Price" %}
|
||||
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
|
||||
</h4>
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
<h4>{% trans 'Stock Pricing' %}
|
||||
<i class="fas fa-info-circle" title="Shows the purchase prices of stock for this part.
|
||||
The part single price is the current purchase price for that supplier part."></i>
|
||||
</h4>
|
||||
{% if price_history|length > 0 %}
|
||||
<div style="max-width: 99%; min-height: 300px">
|
||||
<canvas id="StockPriceChart"></canvas>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-danger alert-block'>
|
||||
{% trans 'No stock pricing history is available for this part.' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if show_internal_price and roles.sales_order.view %}
|
||||
<a class="anchor" id="internal-cost"></a>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Internal Cost" %}
|
||||
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
<div class="row full-height">
|
||||
<div class="col col-md-8">
|
||||
<div style="max-width: 99%; height: 100%;">
|
||||
<canvas id="InternalPriceBreakChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-4">
|
||||
<div id='internal-price-break-toolbar' class='btn-group'>
|
||||
<button class='btn btn-primary' id='new-internal-price-break' type='button'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Add Internal Price Break" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='internal-price-break-table' data-toolbar='#internal-price-break-toolbar'
|
||||
data-sort-name="quantity" data-sort-order="asc">
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if part.has_bom and roles.sales_order.view %}
|
||||
<a class="anchor" id="bom-cost"></a>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "BOM Cost" %}
|
||||
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
<table class='table table-bom table-condensed' data-toolbar="#button-toolbar" id='bom-pricing-table'></table>
|
||||
</div>
|
||||
|
||||
{% if part.bom_count > 0 %}
|
||||
<div class="col col-md-6">
|
||||
<h4>{% trans 'BOM Pricing' %}</h4>
|
||||
<div style="max-width: 99%;">
|
||||
<canvas id="BomChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if part.salable and roles.sales_order.view %}
|
||||
<a class="anchor" id="sale-cost"></a>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Sale Cost" %}
|
||||
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
<div class="row full-height">
|
||||
<div class="col col-md-8">
|
||||
<div style="max-width: 99%; height: 100%;">
|
||||
<canvas id="SalePriceBreakChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-4">
|
||||
<div id='price-break-toolbar' class='btn-group'>
|
||||
<button class='btn btn-primary' id='new-price-break' type='button'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Add Price Break" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'
|
||||
data-sort-name="quantity" data-sort-order="asc">
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="anchor" id="sale-price"></a>
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Sale Price" %}
|
||||
<a href="#overview" title='{% trans "Jump to overview" %}'><span class="fas fa-level-up-alt"></span></a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class='panel-content'>
|
||||
{% if sale_history|length > 0 %}
|
||||
<div style="max-width: 99%; min-height: 300px">
|
||||
<canvas id="SalePriceChart"></canvas>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class='alert alert-danger alert-block'>
|
||||
{% trans 'No sale pice history available for this part.' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
@ -1,80 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='related' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Related Parts" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<div id='button-bar'>
|
||||
<div class='button-toolbar container-fluid' style='float: left;'>
|
||||
{% if roles.part.change %}
|
||||
<button class='btn btn-primary' type='button' id='add-related-part' title='{% trans "Add Related" %}'>{% trans "Add Related" %}</button>
|
||||
<div class='filter-list' id='filter-list-related'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id='table-related-part' class='table table-condensed table-striped' data-toolbar='#button-toolbar'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-field='part' data-serachable='true'>{% trans "Part" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in part.get_related_parts %}
|
||||
{% with part_related=item.0 part=item.1 %}
|
||||
<tr>
|
||||
<td>
|
||||
<a class='hover-icon'>
|
||||
<img class='hover-img-thumb' src='{{ part.get_thumbnail_url }}'>
|
||||
<img class='hover-img-large' src='{{ part.get_thumbnail_url }}'>
|
||||
</a>
|
||||
<a href='/part/{{ part.id }}/'>{{ part }}</a>
|
||||
<div class='btn-group' style='float: right;'>
|
||||
{% if roles.part.change %}
|
||||
<button title='{% trans "Delete" %}' class='btn btn-default btn-glyph delete-related-part' url="{% url 'part-related-delete' part_related.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('#table-related-part').inventreeTable({
|
||||
});
|
||||
|
||||
$("#add-related-part").click(function() {
|
||||
launchModalForm("{% url 'part-related-create' %}", {
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$('.delete-related-part').click(function() {
|
||||
var button = $(this);
|
||||
|
||||
launchModalForm(button.attr('url'), {
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -1,108 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='sales-prices' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Sell Price Information" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<div id='price-break-toolbar' class='btn-group'>
|
||||
<button class='btn btn-primary' id='new-price-break' type='button'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "Add Price Break" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
function reloadPriceBreaks() {
|
||||
$("#price-break-table").bootstrapTable("refresh");
|
||||
}
|
||||
|
||||
$('#new-price-break').click(function() {
|
||||
launchModalForm("{% url 'sale-price-break-create' %}",
|
||||
{
|
||||
success: reloadPriceBreaks,
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('#price-break-table').inventreeTable({
|
||||
name: 'saleprice',
|
||||
formatNoMatches: function() { return "{% trans 'No price break information found' %}"; },
|
||||
queryParams: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
url: "{% url 'api-part-sale-price-list' %}",
|
||||
onPostBody: function() {
|
||||
var table = $('#price-break-table');
|
||||
|
||||
table.find('.button-price-break-delete').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(
|
||||
`/part/sale-price/${pk}/delete/`,
|
||||
{
|
||||
success: reloadPriceBreaks
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
table.find('.button-price-break-edit').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(
|
||||
`/part/sale-price/${pk}/edit/`,
|
||||
{
|
||||
success: reloadPriceBreaks
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
title: 'ID',
|
||||
visible: false,
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '{% trans "Price" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row, index) {
|
||||
var html = value;
|
||||
|
||||
html += `<div class='btn-group float-right' role='group'>`
|
||||
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-price-break-edit', row.pk, '{% trans "Edit price break" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}');
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
{% endblock %}
|
@ -1,41 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='sales-orders' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Sales Orders" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<div id='button-bar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
{% if 0 %}
|
||||
<button class='btn btn-primary' type='button' id='part-order2' title='{% trans "New sales order" %}'>{% trans "New Order" %}</button>
|
||||
{% endif %}
|
||||
<div class='filter-list' id='filter-list-salesorder'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed po-table' id='sales-order-table' data-toolbar='#button-bar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadSalesOrderTable($("#sales-order-table"), {
|
||||
url: "{% url 'api-so-list' %}",
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -1,76 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='stock' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Part Stock" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% if part.is_template %}
|
||||
<div class='alert alert-info alert-block'>
|
||||
{% blocktrans with full_name=part.full_name%}Showing stock for all variants of <i>{{full_name}}</i>{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% include "stock_table.html" %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_load %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('#add-stock-item').click(function () {
|
||||
createNewStockItem({
|
||||
reload: true,
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
loadStockTable($("#stock-table"), {
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
location_detail: true,
|
||||
part_detail: false,
|
||||
},
|
||||
groupByField: 'location',
|
||||
buttons: [
|
||||
'#stock-options',
|
||||
],
|
||||
url: "{% url 'api-stock-list' %}",
|
||||
});
|
||||
|
||||
$("#stock-export").click(function() {
|
||||
launchModalForm("{% url 'stock-export-options' %}", {
|
||||
submit_text: "{% trans 'Export' %}",
|
||||
success: function(response) {
|
||||
var url = "{% url 'stock-export' %}";
|
||||
|
||||
url += "?format=" + response.format;
|
||||
url += "&cascade=" + response.cascade;
|
||||
url += "&part={{ part.id }}";
|
||||
|
||||
location.href = url;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$('#item-create').click(function () {
|
||||
createNewStockItem({
|
||||
reload: true,
|
||||
data: {
|
||||
part: {{ part.id }},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -1,51 +0,0 @@
|
||||
{% extends "part/category.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
{% load static %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/category_navbar.html' with tab='subcategories' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block category_content %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Subcategories" %}</h4>
|
||||
</div>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
|
||||
<div class='filter-list' id='filter-list-category'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='subcategory-table' data-toolbar='#button-toolbar'></table>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
enableNavbar({
|
||||
label: 'category',
|
||||
toggleId: '#category-menu-toggle',
|
||||
});
|
||||
|
||||
loadPartCategoryTable($('#subcategory-table'), {
|
||||
params: {
|
||||
{% if category %}
|
||||
parent: {{ category.pk }}
|
||||
{% else %}
|
||||
parent: 'null'
|
||||
{% endif %}
|
||||
}
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -1,97 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='suppliers' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Part Suppliers" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<div id='button-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button class="btn btn-success" id='supplier-create'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %}
|
||||
</button>
|
||||
<div id='opt-dropdown' class="btn-group">
|
||||
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'>{% trans "Delete" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#button-toolbar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_load %}
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
$('#supplier-create').click(function () {
|
||||
launchModalForm(
|
||||
"{% url 'supplier-part-create' %}",
|
||||
{
|
||||
reload: true,
|
||||
data: {
|
||||
part: {{ part.id }}
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
field: 'supplier',
|
||||
label: '{% trans "New Supplier" %}',
|
||||
title: '{% trans "Create new supplier" %}',
|
||||
url: "{% url 'supplier-create' %}"
|
||||
},
|
||||
{
|
||||
field: 'manufacturer',
|
||||
label: '{% trans "New Manufacturer" %}',
|
||||
title: '{% trans "Create new manufacturer" %}',
|
||||
url: "{% url 'manufacturer-create' %}",
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
$("#supplier-part-delete").click(function() {
|
||||
|
||||
var selections = $("#supplier-table").bootstrapTable("getSelections");
|
||||
|
||||
var parts = [];
|
||||
|
||||
selections.forEach(function(item) {
|
||||
parts.push(item.pk);
|
||||
});
|
||||
|
||||
launchModalForm("{% url 'supplier-part-delete' %}", {
|
||||
data: {
|
||||
parts: parts,
|
||||
},
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
loadSupplierPartTable(
|
||||
"#supplier-table",
|
||||
"{% url 'api-supplier-part-list' %}",
|
||||
{
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
part_detail: false,
|
||||
supplier_detail: true,
|
||||
manufacturer_detail: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
linkButtonsToSelection($("#supplier-table"), ['#supplier-part-options'])
|
||||
|
||||
{% endblock %}
|
@ -1,14 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='track' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Part Tracking" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
{% endblock %}
|
@ -1,39 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/navbar.html' with tab='used' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Assemblies" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div class='filter-list' id='filter-list-usedin'>
|
||||
<!-- Empty div (will be filled out with avilable BOM filters) -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-condensed" id='used-table' data-toolbar='#button-toolbar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadPartTable('#used-table',
|
||||
'{% url "api-part-list" %}',
|
||||
{
|
||||
params: {
|
||||
uses: {{ part.pk }},
|
||||
},
|
||||
filterTarget: '#filter-list-usedin',
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
{% endblock %}
|
@ -1,49 +0,0 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include "part/navbar.html" with tab='variants' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Part Variants" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block details %}
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid'>
|
||||
<div class='btn-group' role='group'>
|
||||
{% if part.is_template and part.active %}
|
||||
<button class='btn btn-success' id='new-variant' title='{% trans "Create new variant" %}'>
|
||||
<span class='fas fa-plus-circle'></span> {% trans "New Variant" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class='filter-list' id='filter-list-variants'>
|
||||
<!-- Empty div (will be filled out with available BOM filters) -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='variants-table' data-toolbar='#button-toolbar'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
loadPartVariantTable($('#variants-table'), {{ part.pk }});
|
||||
|
||||
$('#new-variant').click(function() {
|
||||
launchModalForm(
|
||||
"{% url 'make-part-variant' part.id %}",
|
||||
{
|
||||
follow: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -13,6 +13,107 @@ from InvenTree.status_codes import StockStatus
|
||||
from part.models import Part, PartCategory
|
||||
from stock.models import StockItem
|
||||
from company.models import Company
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
|
||||
class PartOptionsAPITest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Tests for the various OPTIONS endpoints in the /part/ API
|
||||
|
||||
Ensure that the required field details are provided!
|
||||
"""
|
||||
|
||||
roles = [
|
||||
'part.add',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super().setUp()
|
||||
|
||||
def test_part(self):
|
||||
"""
|
||||
Test the Part API OPTIONS
|
||||
"""
|
||||
|
||||
actions = self.getActions(reverse('api-part-list'))['POST']
|
||||
|
||||
# Check that a bunch o' fields are contained
|
||||
for f in ['assembly', 'component', 'description', 'image', 'IPN']:
|
||||
self.assertTrue(f in actions.keys())
|
||||
|
||||
# Active is a 'boolean' field
|
||||
active = actions['active']
|
||||
|
||||
self.assertTrue(active['default'])
|
||||
self.assertEqual(active['help_text'], 'Is this part active?')
|
||||
self.assertEqual(active['type'], 'boolean')
|
||||
self.assertEqual(active['read_only'], False)
|
||||
|
||||
# String field
|
||||
ipn = actions['IPN']
|
||||
self.assertEqual(ipn['type'], 'string')
|
||||
self.assertFalse(ipn['required'])
|
||||
self.assertEqual(ipn['max_length'], 100)
|
||||
self.assertEqual(ipn['help_text'], 'Internal Part Number')
|
||||
|
||||
# Related field
|
||||
category = actions['category']
|
||||
|
||||
self.assertEqual(category['type'], 'related field')
|
||||
self.assertTrue(category['required'])
|
||||
self.assertFalse(category['read_only'])
|
||||
self.assertEqual(category['label'], 'Category')
|
||||
self.assertEqual(category['model'], 'partcategory')
|
||||
self.assertEqual(category['api_url'], reverse('api-part-category-list'))
|
||||
self.assertEqual(category['help_text'], 'Part category')
|
||||
|
||||
def test_category(self):
|
||||
"""
|
||||
Test the PartCategory API OPTIONS endpoint
|
||||
"""
|
||||
|
||||
actions = self.getActions(reverse('api-part-category-list'))
|
||||
|
||||
# actions should *not* contain 'POST' as we do not have the correct role
|
||||
self.assertFalse('POST' in actions)
|
||||
|
||||
self.assignRole('part_category.add')
|
||||
|
||||
actions = self.getActions(reverse('api-part-category-list'))['POST']
|
||||
|
||||
name = actions['name']
|
||||
|
||||
self.assertTrue(name['required'])
|
||||
self.assertEqual(name['label'], 'Name')
|
||||
|
||||
loc = actions['default_location']
|
||||
self.assertEqual(loc['api_url'], reverse('api-location-list'))
|
||||
|
||||
def test_bom_item(self):
|
||||
"""
|
||||
Test the BomItem API OPTIONS endpoint
|
||||
"""
|
||||
|
||||
actions = self.getActions(reverse('api-bom-list'))['POST']
|
||||
|
||||
inherited = actions['inherited']
|
||||
|
||||
self.assertEqual(inherited['type'], 'boolean')
|
||||
|
||||
# 'part' reference
|
||||
part = actions['part']
|
||||
|
||||
self.assertTrue(part['required'])
|
||||
self.assertFalse(part['read_only'])
|
||||
self.assertTrue(part['filters']['assembly'])
|
||||
|
||||
# 'sub_part' reference
|
||||
sub_part = actions['sub_part']
|
||||
|
||||
self.assertTrue(sub_part['required'])
|
||||
self.assertEqual(sub_part['type'], 'related field')
|
||||
self.assertTrue(sub_part['filters']['component'])
|
||||
|
||||
|
||||
class PartAPITest(InvenTreeAPITestCase):
|
||||
@ -311,6 +412,59 @@ class PartAPITest(InvenTreeAPITestCase):
|
||||
|
||||
self.assertEqual(len(data['results']), n)
|
||||
|
||||
def test_default_values(self):
|
||||
"""
|
||||
Tests for 'default' values:
|
||||
|
||||
Ensure that unspecified fields revert to "default" values
|
||||
(as specified in the model field definition)
|
||||
"""
|
||||
|
||||
url = reverse('api-part-list')
|
||||
|
||||
response = self.client.post(url, {
|
||||
'name': 'all defaults',
|
||||
'description': 'my test part',
|
||||
'category': 1,
|
||||
})
|
||||
|
||||
data = response.data
|
||||
|
||||
# Check that the un-specified fields have used correct default values
|
||||
self.assertTrue(data['active'])
|
||||
self.assertFalse(data['virtual'])
|
||||
|
||||
# By default, parts are not purchaseable
|
||||
self.assertFalse(data['purchaseable'])
|
||||
|
||||
# Set the default 'purchaseable' status to True
|
||||
InvenTreeSetting.set_setting(
|
||||
'PART_PURCHASEABLE',
|
||||
True,
|
||||
self.user
|
||||
)
|
||||
|
||||
response = self.client.post(url, {
|
||||
'name': 'all defaults',
|
||||
'description': 'my test part 2',
|
||||
'category': 1,
|
||||
})
|
||||
|
||||
# Part should now be purchaseable by default
|
||||
self.assertTrue(response.data['purchaseable'])
|
||||
|
||||
# "default" values should not be used if the value is specified
|
||||
response = self.client.post(url, {
|
||||
'name': 'all defaults',
|
||||
'description': 'my test part 2',
|
||||
'category': 1,
|
||||
'active': False,
|
||||
'purchaseable': False,
|
||||
})
|
||||
|
||||
self.assertFalse(response.data['active'])
|
||||
self.assertFalse(response.data['purchaseable'])
|
||||
|
||||
|
||||
class PartDetailTests(InvenTreeAPITestCase):
|
||||
"""
|
||||
@ -391,7 +545,14 @@ class PartDetailTests(InvenTreeAPITestCase):
|
||||
|
||||
# Try to remove the part
|
||||
response = self.client.delete(url)
|
||||
|
||||
|
||||
# As the part is 'active' we cannot delete it
|
||||
self.assertEqual(response.status_code, 405)
|
||||
|
||||
# So, let's make it not active
|
||||
response = self.patch(url, {'active': False}, expected_code=200)
|
||||
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
# Part count should have reduced
|
||||
@ -542,8 +703,6 @@ class PartDetailTests(InvenTreeAPITestCase):
|
||||
# And now check that the image has been set
|
||||
p = Part.objects.get(pk=pk)
|
||||
|
||||
print("Image:", p.image.file)
|
||||
|
||||
|
||||
class PartAPIAggregationTest(InvenTreeAPITestCase):
|
||||
"""
|
||||
@ -646,7 +805,7 @@ class PartParameterTest(InvenTreeAPITestCase):
|
||||
Test for listing part parameters
|
||||
"""
|
||||
|
||||
url = reverse('api-part-param-list')
|
||||
url = reverse('api-part-parameter-list')
|
||||
|
||||
response = self.client.get(url, format='json')
|
||||
|
||||
@ -679,7 +838,7 @@ class PartParameterTest(InvenTreeAPITestCase):
|
||||
Test that we can create a param via the API
|
||||
"""
|
||||
|
||||
url = reverse('api-part-param-list')
|
||||
url = reverse('api-part-parameter-list')
|
||||
|
||||
response = self.client.post(
|
||||
url,
|
||||
@ -701,7 +860,7 @@ class PartParameterTest(InvenTreeAPITestCase):
|
||||
Tests for the PartParameter detail endpoint
|
||||
"""
|
||||
|
||||
url = reverse('api-part-param-detail', kwargs={'pk': 5})
|
||||
url = reverse('api-part-parameter-detail', kwargs={'pk': 5})
|
||||
|
||||
response = self.client.get(url)
|
||||
|
||||
|
131
InvenTree/part/test_bom_export.py
Normal file
131
InvenTree/part/test_bom_export.py
Normal file
@ -0,0 +1,131 @@
|
||||
"""
|
||||
Unit testing for BOM export functionality
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
|
||||
class BomExportTest(TestCase):
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
'location',
|
||||
'bom',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Create a user
|
||||
user = get_user_model()
|
||||
|
||||
self.user = user.objects.create_user(
|
||||
username='username',
|
||||
email='user@email.com',
|
||||
password='password'
|
||||
)
|
||||
|
||||
# Put the user into a group with the correct permissions
|
||||
group = Group.objects.create(name='mygroup')
|
||||
self.user.groups.add(group)
|
||||
|
||||
# Give the group *all* the permissions!
|
||||
for rule in group.rule_sets.all():
|
||||
rule.can_view = True
|
||||
rule.can_change = True
|
||||
rule.can_add = True
|
||||
rule.can_delete = True
|
||||
|
||||
rule.save()
|
||||
|
||||
self.client.login(username='username', password='password')
|
||||
|
||||
self.url = reverse('bom-download', kwargs={'pk': 100})
|
||||
|
||||
def test_export_csv(self):
|
||||
"""
|
||||
Test BOM download in CSV format
|
||||
"""
|
||||
|
||||
print("URL", self.url)
|
||||
|
||||
params = {
|
||||
'file_format': 'csv',
|
||||
'cascade': True,
|
||||
'parameter_data': True,
|
||||
'stock_data': True,
|
||||
'supplier_data': True,
|
||||
'manufacturer_data': True,
|
||||
}
|
||||
|
||||
response = self.client.get(self.url, data=params)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
content = response.headers['Content-Disposition']
|
||||
self.assertEqual(content, 'attachment; filename="BOB | Bob | A2_BOM.csv"')
|
||||
|
||||
def test_export_xls(self):
|
||||
"""
|
||||
Test BOM download in XLS format
|
||||
"""
|
||||
|
||||
params = {
|
||||
'file_format': 'xls',
|
||||
'cascade': True,
|
||||
'parameter_data': True,
|
||||
'stock_data': True,
|
||||
'supplier_data': True,
|
||||
'manufacturer_data': True,
|
||||
}
|
||||
|
||||
response = self.client.get(self.url, data=params)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
content = response.headers['Content-Disposition']
|
||||
self.assertEqual(content, 'attachment; filename="BOB | Bob | A2_BOM.xls"')
|
||||
|
||||
def test_export_xlsx(self):
|
||||
"""
|
||||
Test BOM download in XLSX format
|
||||
"""
|
||||
|
||||
params = {
|
||||
'file_format': 'xlsx',
|
||||
'cascade': True,
|
||||
'parameter_data': True,
|
||||
'stock_data': True,
|
||||
'supplier_data': True,
|
||||
'manufacturer_data': True,
|
||||
}
|
||||
|
||||
response = self.client.get(self.url, data=params)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_export_json(self):
|
||||
"""
|
||||
Test BOM download in JSON format
|
||||
"""
|
||||
|
||||
params = {
|
||||
'file_format': 'json',
|
||||
'cascade': True,
|
||||
'parameter_data': True,
|
||||
'stock_data': True,
|
||||
'supplier_data': True,
|
||||
'manufacturer_data': True,
|
||||
}
|
||||
|
||||
response = self.client.get(self.url, data=params)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
content = response.headers['Content-Disposition']
|
||||
self.assertEqual(content, 'attachment; filename="BOB | Bob | A2_BOM.json"')
|
@ -158,21 +158,6 @@ class PartDetailTest(PartViewTestCase):
|
||||
class PartTests(PartViewTestCase):
|
||||
""" Tests for Part forms """
|
||||
|
||||
def test_part_edit(self):
|
||||
|
||||
response = self.client.get(reverse('part-edit', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
keys = response.context.keys()
|
||||
data = str(response.content)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertIn('part', keys)
|
||||
self.assertIn('csrf_token', keys)
|
||||
|
||||
self.assertIn('html_form', data)
|
||||
self.assertIn('"title":', data)
|
||||
|
||||
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')
|
||||
@ -232,29 +217,6 @@ class PartRelatedTests(PartViewTestCase):
|
||||
self.assertEqual(n, 1)
|
||||
|
||||
|
||||
class PartAttachmentTests(PartViewTestCase):
|
||||
|
||||
def test_valid_create(self):
|
||||
""" test creation of an attachment for a valid part """
|
||||
|
||||
response = self.client.get(reverse('part-attachment-create'), {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# TODO - Create a new attachment using this view
|
||||
|
||||
def test_invalid_create(self):
|
||||
""" test creation of an attachment for an invalid part """
|
||||
|
||||
# TODO
|
||||
pass
|
||||
|
||||
def test_edit(self):
|
||||
""" test editing an attachment """
|
||||
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
class PartQRTest(PartViewTestCase):
|
||||
""" Tests for the Part QR Code AJAX view """
|
||||
|
||||
@ -294,11 +256,6 @@ class CategoryTest(PartViewTestCase):
|
||||
# Form should still return OK
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_edit(self):
|
||||
""" Retrieve the part category editing form """
|
||||
response = self.client.get(reverse('category-edit', args=(1,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_set_category(self):
|
||||
""" Test that the "SetCategory" view works """
|
||||
|
||||
|
@ -17,12 +17,6 @@ part_related_urls = [
|
||||
url(r'^(?P<pk>\d+)/delete/?', views.PartRelatedDelete.as_view(), name='part-related-delete'),
|
||||
]
|
||||
|
||||
part_attachment_urls = [
|
||||
url(r'^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'),
|
||||
url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'),
|
||||
url(r'^(?P<pk>\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'),
|
||||
]
|
||||
|
||||
sale_price_break_urls = [
|
||||
url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
|
||||
url(r'^(?P<pk>\d+)/edit/', views.PartSalePriceBreakEdit.as_view(), name='sale-price-break-edit'),
|
||||
@ -39,14 +33,9 @@ part_parameter_urls = [
|
||||
url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'),
|
||||
url(r'^template/(?P<pk>\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'),
|
||||
url(r'^template/(?P<pk>\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'),
|
||||
|
||||
url(r'^new/', views.PartParameterCreate.as_view(), name='part-param-create'),
|
||||
url(r'^(?P<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'),
|
||||
url(r'^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'),
|
||||
]
|
||||
|
||||
part_detail_urls = [
|
||||
url(r'^edit/?', views.PartEdit.as_view(), name='part-edit'),
|
||||
url(r'^delete/?', views.PartDelete.as_view(), name='part-delete'),
|
||||
url(r'^bom-export/?', views.BomExport.as_view(), name='bom-export'),
|
||||
url(r'^bom-download/?', views.BomDownload.as_view(), name='bom-download'),
|
||||
@ -58,30 +47,9 @@ part_detail_urls = [
|
||||
url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'),
|
||||
url(r'^bom-duplicate/?', views.BomDuplicate.as_view(), name='duplicate-bom'),
|
||||
|
||||
url(r'^params/', views.PartDetail.as_view(template_name='part/params.html'), name='part-params'),
|
||||
url(r'^variants/?', views.PartDetail.as_view(template_name='part/variants.html'), name='part-variants'),
|
||||
url(r'^stock/?', views.PartDetail.as_view(template_name='part/stock.html'), name='part-stock'),
|
||||
url(r'^allocation/?', views.PartDetail.as_view(template_name='part/allocation.html'), name='part-allocation'),
|
||||
url(r'^bom/?', views.PartDetail.as_view(template_name='part/bom.html'), name='part-bom'),
|
||||
url(r'^build/?', views.PartDetail.as_view(template_name='part/build.html'), name='part-build'),
|
||||
url(r'^used/?', views.PartDetail.as_view(template_name='part/used_in.html'), name='part-used-in'),
|
||||
url(r'^order-prices/', views.PartPricingView.as_view(template_name='part/order_prices.html'), name='part-order-prices'),
|
||||
url(r'^manufacturers/?', views.PartDetail.as_view(template_name='part/manufacturer.html'), name='part-manufacturers'),
|
||||
url(r'^suppliers/?', views.PartDetail.as_view(template_name='part/supplier.html'), name='part-suppliers'),
|
||||
url(r'^orders/?', views.PartDetail.as_view(template_name='part/orders.html'), name='part-orders'),
|
||||
url(r'^sales-orders/', views.PartDetail.as_view(template_name='part/sales_orders.html'), name='part-sales-orders'),
|
||||
url(r'^sale-prices/', views.PartDetail.as_view(template_name='part/sale_prices.html'), name='part-sale-prices'),
|
||||
url(r'^internal-prices/', views.PartDetail.as_view(template_name='part/internal_prices.html'), name='part-internal-prices'),
|
||||
url(r'^tests/', views.PartDetail.as_view(template_name='part/part_tests.html'), name='part-test-templates'),
|
||||
url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'),
|
||||
url(r'^related-parts/?', views.PartDetail.as_view(template_name='part/related.html'), name='part-related'),
|
||||
url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
|
||||
url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'),
|
||||
|
||||
url(r'^qr_code/?', views.PartQRCode.as_view(), name='part-qr'),
|
||||
|
||||
# Normal thumbnail with form
|
||||
url(r'^thumbnail/?', views.PartImageUpload.as_view(), name='part-image-upload'),
|
||||
url(r'^thumb-select/?', views.PartImageSelect.as_view(), name='part-image-select'),
|
||||
url(r'^thumb-download/', views.PartImageDownloadFromURL.as_view(), name='part-image-download'),
|
||||
|
||||
@ -105,13 +73,9 @@ category_urls = [
|
||||
|
||||
# Category detail views
|
||||
url(r'(?P<pk>\d+)/', include([
|
||||
url(r'^edit/', views.CategoryEdit.as_view(), name='category-edit'),
|
||||
url(r'^delete/', views.CategoryDelete.as_view(), name='category-delete'),
|
||||
url(r'^parameters/', include(category_parameter_urls)),
|
||||
|
||||
url(r'^subcategory/', views.CategoryDetail.as_view(template_name='part/subcategory.html'), name='category-subcategory'),
|
||||
url(r'^parametric/', views.CategoryParametric.as_view(), name='category-parametric'),
|
||||
|
||||
# Anything else
|
||||
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
||||
]))
|
||||
@ -119,7 +83,6 @@ category_urls = [
|
||||
|
||||
part_bom_urls = [
|
||||
url(r'^edit/?', views.BomItemEdit.as_view(), name='bom-item-edit'),
|
||||
url('^delete/?', views.BomItemDelete.as_view(), name='bom-item-delete'),
|
||||
]
|
||||
|
||||
# URL list for part web interface
|
||||
@ -128,6 +91,10 @@ part_urls = [
|
||||
# Create a new part
|
||||
url(r'^new/?', views.PartCreate.as_view(), name='part-create'),
|
||||
|
||||
# Upload a part
|
||||
url(r'^import/', views.PartImport.as_view(), name='part-import'),
|
||||
url(r'^import-api/', views.PartImportAjax.as_view(), name='api-part-import'),
|
||||
|
||||
# Create a new BOM item
|
||||
url(r'^bom/new/?', views.BomItemCreate.as_view(), name='bom-item-create'),
|
||||
|
||||
@ -146,22 +113,12 @@ part_urls = [
|
||||
# Part related
|
||||
url(r'^related-parts/', include(part_related_urls)),
|
||||
|
||||
# Part attachments
|
||||
url(r'^attachment/', include(part_attachment_urls)),
|
||||
|
||||
# Part price breaks
|
||||
url(r'^sale-price/', include(sale_price_break_urls)),
|
||||
|
||||
# Part internal price breaks
|
||||
url(r'^internal-price/', include(internal_price_break_urls)),
|
||||
|
||||
# Part test templates
|
||||
url(r'^test-template/', include([
|
||||
url(r'^new/', views.PartTestTemplateCreate.as_view(), name='part-test-template-create'),
|
||||
url(r'^(?P<pk>\d+)/edit/', views.PartTestTemplateEdit.as_view(), name='part-test-template-edit'),
|
||||
url(r'^(?P<pk>\d+)/delete/', views.PartTestTemplateDelete.as_view(), name='part-test-template-delete'),
|
||||
])),
|
||||
|
||||
# Part parameters
|
||||
url(r'^parameter/', include(part_parameter_urls)),
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user