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" %}
-
- {% trans "Order Multiple" %} | {{ part.multiple }} |
- {% if part.base_cost > 0 %}
- {% trans "Base Price (Flat Fee)" %} | {{ part.base_cost }} |
- {% endif %}
-
- {% trans "Price Breaks" %} |
-
-
-
-
- |
-
-
- {% trans "Quantity" %} |
- {% trans "Price" %} |
-
- {% if part.price_breaks.all %}
- {% for pb in part.price_breaks.all %}
-
- {% decimal pb.quantity %} |
-
- {% if pb.currency %}{{ pb.currency.symbol }}{% endif %}
- {% decimal pb.cost %}
- {% if pb.currency %}{{ pb.currency.suffix }}{% endif %}
-
-
-
-
- |
-
- {% endfor %}
- {% else %}
-
-
- {% trans "No price breaks have been added for this part" %}
- |
-
- {% endif %}
+
+
+
+
+
+
{% 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 @@
{% include "search_form.html" %}
- -
+
-
diff --git a/tasks.py b/tasks.py
index 51c5a68849..cbf9d1722c 100644
--- a/tasks.py
+++ b/tasks.py
@@ -108,6 +108,14 @@ def superuser(c):
manage(c, 'createsuperuser', pty=True)
+@task
+def check(c):
+ """
+ Check validity of django codebase
+ """
+
+ manage(c, "check")
+
@task
def migrate(c):
"""