2
0
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:
Matthias Mair
2021-07-19 21:51:18 +02:00
committed by GitHub
368 changed files with 33194 additions and 31764 deletions

View File

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

View File

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

View File

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

View 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),
),
]

View 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'),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
{% extends "modal_delete_form.html" %}
{% block pre_form_content %}
Are you sure you want to remove this parameter?
{% endblock %}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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"')

View File

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

View File

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