Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-09-20 08:02:13 +10:00
commit c2eea91f76
44 changed files with 968 additions and 136 deletions

View File

@ -11,7 +11,7 @@ from django.core import validators
from django import forms from django import forms
from decimal import Decimal from decimal import Decimal
from InvenTree.helpers import normalize import InvenTree.helpers
class InvenTreeURLFormField(FormURLField): class InvenTreeURLFormField(FormURLField):
@ -55,7 +55,7 @@ class RoundingDecimalFormField(forms.DecimalField):
""" """
if type(value) == Decimal: if type(value) == Decimal:
return normalize(value) return InvenTree.helpers.normalize(value)
else: else:
return value return value

View File

@ -15,7 +15,8 @@ from django.http import StreamingHttpResponse
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from .version import inventreeVersion, inventreeInstanceName import InvenTree.version
from .settings import MEDIA_URL, STATIC_URL 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 data[object_name] = object_pk
else: else:
data['tool'] = 'InvenTree' data['tool'] = 'InvenTree'
data['version'] = inventreeVersion() data['version'] = InvenTree.version.inventreeVersion()
data['instance'] = inventreeInstanceName() data['instance'] = InvenTree.version.inventreeInstanceName()
# Ensure PK is included # Ensure PK is included
object_data['id'] = object_pk object_data['id'] = object_pk

View File

@ -82,9 +82,10 @@
float: left; float: left;
} }
.navbar-barcode-li { #navbar-barcode-li {
border-left: none; border-left: none;
border-right: none; border-right: none;
padding-right: 5px;
} }
.navbar-nav > li { .navbar-nav > li {

View File

@ -6,7 +6,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.models import InvenTreeSetting import common.models
import re import re
@ -43,7 +43,7 @@ def validate_part_name(value):
def validate_part_ipn(value): def validate_part_ipn(value):
""" Validate the Part IPN against regex rule """ """ 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: if pattern:
match = re.search(pattern, value) match = re.search(pattern, value)

View File

@ -3,15 +3,16 @@ Provides information on the current InvenTree version
""" """
import subprocess import subprocess
from common.models import InvenTreeSetting
import django import django
import common.models
INVENTREE_SW_VERSION = "0.1.3 pre" INVENTREE_SW_VERSION = "0.1.3 pre"
def inventreeInstanceName(): def inventreeInstanceName():
""" Returns the InstanceName settings for the current database """ """ Returns the InstanceName settings for the current database """
return InvenTreeSetting.get_setting("InstanceName", "") return common.models.InvenTreeSetting.get_setting("InstanceName", "")
def inventreeVersion(): def inventreeVersion():

View File

@ -12,6 +12,7 @@ from rest_framework import generics, permissions
from django.conf.urls import url, include from django.conf.urls import url, include
from InvenTree.helpers import str2bool from InvenTree.helpers import str2bool
from InvenTree.status_codes import BuildStatus
from .models import Build, BuildItem from .models import Build, BuildItem
from .serializers import BuildSerializer, BuildItemSerializer from .serializers import BuildSerializer, BuildItemSerializer
@ -61,6 +62,17 @@ class BuildList(generics.ListCreateAPIView):
if status is not None: if status is not None:
queryset = queryset.filter(status=status) 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? # Filter by associated part?
part = self.request.query_params.get('part', None) part = self.request.query_params.get('part', None)

View File

@ -22,8 +22,8 @@ from markdownx.models import MarkdownxField
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from InvenTree.status_codes import BuildStatus from InvenTree.status_codes import BuildStatus
from InvenTree.fields import InvenTreeURLField
from InvenTree.helpers import decimal2string from InvenTree.helpers import decimal2string
import InvenTree.fields
from stock import models as StockModels from stock import models as StockModels
from part import models as PartModels from part import models as PartModels
@ -151,7 +151,7 @@ class Build(MPTTModel):
related_name='builds_completed' related_name='builds_completed'
) )
link = InvenTreeURLField( link = InvenTree.fields.InvenTreeURLField(
verbose_name=_('External Link'), verbose_name=_('External Link'),
blank=True, help_text=_('Link to external URL') blank=True, help_text=_('Link to external URL')
) )

View File

@ -50,6 +50,15 @@ InvenTree | Allocate Parts
return {{ build.quantity }} * row.quantity - sumAllocations(row); 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() { function reloadTable() {
// Reload the build allocation table // Reload the build allocation table
buildTable.bootstrapTable('refresh'); buildTable.bootstrapTable('refresh');
@ -76,7 +85,7 @@ InvenTree | Allocate Parts
{ {
field: 'stock_item', field: 'stock_item',
label: '{% trans "New Stock Item" %}', label: '{% trans "New Stock Item" %}',
title: '{% trans "Create new Stock Item"', title: '{% trans "Create new Stock Item" %}',
url: '{% url "stock-item-create" %}', url: '{% url "stock-item-create" %}',
data: { data: {
part: row.sub_part, part: row.sub_part,
@ -146,7 +155,7 @@ InvenTree | Allocate Parts
subTable.bootstrapTable({ subTable.bootstrapTable({
data: row.allocations, data: row.allocations,
showHeader: false, showHeader: true,
columns: [ columns: [
{ {
width: '50%', width: '50%',
@ -177,7 +186,7 @@ InvenTree | Allocate Parts
title: '{% trans "Location" %}', title: '{% trans "Location" %}',
formatter: function(value, row, index, field) { formatter: function(value, row, index, field) {
{% if build.status == BuildStatus.COMPLETE %} {% if build.status == BuildStatus.COMPLETE %}
var text = row.location_detail.pathstring; var text = setExpandedAllocatedLocation(row);
var url = `/stock/location/${row.location}/`; var url = `/stock/location/${row.location}/`;
{% else %} {% else %}
var text = row.stock_item_detail.location_name; var text = row.stock_item_detail.location_name;

View File

@ -7,6 +7,7 @@ These models are 'generic' and do not fit a particular business logic object.
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import decimal
from django.db import models from django.db import models
from django.conf import settings 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.validators import MinValueValidator, MaxValueValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
import InvenTree.fields
class InvenTreeSetting(models.Model): class InvenTreeSetting(models.Model):
""" """
@ -159,6 +162,42 @@ class Currency(models.Model):
super().save(*args, **kwargs) 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): class ColorTheme(models.Model):
""" Color Theme Setting """ """ Color Theme Setting """

View File

@ -8,7 +8,6 @@ from __future__ import unicode_literals
import os import os
import math import math
from decimal import Decimal
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator 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 getMediaUrl, getBlankImage, getBlankThumbnail
from InvenTree.helpers import normalize from InvenTree.helpers import normalize
from InvenTree.fields import InvenTreeURLField, RoundingDecimalField from InvenTree.fields import InvenTreeURLField
from InvenTree.status_codes import PurchaseOrderStatus from InvenTree.status_codes import PurchaseOrderStatus
from common.models import Currency
import common.models
def rename_company_image(instance, filename): def rename_company_image(instance, filename):
@ -433,7 +433,7 @@ class SupplierPart(models.Model):
return s return s
class SupplierPriceBreak(models.Model): class SupplierPriceBreak(common.models.PriceBreak):
""" Represents a quantity price break for a SupplierPart. """ Represents a quantity price break for a SupplierPart.
- Suppliers can offer discounts at larger quantities - Suppliers can offer discounts at larger quantities
- SupplierPart(s) may have zero-or-more associated SupplierPriceBreak(s) - 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') 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: class Meta:
unique_together = ("part", "quantity") unique_together = ("part", "quantity")

View File

@ -137,11 +137,22 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
class SupplierPriceBreakSerializer(InvenTreeModelSerializer): class SupplierPriceBreakSerializer(InvenTreeModelSerializer):
""" Serializer for SupplierPriceBreak object """ """ Serializer for SupplierPriceBreak object """
symbol = serializers.CharField(read_only=True)
suffix = serializers.CharField(read_only=True)
quantity = serializers.FloatField()
cost = serializers.FloatField()
class Meta: class Meta:
model = SupplierPriceBreak model = SupplierPriceBreak
fields = [ fields = [
'pk', 'pk',
'part', 'part',
'quantity', 'quantity',
'cost' 'cost',
'currency',
'symbol',
'suffix',
] ]

View File

@ -10,45 +10,12 @@
<hr> <hr>
<h4>{% trans "Pricing Information" %}</h4> <h4>{% trans "Pricing Information" %}</h4>
<table class="table table-striped table-condensed">
<tr><td>{% trans "Order Multiple" %}</td><td>{{ part.multiple }}</td></tr> <div id='price-break-toolbar' class='btn-group'>
{% if part.base_cost > 0 %} <button class='btn btn-primary' id='new-price-break' type='button'>{% trans "Add Price Break" %}</button>
<tr><td>{% trans "Base Price (Flat Fee)" %}</td><td>{{ part.base_cost }}</td></tr> </div>
{% endif %}
<tr> <table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'>
<th>{% trans "Price Breaks" %}</th>
<th>
<div style='float: right;'>
<button class='btn btn-primary' id='new-price-break' type='button'>{% trans "New Price Break" %}</button>
</div>
</th>
</tr>
<tr>
<th>{% trans "Quantity" %}</th>
<th>{% trans "Price" %}</th>
</tr>
{% if part.price_breaks.all %}
{% for pb in part.price_breaks.all %}
<tr>
<td>{% decimal pb.quantity %}</td>
<td>
{% if pb.currency %}{{ pb.currency.symbol }}{% endif %}
{% decimal pb.cost %}
{% if pb.currency %}{{ pb.currency.suffix }}{% endif %}
<div class='btn-group' style='float: right;'>
<button title='Edit Price Break' class='btn btn-default btn-sm pb-edit-button' type='button' url="{% url 'price-break-edit' pb.id %}"><span class='fas fa-edit icon-green'></span></button>
<button title='Delete Price Break' class='btn btn-default btn-sm pb-delete-button' type='button' url="{% url 'price-break-delete' pb.id %}"><span class='fas fa-trash-alt icon-red'></span></button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan='2'>
<span class='warning-msg'><i>{% trans "No price breaks have been added for this part" %}</i></span>
</td>
</tr>
{% endif %}
</table> </table>
{% endblock %} {% endblock %}
@ -56,7 +23,80 @@
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ 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 += `<div class='btn-group float-right' role='group'>`
html += makeIconButton('fa-edit icon-blue', 'button-price-break-edit', row.pk, '{% trans "Edit price break" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}');
html += `</div>`;
return html;
}
},
]
});
$('#new-price-break').click(function() { $('#new-price-break').click(function() {
launchModalForm("{% url 'price-break-create' %}", 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 %} {% endblock %}

View File

@ -401,7 +401,7 @@ class PriceBreakCreate(AjaxCreateView):
def get_data(self): def get_data(self):
return { return {
'success': 'Added new price break' 'success': _('Added new price break')
} }
def get_part(self): def get_part(self):

View File

@ -13,6 +13,7 @@ from .models import PartAttachment, PartStar
from .models import BomItem from .models import BomItem
from .models import PartParameterTemplate, PartParameter from .models import PartParameterTemplate, PartParameter
from .models import PartTestTemplate from .models import PartTestTemplate
from .models import PartSellPriceBreak
from stock.models import StockLocation from stock.models import StockLocation
from company.models import SupplierPart from company.models import SupplierPart
@ -257,6 +258,14 @@ class ParameterAdmin(ImportExportModelAdmin):
list_display = ('part', 'template', 'data') 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(Part, PartAdmin)
admin.site.register(PartCategory, PartCategoryAdmin) admin.site.register(PartCategory, PartCategoryAdmin)
admin.site.register(PartAttachment, PartAttachmentAdmin) admin.site.register(PartAttachment, PartAttachmentAdmin)
@ -265,3 +274,4 @@ admin.site.register(BomItem, BomItemAdmin)
admin.site.register(PartParameterTemplate, ParameterTemplateAdmin) admin.site.register(PartParameterTemplate, ParameterTemplateAdmin)
admin.site.register(PartParameter, ParameterAdmin) admin.site.register(PartParameter, ParameterAdmin)
admin.site.register(PartTestTemplate, PartTestTemplateAdmin) admin.site.register(PartTestTemplate, PartTestTemplateAdmin)
admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin)

View File

@ -20,6 +20,7 @@ from django.urls import reverse
from .models import Part, PartCategory, BomItem, PartStar from .models import Part, PartCategory, BomItem, PartStar
from .models import PartParameter, PartParameterTemplate from .models import PartParameter, PartParameterTemplate
from .models import PartAttachment, PartTestTemplate from .models import PartAttachment, PartTestTemplate
from .models import PartSellPriceBreak
from . import serializers as part_serializers from . import serializers as part_serializers
@ -107,6 +108,27 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = PartCategory.objects.all() 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): class PartAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """
API endpoint for listing (and creating) a PartAttachment (file upload). API endpoint for listing (and creating) a PartAttachment (file upload).
@ -405,6 +427,28 @@ class PartList(generics.ListCreateAPIView):
except (ValueError, Part.DoesNotExist): except (ValueError, Part.DoesNotExist):
pass 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? # Filter by 'starred' parts?
starred = params.get('starred', None) starred = params.get('starred', None)
@ -454,6 +498,7 @@ class PartList(generics.ListCreateAPIView):
# Filter by whether the part has stock # Filter by whether the part has stock
has_stock = params.get("has_stock", None) has_stock = params.get("has_stock", None)
if has_stock is not None: if has_stock is not None:
has_stock = str2bool(has_stock) 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' # Filter items which have an 'in_stock' level higher than 'minimum_stock'
queryset = queryset.filter(Q(in_stock__gte=F('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 return queryset
permission_classes = [ permission_classes = [
@ -502,6 +577,7 @@ class PartList(generics.ListCreateAPIView):
ordering_fields = [ ordering_fields = [
'name', 'name',
'creation_date',
] ]
# Default ordering # Default ordering
@ -755,6 +831,11 @@ part_api_urls = [
url(r'^(?P<pk>\d+)/?', PartStarDetail.as_view(), name='api-part-star-detail'), url(r'^(?P<pk>\d+)/?', PartStarDetail.as_view(), name='api-part-star-detail'),
url(r'^$', PartStarList.as_view(), name='api-part-star-list'), 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 # Base URL for PartParameter API endpoints
url(r'^parameter/', include([ url(r'^parameter/', include([

View File

@ -17,6 +17,8 @@ from .models import Part, PartCategory, PartAttachment
from .models import BomItem from .models import BomItem
from .models import PartParameterTemplate, PartParameter from .models import PartParameterTemplate, PartParameter
from .models import PartTestTemplate from .models import PartTestTemplate
from .models import PartSellPriceBreak
from common.models import Currency from common.models import Currency
@ -253,3 +255,22 @@ class PartPriceForm(forms.Form):
'quantity', 'quantity',
'currency', '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',
]

View File

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

View File

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

View File

@ -46,6 +46,8 @@ from order import models as OrderModels
from company.models import SupplierPart from company.models import SupplierPart
from stock import models as StockModels from stock import models as StockModels
import common.models
class PartCategory(InvenTreeTree): class PartCategory(InvenTreeTree):
""" PartCategory provides hierarchical organization of Part objects. """ PartCategory provides hierarchical organization of Part objects.
@ -845,7 +847,6 @@ class Part(MPTTModel):
return str(hash.digest()) return str(hash.digest())
@property
def is_bom_valid(self): def is_bom_valid(self):
""" Check if the BOM is 'valid' - if the calculated checksum matches the stored value """ Check if the BOM is 'valid' - if the calculated checksum matches the stored value
""" """
@ -1227,6 +1228,21 @@ class PartAttachment(InvenTreeAttachment):
related_name='attachments') 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): class PartStar(models.Model):
""" A PartStar object creates a relationship between a User and a Part. """ A PartStar object creates a relationship between a User and a Part.

View File

@ -12,6 +12,7 @@ from .models import BomItem
from .models import PartParameter, PartParameterTemplate from .models import PartParameter, PartParameterTemplate
from .models import PartAttachment from .models import PartAttachment
from .models import PartTestTemplate from .models import PartTestTemplate
from .models import PartSellPriceBreak
from stock.models import StockItem 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): class PartThumbSerializer(serializers.Serializer):
""" """
Serializer for the 'image' field of the Part model. Serializer for the 'image' field of the Part model.

View File

@ -0,0 +1,110 @@
{% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
{% block details %}
{% include 'part/tabs.html' with tab='sales-prices' %}
<h4>{% trans "Sale Price" %}</h4>
<hr>
<div id='price-break-toolbar' class='btn-group'>
<button class='btn btn-primary' id='new-price-break' type='button'>{% trans "Add Price Break" %}</button>
</div>
<table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'>
</table>
{% endblock %}
{% block js_ready %}
{{ block.super }}
function reloadPriceBreaks() {
$("#price-break-table").bootstrapTable("refresh");
}
$('#new-price-break').click(function() {
launchModalForm("{% url 'sale-price-break-create' %}",
{
success: reloadPriceBreaks,
data: {
part: {{ part.id }},
}
}
);
});
$('#price-break-table').inventreeTable({
name: 'saleprice',
formatNoMatches: function() { return "{% trans 'No price break information found' %}"; },
queryParams: {
part: {{ part.id }},
},
url: "{% url 'api-part-sale-price-list' %}",
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 += `<div class='btn-group float-right' role='group'>`
html += makeIconButton('fa-edit icon-blue', 'button-price-break-edit', row.pk, '{% trans "Edit price break" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}');
html += `</div>`;
return html;
}
},
]
})
{% endblock %}

View File

@ -46,6 +46,9 @@
</li> </li>
{% endif %} {% endif %}
{% if part.salable %} {% if part.salable %}
<li {% if tab == 'sales-prices' %}class='active'{% endif %}>
<a href="{% url 'part-sale-prices' part.id %}">{% trans "Sale Price" %}</a>
</li>
<li{% ifequal tab 'sales-orders' %} class='active'{% endifequal %}> <li{% ifequal tab 'sales-orders' %} class='active'{% endifequal %}>
<a href="{% url 'part-sales-orders' part.id %}">{% trans "Sales Orders" %} <span class='badge'>{{ part.sales_orders|length }}</span></a> <a href="{% url 'part-sales-orders' part.id %}">{% trans "Sales Orders" %} <span class='badge'>{{ part.sales_orders|length }}</span></a>
</li> </li>

View File

@ -18,6 +18,12 @@ part_attachment_urls = [
url(r'^(?P<pk>\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'), url(r'^(?P<pk>\d+)/delete/?', views.PartAttachmentDelete.as_view(), name='part-attachment-delete'),
] ]
sale_price_break_urls = [
url(r'^new/', views.PartSalePriceBreakCreate.as_view(), name='sale-price-break-create'),
url(r'^(?P<pk>\d+)/edit/', views.PartSalePriceBreakEdit.as_view(), name='sale-price-break-edit'),
url(r'^(?P<pk>\d+)/delete/', views.PartSalePriceBreakDelete.as_view(), name='sale-price-break-delete'),
]
part_parameter_urls = [ part_parameter_urls = [
url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), 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'^new/', views.PartParameterCreate.as_view(), name='part-param-create'),
url(r'^(?P<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'), url(r'^(?P<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'),
url(r'^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'), url(r'^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'),
] ]
part_detail_urls = [ 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'^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'^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'^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'^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'^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'), url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'),
@ -108,6 +114,9 @@ part_urls = [
# Part attachments # Part attachments
url(r'^attachment/', include(part_attachment_urls)), url(r'^attachment/', include(part_attachment_urls)),
# Part price breaks
url(r'^sale-price/', include(sale_price_break_urls)),
# Part test templates # Part test templates
url(r'^test-template/', include([ url(r'^test-template/', include([
url(r'^new/', views.PartTestTemplateCreate.as_view(), name='part-test-template-create'), url(r'^new/', views.PartTestTemplateCreate.as_view(), name='part-test-template-create'),

View File

@ -26,6 +26,7 @@ from .models import PartParameterTemplate, PartParameter
from .models import BomItem from .models import BomItem
from .models import match_part_names from .models import match_part_names
from .models import PartTestTemplate from .models import PartTestTemplate
from .models import PartSellPriceBreak
from common.models import Currency, InvenTreeSetting from common.models import Currency, InvenTreeSetting
from company.models import SupplierPart from company.models import SupplierPart
@ -2097,3 +2098,75 @@ class BomItemDelete(AjaxDeleteView):
ajax_template_name = 'part/bom-delete.html' ajax_template_name = 'part/bom-delete.html'
context_object_name = 'item' context_object_name = 'item'
ajax_form_title = _('Confim BOM item deletion') 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"

View File

@ -440,6 +440,8 @@ class StockList(generics.ListCreateAPIView):
params = self.request.query_params params = self.request.query_params
queryset = super().filter_queryset(queryset)
# Perform basic filtering: # Perform basic filtering:
# Note: We do not let DRF filter here, it be slow AF # Note: We do not let DRF filter here, it be slow AF
@ -680,6 +682,14 @@ class StockList(generics.ListCreateAPIView):
filter_fields = [ filter_fields = [
] ]
search_fields = [
'serial',
'batch',
'part__name',
'part__IPN',
'part__description'
]
class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin): class StockAttachmentList(generics.ListCreateAPIView, AttachmentMixin):
""" """

View File

@ -0,0 +1,15 @@
{% extends "collapse_index.html" %}
{% load i18n %}
{% block collapse_title %}
<span class='fas fa-times-circle icon-header'></span>
{% trans "BOM Waiting Validation" %}<span class='badge' id='bom-invalid-count'><span class='fas fa-spin fa-hourglass-half'></span></span>
{% endblock %}
{% block collapse_content %}
<table class='table table-striped table-condensed' id='bom-invalid-table'>
</table>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "collapse_index.html" %}
{% load i18n %}
{% block collapse_title %}
<span class='fas fa-cogs icon-header'></span>
{% trans "Pending Builds" %}<span class='badge' id='build-pending-count'><span class='fas fa-spin fa-hourglass-half'></span></span>
{% endblock %}
{% block collapse_content %}
<table class='table table-striped table-condensed' id='build-pending-table'>
</table>
{% endblock %}

View File

@ -7,9 +7,43 @@ InvenTree | Index
{% block content %} {% block content %}
<h3>InvenTree</h3> <h3>InvenTree</h3>
<hr> <hr>
{% include "InvenTree/starred_parts.html" with collapse_id="starred" %}
{% include "InvenTree/low_stock.html" with collapse_id="order" %} <div class="row">
<div class="col-sm-6">
{% include "InvenTree/latest_parts.html" with collapse_id="latest_parts" %}
</div>
<div class="col-sm-6">
{% include "InvenTree/starred_parts.html" with collapse_id="starred" %}
</div>
</div>
<div class="row">
<div class="col-sm-6">
{% include "InvenTree/bom_invalid.html" with collapse_id="bom_invalid" %}
</div>
<div class="col-sm-6">
{% include "InvenTree/build_pending.html" with collapse_id="build_pending" %}
</div>
</div>
<div class="row">
<div class="col-sm-6">
{% include "InvenTree/low_stock.html" with collapse_id="order" %}
</div>
<div class="col-sm-6">
{% include "InvenTree/required_stock_build.html" with collapse_id="stock_to_build" %}
</div>
</div>
<div class="row">
<div class="col-sm-6">
{% include "InvenTree/po_outstanding.html" with collapse_id="po_outstanding" %}
</div>
<div class="col-sm-6">
{% include "InvenTree/so_outstanding.html" with collapse_id="so_outstanding" %}
</div>
</div>
{% endblock %} {% endblock %}
@ -21,16 +55,71 @@ InvenTree | Index
{{ block.super }} {{ 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: { params: {
"starred": true, "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: { 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() { $("#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); $("#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() { $("#low-stock-table").on('load-success.bs.table', function() {
var count = $("#low-stock-table").bootstrapTable('getData').length; var count = $("#low-stock-table").bootstrapTable('getData').length;
$("#low-stock-count").html(count); $("#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 %} {% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "collapse_index.html" %}
{% load i18n %}
{% block collapse_title %}
<span class='fas fa-newspaper icon-header'></span>
{% trans "Latest Parts" %}<span class='badge' id='latest-parts-count'><span class='fas fa-spin fa-hourglass-half'></span></span>
{% endblock %}
{% block collapse_content %}
<table class='table table-striped table-condensed' id='latest-parts-table'>
</table>
{% endblock %}

View File

@ -1,10 +1,10 @@
{% extends "collapse.html" %} {% extends "collapse_index.html" %}
{% load i18n %} {% load i18n %}
{% block collapse_title %} {% block collapse_title %}
<span class='fas fa-shopping-cart icon-header'></span> <span class='fas fa-shopping-cart icon-header'></span>
{% trans "Low Stock" %}<span class='badge' id='low-stock-count'>0</span> {% trans "Low Stock" %}<span class='badge' id='low-stock-count'><span class='fas fa-spin fa-hourglass-half'></span></span>
{% endblock %} {% endblock %}
{% block collapse_content %} {% block collapse_content %}

View File

@ -1,15 +0,0 @@
{% extends "collapse.html" %}
{% block collapse_title %}
<span class='fas fa-tools icon-header'></span>
Parts to Build<span class='badge'>{{ to_build | length }}</span>
{% 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 %}

View File

@ -0,0 +1,15 @@
{% extends "collapse_index.html" %}
{% load i18n %}
{% block collapse_title %}
<span class='fas fa-sign-in-alt icon-header'></span>
{% trans "Outstanding Purchase Orders" %}<span class='badge' id='po-outstanding-count'><span class='fas fa-spin fa-hourglass-half'></span></span>
{% endblock %}
{% block collapse_content %}
<table class='table table-striped table-condensed' id='po-outstanding-table'>
</table>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "collapse_index.html" %}
{% load i18n %}
{% block collapse_title %}
<span class='fas fa-bullhorn icon-header'></span>
{% trans "Require Stock To Complete Build" %}<span class='badge' id='stock-to-build-count'><span class='fas fa-spin fa-hourglass-half'></span></span>
{% endblock %}
{% block collapse_content %}
<table class='table table-striped table-condensed' id='stock-to-build-table'>
</table>
{% endblock %}

View File

@ -32,6 +32,8 @@ InvenTree | {% trans "Search Results" %}
{% include "InvenTree/search_stock_location.html" with collapse_id="locations" %} {% include "InvenTree/search_stock_location.html" with collapse_id="locations" %}
{% include "InvenTree/search_stock_items.html" with collapse_id="stock" %}
{% endblock %} {% endblock %}
{% block js_load %} {% block js_load %}
@ -42,9 +44,8 @@ InvenTree | {% trans "Search Results" %}
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
$(".panel-group").hide();
function onSearchResults(table, output) { function onSearchResults(table, output) {
$(table).on('load-success.bs.table', function() { $(table).on('load-success.bs.table', function() {
var panel = $(output).closest('.panel-group'); var panel = $(output).closest('.panel-group');
@ -56,7 +57,6 @@ InvenTree | {% trans "Search Results" %}
text = '<i>No results</i>' text = '<i>No results</i>'
$(panel).hide(); $(panel).hide();
} else { } else {
text = n + ' result'; text = n + ' result';
@ -67,17 +67,23 @@ InvenTree | {% trans "Search Results" %}
$(panel).show(); $(panel).show();
var collapse = panel.find('.panel-collapse');
collapse.collapse('show');
$("#no-search-results").hide(); $("#no-search-results").hide();
} }
$(output).html(text); $(output).html(`<i>${text}</i>`);
}); });
} }
onSearchResults("#category-results-table", "#category-results-count"); onSearchResults("#category-results-table", "#category-results-count");
onSearchResults("#location-results-table", "#location-results-count"); onSearchResults("#location-results-table", "#location-results-count");
onSearchResults("#stock-results-table", "#stock-results-count");
onSearchResults('#part-results-table', '#part-result-count'); onSearchResults('#part-results-table', '#part-result-count');
onSearchResults('#company-results-table', '#company-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 '<i>{% trans "No stock location set" %}</i>';
}
}
}
},
]
});
$("#location-results-table").inventreeTable({ $("#location-results-table").inventreeTable({
url: "{% url 'api-location-list' %}", url: "{% url 'api-location-list' %}",

View File

@ -0,0 +1,16 @@
{% extends "collapse.html" %}
{% load i18n %}
{% block collapse_title %}
<h4>{% trans "Stock Items" %}</h4>
{% endblock %}
{% block collapse_heading %}
<h4><span id='stock-results-count'>{% include "InvenTree/searching.html" %}</span></h4>
{% endblock %}
{% block collapse_content %}
<table class='table table-striped table-condensed' data-toolbar="#button-toolbar" id='stock-results-table'>
</table>
{% endblock %}

View File

@ -1 +1,3 @@
<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> Searching {% load i18n %}
<span class='fas fa-spin fa-hourglass-half'></span> <i>{% trans "Searching" %}</i>

View File

@ -0,0 +1,15 @@
{% extends "collapse_index.html" %}
{% load i18n %}
{% block collapse_title %}
<span class='fas fa-sign-out-alt icon-header'></span>
{% trans "Outstanding Sales Orders" %}<span class='badge' id='so-outstanding-count'><span class='fas fa-spin fa-hourglass-half'></span></span>
{% endblock %}
{% block collapse_content %}
<table class='table table-striped table-condensed' id='so-outstanding-table'>
</table>
{% endblock %}

View File

@ -1,15 +1,15 @@
{% extends "collapse.html" %} {% extends "collapse_index.html" %}
{% load i18n %} {% load i18n %}
{% block collapse_title %} {% block collapse_title %}
<span class='fas fa-star icon-header'></span> <span class='fas fa-star icon-header'></span>
{% trans "Starred Parts" %}<span class='badge' id='starred-parts-count'>0</span> {% trans "Starred Parts" %}<span class='badge' id='starred-parts-count'><span class='fas fa-spin fa-hourglass-half'></span></span>
{% endblock %} {% endblock %}
{% block collapse_content %} {% block collapse_content %}
<table class='table tabe-striped table-condensed' id='starred-parts-table'> <table class='table table-striped table-condensed' id='starred-parts-table'>
</table> </table>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,19 @@
{% block collapse_preamble %}
{% endblock %}
<div class='panel-group'>
<div class='panel panel-default'>
<div {% block collapse_panel_setup %}class='panel panel-heading'{% endblock %}>
<div class='panel-title'>
<a data-toggle='collapse' href="#collapse-item-{{ collapse_id }}">{% block collapse_title %}Title{% endblock %}</a>
</div>
{% block collapse_heading %}
{% endblock %}
</div>
<div class='panel-collapse collapse' id='collapse-item-{{ collapse_id }}'>
<div class='panel-body'>
{% block collapse_content %}
{% endblock %}
</div>
</div>
</div>
</div>

View File

@ -5,7 +5,11 @@ function loadBuildTable(table, options) {
var params = options.params || {}; var params = options.params || {};
var filters = loadTableFilters("build"); var filters = {};
if (!options.disableFilters) {
loadTableFilters("build");
}
for (var key in params) { for (var key in params) {
filters[key] = params[key]; filters[key] = params[key];
@ -13,7 +17,7 @@ function loadBuildTable(table, options) {
setupFilterList("build", table); setupFilterList("build", table);
table.inventreeTable({ $(table).inventreeTable({
method: 'get', method: 'get',
formatNoMatches: function() { formatNoMatches: function() {
return "{% trans "No builds matching query" %}"; return "{% trans "No builds matching query" %}";

View File

@ -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={}) { function loadPartTable(table, url, options={}) {
/* Load part listing data into specified table. /* Load part listing data into specified table.
* *
@ -332,7 +340,7 @@ function loadPartTable(table, url, options={}) {
method: 'get', method: 'get',
queryParams: filters, queryParams: filters,
groupBy: false, groupBy: false,
name: 'part', name: options.name || 'part',
original: params, original: params,
formatNoMatches: function() { return "{% trans "No parts found" %}"; }, formatNoMatches: function() { return "{% trans "No parts found" %}"; },
columns: columns, columns: columns,

View File

@ -26,6 +26,10 @@ function getAvailableTableFilters(tableKey) {
title: "{% trans "Serial number LTE" %}", title: "{% trans "Serial number LTE" %}",
description: "{% trans "Serial number less than or equal to" %}", 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', type: 'bool',
title: '{% trans "Is Serialized" %}', title: '{% trans "Is Serialized" %}',
}, },
serial: {
title: "{% trans "Serial number" %}",
description: "{% trans "Serial number" %}"
},
serial_gte: { serial_gte: {
title: "{% trans "Serial number GTE" %}", title: "{% trans "Serial number GTE" %}",
description: "{% trans "Serial number greater than or equal to" %}" description: "{% trans "Serial number greater than or equal to" %}"
@ -109,7 +117,10 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Build status" %}', title: '{% trans "Build status" %}',
options: buildCodes, options: buildCodes,
}, },
pending: {
type: 'bool',
title: '{% trans "Pending" %}',
}
}; };
} }

View File

@ -36,7 +36,7 @@
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
{% include "search_form.html" %} {% include "search_form.html" %}
<li class ='navbar-barcode-li nav navbar-nav'> <li id='navbar-barcode-li'>
<button id='barcode-scan' class='btn btn-default' title='{% trans "Scan Barcode" %}'> <button id='barcode-scan' class='btn btn-default' title='{% trans "Scan Barcode" %}'>
<span class='fas fa-qrcode'></span> <span class='fas fa-qrcode'></span>
</button> </button>

View File

@ -108,6 +108,14 @@ def superuser(c):
manage(c, 'createsuperuser', pty=True) manage(c, 'createsuperuser', pty=True)
@task
def check(c):
"""
Check validity of django codebase
"""
manage(c, "check")
@task @task
def migrate(c): def migrate(c):
""" """