mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	Merge branch 'master' of https://github.com/inventree/InvenTree into price-history
This commit is contained in:
		| @@ -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,10 +192,11 @@ 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, | ||||
|                       'MPN': MPN, | ||||
|                       } | ||||
|         if manufacturer_id and MPN: | ||||
|             kwargs = { | ||||
|                 'manufacturer': manufacturer_id, | ||||
|                 'MPN': MPN, | ||||
|             } | ||||
|             supplier_part.save(**kwargs) | ||||
|  | ||||
|         return supplier_part | ||||
|   | ||||
| @@ -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: | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user