mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' into part-import
This commit is contained in:
commit
859a3178a4
@ -44,3 +44,8 @@ class InvenTreeConfig(AppConfig):
|
||||
schedule_type=Schedule.MINUTES,
|
||||
minutes=15
|
||||
)
|
||||
|
||||
InvenTree.tasks.schedule_task(
|
||||
'InvenTree.tasks.update_exchange_rates',
|
||||
schedule_type=Schedule.DAILY,
|
||||
)
|
||||
|
@ -1,4 +1,20 @@
|
||||
from django.conf import settings as inventree_settings
|
||||
|
||||
from djmoney.contrib.exchange.backends.base import BaseExchangeBackend
|
||||
from djmoney.contrib.exchange.models import Rate
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
|
||||
|
||||
def get_exchange_rate_backend():
|
||||
""" Return the exchange rate backend set by user """
|
||||
|
||||
custom = InvenTreeSetting.get_setting('CUSTOM_EXCHANGE_RATES', False)
|
||||
|
||||
if custom:
|
||||
return InvenTreeManualExchangeBackend()
|
||||
else:
|
||||
return ExchangeRateHostBackend()
|
||||
|
||||
|
||||
class InvenTreeManualExchangeBackend(BaseExchangeBackend):
|
||||
@ -10,12 +26,95 @@ class InvenTreeManualExchangeBackend(BaseExchangeBackend):
|
||||
Specifically: https://github.com/django-money/django-money/tree/master/djmoney/contrib/exchange/backends
|
||||
"""
|
||||
|
||||
name = "inventree"
|
||||
name = 'inventree'
|
||||
url = None
|
||||
custom_rates = True
|
||||
base_currency = None
|
||||
currencies = []
|
||||
|
||||
def update_default_currency(self):
|
||||
""" Update to base currency """
|
||||
|
||||
self.base_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', 'USD')
|
||||
|
||||
def __init__(self, url=None):
|
||||
""" Overrides init to update url, base currency and currencies """
|
||||
|
||||
self.url = url
|
||||
|
||||
self.update_default_currency()
|
||||
|
||||
# Update name
|
||||
self.name = self.name + '-' + self.base_currency.lower()
|
||||
|
||||
self.currencies = inventree_settings.CURRENCIES
|
||||
|
||||
super().__init__()
|
||||
|
||||
def get_rates(self, **kwargs):
|
||||
"""
|
||||
Do not get any rates...
|
||||
""" Returns a mapping <currency>: <rate> """
|
||||
|
||||
return kwargs.get('rates', {})
|
||||
|
||||
def get_stored_rates(self):
|
||||
""" Returns stored rate for specified backend and base currency """
|
||||
|
||||
stored_rates = {}
|
||||
|
||||
stored_rates_obj = Rate.objects.all().prefetch_related('backend')
|
||||
|
||||
for rate in stored_rates_obj:
|
||||
# Find match for backend and base currency
|
||||
if rate.backend.name == self.name and rate.backend.base_currency == self.base_currency:
|
||||
# print(f'{rate.currency} | {rate.value} | {rate.backend} | {rate.backend.base_currency}')
|
||||
stored_rates[rate.currency] = rate.value
|
||||
|
||||
return stored_rates
|
||||
|
||||
|
||||
class ExchangeRateHostBackend(InvenTreeManualExchangeBackend):
|
||||
"""
|
||||
Backend for https://exchangerate.host/
|
||||
"""
|
||||
|
||||
name = "exchangerate.host"
|
||||
|
||||
def __init__(self):
|
||||
self.url = "https://api.exchangerate.host/latest"
|
||||
|
||||
self.custom_rates = False
|
||||
|
||||
super().__init__(url=self.url)
|
||||
|
||||
def get_params(self):
|
||||
# No API key is required
|
||||
return {}
|
||||
|
||||
def update_rates(self, base_currency=None):
|
||||
""" Override update_rates method using currencies found in the settings
|
||||
"""
|
||||
|
||||
if base_currency:
|
||||
self.base_currency = base_currency
|
||||
else:
|
||||
self.update_default_currency()
|
||||
|
||||
symbols = ','.join(self.currencies)
|
||||
|
||||
super().update_rates(base_currency=self.base_currency, symbols=symbols)
|
||||
|
||||
def get_rates(self, **params):
|
||||
""" Returns a mapping <currency>: <rate> """
|
||||
|
||||
# Set base currency
|
||||
params.update(base=self.base_currency)
|
||||
|
||||
response = self.get_response(**params)
|
||||
|
||||
try:
|
||||
return self.parse_json(response)['rates']
|
||||
except KeyError:
|
||||
# API response did not contain any rate
|
||||
pass
|
||||
|
||||
return {}
|
||||
|
@ -7,13 +7,17 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Field
|
||||
from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
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. """
|
||||
@ -236,3 +240,35 @@ 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,
|
||||
})
|
||||
)
|
||||
|
@ -8,7 +8,7 @@ import json
|
||||
import os.path
|
||||
from PIL import Image
|
||||
|
||||
from decimal import Decimal
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from wsgiref.util import FileWrapper
|
||||
from django.http import StreamingHttpResponse
|
||||
@ -606,3 +606,19 @@ def getNewestMigrationFile(app, exclude_extension=True):
|
||||
newest_file = newest_file.replace('.py', '')
|
||||
|
||||
return newest_file
|
||||
|
||||
|
||||
def clean_decimal(number):
|
||||
""" Clean-up decimal value """
|
||||
|
||||
# Check if empty
|
||||
if number is None or number == '':
|
||||
return Decimal(0)
|
||||
|
||||
# Check if decimal type
|
||||
try:
|
||||
clean_number = Decimal(number)
|
||||
except InvalidOperation:
|
||||
clean_number = number
|
||||
|
||||
return clean_number.quantize(Decimal(1)) if clean_number == clean_number.to_integral() else clean_number.normalize()
|
||||
|
@ -514,9 +514,6 @@ CURRENCIES = CONFIG.get(
|
||||
],
|
||||
)
|
||||
|
||||
# TODO - Allow live web-based backends in the future
|
||||
EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeManualExchangeBackend'
|
||||
|
||||
# Extract email settings from the config file
|
||||
email_config = CONFIG.get('email', {})
|
||||
|
||||
|
@ -901,6 +901,15 @@ input[type="submit"] {
|
||||
color: #e00;
|
||||
}
|
||||
|
||||
.info-messages {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.info-messages .alert {
|
||||
padding: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.part-allocation {
|
||||
padding: 3px 10px;
|
||||
border: 1px solid #ccc;
|
||||
|
@ -161,6 +161,28 @@ def check_for_updates():
|
||||
)
|
||||
|
||||
|
||||
def update_exchange_rates():
|
||||
"""
|
||||
Update currency exchange rates
|
||||
"""
|
||||
|
||||
try:
|
||||
import common.models
|
||||
from InvenTree.exchange import ExchangeRateHostBackend
|
||||
except AppRegistryNotReady:
|
||||
# Apps not yet loaded!
|
||||
return
|
||||
|
||||
backend = ExchangeRateHostBackend()
|
||||
print(f"Updating exchange rates from {backend.url}")
|
||||
|
||||
base = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||
|
||||
print(f"Using base currency '{base}'")
|
||||
|
||||
backend.update_rates(base_currency=base)
|
||||
|
||||
|
||||
def send_email(subject, body, recipients, from_email=None):
|
||||
"""
|
||||
Send an email with the specified subject and body,
|
||||
|
@ -41,6 +41,7 @@ from .views import IndexView, SearchView, DatabaseStatsView
|
||||
from .views import SettingsView, EditUserView, SetPasswordView
|
||||
from .views import AppearanceSelectView, SettingCategorySelectView
|
||||
from .views import DynamicJsView
|
||||
from .views import CurrencySettingsView
|
||||
|
||||
from common.views import SettingEdit
|
||||
|
||||
@ -90,6 +91,7 @@ settings_urls = [
|
||||
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'^(?P<pk>\d+)/edit/', SettingEdit.as_view(), name='setting-edit'),
|
||||
|
||||
|
@ -10,8 +10,15 @@ import common.models
|
||||
|
||||
INVENTREE_SW_VERSION = "0.2.2 pre"
|
||||
|
||||
# Increment this number whenever there is a significant change to the API that any clients need to know about
|
||||
INVENTREE_API_VERSION = 2
|
||||
"""
|
||||
Increment thi API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v3 -> 2021-05-22:
|
||||
- The updated StockItem "history tracking" now uses a different interface
|
||||
|
||||
"""
|
||||
|
||||
INVENTREE_API_VERSION = 3
|
||||
|
||||
|
||||
def inventreeInstanceName():
|
||||
|
@ -23,10 +23,13 @@ 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
|
||||
|
||||
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
|
||||
|
||||
@ -908,3 +911,89 @@ 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)
|
||||
|
@ -9,44 +9,54 @@
|
||||
{% inventree_title %} | {% trans "Build Order" %} - {{ build }}
|
||||
{% endblock %}
|
||||
|
||||
{% block below_thumbnail %}
|
||||
|
||||
<div class='info-messages'>
|
||||
{% if build.sales_order %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% object_link 'so-detail' build.sales_order.id build.sales_order as link %}
|
||||
{% blocktrans %}This Build Order is allocated to Sales Order {{link}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if build.parent %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% object_link 'build-detail' build.parent.id build.parent as link %}
|
||||
{% blocktrans %}This Build Order is a child of Build Order {{link}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if build.active %}
|
||||
{% if build.can_complete %}
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Build Order is ready to mark as completed" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if build.incomplete_count > 0 %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Build Order cannot be completed as outstanding outputs remain" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if build.completed < build.quantity %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "Required build quantity has not yet been completed" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not build.areUntrackedPartsFullyAllocated %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "Stock has not been fully allocated to this Build Order" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block header_pre_content %}
|
||||
{% if build.sales_order %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% object_link 'so-detail' build.sales_order.id build.sales_order as link %}
|
||||
{% blocktrans %}This Build Order is allocated to Sales Order {{link}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if build.parent %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% object_link 'build-detail' build.parent.id build.parent as link %}
|
||||
{% blocktrans %}This Build Order is a child of Build Order {{link}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block header_post_content %}
|
||||
{% if build.active %}
|
||||
{% if build.can_complete %}
|
||||
<div class='alert alert-block alert-success'>
|
||||
{% trans "Build Order is ready to mark as completed" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if build.incomplete_count > 0 %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Build Order cannot be completed as outstanding outputs remain" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if build.completed < build.quantity %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "Required build quantity has not yet been completed" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not build.areUntrackedPartsFullyAllocated %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "Stock has not been fully allocated to this Build Order" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block thumbnail %}
|
||||
|
@ -5,12 +5,11 @@ Django forms for interacting with common objects
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from InvenTree.forms import HelperForm
|
||||
from InvenTree.helpers import clean_decimal
|
||||
|
||||
from .files import FileManager
|
||||
from .models import InvenTreeSetting
|
||||
|
@ -87,6 +87,13 @@ class InvenTreeSetting(models.Model):
|
||||
'choices': djmoney.settings.CURRENCY_CHOICES,
|
||||
},
|
||||
|
||||
'CUSTOM_EXCHANGE_RATES': {
|
||||
'name': _('Custom Exchange Rates'),
|
||||
'description': _('Enable custom exchange rates'),
|
||||
'validator': bool,
|
||||
'default': False,
|
||||
},
|
||||
|
||||
'INVENTREE_DOWNLOAD_FROM_URL': {
|
||||
'name': _('Download from URL'),
|
||||
'description': _('Allow download of remote images and files from external URL'),
|
||||
|
@ -40,13 +40,15 @@ class CompanySimpleTest(TestCase):
|
||||
self.acme0002 = SupplierPart.objects.get(SKU='ACME0002')
|
||||
self.zerglphs = SupplierPart.objects.get(SKU='ZERGLPHS')
|
||||
self.zergm312 = SupplierPart.objects.get(SKU='ZERGM312')
|
||||
|
||||
InvenTreeManualExchangeBackend().update_rates()
|
||||
|
||||
# Exchange rate backend
|
||||
backend = InvenTreeManualExchangeBackend()
|
||||
backend.update_rates(base_currency=backend.base_currency)
|
||||
|
||||
Rate.objects.create(
|
||||
currency='AUD',
|
||||
value='1.35',
|
||||
backend_id='inventree',
|
||||
backend_id=backend.name,
|
||||
)
|
||||
|
||||
def test_company_model(self):
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -9,13 +9,15 @@
|
||||
{% inventree_title %} | {% trans "Sales Order" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block pre_content %}
|
||||
{% if order.status == SalesOrderStatus.PENDING and not order.is_fully_allocated %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "This SalesOrder has not been fully allocated" %}
|
||||
{% block below_thumbnail %}
|
||||
<div class='info-messages'>
|
||||
{% if order.status == SalesOrderStatus.PENDING and not order.is_fully_allocated %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "This Sales Order has not been fully allocated" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block thumbnail %}
|
||||
<img class='part-thumb'
|
||||
|
@ -12,13 +12,6 @@
|
||||
|
||||
<div class='panel panel-default panel-inventree'>
|
||||
|
||||
{% if part.variant_of %}
|
||||
<div class='alert alert-info alert-block'>
|
||||
{% object_link 'part-variants' part.variant_of.id part.variant_of.full_name as link %}
|
||||
{% blocktrans %}This part is a variant of {{link}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
{% include "part/part_thumb.html" %}
|
||||
@ -107,6 +100,15 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='info-messages'>
|
||||
{% if part.variant_of %}
|
||||
<div class='alert alert-info alert-block'>
|
||||
{% object_link 'part-variants' part.variant_of.id part.variant_of.full_name as link %}
|
||||
{% blocktrans %}This part is a variant of {{link}}{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<table class="table table-striped">
|
||||
|
@ -119,6 +119,12 @@ def inventree_version(*args, **kwargs):
|
||||
return version.inventreeVersion()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def inventree_api_version(*args, **kwargs):
|
||||
""" Return InvenTree API version """
|
||||
return version.inventreeApiVersion()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def django_version(*args, **kwargs):
|
||||
""" Return Django version string """
|
||||
|
@ -1018,6 +1018,9 @@ class StockTrackingList(generics.ListAPIView):
|
||||
for item in data:
|
||||
deltas = item['deltas']
|
||||
|
||||
if not deltas:
|
||||
deltas = {}
|
||||
|
||||
# Add location detail
|
||||
if 'location' in deltas:
|
||||
try:
|
||||
|
@ -14,68 +14,6 @@
|
||||
|
||||
{% block pre_content %}
|
||||
{% include 'stock/loc_link.html' with location=item.location %}
|
||||
|
||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
||||
{% if owner_control.value == "True" %}
|
||||
{% authorized_owners item.owner as owners %}
|
||||
|
||||
{% if not user in owners and not user.is_superuser %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "You are not in the list of owners of this item. This stock item cannot be edited." %}<br>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if item.is_building %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This stock item is in production and cannot be edited." %}<br>
|
||||
{% trans "Edit the stock item from the build view." %}<br>
|
||||
|
||||
{% if item.build %}
|
||||
<a href="{% url 'build-detail' item.build.id %}">
|
||||
<b>{{ item.build }}</b>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if item.hasRequiredTests and not item.passedAllRequiredTests %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "This stock item has not passed all required tests" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for allocation in item.sales_order_allocations.all %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% object_link 'so-detail' allocation.line.order.id allocation.line.order as link %}
|
||||
{% decimal allocation.quantity as qty %}
|
||||
{% blocktrans %}This stock item is allocated to Sales Order {{ link }} (Quantity: {{ qty }}){% endblocktrans %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for allocation in item.allocations.all %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% object_link 'build-detail' allocation.build.id allocation.build %}
|
||||
{% decimal allocation.quantity as qty %}
|
||||
{% blocktrans %}This stock item is allocated to Build {{ link }} (Quantity: {{ qty }}){% endblocktrans %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if item.serialized %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
|
||||
</div>
|
||||
{% elif item.child_count > 0 %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "This stock item cannot be deleted as it has child items" %}
|
||||
</div>
|
||||
{% elif item.delete_on_deplete and item.can_delete %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "This stock item will be automatically deleted when all stock is depleted." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block thumbnail %}
|
||||
@ -221,6 +159,73 @@
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block below_thumbnail %}
|
||||
<div class='info-messages'>
|
||||
|
||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
||||
{% if owner_control.value == "True" %}
|
||||
{% authorized_owners item.owner as owners %}
|
||||
|
||||
{% if not user in owners and not user.is_superuser %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "You are not in the list of owners of this item. This stock item cannot be edited." %}<br>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if item.is_building %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% trans "This stock item is in production and cannot be edited." %}<br>
|
||||
{% trans "Edit the stock item from the build view." %}<br>
|
||||
|
||||
{% if item.build %}
|
||||
<a href="{% url 'build-detail' item.build.id %}">
|
||||
<b>{{ item.build }}</b>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if item.hasRequiredTests and not item.passedAllRequiredTests %}
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "This stock item has not passed all required tests" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for allocation in item.sales_order_allocations.all %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% object_link 'so-detail' allocation.line.order.id allocation.line.order as link %}
|
||||
{% decimal allocation.quantity as qty %}
|
||||
{% blocktrans %}This stock item is allocated to Sales Order {{ link }} (Quantity: {{ qty }}){% endblocktrans %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for allocation in item.allocations.all %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
{% object_link 'build-detail' allocation.build.id allocation.build %}
|
||||
{% decimal allocation.quantity as qty %}
|
||||
{% blocktrans %}This stock item is allocated to Build {{ link }} (Quantity: {{ qty }}){% endblocktrans %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if item.serialized %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted." %}
|
||||
</div>
|
||||
{% elif item.child_count > 0 %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "This stock item cannot be deleted as it has child items" %}
|
||||
</div>
|
||||
{% elif item.delete_on_deplete and item.can_delete %}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "This stock item will be automatically deleted when all stock is depleted." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_details %}
|
||||
<h4>{% trans "Stock Item Details" %}</h4>
|
||||
<table class="table table-striped">
|
||||
|
52
InvenTree/templates/InvenTree/settings/currencies.html
Normal file
52
InvenTree/templates/InvenTree/settings/currencies.html
Normal file
@ -0,0 +1,52 @@
|
||||
{% extends "InvenTree/settings/settings.html" %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block tabs %}
|
||||
{% include "InvenTree/settings/tabs.html" with tab='currencies' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}
|
||||
{% trans "Currency Settings" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block settings %}
|
||||
|
||||
<table class='table table-striped table-condensed'>
|
||||
{% include "InvenTree/settings/header.html" %}
|
||||
<tbody>
|
||||
{% 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" %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h4>{% blocktrans with cur=default_currency %}Exchange Rates - Convert to {{cur}}{% endblocktrans %}</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{% url 'settings-currencies' %}" method="post">
|
||||
<div id='exchange_rate_form'>
|
||||
{% csrf_token %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% crispy form %}
|
||||
{% if custom_rates is False %}
|
||||
<button type="submit" class='btn btn-primary'>{% trans "Refresh Exchange Rates" %}</button>
|
||||
{% else %}
|
||||
<button type="submit" class='btn btn-primary'>{% trans "Update Exchange Rates" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% 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 %}
|
@ -19,7 +19,6 @@
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE_TITLE" icon="fa-info-circle" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_BASE_URL" icon="fa-globe" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-dollar-sign" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_FROM_URL" icon="fa-cloud-download-alt" %}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -32,4 +31,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -36,5 +36,8 @@
|
||||
<li {% if tab == 'so' %} class='active'{% endif %}>
|
||||
<a href="{% url 'settings-so' %}"><span class='fas fa-truck'></span> {% trans "Sales Orders" %}</a>
|
||||
</li>
|
||||
<li {% if tab == 'currencies' %} class='active'{% endif %}>
|
||||
<a href="{% url 'settings-currencies' %}"><span class='fas fa-dollar-sign'></span> {% trans "Currencies" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
@ -29,6 +29,11 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-code'></span></td>
|
||||
<td>{% trans "API Version" %}</td>
|
||||
<td>{% inventree_api_version %}{% include "clip.html" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-hashtag'></span></td>
|
||||
<td>{% trans "Django Version" %}</td>
|
||||
|
@ -1017,6 +1017,11 @@ function loadStockTrackingTable(table, options) {
|
||||
formatter: function(details, row, index, field) {
|
||||
var html = `<table class='table table-condensed' id='tracking-table-${row.pk}'>`;
|
||||
|
||||
if (!details) {
|
||||
html += '</table>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// Location information
|
||||
if (details.location) {
|
||||
|
||||
|
@ -27,6 +27,8 @@
|
||||
<!-- Data next to image goes here -->
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% block below_thumbnail %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class='col-sm-6'>
|
||||
{% block page_details %}
|
||||
|
@ -1,3 +1,6 @@
|
||||
"commit_message": "Fix: New translations %original_file_name% from Crowdin"
|
||||
"append_commit_message": false
|
||||
|
||||
files:
|
||||
- source: /InvenTree/locale/en/LC_MESSAGES/django.po
|
||||
translation: /InvenTree/locale/%two_letters_code%/LC_MESSAGES/%original_file_name%
|
||||
|
@ -41,9 +41,10 @@ LABEL org.label-schema.schema-version="1.0" \
|
||||
|
||||
# Create user account
|
||||
RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
|
||||
|
||||
WORKDIR ${INVENTREE_HOME}
|
||||
|
||||
RUN mkdir ${INVENTREE_STATIC_ROOT}
|
||||
RUN mkdir -p ${INVENTREE_STATIC_ROOT}
|
||||
|
||||
# Install required system packages
|
||||
RUN apk add --no-cache git make bash \
|
||||
|
@ -3,12 +3,12 @@
|
||||
# Create required directory structure (if it does not already exist)
|
||||
if [[ ! -d "$INVENTREE_STATIC_ROOT" ]]; then
|
||||
echo "Creating directory $INVENTREE_STATIC_ROOT"
|
||||
mkdir $INVENTREE_STATIC_ROOT
|
||||
mkdir -p $INVENTREE_STATIC_ROOT
|
||||
fi
|
||||
|
||||
if [[ ! -d "$INVENTREE_MEDIA_ROOT" ]]; then
|
||||
echo "Creating directory $INVENTREE_MEDIA_ROOT"
|
||||
mkdir $INVENTREE_MEDIA_ROOT
|
||||
mkdir -p $INVENTREE_MEDIA_ROOT
|
||||
fi
|
||||
|
||||
# Check if "config.yaml" has been copied into the correct location
|
||||
|
@ -3,12 +3,12 @@
|
||||
# Create required directory structure (if it does not already exist)
|
||||
if [[ ! -d "$INVENTREE_STATIC_ROOT" ]]; then
|
||||
echo "Creating directory $INVENTREE_STATIC_ROOT"
|
||||
mkdir $INVENTREE_STATIC_ROOT
|
||||
mkdir -p $INVENTREE_STATIC_ROOT
|
||||
fi
|
||||
|
||||
if [[ ! -d "$INVENTREE_MEDIA_ROOT" ]]; then
|
||||
echo "Creating directory $INVENTREE_MEDIA_ROOT"
|
||||
mkdir $INVENTREE_MEDIA_ROOT
|
||||
mkdir -p $INVENTREE_MEDIA_ROOT
|
||||
fi
|
||||
|
||||
# Check if "config.yaml" has been copied into the correct location
|
||||
|
Loading…
Reference in New Issue
Block a user