diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index ba3648da30..884bd0c49f 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -11,7 +11,7 @@ from django.core import validators from django import forms from decimal import Decimal -from InvenTree.helpers import normalize +import InvenTree.helpers class InvenTreeURLFormField(FormURLField): @@ -55,7 +55,7 @@ class RoundingDecimalFormField(forms.DecimalField): """ if type(value) == Decimal: - return normalize(value) + return InvenTree.helpers.normalize(value) else: return value diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 4ec84c7912..9b470902b1 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -15,7 +15,8 @@ from django.http import StreamingHttpResponse from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ -from .version import inventreeVersion, inventreeInstanceName +import InvenTree.version + from .settings import MEDIA_URL, STATIC_URL @@ -263,8 +264,8 @@ def MakeBarcode(object_name, object_pk, object_data, **kwargs): data[object_name] = object_pk else: data['tool'] = 'InvenTree' - data['version'] = inventreeVersion() - data['instance'] = inventreeInstanceName() + data['version'] = InvenTree.version.inventreeVersion() + data['instance'] = InvenTree.version.inventreeInstanceName() # Ensure PK is included object_data['id'] = object_pk diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 964be5c359..92568eeb11 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -82,9 +82,10 @@ float: left; } -.navbar-barcode-li { +#navbar-barcode-li { border-left: none; border-right: none; + padding-right: 5px; } .navbar-nav > li { diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py index b1e455283b..548bce12ab 100644 --- a/InvenTree/InvenTree/validators.py +++ b/InvenTree/InvenTree/validators.py @@ -6,7 +6,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from common.models import InvenTreeSetting +import common.models import re @@ -43,7 +43,7 @@ def validate_part_name(value): def validate_part_ipn(value): """ Validate the Part IPN against regex rule """ - pattern = InvenTreeSetting.get_setting('part_ipn_regex') + pattern = common.models.InvenTreeSetting.get_setting('part_ipn_regex') if pattern: match = re.search(pattern, value) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index e000f40076..b64a23a6fb 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -3,15 +3,16 @@ Provides information on the current InvenTree version """ import subprocess -from common.models import InvenTreeSetting import django +import common.models + INVENTREE_SW_VERSION = "0.1.3 pre" def inventreeInstanceName(): """ Returns the InstanceName settings for the current database """ - return InvenTreeSetting.get_setting("InstanceName", "") + return common.models.InvenTreeSetting.get_setting("InstanceName", "") def inventreeVersion(): diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 22514ee7b7..c0faee6c15 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -12,6 +12,7 @@ from rest_framework import generics, permissions from django.conf.urls import url, include from InvenTree.helpers import str2bool +from InvenTree.status_codes import BuildStatus from .models import Build, BuildItem from .serializers import BuildSerializer, BuildItemSerializer @@ -61,6 +62,17 @@ class BuildList(generics.ListCreateAPIView): if status is not None: queryset = queryset.filter(status=status) + # Filter by "active" status + active = self.request.query_params.get('active', None) + + if active is not None: + active = str2bool(active) + + if active: + queryset = queryset.filter(status__in=BuildStatus.ACTIVE_CODES) + else: + queryset = queryset.exclude(status__in=BuildStatus.ACTIVE_CODES) + # Filter by associated part? part = self.request.query_params.get('part', None) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 89e79761e8..35870adde4 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -22,8 +22,8 @@ from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey from InvenTree.status_codes import BuildStatus -from InvenTree.fields import InvenTreeURLField from InvenTree.helpers import decimal2string +import InvenTree.fields from stock import models as StockModels from part import models as PartModels @@ -151,7 +151,7 @@ class Build(MPTTModel): related_name='builds_completed' ) - link = InvenTreeURLField( + link = InvenTree.fields.InvenTreeURLField( verbose_name=_('External Link'), blank=True, help_text=_('Link to external URL') ) diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index 61cccae1bf..36aebff870 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -50,6 +50,15 @@ InvenTree | Allocate Parts return {{ build.quantity }} * row.quantity - sumAllocations(row); } + function setExpandedAllocatedLocation(row) { + // Handle case when stock item does not have a location set + if (row.location_detail == null) { + return 'No stock location set'; + } else { + return row.location_detail.pathstring; + } + } + function reloadTable() { // Reload the build allocation table buildTable.bootstrapTable('refresh'); @@ -76,7 +85,7 @@ InvenTree | Allocate Parts { field: 'stock_item', label: '{% trans "New Stock Item" %}', - title: '{% trans "Create new Stock Item"', + title: '{% trans "Create new Stock Item" %}', url: '{% url "stock-item-create" %}', data: { part: row.sub_part, @@ -146,7 +155,7 @@ InvenTree | Allocate Parts subTable.bootstrapTable({ data: row.allocations, - showHeader: false, + showHeader: true, columns: [ { width: '50%', @@ -177,7 +186,7 @@ InvenTree | Allocate Parts title: '{% trans "Location" %}', formatter: function(value, row, index, field) { {% if build.status == BuildStatus.COMPLETE %} - var text = row.location_detail.pathstring; + var text = setExpandedAllocatedLocation(row); var url = `/stock/location/${row.location}/`; {% else %} var text = row.stock_item_detail.location_name; diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index b8e9959f01..b0a836118d 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -7,6 +7,7 @@ These models are 'generic' and do not fit a particular business logic object. from __future__ import unicode_literals import os +import decimal from django.db import models from django.conf import settings @@ -14,6 +15,8 @@ from django.utils.translation import ugettext as _ from django.core.validators import MinValueValidator, MaxValueValidator from django.core.exceptions import ValidationError +import InvenTree.fields + class InvenTreeSetting(models.Model): """ @@ -159,6 +162,42 @@ class Currency(models.Model): super().save(*args, **kwargs) +class PriceBreak(models.Model): + """ + Represents a PriceBreak model + """ + + class Meta: + abstract = True + + quantity = InvenTree.fields.RoundingDecimalField(max_digits=15, decimal_places=5, default=1, validators=[MinValueValidator(1)]) + + cost = InvenTree.fields.RoundingDecimalField(max_digits=10, decimal_places=5, validators=[MinValueValidator(0)]) + + currency = models.ForeignKey(Currency, blank=True, null=True, on_delete=models.SET_NULL) + + @property + def symbol(self): + return self.currency.symbol if self.currency else '' + + @property + def suffix(self): + return self.currency.suffix if self.currency else '' + + @property + def converted_cost(self): + """ + Return the cost of this price break, converted to the base currency + """ + + scaler = decimal.Decimal(1.0) + + if self.currency: + scaler = self.currency.value + + return self.cost * scaler + + class ColorTheme(models.Model): """ Color Theme Setting """ diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index b938266bd5..2a8d907b76 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -8,7 +8,6 @@ from __future__ import unicode_literals import os import math -from decimal import Decimal from django.utils.translation import gettext_lazy as _ from django.core.validators import MinValueValidator @@ -24,9 +23,10 @@ from stdimage.models import StdImageField from InvenTree.helpers import getMediaUrl, getBlankImage, getBlankThumbnail from InvenTree.helpers import normalize -from InvenTree.fields import InvenTreeURLField, RoundingDecimalField +from InvenTree.fields import InvenTreeURLField from InvenTree.status_codes import PurchaseOrderStatus -from common.models import Currency + +import common.models def rename_company_image(instance, filename): @@ -433,7 +433,7 @@ class SupplierPart(models.Model): return s -class SupplierPriceBreak(models.Model): +class SupplierPriceBreak(common.models.PriceBreak): """ Represents a quantity price break for a SupplierPart. - Suppliers can offer discounts at larger quantities - SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s) @@ -447,23 +447,6 @@ class SupplierPriceBreak(models.Model): part = models.ForeignKey(SupplierPart, on_delete=models.CASCADE, related_name='pricebreaks') - quantity = RoundingDecimalField(max_digits=15, decimal_places=5, default=1, validators=[MinValueValidator(1)]) - - cost = RoundingDecimalField(max_digits=10, decimal_places=5, validators=[MinValueValidator(0)]) - - currency = models.ForeignKey(Currency, blank=True, null=True, on_delete=models.SET_NULL) - - @property - def converted_cost(self): - """ Return the cost of this price break, converted to the base currency """ - - scaler = Decimal(1.0) - - if self.currency: - scaler = self.currency.value - - return self.cost * scaler - class Meta: unique_together = ("part", "quantity") diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index a80b3b55a9..f6de7d4f50 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -137,11 +137,22 @@ class SupplierPartSerializer(InvenTreeModelSerializer): class SupplierPriceBreakSerializer(InvenTreeModelSerializer): """ Serializer for SupplierPriceBreak object """ + symbol = serializers.CharField(read_only=True) + + suffix = serializers.CharField(read_only=True) + + quantity = serializers.FloatField() + + cost = serializers.FloatField() + class Meta: model = SupplierPriceBreak fields = [ 'pk', 'part', 'quantity', - 'cost' + 'cost', + 'currency', + 'symbol', + 'suffix', ] diff --git a/InvenTree/company/templates/company/supplier_part_pricing.html b/InvenTree/company/templates/company/supplier_part_pricing.html index 080871ace7..28cc917e1a 100644 --- a/InvenTree/company/templates/company/supplier_part_pricing.html +++ b/InvenTree/company/templates/company/supplier_part_pricing.html @@ -10,45 +10,12 @@

{% trans "Pricing Information" %}

- - - {% if part.base_cost > 0 %} - - {% endif %} - - - - - - - - - {% if part.price_breaks.all %} - {% for pb in part.price_breaks.all %} - - - - - {% endfor %} - {% else %} - - - - {% endif %} + +
+ +
+ +
{% trans "Order Multiple" %}{{ part.multiple }}
{% trans "Base Price (Flat Fee)" %}{{ part.base_cost }}
{% trans "Price Breaks" %} -
- -
-
{% trans "Quantity" %}{% trans "Price" %}
{% decimal pb.quantity %} - {% if pb.currency %}{{ pb.currency.symbol }}{% endif %} - {% decimal pb.cost %} - {% if pb.currency %}{{ pb.currency.suffix }}{% endif %} -
- - -
-
- {% trans "No price breaks have been added for this part" %} -
{% endblock %} @@ -56,7 +23,80 @@ {% block js_ready %} {{ block.super }} +function reloadPriceBreaks() { + $("#price-break-table").bootstrapTable("refresh"); +} +$('#price-break-table').inventreeTable({ + name: 'buypricebreaks', + formatNoMatches: function() { return "{% trans "No price break information found" %}"; }, + queryParams: { + part: {{ part.id }}, + }, + url: "{% url 'api-part-supplier-price' %}", + onLoadSuccess: function() { + var table = $('#price-break-table'); + + table.find('.button-price-break-delete').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm( + `/price-break/${pk}/delete/`, + { + success: reloadPriceBreaks + } + ); + }); + + table.find('.button-price-break-edit').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm( + `/price-break/${pk}/edit/`, + { + success: reloadPriceBreaks + } + ); + }); + }, + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + switchable: false, + }, + { + field: 'quantity', + title: '{% trans "Quantity" %}', + sortable: true, + }, + { + field: 'cost', + title: '{% trans "Price" %}', + sortable: true, + formatter: function(value, row, index) { + var html = ''; + + html += row.symbol || ''; + html += value; + + if (row.suffix) { + html += ' ' + row.suffix || ''; + } + + html += `
` + + 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 += `
`; + + return html; + } + }, + ] +}); $('#new-price-break').click(function() { launchModalForm("{% url 'price-break-create' %}", @@ -69,24 +109,4 @@ $('#new-price-break').click(function() { ); }); -$('.pb-edit-button').click(function() { - var button = $(this); - - launchModalForm(button.attr('url'), - { - reload: true, - } - ); -}); - -$('.pb-delete-button').click(function() { - var button = $(this); - - launchModalForm(button.attr('url'), - { - reload: true, - } - ); -}); - {% endblock %} \ No newline at end of file diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index 457cf74ec2..9ef6adea0e 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -401,7 +401,7 @@ class PriceBreakCreate(AjaxCreateView): def get_data(self): return { - 'success': 'Added new price break' + 'success': _('Added new price break') } def get_part(self): diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 568a48034f..fce80eb219 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -13,6 +13,7 @@ from .models import PartAttachment, PartStar from .models import BomItem from .models import PartParameterTemplate, PartParameter from .models import PartTestTemplate +from .models import PartSellPriceBreak from stock.models import StockLocation from company.models import SupplierPart @@ -257,6 +258,14 @@ class ParameterAdmin(ImportExportModelAdmin): list_display = ('part', 'template', 'data') +class PartSellPriceBreakAdmin(admin.ModelAdmin): + + class Meta: + model = PartSellPriceBreak + + list_display = ('part', 'quantity', 'cost', 'currency') + + admin.site.register(Part, PartAdmin) admin.site.register(PartCategory, PartCategoryAdmin) admin.site.register(PartAttachment, PartAttachmentAdmin) @@ -265,3 +274,4 @@ admin.site.register(BomItem, BomItemAdmin) admin.site.register(PartParameterTemplate, ParameterTemplateAdmin) admin.site.register(PartParameter, ParameterAdmin) admin.site.register(PartTestTemplate, PartTestTemplateAdmin) +admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index abc4181895..72b72f0dae 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -20,6 +20,7 @@ from django.urls import reverse from .models import Part, PartCategory, BomItem, PartStar from .models import PartParameter, PartParameterTemplate from .models import PartAttachment, PartTestTemplate +from .models import PartSellPriceBreak from . import serializers as part_serializers @@ -107,6 +108,27 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): queryset = PartCategory.objects.all() +class PartSalePriceList(generics.ListCreateAPIView): + """ + API endpoint for list view of PartSalePriceBreak model + """ + + queryset = PartSellPriceBreak.objects.all() + serializer_class = part_serializers.PartSalePriceSerializer + + permission_classes = [ + permissions.IsAuthenticated, + ] + + filter_backends = [ + DjangoFilterBackend + ] + + filter_fields = [ + 'part', + ] + + class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ API endpoint for listing (and creating) a PartAttachment (file upload). @@ -405,6 +427,28 @@ class PartList(generics.ListCreateAPIView): except (ValueError, Part.DoesNotExist): pass + # Filter by whether the BOM has been validated (or not) + bom_valid = params.get('bom_valid', None) + + # TODO: Querying bom_valid status may be quite expensive + # TODO: (It needs to be profiled!) + # TODO: It might be worth caching the bom_valid status to a database column + + if bom_valid is not None: + + bom_valid = str2bool(bom_valid) + + # Limit queryset to active assemblies + queryset = queryset.filter(active=True, assembly=True) + + pks = [] + + for part in queryset: + if part.is_bom_valid() == bom_valid: + pks.append(part.pk) + + queryset = queryset.filter(pk__in=pks) + # Filter by 'starred' parts? starred = params.get('starred', None) @@ -454,6 +498,7 @@ class PartList(generics.ListCreateAPIView): # Filter by whether the part has stock has_stock = params.get("has_stock", None) + if has_stock is not None: has_stock = str2bool(has_stock) @@ -477,6 +522,36 @@ class PartList(generics.ListCreateAPIView): # Filter items which have an 'in_stock' level higher than 'minimum_stock' queryset = queryset.filter(Q(in_stock__gte=F('minimum_stock'))) + # Filter by "parts which need stock to complete build" + stock_to_build = params.get('stock_to_build', None) + + # TODO: This is super expensive, database query wise... + # TODO: Need to figure out a cheaper way of making this filter query + + if stock_to_build is not None: + # Filter only active parts + queryset = queryset.filter(active=True) + parts_need_stock = [] + + # Find parts with active builds + # where any subpart's stock is lower than quantity being built + for part in queryset: + if part.active_builds and part.can_build < part.quantity_being_built: + parts_need_stock.append(part.pk) + + queryset = queryset.filter(pk__in=parts_need_stock) + + # Limit choices + limit = params.get('limit', None) + + if limit is not None: + try: + limit = int(limit) + if limit > 0: + queryset = queryset[:limit] + except ValueError: + pass + return queryset permission_classes = [ @@ -502,6 +577,7 @@ class PartList(generics.ListCreateAPIView): ordering_fields = [ 'name', + 'creation_date', ] # Default ordering @@ -755,6 +831,11 @@ part_api_urls = [ url(r'^(?P\d+)/?', PartStarDetail.as_view(), name='api-part-star-detail'), url(r'^$', PartStarList.as_view(), name='api-part-star-list'), ])), + + # Base URL for part sale pricing + url(r'^sale-price/', include([ + url(r'^.*$', PartSalePriceList.as_view(), name='api-part-sale-price-list'), + ])), # Base URL for PartParameter API endpoints url(r'^parameter/', include([ diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index ffcb46114a..0a15d598bd 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -17,6 +17,8 @@ from .models import Part, PartCategory, PartAttachment from .models import BomItem from .models import PartParameterTemplate, PartParameter from .models import PartTestTemplate +from .models import PartSellPriceBreak + from common.models import Currency @@ -253,3 +255,22 @@ class PartPriceForm(forms.Form): 'quantity', 'currency', ] + + +class EditPartSalePriceBreakForm(HelperForm): + """ + Form for creating / editing a sale price for a part + """ + + quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5) + + cost = RoundingDecimalFormField(max_digits=10, decimal_places=5) + + class Meta: + model = PartSellPriceBreak + fields = [ + 'part', + 'quantity', + 'cost', + 'currency', + ] diff --git a/InvenTree/part/migrations/0049_partsellpricebreak.py b/InvenTree/part/migrations/0049_partsellpricebreak.py new file mode 100644 index 0000000000..1d49dcbfac --- /dev/null +++ b/InvenTree/part/migrations/0049_partsellpricebreak.py @@ -0,0 +1,30 @@ +# Generated by Django 3.0.7 on 2020-09-17 13:22 + +import InvenTree.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0007_colortheme'), + ('part', '0048_auto_20200902_1404'), + ] + + operations = [ + migrations.CreateModel( + name='PartSellPriceBreak', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, max_digits=15, validators=[django.core.validators.MinValueValidator(1)])), + ('cost', InvenTree.fields.RoundingDecimalField(decimal_places=5, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), + ('currency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.Currency')), + ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='salepricebreaks', to='part.Part')), + ], + options={ + 'unique_together': {('part', 'quantity')}, + }, + ), + ] diff --git a/InvenTree/part/migrations/0050_auto_20200917_2315.py b/InvenTree/part/migrations/0050_auto_20200917_2315.py new file mode 100644 index 0000000000..635294093d --- /dev/null +++ b/InvenTree/part/migrations/0050_auto_20200917_2315.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-09-17 23:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0049_partsellpricebreak'), + ] + + operations = [ + migrations.AlterField( + model_name='partsellpricebreak', + name='part', + field=models.ForeignKey(limit_choices_to={'salable': True}, on_delete=django.db.models.deletion.CASCADE, related_name='salepricebreaks', to='part.Part'), + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index f1b0890cba..afb2dfa64e 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -46,6 +46,8 @@ from order import models as OrderModels from company.models import SupplierPart from stock import models as StockModels +import common.models + class PartCategory(InvenTreeTree): """ PartCategory provides hierarchical organization of Part objects. @@ -845,7 +847,6 @@ class Part(MPTTModel): return str(hash.digest()) - @property def is_bom_valid(self): """ Check if the BOM is 'valid' - if the calculated checksum matches the stored value """ @@ -1227,6 +1228,21 @@ class PartAttachment(InvenTreeAttachment): related_name='attachments') +class PartSellPriceBreak(common.models.PriceBreak): + """ + Represents a price break for selling this part + """ + + part = models.ForeignKey( + Part, on_delete=models.CASCADE, + related_name='salepricebreaks', + limit_choices_to={'salable': True} + ) + + class Meta: + unique_together = ('part', 'quantity') + + class PartStar(models.Model): """ A PartStar object creates a relationship between a User and a Part. diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 8adb26680e..7c73e9f98b 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -12,6 +12,7 @@ from .models import BomItem from .models import PartParameter, PartParameterTemplate from .models import PartAttachment from .models import PartTestTemplate +from .models import PartSellPriceBreak from stock.models import StockItem @@ -87,6 +88,32 @@ class PartTestTemplateSerializer(InvenTreeModelSerializer): ] +class PartSalePriceSerializer(InvenTreeModelSerializer): + """ + Serializer for sale prices for Part model. + """ + + symbol = serializers.CharField(read_only=True) + + suffix = serializers.CharField(read_only=True) + + quantity = serializers.FloatField() + + cost = serializers.FloatField() + + class Meta: + model = PartSellPriceBreak + fields = [ + 'pk', + 'part', + 'quantity', + 'cost', + 'currency', + 'symbol', + 'suffix', + ] + + class PartThumbSerializer(serializers.Serializer): """ Serializer for the 'image' field of the Part model. diff --git a/InvenTree/part/templates/part/sale_prices.html b/InvenTree/part/templates/part/sale_prices.html new file mode 100644 index 0000000000..8d3cc61afd --- /dev/null +++ b/InvenTree/part/templates/part/sale_prices.html @@ -0,0 +1,110 @@ +{% extends "part/part_base.html" %} +{% load static %} +{% load i18n %} + +{% block details %} + +{% include 'part/tabs.html' with tab='sales-prices' %} + +

{% trans "Sale Price" %}

+
+ +
+ +
+ + +
+ +{% 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' %}", + onLoadSuccess: 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: 'cost', + title: '{% trans "Price" %}', + sortable: true, + formatter: function(value, row, index) { + var html = ''; + + html += row.symbol || ''; + html += value; + + if (row.suffix) { + html += ' ' + row.suffix || ''; + } + + html += `
` + + 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 += `
`; + + return html; + } + }, + ] +}) + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index ffb92cf191..1eab299ed5 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -46,6 +46,9 @@ {% endif %} {% if part.salable %} +
  • + {% trans "Sale Price" %} +
  • {% trans "Sales Orders" %} {{ part.sales_orders|length }} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 2707a563c7..e61947e243 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -18,6 +18,12 @@ part_attachment_urls = [ url(r'^(?P\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\d+)/edit/', views.PartSalePriceBreakEdit.as_view(), name='sale-price-break-edit'), + url(r'^(?P\d+)/delete/', views.PartSalePriceBreakDelete.as_view(), name='sale-price-break-delete'), +] + part_parameter_urls = [ url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), @@ -27,7 +33,6 @@ part_parameter_urls = [ url(r'^new/', views.PartParameterCreate.as_view(), name='part-param-create'), url(r'^(?P\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'), url(r'^(?P\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'), - ] part_detail_urls = [ @@ -52,6 +57,7 @@ part_detail_urls = [ 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'^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'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), @@ -108,6 +114,9 @@ part_urls = [ # Part attachments url(r'^attachment/', include(part_attachment_urls)), + # Part price breaks + url(r'^sale-price/', include(sale_price_break_urls)), + # Part test templates url(r'^test-template/', include([ url(r'^new/', views.PartTestTemplateCreate.as_view(), name='part-test-template-create'), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 58ece9d0b0..ccf607afc0 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -26,6 +26,7 @@ from .models import PartParameterTemplate, PartParameter from .models import BomItem from .models import match_part_names from .models import PartTestTemplate +from .models import PartSellPriceBreak from common.models import Currency, InvenTreeSetting from company.models import SupplierPart @@ -2097,3 +2098,75 @@ class BomItemDelete(AjaxDeleteView): ajax_template_name = 'part/bom-delete.html' context_object_name = 'item' ajax_form_title = _('Confim BOM item deletion') + + +class PartSalePriceBreakCreate(AjaxCreateView): + """ View for creating a sale price break for a part """ + + model = PartSellPriceBreak + form_class = part_forms.EditPartSalePriceBreakForm + ajax_form_title = _('Add Price Break') + + def get_data(self): + return { + 'success': _('Added new price break') + } + + def get_part(self): + try: + part = Part.objects.get(id=self.request.GET.get('part')) + except (ValueError, Part.DoesNotExist): + part = None + + if part is None: + try: + part = Part.objects.get(id=self.request.POST.get('part')) + except (ValueError, Part.DoesNotExist): + part = None + + return part + + def get_form(self): + + form = super(AjaxCreateView, self).get_form() + form.fields['part'].widget = HiddenInput() + + return form + + def get_initial(self): + + initials = super(AjaxCreateView, self).get_initial() + + initials['part'] = self.get_part() + + # Pre-select the default currency + try: + base = Currency.objects.get(base=True) + initials['currency'] = base + except Currency.DoesNotExist: + pass + + return initials + + +class PartSalePriceBreakEdit(AjaxUpdateView): + """ View for editing a sale price break """ + + model = PartSellPriceBreak + form_class = part_forms.EditPartSalePriceBreakForm + ajax_form_title = _('Edit Price Break') + + def get_form(self): + + form = super().get_form() + form.fields['part'].widget = HiddenInput() + + return form + + +class PartSalePriceBreakDelete(AjaxDeleteView): + """ View for deleting a sale price break """ + + model = PartSellPriceBreak + ajax_form_title = _("Delete Price Break") + ajax_template_name = "modal_delete_form.html" diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 7741c67129..54cb5a62cf 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -440,6 +440,8 @@ class StockList(generics.ListCreateAPIView): params = self.request.query_params + queryset = super().filter_queryset(queryset) + # Perform basic filtering: # Note: We do not let DRF filter here, it be slow AF @@ -680,6 +682,14 @@ class StockList(generics.ListCreateAPIView): filter_fields = [ ] + search_fields = [ + 'serial', + 'batch', + 'part__name', + 'part__IPN', + 'part__description' + ] + class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ diff --git a/InvenTree/templates/InvenTree/bom_invalid.html b/InvenTree/templates/InvenTree/bom_invalid.html new file mode 100644 index 0000000000..4a2fe5856c --- /dev/null +++ b/InvenTree/templates/InvenTree/bom_invalid.html @@ -0,0 +1,15 @@ +{% extends "collapse_index.html" %} + +{% load i18n %} + +{% block collapse_title %} + +{% trans "BOM Waiting Validation" %} +{% endblock %} + +{% block collapse_content %} + + +
    + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/build_pending.html b/InvenTree/templates/InvenTree/build_pending.html new file mode 100644 index 0000000000..1b8ebc19c2 --- /dev/null +++ b/InvenTree/templates/InvenTree/build_pending.html @@ -0,0 +1,15 @@ +{% extends "collapse_index.html" %} + +{% load i18n %} + +{% block collapse_title %} + +{% trans "Pending Builds" %} +{% endblock %} + +{% block collapse_content %} + + +
    + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/index.html b/InvenTree/templates/InvenTree/index.html index 570378e55d..df2ae1414a 100644 --- a/InvenTree/templates/InvenTree/index.html +++ b/InvenTree/templates/InvenTree/index.html @@ -7,9 +7,43 @@ InvenTree | Index {% block content %}

    InvenTree


    -{% include "InvenTree/starred_parts.html" with collapse_id="starred" %} -{% include "InvenTree/low_stock.html" with collapse_id="order" %} +
    +
    + {% include "InvenTree/latest_parts.html" with collapse_id="latest_parts" %} +
    +
    + {% include "InvenTree/starred_parts.html" with collapse_id="starred" %} +
    +
    + + +
    +
    + {% include "InvenTree/bom_invalid.html" with collapse_id="bom_invalid" %} +
    +
    + {% include "InvenTree/build_pending.html" with collapse_id="build_pending" %} +
    +
    + +
    +
    + {% include "InvenTree/low_stock.html" with collapse_id="order" %} +
    +
    + {% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %} +
    +
    + +
    +
    + {% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %} +
    +
    + {% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %} +
    +
    {% endblock %} @@ -21,16 +55,71 @@ InvenTree | Index {{ block.super }} -loadPartTable("#starred-parts-table", "{% url 'api-part-list' %}", { +loadSimplePartTable("#latest-parts-table", "{% url 'api-part-list' %}", { + params: { + ordering: "-creation_date", + limit: 10, + }, + name: 'latest_parts', +}); + +loadSimplePartTable("#starred-parts-table", "{% url 'api-part-list' %}", { params: { "starred": true, + }, + name: 'starred_parts', +}); + +loadSimplePartTable("#bom-invalid-table", "{% url 'api-part-list' %}", { + params: { + "bom_valid": false, + }, + name: 'bom_invalid_parts', +}); + +loadBuildTable("#build-pending-table", { + url: "{% url 'api-build-list' %}", + params: { + part_detail: true, + active: true, + }, + disableFilters: true, +}); + +loadSimplePartTable("#low-stock-table", "{% url 'api-part-list' %}", { + params: { + low_stock: true, + }, + name: "low_stock_parts", +}); + +loadSimplePartTable("#stock-to-build-table", "{% url 'api-part-list' %}", { + params: { + stock_to_build: true, + }, + name: "to_build_parts", +}); + +loadPurchaseOrderTable("#po-outstanding-table", { + url: "{% url 'api-po-list' %}", + params: { + supplier_detail: true, + outstanding: true, } }); -loadPartTable("#low-stock-table", "{% url 'api-part-list' %}", { +loadSalesOrderTable("#so-outstanding-table", { + url: "{% url 'api-so-list' %}", params: { - "low_stock": true, - } + customer_detail: true, + outstanding: true, + }, +}); + +$("#latest-parts-table").on('load-success.bs.table', function() { + var count = $("#latest-parts-table").bootstrapTable('getData').length; + + $("#latest-parts-count").html(count); }); $("#starred-parts-table").on('load-success.bs.table', function() { @@ -39,11 +128,40 @@ $("#starred-parts-table").on('load-success.bs.table', function() { $("#starred-parts-count").html(count); }); +$("#bom-invalid-table").on('load-success.bs.table', function() { + var count = $("#bom-invalid-table").bootstrapTable('getData').length; + + $("#bom-invalid-count").html(count); +}); + +$("#build-pending-table").on('load-success.bs.table', function() { + var count = $("#build-pending-table").bootstrapTable('getData').length; + + $("#build-pending-count").html(count); +}); + $("#low-stock-table").on('load-success.bs.table', function() { var count = $("#low-stock-table").bootstrapTable('getData').length; $("#low-stock-count").html(count); }); +$("#stock-to-build-table").on('load-success.bs.table', function() { + var count = $("#stock-to-build-table").bootstrapTable('getData').length; + + $("#stock-to-build-count").html(count); +}); + +$("#po-outstanding-table").on('load-success.bs.table', function() { + var count = $("#po-outstanding-table").bootstrapTable('getData').length; + + $("#po-outstanding-count").html(count); +}); + +$("#so-outstanding-table").on('load-success.bs.table', function() { + var count = $("#so-outstanding-table").bootstrapTable('getData').length; + + $("#so-outstanding-count").html(count); +}); {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/latest_parts.html b/InvenTree/templates/InvenTree/latest_parts.html new file mode 100644 index 0000000000..35e55b7cd2 --- /dev/null +++ b/InvenTree/templates/InvenTree/latest_parts.html @@ -0,0 +1,15 @@ +{% extends "collapse_index.html" %} + +{% load i18n %} + +{% block collapse_title %} + +{% trans "Latest Parts" %} +{% endblock %} + +{% block collapse_content %} + + +
    + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/low_stock.html b/InvenTree/templates/InvenTree/low_stock.html index edafab1756..e7a3dbddc7 100644 --- a/InvenTree/templates/InvenTree/low_stock.html +++ b/InvenTree/templates/InvenTree/low_stock.html @@ -1,10 +1,10 @@ -{% extends "collapse.html" %} +{% extends "collapse_index.html" %} {% load i18n %} {% block collapse_title %} -{% trans "Low Stock" %}0 +{% trans "Low Stock" %} {% endblock %} {% block collapse_content %} diff --git a/InvenTree/templates/InvenTree/parts_to_build.html b/InvenTree/templates/InvenTree/parts_to_build.html deleted file mode 100644 index 156e3dda22..0000000000 --- a/InvenTree/templates/InvenTree/parts_to_build.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "collapse.html" %} -{% block collapse_title %} - -Parts to Build{{ to_build | length }} -{% endblock %} - -{% block collapse_heading %} -There are {{ to_build | length }} parts which need building. -{% endblock %} - -{% block collapse_content %} - -{% include "required_part_table.html" with parts=to_build table_id="to-build-table" %} - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/po_outstanding.html b/InvenTree/templates/InvenTree/po_outstanding.html new file mode 100644 index 0000000000..628695fa68 --- /dev/null +++ b/InvenTree/templates/InvenTree/po_outstanding.html @@ -0,0 +1,15 @@ +{% extends "collapse_index.html" %} + +{% load i18n %} + +{% block collapse_title %} + +{% trans "Outstanding Purchase Orders" %} +{% endblock %} + +{% block collapse_content %} + + +
    + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/required_stock_build.html b/InvenTree/templates/InvenTree/required_stock_build.html new file mode 100644 index 0000000000..fd6ade4a4e --- /dev/null +++ b/InvenTree/templates/InvenTree/required_stock_build.html @@ -0,0 +1,15 @@ +{% extends "collapse_index.html" %} + +{% load i18n %} + +{% block collapse_title %} + +{% trans "Require Stock To Complete Build" %} +{% endblock %} + +{% block collapse_content %} + + +
    + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/search.html b/InvenTree/templates/InvenTree/search.html index 76fd62b697..c030fb42a7 100644 --- a/InvenTree/templates/InvenTree/search.html +++ b/InvenTree/templates/InvenTree/search.html @@ -32,6 +32,8 @@ InvenTree | {% trans "Search Results" %} {% include "InvenTree/search_stock_location.html" with collapse_id="locations" %} +{% include "InvenTree/search_stock_items.html" with collapse_id="stock" %} + {% endblock %} {% block js_load %} @@ -42,9 +44,8 @@ InvenTree | {% trans "Search Results" %} {% block js_ready %} {{ block.super }} - $(".panel-group").hide(); - function onSearchResults(table, output) { + $(table).on('load-success.bs.table', function() { var panel = $(output).closest('.panel-group'); @@ -56,7 +57,6 @@ InvenTree | {% trans "Search Results" %} text = 'No results' $(panel).hide(); - } else { text = n + ' result'; @@ -67,17 +67,23 @@ InvenTree | {% trans "Search Results" %} $(panel).show(); + var collapse = panel.find('.panel-collapse'); + + collapse.collapse('show'); + $("#no-search-results").hide(); } - $(output).html(text); + $(output).html(`${text}`); }); } onSearchResults("#category-results-table", "#category-results-count"); onSearchResults("#location-results-table", "#location-results-count"); - + + onSearchResults("#stock-results-table", "#stock-results-count"); + onSearchResults('#part-results-table', '#part-result-count'); onSearchResults('#company-results-table', '#company-result-count'); @@ -104,6 +110,85 @@ InvenTree | {% trans "Search Results" %} ], }); + $('#stock-results-table').inventreeTable({ + url: "{% url 'api-stock-list' %}", + queryParams: { + search: "{{ query }}", + part_detail: true, + location_detail: true, + }, + columns: [ + { + field: 'part', + title: "{% trans "Part" %}", + sortable: true, + formatter: function(value, row) { + var url = `/stock/item/${row.pk}/`; + var thumb = row.part_detail.thumbnail; + var name = row.part_detail.full_name; + + html = imageHoverIcon(thumb) + renderLink(name, url); + + return html; + } + }, + { + field: 'part_description', + title: '{% trans "Description" %}', + sortable: true, + formatter: function(value, row, index, field) { + return row.part_detail.description; + } + }, + { + field: 'quantity', + title: '{% trans "Stock" %}', + sortable: true, + formatter: function(value, row, index, field) { + + var val = parseFloat(value); + + // If there is a single unit with a serial number, use the serial number + if (row.serial && row.quantity == 1) { + val = '# ' + row.serial; + } else { + val = +val.toFixed(5); + } + + var html = renderLink(val, `/stock/item/${row.pk}/`); + + return html; + } + }, + { + field: 'status', + title: '{% trans "Status" %}', + sortable: 'true', + formatter: function(value, row, index, field) { + return stockStatusDisplay(value); + }, + }, + { + field: 'location_detail.pathstring', + title: '{% trans "Location" %}', + sortable: true, + formatter: function(value, row, index, field) { + if (value) { + return renderLink(value, `/stock/location/${row.location}/`); + } + else { + if (row.customer) { + var text = "{% trans "Shipped to customer" %}"; + return renderLink(text, `/company/${row.customer}/assigned-stock/`); + } else { + return '{% trans "No stock location set" %}'; + } + } + } + }, + ] + }); + $("#location-results-table").inventreeTable({ url: "{% url 'api-location-list' %}", diff --git a/InvenTree/templates/InvenTree/search_stock_items.html b/InvenTree/templates/InvenTree/search_stock_items.html new file mode 100644 index 0000000000..16d1542d96 --- /dev/null +++ b/InvenTree/templates/InvenTree/search_stock_items.html @@ -0,0 +1,16 @@ +{% extends "collapse.html" %} + +{% load i18n %} + +{% block collapse_title %} +

    {% trans "Stock Items" %}

    +{% endblock %} + +{% block collapse_heading %} +

    {% include "InvenTree/searching.html" %}

    +{% endblock %} + +{% block collapse_content %} + +
    +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/searching.html b/InvenTree/templates/InvenTree/searching.html index 5821515ad7..cb33727a9e 100644 --- a/InvenTree/templates/InvenTree/searching.html +++ b/InvenTree/templates/InvenTree/searching.html @@ -1 +1,3 @@ - Searching \ No newline at end of file +{% load i18n %} + + {% trans "Searching" %} diff --git a/InvenTree/templates/InvenTree/so_outstanding.html b/InvenTree/templates/InvenTree/so_outstanding.html new file mode 100644 index 0000000000..29d0261b8b --- /dev/null +++ b/InvenTree/templates/InvenTree/so_outstanding.html @@ -0,0 +1,15 @@ +{% extends "collapse_index.html" %} + +{% load i18n %} + +{% block collapse_title %} + +{% trans "Outstanding Sales Orders" %} +{% endblock %} + +{% block collapse_content %} + + +
    + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/starred_parts.html b/InvenTree/templates/InvenTree/starred_parts.html index f13987e3c5..a0801566c2 100644 --- a/InvenTree/templates/InvenTree/starred_parts.html +++ b/InvenTree/templates/InvenTree/starred_parts.html @@ -1,15 +1,15 @@ -{% extends "collapse.html" %} +{% extends "collapse_index.html" %} {% load i18n %} {% block collapse_title %} -{% trans "Starred Parts" %}0 +{% trans "Starred Parts" %} {% endblock %} {% block collapse_content %} - +
    {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/collapse_index.html b/InvenTree/templates/collapse_index.html new file mode 100644 index 0000000000..d87f63b244 --- /dev/null +++ b/InvenTree/templates/collapse_index.html @@ -0,0 +1,19 @@ +{% block collapse_preamble %} +{% endblock %} +
    +
    +
    + + {% block collapse_heading %} + {% endblock %} +
    +
    +
    + {% block collapse_content %} + {% endblock %} +
    +
    +
    +
    \ No newline at end of file diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.html index 8682e9bd81..e36e15ddeb 100644 --- a/InvenTree/templates/js/build.html +++ b/InvenTree/templates/js/build.html @@ -5,7 +5,11 @@ function loadBuildTable(table, options) { var params = options.params || {}; - var filters = loadTableFilters("build"); + var filters = {}; + + if (!options.disableFilters) { + loadTableFilters("build"); + } for (var key in params) { filters[key] = params[key]; @@ -13,7 +17,7 @@ function loadBuildTable(table, options) { setupFilterList("build", table); - table.inventreeTable({ + $(table).inventreeTable({ method: 'get', formatNoMatches: function() { return "{% trans "No builds matching query" %}"; diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.html index 03c98e09a4..5576d91367 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.html @@ -155,6 +155,14 @@ function loadPartVariantTable(table, partId, options) { } +function loadSimplePartTable(table, url, options={}) { + + options.disableFilters = true; + + loadPartTable(table, url, options); +} + + function loadPartTable(table, url, options={}) { /* Load part listing data into specified table. * @@ -332,7 +340,7 @@ function loadPartTable(table, url, options={}) { method: 'get', queryParams: filters, groupBy: false, - name: 'part', + name: options.name || 'part', original: params, formatNoMatches: function() { return "{% trans "No parts found" %}"; }, columns: columns, diff --git a/InvenTree/templates/js/table_filters.html b/InvenTree/templates/js/table_filters.html index 9050edba6f..bd417882e3 100644 --- a/InvenTree/templates/js/table_filters.html +++ b/InvenTree/templates/js/table_filters.html @@ -26,6 +26,10 @@ function getAvailableTableFilters(tableKey) { title: "{% trans "Serial number LTE" %}", description: "{% trans "Serial number less than or equal to" %}", }, + serial: { + title: "{% trans "Serial number" %}", + description: "{% trans "Serial number" %}" + }, }; } @@ -66,6 +70,10 @@ function getAvailableTableFilters(tableKey) { type: 'bool', title: '{% trans "Is Serialized" %}', }, + serial: { + title: "{% trans "Serial number" %}", + description: "{% trans "Serial number" %}" + }, serial_gte: { title: "{% trans "Serial number GTE" %}", description: "{% trans "Serial number greater than or equal to" %}" @@ -109,7 +117,10 @@ function getAvailableTableFilters(tableKey) { title: '{% trans "Build status" %}', options: buildCodes, }, - + pending: { + type: 'bool', + title: '{% trans "Pending" %}', + } }; } diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 3d6bcd0734..cfadad977c 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -36,7 +36,7 @@