mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
c2eea91f76
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -82,9 +82,10 @@
|
||||
float: left;
|
||||
}
|
||||
|
||||
.navbar-barcode-li {
|
||||
#navbar-barcode-li {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.navbar-nav > li {
|
||||
|
@ -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)
|
||||
|
@ -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():
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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')
|
||||
)
|
||||
|
@ -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;
|
||||
|
@ -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 """
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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',
|
||||
]
|
||||
|
@ -10,45 +10,12 @@
|
||||
<hr>
|
||||
|
||||
<h4>{% trans "Pricing Information" %}</h4>
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr><td>{% trans "Order Multiple" %}</td><td>{{ part.multiple }}</td></tr>
|
||||
{% if part.base_cost > 0 %}
|
||||
<tr><td>{% trans "Base Price (Flat Fee)" %}</td><td>{{ part.base_cost }}</td></tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<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 %}
|
||||
|
||||
<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 %}
|
||||
@ -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 += `<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() {
|
||||
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 %}
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
@ -756,6 +832,11 @@ part_api_urls = [
|
||||
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([
|
||||
url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'),
|
||||
|
@ -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',
|
||||
]
|
||||
|
30
InvenTree/part/migrations/0049_partsellpricebreak.py
Normal file
30
InvenTree/part/migrations/0049_partsellpricebreak.py
Normal 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')},
|
||||
},
|
||||
),
|
||||
]
|
19
InvenTree/part/migrations/0050_auto_20200917_2315.py
Normal file
19
InvenTree/part/migrations/0050_auto_20200917_2315.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
110
InvenTree/part/templates/part/sale_prices.html
Normal file
110
InvenTree/part/templates/part/sale_prices.html
Normal 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 %}
|
@ -46,6 +46,9 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
{% 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 %}>
|
||||
<a href="{% url 'part-sales-orders' part.id %}">{% trans "Sales Orders" %} <span class='badge'>{{ part.sales_orders|length }}</span></a>
|
||||
</li>
|
||||
|
@ -18,6 +18,12 @@ part_attachment_urls = [
|
||||
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 = [
|
||||
|
||||
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<pk>\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'),
|
||||
url(r'^(?P<pk>\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'),
|
||||
|
||||
]
|
||||
|
||||
part_detail_urls = [
|
||||
@ -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'),
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
"""
|
||||
|
15
InvenTree/templates/InvenTree/bom_invalid.html
Normal file
15
InvenTree/templates/InvenTree/bom_invalid.html
Normal 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 %}
|
15
InvenTree/templates/InvenTree/build_pending.html
Normal file
15
InvenTree/templates/InvenTree/build_pending.html
Normal 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 %}
|
@ -7,9 +7,43 @@ InvenTree | Index
|
||||
{% block content %}
|
||||
<h3>InvenTree</h3>
|
||||
<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 %}
|
||||
|
||||
@ -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 %}
|
15
InvenTree/templates/InvenTree/latest_parts.html
Normal file
15
InvenTree/templates/InvenTree/latest_parts.html
Normal 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 %}
|
@ -1,10 +1,10 @@
|
||||
{% extends "collapse.html" %}
|
||||
{% extends "collapse_index.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block collapse_title %}
|
||||
<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 %}
|
||||
|
||||
{% block collapse_content %}
|
||||
|
@ -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 %}
|
15
InvenTree/templates/InvenTree/po_outstanding.html
Normal file
15
InvenTree/templates/InvenTree/po_outstanding.html
Normal 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 %}
|
15
InvenTree/templates/InvenTree/required_stock_build.html
Normal file
15
InvenTree/templates/InvenTree/required_stock_build.html
Normal 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 %}
|
@ -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');
|
||||
@ -57,7 +58,6 @@ InvenTree | {% trans "Search Results" %}
|
||||
|
||||
$(panel).hide();
|
||||
|
||||
|
||||
} else {
|
||||
text = n + ' result';
|
||||
|
||||
@ -67,10 +67,14 @@ InvenTree | {% trans "Search Results" %}
|
||||
|
||||
$(panel).show();
|
||||
|
||||
var collapse = panel.find('.panel-collapse');
|
||||
|
||||
collapse.collapse('show');
|
||||
|
||||
$("#no-search-results").hide();
|
||||
}
|
||||
|
||||
$(output).html(text);
|
||||
$(output).html(`<i>${text}</i>`);
|
||||
});
|
||||
}
|
||||
|
||||
@ -78,6 +82,8 @@ InvenTree | {% trans "Search Results" %}
|
||||
|
||||
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 '<i>{% trans "No stock location set" %}</i>';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
$("#location-results-table").inventreeTable({
|
||||
url: "{% url 'api-location-list' %}",
|
||||
|
16
InvenTree/templates/InvenTree/search_stock_items.html
Normal file
16
InvenTree/templates/InvenTree/search_stock_items.html
Normal 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 %}
|
@ -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>
|
||||
|
15
InvenTree/templates/InvenTree/so_outstanding.html
Normal file
15
InvenTree/templates/InvenTree/so_outstanding.html
Normal 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 %}
|
@ -1,15 +1,15 @@
|
||||
{% extends "collapse.html" %}
|
||||
{% extends "collapse_index.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block collapse_title %}
|
||||
<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 %}
|
||||
|
||||
{% 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>
|
||||
|
||||
{% endblock %}
|
19
InvenTree/templates/collapse_index.html
Normal file
19
InvenTree/templates/collapse_index.html
Normal 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>
|
@ -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" %}";
|
||||
|
@ -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,
|
||||
|
@ -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" %}',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{% 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" %}'>
|
||||
<span class='fas fa-qrcode'></span>
|
||||
</button>
|
||||
|
Loading…
Reference in New Issue
Block a user