mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-20 22:06:28 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into price-history
This commit is contained in:
@ -60,28 +60,43 @@ class CategoryList(generics.ListCreateAPIView):
|
||||
queryset = PartCategory.objects.all()
|
||||
serializer_class = part_serializers.CategorySerializer
|
||||
|
||||
def get_queryset(self):
|
||||
def filter_queryset(self, queryset):
|
||||
"""
|
||||
Custom filtering:
|
||||
- Allow filtering by "null" parent to retrieve top-level part categories
|
||||
"""
|
||||
|
||||
cat_id = self.request.query_params.get('parent', None)
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
queryset = super().get_queryset()
|
||||
params = self.request.query_params
|
||||
|
||||
if cat_id is not None:
|
||||
cat_id = params.get('parent', None)
|
||||
|
||||
cascade = str2bool(params.get('cascade', False))
|
||||
|
||||
# Do not filter by category
|
||||
if cat_id is None:
|
||||
pass
|
||||
# Look for top-level categories
|
||||
elif isNull(cat_id):
|
||||
|
||||
# Look for top-level categories
|
||||
if isNull(cat_id):
|
||||
if not cascade:
|
||||
queryset = queryset.filter(parent=None)
|
||||
|
||||
else:
|
||||
try:
|
||||
cat_id = int(cat_id)
|
||||
queryset = queryset.filter(parent=cat_id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
else:
|
||||
try:
|
||||
category = PartCategory.objects.get(pk=cat_id)
|
||||
|
||||
if cascade:
|
||||
parents = category.get_descendants(include_self=True)
|
||||
parent_ids = [p.id for p in parents]
|
||||
|
||||
queryset = queryset.filter(parent__in=parent_ids)
|
||||
else:
|
||||
queryset = queryset.filter(parent=category)
|
||||
|
||||
except (ValueError, PartCategory.DoesNotExist):
|
||||
pass
|
||||
|
||||
return queryset
|
||||
|
||||
|
@ -1163,7 +1163,16 @@ class Part(MPTTModel):
|
||||
Return the total amount of this part allocated to build orders
|
||||
"""
|
||||
|
||||
query = self.build_order_allocations().aggregate(total=Coalesce(Sum('quantity'), 0))
|
||||
query = self.build_order_allocations().aggregate(
|
||||
total=Coalesce(
|
||||
Sum(
|
||||
'quantity',
|
||||
output_field=models.DecimalField()
|
||||
),
|
||||
0,
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
)
|
||||
|
||||
return query['total']
|
||||
|
||||
@ -1179,7 +1188,16 @@ class Part(MPTTModel):
|
||||
Return the tutal quantity of this part allocated to sales orders
|
||||
"""
|
||||
|
||||
query = self.sales_order_allocations().aggregate(total=Coalesce(Sum('quantity'), 0))
|
||||
query = self.sales_order_allocations().aggregate(
|
||||
total=Coalesce(
|
||||
Sum(
|
||||
'quantity',
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
0,
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
)
|
||||
|
||||
return query['total']
|
||||
|
||||
@ -1189,10 +1207,12 @@ class Part(MPTTModel):
|
||||
against both build orders and sales orders.
|
||||
"""
|
||||
|
||||
return sum([
|
||||
self.build_order_allocation_count(),
|
||||
self.sales_order_allocation_count(),
|
||||
])
|
||||
return sum(
|
||||
[
|
||||
self.build_order_allocation_count(),
|
||||
self.sales_order_allocation_count(),
|
||||
],
|
||||
)
|
||||
|
||||
def stock_entries(self, include_variants=True, in_stock=None):
|
||||
""" Return all stock entries for this Part.
|
||||
|
@ -4,6 +4,7 @@ JSON serializers for Part app
|
||||
import imghdr
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
|
||||
@ -208,7 +209,8 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
queryset = queryset.annotate(
|
||||
in_stock=Coalesce(
|
||||
SubquerySum('stock_items__quantity', filter=StockItem.IN_STOCK_FILTER),
|
||||
Decimal(0)
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
)
|
||||
|
||||
@ -227,6 +229,7 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
building=Coalesce(
|
||||
SubquerySum('builds__quantity', filter=build_filter),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
)
|
||||
|
||||
@ -240,9 +243,11 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
ordering=Coalesce(
|
||||
SubquerySum('supplier_parts__purchase_order_line_items__quantity', filter=order_filter),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
) - Coalesce(
|
||||
SubquerySum('supplier_parts__purchase_order_line_items__received', filter=order_filter),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
)
|
||||
)
|
||||
|
||||
@ -251,6 +256,7 @@ class PartSerializer(InvenTreeModelSerializer):
|
||||
suppliers=Coalesce(
|
||||
SubqueryCount('supplier_parts'),
|
||||
Decimal(0),
|
||||
output_field=models.DecimalField(),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -2,6 +2,10 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/category_navbar.html' with tab='parts' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
@ -100,14 +104,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if category and category.children.all|length > 0 %}
|
||||
{% include "part/subcategories.html" with children=category.children.all collapse_id="categories" %}
|
||||
{% elif children|length > 0 %}
|
||||
{% include "part/subcategories.html" with children=children collapse_id="categories" %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% block category_content %}
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div class='btn-group'>
|
||||
<button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>
|
||||
@ -150,6 +150,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block category_tables %}
|
||||
{% endblock category_tables %}
|
||||
@ -162,24 +164,10 @@
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
{% if category %}
|
||||
enableNavbar({
|
||||
label: 'category',
|
||||
toggleId: '#category-menu-toggle',
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
if (inventreeLoadInt("show-part-cats") == 1) {
|
||||
$("#collapse-item-categories").collapse('show');
|
||||
}
|
||||
|
||||
$("#collapse-item-categories").on('shown.bs.collapse', function() {
|
||||
inventreeSave('show-part-cats', 1);
|
||||
});
|
||||
|
||||
$("#collapse-item-categories").on('hidden.bs.collapse', function() {
|
||||
inventreeDel('show-part-cats');
|
||||
});
|
||||
|
||||
$("#cat-create").click(function() {
|
||||
launchModalForm(
|
||||
|
@ -8,17 +8,34 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "subcategories" %}active{% endif %}' title='{% trans "Subcategories" %}'>
|
||||
{% if category %}
|
||||
<a href='{% url "category-subcategory" category.id %}'>
|
||||
{% else %}
|
||||
<a href='{% url "category-index-subcategory" %}'>
|
||||
{% endif %}
|
||||
<span class='fas fa-sitemap'></span>
|
||||
{% trans "Subcategories" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class='list-group-item {% if tab == "parts" %}active{% endif %}' title='{% trans "Parts" %}'>
|
||||
{% if category %}
|
||||
<a href='{% url "category-detail" category.id %}'>
|
||||
{% else %}
|
||||
<a href='{% url "part-index" %}'>
|
||||
{% endif %}
|
||||
<span class='fas fa-shapes'></span>
|
||||
{% trans "Parts" %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if category %}
|
||||
<li class='list-group-item {% if tab == "parameters" %}active{% endif %}' title='{% trans "Parameters" %}'>
|
||||
<a href='{% url "category-parametric" category.id %}'>
|
||||
<span class='fas fa-tasks'></span>
|
||||
{% trans "Parameters" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
@ -1,14 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
{% if part %}
|
||||
InvenTree | {% trans "Part" %} - {{ part.full_name }}
|
||||
{% inventree_title %} | {% trans "Part" %} - {{ part.full_name }}
|
||||
{% elif category %}
|
||||
InvenTree | {% trans "Part Category" %} - {{ category }}
|
||||
{% inventree_title %} | {% trans "Part Category" %} - {{ category }}
|
||||
{% else %}
|
||||
InvenTree | {% trans "Part List" %}
|
||||
{% inventree_title %} | {% trans "Part List" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
{% extends "collapse.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block collapse_title %}
|
||||
{{ children | length }} {% trans 'Child Categories' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block collapse_content %}
|
||||
<ul class="list-group">
|
||||
{% for child in children %}
|
||||
<li class="list-group-item">
|
||||
<strong><a href="{% url 'category-detail' child.id %}">{{ child.name }}</a></strong>
|
||||
{% if child.description %}
|
||||
<em> - {{ child.description }}</em>
|
||||
{% endif %}
|
||||
{% if child.partcount > 0 %}
|
||||
<span class='badge'>{{ child.partcount }} {% trans 'Part' %}{% if child.partcount > 1 %}s{% endif %}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
51
InvenTree/part/templates/part/subcategory.html
Normal file
51
InvenTree/part/templates/part/subcategory.html
Normal file
@ -0,0 +1,51 @@
|
||||
{% extends "part/category.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
{% load static %}
|
||||
|
||||
{% block menubar %}
|
||||
{% include 'part/category_navbar.html' with tab='subcategories' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block category_content %}
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
|
||||
<div class='panel-heading'>
|
||||
<h4>{% trans "Subcategories" %}</h4>
|
||||
</div>
|
||||
|
||||
<div id='button-toolbar'>
|
||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||
|
||||
<div class='filter-list' id='filter-list-category'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class='table table-striped table-condensed' id='subcategory-table' data-toolbar='#button-toolbar'></table>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
enableNavbar({
|
||||
label: 'category',
|
||||
toggleId: '#category-menu-toggle',
|
||||
});
|
||||
|
||||
loadPartCategoryTable($('#subcategory-table'), {
|
||||
params: {
|
||||
{% if category %}
|
||||
parent: {{ category.pk }}
|
||||
{% else %}
|
||||
parent: 'null'
|
||||
{% endif %}
|
||||
}
|
||||
});
|
||||
|
||||
{% endblock %}
|
@ -6,6 +6,7 @@ import os
|
||||
from django import template
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.templatetags.static import StaticNode
|
||||
from InvenTree import version, settings
|
||||
|
||||
import InvenTree.helpers
|
||||
@ -73,6 +74,12 @@ def inventree_instance_name(*args, **kwargs):
|
||||
return version.inventreeInstanceName()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_title(*args, **kwargs):
|
||||
""" Return the title for the current instance - respecting the settings """
|
||||
return version.inventreeInstanceTitle()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_version(*args, **kwargs):
|
||||
""" Return InvenTree version string """
|
||||
@ -174,3 +181,31 @@ def object_link(url_name, pk, ref):
|
||||
|
||||
ref_url = reverse(url_name, kwargs={'pk': pk})
|
||||
return mark_safe('<b><a href="{}">{}</a></b>'.format(ref_url, ref))
|
||||
|
||||
|
||||
class I18nStaticNode(StaticNode):
|
||||
"""
|
||||
custom StaticNode
|
||||
replaces a variable named *lng* in the path with the current language
|
||||
"""
|
||||
def render(self, context):
|
||||
self.path.var = self.path.var.format(lng=context.request.LANGUAGE_CODE)
|
||||
ret = super().render(context)
|
||||
return ret
|
||||
|
||||
|
||||
@register.tag('i18n_static')
|
||||
def do_i18n_static(parser, token):
|
||||
"""
|
||||
Overrides normal static, adds language - lookup for prerenderd files #1485
|
||||
|
||||
usage (like static):
|
||||
{% i18n_static path [as varname] %}
|
||||
"""
|
||||
bits = token.split_contents()
|
||||
loc_name = settings.STATICFILES_I18_PREFIX
|
||||
|
||||
# change path to called ressource
|
||||
bits[1] = f"'{loc_name}/{{lng}}.{bits[1][1:-1]}'"
|
||||
token.contents = ' '.join(bits)
|
||||
return I18nStaticNode.handle_token(parser, token)
|
||||
|
@ -37,12 +37,54 @@ class PartAPITest(InvenTreeAPITestCase):
|
||||
super().setUp()
|
||||
|
||||
def test_get_categories(self):
|
||||
""" Test that we can retrieve list of part categories """
|
||||
"""
|
||||
Test that we can retrieve list of part categories,
|
||||
with various filtering options.
|
||||
"""
|
||||
|
||||
url = reverse('api-part-category-list')
|
||||
|
||||
# Request *all* part categories
|
||||
response = self.client.get(url, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 8)
|
||||
|
||||
# Request top-level part categories only
|
||||
response = self.client.get(
|
||||
url,
|
||||
{
|
||||
'parent': 'null',
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 2)
|
||||
|
||||
# Children of PartCategory<1>, cascade
|
||||
response = self.client.get(
|
||||
url,
|
||||
{
|
||||
'parent': 1,
|
||||
'cascade': 'true',
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 5)
|
||||
|
||||
# Children of PartCategory<1>, do not cascade
|
||||
response = self.client.get(
|
||||
url,
|
||||
{
|
||||
'parent': 1,
|
||||
'cascade': 'false',
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
def test_add_categories(self):
|
||||
""" Check that we can add categories """
|
||||
data = {
|
||||
|
@ -88,14 +88,26 @@ category_parameter_urls = [
|
||||
url(r'^(?P<pid>\d+)/delete/', views.CategoryParameterTemplateDelete.as_view(), name='category-param-template-delete'),
|
||||
]
|
||||
|
||||
part_category_urls = [
|
||||
url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'),
|
||||
url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'),
|
||||
category_urls = [
|
||||
|
||||
url(r'^parameters/', include(category_parameter_urls)),
|
||||
# Create a new category
|
||||
url(r'^new/', views.CategoryCreate.as_view(), name='category-create'),
|
||||
|
||||
url(r'^parametric/?', views.CategoryParametric.as_view(), name='category-parametric'),
|
||||
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
||||
# Top level subcategory display
|
||||
url(r'^subcategory/', views.PartIndex.as_view(template_name='part/subcategory.html'), name='category-index-subcategory'),
|
||||
|
||||
# Category detail views
|
||||
url(r'(?P<pk>\d+)/', include([
|
||||
url(r'^edit/', views.CategoryEdit.as_view(), name='category-edit'),
|
||||
url(r'^delete/', views.CategoryDelete.as_view(), name='category-delete'),
|
||||
url(r'^parameters/', include(category_parameter_urls)),
|
||||
|
||||
url(r'^subcategory/', views.CategoryDetail.as_view(template_name='part/subcategory.html'), name='category-subcategory'),
|
||||
url(r'^parametric/', views.CategoryParametric.as_view(), name='category-parametric'),
|
||||
|
||||
# Anything else
|
||||
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
||||
]))
|
||||
]
|
||||
|
||||
part_bom_urls = [
|
||||
@ -106,9 +118,6 @@ part_bom_urls = [
|
||||
# URL list for part web interface
|
||||
part_urls = [
|
||||
|
||||
# Create a new category
|
||||
url(r'^category/new/?', views.CategoryCreate.as_view(), name='category-create'),
|
||||
|
||||
# Create a new part
|
||||
url(r'^new/?', views.PartCreate.as_view(), name='part-create'),
|
||||
|
||||
@ -125,7 +134,7 @@ part_urls = [
|
||||
url(r'^(?P<pk>\d+)/', include(part_detail_urls)),
|
||||
|
||||
# Part category
|
||||
url(r'^category/(?P<pk>\d+)/', include(part_category_urls)),
|
||||
url(r'^category/', include(category_urls)),
|
||||
|
||||
# Part related
|
||||
url(r'^related-parts/', include(part_related_urls)),
|
||||
|
Reference in New Issue
Block a user