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
a9f0936cec
@ -357,6 +357,8 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
- Serial numbers must be positive
|
- Serial numbers must be positive
|
||||||
- Serial numbers can be split by whitespace / newline / commma chars
|
- Serial numbers can be split by whitespace / newline / commma chars
|
||||||
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
- Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20
|
||||||
|
- Serial numbers can be supplied as <start>+ for getting all expecteded numbers starting from <start>
|
||||||
|
- Serial numbers can be supplied as <start>+<length> for getting <length> numbers starting from <start>
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
expected_quantity: The number of (unique) serial numbers we expect
|
expected_quantity: The number of (unique) serial numbers we expect
|
||||||
@ -369,6 +371,13 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
numbers = []
|
numbers = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
|
# helpers
|
||||||
|
def number_add(n):
|
||||||
|
if n in numbers:
|
||||||
|
errors.append(_('Duplicate serial: {n}').format(n=n))
|
||||||
|
else:
|
||||||
|
numbers.append(n)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
expected_quantity = int(expected_quantity)
|
expected_quantity = int(expected_quantity)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -395,10 +404,7 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
|
|
||||||
if a < b:
|
if a < b:
|
||||||
for n in range(a, b + 1):
|
for n in range(a, b + 1):
|
||||||
if n in numbers:
|
number_add(n)
|
||||||
errors.append(_('Duplicate serial: {n}').format(n=n))
|
|
||||||
else:
|
|
||||||
numbers.append(n)
|
|
||||||
else:
|
else:
|
||||||
errors.append(_("Invalid group: {g}").format(g=group))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
|
|
||||||
@ -409,6 +415,31 @@ def extract_serial_numbers(serials, expected_quantity):
|
|||||||
errors.append(_("Invalid group: {g}").format(g=group))
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# plus signals either
|
||||||
|
# 1: 'start+': expected number of serials, starting at start
|
||||||
|
# 2: 'start+number': number of serials, starting at start
|
||||||
|
elif '+' in group:
|
||||||
|
items = group.split('+')
|
||||||
|
|
||||||
|
# case 1, 2
|
||||||
|
if len(items) == 2:
|
||||||
|
start = int(items[0])
|
||||||
|
|
||||||
|
# case 2
|
||||||
|
if bool(items[1]):
|
||||||
|
end = start + int(items[1]) + 1
|
||||||
|
|
||||||
|
# case 1
|
||||||
|
else:
|
||||||
|
end = start + expected_quantity
|
||||||
|
|
||||||
|
for n in range(start, end):
|
||||||
|
number_add(n)
|
||||||
|
# no case
|
||||||
|
else:
|
||||||
|
errors.append(_("Invalid group: {g}").format(g=group))
|
||||||
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if group in numbers:
|
if group in numbers:
|
||||||
errors.append(_("Duplicate serial: {g}".format(g=group)))
|
errors.append(_("Duplicate serial: {g}".format(g=group)))
|
||||||
|
@ -491,7 +491,7 @@ LANGUAGES = [
|
|||||||
('en', _('English')),
|
('en', _('English')),
|
||||||
('fr', _('French')),
|
('fr', _('French')),
|
||||||
('de', _('German')),
|
('de', _('German')),
|
||||||
('pk', _('Polish')),
|
('pl', _('Polish')),
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -244,6 +244,14 @@ class TestSerialNumberExtraction(TestCase):
|
|||||||
self.assertIn(3, sn)
|
self.assertIn(3, sn)
|
||||||
self.assertIn(13, sn)
|
self.assertIn(13, sn)
|
||||||
|
|
||||||
|
sn = e("1+", 10)
|
||||||
|
self.assertEqual(len(sn), 10)
|
||||||
|
self.assertEqual(sn, [_ for _ in range(1, 11)])
|
||||||
|
|
||||||
|
sn = e("4, 1+2", 4)
|
||||||
|
self.assertEqual(len(sn), 4)
|
||||||
|
self.assertEqual(sn, ["4", 1, 2, 3])
|
||||||
|
|
||||||
def test_failures(self):
|
def test_failures(self):
|
||||||
|
|
||||||
e = helpers.extract_serial_numbers
|
e = helpers.extract_serial_numbers
|
||||||
|
@ -39,7 +39,7 @@ from rest_framework.documentation import include_docs_urls
|
|||||||
|
|
||||||
from .views import IndexView, SearchView, DatabaseStatsView
|
from .views import IndexView, SearchView, DatabaseStatsView
|
||||||
from .views import SettingsView, EditUserView, SetPasswordView
|
from .views import SettingsView, EditUserView, SetPasswordView
|
||||||
from .views import ColorThemeSelectView, SettingCategorySelectView
|
from .views import AppearanceSelectView, SettingCategorySelectView
|
||||||
from .views import DynamicJsView
|
from .views import DynamicJsView
|
||||||
|
|
||||||
from common.views import SettingEdit
|
from common.views import SettingEdit
|
||||||
@ -79,7 +79,8 @@ apipatterns = [
|
|||||||
settings_urls = [
|
settings_urls = [
|
||||||
|
|
||||||
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
|
url(r'^user/?', SettingsView.as_view(template_name='InvenTree/settings/user.html'), name='settings-user'),
|
||||||
url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'),
|
url(r'^appearance/?', AppearanceSelectView.as_view(), name='settings-appearance'),
|
||||||
|
url(r'^i18n/?', include('django.conf.urls.i18n')),
|
||||||
|
|
||||||
url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'),
|
url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'),
|
||||||
url(r'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
|
url(r'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
|
||||||
|
@ -769,12 +769,12 @@ class SettingsView(TemplateView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class ColorThemeSelectView(FormView):
|
class AppearanceSelectView(FormView):
|
||||||
""" View for selecting a color theme """
|
""" View for selecting a color theme """
|
||||||
|
|
||||||
form_class = ColorThemeSelectForm
|
form_class = ColorThemeSelectForm
|
||||||
success_url = reverse_lazy('settings-theme')
|
success_url = reverse_lazy('settings-appearance')
|
||||||
template_name = "InvenTree/settings/theme.html"
|
template_name = "InvenTree/settings/appearance.html"
|
||||||
|
|
||||||
def get_user_theme(self):
|
def get_user_theme(self):
|
||||||
""" Get current user color theme """
|
""" Get current user color theme """
|
||||||
@ -788,7 +788,7 @@ class ColorThemeSelectView(FormView):
|
|||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
""" Select current user color theme as initial choice """
|
""" Select current user color theme as initial choice """
|
||||||
|
|
||||||
initial = super(ColorThemeSelectView, self).get_initial()
|
initial = super(AppearanceSelectView, self).get_initial()
|
||||||
|
|
||||||
user_theme = self.get_user_theme()
|
user_theme = self.get_user_theme()
|
||||||
if user_theme:
|
if user_theme:
|
||||||
|
@ -996,14 +996,28 @@ class Build(MPTTModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def required_parts(self):
|
def required_parts(self):
|
||||||
""" Returns a dict of parts required to build this part (BOM) """
|
""" Returns a list of parts required to build this part (BOM) """
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
for item in self.part.bom_items.all().prefetch_related('sub_part'):
|
for item in self.bom_items:
|
||||||
parts.append(item.sub_part)
|
parts.append(item.sub_part)
|
||||||
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_parts_to_complete_build(self):
|
||||||
|
""" Returns a list of parts required to complete the full build """
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
for bom_item in self.bom_items:
|
||||||
|
# Get remaining quantity needed
|
||||||
|
required_quantity_to_complete_build = self.remaining * bom_item.quantity
|
||||||
|
# Compare to net stock
|
||||||
|
if bom_item.sub_part.net_stock < required_quantity_to_complete_build:
|
||||||
|
parts.append(bom_item.sub_part)
|
||||||
|
|
||||||
|
return parts
|
||||||
|
|
||||||
def availableStockItems(self, part, output):
|
def availableStockItems(self, part, output):
|
||||||
"""
|
"""
|
||||||
Returns stock items which are available for allocation to this build.
|
Returns stock items which are available for allocation to this build.
|
||||||
|
@ -40,8 +40,8 @@
|
|||||||
<div class='panel-heading'>
|
<div class='panel-heading'>
|
||||||
{% trans "The following items will be created" %}
|
{% trans "The following items will be created" %}
|
||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content' style='padding-bottom:16px'>
|
||||||
{% include "hover_image.html" with image=build.part.image hover=True %}
|
{% include "hover_image.html" with image=build.part.image %}
|
||||||
{% if output.serialized %}
|
{% if output.serialized %}
|
||||||
{{ output.part.full_name }} - {% trans "Serial Number" %} {{ output.serial }}
|
{{ output.part.full_name }} - {% trans "Serial Number" %} {{ output.serial }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -157,6 +157,17 @@ class BuildOutputCreate(AjaxUpdateView):
|
|||||||
quantity = form.cleaned_data.get('output_quantity', None)
|
quantity = form.cleaned_data.get('output_quantity', None)
|
||||||
serials = form.cleaned_data.get('serial_numbers', None)
|
serials = form.cleaned_data.get('serial_numbers', None)
|
||||||
|
|
||||||
|
if quantity:
|
||||||
|
build = self.get_object()
|
||||||
|
|
||||||
|
# Check that requested output don't exceed build remaining quantity
|
||||||
|
maximum_output = int(build.remaining - build.incomplete_count)
|
||||||
|
if quantity > maximum_output:
|
||||||
|
form.add_error(
|
||||||
|
'output_quantity',
|
||||||
|
_('Maximum output quantity is ') + str(maximum_output),
|
||||||
|
)
|
||||||
|
|
||||||
# Check that the serial numbers are valid
|
# Check that the serial numbers are valid
|
||||||
if serials:
|
if serials:
|
||||||
try:
|
try:
|
||||||
@ -212,7 +223,7 @@ class BuildOutputCreate(AjaxUpdateView):
|
|||||||
|
|
||||||
# Calculate the required quantity
|
# Calculate the required quantity
|
||||||
quantity = max(0, build.remaining - build.incomplete_count)
|
quantity = max(0, build.remaining - build.incomplete_count)
|
||||||
initials['output_quantity'] = quantity
|
initials['output_quantity'] = int(quantity)
|
||||||
|
|
||||||
return initials
|
return initials
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
|
|||||||
params = self.request.query_params
|
params = self.request.query_params
|
||||||
|
|
||||||
# Filter by manufacturer
|
# Filter by manufacturer
|
||||||
manufacturer = params.get('company', None)
|
manufacturer = params.get('manufacturer', None)
|
||||||
|
|
||||||
if manufacturer is not None:
|
if manufacturer is not None:
|
||||||
queryset = queryset.filter(manufacturer=manufacturer)
|
queryset = queryset.filter(manufacturer=manufacturer)
|
||||||
|
@ -675,4 +675,4 @@ class SupplierPriceBreak(common.models.PriceBreak):
|
|||||||
db_table = 'part_supplierpricebreak'
|
db_table = 'part_supplierpricebreak'
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.part.MPN} - {self.price} @ {self.quantity}'
|
return f'{self.part.SKU} - {self.price} @ {self.quantity}'
|
||||||
|
@ -192,10 +192,11 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
|||||||
manufacturer_id = self.initial_data.get('manufacturer', None)
|
manufacturer_id = self.initial_data.get('manufacturer', None)
|
||||||
MPN = self.initial_data.get('MPN', None)
|
MPN = self.initial_data.get('MPN', None)
|
||||||
|
|
||||||
if manufacturer_id or MPN:
|
if manufacturer_id and MPN:
|
||||||
kwargs = {'manufacturer': manufacturer_id,
|
kwargs = {
|
||||||
'MPN': MPN,
|
'manufacturer': manufacturer_id,
|
||||||
}
|
'MPN': MPN,
|
||||||
|
}
|
||||||
supplier_part.save(**kwargs)
|
supplier_part.save(**kwargs)
|
||||||
|
|
||||||
return supplier_part
|
return supplier_part
|
||||||
|
@ -100,7 +100,7 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
|||||||
self.assertEqual(response.data['MPN'], 'MPN_TEST')
|
self.assertEqual(response.data['MPN'], 'MPN_TEST')
|
||||||
|
|
||||||
# Filter by manufacturer
|
# Filter by manufacturer
|
||||||
data = {'company': 7}
|
data = {'manufacturer': 7}
|
||||||
response = self.get(url, data)
|
response = self.get(url, data)
|
||||||
self.assertEqual(len(response.data), 3)
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
|
@ -337,14 +337,16 @@ class PurchaseOrder(Order):
|
|||||||
raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")})
|
raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not (quantity % 1 == 0):
|
||||||
|
raise ValidationError({"quantity": _("Quantity must be an integer")})
|
||||||
|
if quantity < 0:
|
||||||
|
raise ValidationError({"quantity": _("Quantity must be a positive number")})
|
||||||
quantity = int(quantity)
|
quantity = int(quantity)
|
||||||
if quantity <= 0:
|
except (ValueError, TypeError):
|
||||||
raise ValidationError({"quantity": _("Quantity must be greater than zero")})
|
|
||||||
except ValueError:
|
|
||||||
raise ValidationError({"quantity": _("Invalid quantity provided")})
|
raise ValidationError({"quantity": _("Invalid quantity provided")})
|
||||||
|
|
||||||
# Create a new stock item
|
# Create a new stock item
|
||||||
if line.part:
|
if line.part and quantity > 0:
|
||||||
stock = stock_models.StockItem(
|
stock = stock_models.StockItem(
|
||||||
part=line.part.part,
|
part=line.part.part,
|
||||||
supplier_part=line.part,
|
supplier_part=line.part,
|
||||||
|
@ -171,11 +171,35 @@ $("#edit-order").click(function() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#receive-order").click(function() {
|
||||||
|
launchModalForm("{% url 'po-receive' order.id %}", {
|
||||||
|
reload: true,
|
||||||
|
secondary: [
|
||||||
|
{
|
||||||
|
field: 'location',
|
||||||
|
label: '{% trans "New Location" %}',
|
||||||
|
title: '{% trans "Create new stock location" %}',
|
||||||
|
url: "{% url 'stock-location-create' %}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#complete-order").click(function() {
|
||||||
|
launchModalForm("{% url 'po-complete' order.id %}", {
|
||||||
|
reload: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$("#cancel-order").click(function() {
|
$("#cancel-order").click(function() {
|
||||||
launchModalForm("{% url 'po-cancel' order.id %}", {
|
launchModalForm("{% url 'po-cancel' order.id %}", {
|
||||||
reload: true,
|
reload: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#export-order").click(function() {
|
||||||
|
location.href = "{% url 'po-export' order.id %}";
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
{% trans "Cancelling this order means that the order will no longer be editable." %}
|
<div class='alert alert-danger alert-block'>
|
||||||
|
{% trans "Cancelling this order means that the order and line items will no longer be editable." %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
{% trans 'Mark this order as complete?' %}
|
{% trans 'Mark this order as complete?' %}
|
||||||
{% if not order.is_complete %}
|
{% if not order.is_complete %}
|
||||||
<div class='alert alert-warning alert-block'>
|
<div class='alert alert-warning alert-block' style='margin-top:12px'>
|
||||||
{% trans 'This order has line items which have not been marked as received.' %}
|
{% trans 'This order has line items which have not been marked as received.' %}</br>
|
||||||
{% trans 'Marking this order as complete will remove these line items.' %}
|
{% trans 'Completing this order means that the order and line items will no longer be editable.' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
{% block pre_form_content %}
|
{% block pre_form_content %}
|
||||||
|
|
||||||
{% trans 'After placing this purchase order, line items will no longer be editable.' %}
|
<div class='alert alert-warning alert-block'>
|
||||||
|
{% trans 'After placing this purchase order, line items will no longer be editable.' %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -35,31 +35,6 @@
|
|||||||
|
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
|
||||||
$("#receive-order").click(function() {
|
|
||||||
launchModalForm("{% url 'po-receive' order.id %}", {
|
|
||||||
reload: true,
|
|
||||||
secondary: [
|
|
||||||
{
|
|
||||||
field: 'location',
|
|
||||||
label: '{% trans "New Location" %}',
|
|
||||||
title: '{% trans "Create new stock location" %}',
|
|
||||||
url: "{% url 'stock-location-create' %}",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#complete-order").click(function() {
|
|
||||||
launchModalForm("{% url 'po-complete' order.id %}", {
|
|
||||||
reload: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#export-order").click(function() {
|
|
||||||
location.href = "{% url 'po-export' order.id %}";
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if order.status == PurchaseOrderStatus.PENDING %}
|
{% if order.status == PurchaseOrderStatus.PENDING %}
|
||||||
$('#new-po-line').click(function() {
|
$('#new-po-line').click(function() {
|
||||||
launchModalForm("{% url 'po-line-item-create' %}",
|
launchModalForm("{% url 'po-line-item-create' %}",
|
||||||
@ -261,5 +236,4 @@ $("#po-table").inventreeTable({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.db.models import Q, F, Count, Prefetch, Sum
|
from django.db.models import Q, F, Count
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -635,29 +635,15 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
# TODO: Need to figure out a cheaper way of making this filter query
|
# TODO: Need to figure out a cheaper way of making this filter query
|
||||||
|
|
||||||
if stock_to_build is not None:
|
if stock_to_build is not None:
|
||||||
# Filter only active parts
|
# Get active builds
|
||||||
queryset = queryset.filter(active=True)
|
builds = Build.objects.filter(status__in=BuildStatus.ACTIVE_CODES)
|
||||||
# Prefetch current active builds
|
|
||||||
build_active_queryset = Build.objects.filter(status__in=BuildStatus.ACTIVE_CODES)
|
|
||||||
build_active_prefetch = Prefetch('builds',
|
|
||||||
queryset=build_active_queryset,
|
|
||||||
to_attr='current_builds')
|
|
||||||
parts = queryset.prefetch_related(build_active_prefetch)
|
|
||||||
|
|
||||||
# Store parts with builds needing stock
|
# Store parts with builds needing stock
|
||||||
parts_need_stock = []
|
parts_needed_to_complete_builds = []
|
||||||
|
# Filter required parts
|
||||||
|
for build in builds:
|
||||||
|
parts_needed_to_complete_builds += [part.pk for part in build.required_parts_to_complete_build]
|
||||||
|
|
||||||
# Find parts with active builds
|
queryset = queryset.filter(pk__in=parts_needed_to_complete_builds)
|
||||||
# where any subpart's stock is lower than quantity being built
|
|
||||||
for part in parts:
|
|
||||||
if part.current_builds:
|
|
||||||
builds_ids = [build.id for build in part.current_builds]
|
|
||||||
total_build_quantity = build_active_queryset.filter(pk__in=builds_ids).aggregate(quantity=Sum('quantity'))['quantity']
|
|
||||||
|
|
||||||
if part.can_build < total_build_quantity:
|
|
||||||
parts_need_stock.append(part.pk)
|
|
||||||
|
|
||||||
queryset = queryset.filter(pk__in=parts_need_stock)
|
|
||||||
|
|
||||||
# Optionally limit the maximum number of returned results
|
# Optionally limit the maximum number of returned results
|
||||||
# e.g. for displaying "recent part" list
|
# e.g. for displaying "recent part" list
|
||||||
|
@ -116,6 +116,12 @@ def inventree_docs_url(*args, **kwargs):
|
|||||||
return "https://inventree.readthedocs.io/"
|
return "https://inventree.readthedocs.io/"
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def inventree_credits_url(*args, **kwargs):
|
||||||
|
""" Return URL for InvenTree credits site """
|
||||||
|
return "https://inventree.readthedocs.io/en/latest/credits/"
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def setting_object(key, *args, **kwargs):
|
def setting_object(key, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
67
InvenTree/templates/InvenTree/settings/appearance.html
Normal file
67
InvenTree/templates/InvenTree/settings/appearance.html
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{% extends "InvenTree/settings/settings.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
{% block tabs %}
|
||||||
|
{% include "InvenTree/settings/tabs.html" with tab='theme' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block subtitle %}
|
||||||
|
{% trans "Theme Settings" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block settings %}
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<h4>{% trans "Color Themes" %}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{% url 'settings-appearance' %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% crispy form %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if invalid_color_theme %}
|
||||||
|
<div class="alert alert-danger alert-block" role="alert" style="display: inline-block;">
|
||||||
|
{% blocktrans %}
|
||||||
|
The CSS sheet "{{invalid_color_theme}}.css" for the currently selected color theme was not found.<br>
|
||||||
|
Please select another color theme :)
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<h4>{% trans "Language" %}</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
|
||||||
|
<input name="next" type="hidden" value="{% url 'settings-appearance' %}">
|
||||||
|
<div class="col-sm-6" style="width: 200px;"><div id="div_id_name" class="form-group"><div class="controls ">
|
||||||
|
<select name="language" class="select form-control">
|
||||||
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
|
{% get_available_languages as LANGUAGES %}
|
||||||
|
{% get_language_info_list for LANGUAGES as languages %}
|
||||||
|
{% for language in languages %}
|
||||||
|
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
|
||||||
|
{{ language.name_local }} ({{ language.code }})
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div></div></div>
|
||||||
|
<div class="col-sm-6" style="width: auto;">
|
||||||
|
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -6,7 +6,7 @@
|
|||||||
<a href="{% url 'settings-user' %}"><span class='fas fa-user'></span> {% trans "Account" %}</a>
|
<a href="{% url 'settings-user' %}"><span class='fas fa-user'></span> {% trans "Account" %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li{% ifequal tab 'theme' %} class='active'{% endifequal %}>
|
<li{% ifequal tab 'theme' %} class='active'{% endifequal %}>
|
||||||
<a href="{% url 'settings-theme' %}"><span class='fas fa-fill'></span> {% trans "Theme" %}</a>
|
<a href="{% url 'settings-appearance' %}"><span class='fas fa-fill'></span> {% trans "Appearance" %}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
{% extends "InvenTree/settings/settings.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load inventree_extras %}
|
|
||||||
|
|
||||||
{% block tabs %}
|
|
||||||
{% include "InvenTree/settings/tabs.html" with tab='theme' %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block subtitle %}
|
|
||||||
{% trans "Theme Settings" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block settings %}
|
|
||||||
|
|
||||||
<div class='row'>
|
|
||||||
<div class='col-sm-6'>
|
|
||||||
<h4>{% trans "Color Themes" %}</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form action="{% url 'settings-theme' %}" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
{% crispy form %}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% if invalid_color_theme %}
|
|
||||||
<div class="alert alert-danger alert-block" role="alert" style="display: inline-block;">
|
|
||||||
{% blocktrans %}
|
|
||||||
The CSS sheet "{{invalid_color_theme}}.css" for the currently selected color theme was not found.<br>
|
|
||||||
Please select another color theme :)
|
|
||||||
{% endblocktrans %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -58,6 +58,11 @@
|
|||||||
<td>{% trans "View Code on GitHub" %}</td>
|
<td>{% trans "View Code on GitHub" %}</td>
|
||||||
<td><a href="{% inventree_github_url %}">{% inventree_github_url %}</a></td>
|
<td><a href="{% inventree_github_url %}">{% inventree_github_url %}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-balance-scale'></span></td>
|
||||||
|
<td>{% trans "Credits" %}</td>
|
||||||
|
<td><a href="{% inventree_credits_url %}">{% inventree_credits_url %}</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class='fas fa-mobile-alt'></span></td>
|
<td><span class='fas fa-mobile-alt'></span></td>
|
||||||
<td>{% trans "Mobile App" %}</td>
|
<td>{% trans "Mobile App" %}</td>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %}
|
{% if owner_control.value == "True" and user in owners or user.is_superuser or owner_control.value == "False" %}
|
||||||
{% if roles.stock.add %}
|
{% if not read_only and roles.stock.add %}
|
||||||
<button class="btn btn-success" id='item-create' title='{% trans "New Stock Item" %}'>
|
<button class="btn btn-success" id='item-create' title='{% trans "New Stock Item" %}'>
|
||||||
<span class='fas fa-plus-circle'></span>
|
<span class='fas fa-plus-circle'></span>
|
||||||
</button>
|
</button>
|
||||||
@ -44,6 +44,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% if not read_only %}
|
||||||
{% if roles.stock.change or roles.stock.delete %}
|
{% if roles.stock.change or roles.stock.delete %}
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown" title='{% trans "Stock Options" %}'>
|
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown" title='{% trans "Stock Options" %}'>
|
||||||
@ -65,6 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
<div class='filter-list' id='filter-list-stock'>
|
<div class='filter-list' id='filter-list-stock'>
|
||||||
<!-- An empty div in which the filter list will be constructed -->
|
<!-- An empty div in which the filter list will be constructed -->
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,6 +51,10 @@ To contribute to the translation effort, navigate to the [InvenTree crowdin proj
|
|||||||
|
|
||||||
For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/).
|
For InvenTree documentation, refer to the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/).
|
||||||
|
|
||||||
|
# Credits
|
||||||
|
|
||||||
|
The credits for all used packages are part of the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/credits/).
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Refer to the [getting started guide](https://inventree.readthedocs.io/en/latest/start/install/) for installation and setup instructions.
|
Refer to the [getting started guide](https://inventree.readthedocs.io/en/latest/start/install/) for installation and setup instructions.
|
||||||
|
Loading…
Reference in New Issue
Block a user