diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index ef696a181b..0000000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,17 +0,0 @@ -# .readthedocs.yml -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -version: 2 - -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: docs/conf.py - -formats: all - -# Optionally set the version of Python and requirements required to build your docs -python: - version: 3.5 - install: - - requirements: docs/requirements.txt \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 67d8c0502a..52d0ef1c5c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,11 +30,24 @@ before_install: script: - cd InvenTree && python3 manage.py makemigrations && cd .. - python3 ci/check_migration_files.py + # Run unit testing / code coverage tests - invoke coverage + # Run unit test for SQL database backend - cd InvenTree && python3 manage.py test --settings=InvenTree.ci_mysql && cd .. + # Run unit test for PostgreSQL database backend - cd InvenTree && python3 manage.py test --settings=InvenTree.ci_postgresql && cd .. - invoke translate - invoke style + # Create an empty database and fill it with test data + - rm inventree_default_db.sqlite3 + - invoke migrate + - invoke import-fixtures + # Export database records + - invoke export-records -f data.json + # Create a new empty database and import the saved data + - rm inventree_default_db.sqlite3 + - invoke migrate + - invoke import-records -f data.json after_success: - coveralls \ No newline at end of file diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py new file mode 100644 index 0000000000..04ceabccd8 --- /dev/null +++ b/InvenTree/InvenTree/exchange.py @@ -0,0 +1,21 @@ +from djmoney.contrib.exchange.backends.base import BaseExchangeBackend + + +class InvenTreeManualExchangeBackend(BaseExchangeBackend): + """ + Backend for manually updating currency exchange rates + + See the documentation for django-money: https://github.com/django-money/django-money + + Specifically: https://github.com/django-money/django-money/tree/master/djmoney/contrib/exchange/backends + """ + + name = "inventree" + url = None + + def get_rates(self, **kwargs): + """ + Do not get any rates... + """ + + return {} diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index f9f8378526..6cc4a9ed5f 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -12,6 +12,7 @@ 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 class HelperForm(forms.ModelForm): @@ -200,3 +201,33 @@ class ColorThemeSelectForm(forms.ModelForm): css_class='row', ), ) + + +class SettingCategorySelectForm(forms.ModelForm): + """ Form for setting category settings """ + + category = forms.ModelChoiceField(queryset=PartCategory.objects.all()) + + class Meta: + model = PartCategory + fields = [ + 'category' + ] + + def __init__(self, *args, **kwargs): + super(SettingCategorySelectForm, self).__init__(*args, **kwargs) + + self.helper = FormHelper() + # Form rendering + self.helper.form_show_labels = False + self.helper.layout = Layout( + Div( + Div(Field('category'), + css_class='col-sm-6', + style='width: 70%;'), + Div(StrictButton(_('Select Category'), css_class='btn btn-primary', type='submit'), + css_class='col-sm-6', + style='width: 30%; padding-left: 0;'), + css_class='row', + ), + ) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 1c587d7b43..f4ce28cf30 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -155,8 +155,9 @@ INSTALLED_APPS = [ 'markdownify', # Markdown template rendering 'django_tex', # LaTeX output 'django_admin_shell', # Python shell for the admin interface + 'djmoney', # django-money integration + 'djmoney.contrib.exchange', # django-money exchange rates 'error_report', # Error reporting in the admin interface - ] LOGGING = { @@ -356,6 +357,17 @@ LANGUAGES = [ ('pk', _('Polish')), ] +# Currencies available for use +CURRENCIES = CONFIG.get( + 'currencies', + [ + 'AUD', 'CAD', 'EUR', 'GBP', 'JPY', 'NZD', 'USD', + ], +) + +# TODO - Allow live web-based backends in the future +EXCHANGE_BACKEND = 'InvenTree.exchange.InvenTreeManualExchangeBackend' + LOCALE_PATHS = ( os.path.join(BASE_DIR, 'locale/'), ) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index 69d701def0..f44542656f 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -27,8 +27,7 @@ class APITests(APITestCase): def setUp(self): # Create a user (but do not log in!) - User = get_user_model() - User.objects.create_user(self.username, 'user@email.com', self.password) + get_user_model().objects.create_user(self.username, 'user@email.com', self.password) def basicAuth(self): # Use basic authentication diff --git a/InvenTree/InvenTree/test_views.py b/InvenTree/InvenTree/test_views.py index 171dcbb05f..4f7dddfde4 100644 --- a/InvenTree/InvenTree/test_views.py +++ b/InvenTree/InvenTree/test_views.py @@ -16,8 +16,7 @@ class ViewTests(TestCase): def setUp(self): # Create a user - User = get_user_model() - User.objects.create_user(self.username, 'user@email.com', self.password) + get_user_model().objects.create_user(self.username, 'user@email.com', self.password) self.client.login(username=self.username, password=self.password) diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 70fb8c87f8..b7e0d64d18 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -36,7 +36,8 @@ from django.views.generic.base import RedirectView from rest_framework.documentation import include_docs_urls from .views import IndexView, SearchView, DatabaseStatsView -from .views import SettingsView, EditUserView, SetPasswordView, ColorThemeSelectView +from .views import SettingsView, EditUserView, SetPasswordView +from .views import ColorThemeSelectView, SettingCategorySelectView from .views import DynamicJsView from common.views import SettingEdit @@ -74,7 +75,7 @@ settings_urls = [ url(r'^theme/?', ColorThemeSelectView.as_view(), name='settings-theme'), url(r'^global/?', SettingsView.as_view(template_name='InvenTree/settings/global.html'), name='settings-global'), - url(r'^currency/?', SettingsView.as_view(template_name='InvenTree/settings/currency.html'), name='settings-currency'), + 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'), diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py index e85dc40810..70322df062 100644 --- a/InvenTree/InvenTree/validators.py +++ b/InvenTree/InvenTree/validators.py @@ -6,11 +6,22 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ +from moneyed import CURRENCIES + import common.models import re +def validate_currency_code(code): + """ + Check that a given code is a valid currency code. + """ + + if code not in CURRENCIES: + raise ValidationError(_('Not a valid currency code')) + + def allowable_url_schemes(): """ Return the list of allowable URL schemes. In addition to the default schemes allowed by Django, diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index dd7a782485..e61e2d497c 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -24,7 +24,8 @@ from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme from users.models import check_user_role, RuleSet -from .forms import DeleteForm, EditUserForm, SetPasswordForm, ColorThemeSelectForm +from .forms import DeleteForm, EditUserForm, SetPasswordForm +from .forms import ColorThemeSelectForm, SettingCategorySelectForm from .helpers import str2bool from rest_framework import views @@ -750,6 +751,42 @@ class ColorThemeSelectView(FormView): return self.form_invalid(form) +class SettingCategorySelectView(FormView): + """ View for selecting categories in settings """ + + form_class = SettingCategorySelectForm + success_url = reverse_lazy('settings-category') + template_name = "InvenTree/settings/category.html" + + def get_initial(self): + """ Set category selection """ + + initial = super(SettingCategorySelectView, self).get_initial() + + category = self.request.GET.get('category', None) + if category: + initial['category'] = category + + return initial + + def post(self, request, *args, **kwargs): + """ Handle POST request (which contains category selection). + + Pass the selected category to the page template + """ + + form = self.get_form() + + if form.is_valid(): + context = self.get_context_data() + + context['category'] = form.cleaned_data['category'] + + return super(SettingCategorySelectView, self).render_to_response(context) + + return self.form_invalid(form) + + class DatabaseStatsView(AjaxView): """ View for displaying database statistics """ diff --git a/InvenTree/barcode/tests.py b/InvenTree/barcode/tests.py index 4b7356aead..5f178d923c 100644 --- a/InvenTree/barcode/tests.py +++ b/InvenTree/barcode/tests.py @@ -24,8 +24,8 @@ class BarcodeAPITest(APITestCase): def setUp(self): # Create a user for auth - User = get_user_model() - User.objects.create_user('testuser', 'test@testing.com', 'password') + user = get_user_model() + user.objects.create_user('testuser', 'test@testing.com', 'password') self.client.login(username='testuser', password='password') diff --git a/InvenTree/build/migrations/0023_auto_20201110_0911.py b/InvenTree/build/migrations/0023_auto_20201110_0911.py new file mode 100644 index 0000000000..3d95e6844e --- /dev/null +++ b/InvenTree/build/migrations/0023_auto_20201110_0911.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.7 on 2020-11-10 09:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0054_remove_stockitem_build_order'), + ('build', '0022_buildorderattachment'), + ] + + operations = [ + migrations.AlterField( + model_name='builditem', + name='stock_item', + field=models.ForeignKey(help_text='Source stock item', limit_choices_to={'belongs_to': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index a18328be1a..a18471ccc6 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -962,7 +962,6 @@ class BuildItem(models.Model): related_name='allocations', help_text=_('Source stock item'), limit_choices_to={ - 'build_order': None, 'sales_order': None, 'belongs_to': None, } diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index b560a4f9c9..1b7886c018 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -267,17 +267,17 @@ class BuildTest(TestCase): # New stock items should have been created! self.assertEqual(StockItem.objects.count(), 4) - A = StockItem.objects.get(pk=self.stock_1_1.pk) + a = StockItem.objects.get(pk=self.stock_1_1.pk) # This stock item has been depleted! with self.assertRaises(StockItem.DoesNotExist): StockItem.objects.get(pk=self.stock_1_2.pk) - C = StockItem.objects.get(pk=self.stock_2_1.pk) + c = StockItem.objects.get(pk=self.stock_2_1.pk) # Stock should have been subtracted from the original items - self.assertEqual(A.quantity, 900) - self.assertEqual(C.quantity, 4500) + self.assertEqual(a.quantity, 900) + self.assertEqual(c.quantity, 4500) # And 10 new stock items created for the build output outputs = StockItem.objects.filter(build=self.build) diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index f01aaa83c9..cb1881e507 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -28,10 +28,10 @@ class BuildTestSimple(TestCase): def setUp(self): # Create a user for auth - User = get_user_model() - User.objects.create_user('testuser', 'test@testing.com', 'password') + user = get_user_model() + user.objects.create_user('testuser', 'test@testing.com', 'password') - self.user = User.objects.get(username='testuser') + self.user = user.objects.get(username='testuser') g = Group.objects.create(name='builders') self.user.groups.add(g) @@ -109,11 +109,11 @@ class TestBuildAPI(APITestCase): def setUp(self): # Create a user for auth - User = get_user_model() - user = User.objects.create_user('testuser', 'test@testing.com', 'password') + user = get_user_model() + self.user = user.objects.create_user('testuser', 'test@testing.com', 'password') g = Group.objects.create(name='builders') - user.groups.add(g) + self.user.groups.add(g) for rule in g.rule_sets.all(): if rule.name == 'build': @@ -185,11 +185,11 @@ class TestBuildViews(TestCase): super().setUp() # Create a user - User = get_user_model() - user = User.objects.create_user('username', 'user@email.com', 'password') + user = get_user_model() + self.user = user.objects.create_user('username', 'user@email.com', 'password') g = Group.objects.create(name='builders') - user.groups.add(g) + self.user.groups.add(g) for rule in g.rule_sets.all(): if rule.name == 'build': diff --git a/InvenTree/common/admin.py b/InvenTree/common/admin.py index da12852d83..3edcd1fa8d 100644 --- a/InvenTree/common/admin.py +++ b/InvenTree/common/admin.py @@ -5,11 +5,7 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin -from .models import Currency, InvenTreeSetting - - -class CurrencyAdmin(ImportExportModelAdmin): - list_display = ('symbol', 'suffix', 'description', 'value', 'base') +from .models import InvenTreeSetting class SettingsAdmin(ImportExportModelAdmin): @@ -17,5 +13,4 @@ class SettingsAdmin(ImportExportModelAdmin): list_display = ('key', 'value') -admin.site.register(Currency, CurrencyAdmin) admin.site.register(InvenTreeSetting, SettingsAdmin) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index e1a9a0e3f0..8a2dfbd6a7 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -5,35 +5,5 @@ Provides a JSON API for common components. # -*- coding: utf-8 -*- from __future__ import unicode_literals -from rest_framework import permissions, generics, filters - -from django.conf.urls import url - -from .models import Currency -from .serializers import CurrencySerializer - - -class CurrencyList(generics.ListCreateAPIView): - """ API endpoint for accessing a list of Currency objects. - - - GET: Return a list of Currencies - - POST: Create a new currency - """ - - queryset = Currency.objects.all() - serializer_class = CurrencySerializer - - permission_classes = [ - permissions.IsAuthenticated, - ] - - filter_backends = [ - filters.OrderingFilter, - ] - - ordering_fields = ['suffix', 'value'] - - common_api_urls = [ - url(r'^currency/?$', CurrencyList.as_view(), name='api-currency-list'), ] diff --git a/InvenTree/common/apps.py b/InvenTree/common/apps.py index 06b825c574..34b43fc68b 100644 --- a/InvenTree/common/apps.py +++ b/InvenTree/common/apps.py @@ -1,92 +1,10 @@ +# -*- coding: utf-8 -*- + from django.apps import AppConfig -from django.db.utils import OperationalError, ProgrammingError, IntegrityError class CommonConfig(AppConfig): name = 'common' def ready(self): - - """ Will be called when the Common app is first loaded """ - self.add_instance_name() - self.add_default_settings() - - def add_instance_name(self): - """ - Check if an InstanceName has been defined for this database. - If not, create a random one! - """ - - # See note above - from .models import InvenTreeSetting - - """ - Note: The "old" instance name was stored under the key 'InstanceName', - but has now been renamed to 'INVENTREE_INSTANCE'. - """ - - try: - - # Quick exit if a value already exists for 'inventree_instance' - if InvenTreeSetting.objects.filter(key='INVENTREE_INSTANCE').exists(): - return - - # Default instance name - instance_name = InvenTreeSetting.get_default_value('INVENTREE_INSTANCE') - - # Use the old name if it exists - if InvenTreeSetting.objects.filter(key='InstanceName').exists(): - instance = InvenTreeSetting.objects.get(key='InstanceName') - instance_name = instance.value - - # Delete the legacy key - instance.delete() - - # Create new value - InvenTreeSetting.objects.create( - key='INVENTREE_INSTANCE', - value=instance_name - ) - - except (OperationalError, ProgrammingError, IntegrityError): - # Migrations have not yet been applied - table does not exist - pass - - def add_default_settings(self): - """ - Create all required settings, if they do not exist. - """ - - from .models import InvenTreeSetting - - for key in InvenTreeSetting.GLOBAL_SETTINGS.keys(): - try: - settings = InvenTreeSetting.objects.filter(key__iexact=key) - - if settings.count() == 0: - value = InvenTreeSetting.get_default_value(key) - - print(f"Creating default setting for {key} -> '{value}'") - - InvenTreeSetting.objects.create( - key=key, - value=value - ) - - return - - elif settings.count() > 1: - # Prevent multiple shadow copies of the same setting! - for setting in settings[1:]: - setting.delete() - - # Ensure that the key has the correct case - setting = settings[0] - - if not setting.key == key: - setting.key = key - setting.save() - - except (OperationalError, ProgrammingError, IntegrityError): - # Table might not yet exist - pass + pass diff --git a/InvenTree/common/fixtures/currency.yaml b/InvenTree/common/fixtures/currency.yaml deleted file mode 100644 index 639b0751df..0000000000 --- a/InvenTree/common/fixtures/currency.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Test fixtures for Currency objects - -- model: common.currency - fields: - symbol: '$' - suffix: 'AUD' - description: 'Australian Dollars' - base: True - -- model: common.currency - fields: - symbol: '$' - suffix: 'USD' - description: 'US Dollars' - base: False - value: 1.4 \ No newline at end of file diff --git a/InvenTree/common/fixtures/settings.yaml b/InvenTree/common/fixtures/settings.yaml new file mode 100644 index 0000000000..70ce23f312 --- /dev/null +++ b/InvenTree/common/fixtures/settings.yaml @@ -0,0 +1,13 @@ +# Sample settings objects + +- model: common.InvenTreeSetting + pk: 1 + fields: + key: INVENTREE_INSTANCE + value: "My very first InvenTree Instance" + +- model: common.InvenTreeSetting + pk: 2 + fields: + key: INVENTREE_COMPANY_NAME + value: "ACME Pty Ltd" \ No newline at end of file diff --git a/InvenTree/common/forms.py b/InvenTree/common/forms.py index ba6289221e..84e44f3a31 100644 --- a/InvenTree/common/forms.py +++ b/InvenTree/common/forms.py @@ -7,21 +7,7 @@ from __future__ import unicode_literals from InvenTree.forms import HelperForm -from .models import Currency, InvenTreeSetting - - -class CurrencyEditForm(HelperForm): - """ Form for creating / editing a currency object """ - - class Meta: - model = Currency - fields = [ - 'symbol', - 'suffix', - 'description', - 'value', - 'base' - ] +from .models import InvenTreeSetting class SettingEditForm(HelperForm): diff --git a/InvenTree/common/migrations/0009_delete_currency.py b/InvenTree/common/migrations/0009_delete_currency.py new file mode 100644 index 0000000000..3e2edce4c7 --- /dev/null +++ b/InvenTree/common/migrations/0009_delete_currency.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-11-10 11:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0027_remove_supplierpricebreak_currency'), + ('part', '0057_remove_partsellpricebreak_currency'), + ('common', '0008_remove_inventreesetting_description'), + ] + + operations = [ + migrations.DeleteModel( + name='Currency', + ), + ] diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 8254d161d6..53803dd94e 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -7,13 +7,18 @@ These models are 'generic' and do not fit a particular business logic object. from __future__ import unicode_literals import os -import decimal from django.db import models 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 + +from django.db.utils import OperationalError from django.utils.translation import ugettext as _ -from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError import InvenTree.helpers @@ -59,6 +64,13 @@ class InvenTreeSetting(models.Model): 'default': 'My company name', }, + 'INVENTREE_DEFAULT_CURRENCY': { + 'name': _('Default Currency'), + 'description': _('Default currency'), + 'default': 'USD', + 'choices': djmoney.settings.CURRENCY_CHOICES, + }, + 'PART_IPN_REGEX': { 'name': _('IPN Regex'), 'description': _('Regular expression pattern for matching Part IPN') @@ -92,6 +104,13 @@ class InvenTreeSetting(models.Model): 'validator': bool }, + 'PART_CATEGORY_PARAMETERS': { + 'name': _('Copy Category Parameter Templates'), + 'description': _('Copy category parameter templates when creating a part'), + 'default': True, + 'validator': bool + }, + 'PART_COMPONENT': { 'name': _('Component'), 'description': _('Parts can be used as sub-components by default'), @@ -226,6 +245,29 @@ class InvenTreeSetting(models.Model): else: return '' + @classmethod + def get_setting_choices(cls, key): + """ + Return the validator choices available for a particular setting. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + choices = setting.get('choices', None) + else: + choices = None + + """ + TODO: + if type(choices) is function: + # Evaluate the function (we expect it will return a list of tuples...) + return choices() + """ + + return choices + @classmethod def get_setting_object(cls, key): """ @@ -239,12 +281,20 @@ class InvenTreeSetting(models.Model): try: setting = InvenTreeSetting.objects.filter(key__iexact=key).first() - except (InvenTreeSetting.DoesNotExist): - # Create the setting if it does not exist - setting = InvenTreeSetting.create( - key=key, - value=InvenTreeSetting.get_default_value(key) - ) + except OperationalError: + # Settings table has not been created yet! + return None + except (ValueError, InvenTreeSetting.DoesNotExist): + + try: + # Attempt Create the setting if it does not exist + setting = InvenTreeSetting.create( + key=key, + value=InvenTreeSetting.get_default_value(key) + ) + except OperationalError: + # Settings table has not been created yet + setting = None return setting @@ -396,6 +446,13 @@ class InvenTreeSetting(models.Model): except InvenTreeSetting.DoesNotExist: pass + def choices(self): + """ + Return the available choices for this setting (or None if no choices are defined) + """ + + return InvenTreeSetting.get_setting_choices(self.key) + def is_bool(self): """ Check if this setting is required to be a boolean value @@ -415,74 +472,6 @@ class InvenTreeSetting(models.Model): return InvenTree.helpers.str2bool(self.value) -class Currency(models.Model): - """ - A Currency object represents a particular unit of currency. - Each Currency has a scaling factor which relates it to the base currency. - There must be one (and only one) currency which is selected as the base currency, - and each other currency is calculated relative to it. - - Attributes: - symbol: Currency symbol e.g. $ - suffix: Currency suffix e.g. AUD - description: Long-form description e.g. "Australian Dollars" - value: The value of this currency compared to the base currency. - base: True if this currency is the base currency - - """ - - symbol = models.CharField(max_length=10, blank=False, unique=False, help_text=_('Currency Symbol e.g. $')) - - suffix = models.CharField(max_length=10, blank=False, unique=True, help_text=_('Currency Suffix e.g. AUD')) - - description = models.CharField(max_length=100, blank=False, help_text=_('Currency Description')) - - value = models.DecimalField(default=1.0, max_digits=10, decimal_places=5, validators=[MinValueValidator(0.00001), MaxValueValidator(100000)], help_text=_('Currency Value')) - - base = models.BooleanField(default=False, help_text=_('Use this currency as the base currency')) - - class Meta: - verbose_name_plural = 'Currencies' - - def __str__(self): - """ Format string for currency representation """ - s = "{sym} {suf} - {desc}".format( - sym=self.symbol, - suf=self.suffix, - desc=self.description - ) - - if self.base: - s += " (Base)" - - else: - s += " = {v}".format(v=self.value) - - return s - - def save(self, *args, **kwargs): - """ Validate the model before saving - - - Ensure that there is only one base currency! - """ - - # If this currency is set as the base currency, ensure no others are - if self.base: - for cur in Currency.objects.filter(base=True).exclude(pk=self.pk): - cur.base = False - cur.save() - - # If there are no currencies set as the base currency, set this as base - if not Currency.objects.exclude(pk=self.pk).filter(base=True).exists(): - self.base = True - - # If this is the base currency, ensure value is set to unity - if self.base: - self.value = 1.0 - - super().save(*args, **kwargs) - - class PriceBreak(models.Model): """ Represents a PriceBreak model @@ -491,32 +480,39 @@ class PriceBreak(models.Model): class Meta: abstract = True - quantity = InvenTree.fields.RoundingDecimalField(max_digits=15, decimal_places=5, default=1, validators=[MinValueValidator(1)]) + quantity = InvenTree.fields.RoundingDecimalField( + max_digits=15, + decimal_places=5, + default=1, + validators=[MinValueValidator(1)], + verbose_name=_('Quantity'), + help_text=_('Price break quantity'), + ) - cost = InvenTree.fields.RoundingDecimalField(max_digits=10, decimal_places=5, validators=[MinValueValidator(0)]) + price = MoneyField( + max_digits=19, + decimal_places=4, + default_currency='USD', + null=True, + verbose_name=_('Price'), + help_text=_('Unit price at specified quantity'), + ) - currency = models.ForeignKey(Currency, blank=True, null=True, on_delete=models.SET_NULL) - - @property - def symbol(self): - return self.currency.symbol if self.currency else '' - - @property - def suffix(self): - return self.currency.suffix if self.currency else '' - - @property - def converted_cost(self): + def convert_to(self, currency_code): """ - Return the cost of this price break, converted to the base currency + Convert the unit-price at this price break to the specified currency code. + + Args: + currency_code - The currency code to convert to (e.g "USD" or "AUD") """ - scaler = decimal.Decimal(1.0) + try: + converted = convert_money(self.price, currency_code) + except MissingRate: + print(f"WARNING: No currency conversion rate available for {self.price_currency} -> {currency_code}") + return self.price.amount - if self.currency: - scaler = self.currency.value - - return self.cost * scaler + return converted.amount class ColorTheme(models.Model): diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 73b4da8adf..99ac03cdfd 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -1,22 +1,3 @@ """ JSON serializers for common components """ - -from .models import Currency - -from InvenTree.serializers import InvenTreeModelSerializer - - -class CurrencySerializer(InvenTreeModelSerializer): - """ Serializer for Currency object """ - - class Meta: - model = Currency - fields = [ - 'pk', - 'symbol', - 'suffix', - 'description', - 'value', - 'base' - ] diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py new file mode 100644 index 0000000000..832a07f040 --- /dev/null +++ b/InvenTree/common/settings.py @@ -0,0 +1,23 @@ +""" +User-configurable settings for the common app +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from moneyed import CURRENCIES + +from common.models import InvenTreeSetting + + +def currency_code_default(): + """ + Returns the default currency code (or USD if not specified) + """ + + code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + + if code not in CURRENCIES: + code = 'USD' + + return code diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 323049f164..4666a0a5a6 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -4,20 +4,7 @@ from __future__ import unicode_literals from django.test import TestCase from django.contrib.auth import get_user_model -from .models import Currency, InvenTreeSetting - - -class CurrencyTest(TestCase): - """ Tests for Currency model """ - - fixtures = [ - 'currency', - ] - - def test_currency(self): - # Simple test for now (improve this later!) - - self.assertEqual(Currency.objects.count(), 2) +from .models import InvenTreeSetting class SettingsTest(TestCase): @@ -25,16 +12,34 @@ class SettingsTest(TestCase): Tests for the 'settings' model """ + fixtures = [ + 'settings', + ] + def setUp(self): - User = get_user_model() + user = get_user_model() - self.user = User.objects.create_user('username', 'user@email.com', 'password') + self.user = user.objects.create_user('username', 'user@email.com', 'password') self.user.is_staff = True self.user.save() self.client.login(username='username', password='password') + def test_settings_objects(self): + + # There should be two settings objects in the database + settings = InvenTreeSetting.objects.all() + + self.assertEqual(settings.count(), 2) + + instance_name = InvenTreeSetting.objects.get(pk=1) + self.assertEqual(instance_name.key, 'INVENTREE_INSTANCE') + self.assertEqual(instance_name.value, 'My very first InvenTree Instance') + + # Check object lookup (case insensitive) + self.assertEqual(InvenTreeSetting.get_setting_object('iNvEnTrEE_inSTanCE').pk, 1) + def test_required_values(self): """ - Ensure that every global setting has a name. diff --git a/InvenTree/common/urls.py b/InvenTree/common/urls.py index b5d6deadde..261ea1a691 100644 --- a/InvenTree/common/urls.py +++ b/InvenTree/common/urls.py @@ -2,17 +2,5 @@ URL lookup for common views """ -from django.conf.urls import url, include - -from . import views - -currency_urls = [ - url(r'^new/', views.CurrencyCreate.as_view(), name='currency-create'), - - url(r'^(?P\d+)/edit/', views.CurrencyEdit.as_view(), name='currency-edit'), - url(r'^(?P\d+)/delete/', views.CurrencyDelete.as_view(), name='currency-delete'), -] - common_urls = [ - url(r'currency/', include(currency_urls)), ] diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index cfcee8bfa9..3bf3769231 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -6,39 +6,15 @@ Django views for interacting with common models from __future__ import unicode_literals from django.utils.translation import ugettext as _ -from django.forms import CheckboxInput +from django.forms import CheckboxInput, Select -from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView +from InvenTree.views import AjaxUpdateView from InvenTree.helpers import str2bool from . import models from . import forms -class CurrencyCreate(AjaxCreateView): - """ View for creating a new Currency object """ - - model = models.Currency - form_class = forms.CurrencyEditForm - ajax_form_title = _('Create new Currency') - - -class CurrencyEdit(AjaxUpdateView): - """ View for editing an existing Currency object """ - - model = models.Currency - form_class = forms.CurrencyEditForm - ajax_form_title = _('Edit Currency') - - -class CurrencyDelete(AjaxDeleteView): - """ View for deleting an existing Currency object """ - - model = models.Currency - ajax_form_title = _('Delete Currency') - ajax_template_name = "common/delete_currency.html" - - class SettingEdit(AjaxUpdateView): """ View for editing an InvenTree key:value settings object, @@ -75,7 +51,11 @@ class SettingEdit(AjaxUpdateView): setting = self.get_object() - if setting.is_bool(): + choices = setting.choices() + + if choices is not None: + form.fields['value'].widget = Select(choices=choices) + elif setting.is_bool(): form.fields['value'].widget = CheckboxInput() self.object.value = str2bool(setting.value) diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index 4a802c6a41..45dd769d67 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -13,7 +13,6 @@ from .models import SupplierPart from .models import SupplierPriceBreak from part.models import Part -from common.models import Currency class CompanyResource(ModelResource): @@ -75,8 +74,6 @@ class SupplierPriceBreakResource(ModelResource): part = Field(attribute='part', widget=widgets.ForeignKeyWidget(SupplierPart)) - currency = Field(attribute='currency', widget=widgets.ForeignKeyWidget(Currency)) - supplier_id = Field(attribute='part__supplier__pk', readonly=True) supplier_name = Field(attribute='part__supplier__name', readonly=True) @@ -98,7 +95,7 @@ class SupplierPriceBreakAdmin(ImportExportModelAdmin): resource_class = SupplierPriceBreakResource - list_display = ('part', 'quantity', 'cost') + list_display = ('part', 'quantity', 'price') admin.site.register(Company, CompanyAdmin) diff --git a/InvenTree/company/fixtures/price_breaks.yaml b/InvenTree/company/fixtures/price_breaks.yaml index 6ae8cce94c..dbcbacb017 100644 --- a/InvenTree/company/fixtures/price_breaks.yaml +++ b/InvenTree/company/fixtures/price_breaks.yaml @@ -7,21 +7,21 @@ fields: part: 1 quantity: 1 - cost: 10 + price: 10 - model: company.supplierpricebreak pk: 2 fields: part: 1 quantity: 5 - cost: 7.50 + price: 7.50 - model: company.supplierpricebreak pk: 3 fields: part: 1 quantity: 25 - cost: 3.50 + price: 3.50 # Price breaks for ACME0002 - model: company.supplierpricebreak @@ -29,14 +29,14 @@ fields: part: 2 quantity: 5 - cost: 7.00 + price: 7.00 - model: company.supplierpricebreak pk: 5 fields: part: 2 quantity: 50 - cost: 1.25 + price: 1.25 # Price breaks for ZERGLPHS - model: company.supplierpricebreak @@ -44,11 +44,11 @@ fields: part: 4 quantity: 25 - cost: 8 + price: 8 - model: company.supplierpricebreak pk: 7 fields: part: 4 quantity: 100 - cost: 1.25 \ No newline at end of file + price: 1.25 \ No newline at end of file diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index ac3cc69c99..0ad95c3e8c 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -8,6 +8,14 @@ from __future__ import unicode_literals from InvenTree.forms import HelperForm from InvenTree.fields import RoundingDecimalFormField +from django.utils.translation import ugettext as _ +import django.forms + +import djmoney.settings +from djmoney.forms.fields import MoneyField + +import common.settings + from .models import Company from .models import SupplierPart from .models import SupplierPriceBreak @@ -24,6 +32,13 @@ class EditCompanyForm(HelperForm): 'phone': 'fa-phone', } + currency = django.forms.ChoiceField( + required=False, + help_text=_('Default currency used for this company'), + choices=[('', '----------')] + djmoney.settings.CURRENCY_CHOICES, + initial=common.settings.currency_code_default, + ) + class Meta: model = Company fields = [ @@ -31,6 +46,7 @@ class EditCompanyForm(HelperForm): 'description', 'website', 'address', + 'currency', 'phone', 'email', 'contact', @@ -60,6 +76,15 @@ class EditSupplierPartForm(HelperForm): 'note': 'fa-pencil-alt', } + single_pricing = MoneyField( + label=_('Single Price'), + default_currency='USD', + help_text=_('Single quantity price'), + decimal_places=4, + max_digits=19, + required=False, + ) + class Meta: model = SupplierPart fields = [ @@ -71,8 +96,9 @@ class EditSupplierPartForm(HelperForm): 'MPN', 'link', 'note', - 'base_cost', - 'multiple', + 'single_pricing', + # 'base_cost', + # 'multiple', 'packaging', ] @@ -80,15 +106,17 @@ class EditSupplierPartForm(HelperForm): class EditPriceBreakForm(HelperForm): """ Form for creating / editing a supplier price break """ - quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5) - - cost = RoundingDecimalFormField(max_digits=10, decimal_places=5) + quantity = RoundingDecimalFormField( + max_digits=10, + decimal_places=5, + label=_('Quantity'), + help_text=_('Price break quantity'), + ) class Meta: model = SupplierPriceBreak fields = [ 'part', 'quantity', - 'cost', - 'currency', + 'price', ] diff --git a/InvenTree/company/migrations/0025_auto_20201110_1001.py b/InvenTree/company/migrations/0025_auto_20201110_1001.py new file mode 100644 index 0000000000..0fd62c0a23 --- /dev/null +++ b/InvenTree/company/migrations/0025_auto_20201110_1001.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.7 on 2020-11-10 10:01 + +from django.db import migrations, connection +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0024_unique_name_email_constraint'), + ] + + operations = [ + migrations.AddField( + model_name='supplierpricebreak', + name='price', + field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='USD', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'), + ), + migrations.AddField( + model_name='supplierpricebreak', + name='price_currency', + field=djmoney.models.fields.CurrencyField(choices=[('AUD', 'Australian Dollar'), ('CAD', 'Canadian Dollar'), ('EUR', 'Euro'), ('NZD', 'New Zealand Dollar'), ('GBP', 'Pound Sterling'), ('USD', 'US Dollar'), ('JPY', 'Yen')], default='USD', editable=False, max_length=3), + ), + ] diff --git a/InvenTree/company/migrations/0026_auto_20201110_1011.py b/InvenTree/company/migrations/0026_auto_20201110_1011.py new file mode 100644 index 0000000000..24fac6f52d --- /dev/null +++ b/InvenTree/company/migrations/0026_auto_20201110_1011.py @@ -0,0 +1,147 @@ +# Generated by Django 3.0.7 on 2020-11-10 10:11 + +import sys + +from moneyed import CURRENCIES +from django.db import migrations, connection +from company.models import SupplierPriceBreak + + +def migrate_currencies(apps, schema_editor): + """ + Migrate from the 'old' method of handling currencies, + to the new method which uses the django-money library. + + Previously, we created a custom Currency model, + which was very simplistic. + + Here we will attempt to map each existing "currency" reference + for the SupplierPriceBreak model, to a new django-money compatible currency. + """ + + print("Updating currency references for SupplierPriceBreak model...") + + # A list of available currency codes + currency_codes = CURRENCIES.keys() + + cursor = connection.cursor() + + # The 'suffix' field denotes the currency code + response = cursor.execute('SELECT id, suffix, description from common_currency;') + + results = cursor.fetchall() + + remap = {} + + for index, row in enumerate(results): + pk, suffix, description = row + + suffix = suffix.strip().upper() + + if suffix not in currency_codes: + print("Missing suffix:", suffix) + + while suffix not in currency_codes: + # Ask the user to input a valid currency + print(f"Could not find a valid currency matching '{suffix}'.") + print("Please enter a valid currency code") + suffix = str(input("> ")).strip() + + if pk not in remap.keys(): + remap[pk] = suffix + + # Now iterate through each SupplierPriceBreak and update the rows + response = cursor.execute('SELECT id, cost, currency_id, price, price_currency from part_supplierpricebreak;') + + results = cursor.fetchall() + + count = 0 + + for index, row in enumerate(results): + pk, cost, currency_id, price, price_currency = row + + # Copy the 'cost' field across to the 'price' field + response = cursor.execute(f'UPDATE part_supplierpricebreak set price={cost} where id={pk};') + + # Extract the updated currency code + currency_code = remap.get(currency_id, 'USD') + + # Update the currency code + response = cursor.execute(f'UPDATE part_supplierpricebreak set price_currency= "{currency_code}" where id={pk};') + + count += 1 + + print(f"Updated {count} SupplierPriceBreak rows") + +def reverse_currencies(apps, schema_editor): + """ + Reverse the "update" process. + + Here we may be in the situation that the legacy "Currency" table is empty, + and so we have to re-populate it based on the new price_currency codes. + """ + + print("Reversing currency migration...") + + cursor = connection.cursor() + + # Extract a list of currency codes which are in use + response = cursor.execute(f'SELECT id, price, price_currency from part_supplierpricebreak;') + + results = cursor.fetchall() + + codes_in_use = set() + + for index, row in enumerate(results): + pk, price, code = row + + codes_in_use.add(code) + + # Copy the 'price' field back into the 'cost' field + response = cursor.execute(f'UPDATE part_supplierpricebreak set cost={price} where id={pk};') + + # Keep a dict of which currency objects map to which code + code_map = {} + + # For each currency code in use, check if we have a matching Currency object + for code in codes_in_use: + response = cursor.execute(f'SELECT id, suffix from common_currency where suffix="{code}";') + row = response.fetchone() + + if row is not None: + # A match exists! + pk, suffix = row + code_map[suffix] = pk + else: + # No currency object exists! + description = CURRENCIES[code] + + # Create a new object in the database + print(f"Creating new Currency object for {code}") + + # Construct a query to create a new Currency object + query = f'INSERT into common_currency (symbol, suffix, description, value, base) VALUES ("$", "{code}", "{description}", 1.0, False);' + + response = cursor.execute(query) + + code_map[code] = cursor.lastrowid + + # Ok, now we know how each suffix maps to a Currency object + for suffix in code_map.keys(): + pk = code_map[suffix] + + # Update the table to point to the Currency objects + print(f"Currency {suffix} -> pk {pk}") + + response = cursor.execute(f'UPDATE part_supplierpricebreak set currency_id={pk} where price_currency="{suffix}";') + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0025_auto_20201110_1001'), + ] + + operations = [ + migrations.RunPython(migrate_currencies, reverse_code=reverse_currencies), + ] diff --git a/InvenTree/company/migrations/0027_remove_supplierpricebreak_currency.py b/InvenTree/company/migrations/0027_remove_supplierpricebreak_currency.py new file mode 100644 index 0000000000..b2e23d7538 --- /dev/null +++ b/InvenTree/company/migrations/0027_remove_supplierpricebreak_currency.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.7 on 2020-11-10 11:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0026_auto_20201110_1011'), + ] + + operations = [ + migrations.RemoveField( + model_name='supplierpricebreak', + name='currency', + ), + ] diff --git a/InvenTree/company/migrations/0028_remove_supplierpricebreak_cost.py b/InvenTree/company/migrations/0028_remove_supplierpricebreak_cost.py new file mode 100644 index 0000000000..0522560f26 --- /dev/null +++ b/InvenTree/company/migrations/0028_remove_supplierpricebreak_cost.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.7 on 2020-11-10 11:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0027_remove_supplierpricebreak_currency'), + ] + + operations = [ + migrations.RemoveField( + model_name='supplierpricebreak', + name='cost', + ), + ] diff --git a/InvenTree/company/migrations/0029_company_currency.py b/InvenTree/company/migrations/0029_company_currency.py new file mode 100644 index 0000000000..fda3c15e96 --- /dev/null +++ b/InvenTree/company/migrations/0029_company_currency.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-11-11 23:22 + +import InvenTree.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0028_remove_supplierpricebreak_cost'), + ] + + operations = [ + migrations.AddField( + model_name='company', + name='currency', + field=models.CharField(blank=True, help_text='Default currency used for this company', max_length=3, validators=[InvenTree.validators.validate_currency_code], verbose_name='Currency'), + ), + ] diff --git a/InvenTree/company/migrations/0030_auto_20201112_1112.py b/InvenTree/company/migrations/0030_auto_20201112_1112.py new file mode 100644 index 0000000000..367ed303e5 --- /dev/null +++ b/InvenTree/company/migrations/0030_auto_20201112_1112.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.7 on 2020-11-12 00:12 + +import InvenTree.fields +import django.core.validators +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('company', '0029_company_currency'), + ] + + operations = [ + migrations.AlterField( + model_name='supplierpricebreak', + name='quantity', + field=InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Price break quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Quantity'), + ), + ] diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index b9fed2ee7b..81718a9acd 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -17,6 +17,8 @@ from django.db.models import Sum, Q, UniqueConstraint from django.apps import apps from django.urls import reverse +from moneyed import CURRENCIES + from markdownx.models import MarkdownxField from stdimage.models import StdImageField @@ -26,7 +28,10 @@ from InvenTree.helpers import normalize from InvenTree.fields import InvenTreeURLField from InvenTree.status_codes import PurchaseOrderStatus +import InvenTree.validators + import common.models +import common.settings def rename_company_image(instance, filename): @@ -77,6 +82,7 @@ class Company(models.Model): is_customer: boolean value, is this company a customer is_supplier: boolean value, is this company a supplier is_manufacturer: boolean value, is this company a manufacturer + currency_code: Specifies the default currency for the company """ class Meta: @@ -126,6 +132,30 @@ class Company(models.Model): is_manufacturer = models.BooleanField(default=False, help_text=_('Does this company manufacture parts?')) + currency = models.CharField( + max_length=3, + verbose_name=_('Currency'), + blank=True, + help_text=_('Default currency used for this company'), + validators=[InvenTree.validators.validate_currency_code], + ) + + @property + def currency_code(self): + """ + Return the currency code associated with this company. + + - If the currency code is invalid, use the default currency + - If the currency code is not specified, use the default currency + """ + + code = self.currency + + if code not in CURRENCIES: + code = common.settings.currency_code_default() + + return code + def __str__(self): """ Get string representation of a Company """ return "{n} - {d}".format(n=self.name, d=self.description) @@ -350,7 +380,26 @@ class SupplierPart(models.Model): def unit_pricing(self): return self.get_price(1) - def get_price(self, quantity, moq=True, multiples=True): + def add_price_break(self, quantity, price): + """ + Create a new price break for this part + + args: + quantity - Numerical quantity + price - Must be a Money object + """ + + # Check if a price break at that quantity already exists... + if self.price_breaks.filter(quantity=quantity, part=self.pk).exists(): + return + + SupplierPriceBreak.objects.create( + part=self, + quantity=quantity, + price=price + ) + + def get_price(self, quantity, moq=True, multiples=True, currency=None): """ Calculate the supplier price based on quantity price breaks. - Don't forget to add in flat-fee cost (base_cost field) @@ -372,6 +421,10 @@ class SupplierPart(models.Model): pb_quantity = -1 pb_cost = 0.0 + if currency is None: + # Default currency selection + currency = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + for pb in self.price_breaks.all(): # Ignore this pricebreak (quantity is too high) if pb.quantity > quantity: @@ -382,8 +435,9 @@ class SupplierPart(models.Model): # If this price-break quantity is the largest so far, use it! if pb.quantity > pb_quantity: pb_quantity = pb.quantity - # Convert everything to base currency - pb_cost = pb.converted_cost + + # Convert everything to the selected currency + pb_cost = pb.convert_to(currency) if pb_found: cost = pb_cost * quantity @@ -462,7 +516,4 @@ class SupplierPriceBreak(common.models.PriceBreak): db_table = 'part_supplierpricebreak' def __str__(self): - return "{mpn} - {cost} @ {quan}".format( - mpn=self.part.MPN, - cost=self.cost, - quan=self.quantity) + return f'{self.part.MPN} - {self.price} @ {self.quantity}' diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index f6de7d4f50..4951bd3ad0 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -137,13 +137,9 @@ class SupplierPartSerializer(InvenTreeModelSerializer): class SupplierPriceBreakSerializer(InvenTreeModelSerializer): """ Serializer for SupplierPriceBreak object """ - symbol = serializers.CharField(read_only=True) - - suffix = serializers.CharField(read_only=True) - quantity = serializers.FloatField() - cost = serializers.FloatField() + price = serializers.CharField() class Meta: model = SupplierPriceBreak @@ -151,8 +147,5 @@ class SupplierPriceBreakSerializer(InvenTreeModelSerializer): 'pk', 'part', 'quantity', - 'cost', - 'currency', - 'symbol', - 'suffix', + 'price', ] diff --git a/InvenTree/company/templates/company/detail.html b/InvenTree/company/templates/company/detail.html index a166de9048..7243cf4fbc 100644 --- a/InvenTree/company/templates/company/detail.html +++ b/InvenTree/company/templates/company/detail.html @@ -8,25 +8,64 @@

{% trans "Company Details" %}


- - - - - - - - - - - - - - - - - - -
{% trans "Manufacturer" %}{% include "yesnolabel.html" with value=company.is_manufacturer %}
{% trans "Supplier" %}{% include 'yesnolabel.html' with value=company.is_supplier %}
{% trans "Customer" %}{% include 'yesnolabel.html' with value=company.is_customer %}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Company Name" %}{{ company.name }}
{% trans "Description" %}{{ company.description }}
{% trans "Website" %} + {% if company.website %}{{ company.website }} + {% else %}{% trans "No website specified" %} + {% endif %} +
{% trans "Currency" %} + {% if company.currency %}{{ company.currency }} + {% else %}{% trans "Uses default currency" %} + {% endif %} +
+
+
+ + + + + + + + + + + + + + + + + + +
{% trans "Manufacturer" %}{% include "yesnolabel.html" with value=company.is_manufacturer %}
{% trans "Supplier" %}{% include 'yesnolabel.html' with value=company.is_supplier %}
{% trans "Customer" %}{% include 'yesnolabel.html' with value=company.is_customer %}
+ +
+
{% endblock %} {% block js_ready %} diff --git a/InvenTree/company/templates/company/supplier_part_pricing.html b/InvenTree/company/templates/company/supplier_part_pricing.html index 6138669bc4..e665339968 100644 --- a/InvenTree/company/templates/company/supplier_part_pricing.html +++ b/InvenTree/company/templates/company/supplier_part_pricing.html @@ -76,18 +76,11 @@ $('#price-break-table').inventreeTable({ sortable: true, }, { - field: 'cost', + field: 'price', title: '{% trans "Price" %}', sortable: true, formatter: function(value, row, index) { - var html = ''; - - html += row.symbol || ''; - html += value; - - if (row.suffix) { - html += ' ' + row.suffix || ''; - } + var html = value; html += `
` diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py index 643608542d..f466a4a223 100644 --- a/InvenTree/company/test_api.py +++ b/InvenTree/company/test_api.py @@ -15,8 +15,8 @@ class CompanyTest(APITestCase): def setUp(self): # Create a user for auth - User = get_user_model() - self.user = User.objects.create_user('testuser', 'test@testing.com', 'password') + user = get_user_model() + self.user = user.objects.create_user('testuser', 'test@testing.com', 'password') perms = [ 'view_company', diff --git a/InvenTree/company/test_views.py b/InvenTree/company/test_views.py index d895c18957..0163e65c29 100644 --- a/InvenTree/company/test_views.py +++ b/InvenTree/company/test_views.py @@ -3,6 +3,8 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import json + from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model @@ -11,7 +13,7 @@ from django.contrib.auth.models import Group from .models import SupplierPart -class CompanyViewTest(TestCase): +class CompanyViewTestBase(TestCase): fixtures = [ 'category', @@ -25,8 +27,9 @@ class CompanyViewTest(TestCase): super().setUp() # Create a user - User = get_user_model() - self.user = User.objects.create_user( + user = get_user_model() + + self.user = user.objects.create_user( username='username', email='user@email.com', password='password' @@ -47,14 +50,104 @@ class CompanyViewTest(TestCase): self.client.login(username='username', password='password') - def test_company_index(self): - """ Test the company index """ + def post(self, url, data, valid=None): + """ + POST against this form and return the response (as a JSON object) + """ + + response = self.client.post(url, data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - response = self.client.get(reverse('company-index')) self.assertEqual(response.status_code, 200) + json_data = json.loads(response.content) + + # If a particular status code is required + if valid is not None: + if valid: + self.assertEqual(json_data['form_valid'], True) + else: + self.assertEqual(json_data['form_valid'], False) + + form_errors = json.loads(json_data['form_errors']) + + return json_data, form_errors + + +class SupplierPartViewTests(CompanyViewTestBase): + """ + Tests for the SupplierPart views. + """ + + def test_supplier_part_create(self): + """ + Test the SupplierPartCreate view. + + This view allows some additional functionality, + specifically it allows the user to create a single-quantity price break + automatically, when saving the new SupplierPart model. + """ + + url = reverse('supplier-part-create') + + # First check that we can GET the form + response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + # How many supplier parts are already in the database? + n = SupplierPart.objects.all().count() + + data = { + 'part': 1, + 'supplier': 1, + } + + # SKU is required! (form should fail) + (response, errors) = self.post(url, data, valid=False) + + self.assertIsNotNone(errors.get('SKU', None)) + + data['SKU'] = 'TEST-ME-123' + + (response, errors) = self.post(url, data, valid=True) + + # Check that the SupplierPart was created! + self.assertEqual(n + 1, SupplierPart.objects.all().count()) + + # Check that it was created *without* a price-break + supplier_part = SupplierPart.objects.get(pk=response['pk']) + + self.assertEqual(supplier_part.price_breaks.count(), 0) + + # Duplicate SKU is prohibited + (response, errors) = self.post(url, data, valid=False) + + self.assertIsNotNone(errors.get('__all__', None)) + + # Add with a different SKU, *and* a single-quantity price + data['SKU'] = 'TEST-ME-1234' + data['single_pricing_0'] = '123.4' + data['single_pricing_1'] = 'CAD' + + (response, errors) = self.post(url, data, valid=True) + + pk = response.get('pk') + + # Check that *another* SupplierPart was created + self.assertEqual(n + 2, SupplierPart.objects.all().count()) + + supplier_part = SupplierPart.objects.get(pk=pk) + + # Check that a price-break has been created! + self.assertEqual(supplier_part.price_breaks.count(), 1) + + price_break = supplier_part.price_breaks.first() + + self.assertEqual(price_break.quantity, 1) + def test_supplier_part_delete(self): - """ Test the SupplierPartDelete view """ + """ + Test the SupplierPartDelete view + """ url = reverse('supplier-part-delete') @@ -80,3 +173,30 @@ class CompanyViewTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(n - 2, SupplierPart.objects.count()) + + +class CompanyViewTest(CompanyViewTestBase): + """ + Tests for various 'Company' views + """ + + def test_company_index(self): + """ Test the company index """ + + response = self.client.get(reverse('company-index')) + self.assertEqual(response.status_code, 200) + + def test_company_create(self): + """ + Test the view for creating a company + """ + + # Check that different company types return different form titles + response = self.client.get(reverse('supplier-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertContains(response, 'Create new Supplier') + + response = self.client.get(reverse('manufacturer-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertContains(response, 'Create new Manufacturer') + + response = self.client.get(reverse('customer-create'), HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertContains(response, 'Create new Customer') diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index db515d3e59..5229fdb045 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -1,4 +1,5 @@ from django.test import TestCase +from django.core.exceptions import ValidationError import os @@ -6,6 +7,9 @@ from .models import Company, Contact, 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): @@ -32,6 +36,14 @@ class CompanySimpleTest(TestCase): self.zerglphs = SupplierPart.objects.get(SKU='ZERGLPHS') self.zergm312 = SupplierPart.objects.get(SKU='ZERGM312') + InvenTreeManualExchangeBackend().update_rates() + + Rate.objects.create( + currency='AUD', + value='1.35', + backend_id='inventree', + ) + def test_company_model(self): c = Company.objects.get(name='ABC Co.') self.assertEqual(c.name, 'ABC Co.') @@ -108,6 +120,30 @@ class CompanySimpleTest(TestCase): self.assertIsNone(m3x12.get_price_info(3)) self.assertIsNotNone(m3x12.get_price_info(50)) + def test_currency_validation(self): + """ + Test validation for currency selection + """ + + # Create a company with a valid currency code (should pass) + company = Company.objects.create( + name='Test', + description='Toast', + currency='AUD', + ) + + company.full_clean() + + # Create a company with an invalid currency code (should fail) + company = Company.objects.create( + name='test', + description='Toasty', + currency='XZY', + ) + + with self.assertRaises(ValidationError): + company.full_clean() + class ContactSimpleTest(TestCase): diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index dce341d184..2f734a7cc5 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -12,12 +12,12 @@ from django.views.generic import DetailView, ListView, UpdateView from django.urls import reverse from django.forms import HiddenInput +from moneyed import CURRENCIES + from InvenTree.views import AjaxCreateView, AjaxUpdateView, AjaxDeleteView from InvenTree.helpers import str2bool from InvenTree.views import InvenTreeRoleMixin -from common.models import Currency - from .models import Company from .models import SupplierPart from .models import SupplierPriceBreak @@ -29,6 +29,9 @@ from .forms import CompanyImageForm from .forms import EditSupplierPartForm from .forms import EditPriceBreakForm +import common.models +import common.settings + class CompanyIndex(InvenTreeRoleMixin, ListView): """ View for displaying list of companies @@ -268,6 +271,14 @@ class SupplierPartEdit(AjaxUpdateView): ajax_form_title = _('Edit Supplier Part') role_required = 'purchase_order.change' + def get_form(self): + form = super().get_form() + + # Hide the single-pricing field (only for creating a new SupplierPart!) + form.fields['single_pricing'].widget = HiddenInput() + + return form + class SupplierPartCreate(AjaxCreateView): """ Create view for making new SupplierPart """ @@ -279,6 +290,30 @@ class SupplierPartCreate(AjaxCreateView): context_object_name = 'part' role_required = 'purchase_order.add' + def validate(self, part, form): + + single_pricing = form.cleaned_data.get('single_pricing', None) + + if single_pricing: + # TODO - What validation steps can be performed on the single_pricing field? + pass + + def save(self, form): + """ + If single_pricing is defined, add a price break for quantity=1 + """ + + # Save the supplier part object + supplier_part = super().save(form) + + single_pricing = form.cleaned_data.get('single_pricing', None) + + if single_pricing: + + supplier_part.add_price_break(1, single_pricing) + + return supplier_part + def get_form(self): """ Create Form instance to create a new SupplierPart object. Hide some fields if they are not appropriate in context @@ -303,11 +338,14 @@ class SupplierPartCreate(AjaxCreateView): supplier_id = self.get_param('supplier') part_id = self.get_param('part') + supplier = None + if supplier_id: try: - initials['supplier'] = Company.objects.get(pk=supplier_id) + supplier = Company.objects.get(pk=supplier_id) + initials['supplier'] = supplier except (ValueError, Company.DoesNotExist): - pass + supplier = None if manufacturer_id: try: @@ -320,6 +358,17 @@ class SupplierPartCreate(AjaxCreateView): initials['part'] = Part.objects.get(pk=part_id) except (ValueError, Part.DoesNotExist): pass + + # Initial value for single pricing + if supplier: + currency_code = supplier.currency_code + else: + currency_code = common.settings.currency_code_default() + + currency = CURRENCIES.get(currency_code, None) + + if currency_code: + initials['single_pricing'] = ('', currency) return initials @@ -417,10 +466,23 @@ class PriceBreakCreate(AjaxCreateView): } def get_part(self): + """ + Attempt to extract SupplierPart object from the supplied data. + """ + try: - return SupplierPart.objects.get(id=self.request.GET.get('part')) - except SupplierPart.DoesNotExist: - return SupplierPart.objects.get(id=self.request.POST.get('part')) + supplier_part = SupplierPart.objects.get(pk=self.request.GET.get('part')) + return supplier_part + except (ValueError, SupplierPart.DoesNotExist): + pass + + try: + supplier_part = SupplierPart.objects.get(pk=self.request.POST.get('part')) + return supplier_part + except (ValueError, SupplierPart.DoesNotExist): + pass + + return None def get_form(self): @@ -433,14 +495,20 @@ class PriceBreakCreate(AjaxCreateView): initials = super(AjaxCreateView, self).get_initial() + supplier_part = self.get_part() + initials['part'] = self.get_part() - # Pre-select the default currency - try: - base = Currency.objects.get(base=True) - initials['currency'] = base - except Currency.DoesNotExist: - pass + if supplier_part is not None: + currency_code = supplier_part.supplier.currency_code + else: + currency_code = common.settings.currency_code_default() + + # Extract the currency object associated with the code + currency = CURRENCIES.get(currency_code, None) + + if currency: + initials['price'] = [1.0, currency] return initials diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index ecd0a241cf..310fedd70e 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -26,6 +26,17 @@ language: en-us # Select an option from the "TZ database name" column timezone: UTC +# List of currencies supported by default. +# Add other currencies here to allow use in InvenTree +currencies: + - AUD + - CAD + - EUR + - GBP + - JPY + - NZD + - USD + # Set debug to False to run in production mode debug: True diff --git a/InvenTree/locale/de/LC_MESSAGES/django.mo b/InvenTree/locale/de/LC_MESSAGES/django.mo index ccc97446ba..4d57b3c49d 100644 Binary files a/InvenTree/locale/de/LC_MESSAGES/django.mo and b/InvenTree/locale/de/LC_MESSAGES/django.mo differ diff --git a/InvenTree/locale/de/LC_MESSAGES/django.po b/InvenTree/locale/de/LC_MESSAGES/django.po index 00c43092f0..e7f273efc2 100644 --- a/InvenTree/locale/de/LC_MESSAGES/django.po +++ b/InvenTree/locale/de/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-11-09 12:47+0000\n" +"POT-Creation-Date: 2020-11-12 22:05+1100\n" "PO-Revision-Date: 2020-05-03 11:32+0200\n" "Last-Translator: Christian Schlüter \n" "Language-Team: C \n" @@ -25,31 +25,37 @@ msgstr "Keine Aktion angegeben" msgid "No matching action found" msgstr "Keine passende Aktion gefunden" -#: InvenTree/forms.py:130 build/forms.py:82 build/forms.py:170 +#: InvenTree/forms.py:108 build/forms.py:82 build/forms.py:170 msgid "Confirm" msgstr "Bestätigen" -#: InvenTree/forms.py:146 +#: InvenTree/forms.py:124 #, fuzzy #| msgid "Confim BOM item deletion" msgid "Confirm item deletion" msgstr "Löschung von BOM-Position bestätigen" -#: InvenTree/forms.py:178 +#: InvenTree/forms.py:156 #, fuzzy #| msgid "Create new part" msgid "Enter new password" msgstr "Neues Teil anlegen" -#: InvenTree/forms.py:185 +#: InvenTree/forms.py:163 msgid "Confirm new password" msgstr "" -#: InvenTree/forms.py:220 +#: InvenTree/forms.py:198 msgid "Apply Theme" msgstr "" -#: InvenTree/helpers.py:361 order/models.py:187 order/models.py:269 +#: InvenTree/forms.py:228 +#, fuzzy +#| msgid "Set Part Category" +msgid "Select Category" +msgstr "Teilkategorie auswählen" + +#: InvenTree/helpers.py:361 order/models.py:189 order/models.py:271 msgid "Invalid quantity provided" msgstr "Keine gültige Menge" @@ -107,19 +113,19 @@ msgstr "Name" msgid "Description (optional)" msgstr "Firmenbeschreibung" -#: InvenTree/settings.py:348 +#: InvenTree/settings.py:354 msgid "English" msgstr "Englisch" -#: InvenTree/settings.py:349 +#: InvenTree/settings.py:355 msgid "German" msgstr "Deutsch" -#: InvenTree/settings.py:350 +#: InvenTree/settings.py:356 msgid "French" msgstr "Französisch" -#: InvenTree/settings.py:351 +#: InvenTree/settings.py:357 msgid "Polish" msgstr "Polnisch" @@ -182,67 +188,71 @@ msgstr "" msgid "Production" msgstr "Standort" -#: InvenTree/validators.py:39 +#: InvenTree/validators.py:22 +msgid "Not a valid currency code" +msgstr "" + +#: InvenTree/validators.py:50 msgid "Invalid character in part name" msgstr "Ungültiger Buchstabe im Teilenamen" -#: InvenTree/validators.py:52 +#: InvenTree/validators.py:63 msgid "IPN must match regex pattern" msgstr "IPN muss zu Regex-Muster passen" -#: InvenTree/validators.py:66 InvenTree/validators.py:80 -#: InvenTree/validators.py:94 +#: InvenTree/validators.py:77 InvenTree/validators.py:91 +#: InvenTree/validators.py:105 #, fuzzy #| msgid "IPN must match regex pattern" msgid "Reference must match pattern" msgstr "IPN muss zu Regex-Muster passen" -#: InvenTree/validators.py:102 +#: InvenTree/validators.py:113 #, python-brace-format msgid "Illegal character in name ({x})" msgstr "Ungültiges Zeichen im Namen ({x})" -#: InvenTree/validators.py:121 InvenTree/validators.py:137 +#: InvenTree/validators.py:132 InvenTree/validators.py:148 msgid "Overage value must not be negative" msgstr "Überschuss-Wert darf nicht negativ sein" -#: InvenTree/validators.py:139 +#: InvenTree/validators.py:150 msgid "Overage must not exceed 100%" msgstr "Überschuss darf 100% nicht überschreiten" -#: InvenTree/validators.py:146 +#: InvenTree/validators.py:157 msgid "Overage must be an integer value or a percentage" msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" -#: InvenTree/views.py:493 +#: InvenTree/views.py:494 #, fuzzy #| msgid "Delete BOM Item" msgid "Delete Item" msgstr "BOM-Position löschen" -#: InvenTree/views.py:542 +#: InvenTree/views.py:543 #, fuzzy #| msgid "Confim BOM item deletion" msgid "Check box to confirm item deletion" msgstr "Löschung von BOM-Position bestätigen" -#: InvenTree/views.py:557 templates/InvenTree/settings/user.html:18 +#: InvenTree/views.py:558 templates/InvenTree/settings/user.html:18 #, fuzzy #| msgid "No user information" msgid "Edit User Information" msgstr "Keine Benutzerinformation" -#: InvenTree/views.py:568 templates/InvenTree/settings/user.html:22 +#: InvenTree/views.py:569 templates/InvenTree/settings/user.html:22 #, fuzzy #| msgid "Select part" msgid "Set Password" msgstr "Teil auswählen" -#: InvenTree/views.py:587 +#: InvenTree/views.py:588 msgid "Password fields must match" msgstr "" -#: InvenTree/views.py:757 +#: InvenTree/views.py:794 msgid "Database Statistics" msgstr "Datenbankstatistiken" @@ -298,15 +308,15 @@ msgstr "Bestell-Referenz" #: build/forms.py:70 build/templates/build/auto_allocate.html:17 #: build/templates/build/build_base.html:78 -#: build/templates/build/detail.html:29 -#: company/templates/company/supplier_part_pricing.html:75 +#: build/templates/build/detail.html:29 common/models.py:488 +#: company/forms.py:112 company/templates/company/supplier_part_pricing.html:75 #: order/templates/order/order_wizard/select_parts.html:32 -#: order/templates/order/purchase_order_detail.html:178 +#: order/templates/order/purchase_order_detail.html:179 #: order/templates/order/sales_order_detail.html:74 #: order/templates/order/sales_order_detail.html:156 #: part/templates/part/allocation.html:16 #: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:80 stock/forms.py:297 +#: part/templates/part/sale_prices.html:82 stock/forms.py:298 #: stock/templates/stock/item_base.html:40 #: stock/templates/stock/item_base.html:46 #: stock/templates/stock/item_base.html:197 @@ -391,7 +401,6 @@ msgstr "Lagerobjekt für Zuordnung auswählen" #: build/models.py:56 build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:35 #: part/templates/part/allocation.html:20 -#: stock/templates/stock/item_base.html:227 msgid "Build Order" msgstr "Bauauftrag" @@ -408,19 +417,20 @@ msgstr "Bauaufträge" msgid "Build Order Reference" msgstr "Bestellreferenz" -#: build/models.py:73 order/templates/order/purchase_order_detail.html:173 +#: build/models.py:73 order/templates/order/purchase_order_detail.html:174 #: templates/js/bom.js:181 templates/js/build.js:493 msgid "Reference" msgstr "Referenz" #: build/models.py:80 build/templates/build/detail.html:19 +#: company/templates/company/detail.html:23 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 -#: order/templates/order/purchase_order_detail.html:160 +#: order/templates/order/purchase_order_detail.html:161 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 #: templates/InvenTree/search.html:147 templates/js/bom.js:174 #: templates/js/bom.js:499 templates/js/build.js:642 templates/js/company.js:56 -#: templates/js/order.js:167 templates/js/order.js:249 templates/js/part.js:188 +#: templates/js/order.js:168 templates/js/order.js:250 templates/js/part.js:188 #: templates/js/part.js:271 templates/js/part.js:391 templates/js/part.js:572 #: templates/js/stock.js:494 templates/js/stock.js:706 msgid "Description" @@ -443,10 +453,10 @@ msgstr "Bestellung, die diesem Bau zugwiesen ist" #: build/models.py:97 build/templates/build/auto_allocate.html:16 #: build/templates/build/build_base.html:73 -#: build/templates/build/detail.html:24 order/models.py:519 +#: build/templates/build/detail.html:24 order/models.py:530 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:148 -#: order/templates/order/receive_parts.html:19 part/models.py:294 +#: order/templates/order/receive_parts.html:19 part/models.py:315 #: part/templates/part/part_app_base.html:7 part/templates/part/related.html:26 #: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 #: templates/js/barcode.js:336 templates/js/bom.js:147 templates/js/bom.js:484 @@ -520,7 +530,7 @@ msgstr "Bau-Status" msgid "Build status code" msgstr "Bau-Statuscode" -#: build/models.py:157 stock/models.py:389 +#: build/models.py:157 stock/models.py:390 msgid "Batch Code" msgstr "Losnummer" @@ -532,20 +542,20 @@ msgstr "Chargennummer für diese Bau-Ausgabe" #: company/templates/company/supplier_part_base.html:68 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 -#: stock/models.py:383 stock/templates/stock/item_base.html:279 +#: stock/models.py:384 stock/templates/stock/item_base.html:280 msgid "External Link" msgstr "Externer Link" -#: build/models.py:177 part/models.py:597 stock/models.py:385 +#: build/models.py:177 part/models.py:672 stock/models.py:386 msgid "Link to external URL" msgstr "Link zu einer externen URL" -#: build/models.py:181 build/templates/build/tabs.html:23 company/models.py:314 +#: build/models.py:181 build/templates/build/tabs.html:23 company/models.py:344 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 -#: order/templates/order/purchase_order_detail.html:203 +#: order/templates/order/purchase_order_detail.html:213 #: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:73 -#: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:455 -#: stock/models.py:1428 stock/templates/stock/tabs.html:26 +#: stock/forms.py:307 stock/forms.py:339 stock/forms.py:367 stock/models.py:448 +#: stock/models.py:1432 stock/templates/stock/tabs.html:26 #: templates/js/barcode.js:391 templates/js/bom.js:250 #: templates/js/stock.js:116 templates/js/stock.js:578 msgid "Notes" @@ -598,11 +608,11 @@ msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" "zugewiesene Anzahl ({n}) darf nicht die verfügbare ({q}) Anzahl überschreiten" -#: build/models.py:908 order/models.py:603 +#: build/models.py:908 order/models.py:614 msgid "StockItem is over-allocated" msgstr "Zu viele Lagerobjekte zugewiesen" -#: build/models.py:912 order/models.py:606 +#: build/models.py:912 order/models.py:617 msgid "Allocation quantity must be greater than zero" msgstr "Anzahl muss größer null sein" @@ -620,11 +630,11 @@ msgstr "Bau starten um Teile zuzuweisen" msgid "Source stock item" msgstr "Bestand entfernen" -#: build/models.py:976 +#: build/models.py:975 msgid "Stock quantity to allocate to build" msgstr "Lagerobjekt-Anzahl dem Bau zuweisen" -#: build/models.py:984 +#: build/models.py:983 #, fuzzy #| msgid "Destination stock location" msgid "Destination stock item" @@ -709,8 +719,8 @@ msgid "" "The following stock items will be allocated to the specified build output" msgstr "Lagerobjekt dem Bau zuweisen" -#: build/templates/build/auto_allocate.html:18 stock/forms.py:336 -#: stock/templates/stock/item_base.html:233 +#: build/templates/build/auto_allocate.html:18 stock/forms.py:337 +#: stock/templates/stock/item_base.html:227 #: stock/templates/stock/stock_adjust.html:17 #: templates/InvenTree/search.html:183 templates/js/barcode.js:337 #: templates/js/build.js:418 templates/js/stock.js:570 @@ -778,9 +788,9 @@ msgstr "Bau-Status" #: build/templates/build/build_base.html:83 #: build/templates/build/detail.html:57 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:311 templates/InvenTree/search.html:175 +#: stock/templates/stock/item_base.html:312 templates/InvenTree/search.html:175 #: templates/js/barcode.js:42 templates/js/build.js:675 -#: templates/js/order.js:172 templates/js/order.js:254 +#: templates/js/order.js:173 templates/js/order.js:255 #: templates/js/stock.js:557 templates/js/stock.js:961 msgid "Status" msgstr "Status" @@ -791,13 +801,13 @@ msgid "Progress" msgstr "" #: build/templates/build/build_base.html:101 -#: build/templates/build/detail.html:82 order/models.py:517 +#: build/templates/build/detail.html:82 order/models.py:528 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:221 templates/js/order.js:221 +#: stock/templates/stock/item_base.html:221 templates/js/order.js:222 msgid "Sales Order" msgstr "Bestellung" @@ -918,7 +928,7 @@ msgstr "Lagerobjekt" msgid "Stock can be taken from any available location." msgstr "Bestand kann jedem verfügbaren Lagerort entnommen werden." -#: build/templates/build/detail.html:44 stock/forms.py:364 +#: build/templates/build/detail.html:44 stock/forms.py:365 #, fuzzy #| msgid "Description" msgid "Destination" @@ -931,7 +941,7 @@ msgid "Destination location not specified" msgstr "Hat dieses Teil Tracking für einzelne Objekte?" #: build/templates/build/detail.html:68 -#: stock/templates/stock/item_base.html:251 templates/js/stock.js:565 +#: stock/templates/stock/item_base.html:245 templates/js/stock.js:565 #: templates/js/stock.js:968 templates/js/table_filters.js:80 #: templates/js/table_filters.js:151 msgid "Batch" @@ -1036,7 +1046,7 @@ msgstr "Lagerbestand dem Bau zuweisen" msgid "Create Build Output" msgstr "Bau-Ausgabe" -#: build/views.py:207 stock/models.py:832 stock/views.py:1647 +#: build/views.py:207 stock/models.py:827 stock/views.py:1660 #, fuzzy #| msgid "Serial numbers already exist: " msgid "Serial numbers already exist" @@ -1058,7 +1068,7 @@ msgstr "Bau entfernt" msgid "Confirm unallocation of build stock" msgstr "Zuweisungsaufhebung bestätigen" -#: build/views.py:303 build/views.py:388 stock/views.py:413 +#: build/views.py:303 build/views.py:388 stock/views.py:417 msgid "Check the confirmation box" msgstr "Bestätigungsbox bestätigen" @@ -1175,8 +1185,8 @@ msgstr "Bauobjekt aktualisiert" msgid "Add Build Order Attachment" msgstr "Auftragsanhang hinzufügen" -#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:164 -#: stock/views.py:176 +#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:168 +#: stock/views.py:180 msgid "Added attachment" msgstr "Anhang hinzugefügt" @@ -1192,192 +1202,221 @@ msgstr "Anhang aktualisiert" msgid "Delete Attachment" msgstr "Anhang löschen" -#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:234 +#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:238 msgid "Deleted attachment" msgstr "Anhang gelöscht" -#: common/models.py:51 +#: common/models.py:56 #, fuzzy #| msgid "Instance Name" msgid "InvenTree Instance Name" msgstr "Instanzname" -#: common/models.py:53 +#: common/models.py:58 #, fuzzy #| msgid "Brief description of the build" msgid "String descriptor for the server instance" msgstr "Kurze Beschreibung des Baus" -#: common/models.py:57 company/models.py:89 company/models.py:90 +#: common/models.py:62 company/models.py:95 company/models.py:96 msgid "Company name" msgstr "Firmenname" -#: common/models.py:58 +#: common/models.py:63 #, fuzzy #| msgid "Company name" msgid "Internal company name" msgstr "Firmenname" -#: common/models.py:63 +#: common/models.py:68 +#, fuzzy +#| msgid "Delete Currency" +msgid "Default Currency" +msgstr "Währung entfernen" + +#: common/models.py:69 +#, fuzzy +#| msgid "Delete Currency" +msgid "Default currency" +msgstr "Währung entfernen" + +#: common/models.py:75 msgid "IPN Regex" msgstr "" -#: common/models.py:64 +#: common/models.py:76 msgid "Regular expression pattern for matching Part IPN" msgstr "" -#: common/models.py:68 +#: common/models.py:80 +#, fuzzy +#| msgid "Duplicate Part" +msgid "Allow Duplicate IPN" +msgstr "Teil duplizieren" + +#: common/models.py:81 +msgid "Allow multiple parts to share the same IPN" +msgstr "" + +#: common/models.py:87 #, fuzzy #| msgid "Import BOM data" msgid "Copy Part BOM Data" msgstr "Stückliste importieren" -#: common/models.py:69 +#: common/models.py:88 msgid "Copy BOM data by default when duplicating a part" msgstr "" -#: common/models.py:75 +#: common/models.py:94 #, fuzzy #| msgid "Parameters" msgid "Copy Part Parameter Data" msgstr "Parameter" -#: common/models.py:76 +#: common/models.py:95 msgid "Copy parameter data by default when duplicating a part" msgstr "" -#: common/models.py:82 +#: common/models.py:101 #, fuzzy #| msgid "Parameters" msgid "Copy Part Test Data" msgstr "Parameter" -#: common/models.py:83 +#: common/models.py:102 msgid "Copy test data by default when duplicating a part" msgstr "" -#: common/models.py:89 part/models.py:668 part/templates/part/detail.html:168 +#: common/models.py:108 +#, fuzzy +#| msgid "Edit Part Parameter Template" +msgid "Copy Category Parameter Templates" +msgstr "Teilparametervorlage bearbeiten" + +#: common/models.py:109 +msgid "Copy category parameter templates when creating a part" +msgstr "" + +#: common/models.py:115 part/models.py:743 part/templates/part/detail.html:168 #: templates/js/table_filters.js:264 msgid "Component" msgstr "Komponente" -#: common/models.py:90 +#: common/models.py:116 #, fuzzy #| msgid "Part can be used in assemblies" msgid "Parts can be used as sub-components by default" msgstr "Teil kann in Baugruppen benutzt werden" -#: common/models.py:96 part/models.py:679 part/templates/part/detail.html:188 +#: common/models.py:122 part/models.py:754 part/templates/part/detail.html:188 msgid "Purchaseable" msgstr "Kaufbar" -#: common/models.py:97 +#: common/models.py:123 msgid "Parts are purchaseable by default" msgstr "" -#: common/models.py:103 part/models.py:684 part/templates/part/detail.html:198 +#: common/models.py:129 part/models.py:759 part/templates/part/detail.html:198 #: templates/js/table_filters.js:272 msgid "Salable" msgstr "Verkäuflich" -#: common/models.py:104 +#: common/models.py:130 msgid "Parts are salable by default" msgstr "" -#: common/models.py:110 part/models.py:674 part/templates/part/detail.html:178 +#: common/models.py:136 part/models.py:749 part/templates/part/detail.html:178 #: templates/js/table_filters.js:31 templates/js/table_filters.js:276 msgid "Trackable" msgstr "nachverfolgbar" -#: common/models.py:111 +#: common/models.py:137 msgid "Parts are trackable by default" msgstr "" -#: common/models.py:117 +#: common/models.py:143 #, fuzzy #| msgid "Order Reference" msgid "Build Order Reference Prefix" msgstr "Bestellreferenz" -#: common/models.py:118 +#: common/models.py:144 #, fuzzy #| msgid "Order reference" msgid "Prefix value for build order reference" msgstr "Bestell-Referenz" -#: common/models.py:123 +#: common/models.py:149 #, fuzzy #| msgid "Order Reference" msgid "Build Order Reference Regex" msgstr "Bestellreferenz" -#: common/models.py:124 +#: common/models.py:150 msgid "Regular expression pattern for matching build order reference" msgstr "" -#: common/models.py:128 +#: common/models.py:154 #, fuzzy #| msgid "Sales Order Reference" msgid "Sales Order Reference Prefix" msgstr "Bestellungsreferenz" -#: common/models.py:129 +#: common/models.py:155 #, fuzzy #| msgid "Order reference" msgid "Prefix value for sales order reference" msgstr "Bestell-Referenz" -#: common/models.py:133 +#: common/models.py:159 #, fuzzy #| msgid "Order reference" msgid "Purchase Order Reference Prefix" msgstr "Bestell-Referenz" -#: common/models.py:134 +#: common/models.py:160 #, fuzzy #| msgid "Order reference" msgid "Prefix value for purchase order reference" msgstr "Bestell-Referenz" -#: common/models.py:312 +#: common/models.py:373 msgid "Settings key (must be unique - case insensitive" msgstr "" "Einstellungs-Schlüssel (muss einzigartig sein, Groß-/ Kleinschreibung wird " "nicht beachtet)" -#: common/models.py:314 +#: common/models.py:375 msgid "Settings value" msgstr "Einstellungs-Wert" -#: common/models.py:366 +#: common/models.py:431 msgid "Value must be a boolean value" msgstr "" -#: common/models.py:380 +#: common/models.py:445 msgid "Key string must be unique" msgstr "Schlüsseltext muss eindeutig sein" -#: common/models.py:419 -msgid "Currency Symbol e.g. $" -msgstr "Währungs-Symbol (z.B. €)" +#: common/models.py:489 company/forms.py:113 +#, fuzzy +#| msgid "Price Breaks" +msgid "Price break quantity" +msgstr "Preisstaffelung" -#: common/models.py:421 -msgid "Currency Suffix e.g. AUD" -msgstr "Währungs-Suffix (z.B. EUR)" +#: common/models.py:497 company/templates/company/supplier_part_pricing.html:80 +#: part/templates/part/sale_prices.html:87 templates/js/bom.js:234 +msgid "Price" +msgstr "Preis" -#: common/models.py:423 -msgid "Currency Description" -msgstr "Währungs-Beschreibung" +#: common/models.py:498 +#, fuzzy +#| msgid "Enter a valid quantity" +msgid "Unit price at specified quantity" +msgstr "Bitte eine gültige Anzahl eingeben" -#: common/models.py:425 -msgid "Currency Value" -msgstr "Währungs-Wert" - -#: common/models.py:427 -msgid "Use this currency as the base currency" -msgstr "Benutze diese Währung als Basis-Währung" - -#: common/models.py:510 +#: common/models.py:521 #, fuzzy #| msgid "Default Location" msgid "Default" @@ -1389,131 +1428,143 @@ msgstr "Standard-Lagerort" msgid "Current value" msgstr "Währungs-Wert" -#: common/views.py:23 -msgid "Create new Currency" -msgstr "Neues Währung hinzufügen" - -#: common/views.py:31 -msgid "Edit Currency" -msgstr "Währung bearbeiten" - -#: common/views.py:38 -msgid "Delete Currency" -msgstr "Währung entfernen" - -#: common/views.py:49 +#: common/views.py:25 #, fuzzy #| msgid "Settings" msgid "Change Setting" msgstr "Einstellungen" -#: company/models.py:92 +#: company/forms.py:37 company/models.py:139 +#, fuzzy +#| msgid "Do you purchase items from this company?" +msgid "Default currency used for this company" +msgstr "Kaufen Sie Teile von dieser Firma?" + +#: company/forms.py:80 +#, fuzzy +#| msgid "Price" +msgid "Single Price" +msgstr "Preis" + +#: company/forms.py:82 +#, fuzzy +#| msgid "Invalid quantity provided" +msgid "Single quantity price" +msgstr "Keine gültige Menge" + +#: company/models.py:98 #, fuzzy #| msgid "Part description" msgid "Company description" msgstr "Beschreibung des Teils" -#: company/models.py:92 +#: company/models.py:98 msgid "Description of the company" msgstr "Firmenbeschreibung" -#: company/models.py:94 company/templates/company/company_base.html:57 -#: templates/js/company.js:61 +#: company/models.py:100 company/templates/company/company_base.html:57 +#: company/templates/company/detail.html:28 templates/js/company.js:61 msgid "Website" msgstr "Website" -#: company/models.py:94 +#: company/models.py:100 msgid "Company website URL" msgstr "Firmenwebsite" -#: company/models.py:97 company/templates/company/company_base.html:64 +#: company/models.py:103 company/templates/company/company_base.html:64 msgid "Address" msgstr "Adresse" -#: company/models.py:98 +#: company/models.py:104 msgid "Company address" msgstr "Firmenadresse" -#: company/models.py:101 +#: company/models.py:107 #, fuzzy #| msgid "Contact phone number" msgid "Phone number" msgstr "Kontakt-Tel." -#: company/models.py:102 +#: company/models.py:108 msgid "Contact phone number" msgstr "Kontakt-Tel." -#: company/models.py:105 company/templates/company/company_base.html:78 +#: company/models.py:111 company/templates/company/company_base.html:78 msgid "Email" msgstr "Email" -#: company/models.py:105 +#: company/models.py:111 msgid "Contact email address" msgstr "Kontakt-Email" -#: company/models.py:108 company/templates/company/company_base.html:85 +#: company/models.py:114 company/templates/company/company_base.html:85 msgid "Contact" msgstr "Kontakt" -#: company/models.py:109 +#: company/models.py:115 msgid "Point of contact" msgstr "Anlaufstelle" -#: company/models.py:111 +#: company/models.py:117 msgid "Link to external company information" msgstr "Link auf externe Firmeninformation" -#: company/models.py:123 +#: company/models.py:129 msgid "Do you sell items to this company?" msgstr "Verkaufen Sie Teile an diese Firma?" -#: company/models.py:125 +#: company/models.py:131 msgid "Do you purchase items from this company?" msgstr "Kaufen Sie Teile von dieser Firma?" -#: company/models.py:127 +#: company/models.py:133 msgid "Does this company manufacture parts?" msgstr "Produziert diese Firma Teile?" -#: company/models.py:283 stock/models.py:337 +#: company/models.py:137 company/templates/company/detail.html:37 +#, fuzzy +#| msgid "Edit Currency" +msgid "Currency" +msgstr "Währung bearbeiten" + +#: company/models.py:313 stock/models.py:338 #: stock/templates/stock/item_base.html:177 msgid "Base Part" msgstr "Basisteil" -#: company/models.py:288 +#: company/models.py:318 msgid "Select part" msgstr "Teil auswählen" -#: company/models.py:294 +#: company/models.py:324 msgid "Select supplier" msgstr "Zulieferer auswählen" -#: company/models.py:297 +#: company/models.py:327 msgid "Supplier stock keeping unit" msgstr "Stock Keeping Units (SKU) des Zulieferers" -#: company/models.py:304 +#: company/models.py:334 msgid "Select manufacturer" msgstr "Hersteller auswählen" -#: company/models.py:308 +#: company/models.py:338 msgid "Manufacturer part number" msgstr "Hersteller-Teilenummer" -#: company/models.py:310 +#: company/models.py:340 msgid "URL for external supplier part link" msgstr "Teil-URL des Zulieferers" -#: company/models.py:312 +#: company/models.py:342 msgid "Supplier part description" msgstr "Zuliefererbeschreibung des Teils" -#: company/models.py:316 +#: company/models.py:346 msgid "Minimum charge (e.g. stocking fee)" msgstr "Mindestpreis" -#: company/models.py:318 +#: company/models.py:348 msgid "Part packaging" msgstr "Teile-Packaging" @@ -1538,27 +1589,45 @@ msgstr "Firmendetails" msgid "Phone" msgstr "Telefon" -#: company/templates/company/detail.html:16 +#: company/templates/company/detail.html:18 +#, fuzzy +#| msgid "Company name" +msgid "Company Name" +msgstr "Firmenname" + +#: company/templates/company/detail.html:31 +#, fuzzy +#| msgid "No lines specified" +msgid "No website specified" +msgstr "Keine Zeilen angegeben" + +#: company/templates/company/detail.html:40 +#, fuzzy +#| msgid "Delete Currency" +msgid "Uses default currency" +msgstr "Währung entfernen" + +#: company/templates/company/detail.html:52 #: company/templates/company/supplier_part_base.html:84 #: company/templates/company/supplier_part_detail.html:30 part/bom.py:172 #: templates/js/company.js:44 templates/js/company.js:188 msgid "Manufacturer" msgstr "Hersteller" -#: company/templates/company/detail.html:21 +#: company/templates/company/detail.html:57 #: company/templates/company/supplier_part_base.html:74 #: company/templates/company/supplier_part_detail.html:21 #: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:286 templates/js/company.js:48 -#: templates/js/company.js:164 templates/js/order.js:154 +#: stock/templates/stock/item_base.html:287 templates/js/company.js:48 +#: templates/js/company.js:164 templates/js/order.js:155 msgid "Supplier" msgstr "Zulieferer" -#: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:81 stock/models.py:372 -#: stock/models.py:373 stock/templates/stock/item_base.html:204 -#: templates/js/company.js:40 templates/js/order.js:236 +#: company/templates/company/detail.html:62 +#: order/templates/order/sales_order_base.html:81 stock/models.py:373 +#: stock/models.py:374 stock/templates/stock/item_base.html:204 +#: templates/js/company.js:40 templates/js/order.js:237 msgid "Customer" msgstr "Kunde" @@ -1609,21 +1678,21 @@ msgstr "Neues Teil" msgid "Create new Part" msgstr "Neues Teil hinzufügen" -#: company/templates/company/detail_part.html:69 company/views.py:53 +#: company/templates/company/detail_part.html:69 company/views.py:56 #: part/templates/part/supplier.html:47 msgid "New Supplier" msgstr "Neuer Zulieferer" -#: company/templates/company/detail_part.html:70 company/views.py:192 +#: company/templates/company/detail_part.html:70 company/views.py:195 msgid "Create new Supplier" msgstr "Neuen Zulieferer anlegen" -#: company/templates/company/detail_part.html:75 company/views.py:60 +#: company/templates/company/detail_part.html:75 company/views.py:63 #: part/templates/part/supplier.html:53 msgid "New Manufacturer" msgstr "Neuer Hersteller" -#: company/templates/company/detail_part.html:76 company/views.py:195 +#: company/templates/company/detail_part.html:76 company/views.py:198 msgid "Create new Manufacturer" msgstr "Neuen Hersteller anlegen" @@ -1694,8 +1763,8 @@ msgid "New Sales Order" msgstr "Neuer Auftrag" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:346 -#: stock/templates/stock/item_base.html:291 templates/js/company.js:180 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:347 +#: stock/templates/stock/item_base.html:292 templates/js/company.js:180 msgid "Supplier Part" msgstr "Zulieferer-Teil" @@ -1751,32 +1820,27 @@ msgstr "Teil bestellen" msgid "Pricing Information" msgstr "Preisinformationen ansehen" -#: company/templates/company/supplier_part_pricing.html:17 company/views.py:410 -#: part/templates/part/sale_prices.html:13 part/views.py:2360 +#: company/templates/company/supplier_part_pricing.html:17 company/views.py:459 +#: part/templates/part/sale_prices.html:14 part/views.py:2546 msgid "Add Price Break" msgstr "Preisstaffel hinzufügen" #: company/templates/company/supplier_part_pricing.html:36 -#: part/templates/part/sale_prices.html:41 +#: part/templates/part/sale_prices.html:43 #, fuzzy #| msgid "No company information found" msgid "No price break information found" msgstr "Keine Firmeninformation gefunden" -#: company/templates/company/supplier_part_pricing.html:80 -#: part/templates/part/sale_prices.html:85 templates/js/bom.js:234 -msgid "Price" -msgstr "Preis" - -#: company/templates/company/supplier_part_pricing.html:94 -#: part/templates/part/sale_prices.html:99 +#: company/templates/company/supplier_part_pricing.html:87 +#: part/templates/part/sale_prices.html:94 #, fuzzy #| msgid "Edit Price Break" msgid "Edit price break" msgstr "Preisstaffel bearbeiten" -#: company/templates/company/supplier_part_pricing.html:95 -#: part/templates/part/sale_prices.html:100 +#: company/templates/company/supplier_part_pricing.html:88 +#: part/templates/part/sale_prices.html:95 #, fuzzy #| msgid "Delete Price Break" msgid "Delete price break" @@ -1804,7 +1868,7 @@ msgid "Orders" msgstr "Bestellungen" #: company/templates/company/tabs.html:9 -#: order/templates/order/receive_parts.html:14 part/models.py:295 +#: order/templates/order/receive_parts.html:14 part/models.py:316 #: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 #: part/templates/part/category_tabs.html:6 #: templates/InvenTree/settings/tabs.html:22 templates/navbar.html:19 @@ -1812,90 +1876,90 @@ msgstr "Bestellungen" msgid "Parts" msgstr "Teile" -#: company/views.py:52 part/templates/part/tabs.html:42 +#: company/views.py:55 part/templates/part/tabs.html:42 #: templates/navbar.html:31 msgid "Suppliers" msgstr "Zulieferer" -#: company/views.py:59 templates/navbar.html:32 +#: company/views.py:62 templates/navbar.html:32 msgid "Manufacturers" msgstr "Hersteller" -#: company/views.py:66 templates/navbar.html:41 +#: company/views.py:69 templates/navbar.html:41 msgid "Customers" msgstr "Kunden" -#: company/views.py:67 +#: company/views.py:70 msgid "New Customer" msgstr "Neuer Kunde" -#: company/views.py:75 +#: company/views.py:78 msgid "Companies" msgstr "Firmen" -#: company/views.py:76 +#: company/views.py:79 msgid "New Company" msgstr "Neue Firma" -#: company/views.py:154 +#: company/views.py:157 msgid "Update Company Image" msgstr "Firmenbild aktualisieren" -#: company/views.py:160 +#: company/views.py:163 msgid "Updated company image" msgstr "Aktualisiertes Firmenbild" -#: company/views.py:170 +#: company/views.py:173 msgid "Edit Company" msgstr "Firma bearbeiten" -#: company/views.py:175 +#: company/views.py:178 msgid "Edited company information" msgstr "Firmeninformation bearbeitet" -#: company/views.py:198 +#: company/views.py:201 msgid "Create new Customer" msgstr "Neuen Kunden anlegen" -#: company/views.py:200 +#: company/views.py:203 msgid "Create new Company" msgstr "Neue Firma anlegen" -#: company/views.py:227 +#: company/views.py:230 msgid "Created new company" msgstr "Neue Firma angelegt" -#: company/views.py:237 +#: company/views.py:240 msgid "Delete Company" msgstr "Firma löschen" -#: company/views.py:243 +#: company/views.py:246 msgid "Company was deleted" msgstr "Firma gelöscht" -#: company/views.py:268 +#: company/views.py:271 msgid "Edit Supplier Part" msgstr "Zuliefererteil bearbeiten" -#: company/views.py:278 templates/js/stock.js:846 +#: company/views.py:289 templates/js/stock.js:846 msgid "Create new Supplier Part" msgstr "Neues Zuliefererteil anlegen" -#: company/views.py:339 +#: company/views.py:388 msgid "Delete Supplier Part" msgstr "Zuliefererteil entfernen" -#: company/views.py:416 part/views.py:2366 +#: company/views.py:465 part/views.py:2552 #, fuzzy #| msgid "Add Price Break" msgid "Added new price break" msgstr "Preisstaffel hinzufügen" -#: company/views.py:453 part/views.py:2411 +#: company/views.py:521 part/views.py:2596 msgid "Edit Price Break" msgstr "Preisstaffel bearbeiten" -#: company/views.py:469 part/views.py:2427 +#: company/views.py:537 part/views.py:2612 msgid "Delete Price Break" msgstr "Preisstaffel löschen" @@ -1960,118 +2024,131 @@ msgstr "Bestell-Referenz" msgid "Enter sales order number" msgstr "Auftrag stornieren" -#: order/models.py:108 +#: order/models.py:110 msgid "Order reference" msgstr "Bestell-Referenz" -#: order/models.py:110 +#: order/models.py:112 msgid "Order description" msgstr "Bestellungs-Beschreibung" -#: order/models.py:112 +#: order/models.py:114 msgid "Link to external page" msgstr "Link auf externe Seite" -#: order/models.py:122 +#: order/models.py:124 msgid "Order notes" msgstr "Bestell-Notizen" -#: order/models.py:140 order/models.py:326 +#: order/models.py:142 order/models.py:328 #, fuzzy #| msgid "Purchase Order Details" msgid "Purchase order status" msgstr "Bestelldetails" -#: order/models.py:148 +#: order/models.py:150 msgid "Company from which the items are being ordered" msgstr "" -#: order/models.py:151 +#: order/models.py:153 msgid "Supplier order reference code" msgstr "Bestellreferenz" -#: order/models.py:160 +#: order/models.py:162 msgid "Date order was issued" msgstr "" -#: order/models.py:162 +#: order/models.py:164 #, fuzzy #| msgid "Mark order as complete" msgid "Date order was completed" msgstr "Bestellung als vollständig markieren" -#: order/models.py:185 order/models.py:267 part/views.py:1477 -#: stock/models.py:243 stock/models.py:816 +#: order/models.py:187 order/models.py:269 part/views.py:1496 +#: stock/models.py:244 stock/models.py:811 msgid "Quantity must be greater than zero" msgstr "Anzahl muss größer Null sein" -#: order/models.py:190 +#: order/models.py:192 msgid "Part supplier must match PO supplier" msgstr "Teile-Zulieferer muss dem Zulieferer des Kaufvertrags entsprechen" -#: order/models.py:262 +#: order/models.py:264 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "Nur Teile aufgegebener Bestllungen können empfangen werden" -#: order/models.py:322 +#: order/models.py:324 msgid "Company to which the items are being sold" msgstr "" -#: order/models.py:328 +#: order/models.py:330 msgid "Customer order reference code" msgstr "Bestellreferenz" -#: order/models.py:367 +#: order/models.py:369 msgid "SalesOrder cannot be shipped as it is not currently pending" msgstr "Bestellung kann nicht versendet werden weil sie nicht anhängig ist" -#: order/models.py:454 +#: order/models.py:456 msgid "Item quantity" msgstr "Anzahl" -#: order/models.py:456 +#: order/models.py:458 msgid "Line item reference" msgstr "Position - Referenz" -#: order/models.py:458 +#: order/models.py:460 msgid "Line item notes" msgstr "Position - Notizen" -#: order/models.py:484 order/templates/order/order_base.html:9 +#: order/models.py:486 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:265 templates/js/order.js:139 +#: stock/templates/stock/item_base.html:259 templates/js/order.js:139 msgid "Purchase Order" msgstr "Kaufvertrag" -#: order/models.py:497 +#: order/models.py:499 msgid "Supplier part" msgstr "Zulieferer-Teil" -#: order/models.py:500 +#: order/models.py:502 msgid "Number of items received" msgstr "Empfangene Objekt-Anzahl" -#: order/models.py:594 +#: order/models.py:509 stock/models.py:457 +#: stock/templates/stock/item_base.html:266 +#, fuzzy +#| msgid "Purchase Order" +msgid "Purchase Price" +msgstr "Kaufvertrag" + +#: order/models.py:510 +#, fuzzy +#| msgid "Purchase Order" +msgid "Unit purchase price" +msgstr "Kaufvertrag" + +#: order/models.py:605 msgid "Cannot allocate stock item to a line with a different part" msgstr "Kann Lagerobjekt keiner Zeile mit einem anderen Teil hinzufügen" -#: order/models.py:596 +#: order/models.py:607 msgid "Cannot allocate stock to a line without a part" msgstr "Kann Lagerobjekt keiner Zeile ohne Teil hinzufügen" -#: order/models.py:599 +#: order/models.py:610 msgid "Allocation quantity cannot exceed stock quantity" msgstr "zugewiesene Anzahl darf nicht die verfügbare Anzahl überschreiten" -#: order/models.py:609 +#: order/models.py:620 msgid "Quantity must be 1 for serialized stock item" msgstr "Anzahl muss 1 für Objekte mit Seriennummer sein" -#: order/models.py:626 +#: order/models.py:636 msgid "Select stock item to allocate" msgstr "Lagerobjekt für Zuordnung auswählen" -#: order/models.py:629 +#: order/models.py:639 msgid "Enter stock allocation quantity" msgstr "Zuordnungsanzahl eingeben" @@ -2111,7 +2188,7 @@ msgstr "Bestellreferenz" msgid "Order Status" msgstr "Bestellstatus" -#: order/templates/order/order_base.html:85 templates/js/order.js:161 +#: order/templates/order/order_base.html:85 templates/js/order.js:162 msgid "Supplier Reference" msgstr "Zuliefererreferenz" @@ -2120,7 +2197,7 @@ msgid "Issued" msgstr "Aufgegeben" #: order/templates/order/order_base.html:111 -#: order/templates/order/purchase_order_detail.html:183 +#: order/templates/order/purchase_order_detail.html:193 #: order/templates/order/receive_parts.html:22 #: order/templates/order/sales_order_base.html:113 msgid "Received" @@ -2168,7 +2245,7 @@ msgid "Select existing purchase orders, or create new orders." msgstr "Bestellungen auswählen oder anlegen." #: order/templates/order/order_wizard/select_pos.html:31 -#: templates/js/order.js:185 templates/js/order.js:272 +#: templates/js/order.js:186 templates/js/order.js:273 msgid "Items" msgstr "Positionen" @@ -2224,21 +2301,27 @@ msgstr "Neuen Lagerort anlegen" msgid "No line items found" msgstr "Keine Positionen gefunden" -#: order/templates/order/purchase_order_detail.html:165 +#: order/templates/order/purchase_order_detail.html:166 #: order/templates/order/receive_parts.html:20 msgid "Order Code" msgstr "Bestellnummer" -#: order/templates/order/purchase_order_detail.html:214 +#: order/templates/order/purchase_order_detail.html:184 +#, fuzzy +#| msgid "Price" +msgid "Unit Price" +msgstr "Preis" + +#: order/templates/order/purchase_order_detail.html:225 #: order/templates/order/sales_order_detail.html:285 msgid "Edit line item" msgstr "Position bearbeiten" -#: order/templates/order/purchase_order_detail.html:215 +#: order/templates/order/purchase_order_detail.html:226 msgid "Delete line item" msgstr "Position löschen" -#: order/templates/order/purchase_order_detail.html:220 +#: order/templates/order/purchase_order_detail.html:231 msgid "Receive line item" msgstr "Position empfangen" @@ -2277,7 +2360,7 @@ msgstr "Packliste" msgid "Sales Order Details" msgstr "Auftragsdetails" -#: order/templates/order/sales_order_base.html:87 templates/js/order.js:243 +#: order/templates/order/sales_order_base.html:87 templates/js/order.js:244 msgid "Customer Reference" msgstr "Kundenreferenz" @@ -2293,7 +2376,7 @@ msgid "Sales Order Items" msgstr "Auftragspositionen" #: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 stock/models.py:377 +#: order/templates/order/sales_order_detail.html:154 stock/models.py:378 #: stock/templates/stock/item_base.html:191 templates/js/build.js:402 msgid "Serial Number" msgstr "Seriennummer" @@ -2542,103 +2625,103 @@ msgstr "Fehler beim Lesen der Stückliste (ungültige Daten)" msgid "Error reading BOM file (incorrect row size)" msgstr "Fehler beim Lesen der Stückliste (ungültige Zeilengröße)" -#: part/forms.py:62 stock/forms.py:254 +#: part/forms.py:61 stock/forms.py:255 msgid "File Format" msgstr "Dateiformat" -#: part/forms.py:62 stock/forms.py:254 +#: part/forms.py:61 stock/forms.py:255 msgid "Select output file format" msgstr "Ausgabe-Dateiformat auswählen" -#: part/forms.py:64 +#: part/forms.py:63 msgid "Cascading" msgstr "Kaskadierend" -#: part/forms.py:64 +#: part/forms.py:63 msgid "Download cascading / multi-level BOM" msgstr "Kaskadierende Stückliste herunterladen" -#: part/forms.py:66 +#: part/forms.py:65 msgid "Levels" msgstr "" -#: part/forms.py:66 +#: part/forms.py:65 msgid "Select maximum number of BOM levels to export (0 = all levels)" msgstr "" -#: part/forms.py:68 +#: part/forms.py:67 #, fuzzy #| msgid "New Parameter" msgid "Include Parameter Data" msgstr "Neuer Parameter" -#: part/forms.py:68 +#: part/forms.py:67 msgid "Include part parameters data in exported BOM" msgstr "" -#: part/forms.py:70 +#: part/forms.py:69 #, fuzzy #| msgid "Include stock in sublocations" msgid "Include Stock Data" msgstr "Bestand in Unterlagerorten einschließen" -#: part/forms.py:70 +#: part/forms.py:69 #, fuzzy #| msgid "Include parts in subcategories" msgid "Include part stock data in exported BOM" msgstr "Teile in Unterkategorien einschließen" -#: part/forms.py:72 +#: part/forms.py:71 #, fuzzy #| msgid "New Supplier Part" msgid "Include Supplier Data" msgstr "Neues Zulieferer-Teil" -#: part/forms.py:72 +#: part/forms.py:71 msgid "Include part supplier data in exported BOM" msgstr "" -#: part/forms.py:93 part/models.py:1632 +#: part/forms.py:92 part/models.py:1715 msgid "Parent Part" msgstr "Ausgangsteil" -#: part/forms.py:94 part/templates/part/bom_duplicate.html:7 +#: part/forms.py:93 part/templates/part/bom_duplicate.html:7 #, fuzzy #| msgid "Select parent part" msgid "Select parent part to copy BOM from" msgstr "Ausgangsteil auswählen" -#: part/forms.py:100 +#: part/forms.py:99 #, fuzzy #| msgid "Select from existing images" msgid "Clear existing BOM items" msgstr "Aus vorhandenen Bildern auswählen" -#: part/forms.py:105 +#: part/forms.py:104 #, fuzzy #| msgid "Confim BOM item deletion" msgid "Confirm BOM duplication" msgstr "Löschung von BOM-Position bestätigen" -#: part/forms.py:123 +#: part/forms.py:122 msgid "Confirm that the BOM is correct" msgstr "Bestätigen, dass die Stückliste korrekt ist" -#: part/forms.py:135 +#: part/forms.py:134 msgid "Select BOM file to upload" msgstr "Stücklisten-Datei zum Upload auswählen" -#: part/forms.py:154 +#: part/forms.py:153 #, fuzzy #| msgid "Delete Parts" msgid "Related Part" msgstr "Teile löschen" -#: part/forms.py:173 +#: part/forms.py:172 msgid "Select part category" msgstr "Teilekategorie wählen" -#: part/forms.py:189 +#: part/forms.py:188 #, fuzzy #| msgid "Perform 'deep copy' which will duplicate all BOM data for this part" msgid "Duplicate all BOM data for this part" @@ -2646,181 +2729,206 @@ msgstr "" "Tiefe Kopie ausführen. Dies wird alle Daten der Stückliste für dieses Teil " "duplizieren" -#: part/forms.py:190 +#: part/forms.py:189 msgid "Copy BOM" msgstr "" -#: part/forms.py:195 +#: part/forms.py:194 msgid "Duplicate all parameter data for this part" msgstr "" -#: part/forms.py:196 +#: part/forms.py:195 #, fuzzy #| msgid "Parameters" msgid "Copy Parameters" msgstr "Parameter" -#: part/forms.py:201 +#: part/forms.py:200 msgid "Confirm part creation" msgstr "Erstellen des Teils bestätigen" -#: part/forms.py:298 +#: part/forms.py:205 +#, fuzzy +#| msgid "No part parameter templates found" +msgid "Include category parameter templates" +msgstr "Keine Teilparametervorlagen gefunden" + +#: part/forms.py:210 +#, fuzzy +#| msgid "No part parameter templates found" +msgid "Include parent categories parameter templates" +msgstr "Keine Teilparametervorlagen gefunden" + +#: part/forms.py:285 +#, fuzzy +#| msgid "Parameter template name must be unique" +msgid "Add parameter template to same level categories" +msgstr "Vorlagen-Name des Parameters muss eindeutig sein" + +#: part/forms.py:289 +#, fuzzy +#| msgid "Parameter template name must be unique" +msgid "Add parameter template to all categories" +msgstr "Vorlagen-Name des Parameters muss eindeutig sein" + +#: part/forms.py:331 msgid "Input quantity for price calculation" msgstr "Eintragsmenge zur Preisberechnung" -#: part/forms.py:301 -msgid "Select currency for price calculation" -msgstr "Währung zur Preisberechnung wählen" - -#: part/models.py:67 +#: part/models.py:68 msgid "Default location for parts in this category" msgstr "Standard-Standort für Teile dieser Kategorie" -#: part/models.py:70 +#: part/models.py:71 msgid "Default keywords for parts in this category" msgstr "Standard-Stichworte für Teile dieser Kategorie" -#: part/models.py:76 part/templates/part/part_app_base.html:9 +#: part/models.py:77 part/models.py:1760 +#: part/templates/part/part_app_base.html:9 msgid "Part Category" msgstr "Teilkategorie" -#: part/models.py:77 part/templates/part/category.html:18 +#: part/models.py:78 part/templates/part/category.html:18 #: part/templates/part/category.html:89 templates/stats.html:12 msgid "Part Categories" msgstr "Teile-Kategorien" -#: part/models.py:346 part/models.py:356 +#: part/models.py:408 part/models.py:418 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "Teil '{p1}' wird in Stückliste für Teil '{p2}' benutzt (rekursiv)" -#: part/models.py:453 +#: part/models.py:515 #, fuzzy #| msgid "No serial numbers found" msgid "Next available serial numbers are" msgstr "Keine Seriennummern gefunden" -#: part/models.py:457 +#: part/models.py:519 msgid "Next available serial number is" msgstr "" -#: part/models.py:462 +#: part/models.py:524 #, fuzzy #| msgid "Empty serial number string" msgid "Most recent serial number is" msgstr "Keine Seriennummer angegeben" -#: part/models.py:540 +#: part/models.py:603 +msgid "Duplicate IPN not allowed in part settings" +msgstr "" + +#: part/models.py:614 msgid "Part must be unique for name, IPN and revision" msgstr "Namen, Teile- und Revisionsnummern müssen eindeutig sein" -#: part/models.py:569 part/templates/part/detail.html:19 +#: part/models.py:644 part/templates/part/detail.html:19 msgid "Part name" msgstr "Name des Teils" -#: part/models.py:573 +#: part/models.py:648 msgid "Is this part a template part?" msgstr "Ist dieses Teil eine Vorlage?" -#: part/models.py:582 +#: part/models.py:657 msgid "Is this part a variant of another part?" msgstr "Ist dieses Teil eine Variante eines anderen Teils?" -#: part/models.py:584 +#: part/models.py:659 msgid "Part description" msgstr "Beschreibung des Teils" -#: part/models.py:586 +#: part/models.py:661 msgid "Part keywords to improve visibility in search results" msgstr "Schlüsselworte um die Sichtbarkeit in Suchergebnissen zu verbessern" -#: part/models.py:591 +#: part/models.py:666 msgid "Part category" msgstr "Teile-Kategorie" -#: part/models.py:593 +#: part/models.py:668 msgid "Internal Part Number" msgstr "Interne Teilenummer" -#: part/models.py:595 +#: part/models.py:670 msgid "Part revision or version number" msgstr "Revisions- oder Versionsnummer" -#: part/models.py:609 +#: part/models.py:684 msgid "Where is this item normally stored?" msgstr "Wo wird dieses Teil normalerweise gelagert?" -#: part/models.py:653 +#: part/models.py:728 msgid "Default supplier part" msgstr "Standard-Zulieferer?" -#: part/models.py:656 +#: part/models.py:731 msgid "Minimum allowed stock level" msgstr "Minimal zulässiger Lagerbestand" -#: part/models.py:658 +#: part/models.py:733 msgid "Stock keeping units for this part" msgstr "Stock Keeping Units (SKU) für dieses Teil" -#: part/models.py:662 part/templates/part/detail.html:158 +#: part/models.py:737 part/templates/part/detail.html:158 #: templates/js/table_filters.js:260 msgid "Assembly" msgstr "Baugruppe" -#: part/models.py:663 +#: part/models.py:738 msgid "Can this part be built from other parts?" msgstr "Kann dieses Teil aus anderen Teilen angefertigt werden?" -#: part/models.py:669 +#: part/models.py:744 msgid "Can this part be used to build other parts?" msgstr "Kann dieses Teil zum Bau von anderen genutzt werden?" -#: part/models.py:675 +#: part/models.py:750 msgid "Does this part have tracking for unique items?" msgstr "Hat dieses Teil Tracking für einzelne Objekte?" -#: part/models.py:680 +#: part/models.py:755 msgid "Can this part be purchased from external suppliers?" msgstr "Kann dieses Teil von externen Zulieferern gekauft werden?" -#: part/models.py:685 +#: part/models.py:760 msgid "Can this part be sold to customers?" msgstr "Kann dieses Teil an Kunden verkauft werden?" -#: part/models.py:689 part/templates/part/detail.html:215 +#: part/models.py:764 part/templates/part/detail.html:215 #: templates/js/table_filters.js:19 templates/js/table_filters.js:55 #: templates/js/table_filters.js:186 templates/js/table_filters.js:243 msgid "Active" msgstr "Aktiv" -#: part/models.py:690 +#: part/models.py:765 msgid "Is this part active?" msgstr "Ist dieses Teil aktiv?" -#: part/models.py:694 part/templates/part/detail.html:138 +#: part/models.py:769 part/templates/part/detail.html:138 #: templates/js/table_filters.js:27 msgid "Virtual" msgstr "Virtuell" -#: part/models.py:695 +#: part/models.py:770 msgid "Is this a virtual part, such as a software product or license?" msgstr "Ist dieses Teil virtuell, wie zum Beispiel eine Software oder Lizenz?" -#: part/models.py:697 +#: part/models.py:772 msgid "Part notes - supports Markdown formatting" msgstr "Bemerkungen - unterstüzt Markdown-Formatierung" -#: part/models.py:699 +#: part/models.py:774 msgid "Stored BOM checksum" msgstr "Prüfsumme der Stückliste gespeichert" -#: part/models.py:1505 +#: part/models.py:1588 #, fuzzy #| msgid "Stock item cannot be created for a template Part" msgid "Test templates can only be created for trackable parts" msgstr "Lagerobjekt kann nicht für Vorlagen-Teile angelegt werden" -#: part/models.py:1522 +#: part/models.py:1605 #, fuzzy #| msgid "" #| "A stock item with this serial number already exists for template part " @@ -2830,133 +2938,140 @@ msgstr "" "Ein Teil mit dieser Seriennummer existiert bereits für die Teilevorlage " "{part}" -#: part/models.py:1541 templates/js/part.js:567 templates/js/stock.js:92 +#: part/models.py:1624 templates/js/part.js:567 templates/js/stock.js:92 #, fuzzy #| msgid "Instance Name" msgid "Test Name" msgstr "Instanzname" -#: part/models.py:1542 +#: part/models.py:1625 #, fuzzy #| msgid "Serial number for this item" msgid "Enter a name for the test" msgstr "Seriennummer für dieses Teil" -#: part/models.py:1547 +#: part/models.py:1630 #, fuzzy #| msgid "Description" msgid "Test Description" msgstr "Beschreibung" -#: part/models.py:1548 +#: part/models.py:1631 #, fuzzy #| msgid "Brief description of the build" msgid "Enter description for this test" msgstr "Kurze Beschreibung des Baus" -#: part/models.py:1553 templates/js/part.js:576 +#: part/models.py:1636 templates/js/part.js:576 #: templates/js/table_filters.js:172 msgid "Required" msgstr "benötigt" -#: part/models.py:1554 +#: part/models.py:1637 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1559 templates/js/part.js:584 +#: part/models.py:1642 templates/js/part.js:584 #, fuzzy #| msgid "Required Parts" msgid "Requires Value" msgstr "benötigte Teile" -#: part/models.py:1560 +#: part/models.py:1643 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1565 templates/js/part.js:591 +#: part/models.py:1648 templates/js/part.js:591 #, fuzzy #| msgid "Delete Attachment" msgid "Requires Attachment" msgstr "Anhang löschen" -#: part/models.py:1566 +#: part/models.py:1649 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1599 +#: part/models.py:1682 msgid "Parameter template name must be unique" msgstr "Vorlagen-Name des Parameters muss eindeutig sein" -#: part/models.py:1604 +#: part/models.py:1687 msgid "Parameter Name" msgstr "Name des Parameters" -#: part/models.py:1606 +#: part/models.py:1689 msgid "Parameter Units" msgstr "Parameter Einheit" -#: part/models.py:1634 +#: part/models.py:1717 part/models.py:1765 +#: templates/InvenTree/settings/category.html:62 msgid "Parameter Template" msgstr "Parameter Vorlage" -#: part/models.py:1636 +#: part/models.py:1719 msgid "Parameter Value" msgstr "Parameter Wert" -#: part/models.py:1673 +#: part/models.py:1769 +#, fuzzy +#| msgid "Parameter Value" +msgid "Default Parameter Value" +msgstr "Parameter Wert" + +#: part/models.py:1799 msgid "Select parent part" msgstr "Ausgangsteil auswählen" -#: part/models.py:1681 +#: part/models.py:1807 msgid "Select part to be used in BOM" msgstr "Teil für die Nutzung in der Stückliste auswählen" -#: part/models.py:1687 +#: part/models.py:1813 msgid "BOM quantity for this BOM item" msgstr "Stücklisten-Anzahl für dieses Stücklisten-Teil" -#: part/models.py:1689 +#: part/models.py:1815 #, fuzzy #| msgid "Confim BOM item deletion" msgid "This BOM item is optional" msgstr "Löschung von BOM-Position bestätigen" -#: part/models.py:1692 +#: part/models.py:1818 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "Geschätzter Ausschuss (absolut oder prozentual)" -#: part/models.py:1695 +#: part/models.py:1821 msgid "BOM item reference" msgstr "Referenz des Objekts auf der Stückliste" -#: part/models.py:1698 +#: part/models.py:1824 msgid "BOM item notes" msgstr "Notizen zum Stücklisten-Objekt" -#: part/models.py:1700 +#: part/models.py:1826 msgid "BOM line checksum" msgstr "Prüfsumme der Stückliste" -#: part/models.py:1767 part/views.py:1483 part/views.py:1535 -#: stock/models.py:233 +#: part/models.py:1893 part/views.py:1502 part/views.py:1554 +#: stock/models.py:234 #, fuzzy #| msgid "Overage must be an integer value or a percentage" msgid "Quantity must be integer value for trackable parts" msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" -#: part/models.py:1783 +#: part/models.py:1909 #, fuzzy #| msgid "New BOM Item" msgid "BOM Item" msgstr "Neue Stücklistenposition" -#: part/models.py:1898 +#: part/models.py:2024 #, fuzzy #| msgid "Select a part" msgid "Select Related Part" msgstr "Teil auswählen" -#: part/models.py:1930 +#: part/models.py:2056 msgid "" "Error creating relationship: check that the part is not related to itself " "and that the relationship is unique" @@ -2977,7 +3092,7 @@ msgstr "Bestellung" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:72 -#: stock/templates/stock/item_base.html:273 +#: stock/templates/stock/item_base.html:274 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.js:724 #: templates/js/stock.js:695 templates/js/stock.js:944 msgid "Stock Item" @@ -3052,7 +3167,7 @@ msgstr "Stückliste validieren" msgid "Validate" msgstr "BOM validieren" -#: part/templates/part/bom.html:62 part/views.py:1774 +#: part/templates/part/bom.html:62 part/views.py:1793 msgid "Export Bill of Materials" msgstr "Stückliste exportieren" @@ -3174,7 +3289,7 @@ msgstr "Neuen Bau beginnen" msgid "All parts" msgstr "Alle Teile" -#: part/templates/part/category.html:24 part/views.py:2177 +#: part/templates/part/category.html:24 part/views.py:2184 msgid "Create new part category" msgstr "Teilkategorie anlegen" @@ -3264,7 +3379,7 @@ msgstr "Teilkategorie anlegen" msgid "Create new Part Category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:216 stock/views.py:1338 +#: part/templates/part/category.html:216 stock/views.py:1342 msgid "Create new Stock Location" msgstr "Neuen Lager-Standort erstellen" @@ -3336,7 +3451,7 @@ msgstr "Einheiten" msgid "Minimum Stock" msgstr "Minimaler Lagerbestand" -#: part/templates/part/detail.html:114 templates/js/order.js:262 +#: part/templates/part/detail.html:114 templates/js/order.js:263 msgid "Creation Date" msgstr "Erstelldatum" @@ -3356,7 +3471,7 @@ msgstr "Teil ist virtuell (kein physisches Teil)" msgid "Part is not a virtual part" msgstr "Teil ist nicht virtuell" -#: part/templates/part/detail.html:148 stock/forms.py:248 +#: part/templates/part/detail.html:148 stock/forms.py:249 #: templates/js/table_filters.js:23 templates/js/table_filters.js:248 msgid "Template" msgstr "Vorlage" @@ -3433,17 +3548,19 @@ msgstr "Teilparameter" msgid "Add new parameter" msgstr "Parameter hinzufügen" -#: part/templates/part/params.html:15 templates/InvenTree/settings/part.html:35 +#: part/templates/part/params.html:15 +#: templates/InvenTree/settings/category.html:29 +#: templates/InvenTree/settings/part.html:38 msgid "New Parameter" msgstr "Neuer Parameter" -#: part/templates/part/params.html:25 stock/models.py:1415 +#: part/templates/part/params.html:25 stock/models.py:1419 #: templates/js/stock.js:112 msgid "Value" msgstr "Wert" #: part/templates/part/params.html:41 part/templates/part/related.html:41 -#: part/templates/part/supplier.html:19 users/models.py:147 +#: part/templates/part/supplier.html:19 users/models.py:152 msgid "Delete" msgstr "Löschen" @@ -3657,7 +3774,7 @@ msgstr "Stückliste" msgid "Used In" msgstr "Benutzt in" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:317 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:318 msgid "Tests" msgstr "" @@ -3693,230 +3810,248 @@ msgstr "Neues Teil hinzufügen" msgid "New Variant" msgstr "Varianten" -#: part/views.py:80 +#: part/views.py:84 #, fuzzy #| msgid "Allocated Parts" msgid "Add Related Part" msgstr "Zugeordnete Teile" -#: part/views.py:136 +#: part/views.py:140 #, fuzzy #| msgid "Delete Supplier Part" msgid "Delete Related Part" msgstr "Zuliefererteil entfernen" -#: part/views.py:148 +#: part/views.py:152 msgid "Add part attachment" msgstr "Teilanhang hinzufügen" -#: part/views.py:203 templates/attachment_table.html:34 +#: part/views.py:207 templates/attachment_table.html:34 msgid "Edit attachment" msgstr "Anhang bearbeiten" -#: part/views.py:209 +#: part/views.py:213 msgid "Part attachment updated" msgstr "Teilanhang aktualisiert" -#: part/views.py:224 +#: part/views.py:228 msgid "Delete Part Attachment" msgstr "Teilanhang löschen" -#: part/views.py:232 +#: part/views.py:236 msgid "Deleted part attachment" msgstr "Teilanhang gelöscht" -#: part/views.py:241 +#: part/views.py:245 #, fuzzy #| msgid "Create Part Parameter Template" msgid "Create Test Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:270 +#: part/views.py:274 #, fuzzy #| msgid "Edit Template" msgid "Edit Test Template" msgstr "Vorlage bearbeiten" -#: part/views.py:286 +#: part/views.py:290 #, fuzzy #| msgid "Delete Template" msgid "Delete Test Template" msgstr "Vorlage löschen" -#: part/views.py:295 +#: part/views.py:299 msgid "Set Part Category" msgstr "Teilkategorie auswählen" -#: part/views.py:345 +#: part/views.py:349 #, python-brace-format msgid "Set category for {n} parts" msgstr "Kategorie für {n} Teile setzen" -#: part/views.py:380 +#: part/views.py:384 msgid "Create Variant" msgstr "Variante anlegen" -#: part/views.py:462 +#: part/views.py:466 msgid "Duplicate Part" msgstr "Teil duplizieren" -#: part/views.py:469 +#: part/views.py:473 msgid "Copied part" msgstr "Teil kopiert" -#: part/views.py:523 part/views.py:653 +#: part/views.py:527 part/views.py:661 msgid "Possible matches exist - confirm creation of new part" msgstr "" -#: part/views.py:588 templates/js/stock.js:840 +#: part/views.py:592 templates/js/stock.js:840 msgid "Create New Part" msgstr "Neues Teil anlegen" -#: part/views.py:595 +#: part/views.py:599 msgid "Created new part" msgstr "Neues Teil angelegt" -#: part/views.py:811 +#: part/views.py:830 msgid "Part QR Code" msgstr "Teil-QR-Code" -#: part/views.py:830 +#: part/views.py:849 msgid "Upload Part Image" msgstr "Teilbild hochladen" -#: part/views.py:838 part/views.py:875 +#: part/views.py:857 part/views.py:894 msgid "Updated part image" msgstr "Teilbild aktualisiert" -#: part/views.py:847 +#: part/views.py:866 msgid "Select Part Image" msgstr "Teilbild auswählen" -#: part/views.py:878 +#: part/views.py:897 msgid "Part image not found" msgstr "Teilbild nicht gefunden" -#: part/views.py:889 +#: part/views.py:908 msgid "Edit Part Properties" msgstr "Teileigenschaften bearbeiten" -#: part/views.py:916 +#: part/views.py:935 #, fuzzy #| msgid "Duplicate Part" msgid "Duplicate BOM" msgstr "Teil duplizieren" -#: part/views.py:947 +#: part/views.py:966 #, fuzzy #| msgid "Confirm unallocation of build stock" msgid "Confirm duplication of BOM from parent" msgstr "Zuweisungsaufhebung bestätigen" -#: part/views.py:968 +#: part/views.py:987 msgid "Validate BOM" msgstr "BOM validieren" -#: part/views.py:991 +#: part/views.py:1010 #, fuzzy #| msgid "Confirm that the BOM is correct" msgid "Confirm that the BOM is valid" msgstr "Bestätigen, dass die Stückliste korrekt ist" -#: part/views.py:1002 +#: part/views.py:1021 #, fuzzy #| msgid "Validate Bill of Materials" msgid "Validated Bill of Materials" msgstr "Stückliste validieren" -#: part/views.py:1136 +#: part/views.py:1155 msgid "No BOM file provided" msgstr "Keine Stückliste angegeben" -#: part/views.py:1486 +#: part/views.py:1505 msgid "Enter a valid quantity" msgstr "Bitte eine gültige Anzahl eingeben" -#: part/views.py:1511 part/views.py:1514 +#: part/views.py:1530 part/views.py:1533 msgid "Select valid part" msgstr "Bitte ein gültiges Teil auswählen" -#: part/views.py:1520 +#: part/views.py:1539 msgid "Duplicate part selected" msgstr "Teil doppelt ausgewählt" -#: part/views.py:1558 +#: part/views.py:1577 msgid "Select a part" msgstr "Teil auswählen" -#: part/views.py:1564 +#: part/views.py:1583 #, fuzzy #| msgid "Select part to be used in BOM" msgid "Selected part creates a circular BOM" msgstr "Teil für die Nutzung in der Stückliste auswählen" -#: part/views.py:1568 +#: part/views.py:1587 msgid "Specify quantity" msgstr "Anzahl angeben" -#: part/views.py:1824 +#: part/views.py:1843 msgid "Confirm Part Deletion" msgstr "Löschen des Teils bestätigen" -#: part/views.py:1833 +#: part/views.py:1852 msgid "Part was deleted" msgstr "Teil wurde gelöscht" -#: part/views.py:1842 +#: part/views.py:1861 msgid "Part Pricing" msgstr "Teilbepreisung" -#: part/views.py:1968 +#: part/views.py:1975 msgid "Create Part Parameter Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:1978 +#: part/views.py:1985 msgid "Edit Part Parameter Template" msgstr "Teilparametervorlage bearbeiten" -#: part/views.py:1987 +#: part/views.py:1994 msgid "Delete Part Parameter Template" msgstr "Teilparametervorlage löschen" -#: part/views.py:1997 +#: part/views.py:2004 msgid "Create Part Parameter" msgstr "Teilparameter anlegen" -#: part/views.py:2049 +#: part/views.py:2056 msgid "Edit Part Parameter" msgstr "Teilparameter bearbeiten" -#: part/views.py:2065 +#: part/views.py:2072 msgid "Delete Part Parameter" msgstr "Teilparameter löschen" -#: part/views.py:2124 +#: part/views.py:2131 msgid "Edit Part Category" msgstr "Teilkategorie bearbeiten" -#: part/views.py:2161 +#: part/views.py:2168 msgid "Delete Part Category" msgstr "Teilkategorie löschen" -#: part/views.py:2169 +#: part/views.py:2176 msgid "Part category was deleted" msgstr "Teilekategorie wurde gelöscht" #: part/views.py:2232 #, fuzzy +#| msgid "Create Part Parameter Template" +msgid "Create Category Parameter Template" +msgstr "Teilparametervorlage anlegen" + +#: part/views.py:2335 +#, fuzzy +#| msgid "Edit Part Parameter Template" +msgid "Edit Category Parameter Template" +msgstr "Teilparametervorlage bearbeiten" + +#: part/views.py:2393 +#, fuzzy +#| msgid "Delete Part Parameter Template" +msgid "Delete Category Parameter Template" +msgstr "Teilparametervorlage löschen" + +#: part/views.py:2418 +#, fuzzy #| msgid "Create BOM item" msgid "Create BOM Item" msgstr "BOM-Position anlegen" -#: part/views.py:2300 +#: part/views.py:2486 msgid "Edit BOM item" msgstr "BOM-Position beaarbeiten" -#: part/views.py:2350 +#: part/views.py:2536 msgid "Confim BOM item deletion" msgstr "Löschung von BOM-Position bestätigen" @@ -3960,342 +4095,342 @@ msgstr "Einstellungs-Beschreibung" msgid "Enter unique serial numbers (or leave blank)" msgstr "Eindeutige Seriennummern eingeben (oder leer lassen)" -#: stock/forms.py:191 +#: stock/forms.py:192 msgid "Label" msgstr "" -#: stock/forms.py:192 stock/forms.py:248 +#: stock/forms.py:193 stock/forms.py:249 #, fuzzy #| msgid "Select stock item to allocate" msgid "Select test report template" msgstr "Lagerobjekt für Zuordnung auswählen" -#: stock/forms.py:256 +#: stock/forms.py:257 msgid "Include stock items in sub locations" msgstr "Lagerobjekte in untergeordneten Lagerorten einschließen" -#: stock/forms.py:291 +#: stock/forms.py:292 #, fuzzy #| msgid "No stock items matching query" msgid "Stock item to install" msgstr "Keine zur Anfrage passenden Lagerobjekte" -#: stock/forms.py:298 +#: stock/forms.py:299 #, fuzzy #| msgid "Stock Quantity" msgid "Stock quantity to assign" msgstr "Bestand" -#: stock/forms.py:326 +#: stock/forms.py:327 #, fuzzy #| msgid "Quantity must not exceed available stock quantity ({n})" msgid "Must not exceed available quantity" msgstr "Anzahl darf nicht die verfügbare Anzahl überschreiten ({n})" -#: stock/forms.py:336 +#: stock/forms.py:337 #, fuzzy #| msgid "Does this part have tracking for unique items?" msgid "Destination location for uninstalled items" msgstr "Hat dieses Teil Tracking für einzelne Objekte?" -#: stock/forms.py:338 +#: stock/forms.py:339 #, fuzzy #| msgid "Description of the company" msgid "Add transaction note (optional)" msgstr "Firmenbeschreibung" -#: stock/forms.py:340 +#: stock/forms.py:341 #, fuzzy #| msgid "Confirm stock allocation" msgid "Confirm uninstall" msgstr "Lagerbestandszuordnung bestätigen" -#: stock/forms.py:340 +#: stock/forms.py:341 #, fuzzy #| msgid "Confirm movement of stock items" msgid "Confirm removal of installed stock items" msgstr "Bewegung der Lagerobjekte bestätigen" -#: stock/forms.py:364 +#: stock/forms.py:365 msgid "Destination stock location" msgstr "Ziel-Lagerbestand" -#: stock/forms.py:366 +#: stock/forms.py:367 msgid "Add note (required)" msgstr "" -#: stock/forms.py:370 stock/views.py:916 stock/views.py:1114 +#: stock/forms.py:371 stock/views.py:920 stock/views.py:1118 msgid "Confirm stock adjustment" msgstr "Bestands-Anpassung bestätigen" -#: stock/forms.py:370 +#: stock/forms.py:371 msgid "Confirm movement of stock items" msgstr "Bewegung der Lagerobjekte bestätigen" -#: stock/forms.py:372 +#: stock/forms.py:373 #, fuzzy #| msgid "Default Location" msgid "Set Default Location" msgstr "Standard-Lagerort" -#: stock/forms.py:372 +#: stock/forms.py:373 msgid "Set the destination as the default location for selected parts" msgstr "Setze das Ziel als Standard-Ziel für ausgewählte Teile" -#: stock/models.py:178 +#: stock/models.py:179 #, fuzzy #| msgid "Created new stock item" msgid "Created stock item" msgstr "Neues Lagerobjekt erstellt" -#: stock/models.py:214 +#: stock/models.py:215 #, fuzzy #| msgid "A stock item with this serial number already exists" msgid "StockItem with this serial number already exists" msgstr "Ein Teil mit dieser Seriennummer existiert bereits" -#: stock/models.py:250 +#: stock/models.py:251 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "Teile-Typ ('{pf}') muss {pe} sein" -#: stock/models.py:260 stock/models.py:269 +#: stock/models.py:261 stock/models.py:270 msgid "Quantity must be 1 for item with a serial number" msgstr "Anzahl muss für Objekte mit Seriennummer \"1\" sein" -#: stock/models.py:261 +#: stock/models.py:262 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" "Seriennummer kann nicht gesetzt werden wenn die Anzahl größer als \"1\" ist" -#: stock/models.py:283 +#: stock/models.py:284 msgid "Item cannot belong to itself" msgstr "Teil kann nicht zu sich selbst gehören" -#: stock/models.py:289 +#: stock/models.py:290 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:296 +#: stock/models.py:297 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:329 +#: stock/models.py:330 msgid "Parent Stock Item" msgstr "Eltern-Lagerobjekt" -#: stock/models.py:338 +#: stock/models.py:339 msgid "Base part" msgstr "Basis-Teil" -#: stock/models.py:347 +#: stock/models.py:348 msgid "Select a matching supplier part for this stock item" msgstr "Passenden Zulieferer für dieses Lagerobjekt auswählen" -#: stock/models.py:352 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:353 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "Lagerort" -#: stock/models.py:355 +#: stock/models.py:356 msgid "Where is this stock item located?" msgstr "Wo wird dieses Teil normalerweise gelagert?" -#: stock/models.py:360 stock/templates/stock/item_base.html:212 +#: stock/models.py:361 stock/templates/stock/item_base.html:212 msgid "Installed In" msgstr "Installiert in" -#: stock/models.py:363 +#: stock/models.py:364 msgid "Is this item installed in another item?" msgstr "Ist dieses Teil in einem anderen verbaut?" -#: stock/models.py:379 +#: stock/models.py:380 msgid "Serial number for this item" msgstr "Seriennummer für dieses Teil" -#: stock/models.py:391 +#: stock/models.py:392 msgid "Batch code for this stock item" msgstr "Losnummer für dieses Lagerobjekt" -#: stock/models.py:395 +#: stock/models.py:396 msgid "Stock Quantity" msgstr "Bestand" -#: stock/models.py:404 +#: stock/models.py:405 msgid "Source Build" msgstr "Quellbau" -#: stock/models.py:406 +#: stock/models.py:407 msgid "Build for this stock item" msgstr "Bau für dieses Lagerobjekt" -#: stock/models.py:417 +#: stock/models.py:418 msgid "Source Purchase Order" msgstr "Quellbestellung" -#: stock/models.py:420 +#: stock/models.py:421 msgid "Purchase order for this stock item" msgstr "Bestellung für dieses Teil" -#: stock/models.py:426 +#: stock/models.py:427 msgid "Destination Sales Order" msgstr "Zielauftrag" -#: stock/models.py:433 -msgid "Destination Build Order" -msgstr "Zielbauauftrag" - -#: stock/models.py:446 +#: stock/models.py:439 msgid "Delete this Stock Item when stock is depleted" msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" -#: stock/models.py:456 stock/templates/stock/item_notes.html:14 +#: stock/models.py:449 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "Lagerobjekt-Notizen" -#: stock/models.py:507 +#: stock/models.py:458 +msgid "Single unit purchase price at time of purchase" +msgstr "" + +#: stock/models.py:509 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assigned to Customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:509 +#: stock/models.py:511 #, fuzzy #| msgid "Item assigned to customer?" msgid "Manually assigned to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:522 +#: stock/models.py:524 #, fuzzy #| msgid "Item assigned to customer?" msgid "Returned from customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:524 +#: stock/models.py:526 #, fuzzy #| msgid "Create new stock location" msgid "Returned to location" msgstr "Neuen Lagerort anlegen" -#: stock/models.py:652 +#: stock/models.py:651 #, fuzzy #| msgid "Installed in Stock Item" msgid "Installed into stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:660 +#: stock/models.py:659 #, fuzzy #| msgid "Installed in Stock Item" msgid "Installed stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:684 +#: stock/models.py:683 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstalled stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:703 +#: stock/models.py:702 #, fuzzy #| msgid "Include sublocations" msgid "Uninstalled into location" msgstr "Unterlagerorte einschließen" -#: stock/models.py:807 +#: stock/models.py:802 #, fuzzy #| msgid "Part is not a virtual part" msgid "Part is not set as trackable" msgstr "Teil ist nicht virtuell" -#: stock/models.py:813 +#: stock/models.py:808 msgid "Quantity must be integer" msgstr "Anzahl muss eine Ganzzahl sein" -#: stock/models.py:819 +#: stock/models.py:814 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "Anzahl darf nicht die verfügbare Anzahl überschreiten ({n})" -#: stock/models.py:822 +#: stock/models.py:817 msgid "Serial numbers must be a list of integers" msgstr "Seriennummern muss eine Liste von Ganzzahlen sein" -#: stock/models.py:825 +#: stock/models.py:820 msgid "Quantity does not match serial numbers" msgstr "Anzahl stimmt nicht mit den Seriennummern überein" -#: stock/models.py:857 +#: stock/models.py:852 msgid "Add serial number" msgstr "Seriennummer hinzufügen" -#: stock/models.py:860 +#: stock/models.py:855 #, python-brace-format msgid "Serialized {n} items" msgstr "{n} Teile serialisiert" -#: stock/models.py:971 +#: stock/models.py:966 msgid "StockItem cannot be moved as it is not in stock" msgstr "Lagerobjekt kann nicht bewegt werden, da kein Bestand vorhanden ist" -#: stock/models.py:1316 +#: stock/models.py:1320 msgid "Tracking entry title" msgstr "Name des Eintrags-Trackings" -#: stock/models.py:1318 +#: stock/models.py:1322 msgid "Entry notes" msgstr "Eintrags-Notizen" -#: stock/models.py:1320 +#: stock/models.py:1324 msgid "Link to external page for further information" msgstr "Link auf externe Seite für weitere Informationen" -#: stock/models.py:1380 +#: stock/models.py:1384 #, fuzzy #| msgid "Serial number for this item" msgid "Value must be provided for this test" msgstr "Seriennummer für dieses Teil" -#: stock/models.py:1386 +#: stock/models.py:1390 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1403 +#: stock/models.py:1407 msgid "Test" msgstr "" -#: stock/models.py:1404 +#: stock/models.py:1408 #, fuzzy #| msgid "Part name" msgid "Test name" msgstr "Name des Teils" -#: stock/models.py:1409 +#: stock/models.py:1413 #, fuzzy #| msgid "Search Results" msgid "Result" msgstr "Suchergebnisse" -#: stock/models.py:1410 templates/js/table_filters.js:162 +#: stock/models.py:1414 templates/js/table_filters.js:162 msgid "Test result" msgstr "" -#: stock/models.py:1416 +#: stock/models.py:1420 msgid "Test output value" msgstr "" -#: stock/models.py:1422 +#: stock/models.py:1426 #, fuzzy #| msgid "Attachments" msgid "Attachment" msgstr "Anhänge" -#: stock/models.py:1423 +#: stock/models.py:1427 #, fuzzy #| msgid "Delete attachment" msgid "Test result attachment" msgstr "Anhang löschen" -#: stock/models.py:1429 +#: stock/models.py:1433 #, fuzzy #| msgid "Edit notes" msgid "Test notes" @@ -4461,36 +4596,36 @@ msgstr "" msgid "Stock Item Details" msgstr "Lagerbestands-Details" -#: stock/templates/stock/item_base.html:237 templates/js/build.js:426 +#: stock/templates/stock/item_base.html:231 templates/js/build.js:426 #, fuzzy #| msgid "No stock location set" msgid "No location set" msgstr "Kein Lagerort gesetzt" -#: stock/templates/stock/item_base.html:244 +#: stock/templates/stock/item_base.html:238 #, fuzzy #| msgid "Unique Identifier" msgid "Barcode Identifier" msgstr "Eindeutiger Bezeichner" -#: stock/templates/stock/item_base.html:258 templates/js/build.js:626 +#: stock/templates/stock/item_base.html:252 templates/js/build.js:626 #: templates/navbar.html:25 msgid "Build" msgstr "Bau" -#: stock/templates/stock/item_base.html:272 +#: stock/templates/stock/item_base.html:273 msgid "Parent Item" msgstr "Elternposition" -#: stock/templates/stock/item_base.html:297 +#: stock/templates/stock/item_base.html:298 msgid "Last Updated" msgstr "Zuletzt aktualisiert" -#: stock/templates/stock/item_base.html:302 +#: stock/templates/stock/item_base.html:303 msgid "Last Stocktake" msgstr "Letzte Inventur" -#: stock/templates/stock/item_base.html:306 +#: stock/templates/stock/item_base.html:307 msgid "No stocktake performed" msgstr "Keine Inventur ausgeführt" @@ -4634,7 +4769,7 @@ msgstr "Sind Sie sicher, dass Sie diesen Anhang löschen wollen?" msgid "The following stock items will be uninstalled" msgstr "Die folgenden Objekte werden erstellt" -#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1310 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1314 #, fuzzy #| msgid "Count Stock Items" msgid "Convert Stock Item" @@ -4672,260 +4807,260 @@ msgstr "Kinder" msgid "Installed Items" msgstr "Installiert in" -#: stock/views.py:119 +#: stock/views.py:123 msgid "Edit Stock Location" msgstr "Lagerobjekt-Standort bearbeiten" -#: stock/views.py:144 +#: stock/views.py:148 msgid "Stock Location QR code" msgstr "QR-Code für diesen Standort" -#: stock/views.py:163 +#: stock/views.py:167 #, fuzzy #| msgid "Add Attachment" msgid "Add Stock Item Attachment" msgstr "Anhang hinzufügen" -#: stock/views.py:210 +#: stock/views.py:214 #, fuzzy #| msgid "Edit Stock Item" msgid "Edit Stock Item Attachment" msgstr "Lagerobjekt bearbeiten" -#: stock/views.py:227 +#: stock/views.py:231 #, fuzzy #| msgid "Delete Part Attachment" msgid "Delete Stock Item Attachment" msgstr "Teilanhang löschen" -#: stock/views.py:244 +#: stock/views.py:248 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assign to Customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/views.py:254 +#: stock/views.py:258 msgid "Customer must be specified" msgstr "" -#: stock/views.py:278 +#: stock/views.py:282 #, fuzzy #| msgid "Part Stock" msgid "Return to Stock" msgstr "Teilbestand" -#: stock/views.py:288 +#: stock/views.py:292 #, fuzzy #| msgid "Include sublocations" msgid "Specify a valid location" msgstr "Unterlagerorte einschließen" -#: stock/views.py:299 +#: stock/views.py:303 msgid "Stock item returned from customer" msgstr "" -#: stock/views.py:309 +#: stock/views.py:313 #, fuzzy #| msgid "Select valid part" msgid "Select Label Template" msgstr "Bitte ein gültiges Teil auswählen" -#: stock/views.py:332 +#: stock/views.py:336 #, fuzzy #| msgid "Select valid part" msgid "Select valid label" msgstr "Bitte ein gültiges Teil auswählen" -#: stock/views.py:396 +#: stock/views.py:400 #, fuzzy #| msgid "Delete Template" msgid "Delete All Test Data" msgstr "Vorlage löschen" -#: stock/views.py:412 +#: stock/views.py:416 #, fuzzy #| msgid "Confirm Part Deletion" msgid "Confirm test data deletion" msgstr "Löschen des Teils bestätigen" -#: stock/views.py:432 +#: stock/views.py:436 msgid "Add Test Result" msgstr "" -#: stock/views.py:473 +#: stock/views.py:477 #, fuzzy #| msgid "Edit Template" msgid "Edit Test Result" msgstr "Vorlage bearbeiten" -#: stock/views.py:491 +#: stock/views.py:495 #, fuzzy #| msgid "Delete Template" msgid "Delete Test Result" msgstr "Vorlage löschen" -#: stock/views.py:503 +#: stock/views.py:507 #, fuzzy #| msgid "Delete Template" msgid "Select Test Report Template" msgstr "Vorlage löschen" -#: stock/views.py:518 +#: stock/views.py:522 #, fuzzy #| msgid "Select valid part" msgid "Select valid template" msgstr "Bitte ein gültiges Teil auswählen" -#: stock/views.py:571 +#: stock/views.py:575 msgid "Stock Export Options" msgstr "Lagerbestandsexportoptionen" -#: stock/views.py:693 +#: stock/views.py:697 msgid "Stock Item QR Code" msgstr "Lagerobjekt-QR-Code" -#: stock/views.py:719 +#: stock/views.py:723 #, fuzzy #| msgid "Installed in Stock Item" msgid "Install Stock Item" msgstr "In Lagerobjekt installiert" -#: stock/views.py:819 +#: stock/views.py:823 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstall Stock Items" msgstr "In Lagerobjekt installiert" -#: stock/views.py:927 +#: stock/views.py:931 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstalled stock items" msgstr "In Lagerobjekt installiert" -#: stock/views.py:952 +#: stock/views.py:956 msgid "Adjust Stock" msgstr "Lagerbestand anpassen" -#: stock/views.py:1062 +#: stock/views.py:1066 msgid "Move Stock Items" msgstr "Lagerobjekte bewegen" -#: stock/views.py:1063 +#: stock/views.py:1067 msgid "Count Stock Items" msgstr "Lagerobjekte zählen" -#: stock/views.py:1064 +#: stock/views.py:1068 msgid "Remove From Stock" msgstr "Aus Lagerbestand entfernen" -#: stock/views.py:1065 +#: stock/views.py:1069 msgid "Add Stock Items" msgstr "Lagerobjekte hinzufügen" -#: stock/views.py:1066 +#: stock/views.py:1070 msgid "Delete Stock Items" msgstr "Lagerobjekte löschen" -#: stock/views.py:1094 +#: stock/views.py:1098 msgid "Must enter integer value" msgstr "Nur Ganzzahl eingeben" -#: stock/views.py:1099 +#: stock/views.py:1103 msgid "Quantity must be positive" msgstr "Anzahl muss positiv sein" -#: stock/views.py:1106 +#: stock/views.py:1110 #, python-brace-format msgid "Quantity must not exceed {x}" msgstr "Anzahl darf {x} nicht überschreiten" -#: stock/views.py:1185 +#: stock/views.py:1189 #, python-brace-format msgid "Added stock to {n} items" msgstr "Vorrat zu {n} Lagerobjekten hinzugefügt" -#: stock/views.py:1200 +#: stock/views.py:1204 #, python-brace-format msgid "Removed stock from {n} items" msgstr "Vorrat von {n} Lagerobjekten entfernt" -#: stock/views.py:1213 +#: stock/views.py:1217 #, python-brace-format msgid "Counted stock for {n} items" msgstr "Bestand für {n} Objekte erfasst" -#: stock/views.py:1241 +#: stock/views.py:1245 msgid "No items were moved" msgstr "Keine Lagerobjekte wurden bewegt" -#: stock/views.py:1244 +#: stock/views.py:1248 #, python-brace-format msgid "Moved {n} items to {dest}" msgstr "{n} Teile nach {dest} bewegt" -#: stock/views.py:1263 +#: stock/views.py:1267 #, python-brace-format msgid "Deleted {n} stock items" msgstr "{n} Teile im Lager gelöscht" -#: stock/views.py:1275 +#: stock/views.py:1279 msgid "Edit Stock Item" msgstr "Lagerobjekt bearbeiten" -#: stock/views.py:1360 +#: stock/views.py:1364 msgid "Serialize Stock" msgstr "Lagerbestand erfassen" -#: stock/views.py:1454 templates/js/build.js:210 +#: stock/views.py:1458 templates/js/build.js:210 msgid "Create new Stock Item" msgstr "Neues Lagerobjekt hinzufügen" -#: stock/views.py:1555 +#: stock/views.py:1559 #, fuzzy #| msgid "Count stock items" msgid "Duplicate Stock Item" msgstr "Lagerobjekte zählen" -#: stock/views.py:1621 +#: stock/views.py:1634 msgid "Invalid quantity" msgstr "Ungültige Menge" -#: stock/views.py:1624 +#: stock/views.py:1637 #, fuzzy #| msgid "Quantity must be greater than zero" msgid "Quantity cannot be less than zero" msgstr "Anzahl muss größer Null sein" -#: stock/views.py:1628 +#: stock/views.py:1641 msgid "Invalid part selection" msgstr "Ungültige Teileauswahl" -#: stock/views.py:1676 +#: stock/views.py:1689 #, python-brace-format msgid "Created {n} new stock items" msgstr "{n} neue Lagerobjekte erstellt" -#: stock/views.py:1695 stock/views.py:1711 +#: stock/views.py:1708 stock/views.py:1724 msgid "Created new stock item" msgstr "Neues Lagerobjekt erstellt" -#: stock/views.py:1730 +#: stock/views.py:1743 msgid "Delete Stock Location" msgstr "Standort löschen" -#: stock/views.py:1744 +#: stock/views.py:1757 msgid "Delete Stock Item" msgstr "Lagerobjekt löschen" -#: stock/views.py:1756 +#: stock/views.py:1769 msgid "Delete Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag löschen" -#: stock/views.py:1775 +#: stock/views.py:1788 msgid "Edit Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag bearbeiten" -#: stock/views.py:1785 +#: stock/views.py:1798 msgid "Add Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag hinzufügen" @@ -5019,23 +5154,39 @@ msgstr "Suche" msgid "Build Order Settings" msgstr "Bauaufträge" -#: templates/InvenTree/settings/currency.html:5 +#: templates/InvenTree/settings/category.html:9 #, fuzzy -#| msgid "Settings" -msgid "General Settings" -msgstr "Einstellungen" +#| msgid "Category Details" +msgid "Category Settings" +msgstr "Kategorie-Details" -#: templates/InvenTree/settings/currency.html:14 +#: templates/InvenTree/settings/category.html:25 #, fuzzy -#| msgid "Currency Value" -msgid "Currencies" -msgstr "Währungs-Wert" +#| msgid "Edit Part Parameter Template" +msgid "Category Parameter Templates" +msgstr "Teilparametervorlage bearbeiten" -#: templates/InvenTree/settings/currency.html:18 +#: templates/InvenTree/settings/category.html:52 #, fuzzy -#| msgid "Delete Currency" -msgid "New Currency" -msgstr "Währung entfernen" +#| msgid "No part parameter templates found" +msgid "No category parameter templates found" +msgstr "Keine Teilparametervorlagen gefunden" + +#: templates/InvenTree/settings/category.html:67 +#, fuzzy +#| msgid "Default Location" +msgid "Default Value" +msgstr "Standard-Lagerort" + +#: templates/InvenTree/settings/category.html:70 +#: templates/InvenTree/settings/part.html:75 +msgid "Edit Template" +msgstr "Vorlage bearbeiten" + +#: templates/InvenTree/settings/category.html:71 +#: templates/InvenTree/settings/part.html:76 +msgid "Delete Template" +msgstr "Vorlage löschen" #: templates/InvenTree/settings/global.html:10 #, fuzzy @@ -5055,24 +5206,16 @@ msgstr "Einstellungen" msgid "Part Options" msgstr "Quell-Standort" -#: templates/InvenTree/settings/part.html:31 +#: templates/InvenTree/settings/part.html:34 #, fuzzy #| msgid "Edit Part Parameter Template" msgid "Part Parameter Templates" msgstr "Teilparametervorlage bearbeiten" -#: templates/InvenTree/settings/part.html:52 +#: templates/InvenTree/settings/part.html:55 msgid "No part parameter templates found" msgstr "Keine Teilparametervorlagen gefunden" -#: templates/InvenTree/settings/part.html:72 -msgid "Edit Template" -msgstr "Vorlage bearbeiten" - -#: templates/InvenTree/settings/part.html:73 -msgid "Delete Template" -msgstr "Vorlage löschen" - #: templates/InvenTree/settings/po.html:9 #, fuzzy #| msgid "Purchase Order Details" @@ -5133,9 +5276,9 @@ msgstr "" #: templates/InvenTree/settings/tabs.html:19 #, fuzzy -#| msgid "Edit Currency" -msgid "Currency" -msgstr "Währung bearbeiten" +#| msgid "Part Categories" +msgid "Categories" +msgstr "Teile-Kategorien" #: templates/InvenTree/settings/theme.html:10 #, fuzzy @@ -5514,15 +5657,15 @@ msgstr "Link" msgid "No purchase orders found" msgstr "Keine Bestellungen gefunden" -#: templates/js/order.js:180 templates/js/stock.js:677 +#: templates/js/order.js:181 templates/js/stock.js:677 msgid "Date" msgstr "Datum" -#: templates/js/order.js:210 +#: templates/js/order.js:211 msgid "No sales orders found" msgstr "Keine Aufträge gefunden" -#: templates/js/order.js:267 +#: templates/js/order.js:268 msgid "Shipment Date" msgstr "Versanddatum" @@ -6032,46 +6175,88 @@ msgstr "Revision" msgid "Important dates" msgstr "Stückliste importieren" -#: users/models.py:130 +#: users/models.py:135 msgid "Permission set" msgstr "" -#: users/models.py:138 +#: users/models.py:143 msgid "Group" msgstr "" -#: users/models.py:141 +#: users/models.py:146 msgid "View" msgstr "" -#: users/models.py:141 +#: users/models.py:146 msgid "Permission to view items" msgstr "" -#: users/models.py:143 +#: users/models.py:148 #, fuzzy #| msgid "Address" msgid "Add" msgstr "Adresse" -#: users/models.py:143 +#: users/models.py:148 msgid "Permission to add items" msgstr "" -#: users/models.py:145 +#: users/models.py:150 msgid "Change" msgstr "" -#: users/models.py:145 +#: users/models.py:150 msgid "Permissions to edit items" msgstr "" -#: users/models.py:147 +#: users/models.py:152 #, fuzzy #| msgid "Remove selected BOM items" msgid "Permission to delete items" msgstr "Ausgewählte Stücklistenpositionen entfernen" +#~ msgid "Currency Symbol e.g. $" +#~ msgstr "Währungs-Symbol (z.B. €)" + +#~ msgid "Currency Suffix e.g. AUD" +#~ msgstr "Währungs-Suffix (z.B. EUR)" + +#~ msgid "Currency Description" +#~ msgstr "Währungs-Beschreibung" + +#~ msgid "Currency Value" +#~ msgstr "Währungs-Wert" + +#~ msgid "Use this currency as the base currency" +#~ msgstr "Benutze diese Währung als Basis-Währung" + +#~ msgid "Create new Currency" +#~ msgstr "Neues Währung hinzufügen" + +#~ msgid "Edit Currency" +#~ msgstr "Währung bearbeiten" + +#~ msgid "Select currency for price calculation" +#~ msgstr "Währung zur Preisberechnung wählen" + +#~ msgid "Destination Build Order" +#~ msgstr "Zielbauauftrag" + +#, fuzzy +#~| msgid "Settings" +#~ msgid "General Settings" +#~ msgstr "Einstellungen" + +#, fuzzy +#~| msgid "Currency Value" +#~ msgid "Currencies" +#~ msgstr "Währungs-Wert" + +#, fuzzy +#~| msgid "Delete Currency" +#~ msgid "New Currency" +#~ msgstr "Währung entfernen" + #, fuzzy #~| msgid "Serial Number" #~ msgid "Serial Numbers" @@ -6250,9 +6435,6 @@ msgstr "Ausgewählte Stücklistenpositionen entfernen" #~ msgid "Base Price (Flat Fee)" #~ msgstr "Grundpreis" -#~ msgid "Price Breaks" -#~ msgstr "Preisstaffelung" - #~ msgid "New Price Break" #~ msgstr "Neue Preisstaffelung" diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po index c4c14849c2..f6cb0f4604 100644 --- a/InvenTree/locale/en/LC_MESSAGES/django.po +++ b/InvenTree/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-11-09 12:47+0000\n" +"POT-Creation-Date: 2020-11-12 22:05+1100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -26,27 +26,31 @@ msgstr "" msgid "No matching action found" msgstr "" -#: InvenTree/forms.py:130 build/forms.py:82 build/forms.py:170 +#: InvenTree/forms.py:108 build/forms.py:82 build/forms.py:170 msgid "Confirm" msgstr "" -#: InvenTree/forms.py:146 +#: InvenTree/forms.py:124 msgid "Confirm item deletion" msgstr "" -#: InvenTree/forms.py:178 +#: InvenTree/forms.py:156 msgid "Enter new password" msgstr "" -#: InvenTree/forms.py:185 +#: InvenTree/forms.py:163 msgid "Confirm new password" msgstr "" -#: InvenTree/forms.py:220 +#: InvenTree/forms.py:198 msgid "Apply Theme" msgstr "" -#: InvenTree/helpers.py:361 order/models.py:187 order/models.py:269 +#: InvenTree/forms.py:228 +msgid "Select Category" +msgstr "" + +#: InvenTree/helpers.py:361 order/models.py:189 order/models.py:271 msgid "Invalid quantity provided" msgstr "" @@ -99,19 +103,19 @@ msgstr "" msgid "Description (optional)" msgstr "" -#: InvenTree/settings.py:348 +#: InvenTree/settings.py:354 msgid "English" msgstr "" -#: InvenTree/settings.py:349 +#: InvenTree/settings.py:355 msgid "German" msgstr "" -#: InvenTree/settings.py:350 +#: InvenTree/settings.py:356 msgid "French" msgstr "" -#: InvenTree/settings.py:351 +#: InvenTree/settings.py:357 msgid "Polish" msgstr "" @@ -172,57 +176,61 @@ msgstr "" msgid "Production" msgstr "" -#: InvenTree/validators.py:39 +#: InvenTree/validators.py:22 +msgid "Not a valid currency code" +msgstr "" + +#: InvenTree/validators.py:50 msgid "Invalid character in part name" msgstr "" -#: InvenTree/validators.py:52 +#: InvenTree/validators.py:63 msgid "IPN must match regex pattern" msgstr "" -#: InvenTree/validators.py:66 InvenTree/validators.py:80 -#: InvenTree/validators.py:94 +#: InvenTree/validators.py:77 InvenTree/validators.py:91 +#: InvenTree/validators.py:105 msgid "Reference must match pattern" msgstr "" -#: InvenTree/validators.py:102 +#: InvenTree/validators.py:113 #, python-brace-format msgid "Illegal character in name ({x})" msgstr "" -#: InvenTree/validators.py:121 InvenTree/validators.py:137 +#: InvenTree/validators.py:132 InvenTree/validators.py:148 msgid "Overage value must not be negative" msgstr "" -#: InvenTree/validators.py:139 +#: InvenTree/validators.py:150 msgid "Overage must not exceed 100%" msgstr "" -#: InvenTree/validators.py:146 +#: InvenTree/validators.py:157 msgid "Overage must be an integer value or a percentage" msgstr "" -#: InvenTree/views.py:493 +#: InvenTree/views.py:494 msgid "Delete Item" msgstr "" -#: InvenTree/views.py:542 +#: InvenTree/views.py:543 msgid "Check box to confirm item deletion" msgstr "" -#: InvenTree/views.py:557 templates/InvenTree/settings/user.html:18 +#: InvenTree/views.py:558 templates/InvenTree/settings/user.html:18 msgid "Edit User Information" msgstr "" -#: InvenTree/views.py:568 templates/InvenTree/settings/user.html:22 +#: InvenTree/views.py:569 templates/InvenTree/settings/user.html:22 msgid "Set Password" msgstr "" -#: InvenTree/views.py:587 +#: InvenTree/views.py:588 msgid "Password fields must match" msgstr "" -#: InvenTree/views.py:757 +#: InvenTree/views.py:794 msgid "Database Statistics" msgstr "" @@ -272,15 +280,15 @@ msgstr "" #: build/forms.py:70 build/templates/build/auto_allocate.html:17 #: build/templates/build/build_base.html:78 -#: build/templates/build/detail.html:29 -#: company/templates/company/supplier_part_pricing.html:75 +#: build/templates/build/detail.html:29 common/models.py:488 +#: company/forms.py:112 company/templates/company/supplier_part_pricing.html:75 #: order/templates/order/order_wizard/select_parts.html:32 -#: order/templates/order/purchase_order_detail.html:178 +#: order/templates/order/purchase_order_detail.html:179 #: order/templates/order/sales_order_detail.html:74 #: order/templates/order/sales_order_detail.html:156 #: part/templates/part/allocation.html:16 #: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:80 stock/forms.py:297 +#: part/templates/part/sale_prices.html:82 stock/forms.py:298 #: stock/templates/stock/item_base.html:40 #: stock/templates/stock/item_base.html:46 #: stock/templates/stock/item_base.html:197 @@ -345,7 +353,6 @@ msgstr "" #: build/models.py:56 build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:35 #: part/templates/part/allocation.html:20 -#: stock/templates/stock/item_base.html:227 msgid "Build Order" msgstr "" @@ -360,19 +367,20 @@ msgstr "" msgid "Build Order Reference" msgstr "" -#: build/models.py:73 order/templates/order/purchase_order_detail.html:173 +#: build/models.py:73 order/templates/order/purchase_order_detail.html:174 #: templates/js/bom.js:181 templates/js/build.js:493 msgid "Reference" msgstr "" #: build/models.py:80 build/templates/build/detail.html:19 +#: company/templates/company/detail.html:23 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 -#: order/templates/order/purchase_order_detail.html:160 +#: order/templates/order/purchase_order_detail.html:161 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 #: templates/InvenTree/search.html:147 templates/js/bom.js:174 #: templates/js/bom.js:499 templates/js/build.js:642 templates/js/company.js:56 -#: templates/js/order.js:167 templates/js/order.js:249 templates/js/part.js:188 +#: templates/js/order.js:168 templates/js/order.js:250 templates/js/part.js:188 #: templates/js/part.js:271 templates/js/part.js:391 templates/js/part.js:572 #: templates/js/stock.js:494 templates/js/stock.js:706 msgid "Description" @@ -393,10 +401,10 @@ msgstr "" #: build/models.py:97 build/templates/build/auto_allocate.html:16 #: build/templates/build/build_base.html:73 -#: build/templates/build/detail.html:24 order/models.py:519 +#: build/templates/build/detail.html:24 order/models.py:530 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:148 -#: order/templates/order/receive_parts.html:19 part/models.py:294 +#: order/templates/order/receive_parts.html:19 part/models.py:315 #: part/templates/part/part_app_base.html:7 part/templates/part/related.html:26 #: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 #: templates/js/barcode.js:336 templates/js/bom.js:147 templates/js/bom.js:484 @@ -460,7 +468,7 @@ msgstr "" msgid "Build status code" msgstr "" -#: build/models.py:157 stock/models.py:389 +#: build/models.py:157 stock/models.py:390 msgid "Batch Code" msgstr "" @@ -472,20 +480,20 @@ msgstr "" #: company/templates/company/supplier_part_base.html:68 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 -#: stock/models.py:383 stock/templates/stock/item_base.html:279 +#: stock/models.py:384 stock/templates/stock/item_base.html:280 msgid "External Link" msgstr "" -#: build/models.py:177 part/models.py:597 stock/models.py:385 +#: build/models.py:177 part/models.py:672 stock/models.py:386 msgid "Link to external URL" msgstr "" -#: build/models.py:181 build/templates/build/tabs.html:23 company/models.py:314 +#: build/models.py:181 build/templates/build/tabs.html:23 company/models.py:344 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 -#: order/templates/order/purchase_order_detail.html:203 +#: order/templates/order/purchase_order_detail.html:213 #: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:73 -#: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:455 -#: stock/models.py:1428 stock/templates/stock/tabs.html:26 +#: stock/forms.py:307 stock/forms.py:339 stock/forms.py:367 stock/models.py:448 +#: stock/models.py:1432 stock/templates/stock/tabs.html:26 #: templates/js/barcode.js:391 templates/js/bom.js:250 #: templates/js/stock.js:116 templates/js/stock.js:578 msgid "Notes" @@ -529,11 +537,11 @@ msgstr "" msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" -#: build/models.py:908 order/models.py:603 +#: build/models.py:908 order/models.py:614 msgid "StockItem is over-allocated" msgstr "" -#: build/models.py:912 order/models.py:606 +#: build/models.py:912 order/models.py:617 msgid "Allocation quantity must be greater than zero" msgstr "" @@ -549,11 +557,11 @@ msgstr "" msgid "Source stock item" msgstr "" -#: build/models.py:976 +#: build/models.py:975 msgid "Stock quantity to allocate to build" msgstr "" -#: build/models.py:984 +#: build/models.py:983 msgid "Destination stock item" msgstr "" @@ -618,8 +626,8 @@ msgid "" "The following stock items will be allocated to the specified build output" msgstr "" -#: build/templates/build/auto_allocate.html:18 stock/forms.py:336 -#: stock/templates/stock/item_base.html:233 +#: build/templates/build/auto_allocate.html:18 stock/forms.py:337 +#: stock/templates/stock/item_base.html:227 #: stock/templates/stock/stock_adjust.html:17 #: templates/InvenTree/search.html:183 templates/js/barcode.js:337 #: templates/js/build.js:418 templates/js/stock.js:570 @@ -675,9 +683,9 @@ msgstr "" #: build/templates/build/build_base.html:83 #: build/templates/build/detail.html:57 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:311 templates/InvenTree/search.html:175 +#: stock/templates/stock/item_base.html:312 templates/InvenTree/search.html:175 #: templates/js/barcode.js:42 templates/js/build.js:675 -#: templates/js/order.js:172 templates/js/order.js:254 +#: templates/js/order.js:173 templates/js/order.js:255 #: templates/js/stock.js:557 templates/js/stock.js:961 msgid "Status" msgstr "" @@ -688,13 +696,13 @@ msgid "Progress" msgstr "" #: build/templates/build/build_base.html:101 -#: build/templates/build/detail.html:82 order/models.py:517 +#: build/templates/build/detail.html:82 order/models.py:528 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:221 templates/js/order.js:221 +#: stock/templates/stock/item_base.html:221 templates/js/order.js:222 msgid "Sales Order" msgstr "" @@ -786,7 +794,7 @@ msgstr "" msgid "Stock can be taken from any available location." msgstr "" -#: build/templates/build/detail.html:44 stock/forms.py:364 +#: build/templates/build/detail.html:44 stock/forms.py:365 msgid "Destination" msgstr "" @@ -795,7 +803,7 @@ msgid "Destination location not specified" msgstr "" #: build/templates/build/detail.html:68 -#: stock/templates/stock/item_base.html:251 templates/js/stock.js:565 +#: stock/templates/stock/item_base.html:245 templates/js/stock.js:565 #: templates/js/stock.js:968 templates/js/table_filters.js:80 #: templates/js/table_filters.js:151 msgid "Batch" @@ -887,7 +895,7 @@ msgstr "" msgid "Create Build Output" msgstr "" -#: build/views.py:207 stock/models.py:832 stock/views.py:1647 +#: build/views.py:207 stock/models.py:827 stock/views.py:1660 msgid "Serial numbers already exist" msgstr "" @@ -903,7 +911,7 @@ msgstr "" msgid "Confirm unallocation of build stock" msgstr "" -#: build/views.py:303 build/views.py:388 stock/views.py:413 +#: build/views.py:303 build/views.py:388 stock/views.py:417 msgid "Check the confirmation box" msgstr "" @@ -992,8 +1000,8 @@ msgstr "" msgid "Add Build Order Attachment" msgstr "" -#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:164 -#: stock/views.py:176 +#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:168 +#: stock/views.py:180 msgid "Added attachment" msgstr "" @@ -1009,162 +1017,179 @@ msgstr "" msgid "Delete Attachment" msgstr "" -#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:234 +#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:238 msgid "Deleted attachment" msgstr "" -#: common/models.py:51 +#: common/models.py:56 msgid "InvenTree Instance Name" msgstr "" -#: common/models.py:53 +#: common/models.py:58 msgid "String descriptor for the server instance" msgstr "" -#: common/models.py:57 company/models.py:89 company/models.py:90 +#: common/models.py:62 company/models.py:95 company/models.py:96 msgid "Company name" msgstr "" -#: common/models.py:58 +#: common/models.py:63 msgid "Internal company name" msgstr "" -#: common/models.py:63 -msgid "IPN Regex" -msgstr "" - -#: common/models.py:64 -msgid "Regular expression pattern for matching Part IPN" -msgstr "" - #: common/models.py:68 -msgid "Copy Part BOM Data" +msgid "Default Currency" msgstr "" #: common/models.py:69 -msgid "Copy BOM data by default when duplicating a part" +msgid "Default currency" msgstr "" #: common/models.py:75 -msgid "Copy Part Parameter Data" +msgid "IPN Regex" msgstr "" #: common/models.py:76 +msgid "Regular expression pattern for matching Part IPN" +msgstr "" + +#: common/models.py:80 +msgid "Allow Duplicate IPN" +msgstr "" + +#: common/models.py:81 +msgid "Allow multiple parts to share the same IPN" +msgstr "" + +#: common/models.py:87 +msgid "Copy Part BOM Data" +msgstr "" + +#: common/models.py:88 +msgid "Copy BOM data by default when duplicating a part" +msgstr "" + +#: common/models.py:94 +msgid "Copy Part Parameter Data" +msgstr "" + +#: common/models.py:95 msgid "Copy parameter data by default when duplicating a part" msgstr "" -#: common/models.py:82 +#: common/models.py:101 msgid "Copy Part Test Data" msgstr "" -#: common/models.py:83 +#: common/models.py:102 msgid "Copy test data by default when duplicating a part" msgstr "" -#: common/models.py:89 part/models.py:668 part/templates/part/detail.html:168 +#: common/models.py:108 +msgid "Copy Category Parameter Templates" +msgstr "" + +#: common/models.py:109 +msgid "Copy category parameter templates when creating a part" +msgstr "" + +#: common/models.py:115 part/models.py:743 part/templates/part/detail.html:168 #: templates/js/table_filters.js:264 msgid "Component" msgstr "" -#: common/models.py:90 +#: common/models.py:116 msgid "Parts can be used as sub-components by default" msgstr "" -#: common/models.py:96 part/models.py:679 part/templates/part/detail.html:188 +#: common/models.py:122 part/models.py:754 part/templates/part/detail.html:188 msgid "Purchaseable" msgstr "" -#: common/models.py:97 +#: common/models.py:123 msgid "Parts are purchaseable by default" msgstr "" -#: common/models.py:103 part/models.py:684 part/templates/part/detail.html:198 +#: common/models.py:129 part/models.py:759 part/templates/part/detail.html:198 #: templates/js/table_filters.js:272 msgid "Salable" msgstr "" -#: common/models.py:104 +#: common/models.py:130 msgid "Parts are salable by default" msgstr "" -#: common/models.py:110 part/models.py:674 part/templates/part/detail.html:178 +#: common/models.py:136 part/models.py:749 part/templates/part/detail.html:178 #: templates/js/table_filters.js:31 templates/js/table_filters.js:276 msgid "Trackable" msgstr "" -#: common/models.py:111 +#: common/models.py:137 msgid "Parts are trackable by default" msgstr "" -#: common/models.py:117 +#: common/models.py:143 msgid "Build Order Reference Prefix" msgstr "" -#: common/models.py:118 +#: common/models.py:144 msgid "Prefix value for build order reference" msgstr "" -#: common/models.py:123 +#: common/models.py:149 msgid "Build Order Reference Regex" msgstr "" -#: common/models.py:124 +#: common/models.py:150 msgid "Regular expression pattern for matching build order reference" msgstr "" -#: common/models.py:128 +#: common/models.py:154 msgid "Sales Order Reference Prefix" msgstr "" -#: common/models.py:129 +#: common/models.py:155 msgid "Prefix value for sales order reference" msgstr "" -#: common/models.py:133 +#: common/models.py:159 msgid "Purchase Order Reference Prefix" msgstr "" -#: common/models.py:134 +#: common/models.py:160 msgid "Prefix value for purchase order reference" msgstr "" -#: common/models.py:312 +#: common/models.py:373 msgid "Settings key (must be unique - case insensitive" msgstr "" -#: common/models.py:314 +#: common/models.py:375 msgid "Settings value" msgstr "" -#: common/models.py:366 +#: common/models.py:431 msgid "Value must be a boolean value" msgstr "" -#: common/models.py:380 +#: common/models.py:445 msgid "Key string must be unique" msgstr "" -#: common/models.py:419 -msgid "Currency Symbol e.g. $" +#: common/models.py:489 company/forms.py:113 +msgid "Price break quantity" msgstr "" -#: common/models.py:421 -msgid "Currency Suffix e.g. AUD" +#: common/models.py:497 company/templates/company/supplier_part_pricing.html:80 +#: part/templates/part/sale_prices.html:87 templates/js/bom.js:234 +msgid "Price" msgstr "" -#: common/models.py:423 -msgid "Currency Description" +#: common/models.py:498 +msgid "Unit price at specified quantity" msgstr "" -#: common/models.py:425 -msgid "Currency Value" -msgstr "" - -#: common/models.py:427 -msgid "Use this currency as the base currency" -msgstr "" - -#: common/models.py:510 +#: common/models.py:521 msgid "Default" msgstr "" @@ -1172,125 +1197,129 @@ msgstr "" msgid "Current value" msgstr "" -#: common/views.py:23 -msgid "Create new Currency" -msgstr "" - -#: common/views.py:31 -msgid "Edit Currency" -msgstr "" - -#: common/views.py:38 -msgid "Delete Currency" -msgstr "" - -#: common/views.py:49 +#: common/views.py:25 msgid "Change Setting" msgstr "" -#: company/models.py:92 -msgid "Company description" +#: company/forms.py:37 company/models.py:139 +msgid "Default currency used for this company" msgstr "" -#: company/models.py:92 -msgid "Description of the company" +#: company/forms.py:80 +msgid "Single Price" msgstr "" -#: company/models.py:94 company/templates/company/company_base.html:57 -#: templates/js/company.js:61 -msgid "Website" -msgstr "" - -#: company/models.py:94 -msgid "Company website URL" -msgstr "" - -#: company/models.py:97 company/templates/company/company_base.html:64 -msgid "Address" +#: company/forms.py:82 +msgid "Single quantity price" msgstr "" #: company/models.py:98 +msgid "Company description" +msgstr "" + +#: company/models.py:98 +msgid "Description of the company" +msgstr "" + +#: company/models.py:100 company/templates/company/company_base.html:57 +#: company/templates/company/detail.html:28 templates/js/company.js:61 +msgid "Website" +msgstr "" + +#: company/models.py:100 +msgid "Company website URL" +msgstr "" + +#: company/models.py:103 company/templates/company/company_base.html:64 +msgid "Address" +msgstr "" + +#: company/models.py:104 msgid "Company address" msgstr "" -#: company/models.py:101 +#: company/models.py:107 msgid "Phone number" msgstr "" -#: company/models.py:102 +#: company/models.py:108 msgid "Contact phone number" msgstr "" -#: company/models.py:105 company/templates/company/company_base.html:78 +#: company/models.py:111 company/templates/company/company_base.html:78 msgid "Email" msgstr "" -#: company/models.py:105 +#: company/models.py:111 msgid "Contact email address" msgstr "" -#: company/models.py:108 company/templates/company/company_base.html:85 +#: company/models.py:114 company/templates/company/company_base.html:85 msgid "Contact" msgstr "" -#: company/models.py:109 +#: company/models.py:115 msgid "Point of contact" msgstr "" -#: company/models.py:111 +#: company/models.py:117 msgid "Link to external company information" msgstr "" -#: company/models.py:123 +#: company/models.py:129 msgid "Do you sell items to this company?" msgstr "" -#: company/models.py:125 +#: company/models.py:131 msgid "Do you purchase items from this company?" msgstr "" -#: company/models.py:127 +#: company/models.py:133 msgid "Does this company manufacture parts?" msgstr "" -#: company/models.py:283 stock/models.py:337 +#: company/models.py:137 company/templates/company/detail.html:37 +msgid "Currency" +msgstr "" + +#: company/models.py:313 stock/models.py:338 #: stock/templates/stock/item_base.html:177 msgid "Base Part" msgstr "" -#: company/models.py:288 +#: company/models.py:318 msgid "Select part" msgstr "" -#: company/models.py:294 +#: company/models.py:324 msgid "Select supplier" msgstr "" -#: company/models.py:297 +#: company/models.py:327 msgid "Supplier stock keeping unit" msgstr "" -#: company/models.py:304 +#: company/models.py:334 msgid "Select manufacturer" msgstr "" -#: company/models.py:308 +#: company/models.py:338 msgid "Manufacturer part number" msgstr "" -#: company/models.py:310 +#: company/models.py:340 msgid "URL for external supplier part link" msgstr "" -#: company/models.py:312 +#: company/models.py:342 msgid "Supplier part description" msgstr "" -#: company/models.py:316 +#: company/models.py:346 msgid "Minimum charge (e.g. stocking fee)" msgstr "" -#: company/models.py:318 +#: company/models.py:348 msgid "Part packaging" msgstr "" @@ -1313,27 +1342,39 @@ msgstr "" msgid "Phone" msgstr "" -#: company/templates/company/detail.html:16 +#: company/templates/company/detail.html:18 +msgid "Company Name" +msgstr "" + +#: company/templates/company/detail.html:31 +msgid "No website specified" +msgstr "" + +#: company/templates/company/detail.html:40 +msgid "Uses default currency" +msgstr "" + +#: company/templates/company/detail.html:52 #: company/templates/company/supplier_part_base.html:84 #: company/templates/company/supplier_part_detail.html:30 part/bom.py:172 #: templates/js/company.js:44 templates/js/company.js:188 msgid "Manufacturer" msgstr "" -#: company/templates/company/detail.html:21 +#: company/templates/company/detail.html:57 #: company/templates/company/supplier_part_base.html:74 #: company/templates/company/supplier_part_detail.html:21 #: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:286 templates/js/company.js:48 -#: templates/js/company.js:164 templates/js/order.js:154 +#: stock/templates/stock/item_base.html:287 templates/js/company.js:48 +#: templates/js/company.js:164 templates/js/order.js:155 msgid "Supplier" msgstr "" -#: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:81 stock/models.py:372 -#: stock/models.py:373 stock/templates/stock/item_base.html:204 -#: templates/js/company.js:40 templates/js/order.js:236 +#: company/templates/company/detail.html:62 +#: order/templates/order/sales_order_base.html:81 stock/models.py:373 +#: stock/models.py:374 stock/templates/stock/item_base.html:204 +#: templates/js/company.js:40 templates/js/order.js:237 msgid "Customer" msgstr "" @@ -1380,21 +1421,21 @@ msgstr "" msgid "Create new Part" msgstr "" -#: company/templates/company/detail_part.html:69 company/views.py:53 +#: company/templates/company/detail_part.html:69 company/views.py:56 #: part/templates/part/supplier.html:47 msgid "New Supplier" msgstr "" -#: company/templates/company/detail_part.html:70 company/views.py:192 +#: company/templates/company/detail_part.html:70 company/views.py:195 msgid "Create new Supplier" msgstr "" -#: company/templates/company/detail_part.html:75 company/views.py:60 +#: company/templates/company/detail_part.html:75 company/views.py:63 #: part/templates/part/supplier.html:53 msgid "New Manufacturer" msgstr "" -#: company/templates/company/detail_part.html:76 company/views.py:195 +#: company/templates/company/detail_part.html:76 company/views.py:198 msgid "Create new Manufacturer" msgstr "" @@ -1464,8 +1505,8 @@ msgid "New Sales Order" msgstr "" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:346 -#: stock/templates/stock/item_base.html:291 templates/js/company.js:180 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:347 +#: stock/templates/stock/item_base.html:292 templates/js/company.js:180 msgid "Supplier Part" msgstr "" @@ -1521,28 +1562,23 @@ msgstr "" msgid "Pricing Information" msgstr "" -#: company/templates/company/supplier_part_pricing.html:17 company/views.py:410 -#: part/templates/part/sale_prices.html:13 part/views.py:2360 +#: company/templates/company/supplier_part_pricing.html:17 company/views.py:459 +#: part/templates/part/sale_prices.html:14 part/views.py:2546 msgid "Add Price Break" msgstr "" #: company/templates/company/supplier_part_pricing.html:36 -#: part/templates/part/sale_prices.html:41 +#: part/templates/part/sale_prices.html:43 msgid "No price break information found" msgstr "" -#: company/templates/company/supplier_part_pricing.html:80 -#: part/templates/part/sale_prices.html:85 templates/js/bom.js:234 -msgid "Price" -msgstr "" - -#: company/templates/company/supplier_part_pricing.html:94 -#: part/templates/part/sale_prices.html:99 +#: company/templates/company/supplier_part_pricing.html:87 +#: part/templates/part/sale_prices.html:94 msgid "Edit price break" msgstr "" -#: company/templates/company/supplier_part_pricing.html:95 -#: part/templates/part/sale_prices.html:100 +#: company/templates/company/supplier_part_pricing.html:88 +#: part/templates/part/sale_prices.html:95 msgid "Delete price break" msgstr "" @@ -1568,7 +1604,7 @@ msgid "Orders" msgstr "" #: company/templates/company/tabs.html:9 -#: order/templates/order/receive_parts.html:14 part/models.py:295 +#: order/templates/order/receive_parts.html:14 part/models.py:316 #: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 #: part/templates/part/category_tabs.html:6 #: templates/InvenTree/settings/tabs.html:22 templates/navbar.html:19 @@ -1576,88 +1612,88 @@ msgstr "" msgid "Parts" msgstr "" -#: company/views.py:52 part/templates/part/tabs.html:42 +#: company/views.py:55 part/templates/part/tabs.html:42 #: templates/navbar.html:31 msgid "Suppliers" msgstr "" -#: company/views.py:59 templates/navbar.html:32 +#: company/views.py:62 templates/navbar.html:32 msgid "Manufacturers" msgstr "" -#: company/views.py:66 templates/navbar.html:41 +#: company/views.py:69 templates/navbar.html:41 msgid "Customers" msgstr "" -#: company/views.py:67 +#: company/views.py:70 msgid "New Customer" msgstr "" -#: company/views.py:75 +#: company/views.py:78 msgid "Companies" msgstr "" -#: company/views.py:76 +#: company/views.py:79 msgid "New Company" msgstr "" -#: company/views.py:154 +#: company/views.py:157 msgid "Update Company Image" msgstr "" -#: company/views.py:160 +#: company/views.py:163 msgid "Updated company image" msgstr "" -#: company/views.py:170 +#: company/views.py:173 msgid "Edit Company" msgstr "" -#: company/views.py:175 +#: company/views.py:178 msgid "Edited company information" msgstr "" -#: company/views.py:198 +#: company/views.py:201 msgid "Create new Customer" msgstr "" -#: company/views.py:200 +#: company/views.py:203 msgid "Create new Company" msgstr "" -#: company/views.py:227 +#: company/views.py:230 msgid "Created new company" msgstr "" -#: company/views.py:237 +#: company/views.py:240 msgid "Delete Company" msgstr "" -#: company/views.py:243 +#: company/views.py:246 msgid "Company was deleted" msgstr "" -#: company/views.py:268 +#: company/views.py:271 msgid "Edit Supplier Part" msgstr "" -#: company/views.py:278 templates/js/stock.js:846 +#: company/views.py:289 templates/js/stock.js:846 msgid "Create new Supplier Part" msgstr "" -#: company/views.py:339 +#: company/views.py:388 msgid "Delete Supplier Part" msgstr "" -#: company/views.py:416 part/views.py:2366 +#: company/views.py:465 part/views.py:2552 msgid "Added new price break" msgstr "" -#: company/views.py:453 part/views.py:2411 +#: company/views.py:521 part/views.py:2596 msgid "Edit Price Break" msgstr "" -#: company/views.py:469 part/views.py:2427 +#: company/views.py:537 part/views.py:2612 msgid "Delete Price Break" msgstr "" @@ -1714,114 +1750,123 @@ msgstr "" msgid "Enter sales order number" msgstr "" -#: order/models.py:108 +#: order/models.py:110 msgid "Order reference" msgstr "" -#: order/models.py:110 +#: order/models.py:112 msgid "Order description" msgstr "" -#: order/models.py:112 +#: order/models.py:114 msgid "Link to external page" msgstr "" -#: order/models.py:122 +#: order/models.py:124 msgid "Order notes" msgstr "" -#: order/models.py:140 order/models.py:326 +#: order/models.py:142 order/models.py:328 msgid "Purchase order status" msgstr "" -#: order/models.py:148 +#: order/models.py:150 msgid "Company from which the items are being ordered" msgstr "" -#: order/models.py:151 +#: order/models.py:153 msgid "Supplier order reference code" msgstr "" -#: order/models.py:160 +#: order/models.py:162 msgid "Date order was issued" msgstr "" -#: order/models.py:162 +#: order/models.py:164 msgid "Date order was completed" msgstr "" -#: order/models.py:185 order/models.py:267 part/views.py:1477 -#: stock/models.py:243 stock/models.py:816 +#: order/models.py:187 order/models.py:269 part/views.py:1496 +#: stock/models.py:244 stock/models.py:811 msgid "Quantity must be greater than zero" msgstr "" -#: order/models.py:190 +#: order/models.py:192 msgid "Part supplier must match PO supplier" msgstr "" -#: order/models.py:262 +#: order/models.py:264 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "" -#: order/models.py:322 +#: order/models.py:324 msgid "Company to which the items are being sold" msgstr "" -#: order/models.py:328 +#: order/models.py:330 msgid "Customer order reference code" msgstr "" -#: order/models.py:367 +#: order/models.py:369 msgid "SalesOrder cannot be shipped as it is not currently pending" msgstr "" -#: order/models.py:454 +#: order/models.py:456 msgid "Item quantity" msgstr "" -#: order/models.py:456 +#: order/models.py:458 msgid "Line item reference" msgstr "" -#: order/models.py:458 +#: order/models.py:460 msgid "Line item notes" msgstr "" -#: order/models.py:484 order/templates/order/order_base.html:9 +#: order/models.py:486 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:265 templates/js/order.js:139 +#: stock/templates/stock/item_base.html:259 templates/js/order.js:139 msgid "Purchase Order" msgstr "" -#: order/models.py:497 +#: order/models.py:499 msgid "Supplier part" msgstr "" -#: order/models.py:500 +#: order/models.py:502 msgid "Number of items received" msgstr "" -#: order/models.py:594 +#: order/models.py:509 stock/models.py:457 +#: stock/templates/stock/item_base.html:266 +msgid "Purchase Price" +msgstr "" + +#: order/models.py:510 +msgid "Unit purchase price" +msgstr "" + +#: order/models.py:605 msgid "Cannot allocate stock item to a line with a different part" msgstr "" -#: order/models.py:596 +#: order/models.py:607 msgid "Cannot allocate stock to a line without a part" msgstr "" -#: order/models.py:599 +#: order/models.py:610 msgid "Allocation quantity cannot exceed stock quantity" msgstr "" -#: order/models.py:609 +#: order/models.py:620 msgid "Quantity must be 1 for serialized stock item" msgstr "" -#: order/models.py:626 +#: order/models.py:636 msgid "Select stock item to allocate" msgstr "" -#: order/models.py:629 +#: order/models.py:639 msgid "Enter stock allocation quantity" msgstr "" @@ -1857,7 +1902,7 @@ msgstr "" msgid "Order Status" msgstr "" -#: order/templates/order/order_base.html:85 templates/js/order.js:161 +#: order/templates/order/order_base.html:85 templates/js/order.js:162 msgid "Supplier Reference" msgstr "" @@ -1866,7 +1911,7 @@ msgid "Issued" msgstr "" #: order/templates/order/order_base.html:111 -#: order/templates/order/purchase_order_detail.html:183 +#: order/templates/order/purchase_order_detail.html:193 #: order/templates/order/receive_parts.html:22 #: order/templates/order/sales_order_base.html:113 msgid "Received" @@ -1913,7 +1958,7 @@ msgid "Select existing purchase orders, or create new orders." msgstr "" #: order/templates/order/order_wizard/select_pos.html:31 -#: templates/js/order.js:185 templates/js/order.js:272 +#: templates/js/order.js:186 templates/js/order.js:273 msgid "Items" msgstr "" @@ -1965,21 +2010,25 @@ msgstr "" msgid "No line items found" msgstr "" -#: order/templates/order/purchase_order_detail.html:165 +#: order/templates/order/purchase_order_detail.html:166 #: order/templates/order/receive_parts.html:20 msgid "Order Code" msgstr "" -#: order/templates/order/purchase_order_detail.html:214 +#: order/templates/order/purchase_order_detail.html:184 +msgid "Unit Price" +msgstr "" + +#: order/templates/order/purchase_order_detail.html:225 #: order/templates/order/sales_order_detail.html:285 msgid "Edit line item" msgstr "" -#: order/templates/order/purchase_order_detail.html:215 +#: order/templates/order/purchase_order_detail.html:226 msgid "Delete line item" msgstr "" -#: order/templates/order/purchase_order_detail.html:220 +#: order/templates/order/purchase_order_detail.html:231 msgid "Receive line item" msgstr "" @@ -2016,7 +2065,7 @@ msgstr "" msgid "Sales Order Details" msgstr "" -#: order/templates/order/sales_order_base.html:87 templates/js/order.js:243 +#: order/templates/order/sales_order_base.html:87 templates/js/order.js:244 msgid "Customer Reference" msgstr "" @@ -2032,7 +2081,7 @@ msgid "Sales Order Items" msgstr "" #: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 stock/models.py:377 +#: order/templates/order/sales_order_detail.html:154 stock/models.py:378 #: stock/templates/stock/item_base.html:191 templates/js/build.js:402 msgid "Serial Number" msgstr "" @@ -2269,367 +2318,389 @@ msgstr "" msgid "Error reading BOM file (incorrect row size)" msgstr "" -#: part/forms.py:62 stock/forms.py:254 +#: part/forms.py:61 stock/forms.py:255 msgid "File Format" msgstr "" -#: part/forms.py:62 stock/forms.py:254 +#: part/forms.py:61 stock/forms.py:255 msgid "Select output file format" msgstr "" -#: part/forms.py:64 +#: part/forms.py:63 msgid "Cascading" msgstr "" -#: part/forms.py:64 +#: part/forms.py:63 msgid "Download cascading / multi-level BOM" msgstr "" -#: part/forms.py:66 +#: part/forms.py:65 msgid "Levels" msgstr "" -#: part/forms.py:66 +#: part/forms.py:65 msgid "Select maximum number of BOM levels to export (0 = all levels)" msgstr "" -#: part/forms.py:68 +#: part/forms.py:67 msgid "Include Parameter Data" msgstr "" -#: part/forms.py:68 +#: part/forms.py:67 msgid "Include part parameters data in exported BOM" msgstr "" -#: part/forms.py:70 +#: part/forms.py:69 msgid "Include Stock Data" msgstr "" -#: part/forms.py:70 +#: part/forms.py:69 msgid "Include part stock data in exported BOM" msgstr "" -#: part/forms.py:72 +#: part/forms.py:71 msgid "Include Supplier Data" msgstr "" -#: part/forms.py:72 +#: part/forms.py:71 msgid "Include part supplier data in exported BOM" msgstr "" -#: part/forms.py:93 part/models.py:1632 +#: part/forms.py:92 part/models.py:1715 msgid "Parent Part" msgstr "" -#: part/forms.py:94 part/templates/part/bom_duplicate.html:7 +#: part/forms.py:93 part/templates/part/bom_duplicate.html:7 msgid "Select parent part to copy BOM from" msgstr "" -#: part/forms.py:100 +#: part/forms.py:99 msgid "Clear existing BOM items" msgstr "" -#: part/forms.py:105 +#: part/forms.py:104 msgid "Confirm BOM duplication" msgstr "" -#: part/forms.py:123 +#: part/forms.py:122 msgid "Confirm that the BOM is correct" msgstr "" -#: part/forms.py:135 +#: part/forms.py:134 msgid "Select BOM file to upload" msgstr "" -#: part/forms.py:154 +#: part/forms.py:153 msgid "Related Part" msgstr "" -#: part/forms.py:173 +#: part/forms.py:172 msgid "Select part category" msgstr "" -#: part/forms.py:189 +#: part/forms.py:188 msgid "Duplicate all BOM data for this part" msgstr "" -#: part/forms.py:190 +#: part/forms.py:189 msgid "Copy BOM" msgstr "" -#: part/forms.py:195 +#: part/forms.py:194 msgid "Duplicate all parameter data for this part" msgstr "" -#: part/forms.py:196 +#: part/forms.py:195 msgid "Copy Parameters" msgstr "" -#: part/forms.py:201 +#: part/forms.py:200 msgid "Confirm part creation" msgstr "" -#: part/forms.py:298 +#: part/forms.py:205 +msgid "Include category parameter templates" +msgstr "" + +#: part/forms.py:210 +msgid "Include parent categories parameter templates" +msgstr "" + +#: part/forms.py:285 +msgid "Add parameter template to same level categories" +msgstr "" + +#: part/forms.py:289 +msgid "Add parameter template to all categories" +msgstr "" + +#: part/forms.py:331 msgid "Input quantity for price calculation" msgstr "" -#: part/forms.py:301 -msgid "Select currency for price calculation" -msgstr "" - -#: part/models.py:67 +#: part/models.py:68 msgid "Default location for parts in this category" msgstr "" -#: part/models.py:70 +#: part/models.py:71 msgid "Default keywords for parts in this category" msgstr "" -#: part/models.py:76 part/templates/part/part_app_base.html:9 +#: part/models.py:77 part/models.py:1760 +#: part/templates/part/part_app_base.html:9 msgid "Part Category" msgstr "" -#: part/models.py:77 part/templates/part/category.html:18 +#: part/models.py:78 part/templates/part/category.html:18 #: part/templates/part/category.html:89 templates/stats.html:12 msgid "Part Categories" msgstr "" -#: part/models.py:346 part/models.py:356 +#: part/models.py:408 part/models.py:418 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "" -#: part/models.py:453 +#: part/models.py:515 msgid "Next available serial numbers are" msgstr "" -#: part/models.py:457 +#: part/models.py:519 msgid "Next available serial number is" msgstr "" -#: part/models.py:462 +#: part/models.py:524 msgid "Most recent serial number is" msgstr "" -#: part/models.py:540 +#: part/models.py:603 +msgid "Duplicate IPN not allowed in part settings" +msgstr "" + +#: part/models.py:614 msgid "Part must be unique for name, IPN and revision" msgstr "" -#: part/models.py:569 part/templates/part/detail.html:19 +#: part/models.py:644 part/templates/part/detail.html:19 msgid "Part name" msgstr "" -#: part/models.py:573 +#: part/models.py:648 msgid "Is this part a template part?" msgstr "" -#: part/models.py:582 +#: part/models.py:657 msgid "Is this part a variant of another part?" msgstr "" -#: part/models.py:584 +#: part/models.py:659 msgid "Part description" msgstr "" -#: part/models.py:586 +#: part/models.py:661 msgid "Part keywords to improve visibility in search results" msgstr "" -#: part/models.py:591 +#: part/models.py:666 msgid "Part category" msgstr "" -#: part/models.py:593 +#: part/models.py:668 msgid "Internal Part Number" msgstr "" -#: part/models.py:595 +#: part/models.py:670 msgid "Part revision or version number" msgstr "" -#: part/models.py:609 +#: part/models.py:684 msgid "Where is this item normally stored?" msgstr "" -#: part/models.py:653 +#: part/models.py:728 msgid "Default supplier part" msgstr "" -#: part/models.py:656 +#: part/models.py:731 msgid "Minimum allowed stock level" msgstr "" -#: part/models.py:658 +#: part/models.py:733 msgid "Stock keeping units for this part" msgstr "" -#: part/models.py:662 part/templates/part/detail.html:158 +#: part/models.py:737 part/templates/part/detail.html:158 #: templates/js/table_filters.js:260 msgid "Assembly" msgstr "" -#: part/models.py:663 +#: part/models.py:738 msgid "Can this part be built from other parts?" msgstr "" -#: part/models.py:669 +#: part/models.py:744 msgid "Can this part be used to build other parts?" msgstr "" -#: part/models.py:675 +#: part/models.py:750 msgid "Does this part have tracking for unique items?" msgstr "" -#: part/models.py:680 +#: part/models.py:755 msgid "Can this part be purchased from external suppliers?" msgstr "" -#: part/models.py:685 +#: part/models.py:760 msgid "Can this part be sold to customers?" msgstr "" -#: part/models.py:689 part/templates/part/detail.html:215 +#: part/models.py:764 part/templates/part/detail.html:215 #: templates/js/table_filters.js:19 templates/js/table_filters.js:55 #: templates/js/table_filters.js:186 templates/js/table_filters.js:243 msgid "Active" msgstr "" -#: part/models.py:690 +#: part/models.py:765 msgid "Is this part active?" msgstr "" -#: part/models.py:694 part/templates/part/detail.html:138 +#: part/models.py:769 part/templates/part/detail.html:138 #: templates/js/table_filters.js:27 msgid "Virtual" msgstr "" -#: part/models.py:695 +#: part/models.py:770 msgid "Is this a virtual part, such as a software product or license?" msgstr "" -#: part/models.py:697 +#: part/models.py:772 msgid "Part notes - supports Markdown formatting" msgstr "" -#: part/models.py:699 +#: part/models.py:774 msgid "Stored BOM checksum" msgstr "" -#: part/models.py:1505 +#: part/models.py:1588 msgid "Test templates can only be created for trackable parts" msgstr "" -#: part/models.py:1522 +#: part/models.py:1605 msgid "Test with this name already exists for this part" msgstr "" -#: part/models.py:1541 templates/js/part.js:567 templates/js/stock.js:92 +#: part/models.py:1624 templates/js/part.js:567 templates/js/stock.js:92 msgid "Test Name" msgstr "" -#: part/models.py:1542 +#: part/models.py:1625 msgid "Enter a name for the test" msgstr "" -#: part/models.py:1547 +#: part/models.py:1630 msgid "Test Description" msgstr "" -#: part/models.py:1548 +#: part/models.py:1631 msgid "Enter description for this test" msgstr "" -#: part/models.py:1553 templates/js/part.js:576 +#: part/models.py:1636 templates/js/part.js:576 #: templates/js/table_filters.js:172 msgid "Required" msgstr "" -#: part/models.py:1554 +#: part/models.py:1637 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1559 templates/js/part.js:584 +#: part/models.py:1642 templates/js/part.js:584 msgid "Requires Value" msgstr "" -#: part/models.py:1560 +#: part/models.py:1643 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1565 templates/js/part.js:591 +#: part/models.py:1648 templates/js/part.js:591 msgid "Requires Attachment" msgstr "" -#: part/models.py:1566 +#: part/models.py:1649 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1599 +#: part/models.py:1682 msgid "Parameter template name must be unique" msgstr "" -#: part/models.py:1604 +#: part/models.py:1687 msgid "Parameter Name" msgstr "" -#: part/models.py:1606 +#: part/models.py:1689 msgid "Parameter Units" msgstr "" -#: part/models.py:1634 +#: part/models.py:1717 part/models.py:1765 +#: templates/InvenTree/settings/category.html:62 msgid "Parameter Template" msgstr "" -#: part/models.py:1636 +#: part/models.py:1719 msgid "Parameter Value" msgstr "" -#: part/models.py:1673 +#: part/models.py:1769 +msgid "Default Parameter Value" +msgstr "" + +#: part/models.py:1799 msgid "Select parent part" msgstr "" -#: part/models.py:1681 +#: part/models.py:1807 msgid "Select part to be used in BOM" msgstr "" -#: part/models.py:1687 +#: part/models.py:1813 msgid "BOM quantity for this BOM item" msgstr "" -#: part/models.py:1689 +#: part/models.py:1815 msgid "This BOM item is optional" msgstr "" -#: part/models.py:1692 +#: part/models.py:1818 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "" -#: part/models.py:1695 +#: part/models.py:1821 msgid "BOM item reference" msgstr "" -#: part/models.py:1698 +#: part/models.py:1824 msgid "BOM item notes" msgstr "" -#: part/models.py:1700 +#: part/models.py:1826 msgid "BOM line checksum" msgstr "" -#: part/models.py:1767 part/views.py:1483 part/views.py:1535 -#: stock/models.py:233 +#: part/models.py:1893 part/views.py:1502 part/views.py:1554 +#: stock/models.py:234 msgid "Quantity must be integer value for trackable parts" msgstr "" -#: part/models.py:1783 +#: part/models.py:1909 msgid "BOM Item" msgstr "" -#: part/models.py:1898 +#: part/models.py:2024 msgid "Select Related Part" msgstr "" -#: part/models.py:1930 +#: part/models.py:2056 msgid "" "Error creating relationship: check that the part is not related to itself " "and that the relationship is unique" @@ -2650,7 +2721,7 @@ msgstr "" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:72 -#: stock/templates/stock/item_base.html:273 +#: stock/templates/stock/item_base.html:274 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.js:724 #: templates/js/stock.js:695 templates/js/stock.js:944 msgid "Stock Item" @@ -2717,7 +2788,7 @@ msgstr "" msgid "Validate" msgstr "" -#: part/templates/part/bom.html:62 part/views.py:1774 +#: part/templates/part/bom.html:62 part/views.py:1793 msgid "Export Bill of Materials" msgstr "" @@ -2813,7 +2884,7 @@ msgstr "" msgid "All parts" msgstr "" -#: part/templates/part/category.html:24 part/views.py:2177 +#: part/templates/part/category.html:24 part/views.py:2184 msgid "Create new part category" msgstr "" @@ -2885,7 +2956,7 @@ msgstr "" msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:216 stock/views.py:1338 +#: part/templates/part/category.html:216 stock/views.py:1342 msgid "Create new Stock Location" msgstr "" @@ -2947,7 +3018,7 @@ msgstr "" msgid "Minimum Stock" msgstr "" -#: part/templates/part/detail.html:114 templates/js/order.js:262 +#: part/templates/part/detail.html:114 templates/js/order.js:263 msgid "Creation Date" msgstr "" @@ -2967,7 +3038,7 @@ msgstr "" msgid "Part is not a virtual part" msgstr "" -#: part/templates/part/detail.html:148 stock/forms.py:248 +#: part/templates/part/detail.html:148 stock/forms.py:249 #: templates/js/table_filters.js:23 templates/js/table_filters.js:248 msgid "Template" msgstr "" @@ -3036,17 +3107,19 @@ msgstr "" msgid "Add new parameter" msgstr "" -#: part/templates/part/params.html:15 templates/InvenTree/settings/part.html:35 +#: part/templates/part/params.html:15 +#: templates/InvenTree/settings/category.html:29 +#: templates/InvenTree/settings/part.html:38 msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:25 stock/models.py:1415 +#: part/templates/part/params.html:25 stock/models.py:1419 #: templates/js/stock.js:112 msgid "Value" msgstr "" #: part/templates/part/params.html:41 part/templates/part/related.html:41 -#: part/templates/part/supplier.html:19 users/models.py:147 +#: part/templates/part/supplier.html:19 users/models.py:152 msgid "Delete" msgstr "" @@ -3226,7 +3299,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:317 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:318 msgid "Tests" msgstr "" @@ -3254,208 +3327,220 @@ msgstr "" msgid "New Variant" msgstr "" -#: part/views.py:80 +#: part/views.py:84 msgid "Add Related Part" msgstr "" -#: part/views.py:136 +#: part/views.py:140 msgid "Delete Related Part" msgstr "" -#: part/views.py:148 +#: part/views.py:152 msgid "Add part attachment" msgstr "" -#: part/views.py:203 templates/attachment_table.html:34 +#: part/views.py:207 templates/attachment_table.html:34 msgid "Edit attachment" msgstr "" -#: part/views.py:209 +#: part/views.py:213 msgid "Part attachment updated" msgstr "" -#: part/views.py:224 +#: part/views.py:228 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:232 +#: part/views.py:236 msgid "Deleted part attachment" msgstr "" -#: part/views.py:241 +#: part/views.py:245 msgid "Create Test Template" msgstr "" -#: part/views.py:270 +#: part/views.py:274 msgid "Edit Test Template" msgstr "" -#: part/views.py:286 +#: part/views.py:290 msgid "Delete Test Template" msgstr "" -#: part/views.py:295 +#: part/views.py:299 msgid "Set Part Category" msgstr "" -#: part/views.py:345 +#: part/views.py:349 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:380 +#: part/views.py:384 msgid "Create Variant" msgstr "" -#: part/views.py:462 +#: part/views.py:466 msgid "Duplicate Part" msgstr "" -#: part/views.py:469 +#: part/views.py:473 msgid "Copied part" msgstr "" -#: part/views.py:523 part/views.py:653 +#: part/views.py:527 part/views.py:661 msgid "Possible matches exist - confirm creation of new part" msgstr "" -#: part/views.py:588 templates/js/stock.js:840 +#: part/views.py:592 templates/js/stock.js:840 msgid "Create New Part" msgstr "" -#: part/views.py:595 +#: part/views.py:599 msgid "Created new part" msgstr "" -#: part/views.py:811 +#: part/views.py:830 msgid "Part QR Code" msgstr "" -#: part/views.py:830 +#: part/views.py:849 msgid "Upload Part Image" msgstr "" -#: part/views.py:838 part/views.py:875 +#: part/views.py:857 part/views.py:894 msgid "Updated part image" msgstr "" -#: part/views.py:847 +#: part/views.py:866 msgid "Select Part Image" msgstr "" -#: part/views.py:878 +#: part/views.py:897 msgid "Part image not found" msgstr "" -#: part/views.py:889 +#: part/views.py:908 msgid "Edit Part Properties" msgstr "" -#: part/views.py:916 +#: part/views.py:935 msgid "Duplicate BOM" msgstr "" -#: part/views.py:947 +#: part/views.py:966 msgid "Confirm duplication of BOM from parent" msgstr "" -#: part/views.py:968 +#: part/views.py:987 msgid "Validate BOM" msgstr "" -#: part/views.py:991 +#: part/views.py:1010 msgid "Confirm that the BOM is valid" msgstr "" -#: part/views.py:1002 +#: part/views.py:1021 msgid "Validated Bill of Materials" msgstr "" -#: part/views.py:1136 +#: part/views.py:1155 msgid "No BOM file provided" msgstr "" -#: part/views.py:1486 +#: part/views.py:1505 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1511 part/views.py:1514 +#: part/views.py:1530 part/views.py:1533 msgid "Select valid part" msgstr "" -#: part/views.py:1520 +#: part/views.py:1539 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1558 +#: part/views.py:1577 msgid "Select a part" msgstr "" -#: part/views.py:1564 +#: part/views.py:1583 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1568 +#: part/views.py:1587 msgid "Specify quantity" msgstr "" -#: part/views.py:1824 +#: part/views.py:1843 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1833 +#: part/views.py:1852 msgid "Part was deleted" msgstr "" -#: part/views.py:1842 +#: part/views.py:1861 msgid "Part Pricing" msgstr "" -#: part/views.py:1968 +#: part/views.py:1975 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1978 +#: part/views.py:1985 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1987 +#: part/views.py:1994 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1997 +#: part/views.py:2004 msgid "Create Part Parameter" msgstr "" -#: part/views.py:2049 +#: part/views.py:2056 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:2065 +#: part/views.py:2072 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:2124 +#: part/views.py:2131 msgid "Edit Part Category" msgstr "" -#: part/views.py:2161 +#: part/views.py:2168 msgid "Delete Part Category" msgstr "" -#: part/views.py:2169 +#: part/views.py:2176 msgid "Part category was deleted" msgstr "" #: part/views.py:2232 +msgid "Create Category Parameter Template" +msgstr "" + +#: part/views.py:2335 +msgid "Edit Category Parameter Template" +msgstr "" + +#: part/views.py:2393 +msgid "Delete Category Parameter Template" +msgstr "" + +#: part/views.py:2418 msgid "Create BOM Item" msgstr "" -#: part/views.py:2300 +#: part/views.py:2486 msgid "Edit BOM item" msgstr "" -#: part/views.py:2350 +#: part/views.py:2536 msgid "Confim BOM item deletion" msgstr "" @@ -3491,291 +3576,291 @@ msgstr "" msgid "Enter unique serial numbers (or leave blank)" msgstr "" -#: stock/forms.py:191 +#: stock/forms.py:192 msgid "Label" msgstr "" -#: stock/forms.py:192 stock/forms.py:248 +#: stock/forms.py:193 stock/forms.py:249 msgid "Select test report template" msgstr "" -#: stock/forms.py:256 +#: stock/forms.py:257 msgid "Include stock items in sub locations" msgstr "" -#: stock/forms.py:291 +#: stock/forms.py:292 msgid "Stock item to install" msgstr "" -#: stock/forms.py:298 +#: stock/forms.py:299 msgid "Stock quantity to assign" msgstr "" -#: stock/forms.py:326 +#: stock/forms.py:327 msgid "Must not exceed available quantity" msgstr "" -#: stock/forms.py:336 +#: stock/forms.py:337 msgid "Destination location for uninstalled items" msgstr "" -#: stock/forms.py:338 +#: stock/forms.py:339 msgid "Add transaction note (optional)" msgstr "" -#: stock/forms.py:340 +#: stock/forms.py:341 msgid "Confirm uninstall" msgstr "" -#: stock/forms.py:340 +#: stock/forms.py:341 msgid "Confirm removal of installed stock items" msgstr "" -#: stock/forms.py:364 +#: stock/forms.py:365 msgid "Destination stock location" msgstr "" -#: stock/forms.py:366 +#: stock/forms.py:367 msgid "Add note (required)" msgstr "" -#: stock/forms.py:370 stock/views.py:916 stock/views.py:1114 +#: stock/forms.py:371 stock/views.py:920 stock/views.py:1118 msgid "Confirm stock adjustment" msgstr "" -#: stock/forms.py:370 +#: stock/forms.py:371 msgid "Confirm movement of stock items" msgstr "" -#: stock/forms.py:372 +#: stock/forms.py:373 msgid "Set Default Location" msgstr "" -#: stock/forms.py:372 +#: stock/forms.py:373 msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:178 +#: stock/models.py:179 msgid "Created stock item" msgstr "" -#: stock/models.py:214 +#: stock/models.py:215 msgid "StockItem with this serial number already exists" msgstr "" -#: stock/models.py:250 +#: stock/models.py:251 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:260 stock/models.py:269 +#: stock/models.py:261 stock/models.py:270 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:261 +#: stock/models.py:262 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:283 +#: stock/models.py:284 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:289 +#: stock/models.py:290 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:296 +#: stock/models.py:297 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:329 +#: stock/models.py:330 msgid "Parent Stock Item" msgstr "" -#: stock/models.py:338 +#: stock/models.py:339 msgid "Base part" msgstr "" -#: stock/models.py:347 +#: stock/models.py:348 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:352 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:353 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "" -#: stock/models.py:355 +#: stock/models.py:356 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:360 stock/templates/stock/item_base.html:212 +#: stock/models.py:361 stock/templates/stock/item_base.html:212 msgid "Installed In" msgstr "" -#: stock/models.py:363 +#: stock/models.py:364 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:379 +#: stock/models.py:380 msgid "Serial number for this item" msgstr "" -#: stock/models.py:391 +#: stock/models.py:392 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:395 +#: stock/models.py:396 msgid "Stock Quantity" msgstr "" -#: stock/models.py:404 +#: stock/models.py:405 msgid "Source Build" msgstr "" -#: stock/models.py:406 +#: stock/models.py:407 msgid "Build for this stock item" msgstr "" -#: stock/models.py:417 +#: stock/models.py:418 msgid "Source Purchase Order" msgstr "" -#: stock/models.py:420 +#: stock/models.py:421 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:426 +#: stock/models.py:427 msgid "Destination Sales Order" msgstr "" -#: stock/models.py:433 -msgid "Destination Build Order" -msgstr "" - -#: stock/models.py:446 +#: stock/models.py:439 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:456 stock/templates/stock/item_notes.html:14 +#: stock/models.py:449 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:507 -msgid "Assigned to Customer" +#: stock/models.py:458 +msgid "Single unit purchase price at time of purchase" msgstr "" #: stock/models.py:509 +msgid "Assigned to Customer" +msgstr "" + +#: stock/models.py:511 msgid "Manually assigned to customer" msgstr "" -#: stock/models.py:522 +#: stock/models.py:524 msgid "Returned from customer" msgstr "" -#: stock/models.py:524 +#: stock/models.py:526 msgid "Returned to location" msgstr "" -#: stock/models.py:652 +#: stock/models.py:651 msgid "Installed into stock item" msgstr "" -#: stock/models.py:660 +#: stock/models.py:659 msgid "Installed stock item" msgstr "" -#: stock/models.py:684 +#: stock/models.py:683 msgid "Uninstalled stock item" msgstr "" -#: stock/models.py:703 +#: stock/models.py:702 msgid "Uninstalled into location" msgstr "" -#: stock/models.py:807 +#: stock/models.py:802 msgid "Part is not set as trackable" msgstr "" -#: stock/models.py:813 +#: stock/models.py:808 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:819 +#: stock/models.py:814 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:822 +#: stock/models.py:817 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:825 +#: stock/models.py:820 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:857 +#: stock/models.py:852 msgid "Add serial number" msgstr "" -#: stock/models.py:860 +#: stock/models.py:855 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:971 +#: stock/models.py:966 msgid "StockItem cannot be moved as it is not in stock" msgstr "" -#: stock/models.py:1316 +#: stock/models.py:1320 msgid "Tracking entry title" msgstr "" -#: stock/models.py:1318 +#: stock/models.py:1322 msgid "Entry notes" msgstr "" -#: stock/models.py:1320 +#: stock/models.py:1324 msgid "Link to external page for further information" msgstr "" -#: stock/models.py:1380 +#: stock/models.py:1384 msgid "Value must be provided for this test" msgstr "" -#: stock/models.py:1386 +#: stock/models.py:1390 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1403 +#: stock/models.py:1407 msgid "Test" msgstr "" -#: stock/models.py:1404 +#: stock/models.py:1408 msgid "Test name" msgstr "" -#: stock/models.py:1409 +#: stock/models.py:1413 msgid "Result" msgstr "" -#: stock/models.py:1410 templates/js/table_filters.js:162 +#: stock/models.py:1414 templates/js/table_filters.js:162 msgid "Test result" msgstr "" -#: stock/models.py:1416 +#: stock/models.py:1420 msgid "Test output value" msgstr "" -#: stock/models.py:1422 +#: stock/models.py:1426 msgid "Attachment" msgstr "" -#: stock/models.py:1423 +#: stock/models.py:1427 msgid "Test result attachment" msgstr "" -#: stock/models.py:1429 +#: stock/models.py:1433 msgid "Test notes" msgstr "" @@ -3905,32 +3990,32 @@ msgstr "" msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:237 templates/js/build.js:426 +#: stock/templates/stock/item_base.html:231 templates/js/build.js:426 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:244 +#: stock/templates/stock/item_base.html:238 msgid "Barcode Identifier" msgstr "" -#: stock/templates/stock/item_base.html:258 templates/js/build.js:626 +#: stock/templates/stock/item_base.html:252 templates/js/build.js:626 #: templates/navbar.html:25 msgid "Build" msgstr "" -#: stock/templates/stock/item_base.html:272 +#: stock/templates/stock/item_base.html:273 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:297 +#: stock/templates/stock/item_base.html:298 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:302 +#: stock/templates/stock/item_base.html:303 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:306 +#: stock/templates/stock/item_base.html:307 msgid "No stocktake performed" msgstr "" @@ -4050,7 +4135,7 @@ msgstr "" msgid "The following stock items will be uninstalled" msgstr "" -#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1310 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1314 msgid "Convert Stock Item" msgstr "" @@ -4082,222 +4167,222 @@ msgstr "" msgid "Installed Items" msgstr "" -#: stock/views.py:119 +#: stock/views.py:123 msgid "Edit Stock Location" msgstr "" -#: stock/views.py:144 +#: stock/views.py:148 msgid "Stock Location QR code" msgstr "" -#: stock/views.py:163 +#: stock/views.py:167 msgid "Add Stock Item Attachment" msgstr "" -#: stock/views.py:210 +#: stock/views.py:214 msgid "Edit Stock Item Attachment" msgstr "" -#: stock/views.py:227 +#: stock/views.py:231 msgid "Delete Stock Item Attachment" msgstr "" -#: stock/views.py:244 +#: stock/views.py:248 msgid "Assign to Customer" msgstr "" -#: stock/views.py:254 +#: stock/views.py:258 msgid "Customer must be specified" msgstr "" -#: stock/views.py:278 +#: stock/views.py:282 msgid "Return to Stock" msgstr "" -#: stock/views.py:288 +#: stock/views.py:292 msgid "Specify a valid location" msgstr "" -#: stock/views.py:299 +#: stock/views.py:303 msgid "Stock item returned from customer" msgstr "" -#: stock/views.py:309 +#: stock/views.py:313 msgid "Select Label Template" msgstr "" -#: stock/views.py:332 +#: stock/views.py:336 msgid "Select valid label" msgstr "" -#: stock/views.py:396 +#: stock/views.py:400 msgid "Delete All Test Data" msgstr "" -#: stock/views.py:412 +#: stock/views.py:416 msgid "Confirm test data deletion" msgstr "" -#: stock/views.py:432 +#: stock/views.py:436 msgid "Add Test Result" msgstr "" -#: stock/views.py:473 +#: stock/views.py:477 msgid "Edit Test Result" msgstr "" -#: stock/views.py:491 +#: stock/views.py:495 msgid "Delete Test Result" msgstr "" -#: stock/views.py:503 +#: stock/views.py:507 msgid "Select Test Report Template" msgstr "" -#: stock/views.py:518 +#: stock/views.py:522 msgid "Select valid template" msgstr "" -#: stock/views.py:571 +#: stock/views.py:575 msgid "Stock Export Options" msgstr "" -#: stock/views.py:693 +#: stock/views.py:697 msgid "Stock Item QR Code" msgstr "" -#: stock/views.py:719 +#: stock/views.py:723 msgid "Install Stock Item" msgstr "" -#: stock/views.py:819 +#: stock/views.py:823 msgid "Uninstall Stock Items" msgstr "" -#: stock/views.py:927 +#: stock/views.py:931 msgid "Uninstalled stock items" msgstr "" -#: stock/views.py:952 +#: stock/views.py:956 msgid "Adjust Stock" msgstr "" -#: stock/views.py:1062 +#: stock/views.py:1066 msgid "Move Stock Items" msgstr "" -#: stock/views.py:1063 +#: stock/views.py:1067 msgid "Count Stock Items" msgstr "" -#: stock/views.py:1064 +#: stock/views.py:1068 msgid "Remove From Stock" msgstr "" -#: stock/views.py:1065 +#: stock/views.py:1069 msgid "Add Stock Items" msgstr "" -#: stock/views.py:1066 +#: stock/views.py:1070 msgid "Delete Stock Items" msgstr "" -#: stock/views.py:1094 +#: stock/views.py:1098 msgid "Must enter integer value" msgstr "" -#: stock/views.py:1099 +#: stock/views.py:1103 msgid "Quantity must be positive" msgstr "" -#: stock/views.py:1106 +#: stock/views.py:1110 #, python-brace-format msgid "Quantity must not exceed {x}" msgstr "" -#: stock/views.py:1185 +#: stock/views.py:1189 #, python-brace-format msgid "Added stock to {n} items" msgstr "" -#: stock/views.py:1200 +#: stock/views.py:1204 #, python-brace-format msgid "Removed stock from {n} items" msgstr "" -#: stock/views.py:1213 +#: stock/views.py:1217 #, python-brace-format msgid "Counted stock for {n} items" msgstr "" -#: stock/views.py:1241 +#: stock/views.py:1245 msgid "No items were moved" msgstr "" -#: stock/views.py:1244 +#: stock/views.py:1248 #, python-brace-format msgid "Moved {n} items to {dest}" msgstr "" -#: stock/views.py:1263 +#: stock/views.py:1267 #, python-brace-format msgid "Deleted {n} stock items" msgstr "" -#: stock/views.py:1275 +#: stock/views.py:1279 msgid "Edit Stock Item" msgstr "" -#: stock/views.py:1360 +#: stock/views.py:1364 msgid "Serialize Stock" msgstr "" -#: stock/views.py:1454 templates/js/build.js:210 +#: stock/views.py:1458 templates/js/build.js:210 msgid "Create new Stock Item" msgstr "" -#: stock/views.py:1555 +#: stock/views.py:1559 msgid "Duplicate Stock Item" msgstr "" -#: stock/views.py:1621 +#: stock/views.py:1634 msgid "Invalid quantity" msgstr "" -#: stock/views.py:1624 +#: stock/views.py:1637 msgid "Quantity cannot be less than zero" msgstr "" -#: stock/views.py:1628 +#: stock/views.py:1641 msgid "Invalid part selection" msgstr "" -#: stock/views.py:1676 +#: stock/views.py:1689 #, python-brace-format msgid "Created {n} new stock items" msgstr "" -#: stock/views.py:1695 stock/views.py:1711 +#: stock/views.py:1708 stock/views.py:1724 msgid "Created new stock item" msgstr "" -#: stock/views.py:1730 +#: stock/views.py:1743 msgid "Delete Stock Location" msgstr "" -#: stock/views.py:1744 +#: stock/views.py:1757 msgid "Delete Stock Item" msgstr "" -#: stock/views.py:1756 +#: stock/views.py:1769 msgid "Delete Stock Tracking Entry" msgstr "" -#: stock/views.py:1775 +#: stock/views.py:1788 msgid "Edit Stock Tracking Entry" msgstr "" -#: stock/views.py:1785 +#: stock/views.py:1798 msgid "Add Stock Tracking Entry" msgstr "" @@ -4369,16 +4454,30 @@ msgstr "" msgid "Build Order Settings" msgstr "" -#: templates/InvenTree/settings/currency.html:5 -msgid "General Settings" +#: templates/InvenTree/settings/category.html:9 +msgid "Category Settings" msgstr "" -#: templates/InvenTree/settings/currency.html:14 -msgid "Currencies" +#: templates/InvenTree/settings/category.html:25 +msgid "Category Parameter Templates" msgstr "" -#: templates/InvenTree/settings/currency.html:18 -msgid "New Currency" +#: templates/InvenTree/settings/category.html:52 +msgid "No category parameter templates found" +msgstr "" + +#: templates/InvenTree/settings/category.html:67 +msgid "Default Value" +msgstr "" + +#: templates/InvenTree/settings/category.html:70 +#: templates/InvenTree/settings/part.html:75 +msgid "Edit Template" +msgstr "" + +#: templates/InvenTree/settings/category.html:71 +#: templates/InvenTree/settings/part.html:76 +msgid "Delete Template" msgstr "" #: templates/InvenTree/settings/global.html:10 @@ -4393,22 +4492,14 @@ msgstr "" msgid "Part Options" msgstr "" -#: templates/InvenTree/settings/part.html:31 +#: templates/InvenTree/settings/part.html:34 msgid "Part Parameter Templates" msgstr "" -#: templates/InvenTree/settings/part.html:52 +#: templates/InvenTree/settings/part.html:55 msgid "No part parameter templates found" msgstr "" -#: templates/InvenTree/settings/part.html:72 -msgid "Edit Template" -msgstr "" - -#: templates/InvenTree/settings/part.html:73 -msgid "Delete Template" -msgstr "" - #: templates/InvenTree/settings/po.html:9 msgid "Purchase Order Settings" msgstr "" @@ -4456,7 +4547,7 @@ msgid "Global" msgstr "" #: templates/InvenTree/settings/tabs.html:19 -msgid "Currency" +msgid "Categories" msgstr "" #: templates/InvenTree/settings/theme.html:10 @@ -4770,15 +4861,15 @@ msgstr "" msgid "No purchase orders found" msgstr "" -#: templates/js/order.js:180 templates/js/stock.js:677 +#: templates/js/order.js:181 templates/js/stock.js:677 msgid "Date" msgstr "" -#: templates/js/order.js:210 +#: templates/js/order.js:211 msgid "No sales orders found" msgstr "" -#: templates/js/order.js:267 +#: templates/js/order.js:268 msgid "Shipment Date" msgstr "" @@ -5188,38 +5279,38 @@ msgstr "" msgid "Important dates" msgstr "" -#: users/models.py:130 +#: users/models.py:135 msgid "Permission set" msgstr "" -#: users/models.py:138 +#: users/models.py:143 msgid "Group" msgstr "" -#: users/models.py:141 +#: users/models.py:146 msgid "View" msgstr "" -#: users/models.py:141 +#: users/models.py:146 msgid "Permission to view items" msgstr "" -#: users/models.py:143 +#: users/models.py:148 msgid "Add" msgstr "" -#: users/models.py:143 +#: users/models.py:148 msgid "Permission to add items" msgstr "" -#: users/models.py:145 +#: users/models.py:150 msgid "Change" msgstr "" -#: users/models.py:145 +#: users/models.py:150 msgid "Permissions to edit items" msgstr "" -#: users/models.py:147 +#: users/models.py:152 msgid "Permission to delete items" msgstr "" diff --git a/InvenTree/locale/es/LC_MESSAGES/django.po b/InvenTree/locale/es/LC_MESSAGES/django.po index c4c14849c2..f6cb0f4604 100644 --- a/InvenTree/locale/es/LC_MESSAGES/django.po +++ b/InvenTree/locale/es/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-11-09 12:47+0000\n" +"POT-Creation-Date: 2020-11-12 22:05+1100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -26,27 +26,31 @@ msgstr "" msgid "No matching action found" msgstr "" -#: InvenTree/forms.py:130 build/forms.py:82 build/forms.py:170 +#: InvenTree/forms.py:108 build/forms.py:82 build/forms.py:170 msgid "Confirm" msgstr "" -#: InvenTree/forms.py:146 +#: InvenTree/forms.py:124 msgid "Confirm item deletion" msgstr "" -#: InvenTree/forms.py:178 +#: InvenTree/forms.py:156 msgid "Enter new password" msgstr "" -#: InvenTree/forms.py:185 +#: InvenTree/forms.py:163 msgid "Confirm new password" msgstr "" -#: InvenTree/forms.py:220 +#: InvenTree/forms.py:198 msgid "Apply Theme" msgstr "" -#: InvenTree/helpers.py:361 order/models.py:187 order/models.py:269 +#: InvenTree/forms.py:228 +msgid "Select Category" +msgstr "" + +#: InvenTree/helpers.py:361 order/models.py:189 order/models.py:271 msgid "Invalid quantity provided" msgstr "" @@ -99,19 +103,19 @@ msgstr "" msgid "Description (optional)" msgstr "" -#: InvenTree/settings.py:348 +#: InvenTree/settings.py:354 msgid "English" msgstr "" -#: InvenTree/settings.py:349 +#: InvenTree/settings.py:355 msgid "German" msgstr "" -#: InvenTree/settings.py:350 +#: InvenTree/settings.py:356 msgid "French" msgstr "" -#: InvenTree/settings.py:351 +#: InvenTree/settings.py:357 msgid "Polish" msgstr "" @@ -172,57 +176,61 @@ msgstr "" msgid "Production" msgstr "" -#: InvenTree/validators.py:39 +#: InvenTree/validators.py:22 +msgid "Not a valid currency code" +msgstr "" + +#: InvenTree/validators.py:50 msgid "Invalid character in part name" msgstr "" -#: InvenTree/validators.py:52 +#: InvenTree/validators.py:63 msgid "IPN must match regex pattern" msgstr "" -#: InvenTree/validators.py:66 InvenTree/validators.py:80 -#: InvenTree/validators.py:94 +#: InvenTree/validators.py:77 InvenTree/validators.py:91 +#: InvenTree/validators.py:105 msgid "Reference must match pattern" msgstr "" -#: InvenTree/validators.py:102 +#: InvenTree/validators.py:113 #, python-brace-format msgid "Illegal character in name ({x})" msgstr "" -#: InvenTree/validators.py:121 InvenTree/validators.py:137 +#: InvenTree/validators.py:132 InvenTree/validators.py:148 msgid "Overage value must not be negative" msgstr "" -#: InvenTree/validators.py:139 +#: InvenTree/validators.py:150 msgid "Overage must not exceed 100%" msgstr "" -#: InvenTree/validators.py:146 +#: InvenTree/validators.py:157 msgid "Overage must be an integer value or a percentage" msgstr "" -#: InvenTree/views.py:493 +#: InvenTree/views.py:494 msgid "Delete Item" msgstr "" -#: InvenTree/views.py:542 +#: InvenTree/views.py:543 msgid "Check box to confirm item deletion" msgstr "" -#: InvenTree/views.py:557 templates/InvenTree/settings/user.html:18 +#: InvenTree/views.py:558 templates/InvenTree/settings/user.html:18 msgid "Edit User Information" msgstr "" -#: InvenTree/views.py:568 templates/InvenTree/settings/user.html:22 +#: InvenTree/views.py:569 templates/InvenTree/settings/user.html:22 msgid "Set Password" msgstr "" -#: InvenTree/views.py:587 +#: InvenTree/views.py:588 msgid "Password fields must match" msgstr "" -#: InvenTree/views.py:757 +#: InvenTree/views.py:794 msgid "Database Statistics" msgstr "" @@ -272,15 +280,15 @@ msgstr "" #: build/forms.py:70 build/templates/build/auto_allocate.html:17 #: build/templates/build/build_base.html:78 -#: build/templates/build/detail.html:29 -#: company/templates/company/supplier_part_pricing.html:75 +#: build/templates/build/detail.html:29 common/models.py:488 +#: company/forms.py:112 company/templates/company/supplier_part_pricing.html:75 #: order/templates/order/order_wizard/select_parts.html:32 -#: order/templates/order/purchase_order_detail.html:178 +#: order/templates/order/purchase_order_detail.html:179 #: order/templates/order/sales_order_detail.html:74 #: order/templates/order/sales_order_detail.html:156 #: part/templates/part/allocation.html:16 #: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:80 stock/forms.py:297 +#: part/templates/part/sale_prices.html:82 stock/forms.py:298 #: stock/templates/stock/item_base.html:40 #: stock/templates/stock/item_base.html:46 #: stock/templates/stock/item_base.html:197 @@ -345,7 +353,6 @@ msgstr "" #: build/models.py:56 build/templates/build/build_base.html:8 #: build/templates/build/build_base.html:35 #: part/templates/part/allocation.html:20 -#: stock/templates/stock/item_base.html:227 msgid "Build Order" msgstr "" @@ -360,19 +367,20 @@ msgstr "" msgid "Build Order Reference" msgstr "" -#: build/models.py:73 order/templates/order/purchase_order_detail.html:173 +#: build/models.py:73 order/templates/order/purchase_order_detail.html:174 #: templates/js/bom.js:181 templates/js/build.js:493 msgid "Reference" msgstr "" #: build/models.py:80 build/templates/build/detail.html:19 +#: company/templates/company/detail.html:23 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 -#: order/templates/order/purchase_order_detail.html:160 +#: order/templates/order/purchase_order_detail.html:161 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 #: templates/InvenTree/search.html:147 templates/js/bom.js:174 #: templates/js/bom.js:499 templates/js/build.js:642 templates/js/company.js:56 -#: templates/js/order.js:167 templates/js/order.js:249 templates/js/part.js:188 +#: templates/js/order.js:168 templates/js/order.js:250 templates/js/part.js:188 #: templates/js/part.js:271 templates/js/part.js:391 templates/js/part.js:572 #: templates/js/stock.js:494 templates/js/stock.js:706 msgid "Description" @@ -393,10 +401,10 @@ msgstr "" #: build/models.py:97 build/templates/build/auto_allocate.html:16 #: build/templates/build/build_base.html:73 -#: build/templates/build/detail.html:24 order/models.py:519 +#: build/templates/build/detail.html:24 order/models.py:530 #: order/templates/order/order_wizard/select_parts.html:30 #: order/templates/order/purchase_order_detail.html:148 -#: order/templates/order/receive_parts.html:19 part/models.py:294 +#: order/templates/order/receive_parts.html:19 part/models.py:315 #: part/templates/part/part_app_base.html:7 part/templates/part/related.html:26 #: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 #: templates/js/barcode.js:336 templates/js/bom.js:147 templates/js/bom.js:484 @@ -460,7 +468,7 @@ msgstr "" msgid "Build status code" msgstr "" -#: build/models.py:157 stock/models.py:389 +#: build/models.py:157 stock/models.py:390 msgid "Batch Code" msgstr "" @@ -472,20 +480,20 @@ msgstr "" #: company/templates/company/supplier_part_base.html:68 #: company/templates/company/supplier_part_detail.html:24 #: part/templates/part/detail.html:80 part/templates/part/part_base.html:102 -#: stock/models.py:383 stock/templates/stock/item_base.html:279 +#: stock/models.py:384 stock/templates/stock/item_base.html:280 msgid "External Link" msgstr "" -#: build/models.py:177 part/models.py:597 stock/models.py:385 +#: build/models.py:177 part/models.py:672 stock/models.py:386 msgid "Link to external URL" msgstr "" -#: build/models.py:181 build/templates/build/tabs.html:23 company/models.py:314 +#: build/models.py:181 build/templates/build/tabs.html:23 company/models.py:344 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 -#: order/templates/order/purchase_order_detail.html:203 +#: order/templates/order/purchase_order_detail.html:213 #: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:73 -#: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:455 -#: stock/models.py:1428 stock/templates/stock/tabs.html:26 +#: stock/forms.py:307 stock/forms.py:339 stock/forms.py:367 stock/models.py:448 +#: stock/models.py:1432 stock/templates/stock/tabs.html:26 #: templates/js/barcode.js:391 templates/js/bom.js:250 #: templates/js/stock.js:116 templates/js/stock.js:578 msgid "Notes" @@ -529,11 +537,11 @@ msgstr "" msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" -#: build/models.py:908 order/models.py:603 +#: build/models.py:908 order/models.py:614 msgid "StockItem is over-allocated" msgstr "" -#: build/models.py:912 order/models.py:606 +#: build/models.py:912 order/models.py:617 msgid "Allocation quantity must be greater than zero" msgstr "" @@ -549,11 +557,11 @@ msgstr "" msgid "Source stock item" msgstr "" -#: build/models.py:976 +#: build/models.py:975 msgid "Stock quantity to allocate to build" msgstr "" -#: build/models.py:984 +#: build/models.py:983 msgid "Destination stock item" msgstr "" @@ -618,8 +626,8 @@ msgid "" "The following stock items will be allocated to the specified build output" msgstr "" -#: build/templates/build/auto_allocate.html:18 stock/forms.py:336 -#: stock/templates/stock/item_base.html:233 +#: build/templates/build/auto_allocate.html:18 stock/forms.py:337 +#: stock/templates/stock/item_base.html:227 #: stock/templates/stock/stock_adjust.html:17 #: templates/InvenTree/search.html:183 templates/js/barcode.js:337 #: templates/js/build.js:418 templates/js/stock.js:570 @@ -675,9 +683,9 @@ msgstr "" #: build/templates/build/build_base.html:83 #: build/templates/build/detail.html:57 #: order/templates/order/receive_parts.html:24 -#: stock/templates/stock/item_base.html:311 templates/InvenTree/search.html:175 +#: stock/templates/stock/item_base.html:312 templates/InvenTree/search.html:175 #: templates/js/barcode.js:42 templates/js/build.js:675 -#: templates/js/order.js:172 templates/js/order.js:254 +#: templates/js/order.js:173 templates/js/order.js:255 #: templates/js/stock.js:557 templates/js/stock.js:961 msgid "Status" msgstr "" @@ -688,13 +696,13 @@ msgid "Progress" msgstr "" #: build/templates/build/build_base.html:101 -#: build/templates/build/detail.html:82 order/models.py:517 +#: build/templates/build/detail.html:82 order/models.py:528 #: order/templates/order/sales_order_base.html:9 #: order/templates/order/sales_order_base.html:33 #: order/templates/order/sales_order_notes.html:10 #: order/templates/order/sales_order_ship.html:25 #: part/templates/part/allocation.html:27 -#: stock/templates/stock/item_base.html:221 templates/js/order.js:221 +#: stock/templates/stock/item_base.html:221 templates/js/order.js:222 msgid "Sales Order" msgstr "" @@ -786,7 +794,7 @@ msgstr "" msgid "Stock can be taken from any available location." msgstr "" -#: build/templates/build/detail.html:44 stock/forms.py:364 +#: build/templates/build/detail.html:44 stock/forms.py:365 msgid "Destination" msgstr "" @@ -795,7 +803,7 @@ msgid "Destination location not specified" msgstr "" #: build/templates/build/detail.html:68 -#: stock/templates/stock/item_base.html:251 templates/js/stock.js:565 +#: stock/templates/stock/item_base.html:245 templates/js/stock.js:565 #: templates/js/stock.js:968 templates/js/table_filters.js:80 #: templates/js/table_filters.js:151 msgid "Batch" @@ -887,7 +895,7 @@ msgstr "" msgid "Create Build Output" msgstr "" -#: build/views.py:207 stock/models.py:832 stock/views.py:1647 +#: build/views.py:207 stock/models.py:827 stock/views.py:1660 msgid "Serial numbers already exist" msgstr "" @@ -903,7 +911,7 @@ msgstr "" msgid "Confirm unallocation of build stock" msgstr "" -#: build/views.py:303 build/views.py:388 stock/views.py:413 +#: build/views.py:303 build/views.py:388 stock/views.py:417 msgid "Check the confirmation box" msgstr "" @@ -992,8 +1000,8 @@ msgstr "" msgid "Add Build Order Attachment" msgstr "" -#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:164 -#: stock/views.py:176 +#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:168 +#: stock/views.py:180 msgid "Added attachment" msgstr "" @@ -1009,162 +1017,179 @@ msgstr "" msgid "Delete Attachment" msgstr "" -#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:234 +#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:238 msgid "Deleted attachment" msgstr "" -#: common/models.py:51 +#: common/models.py:56 msgid "InvenTree Instance Name" msgstr "" -#: common/models.py:53 +#: common/models.py:58 msgid "String descriptor for the server instance" msgstr "" -#: common/models.py:57 company/models.py:89 company/models.py:90 +#: common/models.py:62 company/models.py:95 company/models.py:96 msgid "Company name" msgstr "" -#: common/models.py:58 +#: common/models.py:63 msgid "Internal company name" msgstr "" -#: common/models.py:63 -msgid "IPN Regex" -msgstr "" - -#: common/models.py:64 -msgid "Regular expression pattern for matching Part IPN" -msgstr "" - #: common/models.py:68 -msgid "Copy Part BOM Data" +msgid "Default Currency" msgstr "" #: common/models.py:69 -msgid "Copy BOM data by default when duplicating a part" +msgid "Default currency" msgstr "" #: common/models.py:75 -msgid "Copy Part Parameter Data" +msgid "IPN Regex" msgstr "" #: common/models.py:76 +msgid "Regular expression pattern for matching Part IPN" +msgstr "" + +#: common/models.py:80 +msgid "Allow Duplicate IPN" +msgstr "" + +#: common/models.py:81 +msgid "Allow multiple parts to share the same IPN" +msgstr "" + +#: common/models.py:87 +msgid "Copy Part BOM Data" +msgstr "" + +#: common/models.py:88 +msgid "Copy BOM data by default when duplicating a part" +msgstr "" + +#: common/models.py:94 +msgid "Copy Part Parameter Data" +msgstr "" + +#: common/models.py:95 msgid "Copy parameter data by default when duplicating a part" msgstr "" -#: common/models.py:82 +#: common/models.py:101 msgid "Copy Part Test Data" msgstr "" -#: common/models.py:83 +#: common/models.py:102 msgid "Copy test data by default when duplicating a part" msgstr "" -#: common/models.py:89 part/models.py:668 part/templates/part/detail.html:168 +#: common/models.py:108 +msgid "Copy Category Parameter Templates" +msgstr "" + +#: common/models.py:109 +msgid "Copy category parameter templates when creating a part" +msgstr "" + +#: common/models.py:115 part/models.py:743 part/templates/part/detail.html:168 #: templates/js/table_filters.js:264 msgid "Component" msgstr "" -#: common/models.py:90 +#: common/models.py:116 msgid "Parts can be used as sub-components by default" msgstr "" -#: common/models.py:96 part/models.py:679 part/templates/part/detail.html:188 +#: common/models.py:122 part/models.py:754 part/templates/part/detail.html:188 msgid "Purchaseable" msgstr "" -#: common/models.py:97 +#: common/models.py:123 msgid "Parts are purchaseable by default" msgstr "" -#: common/models.py:103 part/models.py:684 part/templates/part/detail.html:198 +#: common/models.py:129 part/models.py:759 part/templates/part/detail.html:198 #: templates/js/table_filters.js:272 msgid "Salable" msgstr "" -#: common/models.py:104 +#: common/models.py:130 msgid "Parts are salable by default" msgstr "" -#: common/models.py:110 part/models.py:674 part/templates/part/detail.html:178 +#: common/models.py:136 part/models.py:749 part/templates/part/detail.html:178 #: templates/js/table_filters.js:31 templates/js/table_filters.js:276 msgid "Trackable" msgstr "" -#: common/models.py:111 +#: common/models.py:137 msgid "Parts are trackable by default" msgstr "" -#: common/models.py:117 +#: common/models.py:143 msgid "Build Order Reference Prefix" msgstr "" -#: common/models.py:118 +#: common/models.py:144 msgid "Prefix value for build order reference" msgstr "" -#: common/models.py:123 +#: common/models.py:149 msgid "Build Order Reference Regex" msgstr "" -#: common/models.py:124 +#: common/models.py:150 msgid "Regular expression pattern for matching build order reference" msgstr "" -#: common/models.py:128 +#: common/models.py:154 msgid "Sales Order Reference Prefix" msgstr "" -#: common/models.py:129 +#: common/models.py:155 msgid "Prefix value for sales order reference" msgstr "" -#: common/models.py:133 +#: common/models.py:159 msgid "Purchase Order Reference Prefix" msgstr "" -#: common/models.py:134 +#: common/models.py:160 msgid "Prefix value for purchase order reference" msgstr "" -#: common/models.py:312 +#: common/models.py:373 msgid "Settings key (must be unique - case insensitive" msgstr "" -#: common/models.py:314 +#: common/models.py:375 msgid "Settings value" msgstr "" -#: common/models.py:366 +#: common/models.py:431 msgid "Value must be a boolean value" msgstr "" -#: common/models.py:380 +#: common/models.py:445 msgid "Key string must be unique" msgstr "" -#: common/models.py:419 -msgid "Currency Symbol e.g. $" +#: common/models.py:489 company/forms.py:113 +msgid "Price break quantity" msgstr "" -#: common/models.py:421 -msgid "Currency Suffix e.g. AUD" +#: common/models.py:497 company/templates/company/supplier_part_pricing.html:80 +#: part/templates/part/sale_prices.html:87 templates/js/bom.js:234 +msgid "Price" msgstr "" -#: common/models.py:423 -msgid "Currency Description" +#: common/models.py:498 +msgid "Unit price at specified quantity" msgstr "" -#: common/models.py:425 -msgid "Currency Value" -msgstr "" - -#: common/models.py:427 -msgid "Use this currency as the base currency" -msgstr "" - -#: common/models.py:510 +#: common/models.py:521 msgid "Default" msgstr "" @@ -1172,125 +1197,129 @@ msgstr "" msgid "Current value" msgstr "" -#: common/views.py:23 -msgid "Create new Currency" -msgstr "" - -#: common/views.py:31 -msgid "Edit Currency" -msgstr "" - -#: common/views.py:38 -msgid "Delete Currency" -msgstr "" - -#: common/views.py:49 +#: common/views.py:25 msgid "Change Setting" msgstr "" -#: company/models.py:92 -msgid "Company description" +#: company/forms.py:37 company/models.py:139 +msgid "Default currency used for this company" msgstr "" -#: company/models.py:92 -msgid "Description of the company" +#: company/forms.py:80 +msgid "Single Price" msgstr "" -#: company/models.py:94 company/templates/company/company_base.html:57 -#: templates/js/company.js:61 -msgid "Website" -msgstr "" - -#: company/models.py:94 -msgid "Company website URL" -msgstr "" - -#: company/models.py:97 company/templates/company/company_base.html:64 -msgid "Address" +#: company/forms.py:82 +msgid "Single quantity price" msgstr "" #: company/models.py:98 +msgid "Company description" +msgstr "" + +#: company/models.py:98 +msgid "Description of the company" +msgstr "" + +#: company/models.py:100 company/templates/company/company_base.html:57 +#: company/templates/company/detail.html:28 templates/js/company.js:61 +msgid "Website" +msgstr "" + +#: company/models.py:100 +msgid "Company website URL" +msgstr "" + +#: company/models.py:103 company/templates/company/company_base.html:64 +msgid "Address" +msgstr "" + +#: company/models.py:104 msgid "Company address" msgstr "" -#: company/models.py:101 +#: company/models.py:107 msgid "Phone number" msgstr "" -#: company/models.py:102 +#: company/models.py:108 msgid "Contact phone number" msgstr "" -#: company/models.py:105 company/templates/company/company_base.html:78 +#: company/models.py:111 company/templates/company/company_base.html:78 msgid "Email" msgstr "" -#: company/models.py:105 +#: company/models.py:111 msgid "Contact email address" msgstr "" -#: company/models.py:108 company/templates/company/company_base.html:85 +#: company/models.py:114 company/templates/company/company_base.html:85 msgid "Contact" msgstr "" -#: company/models.py:109 +#: company/models.py:115 msgid "Point of contact" msgstr "" -#: company/models.py:111 +#: company/models.py:117 msgid "Link to external company information" msgstr "" -#: company/models.py:123 +#: company/models.py:129 msgid "Do you sell items to this company?" msgstr "" -#: company/models.py:125 +#: company/models.py:131 msgid "Do you purchase items from this company?" msgstr "" -#: company/models.py:127 +#: company/models.py:133 msgid "Does this company manufacture parts?" msgstr "" -#: company/models.py:283 stock/models.py:337 +#: company/models.py:137 company/templates/company/detail.html:37 +msgid "Currency" +msgstr "" + +#: company/models.py:313 stock/models.py:338 #: stock/templates/stock/item_base.html:177 msgid "Base Part" msgstr "" -#: company/models.py:288 +#: company/models.py:318 msgid "Select part" msgstr "" -#: company/models.py:294 +#: company/models.py:324 msgid "Select supplier" msgstr "" -#: company/models.py:297 +#: company/models.py:327 msgid "Supplier stock keeping unit" msgstr "" -#: company/models.py:304 +#: company/models.py:334 msgid "Select manufacturer" msgstr "" -#: company/models.py:308 +#: company/models.py:338 msgid "Manufacturer part number" msgstr "" -#: company/models.py:310 +#: company/models.py:340 msgid "URL for external supplier part link" msgstr "" -#: company/models.py:312 +#: company/models.py:342 msgid "Supplier part description" msgstr "" -#: company/models.py:316 +#: company/models.py:346 msgid "Minimum charge (e.g. stocking fee)" msgstr "" -#: company/models.py:318 +#: company/models.py:348 msgid "Part packaging" msgstr "" @@ -1313,27 +1342,39 @@ msgstr "" msgid "Phone" msgstr "" -#: company/templates/company/detail.html:16 +#: company/templates/company/detail.html:18 +msgid "Company Name" +msgstr "" + +#: company/templates/company/detail.html:31 +msgid "No website specified" +msgstr "" + +#: company/templates/company/detail.html:40 +msgid "Uses default currency" +msgstr "" + +#: company/templates/company/detail.html:52 #: company/templates/company/supplier_part_base.html:84 #: company/templates/company/supplier_part_detail.html:30 part/bom.py:172 #: templates/js/company.js:44 templates/js/company.js:188 msgid "Manufacturer" msgstr "" -#: company/templates/company/detail.html:21 +#: company/templates/company/detail.html:57 #: company/templates/company/supplier_part_base.html:74 #: company/templates/company/supplier_part_detail.html:21 #: order/templates/order/order_base.html:79 #: order/templates/order/order_wizard/select_pos.html:30 part/bom.py:170 -#: stock/templates/stock/item_base.html:286 templates/js/company.js:48 -#: templates/js/company.js:164 templates/js/order.js:154 +#: stock/templates/stock/item_base.html:287 templates/js/company.js:48 +#: templates/js/company.js:164 templates/js/order.js:155 msgid "Supplier" msgstr "" -#: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:81 stock/models.py:372 -#: stock/models.py:373 stock/templates/stock/item_base.html:204 -#: templates/js/company.js:40 templates/js/order.js:236 +#: company/templates/company/detail.html:62 +#: order/templates/order/sales_order_base.html:81 stock/models.py:373 +#: stock/models.py:374 stock/templates/stock/item_base.html:204 +#: templates/js/company.js:40 templates/js/order.js:237 msgid "Customer" msgstr "" @@ -1380,21 +1421,21 @@ msgstr "" msgid "Create new Part" msgstr "" -#: company/templates/company/detail_part.html:69 company/views.py:53 +#: company/templates/company/detail_part.html:69 company/views.py:56 #: part/templates/part/supplier.html:47 msgid "New Supplier" msgstr "" -#: company/templates/company/detail_part.html:70 company/views.py:192 +#: company/templates/company/detail_part.html:70 company/views.py:195 msgid "Create new Supplier" msgstr "" -#: company/templates/company/detail_part.html:75 company/views.py:60 +#: company/templates/company/detail_part.html:75 company/views.py:63 #: part/templates/part/supplier.html:53 msgid "New Manufacturer" msgstr "" -#: company/templates/company/detail_part.html:76 company/views.py:195 +#: company/templates/company/detail_part.html:76 company/views.py:198 msgid "Create new Manufacturer" msgstr "" @@ -1464,8 +1505,8 @@ msgid "New Sales Order" msgstr "" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:346 -#: stock/templates/stock/item_base.html:291 templates/js/company.js:180 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:347 +#: stock/templates/stock/item_base.html:292 templates/js/company.js:180 msgid "Supplier Part" msgstr "" @@ -1521,28 +1562,23 @@ msgstr "" msgid "Pricing Information" msgstr "" -#: company/templates/company/supplier_part_pricing.html:17 company/views.py:410 -#: part/templates/part/sale_prices.html:13 part/views.py:2360 +#: company/templates/company/supplier_part_pricing.html:17 company/views.py:459 +#: part/templates/part/sale_prices.html:14 part/views.py:2546 msgid "Add Price Break" msgstr "" #: company/templates/company/supplier_part_pricing.html:36 -#: part/templates/part/sale_prices.html:41 +#: part/templates/part/sale_prices.html:43 msgid "No price break information found" msgstr "" -#: company/templates/company/supplier_part_pricing.html:80 -#: part/templates/part/sale_prices.html:85 templates/js/bom.js:234 -msgid "Price" -msgstr "" - -#: company/templates/company/supplier_part_pricing.html:94 -#: part/templates/part/sale_prices.html:99 +#: company/templates/company/supplier_part_pricing.html:87 +#: part/templates/part/sale_prices.html:94 msgid "Edit price break" msgstr "" -#: company/templates/company/supplier_part_pricing.html:95 -#: part/templates/part/sale_prices.html:100 +#: company/templates/company/supplier_part_pricing.html:88 +#: part/templates/part/sale_prices.html:95 msgid "Delete price break" msgstr "" @@ -1568,7 +1604,7 @@ msgid "Orders" msgstr "" #: company/templates/company/tabs.html:9 -#: order/templates/order/receive_parts.html:14 part/models.py:295 +#: order/templates/order/receive_parts.html:14 part/models.py:316 #: part/templates/part/cat_link.html:7 part/templates/part/category.html:94 #: part/templates/part/category_tabs.html:6 #: templates/InvenTree/settings/tabs.html:22 templates/navbar.html:19 @@ -1576,88 +1612,88 @@ msgstr "" msgid "Parts" msgstr "" -#: company/views.py:52 part/templates/part/tabs.html:42 +#: company/views.py:55 part/templates/part/tabs.html:42 #: templates/navbar.html:31 msgid "Suppliers" msgstr "" -#: company/views.py:59 templates/navbar.html:32 +#: company/views.py:62 templates/navbar.html:32 msgid "Manufacturers" msgstr "" -#: company/views.py:66 templates/navbar.html:41 +#: company/views.py:69 templates/navbar.html:41 msgid "Customers" msgstr "" -#: company/views.py:67 +#: company/views.py:70 msgid "New Customer" msgstr "" -#: company/views.py:75 +#: company/views.py:78 msgid "Companies" msgstr "" -#: company/views.py:76 +#: company/views.py:79 msgid "New Company" msgstr "" -#: company/views.py:154 +#: company/views.py:157 msgid "Update Company Image" msgstr "" -#: company/views.py:160 +#: company/views.py:163 msgid "Updated company image" msgstr "" -#: company/views.py:170 +#: company/views.py:173 msgid "Edit Company" msgstr "" -#: company/views.py:175 +#: company/views.py:178 msgid "Edited company information" msgstr "" -#: company/views.py:198 +#: company/views.py:201 msgid "Create new Customer" msgstr "" -#: company/views.py:200 +#: company/views.py:203 msgid "Create new Company" msgstr "" -#: company/views.py:227 +#: company/views.py:230 msgid "Created new company" msgstr "" -#: company/views.py:237 +#: company/views.py:240 msgid "Delete Company" msgstr "" -#: company/views.py:243 +#: company/views.py:246 msgid "Company was deleted" msgstr "" -#: company/views.py:268 +#: company/views.py:271 msgid "Edit Supplier Part" msgstr "" -#: company/views.py:278 templates/js/stock.js:846 +#: company/views.py:289 templates/js/stock.js:846 msgid "Create new Supplier Part" msgstr "" -#: company/views.py:339 +#: company/views.py:388 msgid "Delete Supplier Part" msgstr "" -#: company/views.py:416 part/views.py:2366 +#: company/views.py:465 part/views.py:2552 msgid "Added new price break" msgstr "" -#: company/views.py:453 part/views.py:2411 +#: company/views.py:521 part/views.py:2596 msgid "Edit Price Break" msgstr "" -#: company/views.py:469 part/views.py:2427 +#: company/views.py:537 part/views.py:2612 msgid "Delete Price Break" msgstr "" @@ -1714,114 +1750,123 @@ msgstr "" msgid "Enter sales order number" msgstr "" -#: order/models.py:108 +#: order/models.py:110 msgid "Order reference" msgstr "" -#: order/models.py:110 +#: order/models.py:112 msgid "Order description" msgstr "" -#: order/models.py:112 +#: order/models.py:114 msgid "Link to external page" msgstr "" -#: order/models.py:122 +#: order/models.py:124 msgid "Order notes" msgstr "" -#: order/models.py:140 order/models.py:326 +#: order/models.py:142 order/models.py:328 msgid "Purchase order status" msgstr "" -#: order/models.py:148 +#: order/models.py:150 msgid "Company from which the items are being ordered" msgstr "" -#: order/models.py:151 +#: order/models.py:153 msgid "Supplier order reference code" msgstr "" -#: order/models.py:160 +#: order/models.py:162 msgid "Date order was issued" msgstr "" -#: order/models.py:162 +#: order/models.py:164 msgid "Date order was completed" msgstr "" -#: order/models.py:185 order/models.py:267 part/views.py:1477 -#: stock/models.py:243 stock/models.py:816 +#: order/models.py:187 order/models.py:269 part/views.py:1496 +#: stock/models.py:244 stock/models.py:811 msgid "Quantity must be greater than zero" msgstr "" -#: order/models.py:190 +#: order/models.py:192 msgid "Part supplier must match PO supplier" msgstr "" -#: order/models.py:262 +#: order/models.py:264 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "" -#: order/models.py:322 +#: order/models.py:324 msgid "Company to which the items are being sold" msgstr "" -#: order/models.py:328 +#: order/models.py:330 msgid "Customer order reference code" msgstr "" -#: order/models.py:367 +#: order/models.py:369 msgid "SalesOrder cannot be shipped as it is not currently pending" msgstr "" -#: order/models.py:454 +#: order/models.py:456 msgid "Item quantity" msgstr "" -#: order/models.py:456 +#: order/models.py:458 msgid "Line item reference" msgstr "" -#: order/models.py:458 +#: order/models.py:460 msgid "Line item notes" msgstr "" -#: order/models.py:484 order/templates/order/order_base.html:9 +#: order/models.py:486 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:265 templates/js/order.js:139 +#: stock/templates/stock/item_base.html:259 templates/js/order.js:139 msgid "Purchase Order" msgstr "" -#: order/models.py:497 +#: order/models.py:499 msgid "Supplier part" msgstr "" -#: order/models.py:500 +#: order/models.py:502 msgid "Number of items received" msgstr "" -#: order/models.py:594 +#: order/models.py:509 stock/models.py:457 +#: stock/templates/stock/item_base.html:266 +msgid "Purchase Price" +msgstr "" + +#: order/models.py:510 +msgid "Unit purchase price" +msgstr "" + +#: order/models.py:605 msgid "Cannot allocate stock item to a line with a different part" msgstr "" -#: order/models.py:596 +#: order/models.py:607 msgid "Cannot allocate stock to a line without a part" msgstr "" -#: order/models.py:599 +#: order/models.py:610 msgid "Allocation quantity cannot exceed stock quantity" msgstr "" -#: order/models.py:609 +#: order/models.py:620 msgid "Quantity must be 1 for serialized stock item" msgstr "" -#: order/models.py:626 +#: order/models.py:636 msgid "Select stock item to allocate" msgstr "" -#: order/models.py:629 +#: order/models.py:639 msgid "Enter stock allocation quantity" msgstr "" @@ -1857,7 +1902,7 @@ msgstr "" msgid "Order Status" msgstr "" -#: order/templates/order/order_base.html:85 templates/js/order.js:161 +#: order/templates/order/order_base.html:85 templates/js/order.js:162 msgid "Supplier Reference" msgstr "" @@ -1866,7 +1911,7 @@ msgid "Issued" msgstr "" #: order/templates/order/order_base.html:111 -#: order/templates/order/purchase_order_detail.html:183 +#: order/templates/order/purchase_order_detail.html:193 #: order/templates/order/receive_parts.html:22 #: order/templates/order/sales_order_base.html:113 msgid "Received" @@ -1913,7 +1958,7 @@ msgid "Select existing purchase orders, or create new orders." msgstr "" #: order/templates/order/order_wizard/select_pos.html:31 -#: templates/js/order.js:185 templates/js/order.js:272 +#: templates/js/order.js:186 templates/js/order.js:273 msgid "Items" msgstr "" @@ -1965,21 +2010,25 @@ msgstr "" msgid "No line items found" msgstr "" -#: order/templates/order/purchase_order_detail.html:165 +#: order/templates/order/purchase_order_detail.html:166 #: order/templates/order/receive_parts.html:20 msgid "Order Code" msgstr "" -#: order/templates/order/purchase_order_detail.html:214 +#: order/templates/order/purchase_order_detail.html:184 +msgid "Unit Price" +msgstr "" + +#: order/templates/order/purchase_order_detail.html:225 #: order/templates/order/sales_order_detail.html:285 msgid "Edit line item" msgstr "" -#: order/templates/order/purchase_order_detail.html:215 +#: order/templates/order/purchase_order_detail.html:226 msgid "Delete line item" msgstr "" -#: order/templates/order/purchase_order_detail.html:220 +#: order/templates/order/purchase_order_detail.html:231 msgid "Receive line item" msgstr "" @@ -2016,7 +2065,7 @@ msgstr "" msgid "Sales Order Details" msgstr "" -#: order/templates/order/sales_order_base.html:87 templates/js/order.js:243 +#: order/templates/order/sales_order_base.html:87 templates/js/order.js:244 msgid "Customer Reference" msgstr "" @@ -2032,7 +2081,7 @@ msgid "Sales Order Items" msgstr "" #: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 stock/models.py:377 +#: order/templates/order/sales_order_detail.html:154 stock/models.py:378 #: stock/templates/stock/item_base.html:191 templates/js/build.js:402 msgid "Serial Number" msgstr "" @@ -2269,367 +2318,389 @@ msgstr "" msgid "Error reading BOM file (incorrect row size)" msgstr "" -#: part/forms.py:62 stock/forms.py:254 +#: part/forms.py:61 stock/forms.py:255 msgid "File Format" msgstr "" -#: part/forms.py:62 stock/forms.py:254 +#: part/forms.py:61 stock/forms.py:255 msgid "Select output file format" msgstr "" -#: part/forms.py:64 +#: part/forms.py:63 msgid "Cascading" msgstr "" -#: part/forms.py:64 +#: part/forms.py:63 msgid "Download cascading / multi-level BOM" msgstr "" -#: part/forms.py:66 +#: part/forms.py:65 msgid "Levels" msgstr "" -#: part/forms.py:66 +#: part/forms.py:65 msgid "Select maximum number of BOM levels to export (0 = all levels)" msgstr "" -#: part/forms.py:68 +#: part/forms.py:67 msgid "Include Parameter Data" msgstr "" -#: part/forms.py:68 +#: part/forms.py:67 msgid "Include part parameters data in exported BOM" msgstr "" -#: part/forms.py:70 +#: part/forms.py:69 msgid "Include Stock Data" msgstr "" -#: part/forms.py:70 +#: part/forms.py:69 msgid "Include part stock data in exported BOM" msgstr "" -#: part/forms.py:72 +#: part/forms.py:71 msgid "Include Supplier Data" msgstr "" -#: part/forms.py:72 +#: part/forms.py:71 msgid "Include part supplier data in exported BOM" msgstr "" -#: part/forms.py:93 part/models.py:1632 +#: part/forms.py:92 part/models.py:1715 msgid "Parent Part" msgstr "" -#: part/forms.py:94 part/templates/part/bom_duplicate.html:7 +#: part/forms.py:93 part/templates/part/bom_duplicate.html:7 msgid "Select parent part to copy BOM from" msgstr "" -#: part/forms.py:100 +#: part/forms.py:99 msgid "Clear existing BOM items" msgstr "" -#: part/forms.py:105 +#: part/forms.py:104 msgid "Confirm BOM duplication" msgstr "" -#: part/forms.py:123 +#: part/forms.py:122 msgid "Confirm that the BOM is correct" msgstr "" -#: part/forms.py:135 +#: part/forms.py:134 msgid "Select BOM file to upload" msgstr "" -#: part/forms.py:154 +#: part/forms.py:153 msgid "Related Part" msgstr "" -#: part/forms.py:173 +#: part/forms.py:172 msgid "Select part category" msgstr "" -#: part/forms.py:189 +#: part/forms.py:188 msgid "Duplicate all BOM data for this part" msgstr "" -#: part/forms.py:190 +#: part/forms.py:189 msgid "Copy BOM" msgstr "" -#: part/forms.py:195 +#: part/forms.py:194 msgid "Duplicate all parameter data for this part" msgstr "" -#: part/forms.py:196 +#: part/forms.py:195 msgid "Copy Parameters" msgstr "" -#: part/forms.py:201 +#: part/forms.py:200 msgid "Confirm part creation" msgstr "" -#: part/forms.py:298 +#: part/forms.py:205 +msgid "Include category parameter templates" +msgstr "" + +#: part/forms.py:210 +msgid "Include parent categories parameter templates" +msgstr "" + +#: part/forms.py:285 +msgid "Add parameter template to same level categories" +msgstr "" + +#: part/forms.py:289 +msgid "Add parameter template to all categories" +msgstr "" + +#: part/forms.py:331 msgid "Input quantity for price calculation" msgstr "" -#: part/forms.py:301 -msgid "Select currency for price calculation" -msgstr "" - -#: part/models.py:67 +#: part/models.py:68 msgid "Default location for parts in this category" msgstr "" -#: part/models.py:70 +#: part/models.py:71 msgid "Default keywords for parts in this category" msgstr "" -#: part/models.py:76 part/templates/part/part_app_base.html:9 +#: part/models.py:77 part/models.py:1760 +#: part/templates/part/part_app_base.html:9 msgid "Part Category" msgstr "" -#: part/models.py:77 part/templates/part/category.html:18 +#: part/models.py:78 part/templates/part/category.html:18 #: part/templates/part/category.html:89 templates/stats.html:12 msgid "Part Categories" msgstr "" -#: part/models.py:346 part/models.py:356 +#: part/models.py:408 part/models.py:418 #, python-brace-format msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "" -#: part/models.py:453 +#: part/models.py:515 msgid "Next available serial numbers are" msgstr "" -#: part/models.py:457 +#: part/models.py:519 msgid "Next available serial number is" msgstr "" -#: part/models.py:462 +#: part/models.py:524 msgid "Most recent serial number is" msgstr "" -#: part/models.py:540 +#: part/models.py:603 +msgid "Duplicate IPN not allowed in part settings" +msgstr "" + +#: part/models.py:614 msgid "Part must be unique for name, IPN and revision" msgstr "" -#: part/models.py:569 part/templates/part/detail.html:19 +#: part/models.py:644 part/templates/part/detail.html:19 msgid "Part name" msgstr "" -#: part/models.py:573 +#: part/models.py:648 msgid "Is this part a template part?" msgstr "" -#: part/models.py:582 +#: part/models.py:657 msgid "Is this part a variant of another part?" msgstr "" -#: part/models.py:584 +#: part/models.py:659 msgid "Part description" msgstr "" -#: part/models.py:586 +#: part/models.py:661 msgid "Part keywords to improve visibility in search results" msgstr "" -#: part/models.py:591 +#: part/models.py:666 msgid "Part category" msgstr "" -#: part/models.py:593 +#: part/models.py:668 msgid "Internal Part Number" msgstr "" -#: part/models.py:595 +#: part/models.py:670 msgid "Part revision or version number" msgstr "" -#: part/models.py:609 +#: part/models.py:684 msgid "Where is this item normally stored?" msgstr "" -#: part/models.py:653 +#: part/models.py:728 msgid "Default supplier part" msgstr "" -#: part/models.py:656 +#: part/models.py:731 msgid "Minimum allowed stock level" msgstr "" -#: part/models.py:658 +#: part/models.py:733 msgid "Stock keeping units for this part" msgstr "" -#: part/models.py:662 part/templates/part/detail.html:158 +#: part/models.py:737 part/templates/part/detail.html:158 #: templates/js/table_filters.js:260 msgid "Assembly" msgstr "" -#: part/models.py:663 +#: part/models.py:738 msgid "Can this part be built from other parts?" msgstr "" -#: part/models.py:669 +#: part/models.py:744 msgid "Can this part be used to build other parts?" msgstr "" -#: part/models.py:675 +#: part/models.py:750 msgid "Does this part have tracking for unique items?" msgstr "" -#: part/models.py:680 +#: part/models.py:755 msgid "Can this part be purchased from external suppliers?" msgstr "" -#: part/models.py:685 +#: part/models.py:760 msgid "Can this part be sold to customers?" msgstr "" -#: part/models.py:689 part/templates/part/detail.html:215 +#: part/models.py:764 part/templates/part/detail.html:215 #: templates/js/table_filters.js:19 templates/js/table_filters.js:55 #: templates/js/table_filters.js:186 templates/js/table_filters.js:243 msgid "Active" msgstr "" -#: part/models.py:690 +#: part/models.py:765 msgid "Is this part active?" msgstr "" -#: part/models.py:694 part/templates/part/detail.html:138 +#: part/models.py:769 part/templates/part/detail.html:138 #: templates/js/table_filters.js:27 msgid "Virtual" msgstr "" -#: part/models.py:695 +#: part/models.py:770 msgid "Is this a virtual part, such as a software product or license?" msgstr "" -#: part/models.py:697 +#: part/models.py:772 msgid "Part notes - supports Markdown formatting" msgstr "" -#: part/models.py:699 +#: part/models.py:774 msgid "Stored BOM checksum" msgstr "" -#: part/models.py:1505 +#: part/models.py:1588 msgid "Test templates can only be created for trackable parts" msgstr "" -#: part/models.py:1522 +#: part/models.py:1605 msgid "Test with this name already exists for this part" msgstr "" -#: part/models.py:1541 templates/js/part.js:567 templates/js/stock.js:92 +#: part/models.py:1624 templates/js/part.js:567 templates/js/stock.js:92 msgid "Test Name" msgstr "" -#: part/models.py:1542 +#: part/models.py:1625 msgid "Enter a name for the test" msgstr "" -#: part/models.py:1547 +#: part/models.py:1630 msgid "Test Description" msgstr "" -#: part/models.py:1548 +#: part/models.py:1631 msgid "Enter description for this test" msgstr "" -#: part/models.py:1553 templates/js/part.js:576 +#: part/models.py:1636 templates/js/part.js:576 #: templates/js/table_filters.js:172 msgid "Required" msgstr "" -#: part/models.py:1554 +#: part/models.py:1637 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1559 templates/js/part.js:584 +#: part/models.py:1642 templates/js/part.js:584 msgid "Requires Value" msgstr "" -#: part/models.py:1560 +#: part/models.py:1643 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1565 templates/js/part.js:591 +#: part/models.py:1648 templates/js/part.js:591 msgid "Requires Attachment" msgstr "" -#: part/models.py:1566 +#: part/models.py:1649 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1599 +#: part/models.py:1682 msgid "Parameter template name must be unique" msgstr "" -#: part/models.py:1604 +#: part/models.py:1687 msgid "Parameter Name" msgstr "" -#: part/models.py:1606 +#: part/models.py:1689 msgid "Parameter Units" msgstr "" -#: part/models.py:1634 +#: part/models.py:1717 part/models.py:1765 +#: templates/InvenTree/settings/category.html:62 msgid "Parameter Template" msgstr "" -#: part/models.py:1636 +#: part/models.py:1719 msgid "Parameter Value" msgstr "" -#: part/models.py:1673 +#: part/models.py:1769 +msgid "Default Parameter Value" +msgstr "" + +#: part/models.py:1799 msgid "Select parent part" msgstr "" -#: part/models.py:1681 +#: part/models.py:1807 msgid "Select part to be used in BOM" msgstr "" -#: part/models.py:1687 +#: part/models.py:1813 msgid "BOM quantity for this BOM item" msgstr "" -#: part/models.py:1689 +#: part/models.py:1815 msgid "This BOM item is optional" msgstr "" -#: part/models.py:1692 +#: part/models.py:1818 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "" -#: part/models.py:1695 +#: part/models.py:1821 msgid "BOM item reference" msgstr "" -#: part/models.py:1698 +#: part/models.py:1824 msgid "BOM item notes" msgstr "" -#: part/models.py:1700 +#: part/models.py:1826 msgid "BOM line checksum" msgstr "" -#: part/models.py:1767 part/views.py:1483 part/views.py:1535 -#: stock/models.py:233 +#: part/models.py:1893 part/views.py:1502 part/views.py:1554 +#: stock/models.py:234 msgid "Quantity must be integer value for trackable parts" msgstr "" -#: part/models.py:1783 +#: part/models.py:1909 msgid "BOM Item" msgstr "" -#: part/models.py:1898 +#: part/models.py:2024 msgid "Select Related Part" msgstr "" -#: part/models.py:1930 +#: part/models.py:2056 msgid "" "Error creating relationship: check that the part is not related to itself " "and that the relationship is unique" @@ -2650,7 +2721,7 @@ msgstr "" #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 #: stock/templates/stock/item_base.html:72 -#: stock/templates/stock/item_base.html:273 +#: stock/templates/stock/item_base.html:274 #: stock/templates/stock/stock_adjust.html:16 templates/js/build.js:724 #: templates/js/stock.js:695 templates/js/stock.js:944 msgid "Stock Item" @@ -2717,7 +2788,7 @@ msgstr "" msgid "Validate" msgstr "" -#: part/templates/part/bom.html:62 part/views.py:1774 +#: part/templates/part/bom.html:62 part/views.py:1793 msgid "Export Bill of Materials" msgstr "" @@ -2813,7 +2884,7 @@ msgstr "" msgid "All parts" msgstr "" -#: part/templates/part/category.html:24 part/views.py:2177 +#: part/templates/part/category.html:24 part/views.py:2184 msgid "Create new part category" msgstr "" @@ -2885,7 +2956,7 @@ msgstr "" msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:216 stock/views.py:1338 +#: part/templates/part/category.html:216 stock/views.py:1342 msgid "Create new Stock Location" msgstr "" @@ -2947,7 +3018,7 @@ msgstr "" msgid "Minimum Stock" msgstr "" -#: part/templates/part/detail.html:114 templates/js/order.js:262 +#: part/templates/part/detail.html:114 templates/js/order.js:263 msgid "Creation Date" msgstr "" @@ -2967,7 +3038,7 @@ msgstr "" msgid "Part is not a virtual part" msgstr "" -#: part/templates/part/detail.html:148 stock/forms.py:248 +#: part/templates/part/detail.html:148 stock/forms.py:249 #: templates/js/table_filters.js:23 templates/js/table_filters.js:248 msgid "Template" msgstr "" @@ -3036,17 +3107,19 @@ msgstr "" msgid "Add new parameter" msgstr "" -#: part/templates/part/params.html:15 templates/InvenTree/settings/part.html:35 +#: part/templates/part/params.html:15 +#: templates/InvenTree/settings/category.html:29 +#: templates/InvenTree/settings/part.html:38 msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:25 stock/models.py:1415 +#: part/templates/part/params.html:25 stock/models.py:1419 #: templates/js/stock.js:112 msgid "Value" msgstr "" #: part/templates/part/params.html:41 part/templates/part/related.html:41 -#: part/templates/part/supplier.html:19 users/models.py:147 +#: part/templates/part/supplier.html:19 users/models.py:152 msgid "Delete" msgstr "" @@ -3226,7 +3299,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:317 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:318 msgid "Tests" msgstr "" @@ -3254,208 +3327,220 @@ msgstr "" msgid "New Variant" msgstr "" -#: part/views.py:80 +#: part/views.py:84 msgid "Add Related Part" msgstr "" -#: part/views.py:136 +#: part/views.py:140 msgid "Delete Related Part" msgstr "" -#: part/views.py:148 +#: part/views.py:152 msgid "Add part attachment" msgstr "" -#: part/views.py:203 templates/attachment_table.html:34 +#: part/views.py:207 templates/attachment_table.html:34 msgid "Edit attachment" msgstr "" -#: part/views.py:209 +#: part/views.py:213 msgid "Part attachment updated" msgstr "" -#: part/views.py:224 +#: part/views.py:228 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:232 +#: part/views.py:236 msgid "Deleted part attachment" msgstr "" -#: part/views.py:241 +#: part/views.py:245 msgid "Create Test Template" msgstr "" -#: part/views.py:270 +#: part/views.py:274 msgid "Edit Test Template" msgstr "" -#: part/views.py:286 +#: part/views.py:290 msgid "Delete Test Template" msgstr "" -#: part/views.py:295 +#: part/views.py:299 msgid "Set Part Category" msgstr "" -#: part/views.py:345 +#: part/views.py:349 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:380 +#: part/views.py:384 msgid "Create Variant" msgstr "" -#: part/views.py:462 +#: part/views.py:466 msgid "Duplicate Part" msgstr "" -#: part/views.py:469 +#: part/views.py:473 msgid "Copied part" msgstr "" -#: part/views.py:523 part/views.py:653 +#: part/views.py:527 part/views.py:661 msgid "Possible matches exist - confirm creation of new part" msgstr "" -#: part/views.py:588 templates/js/stock.js:840 +#: part/views.py:592 templates/js/stock.js:840 msgid "Create New Part" msgstr "" -#: part/views.py:595 +#: part/views.py:599 msgid "Created new part" msgstr "" -#: part/views.py:811 +#: part/views.py:830 msgid "Part QR Code" msgstr "" -#: part/views.py:830 +#: part/views.py:849 msgid "Upload Part Image" msgstr "" -#: part/views.py:838 part/views.py:875 +#: part/views.py:857 part/views.py:894 msgid "Updated part image" msgstr "" -#: part/views.py:847 +#: part/views.py:866 msgid "Select Part Image" msgstr "" -#: part/views.py:878 +#: part/views.py:897 msgid "Part image not found" msgstr "" -#: part/views.py:889 +#: part/views.py:908 msgid "Edit Part Properties" msgstr "" -#: part/views.py:916 +#: part/views.py:935 msgid "Duplicate BOM" msgstr "" -#: part/views.py:947 +#: part/views.py:966 msgid "Confirm duplication of BOM from parent" msgstr "" -#: part/views.py:968 +#: part/views.py:987 msgid "Validate BOM" msgstr "" -#: part/views.py:991 +#: part/views.py:1010 msgid "Confirm that the BOM is valid" msgstr "" -#: part/views.py:1002 +#: part/views.py:1021 msgid "Validated Bill of Materials" msgstr "" -#: part/views.py:1136 +#: part/views.py:1155 msgid "No BOM file provided" msgstr "" -#: part/views.py:1486 +#: part/views.py:1505 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1511 part/views.py:1514 +#: part/views.py:1530 part/views.py:1533 msgid "Select valid part" msgstr "" -#: part/views.py:1520 +#: part/views.py:1539 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1558 +#: part/views.py:1577 msgid "Select a part" msgstr "" -#: part/views.py:1564 +#: part/views.py:1583 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1568 +#: part/views.py:1587 msgid "Specify quantity" msgstr "" -#: part/views.py:1824 +#: part/views.py:1843 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1833 +#: part/views.py:1852 msgid "Part was deleted" msgstr "" -#: part/views.py:1842 +#: part/views.py:1861 msgid "Part Pricing" msgstr "" -#: part/views.py:1968 +#: part/views.py:1975 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1978 +#: part/views.py:1985 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1987 +#: part/views.py:1994 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1997 +#: part/views.py:2004 msgid "Create Part Parameter" msgstr "" -#: part/views.py:2049 +#: part/views.py:2056 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:2065 +#: part/views.py:2072 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:2124 +#: part/views.py:2131 msgid "Edit Part Category" msgstr "" -#: part/views.py:2161 +#: part/views.py:2168 msgid "Delete Part Category" msgstr "" -#: part/views.py:2169 +#: part/views.py:2176 msgid "Part category was deleted" msgstr "" #: part/views.py:2232 +msgid "Create Category Parameter Template" +msgstr "" + +#: part/views.py:2335 +msgid "Edit Category Parameter Template" +msgstr "" + +#: part/views.py:2393 +msgid "Delete Category Parameter Template" +msgstr "" + +#: part/views.py:2418 msgid "Create BOM Item" msgstr "" -#: part/views.py:2300 +#: part/views.py:2486 msgid "Edit BOM item" msgstr "" -#: part/views.py:2350 +#: part/views.py:2536 msgid "Confim BOM item deletion" msgstr "" @@ -3491,291 +3576,291 @@ msgstr "" msgid "Enter unique serial numbers (or leave blank)" msgstr "" -#: stock/forms.py:191 +#: stock/forms.py:192 msgid "Label" msgstr "" -#: stock/forms.py:192 stock/forms.py:248 +#: stock/forms.py:193 stock/forms.py:249 msgid "Select test report template" msgstr "" -#: stock/forms.py:256 +#: stock/forms.py:257 msgid "Include stock items in sub locations" msgstr "" -#: stock/forms.py:291 +#: stock/forms.py:292 msgid "Stock item to install" msgstr "" -#: stock/forms.py:298 +#: stock/forms.py:299 msgid "Stock quantity to assign" msgstr "" -#: stock/forms.py:326 +#: stock/forms.py:327 msgid "Must not exceed available quantity" msgstr "" -#: stock/forms.py:336 +#: stock/forms.py:337 msgid "Destination location for uninstalled items" msgstr "" -#: stock/forms.py:338 +#: stock/forms.py:339 msgid "Add transaction note (optional)" msgstr "" -#: stock/forms.py:340 +#: stock/forms.py:341 msgid "Confirm uninstall" msgstr "" -#: stock/forms.py:340 +#: stock/forms.py:341 msgid "Confirm removal of installed stock items" msgstr "" -#: stock/forms.py:364 +#: stock/forms.py:365 msgid "Destination stock location" msgstr "" -#: stock/forms.py:366 +#: stock/forms.py:367 msgid "Add note (required)" msgstr "" -#: stock/forms.py:370 stock/views.py:916 stock/views.py:1114 +#: stock/forms.py:371 stock/views.py:920 stock/views.py:1118 msgid "Confirm stock adjustment" msgstr "" -#: stock/forms.py:370 +#: stock/forms.py:371 msgid "Confirm movement of stock items" msgstr "" -#: stock/forms.py:372 +#: stock/forms.py:373 msgid "Set Default Location" msgstr "" -#: stock/forms.py:372 +#: stock/forms.py:373 msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:178 +#: stock/models.py:179 msgid "Created stock item" msgstr "" -#: stock/models.py:214 +#: stock/models.py:215 msgid "StockItem with this serial number already exists" msgstr "" -#: stock/models.py:250 +#: stock/models.py:251 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:260 stock/models.py:269 +#: stock/models.py:261 stock/models.py:270 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:261 +#: stock/models.py:262 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:283 +#: stock/models.py:284 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:289 +#: stock/models.py:290 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:296 +#: stock/models.py:297 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:329 +#: stock/models.py:330 msgid "Parent Stock Item" msgstr "" -#: stock/models.py:338 +#: stock/models.py:339 msgid "Base part" msgstr "" -#: stock/models.py:347 +#: stock/models.py:348 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:352 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:353 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "" -#: stock/models.py:355 +#: stock/models.py:356 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:360 stock/templates/stock/item_base.html:212 +#: stock/models.py:361 stock/templates/stock/item_base.html:212 msgid "Installed In" msgstr "" -#: stock/models.py:363 +#: stock/models.py:364 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:379 +#: stock/models.py:380 msgid "Serial number for this item" msgstr "" -#: stock/models.py:391 +#: stock/models.py:392 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:395 +#: stock/models.py:396 msgid "Stock Quantity" msgstr "" -#: stock/models.py:404 +#: stock/models.py:405 msgid "Source Build" msgstr "" -#: stock/models.py:406 +#: stock/models.py:407 msgid "Build for this stock item" msgstr "" -#: stock/models.py:417 +#: stock/models.py:418 msgid "Source Purchase Order" msgstr "" -#: stock/models.py:420 +#: stock/models.py:421 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:426 +#: stock/models.py:427 msgid "Destination Sales Order" msgstr "" -#: stock/models.py:433 -msgid "Destination Build Order" -msgstr "" - -#: stock/models.py:446 +#: stock/models.py:439 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:456 stock/templates/stock/item_notes.html:14 +#: stock/models.py:449 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:507 -msgid "Assigned to Customer" +#: stock/models.py:458 +msgid "Single unit purchase price at time of purchase" msgstr "" #: stock/models.py:509 +msgid "Assigned to Customer" +msgstr "" + +#: stock/models.py:511 msgid "Manually assigned to customer" msgstr "" -#: stock/models.py:522 +#: stock/models.py:524 msgid "Returned from customer" msgstr "" -#: stock/models.py:524 +#: stock/models.py:526 msgid "Returned to location" msgstr "" -#: stock/models.py:652 +#: stock/models.py:651 msgid "Installed into stock item" msgstr "" -#: stock/models.py:660 +#: stock/models.py:659 msgid "Installed stock item" msgstr "" -#: stock/models.py:684 +#: stock/models.py:683 msgid "Uninstalled stock item" msgstr "" -#: stock/models.py:703 +#: stock/models.py:702 msgid "Uninstalled into location" msgstr "" -#: stock/models.py:807 +#: stock/models.py:802 msgid "Part is not set as trackable" msgstr "" -#: stock/models.py:813 +#: stock/models.py:808 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:819 +#: stock/models.py:814 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:822 +#: stock/models.py:817 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:825 +#: stock/models.py:820 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:857 +#: stock/models.py:852 msgid "Add serial number" msgstr "" -#: stock/models.py:860 +#: stock/models.py:855 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:971 +#: stock/models.py:966 msgid "StockItem cannot be moved as it is not in stock" msgstr "" -#: stock/models.py:1316 +#: stock/models.py:1320 msgid "Tracking entry title" msgstr "" -#: stock/models.py:1318 +#: stock/models.py:1322 msgid "Entry notes" msgstr "" -#: stock/models.py:1320 +#: stock/models.py:1324 msgid "Link to external page for further information" msgstr "" -#: stock/models.py:1380 +#: stock/models.py:1384 msgid "Value must be provided for this test" msgstr "" -#: stock/models.py:1386 +#: stock/models.py:1390 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1403 +#: stock/models.py:1407 msgid "Test" msgstr "" -#: stock/models.py:1404 +#: stock/models.py:1408 msgid "Test name" msgstr "" -#: stock/models.py:1409 +#: stock/models.py:1413 msgid "Result" msgstr "" -#: stock/models.py:1410 templates/js/table_filters.js:162 +#: stock/models.py:1414 templates/js/table_filters.js:162 msgid "Test result" msgstr "" -#: stock/models.py:1416 +#: stock/models.py:1420 msgid "Test output value" msgstr "" -#: stock/models.py:1422 +#: stock/models.py:1426 msgid "Attachment" msgstr "" -#: stock/models.py:1423 +#: stock/models.py:1427 msgid "Test result attachment" msgstr "" -#: stock/models.py:1429 +#: stock/models.py:1433 msgid "Test notes" msgstr "" @@ -3905,32 +3990,32 @@ msgstr "" msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:237 templates/js/build.js:426 +#: stock/templates/stock/item_base.html:231 templates/js/build.js:426 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:244 +#: stock/templates/stock/item_base.html:238 msgid "Barcode Identifier" msgstr "" -#: stock/templates/stock/item_base.html:258 templates/js/build.js:626 +#: stock/templates/stock/item_base.html:252 templates/js/build.js:626 #: templates/navbar.html:25 msgid "Build" msgstr "" -#: stock/templates/stock/item_base.html:272 +#: stock/templates/stock/item_base.html:273 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:297 +#: stock/templates/stock/item_base.html:298 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:302 +#: stock/templates/stock/item_base.html:303 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:306 +#: stock/templates/stock/item_base.html:307 msgid "No stocktake performed" msgstr "" @@ -4050,7 +4135,7 @@ msgstr "" msgid "The following stock items will be uninstalled" msgstr "" -#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1310 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1314 msgid "Convert Stock Item" msgstr "" @@ -4082,222 +4167,222 @@ msgstr "" msgid "Installed Items" msgstr "" -#: stock/views.py:119 +#: stock/views.py:123 msgid "Edit Stock Location" msgstr "" -#: stock/views.py:144 +#: stock/views.py:148 msgid "Stock Location QR code" msgstr "" -#: stock/views.py:163 +#: stock/views.py:167 msgid "Add Stock Item Attachment" msgstr "" -#: stock/views.py:210 +#: stock/views.py:214 msgid "Edit Stock Item Attachment" msgstr "" -#: stock/views.py:227 +#: stock/views.py:231 msgid "Delete Stock Item Attachment" msgstr "" -#: stock/views.py:244 +#: stock/views.py:248 msgid "Assign to Customer" msgstr "" -#: stock/views.py:254 +#: stock/views.py:258 msgid "Customer must be specified" msgstr "" -#: stock/views.py:278 +#: stock/views.py:282 msgid "Return to Stock" msgstr "" -#: stock/views.py:288 +#: stock/views.py:292 msgid "Specify a valid location" msgstr "" -#: stock/views.py:299 +#: stock/views.py:303 msgid "Stock item returned from customer" msgstr "" -#: stock/views.py:309 +#: stock/views.py:313 msgid "Select Label Template" msgstr "" -#: stock/views.py:332 +#: stock/views.py:336 msgid "Select valid label" msgstr "" -#: stock/views.py:396 +#: stock/views.py:400 msgid "Delete All Test Data" msgstr "" -#: stock/views.py:412 +#: stock/views.py:416 msgid "Confirm test data deletion" msgstr "" -#: stock/views.py:432 +#: stock/views.py:436 msgid "Add Test Result" msgstr "" -#: stock/views.py:473 +#: stock/views.py:477 msgid "Edit Test Result" msgstr "" -#: stock/views.py:491 +#: stock/views.py:495 msgid "Delete Test Result" msgstr "" -#: stock/views.py:503 +#: stock/views.py:507 msgid "Select Test Report Template" msgstr "" -#: stock/views.py:518 +#: stock/views.py:522 msgid "Select valid template" msgstr "" -#: stock/views.py:571 +#: stock/views.py:575 msgid "Stock Export Options" msgstr "" -#: stock/views.py:693 +#: stock/views.py:697 msgid "Stock Item QR Code" msgstr "" -#: stock/views.py:719 +#: stock/views.py:723 msgid "Install Stock Item" msgstr "" -#: stock/views.py:819 +#: stock/views.py:823 msgid "Uninstall Stock Items" msgstr "" -#: stock/views.py:927 +#: stock/views.py:931 msgid "Uninstalled stock items" msgstr "" -#: stock/views.py:952 +#: stock/views.py:956 msgid "Adjust Stock" msgstr "" -#: stock/views.py:1062 +#: stock/views.py:1066 msgid "Move Stock Items" msgstr "" -#: stock/views.py:1063 +#: stock/views.py:1067 msgid "Count Stock Items" msgstr "" -#: stock/views.py:1064 +#: stock/views.py:1068 msgid "Remove From Stock" msgstr "" -#: stock/views.py:1065 +#: stock/views.py:1069 msgid "Add Stock Items" msgstr "" -#: stock/views.py:1066 +#: stock/views.py:1070 msgid "Delete Stock Items" msgstr "" -#: stock/views.py:1094 +#: stock/views.py:1098 msgid "Must enter integer value" msgstr "" -#: stock/views.py:1099 +#: stock/views.py:1103 msgid "Quantity must be positive" msgstr "" -#: stock/views.py:1106 +#: stock/views.py:1110 #, python-brace-format msgid "Quantity must not exceed {x}" msgstr "" -#: stock/views.py:1185 +#: stock/views.py:1189 #, python-brace-format msgid "Added stock to {n} items" msgstr "" -#: stock/views.py:1200 +#: stock/views.py:1204 #, python-brace-format msgid "Removed stock from {n} items" msgstr "" -#: stock/views.py:1213 +#: stock/views.py:1217 #, python-brace-format msgid "Counted stock for {n} items" msgstr "" -#: stock/views.py:1241 +#: stock/views.py:1245 msgid "No items were moved" msgstr "" -#: stock/views.py:1244 +#: stock/views.py:1248 #, python-brace-format msgid "Moved {n} items to {dest}" msgstr "" -#: stock/views.py:1263 +#: stock/views.py:1267 #, python-brace-format msgid "Deleted {n} stock items" msgstr "" -#: stock/views.py:1275 +#: stock/views.py:1279 msgid "Edit Stock Item" msgstr "" -#: stock/views.py:1360 +#: stock/views.py:1364 msgid "Serialize Stock" msgstr "" -#: stock/views.py:1454 templates/js/build.js:210 +#: stock/views.py:1458 templates/js/build.js:210 msgid "Create new Stock Item" msgstr "" -#: stock/views.py:1555 +#: stock/views.py:1559 msgid "Duplicate Stock Item" msgstr "" -#: stock/views.py:1621 +#: stock/views.py:1634 msgid "Invalid quantity" msgstr "" -#: stock/views.py:1624 +#: stock/views.py:1637 msgid "Quantity cannot be less than zero" msgstr "" -#: stock/views.py:1628 +#: stock/views.py:1641 msgid "Invalid part selection" msgstr "" -#: stock/views.py:1676 +#: stock/views.py:1689 #, python-brace-format msgid "Created {n} new stock items" msgstr "" -#: stock/views.py:1695 stock/views.py:1711 +#: stock/views.py:1708 stock/views.py:1724 msgid "Created new stock item" msgstr "" -#: stock/views.py:1730 +#: stock/views.py:1743 msgid "Delete Stock Location" msgstr "" -#: stock/views.py:1744 +#: stock/views.py:1757 msgid "Delete Stock Item" msgstr "" -#: stock/views.py:1756 +#: stock/views.py:1769 msgid "Delete Stock Tracking Entry" msgstr "" -#: stock/views.py:1775 +#: stock/views.py:1788 msgid "Edit Stock Tracking Entry" msgstr "" -#: stock/views.py:1785 +#: stock/views.py:1798 msgid "Add Stock Tracking Entry" msgstr "" @@ -4369,16 +4454,30 @@ msgstr "" msgid "Build Order Settings" msgstr "" -#: templates/InvenTree/settings/currency.html:5 -msgid "General Settings" +#: templates/InvenTree/settings/category.html:9 +msgid "Category Settings" msgstr "" -#: templates/InvenTree/settings/currency.html:14 -msgid "Currencies" +#: templates/InvenTree/settings/category.html:25 +msgid "Category Parameter Templates" msgstr "" -#: templates/InvenTree/settings/currency.html:18 -msgid "New Currency" +#: templates/InvenTree/settings/category.html:52 +msgid "No category parameter templates found" +msgstr "" + +#: templates/InvenTree/settings/category.html:67 +msgid "Default Value" +msgstr "" + +#: templates/InvenTree/settings/category.html:70 +#: templates/InvenTree/settings/part.html:75 +msgid "Edit Template" +msgstr "" + +#: templates/InvenTree/settings/category.html:71 +#: templates/InvenTree/settings/part.html:76 +msgid "Delete Template" msgstr "" #: templates/InvenTree/settings/global.html:10 @@ -4393,22 +4492,14 @@ msgstr "" msgid "Part Options" msgstr "" -#: templates/InvenTree/settings/part.html:31 +#: templates/InvenTree/settings/part.html:34 msgid "Part Parameter Templates" msgstr "" -#: templates/InvenTree/settings/part.html:52 +#: templates/InvenTree/settings/part.html:55 msgid "No part parameter templates found" msgstr "" -#: templates/InvenTree/settings/part.html:72 -msgid "Edit Template" -msgstr "" - -#: templates/InvenTree/settings/part.html:73 -msgid "Delete Template" -msgstr "" - #: templates/InvenTree/settings/po.html:9 msgid "Purchase Order Settings" msgstr "" @@ -4456,7 +4547,7 @@ msgid "Global" msgstr "" #: templates/InvenTree/settings/tabs.html:19 -msgid "Currency" +msgid "Categories" msgstr "" #: templates/InvenTree/settings/theme.html:10 @@ -4770,15 +4861,15 @@ msgstr "" msgid "No purchase orders found" msgstr "" -#: templates/js/order.js:180 templates/js/stock.js:677 +#: templates/js/order.js:181 templates/js/stock.js:677 msgid "Date" msgstr "" -#: templates/js/order.js:210 +#: templates/js/order.js:211 msgid "No sales orders found" msgstr "" -#: templates/js/order.js:267 +#: templates/js/order.js:268 msgid "Shipment Date" msgstr "" @@ -5188,38 +5279,38 @@ msgstr "" msgid "Important dates" msgstr "" -#: users/models.py:130 +#: users/models.py:135 msgid "Permission set" msgstr "" -#: users/models.py:138 +#: users/models.py:143 msgid "Group" msgstr "" -#: users/models.py:141 +#: users/models.py:146 msgid "View" msgstr "" -#: users/models.py:141 +#: users/models.py:146 msgid "Permission to view items" msgstr "" -#: users/models.py:143 +#: users/models.py:148 msgid "Add" msgstr "" -#: users/models.py:143 +#: users/models.py:148 msgid "Permission to add items" msgstr "" -#: users/models.py:145 +#: users/models.py:150 msgid "Change" msgstr "" -#: users/models.py:145 +#: users/models.py:150 msgid "Permissions to edit items" msgstr "" -#: users/models.py:147 +#: users/models.py:152 msgid "Permission to delete items" msgstr "" diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 770dd392bf..aa0c897c8a 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -175,6 +175,7 @@ class EditPurchaseOrderLineItemForm(HelperForm): 'part', 'quantity', 'reference', + 'purchase_price', 'notes', ] diff --git a/InvenTree/order/migrations/0037_auto_20201110_0911.py b/InvenTree/order/migrations/0037_auto_20201110_0911.py new file mode 100644 index 0000000000..9594424e5d --- /dev/null +++ b/InvenTree/order/migrations/0037_auto_20201110_0911.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.7 on 2020-11-10 09:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0054_remove_stockitem_build_order'), + ('order', '0036_auto_20200831_0912'), + ] + + operations = [ + migrations.AlterField( + model_name='salesorderallocation', + name='item', + field=models.ForeignKey(help_text='Select stock item to allocate', limit_choices_to={'belongs_to': None, 'part__salable': True, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocations', to='stock.StockItem'), + ), + ] diff --git a/InvenTree/order/migrations/0038_auto_20201112_1737.py b/InvenTree/order/migrations/0038_auto_20201112_1737.py new file mode 100644 index 0000000000..0d563c80d4 --- /dev/null +++ b/InvenTree/order/migrations/0038_auto_20201112_1737.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.7 on 2020-11-12 06:37 + +from django.db import migrations +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0037_auto_20201110_0911'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorderlineitem', + name='purchase_price', + field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='USD', help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'), + ), + migrations.AddField( + model_name='purchaseorderlineitem', + name='purchase_price_currency', + field=djmoney.models.fields.CurrencyField(choices=[('AUD', 'Australian Dollar'), ('CAD', 'Canadian Dollar'), ('EUR', 'Euro'), ('NZD', 'New Zealand Dollar'), ('GBP', 'Pound Sterling'), ('USD', 'US Dollar'), ('JPY', 'Yen')], default='USD', editable=False, max_length=3), + ), + ] diff --git a/InvenTree/order/migrations/0039_auto_20201112_2203.py b/InvenTree/order/migrations/0039_auto_20201112_2203.py new file mode 100644 index 0000000000..bff585688d --- /dev/null +++ b/InvenTree/order/migrations/0039_auto_20201112_2203.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2020-11-12 11:03 + +from django.db import migrations +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0038_auto_20201112_1737'), + ] + + operations = [ + migrations.AlterField( + model_name='purchaseorderlineitem', + name='purchase_price', + field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='USD', help_text='Unit purchase price', max_digits=19, null=True, verbose_name='Purchase Price'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index b193454e1f..b2eb59c52e 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -4,6 +4,10 @@ Order model definitions # -*- coding: utf-8 -*- +import os +from datetime import datetime +from decimal import Decimal + from django.db import models, transaction from django.db.models import F, Sum from django.db.models.functions import Coalesce @@ -15,9 +19,7 @@ from django.utils.translation import ugettext as _ from markdownx.models import MarkdownxField -import os -from datetime import datetime -from decimal import Decimal +from djmoney.models.fields import MoneyField from part import models as PartModels from stock import models as stock_models @@ -499,6 +501,15 @@ class PurchaseOrderLineItem(OrderLineItem): received = models.DecimalField(decimal_places=5, max_digits=15, default=0, help_text=_('Number of items received')) + purchase_price = MoneyField( + max_digits=19, + decimal_places=4, + default_currency='USD', + null=True, blank=True, + verbose_name=_('Purchase Price'), + help_text=_('Unit purchase price'), + ) + def remaining(self): """ Calculate the number of items remaining to be received """ r = self.quantity - self.received @@ -621,7 +632,6 @@ class SalesOrderAllocation(models.Model): 'part__salable': True, 'belongs_to': None, 'sales_order': None, - 'build_order': None, }, help_text=_('Select stock item to allocate') ) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index e9838f3347..c91ec6e02d 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -95,6 +95,8 @@ class POLineItemSerializer(InvenTreeModelSerializer): part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True) supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True) + purchase_price_string = serializers.CharField(source='purchase_price', read_only=True) + class Meta: model = PurchaseOrderLineItem @@ -108,6 +110,9 @@ class POLineItemSerializer(InvenTreeModelSerializer): 'part_detail', 'supplier_part_detail', 'received', + 'purchase_price', + 'purchase_price_currency', + 'purchase_price_string', ] diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 2f9e57ca99..0829eb18e1 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -146,6 +146,7 @@ $("#po-table").inventreeTable({ field: 'part', sortable: true, title: '{% trans "Part" %}', + switchable: false, formatter: function(value, row, index, field) { if (row.part) { return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`); @@ -177,9 +178,18 @@ $("#po-table").inventreeTable({ field: 'quantity', title: '{% trans "Quantity" %}' }, + { + sortable: true, + field: 'purchase_price', + title: '{% trans "Unit Price" %}', + formatter: function(value, row) { + return row.purchase_price_string || row.purchase_price; + } + }, { sortable: true, field: 'received', + switchable: false, title: '{% trans "Received" %}', formatter: function(value, row, index, field) { return makeProgressBar(row.received, row.quantity, { @@ -203,6 +213,7 @@ $("#po-table").inventreeTable({ title: '{% trans "Notes" %}', }, { + switchable: false, field: 'buttons', title: '', formatter: function(value, row, index, field) { diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 3a0c48a809..46c6f19277 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -23,8 +23,7 @@ class OrderTest(APITestCase): def setUp(self): # Create a user for auth - User = get_user_model() - User.objects.create_user('testuser', 'test@testing.com', 'password') + get_user_model().objects.create_user('testuser', 'test@testing.com', 'password') self.client.login(username='testuser', password='password') def doGet(self, url, options=''): diff --git a/InvenTree/order/test_sales_order.py b/InvenTree/order/test_sales_order.py index d40fc4156b..5eb350575e 100644 --- a/InvenTree/order/test_sales_order.py +++ b/InvenTree/order/test_sales_order.py @@ -120,12 +120,12 @@ class SalesOrderTest(TestCase): # There should now be 4 stock items self.assertEqual(StockItem.objects.count(), 4) - Sa = StockItem.objects.get(pk=self.Sa.pk) - Sb = StockItem.objects.get(pk=self.Sb.pk) + sa = StockItem.objects.get(pk=self.Sa.pk) + sb = StockItem.objects.get(pk=self.Sb.pk) # 25 units subtracted from each of the original items - self.assertEqual(Sa.quantity, 75) - self.assertEqual(Sb.quantity, 175) + self.assertEqual(sa.quantity, 75) + self.assertEqual(sb.quantity, 175) # And 2 items created which are associated with the order outputs = StockItem.objects.filter(sales_order=self.order) @@ -134,8 +134,8 @@ class SalesOrderTest(TestCase): for item in outputs.all(): self.assertEqual(item.quantity, 25) - self.assertEqual(Sa.sales_order, None) - self.assertEqual(Sb.sales_order, None) + self.assertEqual(sa.sales_order, None) + self.assertEqual(sb.sales_order, None) # And no allocations self.assertEqual(SalesOrderAllocation.objects.count(), 0) diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index 5c4e2dd405..2f014e751f 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -32,8 +32,7 @@ class OrderViewTestCase(TestCase): super().setUp() # Create a user - User = get_user_model() - user = User.objects.create_user('username', 'user@email.com', 'password') + user = get_user_model().objects.create_user('username', 'user@email.com', 'password') # Ensure that the user has the correct permissions! g = Group.objects.create(name='orders') diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 7476197547..4681404ba1 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -12,6 +12,7 @@ from .models import PartCategory, Part from .models import PartAttachment, PartStar, PartRelated from .models import BomItem from .models import PartParameterTemplate, PartParameter +from .models import PartCategoryParameterTemplate from .models import PartTestTemplate from .models import PartSellPriceBreak @@ -274,12 +275,17 @@ class ParameterAdmin(ImportExportModelAdmin): list_display = ('part', 'template', 'data') +class PartCategoryParameterAdmin(admin.ModelAdmin): + + pass + + class PartSellPriceBreakAdmin(admin.ModelAdmin): class Meta: model = PartSellPriceBreak - list_display = ('part', 'quantity', 'cost', 'currency') + list_display = ('part', 'quantity', 'price',) admin.site.register(Part, PartAdmin) @@ -290,5 +296,6 @@ admin.site.register(PartStar, PartStarAdmin) admin.site.register(BomItem, BomItemAdmin) admin.site.register(PartParameterTemplate, ParameterTemplateAdmin) admin.site.register(PartParameter, ParameterAdmin) +admin.site.register(PartCategoryParameterTemplate, PartCategoryParameterAdmin) admin.site.register(PartTestTemplate, PartTestTemplateAdmin) admin.site.register(PartSellPriceBreak, PartSellPriceBreakAdmin) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index c96cff6da5..a258d58962 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -21,6 +21,7 @@ from .models import Part, PartCategory, BomItem, PartStar from .models import PartParameter, PartParameterTemplate from .models import PartAttachment, PartTestTemplate from .models import PartSellPriceBreak +from .models import PartCategoryParameterTemplate from build.models import Build @@ -111,6 +112,36 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView): queryset = PartCategory.objects.all() +class CategoryParameters(generics.ListAPIView): + """ API endpoint for accessing a list of PartCategory objects. + + - GET: Return a list of PartCategory objects + """ + + queryset = PartCategoryParameterTemplate.objects.all() + serializer_class = part_serializers.CategoryParameterTemplateSerializer + + def get_queryset(self): + """ + Custom filtering: + - Allow filtering by "null" parent to retrieve top-level part categories + """ + + cat_id = self.kwargs.get('pk', None) + + queryset = super().get_queryset() + + if cat_id is not None: + + try: + cat_id = int(cat_id) + queryset = queryset.filter(category=cat_id) + except ValueError: + pass + + return queryset + + class PartSalePriceList(generics.ListCreateAPIView): """ API endpoint for list view of PartSalePriceBreak model @@ -864,6 +895,7 @@ part_api_urls = [ # Base URL for PartCategory API endpoints url(r'^category/', include([ + url(r'^(?P\d+)/parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'), url(r'^(?P\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'), url(r'^$', CategoryList.as_view(), name='api-part-category-list'), ])), diff --git a/InvenTree/part/fixtures/params.yaml b/InvenTree/part/fixtures/params.yaml index 121bb79074..e65c7335cc 100644 --- a/InvenTree/part/fixtures/params.yaml +++ b/InvenTree/part/fixtures/params.yaml @@ -18,7 +18,7 @@ name: Thickness units: mm -# And some parameters (requires part.yaml) +# Add some parameters to parts (requires part.yaml) - model: part.PartParameter pk: 1 fields: @@ -31,4 +31,19 @@ fields: part: 2 template: 1 - data: 12 \ No newline at end of file + data: 12 + +# Add some template parameters to categories (requires category.yaml) +- model: part.PartCategoryParameterTemplate + pk: 1 + fields: + category: 7 + parameter_template: 1 + default_value: '2.8' + +- model: part.PartCategoryParameterTemplate + pk: 2 + fields: + category: 7 + parameter_template: 3 + default_value: '0.5' diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml index 9883edfcd3..f6d9d246db 100644 --- a/InvenTree/part/fixtures/part.yaml +++ b/InvenTree/part/fixtures/part.yaml @@ -8,6 +8,7 @@ category: 8 link: www.acme.com/parts/m2x4lphs tree_id: 0 + purchaseable: True level: 0 lft: 0 rght: 0 diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index d72df4ed9f..c5281b30e6 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -16,11 +16,10 @@ from django.utils.translation import ugettext as _ from .models import Part, PartCategory, PartAttachment, PartRelated from .models import BomItem from .models import PartParameterTemplate, PartParameter +from .models import PartCategoryParameterTemplate from .models import PartTestTemplate from .models import PartSellPriceBreak -from common.models import Currency - class PartModelChoiceField(forms.ModelChoiceField): """ Extending string representation of Part instance with available stock """ @@ -201,10 +200,22 @@ class EditPartForm(HelperForm): help_text=_('Confirm part creation'), widget=forms.HiddenInput()) + selected_category_templates = forms.BooleanField(required=False, + initial=False, + label=_('Include category parameter templates'), + widget=forms.HiddenInput()) + + parent_category_templates = forms.BooleanField(required=False, + initial=False, + label=_('Include parent categories parameter templates'), + widget=forms.HiddenInput()) + class Meta: model = Part fields = [ 'category', + 'selected_category_templates', + 'parent_category_templates', 'name', 'IPN', 'description', @@ -266,6 +277,28 @@ class EditCategoryForm(HelperForm): ] +class EditCategoryParameterTemplateForm(HelperForm): + """ Form for editing a PartCategoryParameterTemplate object """ + + add_to_same_level_categories = forms.BooleanField(required=False, + initial=False, + help_text=_('Add parameter template to same level categories')) + + add_to_all_categories = forms.BooleanField(required=False, + initial=False, + help_text=_('Add parameter template to all categories')) + + class Meta: + model = PartCategoryParameterTemplate + fields = [ + 'category', + 'parameter_template', + 'default_value', + 'add_to_same_level_categories', + 'add_to_all_categories', + ] + + class EditBomItemForm(HelperForm): """ Form for editing a BomItem object """ @@ -298,13 +331,10 @@ class PartPriceForm(forms.Form): help_text=_('Input quantity for price calculation') ) - currency = forms.ModelChoiceField(queryset=Currency.objects.all(), label='Currency', help_text=_('Select currency for price calculation')) - class Meta: model = Part fields = [ 'quantity', - 'currency', ] @@ -315,13 +345,10 @@ class EditPartSalePriceBreakForm(HelperForm): quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5) - cost = RoundingDecimalFormField(max_digits=10, decimal_places=5) - class Meta: model = PartSellPriceBreak fields = [ 'part', 'quantity', - 'cost', - 'currency', + 'price', ] diff --git a/InvenTree/part/migrations/0052_auto_20201027_1557.py b/InvenTree/part/migrations/0052_auto_20201027_1557.py deleted file mode 100644 index 94dbcac06e..0000000000 --- a/InvenTree/part/migrations/0052_auto_20201027_1557.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.0.7 on 2020-10-27 04:57 - -import InvenTree.fields -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('part', '0051_bomitem_optional'), - ] - - operations = [ - migrations.AlterField( - model_name='part', - name='link', - field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True), - ), - ] diff --git a/InvenTree/part/migrations/0052_partrelated.py b/InvenTree/part/migrations/0052_partrelated.py index a8672ba7dc..7f9dc896a5 100644 --- a/InvenTree/part/migrations/0052_partrelated.py +++ b/InvenTree/part/migrations/0052_partrelated.py @@ -2,7 +2,7 @@ from django.db import migrations, models import django.db.models.deletion - +import InvenTree.fields class Migration(migrations.Migration): @@ -19,4 +19,9 @@ class Migration(migrations.Migration): ('part_2', models.ForeignKey(help_text='Select Related Part', on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_2', to='part.Part')), ], ), + migrations.AlterField( + model_name='part', + name='link', + field=InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external URL', null=True), + ), ] diff --git a/InvenTree/part/migrations/0053_merge_20201103_1028.py b/InvenTree/part/migrations/0053_merge_20201103_1028.py deleted file mode 100644 index d42595675a..0000000000 --- a/InvenTree/part/migrations/0053_merge_20201103_1028.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 3.0.7 on 2020-11-03 10:28 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('part', '0052_auto_20201027_1557'), - ('part', '0052_partrelated'), - ] - - operations = [ - ] diff --git a/InvenTree/part/migrations/0053_partcategoryparametertemplate.py b/InvenTree/part/migrations/0053_partcategoryparametertemplate.py new file mode 100644 index 0000000000..6f2809af12 --- /dev/null +++ b/InvenTree/part/migrations/0053_partcategoryparametertemplate.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.7 on 2020-10-30 18:04 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0052_partrelated'), + ] + + operations = [ + migrations.CreateModel( + name='PartCategoryParameterTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('default_value', models.CharField(blank=True, help_text='Default Parameter Value', max_length=500)), + ('category', models.ForeignKey(help_text='Part Category', on_delete=django.db.models.deletion.CASCADE, related_name='parameter_templates', to='part.PartCategory')), + ('parameter_template', models.ForeignKey(help_text='Parameter Template', on_delete=django.db.models.deletion.CASCADE, related_name='part_categories', to='part.PartParameterTemplate')), + ], + ), + migrations.AddConstraint( + model_name='partcategoryparametertemplate', + constraint=models.UniqueConstraint(fields=('category', 'parameter_template'), name='unique_category_parameter_template_pair'), + ), + ] diff --git a/InvenTree/part/migrations/0054_auto_20201109_1246.py b/InvenTree/part/migrations/0054_auto_20201109_1246.py index 705ef51466..5a27c70506 100644 --- a/InvenTree/part/migrations/0054_auto_20201109_1246.py +++ b/InvenTree/part/migrations/0054_auto_20201109_1246.py @@ -7,7 +7,7 @@ import part.settings class Migration(migrations.Migration): dependencies = [ - ('part', '0053_merge_20201103_1028'), + ('part', '0052_partrelated'), ] operations = [ diff --git a/InvenTree/part/migrations/0055_auto_20201110_1001.py b/InvenTree/part/migrations/0055_auto_20201110_1001.py new file mode 100644 index 0000000000..e9649c985e --- /dev/null +++ b/InvenTree/part/migrations/0055_auto_20201110_1001.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.7 on 2020-11-10 10:01 + +from django.db import migrations +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0054_auto_20201109_1246'), + ] + + operations = [ + migrations.AddField( + model_name='partsellpricebreak', + name='price', + field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='USD', help_text='Unit price at specified quantity', max_digits=19, null=True, verbose_name='Price'), + ), + migrations.AddField( + model_name='partsellpricebreak', + name='price_currency', + field=djmoney.models.fields.CurrencyField(choices=[('AUD', 'Australian Dollar'), ('CAD', 'Canadian Dollar'), ('EUR', 'Euro'), ('NZD', 'New Zealand Dollar'), ('GBP', 'Pound Sterling'), ('USD', 'US Dollar'), ('JPY', 'Yen')], default='USD', editable=False, max_length=3), + ), + ] diff --git a/InvenTree/part/migrations/0056_auto_20201110_1125.py b/InvenTree/part/migrations/0056_auto_20201110_1125.py new file mode 100644 index 0000000000..dff86173b6 --- /dev/null +++ b/InvenTree/part/migrations/0056_auto_20201110_1125.py @@ -0,0 +1,147 @@ +# Generated by Django 3.0.7 on 2020-11-10 11:25 + +from django.db import migrations + +from moneyed import CURRENCIES +from django.db import migrations, connection +from company.models import SupplierPriceBreak + + +def migrate_currencies(apps, schema_editor): + """ + Migrate from the 'old' method of handling currencies, + to the new method which uses the django-money library. + + Previously, we created a custom Currency model, + which was very simplistic. + + Here we will attempt to map each existing "currency" reference + for the SupplierPriceBreak model, to a new django-money compatible currency. + """ + + print("Updating currency references for SupplierPriceBreak model...") + + # A list of available currency codes + currency_codes = CURRENCIES.keys() + + cursor = connection.cursor() + + # The 'suffix' field denotes the currency code + response = cursor.execute('SELECT id, suffix, description from common_currency;') + + results = cursor.fetchall() + + remap = {} + + for index, row in enumerate(results): + pk, suffix, description = row + + suffix = suffix.strip().upper() + + if suffix not in currency_codes: + print("Missing suffix:", suffix) + + while suffix not in currency_codes: + # Ask the user to input a valid currency + print(f"Could not find a valid currency matching '{suffix}'.") + print("Please enter a valid currency code") + suffix = str(input("> ")).strip() + + if pk not in remap.keys(): + remap[pk] = suffix + + # Now iterate through each PartSellPriceBreak and update the rows + response = cursor.execute('SELECT id, cost, currency_id, price, price_currency from part_partsellpricebreak;') + + results = cursor.fetchall() + + count = 0 + + for index, row in enumerate(results): + pk, cost, currency_id, price, price_currency = row + + # Copy the 'cost' field across to the 'price' field + response = cursor.execute(f'UPDATE part_partsellpricebreak set price={cost} where id={pk};') + + # Extract the updated currency code + currency_code = remap.get(currency_id, 'USD') + + # Update the currency code + response = cursor.execute(f'UPDATE part_partsellpricebreak set price_currency= "{currency_code}" where id={pk};') + + count += 1 + + print(f"Updated {count} SupplierPriceBreak rows") + +def reverse_currencies(apps, schema_editor): + """ + Reverse the "update" process. + + Here we may be in the situation that the legacy "Currency" table is empty, + and so we have to re-populate it based on the new price_currency codes. + """ + + print("Reversing currency migration...") + + cursor = connection.cursor() + + # Extract a list of currency codes which are in use + response = cursor.execute(f'SELECT id, price, price_currency from part_partsellpricebreak;') + + results = cursor.fetchall() + + codes_in_use = set() + + for index, row in enumerate(results): + pk, price, code = row + + codes_in_use.add(code) + + # Copy the 'price' field back into the 'cost' field + response = cursor.execute(f'UPDATE part_partsellpricebreak set cost={price} where id={pk};') + + # Keep a dict of which currency objects map to which code + code_map = {} + + # For each currency code in use, check if we have a matching Currency object + for code in codes_in_use: + response = cursor.execute(f'SELECT id, suffix from common_currency where suffix="{code}";') + row = response.fetchone() + + if row is not None: + # A match exists! + pk, suffix = row + code_map[suffix] = pk + else: + # No currency object exists! + description = CURRENCIES[code] + + # Create a new object in the database + print(f"Creating new Currency object for {code}") + + # Construct a query to create a new Currency object + query = f'INSERT into common_currency (symbol, suffix, description, value, base) VALUES ("$", "{code}", "{description}", 1.0, False);' + + response = cursor.execute(query) + + code_map[code] = cursor.lastrowid + + # Ok, now we know how each suffix maps to a Currency object + for suffix in code_map.keys(): + pk = code_map[suffix] + + # Update the table to point to the Currency objects + print(f"Currency {suffix} -> pk {pk}") + + response = cursor.execute(f'UPDATE part_partsellpricebreak set currency_id={pk} where price_currency="{suffix}";') + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0055_auto_20201110_1001'), + ] + + operations = [ + migrations.RunPython(migrate_currencies, reverse_code=reverse_currencies), + ] diff --git a/InvenTree/part/migrations/0057_remove_partsellpricebreak_currency.py b/InvenTree/part/migrations/0057_remove_partsellpricebreak_currency.py new file mode 100644 index 0000000000..974aecef4b --- /dev/null +++ b/InvenTree/part/migrations/0057_remove_partsellpricebreak_currency.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.7 on 2020-11-10 11:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0056_auto_20201110_1125'), + ] + + operations = [ + migrations.RemoveField( + model_name='partsellpricebreak', + name='currency', + ), + ] diff --git a/InvenTree/part/migrations/0058_remove_partsellpricebreak_cost.py b/InvenTree/part/migrations/0058_remove_partsellpricebreak_cost.py new file mode 100644 index 0000000000..dcf625aa6f --- /dev/null +++ b/InvenTree/part/migrations/0058_remove_partsellpricebreak_cost.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.7 on 2020-11-10 11:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0057_remove_partsellpricebreak_currency'), + ] + + operations = [ + migrations.RemoveField( + model_name='partsellpricebreak', + name='cost', + ), + ] diff --git a/InvenTree/part/migrations/0059_auto_20201112_1112.py b/InvenTree/part/migrations/0059_auto_20201112_1112.py new file mode 100644 index 0000000000..5075b8431c --- /dev/null +++ b/InvenTree/part/migrations/0059_auto_20201112_1112.py @@ -0,0 +1,20 @@ +# Generated by Django 3.0.7 on 2020-11-12 00:12 + +import InvenTree.fields +import django.core.validators +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0058_remove_partsellpricebreak_cost'), + ] + + operations = [ + migrations.AlterField( + model_name='partsellpricebreak', + name='quantity', + field=InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Price break quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Quantity'), + ), + ] diff --git a/InvenTree/part/migrations/0060_merge_20201112_1722.py b/InvenTree/part/migrations/0060_merge_20201112_1722.py new file mode 100644 index 0000000000..c53e2ddd68 --- /dev/null +++ b/InvenTree/part/migrations/0060_merge_20201112_1722.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.7 on 2020-11-12 06:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0053_partcategoryparametertemplate'), + ('part', '0059_auto_20201112_1112'), + ] + + operations = [ + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 1f1c06e81e..8501fc82b3 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -12,7 +12,8 @@ from django.core.exceptions import ValidationError from django.urls import reverse from django.db import models, transaction -from django.db.models import Sum +from django.db.utils import IntegrityError +from django.db.models import Sum, UniqueConstraint from django.db.models.functions import Coalesce from django.core.validators import MinValueValidator @@ -164,6 +165,26 @@ class PartCategory(InvenTreeTree): return category_parameters + @classmethod + def get_parent_categories(cls): + """ Return tuple list of parent (root) categories """ + + # Get root nodes + root_categories = cls.objects.filter(level=0) + + parent_categories = [] + for category in root_categories: + parent_categories.append((category.id, category.name)) + + return parent_categories + + def get_parameter_templates(self): + """ Return parameter templates associated to category """ + + prefetch = PartCategoryParameterTemplate.objects.prefetch_related('category', 'parameter_template') + + return prefetch.filter(category=self.id) + @receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log') def before_delete_part_category(sender, instance, using, **kwargs): @@ -307,6 +328,9 @@ class Part(MPTTModel): If not, it is considered "orphaned" and will be deleted. """ + # Get category templates settings + add_category_templates = kwargs.pop('add_category_templates', None) + if self.pk: previous = Part.objects.get(pk=self.pk) @@ -322,6 +346,44 @@ class Part(MPTTModel): super().save(*args, **kwargs) + if add_category_templates: + # Get part category + category = self.category + + if add_category_templates: + # Store templates added to part + template_list = [] + + # Create part parameters for selected category + category_templates = add_category_templates['main'] + if category_templates: + for template in category.get_parameter_templates(): + parameter = PartParameter.create(part=self, + template=template.parameter_template, + data=template.default_value, + save=True) + if parameter: + template_list.append(template.parameter_template) + + # Create part parameters for parent category + category_templates = add_category_templates['parent'] + if category_templates: + # Get parent categories + parent_categories = category.get_ancestors() + + for category in parent_categories: + for template in category.get_parameter_templates(): + # Check that template wasn't already added + if template.parameter_template not in template_list: + try: + PartParameter.create(part=self, + template=template.parameter_template, + data=template.default_value, + save=True) + except IntegrityError: + # PartParameter already exists + pass + def __str__(self): return f"{self.full_name} - {self.description}" @@ -1664,6 +1726,49 @@ class PartParameter(models.Model): return part_parameter +class PartCategoryParameterTemplate(models.Model): + """ + A PartCategoryParameterTemplate creates a unique relationship between a PartCategory + and a PartParameterTemplate. + Multiple PartParameterTemplate instances can be associated to a PartCategory to drive + a default list of parameter templates attached to a Part instance upon creation. + + Attributes: + category: Reference to a single PartCategory object + parameter_template: Reference to a single PartParameterTemplate object + default_value: The default value for the parameter in the context of the selected + category + """ + + class Meta: + constraints = [ + UniqueConstraint(fields=['category', 'parameter_template'], + name='unique_category_parameter_template_pair') + ] + + def __str__(self): + """ String representation of a PartCategoryParameterTemplate (admin interface) """ + + if self.default_value: + return f'{self.category.name} | {self.parameter_template.name} | {self.default_value}' + else: + return f'{self.category.name} | {self.parameter_template.name}' + + category = models.ForeignKey(PartCategory, + on_delete=models.CASCADE, + related_name='parameter_templates', + help_text=_('Part Category')) + + parameter_template = models.ForeignKey(PartParameterTemplate, + on_delete=models.CASCADE, + related_name='part_categories', + help_text=_('Parameter Template')) + + default_value = models.CharField(max_length=500, + blank=True, + help_text=_('Default Parameter Value')) + + class BomItem(models.Model): """ A BomItem links a part to its component items. A part can have a BOM (bill of materials) which defines diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index f6eb8dc95b..bda49da671 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -15,7 +15,7 @@ from stock.models import StockItem from .models import (BomItem, Part, PartAttachment, PartCategory, PartParameter, PartParameterTemplate, PartSellPriceBreak, - PartStar, PartTestTemplate) + PartStar, PartTestTemplate, PartCategoryParameterTemplate) class CategorySerializer(InvenTreeModelSerializer): @@ -84,13 +84,9 @@ class PartSalePriceSerializer(InvenTreeModelSerializer): Serializer for sale prices for Part model. """ - symbol = serializers.CharField(read_only=True) - - suffix = serializers.CharField(read_only=True) - quantity = serializers.FloatField() - cost = serializers.FloatField() + price = serializers.CharField() class Meta: model = PartSellPriceBreak @@ -98,10 +94,7 @@ class PartSalePriceSerializer(InvenTreeModelSerializer): 'pk', 'part', 'quantity', - 'cost', - 'currency', - 'symbol', - 'suffix', + 'price', ] @@ -425,3 +418,21 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer): 'name', 'units', ] + + +class CategoryParameterTemplateSerializer(InvenTreeModelSerializer): + """ Serializer for PartCategoryParameterTemplate """ + + parameter_template_detail = PartParameterTemplateSerializer(source='parameter_template', + many=False, + read_only=True) + + class Meta: + model = PartCategoryParameterTemplate + fields = [ + 'pk', + 'category', + 'parameter_template', + 'parameter_template_detail', + 'default_value', + ] diff --git a/InvenTree/part/templates/part/sale_prices.html b/InvenTree/part/templates/part/sale_prices.html index 8d3cc61afd..033f280da8 100644 --- a/InvenTree/part/templates/part/sale_prices.html +++ b/InvenTree/part/templates/part/sale_prices.html @@ -10,7 +10,9 @@
- +
@@ -81,18 +83,11 @@ $('#price-break-table').inventreeTable({ sortable: true, }, { - field: 'cost', + field: 'price', title: '{% trans "Price" %}', sortable: true, formatter: function(value, row, index) { - var html = ''; - - html += row.symbol || ''; - html += value; - - if (row.suffix) { - html += ' ' + row.suffix || ''; - } + var html = value; html += `
` diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 131936b8bd..e31512e1d8 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -29,8 +29,9 @@ class PartAPITest(APITestCase): def setUp(self): # Create a user for auth - User = get_user_model() - self.user = User.objects.create_user( + user = get_user_model() + + self.user = user.objects.create_user( username='testuser', email='test@testing.com', password='password' @@ -269,8 +270,9 @@ class PartAPIAggregationTest(APITestCase): def setUp(self): # Create a user for auth - User = get_user_model() - User.objects.create_user('testuser', 'test@testing.com', 'password') + user = get_user_model() + + user.objects.create_user('testuser', 'test@testing.com', 'password') self.client.login(username='testuser', password='password') diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py index 7fa38d7dcf..b3aa7f1202 100644 --- a/InvenTree/part/test_category.py +++ b/InvenTree/part/test_category.py @@ -165,14 +165,14 @@ class CategoryTest(TestCase): self.assert_equal(p.get_default_location().pathstring, 'Office/Drawer_1') # Any part under electronics should default to 'Home' - R1 = Part.objects.get(name='R_2K2_0805') - self.assertIsNone(R1.default_location) - self.assertEqual(R1.get_default_location().name, 'Home') + r1 = Part.objects.get(name='R_2K2_0805') + self.assertIsNone(r1.default_location) + self.assertEqual(r1.get_default_location().name, 'Home') # But one part has a default_location set - R2 = Part.objects.get(name='R_4K7_0603') - self.assertEqual(R2.get_default_location().name, 'Bathroom') + r2 = Part.objects.get(name='R_4K7_0603') + self.assertEqual(r2.get_default_location().name, 'Bathroom') # And one part should have no default location at all - W = Part.objects.get(name='Widget') - self.assertIsNone(W.get_default_location()) + w = Part.objects.get(name='Widget') + self.assertIsNone(w.get_default_location()) diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py index 6876a2b5df..24eee44d89 100644 --- a/InvenTree/part/test_param.py +++ b/InvenTree/part/test_param.py @@ -3,10 +3,12 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.test import TestCase +from django.test import TestCase, TransactionTestCase import django.core.exceptions as django_exceptions +from .models import Part, PartCategory from .models import PartParameter, PartParameterTemplate +from .models import PartCategoryParameterTemplate class TestParams(TestCase): @@ -24,7 +26,10 @@ class TestParams(TestCase): self.assertEquals(str(t1), 'Length (mm)') p1 = PartParameter.objects.get(pk=1) - self.assertEqual(str(p1), "M2x4 LPHS : Length = 4mm") + self.assertEqual(str(p1), 'M2x4 LPHS : Length = 4mm') + + c1 = PartCategoryParameterTemplate.objects.get(pk=1) + self.assertEqual(str(c1), 'Mechanical | Length | 2.8') def test_validate(self): @@ -40,3 +45,47 @@ class TestParams(TestCase): t3 = PartParameterTemplate(name='aBcde', units='dd') t3.full_clean() t3.save() + + +class TestCategoryTemplates(TransactionTestCase): + + fixtures = [ + 'location', + 'category', + 'part', + 'params' + ] + + def test_validate(self): + + # Category templates + n = PartCategoryParameterTemplate.objects.all().count() + self.assertEqual(n, 2) + + category = PartCategory.objects.get(pk=8) + + t1 = PartParameterTemplate.objects.get(pk=2) + c1 = PartCategoryParameterTemplate(category=category, + parameter_template=t1, + default_value='xyz') + c1.save() + + n = PartCategoryParameterTemplate.objects.all().count() + self.assertEqual(n, 3) + + # Get test part + part = Part.objects.get(pk=1) + + # Get part parameters count + n_param = part.get_parameters().count() + + add_category_templates = { + 'main': True, + 'parent': True, + } + # Save it with category parameters + part.save(**{'add_category_templates': add_category_templates}) + + # Check new part parameters count + # Only 2 parameters should be added as one already existed with same template + self.assertEqual(n_param + 2, part.get_parameters().count()) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 677b159762..c02be211b5 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -54,10 +54,10 @@ class PartTest(TestCase): ] def setUp(self): - self.R1 = Part.objects.get(name='R_2K2_0805') - self.R2 = Part.objects.get(name='R_4K7_0603') + self.r1 = Part.objects.get(name='R_2K2_0805') + self.r2 = Part.objects.get(name='R_4K7_0603') - self.C1 = Part.objects.get(name='C_22N_0805') + self.c1 = Part.objects.get(name='C_22N_0805') Part.objects.rebuild() @@ -78,18 +78,18 @@ class PartTest(TestCase): self.assertEqual(str(p), "BOB | Bob | A2 - Can we build it?") def test_metadata(self): - self.assertEqual(self.R1.name, 'R_2K2_0805') - self.assertEqual(self.R1.get_absolute_url(), '/part/3/') + self.assertEqual(self.r1.name, 'R_2K2_0805') + self.assertEqual(self.r1.get_absolute_url(), '/part/3/') def test_category(self): - self.assertEqual(str(self.C1.category), 'Electronics/Capacitors - Capacitors') + self.assertEqual(str(self.c1.category), 'Electronics/Capacitors - Capacitors') orphan = Part.objects.get(name='Orphan') self.assertIsNone(orphan.category) self.assertEqual(orphan.category_path, '') def test_rename_img(self): - img = rename_part_image(self.R1, 'hello.png') + img = rename_part_image(self.r1, 'hello.png') self.assertEqual(img, os.path.join('part_images', 'hello.png')) def test_stock(self): @@ -100,12 +100,12 @@ class PartTest(TestCase): self.assertEqual(r.available_stock, 0) def test_barcode(self): - barcode = self.R1.format_barcode() + barcode = self.r1.format_barcode() self.assertIn('InvenTree', barcode) - self.assertIn(self.R1.name, barcode) + self.assertIn(self.r1.name, barcode) def test_copy(self): - self.R2.deep_copy(self.R1, image=True, bom=True) + self.r2.deep_copy(self.r1, image=True, bom=True) def test_match_names(self): @@ -181,9 +181,9 @@ class PartSettingsTest(TestCase): def setUp(self): # Create a user for auth - User = get_user_model() + user = get_user_model() - self.user = User.objects.create_user( + self.user = user.objects.create_user( username='testuser', email='test@testing.com', password='password', diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index 2a36dd9012..13349aae15 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -23,8 +23,9 @@ class PartViewTestCase(TestCase): super().setUp() # Create a user - User = get_user_model() - self.user = User.objects.create_user( + user = get_user_model() + + self.user = user.objects.create_user( username='username', email='user@email.com', password='password' diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 624a94b0b6..4373de2385 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -80,10 +80,18 @@ part_detail_urls = [ url(r'^.*$', views.PartDetail.as_view(), name='part-detail'), ] +category_parameter_urls = [ + url(r'^new/', views.CategoryParameterTemplateCreate.as_view(), name='category-param-template-create'), + url(r'^(?P\d+)/edit/', views.CategoryParameterTemplateEdit.as_view(), name='category-param-template-edit'), + url(r'^(?P\d+)/delete/', views.CategoryParameterTemplateDelete.as_view(), name='category-param-template-delete'), +] + part_category_urls = [ url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'), url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'), + url(r'^parameters/', include(category_parameter_urls)), + url(r'^parametric/?', views.CategoryParametric.as_view(), name='category-parametric'), url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'), ] diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 2504b056c5..4de1f0ea02 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals from django.core.exceptions import ValidationError from django.db import transaction +from django.db.utils import IntegrityError from django.shortcuts import get_object_or_404 from django.shortcuts import HttpResponseRedirect from django.utils.translation import gettext_lazy as _ @@ -16,6 +17,8 @@ from django.forms.models import model_to_dict from django.forms import HiddenInput, CheckboxInput from django.conf import settings +from moneyed import CURRENCIES + import os from rapidfuzz import fuzz @@ -23,12 +26,13 @@ from decimal import Decimal, InvalidOperation from .models import PartCategory, Part, PartAttachment, PartRelated from .models import PartParameterTemplate, PartParameter +from .models import PartCategoryParameterTemplate from .models import BomItem from .models import match_part_names from .models import PartTestTemplate from .models import PartSellPriceBreak -from common.models import Currency, InvenTreeSetting +from common.models import InvenTreeSetting from company.models import SupplierPart from . import forms as part_forms @@ -625,6 +629,10 @@ class PartCreate(AjaxCreateView): # Hide the default_supplier field (there are no matching supplier parts yet!) form.fields['default_supplier'].widget = HiddenInput() + # Display category templates widgets + form.fields['selected_category_templates'].widget = CheckboxInput() + form.fields['parent_category_templates'].widget = CheckboxInput() + return form def post(self, request, *args, **kwargs): @@ -667,7 +675,14 @@ class PartCreate(AjaxCreateView): # Record the user who created this part part.creation_user = request.user - part.save() + # Store category templates settings + add_category_templates = { + 'main': form.cleaned_data['selected_category_templates'], + 'parent': form.cleaned_data['parent_category_templates'], + } + + # Save part and pass category template settings + part.save(**{'add_category_templates': add_category_templates}) data['pk'] = part.pk data['text'] = str(part) @@ -700,6 +715,10 @@ class PartCreate(AjaxCreateView): if label in self.request.GET: initials[label] = self.request.GET.get(label) + # Automatically create part parameters from category templates + initials['selected_category_templates'] = str2bool(InvenTreeSetting.get_setting('PART_CATEGORY_PARAMETERS', False)) + initials['parent_category_templates'] = initials['selected_category_templates'] + return initials @@ -1860,19 +1879,12 @@ class PartPricing(AjaxView): if quantity < 1: quantity = 1 - if currency is None: - # No currency selected? Try to select a default one - try: - currency = Currency.objects.get(base=1) - except Currency.DoesNotExist: - currency = None + # TODO - Capacity for price comparison in different currencies + currency = None # Currency scaler scaler = Decimal(1.0) - if currency is not None: - scaler = Decimal(currency.value) - part = self.get_part() ctx = { @@ -1942,13 +1954,8 @@ class PartPricing(AjaxView): except ValueError: quantity = 1 - try: - currency_id = int(self.request.POST.get('currency', None)) - - if currency_id: - currency = Currency.objects.get(pk=currency_id) - except (ValueError, Currency.DoesNotExist): - currency = None + # TODO - How to handle pricing in different currencies? + currency = None # Always mark the form as 'invalid' (the user may wish to keep getting pricing data) data = { @@ -2215,6 +2222,185 @@ class CategoryCreate(AjaxCreateView): return initials +class CategoryParameterTemplateCreate(AjaxCreateView): + """ View for creating a new PartCategoryParameterTemplate """ + + role_required = 'part.add' + + model = PartCategoryParameterTemplate + form_class = part_forms.EditCategoryParameterTemplateForm + ajax_form_title = _('Create Category Parameter Template') + + def get_initial(self): + """ Get initial data for Category """ + initials = super().get_initial() + + category_id = self.kwargs.get('pk', None) + + if category_id: + try: + initials['category'] = PartCategory.objects.get(pk=category_id) + except (PartCategory.DoesNotExist, ValueError): + pass + + return initials + + def get_form(self): + """ Create a form to upload a new CategoryParameterTemplate + - Hide the 'category' field (parent part) + - Display parameter templates which are not yet related + """ + + form = super(AjaxCreateView, self).get_form() + + form.fields['category'].widget = HiddenInput() + + if form.is_valid(): + form.cleaned_data['category'] = self.kwargs.get('pk', None) + + try: + # Get selected category + category = self.get_initial()['category'] + + # Get existing parameter templates + parameters = [template.parameter_template.pk + for template in category.get_parameter_templates()] + + # Exclude templates already linked to category + updated_choices = [] + for choice in form.fields["parameter_template"].choices: + if (choice[0] not in parameters): + updated_choices.append(choice) + + # Update choices for parameter templates + form.fields['parameter_template'].choices = updated_choices + except KeyError: + pass + + return form + + def post(self, request, *args, **kwargs): + """ Capture the POST request + + - If the add_to_all_categories object is set, link parameter template to + all categories + - If the add_to_same_level_categories object is set, link parameter template to + same level categories + """ + + form = self.get_form() + + valid = form.is_valid() + + if valid: + add_to_same_level_categories = form.cleaned_data['add_to_same_level_categories'] + add_to_all_categories = form.cleaned_data['add_to_all_categories'] + + selected_category = PartCategory.objects.get(pk=int(self.kwargs['pk'])) + parameter_template = form.cleaned_data['parameter_template'] + default_value = form.cleaned_data['default_value'] + + categories = PartCategory.objects.all() + + if add_to_same_level_categories and not add_to_all_categories: + # Get level + level = selected_category.level + # Filter same level categories + categories = categories.filter(level=level) + + if add_to_same_level_categories or add_to_all_categories: + # Add parameter template and default value to categories + for category in categories: + # Skip selected category (will be processed in the post call) + if category.pk != selected_category.pk: + try: + cat_template = PartCategoryParameterTemplate.objects.create(category=category, + parameter_template=parameter_template, + default_value=default_value) + cat_template.save() + except IntegrityError: + # Parameter template is already linked to category + pass + + return super().post(request, *args, **kwargs) + + +class CategoryParameterTemplateEdit(AjaxUpdateView): + """ View for editing a PartCategoryParameterTemplate """ + + role_required = 'part.change' + + model = PartCategoryParameterTemplate + form_class = part_forms.EditCategoryParameterTemplateForm + ajax_form_title = _('Edit Category Parameter Template') + + def get_object(self): + try: + self.object = self.model.objects.get(pk=self.kwargs['pid']) + except: + return None + + return self.object + + def get_form(self): + """ Create a form to upload a new CategoryParameterTemplate + - Hide the 'category' field (parent part) + - Display parameter templates which are not yet related + """ + + form = super(AjaxUpdateView, self).get_form() + + form.fields['category'].widget = HiddenInput() + form.fields['add_to_all_categories'].widget = HiddenInput() + form.fields['add_to_same_level_categories'].widget = HiddenInput() + + if form.is_valid(): + form.cleaned_data['category'] = self.kwargs.get('pk', None) + + try: + # Get selected category + category = PartCategory.objects.get(pk=self.kwargs.get('pk', None)) + # Get selected template + selected_template = self.get_object().parameter_template + + # Get existing parameter templates + parameters = [template.parameter_template.pk + for template in category.get_parameter_templates() + if template.parameter_template.pk != selected_template.pk] + + # Exclude templates already linked to category + updated_choices = [] + for choice in form.fields["parameter_template"].choices: + if (choice[0] not in parameters): + updated_choices.append(choice) + + # Update choices for parameter templates + form.fields['parameter_template'].choices = updated_choices + # Set initial choice to current template + form.fields['parameter_template'].initial = selected_template + except KeyError: + pass + + return form + + +class CategoryParameterTemplateDelete(AjaxDeleteView): + """ View for deleting an existing PartCategoryParameterTemplate """ + + role_required = 'part.delete' + + model = PartCategoryParameterTemplate + ajax_form_title = _("Delete Category Parameter Template") + + def get_object(self): + try: + self.object = self.model.objects.get(pk=self.kwargs['pid']) + except: + return None + + return self.object + + class BomItemDetail(InvenTreeRoleMixin, DetailView): """ Detail view for BomItem """ context_object_name = 'item' @@ -2393,12 +2579,11 @@ class PartSalePriceBreakCreate(AjaxCreateView): initials['part'] = self.get_part() - # Pre-select the default currency - try: - base = Currency.objects.get(base=True) - initials['currency'] = base - except Currency.DoesNotExist: - pass + default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + currency = CURRENCIES.get(default_currency, None) + + if currency is not None: + initials['price'] = [1.0, currency] return initials diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 5917ed8e22..5f3c08839d 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -83,8 +83,6 @@ class StockItemResource(ModelResource): sales_order = Field(attribute='sales_order', widget=widgets.ForeignKeyWidget(SalesOrder)) - build_order = Field(attribute='build_order', widget=widgets.ForeignKeyWidget(Build)) - purchase_order = Field(attribute='purchase_order', widget=widgets.ForeignKeyWidget(PurchaseOrder)) # Date management diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 5d71d00a0c..b74ac200f7 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -488,11 +488,7 @@ class StockList(generics.ListCreateAPIView): if build: queryset = queryset.filter(build=build) - build_order = params.get('build_order', None) - - if build_order: - queryset = queryset.filter(build_order=build_order) - + # Filter by 'is building' status is_building = params.get('is_building', None) if is_building: @@ -621,10 +617,10 @@ class StockList(generics.ListCreateAPIView): queryset = queryset.exclude(quantity__lte=0) # Filter by internal part number - IPN = params.get('IPN', None) + ipn = params.get('IPN', None) - if IPN is not None: - queryset = queryset.filter(part__IPN=IPN) + if ipn is not None: + queryset = queryset.filter(part__IPN=ipn) # Does the client wish to filter by the Part ID? part_id = params.get('part', None) diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index d9937f3106..77d0f58295 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -124,6 +124,7 @@ class CreateStockItemForm(HelperForm): fields = [ 'part', 'supplier_part', + 'purchase_price', 'location', 'quantity', 'batch', @@ -399,6 +400,7 @@ class EditStockItemForm(HelperForm): 'serial', 'batch', 'status', + 'purchase_price', 'link', 'delete_on_deplete', ] diff --git a/InvenTree/stock/migrations/0053_auto_20201110_0513.py b/InvenTree/stock/migrations/0053_auto_20201110_0513.py new file mode 100644 index 0000000000..db002c9135 --- /dev/null +++ b/InvenTree/stock/migrations/0053_auto_20201110_0513.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.7 on 2020-11-10 05:13 + +from django.db import migrations +import djmoney.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0052_stockitem_is_building'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='purchase_price', + field=djmoney.models.fields.MoneyField(decimal_places=4, default_currency='USD', help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'), + ), + migrations.AddField( + model_name='stockitem', + name='purchase_price_currency', + field=djmoney.models.fields.CurrencyField(choices=[('XUA', 'ADB Unit of Account'), ('AFN', 'Afghani'), ('DZD', 'Algerian Dinar'), ('ARS', 'Argentine Peso'), ('AMD', 'Armenian Dram'), ('AWG', 'Aruban Guilder'), ('AUD', 'Australian Dollar'), ('AZN', 'Azerbaijanian Manat'), ('BSD', 'Bahamian Dollar'), ('BHD', 'Bahraini Dinar'), ('THB', 'Baht'), ('PAB', 'Balboa'), ('BBD', 'Barbados Dollar'), ('BYN', 'Belarussian Ruble'), ('BYR', 'Belarussian Ruble'), ('BZD', 'Belize Dollar'), ('BMD', 'Bermudian Dollar (customarily known as Bermuda Dollar)'), ('BTN', 'Bhutanese ngultrum'), ('VEF', 'Bolivar Fuerte'), ('BOB', 'Boliviano'), ('XBA', 'Bond Markets Units European Composite Unit (EURCO)'), ('BRL', 'Brazilian Real'), ('BND', 'Brunei Dollar'), ('BGN', 'Bulgarian Lev'), ('BIF', 'Burundi Franc'), ('XOF', 'CFA Franc BCEAO'), ('XAF', 'CFA franc BEAC'), ('XPF', 'CFP Franc'), ('CAD', 'Canadian Dollar'), ('CVE', 'Cape Verde Escudo'), ('KYD', 'Cayman Islands Dollar'), ('CLP', 'Chilean peso'), ('XTS', 'Codes specifically reserved for testing purposes'), ('COP', 'Colombian peso'), ('KMF', 'Comoro Franc'), ('CDF', 'Congolese franc'), ('BAM', 'Convertible Marks'), ('NIO', 'Cordoba Oro'), ('CRC', 'Costa Rican Colon'), ('HRK', 'Croatian Kuna'), ('CUP', 'Cuban Peso'), ('CUC', 'Cuban convertible peso'), ('CZK', 'Czech Koruna'), ('GMD', 'Dalasi'), ('DKK', 'Danish Krone'), ('MKD', 'Denar'), ('DJF', 'Djibouti Franc'), ('STD', 'Dobra'), ('DOP', 'Dominican Peso'), ('VND', 'Dong'), ('XCD', 'East Caribbean Dollar'), ('EGP', 'Egyptian Pound'), ('SVC', 'El Salvador Colon'), ('ETB', 'Ethiopian Birr'), ('EUR', 'Euro'), ('XBB', 'European Monetary Unit (E.M.U.-6)'), ('XBD', 'European Unit of Account 17(E.U.A.-17)'), ('XBC', 'European Unit of Account 9(E.U.A.-9)'), ('FKP', 'Falkland Islands Pound'), ('FJD', 'Fiji Dollar'), ('HUF', 'Forint'), ('GHS', 'Ghana Cedi'), ('GIP', 'Gibraltar Pound'), ('XAU', 'Gold'), ('XFO', 'Gold-Franc'), ('PYG', 'Guarani'), ('GNF', 'Guinea Franc'), ('GYD', 'Guyana Dollar'), ('HTG', 'Haitian gourde'), ('HKD', 'Hong Kong Dollar'), ('UAH', 'Hryvnia'), ('ISK', 'Iceland Krona'), ('INR', 'Indian Rupee'), ('IRR', 'Iranian Rial'), ('IQD', 'Iraqi Dinar'), ('IMP', 'Isle of Man Pound'), ('JMD', 'Jamaican Dollar'), ('JOD', 'Jordanian Dinar'), ('KES', 'Kenyan Shilling'), ('PGK', 'Kina'), ('LAK', 'Kip'), ('KWD', 'Kuwaiti Dinar'), ('AOA', 'Kwanza'), ('MMK', 'Kyat'), ('GEL', 'Lari'), ('LVL', 'Latvian Lats'), ('LBP', 'Lebanese Pound'), ('ALL', 'Lek'), ('HNL', 'Lempira'), ('SLL', 'Leone'), ('LSL', 'Lesotho loti'), ('LRD', 'Liberian Dollar'), ('LYD', 'Libyan Dinar'), ('SZL', 'Lilangeni'), ('LTL', 'Lithuanian Litas'), ('MGA', 'Malagasy Ariary'), ('MWK', 'Malawian Kwacha'), ('MYR', 'Malaysian Ringgit'), ('TMM', 'Manat'), ('MUR', 'Mauritius Rupee'), ('MZN', 'Metical'), ('MXV', 'Mexican Unidad de Inversion (UDI)'), ('MXN', 'Mexican peso'), ('MDL', 'Moldovan Leu'), ('MAD', 'Moroccan Dirham'), ('BOV', 'Mvdol'), ('NGN', 'Naira'), ('ERN', 'Nakfa'), ('NAD', 'Namibian Dollar'), ('NPR', 'Nepalese Rupee'), ('ANG', 'Netherlands Antillian Guilder'), ('ILS', 'New Israeli Sheqel'), ('RON', 'New Leu'), ('TWD', 'New Taiwan Dollar'), ('NZD', 'New Zealand Dollar'), ('KPW', 'North Korean Won'), ('NOK', 'Norwegian Krone'), ('PEN', 'Nuevo Sol'), ('MRO', 'Ouguiya'), ('TOP', 'Paanga'), ('PKR', 'Pakistan Rupee'), ('XPD', 'Palladium'), ('MOP', 'Pataca'), ('PHP', 'Philippine Peso'), ('XPT', 'Platinum'), ('GBP', 'Pound Sterling'), ('BWP', 'Pula'), ('QAR', 'Qatari Rial'), ('GTQ', 'Quetzal'), ('ZAR', 'Rand'), ('OMR', 'Rial Omani'), ('KHR', 'Riel'), ('MVR', 'Rufiyaa'), ('IDR', 'Rupiah'), ('RUB', 'Russian Ruble'), ('RWF', 'Rwanda Franc'), ('XDR', 'SDR'), ('SHP', 'Saint Helena Pound'), ('SAR', 'Saudi Riyal'), ('RSD', 'Serbian Dinar'), ('SCR', 'Seychelles Rupee'), ('XAG', 'Silver'), ('SGD', 'Singapore Dollar'), ('SBD', 'Solomon Islands Dollar'), ('KGS', 'Som'), ('SOS', 'Somali Shilling'), ('TJS', 'Somoni'), ('SSP', 'South Sudanese Pound'), ('LKR', 'Sri Lanka Rupee'), ('XSU', 'Sucre'), ('SDG', 'Sudanese Pound'), ('SRD', 'Surinam Dollar'), ('SEK', 'Swedish Krona'), ('CHF', 'Swiss Franc'), ('SYP', 'Syrian Pound'), ('BDT', 'Taka'), ('WST', 'Tala'), ('TZS', 'Tanzanian Shilling'), ('KZT', 'Tenge'), ('XXX', 'The codes assigned for transactions where no currency is involved'), ('TTD', 'Trinidad and Tobago Dollar'), ('MNT', 'Tugrik'), ('TND', 'Tunisian Dinar'), ('TRY', 'Turkish Lira'), ('TMT', 'Turkmenistan New Manat'), ('TVD', 'Tuvalu dollar'), ('AED', 'UAE Dirham'), ('XFU', 'UIC-Franc'), ('USD', 'US Dollar'), ('USN', 'US Dollar (Next day)'), ('UGX', 'Uganda Shilling'), ('CLF', 'Unidad de Fomento'), ('COU', 'Unidad de Valor Real'), ('UYI', 'Uruguay Peso en Unidades Indexadas (URUIURUI)'), ('UYU', 'Uruguayan peso'), ('UZS', 'Uzbekistan Sum'), ('VUV', 'Vatu'), ('CHE', 'WIR Euro'), ('CHW', 'WIR Franc'), ('KRW', 'Won'), ('YER', 'Yemeni Rial'), ('JPY', 'Yen'), ('CNY', 'Yuan Renminbi'), ('ZMK', 'Zambian Kwacha'), ('ZMW', 'Zambian Kwacha'), ('ZWD', 'Zimbabwe Dollar A/06'), ('ZWN', 'Zimbabwe dollar A/08'), ('ZWL', 'Zimbabwe dollar A/09'), ('PLN', 'Zloty')], default='USD', editable=False, max_length=3), + ), + ] diff --git a/InvenTree/stock/migrations/0054_remove_stockitem_build_order.py b/InvenTree/stock/migrations/0054_remove_stockitem_build_order.py new file mode 100644 index 0000000000..b0da00ab52 --- /dev/null +++ b/InvenTree/stock/migrations/0054_remove_stockitem_build_order.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.7 on 2020-11-10 09:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('stock', '0053_auto_20201110_0513'), + ] + + operations = [ + migrations.RemoveField( + model_name='stockitem', + name='build_order', + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 4899ddee8d..01b7b27cc4 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -24,6 +24,8 @@ from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey +from djmoney.models.fields import MoneyField + from decimal import Decimal, InvalidOperation from datetime import datetime from InvenTree import helpers @@ -134,14 +136,13 @@ class StockItem(MPTTModel): purchase_order: Link to a PurchaseOrder (if this stock item was created from a PurchaseOrder) infinite: If True this StockItem can never be exhausted sales_order: Link to a SalesOrder object (if the StockItem has been assigned to a SalesOrder) - build_order: Link to a BuildOrder object (if the StockItem has been assigned to a BuildOrder) + purchase_price: The unit purchase price for this StockItem - this is the unit price at time of purchase (if this item was purchased from an external supplier) """ # A Query filter which will be re-used in multiple places to determine if a StockItem is actually "in stock" IN_STOCK_FILTER = Q( quantity__gt=0, sales_order=None, - build_order=None, belongs_to=None, customer=None, is_building=False, @@ -427,14 +428,6 @@ class StockItem(MPTTModel): related_name='stock_items', null=True, blank=True) - build_order = models.ForeignKey( - 'build.Build', - on_delete=models.SET_NULL, - verbose_name=_("Destination Build Order"), - related_name='stock_items', - null=True, blank=True - ) - # last time the stock was checked / counted stocktake_date = models.DateField(blank=True, null=True) @@ -456,6 +449,15 @@ class StockItem(MPTTModel): help_text=_('Stock Item Notes') ) + purchase_price = MoneyField( + max_digits=19, + decimal_places=4, + default_currency='USD', + null=True, + verbose_name=_('Purchase Price'), + help_text=_('Single unit purchase price at time of purchase'), + ) + def clearAllocations(self): """ Clear all order allocations for this StockItem: @@ -602,9 +604,6 @@ class StockItem(MPTTModel): if self.sales_order is not None: return False - if self.build_order is not None: - return False - return True def installedItemCount(self): @@ -622,12 +621,12 @@ class StockItem(MPTTModel): return self.installedItemCount() > 0 @transaction.atomic - def installStockItem(self, otherItem, quantity, user, notes): + def installStockItem(self, other_item, quantity, user, notes): """ Install another stock item into this stock item. Args - otherItem: The stock item to install into this stock item + other_item: The stock item to install into this stock item quantity: The quantity of stock to install user: The user performing the operation notes: Any notes associated with the operation @@ -638,10 +637,10 @@ class StockItem(MPTTModel): return False # If the quantity is less than the stock item, split the stock! - stock_item = otherItem.splitStock(quantity, None, user) + stock_item = other_item.splitStock(quantity, None, user) if stock_item is None: - stock_item = otherItem + stock_item = other_item # Assign the other stock item into this one stock_item.belongs_to = self @@ -738,10 +737,6 @@ class StockItem(MPTTModel): if self.sales_order is not None: return False - # Not 'in stock' if it has been allocated to a BuildOrder - if self.build_order is not None: - return False - # Not 'in stock' if it has been assigned to a customer if self.customer is not None: return False diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 5bea1c4aae..e8675a8fff 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -73,7 +73,6 @@ class StockItemSerializer(InvenTreeModelSerializer): return queryset.prefetch_related( 'belongs_to', 'build', - 'build_order', 'customer', 'sales_order', 'supplier_part', @@ -155,7 +154,6 @@ class StockItemSerializer(InvenTreeModelSerializer): 'batch', 'belongs_to', 'build', - 'build_order', 'customer', 'in_stock', 'is_building', diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 71bdc3592d..5e9d26c5cf 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -221,12 +221,6 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
- {% elif item.build_order %} - - - - - {% else %} @@ -266,6 +260,13 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endif %} + {% if item.purchase_price %} + + + + + + {% endif %} {% if item.parent %} diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index 45a1669535..a34e895ed8 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -22,8 +22,9 @@ class StockAPITestCase(APITestCase): def setUp(self): # Create a user for auth - User = get_user_model() - self.user = User.objects.create_user('testuser', 'test@testing.com', 'password') + user = get_user_model() + + self.user = user.objects.create_user('testuser', 'test@testing.com', 'password') # Add the necessary permissions to the user perms = [ diff --git a/InvenTree/stock/test_views.py b/InvenTree/stock/test_views.py index 2245aff17d..1f55d74eec 100644 --- a/InvenTree/stock/test_views.py +++ b/InvenTree/stock/test_views.py @@ -23,8 +23,9 @@ class StockViewTestCase(TestCase): super().setUp() # Create a user - User = get_user_model() - self.user = User.objects.create_user( + user = get_user_model() + + self.user = user.objects.create_user( username='username', email='user@email.com', password='password' diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 1bd4feb02f..3d309c0360 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -38,12 +38,12 @@ class StockTest(TestCase): self.drawer3 = StockLocation.objects.get(name='Drawer_3') # Create a user - User = get_user_model() - User.objects.create_user('username', 'user@email.com', 'password') + user = get_user_model() + user.objects.create_user('username', 'user@email.com', 'password') self.client.login(username='username', password='password') - self.user = User.objects.get(username='username') + self.user = user.objects.get(username='username') # Ensure the MPTT objects are correctly rebuild Part.objects.rebuild() @@ -221,21 +221,21 @@ class StockTest(TestCase): def test_split_stock(self): # Split the 1234 x 2K2 resistors in Drawer_1 - N = StockItem.objects.filter(part=3).count() + n = StockItem.objects.filter(part=3).count() stock = StockItem.objects.get(id=1234) stock.splitStock(1000, None, self.user) self.assertEqual(stock.quantity, 234) # There should be a new stock item too! - self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1) + self.assertEqual(StockItem.objects.filter(part=3).count(), n + 1) # Try to split a negative quantity stock.splitStock(-10, None, self.user) - self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1) + self.assertEqual(StockItem.objects.filter(part=3).count(), n + 1) stock.splitStock(stock.quantity, None, self.user) - self.assertEqual(StockItem.objects.filter(part=3).count(), N + 1) + self.assertEqual(StockItem.objects.filter(part=3).count(), n + 1) def test_stocktake(self): # Perform stocktake diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 1d58647d5a..88c2274ddc 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -14,6 +14,8 @@ from django.urls import reverse from django.utils.translation import ugettext as _ +from moneyed import CURRENCIES + from InvenTree.views import AjaxView from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView from InvenTree.views import QRCodeView @@ -32,6 +34,8 @@ from report.models import TestReport from label.models import StockItemLabel from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult +import common.settings + from .admin import StockItemResource from . import forms as StockForms @@ -1572,6 +1576,8 @@ class StockItemCreate(AjaxCreateView): initials['location'] = part.get_default_location() initials['supplier_part'] = part.default_supplier + currency_code = common.settings.currency_code_default() + # SupplierPart field has been specified # It must match the Part, if that has been supplied if sup_part_id: @@ -1581,6 +1587,8 @@ class StockItemCreate(AjaxCreateView): if part is None or supplier_part.part == part: initials['supplier_part'] = supplier_part + currency_code = supplier_part.supplier.currency_code + except (ValueError, SupplierPart.DoesNotExist): pass @@ -1592,6 +1600,11 @@ class StockItemCreate(AjaxCreateView): except (ValueError, StockLocation.DoesNotExist): pass + currency = CURRENCIES.get(currency_code, None) + + if currency: + initials['purchase_price'] = (None, currency) + return initials def post(self, request, *args, **kwargs): diff --git a/InvenTree/templates/InvenTree/settings/category.html b/InvenTree/templates/InvenTree/settings/category.html new file mode 100644 index 0000000000..c84573e8b4 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/category.html @@ -0,0 +1,114 @@ +{% extends "InvenTree/settings/settings.html" %} +{% load i18n %} + +{% block tabs %} +{% include "InvenTree/settings/tabs.html" with tab='category' %} +{% endblock %} + +{% block subtitle %} +{% trans "Category Settings" %} +{% endblock %} + +{% block settings %} + + + {% csrf_token %} + {% load crispy_forms_tags %} +
+ {% crispy form %} +
+ + +{% if category %} +
+ +

{% trans "Category Parameter Templates" %}

+ +
+ +
+ +
{% trans "Sales Order" %} {{ item.sales_order.reference }} - {{ item.sales_order.customer.name }}
{% trans "Build Order" %}{{ item.build_order }}
{{ item.purchase_order }}
{% trans "Purchase Price" %}{{ item.purchase_price }}
+
+{% endif %} +{% endblock %} + +{% block js_ready %} +{{ block.super }} + + {# Convert dropdown to select2 format #} + $(document).ready(function() { + attachSelect('#category-select'); + }); + +{% if category %} + $("#param-table").inventreeTable({ + url: "{% url 'api-part-category-parameters' category.pk %}", + queryParams: { + ordering: 'name', + }, + formatNoMatches: function() { return '{% trans "No category parameter templates found" %}'; }, + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + switchable: false, + }, + { + field: 'parameter_template_detail.name', + title: '{% trans "Parameter Template" %}', + sortable: 'true', + }, + { + field: 'default_value', + title: '{% trans "Default Value" %}', + sortable: 'true', + formatter: function(value, row, index, field) { + var bEdit = ""; + var bDel = ""; + + var html = value + html += "
" + bEdit + bDel + "
"; + + return html; + } + } + ] + }); + + $("#new-param").click(function() { + launchModalForm("{% url 'category-param-template-create' category.pk %}", { + success: function() { + $("#param-table").bootstrapTable('refresh'); + }, + }); + }); + + $("#param-table").on('click', '.template-edit', function() { + var button = $(this); + + var url = "/part/category/{{ category.pk }}/parameters/" + button.attr('pk') + "/edit/"; + + launchModalForm(url, { + success: function() { + $("#param-table").bootstrapTable('refresh'); + } + }); + }); + + $("#param-table").on('click', '.template-delete', function() { + var button = $(this); + + var url = "/part/category/{{ category.pk }}/parameters/" + button.attr('pk') + "/delete/"; + + launchModalForm(url, { + success: function() { + $("#param-table").bootstrapTable('refresh'); + } + }); + }); +{% endif %} +{% endblock %} diff --git a/InvenTree/templates/InvenTree/settings/currency.html b/InvenTree/templates/InvenTree/settings/currency.html deleted file mode 100644 index 444dd96c66..0000000000 --- a/InvenTree/templates/InvenTree/settings/currency.html +++ /dev/null @@ -1,118 +0,0 @@ -{% extends "InvenTree/settings/settings.html" %} -{% load i18n %} - -{% block subtitle %} -{% trans "General Settings" %} -{% endblock %} - -{% block tabs %} -{% include "InvenTree/settings/tabs.html" with tab='currency' %} -{% endblock %} - -{% block settings %} - -

{% trans "Currencies" %}

- -
- -
- - -
-{% endblock %} - -{% block js_ready %} -{{ block.super }} - - $("#currency-table").inventreeTable({ - url: "{% url 'api-currency-list' %}", - queryParams: { - ordering: 'suffix' - }, - formatNoMatches: function() { return "No currencies found"; }, - rowStyle: function(row, index) { - if (row.base) { - return {classes: 'basecurrency'}; - } else { - return {}; - } - }, - columns: [ - { - field: 'pk', - title: 'ID', - visible: false, - switchable: false, - }, - { - field: 'symbol', - title: 'Symbol', - }, - { - field: 'suffix', - title: 'Currency', - sortable: true, - }, - { - field: 'description', - title: 'Description', - sortable: true, - }, - { - field: 'value', - title: 'Value', - sortable: true, - formatter: function(value, row, index, field) { - if (row.base) { - return "Base Currency"; - } else { - return value; - } - } - }, - { - formatter: function(value, row, index, field) { - - var bEdit = ""; - var bDel = ""; - - var html = "
" + bEdit + bDel + "
"; - - return html; - } - } - ] - }); - - $("#currency-table").on('click', '.cur-edit', function() { - var button = $(this); - var url = "/common/currency/" + button.attr('pk') + "/edit/"; - - launchModalForm(url, { - success: function() { - $("#currency-table").bootstrapTable('refresh'); - }, - }); - }); - - $("#currency-table").on('click', '.cur-delete', function() { - var button = $(this); - var url = "/common/currency/" + button.attr('pk') + "/delete/"; - - launchModalForm(url, { - success: function() { - $("#currency-table").bootstrapTable('refresh'); - }, - }); - }); - - $("#new-currency").click(function() { - launchModalForm("{% url 'currency-create' %}", { - success: function() { - $("#currency-table").bootstrapTable('refresh'); - }, - }); - }); - -{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/global.html b/InvenTree/templates/InvenTree/settings/global.html index 7dcdd54bea..775d30b915 100644 --- a/InvenTree/templates/InvenTree/settings/global.html +++ b/InvenTree/templates/InvenTree/settings/global.html @@ -17,6 +17,7 @@ {% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" %} + {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" %} diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index a19ce83922..d1ad6e98e9 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -27,6 +27,7 @@ {% include "InvenTree/settings/setting.html" with key="PART_COPY_BOM" %} {% include "InvenTree/settings/setting.html" with key="PART_COPY_PARAMETERS" %} {% include "InvenTree/settings/setting.html" with key="PART_COPY_TESTS" %} + {% include "InvenTree/settings/setting.html" with key="PART_CATEGORY_PARAMETERS" %} diff --git a/InvenTree/templates/InvenTree/settings/tabs.html b/InvenTree/templates/InvenTree/settings/tabs.html index d104908c49..ff2bcdb61e 100644 --- a/InvenTree/templates/InvenTree/settings/tabs.html +++ b/InvenTree/templates/InvenTree/settings/tabs.html @@ -15,8 +15,8 @@
  • {% trans "Global" %}
  • - - {% trans "Currency" %} + + {% trans "Categories" %} {% trans "Parts" %} @@ -34,4 +34,4 @@ {% trans "Sales Orders" %} -{% endif %} \ No newline at end of file +{% endif %} diff --git a/InvenTree/templates/js/company.js b/InvenTree/templates/js/company.js index 164329aee4..388a79668b 100644 --- a/InvenTree/templates/js/company.js +++ b/InvenTree/templates/js/company.js @@ -187,7 +187,7 @@ function loadSupplierPartTable(table, url, options) { field: 'manufacturer', title: '{% trans "Manufacturer" %}', formatter: function(value, row, index, field) { - if (value) { + if (value && row.manufacturer_detail) { var name = row.manufacturer_detail.name; var url = `/company/${value}/`; var html = imageHoverIcon(row.manufacturer_detail.image) + renderLink(name, url); diff --git a/InvenTree/templates/js/order.js b/InvenTree/templates/js/order.js index 0c958c65a2..41a1b4c046 100644 --- a/InvenTree/templates/js/order.js +++ b/InvenTree/templates/js/order.js @@ -137,6 +137,7 @@ function loadPurchaseOrderTable(table, options) { sortable: true, field: 'reference', title: '{% trans "Purchase Order" %}', + switchable: false, formatter: function(value, row, index, field) { var prefix = "{% settings_value 'PURCHASEORDER_REFERENCE_PREFIX' %}"; diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 6728f6244d..b54cddf7c4 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -58,6 +58,7 @@ class RuleSet(models.Model): 'part_partparametertemplate', 'part_partparameter', 'part_partrelated', + 'part_partcategoryparametertemplate', ], 'stock': [ 'stock_stockitem', @@ -102,7 +103,6 @@ class RuleSet(models.Model): # Models which currently do not require permissions 'common_colortheme', - 'common_currency', 'common_inventreesetting', 'company_contact', 'label_stockitemlabel', @@ -112,6 +112,8 @@ class RuleSet(models.Model): # Third-party tables 'error_report_error', + 'exchange_rate', + 'exchange_exchangebackend', ] RULE_OPTIONS = [ diff --git a/requirements.txt b/requirements.txt index 01a46bba71..76610600a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ django-import-export==2.0.0 # Data import / export for admin interface django-cleanup==4.0.0 # Manage deletion of old / unused uploaded files django-qr-code==1.2.0 # Generate QR codes flake8==3.8.3 # PEP checking +pep8-naming==0.11.1 # PEP naming convention extension coverage==5.3 # Unit test coverage coveralls==2.1.2 # Coveralls linking (for Travis) rapidfuzz==0.7.6 # Fuzzy string matching @@ -26,6 +27,8 @@ django-tex==1.1.7 # LaTeX PDF export django-weasyprint==1.0.1 # HTML PDF export django-debug-toolbar==2.2 # Debug / profiling toolbar django-admin-shell==0.1.2 # Python shell for the admin interface -django-error-report==0.2.0 # Error report viewer for the admin interface +django-money==1.1 # Django app for currency management +certifi # Certifi is (most likely) installed through one of the requirements above +django-error-report==0.2.0 # Error report viewer for the admin interface -inventree # Install the latest version of the InvenTree API python library \ No newline at end of file +inventree # Install the latest version of the InvenTree API python library diff --git a/setup.cfg b/setup.cfg index ca36d114b6..6e2a44f055 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,5 +7,8 @@ ignore = E501, E722, # - C901 - function is too complex C901, + # - N802 - function name should be lowercase (In the future, we should conform to this!) + N802, + N812, exclude = .git,__pycache__,*/migrations/*,*/lib/*,*/bin/*,*/media/*,*/static/*,*ci_*.py* max-complexity = 20 diff --git a/tasks.py b/tasks.py index 49f3f9445b..30641c7c81 100644 --- a/tasks.py +++ b/tasks.py @@ -6,6 +6,7 @@ from shutil import copyfile import random import string import os +import sys def apps(): """ @@ -238,6 +239,96 @@ def postgresql(c): c.run('sudo apt-get install postgresql postgresql-contrib libpq-dev') c.run('pip3 install psycopg2') +@task(help={'filename': "Output filename (default = 'data.json')"}) +def export_records(c, filename='data.json'): + """ + Export all database records to a file + """ + + # Get an absolute path to the file + if not os.path.isabs(filename): + filename = os.path.join(localDir(), filename) + filename = os.path.abspath(filename) + + print(f"Exporting database records to file '{filename}'") + + if os.path.exists(filename): + response = input("Warning: file already exists. Do you want to overwrite? [y/N]: ") + response = str(response).strip().lower() + + if response not in ['y', 'yes']: + print("Cancelled export operation") + sys.exit(1) + + cmd = f'dumpdata --exclude contenttypes --exclude auth.permission --indent 2 --output {filename}' + + manage(c, cmd, pty=True) + +@task(help={'filename': 'Input filename'}) +def import_records(c, filename='data.json'): + """ + Import database records from a file + """ + + # Get an absolute path to the supplied filename + if not os.path.isabs(filename): + filename = os.path.join(localDir(), filename) + + if not os.path.exists(filename): + print(f"Error: File '{filename}' does not exist") + sys.exit(1) + + print(f"Importing database records from '{filename}'") + + cmd = f'loaddata {filename}' + + manage(c, cmd, pty=True) + +@task +def import_fixtures(c): + """ + Import fixture data into the database. + + This command imports all existing test fixture data into the database. + + Warning: + - Intended for testing / development only! + - Running this command may overwrite existing database data!! + - Don't say you were not warned... + """ + + fixtures = [ + # Build model + 'build', + + # Common models + 'settings', + + # Company model + 'company', + 'price_breaks', + 'supplier_part', + + # Order model + 'order', + + # Part model + 'bom', + 'category', + 'params', + 'part', + 'test_templates', + + # Stock model + 'location', + 'stock_tests', + 'stock', + ] + + command = 'loaddata ' + ' '.join(fixtures) + + manage(c, command, pty=True) + @task def backup(c): """