mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into price-history
This commit is contained in:
commit
75f487aee9
@ -357,6 +357,8 @@ def extract_serial_numbers(serials, expected_quantity):
|
||||
- Serial numbers must be positive
|
||||
- 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 <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:
|
||||
expected_quantity: The number of (unique) serial numbers we expect
|
||||
@ -369,6 +371,13 @@ def extract_serial_numbers(serials, expected_quantity):
|
||||
numbers = []
|
||||
errors = []
|
||||
|
||||
# helpers
|
||||
def number_add(n):
|
||||
if n in numbers:
|
||||
errors.append(_('Duplicate serial: {n}').format(n=n))
|
||||
else:
|
||||
numbers.append(n)
|
||||
|
||||
try:
|
||||
expected_quantity = int(expected_quantity)
|
||||
except ValueError:
|
||||
@ -395,10 +404,7 @@ def extract_serial_numbers(serials, expected_quantity):
|
||||
|
||||
if a < b:
|
||||
for n in range(a, b + 1):
|
||||
if n in numbers:
|
||||
errors.append(_('Duplicate serial: {n}').format(n=n))
|
||||
else:
|
||||
numbers.append(n)
|
||||
number_add(n)
|
||||
else:
|
||||
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))
|
||||
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:
|
||||
if group in numbers:
|
||||
errors.append(_("Duplicate serial: {g}".format(g=group)))
|
||||
|
@ -491,7 +491,7 @@ LANGUAGES = [
|
||||
('en', _('English')),
|
||||
('fr', _('French')),
|
||||
('de', _('German')),
|
||||
('pk', _('Polish')),
|
||||
('pl', _('Polish')),
|
||||
('tr', _('Turkish')),
|
||||
]
|
||||
|
||||
|
@ -244,6 +244,14 @@ class TestSerialNumberExtraction(TestCase):
|
||||
self.assertIn(3, 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):
|
||||
|
||||
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 SettingsView, EditUserView, SetPasswordView
|
||||
from .views import ColorThemeSelectView, SettingCategorySelectView
|
||||
from .views import AppearanceSelectView, SettingCategorySelectView
|
||||
from .views import DynamicJsView
|
||||
|
||||
from common.views import SettingEdit
|
||||
@ -79,7 +79,8 @@ apipatterns = [
|
||||
settings_urls = [
|
||||
|
||||
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'^report/?', SettingsView.as_view(template_name='InvenTree/settings/report.html'), name='settings-report'),
|
||||
|
@ -769,12 +769,12 @@ class SettingsView(TemplateView):
|
||||
return ctx
|
||||
|
||||
|
||||
class ColorThemeSelectView(FormView):
|
||||
class AppearanceSelectView(FormView):
|
||||
""" View for selecting a color theme """
|
||||
|
||||
form_class = ColorThemeSelectForm
|
||||
success_url = reverse_lazy('settings-theme')
|
||||
template_name = "InvenTree/settings/theme.html"
|
||||
success_url = reverse_lazy('settings-appearance')
|
||||
template_name = "InvenTree/settings/appearance.html"
|
||||
|
||||
def get_user_theme(self):
|
||||
""" Get current user color theme """
|
||||
@ -788,7 +788,7 @@ class ColorThemeSelectView(FormView):
|
||||
def get_initial(self):
|
||||
""" 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()
|
||||
if user_theme:
|
||||
|
@ -996,14 +996,28 @@ class Build(MPTTModel):
|
||||
|
||||
@property
|
||||
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 = []
|
||||
|
||||
for item in self.part.bom_items.all().prefetch_related('sub_part'):
|
||||
for item in self.bom_items:
|
||||
parts.append(item.sub_part)
|
||||
|
||||
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):
|
||||
"""
|
||||
Returns stock items which are available for allocation to this build.
|
||||
|
@ -40,8 +40,8 @@
|
||||
<div class='panel-heading'>
|
||||
{% trans "The following items will be created" %}
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% include "hover_image.html" with image=build.part.image hover=True %}
|
||||
<div class='panel-content' style='padding-bottom:16px'>
|
||||
{% include "hover_image.html" with image=build.part.image %}
|
||||
{% if output.serialized %}
|
||||
{{ output.part.full_name }} - {% trans "Serial Number" %} {{ output.serial }}
|
||||
{% else %}
|
||||
|
@ -157,6 +157,17 @@ class BuildOutputCreate(AjaxUpdateView):
|
||||
quantity = form.cleaned_data.get('output_quantity', 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
|
||||
if serials:
|
||||
try:
|
||||
@ -212,7 +223,7 @@ class BuildOutputCreate(AjaxUpdateView):
|
||||
|
||||
# Calculate the required quantity
|
||||
quantity = max(0, build.remaining - build.incomplete_count)
|
||||
initials['output_quantity'] = quantity
|
||||
initials['output_quantity'] = int(quantity)
|
||||
|
||||
return initials
|
||||
|
||||
|
@ -131,7 +131,7 @@ class ManufacturerPartList(generics.ListCreateAPIView):
|
||||
params = self.request.query_params
|
||||
|
||||
# Filter by manufacturer
|
||||
manufacturer = params.get('company', None)
|
||||
manufacturer = params.get('manufacturer', None)
|
||||
|
||||
if manufacturer is not None:
|
||||
queryset = queryset.filter(manufacturer=manufacturer)
|
||||
|
@ -6,7 +6,7 @@ Company database model definitions
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
import decimal
|
||||
import math
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
price_breaks = self.price_breaks.filter(quantity__lte=quantity)
|
||||
price_breaks = self.price_breaks.all()
|
||||
|
||||
# No price break information available?
|
||||
if len(price_breaks) == 0:
|
||||
return None
|
||||
|
||||
# Check if quantity is fraction and disable multiples
|
||||
multiples = (quantity % 1 == 0)
|
||||
|
||||
# Order multiples
|
||||
if multiples:
|
||||
quantity = int(math.ceil(quantity / self.multiple) * self.multiple)
|
||||
@ -584,7 +587,12 @@ class SupplierPart(models.Model):
|
||||
# Default currency selection
|
||||
currency = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||
|
||||
pb_min = None
|
||||
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)
|
||||
if pb.quantity > quantity:
|
||||
continue
|
||||
@ -598,6 +606,17 @@ class SupplierPart(models.Model):
|
||||
# Convert everything to the selected 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:
|
||||
cost = pb_cost * quantity
|
||||
return normalize(cost + self.base_cost)
|
||||
@ -675,4 +694,4 @@ class SupplierPriceBreak(common.models.PriceBreak):
|
||||
db_table = 'part_supplierpricebreak'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.part.MPN} - {self.price} @ {self.quantity}'
|
||||
return f'{self.part.SKU} - {self.price} @ {self.quantity}'
|
||||
|
@ -192,8 +192,9 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
|
||||
manufacturer_id = self.initial_data.get('manufacturer', None)
|
||||
MPN = self.initial_data.get('MPN', None)
|
||||
|
||||
if manufacturer_id or MPN:
|
||||
kwargs = {'manufacturer': manufacturer_id,
|
||||
if manufacturer_id and MPN:
|
||||
kwargs = {
|
||||
'manufacturer': manufacturer_id,
|
||||
'MPN': MPN,
|
||||
}
|
||||
supplier_part.save(**kwargs)
|
||||
|
@ -100,7 +100,7 @@ class ManufacturerTest(InvenTreeAPITestCase):
|
||||
self.assertEqual(response.data['MPN'], 'MPN_TEST')
|
||||
|
||||
# Filter by manufacturer
|
||||
data = {'company': 7}
|
||||
data = {'manufacturer': 7}
|
||||
response = self.get(url, data)
|
||||
self.assertEqual(len(response.data), 3)
|
||||
|
||||
|
@ -5,6 +5,7 @@ from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
import os
|
||||
from decimal import Decimal
|
||||
|
||||
from .models import Company, Contact, ManufacturerPart, SupplierPart
|
||||
from .models import rename_company_image
|
||||
@ -103,8 +104,9 @@ class CompanySimpleTest(TestCase):
|
||||
self.assertEqual(p(100), 350)
|
||||
|
||||
p = self.acme0002.get_price
|
||||
self.assertEqual(p(1), None)
|
||||
self.assertEqual(p(2), None)
|
||||
self.assertEqual(p(0.5), 3.5)
|
||||
self.assertEqual(p(1), 7)
|
||||
self.assertEqual(p(2), 14)
|
||||
self.assertEqual(p(5), 35)
|
||||
self.assertEqual(p(45), 315)
|
||||
self.assertEqual(p(55), 68.75)
|
||||
@ -112,6 +114,7 @@ class CompanySimpleTest(TestCase):
|
||||
def test_part_pricing(self):
|
||||
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(100), "125 - 350")
|
||||
|
||||
@ -121,7 +124,8 @@ class CompanySimpleTest(TestCase):
|
||||
|
||||
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))
|
||||
|
||||
def test_currency_validation(self):
|
||||
|
@ -344,14 +344,16 @@ class PurchaseOrder(Order):
|
||||
raise ValidationError({"status": _("Lines can only be received against an order marked as 'Placed'")})
|
||||
|
||||
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)
|
||||
if quantity <= 0:
|
||||
raise ValidationError({"quantity": _("Quantity must be greater than zero")})
|
||||
except ValueError:
|
||||
except (ValueError, TypeError):
|
||||
raise ValidationError({"quantity": _("Invalid quantity provided")})
|
||||
|
||||
# Create a new stock item
|
||||
if line.part:
|
||||
if line.part and quantity > 0:
|
||||
stock = stock_models.StockItem(
|
||||
part=line.part.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() {
|
||||
launchModalForm("{% url 'po-cancel' order.id %}", {
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
|
||||
$("#export-order").click(function() {
|
||||
location.href = "{% url 'po-export' order.id %}";
|
||||
});
|
||||
|
||||
|
||||
{% endblock %}
|
@ -4,6 +4,8 @@
|
||||
|
||||
{% 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 %}
|
@ -6,9 +6,9 @@
|
||||
|
||||
{% trans 'Mark this order as complete?' %}
|
||||
{% if not order.is_complete %}
|
||||
<div class='alert alert-warning alert-block'>
|
||||
{% trans 'This order has line items which have not been marked as received.' %}
|
||||
{% trans 'Marking this order as complete will remove these line items.' %}
|
||||
<div class='alert alert-warning alert-block' style='margin-top:12px'>
|
||||
{% trans 'This order has line items which have not been marked as received.' %}</br>
|
||||
{% trans 'Completing this order means that the order and line items will no longer be editable.' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
{% 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 %}
|
@ -35,31 +35,6 @@
|
||||
|
||||
{{ 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 %}
|
||||
$('#new-po-line').click(function() {
|
||||
launchModalForm("{% url 'po-line-item-create' %}",
|
||||
@ -261,5 +236,4 @@ $("#po-table").inventreeTable({
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
{% endblock %}
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
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 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
|
||||
|
||||
if stock_to_build is not None:
|
||||
# Filter only active parts
|
||||
queryset = queryset.filter(active=True)
|
||||
# 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)
|
||||
|
||||
# Get active builds
|
||||
builds = Build.objects.filter(status__in=BuildStatus.ACTIVE_CODES)
|
||||
# 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
|
||||
# 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)
|
||||
queryset = queryset.filter(pk__in=parts_needed_to_complete_builds)
|
||||
|
||||
# Optionally limit the maximum number of returned results
|
||||
# e.g. for displaying "recent part" list
|
||||
|
@ -116,6 +116,12 @@ def inventree_docs_url(*args, **kwargs):
|
||||
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()
|
||||
def setting_object(key, *args, **kwargs):
|
||||
"""
|
||||
|
@ -1959,6 +1959,11 @@ class PartPricing(AjaxView):
|
||||
|
||||
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):
|
||||
try:
|
||||
return Part.objects.get(id=self.kwargs['pk'])
|
||||
@ -1967,12 +1972,12 @@ class PartPricing(AjaxView):
|
||||
|
||||
def get_pricing(self, quantity=1, currency=None):
|
||||
|
||||
try:
|
||||
quantity = int(quantity)
|
||||
except ValueError:
|
||||
quantity = 1
|
||||
# try:
|
||||
# quantity = int(quantity)
|
||||
# except ValueError:
|
||||
# quantity = 1
|
||||
|
||||
if quantity < 1:
|
||||
if quantity <= 0:
|
||||
quantity = 1
|
||||
|
||||
# TODO - Capacity for price comparison in different currencies
|
||||
@ -2002,16 +2007,19 @@ class PartPricing(AjaxView):
|
||||
min_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)
|
||||
max_buy_price = round(max_buy_price, 3)
|
||||
|
||||
if 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:
|
||||
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
|
||||
if part.bom_count > 0:
|
||||
@ -2024,16 +2032,19 @@ class PartPricing(AjaxView):
|
||||
min_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)
|
||||
max_bom_price = round(max_bom_price, 3)
|
||||
|
||||
if 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:
|
||||
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
|
||||
if part_settings.part_show_graph and part.total_stock > 1:
|
||||
@ -2077,10 +2088,11 @@ class PartPricing(AjaxView):
|
||||
|
||||
currency = None
|
||||
|
||||
try:
|
||||
quantity = int(self.request.POST.get('quantity', 1))
|
||||
except ValueError:
|
||||
quantity = 1
|
||||
quantity = self.get_quantity()
|
||||
|
||||
# Retain quantity value set by user
|
||||
form = self.form_class()
|
||||
form.fields['quantity'].initial = quantity
|
||||
|
||||
# TODO - How to handle pricing in different currencies?
|
||||
currency = None
|
||||
@ -2090,7 +2102,7 @@ class PartPricing(AjaxView):
|
||||
'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):
|
||||
|
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>
|
||||
</li>
|
||||
<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>
|
||||
</ul>
|
||||
{% 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><a href="{% inventree_github_url %}">{% inventree_github_url %}</a></td>
|
||||
</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>
|
||||
<td><span class='fas fa-mobile-alt'></span></td>
|
||||
<td>{% trans "Mobile App" %}</td>
|
||||
|
@ -16,7 +16,7 @@
|
||||
</button>
|
||||
|
||||
{% 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" %}'>
|
||||
<span class='fas fa-plus-circle'></span>
|
||||
</button>
|
||||
@ -44,6 +44,7 @@
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% if not read_only %}
|
||||
{% if roles.stock.change or roles.stock.delete %}
|
||||
<div class="btn-group">
|
||||
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown" title='{% trans "Stock Options" %}'>
|
||||
@ -65,6 +66,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class='filter-list' id='filter-list-stock'>
|
||||
<!-- An empty div in which the filter list will be constructed -->
|
||||
</div>
|
||||
|
@ -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/).
|
||||
|
||||
## Getting Started
|
||||
# Getting Started
|
||||
|
||||
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:
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user