Merge branch 'master' of https://github.com/inventree/InvenTree into price-history

This commit is contained in:
Matthias 2021-05-04 00:02:15 +02:00
commit 75f487aee9
29 changed files with 279 additions and 139 deletions

View File

@ -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)))

View File

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

View File

@ -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

View File

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

View File

@ -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:

View File

@ -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.

View File

@ -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 %}

View File

@ -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

View File

@ -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)

View File

@ -6,7 +6,7 @@ Company database model definitions
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import decimal
import math import math
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -566,12 +566,15 @@ class SupplierPart(models.Model):
- If order multiples are to be observed, then we need to calculate based on that, too - If order multiples are to be observed, then we need to calculate based on that, too
""" """
price_breaks = self.price_breaks.filter(quantity__lte=quantity) price_breaks = self.price_breaks.all()
# No price break information available? # No price break information available?
if len(price_breaks) == 0: if len(price_breaks) == 0:
return None return None
# Check if quantity is fraction and disable multiples
multiples = (quantity % 1 == 0)
# Order multiples # Order multiples
if multiples: if multiples:
quantity = int(math.ceil(quantity / self.multiple) * self.multiple) quantity = int(math.ceil(quantity / self.multiple) * self.multiple)
@ -584,7 +587,12 @@ class SupplierPart(models.Model):
# Default currency selection # Default currency selection
currency = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') currency = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
pb_min = None
for pb in self.price_breaks.all(): for pb in self.price_breaks.all():
# Store smallest price break
if not pb_min:
pb_min = pb
# Ignore this pricebreak (quantity is too high) # Ignore this pricebreak (quantity is too high)
if pb.quantity > quantity: if pb.quantity > quantity:
continue continue
@ -598,6 +606,17 @@ class SupplierPart(models.Model):
# Convert everything to the selected currency # Convert everything to the selected currency
pb_cost = pb.convert_to(currency) pb_cost = pb.convert_to(currency)
# Use smallest price break
if not pb_found and pb_min:
# Update price break information
pb_quantity = pb_min.quantity
pb_cost = pb_min.convert_to(currency)
# Trigger cost calculation using smallest price break
pb_found = True
# Convert quantity to decimal.Decimal format
quantity = decimal.Decimal(f'{quantity}')
if pb_found: if pb_found:
cost = pb_cost * quantity cost = pb_cost * quantity
return normalize(cost + self.base_cost) return normalize(cost + self.base_cost)
@ -675,4 +694,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}'

View File

@ -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

View File

@ -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)

View File

@ -5,6 +5,7 @@ from django.test import TestCase
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
import os import os
from decimal import Decimal
from .models import Company, Contact, ManufacturerPart, SupplierPart from .models import Company, Contact, ManufacturerPart, SupplierPart
from .models import rename_company_image from .models import rename_company_image
@ -103,8 +104,9 @@ class CompanySimpleTest(TestCase):
self.assertEqual(p(100), 350) self.assertEqual(p(100), 350)
p = self.acme0002.get_price p = self.acme0002.get_price
self.assertEqual(p(1), None) self.assertEqual(p(0.5), 3.5)
self.assertEqual(p(2), None) self.assertEqual(p(1), 7)
self.assertEqual(p(2), 14)
self.assertEqual(p(5), 35) self.assertEqual(p(5), 35)
self.assertEqual(p(45), 315) self.assertEqual(p(45), 315)
self.assertEqual(p(55), 68.75) self.assertEqual(p(55), 68.75)
@ -112,6 +114,7 @@ class CompanySimpleTest(TestCase):
def test_part_pricing(self): def test_part_pricing(self):
m2x4 = Part.objects.get(name='M2x4 LPHS') m2x4 = Part.objects.get(name='M2x4 LPHS')
self.assertEqual(m2x4.get_price_info(5.5), "38.5 - 41.25")
self.assertEqual(m2x4.get_price_info(10), "70 - 75") self.assertEqual(m2x4.get_price_info(10), "70 - 75")
self.assertEqual(m2x4.get_price_info(100), "125 - 350") self.assertEqual(m2x4.get_price_info(100), "125 - 350")
@ -121,7 +124,8 @@ class CompanySimpleTest(TestCase):
m3x12 = Part.objects.get(name='M3x12 SHCS') m3x12 = Part.objects.get(name='M3x12 SHCS')
self.assertIsNone(m3x12.get_price_info(3)) self.assertEqual(m3x12.get_price_info(0.3), Decimal('2.4'))
self.assertEqual(m3x12.get_price_info(3), Decimal('24'))
self.assertIsNotNone(m3x12.get_price_info(50)) self.assertIsNotNone(m3x12.get_price_info(50))
def test_currency_validation(self): def test_currency_validation(self):

View File

@ -344,14 +344,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,

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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

View File

@ -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):
""" """

View File

@ -1959,6 +1959,11 @@ class PartPricing(AjaxView):
role_required = ['sales_order.view', 'part.view'] role_required = ['sales_order.view', 'part.view']
def get_quantity(self):
""" Return set quantity in decimal format """
return Decimal(self.request.POST.get('quantity', 1))
def get_part(self): def get_part(self):
try: try:
return Part.objects.get(id=self.kwargs['pk']) return Part.objects.get(id=self.kwargs['pk'])
@ -1967,12 +1972,12 @@ class PartPricing(AjaxView):
def get_pricing(self, quantity=1, currency=None): def get_pricing(self, quantity=1, currency=None):
try: # try:
quantity = int(quantity) # quantity = int(quantity)
except ValueError: # except ValueError:
quantity = 1 # quantity = 1
if quantity < 1: if quantity <= 0:
quantity = 1 quantity = 1
# TODO - Capacity for price comparison in different currencies # TODO - Capacity for price comparison in different currencies
@ -2002,16 +2007,19 @@ class PartPricing(AjaxView):
min_buy_price /= scaler min_buy_price /= scaler
max_buy_price /= scaler max_buy_price /= scaler
min_unit_buy_price = round(min_buy_price / quantity, 3)
max_unit_buy_price = round(max_buy_price / quantity, 3)
min_buy_price = round(min_buy_price, 3) min_buy_price = round(min_buy_price, 3)
max_buy_price = round(max_buy_price, 3) max_buy_price = round(max_buy_price, 3)
if min_buy_price: if min_buy_price:
ctx['min_total_buy_price'] = min_buy_price ctx['min_total_buy_price'] = min_buy_price
ctx['min_unit_buy_price'] = min_buy_price / quantity ctx['min_unit_buy_price'] = min_unit_buy_price
if max_buy_price: if max_buy_price:
ctx['max_total_buy_price'] = max_buy_price ctx['max_total_buy_price'] = max_buy_price
ctx['max_unit_buy_price'] = max_buy_price / quantity ctx['max_unit_buy_price'] = max_unit_buy_price
# BOM pricing information # BOM pricing information
if part.bom_count > 0: if part.bom_count > 0:
@ -2024,16 +2032,19 @@ class PartPricing(AjaxView):
min_bom_price /= scaler min_bom_price /= scaler
max_bom_price /= scaler max_bom_price /= scaler
min_unit_bom_price = round(min_bom_price / quantity, 3)
max_unit_bom_price = round(max_bom_price / quantity, 3)
min_bom_price = round(min_bom_price, 3) min_bom_price = round(min_bom_price, 3)
max_bom_price = round(max_bom_price, 3) max_bom_price = round(max_bom_price, 3)
if min_bom_price: if min_bom_price:
ctx['min_total_bom_price'] = min_bom_price ctx['min_total_bom_price'] = min_bom_price
ctx['min_unit_bom_price'] = min_bom_price / quantity ctx['min_unit_bom_price'] = min_unit_bom_price
if max_bom_price: if max_bom_price:
ctx['max_total_bom_price'] = max_bom_price ctx['max_total_bom_price'] = max_bom_price
ctx['max_unit_bom_price'] = max_bom_price / quantity ctx['max_unit_bom_price'] = max_unit_bom_price
# Stock history # Stock history
if part_settings.part_show_graph and part.total_stock > 1: if part_settings.part_show_graph and part.total_stock > 1:
@ -2077,10 +2088,11 @@ class PartPricing(AjaxView):
currency = None currency = None
try: quantity = self.get_quantity()
quantity = int(self.request.POST.get('quantity', 1))
except ValueError: # Retain quantity value set by user
quantity = 1 form = self.form_class()
form.fields['quantity'].initial = quantity
# TODO - How to handle pricing in different currencies? # TODO - How to handle pricing in different currencies?
currency = None currency = None
@ -2090,7 +2102,7 @@ class PartPricing(AjaxView):
'form_valid': False, 'form_valid': False,
} }
return self.renderJsonResponse(request, self.form_class(), data=data, context=self.get_pricing(quantity, currency)) return self.renderJsonResponse(request, form, data=data, context=self.get_pricing(quantity, currency))
class PartParameterTemplateCreate(AjaxCreateView): class PartParameterTemplateCreate(AjaxCreateView):

View 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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -51,11 +51,15 @@ 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/).
## 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.
## Integration # Credits
The credits for all used packages are part of the [InvenTree documentation website](https://inventree.readthedocs.io/en/latest/credits/).
# Integration
InvenTree is designed to be extensible, and provides multiple options for integration with external applications or addition of custom plugins: InvenTree is designed to be extensible, and provides multiple options for integration with external applications or addition of custom plugins:

View File

@ -248,6 +248,7 @@ def content_excludes():
"django_q.schedule", "django_q.schedule",
"django_q.task", "django_q.task",
"django_q.ormq", "django_q.ormq",
"users.owner",
] ]
output = "" output = ""