From f2e9f58f1b3916f058e42fcbfa29c3b3e0539b41 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 13 May 2021 15:47:42 -0400 Subject: [PATCH 01/21] Added purchase price range and average to BOM items/view --- InvenTree/part/api.py | 9 ++++++++- InvenTree/part/serializers.py | 9 +++++++++ InvenTree/templates/js/bom.js | 29 +++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index de6ac5f273..659976a0b0 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -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 +from django.db.models import Q, F, Count, Min, Max, Avg from django.utils.translation import ugettext_lazy as _ from rest_framework import status @@ -877,6 +877,13 @@ class BomList(generics.ListCreateAPIView): else: queryset = queryset.exclude(pk__in=pks) + # Annotate with purchase prices + queryset = queryset.annotate( + purchase_price_min=Min('sub_part__stock_items__purchase_price'), + purchase_price_max=Max('sub_part__stock_items__purchase_price'), + purchase_price_avg=Avg('sub_part__stock_items__purchase_price'), + ) + return queryset filter_backends = [ diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 7ab385249c..8c2d72f43d 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -367,6 +367,12 @@ class BomItemSerializer(InvenTreeModelSerializer): validated = serializers.BooleanField(read_only=True, source='is_line_valid') + purchase_price_min = serializers.FloatField() + + purchase_price_max = serializers.FloatField() + + purchase_price_avg = serializers.FloatField() + def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. # This saves a bunch of database requests @@ -410,6 +416,9 @@ class BomItemSerializer(InvenTreeModelSerializer): 'sub_part_detail', # 'price_range', 'validated', + 'purchase_price_min', + 'purchase_price_max', + 'purchase_price_avg', ] diff --git a/InvenTree/templates/js/bom.js b/InvenTree/templates/js/bom.js index 462db6eba4..c97d8b30e5 100644 --- a/InvenTree/templates/js/bom.js +++ b/InvenTree/templates/js/bom.js @@ -243,6 +243,35 @@ function loadBomTable(table, options) { } }); + cols.push( + { + field: 'purchase_price_range', + title: '{% trans "Purchase Price Range" %}', + searchable: false, + sortable: true, + formatter: function(value, row, index, field) { + var purchase_price_range = 0; + + if (row.purchase_price_min > 0) { + if (row.purchase_price_min >= row.purchase_price_max) { + purchase_price_range = row.purchase_price_min; + } else { + purchase_price_range = row.purchase_price_min + " - " + row.purchase_price_max; + } + } + + return purchase_price_range; + }, + }); + + cols.push( + { + field: 'purchase_price_avg', + title: '{% trans "Purchase Price Average" %}', + searchable: false, + sortable: true, + }); + /* // TODO - Re-introduce the pricing column at a later stage, From 32d0f3039de910b8e3c90164c5c80da84907e541 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 13 May 2021 16:17:45 -0400 Subject: [PATCH 02/21] Obviously new float fields should be read-only... --- InvenTree/part/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 8c2d72f43d..2316f2f18b 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -367,11 +367,11 @@ class BomItemSerializer(InvenTreeModelSerializer): validated = serializers.BooleanField(read_only=True, source='is_line_valid') - purchase_price_min = serializers.FloatField() + purchase_price_min = serializers.FloatField(read_only=True) - purchase_price_max = serializers.FloatField() + purchase_price_max = serializers.FloatField(read_only=True) - purchase_price_avg = serializers.FloatField() + purchase_price_avg = serializers.FloatField(read_only=True) def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. From 68f5ec8b6a9f54e8273627e2495edd7fbb169179 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 13 May 2021 17:09:52 -0400 Subject: [PATCH 03/21] Added currency conversion --- InvenTree/part/api.py | 42 +++++++++++++++++++++++++++++++++++ InvenTree/part/serializers.py | 7 +++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 659976a0b0..5b82a168cf 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -15,6 +15,10 @@ from rest_framework.response import Response from rest_framework import filters, serializers from rest_framework import generics +from djmoney.money import Money +from djmoney.contrib.exchange.models import convert_money +from djmoney.contrib.exchange.exceptions import MissingRate + from django.conf.urls import url, include from django.urls import reverse @@ -24,6 +28,7 @@ from .models import PartAttachment, PartTestTemplate from .models import PartSellPriceBreak from .models import PartCategoryParameterTemplate +from common.models import InvenTreeSetting from build.models import Build from . import serializers as part_serializers @@ -882,8 +887,45 @@ class BomList(generics.ListCreateAPIView): purchase_price_min=Min('sub_part__stock_items__purchase_price'), purchase_price_max=Max('sub_part__stock_items__purchase_price'), purchase_price_avg=Avg('sub_part__stock_items__purchase_price'), + purchase_price_currency=F('sub_part__stock_items__purchase_price_currency'), ) + # Convert prices to default currency (using backend conversion rates) + for item in queryset: + # Get default currency from settings + default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + + if default_currency: + if item.purchase_price_min: + # Convert minimum + try: + # Get adjusted price + purchase_price_adjusted = convert_money(Money(item.purchase_price_min, item.purchase_price_currency), default_currency) + # Update queryset + item.purchase_price_min = purchase_price_adjusted + except MissingRate: + pass + + if item.purchase_price_max: + # Convert maximum + try: + # Get adjusted price + purchase_price_adjusted = convert_money(Money(item.purchase_price_max, item.purchase_price_currency), default_currency) + # Update queryset + item.purchase_price_max = purchase_price_adjusted + except MissingRate: + pass + + if item.purchase_price_avg: + # Convert average + try: + # Get adjusted price + purchase_price_adjusted = convert_money(Money(item.purchase_price_avg, item.purchase_price_currency), default_currency) + # Update queryset + item.purchase_price_avg = purchase_price_adjusted + except MissingRate: + pass + return queryset filter_backends = [ diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 2316f2f18b..096d072981 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -12,6 +12,7 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializerField, from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus from rest_framework import serializers from sql_util.utils import SubqueryCount, SubquerySum +from djmoney.contrib.django_rest_framework import MoneyField from stock.models import StockItem from .models import (BomItem, Part, PartAttachment, PartCategory, @@ -367,11 +368,11 @@ class BomItemSerializer(InvenTreeModelSerializer): validated = serializers.BooleanField(read_only=True, source='is_line_valid') - purchase_price_min = serializers.FloatField(read_only=True) + purchase_price_min = MoneyField(max_digits=10, decimal_places=4, read_only=True) - purchase_price_max = serializers.FloatField(read_only=True) + purchase_price_max = MoneyField(max_digits=10, decimal_places=4, read_only=True) - purchase_price_avg = serializers.FloatField(read_only=True) + purchase_price_avg = MoneyField(max_digits=10, decimal_places=4, read_only=True) def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. From 1940fd5199f34e91b6df6b17dc8a8adbd909efbf Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 14 May 2021 16:16:23 -0400 Subject: [PATCH 04/21] Now processing currencies --- InvenTree/part/api.py | 70 ++++++++++++++++++++--------------- InvenTree/part/serializers.py | 43 +++++++++++++++++++-- InvenTree/templates/js/bom.js | 13 ------- 3 files changed, 79 insertions(+), 47 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 5b82a168cf..065eca30c6 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -887,44 +887,54 @@ class BomList(generics.ListCreateAPIView): purchase_price_min=Min('sub_part__stock_items__purchase_price'), purchase_price_max=Max('sub_part__stock_items__purchase_price'), purchase_price_avg=Avg('sub_part__stock_items__purchase_price'), - purchase_price_currency=F('sub_part__stock_items__purchase_price_currency'), ) - # Convert prices to default currency (using backend conversion rates) - for item in queryset: + # Get values for currencies + currencies = queryset.annotate( + purchase_price_currency=F('sub_part__stock_items__purchase_price_currency'), + ).values('pk', 'sub_part', 'purchase_price_currency') + + def convert_price(price, currency, decimal_places=4): + """ Convert price field, returns Money field """ + + price_adjusted = None + # Get default currency from settings default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') - if default_currency: - if item.purchase_price_min: - # Convert minimum + if price: + if currency and default_currency: try: # Get adjusted price - purchase_price_adjusted = convert_money(Money(item.purchase_price_min, item.purchase_price_currency), default_currency) - # Update queryset - item.purchase_price_min = purchase_price_adjusted + price_adjusted = convert_money(Money(price, currency), default_currency) except MissingRate: - pass - - if item.purchase_price_max: - # Convert maximum - try: - # Get adjusted price - purchase_price_adjusted = convert_money(Money(item.purchase_price_max, item.purchase_price_currency), default_currency) - # Update queryset - item.purchase_price_max = purchase_price_adjusted - except MissingRate: - pass - - if item.purchase_price_avg: - # Convert average - try: - # Get adjusted price - purchase_price_adjusted = convert_money(Money(item.purchase_price_avg, item.purchase_price_currency), default_currency) - # Update queryset - item.purchase_price_avg = purchase_price_adjusted - except MissingRate: - pass + # No conversion rate set + price_adjusted = Money(price, currency) + else: + # Currency exists + if currency: + price_adjusted = Money(price, currency) + # Default currency exists + if default_currency: + price_adjusted = Money(price, default_currency) + + if price_adjusted and decimal_places: + price_adjusted.decimal_places = decimal_places + + return price_adjusted + + # Convert prices to default currency (using backend conversion rates) + for bom_item in queryset: + # Find associated currency (select first found) + purchase_price_currency = None + for currency_item in currencies: + if currency_item['pk'] == bom_item.pk and currency_item['sub_part'] == bom_item.sub_part: + purchase_price_currency = currency_item['purchase_price_currency'] + break + # Convert prices + bom_item.purchase_price_min = convert_price(bom_item.purchase_price_min, purchase_price_currency) + bom_item.purchase_price_max = convert_price(bom_item.purchase_price_max, purchase_price_currency) + bom_item.purchase_price_avg = convert_price(bom_item.purchase_price_avg, purchase_price_currency) return queryset diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 096d072981..868997915b 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -368,11 +368,13 @@ class BomItemSerializer(InvenTreeModelSerializer): validated = serializers.BooleanField(read_only=True, source='is_line_valid') - purchase_price_min = MoneyField(max_digits=10, decimal_places=4, read_only=True) + purchase_price_min = MoneyField(max_digits=10, decimal_places=6, read_only=True) - purchase_price_max = MoneyField(max_digits=10, decimal_places=4, read_only=True) - - purchase_price_avg = MoneyField(max_digits=10, decimal_places=4, read_only=True) + purchase_price_max = MoneyField(max_digits=10, decimal_places=6, read_only=True) + + purchase_price_avg = serializers.SerializerMethodField() + + purchase_price_range = serializers.SerializerMethodField() def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. @@ -401,6 +403,38 @@ class BomItemSerializer(InvenTreeModelSerializer): queryset = queryset.prefetch_related('sub_part__supplier_parts__pricebreaks') return queryset + def get_purchase_price_range(self, obj): + """ Return purchase price range """ + + if obj.purchase_price_min and not obj.purchase_price_max: + # Get price range + purchase_price_range = str(obj.purchase_price_max) + elif not obj.purchase_price_min and obj.purchase_price_max: + # Get price range + purchase_price_range = str(obj.purchase_price_max) + elif obj.purchase_price_min and obj.purchase_price_max: + # Get price range + if obj.purchase_price_min >= obj.purchase_price_max: + # If min > max: use min only + purchase_price_range = str(obj.purchase_price_min) + else: + purchase_price_range = str(obj.purchase_price_min) + " - " + str(obj.purchase_price_max) + else: + purchase_price_range = '-' + + return purchase_price_range + + def get_purchase_price_avg(self, obj): + """ Return purchase price average """ + + if obj.purchase_price_avg: + # Get string representation of price average + purchase_price_avg = str(obj.purchase_price_avg) + else: + purchase_price_avg = '-' + + return purchase_price_avg + class Meta: model = BomItem fields = [ @@ -420,6 +454,7 @@ class BomItemSerializer(InvenTreeModelSerializer): 'purchase_price_min', 'purchase_price_max', 'purchase_price_avg', + 'purchase_price_range', ] diff --git a/InvenTree/templates/js/bom.js b/InvenTree/templates/js/bom.js index c97d8b30e5..e35a51d8bd 100644 --- a/InvenTree/templates/js/bom.js +++ b/InvenTree/templates/js/bom.js @@ -249,19 +249,6 @@ function loadBomTable(table, options) { title: '{% trans "Purchase Price Range" %}', searchable: false, sortable: true, - formatter: function(value, row, index, field) { - var purchase_price_range = 0; - - if (row.purchase_price_min > 0) { - if (row.purchase_price_min >= row.purchase_price_max) { - purchase_price_range = row.purchase_price_min; - } else { - purchase_price_range = row.purchase_price_min + " - " + row.purchase_price_max; - } - } - - return purchase_price_range; - }, }); cols.push( From 274eb51e48d7e8e4cea773b3223f33582791c869 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 14 May 2021 16:29:55 -0400 Subject: [PATCH 05/21] Added read_only args --- InvenTree/part/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 868997915b..b6e24fd199 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -372,9 +372,9 @@ class BomItemSerializer(InvenTreeModelSerializer): purchase_price_max = MoneyField(max_digits=10, decimal_places=6, read_only=True) - purchase_price_avg = serializers.SerializerMethodField() + purchase_price_avg = serializers.SerializerMethodField(read_only=True) - purchase_price_range = serializers.SerializerMethodField() + purchase_price_range = serializers.SerializerMethodField(read_only=True) def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. From e9f41a83576f26b08e3f13b4c7e380184752b224 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 14 May 2021 16:38:30 -0400 Subject: [PATCH 06/21] Currency finding fix --- InvenTree/part/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 065eca30c6..5bdd572145 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -928,7 +928,7 @@ class BomList(generics.ListCreateAPIView): # Find associated currency (select first found) purchase_price_currency = None for currency_item in currencies: - if currency_item['pk'] == bom_item.pk and currency_item['sub_part'] == bom_item.sub_part: + if currency_item['pk'] == bom_item.pk and currency_item['sub_part'] == bom_item.sub_part.pk: purchase_price_currency = currency_item['purchase_price_currency'] break # Convert prices From 5ce262172d77d133ee591751adab63eb86fd663f Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 14 May 2021 16:59:59 -0400 Subject: [PATCH 07/21] Fixed bom_item unit test --- InvenTree/part/serializers.py | 39 ++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index b6e24fd199..04e0b7a119 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -372,9 +372,9 @@ class BomItemSerializer(InvenTreeModelSerializer): purchase_price_max = MoneyField(max_digits=10, decimal_places=6, read_only=True) - purchase_price_avg = serializers.SerializerMethodField(read_only=True) + purchase_price_avg = serializers.SerializerMethodField() - purchase_price_range = serializers.SerializerMethodField(read_only=True) + purchase_price_range = serializers.SerializerMethodField() def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. @@ -406,19 +406,29 @@ class BomItemSerializer(InvenTreeModelSerializer): def get_purchase_price_range(self, obj): """ Return purchase price range """ - if obj.purchase_price_min and not obj.purchase_price_max: + try: + purchase_price_min = obj.purchase_price_min + except AttributeError: + return None + + try: + purchase_price_max = obj.purchase_price_max + except AttributeError: + return None + + if purchase_price_min and not purchase_price_max: # Get price range - purchase_price_range = str(obj.purchase_price_max) - elif not obj.purchase_price_min and obj.purchase_price_max: + purchase_price_range = str(purchase_price_max) + elif not purchase_price_min and purchase_price_max: # Get price range - purchase_price_range = str(obj.purchase_price_max) - elif obj.purchase_price_min and obj.purchase_price_max: + purchase_price_range = str(purchase_price_max) + elif purchase_price_min and purchase_price_max: # Get price range - if obj.purchase_price_min >= obj.purchase_price_max: + if purchase_price_min >= purchase_price_max: # If min > max: use min only - purchase_price_range = str(obj.purchase_price_min) + purchase_price_range = str(purchase_price_min) else: - purchase_price_range = str(obj.purchase_price_min) + " - " + str(obj.purchase_price_max) + purchase_price_range = str(purchase_price_min) + " - " + str(purchase_price_max) else: purchase_price_range = '-' @@ -427,9 +437,14 @@ class BomItemSerializer(InvenTreeModelSerializer): def get_purchase_price_avg(self, obj): """ Return purchase price average """ - if obj.purchase_price_avg: + try: + purchase_price_avg = obj.purchase_price_avg + except AttributeError: + return None + + if purchase_price_avg: # Get string representation of price average - purchase_price_avg = str(obj.purchase_price_avg) + purchase_price_avg = str(purchase_price_avg) else: purchase_price_avg = '-' From c2fe5e08b4bb52277050309d98596f275b5ef040 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 May 2021 12:35:55 +1000 Subject: [PATCH 08/21] Expand possibilities for variant conversion - Ref get_conversion_options --- InvenTree/part/models.py | 53 +++++++++++++++++++ InvenTree/part/templates/part/part_base.html | 5 ++ .../stock/templates/stock/item_base.html | 2 +- InvenTree/stock/views.py | 2 +- 4 files changed, 60 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 3c8e65bca7..7db998ab3d 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1861,6 +1861,59 @@ class Part(MPTTModel): return self.get_descendants(include_self=False) + @property + def can_convert(self): + """ + Check if this Part can be "converted" to a different variant: + + It can be converted if: + + a) It has non-virtual variant parts underneath it + b) It has non-virtual template parts above it + c) It has non-virtual sibling variants + + """ + + return self.get_conversion_options().count() > 0 + + def get_conversion_options(self): + """ + Return options for converting this part to a "variant" within the same tree + + a) Variants underneath this one + b) Immediate parent + c) Siblings + """ + + parts = [] + + # Child parts + children = self.get_descendants(include_self=False) + + for child in children: + parts.append(child) + + # Immediate parent + if self.variant_of: + parts.append(self.variant_of) + + siblings = self.get_siblings(include_self=False) + + for sib in siblings: + parts.append(sib) + + filtered_parts = Part.objects.filter(pk__in=[part.pk for part in parts]) + + # Ensure this part is not in the queryset, somehow + filtered_parts = filtered_parts.exclude(pk=self.pk) + + filtered_parts = filtered_parts.filter( + active=True, + virtual=False, + ) + + return filtered_parts + def get_related_parts(self): """ Return list of tuples for all related parts: - first value is PartRelated object diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index f2f1f6557a..ec296d4174 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -102,6 +102,11 @@
+ {% if part.virtual %} +
+ {% trans "This is a virtual part" %} +
+ {% endif %} {% if part.variant_of %}
{% object_link 'part-variants' part.variant_of.id part.variant_of.full_name as link %} diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index da770bab48..991bdee41c 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -139,7 +139,7 @@
{% endif %} From 608547867290cbc29c47372580948b3cc15e5572 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 May 2021 16:34:37 +1000 Subject: [PATCH 13/21] Simplify settings view - Show various currency exchange rates - Button to "refresh now" --- InvenTree/InvenTree/forms.py | 34 ----- InvenTree/InvenTree/urls.py | 20 +-- InvenTree/InvenTree/views.py | 139 +++++++----------- .../InvenTree/settings/currencies.html | 59 ++++---- 4 files changed, 92 insertions(+), 160 deletions(-) diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 488e982ddc..d843c1ddef 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -16,8 +16,6 @@ from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppende from common.models import ColorTheme from part.models import PartCategory -from .exchange import InvenTreeManualExchangeBackend - class HelperForm(forms.ModelForm): """ Provides simple integration of crispy_forms extension. """ @@ -240,35 +238,3 @@ class SettingCategorySelectForm(forms.ModelForm): css_class='row', ), ) - - -class SettingExchangeRatesForm(forms.Form): - """ Form for displaying and setting currency exchange rates manually """ - - def __init__(self, *args, **kwargs): - - super().__init__(*args, **kwargs) - - exchange_rate_backend = InvenTreeManualExchangeBackend() - - # Update default currency (in case it has changed) - exchange_rate_backend.update_default_currency() - - for currency in exchange_rate_backend.currencies: - if currency != exchange_rate_backend.base_currency: - # Set field name - field_name = currency - # Set field input box - self.fields[field_name] = forms.CharField( - label=field_name, - required=False, - widget=forms.NumberInput(attrs={ - 'name': field_name, - 'class': 'numberinput', - 'style': 'width: 200px;', - 'type': 'number', - 'min': '0', - 'step': 'any', - 'value': 0, - }) - ) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 88ee554203..bce493fb23 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -39,6 +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 CurrencySettingsView, CurrencyRefreshView from .views import AppearanceSelectView, SettingCategorySelectView from .views import DynamicJsView @@ -82,15 +83,16 @@ settings_urls = [ 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'), - url(r'^category/?', SettingCategorySelectView.as_view(), name='settings-category'), - url(r'^part/?', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'), - url(r'^stock/?', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'), - url(r'^build/?', SettingsView.as_view(template_name='InvenTree/settings/build.html'), name='settings-build'), - url(r'^purchase-order/?', SettingsView.as_view(template_name='InvenTree/settings/po.html'), name='settings-po'), - url(r'^sales-order/?', SettingsView.as_view(template_name='InvenTree/settings/so.html'), name='settings-so'), - url(r'^currencies/?', SettingsView.as_view(template_name='InvenTree/settings/currencies.html'), name='settings-currencies'), + 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'^category/', SettingCategorySelectView.as_view(), name='settings-category'), + url(r'^part/', SettingsView.as_view(template_name='InvenTree/settings/part.html'), name='settings-part'), + url(r'^stock/', SettingsView.as_view(template_name='InvenTree/settings/stock.html'), name='settings-stock'), + url(r'^build/', SettingsView.as_view(template_name='InvenTree/settings/build.html'), name='settings-build'), + url(r'^purchase-order/', SettingsView.as_view(template_name='InvenTree/settings/po.html'), name='settings-po'), + url(r'^sales-order/', SettingsView.as_view(template_name='InvenTree/settings/so.html'), name='settings-so'), + url(r'^currencies/', CurrencySettingsView.as_view(), name='settings-currencies'), + url(r'^currencies-refresh/', CurrencyRefreshView.as_view(), name='settings-currencies-refresh'), url(r'^(?P\d+)/edit/', SettingEdit.as_view(), name='setting-edit'), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index a18845bf02..4189def492 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -12,12 +12,15 @@ from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string from django.http import JsonResponse, HttpResponseRedirect from django.urls import reverse_lazy +from django.conf import settings from django.contrib.auth.mixins import PermissionRequiredMixin from django.views import View from django.views.generic import ListView, DetailView, CreateView, FormView, DeleteView, UpdateView -from django.views.generic.base import TemplateView +from django.views.generic.base import RedirectView, TemplateView + +from djmoney.contrib.exchange.models import ExchangeBackend, Rate from part.models import Part, PartCategory from stock.models import StockLocation, StockItem @@ -25,11 +28,11 @@ from common.models import InvenTreeSetting, ColorTheme from users.models import check_user_role, RuleSet from InvenTree.helpers import clean_decimal +import InvenTree.tasks + from .forms import DeleteForm, EditUserForm, SetPasswordForm from .forms import ColorThemeSelectForm, SettingCategorySelectForm -from .forms import SettingExchangeRatesForm from .helpers import str2bool -from .exchange import get_exchange_rate_backend from rest_framework import views @@ -772,6 +775,50 @@ class SettingsView(TemplateView): return ctx + +class CurrencyRefreshView(RedirectView): + + url = reverse_lazy("settings-currencies") + + def post(self, request, *args, **kwargs): + """ + On a POST request we will attempt to refresh the exchange rates + """ + + print("POST!") + + # Will block for a little bit + InvenTree.tasks.update_exchange_rates() + + return self.get(request, *args, **kwargs) + + +class CurrencySettingsView(TemplateView): + """ + View for configuring currency settings + """ + + template_name = "InvenTree/settings/currencies.html" + + def get_context_data(self, **kwargs): + + ctx = super().get_context_data(**kwargs).copy() + + ctx['settings'] = InvenTreeSetting.objects.all().order_by('key') + ctx["base_currency"] = settings.BASE_CURRENCY + ctx["currencies"] = settings.CURRENCIES + + ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange") + + # When were the rates last updated? + try: + backend = ExchangeBackend.objects.get(name='InvenTreeExchange') + ctx["rates_updated"] = backend.last_update + except: + ctx["rates_updated"] = None + + return ctx + class AppearanceSelectView(FormView): """ View for selecting a color theme """ @@ -911,89 +958,3 @@ class DatabaseStatsView(AjaxView): """ return ctx - - -class CurrencySettingsView(FormView): - - form_class = SettingExchangeRatesForm - template_name = 'InvenTree/settings/currencies.html' - success_url = reverse_lazy('settings-currencies') - - exchange_rate_backend = None - - def get_exchange_rate_backend(self): - - if not self.exchange_rate_backend: - self.exchange_rate_backend = get_exchange_rate_backend() - - return self.exchange_rate_backend - - def get_context_data(self, **kwargs): - - context = super().get_context_data(**kwargs) - - # Set default API result - if 'api_rates_success' not in context: - context['default_currency'] = True - else: - # Update form - context['form'] = self.get_form() - - # Get exchange rate backend - exchange_rate_backend = self.get_exchange_rate_backend() - - context['default_currency'] = exchange_rate_backend.base_currency - - context['custom_rates'] = exchange_rate_backend.custom_rates - - context['exchange_backend'] = exchange_rate_backend.name - - return context - - def get_form(self): - - form = super().get_form() - - # Get exchange rate backend - exchange_rate_backend = self.get_exchange_rate_backend() - - # Get stored exchange rates - stored_rates = exchange_rate_backend.get_stored_rates() - - for field in form.fields: - if not exchange_rate_backend.custom_rates: - # Disable all the fields - form.fields[field].disabled = True - form.fields[field].initial = clean_decimal(stored_rates.get(field, 0)) - - return form - - def post(self, request, *args, **kwargs): - - form = self.get_form() - - # Get exchange rate backend - exchange_rate_backend = self.get_exchange_rate_backend() - - if not exchange_rate_backend.custom_rates: - # Refresh rate from Fixer.IO API - exchange_rate_backend.update_rates(base_currency=exchange_rate_backend.base_currency) - # Check if rates have been updated - if not exchange_rate_backend.get_stored_rates(): - # Update context - context = {'api_rates_success': False} - # Return view with updated context - return self.render_to_response(self.get_context_data(form=form, **context)) - else: - # Update rates from form - manual_rates = {} - - if form.is_valid(): - for field, value in form.cleaned_data.items(): - manual_rates[field] = clean_decimal(value) - - exchange_rate_backend.update_rates(base_currency=exchange_rate_backend.base_currency, **{'rates': manual_rates}) - else: - return self.form_invalid(form) - - return self.form_valid(form) diff --git a/InvenTree/templates/InvenTree/settings/currencies.html b/InvenTree/templates/InvenTree/settings/currencies.html index dd47bc6cdd..78598236f9 100644 --- a/InvenTree/templates/InvenTree/settings/currencies.html +++ b/InvenTree/templates/InvenTree/settings/currencies.html @@ -13,40 +13,43 @@ {% block settings %} - {% include "InvenTree/settings/header.html" %} - {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %} - {% include "InvenTree/settings/setting.html" with key="CUSTOM_EXCHANGE_RATES" icon="fa-edit" %} + + + + + + + + {% for rate in rates %} + + + + + {% endfor %} + + + +
{% trans "Base Currency" %}{{ base_currency }}
{% trans "Exchange Rates" %}
{{ rate.currency }}{{ rate.value }}
+ {% trans "Last Update" %} + + {% if rates_updated %} + {{ rates_updated }} + {% else %} + {% trans "Never" %} + {% endif %} +
+
+ {% csrf_token %} + +
+
+
-
-
-

{% blocktrans with cur=default_currency %}Exchange Rates - Convert to {{cur}}{% endblocktrans %}

-
-
- -
-
- {% csrf_token %} - {% load crispy_forms_tags %} - {% crispy form %} - {% if custom_rates is False %} - - {% else %} - - {% endif %} -
-
- {% endblock %} {% block js_ready %} {{ block.super }} - -{% if api_rates_success is False %} - var alert_msg = {% blocktrans %}"Failed to refresh exchange rates" {% endblocktrans %}; - showAlertOrCache("alert-danger", alert_msg, null, 5000); -{% endif %} - {% endblock %} \ No newline at end of file From 4520bb74478258b0842bb2af69658cbb99d6884b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 May 2021 16:36:26 +1000 Subject: [PATCH 14/21] PEP style fixes --- InvenTree/InvenTree/exchange.py | 2 +- InvenTree/InvenTree/views.py | 3 +-- InvenTree/common/models.py | 1 - InvenTree/common/test_views.py | 11 +++-------- InvenTree/company/tests.py | 13 ------------- 5 files changed, 5 insertions(+), 25 deletions(-) diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py index dfbfff872c..0695e69f48 100644 --- a/InvenTree/InvenTree/exchange.py +++ b/InvenTree/InvenTree/exchange.py @@ -7,7 +7,7 @@ class InvenTreeExchange(SimpleExchangeBackend): """ Backend for automatically updating currency exchange rates. - Uses the exchangerate.host service API + Uses the exchangerate.host service API """ name = "InvenTreeExchange" diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 4189def492..4ac90bd722 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -26,7 +26,6 @@ from part.models import Part, PartCategory from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme from users.models import check_user_role, RuleSet -from InvenTree.helpers import clean_decimal import InvenTree.tasks @@ -775,7 +774,6 @@ class SettingsView(TemplateView): return ctx - class CurrencyRefreshView(RedirectView): url = reverse_lazy("settings-currencies") @@ -819,6 +817,7 @@ class CurrencySettingsView(TemplateView): return ctx + class AppearanceSelectView(FormView): """ View for selecting a color theme """ diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 6cfe5915e0..236e48770f 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -14,7 +14,6 @@ from django.db import models, transaction from django.db.utils import IntegrityError, OperationalError from django.conf import settings -import djmoney.settings from djmoney.models.fields import MoneyField from djmoney.contrib.exchange.models import convert_money from djmoney.contrib.exchange.exceptions import MissingRate diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index 8dc5830108..56a244ba0c 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -98,20 +98,15 @@ class SettingsViewTest(TestCase): Tests for a setting which has choices """ - setting = InvenTreeSetting.get_setting_object('INVENTREE_DEFAULT_CURRENCY') + setting = InvenTreeSetting.get_setting_object('PURCHASEORDER_REFERENCE_PREFIX') # Default value! - self.assertEqual(setting.value, 'USD') + self.assertEqual(setting.value, 'PO') url = self.get_url(setting.pk) # Try posting an invalid currency option - data, errors = self.post(url, {'value': 'XPQaaa'}, valid=False) - - self.assertIsNotNone(errors.get('value'), None) - - # Try posting a valid currency option - data, errors = self.post(url, {'value': 'AUD'}, valid=True) + data, errors = self.post(url, {'value': 'Purchase Order'}, valid=True) def test_binary_values(self): """ diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index 2c6e722440..b1e05efe14 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -11,9 +11,6 @@ from .models import Company, Contact, ManufacturerPart, SupplierPart from .models import rename_company_image from part.models import Part -from InvenTree.exchange import InvenTreeManualExchangeBackend -from djmoney.contrib.exchange.models import Rate - class CompanySimpleTest(TestCase): @@ -40,16 +37,6 @@ class CompanySimpleTest(TestCase): self.acme0002 = SupplierPart.objects.get(SKU='ACME0002') self.zerglphs = SupplierPart.objects.get(SKU='ZERGLPHS') self.zergm312 = SupplierPart.objects.get(SKU='ZERGM312') - - # Exchange rate backend - backend = InvenTreeManualExchangeBackend() - backend.update_rates(base_currency=backend.base_currency) - - Rate.objects.create( - currency='AUD', - value='1.35', - backend_id=backend.name, - ) def test_company_model(self): c = Company.objects.get(name='ABC Co.') From c71f4ed04577ac5450be7993e322db74bf054c89 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 27 May 2021 16:48:13 +1000 Subject: [PATCH 15/21] Add currency exchange unit tests --- InvenTree/InvenTree/tests.py | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index d65829cf8e..b7e5b98c1b 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -5,6 +5,12 @@ from django.test import TestCase import django.core.exceptions as django_exceptions from django.core.exceptions import ValidationError +from django.conf import settings + +from djmoney.money import Money +from djmoney.contrib.exchange.models import Rate, convert_money +from djmoney.contrib.exchange.exceptions import MissingRate + from .validators import validate_overage, validate_part_name from . import helpers from . import version @@ -13,6 +19,8 @@ from mptt.exceptions import InvalidMove from decimal import Decimal +import InvenTree.tasks + from stock.models import StockLocation @@ -308,3 +316,46 @@ class TestVersionNumber(TestCase): self.assertTrue(v_c > v_b) self.assertTrue(v_d > v_c) self.assertTrue(v_d > v_a) + + +class CurrencyTests(TestCase): + """ + Unit tests for currency / exchange rate functionality + """ + + def test_rates(self): + + # Initially, there will not be any exchange rate information + rates = Rate.objects.all() + + self.assertEqual(rates.count(), 0) + + # Without rate information, we cannot convert anything... + with self.assertRaises(MissingRate): + convert_money(Money(100, 'USD'), 'AUD') + + with self.assertRaises(MissingRate): + convert_money(Money(100, 'AUD'), 'USD') + + currencies = settings.CURRENCIES + + InvenTree.tasks.update_exchange_rates() + + rates = Rate.objects.all() + + self.assertEqual(rates.count(), len(currencies)) + + # Now that we have some exchange rate information, we can perform conversions + + # Forwards + convert_money(Money(100, 'USD'), 'AUD') + + # Backwards + convert_money(Money(100, 'AUD'), 'USD') + + # Convert between non base currencies + convert_money(Money(100, 'CAD'), 'NZD') + + # Convert to a symbol which is not covered + with self.assertRaises(MissingRate): + convert_money(Money(100, 'GBP'), 'ZWL') From 52fc698b51c8b87a02cecb5c1cf613786cf31e03 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 May 2021 12:07:53 +1000 Subject: [PATCH 16/21] Remove debug message --- InvenTree/InvenTree/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 4ac90bd722..108908c571 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -775,6 +775,9 @@ class SettingsView(TemplateView): class CurrencyRefreshView(RedirectView): + """ + POST endpoint to refresh / update exchange rates + """ url = reverse_lazy("settings-currencies") @@ -783,8 +786,6 @@ class CurrencyRefreshView(RedirectView): On a POST request we will attempt to refresh the exchange rates """ - print("POST!") - # Will block for a little bit InvenTree.tasks.update_exchange_rates() From 4ddeab3330fa2af12254a56d02aa17c79cac929a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 May 2021 12:44:39 +1000 Subject: [PATCH 17/21] Update exchange rates when launching the server - Ensures that the exchange rates don't get messed up if the base currency is changed! --- InvenTree/InvenTree/apps.py | 48 ++++++++++++++++++++++++++++++++++++ InvenTree/InvenTree/tasks.py | 4 +++ 2 files changed, 52 insertions(+) diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py index aa60058dcf..465dc2087b 100644 --- a/InvenTree/InvenTree/apps.py +++ b/InvenTree/InvenTree/apps.py @@ -4,6 +4,7 @@ import logging from django.apps import AppConfig from django.core.exceptions import AppRegistryNotReady +from django.conf import settings from InvenTree.ready import canAppAccessDatabase import InvenTree.tasks @@ -19,6 +20,7 @@ class InvenTreeConfig(AppConfig): if canAppAccessDatabase(): self.start_background_tasks() + self.update_exchange_rates() def start_background_tasks(self): @@ -49,3 +51,49 @@ class InvenTreeConfig(AppConfig): 'InvenTree.tasks.update_exchange_rates', schedule_type=Schedule.DAILY, ) + + def update_exchange_rates(self): + """ + Update exchange rates each time the server is started, *if*: + + a) Have not been updated recently (one day or less) + b) The base exchange rate has been altered + """ + + try: + from djmoney.contrib.exchange.models import ExchangeBackend + from datetime import datetime, timedelta + from InvenTree.tasks import update_exchange_rates + except AppRegistryNotReady: + pass + + base_currency = settings.BASE_CURRENCY + + update = False + + try: + backend = ExchangeBackend.objects.get(name='InvenTreeExchange') + + last_update = backend.last_update + + if last_update is not None: + delta = datetime.now().date() - last_update.date() + if delta > timedelta(days=1): + print(f"Last update was {last_update}") + update = True + else: + # Never been updated + print("Exchange backend has never been updated") + update = True + + # Backend currency has changed? + if not base_currency == backend.base_currency: + print(f"Base currency changed from {backend.base_currency} to {base_currency}") + update = True + + except (ExchangeBackend.DoesNotExist): + print("Exchange backend not found - updating") + update = True + + if update: + update_exchange_rates() diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 9a71d5d84c..92c58c24a8 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -168,6 +168,7 @@ def update_exchange_rates(): try: from InvenTree.exchange import InvenTreeExchange + from djmoney.contrib.exchange.models import Rate from django.conf import settings except AppRegistryNotReady: # Apps not yet loaded! @@ -182,6 +183,9 @@ def update_exchange_rates(): backend.update_rates(base_currency=base) + # Remove any exchange rates which are not in the provided currencies + Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=settings.CURRENCIES).delete() + def send_email(subject, body, recipients, from_email=None): """ From 09782353703f6866d26573b5eade9821c7f49ae1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 May 2021 12:49:50 +1000 Subject: [PATCH 18/21] Fix? --- InvenTree/InvenTree/tasks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 92c58c24a8..994a344cc9 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -173,6 +173,9 @@ def update_exchange_rates(): except AppRegistryNotReady: # Apps not yet loaded! return + except: + # Other error? + return backend = InvenTreeExchange() print(f"Updating exchange rates from {backend.url}") From 7832ccccc298911dcabe37e823c67491df9bc052 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 May 2021 12:54:55 +1000 Subject: [PATCH 19/21] Check if database tables are ready --- InvenTree/InvenTree/tasks.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 994a344cc9..d45df99152 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -168,7 +168,7 @@ def update_exchange_rates(): try: from InvenTree.exchange import InvenTreeExchange - from djmoney.contrib.exchange.models import Rate + from djmoney.contrib.exchange.models import ExchangeBackend, Rate from django.conf import settings except AppRegistryNotReady: # Apps not yet loaded! @@ -177,6 +177,16 @@ def update_exchange_rates(): # Other error? return + # Test to see if the database is ready yet + try: + backend = ExchangeBackend.objects.get(name='InvenTreeExchange') + except ExchangeBackend.DoesNotExist: + pass + except: + # Some other error + print("Database not ready") + return + backend = InvenTreeExchange() print(f"Updating exchange rates from {backend.url}") From be6e2aa2769c1d21f30c7b1e9efc5da671380f36 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 May 2021 13:02:34 +1000 Subject: [PATCH 20/21] Better exception handling --- InvenTree/InvenTree/apps.py | 10 ++++++++-- InvenTree/InvenTree/ready.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py index 465dc2087b..aeddb714a0 100644 --- a/InvenTree/InvenTree/apps.py +++ b/InvenTree/InvenTree/apps.py @@ -6,7 +6,7 @@ from django.apps import AppConfig from django.core.exceptions import AppRegistryNotReady from django.conf import settings -from InvenTree.ready import canAppAccessDatabase +from InvenTree.ready import isInTestMode, canAppAccessDatabase import InvenTree.tasks @@ -20,7 +20,9 @@ class InvenTreeConfig(AppConfig): if canAppAccessDatabase(): self.start_background_tasks() - self.update_exchange_rates() + + if not isInTestMode(): + self.update_exchange_rates() def start_background_tasks(self): @@ -95,5 +97,9 @@ class InvenTreeConfig(AppConfig): print("Exchange backend not found - updating") update = True + except: + # Some other error - potentially the tables are not ready yet + return + if update: update_exchange_rates() diff --git a/InvenTree/InvenTree/ready.py b/InvenTree/InvenTree/ready.py index aa31fac947..5a4f1e9576 100644 --- a/InvenTree/InvenTree/ready.py +++ b/InvenTree/InvenTree/ready.py @@ -1,6 +1,17 @@ import sys +def isInTestMode(): + """ + Returns True if the database is in testing mode + """ + + if 'test' in sys.argv: + return True + + return False + + def canAppAccessDatabase(): """ Returns True if the apps.py file can access database records. From b7163124385044508e89fa4fd078f8fabeaa8f99 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 28 May 2021 13:24:18 +1000 Subject: [PATCH 21/21] Ignore actions for l10 branches --- .github/workflows/coverage.yaml | 9 ++++++++- .github/workflows/mysql.yaml | 9 ++++++++- .github/workflows/postgresql.yaml | 9 ++++++++- .github/workflows/style.yaml | 9 ++++++++- .github/workflows/translations.yml | 1 - 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index ad5f7a841e..6d0334b804 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -2,7 +2,14 @@ name: SQLite -on: ["push", "pull_request"] +on: + push: + branches-ignore: + - l10* + + pull_request: + branches-ignore: + - l10* jobs: diff --git a/.github/workflows/mysql.yaml b/.github/workflows/mysql.yaml index 5bafe56253..f0eb0efd7f 100644 --- a/.github/workflows/mysql.yaml +++ b/.github/workflows/mysql.yaml @@ -2,7 +2,14 @@ name: MySQL -on: ["push", "pull_request"] +on: + push: + branches-ignore: + - l10* + + pull_request: + branches-ignore: + - l10* jobs: diff --git a/.github/workflows/postgresql.yaml b/.github/workflows/postgresql.yaml index 3481895d85..9a56382c4e 100644 --- a/.github/workflows/postgresql.yaml +++ b/.github/workflows/postgresql.yaml @@ -2,7 +2,14 @@ name: PostgreSQL -on: ["push", "pull_request"] +on: + push: + branches-ignore: + - l10* + + pull_request: + branches-ignore: + - l10* jobs: diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml index 31da3ec61a..df52de1dcb 100644 --- a/.github/workflows/style.yaml +++ b/.github/workflows/style.yaml @@ -1,6 +1,13 @@ name: Style Checks -on: ["push", "pull_request"] +on: + push: + branches-ignore: + - l10* + + pull_request: + branches-ignore: + - l10* jobs: style: diff --git a/.github/workflows/translations.yml b/.github/workflows/translations.yml index a3950b2002..24106c028e 100644 --- a/.github/workflows/translations.yml +++ b/.github/workflows/translations.yml @@ -5,7 +5,6 @@ on: branches: - master - jobs: build: