diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 921a45c112..37a8bf82e1 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -337,7 +337,7 @@ def DownloadFile(data, filename, content_type='application/text'): return response -def ExtractSerialNumbers(serials, expected_quantity): +def extract_serial_numbers(serials, expected_quantity): """ Attempt to extract serial numbers from an input string. - Serial numbers must be integer values - Serial numbers must be positive diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index 263e28fc01..238fc0a6a6 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -78,7 +78,15 @@ function getImageUrlFromTransfer(transfer) { return url; } -function makeIconButton(icon, cls, pk, title) { +function makeIconBadge(icon, title) { + // Construct an 'icon badge' which floats to the right of an object + + var html = `<span class='fas ${icon} label-right' title='${title}'></span>`; + + return html; +} + +function makeIconButton(icon, cls, pk, title, options={}) { // Construct an 'icon button' using the fontawesome set var classes = `btn btn-default btn-glyph ${cls}`; @@ -86,15 +94,21 @@ function makeIconButton(icon, cls, pk, title) { var id = `${cls}-${pk}`; var html = ''; + + var extraProps = ''; + + if (options.disabled) { + extraProps += "disabled='true' "; + } - html += `<button pk='${pk}' id='${id}' class='${classes}' title='${title}'>`; + html += `<button pk='${pk}' id='${id}' class='${classes}' title='${title}' ${extraProps}>`; html += `<span class='fas ${icon}'></span>`; html += `</button>`; return html; } -function makeProgressBar(value, maximum, opts) { +function makeProgressBar(value, maximum, opts={}) { /* * Render a progessbar! * @@ -120,20 +134,35 @@ function makeProgressBar(value, maximum, opts) { var extraclass = ''; - if (maximum) { - // TODO - Special color? - } - else if (value > maximum) { + if (value > maximum) { extraclass='progress-bar-over'; } else if (value < maximum) { extraclass = 'progress-bar-under'; } - var text = value; + var style = options.style || ''; - if (maximum) { - text += ' / '; - text += maximum; + var text = ''; + + if (style == 'percent') { + // Display e.g. "50%" + + text = `${percent}%`; + } else if (style == 'max') { + // Display just the maximum value + text = `${maximum}`; + } else if (style == 'value') { + // Display just the current value + text = `${value}`; + } else if (style == 'blank') { + // No display! + text = ''; + } else { + /* Default style + * Display e.g. "5 / 10" + */ + + text = `${value} / ${maximum}`; } var id = options.id || 'progress-bar'; diff --git a/InvenTree/InvenTree/static/script/inventree/modals.js b/InvenTree/InvenTree/static/script/inventree/modals.js index 49de96468f..f731a5238b 100644 --- a/InvenTree/InvenTree/static/script/inventree/modals.js +++ b/InvenTree/InvenTree/static/script/inventree/modals.js @@ -134,6 +134,32 @@ function reloadFieldOptions(fieldName, options) { } +function enableField(fieldName, enabled, options={}) { + /* Enable (or disable) a particular field in a modal. + * + * Args: + * - fieldName: The name of the field + * - enabled: boolean enabled / disabled status + * - options: + */ + + var modal = options.modal || '#modal-form'; + + var field = getFieldByName(modal, fieldName); + + field.prop("disabled", !enabled); +} + +function clearField(fieldName, options={}) { + + var modal = options.modal || '#modal-form'; + + var field = getFieldByName(modal, fieldName); + + field.val(""); +} + + function partialMatcher(params, data) { /* Replacement function for the 'matcher' parameter for a select2 dropdown. @@ -467,6 +493,9 @@ function openModal(options) { var modal = options.modal || '#modal-form'; + // Ensure that the 'warning' div is hidden + $(modal).find('#form-validation-warning').css('display', 'none'); + $(modal).on('shown.bs.modal', function() { $(modal + ' .modal-form-content').scrollTop(0); if (options.focus) { @@ -693,6 +722,11 @@ function handleModalForm(url, options) { } // Form was returned, invalid! else { + + var warningDiv = $(modal).find('#form-validation-warning'); + + warningDiv.css('display', 'block'); + if (response.html_form) { injectModalForm(modal, response.html_form); @@ -810,8 +844,13 @@ function launchModalForm(url, options = {}) { $(modal).modal('hide'); - // Permission denied! - if (xhr.status == 400) { + if (xhr.status == 0) { + // No response from the server + showAlertDialog( + "No Response", + "No response from the InvenTree server", + ); + } else if (xhr.status == 400) { showAlertDialog( "Error 400: Bad Request", "Server returned error code 400" diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py index 2032ec75d8..b527009ede 100644 --- a/InvenTree/InvenTree/status_codes.py +++ b/InvenTree/InvenTree/status_codes.py @@ -214,25 +214,25 @@ class BuildStatus(StatusCode): # Build status codes PENDING = 10 # Build is pending / active - ALLOCATED = 20 # Parts have been removed from stock + PRODUCTION = 20 # BuildOrder is in production CANCELLED = 30 # Build was cancelled COMPLETE = 40 # Build is complete options = { PENDING: _("Pending"), - ALLOCATED: _("Allocated"), + PRODUCTION: _("Production"), CANCELLED: _("Cancelled"), COMPLETE: _("Complete"), } colors = { PENDING: 'blue', - ALLOCATED: 'blue', + PRODUCTION: 'blue', COMPLETE: 'green', CANCELLED: 'red', } ACTIVE_CODES = [ PENDING, - ALLOCATED + PRODUCTION, ] diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py index c46a059c8d..6630c0b0af 100644 --- a/InvenTree/InvenTree/tests.py +++ b/InvenTree/InvenTree/tests.py @@ -210,7 +210,7 @@ class TestSerialNumberExtraction(TestCase): def test_simple(self): - e = helpers.ExtractSerialNumbers + e = helpers.extract_serial_numbers sn = e("1-5", 5) self.assertEqual(len(sn), 5) @@ -226,7 +226,7 @@ class TestSerialNumberExtraction(TestCase): def test_failures(self): - e = helpers.ExtractSerialNumbers + e = helpers.extract_serial_numbers # Test duplicates with self.assertRaises(ValidationError): diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 334f285e2c..d729210235 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -89,14 +89,14 @@ settings_urls = [ # Some javascript files are served 'dynamically', allowing them to pass through the Django translation layer dynamic_javascript_urls = [ - url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.html'), name='barcode.js'), - url(r'^part.js', DynamicJsView.as_view(template_name='js/part.html'), name='part.js'), - url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.html'), name='stock.js'), - url(r'^build.js', DynamicJsView.as_view(template_name='js/build.html'), name='build.js'), - url(r'^order.js', DynamicJsView.as_view(template_name='js/order.html'), name='order.js'), - url(r'^company.js', DynamicJsView.as_view(template_name='js/company.html'), name='company.js'), - url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.html'), name='bom.js'), - url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.html'), name='table_filters.js'), + url(r'^barcode.js', DynamicJsView.as_view(template_name='js/barcode.js'), name='barcode.js'), + url(r'^bom.js', DynamicJsView.as_view(template_name='js/bom.js'), name='bom.js'), + url(r'^build.js', DynamicJsView.as_view(template_name='js/build.js'), name='build.js'), + url(r'^company.js', DynamicJsView.as_view(template_name='js/company.js'), name='company.js'), + url(r'^order.js', DynamicJsView.as_view(template_name='js/order.js'), name='order.js'), + url(r'^part.js', DynamicJsView.as_view(template_name='js/part.js'), name='part.js'), + url(r'^stock.js', DynamicJsView.as_view(template_name='js/stock.js'), name='stock.js'), + url(r'^table_filters.js', DynamicJsView.as_view(template_name='js/table_filters.js'), name='table_filters.js'), ] urlpatterns = [ diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index b4bc6ebe06..dd7a782485 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -213,6 +213,19 @@ class AjaxMixin(InvenTreeRoleMixin): """ return {} + def validate(self, obj, form, **kwargs): + """ + Hook for performing custom form validation steps. + + If a form error is detected, add it to the form, + with 'form.add_error()' + + Ref: https://docs.djangoproject.com/en/dev/topics/forms/ + """ + + # Do nothing by default + pass + def renderJsonResponse(self, request, form=None, data={}, context=None): """ Render a JSON response based on specific class context. @@ -320,18 +333,6 @@ class AjaxCreateView(AjaxMixin, CreateView): - Handles form validation via AJAX POST requests """ - def pre_save(self, **kwargs): - """ - Hook for doing something before the form is validated - """ - pass - - def post_save(self, **kwargs): - """ - Hook for doing something with the created object after it is saved - """ - pass - def get(self, request, *args, **kwargs): """ Creates form with initial data, and renders JSON response """ @@ -341,6 +342,17 @@ class AjaxCreateView(AjaxMixin, CreateView): form = self.get_form() return self.renderJsonResponse(request, form) + def save(self, form): + """ + Method for actually saving the form to the database. + Default implementation is very simple, + but can be overridden if required. + """ + + self.object = form.save() + + return self.object + def post(self, request, *args, **kwargs): """ Responds to form POST. Validates POST data and returns status info. @@ -351,16 +363,29 @@ class AjaxCreateView(AjaxMixin, CreateView): self.request = request self.form = self.get_form() + # Perform initial form validation + self.form.is_valid() + + # Perform custom validation (no object can be provided yet) + self.validate(None, self.form) + + valid = self.form.is_valid() + # Extra JSON data sent alongside form data = { - 'form_valid': self.form.is_valid(), + 'form_valid': valid, + 'form_errors': self.form.errors.as_json(), + 'non_field_errors': self.form.non_field_errors().as_json(), } - if self.form.is_valid(): + # Add in any extra class data + for value, key in enumerate(self.get_data()): + data[key] = value - self.pre_save() - self.object = self.form.save() - self.post_save() + if valid: + + # Save the object to the database + self.object = self.save(self.form) # Return the PK of the newly-created object data['pk'] = self.object.pk @@ -391,6 +416,20 @@ class AjaxUpdateView(AjaxMixin, UpdateView): return self.renderJsonResponse(request, self.get_form(), context=self.get_context_data()) + def save(self, object, form, **kwargs): + """ + Method for updating the object in the database. + Default implementation is very simple, but can be overridden if required. + + Args: + object - The current object, to be updated + form - The validated form + """ + + self.object = form.save() + + return self.object + def post(self, request, *args, **kwargs): """ Respond to POST request. @@ -400,37 +439,48 @@ class AjaxUpdateView(AjaxMixin, UpdateView): - Otherwise, return sucess status """ + self.request = request + # Make sure we have an object to point to self.object = self.get_object() form = self.get_form() + # Perform initial form validation + form.is_valid() + + # Perform custom validation + self.validate(self.object, form) + + valid = form.is_valid() + data = { - 'form_valid': form.is_valid() + 'form_valid': valid, + 'form_errors': form.errors.as_json(), + 'non_field_errors': form.non_field_errors().as_json(), } - if form.is_valid(): - obj = form.save() + # Add in any extra class data + for value, key in enumerate(self.get_data()): + data[key] = value + + if valid: + + # Save the updated objec to the database + self.save(self.object, form) + + self.object = self.get_object() # Include context data about the updated object - data['pk'] = obj.id - - self.post_save(obj) + data['pk'] = self.object.pk try: - data['url'] = obj.get_absolute_url() + data['url'] = self.object.get_absolute_url() except AttributeError: pass return self.renderJsonResponse(request, form, data) - def post_save(self, obj, *args, **kwargs): - """ - Hook called after the form data is saved. - (Optional) - """ - pass - class AjaxDeleteView(AjaxMixin, UpdateView): @@ -440,7 +490,7 @@ class AjaxDeleteView(AjaxMixin, UpdateView): """ form_class = DeleteForm - ajax_form_title = "Delete Item" + ajax_form_title = _("Delete Item") ajax_template_name = "modal_delete_form.html" context_object_name = 'item' @@ -489,7 +539,7 @@ class AjaxDeleteView(AjaxMixin, UpdateView): if confirmed: obj.delete() else: - form.errors['confirm_delete'] = ['Check box to confirm item deletion'] + form.add_error('confirm_delete', _('Check box to confirm item deletion')) context[self.context_object_name] = self.get_object() data = { @@ -504,7 +554,7 @@ class EditUserView(AjaxUpdateView): """ View for editing user information """ ajax_template_name = "modal_form.html" - ajax_form_title = "Edit User Information" + ajax_form_title = _("Edit User Information") form_class = EditUserForm def get_object(self): @@ -515,7 +565,7 @@ class SetPasswordView(AjaxUpdateView): """ View for setting user password """ ajax_template_name = "InvenTree/password.html" - ajax_form_title = "Set Password" + ajax_form_title = _("Set Password") form_class = SetPasswordForm def get_object(self): @@ -534,9 +584,9 @@ class SetPasswordView(AjaxUpdateView): # Passwords must match if not p1 == p2: - error = 'Password fields must match' - form.errors['enter_password'] = [error] - form.errors['confirm_password'] = [error] + error = _('Password fields must match') + form.add_error('enter_password', error) + form.add_error('confirm_password', error) valid = False diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index d4e458c506..2401e9d936 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -52,14 +52,16 @@ class BuildList(generics.ListCreateAPIView): queryset = super().filter_queryset(queryset) + params = self.request.query_params + # Filter by build status? - status = self.request.query_params.get('status', None) + status = params.get('status', None) if status is not None: queryset = queryset.filter(status=status) - # Filter by "active" status - active = self.request.query_params.get('active', None) + # Filter by "pending" status + active = params.get('active', None) if active is not None: active = str2bool(active) @@ -70,7 +72,7 @@ class BuildList(generics.ListCreateAPIView): queryset = queryset.exclude(status__in=BuildStatus.ACTIVE_CODES) # Filter by associated part? - part = self.request.query_params.get('part', None) + part = params.get('part', None) if part is not None: queryset = queryset.filter(part=part) @@ -119,14 +121,23 @@ class BuildItemList(generics.ListCreateAPIView): return query def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + params = self.request.query_params + # Does the user wish to filter by part? - part_pk = self.request.query_params.get('part', None) + part_pk = params.get('part', None) if part_pk: queryset = queryset.filter(stock_item__part=part_pk) + # Filter by output target + output = params.get('output', None) + + if output: + queryset = queryset.filter(install_into=output) + return queryset filter_backends = [ @@ -135,7 +146,8 @@ class BuildItemList(generics.ListCreateAPIView): filter_fields = [ 'build', - 'stock_item' + 'stock_item', + 'install_into', ] diff --git a/InvenTree/build/fixtures/build.yaml b/InvenTree/build/fixtures/build.yaml index 0ac5d00a20..cc645f9696 100644 --- a/InvenTree/build/fixtures/build.yaml +++ b/InvenTree/build/fixtures/build.yaml @@ -31,4 +31,52 @@ level: 0 lft: 0 rght: 0 - tree_id: 1 \ No newline at end of file + tree_id: 1 + +- model: build.build + pk: 3 + fields: + part: 50 + reference: "0003" + title: 'Making things' + batch: 'B2' + status: 40 # COMPLETE + quantity: 21 + notes: 'Some even more simple notes' + creation_date: '2019-03-16' + level: 0 + lft: 0 + rght: 0 + tree_id: 1 + +- model: build.build + pk: 4 + fields: + part: 50 + reference: "0004" + title: 'Making things' + batch: 'B4' + status: 40 # COMPLETE + quantity: 21 + notes: 'Some even even more simple notes' + creation_date: '2019-03-16' + level: 0 + lft: 0 + rght: 0 + tree_id: 1 + +- model: build.build + pk: 5 + fields: + part: 25 + reference: "0005" + title: "Building some Widgets" + batch: "B10" + status: 40 # Complete + quantity: 10 + creation_date: '2019-03-16' + notes: "A thing" + level: 0 + lft: 0 + rght: 0 + tree_id: 1 diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py index 74227adb9c..24f193865d 100644 --- a/InvenTree/build/forms.py +++ b/InvenTree/build/forms.py @@ -6,11 +6,14 @@ Django Forms for interacting with Build objects from __future__ import unicode_literals from django.utils.translation import ugettext as _ +from django import forms from InvenTree.forms import HelperForm -from django import forms -from .models import Build, BuildItem -from stock.models import StockLocation +from InvenTree.fields import RoundingDecimalFormField + +from .models import Build, BuildItem, BuildOrderAttachment + +from stock.models import StockLocation, StockItem class EditBuildForm(HelperForm): @@ -21,6 +24,7 @@ class EditBuildForm(HelperForm): 'reference': 'BO', 'link': 'fa-link', 'batch': 'fa-layer-group', + 'serial-numbers': 'fa-hashtag', 'location': 'fa-map-marker-alt', } @@ -35,28 +39,149 @@ class EditBuildForm(HelperForm): 'title', 'part', 'quantity', + 'batch', + 'take_from', + 'destination', 'parent', 'sales_order', - 'take_from', - 'batch', 'link', ] -class ConfirmBuildForm(HelperForm): - """ Form for auto-allocation of stock to a build """ +class BuildOutputCreateForm(HelperForm): + """ + Form for creating a new build output. + """ - confirm = forms.BooleanField(required=False, help_text=_('Confirm')) + def __init__(self, *args, **kwargs): + + build = kwargs.pop('build', None) + + if build: + self.field_placeholder['serial_numbers'] = build.part.getSerialNumberString() + + super().__init__(*args, **kwargs) + + field_prefix = { + 'serial_numbers': 'fa-hashtag', + } + + quantity = forms.IntegerField( + label=_('Quantity'), + help_text=_('Enter quantity for build output'), + ) + + serial_numbers = forms.CharField( + label=_('Serial numbers'), + required=False, + help_text=_('Enter serial numbers for build outputs'), + ) + + confirm = forms.BooleanField( + required=True, + label=_('Confirm'), + help_text=_('Confirm creation of build outut'), + ) class Meta: model = Build fields = [ - 'confirm' + 'quantity', + 'batch', + 'serial_numbers', + 'confirm', + ] + + +class BuildOutputDeleteForm(HelperForm): + """ + Form for deleting a build output. + """ + + confirm = forms.BooleanField( + required=False, + help_text=_('Confirm deletion of build output') + ) + + output_id = forms.IntegerField( + required=True, + widget=forms.HiddenInput() + ) + + class Meta: + model = Build + fields = [ + 'confirm', + 'output_id', + ] + + +class UnallocateBuildForm(HelperForm): + """ + Form for auto-de-allocation of stock from a build + """ + + confirm = forms.BooleanField(required=False, help_text=_('Confirm unallocation of stock')) + + output_id = forms.IntegerField( + required=False, + widget=forms.HiddenInput() + ) + + part_id = forms.IntegerField( + required=False, + widget=forms.HiddenInput(), + ) + + class Meta: + model = Build + fields = [ + 'confirm', + 'output_id', + 'part_id', + ] + + +class AutoAllocateForm(HelperForm): + """ Form for auto-allocation of stock to a build """ + + confirm = forms.BooleanField(required=True, help_text=_('Confirm stock allocation')) + + # Keep track of which build output we are interested in + output = forms.ModelChoiceField( + queryset=StockItem.objects.all(), + ) + + class Meta: + model = Build + fields = [ + 'confirm', + 'output', ] class CompleteBuildForm(HelperForm): - """ Form for marking a Build as complete """ + """ + Form for marking a build as complete + """ + + confirm = forms.BooleanField( + required=True, + label=_('Confirm'), + help_text=_('Mark build as complete'), + ) + + class Meta: + model = Build + fields = [ + 'confirm', + ] + + +class CompleteBuildOutputForm(HelperForm): + """ + Form for completing a single build output + """ field_prefix = { 'serial_numbers': 'fa-hashtag', @@ -70,27 +195,32 @@ class CompleteBuildForm(HelperForm): help_text=_('Location of completed parts'), ) - serial_numbers = forms.CharField( - label=_('Serial numbers'), + confirm_incomplete = forms.BooleanField( required=False, - help_text=_('Enter unique serial numbers (or leave blank)') + help_text=_("Confirm completion with incomplete stock allocation") ) - confirm = forms.BooleanField(required=False, help_text=_('Confirm build completion')) + confirm = forms.BooleanField(required=True, help_text=_('Confirm build completion')) + + output = forms.ModelChoiceField( + queryset=StockItem.objects.all(), # Queryset is narrowed in the view + widget=forms.HiddenInput(), + ) class Meta: model = Build fields = [ - 'serial_numbers', 'location', - 'confirm' + 'output', + 'confirm', + 'confirm_incomplete', ] class CancelBuildForm(HelperForm): """ Form for cancelling a build """ - confirm_cancel = forms.BooleanField(required=False, help_text='Confirm build cancellation') + confirm_cancel = forms.BooleanField(required=False, help_text=_('Confirm build cancellation')) class Meta: model = Build @@ -100,7 +230,13 @@ class CancelBuildForm(HelperForm): class EditBuildItemForm(HelperForm): - """ Form for adding a new BuildItem to a Build """ + """ + Form for creating (or editing) a BuildItem object. + """ + + quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, help_text=_('Select quantity of stock to allocate')) + + part_id = forms.IntegerField(required=False, widget=forms.HiddenInput()) class Meta: model = BuildItem @@ -108,4 +244,19 @@ class EditBuildItemForm(HelperForm): 'build', 'stock_item', 'quantity', + 'install_into', + ] + + +class EditBuildAttachmentForm(HelperForm): + """ + Form for creating / editing a BuildAttachment object + """ + + class Meta: + model = BuildOrderAttachment + fields = [ + 'build', + 'attachment', + 'comment' ] diff --git a/InvenTree/build/migrations/0021_auto_20201020_0908_squashed_0026_auto_20201023_1228.py b/InvenTree/build/migrations/0021_auto_20201020_0908_squashed_0026_auto_20201023_1228.py new file mode 100644 index 0000000000..8db4a7f952 --- /dev/null +++ b/InvenTree/build/migrations/0021_auto_20201020_0908_squashed_0026_auto_20201023_1228.py @@ -0,0 +1,64 @@ +# Generated by Django 3.0.7 on 2020-10-25 21:33 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import mptt.fields + + +class Migration(migrations.Migration): + + replaces = [('build', '0021_auto_20201020_0908'), ('build', '0022_auto_20201020_0953'), ('build', '0023_auto_20201020_1009'), ('build', '0024_auto_20201020_1144'), ('build', '0025_auto_20201020_1248'), ('build', '0026_auto_20201023_1228')] + + dependencies = [ + ('stock', '0052_stockitem_is_building'), + ('build', '0020_auto_20201019_1325'), + ('part', '0051_bomitem_optional'), + ] + + operations = [ + migrations.AddField( + model_name='builditem', + name='install_into', + field=models.ForeignKey(blank=True, help_text='Destination stock item', limit_choices_to={'is_building': True}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='items_to_install', to='stock.StockItem'), + ), + migrations.AlterField( + model_name='builditem', + name='stock_item', + field=models.ForeignKey(help_text='Source stock item', limit_choices_to={'belongs_to': None, 'build_order': None, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem'), + ), + migrations.AddField( + model_name='build', + name='destination', + field=models.ForeignKey(blank=True, help_text='Select location where the completed items will be stored', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incoming_builds', to='stock.StockLocation', verbose_name='Destination Location'), + ), + migrations.AlterField( + model_name='build', + name='parent', + field=mptt.fields.TreeForeignKey(blank=True, help_text='BuildOrder to which this build is allocated', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='children', to='build.Build', verbose_name='Parent Build'), + ), + migrations.AlterField( + model_name='build', + name='status', + field=models.PositiveIntegerField(choices=[(10, 'Pending'), (20, 'Production'), (30, 'Cancelled'), (40, 'Complete')], default=10, help_text='Build status code', validators=[django.core.validators.MinValueValidator(0)], verbose_name='Build Status'), + ), + migrations.AlterField( + model_name='build', + name='part', + field=models.ForeignKey(help_text='Select part to build', limit_choices_to={'active': True, 'assembly': True, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part', verbose_name='Part'), + ), + migrations.AddField( + model_name='build', + name='completed', + field=models.PositiveIntegerField(default=0, help_text='Number of stock items which have been completed', verbose_name='Completed items'), + ), + migrations.AlterField( + model_name='build', + name='quantity', + field=models.PositiveIntegerField(default=1, help_text='Number of stock items to build', validators=[django.core.validators.MinValueValidator(1)], verbose_name='Build Quantity'), + ), + migrations.AlterUniqueTogether( + name='builditem', + unique_together={('build', 'stock_item', 'install_into')}, + ), + ] diff --git a/InvenTree/build/migrations/0022_buildorderattachment.py b/InvenTree/build/migrations/0022_buildorderattachment.py new file mode 100644 index 0000000000..0256649027 --- /dev/null +++ b/InvenTree/build/migrations/0022_buildorderattachment.py @@ -0,0 +1,31 @@ +# Generated by Django 3.0.7 on 2020-10-26 04:17 + +import InvenTree.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('build', '0021_auto_20201020_0908_squashed_0026_auto_20201023_1228'), + ] + + operations = [ + migrations.CreateModel( + name='BuildOrderAttachment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('attachment', models.FileField(help_text='Select file to attach', upload_to=InvenTree.models.rename_attachment)), + ('comment', models.CharField(blank=True, help_text='File comment', max_length=100)), + ('upload_date', models.DateField(auto_now_add=True, null=True)), + ('build', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='build.Build')), + ('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 877affd144..6634b69cdf 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -5,6 +5,7 @@ Build database model definitions # -*- coding: utf-8 -*- from __future__ import unicode_literals +import os from datetime import datetime from django.contrib.auth.models import User @@ -22,8 +23,9 @@ from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey from InvenTree.status_codes import BuildStatus -from InvenTree.helpers import increment, getSetting +from InvenTree.helpers import increment, getSetting, normalize from InvenTree.validators import validate_build_order_reference +from InvenTree.models import InvenTreeAttachment import InvenTree.fields @@ -32,7 +34,7 @@ from part import models as PartModels class Build(MPTTModel): - """ A Build object organises the creation of new parts from the component parts. + """ A Build object organises the creation of new StockItem objects from other existing StockItem objects. Attributes: part: The part to be built (from component BOM items) @@ -62,23 +64,7 @@ class Build(MPTTModel): def get_absolute_url(self): return reverse('build-detail', kwargs={'pk': self.id}) - - def clean(self): - """ - Validation for Build object. - """ - - super().clean() - - try: - if self.part.trackable: - if not self.quantity == int(self.quantity): - raise ValidationError({ - 'quantity': _("Build quantity must be integer value for trackable parts") - }) - except PartModels.Part.DoesNotExist: - pass - + reference = models.CharField( unique=True, max_length=64, @@ -103,7 +89,7 @@ class Build(MPTTModel): blank=True, null=True, related_name='children', verbose_name=_('Parent Build'), - help_text=_('Parent build to which this build is allocated'), + help_text=_('BuildOrder to which this build is allocated'), ) part = models.ForeignKey( @@ -112,7 +98,6 @@ class Build(MPTTModel): on_delete=models.CASCADE, related_name='builds', limit_choices_to={ - 'is_template': False, 'assembly': True, 'active': True, 'virtual': False, @@ -138,11 +123,26 @@ class Build(MPTTModel): help_text=_('Select location to take stock from for this build (leave blank to take from any stock location)') ) + destination = models.ForeignKey( + 'stock.StockLocation', + verbose_name=_('Destination Location'), + on_delete=models.SET_NULL, + related_name='incoming_builds', + null=True, blank=True, + help_text=_('Select location where the completed items will be stored'), + ) + quantity = models.PositiveIntegerField( verbose_name=_('Build Quantity'), default=1, validators=[MinValueValidator(1)], - help_text=_('Number of parts to build') + help_text=_('Number of stock items to build') + ) + + completed = models.PositiveIntegerField( + verbose_name=_('Completed items'), + default=0, + help_text=_('Number of stock items which have been completed') ) status = models.PositiveIntegerField( @@ -182,10 +182,96 @@ class Build(MPTTModel): blank=True, help_text=_('Extra build notes') ) + @property + def bom_items(self): + """ + Returns the BOM items for the part referenced by this BuildOrder + """ + + return self.part.bom_items.all().prefetch_related( + 'sub_part' + ) + + @property + def remaining(self): + """ + Return the number of outputs remaining to be completed. + """ + + return max(0, self.quantity - self.completed) + @property def output_count(self): return self.build_outputs.count() + def get_build_outputs(self, **kwargs): + """ + Return a list of build outputs. + + kwargs: + complete = (True / False) - If supplied, filter by completed status + in_stock = (True / False) - If supplied, filter by 'in-stock' status + """ + + outputs = self.build_outputs.all() + + # Filter by 'in stock' status + in_stock = kwargs.get('in_stock', None) + + if in_stock is not None: + if in_stock: + outputs = outputs.filter(StockModels.StockItem.IN_STOCK_FILTER) + else: + outputs = outputs.exclude(StockModels.StockItem.IN_STOCK_FILTER) + + # Filter by 'complete' status + complete = kwargs.get('complete', None) + + if complete is not None: + if complete: + outputs = outputs.filter(is_building=False) + else: + outputs = outputs.filter(is_building=True) + + return outputs + + @property + def complete_outputs(self): + """ + Return all the "completed" build outputs + """ + + outputs = self.get_build_outputs(complete=True) + + # TODO - Ordering? + + return outputs + + @property + def incomplete_outputs(self): + """ + Return all the "incomplete" build outputs + """ + + outputs = self.get_build_outputs(complete=False) + + # TODO - Order by how "complete" they are? + + return outputs + + @property + def incomplete_count(self): + """ + Return the total number of "incomplete" outputs + """ + + quantity = 0 + + for output in self.incomplete_outputs: + quantity += output.quantity + + return quantity + @classmethod def getNextBuildNumber(cls): """ @@ -218,6 +304,42 @@ class Build(MPTTModel): return new_ref + @property + def can_complete(self): + """ + Returns True if this build can be "completed" + + - Must not have any outstanding build outputs + - 'completed' value must meet (or exceed) the 'quantity' value + """ + + if self.incomplete_count > 0: + return False + + if self.completed < self.quantity: + return False + + # No issues! + return True + + @transaction.atomic + def complete_build(self, user): + """ + Mark this build as complete + """ + + if not self.can_complete: + return + + self.completion_date = datetime.now().date() + self.completed_by = user + self.status = BuildStatus.COMPLETE + self.save() + + # Ensure that there are no longer any BuildItem objects + # which point to thie Build Order + self.allocated_stock.all().delete() + @transaction.atomic def cancelBuild(self, user): """ Mark the Build as CANCELLED @@ -237,58 +359,77 @@ class Build(MPTTModel): self.status = BuildStatus.CANCELLED self.save() - def getAutoAllocations(self): - """ Return a list of parts which will be allocated + def getAutoAllocations(self, output): + """ + Return a list of StockItem objects which will be allocated using the 'AutoAllocate' function. - For each item in the BOM for the attached Part: - - - If there is a single StockItem, use that StockItem - - Take as many parts as available (up to the quantity required for the BOM) - - If there are multiple StockItems available, ignore (leave up to the user) + For each item in the BOM for the attached Part, + the following tests must *all* evaluate to True, + for the part to be auto-allocated: + - The sub_item in the BOM line must *not* be trackable + - There is only a single stock item available (which has not already been allocated to this build) + - The stock item has an availability greater than zero + Returns: - A list object containing the StockItem objects to be allocated (and the quantities) + A list object containing the StockItem objects to be allocated (and the quantities). + Each item in the list is a dict as follows: + { + 'stock_item': stock_item, + 'quantity': stock_quantity, + } """ allocations = [] - for item in self.part.bom_items.all().prefetch_related('sub_part'): + """ + Iterate through each item in the BOM + """ - # How many parts required for this build? - q_required = item.quantity * self.quantity + for bom_item in self.bom_items: - # Grab a list of StockItem objects which are "in stock" - stock = StockModels.StockItem.objects.filter(StockModels.StockItem.IN_STOCK_FILTER) - - # Filter by part reference - stock = stock.filter(part=item.sub_part) + part = bom_item.sub_part + + # Skip any parts which are already fully allocated + if self.isPartFullyAllocated(part, output): + continue + + # How many parts are required to complete the output? + required = self.unallocatedQuantity(part, output) + + # Grab a list of stock items which are available + stock_items = self.availableStockItems(part, output) # Ensure that the available stock items are in the correct location if self.take_from is not None: # Filter for stock that is located downstream of the designated location - stock = stock.filter(location__in=[loc for loc in self.take_from.getUniqueChildren()]) + stock_items = stock_items.filter(location__in=[loc for loc in self.take_from.getUniqueChildren()]) # Only one StockItem to choose from? Default to that one! - if len(stock) == 1: - stock_item = stock[0] + if stock_items.count() == 1: + stock_item = stock_items[0] - # Check that we have not already allocated this stock-item against this build - build_items = BuildItem.objects.filter(build=self, stock_item=stock_item) + # Double check that we have not already allocated this stock-item against this build + build_items = BuildItem.objects.filter( + build=self, + stock_item=stock_item, + install_into=output + ) if len(build_items) > 0: continue - # Are there any parts available? + # How many items are actually available? if stock_item.quantity > 0: # Only take as many as are available - if stock_item.quantity < q_required: - q_required = stock_item.quantity + if stock_item.quantity < required: + required = stock_item.quantity allocation = { 'stock_item': stock_item, - 'quantity': q_required, + 'quantity': required, } allocations.append(allocation) @@ -296,14 +437,130 @@ class Build(MPTTModel): return allocations @transaction.atomic - def unallocateStock(self): - """ Deletes all stock allocations for this build. """ + def unallocateStock(self, output=None, part=None): + """ + Deletes all stock allocations for this build. + + Args: + output: Specify which build output to delete allocations (optional) - BuildItem.objects.filter(build=self.id).delete() + """ + + allocations = BuildItem.objects.filter(build=self.pk) + + if output: + allocations = allocations.filter(install_into=output.pk) + + if part: + allocations = allocations.filter(stock_item__part=part) + + # Remove all the allocations + allocations.delete() @transaction.atomic - def autoAllocate(self): - """ Run auto-allocation routine to allocate StockItems to this Build. + def create_build_output(self, quantity, **kwargs): + """ + Create a new build output against this BuildOrder. + + args: + quantity: The quantity of the item to produce + + kwargs: + batch: Override batch code + serials: Serial numbers + location: Override location + """ + + batch = kwargs.get('batch', self.batch) + location = kwargs.get('location', self.destination) + serials = kwargs.get('serials', None) + + """ + Determine if we can create a single output (with quantity > 0), + or multiple outputs (with quantity = 1) + """ + + multiple = False + + # Serial numbers are provided? We need to split! + if serials: + multiple = True + + # BOM has trackable parts, so we must split! + if self.part.has_trackable_parts: + multiple = True + + if multiple: + """ + Create multiple build outputs with a single quantity of 1 + """ + + for ii in range(quantity): + + if serials: + serial = serials[ii] + else: + serial = None + + StockModels.StockItem.objects.create( + quantity=1, + location=location, + part=self.part, + build=self, + batch=batch, + serial=serial, + is_building=True, + ) + + else: + """ + Create a single build output of the given quantity + """ + + StockModels.StockItem.objects.create( + quantity=quantity, + location=location, + part=self.part, + build=self, + batch=batch, + is_building=True + ) + + if self.status == BuildStatus.PENDING: + self.status = BuildStatus.PRODUCTION + self.save() + + @transaction.atomic + def deleteBuildOutput(self, output): + """ + Remove a build output from the database: + + - Unallocate any build items against the output + - Delete the output StockItem + """ + + if not output: + raise ValidationError(_("No build output specified")) + + if not output.is_building: + raise ValidationError(_("Build output is already completed")) + + if not output.build == self: + raise ValidationError(_("Build output does not match Build Order")) + + # Unallocate all build items against the output + self.unallocateStock(output) + + # Remove the build output from the database + output.delete() + + @transaction.atomic + def autoAllocate(self, output): + """ + Run auto-allocation routine to allocate StockItems to this Build. + + Args: + output: If specified, only auto-allocate against the given built output Returns a list of dict objects with keys like: @@ -315,128 +572,171 @@ class Build(MPTTModel): See: getAutoAllocations() """ - allocations = self.getAutoAllocations() + allocations = self.getAutoAllocations(output) for item in allocations: # Create a new allocation build_item = BuildItem( build=self, stock_item=item['stock_item'], - quantity=item['quantity']) + quantity=item['quantity'], + install_into=output, + ) build_item.save() @transaction.atomic - def completeBuild(self, location, serial_numbers, user): - """ Mark the Build as COMPLETE + def completeBuildOutput(self, output, user, **kwargs): + """ + Complete a particular build output - - Takes allocated items from stock - - Delete pending BuildItem objects + - Remove allocated StockItems + - Mark the output as complete """ - # Complete the build allocation for each BuildItem - for build_item in self.allocated_stock.all().prefetch_related('stock_item'): + # List the allocated BuildItem objects for the given output + allocated_items = output.items_to_install.all() + + for build_item in allocated_items: + + # TODO: This is VERY SLOW as each deletion from the database takes ~1 second to complete + # TODO: Use celery / redis to offload the actual object deletion... + # REF: https://www.botreetechnologies.com/blog/implementing-celery-using-django-for-background-task-processing + # REF: https://code.tutsplus.com/tutorials/using-celery-with-django-for-background-task-processing--cms-28732 + + # Complete the allocation of stock for that item build_item.complete_allocation(user) - # Check that the stock-item has been assigned to this build, and remove the builditem from the database - if build_item.stock_item.build_order == self: - build_item.delete() + # Delete the BuildItem objects from the database + allocated_items.all().delete() - notes = 'Built {q} on {now}'.format( - q=self.quantity, - now=str(datetime.now().date()) + # Ensure that the output is updated correctly + output.build = self + output.is_building = False + + output.save() + + output.addTransactionNote( + _('Completed build output'), + user, + system=True ) - # Generate the build outputs - if self.part.trackable and serial_numbers: - # Add new serial numbers - for serial in serial_numbers: - item = StockModels.StockItem.objects.create( - part=self.part, - build=self, - location=location, - quantity=1, - serial=serial, - batch=str(self.batch) if self.batch else '', - notes=notes - ) - - item.save() - - else: - # Add stock of the newly created item - item = StockModels.StockItem.objects.create( - part=self.part, - build=self, - location=location, - quantity=self.quantity, - batch=str(self.batch) if self.batch else '', - notes=notes - ) - - item.save() - - # Finally, mark the build as complete - self.completion_date = datetime.now().date() - self.completed_by = user - self.status = BuildStatus.COMPLETE + # Increase the completed quantity for this build + self.completed += output.quantity self.save() - return True - - def isFullyAllocated(self): + def requiredQuantity(self, part, output): """ - Return True if this build has been fully allocated. - """ - - bom_items = self.part.bom_items.all() - - for item in bom_items: - part = item.sub_part - - if not self.isPartFullyAllocated(part): - return False - - return True - - def isPartFullyAllocated(self, part): - """ - Check if a given Part is fully allocated for this Build - """ - - return self.getAllocatedQuantity(part) >= self.getRequiredQuantity(part) - - def getRequiredQuantity(self, part): - """ Calculate the quantity of <part> required to make this build. + Get the quantity of a part required to complete the particular build output. + + Args: + part: The Part object + output - The particular build output (StockItem) """ + # Extract the BOM line item from the database try: - item = PartModels.BomItem.objects.get(part=self.part.id, sub_part=part.id) - q = item.quantity - except PartModels.BomItem.DoesNotExist: - q = 0 + bom_item = PartModels.BomItem.objects.get(part=self.part.pk, sub_part=part.pk) + quantity = bom_item.quantity + except (PartModels.BomItem.DoesNotExist): + quantity = 0 - return q * self.quantity + if output: + quantity *= output.quantity + else: + quantity *= self.remaining - def getAllocatedQuantity(self, part): - """ Calculate the total number of <part> currently allocated to this build + return quantity + + def allocatedItems(self, part, output): + """ + Return all BuildItem objects which allocate stock of <part> to <output> + + Args: + part - The part object + output - Build output (StockItem). """ - allocated = BuildItem.objects.filter(build=self.id, stock_item__part=part.id).aggregate(q=Coalesce(Sum('quantity'), 0)) + allocations = BuildItem.objects.filter( + build=self, + stock_item__part=part, + install_into=output, + ) + + return allocations + + def allocatedQuantity(self, part, output): + """ + Return the total quantity of given part allocated to a given build output. + """ + + allocations = self.allocatedItems(part, output) + + allocated = allocations.aggregate(q=Coalesce(Sum('quantity'), 0)) return allocated['q'] - def getUnallocatedQuantity(self, part): - """ Calculate the quantity of <part> which still needs to be allocated to this build. - - Args: - Part - the part to be tested - - Returns: - The remaining allocated quantity + def unallocatedQuantity(self, part, output): + """ + Return the total unallocated (remaining) quantity of a part against a particular output. """ - return max(self.getRequiredQuantity(part) - self.getAllocatedQuantity(part), 0) + required = self.requiredQuantity(part, output) + allocated = self.allocatedQuantity(part, output) + + return max(required - allocated, 0) + + def isPartFullyAllocated(self, part, output): + """ + Returns True if the part has been fully allocated to the particular build output + """ + + return self.unallocatedQuantity(part, output) == 0 + + def isFullyAllocated(self, output): + """ + Returns True if the particular build output is fully allocated. + """ + + for bom_item in self.bom_items: + part = bom_item.sub_part + + if not self.isPartFullyAllocated(part, output): + return False + + # All parts must be fully allocated! + return True + + def allocatedParts(self, output): + """ + Return a list of parts which have been fully allocated against a particular output + """ + + allocated = [] + + for bom_item in self.bom_items: + part = bom_item.sub_part + + if self.isPartFullyAllocated(part, output): + allocated.append(part) + + return allocated + + def unallocatedParts(self, output): + """ + Return a list of parts which have *not* been fully allocated against a particular output + """ + + unallocated = [] + + for bom_item in self.bom_items: + part = bom_item.sub_part + + if not self.isPartFullyAllocated(part, output): + unallocated.append(part) + + return unallocated @property def required_parts(self): @@ -444,26 +744,44 @@ class Build(MPTTModel): parts = [] for item in self.part.bom_items.all().prefetch_related('sub_part'): - part = { - 'part': item.sub_part, - 'per_build': item.quantity, - 'quantity': item.quantity * self.quantity, - 'allocated': self.getAllocatedQuantity(item.sub_part) - } - - parts.append(part) + parts.append(item.sub_part) return parts - @property - def can_build(self): - """ Return true if there are enough parts to supply build """ + def availableStockItems(self, part, output): + """ + Returns stock items which are available for allocation to this build. - for item in self.required_parts: - if item['part'].total_stock < item['quantity']: - return False + Args: + part - Part object + output - The particular build output + """ - return True + # Grab initial query for items which are "in stock" and match the part + items = StockModels.StockItem.objects.filter( + StockModels.StockItem.IN_STOCK_FILTER + ) + + items = items.filter(part=part) + + # Exclude any items which have already been allocated + allocated = BuildItem.objects.filter( + build=self, + stock_item__part=part, + install_into=output, + ) + + items = items.exclude( + id__in=[item.stock_item.id for item in allocated.all()] + ) + + # Limit query to stock items which are "downstream" of the source location + if self.take_from is not None: + items = items.filter( + location__in=[loc for loc in self.take_from.getUniqueChildren()] + ) + + return items @property def is_active(self): @@ -478,9 +796,21 @@ class Build(MPTTModel): @property def is_complete(self): """ Returns True if the build status is COMPLETE """ + return self.status == BuildStatus.COMPLETE +class BuildOrderAttachment(InvenTreeAttachment): + """ + Model for storing file attachments against a BuildOrder object + """ + + def getSubdir(self): + return os.path.join('bo_files', str(self.build.id)) + + build = models.ForeignKey(Build, on_delete=models.CASCADE, related_name='attachments') + + class BuildItem(models.Model): """ A BuildItem links multiple StockItem objects to a Build. These are used to allocate part stock to a build. @@ -500,9 +830,38 @@ class BuildItem(models.Model): class Meta: unique_together = [ - ('build', 'stock_item'), + ('build', 'stock_item', 'install_into'), ] + def save(self, *args, **kwargs): + + self.validate_unique() + self.clean() + + super().save() + + def validate_unique(self, exclude=None): + """ + Test that this BuildItem object is "unique". + Essentially we do not want a stock_item being allocated to a Build multiple times. + """ + + super().validate_unique(exclude) + + items = BuildItem.objects.exclude(id=self.id).filter( + build=self.build, + stock_item=self.stock_item, + install_into=self.install_into + ) + + if items.exists(): + msg = _("BuildItem must be unique for build, stock_item and install_into") + raise ValidationError({ + 'build': msg, + 'stock_item': msg, + 'install_into': msg + }) + def clean(self): """ Check validity of the BuildItem model. The following checks are performed: @@ -510,28 +869,38 @@ class BuildItem(models.Model): - StockItem.part must be in the BOM of the Part object referenced by Build - Allocation quantity cannot exceed available quantity """ + + self.validate_unique() - super(BuildItem, self).clean() + super().clean() errors = {} + if not self.install_into: + raise ValidationError(_('Build item must specify a build output')) + try: - if self.stock_item.part not in self.build.part.required_parts(): + # Allocated part must be in the BOM for the master part + if self.stock_item.part not in self.build.part.getRequiredParts(recursive=False): errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'".format(p=self.build.part.full_name))] + # Allocated quantity cannot exceed available stock quantity if self.quantity > self.stock_item.quantity: errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})".format( - n=self.quantity, - q=self.stock_item.quantity + n=normalize(self.quantity), + q=normalize(self.stock_item.quantity) ))] + # Allocated quantity cannot cause the stock item to be over-allocated if self.stock_item.quantity - self.stock_item.allocation_count() + self.quantity < self.quantity: errors['quantity'] = _('StockItem is over-allocated') + # Allocated quantity must be positive if self.quantity <= 0: errors['quantity'] = _('Allocation quantity must be greater than zero') - if self.stock_item.serial and not self.quantity == 1: + # Quantity must be 1 for serialized stock + if self.stock_item.serialized and not self.quantity == 1: errors['quantity'] = _('Quantity must be 1 for serialized stock') except (StockModels.StockItem.DoesNotExist, PartModels.Part.DoesNotExist): @@ -540,22 +909,33 @@ class BuildItem(models.Model): if len(errors) > 0: raise ValidationError(errors) + @transaction.atomic def complete_allocation(self, user): + """ + Complete the allocation of this BuildItem into the output stock item. + + - If the referenced part is trackable, the stock item will be *installed* into the build output + - If the referenced part is *not* trackable, the stock item will be removed from stock + """ item = self.stock_item - # Split the allocated stock if there are more available than allocated - if item.quantity > self.quantity: - item = item.splitStock(self.quantity, None, user) + # For a trackable part, special consideration needed! + if item.part.trackable: + # Split the allocated stock if there are more available than allocated + if item.quantity > self.quantity: + item = item.splitStock(self.quantity, None, user) - # Update our own reference to the new item - self.stock_item = item - self.save() + # Make sure we are pointing to the new item + self.stock_item = item + self.save() - # TODO - If the item__part object is not trackable, delete the stock item here - - item.build_order = self.build - item.save() + # Install the stock item into the output + item.belongs_to = self.install_into + item.save() + else: + # Simply remove the items from stock + item.take_stock(self.quantity, user) build = models.ForeignKey( Build, @@ -568,7 +948,7 @@ class BuildItem(models.Model): 'stock.StockItem', on_delete=models.CASCADE, related_name='allocations', - help_text=_('Stock Item to allocate to build'), + help_text=_('Source stock item'), limit_choices_to={ 'build_order': None, 'sales_order': None, @@ -583,3 +963,14 @@ class BuildItem(models.Model): validators=[MinValueValidator(0)], help_text=_('Stock quantity to allocate to build') ) + + install_into = models.ForeignKey( + 'stock.StockItem', + on_delete=models.SET_NULL, + blank=True, null=True, + related_name='items_to_install', + help_text=_('Destination stock item'), + limit_choices_to={ + 'is_building': True, + } + ) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 53e75942f0..4423619b8a 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -38,6 +38,7 @@ class BuildSerializer(InvenTreeModelSerializer): 'url', 'title', 'creation_date', + 'completed', 'completion_date', 'part', 'part_detail', @@ -51,9 +52,10 @@ class BuildSerializer(InvenTreeModelSerializer): ] read_only_fields = [ - 'status', + 'completed', 'creation_date', 'completion_data', + 'status', 'status_text', ] @@ -73,6 +75,7 @@ class BuildItemSerializer(InvenTreeModelSerializer): fields = [ 'pk', 'build', + 'install_into', 'part', 'part_name', 'part_image', diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html index bf43d0ca85..e811c37ce8 100644 --- a/InvenTree/build/templates/build/allocate.html +++ b/InvenTree/build/templates/build/allocate.html @@ -11,404 +11,68 @@ InvenTree | Allocate Parts {% include "build/tabs.html" with tab='allocate' %} -<div id='build-item-toolbar'> - {% if build.status == BuildStatus.PENDING %} - <div class='btn-group'> - <button class='btn btn-primary' type='button' id='btn-order-parts' title='Order Parts'>{% trans "Order Parts" %}</button> - <button class='btn btn-primary' type='button' id='btn-allocate' title='{% trans "Automatically allocate stock" %}'>{% trans "Auto Allocate" %}</button> - <button class='btn btn-danger' type='button' id='btn-unallocate' title='Unallocate Stock'>{% trans "Unallocate" %}</button> - </div> - {% endif %} +<h4>{% trans "Incomplete Build Ouputs" %}</h4> + +<hr> + +{% if build.is_complete %} +<div class='alert alert-block alert-success'> + {% trans "Build order has been completed" %} </div> - -<table class='table table-striped table-condensed' id='build-item-list' data-toolbar='#build-item-toolbar'></table> +{% else %} +<div class='btn-group' role='group'> + <button class='btn btn-primary' type='button' id='btn-create-output' title='{% trans "Create new build output" %}'> + <span class='fas fa-plus-circle'></span> {% trans "Create New Output" %} + </button> + <!-- + <button class='btn btn-primary' type='button' id='btn-order-parts' title='{% trans "Order required parts" %}'> + <span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %} + </button> + --> + <button class='btn btn-danger' type='button' id='btn-unallocate' title='{% trans "Unallocate stock" %}'> + <span class='fas fa-minus-circle'></span> {% trans "Unallocate Stock" %} + </button> +</div> + +<hr> +{% if build.incomplete_outputs %} +<div class="panel-group" id="build-output-accordion" role="tablist" aria-multiselectable="true"> + {% for item in build.incomplete_outputs %} + {% include "build/allocation_card.html" with item=item %} + {% endfor %} +</div> +{% else %} +<div class='alert alert-block alert-info'> + <b>{% trans "Create a new build output" %}</b><br> + {% trans "No incomplete build outputs remain." %}<br> + {% trans "Create a new build output using the button above" %} +</div> +{% endif %} + +{% endif %} {% endblock %} {% block js_ready %} {{ block.super }} - var buildTable = $("#build-item-list"); + var buildInfo = { + pk: {{ build.pk }}, + quantity: {{ build.quantity }}, + completed: {{ build.completed }}, + part: {{ build.part.pk }}, + }; - // Calculate sum of allocations for a particular table row - function sumAllocations(row) { - if (row.allocations == null) { - return 0; - } - - var quantity = 0; - - row.allocations.forEach(function(item) { - quantity += item.quantity; - }); - - return quantity; - } - - function getUnallocated(row) { - // Return the number of items remaining to be allocated for a given row - return {{ build.quantity }} * row.quantity - sumAllocations(row); - } - - function setExpandedAllocatedLocation(row) { - // Handle case when stock item does not have a location set - if (row.location_detail == null) { - return 'No stock location set'; - } else { - return row.location_detail.pathstring; - } - } - - function reloadTable() { - // Reload the build allocation table - buildTable.bootstrapTable('refresh'); - } - - function setupCallbacks() { - // Register button callbacks once the table data are loaded - - buildTable.find(".button-add").click(function() { - var pk = $(this).attr('pk'); - - // Extract row data from the table - var idx = $(this).closest('tr').attr('data-index'); - var row = buildTable.bootstrapTable('getData')[idx]; - - launchModalForm('/build/item/new/', { - success: reloadTable, - data: { - part: row.sub_part, - build: {{ build.id }}, - quantity: getUnallocated(row), - }, - secondary: [ - { - field: 'stock_item', - label: '{% trans "New Stock Item" %}', - title: '{% trans "Create new Stock Item" %}', - url: '{% url "stock-item-create" %}', - data: { - part: row.sub_part, - }, - }, - ] - }); - }); - - - buildTable.find(".button-build").click(function() { - // Start a new build for the sub_part - - var pk = $(this).attr('pk'); - - // Extract row data from the table - var idx = $(this).closest('tr').attr('data-index'); - var row = buildTable.bootstrapTable('getData')[idx]; - - launchModalForm('/build/new/', { - follow: true, - data: { - part: row.sub_part, - parent: {{ build.id }}, - quantity: getUnallocated(row), - }, - }); - - }); - - buildTable.find(".button-buy").click(function() { - var pk = $(this).attr('pk'); - - // Extract row data from the table - var idx = $(this).closest('tr').attr('data-index'); - var row = buildTable.bootstrapTable('getData')[idx]; - - launchModalForm("{% url 'order-parts' %}", { - data: { - parts: [row.sub_part], - }, - }); - }); - } - - buildTable.inventreeTable({ - uniqueId: 'sub_part', - url: "{% url 'api-bom-list' %}", - onPostBody: setupCallbacks, - detailViewByClick: true, - detailView: true, - detailFilter: function(index, row) { - return row.allocations != null; - }, - detailFormatter: function(index, row, element) { - // Construct an 'inner table' which shows the stock allocations - - var subTableId = `allocation-table-${row.pk}`; - - var html = `<div class='sub-table'><table class='table table-condensed table-striped' id='${subTableId}'></table></div>`; - - element.html(html); - - var lineItem = row; - - var subTable = $(`#${subTableId}`); - - subTable.bootstrapTable({ - data: row.allocations, - showHeader: true, - columns: [ - { - width: '50%', - field: 'quantity', - title: 'Quantity', - formatter: function(value, row, index, field) { - var text = ''; - - var url = ''; - - if (row.serial && row.quantity == 1) { - text = `{% trans "Serial Number" %}: ${row.serial}`; - } else { - text = `{% trans "Quantity" %}: ${row.quantity}`; - } - - {% if build.status == BuildStatus.COMPLETE %} - url = `/stock/item/${row.pk}/`; - {% else %} - url = `/stock/item/${row.stock_item}/`; - {% endif %} - - return renderLink(text, url); - }, - }, - { - field: 'location', - title: '{% trans "Location" %}', - formatter: function(value, row, index, field) { - {% if build.status == BuildStatus.COMPLETE %} - var text = setExpandedAllocatedLocation(row); - var url = `/stock/location/${row.location}/`; - {% else %} - var text = row.stock_item_detail.location_name; - var url = `/stock/location/${row.stock_item_detail.location}/`; - {% endif %} - - return renderLink(text, url); - } - }, - {% if build.status == BuildStatus.PENDING %} - { - field: 'buttons', - title: 'Actions', - formatter: function(value, row) { - - var pk = row.pk; - - var html = `<div class='btn-group float-right' role='group'>`; - - {% if build.status == BuildStatus.PENDING %} - html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); - html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); - {% endif %} - - html += `</div>`; - - return html; - }, - }, - {% endif %} - ] - }); - - // Assign button callbacks to the newly created allocation buttons - subTable.find(".button-allocation-edit").click(function() { - var pk = $(this).attr('pk'); - launchModalForm(`/build/item/${pk}/edit/`, { - success: reloadTable, - }); - }); - - subTable.find('.button-allocation-delete').click(function() { - var pk = $(this).attr('pk'); - launchModalForm(`/build/item/${pk}/delete/`, { - success: reloadTable, - }); - }); - }, - formatNoMatches: function() { return "{% trans 'No BOM items found' %}"; }, - onLoadSuccess: function(tableData) { - // Once the BOM data are loaded, request allocation data for the build - {% if build.status == BuildStatus.COMPLETE %} - // Request StockItem which have been assigned to this build - inventreeGet('/api/stock/', - { - build_order: {{ build.id }}, - location_detail: true, - }, - { - success: function(data) { - // Iterate through the returned data, group by "part", - var allocations = {}; - - data.forEach(function(item) { - // Group allocations by referenced 'part' - var key = parseInt(item.part); - - if (!(key in allocations)) { - allocations[key] = new Array(); - } - - allocations[key].push(item); - }); - - for (var key in allocations) { - - var tableRow = buildTable.bootstrapTable('getRowByUniqueId', key); - - tableRow.allocations = allocations[key]; - - buildTable.bootstrapTable('updateByUniqueId', key, tableRow, true); - } - }, - }, - ); - - {% else %} - inventreeGet('/api/build/item/', - { - build: {{ build.id }}, - }, - { - success: function(data) { - - // Iterate through the returned data, and group by "part" - var allocations = {}; - - data.forEach(function(item) { - - // Group allocations by referenced 'part' - var part = item.part; - var key = parseInt(part); - - if (!(key in allocations)) { - allocations[key] = new Array(); - } - - // Add the allocation to the list - allocations[key].push(item); - }); - - for (var key in allocations) { - - // Select the associated row in the table - var tableRow = buildTable.bootstrapTable('getRowByUniqueId', key); - - // Set the allocations for the row - tableRow.allocations = allocations[key]; - - // And push the updated row back into the main table - buildTable.bootstrapTable('updateByUniqueId', key, tableRow, true); - } - } - }, - ); - {% endif %} - }, - queryParams: { - part: {{ build.part.id }}, - sub_part_detail: 1, - }, - columns: [ - { - field: 'id', - visible: false, - }, - { - sortable: true, - field: 'sub_part', - title: '{% trans "Part" %}', - formatter: function(value, row, index, field) { - return imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, `/part/${row.sub_part}/`); - }, - }, - { - sortable: true, - field: 'sub_part_detail.description', - title: '{% trans "Description" %}', - }, - { - sortable: true, - field: 'reference', - title: '{% trans "Reference" %}', - }, - { - sortable: true, - field: 'quantity', - title: '{% trans "Required" %}', - formatter: function(value, row) { - return value * {{ build.quantity }}; - }, - }, - { - sortable: true, - field: 'allocated', - {% if build.status == BuildStatus.COMPLETE %} - title: '{% trans "Assigned" %}', - {% else %} - title: '{% trans "Allocated" %}', - {% endif %} - formatter: function(value, row) { - - var allocated = sumAllocations(row); - - return makeProgressBar(allocated, row.quantity * {{ build.quantity }}); - }, - sorter: function(valA, valB, rowA, rowB) { - - var aA = sumAllocations(rowA); - var aB = sumAllocations(rowB); - - var qA = rowA.quantity * {{ build.quantity }}; - var qB = rowB.quantity * {{ build.quantity }}; - - if (aA == 0 && aB == 0) { - return (qA > qB) ? 1 : -1; - } - - var progressA = parseFloat(aA) / qA; - var progressB = parseFloat(aB) / qB; - - return (progressA < progressB) ? 1 : -1; - } - }, - {% if build.status == BuildStatus.PENDING %} - { - field: 'buttons', - formatter: function(value, row, index, field) { - - var html = `<div class='btn-group float-right' role='group'>`; - var pk = row.sub_part; - - {% if build.status == BuildStatus.PENDING %} - if (row.sub_part_detail.purchaseable) { - html += makeIconButton('fa-shopping-cart', 'button-buy', pk, '{% trans "Buy parts" %}'); - } - - if (row.sub_part_detail.assembly) { - html += makeIconButton('fa-tools', 'button-build', pk, '{% trans "Build parts" %}'); - } - - html += makeIconButton('fa-plus icon-green', 'button-add', pk, '{% trans "Allocate stock" %}'); - {% endif %} - - html += '</div>'; - - return html; - }, + {% for item in build.incomplete_outputs %} + // Get the build output as a javascript object + inventreeGet('{% url 'api-stock-detail' item.pk %}', {}, + { + success: function(response) { + loadBuildOutputAllocationTable(buildInfo, response); } - {% endif %} - ], - }); + } + ); + {% endfor %} {% if build.status == BuildStatus.PENDING %} $("#btn-allocate").on('click', function() { @@ -424,7 +88,15 @@ InvenTree | Allocate Parts launchModalForm( "{% url 'build-unallocate' build.id %}", { - success: reloadTable, + reload: true, + } + ); + }); + + $('#btn-create-output').click(function() { + launchModalForm('{% url "build-output-create" build.id %}', + { + reload: true, } ); }); diff --git a/InvenTree/build/templates/build/allocation_card.html b/InvenTree/build/templates/build/allocation_card.html new file mode 100644 index 0000000000..650257bb0d --- /dev/null +++ b/InvenTree/build/templates/build/allocation_card.html @@ -0,0 +1,43 @@ +{% load i18n %} +{% load inventree_extras %} + +{% define item.pk as pk %} + +<div class="panel panel-default" id='allocation-panel-{{ pk }}'> + <div class="panel-heading" role="tab" id="heading-{{ pk }}"> + <div class="panel-title"> + <div class='row'> + <a class='collapsed' aria-expanded='false' role="button" data-toggle="collapse" data-parent="#build-output-accordion" href="#collapse-{{ pk }}" aria-controls="collapse-{{ pk }}"> + <div class='col-sm-4'> + <span class='fas fa-caret-right'></span> + {{ item.part.full_name }} + </div> + <div class='col-sm-2'> + {% if item.serial %} + # {{ item.serial }} + {% else %} + {% decimal item.quantity %} + {% endif %} + </div> + </a> + <div class='col-sm-3'> + <div> + <div id='output-progress-{{ pk }}'> + <span class='fas fa-spin fa-spinner'></span> + </div> + </div> + </div> + <div class='col-sm-3'> + <div class='btn-group float-right' id='output-actions-{{ pk }}'> + <span class='fas fa-spin fa-spinner'></span> + </div> + </div> + </div> + </div> + </div> + <div id="collapse-{{ pk }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading-{{ pk }}"> + <div class="panel-body"> + <table class='table table-striped table-condensed' id='allocation-table-{{ pk }}'></table> + </div> + </div> + </div> \ No newline at end of file diff --git a/InvenTree/build/templates/build/attachments.html b/InvenTree/build/templates/build/attachments.html new file mode 100644 index 0000000000..a2dde0868c --- /dev/null +++ b/InvenTree/build/templates/build/attachments.html @@ -0,0 +1,78 @@ +{% extends "build/build_base.html" %} + +{% load static %} +{% load i18n %} +{% load markdownify %} + +{% block details %} + +{% include "build/tabs.html" with tab='attachments' %} + +<h4>{% trans "Attachments" %}</h4> +<hr> + +{% include "attachment_table.html" with attachments=build.attachments.all %} + +{% endblock %} + +{% block js_ready %} +{{ block.super }} + +enableDragAndDrop( + '#attachment-dropzone', + '{% url "build-attachment-create" %}', + { + data: { + build: {{ build.id }}, + }, + label: 'attachment', + success: function(data, status, xhr) { + location.reload(); + } + } +); + +// Callback for creating a new attachment +$('#new-attachment').click(function() { + launchModalForm( + '{% url "build-attachment-create" %}', + { + reload: true, + data: { + build: {{ build.pk }}, + } + } + ); +}); + +// Callback for editing an attachment +$("#attachment-table").on('click', '.attachment-edit-button', function() { + var pk = $(this).attr('pk'); + + var url = `/build/attachment/${pk}/edit/`; + + launchModalForm( + url, + { + reload: true, + } + ); +}); + +// Callback for deleting an attachment +$("#attachment-table").on('click', '.attachment-delete-button', function() { + var pk = $(this).attr('pk'); + + var url = `/build/attachment/${pk}/delete/`; + + launchModalForm( + url, + { + reload: true, + } + ); +}); + +$("#attachment-table").inventreeTable({}); + +{% endblock %} diff --git a/InvenTree/build/templates/build/auto_allocate.html b/InvenTree/build/templates/build/auto_allocate.html index 241471e304..48d1837ae0 100644 --- a/InvenTree/build/templates/build/auto_allocate.html +++ b/InvenTree/build/templates/build/auto_allocate.html @@ -6,13 +6,10 @@ {{ block.super }} <div class='alert alert-block alert-info'> -<b>{% trans "Automatically Allocate Stock" %}</b><br> -{% trans "Stock Items are selected for automatic allocation if there is only a single stock item available." %}<br> -{% trans "The following stock items will be allocated to the build:" %}<br> + <b>{% trans "Automatically Allocate Stock" %}</b><br> + {% trans "The following stock items will be allocated to the specified build output" %} </div> - {% if allocations %} - <table class='table table-striped table-condensed'> <tr> <th></th> diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index a366459908..3a94398e87 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -68,11 +68,6 @@ src="{% static 'img/blank_image.png' %}" <h4>{% trans "Build Details" %}</h4> <table class='table table-striped table-condensed'> - <tr> - <td><span class='fas fa-hashtag'></span></td> - <td>{% trans "Build Order Reference" %}</td> - <td>{{ build }}</td> - </tr> <tr> <td><span class='fas fa-shapes'></span></td> <td>{% trans "Part" %}</td> @@ -88,6 +83,11 @@ src="{% static 'img/blank_image.png' %}" <td>{% trans "Status" %}</td> <td>{% build_status_label build.status %}</td> </tr> + <tr> + <td><span class='fas fa-spinner'></span></td> + <td>{% trans "Progress" %}</td> + <td> {{ build.completed }} / {{ build.quantity }}</td> + </tr> {% if build.parent %} <tr> <td><span class='fas fa-sitemap'></span></td> @@ -102,20 +102,6 @@ src="{% static 'img/blank_image.png' %}" <td><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a></td> </tr> {% endif %} - <tr> - <td><span class='fas fa-dollar-sign'></span></td> - <td>{% trans "BOM Price" %}</td> - <td> - {% if bom_price %} - {{ bom_price }} - {% if build.part.has_complete_bom_pricing == False %} - <br><span class='warning-msg'><i>{% trans "BOM pricing is incomplete" %}</i></span> - {% endif %} - {% else %} - <span class='warning-msg'><i>{% trans "No pricing information" %}</i></span> - {% endif %} - </td> - </tr> </table> {% endblock %} diff --git a/InvenTree/build/templates/build/build_output_create.html b/InvenTree/build/templates/build/build_output_create.html new file mode 100644 index 0000000000..5de41695a6 --- /dev/null +++ b/InvenTree/build/templates/build/build_output_create.html @@ -0,0 +1,20 @@ +{% extends "modal_form.html" %} +{% load i18n %} +{% block pre_form_content %} + +{% if build.part.has_trackable_parts %} +<div class='alert alert-block alert-warning'> + {% trans "The Bill of Materials contains trackable parts" %}<br> + {% trans "Build outputs must be generated individually." %}<br> + {% trans "Multiple build outputs will be created based on the quantity specified." %} +</div> +{% endif %} + +{% if build.part.trackable %} +<div class='alert alert-block alert-info'> + {% trans "Trackable parts can have serial numbers specified" %}<br> + {% trans "Enter serial numbers to generate multiple single build outputs" %} +</div> +{% endif %} + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/cancel.html b/InvenTree/build/templates/build/cancel.html index d7e4d51b10..48d8ca09bd 100644 --- a/InvenTree/build/templates/build/cancel.html +++ b/InvenTree/build/templates/build/cancel.html @@ -1,7 +1,7 @@ {% extends "modal_form.html" %} - +{% load i18n %} {% block pre_form_content %} -Are you sure you wish to cancel this build? +{% trans "Are you sure you wish to cancel this build?" %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/complete.html b/InvenTree/build/templates/build/complete.html index a48b831645..0db6a336a5 100644 --- a/InvenTree/build/templates/build/complete.html +++ b/InvenTree/build/templates/build/complete.html @@ -3,35 +3,21 @@ {% block pre_form_content %} -<h4>{% trans "Build" %} - {{ build }}</h4> - -{% if build.isFullyAllocated %} -<div class='alert alert-block alert-info'> - <h4>{% trans "Build order allocation is complete" %}</h4> +{% if build.can_complete %} +<div class='alert alert-block alert-success'> + {% trans "Build can be completed" %} </div> {% else %} <div class='alert alert-block alert-danger'> - <h4>{% trans "Warning: Build order allocation is not complete" %}</h4> - {% trans "Build Order has not been fully allocated. Ensure that all Stock Items have been allocated to the Build" %} -</div> -{% endif %} - -<div class='alert alert-block alert-success'> - <h4>{% trans "The following actions will be performed:" %}</h4> + <b>{% trans "Build cannot be completed" %}</b><br> <ul> - <li>{% trans "Remove allocated items from stock" %}</li> - <li>{% trans "Add completed items to stock" %}</li> + {% if build.incomplete_count > 0 %} + <li>{% trans "Incompleted build outputs remain" %}</li> + {% endif %} + {% if build.completed < build.quantity %} + <li>{% trans "Required build quantity has not been completed" %}</li> + {% endif %} </ul> </div> - -<div class='panel panel-default'> - <div class='panel-heading'> - {% trans "The following items will be created" %} - </div> - <div class='panel-content'> - {% include "hover_image.html" with image=build.part.image hover=True %} - {{ build.quantity }} x {{ build.part.full_name }} - </div> -</div> - +{% endif %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/complete_output.html b/InvenTree/build/templates/build/complete_output.html new file mode 100644 index 0000000000..54c3fc6763 --- /dev/null +++ b/InvenTree/build/templates/build/complete_output.html @@ -0,0 +1,48 @@ +{% extends "modal_form.html" %} +{% load inventree_extras %} +{% load i18n %} + +{% block pre_form_content %} + +{% if fully_allocated %} +<div class='alert alert-block alert-info'> + <h4>{% trans "Stock allocation is complete" %}</h4> +</div> +{% else %} +<div class='alert alert-block alert-danger'> + <h4>{% trans "Stock allocation is incomplete" %}</h4> + + <div class='panel-group'> + <div class='panel panel-default'> + <div class='panel panel-heading'> + <a data-toggle='collapse' href='#collapse-unallocated'> + {{ unallocated_parts|length }} {% trans "parts have not been fully allocated" %} + </a> + </div> + <div class='panel-collapse collapse' id='collapse-unallocated'> + <div class='panel-body'> + <ul class='list-group'> + {% for part in unallocated_parts %} + <li class='list-group-item'> + {% include "hover_image.html" with image=part.image %} {{ part }} + </li> + {% endfor %} + </ul> + </div> + </div> + </div> + </div> +</div> +{% endif %} + +<div class='panel panel-info'> + <div class='panel-heading'> + {% trans "The following items will be created" %} + </div> + <div class='panel-content'> + {% include "hover_image.html" with image=build.part.image hover=True %} + {% decimal output.quantity %} x {{ output.part.full_name }} + </div> +</div> + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/create_build_item.html b/InvenTree/build/templates/build/create_build_item.html index c08b987d36..8f58e884d6 100644 --- a/InvenTree/build/templates/build/create_build_item.html +++ b/InvenTree/build/templates/build/create_build_item.html @@ -1,9 +1,22 @@ {% extends "modal_form.html" %} +{% load i18n %} {% block pre_form_content %} +<div class='alert alert-block alert-info'> + <p> + {% trans "Select a stock item to allocate to the selected build output" %} + </p> + {% if output %} + <p> + {% trans "The allocated stock will be installed into the following build output:" %} + <br> + <i>{{ output }}</i> + </p> + {% endif %} +</div> {% if no_stock %} <div class='alert alert-danger alert-block' role='alert'> - No stock available for {{ part }} + {% trans "No stock available for" %} {{ part }} </div> {% endif %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/delete_build_item.html b/InvenTree/build/templates/build/delete_build_item.html index f9c41b9eb5..d5cc285466 100644 --- a/InvenTree/build/templates/build/delete_build_item.html +++ b/InvenTree/build/templates/build/delete_build_item.html @@ -3,7 +3,12 @@ {% load inventree_extras %} {% block pre_form_content %} -{% trans "Are you sure you want to unallocate these parts?" %} -<br> -This will remove {% decimal item.quantity %} parts from build '{{ item.build.title }}'. +<div class='alert alert-block alert-danger'> + <p> + {% trans "Are you sure you want to unallocate this stock?" %} + </p> + <p> + {% trans "The selected stock will be unallocated from the build output" %} + </p> +</div> {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 6abbc69bc5..68a842755d 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -10,77 +10,120 @@ <hr> -<table class='table table-striped'> - <col width='25'> -<tr> - <td></td> - <td>{% trans "Title" %}</td> - <td>{{ build.title }}</td> -</tr> -<tr> - <td><span class='fas fa-shapes'></span></td> - <td>{% trans "Part" %}</td> - <td><a href="{% url 'part-build' build.part.id %}">{{ build.part.full_name }}</a></td> -</tr> -<tr> - <td></td> - <td>{% trans "Quantity" %}</td><td>{{ build.quantity }}</td> -</tr> -<tr> - <td><span class='fas fa-map-marker-alt'></span></td> - <td>{% trans "Stock Source" %}</td> - <td> - {% if build.take_from %} - <a href="{% url 'stock-location-detail' build.take_from.id %}">{{ build.take_from }}</a> - {% else %} - {% trans "Stock can be taken from any available location." %} +<div class='row'> + <div class='col-sm-6'> + <table class='table table-striped'> + <col width='25'> + <tr> + <td><span class='fas fa-info'></span></td> + <td>{% trans "Description" %}</td> + <td>{{ build.title }}</td> + </tr> + <tr> + <td><span class='fas fa-shapes'></span></td> + <td>{% trans "Part" %}</td> + <td><a href="{% url 'part-build' build.part.id %}">{{ build.part.full_name }}</a></td> + </tr> + <tr> + <td></td> + <td>{% trans "Quantity" %}</td><td>{{ build.quantity }}</td> + </tr> + <tr> + <td><span class='fas fa-map-marker-alt'></span></td> + <td>{% trans "Stock Source" %}</td> + <td> + {% if build.take_from %} + <a href="{% url 'stock-location-detail' build.take_from.id %}">{{ build.take_from }}</a> + {% else %} + <i>{% trans "Stock can be taken from any available location." %}</i> + {% endif %} + </td> + </tr> + <tr> + <td><span class='fas fa-map-marker-alt'></span></td> + <td>{% trans "Destination" %}</td> + <td> + {% if build.destination %} + <a href="{% url 'stock-location-detail' build.destination.id %}"> + {{ build.destination }} + </a> + {% else %} + <i>{% trans "Destination location not specified" %}</i> + {% endif %} + </td> + </tr> + <tr> + <td><span class='fas fa-info'></span></td> + <td>{% trans "Status" %}</td> + <td>{% build_status_label build.status %}</td> + </tr> + <tr> + <td><span class='fas fa-spinner'></span></td> + <td>{% trans "Progress" %}</td> + <td>{{ build.completed }} / {{ build.quantity }}</td> + </tr> + {% if build.batch %} + <tr> + <td><span class='fas fa-layer-group'></span></td> + <td>{% trans "Batch" %}</td> + <td>{{ build.batch }}</td> + </tr> {% endif %} - </td> -</tr> -<tr> - <td><span class='fas fa-info'></span></td> - <td>{% trans "Status" %}</td> - <td>{% build_status_label build.status %}</td> -</tr> -{% if build.batch %} -<tr> - <td></td> - <td>{% trans "Batch" %}</td> - <td>{{ build.batch }}</td> -</tr> -{% endif %} -{% if build.link %} -<tr> - <td><span class='fas fa-link'></span></td> - <td>{% trans "External Link" %}</td> - <td><a href="{{ build.link }}">{{ build.link }}</a></td> -</tr> -{% endif %} -<tr> - <td><span class='fas fa-calendar-alt'></span></td> - <td>{% trans "Created" %}</td> - <td>{{ build.creation_date }}</td> -</tr> -{% if build.is_active %} -<tr> - <td></td> - <td>{% trans "Enough Parts?" %}</td> - <td> - {% if build.can_build %} - {% trans "Yes" %} - {% else %} - {% trans "No" %} + {% if build.parent %} + <tr> + <td><span class='fas fa-sitemap'></span></td> + <td>{% trans "Parent Build" %}</td> + <td><a href="{% url 'build-detail' build.parent.id %}">{{ build.parent }}</a></td> + </tr> {% endif %} - </td> -</tr> -{% endif %} -{% if build.completion_date %} -<tr> - <td><span class='fas fa-calendar-alt'></span></td> - <td>{% trans "Completed" %}</td> - <td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td> -</tr> -{% endif %} -</table> + {% if build.sales_order %} + <tr> + <td><span class='fas fa-dolly'></span></td> + <td>{% trans "Sales Order" %}</td> + <td><a href="{% url 'so-detail' build.sales_order.id %}">{{ build.sales_order }}</a></td> + </tr> + {% endif %} + {% if build.link %} + <tr> + <td><span class='fas fa-link'></span></td> + <td>{% trans "External Link" %}</td> + <td><a href="{{ build.link }}">{{ build.link }}</a></td> + </tr> + {% endif %} + <tr> + <td><span class='fas fa-calendar-alt'></span></td> + <td>{% trans "Created" %}</td> + <td>{{ build.creation_date }}</td> + </tr> + </table> + </div> + <div class='col-sm-6'> + <table class='table table-striped'> + <col width='25'> + <tr> + <td><span class='fas fa-dollar-sign'></span></td> + <td>{% trans "BOM Price" %}</td> + <td> + {% if bom_price %} + {{ bom_price }} + {% if build.part.has_complete_bom_pricing == False %} + <br><span class='warning-msg'><i>{% trans "BOM pricing is incomplete" %}</i></span> + {% endif %} + {% else %} + <span class='warning-msg'><i>{% trans "No pricing information" %}</i></span> + {% endif %} + </td> + </tr> + {% if build.completion_date %} + <tr> + <td><span class='fas fa-calendar-alt'></span></td> + <td>{% trans "Completed" %}</td> + <td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td> + </tr> + {% endif %} + + </table> + </div> +</div> {% endblock %} diff --git a/InvenTree/build/templates/build/edit_build_item.html b/InvenTree/build/templates/build/edit_build_item.html new file mode 100644 index 0000000000..99cad71ba2 --- /dev/null +++ b/InvenTree/build/templates/build/edit_build_item.html @@ -0,0 +1,10 @@ +{% extends "modal_form.html" %} +{% load i18n %} + +{% block pre_form_content %} +<div class='alert alert-block alert-info'> + <p> + {% trans "Alter the quantity of stock allocated to the build output" %} + </p> +</div> +{% endblock %} \ No newline at end of file diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html index d72807f9f1..562dc746fb 100644 --- a/InvenTree/build/templates/build/index.html +++ b/InvenTree/build/templates/build/index.html @@ -21,7 +21,8 @@ InvenTree | {% trans "Build Orders" %} <div id='button-toolbar'> <div class='button-toolbar container-fluid' style='float: right;'> - <button type='button' class="btn btn-success" id='new-build'>{% trans "New Build Order" %}</button> + <button type='button' class="btn btn-success" id='new-build'> + <span class='fas fa-tools'></span> {% trans "New Build Order" %}</button> <div class='filter-list' id='filter-list-build'> <!-- An empty div in which the filter list will be constructed --> </div> @@ -40,12 +41,7 @@ InvenTree | {% trans "Build Orders" %} $("#collapse-item-active").collapse().show(); $("#new-build").click(function() { - launchModalForm( - "{% url 'build-create' %}", - { - follow: true - } - ); + newBuildOrder(); }); loadBuildTable($("#build-table"), { diff --git a/InvenTree/build/templates/build/tabs.html b/InvenTree/build/templates/build/tabs.html index ca2e92f290..2c45cd2361 100644 --- a/InvenTree/build/templates/build/tabs.html +++ b/InvenTree/build/templates/build/tabs.html @@ -5,7 +5,7 @@ <a href="{% url 'build-detail' build.id %}">{% trans "Details" %}</a> </li> <li{% if tab == 'allocate' %} class='active'{% endif %}> - <a href="{% url 'build-allocate' build.id %}">{% trans "Allocated Parts" %}</a> + <a href="{% url 'build-allocate' build.id %}">{% trans "Allocate Parts" %}</a> </li> <li{% if tab == 'output' %} class='active'{% endif %}> <a href="{% url 'build-output' build.id %}">{% trans "Build Outputs" %}{% if build.output_count > 0%}<span class='badge'>{{ build.output_count }}</span>{% endif %}</a> @@ -13,4 +13,7 @@ <li{% if tab == 'notes' %} class='active'{% endif %}> <a href="{% url 'build-notes' build.id %}">{% trans "Notes" %}{% if build.notes %} <span class='fas fa-info-circle'></span>{% endif %}</a> </li> + <li {% if tab == 'attachments' %} class='active'{% endif %}> + <a href='{% url "build-attachments" build.id %}'>{% trans "Attachments" %}</a> + </li> </ul> \ No newline at end of file diff --git a/InvenTree/build/templates/build/unallocate.html b/InvenTree/build/templates/build/unallocate.html index f6543b3732..a650e95718 100644 --- a/InvenTree/build/templates/build/unallocate.html +++ b/InvenTree/build/templates/build/unallocate.html @@ -5,6 +5,11 @@ {{ block.super }} -{% trans "Are you sure you wish to unallocate all stock for this build?" %} + +<div class='alert alert-block alert-danger'> + {% trans "Are you sure you wish to unallocate all stock for this build?" %} + <br> + {% trans "All incomplete stock allocations will be removed from the build" %} +</div> {% endblock %} \ No newline at end of file diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index e69853c269..b560a4f9c9 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -3,7 +3,6 @@ from django.test import TestCase from django.core.exceptions import ValidationError -from django.db import transaction from django.db.utils import IntegrityError from build.models import Build, BuildItem @@ -11,8 +10,6 @@ from stock.models import StockItem from part.models import Part, BomItem from InvenTree import status_codes as status -from InvenTree.helpers import ExtractSerialNumbers - class BuildTest(TestCase): """ @@ -64,6 +61,21 @@ class BuildTest(TestCase): quantity=10 ) + # Create some build output (StockItem) objects + self.output_1 = StockItem.objects.create( + part=self.assembly, + quantity=5, + is_building=True, + build=self.build + ) + + self.output_2 = StockItem.objects.create( + part=self.assembly, + quantity=5, + is_building=True, + build=self.build, + ) + # Create some stock items to assign to the build self.stock_1_1 = StockItem.objects.create(part=self.sub_part_1, quantity=1000) self.stock_1_2 = StockItem.objects.create(part=self.sub_part_1, quantity=100) @@ -73,23 +85,28 @@ class BuildTest(TestCase): def test_init(self): # Perform some basic tests before we start the ball rolling - self.assertEqual(StockItem.objects.count(), 3) + self.assertEqual(StockItem.objects.count(), 5) + + # Build is PENDING self.assertEqual(self.build.status, status.BuildStatus.PENDING) - self.assertFalse(self.build.isFullyAllocated()) - self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1)) - self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2)) + # Build has two build outputs + self.assertEqual(self.build.output_count, 2) - self.assertEqual(self.build.getRequiredQuantity(self.sub_part_1), 100) - self.assertEqual(self.build.getRequiredQuantity(self.sub_part_2), 250) + # None of the build outputs have been completed + for output in self.build.get_build_outputs().all(): + self.assertFalse(self.build.isFullyAllocated(output)) + + self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1)) + self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2)) + + self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_1), 50) + self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_2), 50) + self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_1), 125) + self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_2), 125) - self.assertTrue(self.build.can_build) self.assertFalse(self.build.is_complete) - # Delete some stock and see if the build can still be completed - self.stock_2_1.delete() - self.assertFalse(self.build.can_build) - def test_build_item_clean(self): # Ensure that dodgy BuildItem objects cannot be created @@ -99,7 +116,7 @@ class BuildTest(TestCase): b = BuildItem(stock_item=stock, build=self.build, quantity=10) with self.assertRaises(ValidationError): - b.clean() + b.save() # Create a BuildItem which has too much stock assigned b = BuildItem(stock_item=self.stock_1_1, build=self.build, quantity=9999999) @@ -113,6 +130,10 @@ class BuildTest(TestCase): with self.assertRaises(ValidationError): b.clean() + # Ok, what about we make one that does *not* fail? + b = BuildItem(stock_item=self.stock_1_1, build=self.build, install_into=self.output_1, quantity=10) + b.save() + def test_duplicate_bom_line(self): # Try to add a duplicate BOM item - it should fail! @@ -123,105 +144,145 @@ class BuildTest(TestCase): quantity=99 ) - def allocate_stock(self, q11, q12, q21): + def allocate_stock(self, q11, q12, q21, output): # Assign stock to this build - BuildItem.objects.create( - build=self.build, - stock_item=self.stock_1_1, - quantity=q11 - ) + if q11 > 0: + BuildItem.objects.create( + build=self.build, + stock_item=self.stock_1_1, + quantity=q11, + install_into=output + ) - BuildItem.objects.create( - build=self.build, - stock_item=self.stock_1_2, - quantity=q12 - ) + if q12 > 0: + BuildItem.objects.create( + build=self.build, + stock_item=self.stock_1_2, + quantity=q12, + install_into=output + ) - BuildItem.objects.create( - build=self.build, - stock_item=self.stock_2_1, - quantity=q21 - ) + if q21 > 0: + BuildItem.objects.create( + build=self.build, + stock_item=self.stock_2_1, + quantity=q21, + install_into=output, + ) - with transaction.atomic(): - with self.assertRaises(IntegrityError): - BuildItem.objects.create( - build=self.build, - stock_item=self.stock_2_1, - quantity=99 - ) + # Attempt to create another identical BuildItem + b = BuildItem( + build=self.build, + stock_item=self.stock_2_1, + quantity=q21 + ) - self.assertEqual(BuildItem.objects.count(), 3) + with self.assertRaises(ValidationError): + b.clean() def test_partial_allocation(self): + """ + Partially allocate against output 1 + """ - self.allocate_stock(50, 50, 200) + self.allocate_stock(50, 50, 200, self.output_1) - self.assertFalse(self.build.isFullyAllocated()) - self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_1)) - self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2)) + self.assertTrue(self.build.isFullyAllocated(self.output_1)) + self.assertFalse(self.build.isFullyAllocated(self.output_2)) + self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1)) + self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, self.output_1)) + + self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_2)) + self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2)) - self.build.unallocateStock() + # Check that the part has been allocated + self.assertEqual(self.build.allocatedQuantity(self.sub_part_1, self.output_1), 100) + + self.build.unallocateStock(output=self.output_1) self.assertEqual(BuildItem.objects.count(), 0) - def test_auto_allocate(self): + # Check that the part has been unallocated + self.assertEqual(self.build.allocatedQuantity(self.sub_part_1, self.output_1), 0) - allocations = self.build.getAutoAllocations() + def test_auto_allocate(self): + """ + Test auto-allocation functionality against the build outputs + """ + + allocations = self.build.getAutoAllocations(self.output_1) self.assertEqual(len(allocations), 1) - self.build.autoAllocate() + self.build.autoAllocate(self.output_1) self.assertEqual(BuildItem.objects.count(), 1) - self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2)) + + # Check that one part has been fully allocated to the build output + self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, self.output_1)) + + # But, the *other* build output has not been allocated against + self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2)) def test_cancel(self): + """ + Test cancellation of the build + """ - self.allocate_stock(50, 50, 200) + # TODO + + """ + self.allocate_stock(50, 50, 200, self.output_1) self.build.cancelBuild(None) self.assertEqual(BuildItem.objects.count(), 0) + """ + pass def test_complete(self): + """ + Test completion of a build output + """ - self.allocate_stock(50, 50, 250) + self.allocate_stock(50, 50, 250, self.output_1) + self.allocate_stock(50, 50, 250, self.output_2) - self.assertTrue(self.build.isFullyAllocated()) + self.assertTrue(self.build.isFullyAllocated(self.output_1)) + self.assertTrue(self.build.isFullyAllocated(self.output_2)) - # Generate some serial numbers! - serials = ExtractSerialNumbers("1-10", 10) + self.build.completeBuildOutput(self.output_1, None) - self.build.completeBuild(None, serials, None) + self.assertFalse(self.build.can_complete) + self.build.completeBuildOutput(self.output_2, None) + + self.assertTrue(self.build.can_complete) + + self.build.complete_build(None) + self.assertEqual(self.build.status, status.BuildStatus.COMPLETE) # the original BuildItem objects should have been deleted! self.assertEqual(BuildItem.objects.count(), 0) # New stock items should have been created! - # - Ten for the build output (as the part was serialized) - # - Three for the split items assigned to the build - self.assertEqual(StockItem.objects.count(), 16) + self.assertEqual(StockItem.objects.count(), 4) A = StockItem.objects.get(pk=self.stock_1_1.pk) - B = StockItem.objects.get(pk=self.stock_1_2.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) # Stock should have been subtracted from the original items - self.assertEqual(A.quantity, 950) - self.assertEqual(B.quantity, 50) - self.assertEqual(C.quantity, 4750) - - # New stock items should have also been allocated to the build - allocated = StockItem.objects.filter(build_order=self.build) - - self.assertEqual(allocated.count(), 3) - - q = sum([item.quantity for item in allocated.all()]) - - self.assertEqual(q, 350) + 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) - self.assertEqual(outputs.count(), 10) + self.assertEqual(outputs.count(), 2) + + for output in outputs: + self.assertFalse(output.is_building) diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index ded98a441c..f01aaa83c9 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -12,6 +12,7 @@ from rest_framework import status import json from .models import Build +from stock.models import StockItem from InvenTree.status_codes import BuildStatus @@ -49,7 +50,7 @@ class BuildTestSimple(TestCase): def test_build_objects(self): # Ensure the Build objects were correctly created - self.assertEqual(Build.objects.count(), 2) + self.assertEqual(Build.objects.count(), 5) b = Build.objects.get(pk=2) self.assertEqual(b.batch, 'B2') self.assertEqual(b.quantity, 21) @@ -127,11 +128,37 @@ class TestBuildAPI(APITestCase): self.client.login(username='testuser', password='password') def test_get_build_list(self): - """ Test that we can retrieve list of build objects """ + """ + Test that we can retrieve list of build objects + """ + url = reverse('api-build-list') response = self.client.get(url, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 5) + + # Filter query by build status + response = self.client.get(url, {'status': 40}, format='json') + + self.assertEqual(len(response.data), 4) + + # Filter by "active" status + response = self.client.get(url, {'active': True}, format='json') + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]['pk'], 1) + + response = self.client.get(url, {'active': False}, format='json') + self.assertEqual(len(response.data), 4) + + # Filter by 'part' status + response = self.client.get(url, {'part': 25}, format='json') + self.assertEqual(len(response.data), 2) + + # Filter by an invalid part + response = self.client.get(url, {'part': 99999}, format='json') + self.assertEqual(len(response.data), 0) + def test_get_build_item_list(self): """ Test that we can retrieve list of BuildItem objects """ url = reverse('api-build-item-list') @@ -176,6 +203,16 @@ class TestBuildViews(TestCase): self.client.login(username='username', password='password') + # Create a build output for build # 1 + self.build = Build.objects.get(pk=1) + + self.output = StockItem.objects.create( + part=self.build.part, + quantity=self.build.quantity, + build=self.build, + is_building=True, + ) + def test_build_index(self): """ test build index view """ @@ -254,10 +291,15 @@ class TestBuildViews(TestCase): # url = reverse('build-item-edit') pass - def test_build_complete(self): - """ Test the build completion form """ + def test_build_output_complete(self): + """ + Test the build output completion form + """ - url = reverse('build-complete', args=(1,)) + # Firstly, check that the build cannot be completed! + self.assertFalse(self.build.can_complete) + + url = reverse('build-output-complete', args=(1,)) # Test without confirmation response = self.client.post(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') @@ -267,12 +309,26 @@ class TestBuildViews(TestCase): self.assertFalse(data['form_valid']) # Test with confirmation, valid location - response = self.client.post(url, {'confirm': 1, 'location': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response = self.client.post( + url, + { + 'confirm': 1, + 'confirm_incomplete': 1, + 'location': 1, + 'output': self.output.pk, + }, + HTTP_X_REQUESTED_WITH='XMLHttpRequest' + ) + self.assertEqual(response.status_code, 200) - + data = json.loads(response.content) self.assertTrue(data['form_valid']) + # Now the build should be able to be completed + self.build.refresh_from_db() + self.assertTrue(self.build.can_complete) + # Test with confirmation, invalid location response = self.client.post(url, {'confirm': 1, 'location': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(response.status_code, 200) diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index d8cb3c03ea..08142e6939 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -11,12 +11,16 @@ build_detail_urls = [ url(r'^allocate/', views.BuildAllocate.as_view(), name='build-allocate'), url(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'), url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'), - url(r'^complete/?', views.BuildComplete.as_view(), name='build-complete'), + url(r'^create-output/', views.BuildOutputCreate.as_view(), name='build-output-create'), + url(r'^delete-output/', views.BuildOutputDelete.as_view(), name='build-output-delete'), + url(r'^complete-output/?', views.BuildOutputComplete.as_view(), name='build-output-complete'), url(r'^auto-allocate/?', views.BuildAutoAllocate.as_view(), name='build-auto-allocate'), url(r'^unallocate/', views.BuildUnallocate.as_view(), name='build-unallocate'), + url(r'^complete/', views.BuildComplete.as_view(), name='build-complete'), url(r'^notes/', views.BuildNotes.as_view(), name='build-notes'), + url(r'^attachments/', views.BuildDetail.as_view(template_name='build/attachments.html'), name='build-attachments'), url(r'^output/', views.BuildDetail.as_view(template_name='build/build_output.html'), name='build-output'), url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), @@ -31,6 +35,12 @@ build_urls = [ url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'), ])), + url('^attachment/', include([ + url('^new/', views.BuildAttachmentCreate.as_view(), name='build-attachment-create'), + url(r'^(?P<pk>\d+)/edit/', views.BuildAttachmentEdit.as_view(), name='build-attachment-edit'), + url(r'^(?P<pk>\d+)/delete/', views.BuildAttachmentDelete.as_view(), name='build-attachment-delete'), + ])), + url(r'new/', views.BuildCreate.as_view(), name='build-create'), url(r'^(?P<pk>\d+)/', include(build_detail_urls)), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 1296e42fae..5dd36a6871 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -12,13 +12,13 @@ from django.forms import HiddenInput from django.urls import reverse from part.models import Part -from .models import Build, BuildItem +from .models import Build, BuildItem, BuildOrderAttachment from . import forms from stock.models import StockLocation, StockItem from InvenTree.views import AjaxUpdateView, AjaxCreateView, AjaxDeleteView from InvenTree.views import InvenTreeRoleMixin -from InvenTree.helpers import str2bool, ExtractSerialNumbers +from InvenTree.helpers import str2bool, extract_serial_numbers, normalize from InvenTree.status_codes import BuildStatus @@ -60,30 +60,25 @@ class BuildCancel(AjaxUpdateView): form_class = forms.CancelBuildForm role_required = 'build.change' - def post(self, request, *args, **kwargs): - """ Handle POST request. Mark the build status as CANCELLED """ + def validate(self, build, form, **kwargs): - build = self.get_object() + confirm = str2bool(form.cleaned_data.get('confirm_cancel', False)) - form = self.get_form() + if not confirm: + form.add_error('confirm_cancel', _('Confirm build cancellation')) - valid = form.is_valid() + def save(self, build, form, **kwargs): + """ + Cancel the build. + """ - confirm = str2bool(request.POST.get('confirm_cancel', False)) + build.cancelBuild(self.request.user) - if confirm: - build.cancelBuild(request.user) - else: - form.errors['confirm_cancel'] = [_('Confirm build cancellation')] - valid = False - - data = { - 'form_valid': valid, + def get_data(self): + return { 'danger': _('Build was cancelled') } - return self.renderJsonResponse(request, form, data=data) - class BuildAutoAllocate(AjaxUpdateView): """ View to auto-allocate parts for a build. @@ -93,52 +88,246 @@ class BuildAutoAllocate(AjaxUpdateView): """ model = Build - form_class = forms.ConfirmBuildForm + form_class = forms.AutoAllocateForm context_object_name = 'build' ajax_form_title = _('Allocate Stock') ajax_template_name = 'build/auto_allocate.html' role_required = 'build.change' + def get_initial(self): + """ + Initial values for the form. + """ + + initials = super().get_initial() + + # Pointing to a particular build output? + output = self.get_param('output') + + if output: + try: + output = StockItem.objects.get(pk=output) + initials['output'] = output + except (ValueError, StockItem.DoesNotExist): + pass + + return initials + def get_context_data(self, *args, **kwargs): - """ Get the context data for form rendering. """ + """ + Get the context data for form rendering. + """ context = {} + build = self.get_object() + + form = self.get_form() + + output_id = form['output'].value() + try: - build = Build.objects.get(id=self.kwargs['pk']) - context['build'] = build - context['allocations'] = build.getAutoAllocations() - except Build.DoesNotExist: - context['error'] = _('No matching build found') + output = StockItem.objects.get(pk=output_id) + except (ValueError, StockItem.DoesNotExist): + output = None + + if output: + context['output'] = output + context['allocations'] = build.getAutoAllocations(output) + + context['build'] = build return context - def post(self, request, *args, **kwargs): - """ Handle POST request. Perform auto allocations. + def get_form(self): - - If the form validation passes, perform allocations - - Otherwise, the form is passed back to the client + form = super().get_form() + + if form['output'].value(): + # Hide the 'output' field + form.fields['output'].widget = HiddenInput() + + return form + + def validate(self, build, form, **kwargs): + + output = form.cleaned_data.get('output', None) + + if not output: + form.add_error(None, _('Build output must be specified')) + + def save(self, build, form, **kwargs): + """ + Once the form has been validated, + perform auto-allocations """ - build = self.get_object() - form = self.get_form() + output = form.cleaned_data.get('output', None) - confirm = request.POST.get('confirm', False) + build.autoAllocate(output) - valid = False - - if confirm is False: - form.errors['confirm'] = [_('Confirm stock allocation')] - form.non_field_errors = [_('Check the confirmation box at the bottom of the list')] - else: - build.autoAllocate() - valid = True - - data = { - 'form_valid': valid, + def get_data(self): + return { + 'success': _('Allocated stock to build output'), } - return self.renderJsonResponse(request, form, data, context=self.get_context_data()) + +class BuildOutputCreate(AjaxUpdateView): + """ + Create a new build output (StockItem) for a given build. + """ + + model = Build + form_class = forms.BuildOutputCreateForm + ajax_template_name = 'build/build_output_create.html' + ajax_form_title = _('Create Build Output') + role_required = 'build.change' + + def validate(self, build, form, **kwargs): + """ + Validation for the form: + """ + + quantity = form.cleaned_data.get('quantity', None) + serials = form.cleaned_data.get('serial_numbers', None) + + # Check that the serial numbers are valid + if serials: + try: + extracted = extract_serial_numbers(serials, quantity) + + if extracted: + # Check for conflicting serial numbers + conflicts = build.part.find_conflicting_serial_numbers(extracted) + + if len(conflicts) > 0: + msg = ",".join([str(c) for c in conflicts]) + form.add_error( + 'serial_numbers', + _('Serial numbers already exist') + ': ' + msg, + ) + + except ValidationError as e: + form.add_error('serial_numbers', e.messages) + + else: + # If no serial numbers are provided, should they be? + if build.part.trackable: + form.add_error('serial_numbers', _('Serial numbers required for trackable build output')) + + def save(self, build, form, **kwargs): + """ + Create a new build output + """ + + data = form.cleaned_data + + quantity = data.get('quantity', None) + batch = data.get('batch', None) + + serials = data.get('serial_numbers', None) + + if serials: + serial_numbers = extract_serial_numbers(serials, quantity) + else: + serial_numbers = None + + build.create_build_output( + quantity, + serials=serial_numbers, + batch=batch, + ) + + def get_initial(self): + + initials = super().get_initial() + + build = self.get_object() + + # Calculate the required quantity + quantity = max(0, build.remaining - build.incomplete_count) + initials['quantity'] = quantity + + return initials + + def get_form(self): + + build = self.get_object() + part = build.part + + context = self.get_form_kwargs() + + # Pass the 'part' through to the form, + # so we can add the next serial number as a placeholder + context['build'] = build + + form = self.form_class(**context) + + # If the part is not trackable, hide the serial number input + if not part.trackable: + form.fields['serial_numbers'].widget = HiddenInput() + + return form + + +class BuildOutputDelete(AjaxUpdateView): + """ + Delete a build output (StockItem) for a given build. + + Form is a simple confirmation dialog + """ + + model = Build + form_class = forms.BuildOutputDeleteForm + ajax_form_title = _('Delete Build Output') + role_required = 'build.delete' + + def get_initial(self): + + initials = super().get_initial() + + output = self.get_param('output') + + initials['output_id'] = output + + return initials + + def validate(self, build, form, **kwargs): + + data = form.cleaned_data + + confirm = data.get('confirm', False) + + if not confirm: + form.add_error('confirm', _('Confirm unallocation of build stock')) + form.add_error(None, _('Check the confirmation box')) + + output_id = data.get('output_id', None) + output = None + + try: + output = StockItem.objects.get(pk=output_id) + except (ValueError, StockItem.DoesNotExist): + pass + + if output: + if not output.build == build: + form.add_error(None, _('Build output does not match build')) + else: + form.add_error(None, _('Build output must be specified')) + + def save(self, build, form, **kwargs): + + output_id = form.cleaned_data.get('output_id') + + output = StockItem.objects.get(pk=output_id) + + build.deleteBuildOutput(output) + + def get_data(self): + return { + 'danger': _('Build output deleted'), + } class BuildUnallocate(AjaxUpdateView): @@ -148,10 +337,28 @@ class BuildUnallocate(AjaxUpdateView): """ model = Build - form_class = forms.ConfirmBuildForm + form_class = forms.UnallocateBuildForm ajax_form_title = _("Unallocate Stock") ajax_template_name = "build/unallocate.html" - form_required = 'build.change' + role_required = 'build.change' + + def get_initial(self): + + initials = super().get_initial() + + # Pointing to a particular build output? + output = self.get_param('output') + + if output: + initials['output_id'] = output + + # Pointing to a particular part? + part = self.get_param('part') + + if part: + initials['part_id'] = part + + return initials def post(self, request, *args, **kwargs): @@ -160,13 +367,27 @@ class BuildUnallocate(AjaxUpdateView): confirm = request.POST.get('confirm', False) + output_id = request.POST.get('output_id', None) + + try: + output = StockItem.objects.get(pk=output_id) + except (ValueError, StockItem.DoesNotExist): + output = None + + part_id = request.POST.get('part_id', None) + + try: + part = Part.objects.get(pk=part_id) + except (ValueError, Part.DoesNotExist): + part = None + valid = False if confirm is False: - form.errors['confirm'] = [_('Confirm unallocation of build stock')] - form.non_field_errors = [_('Check the confirmation box')] + form.add_error('confirm', _('Confirm unallocation of build stock')) + form.add_error(None, _('Check the confirmation box')) else: - build.unallocateStock() + build.unallocateStock(output=output, part=part) valid = True data = { @@ -177,7 +398,41 @@ class BuildUnallocate(AjaxUpdateView): class BuildComplete(AjaxUpdateView): - """ View to mark a build as Complete. + """ + View to mark the build as complete. + + Requirements: + - There can be no outstanding build outputs + - The "completed" value must meet or exceed the "quantity" value + """ + + model = Build + form_class = forms.CompleteBuildForm + role_required = 'build.change' + ajax_form_title = _('Complete Build Order') + ajax_template_name = 'build/complete.html' + + def validate(self, build, form, **kwargs): + + if not build.can_complete: + form.add_error(None, _('Build order cannot be completed')) + + def save(self, build, form, **kwargs): + """ + Perform the build completion step + """ + + build.complete_build(self.request.user) + + def get_data(self): + return { + 'success': _('Completed build order') + } + + +class BuildOutputComplete(AjaxUpdateView): + """ + View to mark a particular build output as Complete. - Notifies the user of which parts will be removed from stock. - Removes allocated items from stock @@ -185,39 +440,62 @@ class BuildComplete(AjaxUpdateView): """ model = Build - form_class = forms.CompleteBuildForm + form_class = forms.CompleteBuildOutputForm context_object_name = "build" - ajax_form_title = _("Complete Build") - ajax_template_name = "build/complete.html" + ajax_form_title = _("Complete Build Output") + ajax_template_name = "build/complete_output.html" role_required = 'build.change' def get_form(self): - """ Get the form object. - - If the part is trackable, include a field for serial numbers. - """ + build = self.get_object() form = super().get_form() - if not build.part.trackable: - form.fields.pop('serial_numbers') - else: + # Extract the build output object + output = None + output_id = form['output'].value() - form.field_placeholder['serial_numbers'] = build.part.getSerialNumberString(build.quantity) + try: + output = StockItem.objects.get(pk=output_id) + except (ValueError, StockItem.DoesNotExist): + pass - form.rebuild_layout() + if output: + if build.isFullyAllocated(output): + form.fields['confirm_incomplete'].widget = HiddenInput() return form + def validate(self, build, form, **kwargs): + + data = form.cleaned_data + + output = data.get('output', None) + + if output: + + quantity = data.get('quantity', None) + + if quantity and quantity > output.quantity: + form.add_error('quantity', _('Quantity to complete cannot exceed build output quantity')) + + if not build.isFullyAllocated(output): + confirm = str2bool(data.get('confirm_incomplete', False)) + + if not confirm: + form.add_error('confirm_incomplete', _('Confirm completion of incomplete build')) + + else: + form.add_error(None, _('Build output must be specified')) + def get_initial(self): """ Get initial form data for the CompleteBuild form - If the part being built has a default location, pre-select that location """ - initials = super(BuildComplete, self).get_initial().copy() - + initials = super().get_initial() build = self.get_object() if build.part.default_location is not None: @@ -227,100 +505,77 @@ class BuildComplete(AjaxUpdateView): except StockLocation.DoesNotExist: pass + output = self.get_param('output', None) + + if output: + try: + output = StockItem.objects.get(pk=output) + except (ValueError, StockItem.DoesNotExist): + output = None + + # Output has not been supplied? Try to "guess" + if not output: + + incomplete = build.get_build_outputs(complete=False) + + if incomplete.count() == 1: + output = incomplete[0] + + if output is not None: + initials['output'] = output + + initials['location'] = build.destination + return initials def get_context_data(self, **kwargs): - """ Get context data for passing to the rendered form + """ + Get context data for passing to the rendered form - Build information is required """ - build = Build.objects.get(id=self.kwargs['pk']) + build = self.get_object() context = {} # Build object context['build'] = build - # Items to be removed from stock - taking = BuildItem.objects.filter(build=build.id) - context['taking'] = taking - - return context - - def post(self, request, *args, **kwargs): - """ Handle POST request. Mark the build as COMPLETE - - - If the form validation passes, the Build objects completeBuild() method is called - - Otherwise, the form is passed back to the client - """ - - build = self.get_object() - form = self.get_form() - confirm = str2bool(request.POST.get('confirm', False)) + output = form['output'].value() - loc_id = request.POST.get('location', None) - - valid = False - - if confirm is False: - form.errors['confirm'] = [ - _('Confirm completion of build'), - ] - else: + if output: try: - location = StockLocation.objects.get(id=loc_id) - valid = True - except (ValueError, StockLocation.DoesNotExist): - form.errors['location'] = [_('Invalid location selected')] + output = StockItem.objects.get(pk=output) + context['output'] = output + context['fully_allocated'] = build.isFullyAllocated(output) + context['allocated_parts'] = build.allocatedParts(output) + context['unallocated_parts'] = build.unallocatedParts(output) + except (ValueError, StockItem.DoesNotExist): + pass - serials = [] + return context - if build.part.trackable: - # A build for a trackable part may optionally specify serial numbers. + def save(self, build, form, **kwargs): - sn = request.POST.get('serial_numbers', '') + data = form.cleaned_data - sn = str(sn).strip() - - # If the user has specified serial numbers, check they are valid - if len(sn) > 0: - try: - # Exctract a list of provided serial numbers - serials = ExtractSerialNumbers(sn, build.quantity) - - existing = [] - - for serial in serials: - if build.part.checkIfSerialNumberExists(serial): - existing.append(serial) - - if len(existing) > 0: - exists = ",".join([str(x) for x in existing]) - form.errors['serial_numbers'] = [_('The following serial numbers already exist: ({sn})'.format(sn=exists))] - valid = False - - except ValidationError as e: - form.errors['serial_numbers'] = e.messages - valid = False - - if valid: - if not build.completeBuild(location, serials, request.user): - form.non_field_errors = [('Build could not be completed')] - valid = False - - data = { - 'form_valid': valid, - } - - return self.renderJsonResponse(request, form, data, context=self.get_context_data()) + location = data.get('location', None) + output = data.get('output', None) + # Complete the build output + build.completeBuildOutput( + output, + self.request.user, + location=location, + ) + def get_data(self): """ Provide feedback data back to the form """ return { - 'info': _('Build marked as COMPLETE') + 'success': _('Build output completed') } @@ -404,6 +659,14 @@ class BuildCreate(AjaxCreateView): ajax_template_name = 'modal_form.html' role_required = 'build.add' + def get_form(self): + form = super().get_form() + + if form['part'].value(): + form.fields['part'].widget = HiddenInput() + + return form + def get_initial(self): """ Get initial parameters for Build creation. @@ -412,8 +675,17 @@ class BuildCreate(AjaxCreateView): initials = super(BuildCreate, self).get_initial().copy() - # User has provided a Part ID - initials['part'] = self.request.GET.get('part', None) + part = self.request.GET.get('part', None) + + if part: + + try: + part = Part.objects.get(pk=part) + # User has provided a Part ID + initials['part'] = part + initials['destination'] = part.get_default_location() + except (ValueError, Part.DoesNotExist): + pass initials['reference'] = Build.getNextBuildNumber() @@ -431,6 +703,17 @@ class BuildCreate(AjaxCreateView): 'success': _('Created new build'), } + def validate(self, build, form, **kwargs): + """ + Perform extra form validation. + + - If part is trackable, check that either batch or serial numbers are calculated + + By this point form.is_valid() has been executed + """ + + pass + class BuildUpdate(AjaxUpdateView): """ View for editing a Build object """ @@ -442,6 +725,34 @@ class BuildUpdate(AjaxUpdateView): ajax_template_name = 'modal_form.html' role_required = 'build.change' + def get_form(self): + + form = super().get_form() + + build = self.get_object() + + # Fields which are included in the form, but hidden + hidden = [ + 'parent', + 'sales_order', + ] + + if build.is_complete: + # Fields which cannot be edited once the build has been completed + + hidden += [ + 'part', + 'quantity', + 'batch', + 'take_from', + 'destination', + ] + + for field in hidden: + form.fields[field].widget = HiddenInput() + + return form + def get_data(self): return { 'info': _('Edited build'), @@ -475,23 +786,37 @@ class BuildItemDelete(AjaxDeleteView): class BuildItemCreate(AjaxCreateView): - """ View for allocating a new part to a build """ + """ + View for allocating a StockItem to a build output. + """ model = BuildItem form_class = forms.EditBuildItemForm ajax_template_name = 'build/create_build_item.html' - ajax_form_title = _('Allocate new Part') + ajax_form_title = _('Allocate stock to build output') role_required = 'build.add' + # The output StockItem against which the allocation is being made + output = None + + # The "part" which is being allocated to the output part = None + available_stock = None def get_context_data(self): - ctx = super(AjaxCreateView, self).get_context_data() + """ + Provide context data to the template which renders the form. + """ + + ctx = super().get_context_data() if self.part: ctx['part'] = self.part + if self.output: + ctx['output'] = self.output + if self.available_stock: ctx['stock'] = self.available_stock else: @@ -499,73 +824,108 @@ class BuildItemCreate(AjaxCreateView): return ctx + def validate(self, build_item, form, **kwargs): + """ + Extra validation steps as required + """ + + data = form.cleaned_data + + stock_item = data.get('stock_item', None) + quantity = data.get('quantity', None) + + if stock_item: + # Stock item must actually be in stock! + if not stock_item.in_stock: + form.add_error('stock_item', _('Item must be currently in stock')) + + # Check that there are enough items available + if quantity is not None: + available = stock_item.unallocated_quantity() + if quantity > available: + form.add_error('stock_item', _('Stock item is over-allocated')) + form.add_error('quantity', _('Available') + ': ' + str(normalize(available))) + else: + form.add_error('stock_item', _('Stock item must be selected')) + def get_form(self): """ Create Form for making / editing new Part object """ form = super(AjaxCreateView, self).get_form() + self.build = None + self.part = None + self.output = None + # If the Build object is specified, hide the input field. # We do not want the users to be able to move a BuildItem to a different build build_id = form['build'].value() if build_id is not None: + """ + If the build has been provided, hide the widget to change the build selection. + Additionally, update the allowable selections for other fields. + """ form.fields['build'].widget = HiddenInput() + form.fields['install_into'].queryset = StockItem.objects.filter(build=build_id, is_building=True) + self.build = Build.objects.get(pk=build_id) + else: + """ + Build has *not* been selected + """ + pass # If the sub_part is supplied, limit to matching stock items - part_id = self.get_param('part') + part_id = form['part_id'].value() if part_id: try: self.part = Part.objects.get(pk=part_id) - - query = form.fields['stock_item'].queryset - - # Only allow StockItem objects which match the current part - query = query.filter(part=part_id) - - if build_id is not None: - try: - build = Build.objects.get(id=build_id) - - if build.take_from is not None: - # Limit query to stock items that are downstream of the 'take_from' location - query = query.filter(location__in=[loc for loc in build.take_from.getUniqueChildren()]) - - except Build.DoesNotExist: - pass - - # Exclude StockItem objects which are already allocated to this build and part - query = query.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)]) - - form.fields['stock_item'].queryset = query - - stocks = query.all() - self.available_stock = stocks - - # If there is only one item selected, select it - if len(stocks) == 1: - form.fields['stock_item'].initial = stocks[0].id - # There is no stock available - elif len(stocks) == 0: - # TODO - Add a message to the form describing the problem - pass - - except Part.DoesNotExist: - self.part = None + + except (ValueError, Part.DoesNotExist): pass + # If the output stock item is specified, hide the input field + output_id = form['install_into'].value() + + if output_id is not None: + + try: + self.output = StockItem.objects.get(pk=output_id) + form.fields['install_into'].widget = HiddenInput() + except (ValueError, StockItem.DoesNotExist): + pass + + else: + # If the output is not specified, but we know that the part is non-trackable, hide the install_into field + if self.part and not self.part.trackable: + form.fields['install_into'].widget = HiddenInput() + + if self.build and self.part: + available_items = self.build.availableStockItems(self.part, self.output) + form.fields['stock_item'].queryset = available_items + + self.available_stock = form.fields['stock_item'].queryset.all() + + # If there is only a single stockitem available, select it! + if len(self.available_stock) == 1: + form.fields['stock_item'].initial = self.available_stock[0].pk + return form def get_initial(self): """ Provide initial data for BomItem. Look for the folllowing in the GET data: - build: pk of the Build object + - part: pk of the Part object which we are assigning + - output: pk of the StockItem object into which the allocated stock will be installed """ initials = super(AjaxCreateView, self).get_initial().copy() build_id = self.get_param('build') part_id = self.get_param('part') + output_id = self.get_param('install_into') # Reference to a Part object part = None @@ -576,10 +936,13 @@ class BuildItemCreate(AjaxCreateView): # Reference to a Build object build = None + # Reference to a StockItem object + output = None + if part_id: try: part = Part.objects.get(pk=part_id) - initials['part'] = part + initials['part_id'] = part.pk except Part.DoesNotExist: pass @@ -590,15 +953,26 @@ class BuildItemCreate(AjaxCreateView): except Build.DoesNotExist: pass + # If the output has been specified + if output_id: + try: + output = StockItem.objects.get(pk=output_id) + initials['install_into'] = output + except (ValueError, StockItem.DoesNotExist): + pass + + # Work out how much stock is required + if build and part: + required_quantity = build.unallocatedQuantity(part, output) + else: + required_quantity = None + quantity = self.request.GET.get('quantity', None) if quantity is not None: quantity = float(quantity) - - if quantity is None: - # Work out how many parts remain to be alloacted for the build - if part: - quantity = build.getUnallocatedQuantity(part) + elif required_quantity is not None: + quantity = required_quantity item_id = self.get_param('item') @@ -606,7 +980,7 @@ class BuildItemCreate(AjaxCreateView): if item_id: try: item = StockItem.objects.get(pk=item_id) - except: + except (ValueError, StockItem.DoesNotExist): pass # If a StockItem is not selected, try to auto-select one @@ -632,7 +1006,7 @@ class BuildItemEdit(AjaxUpdateView): """ View to edit a BuildItem object """ model = BuildItem - ajax_template_name = 'modal_form.html' + ajax_template_name = 'build/edit_build_item.html' form_class = forms.EditBuildItemForm ajax_form_title = _('Edit Stock Allocation') role_required = 'build.change' @@ -643,23 +1017,107 @@ class BuildItemEdit(AjaxUpdateView): } def get_form(self): - """ Create form for editing a BuildItem. + """ + Create form for editing a BuildItem. - Limit the StockItem options to items that match the part """ - build_item = self.get_object() - form = super(BuildItemEdit, self).get_form() - query = StockItem.objects.all() - - if build_item.stock_item: - part_id = build_item.stock_item.part.id - query = query.filter(part=part_id) + # Hide fields which we do not wish the user to edit + for field in ['build', 'stock_item']: + if form[field].value(): + form.fields[field].widget = HiddenInput() - form.fields['stock_item'].queryset = query + form.fields['install_into'].widget = HiddenInput() + return form + + +class BuildAttachmentCreate(AjaxCreateView): + """ + View for creating a BuildAttachment + """ + + model = BuildOrderAttachment + form_class = forms.EditBuildAttachmentForm + ajax_form_title = _('Add Build Order Attachment') + role_required = 'build.add' + + def save(self, form, **kwargs): + """ + Add information on the user that uploaded the attachment + """ + + attachment = form.save(commit=False) + attachment.user = self.request.user + attachment.save() + + def get_data(self): + return { + 'success': _('Added attachment') + } + + def get_initial(self): + """ + Get initial data for creating an attachment + """ + + initials = super().get_initial() + + try: + initials['build'] = Build.objects.get(pk=self.request.GET.get('build', -1)) + except (ValueError, Build.DoesNotExist): + pass + + return initials + + def get_form(self): + """ + Hide the 'build' field if specified + """ + + form = super().get_form() + + form.fields['build'].widget = HiddenInput() + + return form + + +class BuildAttachmentEdit(AjaxUpdateView): + """ + View for editing a BuildAttachment object + """ + + model = BuildOrderAttachment + form_class = forms.EditBuildAttachmentForm + ajax_form_title = _('Edit Attachment') + role_required = 'build.change' + + def get_form(self): + form = super().get_form() form.fields['build'].widget = HiddenInput() return form + + def get_data(self): + return { + 'success': _('Attachment updated') + } + + +class BuildAttachmentDelete(AjaxDeleteView): + """ + View for deleting a BuildAttachment + """ + + model = BuildOrderAttachment + ajax_form_title = _('Delete Attachment') + context_object_name = 'attachment' + role_required = 'build.delete' + + def get_data(self): + return { + 'danger': _('Deleted attachment') + } diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index 548ac96016..731fd193fa 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -131,6 +131,13 @@ class SupplierPartList(generics.ListCreateAPIView): if part is not None: queryset = queryset.filter(part=part) + # Filter by 'active' status of the part? + active = params.get('active', None) + + if active is not None: + active = str2bool(active) + queryset = queryset.filter(part__active=active) + return queryset def get_serializer(self, *args, **kwargs): diff --git a/InvenTree/company/templates/company/assigned_stock.html b/InvenTree/company/templates/company/assigned_stock.html index 392dcaa999..b1a98a155a 100644 --- a/InvenTree/company/templates/company/assigned_stock.html +++ b/InvenTree/company/templates/company/assigned_stock.html @@ -15,7 +15,7 @@ </div> </div> -<table class='table table-striped table-condensed' id='stock-table'></table> +<table class='table table-striped table-condensed' id='stock-table' data-toolbar='#button-toolbar'></table> {% endblock %} diff --git a/InvenTree/company/templates/company/detail_part.html b/InvenTree/company/templates/company/detail_part.html index 463bf5814d..5f212d839a 100644 --- a/InvenTree/company/templates/company/detail_part.html +++ b/InvenTree/company/templates/company/detail_part.html @@ -10,21 +10,33 @@ <hr> {% if roles.purchase_order.change %} -<div id='button-toolbar' class='btn-group'> - {% if roles.purchase_order.add %} - <button class="btn btn-success" id='part-create' title='{% trans "Create new supplier part" %}'>{% trans "New Supplier Part" %}</button> - {% endif %} - <div class="dropdown" style="float: right;"> - <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %} - <span class="caret"></span></button> - <ul class="dropdown-menu"> +<div id='button-toolbar'> + <div class='button-toolbar container-fluid'> + <div class='btn-group role='group'> {% if roles.purchase_order.add %} - <li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li> + <button class="btn btn-success" id='part-create' title='{% trans "Create new supplier part" %}'> + <span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %} + </button> {% endif %} - {% if roles.purchase_order.delete %} - <li><a href='#' id='multi-part-delete' title='{% trans "Delete parts" %}'>{% trans "Delete Parts" %}</a></li> - {% endif %} - </ul> + <div class='btn-group'> + <div class="dropdown" style="float: right;"> + <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %} + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + {% if roles.purchase_order.add %} + <li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li> + {% endif %} + {% if roles.purchase_order.delete %} + <li><a href='#' id='multi-part-delete' title='{% trans "Delete parts" %}'>{% trans "Delete Parts" %}</a></li> + {% endif %} + </ul> + </div> + </div> + </div> + <div class='filter-list' id='filter-list-supplier-part'> + <!-- Empty div (will be filled out with available BOM filters) --> + </div> </div> </div> {% endif %} diff --git a/InvenTree/company/templates/company/purchase_orders.html b/InvenTree/company/templates/company/purchase_orders.html index a0ef1612fa..ce0763ae74 100644 --- a/InvenTree/company/templates/company/purchase_orders.html +++ b/InvenTree/company/templates/company/purchase_orders.html @@ -12,7 +12,8 @@ {% if roles.purchase_order.add %} <div id='button-bar'> <div class='button-toolbar container-fluid' style='float: right;'> - <button class='btn btn-primary' type='button' id='company-order2' title='{% trans "Create new purchase order" %}'>{% trans "New Purchase Order" %}</button> + <button class='btn btn-primary' type='button' id='company-order2' title='{% trans "Create new purchase order" %}'> + <span class='fas fa-plus-circle'></span> {% trans "New Purchase Order" %}</button> <div class='filter-list' id='filter-list-purchaseorder'> <!-- Empty div --> </div> diff --git a/InvenTree/company/templates/company/sales_orders.html b/InvenTree/company/templates/company/sales_orders.html index 03c64d5b88..d84cfc1d73 100644 --- a/InvenTree/company/templates/company/sales_orders.html +++ b/InvenTree/company/templates/company/sales_orders.html @@ -12,7 +12,9 @@ {% if roles.sales_order.add %} <div id='button-bar'> <div class='button-toolbar container-fluid' style='float: right;'> - <button class='btn btn-primary' type='button' id='new-sales-order' title='{% trans "Create new sales order" %}'>{% trans "New Sales Order" %}</button> + <button class='btn btn-primary' type='button' id='new-sales-order' title='{% trans "Create new sales order" %}'> + <div class='fas fa-plus-circle'></div> {% trans "New Sales Order" %} + </button> <div class='filter-list' id='filter-list-salesorder'> <!-- Empty div --> </div> diff --git a/InvenTree/company/templates/company/supplier_part_orders.html b/InvenTree/company/templates/company/supplier_part_orders.html index 29eb8ee874..88bc061bee 100644 --- a/InvenTree/company/templates/company/supplier_part_orders.html +++ b/InvenTree/company/templates/company/supplier_part_orders.html @@ -13,7 +13,8 @@ {% if roles.purchase_order.add %} <div id='button-bar'> <div class='btn-group'> - <button class='btn btn-primary' type='button' id='order-part2' title='Order part'>Order Part</button> + <button class='btn btn-primary' type='button' id='order-part2' title='Order part'> + <span class='fas fa-shopping-cart'></span> {% trans "Order Part" %}</button> </div> </div> {% endif %} diff --git a/InvenTree/company/templates/company/supplier_part_pricing.html b/InvenTree/company/templates/company/supplier_part_pricing.html index 97022024f5..6138669bc4 100644 --- a/InvenTree/company/templates/company/supplier_part_pricing.html +++ b/InvenTree/company/templates/company/supplier_part_pricing.html @@ -13,7 +13,9 @@ {% if roles.purchase_order.add %} <div id='price-break-toolbar' class='btn-group'> - <button class='btn btn-primary' id='new-price-break' type='button'>{% trans "Add Price Break" %}</button> + <button class='btn btn-primary' id='new-price-break' type='button'> + <span class='fas fa-plus-circle'></span> {% trans "Add Price Break" %} + </button> </div> {% endif %} diff --git a/InvenTree/config_template.yaml b/InvenTree/config_template.yaml index 94ec36bec7..ecd0a241cf 100644 --- a/InvenTree/config_template.yaml +++ b/InvenTree/config_template.yaml @@ -29,6 +29,12 @@ timezone: UTC # Set debug to False to run in production mode debug: True +# Set debug_toolbar to True to enable a debugging toolbar for InvenTree +# Note: This will only be displayed if DEBUG mode is enabled, +# and only if InvenTree is accessed from a local IP (127.0.0.1) +debug_toolbar: False + + # Allowed hosts (see ALLOWED_HOSTS in Django settings documentation) # A list of strings representing the host/domain names that this Django site can serve. # Default behaviour is to allow all hosts (THIS IS NOT SECURE!) @@ -63,11 +69,6 @@ static_root: '../inventree_static' # - git # - ssh -# Set debug_toolbar to True to enable a debugging toolbar for InvenTree -# Note: This will only be displayed if DEBUG mode is enabled, -# and only if InvenTree is accessed from a local IP (127.0.0.1) -debug_toolbar: False - # Backup options # Set the backup_dir parameter to store backup files in a specific location # If unspecified, the local user's temp directory will be used diff --git a/InvenTree/locale/de/LC_MESSAGES/django.mo b/InvenTree/locale/de/LC_MESSAGES/django.mo index 23c7009592..ccc97446ba 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 12f23f2b25..8a486733dc 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-10-25 10:54+0000\n" +"POT-Creation-Date: 2020-11-03 10:02+0000\n" "PO-Revision-Date: 2020-05-03 11:32+0200\n" "Last-Translator: Christian Schlüter <chschlue@gmail.com>\n" "Language-Team: C <kde-i18n-doc@kde.org>\n" @@ -25,7 +25,7 @@ msgstr "Keine Aktion angegeben" msgid "No matching action found" msgstr "Keine passende Aktion gefunden" -#: InvenTree/forms.py:102 build/forms.py:49 +#: InvenTree/forms.py:102 build/forms.py:82 build/forms.py:170 msgid "Confirm" msgstr "Bestätigen" @@ -49,7 +49,7 @@ msgstr "" msgid "Apply Theme" msgstr "" -#: InvenTree/helpers.py:361 order/models.py:187 order/models.py:261 +#: InvenTree/helpers.py:361 order/models.py:187 order/models.py:269 msgid "Invalid quantity provided" msgstr "Keine gültige Menge" @@ -92,12 +92,12 @@ msgstr "Datei zum Anhängen auswählen" msgid "File comment" msgstr "Datei-Kommentar" -#: InvenTree/models.py:68 templates/js/stock.html:700 +#: InvenTree/models.py:68 templates/js/stock.js:734 msgid "User" msgstr "Benutzer" -#: InvenTree/models.py:106 part/templates/part/params.html:22 -#: templates/js/part.html:81 +#: InvenTree/models.py:106 part/templates/part/params.html:24 +#: templates/js/part.js:129 msgid "Name" msgstr "Name" @@ -124,7 +124,7 @@ msgid "Polish" msgstr "Polnisch" #: InvenTree/status_codes.py:94 InvenTree/status_codes.py:135 -#: InvenTree/status_codes.py:222 templates/js/table_filters.html:135 +#: InvenTree/status_codes.py:222 msgid "Pending" msgstr "Ausstehend" @@ -176,11 +176,11 @@ msgstr "Zerstört" msgid "Rejected" msgstr "" -#: InvenTree/status_codes.py:223 build/templates/build/allocate.html:358 -#: order/templates/order/sales_order_detail.html:223 -#: part/templates/part/tabs.html:23 templates/js/build.html:140 -msgid "Allocated" -msgstr "Zugeordnet" +#: InvenTree/status_codes.py:223 +#, fuzzy +#| msgid "Location" +msgid "Production" +msgstr "Standort" #: InvenTree/validators.py:39 msgid "Invalid character in part name" @@ -214,7 +214,35 @@ msgstr "Überschuss darf 100% nicht überschreiten" msgid "Overage must be an integer value or a percentage" msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" -#: InvenTree/views.py:707 +#: InvenTree/views.py:493 +#, fuzzy +#| msgid "Delete BOM Item" +msgid "Delete Item" +msgstr "BOM-Position löschen" + +#: InvenTree/views.py:542 +#, 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 +#, fuzzy +#| msgid "No user information" +msgid "Edit User Information" +msgstr "Keine Benutzerinformation" + +#: InvenTree/views.py:568 templates/InvenTree/settings/user.html:22 +#, fuzzy +#| msgid "Select part" +msgid "Set Password" +msgstr "Teil auswählen" + +#: InvenTree/views.py:587 +msgid "Password fields must match" +msgstr "" + +#: InvenTree/views.py:757 msgid "Database Statistics" msgstr "Datenbankstatistiken" @@ -262,123 +290,189 @@ msgstr "" msgid "Barcode associated with StockItem" msgstr "Neues Lagerobjekt hinzufügen" -#: build/forms.py:28 +#: build/forms.py:32 #, fuzzy #| msgid "Order reference" msgid "Build Order reference" msgstr "Bestell-Referenz" -#: build/forms.py:70 -#, fuzzy -#| msgid "Location Details" -msgid "Location of completed parts" -msgstr "Standort-Details" +#: 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 +#: order/templates/order/order_wizard/select_parts.html:32 +#: order/templates/order/purchase_order_detail.html:178 +#: 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 +#: stock/templates/stock/item_base.html:40 +#: stock/templates/stock/item_base.html:46 +#: stock/templates/stock/item_base.html:197 +#: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.js:338 +#: templates/js/bom.js:189 templates/js/build.js:404 templates/js/stock.js:725 +#: templates/js/stock.js:953 +msgid "Quantity" +msgstr "Anzahl" -#: build/forms.py:74 +#: build/forms.py:71 +#, fuzzy +#| msgid "Serial number for this item" +msgid "Enter quantity for build output" +msgstr "Seriennummer für dieses Teil" + +#: build/forms.py:75 stock/forms.py:111 #, fuzzy #| msgid "Serial Number" msgid "Serial numbers" msgstr "Seriennummer" -#: build/forms.py:76 stock/forms.py:111 -msgid "Enter unique serial numbers (or leave blank)" -msgstr "Eindeutige Seriennummern eingeben (oder leer lassen)" +#: build/forms.py:77 +#, fuzzy +#| msgid "Serial number for this item" +msgid "Enter serial numbers for build outputs" +msgstr "Seriennummer für dieses Teil" -#: build/forms.py:79 +#: build/forms.py:83 +#, fuzzy +#| msgid "Confirm completion of build" +msgid "Confirm creation of build outut" +msgstr "Baufertigstellung bestätigen" + +#: build/forms.py:103 +#, fuzzy +#| msgid "Confirm completion of build" +msgid "Confirm deletion of build output" +msgstr "Baufertigstellung bestätigen" + +#: build/forms.py:124 +#, fuzzy +#| msgid "Confirm unallocation of build stock" +msgid "Confirm unallocation of stock" +msgstr "Zuweisungsaufhebung bestätigen" + +#: build/forms.py:148 +msgid "Confirm stock allocation" +msgstr "Lagerbestandszuordnung bestätigen" + +#: build/forms.py:171 +#, fuzzy +#| msgid "Mark order as complete" +msgid "Mark build as complete" +msgstr "Bestellung als vollständig markieren" + +#: build/forms.py:195 +#, fuzzy +#| msgid "Location Details" +msgid "Location of completed parts" +msgstr "Standort-Details" + +#: build/forms.py:200 +#, fuzzy +#| msgid "Confirm stock allocation" +msgid "Confirm completion with incomplete stock allocation" +msgstr "Lagerbestandszuordnung bestätigen" + +#: build/forms.py:203 msgid "Confirm build completion" msgstr "Bau-Fertigstellung bestätigen" -#: build/models.py:54 build/templates/build/build_base.html:8 +#: build/forms.py:223 build/views.py:68 +msgid "Confirm build cancellation" +msgstr "Bauabbruch bestätigen" + +#: build/forms.py:237 +#, fuzzy +#| msgid "Select stock item to allocate" +msgid "Select quantity of stock to allocate" +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:214 +#: stock/templates/stock/item_base.html:227 msgid "Build Order" msgstr "Bauauftrag" -#: build/models.py:55 build/templates/build/index.html:6 +#: build/models.py:57 build/templates/build/index.html:6 #: build/templates/build/index.html:14 order/templates/order/so_builds.html:11 #: order/templates/order/so_tabs.html:9 part/templates/part/tabs.html:31 #: templates/InvenTree/settings/tabs.html:28 users/models.py:30 msgid "Build Orders" msgstr "Bauaufträge" -#: build/models.py:77 -#, fuzzy -#| msgid "Overage must be an integer value or a percentage" -msgid "Build quantity must be integer value for trackable parts" -msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" - -#: build/models.py:86 build/templates/build/build_base.html:73 +#: build/models.py:72 #, fuzzy #| msgid "Order Reference" msgid "Build Order Reference" msgstr "Bestellreferenz" -#: build/models.py:87 build/templates/build/allocate.html:342 -#: order/templates/order/purchase_order_detail.html:172 -#: templates/js/bom.html:154 +#: build/models.py:73 order/templates/order/purchase_order_detail.html:173 +#: templates/js/bom.js:181 templates/js/build.js:493 msgid "Reference" msgstr "Referenz" -#: build/models.py:94 build/templates/build/allocate.html:337 +#: build/models.py:80 build/templates/build/detail.html:19 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 -#: order/templates/order/purchase_order_detail.html:159 +#: order/templates/order/purchase_order_detail.html:160 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 -#: templates/InvenTree/search.html:147 templates/js/bom.html:147 -#: templates/js/build.html:56 templates/js/company.html:56 -#: templates/js/order.html:167 templates/js/order.html:249 -#: templates/js/part.html:120 templates/js/part.html:203 -#: templates/js/part.html:345 templates/js/part.html:526 -#: templates/js/stock.html:445 templates/js/stock.html:672 +#: 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/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" msgstr "Beschreibung" -#: build/models.py:97 +#: build/models.py:83 msgid "Brief description of the build" msgstr "Kurze Beschreibung des Baus" -#: build/models.py:105 build/templates/build/build_base.html:94 +#: build/models.py:91 build/templates/build/build_base.html:94 +#: build/templates/build/detail.html:75 msgid "Parent Build" msgstr "Eltern-Bau" -#: build/models.py:106 -msgid "Parent build to which this build is allocated" -msgstr "Eltern-Bau, dem dieser Bau zugewiesen ist" +#: build/models.py:92 +#, fuzzy +#| msgid "SalesOrder to which this build is allocated" +msgid "BuildOrder to which this build is allocated" +msgstr "Bestellung, die diesem Bau zugwiesen ist" -#: build/models.py:111 build/templates/build/allocate.html:329 -#: build/templates/build/auto_allocate.html:19 -#: build/templates/build/build_base.html:78 -#: build/templates/build/detail.html:22 order/models.py:501 +#: 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 #: order/templates/order/order_wizard/select_parts.html:30 -#: order/templates/order/purchase_order_detail.html:147 +#: order/templates/order/purchase_order_detail.html:148 #: order/templates/order/receive_parts.html:19 part/models.py:293 #: part/templates/part/part_app_base.html:7 #: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 -#: templates/js/barcode.html:336 templates/js/bom.html:124 -#: templates/js/build.html:61 templates/js/company.html:137 -#: templates/js/part.html:184 templates/js/part.html:289 -#: templates/js/stock.html:421 templates/js/stock.html:978 +#: templates/js/barcode.js:336 templates/js/bom.js:147 templates/js/bom.js:484 +#: templates/js/build.js:647 templates/js/company.js:138 +#: templates/js/part.js:252 templates/js/part.js:357 templates/js/stock.js:468 +#: templates/js/stock.js:1025 msgid "Part" msgstr "Teil" -#: build/models.py:120 +#: build/models.py:105 msgid "Select part to build" msgstr "Teil für den Bau wählen" -#: build/models.py:125 +#: build/models.py:110 msgid "Sales Order Reference" msgstr "Bestellungsreferenz" -#: build/models.py:129 +#: build/models.py:114 msgid "SalesOrder to which this build is allocated" msgstr "Bestellung, die diesem Bau zugwiesen ist" -#: build/models.py:134 +#: build/models.py:119 msgid "Source Location" msgstr "Quell-Standort" -#: build/models.py:138 +#: build/models.py:123 msgid "" "Select location to take stock from for this build (leave blank to take from " "any stock location)" @@ -386,14 +480,38 @@ msgstr "" "Lager-Entnahmestandort für diesen Bau wählen (oder leer lassen für einen " "beliebigen Lager-Standort)" -#: build/models.py:142 +#: build/models.py:128 +#, fuzzy +#| msgid "Destination stock location" +msgid "Destination Location" +msgstr "Ziel-Lagerbestand" + +#: build/models.py:132 +msgid "Select location where the completed items will be stored" +msgstr "" + +#: build/models.py:136 msgid "Build Quantity" msgstr "Bau-Anzahl" -#: build/models.py:145 -msgid "Number of parts to build" +#: build/models.py:139 +#, fuzzy +#| msgid "Number of parts to build" +msgid "Number of stock items to build" msgstr "Anzahl der zu bauenden Teile" +#: build/models.py:143 +#, fuzzy +#| msgid "Completed" +msgid "Completed items" +msgstr "Fertig" + +#: build/models.py:145 +#, fuzzy +#| msgid "Delete this Stock Item when stock is depleted" +msgid "Number of stock items which have been completed" +msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" + #: build/models.py:149 part/templates/part/part_base.html:155 msgid "Build Status" msgstr "Bau-Status" @@ -402,7 +520,7 @@ msgstr "Bau-Status" msgid "Build status code" msgstr "Bau-Statuscode" -#: build/models.py:157 stock/models.py:387 +#: build/models.py:157 stock/models.py:389 msgid "Batch Code" msgstr "Losnummer" @@ -410,26 +528,26 @@ msgstr "Losnummer" msgid "Batch code for this build output" msgstr "Chargennummer für diese Bau-Ausgabe" -#: build/models.py:176 build/templates/build/detail.html:55 +#: build/models.py:176 build/templates/build/detail.html:89 #: 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:381 stock/templates/stock/item_base.html:266 +#: stock/models.py:383 stock/templates/stock/item_base.html:279 msgid "External Link" msgstr "Externer Link" -#: build/models.py:177 stock/models.py:383 +#: build/models.py:177 part/models.py:596 stock/models.py:385 msgid "Link to external URL" msgstr "Link zu einer externen URL" #: build/models.py:181 build/templates/build/tabs.html:14 company/models.py:314 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 -#: order/templates/order/purchase_order_detail.html:202 +#: order/templates/order/purchase_order_detail.html:203 #: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:70 -#: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 -#: stock/models.py:1404 stock/templates/stock/tabs.html:26 -#: templates/js/barcode.html:391 templates/js/bom.html:223 -#: templates/js/stock.html:116 templates/js/stock.html:544 +#: 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 +#: templates/js/barcode.js:391 templates/js/bom.js:250 +#: templates/js/stock.js:116 templates/js/stock.js:578 msgid "Notes" msgstr "Notizen" @@ -437,163 +555,175 @@ msgstr "Notizen" msgid "Extra build notes" msgstr "Notizen für den Bau" -#: build/models.py:520 +#: build/models.py:543 +#, fuzzy +#| msgid "No action specified" +msgid "No build output specified" +msgstr "Keine Aktion angegeben" + +#: build/models.py:546 +msgid "Build output is already completed" +msgstr "" + +#: build/models.py:549 +#, fuzzy +#| msgid "Quantity does not match serial numbers" +msgid "Build output does not match Build Order" +msgstr "Anzahl stimmt nicht mit den Seriennummern überein" + +#: build/models.py:620 +#, fuzzy +#| msgid "Complete Build" +msgid "Completed build output" +msgstr "Bau fertigstellen" + +#: build/models.py:858 +msgid "BuildItem must be unique for build, stock_item and install_into" +msgstr "" + +#: build/models.py:880 +#, fuzzy +#| msgid "Allocate Stock to Build" +msgid "Build item must specify a build output" +msgstr "Lagerbestand dem Bau zuweisen" + +#: build/models.py:885 #, python-brace-format msgid "Selected stock item not found in BOM for part '{p}'" msgstr "Ausgewähltes Lagerobjekt nicht in BOM für Teil '{p}' gefunden" -#: build/models.py:523 +#: build/models.py:889 #, python-brace-format 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:529 order/models.py:585 +#: build/models.py:896 order/models.py:603 msgid "StockItem is over-allocated" msgstr "Zu viele Lagerobjekte zugewiesen" -#: build/models.py:532 order/models.py:588 +#: build/models.py:900 order/models.py:606 msgid "Allocation quantity must be greater than zero" msgstr "Anzahl muss größer null sein" -#: build/models.py:535 +#: build/models.py:904 msgid "Quantity must be 1 for serialized stock" msgstr "Anzahl muss 1 für Objekte mit Seriennummer sein" -#: build/models.py:564 +#: build/models.py:944 msgid "Build to allocate parts" msgstr "Bau starten um Teile zuzuweisen" -#: build/models.py:571 -msgid "Stock Item to allocate to build" -msgstr "Lagerobjekt dem Bau zuweisen" +#: build/models.py:951 +#, fuzzy +#| msgid "Remove stock" +msgid "Source stock item" +msgstr "Bestand entfernen" -#: build/models.py:584 +#: build/models.py:964 msgid "Stock quantity to allocate to build" msgstr "Lagerobjekt-Anzahl dem Bau zuweisen" -#: build/templates/build/allocate.html:17 -#: company/templates/company/detail_part.html:22 order/views.py:804 -#: part/templates/part/category.html:122 +#: build/models.py:972 +#, fuzzy +#| msgid "Destination stock location" +msgid "Destination stock item" +msgstr "Ziel-Lagerbestand" + +#: build/templates/build/allocate.html:14 +#, fuzzy +#| msgid "Complete Build" +msgid "Incomplete Build Ouputs" +msgstr "Bau fertigstellen" + +#: build/templates/build/allocate.html:20 +#, fuzzy +#| msgid "Build order allocation is complete" +msgid "Build order has been completed" +msgstr "Bau-Zuweisung ist vollständig" + +#: build/templates/build/allocate.html:24 +#, fuzzy +#| msgid "Created new build" +msgid "Create new build output" +msgstr "Neuen Bau angelegt" + +#: build/templates/build/allocate.html:25 +#, fuzzy +#| msgid "Create New Part" +msgid "Create New Output" +msgstr "Neues Teil anlegen" + +#: build/templates/build/allocate.html:28 +#, fuzzy +#| msgid "Order part" +msgid "Order required parts" +msgstr "Teil bestellen" + +#: build/templates/build/allocate.html:29 +#: company/templates/company/detail_part.html:28 order/views.py:801 +#: part/templates/part/category.html:125 msgid "Order Parts" msgstr "Teile bestellen" -#: build/templates/build/allocate.html:18 -msgid "Automatically allocate stock" -msgstr "Lagerbestand automatisch zuweisen" - -#: build/templates/build/allocate.html:18 -msgid "Auto Allocate" -msgstr "Automatisches Zuweisen" - -#: build/templates/build/allocate.html:19 -msgid "Unallocate" +#: build/templates/build/allocate.html:32 templates/js/build.js:574 +#, fuzzy +#| msgid "Unallocate Stock" +msgid "Unallocate stock" msgstr "Zuweisung aufheben" -#: build/templates/build/allocate.html:87 templates/stock_table.html:10 -msgid "New Stock Item" -msgstr "Neues Lagerobjekt" +#: build/templates/build/allocate.html:33 build/views.py:341 build/views.py:778 +msgid "Unallocate Stock" +msgstr "Zuweisung aufheben" -#: build/templates/build/allocate.html:88 stock/views.py:1459 -msgid "Create new Stock Item" -msgstr "Neues Lagerobjekt hinzufügen" +#: build/templates/build/allocate.html:46 +#, fuzzy +#| msgid "Created new build" +msgid "Create a new build output" +msgstr "Neuen Bau angelegt" -#: build/templates/build/allocate.html:170 -#: order/templates/order/sales_order_detail.html:70 -#: order/templates/order/sales_order_detail.html:152 stock/models.py:375 -#: stock/templates/stock/item_base.html:178 -msgid "Serial Number" -msgstr "Seriennummer" +#: build/templates/build/allocate.html:47 +#, fuzzy +#| msgid "Complete Build" +msgid "No incomplete build outputs remain." +msgstr "Bau fertigstellen" -#: build/templates/build/allocate.html:172 -#: build/templates/build/auto_allocate.html:20 -#: build/templates/build/build_base.html:83 -#: build/templates/build/detail.html:27 -#: company/templates/company/supplier_part_pricing.html:73 -#: order/templates/order/order_wizard/select_parts.html:32 -#: order/templates/order/purchase_order_detail.html:177 -#: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 -#: part/templates/part/allocation.html:16 -#: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:80 stock/forms.py:297 -#: stock/templates/stock/item_base.html:26 -#: stock/templates/stock/item_base.html:32 -#: stock/templates/stock/item_base.html:184 -#: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.html:338 -#: templates/js/bom.html:162 templates/js/build.html:72 -#: templates/js/stock.html:691 templates/js/stock.html:906 -msgid "Quantity" -msgstr "Anzahl" +#: build/templates/build/allocate.html:48 +msgid "Create a new build output using the button above" +msgstr "" -#: build/templates/build/allocate.html:186 -#: build/templates/build/auto_allocate.html:21 stock/forms.py:336 -#: stock/templates/stock/item_base.html:220 -#: stock/templates/stock/stock_adjust.html:17 -#: templates/InvenTree/search.html:183 templates/js/barcode.html:337 -#: templates/js/stock.html:519 -msgid "Location" -msgstr "Standort" - -#: build/templates/build/allocate.html:210 -#: order/templates/order/sales_order_detail.html:94 templates/js/build.html:144 -msgid "Edit stock allocation" -msgstr "Lagerobjekt-Standort bearbeiten" - -#: build/templates/build/allocate.html:211 -#: order/templates/order/sales_order_detail.html:95 templates/js/build.html:145 -msgid "Delete stock allocation" -msgstr "Zuweisung löschen" - -#: build/templates/build/allocate.html:238 templates/js/bom.html:334 -msgid "No BOM items found" -msgstr "Keine BOM-Einträge gefunden" - -#: build/templates/build/allocate.html:347 part/models.py:1401 -#: templates/js/part.html:530 templates/js/table_filters.html:121 -msgid "Required" -msgstr "benötigt" - -#: build/templates/build/allocate.html:356 -msgid "Assigned" -msgstr "Zugewiesen" - -#: build/templates/build/allocate.html:394 -#: order/templates/order/sales_order_detail.html:273 -msgid "Buy parts" -msgstr "Teile kaufen" - -#: build/templates/build/allocate.html:398 -#: order/templates/order/sales_order_detail.html:277 -msgid "Build parts" -msgstr "Bauteile" - -#: build/templates/build/allocate.html:401 -msgid "Allocate stock" -msgstr "Lagerbestand zuweisen" +#: build/templates/build/attachments.html:11 build/templates/build/tabs.html:17 +#: order/templates/order/po_tabs.html:11 order/templates/order/so_tabs.html:16 +#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 +msgid "Attachments" +msgstr "Anhänge" #: build/templates/build/auto_allocate.html:9 msgid "Automatically Allocate Stock" msgstr "Lagerbestand automatisch zuweisen" #: build/templates/build/auto_allocate.html:10 +#, fuzzy +#| msgid "Stock Item to allocate to build" msgid "" -"Stock Items are selected for automatic allocation if there is only a single " -"stock item available." -msgstr "" -"Teile werden automatisch zugewiesen, wenn nur ein Lagerobjekt verfügbar ist" +"The following stock items will be allocated to the specified build output" +msgstr "Lagerobjekt dem Bau zuweisen" -#: build/templates/build/auto_allocate.html:11 -msgid "The following stock items will be allocated to the build:" -msgstr "Folgende Lagerobjekte werden dem Bau automatisch zugewiesen:" +#: build/templates/build/auto_allocate.html:18 stock/forms.py:336 +#: stock/templates/stock/item_base.html:233 +#: 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 +msgid "Location" +msgstr "Standort" -#: build/templates/build/auto_allocate.html:40 +#: build/templates/build/auto_allocate.html:37 #, fuzzy #| msgid "No stock items found that can be allocated to this build" msgid "No stock items found that can be automatically allocated to this build" msgstr "Keine Lagerobjekt gefunden, die diesem Bau zugewiesen werden können" -#: build/templates/build/auto_allocate.html:42 +#: build/templates/build/auto_allocate.html:39 #, fuzzy #| msgid "StockItem has been allocated" msgid "Stock items will have to be manually allocated" @@ -616,7 +746,7 @@ msgstr "Dieser Bau ist Kind von Bau" #: order/templates/order/order_base.html:26 #: order/templates/order/sales_order_base.html:35 #: part/templates/part/category.html:13 part/templates/part/part_base.html:32 -#: stock/templates/stock/item_base.html:76 +#: stock/templates/stock/item_base.html:90 #: stock/templates/stock/location.html:12 #, fuzzy #| msgid "Admin" @@ -629,7 +759,7 @@ msgstr "Admin" msgid "Edit Build" msgstr "Bau bearbeitet" -#: build/templates/build/build_base.html:50 build/views.py:190 +#: build/templates/build/build_base.html:50 msgid "Complete Build" msgstr "Bau fertigstellen" @@ -637,7 +767,7 @@ msgstr "Bau fertigstellen" msgid "Cancel Build" msgstr "Bau abbrechen" -#: build/templates/build/build_base.html:59 build/views.py:456 +#: build/templates/build/build_base.html:59 build/views.py:767 msgid "Delete Build" msgstr "Bau entfernt" @@ -645,126 +775,198 @@ msgstr "Bau entfernt" msgid "Build Details" msgstr "Bau-Status" -#: build/templates/build/build_base.html:88 -#: build/templates/build/detail.html:42 +#: 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:298 templates/InvenTree/search.html:175 -#: templates/js/barcode.html:42 templates/js/build.html:77 -#: templates/js/order.html:172 templates/js/order.html:254 -#: templates/js/stock.html:506 templates/js/stock.html:914 +#: stock/templates/stock/item_base.html:311 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/stock.js:557 templates/js/stock.js:961 msgid "Status" msgstr "Status" -#: build/templates/build/build_base.html:101 order/models.py:499 +#: build/templates/build/build_base.html:88 +#: build/templates/build/detail.html:62 +msgid "Progress" +msgstr "" + +#: build/templates/build/build_base.html:101 +#: build/templates/build/detail.html:82 order/models.py:517 #: 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:208 templates/js/order.html:221 +#: stock/templates/stock/item_base.html:221 templates/js/order.js:221 msgid "Sales Order" msgstr "Bestellung" -#: build/templates/build/build_base.html:107 -msgid "BOM Price" -msgstr "Stücklistenpreis" - -#: build/templates/build/build_base.html:112 -msgid "BOM pricing is incomplete" -msgstr "Stücklistenbepreisung ist unvollständig" - -#: build/templates/build/build_base.html:115 -msgid "No pricing information" -msgstr "Keine Preisinformation" - #: build/templates/build/build_output.html:9 build/templates/build/tabs.html:11 msgid "Build Outputs" msgstr "Bau-Ausgabe" -#: build/templates/build/complete.html:6 -#: stock/templates/stock/item_base.html:245 templates/js/build.html:40 -#: templates/navbar.html:25 -msgid "Build" -msgstr "Bau" +#: build/templates/build/build_output_create.html:7 +msgid "The Bill of Materials contains trackable parts" +msgstr "" -#: build/templates/build/complete.html:10 -msgid "Build order allocation is complete" +#: build/templates/build/build_output_create.html:8 +msgid "Build outputs must be generated individually." +msgstr "" + +#: build/templates/build/build_output_create.html:9 +msgid "Multiple build outputs will be created based on the quantity specified." +msgstr "" + +#: build/templates/build/build_output_create.html:15 +msgid "Trackable parts can have serial numbers specified" +msgstr "" + +#: build/templates/build/build_output_create.html:16 +#, fuzzy +#| msgid "Serial number for this item" +msgid "Enter serial numbers to generate multiple single build outputs" +msgstr "Seriennummer für dieses Teil" + +#: build/templates/build/cancel.html:5 +#, fuzzy +#| msgid "Are you sure you wish to unallocate all stock for this build?" +msgid "Are you sure you wish to cancel this build?" +msgstr "" +"Sind Sie sicher, dass sie alle Lagerobjekte von diesem Bau entfernen möchten?" + +#: build/templates/build/complete.html:8 +#, fuzzy +#| msgid "Build order allocation is complete" +msgid "Build can be completed" msgstr "Bau-Zuweisung ist vollständig" -#: build/templates/build/complete.html:14 -msgid "Warning: Build order allocation is not complete" -msgstr "Warnung: Bau-Zuweisung ist unvollständig" +#: build/templates/build/complete.html:12 +#, fuzzy +#| msgid "Build order allocation is complete" +msgid "Build cannot be completed" +msgstr "Bau-Zuweisung ist vollständig" #: build/templates/build/complete.html:15 -msgid "" -"Build Order has not been fully allocated. Ensure that all Stock Items have " -"been allocated to the Build" -msgstr "" -"Bau-Zuweisung ist unvollständig. Sicherstellen, dass alle Lagerobjekte dem " -"Bau zugewiesen wurden" +#, fuzzy +#| msgid "Complete Build" +msgid "Incompleted build outputs remain" +msgstr "Bau fertigstellen" -#: build/templates/build/complete.html:20 -msgid "The following actions will be performed:" -msgstr "Die folgenden Aktionen werden ausgeführt:" +#: build/templates/build/complete.html:18 +#, fuzzy +#| msgid "Build order allocation is complete" +msgid "Required build quantity has not been completed" +msgstr "Bau-Zuweisung ist vollständig" -#: build/templates/build/complete.html:22 -msgid "Remove allocated items from stock" -msgstr "Zugewiesene Teile dem Lager entnehmen" +#: build/templates/build/complete_output.html:9 +#, fuzzy +#| msgid "Build order allocation is complete" +msgid "Stock allocation is complete" +msgstr "Bau-Zuweisung ist vollständig" -#: build/templates/build/complete.html:23 -msgid "Add completed items to stock" -msgstr "Komplettierte Teile dem Lager hinzufügen" +#: build/templates/build/complete_output.html:13 +#, fuzzy +#| msgid "Build order allocation is complete" +msgid "Stock allocation is incomplete" +msgstr "Bau-Zuweisung ist vollständig" -#: build/templates/build/complete.html:29 +#: build/templates/build/complete_output.html:19 +#, fuzzy +#| msgid "This SalesOrder has not been fully allocated" +msgid "parts have not been fully allocated" +msgstr "Dieser Auftrag ist nicht vollständig zugeordnet" + +#: build/templates/build/complete_output.html:40 msgid "The following items will be created" msgstr "Die folgenden Objekte werden erstellt" -#: build/templates/build/delete_build_item.html:6 -msgid "Are you sure you want to unallocate these parts?" +#: build/templates/build/create_build_item.html:7 +#, fuzzy +#| msgid "Stock Item to allocate to build" +msgid "Select a stock item to allocate to the selected build output" +msgstr "Lagerobjekt dem Bau zuweisen" + +#: build/templates/build/create_build_item.html:11 +#, fuzzy +#| msgid "The following stock items will be allocated to the build:" +msgid "The allocated stock will be installed into the following build output:" +msgstr "Folgende Lagerobjekte werden dem Bau automatisch zugewiesen:" + +#: build/templates/build/create_build_item.html:19 +#, fuzzy +#| msgid "Stock available" +msgid "No stock available for" +msgstr "Bestand verfügbar" + +#: build/templates/build/delete_build_item.html:8 +#, fuzzy +#| msgid "Are you sure you want to unallocate these parts?" +msgid "Are you sure you want to unallocate this stock?" msgstr "Sind Sie sicher, dass sie die folgenden Teile entfernen möchten?" -#: build/templates/build/detail.html:17 -msgid "Title" -msgstr "Titel" +#: build/templates/build/delete_build_item.html:11 +#, fuzzy +#| msgid "The following stock items will be allocated to the build:" +msgid "The selected stock will be unallocated from the build output" +msgstr "Folgende Lagerobjekte werden dem Bau automatisch zugewiesen:" -#: build/templates/build/detail.html:31 +#: build/templates/build/detail.html:33 msgid "Stock Source" msgstr "Lagerobjekt" -#: build/templates/build/detail.html:36 +#: build/templates/build/detail.html:38 msgid "Stock can be taken from any available location." msgstr "Bestand kann jedem verfügbaren Lagerort entnommen werden." -#: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:238 templates/js/stock.html:514 -#: templates/js/stock.html:921 templates/js/table_filters.html:34 -#: templates/js/table_filters.html:100 +#: build/templates/build/detail.html:44 stock/forms.py:364 +#, fuzzy +#| msgid "Description" +msgid "Destination" +msgstr "Beschreibung" + +#: build/templates/build/detail.html:51 +#, fuzzy +#| msgid "Does this part have tracking for unique items?" +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 +#: templates/js/stock.js:968 templates/js/table_filters.js:80 +#: templates/js/table_filters.js:151 msgid "Batch" msgstr "Los" -#: build/templates/build/detail.html:61 +#: build/templates/build/detail.html:95 #: order/templates/order/order_base.html:98 -#: order/templates/order/sales_order_base.html:100 templates/js/build.html:85 +#: order/templates/order/sales_order_base.html:100 templates/js/build.js:683 msgid "Created" msgstr "Erstellt" -#: build/templates/build/detail.html:67 -msgid "Enough Parts?" -msgstr "Genügend Teile?" +#: build/templates/build/detail.html:105 +msgid "BOM Price" +msgstr "Stücklistenpreis" -#: build/templates/build/detail.html:70 -msgid "Yes" -msgstr "Ja" +#: build/templates/build/detail.html:110 +msgid "BOM pricing is incomplete" +msgstr "Stücklistenbepreisung ist unvollständig" -#: build/templates/build/detail.html:72 -msgid "No" -msgstr "Nein" +#: build/templates/build/detail.html:113 +msgid "No pricing information" +msgstr "Keine Preisinformation" -#: build/templates/build/detail.html:80 templates/js/build.html:90 +#: build/templates/build/detail.html:120 templates/js/build.js:661 +#: templates/js/build.js:688 msgid "Completed" msgstr "Fertig" -#: build/templates/build/index.html:24 build/views.py:403 +#: build/templates/build/edit_build_item.html:7 +#, fuzzy +#| msgid "Stock quantity to allocate to build" +msgid "Alter the quantity of stock allocated to the build output" +msgstr "Lagerobjekt-Anzahl dem Bau zuweisen" + +#: build/templates/build/index.html:25 build/views.py:658 msgid "New Build Order" msgstr "Neuer Bauauftrag" @@ -792,95 +994,208 @@ msgid "Details" msgstr "Details" #: build/templates/build/tabs.html:8 -msgid "Allocated Parts" +#, fuzzy +#| msgid "Allocated Parts" +msgid "Allocate Parts" msgstr "Zugeordnete Teile" -#: build/templates/build/unallocate.html:8 +#: build/templates/build/unallocate.html:10 msgid "Are you sure you wish to unallocate all stock for this build?" msgstr "" "Sind Sie sicher, dass sie alle Lagerobjekte von diesem Bau entfernen möchten?" -#: build/views.py:77 -msgid "Confirm build cancellation" -msgstr "Bauabbruch bestätigen" +#: build/templates/build/unallocate.html:12 +#, fuzzy +#| msgid "The following stock items will be allocated to the build:" +msgid "All incomplete stock allocations will be removed from the build" +msgstr "Folgende Lagerobjekte werden dem Bau automatisch zugewiesen:" -#: build/views.py:82 +#: build/views.py:79 msgid "Build was cancelled" msgstr "Bau wurde abgebrochen" -#: build/views.py:98 +#: build/views.py:93 msgid "Allocate Stock" msgstr "Lagerbestand zuweisen" -#: build/views.py:112 -msgid "No matching build found" -msgstr "Kein passender Bau gefunden" +#: build/views.py:157 build/views.py:317 build/views.py:490 +#, fuzzy +#| msgid "No action specified" +msgid "Build output must be specified" +msgstr "Keine Aktion angegeben" -#: build/views.py:131 -msgid "Confirm stock allocation" -msgstr "Lagerbestandszuordnung bestätigen" +#: build/views.py:171 +#, fuzzy +#| msgid "Allocate Stock to Build" +msgid "Allocated stock to build output" +msgstr "Lagerbestand dem Bau zuweisen" -#: build/views.py:132 -msgid "Check the confirmation box at the bottom of the list" -msgstr "Bestätigunsbox am Ende der Liste bestätigen" +#: build/views.py:183 +#, fuzzy +#| msgid "Build Outputs" +msgid "Create Build Output" +msgstr "Bau-Ausgabe" -#: build/views.py:152 build/views.py:467 -msgid "Unallocate Stock" -msgstr "Zuweisung aufheben" +#: build/views.py:207 stock/models.py:832 stock/views.py:1645 +#, fuzzy +#| msgid "Serial numbers already exist: " +msgid "Serial numbers already exist" +msgstr "Seriennummern existieren bereits:" -#: build/views.py:166 +#: build/views.py:216 +#, fuzzy +#| msgid "Serial number for this item" +msgid "Serial numbers required for trackable build output" +msgstr "Seriennummer für dieses Teil" + +#: build/views.py:282 +#, fuzzy +#| msgid "Delete Build" +msgid "Delete Build Output" +msgstr "Bau entfernt" + +#: build/views.py:302 build/views.py:387 msgid "Confirm unallocation of build stock" msgstr "Zuweisungsaufhebung bestätigen" -#: build/views.py:167 stock/views.py:421 +#: build/views.py:303 build/views.py:388 stock/views.py:413 msgid "Check the confirmation box" msgstr "Bestätigungsbox bestätigen" -#: build/views.py:270 -msgid "Confirm completion of build" +#: build/views.py:315 +#, fuzzy +#| msgid "Quantity does not match serial numbers" +msgid "Build output does not match build" +msgstr "Anzahl stimmt nicht mit den Seriennummern überein" + +#: build/views.py:329 +#, fuzzy +#| msgid "No action specified" +msgid "Build output deleted" +msgstr "Keine Aktion angegeben" + +#: build/views.py:412 +#, fuzzy +#| msgid "Complete Build" +msgid "Complete Build Order" +msgstr "Bau fertigstellen" + +#: build/views.py:418 +#, fuzzy +#| msgid "Build order allocation is complete" +msgid "Build order cannot be completed" +msgstr "Bau-Zuweisung ist vollständig" + +#: build/views.py:429 +#, fuzzy +#| msgid "Complete Build" +msgid "Completed build order" +msgstr "Bau fertigstellen" + +#: build/views.py:445 +#, fuzzy +#| msgid "Complete Build" +msgid "Complete Build Output" +msgstr "Bau fertigstellen" + +#: build/views.py:481 +#, fuzzy +#| msgid "Quantity must not exceed available stock quantity ({n})" +msgid "Quantity to complete cannot exceed build output quantity" +msgstr "Anzahl darf nicht die verfügbare Anzahl überschreiten ({n})" + +#: build/views.py:487 +#, fuzzy +#| msgid "Confirm completion of build" +msgid "Confirm completion of incomplete build" msgstr "Baufertigstellung bestätigen" -#: build/views.py:277 -msgid "Invalid location selected" -msgstr "Ungültige Ortsauswahl" +#: build/views.py:578 +#, fuzzy +#| msgid "Build order allocation is complete" +msgid "Build output completed" +msgstr "Bau-Zuweisung ist vollständig" -#: build/views.py:302 stock/views.py:1653 -#, python-brace-format -msgid "The following serial numbers already exist: ({sn})" -msgstr "Die folgende Seriennummer existiert bereits: ({sn})" - -#: build/views.py:323 -msgid "Build marked as COMPLETE" -msgstr "Bau als FERTIG markiert" - -#: build/views.py:431 +#: build/views.py:703 msgid "Created new build" msgstr "Neuen Bau angelegt" -#: build/views.py:441 +#: build/views.py:724 msgid "Edit Build Details" msgstr "Baudetails bearbeiten" -#: build/views.py:447 +#: build/views.py:758 msgid "Edited build" msgstr "Bau bearbeitet" -#: build/views.py:473 +#: build/views.py:784 msgid "Removed parts from build allocation" msgstr "Teile von Bauzuordnung entfernt" -#: build/views.py:483 -msgid "Allocate new Part" -msgstr "Neues Teil zuordnen" +#: build/views.py:796 +#, fuzzy +#| msgid "Allocate Stock to Build" +msgid "Allocate stock to build output" +msgstr "Lagerbestand dem Bau zuweisen" -#: build/views.py:637 +#: build/views.py:840 +#, fuzzy +#| msgid "This stock item is allocated to Build" +msgid "Item must be currently in stock" +msgstr "Dieses Lagerobjekt ist dem Bau zugewiesen" + +#: build/views.py:846 +#, fuzzy +#| msgid "StockItem is over-allocated" +msgid "Stock item is over-allocated" +msgstr "Zu viele Lagerobjekte zugewiesen" + +#: build/views.py:847 templates/js/bom.js:215 templates/js/build.js:503 +#: templates/js/build.js:731 +msgid "Available" +msgstr "verfügbar" + +#: build/views.py:849 +#, fuzzy +#| msgid "StockItem has been allocated" +msgid "Stock item must be selected" +msgstr "Lagerobjekt wurde zugewiesen" + +#: build/views.py:1011 msgid "Edit Stock Allocation" msgstr "Teilzuordnung bearbeiten" -#: build/views.py:642 +#: build/views.py:1016 msgid "Updated Build Item" msgstr "Bauobjekt aktualisiert" +#: build/views.py:1045 +#, fuzzy +#| msgid "Add Sales Order Attachment" +msgid "Add Build Order Attachment" +msgstr "Auftragsanhang hinzufügen" + +#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:96 +#: stock/views.py:176 +msgid "Added attachment" +msgstr "Anhang hinzugefügt" + +#: build/views.py:1095 order/views.py:191 order/views.py:213 +msgid "Edit Attachment" +msgstr "Anhang bearbeiten" + +#: build/views.py:1106 order/views.py:196 order/views.py:218 +msgid "Attachment updated" +msgstr "Anhang aktualisiert" + +#: build/views.py:1116 order/views.py:233 order/views.py:248 +msgid "Delete Attachment" +msgstr "Anhang löschen" + +#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:234 +msgid "Deleted attachment" +msgstr "Anhang gelöscht" + #: common/models.py:51 #, fuzzy #| msgid "Instance Name" @@ -987,45 +1302,45 @@ msgstr "Bestell-Referenz" msgid "Prefix value for purchase order reference" msgstr "Bestell-Referenz" -#: common/models.py:272 +#: common/models.py:277 msgid "Settings key (must be unique - case insensitive" msgstr "" "Einstellungs-Schlüssel (muss einzigartig sein, Groß-/ Kleinschreibung wird " "nicht beachtet)" -#: common/models.py:274 +#: common/models.py:279 msgid "Settings value" msgstr "Einstellungs-Wert" -#: common/models.py:326 +#: common/models.py:331 msgid "Value must be a boolean value" msgstr "" -#: common/models.py:340 +#: common/models.py:345 msgid "Key string must be unique" msgstr "Schlüsseltext muss eindeutig sein" -#: common/models.py:379 +#: common/models.py:384 msgid "Currency Symbol e.g. $" msgstr "Währungs-Symbol (z.B. €)" -#: common/models.py:381 +#: common/models.py:386 msgid "Currency Suffix e.g. AUD" msgstr "Währungs-Suffix (z.B. EUR)" -#: common/models.py:383 +#: common/models.py:388 msgid "Currency Description" msgstr "Währungs-Beschreibung" -#: common/models.py:385 +#: common/models.py:390 msgid "Currency Value" msgstr "Währungs-Wert" -#: common/models.py:387 +#: common/models.py:392 msgid "Use this currency as the base currency" msgstr "Benutze diese Währung als Basis-Währung" -#: common/models.py:470 +#: common/models.py:475 #, fuzzy #| msgid "Default Location" msgid "Default" @@ -1066,7 +1381,7 @@ msgid "Description of the company" msgstr "Firmenbeschreibung" #: company/models.py:94 company/templates/company/company_base.html:57 -#: templates/js/company.html:61 +#: templates/js/company.js:61 msgid "Website" msgstr "Website" @@ -1124,8 +1439,8 @@ msgstr "Kaufen Sie Teile von dieser Firma?" msgid "Does this company manufacture parts?" msgstr "Produziert diese Firma Teile?" -#: company/models.py:283 stock/models.py:335 -#: stock/templates/stock/item_base.html:164 +#: company/models.py:283 stock/models.py:337 +#: stock/templates/stock/item_base.html:177 msgid "Base Part" msgstr "Basisteil" @@ -1166,14 +1481,14 @@ msgid "Part packaging" msgstr "Teile-Packaging" #: company/templates/company/assigned_stock.html:9 -#: company/templates/company/tabs.html:25 +#: company/templates/company/tabs.html:25 templates/js/build.js:395 #, fuzzy #| msgid "Assigned" msgid "Assigned Stock" msgstr "Zugewiesen" #: company/templates/company/company_base.html:7 -#: company/templates/company/company_base.html:22 templates/js/company.html:33 +#: company/templates/company/company_base.html:22 templates/js/company.js:33 msgid "Company" msgstr "Firma" @@ -1189,7 +1504,7 @@ msgstr "Telefon" #: company/templates/company/detail.html:16 #: company/templates/company/supplier_part_base.html:84 #: company/templates/company/supplier_part_detail.html:30 part/bom.py:172 -#: templates/js/company.html:44 templates/js/company.html:186 +#: templates/js/company.js:44 templates/js/company.js:188 msgid "Manufacturer" msgstr "Hersteller" @@ -1198,15 +1513,15 @@ msgstr "Hersteller" #: 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:273 templates/js/company.html:48 -#: templates/js/company.html:162 templates/js/order.html:154 +#: stock/templates/stock/item_base.html:286 templates/js/company.js:48 +#: templates/js/company.js:164 templates/js/order.js:154 msgid "Supplier" msgstr "Zulieferer" #: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:81 stock/models.py:370 -#: stock/models.py:371 stock/templates/stock/item_base.html:191 -#: templates/js/company.html:40 templates/js/order.html:236 +#: 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 msgid "Customer" msgstr "Kunde" @@ -1214,64 +1529,64 @@ msgstr "Kunde" msgid "Supplier Parts" msgstr "Zulieferer-Teile" -#: company/templates/company/detail_part.html:15 -#: order/templates/order/purchase_order_detail.html:68 +#: company/templates/company/detail_part.html:17 +#: order/templates/order/purchase_order_detail.html:69 msgid "Create new supplier part" msgstr "Neues Zuliefererteil anlegen" -#: company/templates/company/detail_part.html:15 -#: order/templates/order/purchase_order_detail.html:67 -#: part/templates/part/supplier.html:13 templates/js/stock.html:798 +#: company/templates/company/detail_part.html:18 +#: order/templates/order/purchase_order_detail.html:68 +#: part/templates/part/supplier.html:14 templates/js/stock.js:845 msgid "New Supplier Part" msgstr "Neues Zulieferer-Teil" -#: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:117 part/templates/part/supplier.html:15 -#: templates/stock_table.html:14 +#: company/templates/company/detail_part.html:23 +#: part/templates/part/category.html:120 part/templates/part/supplier.html:17 +#: templates/stock_table.html:18 msgid "Options" msgstr "Optionen" -#: company/templates/company/detail_part.html:22 -#: part/templates/part/category.html:122 +#: company/templates/company/detail_part.html:28 +#: part/templates/part/category.html:125 #, fuzzy #| msgid "Order part" msgid "Order parts" msgstr "Teil bestellen" -#: company/templates/company/detail_part.html:25 +#: company/templates/company/detail_part.html:31 #, fuzzy #| msgid "Delete Parts" msgid "Delete parts" msgstr "Teile löschen" -#: company/templates/company/detail_part.html:25 +#: company/templates/company/detail_part.html:31 msgid "Delete Parts" msgstr "Teile löschen" -#: company/templates/company/detail_part.html:51 -#: part/templates/part/category.html:114 templates/js/stock.html:792 +#: company/templates/company/detail_part.html:63 +#: part/templates/part/category.html:116 templates/js/stock.js:839 msgid "New Part" msgstr "Neues Teil" -#: company/templates/company/detail_part.html:52 +#: company/templates/company/detail_part.html:64 msgid "Create new Part" msgstr "Neues Teil hinzufügen" -#: company/templates/company/detail_part.html:57 company/views.py:53 -#: part/templates/part/supplier.html:45 +#: company/templates/company/detail_part.html:69 company/views.py:53 +#: part/templates/part/supplier.html:47 msgid "New Supplier" msgstr "Neuer Zulieferer" -#: company/templates/company/detail_part.html:58 company/views.py:192 +#: company/templates/company/detail_part.html:70 company/views.py:192 msgid "Create new Supplier" msgstr "Neuen Zulieferer anlegen" -#: company/templates/company/detail_part.html:63 company/views.py:60 -#: part/templates/part/supplier.html:51 +#: company/templates/company/detail_part.html:75 company/views.py:60 +#: part/templates/part/supplier.html:53 msgid "New Manufacturer" msgstr "Neuer Hersteller" -#: company/templates/company/detail_part.html:64 company/views.py:195 +#: company/templates/company/detail_part.html:76 company/views.py:195 msgid "Create new Manufacturer" msgstr "Neuen Hersteller anlegen" @@ -1281,8 +1596,9 @@ msgstr "Zuliefererbestand" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:112 part/templates/part/category.html:123 -#: part/templates/part/stock.html:51 templates/stock_table.html:6 +#: part/templates/part/bom.html:63 part/templates/part/category.html:112 +#: part/templates/part/category.html:126 part/templates/part/stock.html:51 +#: templates/stock_table.html:7 msgid "Export" msgstr "Exportieren" @@ -1315,8 +1631,8 @@ msgstr "Bestellungen" msgid "Create new purchase order" msgstr "Neue Bestellung anlegen" -#: company/templates/company/purchase_orders.html:15 -#: order/templates/order/purchase_orders.html:18 +#: company/templates/company/purchase_orders.html:16 +#: order/templates/order/purchase_orders.html:19 msgid "New Purchase Order" msgstr "Neue Bestellung" @@ -1335,14 +1651,14 @@ msgstr "Bestellungen" msgid "Create new sales order" msgstr "Neuen Auftrag anlegen" -#: company/templates/company/sales_orders.html:15 -#: order/templates/order/sales_orders.html:18 +#: company/templates/company/sales_orders.html:16 +#: order/templates/order/sales_orders.html:19 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:344 -#: stock/templates/stock/item_base.html:278 templates/js/company.html:178 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:346 +#: stock/templates/stock/item_base.html:291 templates/js/company.js:180 msgid "Supplier Part" msgstr "Zulieferer-Teil" @@ -1376,7 +1692,7 @@ msgstr "SKU" #: company/templates/company/supplier_part_base.html:88 #: company/templates/company/supplier_part_detail.html:31 part/bom.py:173 -#: templates/js/company.html:202 +#: templates/js/company.js:204 msgid "MPN" msgstr "MPN" @@ -1389,35 +1705,40 @@ msgstr "Notiz" msgid "Supplier Part Orders" msgstr "Zuliefererbestellungen" +#: company/templates/company/supplier_part_orders.html:17 +#: part/templates/part/orders.html:15 +msgid "Order Part" +msgstr "Teil bestellen" + #: company/templates/company/supplier_part_pricing.html:10 msgid "Pricing Information" msgstr "Preisinformationen ansehen" -#: company/templates/company/supplier_part_pricing.html:16 company/views.py:410 -#: part/templates/part/sale_prices.html:13 part/views.py:2228 +#: company/templates/company/supplier_part_pricing.html:17 company/views.py:410 +#: part/templates/part/sale_prices.html:13 part/views.py:2292 msgid "Add Price Break" msgstr "Preisstaffel hinzufügen" -#: company/templates/company/supplier_part_pricing.html:34 +#: company/templates/company/supplier_part_pricing.html:36 #: part/templates/part/sale_prices.html:41 #, fuzzy #| msgid "No company information found" msgid "No price break information found" msgstr "Keine Firmeninformation gefunden" -#: company/templates/company/supplier_part_pricing.html:78 -#: part/templates/part/sale_prices.html:85 templates/js/bom.html:207 +#: 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:92 +#: company/templates/company/supplier_part_pricing.html:94 #: part/templates/part/sale_prices.html:99 #, fuzzy #| msgid "Edit Price Break" msgid "Edit price break" msgstr "Preisstaffel bearbeiten" -#: company/templates/company/supplier_part_pricing.html:93 +#: company/templates/company/supplier_part_pricing.html:95 #: part/templates/part/sale_prices.html:100 #, fuzzy #| msgid "Delete Price Break" @@ -1435,9 +1756,9 @@ msgstr "Bepreisung" #: company/templates/company/supplier_part_tabs.html:8 #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 -#: templates/InvenTree/settings/tabs.html:25 templates/js/part.html:124 -#: templates/js/part.html:372 templates/js/stock.html:453 -#: templates/navbar.html:22 users/models.py:29 +#: templates/InvenTree/settings/tabs.html:25 templates/js/part.js:192 +#: templates/js/part.js:418 templates/js/stock.js:502 templates/navbar.html:22 +#: users/models.py:29 msgid "Stock" msgstr "Lagerbestand" @@ -1519,7 +1840,7 @@ msgstr "Firma gelöscht" msgid "Edit Supplier Part" msgstr "Zuliefererteil bearbeiten" -#: company/views.py:278 templates/js/stock.html:799 +#: company/views.py:278 templates/js/stock.js:846 msgid "Create new Supplier Part" msgstr "Neues Zuliefererteil anlegen" @@ -1527,17 +1848,17 @@ msgstr "Neues Zuliefererteil anlegen" msgid "Delete Supplier Part" msgstr "Zuliefererteil entfernen" -#: company/views.py:416 part/views.py:2234 +#: company/views.py:416 part/views.py:2298 #, fuzzy #| msgid "Add Price Break" msgid "Added new price break" msgstr "Preisstaffel hinzufügen" -#: company/views.py:453 part/views.py:2279 +#: company/views.py:453 part/views.py:2343 msgid "Edit Price Break" msgstr "Preisstaffel bearbeiten" -#: company/views.py:469 part/views.py:2295 +#: company/views.py:469 part/views.py:2359 msgid "Delete Price Break" msgstr "Preisstaffel löschen" @@ -1618,7 +1939,7 @@ msgstr "Link auf externe Seite" msgid "Order notes" msgstr "Bestell-Notizen" -#: order/models.py:140 order/models.py:318 +#: order/models.py:140 order/models.py:326 #, fuzzy #| msgid "Purchase Order Details" msgid "Purchase order status" @@ -1642,8 +1963,8 @@ msgstr "" msgid "Date order was completed" msgstr "Bestellung als vollständig markieren" -#: order/models.py:185 order/models.py:259 part/views.py:1345 -#: stock/models.py:241 stock/models.py:805 +#: order/models.py:185 order/models.py:267 part/views.py:1409 +#: stock/models.py:243 stock/models.py:816 msgid "Quantity must be greater than zero" msgstr "Anzahl muss größer Null sein" @@ -1651,69 +1972,69 @@ msgstr "Anzahl muss größer Null sein" msgid "Part supplier must match PO supplier" msgstr "Teile-Zulieferer muss dem Zulieferer des Kaufvertrags entsprechen" -#: order/models.py:254 +#: order/models.py:262 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "Nur Teile aufgegebener Bestllungen können empfangen werden" -#: order/models.py:314 +#: order/models.py:322 msgid "Company to which the items are being sold" msgstr "" -#: order/models.py:320 +#: order/models.py:328 msgid "Customer order reference code" msgstr "Bestellreferenz" -#: order/models.py:359 +#: order/models.py:367 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:436 +#: order/models.py:454 msgid "Item quantity" msgstr "Anzahl" -#: order/models.py:438 +#: order/models.py:456 msgid "Line item reference" msgstr "Position - Referenz" -#: order/models.py:440 +#: order/models.py:458 msgid "Line item notes" msgstr "Position - Notizen" -#: order/models.py:466 order/templates/order/order_base.html:9 +#: order/models.py:484 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:252 templates/js/order.html:139 +#: stock/templates/stock/item_base.html:265 templates/js/order.js:139 msgid "Purchase Order" msgstr "Kaufvertrag" -#: order/models.py:479 +#: order/models.py:497 msgid "Supplier part" msgstr "Zulieferer-Teil" -#: order/models.py:482 +#: order/models.py:500 msgid "Number of items received" msgstr "Empfangene Objekt-Anzahl" -#: order/models.py:576 +#: order/models.py:594 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:578 +#: order/models.py:596 msgid "Cannot allocate stock to a line without a part" msgstr "Kann Lagerobjekt keiner Zeile ohne Teil hinzufügen" -#: order/models.py:581 +#: order/models.py:599 msgid "Allocation quantity cannot exceed stock quantity" msgstr "zugewiesene Anzahl darf nicht die verfügbare Anzahl überschreiten" -#: order/models.py:591 +#: order/models.py:609 msgid "Quantity must be 1 for serialized stock item" msgstr "Anzahl muss 1 für Objekte mit Seriennummer sein" -#: order/models.py:608 +#: order/models.py:626 msgid "Select stock item to allocate" msgstr "Lagerobjekt für Zuordnung auswählen" -#: order/models.py:611 +#: order/models.py:629 msgid "Enter stock allocation quantity" msgstr "Zuordnungsanzahl eingeben" @@ -1753,7 +2074,7 @@ msgstr "Bestellreferenz" msgid "Order Status" msgstr "Bestellstatus" -#: order/templates/order/order_base.html:85 templates/js/order.html:161 +#: order/templates/order/order_base.html:85 templates/js/order.js:161 msgid "Supplier Reference" msgstr "Zuliefererreferenz" @@ -1762,7 +2083,7 @@ msgid "Issued" msgstr "Aufgegeben" #: order/templates/order/order_base.html:111 -#: order/templates/order/purchase_order_detail.html:182 +#: order/templates/order/purchase_order_detail.html:183 #: order/templates/order/receive_parts.html:22 #: order/templates/order/sales_order_base.html:113 msgid "Received" @@ -1810,7 +2131,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.html:185 templates/js/order.html:272 +#: templates/js/order.js:185 templates/js/order.js:272 msgid "Items" msgstr "Positionen" @@ -1839,53 +2160,48 @@ msgstr "Position empfangen" msgid "Line Items" msgstr "Position hinzufügen" -#: order/templates/order/po_tabs.html:11 order/templates/order/so_tabs.html:16 -#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 -msgid "Attachments" -msgstr "Anhänge" - -#: order/templates/order/purchase_order_detail.html:16 -#: order/templates/order/sales_order_detail.html:18 order/views.py:1117 -#: order/views.py:1232 +#: order/templates/order/purchase_order_detail.html:17 +#: order/templates/order/sales_order_detail.html:19 order/views.py:1115 +#: order/views.py:1199 msgid "Add Line Item" msgstr "Position hinzufügen" -#: order/templates/order/purchase_order_detail.html:20 +#: order/templates/order/purchase_order_detail.html:21 msgid "Purchase Order Items" msgstr "Bestellpositionen" -#: order/templates/order/purchase_order_detail.html:38 -#: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:171 part/templates/part/category.html:213 -#: templates/js/stock.html:804 +#: order/templates/order/purchase_order_detail.html:39 +#: order/templates/order/purchase_order_detail.html:119 +#: part/templates/part/category.html:173 part/templates/part/category.html:215 +#: templates/js/stock.js:851 msgid "New Location" msgstr "Neuer Standort" -#: order/templates/order/purchase_order_detail.html:39 -#: order/templates/order/purchase_order_detail.html:119 +#: order/templates/order/purchase_order_detail.html:40 +#: order/templates/order/purchase_order_detail.html:120 #: stock/templates/stock/location.html:22 msgid "Create new stock location" msgstr "Neuen Lagerort anlegen" -#: order/templates/order/purchase_order_detail.html:131 +#: order/templates/order/purchase_order_detail.html:132 msgid "No line items found" msgstr "Keine Positionen gefunden" -#: order/templates/order/purchase_order_detail.html:164 +#: order/templates/order/purchase_order_detail.html:165 #: order/templates/order/receive_parts.html:20 msgid "Order Code" msgstr "Bestellnummer" -#: order/templates/order/purchase_order_detail.html:213 -#: order/templates/order/sales_order_detail.html:283 +#: order/templates/order/purchase_order_detail.html:214 +#: order/templates/order/sales_order_detail.html:285 msgid "Edit line item" msgstr "Position bearbeiten" -#: order/templates/order/purchase_order_detail.html:214 +#: order/templates/order/purchase_order_detail.html:215 msgid "Delete line item" msgstr "Position löschen" -#: order/templates/order/purchase_order_detail.html:219 +#: order/templates/order/purchase_order_detail.html:220 msgid "Receive line item" msgstr "Position empfangen" @@ -1898,7 +2214,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:145 templates/js/part.html:388 +#: part/templates/part/part_base.html:145 templates/js/part.js:434 msgid "On Order" msgstr "bestellt" @@ -1924,12 +2240,13 @@ msgstr "Packliste" msgid "Sales Order Details" msgstr "Auftragsdetails" -#: order/templates/order/sales_order_base.html:87 templates/js/order.html:243 +#: order/templates/order/sales_order_base.html:87 templates/js/order.js:243 msgid "Customer Reference" msgstr "Kundenreferenz" #: order/templates/order/sales_order_cancel.html:8 #: order/templates/order/sales_order_ship.html:9 +#: part/templates/part/bom_duplicate.html:12 #: stock/templates/stock/stockitem_convert.html:13 msgid "Warning" msgstr "Warnung" @@ -1938,15 +2255,45 @@ msgstr "Warnung" 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 +#: stock/templates/stock/item_base.html:191 templates/js/build.js:402 +msgid "Serial Number" +msgstr "Seriennummer" + +#: order/templates/order/sales_order_detail.html:96 templates/js/build.js:443 +#: templates/js/build.js:742 +msgid "Edit stock allocation" +msgstr "Lagerobjekt-Standort bearbeiten" + +#: order/templates/order/sales_order_detail.html:97 templates/js/build.js:445 +#: templates/js/build.js:743 +msgid "Delete stock allocation" +msgstr "Zuweisung löschen" + #: order/templates/order/sales_order_detail.html:225 +#: part/templates/part/tabs.html:23 templates/js/build.js:507 +#: templates/js/build.js:738 +msgid "Allocated" +msgstr "Zugeordnet" + +#: order/templates/order/sales_order_detail.html:227 msgid "Fulfilled" msgstr "Erledigt" -#: order/templates/order/sales_order_detail.html:280 +#: order/templates/order/sales_order_detail.html:275 +msgid "Buy parts" +msgstr "Teile kaufen" + +#: order/templates/order/sales_order_detail.html:279 +msgid "Build parts" +msgstr "Bauteile" + +#: order/templates/order/sales_order_detail.html:282 msgid "Allocate parts" msgstr "Teile zuordnen" -#: order/templates/order/sales_order_detail.html:284 +#: order/templates/order/sales_order_detail.html:286 msgid "Delete line item " msgstr "Position löschen" @@ -1996,143 +2343,139 @@ msgstr "Bestellungspositionen" msgid "Add Purchase Order Attachment" msgstr "Bestellanhang hinzufügen" -#: order/views.py:109 order/views.py:157 part/views.py:92 stock/views.py:175 -msgid "Added attachment" -msgstr "Anhang hinzugefügt" - -#: order/views.py:148 +#: order/views.py:150 msgid "Add Sales Order Attachment" msgstr "Auftragsanhang hinzufügen" -#: order/views.py:184 order/views.py:206 -msgid "Edit Attachment" -msgstr "Anhang bearbeiten" - -#: order/views.py:189 order/views.py:211 -msgid "Attachment updated" -msgstr "Anhang aktualisiert" - -#: order/views.py:226 order/views.py:241 -msgid "Delete Attachment" -msgstr "Anhang löschen" - -#: order/views.py:233 order/views.py:248 stock/views.py:233 -msgid "Deleted attachment" -msgstr "Anhang gelöscht" - -#: order/views.py:301 +#: order/views.py:310 msgid "Create Purchase Order" msgstr "Bestellung anlegen" -#: order/views.py:333 +#: order/views.py:345 msgid "Create Sales Order" msgstr "Auftrag anlegen" -#: order/views.py:364 +#: order/views.py:380 msgid "Edit Purchase Order" msgstr "Bestellung bearbeiten" -#: order/views.py:385 +#: order/views.py:401 msgid "Edit Sales Order" msgstr "Auftrag bearbeiten" -#: order/views.py:402 +#: order/views.py:418 msgid "Cancel Order" msgstr "Bestellung stornieren" -#: order/views.py:418 order/views.py:451 +#: order/views.py:428 order/views.py:455 msgid "Confirm order cancellation" msgstr "Bestellstornierung bestätigen" -#: order/views.py:436 +#: order/views.py:431 order/views.py:458 +msgid "Order cannot be cancelled" +msgstr "" + +#: order/views.py:445 msgid "Cancel sales order" msgstr "Auftrag stornieren" -#: order/views.py:457 -msgid "Could not cancel order" -msgstr "Stornierung fehlgeschlagen" - -#: order/views.py:471 +#: order/views.py:472 msgid "Issue Order" msgstr "Bestellung aufgeben" -#: order/views.py:487 +#: order/views.py:482 msgid "Confirm order placement" msgstr "Bestellungstätigung bestätigen" -#: order/views.py:508 +#: order/views.py:492 +#, fuzzy +#| msgid "Purchase Order Details" +msgid "Purchase order issued" +msgstr "Bestelldetails" + +#: order/views.py:503 msgid "Complete Order" msgstr "Auftrag fertigstellen" -#: order/views.py:544 +#: order/views.py:520 +#, fuzzy +#| msgid "Confirm build completion" +msgid "Confirm order completion" +msgstr "Bau-Fertigstellung bestätigen" + +#: order/views.py:531 +#, fuzzy +#| msgid "Mark order as complete" +msgid "Purchase order completed" +msgstr "Bestellung als vollständig markieren" + +#: order/views.py:541 msgid "Ship Order" msgstr "Versenden" -#: order/views.py:561 +#: order/views.py:558 msgid "Confirm order shipment" msgstr "Versand bestätigen" -#: order/views.py:567 +#: order/views.py:564 msgid "Could not ship order" msgstr "Versand fehlgeschlagen" -#: order/views.py:619 +#: order/views.py:616 msgid "Receive Parts" msgstr "Teile empfangen" -#: order/views.py:687 +#: order/views.py:684 msgid "Items received" msgstr "Anzahl empfangener Positionen" -#: order/views.py:701 +#: order/views.py:698 msgid "No destination set" msgstr "Kein Ziel gesetzt" -#: order/views.py:746 +#: order/views.py:743 msgid "Error converting quantity to number" msgstr "Fehler beim Konvertieren zu Zahl" -#: order/views.py:752 +#: order/views.py:749 msgid "Receive quantity less than zero" msgstr "Anzahl kleiner null empfangen" -#: order/views.py:758 +#: order/views.py:755 msgid "No lines specified" msgstr "Keine Zeilen angegeben" -#: order/views.py:1138 -msgid "Invalid Purchase Order" -msgstr "Ungültige Bestellung" +#: order/views.py:1125 +#, fuzzy +#| msgid "Supplier part description" +msgid "Supplier part must be specified" +msgstr "Zuliefererbeschreibung des Teils" -#: order/views.py:1146 +#: order/views.py:1131 msgid "Supplier must match for Part and Order" msgstr "Zulieferer muss zum Teil und zur Bestellung passen" -#: order/views.py:1151 -msgid "Invalid SupplierPart selection" -msgstr "Ungültige Wahl des Zulieferer-Teils" - -#: order/views.py:1284 order/views.py:1303 +#: order/views.py:1251 order/views.py:1270 msgid "Edit Line Item" msgstr "Position bearbeiten" -#: order/views.py:1320 order/views.py:1333 +#: order/views.py:1287 order/views.py:1300 msgid "Delete Line Item" msgstr "Position löschen" -#: order/views.py:1326 order/views.py:1339 +#: order/views.py:1293 order/views.py:1306 msgid "Deleted line item" msgstr "Position gelöscht" -#: order/views.py:1348 +#: order/views.py:1315 msgid "Allocate Stock to Order" msgstr "Lagerbestand dem Auftrag zuweisen" -#: order/views.py:1418 +#: order/views.py:1385 msgid "Edit Allocation Quantity" msgstr "Zuordnung bearbeiten" -#: order/views.py:1434 +#: order/views.py:1401 msgid "Remove allocation" msgstr "Zuordnung entfernen" @@ -2158,75 +2501,97 @@ 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:57 stock/forms.py:254 +#: part/forms.py:62 stock/forms.py:254 msgid "File Format" msgstr "Dateiformat" -#: part/forms.py:57 stock/forms.py:254 +#: part/forms.py:62 stock/forms.py:254 msgid "Select output file format" msgstr "Ausgabe-Dateiformat auswählen" -#: part/forms.py:59 +#: part/forms.py:64 msgid "Cascading" msgstr "Kaskadierend" -#: part/forms.py:59 +#: part/forms.py:64 msgid "Download cascading / multi-level BOM" msgstr "Kaskadierende Stückliste herunterladen" -#: part/forms.py:61 +#: part/forms.py:66 msgid "Levels" msgstr "" -#: part/forms.py:61 +#: part/forms.py:66 msgid "Select maximum number of BOM levels to export (0 = all levels)" msgstr "" -#: part/forms.py:63 +#: part/forms.py:68 #, fuzzy #| msgid "New Parameter" msgid "Include Parameter Data" msgstr "Neuer Parameter" -#: part/forms.py:63 +#: part/forms.py:68 msgid "Include part parameters data in exported BOM" msgstr "" -#: part/forms.py:65 +#: part/forms.py:70 #, fuzzy #| msgid "Include stock in sublocations" msgid "Include Stock Data" msgstr "Bestand in Unterlagerorten einschließen" -#: part/forms.py:65 +#: part/forms.py:70 #, fuzzy #| msgid "Include parts in subcategories" msgid "Include part stock data in exported BOM" msgstr "Teile in Unterkategorien einschließen" -#: part/forms.py:67 +#: part/forms.py:72 #, fuzzy #| msgid "New Supplier Part" msgid "Include Supplier Data" msgstr "Neues Zulieferer-Teil" -#: part/forms.py:67 +#: part/forms.py:72 msgid "Include part supplier data in exported BOM" msgstr "" -#: part/forms.py:86 +#: part/forms.py:93 part/models.py:1582 +msgid "Parent Part" +msgstr "Ausgangsteil" + +#: part/forms.py:94 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 +#, fuzzy +#| msgid "Select from existing images" +msgid "Clear existing BOM items" +msgstr "Aus vorhandenen Bildern auswählen" + +#: part/forms.py:105 +#, fuzzy +#| msgid "Confim BOM item deletion" +msgid "Confirm BOM duplication" +msgstr "Löschung von BOM-Position bestätigen" + +#: part/forms.py:123 msgid "Confirm that the BOM is correct" msgstr "Bestätigen, dass die Stückliste korrekt ist" -#: part/forms.py:98 +#: part/forms.py:135 msgid "Select BOM file to upload" msgstr "Stücklisten-Datei zum Upload auswählen" -#: part/forms.py:122 +#: part/forms.py:159 msgid "Select part category" msgstr "Teilekategorie wählen" -#: part/forms.py:136 +#: part/forms.py:173 #, fuzzy #| msgid "Perform 'deep copy' which will duplicate all BOM data for this part" msgid "Duplicate all BOM data for this part" @@ -2234,29 +2599,29 @@ msgstr "" "Tiefe Kopie ausführen. Dies wird alle Daten der Stückliste für dieses Teil " "duplizieren" -#: part/forms.py:137 +#: part/forms.py:174 msgid "Copy BOM" msgstr "" -#: part/forms.py:142 +#: part/forms.py:179 msgid "Duplicate all parameter data for this part" msgstr "" -#: part/forms.py:143 +#: part/forms.py:180 #, fuzzy #| msgid "Parameters" msgid "Copy Parameters" msgstr "Parameter" -#: part/forms.py:148 +#: part/forms.py:185 msgid "Confirm part creation" msgstr "Erstellen des Teils bestätigen" -#: part/forms.py:248 +#: part/forms.py:279 msgid "Input quantity for price calculation" msgstr "Eintragsmenge zur Preisberechnung" -#: part/forms.py:251 +#: part/forms.py:282 msgid "Select currency for price calculation" msgstr "Währung zur Preisberechnung wählen" @@ -2282,121 +2647,117 @@ msgstr "Teile-Kategorien" 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:435 +#: part/models.py:452 #, fuzzy #| msgid "No serial numbers found" msgid "Next available serial numbers are" msgstr "Keine Seriennummern gefunden" -#: part/models.py:439 +#: part/models.py:456 msgid "Next available serial number is" msgstr "" -#: part/models.py:444 +#: part/models.py:461 #, fuzzy #| msgid "Empty serial number string" msgid "Most recent serial number is" msgstr "Keine Seriennummer angegeben" -#: part/models.py:522 +#: part/models.py:539 msgid "Part must be unique for name, IPN and revision" msgstr "Namen, Teile- und Revisionsnummern müssen eindeutig sein" -#: part/models.py:537 part/templates/part/detail.html:19 +#: part/models.py:568 part/templates/part/detail.html:19 msgid "Part name" msgstr "Name des Teils" -#: part/models.py:541 +#: part/models.py:572 msgid "Is this part a template part?" msgstr "Ist dieses Teil eine Vorlage?" -#: part/models.py:550 +#: part/models.py:581 msgid "Is this part a variant of another part?" msgstr "Ist dieses Teil eine Variante eines anderen Teils?" -#: part/models.py:552 +#: part/models.py:583 msgid "Part description" msgstr "Beschreibung des Teils" -#: part/models.py:554 +#: part/models.py:585 msgid "Part keywords to improve visibility in search results" msgstr "Schlüsselworte um die Sichtbarkeit in Suchergebnissen zu verbessern" -#: part/models.py:559 +#: part/models.py:590 msgid "Part category" msgstr "Teile-Kategorie" -#: part/models.py:561 +#: part/models.py:592 msgid "Internal Part Number" msgstr "Interne Teilenummer" -#: part/models.py:563 +#: part/models.py:594 msgid "Part revision or version number" msgstr "Revisions- oder Versionsnummer" -#: part/models.py:565 -msgid "Link to extenal URL" -msgstr "Link zu einer Externen URL" - -#: part/models.py:577 +#: part/models.py:608 msgid "Where is this item normally stored?" msgstr "Wo wird dieses Teil normalerweise gelagert?" -#: part/models.py:621 +#: part/models.py:652 msgid "Default supplier part" msgstr "Standard-Zulieferer?" -#: part/models.py:624 +#: part/models.py:655 msgid "Minimum allowed stock level" msgstr "Minimal zulässiger Lagerbestand" -#: part/models.py:626 +#: part/models.py:657 msgid "Stock keeping units for this part" msgstr "Stock Keeping Units (SKU) für dieses Teil" -#: part/models.py:628 +#: part/models.py:659 msgid "Can this part be built from other parts?" msgstr "Kann dieses Teil aus anderen Teilen angefertigt werden?" -#: part/models.py:630 +#: part/models.py:661 msgid "Can this part be used to build other parts?" msgstr "Kann dieses Teil zum Bau von anderen genutzt werden?" -#: part/models.py:632 +#: part/models.py:663 msgid "Does this part have tracking for unique items?" msgstr "Hat dieses Teil Tracking für einzelne Objekte?" -#: part/models.py:634 +#: part/models.py:665 msgid "Can this part be purchased from external suppliers?" msgstr "Kann dieses Teil von externen Zulieferern gekauft werden?" -#: part/models.py:636 +#: part/models.py:667 msgid "Can this part be sold to customers?" msgstr "Kann dieses Teil an Kunden verkauft werden?" -#: part/models.py:638 +#: part/models.py:669 msgid "Is this part active?" msgstr "Ist dieses Teil aktiv?" -#: part/models.py:640 +#: part/models.py:671 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:642 +#: part/models.py:673 msgid "Part notes - supports Markdown formatting" msgstr "Bemerkungen - unterstüzt Markdown-Formatierung" -#: part/models.py:644 +#: part/models.py:675 msgid "Stored BOM checksum" msgstr "Prüfsumme der Stückliste gespeichert" -#: part/models.py:1353 +#: part/models.py:1455 #, 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:1370 +#: part/models.py:1472 #, fuzzy #| msgid "" #| "A stock item with this serial number already exists for template part " @@ -2406,120 +2767,121 @@ msgstr "" "Ein Teil mit dieser Seriennummer existiert bereits für die Teilevorlage " "{part}" -#: part/models.py:1389 templates/js/part.html:521 templates/js/stock.html:92 +#: part/models.py:1491 templates/js/part.js:567 templates/js/stock.js:92 #, fuzzy #| msgid "Instance Name" msgid "Test Name" msgstr "Instanzname" -#: part/models.py:1390 +#: part/models.py:1492 #, fuzzy #| msgid "Serial number for this item" msgid "Enter a name for the test" msgstr "Seriennummer für dieses Teil" -#: part/models.py:1395 +#: part/models.py:1497 #, fuzzy #| msgid "Description" msgid "Test Description" msgstr "Beschreibung" -#: part/models.py:1396 +#: part/models.py:1498 #, fuzzy #| msgid "Brief description of the build" msgid "Enter description for this test" msgstr "Kurze Beschreibung des Baus" -#: part/models.py:1402 +#: part/models.py:1503 templates/js/part.js:576 +#: templates/js/table_filters.js:172 +msgid "Required" +msgstr "benötigt" + +#: part/models.py:1504 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1407 templates/js/part.html:538 +#: part/models.py:1509 templates/js/part.js:584 #, fuzzy #| msgid "Required Parts" msgid "Requires Value" msgstr "benötigte Teile" -#: part/models.py:1408 +#: part/models.py:1510 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1413 templates/js/part.html:545 +#: part/models.py:1515 templates/js/part.js:591 #, fuzzy #| msgid "Delete Attachment" msgid "Requires Attachment" msgstr "Anhang löschen" -#: part/models.py:1414 +#: part/models.py:1516 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1447 +#: part/models.py:1549 msgid "Parameter template name must be unique" msgstr "Vorlagen-Name des Parameters muss eindeutig sein" -#: part/models.py:1452 +#: part/models.py:1554 msgid "Parameter Name" msgstr "Name des Parameters" -#: part/models.py:1454 +#: part/models.py:1556 msgid "Parameter Units" msgstr "Parameter Einheit" -#: part/models.py:1480 -msgid "Parent Part" -msgstr "Ausgangsteil" - -#: part/models.py:1482 +#: part/models.py:1584 msgid "Parameter Template" msgstr "Parameter Vorlage" -#: part/models.py:1484 +#: part/models.py:1586 msgid "Parameter Value" msgstr "Parameter Wert" -#: part/models.py:1521 +#: part/models.py:1623 msgid "Select parent part" msgstr "Ausgangsteil auswählen" -#: part/models.py:1529 +#: part/models.py:1631 msgid "Select part to be used in BOM" msgstr "Teil für die Nutzung in der Stückliste auswählen" -#: part/models.py:1535 +#: part/models.py:1637 msgid "BOM quantity for this BOM item" msgstr "Stücklisten-Anzahl für dieses Stücklisten-Teil" -#: part/models.py:1537 +#: part/models.py:1639 #, fuzzy #| msgid "Confim BOM item deletion" msgid "This BOM item is optional" msgstr "Löschung von BOM-Position bestätigen" -#: part/models.py:1540 +#: part/models.py:1642 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "Geschätzter Ausschuss (absolut oder prozentual)" -#: part/models.py:1543 +#: part/models.py:1645 msgid "BOM item reference" msgstr "Referenz des Objekts auf der Stückliste" -#: part/models.py:1546 +#: part/models.py:1648 msgid "BOM item notes" msgstr "Notizen zum Stücklisten-Objekt" -#: part/models.py:1548 +#: part/models.py:1650 msgid "BOM line checksum" msgstr "Prüfsumme der Stückliste" -#: part/models.py:1612 part/views.py:1351 part/views.py:1403 -#: stock/models.py:231 +#: part/models.py:1717 part/views.py:1415 part/views.py:1467 +#: stock/models.py:233 #, 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:1621 +#: part/models.py:1733 #, fuzzy #| msgid "New BOM Item" msgid "BOM Item" @@ -2539,10 +2901,10 @@ msgstr "Bestellung" #: part/templates/part/allocation.html:28 #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 -#: stock/templates/stock/item_base.html:58 -#: stock/templates/stock/item_base.html:260 -#: stock/templates/stock/stock_adjust.html:16 templates/js/build.html:126 -#: templates/js/stock.html:661 templates/js/stock.html:897 +#: stock/templates/stock/item_base.html:72 +#: stock/templates/stock/item_base.html:273 +#: 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" msgstr "Lagerobjekt" @@ -2554,46 +2916,89 @@ msgstr "Anhänge" msgid "Bill of Materials" msgstr "Stückliste" -#: part/templates/part/bom.html:37 +#: part/templates/part/bom.html:34 msgid "Remove selected BOM items" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: part/templates/part/bom.html:38 +#: part/templates/part/bom.html:37 msgid "Import BOM data" msgstr "Stückliste importieren" -#: part/templates/part/bom.html:39 +#: part/templates/part/bom.html:38 +msgid "Import from File" +msgstr "" + +#: part/templates/part/bom.html:41 +msgid "Copy BOM from parent part" +msgstr "" + +#: part/templates/part/bom.html:42 +#, fuzzy +#| msgid "Parameters" +msgid "Copy from Parent" +msgstr "Parameter" + +#: part/templates/part/bom.html:45 msgid "New BOM Item" msgstr "Neue Stücklistenposition" -#: part/templates/part/bom.html:40 +#: part/templates/part/bom.html:46 +#, fuzzy +#| msgid "Add Line Item" +msgid "Add Item" +msgstr "Position hinzufügen" + +#: part/templates/part/bom.html:48 msgid "Finish Editing" msgstr "Bearbeitung beenden" -#: part/templates/part/bom.html:43 +#: part/templates/part/bom.html:49 +#, fuzzy +#| msgid "Finish Editing" +msgid "Finished" +msgstr "Bearbeitung beenden" + +#: part/templates/part/bom.html:53 msgid "Edit BOM" msgstr "Stückliste bearbeiten" -#: part/templates/part/bom.html:45 +#: part/templates/part/bom.html:54 part/templates/part/params.html:38 +#: templates/InvenTree/settings/user.html:19 +msgid "Edit" +msgstr "Bearbeiten" + +#: part/templates/part/bom.html:57 msgid "Validate Bill of Materials" msgstr "Stückliste validieren" -#: part/templates/part/bom.html:48 part/views.py:1642 +#: part/templates/part/bom.html:58 +#, fuzzy +#| msgid "Validate BOM" +msgid "Validate" +msgstr "BOM validieren" + +#: part/templates/part/bom.html:62 part/views.py:1706 msgid "Export Bill of Materials" msgstr "Stückliste exportieren" -#: part/templates/part/bom.html:103 +#: part/templates/part/bom.html:123 #, fuzzy #| msgid "Remove selected BOM items" msgid "Delete selected BOM items?" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: part/templates/part/bom.html:104 +#: part/templates/part/bom.html:124 #, fuzzy #| msgid "Remove selected BOM items" msgid "All selected BOM items will be deleted" msgstr "Ausgewählte Stücklistenpositionen entfernen" +#: part/templates/part/bom_duplicate.html:13 +#, fuzzy +#| msgid "Export Bill of Materials" +msgid "This part already has a Bill of Materials" +msgstr "Stückliste exportieren" + #: part/templates/part/bom_upload/select_fields.html:8 #: part/templates/part/bom_upload/select_parts.html:8 #: part/templates/part/bom_upload/upload_file.html:10 @@ -2684,7 +3089,7 @@ msgstr "" msgid "Part Builds" msgstr "Eltern-Bau" -#: part/templates/part/build.html:14 +#: part/templates/part/build.html:15 #, fuzzy #| msgid "Start new Build" msgid "Start New Build" @@ -2694,7 +3099,7 @@ msgstr "Neuen Bau beginnen" msgid "All parts" msgstr "Alle Teile" -#: part/templates/part/category.html:24 part/views.py:2045 +#: part/templates/part/category.html:24 part/views.py:2109 msgid "Create new part category" msgstr "Teilkategorie anlegen" @@ -2734,57 +3139,57 @@ msgstr "Unter-Kategorien" msgid "Parts (Including subcategories)" msgstr "Teile (inklusive Unter-Kategorien)" -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:111 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:114 part/views.py:513 +#: part/templates/part/category.html:115 msgid "Create new part" msgstr "Neues Teil anlegen" -#: part/templates/part/category.html:120 +#: part/templates/part/category.html:123 #, fuzzy #| msgid "Part category" msgid "Set category" msgstr "Teile-Kategorie" -#: part/templates/part/category.html:120 +#: part/templates/part/category.html:123 #, fuzzy #| msgid "Set Part Category" msgid "Set Category" msgstr "Teilkategorie auswählen" -#: part/templates/part/category.html:123 +#: part/templates/part/category.html:126 #, fuzzy #| msgid "Export" msgid "Export Data" msgstr "Exportieren" -#: part/templates/part/category.html:172 +#: part/templates/part/category.html:174 #, fuzzy #| msgid "Create New Location" msgid "Create new location" msgstr "Neuen Standort anlegen" -#: part/templates/part/category.html:177 part/templates/part/category.html:207 +#: part/templates/part/category.html:179 part/templates/part/category.html:209 #, fuzzy #| msgid "Category" msgid "New Category" msgstr "Kategorie" -#: part/templates/part/category.html:178 +#: part/templates/part/category.html:180 #, fuzzy #| msgid "Create new part category" msgid "Create new category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:208 +#: part/templates/part/category.html:210 #, fuzzy #| msgid "Create new part category" msgid "Create new Part Category" msgstr "Teilkategorie anlegen" -#: part/templates/part/category.html:214 stock/views.py:1343 +#: part/templates/part/category.html:216 stock/views.py:1338 msgid "Create new Stock Location" msgstr "Neuen Lager-Standort erstellen" @@ -2794,16 +3199,32 @@ msgstr "Neuen Lager-Standort erstellen" msgid "Parametric Table" msgstr "Parameter Wert" +#: part/templates/part/create_part.html:11 +#, fuzzy +#| msgid "No matching data" +msgid "Possible Matching Parts" +msgstr "Keine passenden Daten" + +#: part/templates/part/create_part.html:12 +msgid "The new part may be a duplicate of these existing parts" +msgstr "" + +#: part/templates/part/create_part.html:16 +#, fuzzy +#| msgid "Batch" +msgid "match" +msgstr "Los" + #: part/templates/part/detail.html:9 msgid "Part Details" msgstr "Teile-Details" #: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 -#: templates/js/part.html:112 +#: templates/js/part.js:180 msgid "IPN" msgstr "IPN (Interne Produktnummer)" -#: part/templates/part/detail.html:32 templates/js/part.html:116 +#: part/templates/part/detail.html:32 templates/js/part.js:184 msgid "Revision" msgstr "Revision" @@ -2824,7 +3245,7 @@ msgid "Variant Of" msgstr "Variante von" #: part/templates/part/detail.html:70 part/templates/part/set_category.html:15 -#: templates/js/part.html:359 +#: templates/js/part.js:405 msgid "Category" msgstr "Kategorie" @@ -2832,7 +3253,7 @@ msgstr "Kategorie" msgid "Default Supplier" msgstr "Standard-Zulieferer" -#: part/templates/part/detail.html:102 part/templates/part/params.html:24 +#: part/templates/part/detail.html:102 part/templates/part/params.html:26 msgid "Units" msgstr "Einheiten" @@ -2840,7 +3261,7 @@ msgstr "Einheiten" msgid "Minimum Stock" msgstr "Minimaler Lagerbestand" -#: part/templates/part/detail.html:114 templates/js/order.html:262 +#: part/templates/part/detail.html:114 templates/js/order.js:262 msgid "Creation Date" msgstr "Erstelldatum" @@ -2852,102 +3273,105 @@ msgstr "Erstellt von" msgid "Responsible User" msgstr "Verantwortlicher Benutzer" -#: part/templates/part/detail.html:136 +#: part/templates/part/detail.html:138 templates/js/table_filters.js:27 msgid "Virtual" msgstr "Virtuell" -#: part/templates/part/detail.html:139 +#: part/templates/part/detail.html:141 msgid "Part is virtual (not a physical part)" msgstr "Teil ist virtuell (kein physisches Teil)" -#: part/templates/part/detail.html:141 +#: part/templates/part/detail.html:143 msgid "Part is not a virtual part" msgstr "Teil ist nicht virtuell" -#: part/templates/part/detail.html:145 stock/forms.py:248 -#: templates/js/table_filters.html:188 +#: part/templates/part/detail.html:148 stock/forms.py:248 +#: templates/js/table_filters.js:23 templates/js/table_filters.js:248 msgid "Template" msgstr "Vorlage" -#: part/templates/part/detail.html:148 +#: part/templates/part/detail.html:151 #, fuzzy #| msgid "Part cannot be a template part if it is a variant of another part" msgid "Part is a template part (variants can be made from this part)" msgstr "Teil kann keine Vorlage sein wenn es Variante eines anderen Teils ist" -#: part/templates/part/detail.html:150 +#: part/templates/part/detail.html:153 #, fuzzy #| msgid "Part is not a virtual part" msgid "Part is not a template part" msgstr "Teil ist nicht virtuell" -#: part/templates/part/detail.html:154 templates/js/table_filters.html:200 +#: part/templates/part/detail.html:158 templates/js/table_filters.js:260 msgid "Assembly" msgstr "Baugruppe" -#: part/templates/part/detail.html:157 +#: part/templates/part/detail.html:161 msgid "Part can be assembled from other parts" msgstr "Teil kann aus anderen Teilen angefertigt werden" -#: part/templates/part/detail.html:159 +#: part/templates/part/detail.html:163 msgid "Part cannot be assembled from other parts" msgstr "Teil kann nicht aus anderen Teilen angefertigt werden" -#: part/templates/part/detail.html:163 templates/js/table_filters.html:204 +#: part/templates/part/detail.html:168 templates/js/table_filters.js:264 msgid "Component" msgstr "Komponente" -#: part/templates/part/detail.html:166 +#: part/templates/part/detail.html:171 msgid "Part can be used in assemblies" msgstr "Teil kann in Baugruppen benutzt werden" -#: part/templates/part/detail.html:168 +#: part/templates/part/detail.html:173 msgid "Part cannot be used in assemblies" msgstr "Teil kann nicht in Baugruppen benutzt werden" -#: part/templates/part/detail.html:172 templates/js/table_filters.html:216 +#: part/templates/part/detail.html:178 templates/js/table_filters.js:31 +#: templates/js/table_filters.js:276 msgid "Trackable" msgstr "nachverfolgbar" -#: part/templates/part/detail.html:175 +#: part/templates/part/detail.html:181 msgid "Part stock is tracked by serial number" msgstr "Teilebestand in der Seriennummer hinterlegt" -#: part/templates/part/detail.html:177 +#: part/templates/part/detail.html:183 msgid "Part stock is not tracked by serial number" msgstr "Teilebestand ist nicht in der Seriennummer hinterlegt" -#: part/templates/part/detail.html:181 +#: part/templates/part/detail.html:188 msgid "Purchaseable" msgstr "Kaufbar" -#: part/templates/part/detail.html:184 part/templates/part/detail.html:186 +#: part/templates/part/detail.html:191 part/templates/part/detail.html:193 msgid "Part can be purchased from external suppliers" msgstr "Teil kann von externen Zulieferern gekauft werden" -#: part/templates/part/detail.html:190 templates/js/table_filters.html:212 +#: part/templates/part/detail.html:198 templates/js/table_filters.js:272 msgid "Salable" msgstr "Verkäuflich" -#: part/templates/part/detail.html:193 +#: part/templates/part/detail.html:201 msgid "Part can be sold to customers" msgstr "Teil kann an Kunden verkauft werden" -#: part/templates/part/detail.html:195 +#: part/templates/part/detail.html:203 msgid "Part cannot be sold to customers" msgstr "Teil kann nicht an Kunden verkauft werden" -#: part/templates/part/detail.html:199 templates/js/table_filters.html:183 +#: part/templates/part/detail.html:214 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/templates/part/detail.html:202 +#: part/templates/part/detail.html:217 #, fuzzy #| msgid "This part is not active" msgid "Part is active" msgstr "Dieses Teil ist nicht aktiv" -#: part/templates/part/detail.html:204 +#: part/templates/part/detail.html:219 #, fuzzy #| msgid "This part is not active" msgid "Part is not active" @@ -2957,10 +3381,6 @@ msgstr "Dieses Teil ist nicht aktiv" msgid "Part Notes" msgstr "Teil-Bemerkungen" -#: part/templates/part/orders.html:14 -msgid "Order Part" -msgstr "Teil bestellen" - #: part/templates/part/params.html:8 msgid "Part Parameters" msgstr "Teilparameter" @@ -2969,21 +3389,17 @@ msgstr "Teilparameter" msgid "Add new parameter" msgstr "Parameter hinzufügen" -#: part/templates/part/params.html:14 templates/InvenTree/settings/part.html:27 +#: part/templates/part/params.html:15 templates/InvenTree/settings/part.html:28 msgid "New Parameter" msgstr "Neuer Parameter" -#: part/templates/part/params.html:23 stock/models.py:1391 -#: templates/js/stock.html:112 +#: part/templates/part/params.html:25 stock/models.py:1415 +#: templates/js/stock.js:112 msgid "Value" msgstr "Wert" -#: part/templates/part/params.html:36 -msgid "Edit" -msgstr "Bearbeiten" - -#: part/templates/part/params.html:39 part/templates/part/supplier.html:17 -#: users/models.py:145 +#: part/templates/part/params.html:41 part/templates/part/supplier.html:19 +#: users/models.py:146 msgid "Delete" msgstr "Löschen" @@ -3005,8 +3421,8 @@ msgstr "Dieses Teil ist eine Vorlage." msgid "This part is a variant of" msgstr "Dieses Teil ist eine Variante von" -#: part/templates/part/part_base.html:36 templates/js/company.html:153 -#: templates/js/part.html:336 +#: part/templates/part/part_base.html:36 templates/js/company.js:155 +#: templates/js/part.js:95 templates/js/part.js:172 msgid "Inactive" msgstr "Inaktiv" @@ -3015,7 +3431,7 @@ msgid "Star this part" msgstr "Teil favorisieren" #: part/templates/part/part_base.html:49 -#: stock/templates/stock/item_base.html:88 +#: stock/templates/stock/item_base.html:101 #: stock/templates/stock/location.html:29 #, fuzzy #| msgid "Source Location" @@ -3023,7 +3439,7 @@ msgid "Barcode actions" msgstr "Quell-Standort" #: part/templates/part/part_base.html:51 -#: stock/templates/stock/item_base.html:90 +#: stock/templates/stock/item_base.html:103 #: stock/templates/stock/location.html:31 #, fuzzy #| msgid "Part QR Code" @@ -3031,7 +3447,7 @@ msgid "Show QR Code" msgstr "Teil-QR-Code" #: part/templates/part/part_base.html:52 -#: stock/templates/stock/item_base.html:91 +#: stock/templates/stock/item_base.html:104 #: stock/templates/stock/location.html:32 msgid "Print Label" msgstr "" @@ -3070,7 +3486,7 @@ msgstr "Vorlage bearbeiten" msgid "Delete part" msgstr "Teile löschen" -#: part/templates/part/part_base.html:124 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:124 templates/js/table_filters.js:111 msgid "In Stock" msgstr "Auf Lager" @@ -3136,8 +3552,8 @@ msgstr "Teil entfernen" msgid "Part Stock" msgstr "Teilbestand" -#: part/templates/part/stock_count.html:7 templates/js/bom.html:197 -#: templates/js/part.html:396 +#: part/templates/part/stock_count.html:7 templates/js/bom.js:224 +#: templates/js/part.js:442 msgid "No Stock" msgstr "Kein Bestand" @@ -3151,19 +3567,19 @@ msgstr "Niedriger Bestand" msgid "Part Suppliers" msgstr "Zulieferer" -#: part/templates/part/supplier.html:17 +#: part/templates/part/supplier.html:19 #, fuzzy #| msgid "Delete supplier part" msgid "Delete supplier parts" msgstr "Zuliefererteil entfernen" -#: part/templates/part/supplier.html:46 +#: part/templates/part/supplier.html:48 #, fuzzy #| msgid "Create new Supplier" msgid "Create new supplier" msgstr "Neuen Zulieferer anlegen" -#: part/templates/part/supplier.html:52 +#: part/templates/part/supplier.html:54 #, fuzzy #| msgid "Create new Manufacturer" msgid "Create new manufacturer" @@ -3185,7 +3601,7 @@ msgstr "Stückliste" msgid "Used In" msgstr "Benutzt in" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:304 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:317 msgid "Tests" msgstr "" @@ -3197,23 +3613,19 @@ msgstr "Teilverfolgung" msgid "Assemblies" msgstr "Baugruppen" -#: part/templates/part/used_in.html:43 -msgid "INACTIVE" -msgstr "INAKTIV" - #: part/templates/part/variants.html:11 #, fuzzy #| msgid "Variants" msgid "Part Variants" msgstr "Varianten" -#: part/templates/part/variants.html:21 +#: part/templates/part/variants.html:22 #, fuzzy #| msgid "Create new Part" msgid "Create new variant" msgstr "Neues Teil hinzufügen" -#: part/templates/part/variants.html:21 +#: part/templates/part/variants.html:23 #, fuzzy #| msgid "Variants" msgid "New Variant" @@ -3223,180 +3635,214 @@ msgstr "Varianten" msgid "Add part attachment" msgstr "Teilanhang hinzufügen" -#: part/views.py:131 templates/attachment_table.html:32 +#: part/views.py:135 templates/attachment_table.html:34 msgid "Edit attachment" msgstr "Anhang bearbeiten" -#: part/views.py:137 +#: part/views.py:141 msgid "Part attachment updated" msgstr "Teilanhang aktualisiert" -#: part/views.py:152 +#: part/views.py:156 msgid "Delete Part Attachment" msgstr "Teilanhang löschen" -#: part/views.py:160 +#: part/views.py:164 msgid "Deleted part attachment" msgstr "Teilanhang gelöscht" -#: part/views.py:169 +#: part/views.py:173 #, fuzzy #| msgid "Create Part Parameter Template" msgid "Create Test Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:198 +#: part/views.py:202 #, fuzzy #| msgid "Edit Template" msgid "Edit Test Template" msgstr "Vorlage bearbeiten" -#: part/views.py:214 +#: part/views.py:218 #, fuzzy #| msgid "Delete Template" msgid "Delete Test Template" msgstr "Vorlage löschen" -#: part/views.py:223 +#: part/views.py:227 msgid "Set Part Category" msgstr "Teilkategorie auswählen" -#: part/views.py:273 +#: part/views.py:277 #, python-brace-format msgid "Set category for {n} parts" msgstr "Kategorie für {n} Teile setzen" -#: part/views.py:308 +#: part/views.py:312 msgid "Create Variant" msgstr "Variante anlegen" -#: part/views.py:388 +#: part/views.py:394 msgid "Duplicate Part" msgstr "Teil duplizieren" -#: part/views.py:395 +#: part/views.py:401 msgid "Copied part" msgstr "Teil kopiert" -#: part/views.py:520 +#: part/views.py:455 part/views.py:585 +msgid "Possible matches exist - confirm creation of new part" +msgstr "" + +#: part/views.py:520 templates/js/stock.js:840 +msgid "Create New Part" +msgstr "Neues Teil anlegen" + +#: part/views.py:527 msgid "Created new part" msgstr "Neues Teil angelegt" -#: part/views.py:735 +#: part/views.py:743 msgid "Part QR Code" msgstr "Teil-QR-Code" -#: part/views.py:754 +#: part/views.py:762 msgid "Upload Part Image" msgstr "Teilbild hochladen" -#: part/views.py:762 part/views.py:799 +#: part/views.py:770 part/views.py:807 msgid "Updated part image" msgstr "Teilbild aktualisiert" -#: part/views.py:771 +#: part/views.py:779 msgid "Select Part Image" msgstr "Teilbild auswählen" -#: part/views.py:802 +#: part/views.py:810 msgid "Part image not found" msgstr "Teilbild nicht gefunden" -#: part/views.py:813 +#: part/views.py:821 msgid "Edit Part Properties" msgstr "Teileigenschaften bearbeiten" -#: part/views.py:837 +#: part/views.py:848 +#, fuzzy +#| msgid "Duplicate Part" +msgid "Duplicate BOM" +msgstr "Teil duplizieren" + +#: part/views.py:879 +#, fuzzy +#| msgid "Confirm unallocation of build stock" +msgid "Confirm duplication of BOM from parent" +msgstr "Zuweisungsaufhebung bestätigen" + +#: part/views.py:900 msgid "Validate BOM" msgstr "BOM validieren" -#: part/views.py:1004 +#: part/views.py:923 +#, 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:934 +#, fuzzy +#| msgid "Validate Bill of Materials" +msgid "Validated Bill of Materials" +msgstr "Stückliste validieren" + +#: part/views.py:1068 msgid "No BOM file provided" msgstr "Keine Stückliste angegeben" -#: part/views.py:1354 +#: part/views.py:1418 msgid "Enter a valid quantity" msgstr "Bitte eine gültige Anzahl eingeben" -#: part/views.py:1379 part/views.py:1382 +#: part/views.py:1443 part/views.py:1446 msgid "Select valid part" msgstr "Bitte ein gültiges Teil auswählen" -#: part/views.py:1388 +#: part/views.py:1452 msgid "Duplicate part selected" msgstr "Teil doppelt ausgewählt" -#: part/views.py:1426 +#: part/views.py:1490 msgid "Select a part" msgstr "Teil auswählen" -#: part/views.py:1432 +#: part/views.py:1496 #, 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:1436 +#: part/views.py:1500 msgid "Specify quantity" msgstr "Anzahl angeben" -#: part/views.py:1692 +#: part/views.py:1756 msgid "Confirm Part Deletion" msgstr "Löschen des Teils bestätigen" -#: part/views.py:1701 +#: part/views.py:1765 msgid "Part was deleted" msgstr "Teil wurde gelöscht" -#: part/views.py:1710 +#: part/views.py:1774 msgid "Part Pricing" msgstr "Teilbepreisung" -#: part/views.py:1836 +#: part/views.py:1900 msgid "Create Part Parameter Template" msgstr "Teilparametervorlage anlegen" -#: part/views.py:1846 +#: part/views.py:1910 msgid "Edit Part Parameter Template" msgstr "Teilparametervorlage bearbeiten" -#: part/views.py:1855 +#: part/views.py:1919 msgid "Delete Part Parameter Template" msgstr "Teilparametervorlage löschen" -#: part/views.py:1865 +#: part/views.py:1929 msgid "Create Part Parameter" msgstr "Teilparameter anlegen" -#: part/views.py:1917 +#: part/views.py:1981 msgid "Edit Part Parameter" msgstr "Teilparameter bearbeiten" -#: part/views.py:1933 +#: part/views.py:1997 msgid "Delete Part Parameter" msgstr "Teilparameter löschen" -#: part/views.py:1992 +#: part/views.py:2056 msgid "Edit Part Category" msgstr "Teilkategorie bearbeiten" -#: part/views.py:2029 +#: part/views.py:2093 msgid "Delete Part Category" msgstr "Teilkategorie löschen" -#: part/views.py:2037 +#: part/views.py:2101 msgid "Part category was deleted" msgstr "Teilekategorie wurde gelöscht" -#: part/views.py:2100 -msgid "Create BOM item" +#: part/views.py:2164 +#, fuzzy +#| msgid "Create BOM item" +msgid "Create BOM Item" msgstr "BOM-Position anlegen" -#: part/views.py:2168 +#: part/views.py:2232 msgid "Edit BOM item" msgstr "BOM-Position beaarbeiten" -#: part/views.py:2218 +#: part/views.py:2282 msgid "Confim BOM item deletion" msgstr "Löschung von BOM-Position bestätigen" @@ -3436,6 +3882,10 @@ msgstr "" msgid "Asset file description" msgstr "Einstellungs-Beschreibung" +#: stock/forms.py:111 +msgid "Enter unique serial numbers (or leave blank)" +msgstr "Eindeutige Seriennummern eingeben (oder leer lassen)" + #: stock/forms.py:191 msgid "Label" msgstr "" @@ -3492,12 +3942,6 @@ msgstr "Lagerbestandszuordnung bestätigen" msgid "Confirm removal of installed stock items" msgstr "Bewegung der Lagerobjekte bestätigen" -#: stock/forms.py:364 -#, fuzzy -#| msgid "Description" -msgid "Destination" -msgstr "Beschreibung" - #: stock/forms.py:364 msgid "Destination stock location" msgstr "Ziel-Lagerbestand" @@ -3506,7 +3950,7 @@ msgstr "Ziel-Lagerbestand" msgid "Add note (required)" msgstr "" -#: stock/forms.py:370 stock/views.py:921 stock/views.py:1119 +#: stock/forms.py:370 stock/views.py:916 stock/views.py:1114 msgid "Confirm stock adjustment" msgstr "Bestands-Anpassung bestätigen" @@ -3524,258 +3968,260 @@ msgstr "Standard-Lagerort" 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:212 +#: stock/models.py:178 +#, fuzzy +#| msgid "Created new stock item" +msgid "Created stock item" +msgstr "Neues Lagerobjekt erstellt" + +#: stock/models.py:214 #, 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:248 +#: stock/models.py:250 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "Teile-Typ ('{pf}') muss {pe} sein" -#: stock/models.py:258 stock/models.py:267 +#: stock/models.py:260 stock/models.py:269 msgid "Quantity must be 1 for item with a serial number" msgstr "Anzahl muss für Objekte mit Seriennummer \"1\" sein" -#: stock/models.py:259 +#: stock/models.py:261 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:281 +#: stock/models.py:283 msgid "Item cannot belong to itself" msgstr "Teil kann nicht zu sich selbst gehören" -#: stock/models.py:287 +#: stock/models.py:289 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:294 +#: stock/models.py:296 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:327 +#: stock/models.py:329 msgid "Parent Stock Item" msgstr "Eltern-Lagerobjekt" -#: stock/models.py:336 +#: stock/models.py:338 msgid "Base part" msgstr "Basis-Teil" -#: stock/models.py:345 +#: stock/models.py:347 msgid "Select a matching supplier part for this stock item" msgstr "Passenden Zulieferer für dieses Lagerobjekt auswählen" -#: stock/models.py:350 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:352 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "Lagerort" -#: stock/models.py:353 +#: stock/models.py:355 msgid "Where is this stock item located?" msgstr "Wo wird dieses Teil normalerweise gelagert?" -#: stock/models.py:358 stock/templates/stock/item_base.html:199 +#: stock/models.py:360 stock/templates/stock/item_base.html:212 msgid "Installed In" msgstr "Installiert in" -#: stock/models.py:361 +#: stock/models.py:363 msgid "Is this item installed in another item?" msgstr "Ist dieses Teil in einem anderen verbaut?" -#: stock/models.py:377 +#: stock/models.py:379 msgid "Serial number for this item" msgstr "Seriennummer für dieses Teil" -#: stock/models.py:389 +#: stock/models.py:391 msgid "Batch code for this stock item" msgstr "Losnummer für dieses Lagerobjekt" -#: stock/models.py:393 +#: stock/models.py:395 msgid "Stock Quantity" msgstr "Bestand" -#: stock/models.py:402 +#: stock/models.py:404 msgid "Source Build" msgstr "Quellbau" -#: stock/models.py:404 +#: stock/models.py:406 msgid "Build for this stock item" msgstr "Bau für dieses Lagerobjekt" -#: stock/models.py:415 +#: stock/models.py:417 msgid "Source Purchase Order" msgstr "Quellbestellung" -#: stock/models.py:418 +#: stock/models.py:420 msgid "Purchase order for this stock item" msgstr "Bestellung für dieses Teil" -#: stock/models.py:424 +#: stock/models.py:426 msgid "Destination Sales Order" msgstr "Zielauftrag" -#: stock/models.py:431 +#: stock/models.py:433 msgid "Destination Build Order" msgstr "Zielbauauftrag" -#: stock/models.py:444 +#: stock/models.py:446 msgid "Delete this Stock Item when stock is depleted" msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" -#: stock/models.py:454 stock/templates/stock/item_notes.html:14 +#: stock/models.py:456 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "Lagerobjekt-Notizen" -#: stock/models.py:505 +#: stock/models.py:507 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assigned to Customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:507 +#: stock/models.py:509 #, fuzzy #| msgid "Item assigned to customer?" msgid "Manually assigned to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:520 +#: stock/models.py:522 #, fuzzy #| msgid "Item assigned to customer?" msgid "Returned from customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/models.py:522 +#: stock/models.py:524 #, fuzzy #| msgid "Create new stock location" msgid "Returned to location" msgstr "Neuen Lagerort anlegen" -#: stock/models.py:650 +#: stock/models.py:652 #, fuzzy #| msgid "Installed in Stock Item" msgid "Installed into stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:658 +#: stock/models.py:660 #, fuzzy #| msgid "Installed in Stock Item" msgid "Installed stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:682 +#: stock/models.py:684 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstalled stock item" msgstr "In Lagerobjekt installiert" -#: stock/models.py:701 +#: stock/models.py:703 #, fuzzy #| msgid "Include sublocations" msgid "Uninstalled into location" msgstr "Unterlagerorte einschließen" -#: stock/models.py:796 +#: stock/models.py:807 #, fuzzy #| msgid "Part is not a virtual part" msgid "Part is not set as trackable" msgstr "Teil ist nicht virtuell" -#: stock/models.py:802 +#: stock/models.py:813 msgid "Quantity must be integer" msgstr "Anzahl muss eine Ganzzahl sein" -#: stock/models.py:808 +#: stock/models.py:819 #, 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:811 +#: stock/models.py:822 msgid "Serial numbers must be a list of integers" msgstr "Seriennummern muss eine Liste von Ganzzahlen sein" -#: stock/models.py:814 +#: stock/models.py:825 msgid "Quantity does not match serial numbers" msgstr "Anzahl stimmt nicht mit den Seriennummern überein" -#: stock/models.py:824 -msgid "Serial numbers already exist: " -msgstr "Seriennummern existieren bereits:" - -#: stock/models.py:849 +#: stock/models.py:857 msgid "Add serial number" msgstr "Seriennummer hinzufügen" -#: stock/models.py:852 +#: stock/models.py:860 #, python-brace-format msgid "Serialized {n} items" msgstr "{n} Teile serialisiert" -#: stock/models.py:963 +#: stock/models.py:971 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:1292 +#: stock/models.py:1316 msgid "Tracking entry title" msgstr "Name des Eintrags-Trackings" -#: stock/models.py:1294 +#: stock/models.py:1318 msgid "Entry notes" msgstr "Eintrags-Notizen" -#: stock/models.py:1296 +#: stock/models.py:1320 msgid "Link to external page for further information" msgstr "Link auf externe Seite für weitere Informationen" -#: stock/models.py:1356 +#: stock/models.py:1380 #, fuzzy #| msgid "Serial number for this item" msgid "Value must be provided for this test" msgstr "Seriennummer für dieses Teil" -#: stock/models.py:1362 +#: stock/models.py:1386 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1379 +#: stock/models.py:1403 msgid "Test" msgstr "" -#: stock/models.py:1380 +#: stock/models.py:1404 #, fuzzy #| msgid "Part name" msgid "Test name" msgstr "Name des Teils" -#: stock/models.py:1385 +#: stock/models.py:1409 #, fuzzy #| msgid "Search Results" msgid "Result" msgstr "Suchergebnisse" -#: stock/models.py:1386 templates/js/table_filters.html:111 +#: stock/models.py:1410 templates/js/table_filters.js:162 msgid "Test result" msgstr "" -#: stock/models.py:1392 +#: stock/models.py:1416 msgid "Test output value" msgstr "" -#: stock/models.py:1398 +#: stock/models.py:1422 #, fuzzy #| msgid "Attachments" msgid "Attachment" msgstr "Anhänge" -#: stock/models.py:1399 +#: stock/models.py:1423 #, fuzzy #| msgid "Delete attachment" msgid "Test result attachment" msgstr "Anhang löschen" -#: stock/models.py:1405 +#: stock/models.py:1429 #, fuzzy #| msgid "Edit notes" msgid "Test notes" @@ -3785,6 +4231,12 @@ msgstr "Bermerkungen bearbeiten" msgid "Stock Tracking Information" msgstr "Informationen zum Lagerbestands-Tracking" +#: stock/templates/stock/item.html:18 +#, fuzzy +#| msgid "Category" +msgid "New Entry" +msgstr "Kategorie" + #: stock/templates/stock/item_attachments.html:10 #, fuzzy #| msgid "Stock Item Notes" @@ -3794,18 +4246,28 @@ msgstr "Lagerobjekt-Notizen" #: stock/templates/stock/item_base.html:20 #, fuzzy #| msgid "This stock item does not have any child items" +msgid "This stock item is in production and cannot be edited." +msgstr "Dieses Lagerobjekt hat keine Kinder" + +#: stock/templates/stock/item_base.html:21 +msgid "Edit the stock item from the build view." +msgstr "" + +#: stock/templates/stock/item_base.html:34 +#, fuzzy +#| msgid "This stock item does not have any child items" msgid "This stock item has not passed all required tests" msgstr "Dieses Lagerobjekt hat keine Kinder" -#: stock/templates/stock/item_base.html:26 +#: stock/templates/stock/item_base.html:40 msgid "This stock item is allocated to Sales Order" msgstr "Dieses Lagerobjekt ist dem Auftrag zugewiesen" -#: stock/templates/stock/item_base.html:32 +#: stock/templates/stock/item_base.html:46 msgid "This stock item is allocated to Build" msgstr "Dieses Lagerobjekt ist dem Bau zugewiesen" -#: stock/templates/stock/item_base.html:38 +#: stock/templates/stock/item_base.html:52 msgid "" "This stock item is serialized - it has a unique serial number and the " "quantity cannot be adjusted." @@ -3813,143 +4275,148 @@ msgstr "" "Dieses Lagerobjekt ist serialisiert. Es hat eine eindeutige Seriennummer und " "die Anzahl kann nicht angepasst werden." -#: stock/templates/stock/item_base.html:42 +#: stock/templates/stock/item_base.html:56 msgid "This stock item cannot be deleted as it has child items" msgstr "Dieses Lagerobjekt kann nicht gelöscht werden, da es Kinder besitzt" -#: stock/templates/stock/item_base.html:46 +#: stock/templates/stock/item_base.html:60 msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" "Dieses Lagerobjekt wird automatisch gelöscht wenn der Lagerbestand " "aufgebraucht ist." -#: stock/templates/stock/item_base.html:94 templates/js/barcode.html:283 -#: templates/js/barcode.html:288 +#: stock/templates/stock/item_base.html:107 templates/js/barcode.js:283 +#: templates/js/barcode.js:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:96 +#: stock/templates/stock/item_base.html:109 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:104 +#: stock/templates/stock/item_base.html:117 #, fuzzy #| msgid "Confirm stock adjustment" msgid "Stock adjustment actions" msgstr "Bestands-Anpassung bestätigen" -#: stock/templates/stock/item_base.html:108 -#: stock/templates/stock/location.html:41 templates/stock_table.html:19 +#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/location.html:41 templates/stock_table.html:23 msgid "Count stock" msgstr "Bestand zählen" -#: stock/templates/stock/item_base.html:109 templates/stock_table.html:17 +#: stock/templates/stock/item_base.html:122 templates/stock_table.html:21 msgid "Add stock" msgstr "Bestand hinzufügen" -#: stock/templates/stock/item_base.html:110 templates/stock_table.html:18 +#: stock/templates/stock/item_base.html:123 templates/stock_table.html:22 msgid "Remove stock" msgstr "Bestand entfernen" -#: stock/templates/stock/item_base.html:112 +#: stock/templates/stock/item_base.html:125 #, fuzzy #| msgid "Order stock" msgid "Transfer stock" msgstr "Bestand bestellen" -#: stock/templates/stock/item_base.html:114 +#: stock/templates/stock/item_base.html:127 #, fuzzy #| msgid "Serialize Stock" msgid "Serialize stock" msgstr "Lagerbestand erfassen" -#: stock/templates/stock/item_base.html:118 +#: stock/templates/stock/item_base.html:131 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assign to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/item_base.html:134 #, fuzzy #| msgid "Count stock" msgid "Return to stock" msgstr "Bestand zählen" -#: stock/templates/stock/item_base.html:125 templates/js/stock.html:934 +#: stock/templates/stock/item_base.html:138 templates/js/stock.js:981 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstall stock item" msgstr "In Lagerobjekt installiert" -#: stock/templates/stock/item_base.html:125 +#: stock/templates/stock/item_base.html:138 msgid "Uninstall" msgstr "" -#: stock/templates/stock/item_base.html:134 +#: stock/templates/stock/item_base.html:147 #: stock/templates/stock/location.html:38 #, fuzzy #| msgid "Stock Locations" msgid "Stock actions" msgstr "Lagerobjekt-Standorte" -#: stock/templates/stock/item_base.html:137 +#: stock/templates/stock/item_base.html:150 #, fuzzy #| msgid "Count stock items" msgid "Convert to variant" msgstr "Lagerobjekte zählen" -#: stock/templates/stock/item_base.html:140 +#: stock/templates/stock/item_base.html:153 #, fuzzy #| msgid "Count stock items" msgid "Duplicate stock item" msgstr "Lagerobjekte zählen" -#: stock/templates/stock/item_base.html:142 +#: stock/templates/stock/item_base.html:155 #, fuzzy #| msgid "Edit Stock Item" msgid "Edit stock item" msgstr "Lagerobjekt bearbeiten" -#: stock/templates/stock/item_base.html:145 +#: stock/templates/stock/item_base.html:158 #, fuzzy #| msgid "Delete Stock Item" msgid "Delete stock item" msgstr "Lagerobjekt löschen" -#: stock/templates/stock/item_base.html:151 +#: stock/templates/stock/item_base.html:164 msgid "Generate test report" msgstr "" -#: stock/templates/stock/item_base.html:159 +#: stock/templates/stock/item_base.html:172 msgid "Stock Item Details" msgstr "Lagerbestands-Details" -#: stock/templates/stock/item_base.html:224 +#: stock/templates/stock/item_base.html:237 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:231 +#: stock/templates/stock/item_base.html:244 #, fuzzy #| msgid "Unique Identifier" msgid "Barcode Identifier" msgstr "Eindeutiger Bezeichner" -#: stock/templates/stock/item_base.html:259 +#: stock/templates/stock/item_base.html:258 templates/js/build.js:626 +#: templates/navbar.html:25 +msgid "Build" +msgstr "Bau" + +#: stock/templates/stock/item_base.html:272 msgid "Parent Item" msgstr "Elternposition" -#: stock/templates/stock/item_base.html:284 +#: stock/templates/stock/item_base.html:297 msgid "Last Updated" msgstr "Zuletzt aktualisiert" -#: stock/templates/stock/item_base.html:289 +#: stock/templates/stock/item_base.html:302 msgid "Last Stocktake" msgstr "Letzte Inventur" -#: stock/templates/stock/item_base.html:293 +#: stock/templates/stock/item_base.html:306 msgid "No stocktake performed" msgstr "Keine Inventur ausgeführt" @@ -4007,17 +4474,17 @@ msgstr "" msgid "Test Data" msgstr "" -#: stock/templates/stock/item_tests.html:17 +#: stock/templates/stock/item_tests.html:18 #, fuzzy #| msgid "Delete Template" msgid "Delete Test Data" msgstr "Vorlage löschen" -#: stock/templates/stock/item_tests.html:19 +#: stock/templates/stock/item_tests.html:22 msgid "Add Test Data" msgstr "" -#: stock/templates/stock/item_tests.html:20 +#: stock/templates/stock/item_tests.html:25 msgid "Test Report" msgstr "" @@ -4093,7 +4560,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:1315 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1310 #, fuzzy #| msgid "Count Stock Items" msgid "Convert Stock Item" @@ -4145,238 +4612,246 @@ msgstr "QR-Code für diesen Standort" msgid "Add Stock Item Attachment" msgstr "Anhang hinzufügen" -#: stock/views.py:209 +#: stock/views.py:210 #, fuzzy #| msgid "Edit Stock Item" msgid "Edit Stock Item Attachment" msgstr "Lagerobjekt bearbeiten" -#: stock/views.py:226 +#: stock/views.py:227 #, fuzzy #| msgid "Delete Part Attachment" msgid "Delete Stock Item Attachment" msgstr "Teilanhang löschen" -#: stock/views.py:243 +#: stock/views.py:244 #, fuzzy #| msgid "Item assigned to customer?" msgid "Assign to Customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: stock/views.py:281 +#: stock/views.py:254 +msgid "Customer must be specified" +msgstr "" + +#: stock/views.py:278 #, fuzzy #| msgid "Part Stock" msgid "Return to Stock" msgstr "Teilbestand" -#: stock/views.py:301 +#: stock/views.py:288 #, fuzzy #| msgid "Include sublocations" msgid "Specify a valid location" msgstr "Unterlagerorte einschließen" -#: stock/views.py:305 +#: stock/views.py:299 msgid "Stock item returned from customer" msgstr "" -#: stock/views.py:317 +#: stock/views.py:309 #, fuzzy #| msgid "Select valid part" msgid "Select Label Template" msgstr "Bitte ein gültiges Teil auswählen" -#: stock/views.py:340 +#: stock/views.py:332 #, fuzzy #| msgid "Select valid part" msgid "Select valid label" msgstr "Bitte ein gültiges Teil auswählen" -#: stock/views.py:404 +#: stock/views.py:396 #, fuzzy #| msgid "Delete Template" msgid "Delete All Test Data" msgstr "Vorlage löschen" -#: stock/views.py:420 +#: stock/views.py:412 #, fuzzy #| msgid "Confirm Part Deletion" msgid "Confirm test data deletion" msgstr "Löschen des Teils bestätigen" -#: stock/views.py:440 +#: stock/views.py:432 msgid "Add Test Result" msgstr "" -#: stock/views.py:478 +#: stock/views.py:473 #, fuzzy #| msgid "Edit Template" msgid "Edit Test Result" msgstr "Vorlage bearbeiten" -#: stock/views.py:496 +#: stock/views.py:491 #, fuzzy #| msgid "Delete Template" msgid "Delete Test Result" msgstr "Vorlage löschen" -#: stock/views.py:508 +#: stock/views.py:503 #, fuzzy #| msgid "Delete Template" msgid "Select Test Report Template" msgstr "Vorlage löschen" -#: stock/views.py:523 +#: stock/views.py:518 #, fuzzy #| msgid "Select valid part" msgid "Select valid template" msgstr "Bitte ein gültiges Teil auswählen" -#: stock/views.py:576 +#: stock/views.py:571 msgid "Stock Export Options" msgstr "Lagerbestandsexportoptionen" -#: stock/views.py:698 +#: stock/views.py:693 msgid "Stock Item QR Code" msgstr "Lagerobjekt-QR-Code" -#: stock/views.py:724 +#: stock/views.py:719 #, fuzzy #| msgid "Installed in Stock Item" msgid "Install Stock Item" msgstr "In Lagerobjekt installiert" -#: stock/views.py:824 +#: stock/views.py:819 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstall Stock Items" msgstr "In Lagerobjekt installiert" -#: stock/views.py:932 +#: stock/views.py:927 #, fuzzy #| msgid "Installed in Stock Item" msgid "Uninstalled stock items" msgstr "In Lagerobjekt installiert" -#: stock/views.py:957 +#: stock/views.py:952 msgid "Adjust Stock" msgstr "Lagerbestand anpassen" -#: stock/views.py:1067 +#: stock/views.py:1062 msgid "Move Stock Items" msgstr "Lagerobjekte bewegen" -#: stock/views.py:1068 +#: stock/views.py:1063 msgid "Count Stock Items" msgstr "Lagerobjekte zählen" -#: stock/views.py:1069 +#: stock/views.py:1064 msgid "Remove From Stock" msgstr "Aus Lagerbestand entfernen" -#: stock/views.py:1070 +#: stock/views.py:1065 msgid "Add Stock Items" msgstr "Lagerobjekte hinzufügen" -#: stock/views.py:1071 +#: stock/views.py:1066 msgid "Delete Stock Items" msgstr "Lagerobjekte löschen" -#: stock/views.py:1099 +#: stock/views.py:1094 msgid "Must enter integer value" msgstr "Nur Ganzzahl eingeben" -#: stock/views.py:1104 +#: stock/views.py:1099 msgid "Quantity must be positive" msgstr "Anzahl muss positiv sein" -#: stock/views.py:1111 +#: stock/views.py:1106 #, python-brace-format msgid "Quantity must not exceed {x}" msgstr "Anzahl darf {x} nicht überschreiten" -#: stock/views.py:1190 +#: stock/views.py:1185 #, python-brace-format msgid "Added stock to {n} items" msgstr "Vorrat zu {n} Lagerobjekten hinzugefügt" -#: stock/views.py:1205 +#: stock/views.py:1200 #, python-brace-format msgid "Removed stock from {n} items" msgstr "Vorrat von {n} Lagerobjekten entfernt" -#: stock/views.py:1218 +#: stock/views.py:1213 #, python-brace-format msgid "Counted stock for {n} items" msgstr "Bestand für {n} Objekte erfasst" -#: stock/views.py:1246 +#: stock/views.py:1241 msgid "No items were moved" msgstr "Keine Lagerobjekte wurden bewegt" -#: stock/views.py:1249 +#: stock/views.py:1244 #, python-brace-format msgid "Moved {n} items to {dest}" msgstr "{n} Teile nach {dest} bewegt" -#: stock/views.py:1268 +#: stock/views.py:1263 #, python-brace-format msgid "Deleted {n} stock items" msgstr "{n} Teile im Lager gelöscht" -#: stock/views.py:1280 +#: stock/views.py:1275 msgid "Edit Stock Item" msgstr "Lagerobjekt bearbeiten" -#: stock/views.py:1365 +#: stock/views.py:1360 msgid "Serialize Stock" msgstr "Lagerbestand erfassen" -#: stock/views.py:1559 +#: stock/views.py:1454 templates/js/build.js:210 +msgid "Create new Stock Item" +msgstr "Neues Lagerobjekt hinzufügen" + +#: stock/views.py:1553 #, fuzzy #| msgid "Count stock items" msgid "Duplicate Stock Item" msgstr "Lagerobjekte zählen" -#: stock/views.py:1625 +#: stock/views.py:1619 msgid "Invalid quantity" msgstr "Ungültige Menge" -#: stock/views.py:1628 +#: stock/views.py:1622 #, 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:1632 +#: stock/views.py:1626 msgid "Invalid part selection" msgstr "Ungültige Teileauswahl" -#: stock/views.py:1681 +#: stock/views.py:1674 #, python-brace-format msgid "Created {n} new stock items" msgstr "{n} neue Lagerobjekte erstellt" -#: stock/views.py:1700 stock/views.py:1716 +#: stock/views.py:1693 stock/views.py:1709 msgid "Created new stock item" msgstr "Neues Lagerobjekt erstellt" -#: stock/views.py:1735 +#: stock/views.py:1728 msgid "Delete Stock Location" msgstr "Standort löschen" -#: stock/views.py:1749 +#: stock/views.py:1742 msgid "Delete Stock Item" msgstr "Lagerobjekt löschen" -#: stock/views.py:1761 +#: stock/views.py:1754 msgid "Delete Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag löschen" -#: stock/views.py:1780 +#: stock/views.py:1773 msgid "Edit Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag bearbeiten" -#: stock/views.py:1790 +#: stock/views.py:1783 msgid "Add Stock Tracking Entry" msgstr "Lagerbestands-Tracking-Eintrag hinzufügen" @@ -4448,13 +4923,13 @@ msgstr "Keine Ergebnisse gefunden" msgid "Enter a search query" msgstr "Auftrag stornieren" -#: templates/InvenTree/search.html:191 templates/js/stock.html:528 +#: templates/InvenTree/search.html:191 templates/js/stock.js:289 #, fuzzy #| msgid "Item assigned to customer?" msgid "Shipped to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: templates/InvenTree/search.html:194 templates/js/stock.html:538 +#: templates/InvenTree/search.html:194 templates/js/stock.js:299 msgid "No stock location set" msgstr "Kein Lagerort gesetzt" @@ -4482,7 +4957,7 @@ msgstr "Einstellungen" msgid "Currencies" msgstr "Währungs-Wert" -#: templates/InvenTree/settings/currency.html:17 +#: templates/InvenTree/settings/currency.html:18 #, fuzzy #| msgid "Delete Currency" msgid "New Currency" @@ -4506,15 +4981,15 @@ msgstr "Einstellungen" msgid "Part Parameter Templates" msgstr "Teilparametervorlage bearbeiten" -#: templates/InvenTree/settings/part.html:43 +#: templates/InvenTree/settings/part.html:45 msgid "No part parameter templates found" msgstr "Keine Teilparametervorlagen gefunden" -#: templates/InvenTree/settings/part.html:63 +#: templates/InvenTree/settings/part.html:65 msgid "Edit Template" msgstr "Vorlage bearbeiten" -#: templates/InvenTree/settings/part.html:64 +#: templates/InvenTree/settings/part.html:66 msgid "Delete Template" msgstr "Vorlage löschen" @@ -4608,25 +5083,31 @@ msgstr "" msgid "User Information" msgstr "Keine Benutzerinformation" -#: templates/InvenTree/settings/user.html:24 +#: templates/InvenTree/settings/user.html:21 +#, fuzzy +#| msgid "Create new part" +msgid "Change Password" +msgstr "Neues Teil anlegen" + +#: templates/InvenTree/settings/user.html:28 #, fuzzy #| msgid "User" msgid "Username" msgstr "Benutzer" -#: templates/InvenTree/settings/user.html:28 +#: templates/InvenTree/settings/user.html:32 #, fuzzy #| msgid "Instance Name" msgid "First Name" msgstr "Instanzname" -#: templates/InvenTree/settings/user.html:32 +#: templates/InvenTree/settings/user.html:36 #, fuzzy #| msgid "Instance Name" msgid "Last Name" msgstr "Instanzname" -#: templates/InvenTree/settings/user.html:36 +#: templates/InvenTree/settings/user.html:40 #, fuzzy #| msgid "Address" msgid "Email Address" @@ -4678,565 +5159,681 @@ msgstr "Code auf GitHub ansehen" msgid "Submit Bug Report" msgstr "Fehlerbericht senden" -#: templates/attachment_table.html:6 +#: templates/attachment_table.html:7 msgid "Add Attachment" msgstr "Anhang hinzufügen" -#: templates/attachment_table.html:15 +#: templates/attachment_table.html:17 msgid "File" msgstr "Datei" -#: templates/attachment_table.html:16 +#: templates/attachment_table.html:18 msgid "Comment" msgstr "Kommentar" -#: templates/attachment_table.html:17 +#: templates/attachment_table.html:19 msgid "Uploaded" msgstr "" -#: templates/attachment_table.html:35 +#: templates/attachment_table.html:37 msgid "Delete attachment" msgstr "Anhang löschen" -#: templates/js/barcode.html:8 +#: templates/js/barcode.js:8 #, fuzzy #| msgid "No barcode data provided" msgid "Scan barcode data here using wedge scanner" msgstr "Keine Strichcodedaten bereitgestellt" -#: templates/js/barcode.html:12 +#: templates/js/barcode.js:12 #, fuzzy #| msgid "Source Location" msgid "Barcode" msgstr "Quell-Standort" -#: templates/js/barcode.html:20 +#: templates/js/barcode.js:20 #, fuzzy #| msgid "No barcode data provided" msgid "Enter barcode data" msgstr "Keine Strichcodedaten bereitgestellt" -#: templates/js/barcode.html:42 +#: templates/js/barcode.js:42 msgid "Invalid server response" msgstr "" -#: templates/js/barcode.html:143 +#: templates/js/barcode.js:143 #, fuzzy #| msgid "No barcode data provided" msgid "Scan barcode data below" msgstr "Keine Strichcodedaten bereitgestellt" -#: templates/js/barcode.html:217 templates/js/barcode.html:263 +#: templates/js/barcode.js:217 templates/js/barcode.js:263 #, fuzzy #| msgid "Unknown barcode format" msgid "Unknown response from server" msgstr "Unbekanntes Strichcode-Format" -#: templates/js/barcode.html:239 +#: templates/js/barcode.js:239 #, fuzzy #| msgid "Parent Stock Item" msgid "Link Barcode to Stock Item" msgstr "Eltern-Lagerobjekt" -#: templates/js/barcode.html:285 +#: templates/js/barcode.js:285 msgid "" "This will remove the association between this stock item and the barcode" msgstr "" -#: templates/js/barcode.html:291 +#: templates/js/barcode.js:291 msgid "Unlink" msgstr "" -#: templates/js/barcode.html:350 +#: templates/js/barcode.js:350 #, fuzzy #| msgid "Remove stock" msgid "Remove stock item" msgstr "Bestand entfernen" -#: templates/js/barcode.html:397 +#: templates/js/barcode.js:397 #, fuzzy #| msgid "Entry notes" msgid "Enter notes" msgstr "Eintrags-Notizen" -#: templates/js/barcode.html:399 +#: templates/js/barcode.js:399 msgid "Enter optional notes for stock transfer" msgstr "" -#: templates/js/barcode.html:404 +#: templates/js/barcode.js:404 #, fuzzy #| msgid "Include stock items in sub locations" msgid "Check Stock Items into Location" msgstr "Lagerobjekte in untergeordneten Lagerorten einschließen" -#: templates/js/barcode.html:408 +#: templates/js/barcode.js:408 msgid "Check In" msgstr "" -#: templates/js/barcode.html:466 +#: templates/js/barcode.js:466 msgid "Server error" msgstr "" -#: templates/js/barcode.html:485 +#: templates/js/barcode.js:485 #, fuzzy #| msgid "Stock Item Details" msgid "Stock Item already scanned" msgstr "Lagerbestands-Details" -#: templates/js/barcode.html:489 +#: templates/js/barcode.js:489 #, fuzzy #| msgid "Include stock items in sub locations" msgid "Stock Item already in this location" msgstr "Lagerobjekte in untergeordneten Lagerorten einschließen" -#: templates/js/barcode.html:496 +#: templates/js/barcode.js:496 #, fuzzy #| msgid "Added stock to {n} items" msgid "Added stock item" msgstr "Vorrat zu {n} Lagerobjekten hinzugefügt" -#: templates/js/barcode.html:503 +#: templates/js/barcode.js:503 #, fuzzy #| msgid "Create new Stock Item" msgid "Barcode does not match Stock Item" msgstr "Neues Lagerobjekt hinzufügen" -#: templates/js/bom.html:132 +#: templates/js/bom.js:159 msgid "Open subassembly" msgstr "Unterbaugruppe öffnen" -#: templates/js/bom.html:173 +#: templates/js/bom.js:200 #, fuzzy #| msgid "Options" msgid "Optional" msgstr "Optionen" -#: templates/js/bom.html:188 templates/js/build.html:133 -msgid "Available" -msgstr "verfügbar" - -#: templates/js/bom.html:213 +#: templates/js/bom.js:240 msgid "No pricing available" msgstr "Keine Preisinformation verfügbar" -#: templates/js/bom.html:232 +#: templates/js/bom.js:259 templates/js/build.js:555 #, fuzzy #| msgid "Options" msgid "Actions" msgstr "Optionen" -#: templates/js/bom.html:240 +#: templates/js/bom.js:267 msgid "Validate BOM Item" msgstr "BOM-Position validieren" -#: templates/js/bom.html:242 +#: templates/js/bom.js:269 msgid "This line has been validated" msgstr "Diese Position wurde validiert" -#: templates/js/bom.html:244 +#: templates/js/bom.js:271 msgid "Edit BOM Item" msgstr "BOM-Position bearbeiten" -#: templates/js/bom.html:246 +#: templates/js/bom.js:273 msgid "Delete BOM Item" msgstr "BOM-Position löschen" -#: templates/js/build.html:24 +#: templates/js/bom.js:346 templates/js/build.js:289 +msgid "No BOM items found" +msgstr "Keine BOM-Einträge gefunden" + +#: templates/js/bom.js:491 +msgid "INACTIVE" +msgstr "INAKTIV" + +#: templates/js/bom.js:505 +msgid "Uses" +msgstr "" + +#: templates/js/bom.js:516 +#, fuzzy +#| msgid "No matching action found" +msgid "No matching parts found" +msgstr "Keine passende Aktion gefunden" + +#: templates/js/build.js:56 +#, fuzzy +#| msgid "Installed in Stock Item" +msgid "Auto-allocate stock items to this output" +msgstr "In Lagerobjekt installiert" + +#: templates/js/build.js:62 +#, fuzzy +#| msgid "Complete Build" +msgid "Complete build output" +msgstr "Bau fertigstellen" + +#: templates/js/build.js:71 +#, fuzzy +#| msgid "Allocate Stock to Build" +msgid "Unallocate stock from build output" +msgstr "Lagerbestand dem Bau zuweisen" + +#: templates/js/build.js:77 +#, fuzzy +#| msgid "Delete Build" +msgid "Delete build output" +msgstr "Bau entfernt" + +#: templates/js/build.js:209 templates/stock_table.html:13 +msgid "New Stock Item" +msgstr "Neues Lagerobjekt" + +#: templates/js/build.js:477 +#, fuzzy +#| msgid "Required" +msgid "Required Part" +msgstr "benötigt" + +#: templates/js/build.js:498 +#, fuzzy +#| msgid "Quantity" +msgid "Quantity Per" +msgstr "Anzahl" + +#: templates/js/build.js:562 +#, fuzzy +#| msgid "Builds" +msgid "Build stock" +msgstr "Baue" + +#: templates/js/build.js:566 templates/stock_table.html:25 +msgid "Order stock" +msgstr "Bestand bestellen" + +#: templates/js/build.js:569 +msgid "Allocate stock" +msgstr "Lagerbestand zuweisen" + +#: templates/js/build.js:610 msgid "No builds matching query" msgstr "Keine Baue passen zur Anfrage" -#: templates/js/build.html:122 +#: templates/js/build.js:720 msgid "No parts allocated for" msgstr "Keine Teile zugeordnet zu" -#: templates/js/company.html:75 +#: templates/js/company.js:75 #, fuzzy #| msgid "Suppliers" msgid "Parts Supplied" msgstr "Zulieferer" -#: templates/js/company.html:84 +#: templates/js/company.js:84 #, fuzzy #| msgid "Manufacturer" msgid "Parts Manufactured" msgstr "Hersteller" -#: templates/js/company.html:96 +#: templates/js/company.js:96 msgid "No company information found" msgstr "Keine Firmeninformation gefunden" -#: templates/js/company.html:128 +#: templates/js/company.js:129 msgid "No supplier parts found" msgstr "Keine Zuliefererteile gefunden" -#: templates/js/company.html:145 templates/js/part.html:314 +#: templates/js/company.js:147 templates/js/part.js:79 templates/js/part.js:164 msgid "Template part" msgstr "Vorlagenteil" -#: templates/js/company.html:149 templates/js/part.html:318 +#: templates/js/company.js:151 templates/js/part.js:83 templates/js/part.js:168 msgid "Assembled part" msgstr "Baugruppe" -#: templates/js/company.html:206 +#: templates/js/company.js:208 msgid "Link" msgstr "Link" -#: templates/js/order.html:128 +#: templates/js/order.js:128 msgid "No purchase orders found" msgstr "Keine Bestellungen gefunden" -#: templates/js/order.html:180 templates/js/stock.html:643 +#: templates/js/order.js:180 templates/js/stock.js:677 msgid "Date" msgstr "Datum" -#: templates/js/order.html:210 +#: templates/js/order.js:210 msgid "No sales orders found" msgstr "Keine Aufträge gefunden" -#: templates/js/order.html:267 +#: templates/js/order.js:267 msgid "Shipment Date" msgstr "Versanddatum" -#: templates/js/part.html:137 +#: templates/js/part.js:71 templates/js/part.js:156 +#, fuzzy +#| msgid "Trackable" +msgid "Trackable part" +msgstr "nachverfolgbar" + +#: templates/js/part.js:75 templates/js/part.js:160 +#, fuzzy +#| msgid "Virtual" +msgid "Virtual part" +msgstr "Virtuell" + +#: templates/js/part.js:87 +msgid "Starred part" +msgstr "Favoritenteil" + +#: templates/js/part.js:91 +msgid "Salable part" +msgstr "Verkäufliches Teil" + +#: templates/js/part.js:205 #, fuzzy #| msgid "No parts found" msgid "No variants found" msgstr "Keine Teile gefunden" -#: templates/js/part.html:223 templates/js/part.html:411 +#: templates/js/part.js:291 templates/js/part.js:457 msgid "No parts found" msgstr "Keine Teile gefunden" -#: templates/js/part.html:275 templates/js/stock.html:409 -#: templates/js/stock.html:966 +#: templates/js/part.js:343 templates/js/stock.js:456 +#: templates/js/stock.js:1013 msgid "Select" msgstr "Auswählen" -#: templates/js/part.html:322 -msgid "Starred part" -msgstr "Favoritenteil" - -#: templates/js/part.html:326 -msgid "Salable part" -msgstr "Verkäufliches Teil" - -#: templates/js/part.html:365 +#: templates/js/part.js:411 msgid "No category" msgstr "Keine Kategorie" -#: templates/js/part.html:383 templates/js/table_filters.html:196 +#: templates/js/part.js:429 templates/js/table_filters.js:256 msgid "Low stock" msgstr "Bestand niedrig" -#: templates/js/part.html:392 +#: templates/js/part.js:438 msgid "Building" msgstr "Im Bau" -#: templates/js/part.html:471 +#: templates/js/part.js:517 msgid "YES" msgstr "" -#: templates/js/part.html:473 +#: templates/js/part.js:519 msgid "NO" msgstr "" -#: templates/js/part.html:507 +#: templates/js/part.js:553 #, fuzzy #| msgid "No stock items matching query" msgid "No test templates matching query" msgstr "Keine zur Anfrage passenden Lagerobjekte" -#: templates/js/part.html:558 templates/js/stock.html:63 +#: templates/js/part.js:604 templates/js/stock.js:63 #, fuzzy #| msgid "Edit Sales Order" msgid "Edit test result" msgstr "Auftrag bearbeiten" -#: templates/js/part.html:559 templates/js/stock.html:64 +#: templates/js/part.js:605 templates/js/stock.js:64 #, fuzzy #| msgid "Delete attachment" msgid "Delete test result" msgstr "Anhang löschen" -#: templates/js/part.html:565 +#: templates/js/part.js:611 msgid "This test is defined for a parent part" msgstr "" -#: templates/js/stock.html:26 +#: templates/js/stock.js:26 msgid "PASS" msgstr "" -#: templates/js/stock.html:28 +#: templates/js/stock.js:28 msgid "FAIL" msgstr "" -#: templates/js/stock.html:33 +#: templates/js/stock.js:33 msgid "NO RESULT" msgstr "" -#: templates/js/stock.html:59 +#: templates/js/stock.js:59 #, fuzzy #| msgid "Edit Sales Order" msgid "Add test result" msgstr "Auftrag bearbeiten" -#: templates/js/stock.html:78 +#: templates/js/stock.js:78 #, fuzzy #| msgid "No results found" msgid "No test results found" msgstr "Keine Ergebnisse gefunden" -#: templates/js/stock.html:120 +#: templates/js/stock.js:120 #, fuzzy #| msgid "Shipment Date" msgid "Test Date" msgstr "Versanddatum" -#: templates/js/stock.html:263 +#: templates/js/stock.js:281 +msgid "In production" +msgstr "" + +#: templates/js/stock.js:285 +#, fuzzy +#| msgid "Installed in Stock Item" +msgid "Installed in Stock Item" +msgstr "In Lagerobjekt installiert" + +#: templates/js/stock.js:293 +#, fuzzy +#| msgid "Item assigned to customer?" +msgid "Assigned to Sales Order" +msgstr "Ist dieses Objekt einem Kunden zugeteilt?" + +#: templates/js/stock.js:313 msgid "No stock items matching query" msgstr "Keine zur Anfrage passenden Lagerobjekte" -#: templates/js/stock.html:361 templates/js/stock.html:376 +#: templates/js/stock.js:424 #, fuzzy #| msgid "Include sublocations" msgid "Undefined location" msgstr "Unterlagerorte einschließen" -#: templates/js/stock.html:469 +#: templates/js/stock.js:518 +#, fuzzy +#| msgid "StockItem is lost" +msgid "Stock item is in production" +msgstr "Lagerobjekt verloren" + +#: templates/js/stock.js:523 +#, fuzzy +#| msgid "This stock item is allocated to Sales Order" +msgid "Stock item assigned to sales order" +msgstr "Dieses Lagerobjekt ist dem Auftrag zugewiesen" + +#: templates/js/stock.js:526 +#, fuzzy +#| msgid "StockItem has been allocated" +msgid "Stock item assigned to customer" +msgstr "Lagerobjekt wurde zugewiesen" + +#: templates/js/stock.js:530 #, fuzzy #| msgid "StockItem has been allocated" msgid "Stock item has been allocated" msgstr "Lagerobjekt wurde zugewiesen" -#: templates/js/stock.html:473 -#, fuzzy -#| msgid "StockItem has been allocated" -msgid "Stock item has been assigned to customer" -msgstr "Lagerobjekt wurde zugewiesen" - -#: templates/js/stock.html:476 -#, fuzzy -#| msgid "This stock item is allocated to Sales Order" -msgid "Stock item was assigned to a build order" -msgstr "Dieses Lagerobjekt ist dem Auftrag zugewiesen" - -#: templates/js/stock.html:478 -#, fuzzy -#| msgid "This stock item is allocated to Sales Order" -msgid "Stock item was assigned to a sales order" -msgstr "Dieses Lagerobjekt ist dem Auftrag zugewiesen" - -#: templates/js/stock.html:483 +#: templates/js/stock.js:534 #, fuzzy #| msgid "Is this item installed in another item?" msgid "Stock item has been installed in another item" msgstr "Ist dieses Teil in einem anderen verbaut?" -#: templates/js/stock.html:490 +#: templates/js/stock.js:541 #, fuzzy #| msgid "StockItem has been allocated" msgid "Stock item has been rejected" msgstr "Lagerobjekt wurde zugewiesen" -#: templates/js/stock.html:494 +#: templates/js/stock.js:545 #, fuzzy #| msgid "StockItem is lost" msgid "Stock item is lost" msgstr "Lagerobjekt verloren" -#: templates/js/stock.html:498 templates/js/table_filters.html:60 +#: templates/js/stock.js:549 templates/js/table_filters.js:106 #, fuzzy #| msgid "Delete" msgid "Depleted" msgstr "Löschen" -#: templates/js/stock.html:523 -#, fuzzy -#| msgid "Installed in Stock Item" -msgid "Installed in Stock Item " -msgstr "In Lagerobjekt installiert" - -#: templates/js/stock.html:531 -#, fuzzy -#| msgid "Item assigned to customer?" -msgid "Assigned to sales order" -msgstr "Ist dieses Objekt einem Kunden zugeteilt?" - -#: templates/js/stock.html:709 +#: templates/js/stock.js:743 msgid "No user information" msgstr "Keine Benutzerinformation" -#: templates/js/stock.html:793 -msgid "Create New Part" -msgstr "Neues Teil anlegen" - -#: templates/js/stock.html:805 +#: templates/js/stock.js:852 msgid "Create New Location" msgstr "Neuen Standort anlegen" -#: templates/js/stock.html:904 +#: templates/js/stock.js:951 #, fuzzy #| msgid "Serial Number" msgid "Serial" msgstr "Seriennummer" -#: templates/js/stock.html:997 templates/js/table_filters.html:70 +#: templates/js/stock.js:1044 templates/js/table_filters.js:121 #, fuzzy #| msgid "Installed In" msgid "Installed" msgstr "Installiert in" -#: templates/js/stock.html:1022 +#: templates/js/stock.js:1069 #, fuzzy #| msgid "Installed In" msgid "Install item" msgstr "Installiert in" -#: templates/js/table_filters.html:19 templates/js/table_filters.html:80 +#: templates/js/table_filters.js:41 +#, fuzzy +#| msgid "Trackable" +msgid "Trackable Part" +msgstr "nachverfolgbar" + +#: templates/js/table_filters.js:45 +#, fuzzy +#| msgid "Validate BOM" +msgid "Validated" +msgstr "BOM validieren" + +#: templates/js/table_filters.js:65 templates/js/table_filters.js:131 #, fuzzy #| msgid "Serialize Stock" msgid "Is Serialized" msgstr "Lagerbestand erfassen" -#: templates/js/table_filters.html:22 templates/js/table_filters.html:87 +#: templates/js/table_filters.js:68 templates/js/table_filters.js:138 #, fuzzy #| msgid "Serial Number" msgid "Serial number GTE" msgstr "Seriennummer" -#: templates/js/table_filters.html:23 templates/js/table_filters.html:88 +#: templates/js/table_filters.js:69 templates/js/table_filters.js:139 #, fuzzy #| msgid "Serial number for this item" msgid "Serial number greater than or equal to" msgstr "Seriennummer für dieses Teil" -#: templates/js/table_filters.html:26 templates/js/table_filters.html:91 +#: templates/js/table_filters.js:72 templates/js/table_filters.js:142 #, fuzzy #| msgid "Serial Number" msgid "Serial number LTE" msgstr "Seriennummer" -#: templates/js/table_filters.html:27 templates/js/table_filters.html:92 +#: templates/js/table_filters.js:73 templates/js/table_filters.js:143 #, fuzzy #| msgid "Serial numbers already exist: " msgid "Serial number less than or equal to" msgstr "Seriennummern existieren bereits:" -#: templates/js/table_filters.html:30 templates/js/table_filters.html:31 -#: templates/js/table_filters.html:83 templates/js/table_filters.html:84 +#: templates/js/table_filters.js:76 templates/js/table_filters.js:77 +#: templates/js/table_filters.js:134 templates/js/table_filters.js:135 #, fuzzy #| msgid "Serial Number" msgid "Serial number" msgstr "Seriennummer" -#: templates/js/table_filters.html:35 templates/js/table_filters.html:101 +#: templates/js/table_filters.js:81 templates/js/table_filters.js:152 #, fuzzy #| msgid "Batch Code" msgid "Batch code" msgstr "Losnummer" -#: templates/js/table_filters.html:45 +#: templates/js/table_filters.js:91 templates/js/table_filters.js:223 msgid "Active parts" msgstr "Aktive Teile" -#: templates/js/table_filters.html:46 +#: templates/js/table_filters.js:92 msgid "Show stock for active parts" msgstr "Bestand aktiver Teile anzeigen" -#: templates/js/table_filters.html:50 +#: templates/js/table_filters.js:96 msgid "Is allocated" msgstr "Ist zugeordnet" -#: templates/js/table_filters.html:51 +#: templates/js/table_filters.js:97 msgid "Item has been alloacted" msgstr "Position wurde zugeordnet" -#: templates/js/table_filters.html:55 +#: templates/js/table_filters.js:101 msgid "Include sublocations" msgstr "Unterlagerorte einschließen" -#: templates/js/table_filters.html:56 +#: templates/js/table_filters.js:102 msgid "Include stock in sublocations" msgstr "Bestand in Unterlagerorten einschließen" -#: templates/js/table_filters.html:61 +#: templates/js/table_filters.js:107 #, fuzzy #| msgid "Delete this Stock Item when stock is depleted" msgid "Show stock items which are depleted" msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" -#: templates/js/table_filters.html:66 +#: templates/js/table_filters.js:112 msgid "Show items which are in stock" msgstr "" -#: templates/js/table_filters.html:71 +#: templates/js/table_filters.js:116 +msgid "In Production" +msgstr "" + +#: templates/js/table_filters.js:117 +#, fuzzy +#| msgid "Delete this Stock Item when stock is depleted" +msgid "Show items which are in production" +msgstr "Objekt löschen wenn Lagerbestand aufgebraucht" + +#: templates/js/table_filters.js:122 #, fuzzy #| msgid "Is this item installed in another item?" msgid "Show stock items which are installed in another item" msgstr "Ist dieses Teil in einem anderen verbaut?" -#: templates/js/table_filters.html:75 +#: templates/js/table_filters.js:126 #, fuzzy #| msgid "Item assigned to customer?" msgid "Sent to customer" msgstr "Ist dieses Objekt einem Kunden zugeteilt?" -#: templates/js/table_filters.html:76 +#: templates/js/table_filters.js:127 msgid "Show items which have been assigned to a customer" msgstr "" -#: templates/js/table_filters.html:96 templates/js/table_filters.html:97 +#: templates/js/table_filters.js:147 templates/js/table_filters.js:148 msgid "Stock status" msgstr "Bestandsstatus" -#: templates/js/table_filters.html:130 +#: templates/js/table_filters.js:181 msgid "Build status" msgstr "Bau-Status" -#: templates/js/table_filters.html:145 templates/js/table_filters.html:158 +#: templates/js/table_filters.js:196 templates/js/table_filters.js:209 msgid "Order status" msgstr "Bestellstatus" -#: templates/js/table_filters.html:150 templates/js/table_filters.html:163 +#: templates/js/table_filters.js:201 templates/js/table_filters.js:214 #, fuzzy #| msgid "Cascading" msgid "Outstanding" msgstr "Kaskadierend" -#: templates/js/table_filters.html:173 +#: templates/js/table_filters.js:233 msgid "Include subcategories" msgstr "Unterkategorien einschließen" -#: templates/js/table_filters.html:174 +#: templates/js/table_filters.js:234 msgid "Include parts in subcategories" msgstr "Teile in Unterkategorien einschließen" -#: templates/js/table_filters.html:178 +#: templates/js/table_filters.js:238 msgid "Has IPN" msgstr "" -#: templates/js/table_filters.html:179 +#: templates/js/table_filters.js:239 #, fuzzy #| msgid "Internal Part Number" msgid "Part has internal part number" msgstr "Interne Teilenummer" -#: templates/js/table_filters.html:184 +#: templates/js/table_filters.js:244 msgid "Show active parts" msgstr "Aktive Teile anzeigen" -#: templates/js/table_filters.html:192 +#: templates/js/table_filters.js:252 msgid "Stock available" msgstr "Bestand verfügbar" -#: templates/js/table_filters.html:208 +#: templates/js/table_filters.js:268 msgid "Starred" msgstr "Favorit" -#: templates/js/table_filters.html:220 +#: templates/js/table_filters.js:280 msgid "Purchasable" msgstr "Käuflich" +#: templates/modals.html:13 templates/modals.html:35 +msgid "Form errors exist" +msgstr "" + +#: templates/modals.html:18 templates/modals.html:40 +msgid "Close" +msgstr "" + +#: templates/modals.html:19 templates/modals.html:41 +#, fuzzy +#| msgid "Edit BOM" +msgid "Submit" +msgstr "Stückliste bearbeiten" + #: templates/navbar.html:29 msgid "Buy" msgstr "Kaufen" @@ -5279,51 +5876,47 @@ msgstr "Suche" msgid "Export Stock Information" msgstr "Lagerobjekt-Standort bearbeiten" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 #, fuzzy #| msgid "Added stock to {n} items" msgid "Add to selected stock items" msgstr "Vorrat zu {n} Lagerobjekten hinzugefügt" -#: templates/stock_table.html:18 +#: templates/stock_table.html:22 #, fuzzy #| msgid "Remove selected BOM items" msgid "Remove from selected stock items" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: templates/stock_table.html:19 +#: templates/stock_table.html:23 #, fuzzy #| msgid "Delete Stock Item" msgid "Stocktake selected stock items" msgstr "Lagerobjekt löschen" -#: templates/stock_table.html:20 +#: templates/stock_table.html:24 #, fuzzy #| msgid "Delete Stock Item" msgid "Move selected stock items" msgstr "Lagerobjekt löschen" -#: templates/stock_table.html:20 +#: templates/stock_table.html:24 msgid "Move stock" msgstr "Bestand bewegen" -#: templates/stock_table.html:21 +#: templates/stock_table.html:25 #, fuzzy #| msgid "Remove selected BOM items" msgid "Order selected items" msgstr "Ausgewählte Stücklistenpositionen entfernen" -#: templates/stock_table.html:21 -msgid "Order stock" -msgstr "Bestand bestellen" - -#: templates/stock_table.html:24 +#: templates/stock_table.html:28 #, fuzzy #| msgid "Delete line item" msgid "Delete selected items" msgstr "Position löschen" -#: templates/stock_table.html:24 +#: templates/stock_table.html:28 msgid "Delete Stock" msgstr "Bestand löschen" @@ -5355,46 +5948,176 @@ msgstr "Revision" msgid "Important dates" msgstr "Stückliste importieren" -#: users/models.py:128 +#: users/models.py:129 msgid "Permission set" msgstr "" -#: users/models.py:136 +#: users/models.py:137 msgid "Group" msgstr "" -#: users/models.py:139 +#: users/models.py:140 msgid "View" msgstr "" -#: users/models.py:139 +#: users/models.py:140 msgid "Permission to view items" msgstr "" -#: users/models.py:141 +#: users/models.py:142 #, fuzzy #| msgid "Address" msgid "Add" msgstr "Adresse" -#: users/models.py:141 +#: users/models.py:142 msgid "Permission to add items" msgstr "" -#: users/models.py:143 +#: users/models.py:144 msgid "Change" msgstr "" -#: users/models.py:143 +#: users/models.py:144 msgid "Permissions to edit items" msgstr "" -#: users/models.py:145 +#: users/models.py:146 #, fuzzy #| msgid "Remove selected BOM items" msgid "Permission to delete items" msgstr "Ausgewählte Stücklistenpositionen entfernen" +#, fuzzy +#~| msgid "Serial Number" +#~ msgid "Serial Numbers" +#~ msgstr "Seriennummer" + +#~ msgid "Automatically allocate stock" +#~ msgstr "Lagerbestand automatisch zuweisen" + +#~ msgid "Auto Allocate" +#~ msgstr "Automatisches Zuweisen" + +#~ msgid "Unallocate" +#~ msgstr "Zuweisung aufheben" + +#~ msgid "Assigned" +#~ msgstr "Zugewiesen" + +#, fuzzy +#~| msgid "No stock items found that can be allocated to this build" +#~ msgid "" +#~ "Where the following conditions are met, stock will be automatically " +#~ "allocated to this build" +#~ msgstr "Keine Lagerobjekt gefunden, die diesem Bau zugewiesen werden können" + +#, fuzzy +#~| msgid "Part is not a virtual part" +#~ msgid "The part is not marked as trackable" +#~ msgstr "Teil ist nicht virtuell" + +#, fuzzy +#~| msgid "Installed in Stock Item" +#~ msgid "Only single stock items exists" +#~ msgstr "In Lagerobjekt installiert" + +#, fuzzy +#~| msgid "This stock item is allocated to Build" +#~ msgid "The stock item is not already allocated to this build" +#~ msgstr "Dieses Lagerobjekt ist dem Bau zugewiesen" + +#~ msgid "Warning: Build order allocation is not complete" +#~ msgstr "Warnung: Bau-Zuweisung ist unvollständig" + +#~ msgid "" +#~ "Build Order has not been fully allocated. Ensure that all Stock Items " +#~ "have been allocated to the Build" +#~ msgstr "" +#~ "Bau-Zuweisung ist unvollständig. Sicherstellen, dass alle Lagerobjekte " +#~ "dem Bau zugewiesen wurden" + +#~ msgid "The following actions will be performed:" +#~ msgstr "Die folgenden Aktionen werden ausgeführt:" + +#~ msgid "Remove allocated items from stock" +#~ msgstr "Zugewiesene Teile dem Lager entnehmen" + +#~ msgid "Add completed items to stock" +#~ msgstr "Komplettierte Teile dem Lager hinzufügen" + +#~ msgid "Enough Parts?" +#~ msgstr "Genügend Teile?" + +#~ msgid "Yes" +#~ msgstr "Ja" + +#~ msgid "No" +#~ msgstr "Nein" + +#~ msgid "No matching build found" +#~ msgstr "Kein passender Bau gefunden" + +#~ msgid "Check the confirmation box at the bottom of the list" +#~ msgstr "Bestätigunsbox am Ende der Liste bestätigen" + +#~ msgid "Invalid location selected" +#~ msgstr "Ungültige Ortsauswahl" + +#~ msgid "The following serial numbers already exist: ({sn})" +#~ msgstr "Die folgende Seriennummer existiert bereits: ({sn})" + +#~ msgid "Build marked as COMPLETE" +#~ msgstr "Bau als FERTIG markiert" + +#, fuzzy +#~| msgid "Available" +#~ msgid "Avaialabe" +#~ msgstr "verfügbar" + +#, fuzzy +#~| msgid "Overage must be an integer value or a percentage" +#~ msgid "Build quantity must be integer value for trackable parts" +#~ msgstr "Überschuss muss eine Ganzzahl oder ein Prozentwert sein" + +#~ msgid "Parent build to which this build is allocated" +#~ msgstr "Eltern-Bau, dem dieser Bau zugewiesen ist" + +#~ msgid "" +#~ "Stock Items are selected for automatic allocation if there is only a " +#~ "single stock item available." +#~ msgstr "" +#~ "Teile werden automatisch zugewiesen, wenn nur ein Lagerobjekt verfügbar " +#~ "ist" + +#~ msgid "Title" +#~ msgstr "Titel" + +#~ msgid "Allocate new Part" +#~ msgstr "Neues Teil zuordnen" + +#~ msgid "Could not cancel order" +#~ msgstr "Stornierung fehlgeschlagen" + +#~ msgid "Invalid Purchase Order" +#~ msgstr "Ungültige Bestellung" + +#~ msgid "Invalid SupplierPart selection" +#~ msgstr "Ungültige Wahl des Zulieferer-Teils" + +#, fuzzy +#~| msgid "This stock item is allocated to Sales Order" +#~ msgid "Stock item was assigned to a build order" +#~ msgstr "Dieses Lagerobjekt ist dem Auftrag zugewiesen" + +#, fuzzy +#~| msgid "Template part" +#~ msgid "Templat part" +#~ msgstr "Vorlagenteil" + +#~ msgid "Link to extenal URL" +#~ msgstr "Link zu einer Externen URL" + #, fuzzy #~| msgid "Reference" #~ msgid "Reference Prefix" @@ -5482,9 +6205,6 @@ msgstr "Ausgewählte Stücklistenpositionen entfernen" #~ msgid "StockItem does not exist" #~ msgstr "Lagerobjekt existiert nicht" -#~ msgid "No matching data" -#~ msgstr "Keine passenden Daten" - #, fuzzy #~| msgid "Add stock" #~ msgid "Add to stock" @@ -5508,9 +6228,6 @@ msgstr "Ausgewählte Stücklistenpositionen entfernen" #~ "Es ist kein echtes Teil, aber echte Teile können auf dieser Vorlage " #~ "basieren." -#~ msgid "Allocate Stock to Build" -#~ msgstr "Lagerbestand dem Bau zuweisen" - #~ msgid "Allocate" #~ msgstr "zuweisen" diff --git a/InvenTree/locale/en/LC_MESSAGES/django.po b/InvenTree/locale/en/LC_MESSAGES/django.po index a5f56143f0..aacb4b74ec 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-10-25 10:54+0000\n" +"POT-Creation-Date: 2020-11-03 10:02+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -26,7 +26,7 @@ msgstr "" msgid "No matching action found" msgstr "" -#: InvenTree/forms.py:102 build/forms.py:49 +#: InvenTree/forms.py:102 build/forms.py:82 build/forms.py:170 msgid "Confirm" msgstr "" @@ -46,7 +46,7 @@ msgstr "" msgid "Apply Theme" msgstr "" -#: InvenTree/helpers.py:361 order/models.py:187 order/models.py:261 +#: InvenTree/helpers.py:361 order/models.py:187 order/models.py:269 msgid "Invalid quantity provided" msgstr "" @@ -86,12 +86,12 @@ msgstr "" msgid "File comment" msgstr "" -#: InvenTree/models.py:68 templates/js/stock.html:700 +#: InvenTree/models.py:68 templates/js/stock.js:734 msgid "User" msgstr "" -#: InvenTree/models.py:106 part/templates/part/params.html:22 -#: templates/js/part.html:81 +#: InvenTree/models.py:106 part/templates/part/params.html:24 +#: templates/js/part.js:129 msgid "Name" msgstr "" @@ -116,7 +116,7 @@ msgid "Polish" msgstr "" #: InvenTree/status_codes.py:94 InvenTree/status_codes.py:135 -#: InvenTree/status_codes.py:222 templates/js/table_filters.html:135 +#: InvenTree/status_codes.py:222 msgid "Pending" msgstr "" @@ -168,10 +168,8 @@ msgstr "" msgid "Rejected" msgstr "" -#: InvenTree/status_codes.py:223 build/templates/build/allocate.html:358 -#: order/templates/order/sales_order_detail.html:223 -#: part/templates/part/tabs.html:23 templates/js/build.html:140 -msgid "Allocated" +#: InvenTree/status_codes.py:223 +msgid "Production" msgstr "" #: InvenTree/validators.py:39 @@ -204,7 +202,27 @@ msgstr "" msgid "Overage must be an integer value or a percentage" msgstr "" -#: InvenTree/views.py:707 +#: InvenTree/views.py:493 +msgid "Delete Item" +msgstr "" + +#: InvenTree/views.py:542 +msgid "Check box to confirm item deletion" +msgstr "" + +#: InvenTree/views.py:557 templates/InvenTree/settings/user.html:18 +msgid "Edit User Information" +msgstr "" + +#: InvenTree/views.py:568 templates/InvenTree/settings/user.html:22 +msgid "Set Password" +msgstr "" + +#: InvenTree/views.py:587 +msgid "Password fields must match" +msgstr "" + +#: InvenTree/views.py:757 msgid "Database Statistics" msgstr "" @@ -248,124 +266,190 @@ msgstr "" msgid "Barcode associated with StockItem" msgstr "" -#: build/forms.py:28 +#: build/forms.py:32 msgid "Build Order reference" msgstr "" -#: build/forms.py:70 -msgid "Location of completed parts" +#: 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 +#: order/templates/order/order_wizard/select_parts.html:32 +#: order/templates/order/purchase_order_detail.html:178 +#: 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 +#: stock/templates/stock/item_base.html:40 +#: stock/templates/stock/item_base.html:46 +#: stock/templates/stock/item_base.html:197 +#: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.js:338 +#: templates/js/bom.js:189 templates/js/build.js:404 templates/js/stock.js:725 +#: templates/js/stock.js:953 +msgid "Quantity" msgstr "" -#: build/forms.py:74 +#: build/forms.py:71 +msgid "Enter quantity for build output" +msgstr "" + +#: build/forms.py:75 stock/forms.py:111 msgid "Serial numbers" msgstr "" -#: build/forms.py:76 stock/forms.py:111 -msgid "Enter unique serial numbers (or leave blank)" +#: build/forms.py:77 +msgid "Enter serial numbers for build outputs" msgstr "" -#: build/forms.py:79 +#: build/forms.py:83 +msgid "Confirm creation of build outut" +msgstr "" + +#: build/forms.py:103 +msgid "Confirm deletion of build output" +msgstr "" + +#: build/forms.py:124 +msgid "Confirm unallocation of stock" +msgstr "" + +#: build/forms.py:148 +msgid "Confirm stock allocation" +msgstr "" + +#: build/forms.py:171 +msgid "Mark build as complete" +msgstr "" + +#: build/forms.py:195 +msgid "Location of completed parts" +msgstr "" + +#: build/forms.py:200 +msgid "Confirm completion with incomplete stock allocation" +msgstr "" + +#: build/forms.py:203 msgid "Confirm build completion" msgstr "" -#: build/models.py:54 build/templates/build/build_base.html:8 +#: build/forms.py:223 build/views.py:68 +msgid "Confirm build cancellation" +msgstr "" + +#: build/forms.py:237 +msgid "Select quantity of stock to allocate" +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:214 +#: stock/templates/stock/item_base.html:227 msgid "Build Order" msgstr "" -#: build/models.py:55 build/templates/build/index.html:6 +#: build/models.py:57 build/templates/build/index.html:6 #: build/templates/build/index.html:14 order/templates/order/so_builds.html:11 #: order/templates/order/so_tabs.html:9 part/templates/part/tabs.html:31 #: templates/InvenTree/settings/tabs.html:28 users/models.py:30 msgid "Build Orders" msgstr "" -#: build/models.py:77 -msgid "Build quantity must be integer value for trackable parts" -msgstr "" - -#: build/models.py:86 build/templates/build/build_base.html:73 +#: build/models.py:72 msgid "Build Order Reference" msgstr "" -#: build/models.py:87 build/templates/build/allocate.html:342 -#: order/templates/order/purchase_order_detail.html:172 -#: templates/js/bom.html:154 +#: build/models.py:73 order/templates/order/purchase_order_detail.html:173 +#: templates/js/bom.js:181 templates/js/build.js:493 msgid "Reference" msgstr "" -#: build/models.py:94 build/templates/build/allocate.html:337 +#: build/models.py:80 build/templates/build/detail.html:19 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 -#: order/templates/order/purchase_order_detail.html:159 +#: order/templates/order/purchase_order_detail.html:160 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 -#: templates/InvenTree/search.html:147 templates/js/bom.html:147 -#: templates/js/build.html:56 templates/js/company.html:56 -#: templates/js/order.html:167 templates/js/order.html:249 -#: templates/js/part.html:120 templates/js/part.html:203 -#: templates/js/part.html:345 templates/js/part.html:526 -#: templates/js/stock.html:445 templates/js/stock.html:672 +#: 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/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" msgstr "" -#: build/models.py:97 +#: build/models.py:83 msgid "Brief description of the build" msgstr "" -#: build/models.py:105 build/templates/build/build_base.html:94 +#: build/models.py:91 build/templates/build/build_base.html:94 +#: build/templates/build/detail.html:75 msgid "Parent Build" msgstr "" -#: build/models.py:106 -msgid "Parent build to which this build is allocated" +#: build/models.py:92 +msgid "BuildOrder to which this build is allocated" msgstr "" -#: build/models.py:111 build/templates/build/allocate.html:329 -#: build/templates/build/auto_allocate.html:19 -#: build/templates/build/build_base.html:78 -#: build/templates/build/detail.html:22 order/models.py:501 +#: 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 #: order/templates/order/order_wizard/select_parts.html:30 -#: order/templates/order/purchase_order_detail.html:147 +#: order/templates/order/purchase_order_detail.html:148 #: order/templates/order/receive_parts.html:19 part/models.py:293 #: part/templates/part/part_app_base.html:7 #: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 -#: templates/js/barcode.html:336 templates/js/bom.html:124 -#: templates/js/build.html:61 templates/js/company.html:137 -#: templates/js/part.html:184 templates/js/part.html:289 -#: templates/js/stock.html:421 templates/js/stock.html:978 +#: templates/js/barcode.js:336 templates/js/bom.js:147 templates/js/bom.js:484 +#: templates/js/build.js:647 templates/js/company.js:138 +#: templates/js/part.js:252 templates/js/part.js:357 templates/js/stock.js:468 +#: templates/js/stock.js:1025 msgid "Part" msgstr "" -#: build/models.py:120 +#: build/models.py:105 msgid "Select part to build" msgstr "" -#: build/models.py:125 +#: build/models.py:110 msgid "Sales Order Reference" msgstr "" -#: build/models.py:129 +#: build/models.py:114 msgid "SalesOrder to which this build is allocated" msgstr "" -#: build/models.py:134 +#: build/models.py:119 msgid "Source Location" msgstr "" -#: build/models.py:138 +#: build/models.py:123 msgid "" "Select location to take stock from for this build (leave blank to take from " "any stock location)" msgstr "" -#: build/models.py:142 +#: build/models.py:128 +msgid "Destination Location" +msgstr "" + +#: build/models.py:132 +msgid "Select location where the completed items will be stored" +msgstr "" + +#: build/models.py:136 msgid "Build Quantity" msgstr "" +#: build/models.py:139 +msgid "Number of stock items to build" +msgstr "" + +#: build/models.py:143 +msgid "Completed items" +msgstr "" + #: build/models.py:145 -msgid "Number of parts to build" +msgid "Number of stock items which have been completed" msgstr "" #: build/models.py:149 part/templates/part/part_base.html:155 @@ -376,7 +460,7 @@ msgstr "" msgid "Build status code" msgstr "" -#: build/models.py:157 stock/models.py:387 +#: build/models.py:157 stock/models.py:389 msgid "Batch Code" msgstr "" @@ -384,26 +468,26 @@ msgstr "" msgid "Batch code for this build output" msgstr "" -#: build/models.py:176 build/templates/build/detail.html:55 +#: build/models.py:176 build/templates/build/detail.html:89 #: 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:381 stock/templates/stock/item_base.html:266 +#: stock/models.py:383 stock/templates/stock/item_base.html:279 msgid "External Link" msgstr "" -#: build/models.py:177 stock/models.py:383 +#: build/models.py:177 part/models.py:596 stock/models.py:385 msgid "Link to external URL" msgstr "" #: build/models.py:181 build/templates/build/tabs.html:14 company/models.py:314 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 -#: order/templates/order/purchase_order_detail.html:202 +#: order/templates/order/purchase_order_detail.html:203 #: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:70 -#: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 -#: stock/models.py:1404 stock/templates/stock/tabs.html:26 -#: templates/js/barcode.html:391 templates/js/bom.html:223 -#: templates/js/stock.html:116 templates/js/stock.html:544 +#: 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 +#: templates/js/barcode.js:391 templates/js/bom.js:250 +#: templates/js/stock.js:116 templates/js/stock.js:578 msgid "Notes" msgstr "" @@ -411,138 +495,118 @@ msgstr "" msgid "Extra build notes" msgstr "" -#: build/models.py:520 +#: build/models.py:543 +msgid "No build output specified" +msgstr "" + +#: build/models.py:546 +msgid "Build output is already completed" +msgstr "" + +#: build/models.py:549 +msgid "Build output does not match Build Order" +msgstr "" + +#: build/models.py:620 +msgid "Completed build output" +msgstr "" + +#: build/models.py:858 +msgid "BuildItem must be unique for build, stock_item and install_into" +msgstr "" + +#: build/models.py:880 +msgid "Build item must specify a build output" +msgstr "" + +#: build/models.py:885 #, python-brace-format msgid "Selected stock item not found in BOM for part '{p}'" msgstr "" -#: build/models.py:523 +#: build/models.py:889 #, python-brace-format msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" -#: build/models.py:529 order/models.py:585 +#: build/models.py:896 order/models.py:603 msgid "StockItem is over-allocated" msgstr "" -#: build/models.py:532 order/models.py:588 +#: build/models.py:900 order/models.py:606 msgid "Allocation quantity must be greater than zero" msgstr "" -#: build/models.py:535 +#: build/models.py:904 msgid "Quantity must be 1 for serialized stock" msgstr "" -#: build/models.py:564 +#: build/models.py:944 msgid "Build to allocate parts" msgstr "" -#: build/models.py:571 -msgid "Stock Item to allocate to build" +#: build/models.py:951 +msgid "Source stock item" msgstr "" -#: build/models.py:584 +#: build/models.py:964 msgid "Stock quantity to allocate to build" msgstr "" -#: build/templates/build/allocate.html:17 -#: company/templates/company/detail_part.html:22 order/views.py:804 -#: part/templates/part/category.html:122 +#: build/models.py:972 +msgid "Destination stock item" +msgstr "" + +#: build/templates/build/allocate.html:14 +msgid "Incomplete Build Ouputs" +msgstr "" + +#: build/templates/build/allocate.html:20 +msgid "Build order has been completed" +msgstr "" + +#: build/templates/build/allocate.html:24 +msgid "Create new build output" +msgstr "" + +#: build/templates/build/allocate.html:25 +msgid "Create New Output" +msgstr "" + +#: build/templates/build/allocate.html:28 +msgid "Order required parts" +msgstr "" + +#: build/templates/build/allocate.html:29 +#: company/templates/company/detail_part.html:28 order/views.py:801 +#: part/templates/part/category.html:125 msgid "Order Parts" msgstr "" -#: build/templates/build/allocate.html:18 -msgid "Automatically allocate stock" +#: build/templates/build/allocate.html:32 templates/js/build.js:574 +msgid "Unallocate stock" msgstr "" -#: build/templates/build/allocate.html:18 -msgid "Auto Allocate" +#: build/templates/build/allocate.html:33 build/views.py:341 build/views.py:778 +msgid "Unallocate Stock" msgstr "" -#: build/templates/build/allocate.html:19 -msgid "Unallocate" +#: build/templates/build/allocate.html:46 +msgid "Create a new build output" msgstr "" -#: build/templates/build/allocate.html:87 templates/stock_table.html:10 -msgid "New Stock Item" +#: build/templates/build/allocate.html:47 +msgid "No incomplete build outputs remain." msgstr "" -#: build/templates/build/allocate.html:88 stock/views.py:1459 -msgid "Create new Stock Item" +#: build/templates/build/allocate.html:48 +msgid "Create a new build output using the button above" msgstr "" -#: build/templates/build/allocate.html:170 -#: order/templates/order/sales_order_detail.html:70 -#: order/templates/order/sales_order_detail.html:152 stock/models.py:375 -#: stock/templates/stock/item_base.html:178 -msgid "Serial Number" -msgstr "" - -#: build/templates/build/allocate.html:172 -#: build/templates/build/auto_allocate.html:20 -#: build/templates/build/build_base.html:83 -#: build/templates/build/detail.html:27 -#: company/templates/company/supplier_part_pricing.html:73 -#: order/templates/order/order_wizard/select_parts.html:32 -#: order/templates/order/purchase_order_detail.html:177 -#: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 -#: part/templates/part/allocation.html:16 -#: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:80 stock/forms.py:297 -#: stock/templates/stock/item_base.html:26 -#: stock/templates/stock/item_base.html:32 -#: stock/templates/stock/item_base.html:184 -#: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.html:338 -#: templates/js/bom.html:162 templates/js/build.html:72 -#: templates/js/stock.html:691 templates/js/stock.html:906 -msgid "Quantity" -msgstr "" - -#: build/templates/build/allocate.html:186 -#: build/templates/build/auto_allocate.html:21 stock/forms.py:336 -#: stock/templates/stock/item_base.html:220 -#: stock/templates/stock/stock_adjust.html:17 -#: templates/InvenTree/search.html:183 templates/js/barcode.html:337 -#: templates/js/stock.html:519 -msgid "Location" -msgstr "" - -#: build/templates/build/allocate.html:210 -#: order/templates/order/sales_order_detail.html:94 templates/js/build.html:144 -msgid "Edit stock allocation" -msgstr "" - -#: build/templates/build/allocate.html:211 -#: order/templates/order/sales_order_detail.html:95 templates/js/build.html:145 -msgid "Delete stock allocation" -msgstr "" - -#: build/templates/build/allocate.html:238 templates/js/bom.html:334 -msgid "No BOM items found" -msgstr "" - -#: build/templates/build/allocate.html:347 part/models.py:1401 -#: templates/js/part.html:530 templates/js/table_filters.html:121 -msgid "Required" -msgstr "" - -#: build/templates/build/allocate.html:356 -msgid "Assigned" -msgstr "" - -#: build/templates/build/allocate.html:394 -#: order/templates/order/sales_order_detail.html:273 -msgid "Buy parts" -msgstr "" - -#: build/templates/build/allocate.html:398 -#: order/templates/order/sales_order_detail.html:277 -msgid "Build parts" -msgstr "" - -#: build/templates/build/allocate.html:401 -msgid "Allocate stock" +#: build/templates/build/attachments.html:11 build/templates/build/tabs.html:17 +#: order/templates/order/po_tabs.html:11 order/templates/order/so_tabs.html:16 +#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 +msgid "Attachments" msgstr "" #: build/templates/build/auto_allocate.html:9 @@ -551,19 +615,22 @@ msgstr "" #: build/templates/build/auto_allocate.html:10 msgid "" -"Stock Items are selected for automatic allocation if there is only a single " -"stock item available." +"The following stock items will be allocated to the specified build output" msgstr "" -#: build/templates/build/auto_allocate.html:11 -msgid "The following stock items will be allocated to the build:" +#: build/templates/build/auto_allocate.html:18 stock/forms.py:336 +#: stock/templates/stock/item_base.html:233 +#: 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 +msgid "Location" msgstr "" -#: build/templates/build/auto_allocate.html:40 +#: build/templates/build/auto_allocate.html:37 msgid "No stock items found that can be automatically allocated to this build" msgstr "" -#: build/templates/build/auto_allocate.html:42 +#: build/templates/build/auto_allocate.html:39 msgid "Stock items will have to be manually allocated" msgstr "" @@ -580,7 +647,7 @@ msgstr "" #: order/templates/order/order_base.html:26 #: order/templates/order/sales_order_base.html:35 #: part/templates/part/category.html:13 part/templates/part/part_base.html:32 -#: stock/templates/stock/item_base.html:76 +#: stock/templates/stock/item_base.html:90 #: stock/templates/stock/location.html:12 msgid "Admin view" msgstr "" @@ -589,7 +656,7 @@ msgstr "" msgid "Edit Build" msgstr "" -#: build/templates/build/build_base.html:50 build/views.py:190 +#: build/templates/build/build_base.html:50 msgid "Complete Build" msgstr "" @@ -597,7 +664,7 @@ msgstr "" msgid "Cancel Build" msgstr "" -#: build/templates/build/build_base.html:59 build/views.py:456 +#: build/templates/build/build_base.html:59 build/views.py:767 msgid "Delete Build" msgstr "" @@ -605,124 +672,163 @@ msgstr "" msgid "Build Details" msgstr "" -#: build/templates/build/build_base.html:88 -#: build/templates/build/detail.html:42 +#: 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:298 templates/InvenTree/search.html:175 -#: templates/js/barcode.html:42 templates/js/build.html:77 -#: templates/js/order.html:172 templates/js/order.html:254 -#: templates/js/stock.html:506 templates/js/stock.html:914 +#: stock/templates/stock/item_base.html:311 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/stock.js:557 templates/js/stock.js:961 msgid "Status" msgstr "" -#: build/templates/build/build_base.html:101 order/models.py:499 +#: build/templates/build/build_base.html:88 +#: build/templates/build/detail.html:62 +msgid "Progress" +msgstr "" + +#: build/templates/build/build_base.html:101 +#: build/templates/build/detail.html:82 order/models.py:517 #: 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:208 templates/js/order.html:221 +#: stock/templates/stock/item_base.html:221 templates/js/order.js:221 msgid "Sales Order" msgstr "" -#: build/templates/build/build_base.html:107 -msgid "BOM Price" -msgstr "" - -#: build/templates/build/build_base.html:112 -msgid "BOM pricing is incomplete" -msgstr "" - -#: build/templates/build/build_base.html:115 -msgid "No pricing information" -msgstr "" - #: build/templates/build/build_output.html:9 build/templates/build/tabs.html:11 msgid "Build Outputs" msgstr "" -#: build/templates/build/complete.html:6 -#: stock/templates/stock/item_base.html:245 templates/js/build.html:40 -#: templates/navbar.html:25 -msgid "Build" +#: build/templates/build/build_output_create.html:7 +msgid "The Bill of Materials contains trackable parts" msgstr "" -#: build/templates/build/complete.html:10 -msgid "Build order allocation is complete" +#: build/templates/build/build_output_create.html:8 +msgid "Build outputs must be generated individually." msgstr "" -#: build/templates/build/complete.html:14 -msgid "Warning: Build order allocation is not complete" +#: build/templates/build/build_output_create.html:9 +msgid "Multiple build outputs will be created based on the quantity specified." +msgstr "" + +#: build/templates/build/build_output_create.html:15 +msgid "Trackable parts can have serial numbers specified" +msgstr "" + +#: build/templates/build/build_output_create.html:16 +msgid "Enter serial numbers to generate multiple single build outputs" +msgstr "" + +#: build/templates/build/cancel.html:5 +msgid "Are you sure you wish to cancel this build?" +msgstr "" + +#: build/templates/build/complete.html:8 +msgid "Build can be completed" +msgstr "" + +#: build/templates/build/complete.html:12 +msgid "Build cannot be completed" msgstr "" #: build/templates/build/complete.html:15 -msgid "" -"Build Order has not been fully allocated. Ensure that all Stock Items have " -"been allocated to the Build" +msgid "Incompleted build outputs remain" msgstr "" -#: build/templates/build/complete.html:20 -msgid "The following actions will be performed:" +#: build/templates/build/complete.html:18 +msgid "Required build quantity has not been completed" msgstr "" -#: build/templates/build/complete.html:22 -msgid "Remove allocated items from stock" +#: build/templates/build/complete_output.html:9 +msgid "Stock allocation is complete" msgstr "" -#: build/templates/build/complete.html:23 -msgid "Add completed items to stock" +#: build/templates/build/complete_output.html:13 +msgid "Stock allocation is incomplete" msgstr "" -#: build/templates/build/complete.html:29 +#: build/templates/build/complete_output.html:19 +msgid "parts have not been fully allocated" +msgstr "" + +#: build/templates/build/complete_output.html:40 msgid "The following items will be created" msgstr "" -#: build/templates/build/delete_build_item.html:6 -msgid "Are you sure you want to unallocate these parts?" +#: build/templates/build/create_build_item.html:7 +msgid "Select a stock item to allocate to the selected build output" msgstr "" -#: build/templates/build/detail.html:17 -msgid "Title" +#: build/templates/build/create_build_item.html:11 +msgid "The allocated stock will be installed into the following build output:" msgstr "" -#: build/templates/build/detail.html:31 +#: build/templates/build/create_build_item.html:19 +msgid "No stock available for" +msgstr "" + +#: build/templates/build/delete_build_item.html:8 +msgid "Are you sure you want to unallocate this stock?" +msgstr "" + +#: build/templates/build/delete_build_item.html:11 +msgid "The selected stock will be unallocated from the build output" +msgstr "" + +#: build/templates/build/detail.html:33 msgid "Stock Source" msgstr "" -#: build/templates/build/detail.html:36 +#: build/templates/build/detail.html:38 msgid "Stock can be taken from any available location." msgstr "" -#: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:238 templates/js/stock.html:514 -#: templates/js/stock.html:921 templates/js/table_filters.html:34 -#: templates/js/table_filters.html:100 +#: build/templates/build/detail.html:44 stock/forms.py:364 +msgid "Destination" +msgstr "" + +#: build/templates/build/detail.html:51 +msgid "Destination location not specified" +msgstr "" + +#: build/templates/build/detail.html:68 +#: stock/templates/stock/item_base.html:251 templates/js/stock.js:565 +#: templates/js/stock.js:968 templates/js/table_filters.js:80 +#: templates/js/table_filters.js:151 msgid "Batch" msgstr "" -#: build/templates/build/detail.html:61 +#: build/templates/build/detail.html:95 #: order/templates/order/order_base.html:98 -#: order/templates/order/sales_order_base.html:100 templates/js/build.html:85 +#: order/templates/order/sales_order_base.html:100 templates/js/build.js:683 msgid "Created" msgstr "" -#: build/templates/build/detail.html:67 -msgid "Enough Parts?" +#: build/templates/build/detail.html:105 +msgid "BOM Price" msgstr "" -#: build/templates/build/detail.html:70 -msgid "Yes" +#: build/templates/build/detail.html:110 +msgid "BOM pricing is incomplete" msgstr "" -#: build/templates/build/detail.html:72 -msgid "No" +#: build/templates/build/detail.html:113 +msgid "No pricing information" msgstr "" -#: build/templates/build/detail.html:80 templates/js/build.html:90 +#: build/templates/build/detail.html:120 templates/js/build.js:661 +#: templates/js/build.js:688 msgid "Completed" msgstr "" -#: build/templates/build/index.html:24 build/views.py:403 +#: build/templates/build/edit_build_item.html:7 +msgid "Alter the quantity of stock allocated to the build output" +msgstr "" + +#: build/templates/build/index.html:25 build/views.py:658 msgid "New Build Order" msgstr "" @@ -750,94 +856,163 @@ msgid "Details" msgstr "" #: build/templates/build/tabs.html:8 -msgid "Allocated Parts" +msgid "Allocate Parts" msgstr "" -#: build/templates/build/unallocate.html:8 +#: build/templates/build/unallocate.html:10 msgid "Are you sure you wish to unallocate all stock for this build?" msgstr "" -#: build/views.py:77 -msgid "Confirm build cancellation" +#: build/templates/build/unallocate.html:12 +msgid "All incomplete stock allocations will be removed from the build" msgstr "" -#: build/views.py:82 +#: build/views.py:79 msgid "Build was cancelled" msgstr "" -#: build/views.py:98 +#: build/views.py:93 msgid "Allocate Stock" msgstr "" -#: build/views.py:112 -msgid "No matching build found" +#: build/views.py:157 build/views.py:317 build/views.py:490 +msgid "Build output must be specified" msgstr "" -#: build/views.py:131 -msgid "Confirm stock allocation" +#: build/views.py:171 +msgid "Allocated stock to build output" msgstr "" -#: build/views.py:132 -msgid "Check the confirmation box at the bottom of the list" +#: build/views.py:183 +msgid "Create Build Output" msgstr "" -#: build/views.py:152 build/views.py:467 -msgid "Unallocate Stock" +#: build/views.py:207 stock/models.py:832 stock/views.py:1645 +msgid "Serial numbers already exist" msgstr "" -#: build/views.py:166 +#: build/views.py:216 +msgid "Serial numbers required for trackable build output" +msgstr "" + +#: build/views.py:282 +msgid "Delete Build Output" +msgstr "" + +#: build/views.py:302 build/views.py:387 msgid "Confirm unallocation of build stock" msgstr "" -#: build/views.py:167 stock/views.py:421 +#: build/views.py:303 build/views.py:388 stock/views.py:413 msgid "Check the confirmation box" msgstr "" -#: build/views.py:270 -msgid "Confirm completion of build" +#: build/views.py:315 +msgid "Build output does not match build" msgstr "" -#: build/views.py:277 -msgid "Invalid location selected" +#: build/views.py:329 +msgid "Build output deleted" msgstr "" -#: build/views.py:302 stock/views.py:1653 -#, python-brace-format -msgid "The following serial numbers already exist: ({sn})" +#: build/views.py:412 +msgid "Complete Build Order" msgstr "" -#: build/views.py:323 -msgid "Build marked as COMPLETE" +#: build/views.py:418 +msgid "Build order cannot be completed" msgstr "" -#: build/views.py:431 +#: build/views.py:429 +msgid "Completed build order" +msgstr "" + +#: build/views.py:445 +msgid "Complete Build Output" +msgstr "" + +#: build/views.py:481 +msgid "Quantity to complete cannot exceed build output quantity" +msgstr "" + +#: build/views.py:487 +msgid "Confirm completion of incomplete build" +msgstr "" + +#: build/views.py:578 +msgid "Build output completed" +msgstr "" + +#: build/views.py:703 msgid "Created new build" msgstr "" -#: build/views.py:441 +#: build/views.py:724 msgid "Edit Build Details" msgstr "" -#: build/views.py:447 +#: build/views.py:758 msgid "Edited build" msgstr "" -#: build/views.py:473 +#: build/views.py:784 msgid "Removed parts from build allocation" msgstr "" -#: build/views.py:483 -msgid "Allocate new Part" +#: build/views.py:796 +msgid "Allocate stock to build output" msgstr "" -#: build/views.py:637 +#: build/views.py:840 +msgid "Item must be currently in stock" +msgstr "" + +#: build/views.py:846 +msgid "Stock item is over-allocated" +msgstr "" + +#: build/views.py:847 templates/js/bom.js:215 templates/js/build.js:503 +#: templates/js/build.js:731 +msgid "Available" +msgstr "" + +#: build/views.py:849 +msgid "Stock item must be selected" +msgstr "" + +#: build/views.py:1011 msgid "Edit Stock Allocation" msgstr "" -#: build/views.py:642 +#: build/views.py:1016 msgid "Updated Build Item" msgstr "" +#: build/views.py:1045 +msgid "Add Build Order Attachment" +msgstr "" + +#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:96 +#: stock/views.py:176 +msgid "Added attachment" +msgstr "" + +#: build/views.py:1095 order/views.py:191 order/views.py:213 +msgid "Edit Attachment" +msgstr "" + +#: build/views.py:1106 order/views.py:196 order/views.py:218 +msgid "Attachment updated" +msgstr "" + +#: build/views.py:1116 order/views.py:233 order/views.py:248 +msgid "Delete Attachment" +msgstr "" + +#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:234 +msgid "Deleted attachment" +msgstr "" + #: common/models.py:51 msgid "InvenTree Instance Name" msgstr "" @@ -918,43 +1093,43 @@ msgstr "" msgid "Prefix value for purchase order reference" msgstr "" -#: common/models.py:272 +#: common/models.py:277 msgid "Settings key (must be unique - case insensitive" msgstr "" -#: common/models.py:274 +#: common/models.py:279 msgid "Settings value" msgstr "" -#: common/models.py:326 +#: common/models.py:331 msgid "Value must be a boolean value" msgstr "" -#: common/models.py:340 +#: common/models.py:345 msgid "Key string must be unique" msgstr "" -#: common/models.py:379 +#: common/models.py:384 msgid "Currency Symbol e.g. $" msgstr "" -#: common/models.py:381 +#: common/models.py:386 msgid "Currency Suffix e.g. AUD" msgstr "" -#: common/models.py:383 +#: common/models.py:388 msgid "Currency Description" msgstr "" -#: common/models.py:385 +#: common/models.py:390 msgid "Currency Value" msgstr "" -#: common/models.py:387 +#: common/models.py:392 msgid "Use this currency as the base currency" msgstr "" -#: common/models.py:470 +#: common/models.py:475 msgid "Default" msgstr "" @@ -987,7 +1162,7 @@ msgid "Description of the company" msgstr "" #: company/models.py:94 company/templates/company/company_base.html:57 -#: templates/js/company.html:61 +#: templates/js/company.js:61 msgid "Website" msgstr "" @@ -1043,8 +1218,8 @@ msgstr "" msgid "Does this company manufacture parts?" msgstr "" -#: company/models.py:283 stock/models.py:335 -#: stock/templates/stock/item_base.html:164 +#: company/models.py:283 stock/models.py:337 +#: stock/templates/stock/item_base.html:177 msgid "Base Part" msgstr "" @@ -1085,12 +1260,12 @@ msgid "Part packaging" msgstr "" #: company/templates/company/assigned_stock.html:9 -#: company/templates/company/tabs.html:25 +#: company/templates/company/tabs.html:25 templates/js/build.js:395 msgid "Assigned Stock" msgstr "" #: company/templates/company/company_base.html:7 -#: company/templates/company/company_base.html:22 templates/js/company.html:33 +#: company/templates/company/company_base.html:22 templates/js/company.js:33 msgid "Company" msgstr "" @@ -1106,7 +1281,7 @@ msgstr "" #: company/templates/company/detail.html:16 #: company/templates/company/supplier_part_base.html:84 #: company/templates/company/supplier_part_detail.html:30 part/bom.py:172 -#: templates/js/company.html:44 templates/js/company.html:186 +#: templates/js/company.js:44 templates/js/company.js:188 msgid "Manufacturer" msgstr "" @@ -1115,15 +1290,15 @@ msgstr "" #: 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:273 templates/js/company.html:48 -#: templates/js/company.html:162 templates/js/order.html:154 +#: stock/templates/stock/item_base.html:286 templates/js/company.js:48 +#: templates/js/company.js:164 templates/js/order.js:154 msgid "Supplier" msgstr "" #: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:81 stock/models.py:370 -#: stock/models.py:371 stock/templates/stock/item_base.html:191 -#: templates/js/company.html:40 templates/js/order.html:236 +#: 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 msgid "Customer" msgstr "" @@ -1131,60 +1306,60 @@ msgstr "" msgid "Supplier Parts" msgstr "" -#: company/templates/company/detail_part.html:15 -#: order/templates/order/purchase_order_detail.html:68 +#: company/templates/company/detail_part.html:17 +#: order/templates/order/purchase_order_detail.html:69 msgid "Create new supplier part" msgstr "" -#: company/templates/company/detail_part.html:15 -#: order/templates/order/purchase_order_detail.html:67 -#: part/templates/part/supplier.html:13 templates/js/stock.html:798 +#: company/templates/company/detail_part.html:18 +#: order/templates/order/purchase_order_detail.html:68 +#: part/templates/part/supplier.html:14 templates/js/stock.js:845 msgid "New Supplier Part" msgstr "" -#: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:117 part/templates/part/supplier.html:15 -#: templates/stock_table.html:14 +#: company/templates/company/detail_part.html:23 +#: part/templates/part/category.html:120 part/templates/part/supplier.html:17 +#: templates/stock_table.html:18 msgid "Options" msgstr "" -#: company/templates/company/detail_part.html:22 -#: part/templates/part/category.html:122 +#: company/templates/company/detail_part.html:28 +#: part/templates/part/category.html:125 msgid "Order parts" msgstr "" -#: company/templates/company/detail_part.html:25 +#: company/templates/company/detail_part.html:31 msgid "Delete parts" msgstr "" -#: company/templates/company/detail_part.html:25 +#: company/templates/company/detail_part.html:31 msgid "Delete Parts" msgstr "" -#: company/templates/company/detail_part.html:51 -#: part/templates/part/category.html:114 templates/js/stock.html:792 +#: company/templates/company/detail_part.html:63 +#: part/templates/part/category.html:116 templates/js/stock.js:839 msgid "New Part" msgstr "" -#: company/templates/company/detail_part.html:52 +#: company/templates/company/detail_part.html:64 msgid "Create new Part" msgstr "" -#: company/templates/company/detail_part.html:57 company/views.py:53 -#: part/templates/part/supplier.html:45 +#: company/templates/company/detail_part.html:69 company/views.py:53 +#: part/templates/part/supplier.html:47 msgid "New Supplier" msgstr "" -#: company/templates/company/detail_part.html:58 company/views.py:192 +#: company/templates/company/detail_part.html:70 company/views.py:192 msgid "Create new Supplier" msgstr "" -#: company/templates/company/detail_part.html:63 company/views.py:60 -#: part/templates/part/supplier.html:51 +#: company/templates/company/detail_part.html:75 company/views.py:60 +#: part/templates/part/supplier.html:53 msgid "New Manufacturer" msgstr "" -#: company/templates/company/detail_part.html:64 company/views.py:195 +#: company/templates/company/detail_part.html:76 company/views.py:195 msgid "Create new Manufacturer" msgstr "" @@ -1194,8 +1369,9 @@ msgstr "" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:112 part/templates/part/category.html:123 -#: part/templates/part/stock.html:51 templates/stock_table.html:6 +#: part/templates/part/bom.html:63 part/templates/part/category.html:112 +#: part/templates/part/category.html:126 part/templates/part/stock.html:51 +#: templates/stock_table.html:7 msgid "Export" msgstr "" @@ -1227,8 +1403,8 @@ msgstr "" msgid "Create new purchase order" msgstr "" -#: company/templates/company/purchase_orders.html:15 -#: order/templates/order/purchase_orders.html:18 +#: company/templates/company/purchase_orders.html:16 +#: order/templates/order/purchase_orders.html:19 msgid "New Purchase Order" msgstr "" @@ -1247,14 +1423,14 @@ msgstr "" msgid "Create new sales order" msgstr "" -#: company/templates/company/sales_orders.html:15 -#: order/templates/order/sales_orders.html:18 +#: company/templates/company/sales_orders.html:16 +#: order/templates/order/sales_orders.html:19 msgid "New Sales Order" msgstr "" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:344 -#: stock/templates/stock/item_base.html:278 templates/js/company.html:178 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:346 +#: stock/templates/stock/item_base.html:291 templates/js/company.js:180 msgid "Supplier Part" msgstr "" @@ -1288,7 +1464,7 @@ msgstr "" #: company/templates/company/supplier_part_base.html:88 #: company/templates/company/supplier_part_detail.html:31 part/bom.py:173 -#: templates/js/company.html:202 +#: templates/js/company.js:204 msgid "MPN" msgstr "" @@ -1301,31 +1477,36 @@ msgstr "" msgid "Supplier Part Orders" msgstr "" +#: company/templates/company/supplier_part_orders.html:17 +#: part/templates/part/orders.html:15 +msgid "Order Part" +msgstr "" + #: company/templates/company/supplier_part_pricing.html:10 msgid "Pricing Information" msgstr "" -#: company/templates/company/supplier_part_pricing.html:16 company/views.py:410 -#: part/templates/part/sale_prices.html:13 part/views.py:2228 +#: company/templates/company/supplier_part_pricing.html:17 company/views.py:410 +#: part/templates/part/sale_prices.html:13 part/views.py:2292 msgid "Add Price Break" msgstr "" -#: company/templates/company/supplier_part_pricing.html:34 +#: company/templates/company/supplier_part_pricing.html:36 #: part/templates/part/sale_prices.html:41 msgid "No price break information found" msgstr "" -#: company/templates/company/supplier_part_pricing.html:78 -#: part/templates/part/sale_prices.html:85 templates/js/bom.html:207 +#: 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:92 +#: company/templates/company/supplier_part_pricing.html:94 #: part/templates/part/sale_prices.html:99 msgid "Edit price break" msgstr "" -#: company/templates/company/supplier_part_pricing.html:93 +#: company/templates/company/supplier_part_pricing.html:95 #: part/templates/part/sale_prices.html:100 msgid "Delete price break" msgstr "" @@ -1341,9 +1522,9 @@ msgstr "" #: company/templates/company/supplier_part_tabs.html:8 #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 -#: templates/InvenTree/settings/tabs.html:25 templates/js/part.html:124 -#: templates/js/part.html:372 templates/js/stock.html:453 -#: templates/navbar.html:22 users/models.py:29 +#: templates/InvenTree/settings/tabs.html:25 templates/js/part.js:192 +#: templates/js/part.js:418 templates/js/stock.js:502 templates/navbar.html:22 +#: users/models.py:29 msgid "Stock" msgstr "" @@ -1425,7 +1606,7 @@ msgstr "" msgid "Edit Supplier Part" msgstr "" -#: company/views.py:278 templates/js/stock.html:799 +#: company/views.py:278 templates/js/stock.js:846 msgid "Create new Supplier Part" msgstr "" @@ -1433,15 +1614,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:416 part/views.py:2234 +#: company/views.py:416 part/views.py:2298 msgid "Added new price break" msgstr "" -#: company/views.py:453 part/views.py:2279 +#: company/views.py:453 part/views.py:2343 msgid "Edit Price Break" msgstr "" -#: company/views.py:469 part/views.py:2295 +#: company/views.py:469 part/views.py:2359 msgid "Delete Price Break" msgstr "" @@ -1514,7 +1695,7 @@ msgstr "" msgid "Order notes" msgstr "" -#: order/models.py:140 order/models.py:318 +#: order/models.py:140 order/models.py:326 msgid "Purchase order status" msgstr "" @@ -1534,8 +1715,8 @@ msgstr "" msgid "Date order was completed" msgstr "" -#: order/models.py:185 order/models.py:259 part/views.py:1345 -#: stock/models.py:241 stock/models.py:805 +#: order/models.py:185 order/models.py:267 part/views.py:1409 +#: stock/models.py:243 stock/models.py:816 msgid "Quantity must be greater than zero" msgstr "" @@ -1543,69 +1724,69 @@ msgstr "" msgid "Part supplier must match PO supplier" msgstr "" -#: order/models.py:254 +#: order/models.py:262 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "" -#: order/models.py:314 +#: order/models.py:322 msgid "Company to which the items are being sold" msgstr "" -#: order/models.py:320 +#: order/models.py:328 msgid "Customer order reference code" msgstr "" -#: order/models.py:359 +#: order/models.py:367 msgid "SalesOrder cannot be shipped as it is not currently pending" msgstr "" -#: order/models.py:436 +#: order/models.py:454 msgid "Item quantity" msgstr "" -#: order/models.py:438 +#: order/models.py:456 msgid "Line item reference" msgstr "" -#: order/models.py:440 +#: order/models.py:458 msgid "Line item notes" msgstr "" -#: order/models.py:466 order/templates/order/order_base.html:9 +#: order/models.py:484 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:252 templates/js/order.html:139 +#: stock/templates/stock/item_base.html:265 templates/js/order.js:139 msgid "Purchase Order" msgstr "" -#: order/models.py:479 +#: order/models.py:497 msgid "Supplier part" msgstr "" -#: order/models.py:482 +#: order/models.py:500 msgid "Number of items received" msgstr "" -#: order/models.py:576 +#: order/models.py:594 msgid "Cannot allocate stock item to a line with a different part" msgstr "" -#: order/models.py:578 +#: order/models.py:596 msgid "Cannot allocate stock to a line without a part" msgstr "" -#: order/models.py:581 +#: order/models.py:599 msgid "Allocation quantity cannot exceed stock quantity" msgstr "" -#: order/models.py:591 +#: order/models.py:609 msgid "Quantity must be 1 for serialized stock item" msgstr "" -#: order/models.py:608 +#: order/models.py:626 msgid "Select stock item to allocate" msgstr "" -#: order/models.py:611 +#: order/models.py:629 msgid "Enter stock allocation quantity" msgstr "" @@ -1641,7 +1822,7 @@ msgstr "" msgid "Order Status" msgstr "" -#: order/templates/order/order_base.html:85 templates/js/order.html:161 +#: order/templates/order/order_base.html:85 templates/js/order.js:161 msgid "Supplier Reference" msgstr "" @@ -1650,7 +1831,7 @@ msgid "Issued" msgstr "" #: order/templates/order/order_base.html:111 -#: order/templates/order/purchase_order_detail.html:182 +#: order/templates/order/purchase_order_detail.html:183 #: order/templates/order/receive_parts.html:22 #: order/templates/order/sales_order_base.html:113 msgid "Received" @@ -1697,7 +1878,7 @@ msgid "Select existing purchase orders, or create new orders." msgstr "" #: order/templates/order/order_wizard/select_pos.html:31 -#: templates/js/order.html:185 templates/js/order.html:272 +#: templates/js/order.js:185 templates/js/order.js:272 msgid "Items" msgstr "" @@ -1722,53 +1903,48 @@ msgstr "" msgid "Line Items" msgstr "" -#: order/templates/order/po_tabs.html:11 order/templates/order/so_tabs.html:16 -#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 -msgid "Attachments" -msgstr "" - -#: order/templates/order/purchase_order_detail.html:16 -#: order/templates/order/sales_order_detail.html:18 order/views.py:1117 -#: order/views.py:1232 +#: order/templates/order/purchase_order_detail.html:17 +#: order/templates/order/sales_order_detail.html:19 order/views.py:1115 +#: order/views.py:1199 msgid "Add Line Item" msgstr "" -#: order/templates/order/purchase_order_detail.html:20 +#: order/templates/order/purchase_order_detail.html:21 msgid "Purchase Order Items" msgstr "" -#: order/templates/order/purchase_order_detail.html:38 -#: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:171 part/templates/part/category.html:213 -#: templates/js/stock.html:804 -msgid "New Location" -msgstr "" - #: order/templates/order/purchase_order_detail.html:39 #: order/templates/order/purchase_order_detail.html:119 +#: part/templates/part/category.html:173 part/templates/part/category.html:215 +#: templates/js/stock.js:851 +msgid "New Location" +msgstr "" + +#: order/templates/order/purchase_order_detail.html:40 +#: order/templates/order/purchase_order_detail.html:120 #: stock/templates/stock/location.html:22 msgid "Create new stock location" msgstr "" -#: order/templates/order/purchase_order_detail.html:131 +#: order/templates/order/purchase_order_detail.html:132 msgid "No line items found" msgstr "" -#: order/templates/order/purchase_order_detail.html:164 +#: order/templates/order/purchase_order_detail.html:165 #: order/templates/order/receive_parts.html:20 msgid "Order Code" msgstr "" -#: order/templates/order/purchase_order_detail.html:213 -#: order/templates/order/sales_order_detail.html:283 +#: order/templates/order/purchase_order_detail.html:214 +#: order/templates/order/sales_order_detail.html:285 msgid "Edit line item" msgstr "" -#: order/templates/order/purchase_order_detail.html:214 +#: order/templates/order/purchase_order_detail.html:215 msgid "Delete line item" msgstr "" -#: order/templates/order/purchase_order_detail.html:219 +#: order/templates/order/purchase_order_detail.html:220 msgid "Receive line item" msgstr "" @@ -1781,7 +1957,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:145 templates/js/part.html:388 +#: part/templates/part/part_base.html:145 templates/js/part.js:434 msgid "On Order" msgstr "" @@ -1805,12 +1981,13 @@ msgstr "" msgid "Sales Order Details" msgstr "" -#: order/templates/order/sales_order_base.html:87 templates/js/order.html:243 +#: order/templates/order/sales_order_base.html:87 templates/js/order.js:243 msgid "Customer Reference" msgstr "" #: order/templates/order/sales_order_cancel.html:8 #: order/templates/order/sales_order_ship.html:9 +#: part/templates/part/bom_duplicate.html:12 #: stock/templates/stock/stockitem_convert.html:13 msgid "Warning" msgstr "" @@ -1819,15 +1996,45 @@ msgstr "" 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 +#: stock/templates/stock/item_base.html:191 templates/js/build.js:402 +msgid "Serial Number" +msgstr "" + +#: order/templates/order/sales_order_detail.html:96 templates/js/build.js:443 +#: templates/js/build.js:742 +msgid "Edit stock allocation" +msgstr "" + +#: order/templates/order/sales_order_detail.html:97 templates/js/build.js:445 +#: templates/js/build.js:743 +msgid "Delete stock allocation" +msgstr "" + #: order/templates/order/sales_order_detail.html:225 +#: part/templates/part/tabs.html:23 templates/js/build.js:507 +#: templates/js/build.js:738 +msgid "Allocated" +msgstr "" + +#: order/templates/order/sales_order_detail.html:227 msgid "Fulfilled" msgstr "" -#: order/templates/order/sales_order_detail.html:280 +#: order/templates/order/sales_order_detail.html:275 +msgid "Buy parts" +msgstr "" + +#: order/templates/order/sales_order_detail.html:279 +msgid "Build parts" +msgstr "" + +#: order/templates/order/sales_order_detail.html:282 msgid "Allocate parts" msgstr "" -#: order/templates/order/sales_order_detail.html:284 +#: order/templates/order/sales_order_detail.html:286 msgid "Delete line item " msgstr "" @@ -1873,143 +2080,131 @@ msgstr "" msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:109 order/views.py:157 part/views.py:92 stock/views.py:175 -msgid "Added attachment" -msgstr "" - -#: order/views.py:148 +#: order/views.py:150 msgid "Add Sales Order Attachment" msgstr "" -#: order/views.py:184 order/views.py:206 -msgid "Edit Attachment" -msgstr "" - -#: order/views.py:189 order/views.py:211 -msgid "Attachment updated" -msgstr "" - -#: order/views.py:226 order/views.py:241 -msgid "Delete Attachment" -msgstr "" - -#: order/views.py:233 order/views.py:248 stock/views.py:233 -msgid "Deleted attachment" -msgstr "" - -#: order/views.py:301 +#: order/views.py:310 msgid "Create Purchase Order" msgstr "" -#: order/views.py:333 +#: order/views.py:345 msgid "Create Sales Order" msgstr "" -#: order/views.py:364 +#: order/views.py:380 msgid "Edit Purchase Order" msgstr "" -#: order/views.py:385 +#: order/views.py:401 msgid "Edit Sales Order" msgstr "" -#: order/views.py:402 +#: order/views.py:418 msgid "Cancel Order" msgstr "" -#: order/views.py:418 order/views.py:451 +#: order/views.py:428 order/views.py:455 msgid "Confirm order cancellation" msgstr "" -#: order/views.py:436 +#: order/views.py:431 order/views.py:458 +msgid "Order cannot be cancelled" +msgstr "" + +#: order/views.py:445 msgid "Cancel sales order" msgstr "" -#: order/views.py:457 -msgid "Could not cancel order" -msgstr "" - -#: order/views.py:471 +#: order/views.py:472 msgid "Issue Order" msgstr "" -#: order/views.py:487 +#: order/views.py:482 msgid "Confirm order placement" msgstr "" -#: order/views.py:508 +#: order/views.py:492 +msgid "Purchase order issued" +msgstr "" + +#: order/views.py:503 msgid "Complete Order" msgstr "" -#: order/views.py:544 +#: order/views.py:520 +msgid "Confirm order completion" +msgstr "" + +#: order/views.py:531 +msgid "Purchase order completed" +msgstr "" + +#: order/views.py:541 msgid "Ship Order" msgstr "" -#: order/views.py:561 +#: order/views.py:558 msgid "Confirm order shipment" msgstr "" -#: order/views.py:567 +#: order/views.py:564 msgid "Could not ship order" msgstr "" -#: order/views.py:619 +#: order/views.py:616 msgid "Receive Parts" msgstr "" -#: order/views.py:687 +#: order/views.py:684 msgid "Items received" msgstr "" -#: order/views.py:701 +#: order/views.py:698 msgid "No destination set" msgstr "" -#: order/views.py:746 +#: order/views.py:743 msgid "Error converting quantity to number" msgstr "" -#: order/views.py:752 +#: order/views.py:749 msgid "Receive quantity less than zero" msgstr "" -#: order/views.py:758 +#: order/views.py:755 msgid "No lines specified" msgstr "" -#: order/views.py:1138 -msgid "Invalid Purchase Order" +#: order/views.py:1125 +msgid "Supplier part must be specified" msgstr "" -#: order/views.py:1146 +#: order/views.py:1131 msgid "Supplier must match for Part and Order" msgstr "" -#: order/views.py:1151 -msgid "Invalid SupplierPart selection" -msgstr "" - -#: order/views.py:1284 order/views.py:1303 +#: order/views.py:1251 order/views.py:1270 msgid "Edit Line Item" msgstr "" -#: order/views.py:1320 order/views.py:1333 +#: order/views.py:1287 order/views.py:1300 msgid "Delete Line Item" msgstr "" -#: order/views.py:1326 order/views.py:1339 +#: order/views.py:1293 order/views.py:1306 msgid "Deleted line item" msgstr "" -#: order/views.py:1348 +#: order/views.py:1315 msgid "Allocate Stock to Order" msgstr "" -#: order/views.py:1418 +#: order/views.py:1385 msgid "Edit Allocation Quantity" msgstr "" -#: order/views.py:1434 +#: order/views.py:1401 msgid "Remove allocation" msgstr "" @@ -2035,91 +2230,107 @@ msgstr "" msgid "Error reading BOM file (incorrect row size)" msgstr "" -#: part/forms.py:57 stock/forms.py:254 +#: part/forms.py:62 stock/forms.py:254 msgid "File Format" msgstr "" -#: part/forms.py:57 stock/forms.py:254 +#: part/forms.py:62 stock/forms.py:254 msgid "Select output file format" msgstr "" -#: part/forms.py:59 +#: part/forms.py:64 msgid "Cascading" msgstr "" -#: part/forms.py:59 +#: part/forms.py:64 msgid "Download cascading / multi-level BOM" msgstr "" -#: part/forms.py:61 +#: part/forms.py:66 msgid "Levels" msgstr "" -#: part/forms.py:61 +#: part/forms.py:66 msgid "Select maximum number of BOM levels to export (0 = all levels)" msgstr "" -#: part/forms.py:63 +#: part/forms.py:68 msgid "Include Parameter Data" msgstr "" -#: part/forms.py:63 +#: part/forms.py:68 msgid "Include part parameters data in exported BOM" msgstr "" -#: part/forms.py:65 +#: part/forms.py:70 msgid "Include Stock Data" msgstr "" -#: part/forms.py:65 +#: part/forms.py:70 msgid "Include part stock data in exported BOM" msgstr "" -#: part/forms.py:67 +#: part/forms.py:72 msgid "Include Supplier Data" msgstr "" -#: part/forms.py:67 +#: part/forms.py:72 msgid "Include part supplier data in exported BOM" msgstr "" -#: part/forms.py:86 +#: part/forms.py:93 part/models.py:1582 +msgid "Parent Part" +msgstr "" + +#: part/forms.py:94 part/templates/part/bom_duplicate.html:7 +msgid "Select parent part to copy BOM from" +msgstr "" + +#: part/forms.py:100 +msgid "Clear existing BOM items" +msgstr "" + +#: part/forms.py:105 +msgid "Confirm BOM duplication" +msgstr "" + +#: part/forms.py:123 msgid "Confirm that the BOM is correct" msgstr "" -#: part/forms.py:98 +#: part/forms.py:135 msgid "Select BOM file to upload" msgstr "" -#: part/forms.py:122 +#: part/forms.py:159 msgid "Select part category" msgstr "" -#: part/forms.py:136 +#: part/forms.py:173 msgid "Duplicate all BOM data for this part" msgstr "" -#: part/forms.py:137 +#: part/forms.py:174 msgid "Copy BOM" msgstr "" -#: part/forms.py:142 +#: part/forms.py:179 msgid "Duplicate all parameter data for this part" msgstr "" -#: part/forms.py:143 +#: part/forms.py:180 msgid "Copy Parameters" msgstr "" -#: part/forms.py:148 +#: part/forms.py:185 msgid "Confirm part creation" msgstr "" -#: part/forms.py:248 +#: part/forms.py:279 msgid "Input quantity for price calculation" msgstr "" -#: part/forms.py:251 +#: part/forms.py:282 msgid "Select currency for price calculation" msgstr "" @@ -2145,216 +2356,213 @@ msgstr "" msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "" -#: part/models.py:435 +#: part/models.py:452 msgid "Next available serial numbers are" msgstr "" -#: part/models.py:439 +#: part/models.py:456 msgid "Next available serial number is" msgstr "" -#: part/models.py:444 +#: part/models.py:461 msgid "Most recent serial number is" msgstr "" -#: part/models.py:522 +#: part/models.py:539 msgid "Part must be unique for name, IPN and revision" msgstr "" -#: part/models.py:537 part/templates/part/detail.html:19 +#: part/models.py:568 part/templates/part/detail.html:19 msgid "Part name" msgstr "" -#: part/models.py:541 +#: part/models.py:572 msgid "Is this part a template part?" msgstr "" -#: part/models.py:550 +#: part/models.py:581 msgid "Is this part a variant of another part?" msgstr "" -#: part/models.py:552 +#: part/models.py:583 msgid "Part description" msgstr "" -#: part/models.py:554 +#: part/models.py:585 msgid "Part keywords to improve visibility in search results" msgstr "" -#: part/models.py:559 +#: part/models.py:590 msgid "Part category" msgstr "" -#: part/models.py:561 +#: part/models.py:592 msgid "Internal Part Number" msgstr "" -#: part/models.py:563 +#: part/models.py:594 msgid "Part revision or version number" msgstr "" -#: part/models.py:565 -msgid "Link to extenal URL" -msgstr "" - -#: part/models.py:577 +#: part/models.py:608 msgid "Where is this item normally stored?" msgstr "" -#: part/models.py:621 +#: part/models.py:652 msgid "Default supplier part" msgstr "" -#: part/models.py:624 +#: part/models.py:655 msgid "Minimum allowed stock level" msgstr "" -#: part/models.py:626 +#: part/models.py:657 msgid "Stock keeping units for this part" msgstr "" -#: part/models.py:628 +#: part/models.py:659 msgid "Can this part be built from other parts?" msgstr "" -#: part/models.py:630 +#: part/models.py:661 msgid "Can this part be used to build other parts?" msgstr "" -#: part/models.py:632 +#: part/models.py:663 msgid "Does this part have tracking for unique items?" msgstr "" -#: part/models.py:634 +#: part/models.py:665 msgid "Can this part be purchased from external suppliers?" msgstr "" -#: part/models.py:636 +#: part/models.py:667 msgid "Can this part be sold to customers?" msgstr "" -#: part/models.py:638 +#: part/models.py:669 msgid "Is this part active?" msgstr "" -#: part/models.py:640 +#: part/models.py:671 msgid "Is this a virtual part, such as a software product or license?" msgstr "" -#: part/models.py:642 +#: part/models.py:673 msgid "Part notes - supports Markdown formatting" msgstr "" -#: part/models.py:644 +#: part/models.py:675 msgid "Stored BOM checksum" msgstr "" -#: part/models.py:1353 +#: part/models.py:1455 msgid "Test templates can only be created for trackable parts" msgstr "" -#: part/models.py:1370 +#: part/models.py:1472 msgid "Test with this name already exists for this part" msgstr "" -#: part/models.py:1389 templates/js/part.html:521 templates/js/stock.html:92 +#: part/models.py:1491 templates/js/part.js:567 templates/js/stock.js:92 msgid "Test Name" msgstr "" -#: part/models.py:1390 +#: part/models.py:1492 msgid "Enter a name for the test" msgstr "" -#: part/models.py:1395 +#: part/models.py:1497 msgid "Test Description" msgstr "" -#: part/models.py:1396 +#: part/models.py:1498 msgid "Enter description for this test" msgstr "" -#: part/models.py:1402 +#: part/models.py:1503 templates/js/part.js:576 +#: templates/js/table_filters.js:172 +msgid "Required" +msgstr "" + +#: part/models.py:1504 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1407 templates/js/part.html:538 +#: part/models.py:1509 templates/js/part.js:584 msgid "Requires Value" msgstr "" -#: part/models.py:1408 +#: part/models.py:1510 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1413 templates/js/part.html:545 +#: part/models.py:1515 templates/js/part.js:591 msgid "Requires Attachment" msgstr "" -#: part/models.py:1414 +#: part/models.py:1516 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1447 +#: part/models.py:1549 msgid "Parameter template name must be unique" msgstr "" -#: part/models.py:1452 +#: part/models.py:1554 msgid "Parameter Name" msgstr "" -#: part/models.py:1454 +#: part/models.py:1556 msgid "Parameter Units" msgstr "" -#: part/models.py:1480 -msgid "Parent Part" -msgstr "" - -#: part/models.py:1482 +#: part/models.py:1584 msgid "Parameter Template" msgstr "" -#: part/models.py:1484 +#: part/models.py:1586 msgid "Parameter Value" msgstr "" -#: part/models.py:1521 +#: part/models.py:1623 msgid "Select parent part" msgstr "" -#: part/models.py:1529 +#: part/models.py:1631 msgid "Select part to be used in BOM" msgstr "" -#: part/models.py:1535 +#: part/models.py:1637 msgid "BOM quantity for this BOM item" msgstr "" -#: part/models.py:1537 +#: part/models.py:1639 msgid "This BOM item is optional" msgstr "" -#: part/models.py:1540 +#: part/models.py:1642 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "" -#: part/models.py:1543 +#: part/models.py:1645 msgid "BOM item reference" msgstr "" -#: part/models.py:1546 +#: part/models.py:1648 msgid "BOM item notes" msgstr "" -#: part/models.py:1548 +#: part/models.py:1650 msgid "BOM line checksum" msgstr "" -#: part/models.py:1612 part/views.py:1351 part/views.py:1403 -#: stock/models.py:231 +#: part/models.py:1717 part/views.py:1415 part/views.py:1467 +#: stock/models.py:233 msgid "Quantity must be integer value for trackable parts" msgstr "" -#: part/models.py:1621 +#: part/models.py:1733 msgid "BOM Item" msgstr "" @@ -2372,10 +2580,10 @@ msgstr "" #: part/templates/part/allocation.html:28 #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 -#: stock/templates/stock/item_base.html:58 -#: stock/templates/stock/item_base.html:260 -#: stock/templates/stock/stock_adjust.html:16 templates/js/build.html:126 -#: templates/js/stock.html:661 templates/js/stock.html:897 +#: stock/templates/stock/item_base.html:72 +#: stock/templates/stock/item_base.html:273 +#: 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" msgstr "" @@ -2387,42 +2595,75 @@ msgstr "" msgid "Bill of Materials" msgstr "" -#: part/templates/part/bom.html:37 +#: part/templates/part/bom.html:34 msgid "Remove selected BOM items" msgstr "" -#: part/templates/part/bom.html:38 +#: part/templates/part/bom.html:37 msgid "Import BOM data" msgstr "" -#: part/templates/part/bom.html:39 -msgid "New BOM Item" +#: part/templates/part/bom.html:38 +msgid "Import from File" msgstr "" -#: part/templates/part/bom.html:40 -msgid "Finish Editing" +#: part/templates/part/bom.html:41 +msgid "Copy BOM from parent part" msgstr "" -#: part/templates/part/bom.html:43 -msgid "Edit BOM" +#: part/templates/part/bom.html:42 +msgid "Copy from Parent" msgstr "" #: part/templates/part/bom.html:45 +msgid "New BOM Item" +msgstr "" + +#: part/templates/part/bom.html:46 +msgid "Add Item" +msgstr "" + +#: part/templates/part/bom.html:48 +msgid "Finish Editing" +msgstr "" + +#: part/templates/part/bom.html:49 +msgid "Finished" +msgstr "" + +#: part/templates/part/bom.html:53 +msgid "Edit BOM" +msgstr "" + +#: part/templates/part/bom.html:54 part/templates/part/params.html:38 +#: templates/InvenTree/settings/user.html:19 +msgid "Edit" +msgstr "" + +#: part/templates/part/bom.html:57 msgid "Validate Bill of Materials" msgstr "" -#: part/templates/part/bom.html:48 part/views.py:1642 +#: part/templates/part/bom.html:58 +msgid "Validate" +msgstr "" + +#: part/templates/part/bom.html:62 part/views.py:1706 msgid "Export Bill of Materials" msgstr "" -#: part/templates/part/bom.html:103 +#: part/templates/part/bom.html:123 msgid "Delete selected BOM items?" msgstr "" -#: part/templates/part/bom.html:104 +#: part/templates/part/bom.html:124 msgid "All selected BOM items will be deleted" msgstr "" +#: part/templates/part/bom_duplicate.html:13 +msgid "This part already has a Bill of Materials" +msgstr "" + #: part/templates/part/bom_upload/select_fields.html:8 #: part/templates/part/bom_upload/select_parts.html:8 #: part/templates/part/bom_upload/upload_file.html:10 @@ -2495,7 +2736,7 @@ msgstr "" msgid "Part Builds" msgstr "" -#: part/templates/part/build.html:14 +#: part/templates/part/build.html:15 msgid "Start New Build" msgstr "" @@ -2503,7 +2744,7 @@ msgstr "" msgid "All parts" msgstr "" -#: part/templates/part/category.html:24 part/views.py:2045 +#: part/templates/part/category.html:24 part/views.py:2109 msgid "Create new part category" msgstr "" @@ -2539,43 +2780,43 @@ msgstr "" msgid "Parts (Including subcategories)" msgstr "" -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:111 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:114 part/views.py:513 +#: part/templates/part/category.html:115 msgid "Create new part" msgstr "" -#: part/templates/part/category.html:120 +#: part/templates/part/category.html:123 msgid "Set category" msgstr "" -#: part/templates/part/category.html:120 +#: part/templates/part/category.html:123 msgid "Set Category" msgstr "" -#: part/templates/part/category.html:123 +#: part/templates/part/category.html:126 msgid "Export Data" msgstr "" -#: part/templates/part/category.html:172 +#: part/templates/part/category.html:174 msgid "Create new location" msgstr "" -#: part/templates/part/category.html:177 part/templates/part/category.html:207 +#: part/templates/part/category.html:179 part/templates/part/category.html:209 msgid "New Category" msgstr "" -#: part/templates/part/category.html:178 +#: part/templates/part/category.html:180 msgid "Create new category" msgstr "" -#: part/templates/part/category.html:208 +#: part/templates/part/category.html:210 msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:214 stock/views.py:1343 +#: part/templates/part/category.html:216 stock/views.py:1338 msgid "Create new Stock Location" msgstr "" @@ -2583,16 +2824,28 @@ msgstr "" msgid "Parametric Table" msgstr "" +#: part/templates/part/create_part.html:11 +msgid "Possible Matching Parts" +msgstr "" + +#: part/templates/part/create_part.html:12 +msgid "The new part may be a duplicate of these existing parts" +msgstr "" + +#: part/templates/part/create_part.html:16 +msgid "match" +msgstr "" + #: part/templates/part/detail.html:9 msgid "Part Details" msgstr "" #: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 -#: templates/js/part.html:112 +#: templates/js/part.js:180 msgid "IPN" msgstr "" -#: part/templates/part/detail.html:32 templates/js/part.html:116 +#: part/templates/part/detail.html:32 templates/js/part.js:184 msgid "Revision" msgstr "" @@ -2609,7 +2862,7 @@ msgid "Variant Of" msgstr "" #: part/templates/part/detail.html:70 part/templates/part/set_category.html:15 -#: templates/js/part.html:359 +#: templates/js/part.js:405 msgid "Category" msgstr "" @@ -2617,7 +2870,7 @@ msgstr "" msgid "Default Supplier" msgstr "" -#: part/templates/part/detail.html:102 part/templates/part/params.html:24 +#: part/templates/part/detail.html:102 part/templates/part/params.html:26 msgid "Units" msgstr "" @@ -2625,7 +2878,7 @@ msgstr "" msgid "Minimum Stock" msgstr "" -#: part/templates/part/detail.html:114 templates/js/order.html:262 +#: part/templates/part/detail.html:114 templates/js/order.js:262 msgid "Creation Date" msgstr "" @@ -2637,96 +2890,99 @@ msgstr "" msgid "Responsible User" msgstr "" -#: part/templates/part/detail.html:136 +#: part/templates/part/detail.html:138 templates/js/table_filters.js:27 msgid "Virtual" msgstr "" -#: part/templates/part/detail.html:139 +#: part/templates/part/detail.html:141 msgid "Part is virtual (not a physical part)" msgstr "" -#: part/templates/part/detail.html:141 +#: part/templates/part/detail.html:143 msgid "Part is not a virtual part" msgstr "" -#: part/templates/part/detail.html:145 stock/forms.py:248 -#: templates/js/table_filters.html:188 +#: part/templates/part/detail.html:148 stock/forms.py:248 +#: templates/js/table_filters.js:23 templates/js/table_filters.js:248 msgid "Template" msgstr "" -#: part/templates/part/detail.html:148 +#: part/templates/part/detail.html:151 msgid "Part is a template part (variants can be made from this part)" msgstr "" -#: part/templates/part/detail.html:150 +#: part/templates/part/detail.html:153 msgid "Part is not a template part" msgstr "" -#: part/templates/part/detail.html:154 templates/js/table_filters.html:200 +#: part/templates/part/detail.html:158 templates/js/table_filters.js:260 msgid "Assembly" msgstr "" -#: part/templates/part/detail.html:157 +#: part/templates/part/detail.html:161 msgid "Part can be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:159 +#: part/templates/part/detail.html:163 msgid "Part cannot be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:163 templates/js/table_filters.html:204 +#: part/templates/part/detail.html:168 templates/js/table_filters.js:264 msgid "Component" msgstr "" -#: part/templates/part/detail.html:166 +#: part/templates/part/detail.html:171 msgid "Part can be used in assemblies" msgstr "" -#: part/templates/part/detail.html:168 +#: part/templates/part/detail.html:173 msgid "Part cannot be used in assemblies" msgstr "" -#: part/templates/part/detail.html:172 templates/js/table_filters.html:216 +#: part/templates/part/detail.html:178 templates/js/table_filters.js:31 +#: templates/js/table_filters.js:276 msgid "Trackable" msgstr "" -#: part/templates/part/detail.html:175 +#: part/templates/part/detail.html:181 msgid "Part stock is tracked by serial number" msgstr "" -#: part/templates/part/detail.html:177 +#: part/templates/part/detail.html:183 msgid "Part stock is not tracked by serial number" msgstr "" -#: part/templates/part/detail.html:181 +#: part/templates/part/detail.html:188 msgid "Purchaseable" msgstr "" -#: part/templates/part/detail.html:184 part/templates/part/detail.html:186 +#: part/templates/part/detail.html:191 part/templates/part/detail.html:193 msgid "Part can be purchased from external suppliers" msgstr "" -#: part/templates/part/detail.html:190 templates/js/table_filters.html:212 +#: part/templates/part/detail.html:198 templates/js/table_filters.js:272 msgid "Salable" msgstr "" -#: part/templates/part/detail.html:193 +#: part/templates/part/detail.html:201 msgid "Part can be sold to customers" msgstr "" -#: part/templates/part/detail.html:195 +#: part/templates/part/detail.html:203 msgid "Part cannot be sold to customers" msgstr "" -#: part/templates/part/detail.html:199 templates/js/table_filters.html:183 +#: part/templates/part/detail.html:214 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/templates/part/detail.html:202 +#: part/templates/part/detail.html:217 msgid "Part is active" msgstr "" -#: part/templates/part/detail.html:204 +#: part/templates/part/detail.html:219 msgid "Part is not active" msgstr "" @@ -2734,10 +2990,6 @@ msgstr "" msgid "Part Notes" msgstr "" -#: part/templates/part/orders.html:14 -msgid "Order Part" -msgstr "" - #: part/templates/part/params.html:8 msgid "Part Parameters" msgstr "" @@ -2746,21 +2998,17 @@ msgstr "" msgid "Add new parameter" msgstr "" -#: part/templates/part/params.html:14 templates/InvenTree/settings/part.html:27 +#: part/templates/part/params.html:15 templates/InvenTree/settings/part.html:28 msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:23 stock/models.py:1391 -#: templates/js/stock.html:112 +#: part/templates/part/params.html:25 stock/models.py:1415 +#: templates/js/stock.js:112 msgid "Value" msgstr "" -#: part/templates/part/params.html:36 -msgid "Edit" -msgstr "" - -#: part/templates/part/params.html:39 part/templates/part/supplier.html:17 -#: users/models.py:145 +#: part/templates/part/params.html:41 part/templates/part/supplier.html:19 +#: users/models.py:146 msgid "Delete" msgstr "" @@ -2780,8 +3028,8 @@ msgstr "" msgid "This part is a variant of" msgstr "" -#: part/templates/part/part_base.html:36 templates/js/company.html:153 -#: templates/js/part.html:336 +#: part/templates/part/part_base.html:36 templates/js/company.js:155 +#: templates/js/part.js:95 templates/js/part.js:172 msgid "Inactive" msgstr "" @@ -2790,19 +3038,19 @@ msgid "Star this part" msgstr "" #: part/templates/part/part_base.html:49 -#: stock/templates/stock/item_base.html:88 +#: stock/templates/stock/item_base.html:101 #: stock/templates/stock/location.html:29 msgid "Barcode actions" msgstr "" #: part/templates/part/part_base.html:51 -#: stock/templates/stock/item_base.html:90 +#: stock/templates/stock/item_base.html:103 #: stock/templates/stock/location.html:31 msgid "Show QR Code" msgstr "" #: part/templates/part/part_base.html:52 -#: stock/templates/stock/item_base.html:91 +#: stock/templates/stock/item_base.html:104 #: stock/templates/stock/location.html:32 msgid "Print Label" msgstr "" @@ -2831,7 +3079,7 @@ msgstr "" msgid "Delete part" msgstr "" -#: part/templates/part/part_base.html:124 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:124 templates/js/table_filters.js:111 msgid "In Stock" msgstr "" @@ -2891,8 +3139,8 @@ msgstr "" msgid "Part Stock" msgstr "" -#: part/templates/part/stock_count.html:7 templates/js/bom.html:197 -#: templates/js/part.html:396 +#: part/templates/part/stock_count.html:7 templates/js/bom.js:224 +#: templates/js/part.js:442 msgid "No Stock" msgstr "" @@ -2904,15 +3152,15 @@ msgstr "" msgid "Part Suppliers" msgstr "" -#: part/templates/part/supplier.html:17 +#: part/templates/part/supplier.html:19 msgid "Delete supplier parts" msgstr "" -#: part/templates/part/supplier.html:46 +#: part/templates/part/supplier.html:48 msgid "Create new supplier" msgstr "" -#: part/templates/part/supplier.html:52 +#: part/templates/part/supplier.html:54 msgid "Create new manufacturer" msgstr "" @@ -2932,7 +3180,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:304 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:317 msgid "Tests" msgstr "" @@ -2944,19 +3192,15 @@ msgstr "" msgid "Assemblies" msgstr "" -#: part/templates/part/used_in.html:43 -msgid "INACTIVE" -msgstr "" - #: part/templates/part/variants.html:11 msgid "Part Variants" msgstr "" -#: part/templates/part/variants.html:21 +#: part/templates/part/variants.html:22 msgid "Create new variant" msgstr "" -#: part/templates/part/variants.html:21 +#: part/templates/part/variants.html:23 msgid "New Variant" msgstr "" @@ -2964,172 +3208,196 @@ msgstr "" msgid "Add part attachment" msgstr "" -#: part/views.py:131 templates/attachment_table.html:32 +#: part/views.py:135 templates/attachment_table.html:34 msgid "Edit attachment" msgstr "" -#: part/views.py:137 +#: part/views.py:141 msgid "Part attachment updated" msgstr "" -#: part/views.py:152 +#: part/views.py:156 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:160 +#: part/views.py:164 msgid "Deleted part attachment" msgstr "" -#: part/views.py:169 +#: part/views.py:173 msgid "Create Test Template" msgstr "" -#: part/views.py:198 +#: part/views.py:202 msgid "Edit Test Template" msgstr "" -#: part/views.py:214 +#: part/views.py:218 msgid "Delete Test Template" msgstr "" -#: part/views.py:223 +#: part/views.py:227 msgid "Set Part Category" msgstr "" -#: part/views.py:273 +#: part/views.py:277 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:308 +#: part/views.py:312 msgid "Create Variant" msgstr "" -#: part/views.py:388 +#: part/views.py:394 msgid "Duplicate Part" msgstr "" -#: part/views.py:395 +#: part/views.py:401 msgid "Copied part" msgstr "" -#: part/views.py:520 +#: part/views.py:455 part/views.py:585 +msgid "Possible matches exist - confirm creation of new part" +msgstr "" + +#: part/views.py:520 templates/js/stock.js:840 +msgid "Create New Part" +msgstr "" + +#: part/views.py:527 msgid "Created new part" msgstr "" -#: part/views.py:735 +#: part/views.py:743 msgid "Part QR Code" msgstr "" -#: part/views.py:754 +#: part/views.py:762 msgid "Upload Part Image" msgstr "" -#: part/views.py:762 part/views.py:799 +#: part/views.py:770 part/views.py:807 msgid "Updated part image" msgstr "" -#: part/views.py:771 +#: part/views.py:779 msgid "Select Part Image" msgstr "" -#: part/views.py:802 +#: part/views.py:810 msgid "Part image not found" msgstr "" -#: part/views.py:813 +#: part/views.py:821 msgid "Edit Part Properties" msgstr "" -#: part/views.py:837 +#: part/views.py:848 +msgid "Duplicate BOM" +msgstr "" + +#: part/views.py:879 +msgid "Confirm duplication of BOM from parent" +msgstr "" + +#: part/views.py:900 msgid "Validate BOM" msgstr "" -#: part/views.py:1004 +#: part/views.py:923 +msgid "Confirm that the BOM is valid" +msgstr "" + +#: part/views.py:934 +msgid "Validated Bill of Materials" +msgstr "" + +#: part/views.py:1068 msgid "No BOM file provided" msgstr "" -#: part/views.py:1354 +#: part/views.py:1418 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1379 part/views.py:1382 +#: part/views.py:1443 part/views.py:1446 msgid "Select valid part" msgstr "" -#: part/views.py:1388 +#: part/views.py:1452 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1426 +#: part/views.py:1490 msgid "Select a part" msgstr "" -#: part/views.py:1432 +#: part/views.py:1496 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1436 +#: part/views.py:1500 msgid "Specify quantity" msgstr "" -#: part/views.py:1692 +#: part/views.py:1756 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1701 +#: part/views.py:1765 msgid "Part was deleted" msgstr "" -#: part/views.py:1710 +#: part/views.py:1774 msgid "Part Pricing" msgstr "" -#: part/views.py:1836 +#: part/views.py:1900 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1846 +#: part/views.py:1910 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1855 +#: part/views.py:1919 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1865 +#: part/views.py:1929 msgid "Create Part Parameter" msgstr "" -#: part/views.py:1917 +#: part/views.py:1981 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:1933 +#: part/views.py:1997 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1992 +#: part/views.py:2056 msgid "Edit Part Category" msgstr "" -#: part/views.py:2029 +#: part/views.py:2093 msgid "Delete Part Category" msgstr "" -#: part/views.py:2037 +#: part/views.py:2101 msgid "Part category was deleted" msgstr "" -#: part/views.py:2100 -msgid "Create BOM item" +#: part/views.py:2164 +msgid "Create BOM Item" msgstr "" -#: part/views.py:2168 +#: part/views.py:2232 msgid "Edit BOM item" msgstr "" -#: part/views.py:2218 +#: part/views.py:2282 msgid "Confim BOM item deletion" msgstr "" @@ -3161,6 +3429,10 @@ msgstr "" msgid "Asset file description" msgstr "" +#: stock/forms.py:111 +msgid "Enter unique serial numbers (or leave blank)" +msgstr "" + #: stock/forms.py:191 msgid "Label" msgstr "" @@ -3201,10 +3473,6 @@ msgstr "" msgid "Confirm removal of installed stock items" msgstr "" -#: stock/forms.py:364 -msgid "Destination" -msgstr "" - #: stock/forms.py:364 msgid "Destination stock location" msgstr "" @@ -3213,7 +3481,7 @@ msgstr "" msgid "Add note (required)" msgstr "" -#: stock/forms.py:370 stock/views.py:921 stock/views.py:1119 +#: stock/forms.py:370 stock/views.py:916 stock/views.py:1114 msgid "Confirm stock adjustment" msgstr "" @@ -3229,227 +3497,227 @@ msgstr "" msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:212 +#: stock/models.py:178 +msgid "Created stock item" +msgstr "" + +#: stock/models.py:214 msgid "StockItem with this serial number already exists" msgstr "" -#: stock/models.py:248 +#: stock/models.py:250 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:258 stock/models.py:267 +#: stock/models.py:260 stock/models.py:269 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:259 +#: stock/models.py:261 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:281 +#: stock/models.py:283 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:287 +#: stock/models.py:289 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:294 +#: stock/models.py:296 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:327 +#: stock/models.py:329 msgid "Parent Stock Item" msgstr "" -#: stock/models.py:336 +#: stock/models.py:338 msgid "Base part" msgstr "" -#: stock/models.py:345 +#: stock/models.py:347 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:350 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:352 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "" -#: stock/models.py:353 +#: stock/models.py:355 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:358 stock/templates/stock/item_base.html:199 +#: stock/models.py:360 stock/templates/stock/item_base.html:212 msgid "Installed In" msgstr "" -#: stock/models.py:361 +#: stock/models.py:363 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:377 +#: stock/models.py:379 msgid "Serial number for this item" msgstr "" -#: stock/models.py:389 +#: stock/models.py:391 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:393 +#: stock/models.py:395 msgid "Stock Quantity" msgstr "" -#: stock/models.py:402 +#: stock/models.py:404 msgid "Source Build" msgstr "" -#: stock/models.py:404 +#: stock/models.py:406 msgid "Build for this stock item" msgstr "" -#: stock/models.py:415 +#: stock/models.py:417 msgid "Source Purchase Order" msgstr "" -#: stock/models.py:418 +#: stock/models.py:420 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:424 +#: stock/models.py:426 msgid "Destination Sales Order" msgstr "" -#: stock/models.py:431 +#: stock/models.py:433 msgid "Destination Build Order" msgstr "" -#: stock/models.py:444 +#: stock/models.py:446 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:454 stock/templates/stock/item_notes.html:14 +#: stock/models.py:456 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:505 +#: stock/models.py:507 msgid "Assigned to Customer" msgstr "" -#: stock/models.py:507 +#: stock/models.py:509 msgid "Manually assigned to customer" msgstr "" -#: stock/models.py:520 +#: stock/models.py:522 msgid "Returned from customer" msgstr "" -#: stock/models.py:522 +#: stock/models.py:524 msgid "Returned to location" msgstr "" -#: stock/models.py:650 +#: stock/models.py:652 msgid "Installed into stock item" msgstr "" -#: stock/models.py:658 +#: stock/models.py:660 msgid "Installed stock item" msgstr "" -#: stock/models.py:682 +#: stock/models.py:684 msgid "Uninstalled stock item" msgstr "" -#: stock/models.py:701 +#: stock/models.py:703 msgid "Uninstalled into location" msgstr "" -#: stock/models.py:796 +#: stock/models.py:807 msgid "Part is not set as trackable" msgstr "" -#: stock/models.py:802 +#: stock/models.py:813 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:808 +#: stock/models.py:819 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:811 +#: stock/models.py:822 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:814 +#: stock/models.py:825 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:824 -msgid "Serial numbers already exist: " -msgstr "" - -#: stock/models.py:849 +#: stock/models.py:857 msgid "Add serial number" msgstr "" -#: stock/models.py:852 +#: stock/models.py:860 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:963 +#: stock/models.py:971 msgid "StockItem cannot be moved as it is not in stock" msgstr "" -#: stock/models.py:1292 +#: stock/models.py:1316 msgid "Tracking entry title" msgstr "" -#: stock/models.py:1294 +#: stock/models.py:1318 msgid "Entry notes" msgstr "" -#: stock/models.py:1296 +#: stock/models.py:1320 msgid "Link to external page for further information" msgstr "" -#: stock/models.py:1356 +#: stock/models.py:1380 msgid "Value must be provided for this test" msgstr "" -#: stock/models.py:1362 +#: stock/models.py:1386 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1379 +#: stock/models.py:1403 msgid "Test" msgstr "" -#: stock/models.py:1380 +#: stock/models.py:1404 msgid "Test name" msgstr "" -#: stock/models.py:1385 +#: stock/models.py:1409 msgid "Result" msgstr "" -#: stock/models.py:1386 templates/js/table_filters.html:111 +#: stock/models.py:1410 templates/js/table_filters.js:162 msgid "Test result" msgstr "" -#: stock/models.py:1392 +#: stock/models.py:1416 msgid "Test output value" msgstr "" -#: stock/models.py:1398 +#: stock/models.py:1422 msgid "Attachment" msgstr "" -#: stock/models.py:1399 +#: stock/models.py:1423 msgid "Test result attachment" msgstr "" -#: stock/models.py:1405 +#: stock/models.py:1429 msgid "Test notes" msgstr "" @@ -3457,137 +3725,154 @@ msgstr "" msgid "Stock Tracking Information" msgstr "" +#: stock/templates/stock/item.html:18 +msgid "New Entry" +msgstr "" + #: stock/templates/stock/item_attachments.html:10 msgid "Stock Item Attachments" msgstr "" #: stock/templates/stock/item_base.html:20 +msgid "This stock item is in production and cannot be edited." +msgstr "" + +#: stock/templates/stock/item_base.html:21 +msgid "Edit the stock item from the build view." +msgstr "" + +#: stock/templates/stock/item_base.html:34 msgid "This stock item has not passed all required tests" msgstr "" -#: stock/templates/stock/item_base.html:26 +#: stock/templates/stock/item_base.html:40 msgid "This stock item is allocated to Sales Order" msgstr "" -#: stock/templates/stock/item_base.html:32 +#: stock/templates/stock/item_base.html:46 msgid "This stock item is allocated to Build" msgstr "" -#: stock/templates/stock/item_base.html:38 +#: stock/templates/stock/item_base.html:52 msgid "" "This stock item is serialized - it has a unique serial number and the " "quantity cannot be adjusted." msgstr "" -#: stock/templates/stock/item_base.html:42 +#: stock/templates/stock/item_base.html:56 msgid "This stock item cannot be deleted as it has child items" msgstr "" -#: stock/templates/stock/item_base.html:46 +#: stock/templates/stock/item_base.html:60 msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" -#: stock/templates/stock/item_base.html:94 templates/js/barcode.html:283 -#: templates/js/barcode.html:288 +#: stock/templates/stock/item_base.html:107 templates/js/barcode.js:283 +#: templates/js/barcode.js:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:96 +#: stock/templates/stock/item_base.html:109 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:104 +#: stock/templates/stock/item_base.html:117 msgid "Stock adjustment actions" msgstr "" -#: stock/templates/stock/item_base.html:108 -#: stock/templates/stock/location.html:41 templates/stock_table.html:19 +#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/location.html:41 templates/stock_table.html:23 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:109 templates/stock_table.html:17 +#: stock/templates/stock/item_base.html:122 templates/stock_table.html:21 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:110 templates/stock_table.html:18 +#: stock/templates/stock/item_base.html:123 templates/stock_table.html:22 msgid "Remove stock" msgstr "" -#: stock/templates/stock/item_base.html:112 +#: stock/templates/stock/item_base.html:125 msgid "Transfer stock" msgstr "" -#: stock/templates/stock/item_base.html:114 +#: stock/templates/stock/item_base.html:127 msgid "Serialize stock" msgstr "" -#: stock/templates/stock/item_base.html:118 +#: stock/templates/stock/item_base.html:131 msgid "Assign to customer" msgstr "" -#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/item_base.html:134 msgid "Return to stock" msgstr "" -#: stock/templates/stock/item_base.html:125 templates/js/stock.html:934 +#: stock/templates/stock/item_base.html:138 templates/js/stock.js:981 msgid "Uninstall stock item" msgstr "" -#: stock/templates/stock/item_base.html:125 +#: stock/templates/stock/item_base.html:138 msgid "Uninstall" msgstr "" -#: stock/templates/stock/item_base.html:134 +#: stock/templates/stock/item_base.html:147 #: stock/templates/stock/location.html:38 msgid "Stock actions" msgstr "" -#: stock/templates/stock/item_base.html:137 +#: stock/templates/stock/item_base.html:150 msgid "Convert to variant" msgstr "" -#: stock/templates/stock/item_base.html:140 +#: stock/templates/stock/item_base.html:153 msgid "Duplicate stock item" msgstr "" -#: stock/templates/stock/item_base.html:142 +#: stock/templates/stock/item_base.html:155 msgid "Edit stock item" msgstr "" -#: stock/templates/stock/item_base.html:145 +#: stock/templates/stock/item_base.html:158 msgid "Delete stock item" msgstr "" -#: stock/templates/stock/item_base.html:151 +#: stock/templates/stock/item_base.html:164 msgid "Generate test report" msgstr "" -#: stock/templates/stock/item_base.html:159 +#: stock/templates/stock/item_base.html:172 msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:224 +#: stock/templates/stock/item_base.html:237 templates/js/build.js:426 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:231 +#: stock/templates/stock/item_base.html:244 msgid "Barcode Identifier" msgstr "" -#: stock/templates/stock/item_base.html:259 +#: stock/templates/stock/item_base.html:258 templates/js/build.js:626 +#: templates/navbar.html:25 +msgid "Build" +msgstr "" + +#: stock/templates/stock/item_base.html:272 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:284 +#: stock/templates/stock/item_base.html:297 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:289 +#: stock/templates/stock/item_base.html:302 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:293 +#: stock/templates/stock/item_base.html:306 msgid "No stocktake performed" msgstr "" @@ -3635,15 +3920,15 @@ msgstr "" msgid "Test Data" msgstr "" -#: stock/templates/stock/item_tests.html:17 +#: stock/templates/stock/item_tests.html:18 msgid "Delete Test Data" msgstr "" -#: stock/templates/stock/item_tests.html:19 +#: stock/templates/stock/item_tests.html:22 msgid "Add Test Data" msgstr "" -#: stock/templates/stock/item_tests.html:20 +#: stock/templates/stock/item_tests.html:25 msgid "Test Report" msgstr "" @@ -3707,7 +3992,7 @@ msgstr "" msgid "The following stock items will be uninstalled" msgstr "" -#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1315 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1310 msgid "Convert Stock Item" msgstr "" @@ -3751,202 +4036,210 @@ msgstr "" msgid "Add Stock Item Attachment" msgstr "" -#: stock/views.py:209 +#: stock/views.py:210 msgid "Edit Stock Item Attachment" msgstr "" -#: stock/views.py:226 +#: stock/views.py:227 msgid "Delete Stock Item Attachment" msgstr "" -#: stock/views.py:243 +#: stock/views.py:244 msgid "Assign to Customer" msgstr "" -#: stock/views.py:281 +#: stock/views.py:254 +msgid "Customer must be specified" +msgstr "" + +#: stock/views.py:278 msgid "Return to Stock" msgstr "" -#: stock/views.py:301 +#: stock/views.py:288 msgid "Specify a valid location" msgstr "" -#: stock/views.py:305 +#: stock/views.py:299 msgid "Stock item returned from customer" msgstr "" -#: stock/views.py:317 +#: stock/views.py:309 msgid "Select Label Template" msgstr "" -#: stock/views.py:340 +#: stock/views.py:332 msgid "Select valid label" msgstr "" -#: stock/views.py:404 +#: stock/views.py:396 msgid "Delete All Test Data" msgstr "" -#: stock/views.py:420 +#: stock/views.py:412 msgid "Confirm test data deletion" msgstr "" -#: stock/views.py:440 +#: stock/views.py:432 msgid "Add Test Result" msgstr "" -#: stock/views.py:478 +#: stock/views.py:473 msgid "Edit Test Result" msgstr "" -#: stock/views.py:496 +#: stock/views.py:491 msgid "Delete Test Result" msgstr "" -#: stock/views.py:508 +#: stock/views.py:503 msgid "Select Test Report Template" msgstr "" -#: stock/views.py:523 +#: stock/views.py:518 msgid "Select valid template" msgstr "" -#: stock/views.py:576 +#: stock/views.py:571 msgid "Stock Export Options" msgstr "" -#: stock/views.py:698 +#: stock/views.py:693 msgid "Stock Item QR Code" msgstr "" -#: stock/views.py:724 +#: stock/views.py:719 msgid "Install Stock Item" msgstr "" -#: stock/views.py:824 +#: stock/views.py:819 msgid "Uninstall Stock Items" msgstr "" -#: stock/views.py:932 +#: stock/views.py:927 msgid "Uninstalled stock items" msgstr "" -#: stock/views.py:957 +#: stock/views.py:952 msgid "Adjust Stock" msgstr "" -#: stock/views.py:1067 +#: stock/views.py:1062 msgid "Move Stock Items" msgstr "" -#: stock/views.py:1068 +#: stock/views.py:1063 msgid "Count Stock Items" msgstr "" -#: stock/views.py:1069 +#: stock/views.py:1064 msgid "Remove From Stock" msgstr "" -#: stock/views.py:1070 +#: stock/views.py:1065 msgid "Add Stock Items" msgstr "" -#: stock/views.py:1071 +#: stock/views.py:1066 msgid "Delete Stock Items" msgstr "" -#: stock/views.py:1099 +#: stock/views.py:1094 msgid "Must enter integer value" msgstr "" -#: stock/views.py:1104 +#: stock/views.py:1099 msgid "Quantity must be positive" msgstr "" -#: stock/views.py:1111 +#: stock/views.py:1106 #, python-brace-format msgid "Quantity must not exceed {x}" msgstr "" -#: stock/views.py:1190 +#: stock/views.py:1185 #, python-brace-format msgid "Added stock to {n} items" msgstr "" -#: stock/views.py:1205 +#: stock/views.py:1200 #, python-brace-format msgid "Removed stock from {n} items" msgstr "" -#: stock/views.py:1218 +#: stock/views.py:1213 #, python-brace-format msgid "Counted stock for {n} items" msgstr "" -#: stock/views.py:1246 +#: stock/views.py:1241 msgid "No items were moved" msgstr "" -#: stock/views.py:1249 +#: stock/views.py:1244 #, python-brace-format msgid "Moved {n} items to {dest}" msgstr "" -#: stock/views.py:1268 +#: stock/views.py:1263 #, python-brace-format msgid "Deleted {n} stock items" msgstr "" -#: stock/views.py:1280 +#: stock/views.py:1275 msgid "Edit Stock Item" msgstr "" -#: stock/views.py:1365 +#: stock/views.py:1360 msgid "Serialize Stock" msgstr "" -#: stock/views.py:1559 +#: stock/views.py:1454 templates/js/build.js:210 +msgid "Create new Stock Item" +msgstr "" + +#: stock/views.py:1553 msgid "Duplicate Stock Item" msgstr "" -#: stock/views.py:1625 +#: stock/views.py:1619 msgid "Invalid quantity" msgstr "" -#: stock/views.py:1628 +#: stock/views.py:1622 msgid "Quantity cannot be less than zero" msgstr "" -#: stock/views.py:1632 +#: stock/views.py:1626 msgid "Invalid part selection" msgstr "" -#: stock/views.py:1681 +#: stock/views.py:1674 #, python-brace-format msgid "Created {n} new stock items" msgstr "" -#: stock/views.py:1700 stock/views.py:1716 +#: stock/views.py:1693 stock/views.py:1709 msgid "Created new stock item" msgstr "" -#: stock/views.py:1735 +#: stock/views.py:1728 msgid "Delete Stock Location" msgstr "" -#: stock/views.py:1749 +#: stock/views.py:1742 msgid "Delete Stock Item" msgstr "" -#: stock/views.py:1761 +#: stock/views.py:1754 msgid "Delete Stock Tracking Entry" msgstr "" -#: stock/views.py:1780 +#: stock/views.py:1773 msgid "Edit Stock Tracking Entry" msgstr "" -#: stock/views.py:1790 +#: stock/views.py:1783 msgid "Add Stock Tracking Entry" msgstr "" @@ -4002,11 +4295,11 @@ msgstr "" msgid "Enter a search query" msgstr "" -#: templates/InvenTree/search.html:191 templates/js/stock.html:528 +#: templates/InvenTree/search.html:191 templates/js/stock.js:289 msgid "Shipped to customer" msgstr "" -#: templates/InvenTree/search.html:194 templates/js/stock.html:538 +#: templates/InvenTree/search.html:194 templates/js/stock.js:299 msgid "No stock location set" msgstr "" @@ -4026,7 +4319,7 @@ msgstr "" msgid "Currencies" msgstr "" -#: templates/InvenTree/settings/currency.html:17 +#: templates/InvenTree/settings/currency.html:18 msgid "New Currency" msgstr "" @@ -4042,15 +4335,15 @@ msgstr "" msgid "Part Parameter Templates" msgstr "" -#: templates/InvenTree/settings/part.html:43 +#: templates/InvenTree/settings/part.html:45 msgid "No part parameter templates found" msgstr "" -#: templates/InvenTree/settings/part.html:63 +#: templates/InvenTree/settings/part.html:65 msgid "Edit Template" msgstr "" -#: templates/InvenTree/settings/part.html:64 +#: templates/InvenTree/settings/part.html:66 msgid "Delete Template" msgstr "" @@ -4126,19 +4419,23 @@ msgstr "" msgid "User Information" msgstr "" -#: templates/InvenTree/settings/user.html:24 -msgid "Username" +#: templates/InvenTree/settings/user.html:21 +msgid "Change Password" msgstr "" #: templates/InvenTree/settings/user.html:28 -msgid "First Name" +msgid "Username" msgstr "" #: templates/InvenTree/settings/user.html:32 -msgid "Last Name" +msgid "First Name" msgstr "" #: templates/InvenTree/settings/user.html:36 +msgid "Last Name" +msgstr "" + +#: templates/InvenTree/settings/user.html:40 msgid "Email Address" msgstr "" @@ -4186,465 +4483,553 @@ msgstr "" msgid "Submit Bug Report" msgstr "" -#: templates/attachment_table.html:6 +#: templates/attachment_table.html:7 msgid "Add Attachment" msgstr "" -#: templates/attachment_table.html:15 +#: templates/attachment_table.html:17 msgid "File" msgstr "" -#: templates/attachment_table.html:16 +#: templates/attachment_table.html:18 msgid "Comment" msgstr "" -#: templates/attachment_table.html:17 +#: templates/attachment_table.html:19 msgid "Uploaded" msgstr "" -#: templates/attachment_table.html:35 +#: templates/attachment_table.html:37 msgid "Delete attachment" msgstr "" -#: templates/js/barcode.html:8 +#: templates/js/barcode.js:8 msgid "Scan barcode data here using wedge scanner" msgstr "" -#: templates/js/barcode.html:12 +#: templates/js/barcode.js:12 msgid "Barcode" msgstr "" -#: templates/js/barcode.html:20 +#: templates/js/barcode.js:20 msgid "Enter barcode data" msgstr "" -#: templates/js/barcode.html:42 +#: templates/js/barcode.js:42 msgid "Invalid server response" msgstr "" -#: templates/js/barcode.html:143 +#: templates/js/barcode.js:143 msgid "Scan barcode data below" msgstr "" -#: templates/js/barcode.html:217 templates/js/barcode.html:263 +#: templates/js/barcode.js:217 templates/js/barcode.js:263 msgid "Unknown response from server" msgstr "" -#: templates/js/barcode.html:239 +#: templates/js/barcode.js:239 msgid "Link Barcode to Stock Item" msgstr "" -#: templates/js/barcode.html:285 +#: templates/js/barcode.js:285 msgid "" "This will remove the association between this stock item and the barcode" msgstr "" -#: templates/js/barcode.html:291 +#: templates/js/barcode.js:291 msgid "Unlink" msgstr "" -#: templates/js/barcode.html:350 +#: templates/js/barcode.js:350 msgid "Remove stock item" msgstr "" -#: templates/js/barcode.html:397 +#: templates/js/barcode.js:397 msgid "Enter notes" msgstr "" -#: templates/js/barcode.html:399 +#: templates/js/barcode.js:399 msgid "Enter optional notes for stock transfer" msgstr "" -#: templates/js/barcode.html:404 +#: templates/js/barcode.js:404 msgid "Check Stock Items into Location" msgstr "" -#: templates/js/barcode.html:408 +#: templates/js/barcode.js:408 msgid "Check In" msgstr "" -#: templates/js/barcode.html:466 +#: templates/js/barcode.js:466 msgid "Server error" msgstr "" -#: templates/js/barcode.html:485 +#: templates/js/barcode.js:485 msgid "Stock Item already scanned" msgstr "" -#: templates/js/barcode.html:489 +#: templates/js/barcode.js:489 msgid "Stock Item already in this location" msgstr "" -#: templates/js/barcode.html:496 +#: templates/js/barcode.js:496 msgid "Added stock item" msgstr "" -#: templates/js/barcode.html:503 +#: templates/js/barcode.js:503 msgid "Barcode does not match Stock Item" msgstr "" -#: templates/js/bom.html:132 +#: templates/js/bom.js:159 msgid "Open subassembly" msgstr "" -#: templates/js/bom.html:173 +#: templates/js/bom.js:200 msgid "Optional" msgstr "" -#: templates/js/bom.html:188 templates/js/build.html:133 -msgid "Available" -msgstr "" - -#: templates/js/bom.html:213 +#: templates/js/bom.js:240 msgid "No pricing available" msgstr "" -#: templates/js/bom.html:232 +#: templates/js/bom.js:259 templates/js/build.js:555 msgid "Actions" msgstr "" -#: templates/js/bom.html:240 +#: templates/js/bom.js:267 msgid "Validate BOM Item" msgstr "" -#: templates/js/bom.html:242 +#: templates/js/bom.js:269 msgid "This line has been validated" msgstr "" -#: templates/js/bom.html:244 +#: templates/js/bom.js:271 msgid "Edit BOM Item" msgstr "" -#: templates/js/bom.html:246 +#: templates/js/bom.js:273 msgid "Delete BOM Item" msgstr "" -#: templates/js/build.html:24 +#: templates/js/bom.js:346 templates/js/build.js:289 +msgid "No BOM items found" +msgstr "" + +#: templates/js/bom.js:491 +msgid "INACTIVE" +msgstr "" + +#: templates/js/bom.js:505 +msgid "Uses" +msgstr "" + +#: templates/js/bom.js:516 +msgid "No matching parts found" +msgstr "" + +#: templates/js/build.js:56 +msgid "Auto-allocate stock items to this output" +msgstr "" + +#: templates/js/build.js:62 +msgid "Complete build output" +msgstr "" + +#: templates/js/build.js:71 +msgid "Unallocate stock from build output" +msgstr "" + +#: templates/js/build.js:77 +msgid "Delete build output" +msgstr "" + +#: templates/js/build.js:209 templates/stock_table.html:13 +msgid "New Stock Item" +msgstr "" + +#: templates/js/build.js:477 +msgid "Required Part" +msgstr "" + +#: templates/js/build.js:498 +msgid "Quantity Per" +msgstr "" + +#: templates/js/build.js:562 +msgid "Build stock" +msgstr "" + +#: templates/js/build.js:566 templates/stock_table.html:25 +msgid "Order stock" +msgstr "" + +#: templates/js/build.js:569 +msgid "Allocate stock" +msgstr "" + +#: templates/js/build.js:610 msgid "No builds matching query" msgstr "" -#: templates/js/build.html:122 +#: templates/js/build.js:720 msgid "No parts allocated for" msgstr "" -#: templates/js/company.html:75 +#: templates/js/company.js:75 msgid "Parts Supplied" msgstr "" -#: templates/js/company.html:84 +#: templates/js/company.js:84 msgid "Parts Manufactured" msgstr "" -#: templates/js/company.html:96 +#: templates/js/company.js:96 msgid "No company information found" msgstr "" -#: templates/js/company.html:128 +#: templates/js/company.js:129 msgid "No supplier parts found" msgstr "" -#: templates/js/company.html:145 templates/js/part.html:314 +#: templates/js/company.js:147 templates/js/part.js:79 templates/js/part.js:164 msgid "Template part" msgstr "" -#: templates/js/company.html:149 templates/js/part.html:318 +#: templates/js/company.js:151 templates/js/part.js:83 templates/js/part.js:168 msgid "Assembled part" msgstr "" -#: templates/js/company.html:206 +#: templates/js/company.js:208 msgid "Link" msgstr "" -#: templates/js/order.html:128 +#: templates/js/order.js:128 msgid "No purchase orders found" msgstr "" -#: templates/js/order.html:180 templates/js/stock.html:643 +#: templates/js/order.js:180 templates/js/stock.js:677 msgid "Date" msgstr "" -#: templates/js/order.html:210 +#: templates/js/order.js:210 msgid "No sales orders found" msgstr "" -#: templates/js/order.html:267 +#: templates/js/order.js:267 msgid "Shipment Date" msgstr "" -#: templates/js/part.html:137 -msgid "No variants found" +#: templates/js/part.js:71 templates/js/part.js:156 +msgid "Trackable part" msgstr "" -#: templates/js/part.html:223 templates/js/part.html:411 -msgid "No parts found" +#: templates/js/part.js:75 templates/js/part.js:160 +msgid "Virtual part" msgstr "" -#: templates/js/part.html:275 templates/js/stock.html:409 -#: templates/js/stock.html:966 -msgid "Select" -msgstr "" - -#: templates/js/part.html:322 +#: templates/js/part.js:87 msgid "Starred part" msgstr "" -#: templates/js/part.html:326 +#: templates/js/part.js:91 msgid "Salable part" msgstr "" -#: templates/js/part.html:365 +#: templates/js/part.js:205 +msgid "No variants found" +msgstr "" + +#: templates/js/part.js:291 templates/js/part.js:457 +msgid "No parts found" +msgstr "" + +#: templates/js/part.js:343 templates/js/stock.js:456 +#: templates/js/stock.js:1013 +msgid "Select" +msgstr "" + +#: templates/js/part.js:411 msgid "No category" msgstr "" -#: templates/js/part.html:383 templates/js/table_filters.html:196 +#: templates/js/part.js:429 templates/js/table_filters.js:256 msgid "Low stock" msgstr "" -#: templates/js/part.html:392 +#: templates/js/part.js:438 msgid "Building" msgstr "" -#: templates/js/part.html:471 +#: templates/js/part.js:517 msgid "YES" msgstr "" -#: templates/js/part.html:473 +#: templates/js/part.js:519 msgid "NO" msgstr "" -#: templates/js/part.html:507 +#: templates/js/part.js:553 msgid "No test templates matching query" msgstr "" -#: templates/js/part.html:558 templates/js/stock.html:63 +#: templates/js/part.js:604 templates/js/stock.js:63 msgid "Edit test result" msgstr "" -#: templates/js/part.html:559 templates/js/stock.html:64 +#: templates/js/part.js:605 templates/js/stock.js:64 msgid "Delete test result" msgstr "" -#: templates/js/part.html:565 +#: templates/js/part.js:611 msgid "This test is defined for a parent part" msgstr "" -#: templates/js/stock.html:26 +#: templates/js/stock.js:26 msgid "PASS" msgstr "" -#: templates/js/stock.html:28 +#: templates/js/stock.js:28 msgid "FAIL" msgstr "" -#: templates/js/stock.html:33 +#: templates/js/stock.js:33 msgid "NO RESULT" msgstr "" -#: templates/js/stock.html:59 +#: templates/js/stock.js:59 msgid "Add test result" msgstr "" -#: templates/js/stock.html:78 +#: templates/js/stock.js:78 msgid "No test results found" msgstr "" -#: templates/js/stock.html:120 +#: templates/js/stock.js:120 msgid "Test Date" msgstr "" -#: templates/js/stock.html:263 +#: templates/js/stock.js:281 +msgid "In production" +msgstr "" + +#: templates/js/stock.js:285 +msgid "Installed in Stock Item" +msgstr "" + +#: templates/js/stock.js:293 +msgid "Assigned to Sales Order" +msgstr "" + +#: templates/js/stock.js:313 msgid "No stock items matching query" msgstr "" -#: templates/js/stock.html:361 templates/js/stock.html:376 +#: templates/js/stock.js:424 msgid "Undefined location" msgstr "" -#: templates/js/stock.html:469 +#: templates/js/stock.js:518 +msgid "Stock item is in production" +msgstr "" + +#: templates/js/stock.js:523 +msgid "Stock item assigned to sales order" +msgstr "" + +#: templates/js/stock.js:526 +msgid "Stock item assigned to customer" +msgstr "" + +#: templates/js/stock.js:530 msgid "Stock item has been allocated" msgstr "" -#: templates/js/stock.html:473 -msgid "Stock item has been assigned to customer" -msgstr "" - -#: templates/js/stock.html:476 -msgid "Stock item was assigned to a build order" -msgstr "" - -#: templates/js/stock.html:478 -msgid "Stock item was assigned to a sales order" -msgstr "" - -#: templates/js/stock.html:483 +#: templates/js/stock.js:534 msgid "Stock item has been installed in another item" msgstr "" -#: templates/js/stock.html:490 +#: templates/js/stock.js:541 msgid "Stock item has been rejected" msgstr "" -#: templates/js/stock.html:494 +#: templates/js/stock.js:545 msgid "Stock item is lost" msgstr "" -#: templates/js/stock.html:498 templates/js/table_filters.html:60 +#: templates/js/stock.js:549 templates/js/table_filters.js:106 msgid "Depleted" msgstr "" -#: templates/js/stock.html:523 -msgid "Installed in Stock Item " -msgstr "" - -#: templates/js/stock.html:531 -msgid "Assigned to sales order" -msgstr "" - -#: templates/js/stock.html:709 +#: templates/js/stock.js:743 msgid "No user information" msgstr "" -#: templates/js/stock.html:793 -msgid "Create New Part" -msgstr "" - -#: templates/js/stock.html:805 +#: templates/js/stock.js:852 msgid "Create New Location" msgstr "" -#: templates/js/stock.html:904 +#: templates/js/stock.js:951 msgid "Serial" msgstr "" -#: templates/js/stock.html:997 templates/js/table_filters.html:70 +#: templates/js/stock.js:1044 templates/js/table_filters.js:121 msgid "Installed" msgstr "" -#: templates/js/stock.html:1022 +#: templates/js/stock.js:1069 msgid "Install item" msgstr "" -#: templates/js/table_filters.html:19 templates/js/table_filters.html:80 +#: templates/js/table_filters.js:41 +msgid "Trackable Part" +msgstr "" + +#: templates/js/table_filters.js:45 +msgid "Validated" +msgstr "" + +#: templates/js/table_filters.js:65 templates/js/table_filters.js:131 msgid "Is Serialized" msgstr "" -#: templates/js/table_filters.html:22 templates/js/table_filters.html:87 +#: templates/js/table_filters.js:68 templates/js/table_filters.js:138 msgid "Serial number GTE" msgstr "" -#: templates/js/table_filters.html:23 templates/js/table_filters.html:88 +#: templates/js/table_filters.js:69 templates/js/table_filters.js:139 msgid "Serial number greater than or equal to" msgstr "" -#: templates/js/table_filters.html:26 templates/js/table_filters.html:91 +#: templates/js/table_filters.js:72 templates/js/table_filters.js:142 msgid "Serial number LTE" msgstr "" -#: templates/js/table_filters.html:27 templates/js/table_filters.html:92 +#: templates/js/table_filters.js:73 templates/js/table_filters.js:143 msgid "Serial number less than or equal to" msgstr "" -#: templates/js/table_filters.html:30 templates/js/table_filters.html:31 -#: templates/js/table_filters.html:83 templates/js/table_filters.html:84 +#: templates/js/table_filters.js:76 templates/js/table_filters.js:77 +#: templates/js/table_filters.js:134 templates/js/table_filters.js:135 msgid "Serial number" msgstr "" -#: templates/js/table_filters.html:35 templates/js/table_filters.html:101 +#: templates/js/table_filters.js:81 templates/js/table_filters.js:152 msgid "Batch code" msgstr "" -#: templates/js/table_filters.html:45 +#: templates/js/table_filters.js:91 templates/js/table_filters.js:223 msgid "Active parts" msgstr "" -#: templates/js/table_filters.html:46 +#: templates/js/table_filters.js:92 msgid "Show stock for active parts" msgstr "" -#: templates/js/table_filters.html:50 +#: templates/js/table_filters.js:96 msgid "Is allocated" msgstr "" -#: templates/js/table_filters.html:51 +#: templates/js/table_filters.js:97 msgid "Item has been alloacted" msgstr "" -#: templates/js/table_filters.html:55 +#: templates/js/table_filters.js:101 msgid "Include sublocations" msgstr "" -#: templates/js/table_filters.html:56 +#: templates/js/table_filters.js:102 msgid "Include stock in sublocations" msgstr "" -#: templates/js/table_filters.html:61 +#: templates/js/table_filters.js:107 msgid "Show stock items which are depleted" msgstr "" -#: templates/js/table_filters.html:66 +#: templates/js/table_filters.js:112 msgid "Show items which are in stock" msgstr "" -#: templates/js/table_filters.html:71 +#: templates/js/table_filters.js:116 +msgid "In Production" +msgstr "" + +#: templates/js/table_filters.js:117 +msgid "Show items which are in production" +msgstr "" + +#: templates/js/table_filters.js:122 msgid "Show stock items which are installed in another item" msgstr "" -#: templates/js/table_filters.html:75 +#: templates/js/table_filters.js:126 msgid "Sent to customer" msgstr "" -#: templates/js/table_filters.html:76 +#: templates/js/table_filters.js:127 msgid "Show items which have been assigned to a customer" msgstr "" -#: templates/js/table_filters.html:96 templates/js/table_filters.html:97 +#: templates/js/table_filters.js:147 templates/js/table_filters.js:148 msgid "Stock status" msgstr "" -#: templates/js/table_filters.html:130 +#: templates/js/table_filters.js:181 msgid "Build status" msgstr "" -#: templates/js/table_filters.html:145 templates/js/table_filters.html:158 +#: templates/js/table_filters.js:196 templates/js/table_filters.js:209 msgid "Order status" msgstr "" -#: templates/js/table_filters.html:150 templates/js/table_filters.html:163 +#: templates/js/table_filters.js:201 templates/js/table_filters.js:214 msgid "Outstanding" msgstr "" -#: templates/js/table_filters.html:173 +#: templates/js/table_filters.js:233 msgid "Include subcategories" msgstr "" -#: templates/js/table_filters.html:174 +#: templates/js/table_filters.js:234 msgid "Include parts in subcategories" msgstr "" -#: templates/js/table_filters.html:178 +#: templates/js/table_filters.js:238 msgid "Has IPN" msgstr "" -#: templates/js/table_filters.html:179 +#: templates/js/table_filters.js:239 msgid "Part has internal part number" msgstr "" -#: templates/js/table_filters.html:184 +#: templates/js/table_filters.js:244 msgid "Show active parts" msgstr "" -#: templates/js/table_filters.html:192 +#: templates/js/table_filters.js:252 msgid "Stock available" msgstr "" -#: templates/js/table_filters.html:208 +#: templates/js/table_filters.js:268 msgid "Starred" msgstr "" -#: templates/js/table_filters.html:220 +#: templates/js/table_filters.js:280 msgid "Purchasable" msgstr "" +#: templates/modals.html:13 templates/modals.html:35 +msgid "Form errors exist" +msgstr "" + +#: templates/modals.html:18 templates/modals.html:40 +msgid "Close" +msgstr "" + +#: templates/modals.html:19 templates/modals.html:41 +msgid "Submit" +msgstr "" + #: templates/navbar.html:29 msgid "Buy" msgstr "" @@ -4685,39 +5070,35 @@ msgstr "" msgid "Export Stock Information" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Add to selected stock items" msgstr "" -#: templates/stock_table.html:18 +#: templates/stock_table.html:22 msgid "Remove from selected stock items" msgstr "" -#: templates/stock_table.html:19 +#: templates/stock_table.html:23 msgid "Stocktake selected stock items" msgstr "" -#: templates/stock_table.html:20 +#: templates/stock_table.html:24 msgid "Move selected stock items" msgstr "" -#: templates/stock_table.html:20 +#: templates/stock_table.html:24 msgid "Move stock" msgstr "" -#: templates/stock_table.html:21 +#: templates/stock_table.html:25 msgid "Order selected items" msgstr "" -#: templates/stock_table.html:21 -msgid "Order stock" -msgstr "" - -#: templates/stock_table.html:24 +#: templates/stock_table.html:28 msgid "Delete selected items" msgstr "" -#: templates/stock_table.html:24 +#: templates/stock_table.html:28 msgid "Delete Stock" msgstr "" @@ -4741,38 +5122,38 @@ msgstr "" msgid "Important dates" msgstr "" -#: users/models.py:128 +#: users/models.py:129 msgid "Permission set" msgstr "" -#: users/models.py:136 +#: users/models.py:137 msgid "Group" msgstr "" -#: users/models.py:139 +#: users/models.py:140 msgid "View" msgstr "" -#: users/models.py:139 +#: users/models.py:140 msgid "Permission to view items" msgstr "" -#: users/models.py:141 +#: users/models.py:142 msgid "Add" msgstr "" -#: users/models.py:141 +#: users/models.py:142 msgid "Permission to add items" msgstr "" -#: users/models.py:143 +#: users/models.py:144 msgid "Change" msgstr "" -#: users/models.py:143 +#: users/models.py:144 msgid "Permissions to edit items" msgstr "" -#: users/models.py:145 +#: users/models.py:146 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 a5f56143f0..aacb4b74ec 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-10-25 10:54+0000\n" +"POT-Creation-Date: 2020-11-03 10:02+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -26,7 +26,7 @@ msgstr "" msgid "No matching action found" msgstr "" -#: InvenTree/forms.py:102 build/forms.py:49 +#: InvenTree/forms.py:102 build/forms.py:82 build/forms.py:170 msgid "Confirm" msgstr "" @@ -46,7 +46,7 @@ msgstr "" msgid "Apply Theme" msgstr "" -#: InvenTree/helpers.py:361 order/models.py:187 order/models.py:261 +#: InvenTree/helpers.py:361 order/models.py:187 order/models.py:269 msgid "Invalid quantity provided" msgstr "" @@ -86,12 +86,12 @@ msgstr "" msgid "File comment" msgstr "" -#: InvenTree/models.py:68 templates/js/stock.html:700 +#: InvenTree/models.py:68 templates/js/stock.js:734 msgid "User" msgstr "" -#: InvenTree/models.py:106 part/templates/part/params.html:22 -#: templates/js/part.html:81 +#: InvenTree/models.py:106 part/templates/part/params.html:24 +#: templates/js/part.js:129 msgid "Name" msgstr "" @@ -116,7 +116,7 @@ msgid "Polish" msgstr "" #: InvenTree/status_codes.py:94 InvenTree/status_codes.py:135 -#: InvenTree/status_codes.py:222 templates/js/table_filters.html:135 +#: InvenTree/status_codes.py:222 msgid "Pending" msgstr "" @@ -168,10 +168,8 @@ msgstr "" msgid "Rejected" msgstr "" -#: InvenTree/status_codes.py:223 build/templates/build/allocate.html:358 -#: order/templates/order/sales_order_detail.html:223 -#: part/templates/part/tabs.html:23 templates/js/build.html:140 -msgid "Allocated" +#: InvenTree/status_codes.py:223 +msgid "Production" msgstr "" #: InvenTree/validators.py:39 @@ -204,7 +202,27 @@ msgstr "" msgid "Overage must be an integer value or a percentage" msgstr "" -#: InvenTree/views.py:707 +#: InvenTree/views.py:493 +msgid "Delete Item" +msgstr "" + +#: InvenTree/views.py:542 +msgid "Check box to confirm item deletion" +msgstr "" + +#: InvenTree/views.py:557 templates/InvenTree/settings/user.html:18 +msgid "Edit User Information" +msgstr "" + +#: InvenTree/views.py:568 templates/InvenTree/settings/user.html:22 +msgid "Set Password" +msgstr "" + +#: InvenTree/views.py:587 +msgid "Password fields must match" +msgstr "" + +#: InvenTree/views.py:757 msgid "Database Statistics" msgstr "" @@ -248,124 +266,190 @@ msgstr "" msgid "Barcode associated with StockItem" msgstr "" -#: build/forms.py:28 +#: build/forms.py:32 msgid "Build Order reference" msgstr "" -#: build/forms.py:70 -msgid "Location of completed parts" +#: 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 +#: order/templates/order/order_wizard/select_parts.html:32 +#: order/templates/order/purchase_order_detail.html:178 +#: 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 +#: stock/templates/stock/item_base.html:40 +#: stock/templates/stock/item_base.html:46 +#: stock/templates/stock/item_base.html:197 +#: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.js:338 +#: templates/js/bom.js:189 templates/js/build.js:404 templates/js/stock.js:725 +#: templates/js/stock.js:953 +msgid "Quantity" msgstr "" -#: build/forms.py:74 +#: build/forms.py:71 +msgid "Enter quantity for build output" +msgstr "" + +#: build/forms.py:75 stock/forms.py:111 msgid "Serial numbers" msgstr "" -#: build/forms.py:76 stock/forms.py:111 -msgid "Enter unique serial numbers (or leave blank)" +#: build/forms.py:77 +msgid "Enter serial numbers for build outputs" msgstr "" -#: build/forms.py:79 +#: build/forms.py:83 +msgid "Confirm creation of build outut" +msgstr "" + +#: build/forms.py:103 +msgid "Confirm deletion of build output" +msgstr "" + +#: build/forms.py:124 +msgid "Confirm unallocation of stock" +msgstr "" + +#: build/forms.py:148 +msgid "Confirm stock allocation" +msgstr "" + +#: build/forms.py:171 +msgid "Mark build as complete" +msgstr "" + +#: build/forms.py:195 +msgid "Location of completed parts" +msgstr "" + +#: build/forms.py:200 +msgid "Confirm completion with incomplete stock allocation" +msgstr "" + +#: build/forms.py:203 msgid "Confirm build completion" msgstr "" -#: build/models.py:54 build/templates/build/build_base.html:8 +#: build/forms.py:223 build/views.py:68 +msgid "Confirm build cancellation" +msgstr "" + +#: build/forms.py:237 +msgid "Select quantity of stock to allocate" +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:214 +#: stock/templates/stock/item_base.html:227 msgid "Build Order" msgstr "" -#: build/models.py:55 build/templates/build/index.html:6 +#: build/models.py:57 build/templates/build/index.html:6 #: build/templates/build/index.html:14 order/templates/order/so_builds.html:11 #: order/templates/order/so_tabs.html:9 part/templates/part/tabs.html:31 #: templates/InvenTree/settings/tabs.html:28 users/models.py:30 msgid "Build Orders" msgstr "" -#: build/models.py:77 -msgid "Build quantity must be integer value for trackable parts" -msgstr "" - -#: build/models.py:86 build/templates/build/build_base.html:73 +#: build/models.py:72 msgid "Build Order Reference" msgstr "" -#: build/models.py:87 build/templates/build/allocate.html:342 -#: order/templates/order/purchase_order_detail.html:172 -#: templates/js/bom.html:154 +#: build/models.py:73 order/templates/order/purchase_order_detail.html:173 +#: templates/js/bom.js:181 templates/js/build.js:493 msgid "Reference" msgstr "" -#: build/models.py:94 build/templates/build/allocate.html:337 +#: build/models.py:80 build/templates/build/detail.html:19 #: company/templates/company/supplier_part_base.html:61 #: company/templates/company/supplier_part_detail.html:27 -#: order/templates/order/purchase_order_detail.html:159 +#: order/templates/order/purchase_order_detail.html:160 #: part/templates/part/detail.html:51 part/templates/part/set_category.html:14 -#: templates/InvenTree/search.html:147 templates/js/bom.html:147 -#: templates/js/build.html:56 templates/js/company.html:56 -#: templates/js/order.html:167 templates/js/order.html:249 -#: templates/js/part.html:120 templates/js/part.html:203 -#: templates/js/part.html:345 templates/js/part.html:526 -#: templates/js/stock.html:445 templates/js/stock.html:672 +#: 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/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" msgstr "" -#: build/models.py:97 +#: build/models.py:83 msgid "Brief description of the build" msgstr "" -#: build/models.py:105 build/templates/build/build_base.html:94 +#: build/models.py:91 build/templates/build/build_base.html:94 +#: build/templates/build/detail.html:75 msgid "Parent Build" msgstr "" -#: build/models.py:106 -msgid "Parent build to which this build is allocated" +#: build/models.py:92 +msgid "BuildOrder to which this build is allocated" msgstr "" -#: build/models.py:111 build/templates/build/allocate.html:329 -#: build/templates/build/auto_allocate.html:19 -#: build/templates/build/build_base.html:78 -#: build/templates/build/detail.html:22 order/models.py:501 +#: 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 #: order/templates/order/order_wizard/select_parts.html:30 -#: order/templates/order/purchase_order_detail.html:147 +#: order/templates/order/purchase_order_detail.html:148 #: order/templates/order/receive_parts.html:19 part/models.py:293 #: part/templates/part/part_app_base.html:7 #: part/templates/part/set_category.html:13 templates/InvenTree/search.html:133 -#: templates/js/barcode.html:336 templates/js/bom.html:124 -#: templates/js/build.html:61 templates/js/company.html:137 -#: templates/js/part.html:184 templates/js/part.html:289 -#: templates/js/stock.html:421 templates/js/stock.html:978 +#: templates/js/barcode.js:336 templates/js/bom.js:147 templates/js/bom.js:484 +#: templates/js/build.js:647 templates/js/company.js:138 +#: templates/js/part.js:252 templates/js/part.js:357 templates/js/stock.js:468 +#: templates/js/stock.js:1025 msgid "Part" msgstr "" -#: build/models.py:120 +#: build/models.py:105 msgid "Select part to build" msgstr "" -#: build/models.py:125 +#: build/models.py:110 msgid "Sales Order Reference" msgstr "" -#: build/models.py:129 +#: build/models.py:114 msgid "SalesOrder to which this build is allocated" msgstr "" -#: build/models.py:134 +#: build/models.py:119 msgid "Source Location" msgstr "" -#: build/models.py:138 +#: build/models.py:123 msgid "" "Select location to take stock from for this build (leave blank to take from " "any stock location)" msgstr "" -#: build/models.py:142 +#: build/models.py:128 +msgid "Destination Location" +msgstr "" + +#: build/models.py:132 +msgid "Select location where the completed items will be stored" +msgstr "" + +#: build/models.py:136 msgid "Build Quantity" msgstr "" +#: build/models.py:139 +msgid "Number of stock items to build" +msgstr "" + +#: build/models.py:143 +msgid "Completed items" +msgstr "" + #: build/models.py:145 -msgid "Number of parts to build" +msgid "Number of stock items which have been completed" msgstr "" #: build/models.py:149 part/templates/part/part_base.html:155 @@ -376,7 +460,7 @@ msgstr "" msgid "Build status code" msgstr "" -#: build/models.py:157 stock/models.py:387 +#: build/models.py:157 stock/models.py:389 msgid "Batch Code" msgstr "" @@ -384,26 +468,26 @@ msgstr "" msgid "Batch code for this build output" msgstr "" -#: build/models.py:176 build/templates/build/detail.html:55 +#: build/models.py:176 build/templates/build/detail.html:89 #: 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:381 stock/templates/stock/item_base.html:266 +#: stock/models.py:383 stock/templates/stock/item_base.html:279 msgid "External Link" msgstr "" -#: build/models.py:177 stock/models.py:383 +#: build/models.py:177 part/models.py:596 stock/models.py:385 msgid "Link to external URL" msgstr "" #: build/models.py:181 build/templates/build/tabs.html:14 company/models.py:314 #: company/templates/company/tabs.html:33 order/templates/order/po_tabs.html:18 -#: order/templates/order/purchase_order_detail.html:202 +#: order/templates/order/purchase_order_detail.html:203 #: order/templates/order/so_tabs.html:23 part/templates/part/tabs.html:70 -#: stock/forms.py:306 stock/forms.py:338 stock/forms.py:366 stock/models.py:453 -#: stock/models.py:1404 stock/templates/stock/tabs.html:26 -#: templates/js/barcode.html:391 templates/js/bom.html:223 -#: templates/js/stock.html:116 templates/js/stock.html:544 +#: 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 +#: templates/js/barcode.js:391 templates/js/bom.js:250 +#: templates/js/stock.js:116 templates/js/stock.js:578 msgid "Notes" msgstr "" @@ -411,138 +495,118 @@ msgstr "" msgid "Extra build notes" msgstr "" -#: build/models.py:520 +#: build/models.py:543 +msgid "No build output specified" +msgstr "" + +#: build/models.py:546 +msgid "Build output is already completed" +msgstr "" + +#: build/models.py:549 +msgid "Build output does not match Build Order" +msgstr "" + +#: build/models.py:620 +msgid "Completed build output" +msgstr "" + +#: build/models.py:858 +msgid "BuildItem must be unique for build, stock_item and install_into" +msgstr "" + +#: build/models.py:880 +msgid "Build item must specify a build output" +msgstr "" + +#: build/models.py:885 #, python-brace-format msgid "Selected stock item not found in BOM for part '{p}'" msgstr "" -#: build/models.py:523 +#: build/models.py:889 #, python-brace-format msgid "Allocated quantity ({n}) must not exceed available quantity ({q})" msgstr "" -#: build/models.py:529 order/models.py:585 +#: build/models.py:896 order/models.py:603 msgid "StockItem is over-allocated" msgstr "" -#: build/models.py:532 order/models.py:588 +#: build/models.py:900 order/models.py:606 msgid "Allocation quantity must be greater than zero" msgstr "" -#: build/models.py:535 +#: build/models.py:904 msgid "Quantity must be 1 for serialized stock" msgstr "" -#: build/models.py:564 +#: build/models.py:944 msgid "Build to allocate parts" msgstr "" -#: build/models.py:571 -msgid "Stock Item to allocate to build" +#: build/models.py:951 +msgid "Source stock item" msgstr "" -#: build/models.py:584 +#: build/models.py:964 msgid "Stock quantity to allocate to build" msgstr "" -#: build/templates/build/allocate.html:17 -#: company/templates/company/detail_part.html:22 order/views.py:804 -#: part/templates/part/category.html:122 +#: build/models.py:972 +msgid "Destination stock item" +msgstr "" + +#: build/templates/build/allocate.html:14 +msgid "Incomplete Build Ouputs" +msgstr "" + +#: build/templates/build/allocate.html:20 +msgid "Build order has been completed" +msgstr "" + +#: build/templates/build/allocate.html:24 +msgid "Create new build output" +msgstr "" + +#: build/templates/build/allocate.html:25 +msgid "Create New Output" +msgstr "" + +#: build/templates/build/allocate.html:28 +msgid "Order required parts" +msgstr "" + +#: build/templates/build/allocate.html:29 +#: company/templates/company/detail_part.html:28 order/views.py:801 +#: part/templates/part/category.html:125 msgid "Order Parts" msgstr "" -#: build/templates/build/allocate.html:18 -msgid "Automatically allocate stock" +#: build/templates/build/allocate.html:32 templates/js/build.js:574 +msgid "Unallocate stock" msgstr "" -#: build/templates/build/allocate.html:18 -msgid "Auto Allocate" +#: build/templates/build/allocate.html:33 build/views.py:341 build/views.py:778 +msgid "Unallocate Stock" msgstr "" -#: build/templates/build/allocate.html:19 -msgid "Unallocate" +#: build/templates/build/allocate.html:46 +msgid "Create a new build output" msgstr "" -#: build/templates/build/allocate.html:87 templates/stock_table.html:10 -msgid "New Stock Item" +#: build/templates/build/allocate.html:47 +msgid "No incomplete build outputs remain." msgstr "" -#: build/templates/build/allocate.html:88 stock/views.py:1459 -msgid "Create new Stock Item" +#: build/templates/build/allocate.html:48 +msgid "Create a new build output using the button above" msgstr "" -#: build/templates/build/allocate.html:170 -#: order/templates/order/sales_order_detail.html:70 -#: order/templates/order/sales_order_detail.html:152 stock/models.py:375 -#: stock/templates/stock/item_base.html:178 -msgid "Serial Number" -msgstr "" - -#: build/templates/build/allocate.html:172 -#: build/templates/build/auto_allocate.html:20 -#: build/templates/build/build_base.html:83 -#: build/templates/build/detail.html:27 -#: company/templates/company/supplier_part_pricing.html:73 -#: order/templates/order/order_wizard/select_parts.html:32 -#: order/templates/order/purchase_order_detail.html:177 -#: order/templates/order/sales_order_detail.html:72 -#: order/templates/order/sales_order_detail.html:154 -#: part/templates/part/allocation.html:16 -#: part/templates/part/allocation.html:49 -#: part/templates/part/sale_prices.html:80 stock/forms.py:297 -#: stock/templates/stock/item_base.html:26 -#: stock/templates/stock/item_base.html:32 -#: stock/templates/stock/item_base.html:184 -#: stock/templates/stock/stock_adjust.html:18 templates/js/barcode.html:338 -#: templates/js/bom.html:162 templates/js/build.html:72 -#: templates/js/stock.html:691 templates/js/stock.html:906 -msgid "Quantity" -msgstr "" - -#: build/templates/build/allocate.html:186 -#: build/templates/build/auto_allocate.html:21 stock/forms.py:336 -#: stock/templates/stock/item_base.html:220 -#: stock/templates/stock/stock_adjust.html:17 -#: templates/InvenTree/search.html:183 templates/js/barcode.html:337 -#: templates/js/stock.html:519 -msgid "Location" -msgstr "" - -#: build/templates/build/allocate.html:210 -#: order/templates/order/sales_order_detail.html:94 templates/js/build.html:144 -msgid "Edit stock allocation" -msgstr "" - -#: build/templates/build/allocate.html:211 -#: order/templates/order/sales_order_detail.html:95 templates/js/build.html:145 -msgid "Delete stock allocation" -msgstr "" - -#: build/templates/build/allocate.html:238 templates/js/bom.html:334 -msgid "No BOM items found" -msgstr "" - -#: build/templates/build/allocate.html:347 part/models.py:1401 -#: templates/js/part.html:530 templates/js/table_filters.html:121 -msgid "Required" -msgstr "" - -#: build/templates/build/allocate.html:356 -msgid "Assigned" -msgstr "" - -#: build/templates/build/allocate.html:394 -#: order/templates/order/sales_order_detail.html:273 -msgid "Buy parts" -msgstr "" - -#: build/templates/build/allocate.html:398 -#: order/templates/order/sales_order_detail.html:277 -msgid "Build parts" -msgstr "" - -#: build/templates/build/allocate.html:401 -msgid "Allocate stock" +#: build/templates/build/attachments.html:11 build/templates/build/tabs.html:17 +#: order/templates/order/po_tabs.html:11 order/templates/order/so_tabs.html:16 +#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 +msgid "Attachments" msgstr "" #: build/templates/build/auto_allocate.html:9 @@ -551,19 +615,22 @@ msgstr "" #: build/templates/build/auto_allocate.html:10 msgid "" -"Stock Items are selected for automatic allocation if there is only a single " -"stock item available." +"The following stock items will be allocated to the specified build output" msgstr "" -#: build/templates/build/auto_allocate.html:11 -msgid "The following stock items will be allocated to the build:" +#: build/templates/build/auto_allocate.html:18 stock/forms.py:336 +#: stock/templates/stock/item_base.html:233 +#: 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 +msgid "Location" msgstr "" -#: build/templates/build/auto_allocate.html:40 +#: build/templates/build/auto_allocate.html:37 msgid "No stock items found that can be automatically allocated to this build" msgstr "" -#: build/templates/build/auto_allocate.html:42 +#: build/templates/build/auto_allocate.html:39 msgid "Stock items will have to be manually allocated" msgstr "" @@ -580,7 +647,7 @@ msgstr "" #: order/templates/order/order_base.html:26 #: order/templates/order/sales_order_base.html:35 #: part/templates/part/category.html:13 part/templates/part/part_base.html:32 -#: stock/templates/stock/item_base.html:76 +#: stock/templates/stock/item_base.html:90 #: stock/templates/stock/location.html:12 msgid "Admin view" msgstr "" @@ -589,7 +656,7 @@ msgstr "" msgid "Edit Build" msgstr "" -#: build/templates/build/build_base.html:50 build/views.py:190 +#: build/templates/build/build_base.html:50 msgid "Complete Build" msgstr "" @@ -597,7 +664,7 @@ msgstr "" msgid "Cancel Build" msgstr "" -#: build/templates/build/build_base.html:59 build/views.py:456 +#: build/templates/build/build_base.html:59 build/views.py:767 msgid "Delete Build" msgstr "" @@ -605,124 +672,163 @@ msgstr "" msgid "Build Details" msgstr "" -#: build/templates/build/build_base.html:88 -#: build/templates/build/detail.html:42 +#: 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:298 templates/InvenTree/search.html:175 -#: templates/js/barcode.html:42 templates/js/build.html:77 -#: templates/js/order.html:172 templates/js/order.html:254 -#: templates/js/stock.html:506 templates/js/stock.html:914 +#: stock/templates/stock/item_base.html:311 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/stock.js:557 templates/js/stock.js:961 msgid "Status" msgstr "" -#: build/templates/build/build_base.html:101 order/models.py:499 +#: build/templates/build/build_base.html:88 +#: build/templates/build/detail.html:62 +msgid "Progress" +msgstr "" + +#: build/templates/build/build_base.html:101 +#: build/templates/build/detail.html:82 order/models.py:517 #: 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:208 templates/js/order.html:221 +#: stock/templates/stock/item_base.html:221 templates/js/order.js:221 msgid "Sales Order" msgstr "" -#: build/templates/build/build_base.html:107 -msgid "BOM Price" -msgstr "" - -#: build/templates/build/build_base.html:112 -msgid "BOM pricing is incomplete" -msgstr "" - -#: build/templates/build/build_base.html:115 -msgid "No pricing information" -msgstr "" - #: build/templates/build/build_output.html:9 build/templates/build/tabs.html:11 msgid "Build Outputs" msgstr "" -#: build/templates/build/complete.html:6 -#: stock/templates/stock/item_base.html:245 templates/js/build.html:40 -#: templates/navbar.html:25 -msgid "Build" +#: build/templates/build/build_output_create.html:7 +msgid "The Bill of Materials contains trackable parts" msgstr "" -#: build/templates/build/complete.html:10 -msgid "Build order allocation is complete" +#: build/templates/build/build_output_create.html:8 +msgid "Build outputs must be generated individually." msgstr "" -#: build/templates/build/complete.html:14 -msgid "Warning: Build order allocation is not complete" +#: build/templates/build/build_output_create.html:9 +msgid "Multiple build outputs will be created based on the quantity specified." +msgstr "" + +#: build/templates/build/build_output_create.html:15 +msgid "Trackable parts can have serial numbers specified" +msgstr "" + +#: build/templates/build/build_output_create.html:16 +msgid "Enter serial numbers to generate multiple single build outputs" +msgstr "" + +#: build/templates/build/cancel.html:5 +msgid "Are you sure you wish to cancel this build?" +msgstr "" + +#: build/templates/build/complete.html:8 +msgid "Build can be completed" +msgstr "" + +#: build/templates/build/complete.html:12 +msgid "Build cannot be completed" msgstr "" #: build/templates/build/complete.html:15 -msgid "" -"Build Order has not been fully allocated. Ensure that all Stock Items have " -"been allocated to the Build" +msgid "Incompleted build outputs remain" msgstr "" -#: build/templates/build/complete.html:20 -msgid "The following actions will be performed:" +#: build/templates/build/complete.html:18 +msgid "Required build quantity has not been completed" msgstr "" -#: build/templates/build/complete.html:22 -msgid "Remove allocated items from stock" +#: build/templates/build/complete_output.html:9 +msgid "Stock allocation is complete" msgstr "" -#: build/templates/build/complete.html:23 -msgid "Add completed items to stock" +#: build/templates/build/complete_output.html:13 +msgid "Stock allocation is incomplete" msgstr "" -#: build/templates/build/complete.html:29 +#: build/templates/build/complete_output.html:19 +msgid "parts have not been fully allocated" +msgstr "" + +#: build/templates/build/complete_output.html:40 msgid "The following items will be created" msgstr "" -#: build/templates/build/delete_build_item.html:6 -msgid "Are you sure you want to unallocate these parts?" +#: build/templates/build/create_build_item.html:7 +msgid "Select a stock item to allocate to the selected build output" msgstr "" -#: build/templates/build/detail.html:17 -msgid "Title" +#: build/templates/build/create_build_item.html:11 +msgid "The allocated stock will be installed into the following build output:" msgstr "" -#: build/templates/build/detail.html:31 +#: build/templates/build/create_build_item.html:19 +msgid "No stock available for" +msgstr "" + +#: build/templates/build/delete_build_item.html:8 +msgid "Are you sure you want to unallocate this stock?" +msgstr "" + +#: build/templates/build/delete_build_item.html:11 +msgid "The selected stock will be unallocated from the build output" +msgstr "" + +#: build/templates/build/detail.html:33 msgid "Stock Source" msgstr "" -#: build/templates/build/detail.html:36 +#: build/templates/build/detail.html:38 msgid "Stock can be taken from any available location." msgstr "" -#: build/templates/build/detail.html:48 -#: stock/templates/stock/item_base.html:238 templates/js/stock.html:514 -#: templates/js/stock.html:921 templates/js/table_filters.html:34 -#: templates/js/table_filters.html:100 +#: build/templates/build/detail.html:44 stock/forms.py:364 +msgid "Destination" +msgstr "" + +#: build/templates/build/detail.html:51 +msgid "Destination location not specified" +msgstr "" + +#: build/templates/build/detail.html:68 +#: stock/templates/stock/item_base.html:251 templates/js/stock.js:565 +#: templates/js/stock.js:968 templates/js/table_filters.js:80 +#: templates/js/table_filters.js:151 msgid "Batch" msgstr "" -#: build/templates/build/detail.html:61 +#: build/templates/build/detail.html:95 #: order/templates/order/order_base.html:98 -#: order/templates/order/sales_order_base.html:100 templates/js/build.html:85 +#: order/templates/order/sales_order_base.html:100 templates/js/build.js:683 msgid "Created" msgstr "" -#: build/templates/build/detail.html:67 -msgid "Enough Parts?" +#: build/templates/build/detail.html:105 +msgid "BOM Price" msgstr "" -#: build/templates/build/detail.html:70 -msgid "Yes" +#: build/templates/build/detail.html:110 +msgid "BOM pricing is incomplete" msgstr "" -#: build/templates/build/detail.html:72 -msgid "No" +#: build/templates/build/detail.html:113 +msgid "No pricing information" msgstr "" -#: build/templates/build/detail.html:80 templates/js/build.html:90 +#: build/templates/build/detail.html:120 templates/js/build.js:661 +#: templates/js/build.js:688 msgid "Completed" msgstr "" -#: build/templates/build/index.html:24 build/views.py:403 +#: build/templates/build/edit_build_item.html:7 +msgid "Alter the quantity of stock allocated to the build output" +msgstr "" + +#: build/templates/build/index.html:25 build/views.py:658 msgid "New Build Order" msgstr "" @@ -750,94 +856,163 @@ msgid "Details" msgstr "" #: build/templates/build/tabs.html:8 -msgid "Allocated Parts" +msgid "Allocate Parts" msgstr "" -#: build/templates/build/unallocate.html:8 +#: build/templates/build/unallocate.html:10 msgid "Are you sure you wish to unallocate all stock for this build?" msgstr "" -#: build/views.py:77 -msgid "Confirm build cancellation" +#: build/templates/build/unallocate.html:12 +msgid "All incomplete stock allocations will be removed from the build" msgstr "" -#: build/views.py:82 +#: build/views.py:79 msgid "Build was cancelled" msgstr "" -#: build/views.py:98 +#: build/views.py:93 msgid "Allocate Stock" msgstr "" -#: build/views.py:112 -msgid "No matching build found" +#: build/views.py:157 build/views.py:317 build/views.py:490 +msgid "Build output must be specified" msgstr "" -#: build/views.py:131 -msgid "Confirm stock allocation" +#: build/views.py:171 +msgid "Allocated stock to build output" msgstr "" -#: build/views.py:132 -msgid "Check the confirmation box at the bottom of the list" +#: build/views.py:183 +msgid "Create Build Output" msgstr "" -#: build/views.py:152 build/views.py:467 -msgid "Unallocate Stock" +#: build/views.py:207 stock/models.py:832 stock/views.py:1645 +msgid "Serial numbers already exist" msgstr "" -#: build/views.py:166 +#: build/views.py:216 +msgid "Serial numbers required for trackable build output" +msgstr "" + +#: build/views.py:282 +msgid "Delete Build Output" +msgstr "" + +#: build/views.py:302 build/views.py:387 msgid "Confirm unallocation of build stock" msgstr "" -#: build/views.py:167 stock/views.py:421 +#: build/views.py:303 build/views.py:388 stock/views.py:413 msgid "Check the confirmation box" msgstr "" -#: build/views.py:270 -msgid "Confirm completion of build" +#: build/views.py:315 +msgid "Build output does not match build" msgstr "" -#: build/views.py:277 -msgid "Invalid location selected" +#: build/views.py:329 +msgid "Build output deleted" msgstr "" -#: build/views.py:302 stock/views.py:1653 -#, python-brace-format -msgid "The following serial numbers already exist: ({sn})" +#: build/views.py:412 +msgid "Complete Build Order" msgstr "" -#: build/views.py:323 -msgid "Build marked as COMPLETE" +#: build/views.py:418 +msgid "Build order cannot be completed" msgstr "" -#: build/views.py:431 +#: build/views.py:429 +msgid "Completed build order" +msgstr "" + +#: build/views.py:445 +msgid "Complete Build Output" +msgstr "" + +#: build/views.py:481 +msgid "Quantity to complete cannot exceed build output quantity" +msgstr "" + +#: build/views.py:487 +msgid "Confirm completion of incomplete build" +msgstr "" + +#: build/views.py:578 +msgid "Build output completed" +msgstr "" + +#: build/views.py:703 msgid "Created new build" msgstr "" -#: build/views.py:441 +#: build/views.py:724 msgid "Edit Build Details" msgstr "" -#: build/views.py:447 +#: build/views.py:758 msgid "Edited build" msgstr "" -#: build/views.py:473 +#: build/views.py:784 msgid "Removed parts from build allocation" msgstr "" -#: build/views.py:483 -msgid "Allocate new Part" +#: build/views.py:796 +msgid "Allocate stock to build output" msgstr "" -#: build/views.py:637 +#: build/views.py:840 +msgid "Item must be currently in stock" +msgstr "" + +#: build/views.py:846 +msgid "Stock item is over-allocated" +msgstr "" + +#: build/views.py:847 templates/js/bom.js:215 templates/js/build.js:503 +#: templates/js/build.js:731 +msgid "Available" +msgstr "" + +#: build/views.py:849 +msgid "Stock item must be selected" +msgstr "" + +#: build/views.py:1011 msgid "Edit Stock Allocation" msgstr "" -#: build/views.py:642 +#: build/views.py:1016 msgid "Updated Build Item" msgstr "" +#: build/views.py:1045 +msgid "Add Build Order Attachment" +msgstr "" + +#: build/views.py:1059 order/views.py:111 order/views.py:164 part/views.py:96 +#: stock/views.py:176 +msgid "Added attachment" +msgstr "" + +#: build/views.py:1095 order/views.py:191 order/views.py:213 +msgid "Edit Attachment" +msgstr "" + +#: build/views.py:1106 order/views.py:196 order/views.py:218 +msgid "Attachment updated" +msgstr "" + +#: build/views.py:1116 order/views.py:233 order/views.py:248 +msgid "Delete Attachment" +msgstr "" + +#: build/views.py:1122 order/views.py:240 order/views.py:255 stock/views.py:234 +msgid "Deleted attachment" +msgstr "" + #: common/models.py:51 msgid "InvenTree Instance Name" msgstr "" @@ -918,43 +1093,43 @@ msgstr "" msgid "Prefix value for purchase order reference" msgstr "" -#: common/models.py:272 +#: common/models.py:277 msgid "Settings key (must be unique - case insensitive" msgstr "" -#: common/models.py:274 +#: common/models.py:279 msgid "Settings value" msgstr "" -#: common/models.py:326 +#: common/models.py:331 msgid "Value must be a boolean value" msgstr "" -#: common/models.py:340 +#: common/models.py:345 msgid "Key string must be unique" msgstr "" -#: common/models.py:379 +#: common/models.py:384 msgid "Currency Symbol e.g. $" msgstr "" -#: common/models.py:381 +#: common/models.py:386 msgid "Currency Suffix e.g. AUD" msgstr "" -#: common/models.py:383 +#: common/models.py:388 msgid "Currency Description" msgstr "" -#: common/models.py:385 +#: common/models.py:390 msgid "Currency Value" msgstr "" -#: common/models.py:387 +#: common/models.py:392 msgid "Use this currency as the base currency" msgstr "" -#: common/models.py:470 +#: common/models.py:475 msgid "Default" msgstr "" @@ -987,7 +1162,7 @@ msgid "Description of the company" msgstr "" #: company/models.py:94 company/templates/company/company_base.html:57 -#: templates/js/company.html:61 +#: templates/js/company.js:61 msgid "Website" msgstr "" @@ -1043,8 +1218,8 @@ msgstr "" msgid "Does this company manufacture parts?" msgstr "" -#: company/models.py:283 stock/models.py:335 -#: stock/templates/stock/item_base.html:164 +#: company/models.py:283 stock/models.py:337 +#: stock/templates/stock/item_base.html:177 msgid "Base Part" msgstr "" @@ -1085,12 +1260,12 @@ msgid "Part packaging" msgstr "" #: company/templates/company/assigned_stock.html:9 -#: company/templates/company/tabs.html:25 +#: company/templates/company/tabs.html:25 templates/js/build.js:395 msgid "Assigned Stock" msgstr "" #: company/templates/company/company_base.html:7 -#: company/templates/company/company_base.html:22 templates/js/company.html:33 +#: company/templates/company/company_base.html:22 templates/js/company.js:33 msgid "Company" msgstr "" @@ -1106,7 +1281,7 @@ msgstr "" #: company/templates/company/detail.html:16 #: company/templates/company/supplier_part_base.html:84 #: company/templates/company/supplier_part_detail.html:30 part/bom.py:172 -#: templates/js/company.html:44 templates/js/company.html:186 +#: templates/js/company.js:44 templates/js/company.js:188 msgid "Manufacturer" msgstr "" @@ -1115,15 +1290,15 @@ msgstr "" #: 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:273 templates/js/company.html:48 -#: templates/js/company.html:162 templates/js/order.html:154 +#: stock/templates/stock/item_base.html:286 templates/js/company.js:48 +#: templates/js/company.js:164 templates/js/order.js:154 msgid "Supplier" msgstr "" #: company/templates/company/detail.html:26 -#: order/templates/order/sales_order_base.html:81 stock/models.py:370 -#: stock/models.py:371 stock/templates/stock/item_base.html:191 -#: templates/js/company.html:40 templates/js/order.html:236 +#: 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 msgid "Customer" msgstr "" @@ -1131,60 +1306,60 @@ msgstr "" msgid "Supplier Parts" msgstr "" -#: company/templates/company/detail_part.html:15 -#: order/templates/order/purchase_order_detail.html:68 +#: company/templates/company/detail_part.html:17 +#: order/templates/order/purchase_order_detail.html:69 msgid "Create new supplier part" msgstr "" -#: company/templates/company/detail_part.html:15 -#: order/templates/order/purchase_order_detail.html:67 -#: part/templates/part/supplier.html:13 templates/js/stock.html:798 +#: company/templates/company/detail_part.html:18 +#: order/templates/order/purchase_order_detail.html:68 +#: part/templates/part/supplier.html:14 templates/js/stock.js:845 msgid "New Supplier Part" msgstr "" -#: company/templates/company/detail_part.html:18 -#: part/templates/part/category.html:117 part/templates/part/supplier.html:15 -#: templates/stock_table.html:14 +#: company/templates/company/detail_part.html:23 +#: part/templates/part/category.html:120 part/templates/part/supplier.html:17 +#: templates/stock_table.html:18 msgid "Options" msgstr "" -#: company/templates/company/detail_part.html:22 -#: part/templates/part/category.html:122 +#: company/templates/company/detail_part.html:28 +#: part/templates/part/category.html:125 msgid "Order parts" msgstr "" -#: company/templates/company/detail_part.html:25 +#: company/templates/company/detail_part.html:31 msgid "Delete parts" msgstr "" -#: company/templates/company/detail_part.html:25 +#: company/templates/company/detail_part.html:31 msgid "Delete Parts" msgstr "" -#: company/templates/company/detail_part.html:51 -#: part/templates/part/category.html:114 templates/js/stock.html:792 +#: company/templates/company/detail_part.html:63 +#: part/templates/part/category.html:116 templates/js/stock.js:839 msgid "New Part" msgstr "" -#: company/templates/company/detail_part.html:52 +#: company/templates/company/detail_part.html:64 msgid "Create new Part" msgstr "" -#: company/templates/company/detail_part.html:57 company/views.py:53 -#: part/templates/part/supplier.html:45 +#: company/templates/company/detail_part.html:69 company/views.py:53 +#: part/templates/part/supplier.html:47 msgid "New Supplier" msgstr "" -#: company/templates/company/detail_part.html:58 company/views.py:192 +#: company/templates/company/detail_part.html:70 company/views.py:192 msgid "Create new Supplier" msgstr "" -#: company/templates/company/detail_part.html:63 company/views.py:60 -#: part/templates/part/supplier.html:51 +#: company/templates/company/detail_part.html:75 company/views.py:60 +#: part/templates/part/supplier.html:53 msgid "New Manufacturer" msgstr "" -#: company/templates/company/detail_part.html:64 company/views.py:195 +#: company/templates/company/detail_part.html:76 company/views.py:195 msgid "Create new Manufacturer" msgstr "" @@ -1194,8 +1369,9 @@ msgstr "" #: company/templates/company/detail_stock.html:35 #: company/templates/company/supplier_part_stock.html:33 -#: part/templates/part/category.html:112 part/templates/part/category.html:123 -#: part/templates/part/stock.html:51 templates/stock_table.html:6 +#: part/templates/part/bom.html:63 part/templates/part/category.html:112 +#: part/templates/part/category.html:126 part/templates/part/stock.html:51 +#: templates/stock_table.html:7 msgid "Export" msgstr "" @@ -1227,8 +1403,8 @@ msgstr "" msgid "Create new purchase order" msgstr "" -#: company/templates/company/purchase_orders.html:15 -#: order/templates/order/purchase_orders.html:18 +#: company/templates/company/purchase_orders.html:16 +#: order/templates/order/purchase_orders.html:19 msgid "New Purchase Order" msgstr "" @@ -1247,14 +1423,14 @@ msgstr "" msgid "Create new sales order" msgstr "" -#: company/templates/company/sales_orders.html:15 -#: order/templates/order/sales_orders.html:18 +#: company/templates/company/sales_orders.html:16 +#: order/templates/order/sales_orders.html:19 msgid "New Sales Order" msgstr "" #: company/templates/company/supplier_part_base.html:6 -#: company/templates/company/supplier_part_base.html:19 stock/models.py:344 -#: stock/templates/stock/item_base.html:278 templates/js/company.html:178 +#: company/templates/company/supplier_part_base.html:19 stock/models.py:346 +#: stock/templates/stock/item_base.html:291 templates/js/company.js:180 msgid "Supplier Part" msgstr "" @@ -1288,7 +1464,7 @@ msgstr "" #: company/templates/company/supplier_part_base.html:88 #: company/templates/company/supplier_part_detail.html:31 part/bom.py:173 -#: templates/js/company.html:202 +#: templates/js/company.js:204 msgid "MPN" msgstr "" @@ -1301,31 +1477,36 @@ msgstr "" msgid "Supplier Part Orders" msgstr "" +#: company/templates/company/supplier_part_orders.html:17 +#: part/templates/part/orders.html:15 +msgid "Order Part" +msgstr "" + #: company/templates/company/supplier_part_pricing.html:10 msgid "Pricing Information" msgstr "" -#: company/templates/company/supplier_part_pricing.html:16 company/views.py:410 -#: part/templates/part/sale_prices.html:13 part/views.py:2228 +#: company/templates/company/supplier_part_pricing.html:17 company/views.py:410 +#: part/templates/part/sale_prices.html:13 part/views.py:2292 msgid "Add Price Break" msgstr "" -#: company/templates/company/supplier_part_pricing.html:34 +#: company/templates/company/supplier_part_pricing.html:36 #: part/templates/part/sale_prices.html:41 msgid "No price break information found" msgstr "" -#: company/templates/company/supplier_part_pricing.html:78 -#: part/templates/part/sale_prices.html:85 templates/js/bom.html:207 +#: 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:92 +#: company/templates/company/supplier_part_pricing.html:94 #: part/templates/part/sale_prices.html:99 msgid "Edit price break" msgstr "" -#: company/templates/company/supplier_part_pricing.html:93 +#: company/templates/company/supplier_part_pricing.html:95 #: part/templates/part/sale_prices.html:100 msgid "Delete price break" msgstr "" @@ -1341,9 +1522,9 @@ msgstr "" #: company/templates/company/supplier_part_tabs.html:8 #: company/templates/company/tabs.html:12 part/templates/part/tabs.html:18 #: stock/templates/stock/location.html:17 templates/InvenTree/search.html:155 -#: templates/InvenTree/settings/tabs.html:25 templates/js/part.html:124 -#: templates/js/part.html:372 templates/js/stock.html:453 -#: templates/navbar.html:22 users/models.py:29 +#: templates/InvenTree/settings/tabs.html:25 templates/js/part.js:192 +#: templates/js/part.js:418 templates/js/stock.js:502 templates/navbar.html:22 +#: users/models.py:29 msgid "Stock" msgstr "" @@ -1425,7 +1606,7 @@ msgstr "" msgid "Edit Supplier Part" msgstr "" -#: company/views.py:278 templates/js/stock.html:799 +#: company/views.py:278 templates/js/stock.js:846 msgid "Create new Supplier Part" msgstr "" @@ -1433,15 +1614,15 @@ msgstr "" msgid "Delete Supplier Part" msgstr "" -#: company/views.py:416 part/views.py:2234 +#: company/views.py:416 part/views.py:2298 msgid "Added new price break" msgstr "" -#: company/views.py:453 part/views.py:2279 +#: company/views.py:453 part/views.py:2343 msgid "Edit Price Break" msgstr "" -#: company/views.py:469 part/views.py:2295 +#: company/views.py:469 part/views.py:2359 msgid "Delete Price Break" msgstr "" @@ -1514,7 +1695,7 @@ msgstr "" msgid "Order notes" msgstr "" -#: order/models.py:140 order/models.py:318 +#: order/models.py:140 order/models.py:326 msgid "Purchase order status" msgstr "" @@ -1534,8 +1715,8 @@ msgstr "" msgid "Date order was completed" msgstr "" -#: order/models.py:185 order/models.py:259 part/views.py:1345 -#: stock/models.py:241 stock/models.py:805 +#: order/models.py:185 order/models.py:267 part/views.py:1409 +#: stock/models.py:243 stock/models.py:816 msgid "Quantity must be greater than zero" msgstr "" @@ -1543,69 +1724,69 @@ msgstr "" msgid "Part supplier must match PO supplier" msgstr "" -#: order/models.py:254 +#: order/models.py:262 msgid "Lines can only be received against an order marked as 'Placed'" msgstr "" -#: order/models.py:314 +#: order/models.py:322 msgid "Company to which the items are being sold" msgstr "" -#: order/models.py:320 +#: order/models.py:328 msgid "Customer order reference code" msgstr "" -#: order/models.py:359 +#: order/models.py:367 msgid "SalesOrder cannot be shipped as it is not currently pending" msgstr "" -#: order/models.py:436 +#: order/models.py:454 msgid "Item quantity" msgstr "" -#: order/models.py:438 +#: order/models.py:456 msgid "Line item reference" msgstr "" -#: order/models.py:440 +#: order/models.py:458 msgid "Line item notes" msgstr "" -#: order/models.py:466 order/templates/order/order_base.html:9 +#: order/models.py:484 order/templates/order/order_base.html:9 #: order/templates/order/order_base.html:24 -#: stock/templates/stock/item_base.html:252 templates/js/order.html:139 +#: stock/templates/stock/item_base.html:265 templates/js/order.js:139 msgid "Purchase Order" msgstr "" -#: order/models.py:479 +#: order/models.py:497 msgid "Supplier part" msgstr "" -#: order/models.py:482 +#: order/models.py:500 msgid "Number of items received" msgstr "" -#: order/models.py:576 +#: order/models.py:594 msgid "Cannot allocate stock item to a line with a different part" msgstr "" -#: order/models.py:578 +#: order/models.py:596 msgid "Cannot allocate stock to a line without a part" msgstr "" -#: order/models.py:581 +#: order/models.py:599 msgid "Allocation quantity cannot exceed stock quantity" msgstr "" -#: order/models.py:591 +#: order/models.py:609 msgid "Quantity must be 1 for serialized stock item" msgstr "" -#: order/models.py:608 +#: order/models.py:626 msgid "Select stock item to allocate" msgstr "" -#: order/models.py:611 +#: order/models.py:629 msgid "Enter stock allocation quantity" msgstr "" @@ -1641,7 +1822,7 @@ msgstr "" msgid "Order Status" msgstr "" -#: order/templates/order/order_base.html:85 templates/js/order.html:161 +#: order/templates/order/order_base.html:85 templates/js/order.js:161 msgid "Supplier Reference" msgstr "" @@ -1650,7 +1831,7 @@ msgid "Issued" msgstr "" #: order/templates/order/order_base.html:111 -#: order/templates/order/purchase_order_detail.html:182 +#: order/templates/order/purchase_order_detail.html:183 #: order/templates/order/receive_parts.html:22 #: order/templates/order/sales_order_base.html:113 msgid "Received" @@ -1697,7 +1878,7 @@ msgid "Select existing purchase orders, or create new orders." msgstr "" #: order/templates/order/order_wizard/select_pos.html:31 -#: templates/js/order.html:185 templates/js/order.html:272 +#: templates/js/order.js:185 templates/js/order.js:272 msgid "Items" msgstr "" @@ -1722,53 +1903,48 @@ msgstr "" msgid "Line Items" msgstr "" -#: order/templates/order/po_tabs.html:11 order/templates/order/so_tabs.html:16 -#: part/templates/part/tabs.html:67 stock/templates/stock/tabs.html:32 -msgid "Attachments" -msgstr "" - -#: order/templates/order/purchase_order_detail.html:16 -#: order/templates/order/sales_order_detail.html:18 order/views.py:1117 -#: order/views.py:1232 +#: order/templates/order/purchase_order_detail.html:17 +#: order/templates/order/sales_order_detail.html:19 order/views.py:1115 +#: order/views.py:1199 msgid "Add Line Item" msgstr "" -#: order/templates/order/purchase_order_detail.html:20 +#: order/templates/order/purchase_order_detail.html:21 msgid "Purchase Order Items" msgstr "" -#: order/templates/order/purchase_order_detail.html:38 -#: order/templates/order/purchase_order_detail.html:118 -#: part/templates/part/category.html:171 part/templates/part/category.html:213 -#: templates/js/stock.html:804 -msgid "New Location" -msgstr "" - #: order/templates/order/purchase_order_detail.html:39 #: order/templates/order/purchase_order_detail.html:119 +#: part/templates/part/category.html:173 part/templates/part/category.html:215 +#: templates/js/stock.js:851 +msgid "New Location" +msgstr "" + +#: order/templates/order/purchase_order_detail.html:40 +#: order/templates/order/purchase_order_detail.html:120 #: stock/templates/stock/location.html:22 msgid "Create new stock location" msgstr "" -#: order/templates/order/purchase_order_detail.html:131 +#: order/templates/order/purchase_order_detail.html:132 msgid "No line items found" msgstr "" -#: order/templates/order/purchase_order_detail.html:164 +#: order/templates/order/purchase_order_detail.html:165 #: order/templates/order/receive_parts.html:20 msgid "Order Code" msgstr "" -#: order/templates/order/purchase_order_detail.html:213 -#: order/templates/order/sales_order_detail.html:283 +#: order/templates/order/purchase_order_detail.html:214 +#: order/templates/order/sales_order_detail.html:285 msgid "Edit line item" msgstr "" -#: order/templates/order/purchase_order_detail.html:214 +#: order/templates/order/purchase_order_detail.html:215 msgid "Delete line item" msgstr "" -#: order/templates/order/purchase_order_detail.html:219 +#: order/templates/order/purchase_order_detail.html:220 msgid "Receive line item" msgstr "" @@ -1781,7 +1957,7 @@ msgid "Select parts to receive against this order" msgstr "" #: order/templates/order/receive_parts.html:21 -#: part/templates/part/part_base.html:145 templates/js/part.html:388 +#: part/templates/part/part_base.html:145 templates/js/part.js:434 msgid "On Order" msgstr "" @@ -1805,12 +1981,13 @@ msgstr "" msgid "Sales Order Details" msgstr "" -#: order/templates/order/sales_order_base.html:87 templates/js/order.html:243 +#: order/templates/order/sales_order_base.html:87 templates/js/order.js:243 msgid "Customer Reference" msgstr "" #: order/templates/order/sales_order_cancel.html:8 #: order/templates/order/sales_order_ship.html:9 +#: part/templates/part/bom_duplicate.html:12 #: stock/templates/stock/stockitem_convert.html:13 msgid "Warning" msgstr "" @@ -1819,15 +1996,45 @@ msgstr "" 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 +#: stock/templates/stock/item_base.html:191 templates/js/build.js:402 +msgid "Serial Number" +msgstr "" + +#: order/templates/order/sales_order_detail.html:96 templates/js/build.js:443 +#: templates/js/build.js:742 +msgid "Edit stock allocation" +msgstr "" + +#: order/templates/order/sales_order_detail.html:97 templates/js/build.js:445 +#: templates/js/build.js:743 +msgid "Delete stock allocation" +msgstr "" + #: order/templates/order/sales_order_detail.html:225 +#: part/templates/part/tabs.html:23 templates/js/build.js:507 +#: templates/js/build.js:738 +msgid "Allocated" +msgstr "" + +#: order/templates/order/sales_order_detail.html:227 msgid "Fulfilled" msgstr "" -#: order/templates/order/sales_order_detail.html:280 +#: order/templates/order/sales_order_detail.html:275 +msgid "Buy parts" +msgstr "" + +#: order/templates/order/sales_order_detail.html:279 +msgid "Build parts" +msgstr "" + +#: order/templates/order/sales_order_detail.html:282 msgid "Allocate parts" msgstr "" -#: order/templates/order/sales_order_detail.html:284 +#: order/templates/order/sales_order_detail.html:286 msgid "Delete line item " msgstr "" @@ -1873,143 +2080,131 @@ msgstr "" msgid "Add Purchase Order Attachment" msgstr "" -#: order/views.py:109 order/views.py:157 part/views.py:92 stock/views.py:175 -msgid "Added attachment" -msgstr "" - -#: order/views.py:148 +#: order/views.py:150 msgid "Add Sales Order Attachment" msgstr "" -#: order/views.py:184 order/views.py:206 -msgid "Edit Attachment" -msgstr "" - -#: order/views.py:189 order/views.py:211 -msgid "Attachment updated" -msgstr "" - -#: order/views.py:226 order/views.py:241 -msgid "Delete Attachment" -msgstr "" - -#: order/views.py:233 order/views.py:248 stock/views.py:233 -msgid "Deleted attachment" -msgstr "" - -#: order/views.py:301 +#: order/views.py:310 msgid "Create Purchase Order" msgstr "" -#: order/views.py:333 +#: order/views.py:345 msgid "Create Sales Order" msgstr "" -#: order/views.py:364 +#: order/views.py:380 msgid "Edit Purchase Order" msgstr "" -#: order/views.py:385 +#: order/views.py:401 msgid "Edit Sales Order" msgstr "" -#: order/views.py:402 +#: order/views.py:418 msgid "Cancel Order" msgstr "" -#: order/views.py:418 order/views.py:451 +#: order/views.py:428 order/views.py:455 msgid "Confirm order cancellation" msgstr "" -#: order/views.py:436 +#: order/views.py:431 order/views.py:458 +msgid "Order cannot be cancelled" +msgstr "" + +#: order/views.py:445 msgid "Cancel sales order" msgstr "" -#: order/views.py:457 -msgid "Could not cancel order" -msgstr "" - -#: order/views.py:471 +#: order/views.py:472 msgid "Issue Order" msgstr "" -#: order/views.py:487 +#: order/views.py:482 msgid "Confirm order placement" msgstr "" -#: order/views.py:508 +#: order/views.py:492 +msgid "Purchase order issued" +msgstr "" + +#: order/views.py:503 msgid "Complete Order" msgstr "" -#: order/views.py:544 +#: order/views.py:520 +msgid "Confirm order completion" +msgstr "" + +#: order/views.py:531 +msgid "Purchase order completed" +msgstr "" + +#: order/views.py:541 msgid "Ship Order" msgstr "" -#: order/views.py:561 +#: order/views.py:558 msgid "Confirm order shipment" msgstr "" -#: order/views.py:567 +#: order/views.py:564 msgid "Could not ship order" msgstr "" -#: order/views.py:619 +#: order/views.py:616 msgid "Receive Parts" msgstr "" -#: order/views.py:687 +#: order/views.py:684 msgid "Items received" msgstr "" -#: order/views.py:701 +#: order/views.py:698 msgid "No destination set" msgstr "" -#: order/views.py:746 +#: order/views.py:743 msgid "Error converting quantity to number" msgstr "" -#: order/views.py:752 +#: order/views.py:749 msgid "Receive quantity less than zero" msgstr "" -#: order/views.py:758 +#: order/views.py:755 msgid "No lines specified" msgstr "" -#: order/views.py:1138 -msgid "Invalid Purchase Order" +#: order/views.py:1125 +msgid "Supplier part must be specified" msgstr "" -#: order/views.py:1146 +#: order/views.py:1131 msgid "Supplier must match for Part and Order" msgstr "" -#: order/views.py:1151 -msgid "Invalid SupplierPart selection" -msgstr "" - -#: order/views.py:1284 order/views.py:1303 +#: order/views.py:1251 order/views.py:1270 msgid "Edit Line Item" msgstr "" -#: order/views.py:1320 order/views.py:1333 +#: order/views.py:1287 order/views.py:1300 msgid "Delete Line Item" msgstr "" -#: order/views.py:1326 order/views.py:1339 +#: order/views.py:1293 order/views.py:1306 msgid "Deleted line item" msgstr "" -#: order/views.py:1348 +#: order/views.py:1315 msgid "Allocate Stock to Order" msgstr "" -#: order/views.py:1418 +#: order/views.py:1385 msgid "Edit Allocation Quantity" msgstr "" -#: order/views.py:1434 +#: order/views.py:1401 msgid "Remove allocation" msgstr "" @@ -2035,91 +2230,107 @@ msgstr "" msgid "Error reading BOM file (incorrect row size)" msgstr "" -#: part/forms.py:57 stock/forms.py:254 +#: part/forms.py:62 stock/forms.py:254 msgid "File Format" msgstr "" -#: part/forms.py:57 stock/forms.py:254 +#: part/forms.py:62 stock/forms.py:254 msgid "Select output file format" msgstr "" -#: part/forms.py:59 +#: part/forms.py:64 msgid "Cascading" msgstr "" -#: part/forms.py:59 +#: part/forms.py:64 msgid "Download cascading / multi-level BOM" msgstr "" -#: part/forms.py:61 +#: part/forms.py:66 msgid "Levels" msgstr "" -#: part/forms.py:61 +#: part/forms.py:66 msgid "Select maximum number of BOM levels to export (0 = all levels)" msgstr "" -#: part/forms.py:63 +#: part/forms.py:68 msgid "Include Parameter Data" msgstr "" -#: part/forms.py:63 +#: part/forms.py:68 msgid "Include part parameters data in exported BOM" msgstr "" -#: part/forms.py:65 +#: part/forms.py:70 msgid "Include Stock Data" msgstr "" -#: part/forms.py:65 +#: part/forms.py:70 msgid "Include part stock data in exported BOM" msgstr "" -#: part/forms.py:67 +#: part/forms.py:72 msgid "Include Supplier Data" msgstr "" -#: part/forms.py:67 +#: part/forms.py:72 msgid "Include part supplier data in exported BOM" msgstr "" -#: part/forms.py:86 +#: part/forms.py:93 part/models.py:1582 +msgid "Parent Part" +msgstr "" + +#: part/forms.py:94 part/templates/part/bom_duplicate.html:7 +msgid "Select parent part to copy BOM from" +msgstr "" + +#: part/forms.py:100 +msgid "Clear existing BOM items" +msgstr "" + +#: part/forms.py:105 +msgid "Confirm BOM duplication" +msgstr "" + +#: part/forms.py:123 msgid "Confirm that the BOM is correct" msgstr "" -#: part/forms.py:98 +#: part/forms.py:135 msgid "Select BOM file to upload" msgstr "" -#: part/forms.py:122 +#: part/forms.py:159 msgid "Select part category" msgstr "" -#: part/forms.py:136 +#: part/forms.py:173 msgid "Duplicate all BOM data for this part" msgstr "" -#: part/forms.py:137 +#: part/forms.py:174 msgid "Copy BOM" msgstr "" -#: part/forms.py:142 +#: part/forms.py:179 msgid "Duplicate all parameter data for this part" msgstr "" -#: part/forms.py:143 +#: part/forms.py:180 msgid "Copy Parameters" msgstr "" -#: part/forms.py:148 +#: part/forms.py:185 msgid "Confirm part creation" msgstr "" -#: part/forms.py:248 +#: part/forms.py:279 msgid "Input quantity for price calculation" msgstr "" -#: part/forms.py:251 +#: part/forms.py:282 msgid "Select currency for price calculation" msgstr "" @@ -2145,216 +2356,213 @@ msgstr "" msgid "Part '{p1}' is used in BOM for '{p2}' (recursive)" msgstr "" -#: part/models.py:435 +#: part/models.py:452 msgid "Next available serial numbers are" msgstr "" -#: part/models.py:439 +#: part/models.py:456 msgid "Next available serial number is" msgstr "" -#: part/models.py:444 +#: part/models.py:461 msgid "Most recent serial number is" msgstr "" -#: part/models.py:522 +#: part/models.py:539 msgid "Part must be unique for name, IPN and revision" msgstr "" -#: part/models.py:537 part/templates/part/detail.html:19 +#: part/models.py:568 part/templates/part/detail.html:19 msgid "Part name" msgstr "" -#: part/models.py:541 +#: part/models.py:572 msgid "Is this part a template part?" msgstr "" -#: part/models.py:550 +#: part/models.py:581 msgid "Is this part a variant of another part?" msgstr "" -#: part/models.py:552 +#: part/models.py:583 msgid "Part description" msgstr "" -#: part/models.py:554 +#: part/models.py:585 msgid "Part keywords to improve visibility in search results" msgstr "" -#: part/models.py:559 +#: part/models.py:590 msgid "Part category" msgstr "" -#: part/models.py:561 +#: part/models.py:592 msgid "Internal Part Number" msgstr "" -#: part/models.py:563 +#: part/models.py:594 msgid "Part revision or version number" msgstr "" -#: part/models.py:565 -msgid "Link to extenal URL" -msgstr "" - -#: part/models.py:577 +#: part/models.py:608 msgid "Where is this item normally stored?" msgstr "" -#: part/models.py:621 +#: part/models.py:652 msgid "Default supplier part" msgstr "" -#: part/models.py:624 +#: part/models.py:655 msgid "Minimum allowed stock level" msgstr "" -#: part/models.py:626 +#: part/models.py:657 msgid "Stock keeping units for this part" msgstr "" -#: part/models.py:628 +#: part/models.py:659 msgid "Can this part be built from other parts?" msgstr "" -#: part/models.py:630 +#: part/models.py:661 msgid "Can this part be used to build other parts?" msgstr "" -#: part/models.py:632 +#: part/models.py:663 msgid "Does this part have tracking for unique items?" msgstr "" -#: part/models.py:634 +#: part/models.py:665 msgid "Can this part be purchased from external suppliers?" msgstr "" -#: part/models.py:636 +#: part/models.py:667 msgid "Can this part be sold to customers?" msgstr "" -#: part/models.py:638 +#: part/models.py:669 msgid "Is this part active?" msgstr "" -#: part/models.py:640 +#: part/models.py:671 msgid "Is this a virtual part, such as a software product or license?" msgstr "" -#: part/models.py:642 +#: part/models.py:673 msgid "Part notes - supports Markdown formatting" msgstr "" -#: part/models.py:644 +#: part/models.py:675 msgid "Stored BOM checksum" msgstr "" -#: part/models.py:1353 +#: part/models.py:1455 msgid "Test templates can only be created for trackable parts" msgstr "" -#: part/models.py:1370 +#: part/models.py:1472 msgid "Test with this name already exists for this part" msgstr "" -#: part/models.py:1389 templates/js/part.html:521 templates/js/stock.html:92 +#: part/models.py:1491 templates/js/part.js:567 templates/js/stock.js:92 msgid "Test Name" msgstr "" -#: part/models.py:1390 +#: part/models.py:1492 msgid "Enter a name for the test" msgstr "" -#: part/models.py:1395 +#: part/models.py:1497 msgid "Test Description" msgstr "" -#: part/models.py:1396 +#: part/models.py:1498 msgid "Enter description for this test" msgstr "" -#: part/models.py:1402 +#: part/models.py:1503 templates/js/part.js:576 +#: templates/js/table_filters.js:172 +msgid "Required" +msgstr "" + +#: part/models.py:1504 msgid "Is this test required to pass?" msgstr "" -#: part/models.py:1407 templates/js/part.html:538 +#: part/models.py:1509 templates/js/part.js:584 msgid "Requires Value" msgstr "" -#: part/models.py:1408 +#: part/models.py:1510 msgid "Does this test require a value when adding a test result?" msgstr "" -#: part/models.py:1413 templates/js/part.html:545 +#: part/models.py:1515 templates/js/part.js:591 msgid "Requires Attachment" msgstr "" -#: part/models.py:1414 +#: part/models.py:1516 msgid "Does this test require a file attachment when adding a test result?" msgstr "" -#: part/models.py:1447 +#: part/models.py:1549 msgid "Parameter template name must be unique" msgstr "" -#: part/models.py:1452 +#: part/models.py:1554 msgid "Parameter Name" msgstr "" -#: part/models.py:1454 +#: part/models.py:1556 msgid "Parameter Units" msgstr "" -#: part/models.py:1480 -msgid "Parent Part" -msgstr "" - -#: part/models.py:1482 +#: part/models.py:1584 msgid "Parameter Template" msgstr "" -#: part/models.py:1484 +#: part/models.py:1586 msgid "Parameter Value" msgstr "" -#: part/models.py:1521 +#: part/models.py:1623 msgid "Select parent part" msgstr "" -#: part/models.py:1529 +#: part/models.py:1631 msgid "Select part to be used in BOM" msgstr "" -#: part/models.py:1535 +#: part/models.py:1637 msgid "BOM quantity for this BOM item" msgstr "" -#: part/models.py:1537 +#: part/models.py:1639 msgid "This BOM item is optional" msgstr "" -#: part/models.py:1540 +#: part/models.py:1642 msgid "Estimated build wastage quantity (absolute or percentage)" msgstr "" -#: part/models.py:1543 +#: part/models.py:1645 msgid "BOM item reference" msgstr "" -#: part/models.py:1546 +#: part/models.py:1648 msgid "BOM item notes" msgstr "" -#: part/models.py:1548 +#: part/models.py:1650 msgid "BOM line checksum" msgstr "" -#: part/models.py:1612 part/views.py:1351 part/views.py:1403 -#: stock/models.py:231 +#: part/models.py:1717 part/views.py:1415 part/views.py:1467 +#: stock/models.py:233 msgid "Quantity must be integer value for trackable parts" msgstr "" -#: part/models.py:1621 +#: part/models.py:1733 msgid "BOM Item" msgstr "" @@ -2372,10 +2580,10 @@ msgstr "" #: part/templates/part/allocation.html:28 #: part/templates/part/allocation.html:45 #: stock/templates/stock/item_base.html:8 -#: stock/templates/stock/item_base.html:58 -#: stock/templates/stock/item_base.html:260 -#: stock/templates/stock/stock_adjust.html:16 templates/js/build.html:126 -#: templates/js/stock.html:661 templates/js/stock.html:897 +#: stock/templates/stock/item_base.html:72 +#: stock/templates/stock/item_base.html:273 +#: 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" msgstr "" @@ -2387,42 +2595,75 @@ msgstr "" msgid "Bill of Materials" msgstr "" -#: part/templates/part/bom.html:37 +#: part/templates/part/bom.html:34 msgid "Remove selected BOM items" msgstr "" -#: part/templates/part/bom.html:38 +#: part/templates/part/bom.html:37 msgid "Import BOM data" msgstr "" -#: part/templates/part/bom.html:39 -msgid "New BOM Item" +#: part/templates/part/bom.html:38 +msgid "Import from File" msgstr "" -#: part/templates/part/bom.html:40 -msgid "Finish Editing" +#: part/templates/part/bom.html:41 +msgid "Copy BOM from parent part" msgstr "" -#: part/templates/part/bom.html:43 -msgid "Edit BOM" +#: part/templates/part/bom.html:42 +msgid "Copy from Parent" msgstr "" #: part/templates/part/bom.html:45 +msgid "New BOM Item" +msgstr "" + +#: part/templates/part/bom.html:46 +msgid "Add Item" +msgstr "" + +#: part/templates/part/bom.html:48 +msgid "Finish Editing" +msgstr "" + +#: part/templates/part/bom.html:49 +msgid "Finished" +msgstr "" + +#: part/templates/part/bom.html:53 +msgid "Edit BOM" +msgstr "" + +#: part/templates/part/bom.html:54 part/templates/part/params.html:38 +#: templates/InvenTree/settings/user.html:19 +msgid "Edit" +msgstr "" + +#: part/templates/part/bom.html:57 msgid "Validate Bill of Materials" msgstr "" -#: part/templates/part/bom.html:48 part/views.py:1642 +#: part/templates/part/bom.html:58 +msgid "Validate" +msgstr "" + +#: part/templates/part/bom.html:62 part/views.py:1706 msgid "Export Bill of Materials" msgstr "" -#: part/templates/part/bom.html:103 +#: part/templates/part/bom.html:123 msgid "Delete selected BOM items?" msgstr "" -#: part/templates/part/bom.html:104 +#: part/templates/part/bom.html:124 msgid "All selected BOM items will be deleted" msgstr "" +#: part/templates/part/bom_duplicate.html:13 +msgid "This part already has a Bill of Materials" +msgstr "" + #: part/templates/part/bom_upload/select_fields.html:8 #: part/templates/part/bom_upload/select_parts.html:8 #: part/templates/part/bom_upload/upload_file.html:10 @@ -2495,7 +2736,7 @@ msgstr "" msgid "Part Builds" msgstr "" -#: part/templates/part/build.html:14 +#: part/templates/part/build.html:15 msgid "Start New Build" msgstr "" @@ -2503,7 +2744,7 @@ msgstr "" msgid "All parts" msgstr "" -#: part/templates/part/category.html:24 part/views.py:2045 +#: part/templates/part/category.html:24 part/views.py:2109 msgid "Create new part category" msgstr "" @@ -2539,43 +2780,43 @@ msgstr "" msgid "Parts (Including subcategories)" msgstr "" -#: part/templates/part/category.html:112 +#: part/templates/part/category.html:111 msgid "Export Part Data" msgstr "" -#: part/templates/part/category.html:114 part/views.py:513 +#: part/templates/part/category.html:115 msgid "Create new part" msgstr "" -#: part/templates/part/category.html:120 +#: part/templates/part/category.html:123 msgid "Set category" msgstr "" -#: part/templates/part/category.html:120 +#: part/templates/part/category.html:123 msgid "Set Category" msgstr "" -#: part/templates/part/category.html:123 +#: part/templates/part/category.html:126 msgid "Export Data" msgstr "" -#: part/templates/part/category.html:172 +#: part/templates/part/category.html:174 msgid "Create new location" msgstr "" -#: part/templates/part/category.html:177 part/templates/part/category.html:207 +#: part/templates/part/category.html:179 part/templates/part/category.html:209 msgid "New Category" msgstr "" -#: part/templates/part/category.html:178 +#: part/templates/part/category.html:180 msgid "Create new category" msgstr "" -#: part/templates/part/category.html:208 +#: part/templates/part/category.html:210 msgid "Create new Part Category" msgstr "" -#: part/templates/part/category.html:214 stock/views.py:1343 +#: part/templates/part/category.html:216 stock/views.py:1338 msgid "Create new Stock Location" msgstr "" @@ -2583,16 +2824,28 @@ msgstr "" msgid "Parametric Table" msgstr "" +#: part/templates/part/create_part.html:11 +msgid "Possible Matching Parts" +msgstr "" + +#: part/templates/part/create_part.html:12 +msgid "The new part may be a duplicate of these existing parts" +msgstr "" + +#: part/templates/part/create_part.html:16 +msgid "match" +msgstr "" + #: part/templates/part/detail.html:9 msgid "Part Details" msgstr "" #: part/templates/part/detail.html:25 part/templates/part/part_base.html:95 -#: templates/js/part.html:112 +#: templates/js/part.js:180 msgid "IPN" msgstr "" -#: part/templates/part/detail.html:32 templates/js/part.html:116 +#: part/templates/part/detail.html:32 templates/js/part.js:184 msgid "Revision" msgstr "" @@ -2609,7 +2862,7 @@ msgid "Variant Of" msgstr "" #: part/templates/part/detail.html:70 part/templates/part/set_category.html:15 -#: templates/js/part.html:359 +#: templates/js/part.js:405 msgid "Category" msgstr "" @@ -2617,7 +2870,7 @@ msgstr "" msgid "Default Supplier" msgstr "" -#: part/templates/part/detail.html:102 part/templates/part/params.html:24 +#: part/templates/part/detail.html:102 part/templates/part/params.html:26 msgid "Units" msgstr "" @@ -2625,7 +2878,7 @@ msgstr "" msgid "Minimum Stock" msgstr "" -#: part/templates/part/detail.html:114 templates/js/order.html:262 +#: part/templates/part/detail.html:114 templates/js/order.js:262 msgid "Creation Date" msgstr "" @@ -2637,96 +2890,99 @@ msgstr "" msgid "Responsible User" msgstr "" -#: part/templates/part/detail.html:136 +#: part/templates/part/detail.html:138 templates/js/table_filters.js:27 msgid "Virtual" msgstr "" -#: part/templates/part/detail.html:139 +#: part/templates/part/detail.html:141 msgid "Part is virtual (not a physical part)" msgstr "" -#: part/templates/part/detail.html:141 +#: part/templates/part/detail.html:143 msgid "Part is not a virtual part" msgstr "" -#: part/templates/part/detail.html:145 stock/forms.py:248 -#: templates/js/table_filters.html:188 +#: part/templates/part/detail.html:148 stock/forms.py:248 +#: templates/js/table_filters.js:23 templates/js/table_filters.js:248 msgid "Template" msgstr "" -#: part/templates/part/detail.html:148 +#: part/templates/part/detail.html:151 msgid "Part is a template part (variants can be made from this part)" msgstr "" -#: part/templates/part/detail.html:150 +#: part/templates/part/detail.html:153 msgid "Part is not a template part" msgstr "" -#: part/templates/part/detail.html:154 templates/js/table_filters.html:200 +#: part/templates/part/detail.html:158 templates/js/table_filters.js:260 msgid "Assembly" msgstr "" -#: part/templates/part/detail.html:157 +#: part/templates/part/detail.html:161 msgid "Part can be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:159 +#: part/templates/part/detail.html:163 msgid "Part cannot be assembled from other parts" msgstr "" -#: part/templates/part/detail.html:163 templates/js/table_filters.html:204 +#: part/templates/part/detail.html:168 templates/js/table_filters.js:264 msgid "Component" msgstr "" -#: part/templates/part/detail.html:166 +#: part/templates/part/detail.html:171 msgid "Part can be used in assemblies" msgstr "" -#: part/templates/part/detail.html:168 +#: part/templates/part/detail.html:173 msgid "Part cannot be used in assemblies" msgstr "" -#: part/templates/part/detail.html:172 templates/js/table_filters.html:216 +#: part/templates/part/detail.html:178 templates/js/table_filters.js:31 +#: templates/js/table_filters.js:276 msgid "Trackable" msgstr "" -#: part/templates/part/detail.html:175 +#: part/templates/part/detail.html:181 msgid "Part stock is tracked by serial number" msgstr "" -#: part/templates/part/detail.html:177 +#: part/templates/part/detail.html:183 msgid "Part stock is not tracked by serial number" msgstr "" -#: part/templates/part/detail.html:181 +#: part/templates/part/detail.html:188 msgid "Purchaseable" msgstr "" -#: part/templates/part/detail.html:184 part/templates/part/detail.html:186 +#: part/templates/part/detail.html:191 part/templates/part/detail.html:193 msgid "Part can be purchased from external suppliers" msgstr "" -#: part/templates/part/detail.html:190 templates/js/table_filters.html:212 +#: part/templates/part/detail.html:198 templates/js/table_filters.js:272 msgid "Salable" msgstr "" -#: part/templates/part/detail.html:193 +#: part/templates/part/detail.html:201 msgid "Part can be sold to customers" msgstr "" -#: part/templates/part/detail.html:195 +#: part/templates/part/detail.html:203 msgid "Part cannot be sold to customers" msgstr "" -#: part/templates/part/detail.html:199 templates/js/table_filters.html:183 +#: part/templates/part/detail.html:214 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/templates/part/detail.html:202 +#: part/templates/part/detail.html:217 msgid "Part is active" msgstr "" -#: part/templates/part/detail.html:204 +#: part/templates/part/detail.html:219 msgid "Part is not active" msgstr "" @@ -2734,10 +2990,6 @@ msgstr "" msgid "Part Notes" msgstr "" -#: part/templates/part/orders.html:14 -msgid "Order Part" -msgstr "" - #: part/templates/part/params.html:8 msgid "Part Parameters" msgstr "" @@ -2746,21 +2998,17 @@ msgstr "" msgid "Add new parameter" msgstr "" -#: part/templates/part/params.html:14 templates/InvenTree/settings/part.html:27 +#: part/templates/part/params.html:15 templates/InvenTree/settings/part.html:28 msgid "New Parameter" msgstr "" -#: part/templates/part/params.html:23 stock/models.py:1391 -#: templates/js/stock.html:112 +#: part/templates/part/params.html:25 stock/models.py:1415 +#: templates/js/stock.js:112 msgid "Value" msgstr "" -#: part/templates/part/params.html:36 -msgid "Edit" -msgstr "" - -#: part/templates/part/params.html:39 part/templates/part/supplier.html:17 -#: users/models.py:145 +#: part/templates/part/params.html:41 part/templates/part/supplier.html:19 +#: users/models.py:146 msgid "Delete" msgstr "" @@ -2780,8 +3028,8 @@ msgstr "" msgid "This part is a variant of" msgstr "" -#: part/templates/part/part_base.html:36 templates/js/company.html:153 -#: templates/js/part.html:336 +#: part/templates/part/part_base.html:36 templates/js/company.js:155 +#: templates/js/part.js:95 templates/js/part.js:172 msgid "Inactive" msgstr "" @@ -2790,19 +3038,19 @@ msgid "Star this part" msgstr "" #: part/templates/part/part_base.html:49 -#: stock/templates/stock/item_base.html:88 +#: stock/templates/stock/item_base.html:101 #: stock/templates/stock/location.html:29 msgid "Barcode actions" msgstr "" #: part/templates/part/part_base.html:51 -#: stock/templates/stock/item_base.html:90 +#: stock/templates/stock/item_base.html:103 #: stock/templates/stock/location.html:31 msgid "Show QR Code" msgstr "" #: part/templates/part/part_base.html:52 -#: stock/templates/stock/item_base.html:91 +#: stock/templates/stock/item_base.html:104 #: stock/templates/stock/location.html:32 msgid "Print Label" msgstr "" @@ -2831,7 +3079,7 @@ msgstr "" msgid "Delete part" msgstr "" -#: part/templates/part/part_base.html:124 templates/js/table_filters.html:65 +#: part/templates/part/part_base.html:124 templates/js/table_filters.js:111 msgid "In Stock" msgstr "" @@ -2891,8 +3139,8 @@ msgstr "" msgid "Part Stock" msgstr "" -#: part/templates/part/stock_count.html:7 templates/js/bom.html:197 -#: templates/js/part.html:396 +#: part/templates/part/stock_count.html:7 templates/js/bom.js:224 +#: templates/js/part.js:442 msgid "No Stock" msgstr "" @@ -2904,15 +3152,15 @@ msgstr "" msgid "Part Suppliers" msgstr "" -#: part/templates/part/supplier.html:17 +#: part/templates/part/supplier.html:19 msgid "Delete supplier parts" msgstr "" -#: part/templates/part/supplier.html:46 +#: part/templates/part/supplier.html:48 msgid "Create new supplier" msgstr "" -#: part/templates/part/supplier.html:52 +#: part/templates/part/supplier.html:54 msgid "Create new manufacturer" msgstr "" @@ -2932,7 +3180,7 @@ msgstr "" msgid "Used In" msgstr "" -#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:304 +#: part/templates/part/tabs.html:61 stock/templates/stock/item_base.html:317 msgid "Tests" msgstr "" @@ -2944,19 +3192,15 @@ msgstr "" msgid "Assemblies" msgstr "" -#: part/templates/part/used_in.html:43 -msgid "INACTIVE" -msgstr "" - #: part/templates/part/variants.html:11 msgid "Part Variants" msgstr "" -#: part/templates/part/variants.html:21 +#: part/templates/part/variants.html:22 msgid "Create new variant" msgstr "" -#: part/templates/part/variants.html:21 +#: part/templates/part/variants.html:23 msgid "New Variant" msgstr "" @@ -2964,172 +3208,196 @@ msgstr "" msgid "Add part attachment" msgstr "" -#: part/views.py:131 templates/attachment_table.html:32 +#: part/views.py:135 templates/attachment_table.html:34 msgid "Edit attachment" msgstr "" -#: part/views.py:137 +#: part/views.py:141 msgid "Part attachment updated" msgstr "" -#: part/views.py:152 +#: part/views.py:156 msgid "Delete Part Attachment" msgstr "" -#: part/views.py:160 +#: part/views.py:164 msgid "Deleted part attachment" msgstr "" -#: part/views.py:169 +#: part/views.py:173 msgid "Create Test Template" msgstr "" -#: part/views.py:198 +#: part/views.py:202 msgid "Edit Test Template" msgstr "" -#: part/views.py:214 +#: part/views.py:218 msgid "Delete Test Template" msgstr "" -#: part/views.py:223 +#: part/views.py:227 msgid "Set Part Category" msgstr "" -#: part/views.py:273 +#: part/views.py:277 #, python-brace-format msgid "Set category for {n} parts" msgstr "" -#: part/views.py:308 +#: part/views.py:312 msgid "Create Variant" msgstr "" -#: part/views.py:388 +#: part/views.py:394 msgid "Duplicate Part" msgstr "" -#: part/views.py:395 +#: part/views.py:401 msgid "Copied part" msgstr "" -#: part/views.py:520 +#: part/views.py:455 part/views.py:585 +msgid "Possible matches exist - confirm creation of new part" +msgstr "" + +#: part/views.py:520 templates/js/stock.js:840 +msgid "Create New Part" +msgstr "" + +#: part/views.py:527 msgid "Created new part" msgstr "" -#: part/views.py:735 +#: part/views.py:743 msgid "Part QR Code" msgstr "" -#: part/views.py:754 +#: part/views.py:762 msgid "Upload Part Image" msgstr "" -#: part/views.py:762 part/views.py:799 +#: part/views.py:770 part/views.py:807 msgid "Updated part image" msgstr "" -#: part/views.py:771 +#: part/views.py:779 msgid "Select Part Image" msgstr "" -#: part/views.py:802 +#: part/views.py:810 msgid "Part image not found" msgstr "" -#: part/views.py:813 +#: part/views.py:821 msgid "Edit Part Properties" msgstr "" -#: part/views.py:837 +#: part/views.py:848 +msgid "Duplicate BOM" +msgstr "" + +#: part/views.py:879 +msgid "Confirm duplication of BOM from parent" +msgstr "" + +#: part/views.py:900 msgid "Validate BOM" msgstr "" -#: part/views.py:1004 +#: part/views.py:923 +msgid "Confirm that the BOM is valid" +msgstr "" + +#: part/views.py:934 +msgid "Validated Bill of Materials" +msgstr "" + +#: part/views.py:1068 msgid "No BOM file provided" msgstr "" -#: part/views.py:1354 +#: part/views.py:1418 msgid "Enter a valid quantity" msgstr "" -#: part/views.py:1379 part/views.py:1382 +#: part/views.py:1443 part/views.py:1446 msgid "Select valid part" msgstr "" -#: part/views.py:1388 +#: part/views.py:1452 msgid "Duplicate part selected" msgstr "" -#: part/views.py:1426 +#: part/views.py:1490 msgid "Select a part" msgstr "" -#: part/views.py:1432 +#: part/views.py:1496 msgid "Selected part creates a circular BOM" msgstr "" -#: part/views.py:1436 +#: part/views.py:1500 msgid "Specify quantity" msgstr "" -#: part/views.py:1692 +#: part/views.py:1756 msgid "Confirm Part Deletion" msgstr "" -#: part/views.py:1701 +#: part/views.py:1765 msgid "Part was deleted" msgstr "" -#: part/views.py:1710 +#: part/views.py:1774 msgid "Part Pricing" msgstr "" -#: part/views.py:1836 +#: part/views.py:1900 msgid "Create Part Parameter Template" msgstr "" -#: part/views.py:1846 +#: part/views.py:1910 msgid "Edit Part Parameter Template" msgstr "" -#: part/views.py:1855 +#: part/views.py:1919 msgid "Delete Part Parameter Template" msgstr "" -#: part/views.py:1865 +#: part/views.py:1929 msgid "Create Part Parameter" msgstr "" -#: part/views.py:1917 +#: part/views.py:1981 msgid "Edit Part Parameter" msgstr "" -#: part/views.py:1933 +#: part/views.py:1997 msgid "Delete Part Parameter" msgstr "" -#: part/views.py:1992 +#: part/views.py:2056 msgid "Edit Part Category" msgstr "" -#: part/views.py:2029 +#: part/views.py:2093 msgid "Delete Part Category" msgstr "" -#: part/views.py:2037 +#: part/views.py:2101 msgid "Part category was deleted" msgstr "" -#: part/views.py:2100 -msgid "Create BOM item" +#: part/views.py:2164 +msgid "Create BOM Item" msgstr "" -#: part/views.py:2168 +#: part/views.py:2232 msgid "Edit BOM item" msgstr "" -#: part/views.py:2218 +#: part/views.py:2282 msgid "Confim BOM item deletion" msgstr "" @@ -3161,6 +3429,10 @@ msgstr "" msgid "Asset file description" msgstr "" +#: stock/forms.py:111 +msgid "Enter unique serial numbers (or leave blank)" +msgstr "" + #: stock/forms.py:191 msgid "Label" msgstr "" @@ -3201,10 +3473,6 @@ msgstr "" msgid "Confirm removal of installed stock items" msgstr "" -#: stock/forms.py:364 -msgid "Destination" -msgstr "" - #: stock/forms.py:364 msgid "Destination stock location" msgstr "" @@ -3213,7 +3481,7 @@ msgstr "" msgid "Add note (required)" msgstr "" -#: stock/forms.py:370 stock/views.py:921 stock/views.py:1119 +#: stock/forms.py:370 stock/views.py:916 stock/views.py:1114 msgid "Confirm stock adjustment" msgstr "" @@ -3229,227 +3497,227 @@ msgstr "" msgid "Set the destination as the default location for selected parts" msgstr "" -#: stock/models.py:212 +#: stock/models.py:178 +msgid "Created stock item" +msgstr "" + +#: stock/models.py:214 msgid "StockItem with this serial number already exists" msgstr "" -#: stock/models.py:248 +#: stock/models.py:250 #, python-brace-format msgid "Part type ('{pf}') must be {pe}" msgstr "" -#: stock/models.py:258 stock/models.py:267 +#: stock/models.py:260 stock/models.py:269 msgid "Quantity must be 1 for item with a serial number" msgstr "" -#: stock/models.py:259 +#: stock/models.py:261 msgid "Serial number cannot be set if quantity greater than 1" msgstr "" -#: stock/models.py:281 +#: stock/models.py:283 msgid "Item cannot belong to itself" msgstr "" -#: stock/models.py:287 +#: stock/models.py:289 msgid "Item must have a build reference if is_building=True" msgstr "" -#: stock/models.py:294 +#: stock/models.py:296 msgid "Build reference does not point to the same part object" msgstr "" -#: stock/models.py:327 +#: stock/models.py:329 msgid "Parent Stock Item" msgstr "" -#: stock/models.py:336 +#: stock/models.py:338 msgid "Base part" msgstr "" -#: stock/models.py:345 +#: stock/models.py:347 msgid "Select a matching supplier part for this stock item" msgstr "" -#: stock/models.py:350 stock/templates/stock/stock_app_base.html:7 +#: stock/models.py:352 stock/templates/stock/stock_app_base.html:7 msgid "Stock Location" msgstr "" -#: stock/models.py:353 +#: stock/models.py:355 msgid "Where is this stock item located?" msgstr "" -#: stock/models.py:358 stock/templates/stock/item_base.html:199 +#: stock/models.py:360 stock/templates/stock/item_base.html:212 msgid "Installed In" msgstr "" -#: stock/models.py:361 +#: stock/models.py:363 msgid "Is this item installed in another item?" msgstr "" -#: stock/models.py:377 +#: stock/models.py:379 msgid "Serial number for this item" msgstr "" -#: stock/models.py:389 +#: stock/models.py:391 msgid "Batch code for this stock item" msgstr "" -#: stock/models.py:393 +#: stock/models.py:395 msgid "Stock Quantity" msgstr "" -#: stock/models.py:402 +#: stock/models.py:404 msgid "Source Build" msgstr "" -#: stock/models.py:404 +#: stock/models.py:406 msgid "Build for this stock item" msgstr "" -#: stock/models.py:415 +#: stock/models.py:417 msgid "Source Purchase Order" msgstr "" -#: stock/models.py:418 +#: stock/models.py:420 msgid "Purchase order for this stock item" msgstr "" -#: stock/models.py:424 +#: stock/models.py:426 msgid "Destination Sales Order" msgstr "" -#: stock/models.py:431 +#: stock/models.py:433 msgid "Destination Build Order" msgstr "" -#: stock/models.py:444 +#: stock/models.py:446 msgid "Delete this Stock Item when stock is depleted" msgstr "" -#: stock/models.py:454 stock/templates/stock/item_notes.html:14 +#: stock/models.py:456 stock/templates/stock/item_notes.html:14 #: stock/templates/stock/item_notes.html:30 msgid "Stock Item Notes" msgstr "" -#: stock/models.py:505 +#: stock/models.py:507 msgid "Assigned to Customer" msgstr "" -#: stock/models.py:507 +#: stock/models.py:509 msgid "Manually assigned to customer" msgstr "" -#: stock/models.py:520 +#: stock/models.py:522 msgid "Returned from customer" msgstr "" -#: stock/models.py:522 +#: stock/models.py:524 msgid "Returned to location" msgstr "" -#: stock/models.py:650 +#: stock/models.py:652 msgid "Installed into stock item" msgstr "" -#: stock/models.py:658 +#: stock/models.py:660 msgid "Installed stock item" msgstr "" -#: stock/models.py:682 +#: stock/models.py:684 msgid "Uninstalled stock item" msgstr "" -#: stock/models.py:701 +#: stock/models.py:703 msgid "Uninstalled into location" msgstr "" -#: stock/models.py:796 +#: stock/models.py:807 msgid "Part is not set as trackable" msgstr "" -#: stock/models.py:802 +#: stock/models.py:813 msgid "Quantity must be integer" msgstr "" -#: stock/models.py:808 +#: stock/models.py:819 #, python-brace-format msgid "Quantity must not exceed available stock quantity ({n})" msgstr "" -#: stock/models.py:811 +#: stock/models.py:822 msgid "Serial numbers must be a list of integers" msgstr "" -#: stock/models.py:814 +#: stock/models.py:825 msgid "Quantity does not match serial numbers" msgstr "" -#: stock/models.py:824 -msgid "Serial numbers already exist: " -msgstr "" - -#: stock/models.py:849 +#: stock/models.py:857 msgid "Add serial number" msgstr "" -#: stock/models.py:852 +#: stock/models.py:860 #, python-brace-format msgid "Serialized {n} items" msgstr "" -#: stock/models.py:963 +#: stock/models.py:971 msgid "StockItem cannot be moved as it is not in stock" msgstr "" -#: stock/models.py:1292 +#: stock/models.py:1316 msgid "Tracking entry title" msgstr "" -#: stock/models.py:1294 +#: stock/models.py:1318 msgid "Entry notes" msgstr "" -#: stock/models.py:1296 +#: stock/models.py:1320 msgid "Link to external page for further information" msgstr "" -#: stock/models.py:1356 +#: stock/models.py:1380 msgid "Value must be provided for this test" msgstr "" -#: stock/models.py:1362 +#: stock/models.py:1386 msgid "Attachment must be uploaded for this test" msgstr "" -#: stock/models.py:1379 +#: stock/models.py:1403 msgid "Test" msgstr "" -#: stock/models.py:1380 +#: stock/models.py:1404 msgid "Test name" msgstr "" -#: stock/models.py:1385 +#: stock/models.py:1409 msgid "Result" msgstr "" -#: stock/models.py:1386 templates/js/table_filters.html:111 +#: stock/models.py:1410 templates/js/table_filters.js:162 msgid "Test result" msgstr "" -#: stock/models.py:1392 +#: stock/models.py:1416 msgid "Test output value" msgstr "" -#: stock/models.py:1398 +#: stock/models.py:1422 msgid "Attachment" msgstr "" -#: stock/models.py:1399 +#: stock/models.py:1423 msgid "Test result attachment" msgstr "" -#: stock/models.py:1405 +#: stock/models.py:1429 msgid "Test notes" msgstr "" @@ -3457,137 +3725,154 @@ msgstr "" msgid "Stock Tracking Information" msgstr "" +#: stock/templates/stock/item.html:18 +msgid "New Entry" +msgstr "" + #: stock/templates/stock/item_attachments.html:10 msgid "Stock Item Attachments" msgstr "" #: stock/templates/stock/item_base.html:20 +msgid "This stock item is in production and cannot be edited." +msgstr "" + +#: stock/templates/stock/item_base.html:21 +msgid "Edit the stock item from the build view." +msgstr "" + +#: stock/templates/stock/item_base.html:34 msgid "This stock item has not passed all required tests" msgstr "" -#: stock/templates/stock/item_base.html:26 +#: stock/templates/stock/item_base.html:40 msgid "This stock item is allocated to Sales Order" msgstr "" -#: stock/templates/stock/item_base.html:32 +#: stock/templates/stock/item_base.html:46 msgid "This stock item is allocated to Build" msgstr "" -#: stock/templates/stock/item_base.html:38 +#: stock/templates/stock/item_base.html:52 msgid "" "This stock item is serialized - it has a unique serial number and the " "quantity cannot be adjusted." msgstr "" -#: stock/templates/stock/item_base.html:42 +#: stock/templates/stock/item_base.html:56 msgid "This stock item cannot be deleted as it has child items" msgstr "" -#: stock/templates/stock/item_base.html:46 +#: stock/templates/stock/item_base.html:60 msgid "" "This stock item will be automatically deleted when all stock is depleted." msgstr "" -#: stock/templates/stock/item_base.html:94 templates/js/barcode.html:283 -#: templates/js/barcode.html:288 +#: stock/templates/stock/item_base.html:107 templates/js/barcode.js:283 +#: templates/js/barcode.js:288 msgid "Unlink Barcode" msgstr "" -#: stock/templates/stock/item_base.html:96 +#: stock/templates/stock/item_base.html:109 msgid "Link Barcode" msgstr "" -#: stock/templates/stock/item_base.html:104 +#: stock/templates/stock/item_base.html:117 msgid "Stock adjustment actions" msgstr "" -#: stock/templates/stock/item_base.html:108 -#: stock/templates/stock/location.html:41 templates/stock_table.html:19 +#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/location.html:41 templates/stock_table.html:23 msgid "Count stock" msgstr "" -#: stock/templates/stock/item_base.html:109 templates/stock_table.html:17 +#: stock/templates/stock/item_base.html:122 templates/stock_table.html:21 msgid "Add stock" msgstr "" -#: stock/templates/stock/item_base.html:110 templates/stock_table.html:18 +#: stock/templates/stock/item_base.html:123 templates/stock_table.html:22 msgid "Remove stock" msgstr "" -#: stock/templates/stock/item_base.html:112 +#: stock/templates/stock/item_base.html:125 msgid "Transfer stock" msgstr "" -#: stock/templates/stock/item_base.html:114 +#: stock/templates/stock/item_base.html:127 msgid "Serialize stock" msgstr "" -#: stock/templates/stock/item_base.html:118 +#: stock/templates/stock/item_base.html:131 msgid "Assign to customer" msgstr "" -#: stock/templates/stock/item_base.html:121 +#: stock/templates/stock/item_base.html:134 msgid "Return to stock" msgstr "" -#: stock/templates/stock/item_base.html:125 templates/js/stock.html:934 +#: stock/templates/stock/item_base.html:138 templates/js/stock.js:981 msgid "Uninstall stock item" msgstr "" -#: stock/templates/stock/item_base.html:125 +#: stock/templates/stock/item_base.html:138 msgid "Uninstall" msgstr "" -#: stock/templates/stock/item_base.html:134 +#: stock/templates/stock/item_base.html:147 #: stock/templates/stock/location.html:38 msgid "Stock actions" msgstr "" -#: stock/templates/stock/item_base.html:137 +#: stock/templates/stock/item_base.html:150 msgid "Convert to variant" msgstr "" -#: stock/templates/stock/item_base.html:140 +#: stock/templates/stock/item_base.html:153 msgid "Duplicate stock item" msgstr "" -#: stock/templates/stock/item_base.html:142 +#: stock/templates/stock/item_base.html:155 msgid "Edit stock item" msgstr "" -#: stock/templates/stock/item_base.html:145 +#: stock/templates/stock/item_base.html:158 msgid "Delete stock item" msgstr "" -#: stock/templates/stock/item_base.html:151 +#: stock/templates/stock/item_base.html:164 msgid "Generate test report" msgstr "" -#: stock/templates/stock/item_base.html:159 +#: stock/templates/stock/item_base.html:172 msgid "Stock Item Details" msgstr "" -#: stock/templates/stock/item_base.html:224 +#: stock/templates/stock/item_base.html:237 templates/js/build.js:426 msgid "No location set" msgstr "" -#: stock/templates/stock/item_base.html:231 +#: stock/templates/stock/item_base.html:244 msgid "Barcode Identifier" msgstr "" -#: stock/templates/stock/item_base.html:259 +#: stock/templates/stock/item_base.html:258 templates/js/build.js:626 +#: templates/navbar.html:25 +msgid "Build" +msgstr "" + +#: stock/templates/stock/item_base.html:272 msgid "Parent Item" msgstr "" -#: stock/templates/stock/item_base.html:284 +#: stock/templates/stock/item_base.html:297 msgid "Last Updated" msgstr "" -#: stock/templates/stock/item_base.html:289 +#: stock/templates/stock/item_base.html:302 msgid "Last Stocktake" msgstr "" -#: stock/templates/stock/item_base.html:293 +#: stock/templates/stock/item_base.html:306 msgid "No stocktake performed" msgstr "" @@ -3635,15 +3920,15 @@ msgstr "" msgid "Test Data" msgstr "" -#: stock/templates/stock/item_tests.html:17 +#: stock/templates/stock/item_tests.html:18 msgid "Delete Test Data" msgstr "" -#: stock/templates/stock/item_tests.html:19 +#: stock/templates/stock/item_tests.html:22 msgid "Add Test Data" msgstr "" -#: stock/templates/stock/item_tests.html:20 +#: stock/templates/stock/item_tests.html:25 msgid "Test Report" msgstr "" @@ -3707,7 +3992,7 @@ msgstr "" msgid "The following stock items will be uninstalled" msgstr "" -#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1315 +#: stock/templates/stock/stockitem_convert.html:7 stock/views.py:1310 msgid "Convert Stock Item" msgstr "" @@ -3751,202 +4036,210 @@ msgstr "" msgid "Add Stock Item Attachment" msgstr "" -#: stock/views.py:209 +#: stock/views.py:210 msgid "Edit Stock Item Attachment" msgstr "" -#: stock/views.py:226 +#: stock/views.py:227 msgid "Delete Stock Item Attachment" msgstr "" -#: stock/views.py:243 +#: stock/views.py:244 msgid "Assign to Customer" msgstr "" -#: stock/views.py:281 +#: stock/views.py:254 +msgid "Customer must be specified" +msgstr "" + +#: stock/views.py:278 msgid "Return to Stock" msgstr "" -#: stock/views.py:301 +#: stock/views.py:288 msgid "Specify a valid location" msgstr "" -#: stock/views.py:305 +#: stock/views.py:299 msgid "Stock item returned from customer" msgstr "" -#: stock/views.py:317 +#: stock/views.py:309 msgid "Select Label Template" msgstr "" -#: stock/views.py:340 +#: stock/views.py:332 msgid "Select valid label" msgstr "" -#: stock/views.py:404 +#: stock/views.py:396 msgid "Delete All Test Data" msgstr "" -#: stock/views.py:420 +#: stock/views.py:412 msgid "Confirm test data deletion" msgstr "" -#: stock/views.py:440 +#: stock/views.py:432 msgid "Add Test Result" msgstr "" -#: stock/views.py:478 +#: stock/views.py:473 msgid "Edit Test Result" msgstr "" -#: stock/views.py:496 +#: stock/views.py:491 msgid "Delete Test Result" msgstr "" -#: stock/views.py:508 +#: stock/views.py:503 msgid "Select Test Report Template" msgstr "" -#: stock/views.py:523 +#: stock/views.py:518 msgid "Select valid template" msgstr "" -#: stock/views.py:576 +#: stock/views.py:571 msgid "Stock Export Options" msgstr "" -#: stock/views.py:698 +#: stock/views.py:693 msgid "Stock Item QR Code" msgstr "" -#: stock/views.py:724 +#: stock/views.py:719 msgid "Install Stock Item" msgstr "" -#: stock/views.py:824 +#: stock/views.py:819 msgid "Uninstall Stock Items" msgstr "" -#: stock/views.py:932 +#: stock/views.py:927 msgid "Uninstalled stock items" msgstr "" -#: stock/views.py:957 +#: stock/views.py:952 msgid "Adjust Stock" msgstr "" -#: stock/views.py:1067 +#: stock/views.py:1062 msgid "Move Stock Items" msgstr "" -#: stock/views.py:1068 +#: stock/views.py:1063 msgid "Count Stock Items" msgstr "" -#: stock/views.py:1069 +#: stock/views.py:1064 msgid "Remove From Stock" msgstr "" -#: stock/views.py:1070 +#: stock/views.py:1065 msgid "Add Stock Items" msgstr "" -#: stock/views.py:1071 +#: stock/views.py:1066 msgid "Delete Stock Items" msgstr "" -#: stock/views.py:1099 +#: stock/views.py:1094 msgid "Must enter integer value" msgstr "" -#: stock/views.py:1104 +#: stock/views.py:1099 msgid "Quantity must be positive" msgstr "" -#: stock/views.py:1111 +#: stock/views.py:1106 #, python-brace-format msgid "Quantity must not exceed {x}" msgstr "" -#: stock/views.py:1190 +#: stock/views.py:1185 #, python-brace-format msgid "Added stock to {n} items" msgstr "" -#: stock/views.py:1205 +#: stock/views.py:1200 #, python-brace-format msgid "Removed stock from {n} items" msgstr "" -#: stock/views.py:1218 +#: stock/views.py:1213 #, python-brace-format msgid "Counted stock for {n} items" msgstr "" -#: stock/views.py:1246 +#: stock/views.py:1241 msgid "No items were moved" msgstr "" -#: stock/views.py:1249 +#: stock/views.py:1244 #, python-brace-format msgid "Moved {n} items to {dest}" msgstr "" -#: stock/views.py:1268 +#: stock/views.py:1263 #, python-brace-format msgid "Deleted {n} stock items" msgstr "" -#: stock/views.py:1280 +#: stock/views.py:1275 msgid "Edit Stock Item" msgstr "" -#: stock/views.py:1365 +#: stock/views.py:1360 msgid "Serialize Stock" msgstr "" -#: stock/views.py:1559 +#: stock/views.py:1454 templates/js/build.js:210 +msgid "Create new Stock Item" +msgstr "" + +#: stock/views.py:1553 msgid "Duplicate Stock Item" msgstr "" -#: stock/views.py:1625 +#: stock/views.py:1619 msgid "Invalid quantity" msgstr "" -#: stock/views.py:1628 +#: stock/views.py:1622 msgid "Quantity cannot be less than zero" msgstr "" -#: stock/views.py:1632 +#: stock/views.py:1626 msgid "Invalid part selection" msgstr "" -#: stock/views.py:1681 +#: stock/views.py:1674 #, python-brace-format msgid "Created {n} new stock items" msgstr "" -#: stock/views.py:1700 stock/views.py:1716 +#: stock/views.py:1693 stock/views.py:1709 msgid "Created new stock item" msgstr "" -#: stock/views.py:1735 +#: stock/views.py:1728 msgid "Delete Stock Location" msgstr "" -#: stock/views.py:1749 +#: stock/views.py:1742 msgid "Delete Stock Item" msgstr "" -#: stock/views.py:1761 +#: stock/views.py:1754 msgid "Delete Stock Tracking Entry" msgstr "" -#: stock/views.py:1780 +#: stock/views.py:1773 msgid "Edit Stock Tracking Entry" msgstr "" -#: stock/views.py:1790 +#: stock/views.py:1783 msgid "Add Stock Tracking Entry" msgstr "" @@ -4002,11 +4295,11 @@ msgstr "" msgid "Enter a search query" msgstr "" -#: templates/InvenTree/search.html:191 templates/js/stock.html:528 +#: templates/InvenTree/search.html:191 templates/js/stock.js:289 msgid "Shipped to customer" msgstr "" -#: templates/InvenTree/search.html:194 templates/js/stock.html:538 +#: templates/InvenTree/search.html:194 templates/js/stock.js:299 msgid "No stock location set" msgstr "" @@ -4026,7 +4319,7 @@ msgstr "" msgid "Currencies" msgstr "" -#: templates/InvenTree/settings/currency.html:17 +#: templates/InvenTree/settings/currency.html:18 msgid "New Currency" msgstr "" @@ -4042,15 +4335,15 @@ msgstr "" msgid "Part Parameter Templates" msgstr "" -#: templates/InvenTree/settings/part.html:43 +#: templates/InvenTree/settings/part.html:45 msgid "No part parameter templates found" msgstr "" -#: templates/InvenTree/settings/part.html:63 +#: templates/InvenTree/settings/part.html:65 msgid "Edit Template" msgstr "" -#: templates/InvenTree/settings/part.html:64 +#: templates/InvenTree/settings/part.html:66 msgid "Delete Template" msgstr "" @@ -4126,19 +4419,23 @@ msgstr "" msgid "User Information" msgstr "" -#: templates/InvenTree/settings/user.html:24 -msgid "Username" +#: templates/InvenTree/settings/user.html:21 +msgid "Change Password" msgstr "" #: templates/InvenTree/settings/user.html:28 -msgid "First Name" +msgid "Username" msgstr "" #: templates/InvenTree/settings/user.html:32 -msgid "Last Name" +msgid "First Name" msgstr "" #: templates/InvenTree/settings/user.html:36 +msgid "Last Name" +msgstr "" + +#: templates/InvenTree/settings/user.html:40 msgid "Email Address" msgstr "" @@ -4186,465 +4483,553 @@ msgstr "" msgid "Submit Bug Report" msgstr "" -#: templates/attachment_table.html:6 +#: templates/attachment_table.html:7 msgid "Add Attachment" msgstr "" -#: templates/attachment_table.html:15 +#: templates/attachment_table.html:17 msgid "File" msgstr "" -#: templates/attachment_table.html:16 +#: templates/attachment_table.html:18 msgid "Comment" msgstr "" -#: templates/attachment_table.html:17 +#: templates/attachment_table.html:19 msgid "Uploaded" msgstr "" -#: templates/attachment_table.html:35 +#: templates/attachment_table.html:37 msgid "Delete attachment" msgstr "" -#: templates/js/barcode.html:8 +#: templates/js/barcode.js:8 msgid "Scan barcode data here using wedge scanner" msgstr "" -#: templates/js/barcode.html:12 +#: templates/js/barcode.js:12 msgid "Barcode" msgstr "" -#: templates/js/barcode.html:20 +#: templates/js/barcode.js:20 msgid "Enter barcode data" msgstr "" -#: templates/js/barcode.html:42 +#: templates/js/barcode.js:42 msgid "Invalid server response" msgstr "" -#: templates/js/barcode.html:143 +#: templates/js/barcode.js:143 msgid "Scan barcode data below" msgstr "" -#: templates/js/barcode.html:217 templates/js/barcode.html:263 +#: templates/js/barcode.js:217 templates/js/barcode.js:263 msgid "Unknown response from server" msgstr "" -#: templates/js/barcode.html:239 +#: templates/js/barcode.js:239 msgid "Link Barcode to Stock Item" msgstr "" -#: templates/js/barcode.html:285 +#: templates/js/barcode.js:285 msgid "" "This will remove the association between this stock item and the barcode" msgstr "" -#: templates/js/barcode.html:291 +#: templates/js/barcode.js:291 msgid "Unlink" msgstr "" -#: templates/js/barcode.html:350 +#: templates/js/barcode.js:350 msgid "Remove stock item" msgstr "" -#: templates/js/barcode.html:397 +#: templates/js/barcode.js:397 msgid "Enter notes" msgstr "" -#: templates/js/barcode.html:399 +#: templates/js/barcode.js:399 msgid "Enter optional notes for stock transfer" msgstr "" -#: templates/js/barcode.html:404 +#: templates/js/barcode.js:404 msgid "Check Stock Items into Location" msgstr "" -#: templates/js/barcode.html:408 +#: templates/js/barcode.js:408 msgid "Check In" msgstr "" -#: templates/js/barcode.html:466 +#: templates/js/barcode.js:466 msgid "Server error" msgstr "" -#: templates/js/barcode.html:485 +#: templates/js/barcode.js:485 msgid "Stock Item already scanned" msgstr "" -#: templates/js/barcode.html:489 +#: templates/js/barcode.js:489 msgid "Stock Item already in this location" msgstr "" -#: templates/js/barcode.html:496 +#: templates/js/barcode.js:496 msgid "Added stock item" msgstr "" -#: templates/js/barcode.html:503 +#: templates/js/barcode.js:503 msgid "Barcode does not match Stock Item" msgstr "" -#: templates/js/bom.html:132 +#: templates/js/bom.js:159 msgid "Open subassembly" msgstr "" -#: templates/js/bom.html:173 +#: templates/js/bom.js:200 msgid "Optional" msgstr "" -#: templates/js/bom.html:188 templates/js/build.html:133 -msgid "Available" -msgstr "" - -#: templates/js/bom.html:213 +#: templates/js/bom.js:240 msgid "No pricing available" msgstr "" -#: templates/js/bom.html:232 +#: templates/js/bom.js:259 templates/js/build.js:555 msgid "Actions" msgstr "" -#: templates/js/bom.html:240 +#: templates/js/bom.js:267 msgid "Validate BOM Item" msgstr "" -#: templates/js/bom.html:242 +#: templates/js/bom.js:269 msgid "This line has been validated" msgstr "" -#: templates/js/bom.html:244 +#: templates/js/bom.js:271 msgid "Edit BOM Item" msgstr "" -#: templates/js/bom.html:246 +#: templates/js/bom.js:273 msgid "Delete BOM Item" msgstr "" -#: templates/js/build.html:24 +#: templates/js/bom.js:346 templates/js/build.js:289 +msgid "No BOM items found" +msgstr "" + +#: templates/js/bom.js:491 +msgid "INACTIVE" +msgstr "" + +#: templates/js/bom.js:505 +msgid "Uses" +msgstr "" + +#: templates/js/bom.js:516 +msgid "No matching parts found" +msgstr "" + +#: templates/js/build.js:56 +msgid "Auto-allocate stock items to this output" +msgstr "" + +#: templates/js/build.js:62 +msgid "Complete build output" +msgstr "" + +#: templates/js/build.js:71 +msgid "Unallocate stock from build output" +msgstr "" + +#: templates/js/build.js:77 +msgid "Delete build output" +msgstr "" + +#: templates/js/build.js:209 templates/stock_table.html:13 +msgid "New Stock Item" +msgstr "" + +#: templates/js/build.js:477 +msgid "Required Part" +msgstr "" + +#: templates/js/build.js:498 +msgid "Quantity Per" +msgstr "" + +#: templates/js/build.js:562 +msgid "Build stock" +msgstr "" + +#: templates/js/build.js:566 templates/stock_table.html:25 +msgid "Order stock" +msgstr "" + +#: templates/js/build.js:569 +msgid "Allocate stock" +msgstr "" + +#: templates/js/build.js:610 msgid "No builds matching query" msgstr "" -#: templates/js/build.html:122 +#: templates/js/build.js:720 msgid "No parts allocated for" msgstr "" -#: templates/js/company.html:75 +#: templates/js/company.js:75 msgid "Parts Supplied" msgstr "" -#: templates/js/company.html:84 +#: templates/js/company.js:84 msgid "Parts Manufactured" msgstr "" -#: templates/js/company.html:96 +#: templates/js/company.js:96 msgid "No company information found" msgstr "" -#: templates/js/company.html:128 +#: templates/js/company.js:129 msgid "No supplier parts found" msgstr "" -#: templates/js/company.html:145 templates/js/part.html:314 +#: templates/js/company.js:147 templates/js/part.js:79 templates/js/part.js:164 msgid "Template part" msgstr "" -#: templates/js/company.html:149 templates/js/part.html:318 +#: templates/js/company.js:151 templates/js/part.js:83 templates/js/part.js:168 msgid "Assembled part" msgstr "" -#: templates/js/company.html:206 +#: templates/js/company.js:208 msgid "Link" msgstr "" -#: templates/js/order.html:128 +#: templates/js/order.js:128 msgid "No purchase orders found" msgstr "" -#: templates/js/order.html:180 templates/js/stock.html:643 +#: templates/js/order.js:180 templates/js/stock.js:677 msgid "Date" msgstr "" -#: templates/js/order.html:210 +#: templates/js/order.js:210 msgid "No sales orders found" msgstr "" -#: templates/js/order.html:267 +#: templates/js/order.js:267 msgid "Shipment Date" msgstr "" -#: templates/js/part.html:137 -msgid "No variants found" +#: templates/js/part.js:71 templates/js/part.js:156 +msgid "Trackable part" msgstr "" -#: templates/js/part.html:223 templates/js/part.html:411 -msgid "No parts found" +#: templates/js/part.js:75 templates/js/part.js:160 +msgid "Virtual part" msgstr "" -#: templates/js/part.html:275 templates/js/stock.html:409 -#: templates/js/stock.html:966 -msgid "Select" -msgstr "" - -#: templates/js/part.html:322 +#: templates/js/part.js:87 msgid "Starred part" msgstr "" -#: templates/js/part.html:326 +#: templates/js/part.js:91 msgid "Salable part" msgstr "" -#: templates/js/part.html:365 +#: templates/js/part.js:205 +msgid "No variants found" +msgstr "" + +#: templates/js/part.js:291 templates/js/part.js:457 +msgid "No parts found" +msgstr "" + +#: templates/js/part.js:343 templates/js/stock.js:456 +#: templates/js/stock.js:1013 +msgid "Select" +msgstr "" + +#: templates/js/part.js:411 msgid "No category" msgstr "" -#: templates/js/part.html:383 templates/js/table_filters.html:196 +#: templates/js/part.js:429 templates/js/table_filters.js:256 msgid "Low stock" msgstr "" -#: templates/js/part.html:392 +#: templates/js/part.js:438 msgid "Building" msgstr "" -#: templates/js/part.html:471 +#: templates/js/part.js:517 msgid "YES" msgstr "" -#: templates/js/part.html:473 +#: templates/js/part.js:519 msgid "NO" msgstr "" -#: templates/js/part.html:507 +#: templates/js/part.js:553 msgid "No test templates matching query" msgstr "" -#: templates/js/part.html:558 templates/js/stock.html:63 +#: templates/js/part.js:604 templates/js/stock.js:63 msgid "Edit test result" msgstr "" -#: templates/js/part.html:559 templates/js/stock.html:64 +#: templates/js/part.js:605 templates/js/stock.js:64 msgid "Delete test result" msgstr "" -#: templates/js/part.html:565 +#: templates/js/part.js:611 msgid "This test is defined for a parent part" msgstr "" -#: templates/js/stock.html:26 +#: templates/js/stock.js:26 msgid "PASS" msgstr "" -#: templates/js/stock.html:28 +#: templates/js/stock.js:28 msgid "FAIL" msgstr "" -#: templates/js/stock.html:33 +#: templates/js/stock.js:33 msgid "NO RESULT" msgstr "" -#: templates/js/stock.html:59 +#: templates/js/stock.js:59 msgid "Add test result" msgstr "" -#: templates/js/stock.html:78 +#: templates/js/stock.js:78 msgid "No test results found" msgstr "" -#: templates/js/stock.html:120 +#: templates/js/stock.js:120 msgid "Test Date" msgstr "" -#: templates/js/stock.html:263 +#: templates/js/stock.js:281 +msgid "In production" +msgstr "" + +#: templates/js/stock.js:285 +msgid "Installed in Stock Item" +msgstr "" + +#: templates/js/stock.js:293 +msgid "Assigned to Sales Order" +msgstr "" + +#: templates/js/stock.js:313 msgid "No stock items matching query" msgstr "" -#: templates/js/stock.html:361 templates/js/stock.html:376 +#: templates/js/stock.js:424 msgid "Undefined location" msgstr "" -#: templates/js/stock.html:469 +#: templates/js/stock.js:518 +msgid "Stock item is in production" +msgstr "" + +#: templates/js/stock.js:523 +msgid "Stock item assigned to sales order" +msgstr "" + +#: templates/js/stock.js:526 +msgid "Stock item assigned to customer" +msgstr "" + +#: templates/js/stock.js:530 msgid "Stock item has been allocated" msgstr "" -#: templates/js/stock.html:473 -msgid "Stock item has been assigned to customer" -msgstr "" - -#: templates/js/stock.html:476 -msgid "Stock item was assigned to a build order" -msgstr "" - -#: templates/js/stock.html:478 -msgid "Stock item was assigned to a sales order" -msgstr "" - -#: templates/js/stock.html:483 +#: templates/js/stock.js:534 msgid "Stock item has been installed in another item" msgstr "" -#: templates/js/stock.html:490 +#: templates/js/stock.js:541 msgid "Stock item has been rejected" msgstr "" -#: templates/js/stock.html:494 +#: templates/js/stock.js:545 msgid "Stock item is lost" msgstr "" -#: templates/js/stock.html:498 templates/js/table_filters.html:60 +#: templates/js/stock.js:549 templates/js/table_filters.js:106 msgid "Depleted" msgstr "" -#: templates/js/stock.html:523 -msgid "Installed in Stock Item " -msgstr "" - -#: templates/js/stock.html:531 -msgid "Assigned to sales order" -msgstr "" - -#: templates/js/stock.html:709 +#: templates/js/stock.js:743 msgid "No user information" msgstr "" -#: templates/js/stock.html:793 -msgid "Create New Part" -msgstr "" - -#: templates/js/stock.html:805 +#: templates/js/stock.js:852 msgid "Create New Location" msgstr "" -#: templates/js/stock.html:904 +#: templates/js/stock.js:951 msgid "Serial" msgstr "" -#: templates/js/stock.html:997 templates/js/table_filters.html:70 +#: templates/js/stock.js:1044 templates/js/table_filters.js:121 msgid "Installed" msgstr "" -#: templates/js/stock.html:1022 +#: templates/js/stock.js:1069 msgid "Install item" msgstr "" -#: templates/js/table_filters.html:19 templates/js/table_filters.html:80 +#: templates/js/table_filters.js:41 +msgid "Trackable Part" +msgstr "" + +#: templates/js/table_filters.js:45 +msgid "Validated" +msgstr "" + +#: templates/js/table_filters.js:65 templates/js/table_filters.js:131 msgid "Is Serialized" msgstr "" -#: templates/js/table_filters.html:22 templates/js/table_filters.html:87 +#: templates/js/table_filters.js:68 templates/js/table_filters.js:138 msgid "Serial number GTE" msgstr "" -#: templates/js/table_filters.html:23 templates/js/table_filters.html:88 +#: templates/js/table_filters.js:69 templates/js/table_filters.js:139 msgid "Serial number greater than or equal to" msgstr "" -#: templates/js/table_filters.html:26 templates/js/table_filters.html:91 +#: templates/js/table_filters.js:72 templates/js/table_filters.js:142 msgid "Serial number LTE" msgstr "" -#: templates/js/table_filters.html:27 templates/js/table_filters.html:92 +#: templates/js/table_filters.js:73 templates/js/table_filters.js:143 msgid "Serial number less than or equal to" msgstr "" -#: templates/js/table_filters.html:30 templates/js/table_filters.html:31 -#: templates/js/table_filters.html:83 templates/js/table_filters.html:84 +#: templates/js/table_filters.js:76 templates/js/table_filters.js:77 +#: templates/js/table_filters.js:134 templates/js/table_filters.js:135 msgid "Serial number" msgstr "" -#: templates/js/table_filters.html:35 templates/js/table_filters.html:101 +#: templates/js/table_filters.js:81 templates/js/table_filters.js:152 msgid "Batch code" msgstr "" -#: templates/js/table_filters.html:45 +#: templates/js/table_filters.js:91 templates/js/table_filters.js:223 msgid "Active parts" msgstr "" -#: templates/js/table_filters.html:46 +#: templates/js/table_filters.js:92 msgid "Show stock for active parts" msgstr "" -#: templates/js/table_filters.html:50 +#: templates/js/table_filters.js:96 msgid "Is allocated" msgstr "" -#: templates/js/table_filters.html:51 +#: templates/js/table_filters.js:97 msgid "Item has been alloacted" msgstr "" -#: templates/js/table_filters.html:55 +#: templates/js/table_filters.js:101 msgid "Include sublocations" msgstr "" -#: templates/js/table_filters.html:56 +#: templates/js/table_filters.js:102 msgid "Include stock in sublocations" msgstr "" -#: templates/js/table_filters.html:61 +#: templates/js/table_filters.js:107 msgid "Show stock items which are depleted" msgstr "" -#: templates/js/table_filters.html:66 +#: templates/js/table_filters.js:112 msgid "Show items which are in stock" msgstr "" -#: templates/js/table_filters.html:71 +#: templates/js/table_filters.js:116 +msgid "In Production" +msgstr "" + +#: templates/js/table_filters.js:117 +msgid "Show items which are in production" +msgstr "" + +#: templates/js/table_filters.js:122 msgid "Show stock items which are installed in another item" msgstr "" -#: templates/js/table_filters.html:75 +#: templates/js/table_filters.js:126 msgid "Sent to customer" msgstr "" -#: templates/js/table_filters.html:76 +#: templates/js/table_filters.js:127 msgid "Show items which have been assigned to a customer" msgstr "" -#: templates/js/table_filters.html:96 templates/js/table_filters.html:97 +#: templates/js/table_filters.js:147 templates/js/table_filters.js:148 msgid "Stock status" msgstr "" -#: templates/js/table_filters.html:130 +#: templates/js/table_filters.js:181 msgid "Build status" msgstr "" -#: templates/js/table_filters.html:145 templates/js/table_filters.html:158 +#: templates/js/table_filters.js:196 templates/js/table_filters.js:209 msgid "Order status" msgstr "" -#: templates/js/table_filters.html:150 templates/js/table_filters.html:163 +#: templates/js/table_filters.js:201 templates/js/table_filters.js:214 msgid "Outstanding" msgstr "" -#: templates/js/table_filters.html:173 +#: templates/js/table_filters.js:233 msgid "Include subcategories" msgstr "" -#: templates/js/table_filters.html:174 +#: templates/js/table_filters.js:234 msgid "Include parts in subcategories" msgstr "" -#: templates/js/table_filters.html:178 +#: templates/js/table_filters.js:238 msgid "Has IPN" msgstr "" -#: templates/js/table_filters.html:179 +#: templates/js/table_filters.js:239 msgid "Part has internal part number" msgstr "" -#: templates/js/table_filters.html:184 +#: templates/js/table_filters.js:244 msgid "Show active parts" msgstr "" -#: templates/js/table_filters.html:192 +#: templates/js/table_filters.js:252 msgid "Stock available" msgstr "" -#: templates/js/table_filters.html:208 +#: templates/js/table_filters.js:268 msgid "Starred" msgstr "" -#: templates/js/table_filters.html:220 +#: templates/js/table_filters.js:280 msgid "Purchasable" msgstr "" +#: templates/modals.html:13 templates/modals.html:35 +msgid "Form errors exist" +msgstr "" + +#: templates/modals.html:18 templates/modals.html:40 +msgid "Close" +msgstr "" + +#: templates/modals.html:19 templates/modals.html:41 +msgid "Submit" +msgstr "" + #: templates/navbar.html:29 msgid "Buy" msgstr "" @@ -4685,39 +5070,35 @@ msgstr "" msgid "Export Stock Information" msgstr "" -#: templates/stock_table.html:17 +#: templates/stock_table.html:21 msgid "Add to selected stock items" msgstr "" -#: templates/stock_table.html:18 +#: templates/stock_table.html:22 msgid "Remove from selected stock items" msgstr "" -#: templates/stock_table.html:19 +#: templates/stock_table.html:23 msgid "Stocktake selected stock items" msgstr "" -#: templates/stock_table.html:20 +#: templates/stock_table.html:24 msgid "Move selected stock items" msgstr "" -#: templates/stock_table.html:20 +#: templates/stock_table.html:24 msgid "Move stock" msgstr "" -#: templates/stock_table.html:21 +#: templates/stock_table.html:25 msgid "Order selected items" msgstr "" -#: templates/stock_table.html:21 -msgid "Order stock" -msgstr "" - -#: templates/stock_table.html:24 +#: templates/stock_table.html:28 msgid "Delete selected items" msgstr "" -#: templates/stock_table.html:24 +#: templates/stock_table.html:28 msgid "Delete Stock" msgstr "" @@ -4741,38 +5122,38 @@ msgstr "" msgid "Important dates" msgstr "" -#: users/models.py:128 +#: users/models.py:129 msgid "Permission set" msgstr "" -#: users/models.py:136 +#: users/models.py:137 msgid "Group" msgstr "" -#: users/models.py:139 +#: users/models.py:140 msgid "View" msgstr "" -#: users/models.py:139 +#: users/models.py:140 msgid "Permission to view items" msgstr "" -#: users/models.py:141 +#: users/models.py:142 msgid "Add" msgstr "" -#: users/models.py:141 +#: users/models.py:142 msgid "Permission to add items" msgstr "" -#: users/models.py:143 +#: users/models.py:144 msgid "Change" msgstr "" -#: users/models.py:143 +#: users/models.py:144 msgid "Permissions to edit items" msgstr "" -#: users/models.py:145 +#: users/models.py:146 msgid "Permission to delete items" msgstr "" diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 1f412271e0..770dd392bf 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -21,7 +21,7 @@ from .models import SalesOrderAllocation class IssuePurchaseOrderForm(HelperForm): - confirm = forms.BooleanField(required=False, help_text=_('Place order')) + confirm = forms.BooleanField(required=True, initial=False, help_text=_('Place order')) class Meta: model = PurchaseOrder @@ -32,7 +32,7 @@ class IssuePurchaseOrderForm(HelperForm): class CompletePurchaseOrderForm(HelperForm): - confirm = forms.BooleanField(required=False, help_text=_("Mark order as complete")) + confirm = forms.BooleanField(required=True, help_text=_("Mark order as complete")) class Meta: model = PurchaseOrder @@ -43,7 +43,7 @@ class CompletePurchaseOrderForm(HelperForm): class CancelPurchaseOrderForm(HelperForm): - confirm = forms.BooleanField(required=False, help_text=_('Cancel order')) + confirm = forms.BooleanField(required=True, help_text=_('Cancel order')) class Meta: model = PurchaseOrder @@ -54,7 +54,7 @@ class CancelPurchaseOrderForm(HelperForm): class CancelSalesOrderForm(HelperForm): - confirm = forms.BooleanField(required=False, help_text=_('Cancel order')) + confirm = forms.BooleanField(required=True, help_text=_('Cancel order')) class Meta: model = SalesOrder @@ -65,7 +65,7 @@ class CancelSalesOrderForm(HelperForm): class ShipSalesOrderForm(HelperForm): - confirm = forms.BooleanField(required=False, help_text=_('Ship order')) + confirm = forms.BooleanField(required=True, help_text=_('Ship order')) class Meta: model = SalesOrder diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 7d561b95ba..b193454e1f 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -209,6 +209,7 @@ class PurchaseOrder(Order): line.save() + @transaction.atomic def place_order(self): """ Marks the PurchaseOrder as PLACED. Order must be currently PENDING. """ @@ -217,6 +218,7 @@ class PurchaseOrder(Order): self.issue_date = datetime.now().date() self.save() + @transaction.atomic def complete_order(self): """ Marks the PurchaseOrder as COMPLETE. Order must be currently PLACED. """ @@ -225,10 +227,16 @@ class PurchaseOrder(Order): self.complete_date = datetime.now().date() self.save() + def can_cancel(self): + return self.status not in [ + PurchaseOrderStatus.PLACED, + PurchaseOrderStatus.PENDING + ] + def cancel_order(self): """ Marks the PurchaseOrder as CANCELLED. """ - if self.status in [PurchaseOrderStatus.PLACED, PurchaseOrderStatus.PENDING]: + if self.can_cancel(): self.status = PurchaseOrderStatus.CANCELLED self.save() @@ -377,6 +385,16 @@ class SalesOrder(Order): return True + def can_cancel(self): + """ + Return True if this order can be cancelled + """ + + if not self.status == SalesOrderStatus.PENDING: + return False + + return True + @transaction.atomic def cancel_order(self): """ @@ -386,7 +404,7 @@ class SalesOrder(Order): - Delete any StockItems which have been allocated """ - if not self.status == SalesOrderStatus.PENDING: + if not self.can_cancel(): return False self.status = SalesOrderStatus.CANCELLED diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 0c569bdca8..2f9e57ca99 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -13,7 +13,8 @@ <div id='order-toolbar-buttons' class='btn-group' style='float: right;'> {% if order.status == PurchaseOrderStatus.PENDING and roles.purchase_order.change %} - <button type='button' class='btn btn-primary' id='new-po-line'>{% trans "Add Line Item" %}</button> + <button type='button' class='btn btn-primary' id='new-po-line'> + <span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %}</button> {% endif %} </div> diff --git a/InvenTree/order/templates/order/purchase_orders.html b/InvenTree/order/templates/order/purchase_orders.html index d02af36ff5..347033a888 100644 --- a/InvenTree/order/templates/order/purchase_orders.html +++ b/InvenTree/order/templates/order/purchase_orders.html @@ -15,7 +15,8 @@ InvenTree | {% trans "Purchase Orders" %} <div id='table-buttons'> <div class='button-toolbar container-fluid' style='float: right;'> {% if roles.purchase_order.add %} - <button class='btn btn-primary' type='button' id='po-create' title='{% trans "Create new purchase order" %}'>{% trans "New Purchase Order" %}</button> + <button class='btn btn-primary' type='button' id='po-create' title='{% trans "Create new purchase order" %}'> + <span class='fas fa-plus-circle'></span> {% trans "New Purchase Order" %}</button> {% endif %} <div class='filter-list' id='filter-list-purchaseorder'> <!-- An empty div in which the filter list will be constructed --> diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index d21a4e950e..8e3128e1f3 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -15,7 +15,9 @@ {% if roles.sales_order.change %} <div id='order-toolbar-buttons' class='btn-group' style='float: right;'> - <button type='button' class='btn btn-default' id='new-so-line'>{% trans "Add Line Item" %}</button> + <button type='button' class='btn btn-success' id='new-so-line'> + <span class='fas fa-plus-circle'></span> {% trans "Add Line Item" %} + </button> </div> {% endif %} diff --git a/InvenTree/order/templates/order/sales_orders.html b/InvenTree/order/templates/order/sales_orders.html index dfe09d5d0d..acfd875078 100644 --- a/InvenTree/order/templates/order/sales_orders.html +++ b/InvenTree/order/templates/order/sales_orders.html @@ -15,7 +15,8 @@ InvenTree | {% trans "Sales Orders" %} <div id='table-buttons'> <div class='button-toolbar container-fluid' style='float: right;'> {% if roles.sales_order.add %} - <button class='btn btn-primary' type='button' id='so-create' title='{% trans "Create new sales order" %}'>{% trans "New Sales Order" %}</button> + <button class='btn btn-primary' type='button' id='so-create' title='{% trans "Create new sales order" %}'> + <span class='fas fa-plus-circle'></span> {% trans "New Sales Order" %}</button> {% endif %} <div class='filter-list' id='filter-list-salesorder'> <!-- An empty div in which the filter list will be constructed --> diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index 246cc2dd48..5c4e2dd405 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -113,6 +113,7 @@ class POTests(OrderViewTestCase): self.assertEqual(response.status_code, 200) data = json.loads(response.content) + self.assertFalse(data['form_valid']) # Test WITH confirmation @@ -151,7 +152,6 @@ class POTests(OrderViewTestCase): response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') data = json.loads(response.content) self.assertFalse(data['form_valid']) - self.assertIn('Invalid Purchase Order', str(data['html_form'])) # POST with a part that does not match the purchase order post_data['order'] = 1 @@ -159,14 +159,12 @@ class POTests(OrderViewTestCase): response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') data = json.loads(response.content) self.assertFalse(data['form_valid']) - self.assertIn('must match for Part and Order', str(data['html_form'])) # POST with an invalid part post_data['part'] = 12345 response = self.client.post(url, post_data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') data = json.loads(response.content) self.assertFalse(data['form_valid']) - self.assertIn('Invalid SupplierPart selection', str(data['html_form'])) # POST the form with valid data post_data['part'] = 100 diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index d2d02bb2c9..a65cc300fa 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -91,7 +91,7 @@ class SalesOrderDetail(InvenTreeRoleMixin, DetailView): class PurchaseOrderAttachmentCreate(AjaxCreateView): """ - View for creating a new PurchaseOrderAtt + View for creating a new PurchaseOrderAttachment """ model = PurchaseOrderAttachment @@ -100,9 +100,11 @@ class PurchaseOrderAttachmentCreate(AjaxCreateView): ajax_template_name = "modal_form.html" role_required = 'purchase_order.add' - def post_save(self, **kwargs): - self.object.user = self.request.user - self.object.save() + def save(self, form, **kwargs): + + attachment = form.save(commit=False) + attachment.user = self.request.user + attachment.save() def get_data(self): return { @@ -148,9 +150,14 @@ class SalesOrderAttachmentCreate(AjaxCreateView): ajax_form_title = _('Add Sales Order Attachment') role_required = 'sales_order.add' - def post_save(self, **kwargs): - self.object.user = self.request.user - self.object.save() + def save(self, form, **kwargs): + """ + Save the user that uploaded the attachment + """ + + attachment = form.save(commit=False) + attachment.user = self.request.user + attachment.save() def get_data(self): return { @@ -295,7 +302,9 @@ class SalesOrderNotes(InvenTreeRoleMixin, UpdateView): class PurchaseOrderCreate(AjaxCreateView): - """ View for creating a new PurchaseOrder object using a modal form """ + """ + View for creating a new PurchaseOrder object using a modal form + """ model = PurchaseOrder ajax_form_title = _("Create Purchase Order") @@ -319,11 +328,14 @@ class PurchaseOrderCreate(AjaxCreateView): return initials - def post_save(self, **kwargs): - # Record the user who created this purchase order + def save(self, form, **kwargs): + """ + Record the user who created this PurchaseOrder + """ - self.object.created_by = self.request.user - self.object.save() + order = form.save(commit=False) + order.created_by = self.request.user + order.save() class SalesOrderCreate(AjaxCreateView): @@ -351,10 +363,14 @@ class SalesOrderCreate(AjaxCreateView): return initials - def post_save(self, **kwargs): - # Record the user who created this sales order - self.object.created_by = self.request.user - self.object.save() + def save(self, form, **kwargs): + """ + Record the user who created this SalesOrder + """ + + order = form.save(commit=False) + order.created_by = self.request.user + order.save() class PurchaseOrderEdit(AjaxUpdateView): @@ -404,29 +420,22 @@ class PurchaseOrderCancel(AjaxUpdateView): form_class = order_forms.CancelPurchaseOrderForm role_required = 'purchase_order.change' - def post(self, request, *args, **kwargs): - """ Mark the PO as 'CANCELLED' """ - - order = self.get_object() - form = self.get_form() - - confirm = str2bool(request.POST.get('confirm', False)) - - valid = False + def validate(self, order, form, **kwargs): + + confirm = str2bool(form.cleaned_data.get('confirm', False)) if not confirm: - form.errors['confirm'] = [_('Confirm order cancellation')] - else: - valid = True + form.add_error('confirm', _('Confirm order cancellation')) - data = { - 'form_valid': valid - } + if not order.can_cancel(): + form.add_error(None, _('Order cannot be cancelled')) - if valid: - order.cancel_order() + def save(self, order, form, **kwargs): + """ + Cancel the PurchaseOrder + """ - return self.renderJsonResponse(request, form, data) + order.cancel_order() class SalesOrderCancel(AjaxUpdateView): @@ -438,30 +447,22 @@ class SalesOrderCancel(AjaxUpdateView): form_class = order_forms.CancelSalesOrderForm role_required = 'sales_order.change' - def post(self, request, *args, **kwargs): + def validate(self, order, form, **kwargs): - order = self.get_object() - form = self.get_form() - - confirm = str2bool(request.POST.get('confirm', False)) - - valid = False + confirm = str2bool(form.cleaned_data.get('confirm', False)) if not confirm: - form.errors['confirm'] = [_('Confirm order cancellation')] - else: - valid = True + form.add_error('confirm', _('Confirm order cancellation')) - if valid: - if not order.cancel_order(): - form.non_field_errors = [_('Could not cancel order')] - valid = False + if not order.can_cancel(): + form.add_error(None, _('Order cannot be cancelled')) - data = { - 'form_valid': valid, - } + def save(self, order, form, **kwargs): + """ + Once the form has been validated, cancel the SalesOrder + """ - return self.renderJsonResponse(request, form, data) + order.cancel_order() class PurchaseOrderIssue(AjaxUpdateView): @@ -473,30 +474,24 @@ class PurchaseOrderIssue(AjaxUpdateView): form_class = order_forms.IssuePurchaseOrderForm role_required = 'purchase_order.change' - def post(self, request, *args, **kwargs): - """ Mark the purchase order as 'PLACED' """ + def validate(self, order, form, **kwargs): - order = self.get_object() - form = self.get_form() - - confirm = str2bool(request.POST.get('confirm', False)) - - valid = False + confirm = str2bool(self.request.POST.get('confirm', False)) if not confirm: - form.errors['confirm'] = [_('Confirm order placement')] - else: - valid = True + form.add_error('confirm', _('Confirm order placement')) - data = { - 'form_valid': valid, + def save(self, order, form, **kwargs): + """ + Once the form has been validated, place the order. + """ + order.place_order() + + def get_data(self): + return { + 'success': _('Purchase order issued') } - if valid: - order.place_order() - - return self.renderJsonResponse(request, form, data) - class PurchaseOrderComplete(AjaxUpdateView): """ View for marking a PurchaseOrder as complete. @@ -517,23 +512,25 @@ class PurchaseOrderComplete(AjaxUpdateView): return ctx - def post(self, request, *args, **kwargs): + def validate(self, order, form, **kwargs): - confirm = str2bool(request.POST.get('confirm', False)) + confirm = str2bool(form.cleaned_data.get('confirm', False)) - if confirm: - po = self.get_object() - po.status = PurchaseOrderStatus.COMPLETE - po.save() + if not confirm: + form.add_error('confirm', _('Confirm order completion')) - data = { - 'form_valid': confirm + def save(self, order, form, **kwargs): + """ + Complete the PurchaseOrder + """ + + order.complete_order() + + def get_data(self): + return { + 'success': _('Purchase order completed') } - form = self.get_form() - - return self.renderJsonResponse(request, form, data) - class SalesOrderShip(AjaxUpdateView): """ View for 'shipping' a SalesOrder """ @@ -558,13 +555,13 @@ class SalesOrderShip(AjaxUpdateView): valid = False if not confirm: - form.errors['confirm'] = [_('Confirm order shipment')] + form.add_error('confirm', _('Confirm order shipment')) else: valid = True if valid: if not order.ship_order(request.user): - form.non_field_errors = [_('Could not ship order')] + form.add_error(None, _('Could not ship order')) valid = False data = { @@ -913,9 +910,10 @@ class OrderParts(AjaxView): try: build = Build.objects.get(id=build_id) - parts = build.part.required_parts() + parts = build.required_parts for part in parts: + # If ordering from a Build page, ignore parts that we have enough of if part.quantity_to_order <= 0: continue @@ -1117,52 +1115,21 @@ class POLineItemCreate(AjaxCreateView): ajax_form_title = _('Add Line Item') role_required = 'purchase_order.add' - def post(self, request, *arg, **kwargs): + def validate(self, item, form, **kwargs): - self.request = request + order = form.cleaned_data.get('order', None) - form = self.get_form() + part = form.cleaned_data.get('part', None) - valid = form.is_valid() + if not part: + form.add_error('part', _('Supplier part must be specified')) - # Extract the SupplierPart ID from the form - part_id = form['part'].value() - - # Extract the Order ID from the form - order_id = form['order'].value() - - try: - order = PurchaseOrder.objects.get(id=order_id) - except (ValueError, PurchaseOrder.DoesNotExist): - order = None - form.errors['order'] = [_('Invalid Purchase Order')] - valid = False - - try: - sp = SupplierPart.objects.get(id=part_id) - - if order is not None: - if not sp.supplier == order.supplier: - form.errors['part'] = [_('Supplier must match for Part and Order')] - valid = False - - except (SupplierPart.DoesNotExist, ValueError): - valid = False - form.errors['part'] = [_('Invalid SupplierPart selection')] - - data = { - 'form_valid': valid, - } - - if valid: - self.object = form.save() - - data['pk'] = self.object.pk - data['text'] = str(self.object) - else: - self.object = None - - return self.renderJsonResponse(request, form, data,) + if part and order: + if not part.supplier == order.supplier: + form.add_error( + 'part', + _('Supplier must match for Part and Order') + ) def get_form(self): """ Limit choice options based on the selected order, etc diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index f0c9e3f233..7476197547 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -9,7 +9,7 @@ from import_export.fields import Field import import_export.widgets as widgets from .models import PartCategory, Part -from .models import PartAttachment, PartStar +from .models import PartAttachment, PartStar, PartRelated from .models import BomItem from .models import PartParameterTemplate, PartParameter from .models import PartTestTemplate @@ -121,6 +121,11 @@ class PartCategoryAdmin(ImportExportModelAdmin): search_fields = ('name', 'description') +class PartRelatedAdmin(admin.ModelAdmin): + ''' Class to manage PartRelated objects ''' + pass + + class PartAttachmentAdmin(admin.ModelAdmin): list_display = ('part', 'attachment', 'comment') @@ -279,6 +284,7 @@ class PartSellPriceBreakAdmin(admin.ModelAdmin): admin.site.register(Part, PartAdmin) admin.site.register(PartCategory, PartCategoryAdmin) +admin.site.register(PartRelated, PartRelatedAdmin) admin.site.register(PartAttachment, PartAttachmentAdmin) admin.site.register(PartStar, PartStarAdmin) admin.site.register(BomItem, BomItemAdmin) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index d643b8671a..496153a6d0 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -461,8 +461,8 @@ class PartList(generics.ListCreateAPIView): else: queryset = queryset.exclude(pk__in=starred_parts) - # Cascade? - cascade = str2bool(params.get('cascade', None)) + # Cascade? (Default = True) + cascade = str2bool(params.get('cascade', True)) # Does the user wish to filter by category? cat_id = params.get('category', None) @@ -761,12 +761,44 @@ class BomList(generics.ListCreateAPIView): if sub_part is not None: queryset = queryset.filter(sub_part=sub_part) - # Filter by "trackable" status of the sub-part - trackable = params.get('trackable', None) + # Filter by "active" status of the part + part_active = params.get('part_active', None) - if trackable is not None: - trackable = str2bool(trackable) - queryset = queryset.filter(sub_part__trackable=trackable) + if part_active is not None: + part_active = str2bool(part_active) + queryset = queryset.filter(part__active=part_active) + + # Filter by "trackable" status of the part + part_trackable = params.get('part_trackable', None) + + if part_trackable is not None: + part_trackable = str2bool(part_trackable) + queryset = queryset.filter(part__trackable=part_trackable) + + # Filter by "trackable" status of the sub-part + sub_part_trackable = params.get('sub_part_trackable', None) + + if sub_part_trackable is not None: + sub_part_trackable = str2bool(sub_part_trackable) + queryset = queryset.filter(sub_part__trackable=sub_part_trackable) + + # Filter by whether the BOM line has been validated + validated = params.get('validated', None) + + if validated is not None: + validated = str2bool(validated) + + # Work out which lines have actually been validated + pks = [] + + for bom_item in queryset.all(): + if bom_item.is_line_valid: + pks.append(bom_item.pk) + + if validated: + queryset = queryset.filter(pk__in=pks) + else: + queryset = queryset.exclude(pk__in=pks) return queryset diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py index 198e58e337..2f72537136 100644 --- a/InvenTree/part/apps.py +++ b/InvenTree/part/apps.py @@ -16,8 +16,15 @@ class PartConfig(AppConfig): """ self.generate_part_thumbnails() + self.update_trackable_status() def generate_part_thumbnails(self): + """ + Generate thumbnail images for any Part that does not have one. + This function exists mainly for legacy support, + as any *new* image uploaded will have a thumbnail generated automatically. + """ + from .models import Part print("InvenTree: Checking Part image thumbnails") @@ -37,4 +44,27 @@ class PartConfig(AppConfig): part.image = None part.save() except (OperationalError, ProgrammingError): + # Exception if the database has not been migrated yet + pass + + def update_trackable_status(self): + """ + Check for any instances where a trackable part is used in the BOM + for a non-trackable part. + + In such a case, force the top-level part to be trackable too. + """ + + from .models import BomItem + + try: + items = BomItem.objects.filter(part__trackable=False, sub_part__trackable=True) + + for item in items: + print(f"Marking part '{item.part.name}' as trackable") + item.part.trackable = True + item.part.clean() + item.part.save() + except (OperationalError, ProgrammingError): + # Exception if the database has not been migrated yet pass diff --git a/InvenTree/part/fixtures/part.yaml b/InvenTree/part/fixtures/part.yaml index c77fd5dc57..9883edfcd3 100644 --- a/InvenTree/part/fixtures/part.yaml +++ b/InvenTree/part/fixtures/part.yaml @@ -67,6 +67,7 @@ name: 'Widget' description: 'A watchamacallit' category: 7 + assembly: true trackable: true tree_id: 0 level: 0 diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index c64bbb8362..c4523113e5 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -13,16 +13,21 @@ from mptt.fields import TreeNodeChoiceField from django import forms from django.utils.translation import ugettext as _ -from .models import Part, PartCategory, PartAttachment +from .models import Part, PartCategory, PartAttachment, PartRelated from .models import BomItem from .models import PartParameterTemplate, PartParameter 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 """ + def label_from_instance(self, part): + return f'{part} - {part.available_stock}' + + class PartImageForm(HelperForm): """ Form for uploading a Part image """ @@ -77,6 +82,38 @@ class BomExportForm(forms.Form): self.fields['file_format'].choices = self.get_choices() +class BomDuplicateForm(HelperForm): + """ + Simple confirmation form for BOM duplication. + + Select which parent to select from. + """ + + parent = PartModelChoiceField( + label=_('Parent Part'), + help_text=_('Select parent part to copy BOM from'), + queryset=Part.objects.filter(is_template=True), + ) + + clear = forms.BooleanField( + required=False, initial=True, + help_text=_('Clear existing BOM items') + ) + + confirm = forms.BooleanField( + required=False, initial=False, + help_text=_('Confirm BOM duplication') + ) + + class Meta: + model = Part + fields = [ + 'parent', + 'clear', + 'confirm', + ] + + class BomValidateForm(HelperForm): """ Simple confirmation form for BOM validation. User is presented with a single checkbox input, @@ -104,6 +141,25 @@ class BomUploadSelectFile(HelperForm): ] +class CreatePartRelatedForm(HelperForm): + """ Form for creating a PartRelated object """ + + class Meta: + model = PartRelated + fields = [ + 'part_1', + 'part_2', + ] + labels = { + 'part_2': _('Related Part'), + } + + def save(self): + """ Disable model saving """ + + return super(CreatePartRelatedForm, self).save(commit=False) + + class EditPartAttachmentForm(HelperForm): """ Form for editing a PartAttachment object """ @@ -210,12 +266,6 @@ class EditCategoryForm(HelperForm): ] -class PartModelChoiceField(forms.ModelChoiceField): - """ Extending string representation of Part instance with available stock """ - def label_from_instance(self, part): - return f'{part} - {part.available_stock}' - - class EditBomItemForm(HelperForm): """ Form for editing a BomItem object """ diff --git a/InvenTree/part/migrations/0052_auto_20201027_1557.py b/InvenTree/part/migrations/0052_auto_20201027_1557.py new file mode 100644 index 0000000000..94dbcac06e --- /dev/null +++ b/InvenTree/part/migrations/0052_auto_20201027_1557.py @@ -0,0 +1,19 @@ +# 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 new file mode 100644 index 0000000000..a8672ba7dc --- /dev/null +++ b/InvenTree/part/migrations/0052_partrelated.py @@ -0,0 +1,22 @@ +# Generated by Django 3.0.7 on 2020-10-16 20:42 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0051_bomitem_optional'), + ] + + operations = [ + migrations.CreateModel( + name='PartRelated', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('part_1', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='related_parts_1', to='part.Part')), + ('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')), + ], + ), + ] diff --git a/InvenTree/part/migrations/0053_merge_20201103_1028.py b/InvenTree/part/migrations/0053_merge_20201103_1028.py new file mode 100644 index 0000000000..d42595675a --- /dev/null +++ b/InvenTree/part/migrations/0053_merge_20201103_1028.py @@ -0,0 +1,14 @@ +# 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/models.py b/InvenTree/part/models.py index d427409c71..60e100b563 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -245,7 +245,7 @@ def match_part_names(match, threshold=80, reverse=True, compare_length=False): if ratio >= threshold: matches.append({ 'part': part, - 'ratio': ratio + 'ratio': round(ratio, 1) }) matches = sorted(matches, key=lambda item: item['ratio'], reverse=reverse) @@ -360,7 +360,7 @@ class Part(MPTTModel): # And recursively check too item.sub_part.checkAddToBOM(parent) - def checkIfSerialNumberExists(self, sn): + def checkIfSerialNumberExists(self, sn, exclude_self=False): """ Check if a serial number exists for this Part. @@ -369,10 +369,27 @@ class Part(MPTTModel): """ parts = Part.objects.filter(tree_id=self.tree_id) + stock = StockModels.StockItem.objects.filter(part__in=parts, serial=sn) + if exclude_self: + stock = stock.exclude(pk=self.pk) + return stock.exists() + def find_conflicting_serial_numbers(self, serials): + """ + For a provided list of serials, return a list of those which are conflicting. + """ + + conflicts = [] + + for serial in serials: + if self.checkIfSerialNumberExists(serial, exclude_self=True): + conflicts.append(serial) + + return conflicts + def getLatestSerialNumber(self): """ Return the "latest" serial number for this Part. @@ -529,10 +546,24 @@ class Part(MPTTModel): pass def clean(self): - """ Perform cleaning operations for the Part model """ + """ + Perform cleaning operations for the Part model + + Update trackable status: + If this part is trackable, and it is used in the BOM + for a parent part which is *not* trackable, + then we will force the parent part to be trackable. + """ super().clean() + if self.trackable: + for parent_part in self.used_in.all(): + if not parent_part.trackable: + parent_part.trackable = True + parent_part.clean() + parent_part.save() + name = models.CharField(max_length=100, blank=False, help_text=_('Part name'), validators=[validators.validate_part_name] @@ -562,7 +593,7 @@ class Part(MPTTModel): revision = models.CharField(max_length=100, blank=True, null=True, help_text=_('Part revision or version number')) - link = InvenTreeURLField(blank=True, null=True, help_text=_('Link to extenal URL')) + link = InvenTreeURLField(blank=True, null=True, help_text=_('Link to external URL')) image = StdImageField( upload_to=rename_part_image, @@ -874,6 +905,19 @@ class Part(MPTTModel): def has_bom(self): return self.bom_count > 0 + @property + def has_trackable_parts(self): + """ + Return True if any parts linked in the Bill of Materials are trackable. + This is important when building the part. + """ + + for bom_item in self.bom_items.all(): + if bom_item.sub_part.trackable: + return True + + return False + @property def bom_count(self): """ Return the number of items contained in the BOM for this part """ @@ -931,15 +975,31 @@ class Part(MPTTModel): self.bom_items.all().delete() - def required_parts(self): - """ Return a list of parts required to make this part (list of BOM items) """ - parts = [] - for bom in self.bom_items.all().select_related('sub_part'): - parts.append(bom.sub_part) + def getRequiredParts(self, recursive=False, parts=set()): + """ + Return a list of parts required to make this part (i.e. BOM items). + + Args: + recursive: If True iterate down through sub-assemblies + parts: Set of parts already found (to prevent recursion issues) + """ + + for bom_item in self.bom_items.all().select_related('sub_part'): + + sub_part = bom_item.sub_part + + if sub_part not in parts: + + parts.add(sub_part) + + if recursive: + sub_part.getRequiredParts(recursive=True, parts=parts) + return parts def get_allowed_bom_items(self): - """ Return a list of parts which can be added to a BOM for this part. + """ + Return a list of parts which can be added to a BOM for this part. - Exclude parts which are not 'component' parts - Exclude parts which this part is in the BOM for @@ -1087,7 +1147,61 @@ class Part(MPTTModel): max(buy_price_range[1], bom_price_range[1]) ) - def deepCopy(self, other, **kwargs): + @transaction.atomic + def copy_bom_from(self, other, clear=True, **kwargs): + """ + Copy the BOM from another part. + + args: + other - The part to copy the BOM from + clear - Remove existing BOM items first (default=True) + """ + + if clear: + # Remove existing BOM items + self.bom_items.all().delete() + + for bom_item in other.bom_items.all(): + # If this part already has a BomItem pointing to the same sub-part, + # delete that BomItem from this part first! + + try: + existing = BomItem.objects.get(part=self, sub_part=bom_item.sub_part) + existing.delete() + except (BomItem.DoesNotExist): + pass + + bom_item.part = self + bom_item.pk = None + + bom_item.save() + + @transaction.atomic + def copy_parameters_from(self, other, **kwargs): + + clear = kwargs.get('clear', True) + + if clear: + self.get_parameters().delete() + + for parameter in other.get_parameters(): + + # If this part already has a parameter pointing to the same template, + # delete that parameter from this part first! + + try: + existing = PartParameter.objects.get(part=self, template=parameter.template) + existing.delete() + except (PartParameter.DoesNotExist): + pass + + parameter.part = self + parameter.pk = None + + parameter.save() + + @transaction.atomic + def deep_copy(self, other, **kwargs): """ Duplicates non-field data from another part. Does not alter the normal fields of this part, but can be used to copy other data linked by ForeignKey refernce. @@ -1106,24 +1220,12 @@ class Part(MPTTModel): # Copy the BOM data if kwargs.get('bom', False): - for item in other.bom_items.all(): - # Point the item to THIS part. - # Set the pk to None so a new entry is created. - item.part = self - item.pk = None - item.save() + self.copy_bom_from(other) # Copy the parameters data if kwargs.get('parameters', True): - # Get template part parameters - parameters = other.get_parameters() - # Copy template part parameters to new variant part - for parameter in parameters: - PartParameter.create(part=self, - template=parameter.template, - data=parameter.data, - save=True) - + self.copy_parameters_from(other) + # Copy the fields that aren't available in the duplicate form self.salable = other.salable self.assembly = other.assembly @@ -1254,6 +1356,32 @@ class Part(MPTTModel): return self.get_descendants(include_self=False) + def get_related_parts(self): + """ Return list of tuples for all related parts: + - first value is PartRelated object + - second value is matching Part object + """ + + related_parts = [] + + related_parts_1 = self.related_parts_1.filter(part_1__id=self.pk) + + related_parts_2 = self.related_parts_2.filter(part_2__id=self.pk) + + for related_part in related_parts_1: + # Add to related parts list + related_parts.append((related_part, related_part.part_2)) + + for related_part in related_parts_2: + # Add to related parts list + related_parts.append((related_part, related_part.part_1)) + + return related_parts + + @property + def related_count(self): + return len(self.get_related_parts()) + def attach_file(instance, filename): """ Function for storing a file for a PartAttachment @@ -1596,12 +1724,15 @@ class BomItem(models.Model): return self.get_item_hash() == self.checksum def clean(self): - """ Check validity of the BomItem model. + """ + Check validity of the BomItem model. Performs model checks beyond simple field validation. - A part cannot refer to itself in its BOM - A part cannot refer to a part which refers to it + + - If the "sub_part" is trackable, then the "part" must be trackable too! """ # If the sub_part is 'trackable' then the 'quantity' field must be an integer @@ -1611,6 +1742,13 @@ class BomItem(models.Model): raise ValidationError({ "quantity": _("Quantity must be integer value for trackable parts") }) + + # Force the upstream part to be trackable if the sub_part is trackable + if not self.part.trackable: + self.part.trackable = True + self.part.clean() + self.part.save() + except Part.DoesNotExist: pass @@ -1723,3 +1861,71 @@ class BomItem(models.Model): pmax = decimal2string(pmax) return "{pmin} to {pmax}".format(pmin=pmin, pmax=pmax) + + +class PartRelated(models.Model): + """ Store and handle related parts (eg. mating connector, crimps, etc.) """ + + part_1 = models.ForeignKey(Part, related_name='related_parts_1', + on_delete=models.DO_NOTHING) + + part_2 = models.ForeignKey(Part, related_name='related_parts_2', + on_delete=models.DO_NOTHING, + help_text=_('Select Related Part')) + + def __str__(self): + return f'{self.part_1} <--> {self.part_2}' + + def validate(self, part_1, part_2): + ''' Validate that the two parts relationship is unique ''' + + validate = True + + parts = Part.objects.all() + related_parts = PartRelated.objects.all() + + # Check if part exist and there are not the same part + if (part_1 in parts and part_2 in parts) and (part_1.pk != part_2.pk): + # Check if relation exists already + for relation in related_parts: + if (part_1 == relation.part_1 and part_2 == relation.part_2) \ + or (part_1 == relation.part_2 and part_2 == relation.part_1): + validate = False + break + else: + validate = False + + return validate + + def clean(self): + ''' Overwrite clean method to check that relation is unique ''' + + validate = self.validate(self.part_1, self.part_2) + + if not validate: + error_message = _('Error creating relationship: check that ' + 'the part is not related to itself ' + 'and that the relationship is unique') + + raise ValidationError(error_message) + + def create_relationship(self, part_1, part_2): + ''' Create relationship between two parts ''' + + validate = self.validate(part_1, part_2) + + if validate: + # Add relationship + self.part_1 = part_1 + self.part_2 = part_2 + self.save() + + return validate + + @classmethod + def create(cls, part_1, part_2): + ''' Create PartRelated object and relationship between two parts ''' + + related_part = cls() + related_part.create_relationship(part_1, part_2) + return related_part diff --git a/InvenTree/part/templates/part/allocation.html b/InvenTree/part/templates/part/allocation.html index 7892fe30e5..e574742ad5 100644 --- a/InvenTree/part/templates/part/allocation.html +++ b/InvenTree/part/templates/part/allocation.html @@ -17,15 +17,15 @@ </tr> {% for allocation in part.build_order_allocations %} <tr> - <td><a href="{% url 'build-detail' allocation.build.id %}">{% trans "Build Order" %}: {{ allocation.build.pk }}</a></td> - <td><a href="{% url 'stock-item-detail' allocation.stock_item.id %}">{% trans "Stock Item" %}: {{ allocation.stock_item.id }}</a></td> + <td><a href="{% url 'build-detail' allocation.build.id %}">{% trans "Build Order" %}: {{ allocation.build }}</a></td> + <td><a href="{% url 'stock-item-detail' allocation.stock_item.id %}">{% trans "Stock Item" %}: {{ allocation.stock_item }}</a></td> <td>{% decimal allocation.quantity %}</td> </tr> {% endfor %} {% for allocation in part.sales_order_allocations %} <tr> - <td><a href="{% url 'so-detail' allocation.line.order.id %}">{% trans "Sales Order" %}: {{ allocation.line.order.pk }}</a></td> - <td><a href="{% url 'stock-item-detail' allocation.item.id %}">{% trans "Stock Item" %}: {{ allocation.item.id }}</a></td> + <td><a href="{% url 'so-detail' allocation.line.order.id %}">{% trans "Sales Order" %}: {{ allocation.line.order }}</a></td> + <td><a href="{% url 'stock-item-detail' allocation.item.id %}">{% trans "Stock Item" %}: {{ allocation.item }}</a></td> <td>{% decimal allocation.quantity %}</td> </tr> {% endfor %} diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index c996edc2db..38d07cb00e 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -10,13 +10,9 @@ {% include 'part/tabs.html' with tab='bom' %} -<h3>{% trans "Bill of Materials" %}</h3> +<h4>{% trans "Bill of Materials" %}</h4> +<hr> -{% if part.has_complete_bom_pricing == False %} -<div class='alert alert-block alert-warning'> - The BOM for <i>{{ part.full_name }}</i> does not have complete pricing information -</div> -{% endif %} {% if part.bom_checked_date %} {% if part.is_bom_valid %} <div class='alert alert-block alert-info'> @@ -32,21 +28,45 @@ </div> {% endif %} -<div id='button-toolbar' class="btn-group" role="group" aria-label="..."> - {% if editing_enabled %} - <button class='btn btn-default action-button' type='button' title='{% trans "Remove selected BOM items" %}' id='bom-item-delete'><span class='fas fa-trash-alt'></span></button> - <button class='btn btn-default action-button' type='button' title='{% trans "Import BOM data" %}' id='bom-upload'><span class='fas fa-file-upload'></span></button> - <button class='btn btn-default action-button' type='button' title='{% trans "New BOM Item" %}' id='bom-item-new'><span class='fas fa-plus-circle'></span></button> - <button class='btn btn-default action-button' type='button' title='{% trans "Finish Editing" %}' id='editing-finished'><span class='fas fa-check-circle'></span></button> - {% elif part.active %} - {% if roles.part.change %} - <button class='btn btn-default action-button' type='button' title='{% trans "Edit BOM" %}' id='edit-bom'><span class='fas fa-edit'></span></button> - {% if part.is_bom_valid == False %} - <button class='btn btn-default action-button' id='validate-bom' title='{% trans "Validate Bill of Materials" %}' type='button'><span class='fas fa-clipboard-check'></span></button> - {% endif %} - {% endif %} - <button title='{% trans "Export Bill of Materials" %}' class='btn btn-default action-button' id='download-bom' type='button'><span class='fas fa-file-download'></span></button> - {% endif %} +<div id='button-toolbar'> + <div class="btn-group" role="group" aria-label="..."> + {% if editing_enabled %} + <button class='btn btn-default' type='button' title='{% trans "Remove selected BOM items" %}' id='bom-item-delete'> + <span class='fas fa-trash-alt'></span> + </button> + <button class='btn btn-primary' type='button' title='{% trans "Import BOM data" %}' id='bom-upload'> + <span class='fas fa-file-upload'></span> {% trans "Import from File" %} + </button> + {% if part.variant_of %} + <button class='btn btn-default' type='button' title='{% trans "Copy BOM from parent part" %}' id='bom-duplicate'> + <span class='fas fa-clone'></span> {% trans "Copy from Parent" %} + </button> + {% endif %} + <button class='btn btn-default' type='button' title='{% trans "New BOM Item" %}' id='bom-item-new'> + <span class='fas fa-plus-circle'></span> {% trans "Add Item" %} + </button> + <button class='btn btn-success' type='button' title='{% trans "Finish Editing" %}' id='editing-finished'> + <span class='fas fa-check-circle'></span> {% trans "Finished" %} + </button> + {% elif part.active %} + {% if roles.part.change %} + <button class='btn btn-primary' type='button' title='{% trans "Edit BOM" %}' id='edit-bom'> + <span class='fas fa-edit'></span> {% trans "Edit" %} + </button> + {% if part.is_bom_valid == False %} + <button class='btn btn-success' id='validate-bom' title='{% trans "Validate Bill of Materials" %}' type='button'> + <span class='fas fa-clipboard-check'></span> {% trans "Validate" %} + </button> + {% endif %} + {% endif %} + <button title='{% trans "Export Bill of Materials" %}' class='btn btn-default' id='download-bom' type='button'> + <span class='fas fa-file-download'></span> {% trans "Export" %} + </button> + {% endif %} + </div> + <div class='filter-list' id='filter-list-bom'> + <!-- Empty div (will be filled out with avilable BOM filters) --> + </div> </div> <table class='table table-striped table-condensed' data-toolbar="#button-toolbar" id='bom-table'> @@ -138,6 +158,17 @@ location.href = "{% url 'upload-bom' part.id %}"; }); + $('#bom-duplicate').click(function() { + launchModalForm( + "{% url 'duplicate-bom' part.id %}", + { + success: function() { + $('#bom-table').bootstrapTable('refresh'); + } + } + ); + }); + $("#bom-item-new").click(function () { launchModalForm( "{% url 'bom-item-create' %}?parent={{ part.id }}", @@ -184,4 +215,4 @@ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/InvenTree/part/templates/part/bom_duplicate.html b/InvenTree/part/templates/part/bom_duplicate.html new file mode 100644 index 0000000000..7fd45afcbf --- /dev/null +++ b/InvenTree/part/templates/part/bom_duplicate.html @@ -0,0 +1,17 @@ +{% extends "modal_form.html" %} +{% load i18n %} + +{% block pre_form_content %} + +<p> + {% trans "Select parent part to copy BOM from" %} +</p> + +{% if part.has_bom %} +<div class='alert alert-block alert-danger'> + <b>{% trans "Warning" %}</b><br> + {% trans "This part already has a Bill of Materials" %}<br> +</div> +{% endif %} + +{% endblock %} diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html index bfd72a2f70..9641181af8 100644 --- a/InvenTree/part/templates/part/build.html +++ b/InvenTree/part/templates/part/build.html @@ -5,13 +5,14 @@ {% include 'part/tabs.html' with tab='build' %} -<h3>{% trans "Part Builds" %}</h3> +<h4>{% trans "Part Builds" %}</h4> +<hr> <div id='button-toolbar'> <div class='button-toolbar container-flui' style='float: right';> {% if part.active %} {% if roles.build.add %} - <button class="btn btn-success" id='start-build'>{% trans "Start New Build" %}</button> + <button class="btn btn-success" id='start-build'><span class='fas fa-tools'></span> {% trans "Start New Build" %}</button> {% endif %} {% endif %} <div class='filter-list' id='filter-list-build'> @@ -29,14 +30,11 @@ {% block js_ready %} {{ block.super }} $("#start-build").click(function() { - launchModalForm( - "{% url 'build-create' %}", - { - follow: true, - data: { - part: {{ part.id }} - } - }); + newBuildOrder({ + data: { + part: {{ part.id }}, + } + }); }); loadBuildTable($("#build-table"), { diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index d73aba0291..6992d9bac4 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -107,22 +107,24 @@ <hr> <div id='button-toolbar'> - <div class='button-toolbar container-fluid' style="float: right;"> + <div class='btn-group'> + <button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'> + <span class='fas fa-file-download'></span> {% trans "Export" %} + </button> + {% if roles.part.add %} + <button class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'> + <span class='fas fa-plus-circle'></span> {% trans "New Part" %} + </button> + {% endif %} <div class='btn-group'> - <button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>{% trans "Export" %}</button> - {% if roles.part.add %} - <button class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>{% trans "New Part" %}</button> - {% endif %} - <div class='btn-group'> - <button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown">{% trans "Options" %}<span class='caret'></span></button> - <ul class='dropdown-menu'> - {% if roles.part.change %} - <li><a href='#' id='multi-part-category' title='{% trans "Set category" %}'>{% trans "Set Category" %}</a></li> - {% endif %} - <li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li> - <li><a href='#' id='multi-part-export' title='{% trans "Export" %}'>{% trans "Export Data" %}</a></li> - </ul> - </div> + <button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown">{% trans "Options" %}<span class='caret'></span></button> + <ul class='dropdown-menu'> + {% if roles.part.change %} + <li><a href='#' id='multi-part-category' title='{% trans "Set category" %}'>{% trans "Set Category" %}</a></li> + {% endif %} + <li><a href='#' id='multi-part-order' title='{% trans "Order parts" %}'>{% trans "Order Parts" %}</a></li> + <li><a href='#' id='multi-part-export' title='{% trans "Export" %}'>{% trans "Export Data" %}</a></li> + </ul> </div> </div> <div class='filter-list' id='filter-list-parts'> diff --git a/InvenTree/part/templates/part/create_part.html b/InvenTree/part/templates/part/create_part.html index 4f1fcfb03a..d0e322d887 100644 --- a/InvenTree/part/templates/part/create_part.html +++ b/InvenTree/part/templates/part/create_part.html @@ -1,19 +1,23 @@ {% extends "modal_form.html" %} +{% load inventree_extras %} +{% load i18n %} {% block pre_form_content %} {{ block.super }} {% if matches %} -<b>Possible Matching Parts</b> -<p>The new part may be a duplicate of these existing parts:</p> -<ul class='list-group'> -{% for match in matches %} -<li class='list-group-item list-group-item-condensed'> - {{ match.part.full_name }} - <i>{{ match.part.description }}</i> ({{ match.ratio }}%) -</li> -{% endfor %} +<div class='alert alert-block alert-warning'> + <b>{% trans "Possible Matching Parts" %}</b> + <p>{% trans "The new part may be a duplicate of these existing parts" %}:</p> + <ul class='list-group'> + {% for match in matches %} + <li class='list-group-item list-group-item-condensed'> + {{ match.part.full_name }} - <i>{{ match.part.description }}</i> ({% decimal match.ratio %}% {% trans "match" %}) + </li> + {% endfor %} </ul> +</div> {% endif %} {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 2714347414..a3c11e5669 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -15,7 +15,7 @@ <table class='table table-striped'> <col width='25'> <tr> - <td></td> + <td><span class='fas fa-font'></span></td> <td><b>{% trans "Part name" %}</b></td> <td>{{ part.name }}</td> </tr> @@ -28,7 +28,7 @@ {% endif %} {% if part.revision %} <tr> - <td></td> + <td><span class='fas fa-code-branch'></span></td> <td><b>{% trans "Revision" %}</b></td> <td>{{ part.revision }}</td> </tr> @@ -41,7 +41,7 @@ {% if part.getLatestSerialNumber %} {{ part.getLatestSerialNumber }} {% else %} - {% trans "No serial numbers recorded" %} + <i>{% trans "No serial numbers recorded" %}</i> {% endif %} </td> </tr> @@ -132,7 +132,9 @@ </div> <div class='col-sm-6'> <table class='table table-striped'> + <col width='25'> <tr> + <td><span class='fas fa-ghost'%></span></td> <td><b>{% trans "Virtual" %}</b></td> <td>{% include "slide.html" with state=part.virtual field='virtual' %}</td> {% if part.virtual %} @@ -142,6 +144,7 @@ {% endif %} </tr> <tr> + <td><span class='fas fa-clone'></span></td> <td><b>{% trans "Template" %}</b></td> <td>{% include "slide.html" with state=part.is_template field='is_template' %}</td> {% if part.is_template %} @@ -151,6 +154,7 @@ {% endif %} </tr> <tr> + <td><span class='fas fa-tools'></span></td> <td><b>{% trans "Assembly" %}</b></td> <td>{% include "slide.html" with state=part.assembly field='assembly' %}</td> {% if part.assembly %} @@ -160,6 +164,7 @@ {% endif %} </tr> <tr> + <td><span class='fas fa-th'></span></td> <td><b>{% trans "Component" %}</b></td> <td>{% include "slide.html" with state=part.component field='component' %}</td> {% if part.component %} @@ -169,6 +174,7 @@ {% endif %} </tr> <tr> + <td><span class='fas fa-directions'></span></td> <td><b>{% trans "Trackable" %}</b></td> <td>{% include "slide.html" with state=part.trackable field='trackable' %}</td> {% if part.trackable %} @@ -178,6 +184,7 @@ {% endif %} </tr> <tr> + <td><span class='fas fa-shopping-cart'></span></td> <td><b>{% trans "Purchaseable" %}</b></td> <td>{% include "slide.html" with state=part.purchaseable field='purchaseable' %}</td> {% if part.purchaseable %} @@ -187,6 +194,7 @@ {% endif %} </tr> <tr> + <td><span class='fas fa-dollar-sign'></span></td> <td><b>{% trans "Salable" %}</b></td> <td>{% include "slide.html" with state=part.salable field='salable' %}</td> {% if part.salable %} @@ -196,6 +204,13 @@ {% endif %} </tr> <tr> + <td> + {% if part.active %} + <span class='fas fa-check-square'></span> + {% else %} + <span class='fas fa-times-square'></span> + {% endif %} + </td> <td><b>{% trans "Active" %}</b></td> <td>{% include "slide.html" with state=part.active field='active' disabled=False %}</td> {% if part.active %} diff --git a/InvenTree/part/templates/part/orders.html b/InvenTree/part/templates/part/orders.html index 8fe5e71688..3611418c6c 100644 --- a/InvenTree/part/templates/part/orders.html +++ b/InvenTree/part/templates/part/orders.html @@ -11,7 +11,9 @@ <div id='button-bar'> <div class='button-toolbar container-fluid' style='float: right;'> - <button class='btn btn-primary' type='button' id='part-order2' title='{% trans "Order part" %}'>{% trans "Order Part" %}</button> + <button class='btn btn-primary' type='button' id='part-order2' title='{% trans "Order part" %}'> + <span class='fas fa-shopping-cart'></span> {% trans "Order Part" %} + </button> <div class='filter-list' id='filter-list-purchaseorder'> <!-- An empty div in which the filter list will be constructed --> </div> diff --git a/InvenTree/part/templates/part/params.html b/InvenTree/part/templates/part/params.html index ba8aa0566d..af1c4cdab6 100644 --- a/InvenTree/part/templates/part/params.html +++ b/InvenTree/part/templates/part/params.html @@ -11,7 +11,9 @@ <div id='button-toolbar'> <div class='button-toolbar container-fluid' style='float: right;'> {% if roles.part.add %} - <button title='{% trans "Add new parameter" %}' class='btn btn-success' id='param-create'>{% trans "New Parameter" %}</button> + <button title='{% trans "Add new parameter" %}' class='btn btn-success' id='param-create'> + <span class='fas fa-plus-circle'></span> {% trans "New Parameter" %} + </button> {% endif %} </div> </div> diff --git a/InvenTree/part/templates/part/related.html b/InvenTree/part/templates/part/related.html new file mode 100644 index 0000000000..8c5cc074c8 --- /dev/null +++ b/InvenTree/part/templates/part/related.html @@ -0,0 +1,77 @@ +{% extends "part/part_base.html" %} +{% load static %} +{% load i18n %} + +{% block details %} + +{% include 'part/tabs.html' with tab='related-parts' %} + +<h4>{% trans "Related Parts" %}</h4> +<hr> + +<div id='button-bar'> + <div class='button-toolbar container-fluid' style='float: left;'> + {% if roles.part.change %} + <button class='btn btn-primary' type='button' id='add-related-part' title='{% trans "Add Related" %}'>{% trans "Add Related" %}</button> + <div class='filter-list' id='filter-list-related'> + <!-- An empty div in which the filter list will be constructed --> + </div> + {% endif %} + </div> +</div> + +<table id='table-related-part' class='table table-condensed table-striped' data-toolbar='#button-toolbar'> + <thead> + <tr> + <th data-field='part' data-serachable='true'>{% trans "Part" %}</th> + </tr> + </thead> + <tbody> + {% for item in part.get_related_parts %} + {% with part_related=item.0 part=item.1 %} + <tr> + <td> + <a class='hover-icon'> + <img class='hover-img-thumb' src='{{ part.get_thumbnail_url }}'> + <img class='hover-img-large' src='{{ part.get_thumbnail_url }}'> + </a> + <a href='/part/{{ part.id }}/'>{{ part }}</a> + <div class='btn-group' style='float: right;'> + {% if roles.part.change %} + <button title='{% trans "Delete" %}' class='btn btn-default btn-glyph delete-related-part' url="{% url 'part-related-delete' part_related.id %}" type='button'><span class='fas fa-trash-alt icon-red'/></button> + {% endif %} + </div> + </td> + </tr> + {% endwith %} + {% endfor %} + </tbody> +</table> + + +{% endblock %} + +{% block js_ready %} +{{ block.super }} + + $('#table-related-part').inventreeTable({ + }); + + $("#add-related-part").click(function() { + launchModalForm("{% url 'part-related-create' %}", { + data: { + part: {{ part.id }}, + }, + reload: true, + }); + }); + + $('.delete-related-part').click(function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + reload: true, + }); + }); + +{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/supplier.html b/InvenTree/part/templates/part/supplier.html index 4de56b0d2e..5d151eb4cc 100644 --- a/InvenTree/part/templates/part/supplier.html +++ b/InvenTree/part/templates/part/supplier.html @@ -10,7 +10,9 @@ <div id='button-toolbar'> <div class='btn-group'> - <button class="btn btn-success" id='supplier-create'>{% trans "New Supplier Part" %}</button> + <button class="btn btn-success" id='supplier-create'> + <span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %} + </button> <div id='opt-dropdown' class="btn-group"> <button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button> <ul class="dropdown-menu"> diff --git a/InvenTree/part/templates/part/tabs.html b/InvenTree/part/templates/part/tabs.html index 8322a225bc..8bfaba4d89 100644 --- a/InvenTree/part/templates/part/tabs.html +++ b/InvenTree/part/templates/part/tabs.html @@ -63,6 +63,9 @@ </a> </li> {% endif %} + <li{% ifequal tab 'related-parts' %} class="active"{% endifequal %}> + <a href="{% url 'part-related' part.id %}">{% trans "Related" %} {% if part.related_count > 0 %}<span class="badge">{{ part.related_count }}</span>{% endif %}</a> + </li> <li{% ifequal tab 'attachments' %} class="active"{% endifequal %}> <a href="{% url 'part-attachments' part.id %}">{% trans "Attachments" %} {% if part.attachment_count > 0 %}<span class="badge">{{ part.attachment_count }}</span>{% endif %}</a> </li> diff --git a/InvenTree/part/templates/part/used_in.html b/InvenTree/part/templates/part/used_in.html index 3a6605847e..25e858b5f0 100644 --- a/InvenTree/part/templates/part/used_in.html +++ b/InvenTree/part/templates/part/used_in.html @@ -8,7 +8,13 @@ <hr> -<table class="table table-striped table-condensed" id='used-table'> +<div id='button-toolbar'> + <div class='filter-list' id='filter-list-usedin'> + <!-- Empty div (will be filled out with avilable BOM filters) --> + </div> +</div> + +<table class="table table-striped table-condensed" id='used-table' data-toolbar='#button-toolbar'> </table> {% endblock %} @@ -16,52 +22,10 @@ {% block js_ready %} {{ block.super }} - $("#used-table").inventreeTable({ - formatNoMatches: function() { return "{{ part.full_name }} is not used to make any other parts"; }, - queryParams: function(p) { - return { - sub_part: {{ part.id }}, - part_detail: true, - } - }, - columns: [ - { - field: 'pk', - title: 'ID', - visible: false, - switchable: false, - }, - { - field: 'part_detail', - title: 'Part', - sortable: true, - formatter: function(value, row, index, field) { - var link = `/part/${value.pk}/bom/`; - var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value.full_name, link); + loadUsedInTable('#used-table', { + part_detail: true, + part_id: {{ part.pk }} + }); - if (!row.part_detail.active) { - html += "<span class='label label-warning' style='float: right;'>{% trans "INACTIVE" %}</span>"; - } - - return html; - } - }, - { - field: 'part_detail.description', - title: 'Description', - sortable: true, - }, - { - sortable: true, - field: 'quantity', - title: 'Uses', - formatter: function(value, row, index, field) { - return parseFloat(value); - }, - } - - ], - url: "{% url 'api-bom-list' %}" - }) {% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/variants.html b/InvenTree/part/templates/part/variants.html index e31d6deee2..56aacdd1dc 100644 --- a/InvenTree/part/templates/part/variants.html +++ b/InvenTree/part/templates/part/variants.html @@ -16,10 +16,17 @@ <hr> <div id='button-toolbar'> - <div class='btn-group'> - {% if part.is_template and part.active %} - <button class='btn btn-success' id='new-variant' title='{% trans "Create new variant" %}'>{% trans "New Variant" %}</button> - {% endif %} + <div class='button-toolbar container-fluid'> + <div class='btn-group' role='group'> + {% if part.is_template and part.active %} + <button class='btn btn-success' id='new-variant' title='{% trans "Create new variant" %}'> + <span class='fas fa-plus-circle'></span> {% trans "New Variant" %} + </button> + {% endif %} + </div> + <div class='filter-list' id='filter-list-variants'> + <!-- Empty div (will be filled out with available BOM filters) --> + </div> </div> </div> diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 3af495cc1c..9692e208e2 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -13,6 +13,19 @@ from common.models import InvenTreeSetting, ColorTheme register = template.Library() +@register.simple_tag() +def define(value, *args, **kwargs): + """ + Shortcut function to overcome the shortcomings of the django templating language + + Use as follows: {% define "hello_world" as hello %} + + Ref: https://stackoverflow.com/questions/1070398/how-to-set-a-value-of-a-variable-inside-a-template-code + """ + + return value + + @register.simple_tag() def decimal(x, *args, **kwargs): """ Simplified rendering of a decimal number """ @@ -106,6 +119,15 @@ def setting_object(key, *args, **kwargs): return setting +@register.simple_tag() +def settings_value(key, *args, **kwargs): + """ + Return a settings value specified by the given key + """ + + return InvenTreeSetting.get_setting(key) + + @register.simple_tag() def get_color_theme_css(username): try: diff --git a/InvenTree/part/test_bom_item.py b/InvenTree/part/test_bom_item.py index 91aa95c17c..a518ca1ddc 100644 --- a/InvenTree/part/test_bom_item.py +++ b/InvenTree/part/test_bom_item.py @@ -28,10 +28,12 @@ class BomItemTest(TestCase): self.assertEqual(self.bob.bom_count, 4) def test_in_bom(self): - parts = self.bob.required_parts() + parts = self.bob.getRequiredParts() self.assertIn(self.orphan, parts) + # TODO: Tests for multi-level BOMs + def test_used_in(self): self.assertEqual(self.bob.used_in_count, 0) self.assertEqual(self.orphan.used_in_count, 1) diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index 97330f1ced..1301df3c91 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -99,7 +99,7 @@ class PartTest(TestCase): self.assertIn(self.R1.name, barcode) def test_copy(self): - self.R2.deepCopy(self.R1, image=True, bom=True) + self.R2.deep_copy(self.R1, image=True, bom=True) def test_match_names(self): diff --git a/InvenTree/part/test_views.py b/InvenTree/part/test_views.py index d8c345d243..1ad5c33e45 100644 --- a/InvenTree/part/test_views.py +++ b/InvenTree/part/test_views.py @@ -201,6 +201,29 @@ class PartTests(PartViewTestCase): self.assertEqual(response.status_code, 200) +class PartRelatedTests(PartViewTestCase): + + def test_valid_create(self): + """ test creation of an attachment for a valid part """ + + response = self.client.get(reverse('part-related-create'), {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + # TODO - Create a new attachment using this view + + def test_invalid_create(self): + """ test creation of an attachment for an invalid part """ + + # TODO + pass + + def test_edit(self): + """ test editing an attachment """ + + # TODO + pass + + class PartAttachmentTests(PartViewTestCase): def test_valid_create(self): diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 32cd1b0615..624a94b0b6 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -12,6 +12,11 @@ from django.conf.urls import url, include from . import views +part_related_urls = [ + url(r'^new/?', views.PartRelatedCreate.as_view(), name='part-related-create'), + url(r'^(?P<pk>\d+)/delete/?', views.PartRelatedDelete.as_view(), name='part-related-delete'), +] + part_attachment_urls = [ url(r'^new/?', views.PartAttachmentCreate.as_view(), name='part-attachment-create'), url(r'^(?P<pk>\d+)/edit/?', views.PartAttachmentEdit.as_view(), name='part-attachment-edit'), @@ -46,6 +51,7 @@ part_detail_urls = [ url(r'^pricing/', views.PartPricing.as_view(), name='part-pricing'), url(r'^bom-upload/?', views.BomUpload.as_view(), name='upload-bom'), + url(r'^bom-duplicate/?', views.BomDuplicate.as_view(), name='duplicate-bom'), url(r'^params/', views.PartDetail.as_view(template_name='part/params.html'), name='part-params'), url(r'^variants/?', views.PartDetail.as_view(template_name='part/variants.html'), name='part-variants'), @@ -60,6 +66,7 @@ part_detail_urls = [ url(r'^sale-prices/', views.PartDetail.as_view(template_name='part/sale_prices.html'), name='part-sale-prices'), url(r'^tests/', views.PartDetail.as_view(template_name='part/part_tests.html'), name='part-test-templates'), url(r'^track/?', views.PartDetail.as_view(template_name='part/track.html'), name='part-track'), + url(r'^related-parts/?', views.PartDetail.as_view(template_name='part/related.html'), name='part-related'), url(r'^attachments/?', views.PartDetail.as_view(template_name='part/attachments.html'), name='part-attachments'), url(r'^notes/?', views.PartNotes.as_view(), name='part-notes'), @@ -112,6 +119,9 @@ part_urls = [ # Part category url(r'^category/(?P<pk>\d+)/', include(part_category_urls)), + # Part related + url(r'^related-parts/', include(part_related_urls)), + # Part attachments url(r'^attachment/', include(part_attachment_urls)), diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index a1ce33df51..bd602448db 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -21,7 +21,7 @@ import os from rapidfuzz import fuzz from decimal import Decimal, InvalidOperation -from .models import PartCategory, Part, PartAttachment +from .models import PartCategory, Part, PartAttachment, PartRelated from .models import PartParameterTemplate, PartParameter from .models import BomItem from .models import match_part_names @@ -70,6 +70,85 @@ class PartIndex(InvenTreeRoleMixin, ListView): return context +class PartRelatedCreate(AjaxCreateView): + """ View for creating a new PartRelated object + + - The view only makes sense if a Part object is passed to it + """ + model = PartRelated + form_class = part_forms.CreatePartRelatedForm + ajax_form_title = _("Add Related Part") + ajax_template_name = "modal_form.html" + role_required = 'part.change' + + def get_initial(self): + """ Set parent part as part_1 field """ + + initials = {} + + part_id = self.request.GET.get('part', None) + + if part_id: + try: + initials['part_1'] = Part.objects.get(pk=part_id) + except (Part.DoesNotExist, ValueError): + pass + + return initials + + def get_form(self): + """ Create a form to upload a new PartRelated + + - Hide the 'part_1' field (parent part) + - Display parts which are not yet related + """ + + form = super(AjaxCreateView, self).get_form() + + form.fields['part_1'].widget = HiddenInput() + + try: + # Get parent part + parent_part = self.get_initial()['part_1'] + # Get existing related parts + related_parts = [related_part[1].pk for related_part in parent_part.get_related_parts()] + + # Build updated choice list excluding + # - parts already related to parent part + # - the parent part itself + updated_choices = [] + for choice in form.fields["part_2"].choices: + if (choice[0] not in related_parts) and (choice[0] != parent_part.pk): + updated_choices.append(choice) + + # Update choices for related part + form.fields['part_2'].choices = updated_choices + except KeyError: + pass + + return form + + def post_save(self): + """ Save PartRelated model (POST method does not) """ + + form = self.get_form() + + if form.is_valid(): + part_1 = form.cleaned_data['part_1'] + part_2 = form.cleaned_data['part_2'] + + PartRelated.create(part_1, part_2) + + +class PartRelatedDelete(AjaxDeleteView): + """ View for deleting a PartRelated object """ + + model = PartRelated + ajax_form_title = _("Delete Related Part") + context_object_name = "related" + role_required = 'part.change' + + class PartAttachmentCreate(AjaxCreateView): """ View for creating a new PartAttachment object @@ -82,10 +161,14 @@ class PartAttachmentCreate(AjaxCreateView): role_required = 'part.add' - def post_save(self): - """ Record the user that uploaded the attachment """ - self.object.user = self.request.user - self.object.save() + def save(self, form, **kwargs): + """ + Record the user that uploaded this attachment + """ + + attachment = form.save(commit=False) + attachment.user = self.request.user + attachment.save() def get_data(self): return { @@ -360,7 +443,7 @@ class MakePartVariant(AjaxCreateView): parameters_copy = str2bool(request.POST.get('parameters_copy', False)) # Copy relevent information from the template part - part.deepCopy(part_template, bom=bom_copy, parameters=parameters_copy) + part.deep_copy(part_template, bom=bom_copy, parameters=parameters_copy) return self.renderJsonResponse(request, form, data, context=context) @@ -371,6 +454,8 @@ class MakePartVariant(AjaxCreateView): initials = model_to_dict(part_template) initials['is_template'] = False initials['variant_of'] = part_template + initials['bom_copy'] = InvenTreeSetting.get_setting('PART_COPY_BOM') + initials['parameters_copy'] = InvenTreeSetting.get_setting('PART_COPY_PARAMETERS') return initials @@ -436,7 +521,8 @@ class PartDuplicate(AjaxCreateView): matches = match_part_names(name) if len(matches) > 0: - context['matches'] = matches + # Display the first five closest matches + context['matches'] = matches[:5] # Enforce display of the checkbox form.fields['confirm_creation'].widget = CheckboxInput() @@ -445,9 +531,9 @@ class PartDuplicate(AjaxCreateView): confirmed = str2bool(request.POST.get('confirm_creation', False)) if not confirmed: - form.errors['confirm_creation'] = ['Possible matches exist - confirm creation of new part'] - - form.pre_form_warning = 'Possible matches exist - confirm creation of new part' + msg = _('Possible matches exist - confirm creation of new part') + form.add_error('confirm_creation', msg) + form.pre_form_warning = msg valid = False data = { @@ -470,7 +556,7 @@ class PartDuplicate(AjaxCreateView): original = self.get_part_to_copy() if original: - part.deepCopy(original, bom=bom_copy, parameters=parameters_copy) + part.deep_copy(original, bom=bom_copy, parameters=parameters_copy) try: data['url'] = part.get_absolute_url() @@ -510,7 +596,7 @@ class PartCreate(AjaxCreateView): model = Part form_class = part_forms.EditPartForm - ajax_form_title = _('Create new part') + ajax_form_title = _('Create New Part') ajax_template_name = 'part/create_part.html' role_required = 'part.add' @@ -575,9 +661,10 @@ class PartCreate(AjaxCreateView): confirmed = str2bool(request.POST.get('confirm_creation', False)) if not confirmed: - form.errors['confirm_creation'] = ['Possible matches exist - confirm creation of new part'] + msg = _('Possible matches exist - confirm creation of new part') + form.add_error('confirm_creation', msg) - form.pre_form_warning = 'Possible matches exist - confirm creation of new part' + form.pre_form_warning = msg valid = False data = { @@ -830,8 +917,63 @@ class PartEdit(AjaxUpdateView): return form +class BomDuplicate(AjaxUpdateView): + """ + View for duplicating BOM from a parent item. + """ + + model = Part + context_object_name = 'part' + ajax_form_title = _('Duplicate BOM') + ajax_template_name = 'part/bom_duplicate.html' + form_class = part_forms.BomDuplicateForm + role_required = 'part.change' + + def get_form(self): + + form = super().get_form() + + # Limit choices to parents of the current part + parents = self.get_object().get_ancestors() + + form.fields['parent'].queryset = parents + + return form + + def get_initial(self): + initials = super().get_initial() + + parents = self.get_object().get_ancestors() + + if parents.count() == 1: + initials['parent'] = parents[0] + + return initials + + def validate(self, part, form): + + confirm = str2bool(form.cleaned_data.get('confirm', False)) + + if not confirm: + form.add_error('confirm', _('Confirm duplication of BOM from parent')) + + def save(self, part, form): + """ + Duplicate BOM from the specified parent + """ + + parent = form.cleaned_data.get('parent', None) + + clear = str2bool(form.cleaned_data.get('clear', True)) + + if parent: + part.copy_bom_from(parent, clear=clear) + + class BomValidate(AjaxUpdateView): - """ Modal form view for validating a part BOM """ + """ + Modal form view for validating a part BOM + """ model = Part ajax_form_title = _("Validate BOM") @@ -852,24 +994,25 @@ class BomValidate(AjaxUpdateView): return self.renderJsonResponse(request, form, context=self.get_context()) - def post(self, request, *args, **kwargs): + def validate(self, part, form, **kwargs): - form = self.get_form() - part = self.get_object() + confirm = str2bool(form.cleaned_data.get('validate', False)) - confirmed = str2bool(request.POST.get('validate', False)) + if not confirm: + form.add_error('validate', _('Confirm that the BOM is valid')) - if confirmed: - part.validate_bom(request.user) - else: - form.errors['validate'] = ['Confirm that the BOM is valid'] + def save(self, part, form, **kwargs): + """ + Mark the BOM as validated + """ - data = { - 'form_valid': confirmed + part.validate_bom(self.request.user) + + def get_data(self): + return { + 'success': _('Validated Bill of Materials') } - return self.renderJsonResponse(request, form, data, context=self.get_context()) - class BomUpload(InvenTreeRoleMixin, FormView): """ View for uploading a BOM file, and handling BOM data importing. @@ -1001,7 +1144,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): bom_file_valid = False if bom_file is None: - self.form.errors['bom_file'] = [_('No BOM file provided')] + self.form.add_error('bom_file', _('No BOM file provided')) else: # Create a BomUploadManager object - will perform initial data validation # (and raise a ValidationError if there is something wrong with the file) @@ -1012,7 +1155,7 @@ class BomUpload(InvenTreeRoleMixin, FormView): errors = e.error_dict for k, v in errors.items(): - self.form.errors[k] = v + self.form.add_error(k, v) if bom_file_valid: # BOM file is valid? Proceed to the next step! @@ -2097,7 +2240,7 @@ class BomItemCreate(AjaxCreateView): model = BomItem form_class = part_forms.EditBomItemForm ajax_template_name = 'modal_form.html' - ajax_form_title = _('Create BOM item') + ajax_form_title = _('Create BOM Item') role_required = 'part.add' @@ -2127,7 +2270,7 @@ class BomItemCreate(AjaxCreateView): query = query.filter(active=True) # Eliminate any options that are already in the BOM! - query = query.exclude(id__in=[item.id for item in part.required_parts()]) + query = query.exclude(id__in=[item.id for item in part.getRequiredParts()]) form.fields['sub_part'].queryset = query @@ -2195,7 +2338,7 @@ class BomItemEdit(AjaxUpdateView): except ValueError: sub_part_id = -1 - existing = [item.pk for item in part.required_parts()] + existing = [item.pk for item in part.getRequiredParts()] if sub_part_id in existing: existing.remove(sub_part_id) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index dabeaa20ea..5d71d00a0c 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -493,6 +493,12 @@ class StockList(generics.ListCreateAPIView): if build_order: queryset = queryset.filter(build_order=build_order) + is_building = params.get('is_building', None) + + if is_building: + is_building = str2bool(is_building) + queryset = queryset.filter(is_building=is_building) + sales_order = params.get('sales_order', None) if sales_order: diff --git a/InvenTree/stock/fixtures/stock.yaml b/InvenTree/stock/fixtures/stock.yaml index dd528775eb..719d8a34ce 100644 --- a/InvenTree/stock/fixtures/stock.yaml +++ b/InvenTree/stock/fixtures/stock.yaml @@ -41,6 +41,7 @@ - model: stock.stockitem pk: 100 fields: + batch: "B1234" part: 25 location: 7 quantity: 10 @@ -54,6 +55,7 @@ pk: 101 fields: part: 25 + batch: "B2345" location: 7 quantity: 5 level: 0 @@ -65,6 +67,7 @@ pk: 102 fields: part: 25 + batch: 'ABCDE' location: 7 quantity: 3 level: 0 @@ -91,6 +94,7 @@ part: 10001 location: 7 quantity: 5 + batch: "AAA" level: 0 tree_id: 0 lft: 0 @@ -101,6 +105,7 @@ fields: part: 10001 location: 7 + batch: "AAA" quantity: 1 serial: 1 level: 0 diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 548a03ae90..d9937f3106 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -108,7 +108,7 @@ class ConvertStockItemForm(HelperForm): class CreateStockItemForm(HelperForm): """ Form for creating a new StockItem """ - serial_numbers = forms.CharField(label='Serial numbers', required=False, help_text=_('Enter unique serial numbers (or leave blank)')) + serial_numbers = forms.CharField(label=_('Serial numbers'), required=False, help_text=_('Enter unique serial numbers (or leave blank)')) def __init__(self, *args, **kwargs): diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 1535ded420..2ec42dd2f3 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -130,7 +130,7 @@ class StockItem(MPTTModel): status: Status of this StockItem (ref: InvenTree.status_codes.StockStatus) notes: Extra notes field build: Link to a Build (if this stock item was created from a build) - is_building: Boolean field indicating if this stock item is currently being built + is_building: Boolean field indicating if this stock item is currently being built (or is "in production") 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) @@ -139,6 +139,7 @@ class StockItem(MPTTModel): # 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, @@ -174,7 +175,7 @@ class StockItem(MPTTModel): if add_note: # This StockItem is being saved for the first time self.addTransactionNote( - 'Created stock item', + _('Created stock item'), user, notes="Created new stock item for part '{p}'".format(p=str(self.part)), system=True @@ -198,7 +199,8 @@ class StockItem(MPTTModel): """ super(StockItem, self).validate_unique(exclude) - + + # If the serial number is set, make sure it is not a duplicate if self.serial is not None: # Query to look for duplicate serial numbers parts = PartModels.Part.objects.filter(tree_id=self.part.tree_id) @@ -718,6 +720,15 @@ class StockItem(MPTTModel): @property def in_stock(self): + """ + Returns True if this item is in stock + + See also: IN_STOCK_FILTER + """ + + # Quantity must be above zero (unless infinite) + if self.quantity <= 0 and not self.infinite: + return False # Not 'in stock' if it has been installed inside another StockItem if self.belongs_to is not None: @@ -814,14 +825,11 @@ class StockItem(MPTTModel): raise ValidationError({"quantity": _("Quantity does not match serial numbers")}) # Test if each of the serial numbers are valid - existing = [] - - for serial in serials: - if self.part.checkIfSerialNumberExists(serial): - existing.append(serial) + existing = self.part.find_conflicting_serial_numbers(serials) if len(existing) > 0: - raise ValidationError({"serial_numbers": _("Serial numbers already exist: ") + str(existing)}) + exists = ','.join([str(x) for x in existing]) + raise ValidationError({"serial_numbers": _("Serial numbers already exist") + ': ' + exists}) # Create a new stock item for each unique serial number for serial in serials: @@ -1052,7 +1060,7 @@ class StockItem(MPTTModel): if self.updateQuantity(count): - self.addTransactionNote('Stocktake - counted {n} items'.format(n=count), + self.addTransactionNote('Stocktake - counted {n} items'.format(n=helpers.normalize(count)), user, notes=notes, system=True) @@ -1081,7 +1089,7 @@ class StockItem(MPTTModel): if self.updateQuantity(self.quantity + quantity): - self.addTransactionNote('Added {n} items to stock'.format(n=quantity), + self.addTransactionNote('Added {n} items to stock'.format(n=helpers.normalize(quantity)), user, notes=notes, system=True) @@ -1107,7 +1115,7 @@ class StockItem(MPTTModel): if self.updateQuantity(self.quantity - quantity): - self.addTransactionNote('Removed {n} items from stock'.format(n=quantity), + self.addTransactionNote('Removed {n} items from stock'.format(n=helpers.normalize(quantity)), user, notes=notes, system=True) @@ -1129,6 +1137,22 @@ class StockItem(MPTTModel): return s + @transaction.atomic + def clear_test_results(self, **kwargs): + """ + Remove all test results + + kwargs: + TODO + """ + + # All test results + results = self.test_results.all() + + # TODO - Perhaps some filtering options supplied by kwargs? + + results.delete() + def getTestResults(self, test=None, result=None, user=None): """ Return all test results associated with this StockItem. diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 4a9b5a886b..5bea1c4aae 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -154,9 +154,11 @@ class StockItemSerializer(InvenTreeModelSerializer): 'allocated', 'batch', 'belongs_to', - 'customer', + 'build', 'build_order', + 'customer', 'in_stock', + 'is_building', 'link', 'location', 'location_detail', diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index b19887c8aa..8f8502af2a 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -14,7 +14,9 @@ {% if roles.stock.change %} <div id='table-toolbar'> <div class='btn-group'> - <button class='btn btn-success' type='button' title='New tracking entry' id='new-entry'>New Entry</button> + <button class='btn btn-success' type='button' title='New tracking entry' id='new-entry'> + <span class='fas fa-plus-circle'></span> {% trans "New Entry" %} + </button> </div> </div> {% endif %} diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index d7eb987aab..71bdc3592d 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -15,6 +15,20 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% block pre_content %} {% include 'stock/loc_link.html' with location=item.location %} +{% if item.is_building %} +<div class='alert alert-block alert-info'> + {% trans "This stock item is in production and cannot be edited." %}<br> + {% trans "Edit the stock item from the build view." %}<br> + + {% if item.build %} + <a href="{% url 'build-detail' item.build.id %}"> + <b>{{ item.build }}</b> + </a> + {% endif %} + +</div> +{% endif %} + {% if item.hasRequiredTests and not item.passedAllRequiredTests %} <div class='alert alert-block alert-danger'> {% trans "This stock item has not passed all required tests" %} @@ -23,13 +37,13 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% for allocation in item.sales_order_allocations.all %} <div class='alert alert-block alert-info'> - {% trans "This stock item is allocated to Sales Order" %} <a href="{% url 'so-detail' allocation.line.order.id %}"><b>#{{ allocation.line.order.reference }}</b></a> ({% trans "Quantity" %}: {% decimal allocation.quantity %}) + {% trans "This stock item is allocated to Sales Order" %} <a href="{% url 'so-detail' allocation.line.order.id %}"><b>#{{ allocation.line.order }}</b></a> ({% trans "Quantity" %}: {% decimal allocation.quantity %}) </div> {% endfor %} {% for allocation in item.allocations.all %} <div class='alert alert-block alert-info'> - {% trans "This stock item is allocated to Build" %} <a href="{% url 'build-detail' allocation.build.id %}"><b>#{{ allocation.build.id }}</b></a> ({% trans "Quantity" %}: {% decimal allocation.quantity %}) + {% trans "This stock item is allocated to Build" %} <a href="{% url 'build-detail' allocation.build.id %}"><b>#{{ allocation.build }}</b></a> ({% trans "Quantity" %}: {% decimal allocation.quantity %}) </div> {% endfor %} @@ -79,7 +93,6 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} <div class='btn-group' role='group'> - </div> <div class='btn-group action-buttons' role='group'> @@ -99,7 +112,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} </ul> </div> <!-- Stock adjustment menu --> - {% if roles.stock.change %} + {% if roles.stock.change and not item.is_building %} <div class='btn-group'> <button id='stock-options' title='{% trans "Stock adjustment actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-boxes'></span> <span class='caret'></span></button> <ul class='dropdown-menu' role='menu'> @@ -129,7 +142,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} </div> {% endif %} <!-- Edit stock item --> - {% if roles.stock.change %} + {% if roles.stock.change and not item.is_building %} <div class='btn-group'> <button id='stock-edit-actions' title='{% trans "Stock actions" %}' class='btn btn-default dropdown-toggle' type='button' data-toggle='dropdown'><span class='fas fa-tools'></span> <span class='caret'></span></button> <ul class='dropdown-menu' role='menu'> @@ -221,7 +234,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% if item.location %} <td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td> {% else %} - <td>{% trans "No location set" %}</td> + <td><i>{% trans "No location set" %}</i></td> {% endif %} </tr> {% endif %} @@ -234,7 +247,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endif %} {% if item.batch %} <tr> - <td></td> + <td><span class='fas fa-layer-group'></span></td> <td>{% trans "Batch" %}</td> <td>{{ item.batch }}</td> </tr> @@ -248,7 +261,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% endif %} {% if item.purchase_order %} <tr> - <td></td> + <td><span class='fas fa-shopping-cart'></span></td> <td>{% trans "Purchase Order" %}</td> <td><a href="{% url 'po-detail' item.purchase_order.id %}">{{ item.purchase_order }}</a></td> </tr> @@ -290,7 +303,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% if item.stocktake_date %} <td>{{ item.stocktake_date }} <span class='badge'>{{ item.stocktake_user }}</span></td> {% else %} - <td>{% trans "No stocktake performed" %}</td> + <td><i>{% trans "No stocktake performed" %}</i></td> {% endif %} </tr> <tr> diff --git a/InvenTree/stock/templates/stock/item_tests.html b/InvenTree/stock/templates/stock/item_tests.html index 6bc120711d..11ab443d0c 100644 --- a/InvenTree/stock/templates/stock/item_tests.html +++ b/InvenTree/stock/templates/stock/item_tests.html @@ -14,10 +14,16 @@ <div class='button-toolbar container-fluid' style="float: right;"> <div class='btn-group' role='group'> {% if user.is_staff %} - <button type='button' class='btn btn-danger' id='delete-test-results'>{% trans "Delete Test Data" %}</button> + <button type='button' class='btn btn-danger' id='delete-test-results'> + <span class='fas fa-trash-alt'></span> {% trans "Delete Test Data" %} + </button> {% endif %} - <button type='button' class='btn btn-success' id='add-test-result'>{% trans "Add Test Data" %}</button> - <button type='button' class='btn btn-default' id='test-report'>{% trans "Test Report" %} <span class='fas fa-tasks'></span></button> + <button type='button' class='btn btn-success' id='add-test-result'> + <span class='fas fa-plus-circle'></span> {% trans "Add Test Data" %} + </button> + <button type='button' class='btn btn-default' id='test-report'> + <span class='fas fa-tasks'></span> {% trans "Test Report" %} + </button> </div> <div class='filter-list' id='filter-list-stocktests'> <!-- Empty div --> diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 34fe8877f8..1bd4feb02f 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -197,6 +197,10 @@ class StockTest(TestCase): def test_partial_move(self): w1 = StockItem.objects.get(pk=100) + # A batch code is required to split partial stock! + w1.batch = 'BW1' + w1.save() + # Move 6 of the units self.assertTrue(w1.move(self.diningroom, 'Moved', None, quantity=6)) @@ -339,6 +343,7 @@ class StockTest(TestCase): # Item will deplete when deleted item = StockItem.objects.get(pk=100) item.delete_on_deplete = True + item.save() n = StockItem.objects.filter(part=25).count() @@ -407,6 +412,13 @@ class VariantTest(StockTest): self.assertEqual(chair.getLatestSerialNumber(), '22') + # Check for conflicting serial numbers + to_check = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + conflicts = chair.find_conflicting_serial_numbers(to_check) + + self.assertEqual(len(conflicts), 6) + # Same operations on a sub-item variant = Part.objects.get(pk=10003) self.assertEqual(variant.getLatestSerialNumber(), '22') @@ -516,6 +528,7 @@ class TestResultTest(StockTest): item.pk = None item.serial = None item.quantity = 50 + item.batch = "B344" item.save() diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index 65c8f30893..2e45ceb00e 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -21,7 +21,7 @@ from InvenTree.views import InvenTreeRoleMixin from InvenTree.forms import ConfirmForm from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats -from InvenTree.helpers import ExtractSerialNumbers +from InvenTree.helpers import extract_serial_numbers from decimal import Decimal, InvalidOperation from datetime import datetime @@ -164,11 +164,12 @@ class StockItemAttachmentCreate(AjaxCreateView): ajax_template_name = "modal_form.html" role_required = 'stock.add' - def post_save(self, **kwargs): + def save(self, form, **kwargs): """ Record the user that uploaded the attachment """ - self.object.user = self.request.user - self.object.save() + attachment = form.save(commit=False) + attachment.user = self.request.user + attachment.save() def get_data(self): return { @@ -245,32 +246,28 @@ class StockItemAssignToCustomer(AjaxUpdateView): form_class = StockForms.AssignStockItemToCustomerForm role_required = 'stock.change' - def post(self, request, *args, **kwargs): + def validate(self, item, form, **kwargs): - customer = request.POST.get('customer', None) + customer = form.cleaned_data.get('customer', None) + + if not customer: + form.add_error('customer', _('Customer must be specified')) + + def save(self, item, form, **kwargs): + """ + Assign the stock item to the customer. + """ + + customer = form.cleaned_data.get('customer', None) if customer: - try: - customer = Company.objects.get(pk=customer) - except (ValueError, Company.DoesNotExist): - customer = None - - if customer is not None: - stock_item = self.get_object() - - item = stock_item.allocateToCustomer( + item = item.allocateToCustomer( customer, - user=request.user + user=self.request.user ) item.clearAllocations() - data = { - 'form_valid': True, - } - - return self.renderJsonResponse(request, self.get_form(), data) - class StockItemReturnToStock(AjaxUpdateView): """ @@ -283,30 +280,25 @@ class StockItemReturnToStock(AjaxUpdateView): form_class = StockForms.ReturnStockItemForm role_required = 'stock.change' - def post(self, request, *args, **kwargs): + def validate(self, item, form, **kwargs): - location = request.POST.get('location', None) + location = form.cleaned_data.get('location', None) + + if not location: + form.add_error('location', _('Specify a valid location')) + + def save(self, item, form, **kwargs): + + location = form.cleaned_data.get('location', None) if location: - try: - location = StockLocation.objects.get(pk=location) - except (ValueError, StockLocation.DoesNotExist): - location = None + item.returnFromCustomer(location, self.request.user) - if location: - stock_item = self.get_object() - - stock_item.returnFromCustomer(location, request.user) - else: - raise ValidationError({'location': _("Specify a valid location")}) - - data = { - 'form_valid': True, - 'success': _("Stock item returned from customer") + def get_data(self): + return { + 'success': _('Stock item returned from customer') } - return self.renderJsonResponse(request, self.get_form(), data) - class StockItemSelectLabels(AjaxView): """ @@ -417,8 +409,8 @@ class StockItemDeleteTestData(AjaxUpdateView): confirm = str2bool(request.POST.get('confirm', False)) if confirm is not True: - form.errors['confirm'] = [_('Confirm test data deletion')] - form.non_field_errors = [_('Check the confirmation box')] + form.add_error('confirm', _('Confirm test data deletion')) + form.add_error(None, _('Check the confirmation box')) else: stock_item.test_results.all().delete() valid = True @@ -440,11 +432,14 @@ class StockItemTestResultCreate(AjaxCreateView): ajax_form_title = _("Add Test Result") role_required = 'stock.add' - def post_save(self, **kwargs): - """ Record the user that uploaded the test result """ + def save(self, form, **kwargs): + """ + Record the user that uploaded the test result + """ - self.object.user = self.request.user - self.object.save() + result = form.save(commit=False) + result.user = self.request.user + result.save() def get_initial(self): @@ -918,7 +913,7 @@ class StockItemUninstall(AjaxView, FormMixin): if not confirmed: valid = False - form.errors['confirm'] = [_('Confirm stock adjustment')] + form.add_error('confirm', _('Confirm stock adjustment')) data = { 'form_valid': valid, @@ -1116,7 +1111,7 @@ class StockAdjust(AjaxView, FormMixin): if not confirmed: valid = False - form.errors['confirm'] = [_('Confirm stock adjustment')] + form.add_error('confirm', _('Confirm stock adjustment')) data = { 'form_valid': valid, @@ -1414,9 +1409,9 @@ class StockItemSerialize(AjaxUpdateView): destination = None try: - numbers = ExtractSerialNumbers(serials, quantity) + numbers = extract_serial_numbers(serials, quantity) except ValidationError as e: - form.errors['serial_numbers'] = e.messages + form.add_error('serial_numbers', e.messages) valid = False numbers = [] @@ -1428,9 +1423,9 @@ class StockItemSerialize(AjaxUpdateView): for k in messages.keys(): if k in ['quantity', 'destination', 'serial_numbers']: - form.errors[k] = messages[k] + form.add_error(k, messages[k]) else: - form.non_field_errors = [messages[k]] + form.add_error(None, messages[k]) valid = False @@ -1510,11 +1505,8 @@ class StockItemCreate(AjaxCreateView): # form.fields['part'].widget = HiddenInput() # Trackable parts get special consideration: - if part.trackable: - form.fields['delete_on_deplete'].widget = HiddenInput() - form.fields['delete_on_deplete'].initial = False - else: - form.fields['serial_numbers'].widget = HiddenInput() + form.fields['delete_on_deplete'].disabled = not part.trackable + form.fields['serial_numbers'].disabled = not part.trackable # If the part is NOT purchaseable, hide the supplier_part field if not part.purchaseable: @@ -1539,6 +1531,8 @@ class StockItemCreate(AjaxCreateView): # We must not provide *any* options for SupplierPart form.fields['supplier_part'].queryset = SupplierPart.objects.none() + form.fields['serial_numbers'].disabled = True + # Otherwise if the user has selected a SupplierPart, we know what Part they meant! if form['supplier_part'].value() is not None: pass @@ -1622,14 +1616,14 @@ class StockItemCreate(AjaxCreateView): part = None quantity = 1 valid = False - form.errors['quantity'] = [_('Invalid quantity')] + form.add_error('quantity', _('Invalid quantity')) if quantity < 0: - form.errors['quantity'] = [_('Quantity cannot be less than zero')] + form.add_error('quantity', _('Quantity cannot be less than zero')) valid = False if part is None: - form.errors['part'] = [_('Invalid part selection')] + form.add_error('part', _('Invalid part selection')) else: # A trackable part must provide serial numbesr if part.trackable: @@ -1640,17 +1634,16 @@ class StockItemCreate(AjaxCreateView): # If user has specified a range of serial numbers if len(sn) > 0: try: - serials = ExtractSerialNumbers(sn, quantity) + serials = extract_serial_numbers(sn, quantity) - existing = [] - - for serial in serials: - if part.checkIfSerialNumberExists(serial): - existing.append(serial) + existing = part.find_conflicting_serial_numbers(serials) if len(existing) > 0: exists = ",".join([str(x) for x in existing]) - form.errors['serial_numbers'] = [_('The following serial numbers already exist: ({sn})'.format(sn=exists))] + form.add_error( + 'serial_numbers', + _('Serial numbers already exist') + ': ' + exists + ) valid = False else: @@ -1682,7 +1675,7 @@ class StockItemCreate(AjaxCreateView): valid = True except ValidationError as e: - form.errors['serial_numbers'] = e.messages + form.add_error('serial_numbers', e.messages) valid = False else: diff --git a/InvenTree/templates/InvenTree/bom_invalid.html b/InvenTree/templates/InvenTree/bom_invalid.html index 4a2fe5856c..46e698dcc3 100644 --- a/InvenTree/templates/InvenTree/bom_invalid.html +++ b/InvenTree/templates/InvenTree/bom_invalid.html @@ -4,7 +4,7 @@ {% block collapse_title %} <span class='fas fa-times-circle icon-header'></span> -{% trans "BOM Waiting Validation" %}<span class='badge' id='bom-invalid-count'><span class='fas fa-spin fa-hourglass-half'></span></span> +{% trans "BOM Waiting Validation" %}<span class='badge' id='bom-invalid-count'><span class='fas fa-spin fa-spinner'></span></span> {% endblock %} {% block collapse_content %} diff --git a/InvenTree/templates/InvenTree/build_pending.html b/InvenTree/templates/InvenTree/build_pending.html index 1b8ebc19c2..fce1c6df02 100644 --- a/InvenTree/templates/InvenTree/build_pending.html +++ b/InvenTree/templates/InvenTree/build_pending.html @@ -4,7 +4,7 @@ {% block collapse_title %} <span class='fas fa-cogs icon-header'></span> -{% trans "Pending Builds" %}<span class='badge' id='build-pending-count'><span class='fas fa-spin fa-hourglass-half'></span></span> +{% trans "Pending Builds" %}<span class='badge' id='build-pending-count'><span class='fas fa-spin fa-spinner'></span></span> {% endblock %} {% block collapse_content %} diff --git a/InvenTree/templates/InvenTree/latest_parts.html b/InvenTree/templates/InvenTree/latest_parts.html index 35e55b7cd2..e88ada9548 100644 --- a/InvenTree/templates/InvenTree/latest_parts.html +++ b/InvenTree/templates/InvenTree/latest_parts.html @@ -4,7 +4,7 @@ {% block collapse_title %} <span class='fas fa-newspaper icon-header'></span> -{% trans "Latest Parts" %}<span class='badge' id='latest-parts-count'><span class='fas fa-spin fa-hourglass-half'></span></span> +{% trans "Latest Parts" %}<span class='badge' id='latest-parts-count'><span class='fas fa-spin fa-spinner'></span></span> {% endblock %} {% block collapse_content %} diff --git a/InvenTree/templates/InvenTree/low_stock.html b/InvenTree/templates/InvenTree/low_stock.html index e7a3dbddc7..a4d2fb4e0f 100644 --- a/InvenTree/templates/InvenTree/low_stock.html +++ b/InvenTree/templates/InvenTree/low_stock.html @@ -4,7 +4,7 @@ {% block collapse_title %} <span class='fas fa-shopping-cart icon-header'></span> -{% trans "Low Stock" %}<span class='badge' id='low-stock-count'><span class='fas fa-spin fa-hourglass-half'></span></span> +{% trans "Low Stock" %}<span class='badge' id='low-stock-count'><span class='fas fa-spin fa-spinner'></span></span> {% endblock %} {% block collapse_content %} diff --git a/InvenTree/templates/InvenTree/po_outstanding.html b/InvenTree/templates/InvenTree/po_outstanding.html index 628695fa68..8393e36b97 100644 --- a/InvenTree/templates/InvenTree/po_outstanding.html +++ b/InvenTree/templates/InvenTree/po_outstanding.html @@ -4,7 +4,7 @@ {% block collapse_title %} <span class='fas fa-sign-in-alt icon-header'></span> -{% trans "Outstanding Purchase Orders" %}<span class='badge' id='po-outstanding-count'><span class='fas fa-spin fa-hourglass-half'></span></span> +{% trans "Outstanding Purchase Orders" %}<span class='badge' id='po-outstanding-count'><span class='fas fa-spin fa-spinner'></span></span> {% endblock %} {% block collapse_content %} diff --git a/InvenTree/templates/InvenTree/required_stock_build.html b/InvenTree/templates/InvenTree/required_stock_build.html index fd6ade4a4e..337ed289ba 100644 --- a/InvenTree/templates/InvenTree/required_stock_build.html +++ b/InvenTree/templates/InvenTree/required_stock_build.html @@ -4,7 +4,7 @@ {% block collapse_title %} <span class='fas fa-bullhorn icon-header'></span> -{% trans "Require Stock To Complete Build" %}<span class='badge' id='stock-to-build-count'><span class='fas fa-spin fa-hourglass-half'></span></span> +{% trans "Require Stock To Complete Build" %}<span class='badge' id='stock-to-build-count'><span class='fas fa-spin fa-spinner'></span></span> {% endblock %} {% block collapse_content %} diff --git a/InvenTree/templates/InvenTree/searching.html b/InvenTree/templates/InvenTree/searching.html index cb33727a9e..d2c8dd0363 100644 --- a/InvenTree/templates/InvenTree/searching.html +++ b/InvenTree/templates/InvenTree/searching.html @@ -1,3 +1,3 @@ {% load i18n %} -<span class='fas fa-spin fa-hourglass-half'></span> <i>{% trans "Searching" %}</i> +<span class='fas fa-spin fa-spinner'></span> <i>{% trans "Searching" %}</i> diff --git a/InvenTree/templates/InvenTree/settings/currency.html b/InvenTree/templates/InvenTree/settings/currency.html index 99948eeeb1..444dd96c66 100644 --- a/InvenTree/templates/InvenTree/settings/currency.html +++ b/InvenTree/templates/InvenTree/settings/currency.html @@ -14,7 +14,8 @@ <h4>{% trans "Currencies" %}</h4> <div id='currency-buttons'> - <button class='btn btn-success' id='new-currency'>{% trans "New Currency" %}</button> + <button class='btn btn-success' id='new-currency'> + <span class='fas fa-plus-circle'></span> {% trans "New Currency" %}</button> </div> <table class='table table-striped table-condensed' id='currency-table' data-toolbar='#currency-buttons'> diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index cac04a60ff..19578ba858 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -24,7 +24,9 @@ <h4>{% trans "Part Parameter Templates" %}</h4> <div id='param-buttons'> - <button class='btn btn-success' id='new-param'>{% trans "New Parameter" %}</button> + <button class='btn btn-success' id='new-param'> + <span class='fas fa-plus-circle'></span> {% trans "New Parameter" %} + </button> </div> <table class='table table-striped table-condensed' id='param-table' data-toolbar='#param-buttons'> diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html index 28ab8746cb..1606a05cec 100644 --- a/InvenTree/templates/InvenTree/settings/user.html +++ b/InvenTree/templates/InvenTree/settings/user.html @@ -15,8 +15,12 @@ <div class='container'> <h4>{% trans "User Information" %}</h4> <div class='btn-group' style='float: right;'> - <div class='btn btn-primary' type='button' id='edit-user' title='Edit User Information'>Edit</div> - <div class='btn btn-primary' type='button' id='edit-password' title='Change Password'>Set Password</div> + <div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'> + <span class='fas fa-user-cog'></span> {% trans "Edit" %} + </div> + <div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'> + <span class='fas fa-key'></span> {% trans "Set Password" %} + </div> </div> <table class='table table-striped table-condensed'> diff --git a/InvenTree/templates/InvenTree/so_outstanding.html b/InvenTree/templates/InvenTree/so_outstanding.html index 29d0261b8b..a1c978264f 100644 --- a/InvenTree/templates/InvenTree/so_outstanding.html +++ b/InvenTree/templates/InvenTree/so_outstanding.html @@ -4,7 +4,7 @@ {% block collapse_title %} <span class='fas fa-sign-out-alt icon-header'></span> -{% trans "Outstanding Sales Orders" %}<span class='badge' id='so-outstanding-count'><span class='fas fa-spin fa-hourglass-half'></span></span> +{% trans "Outstanding Sales Orders" %}<span class='badge' id='so-outstanding-count'><span class='fas fa-spin fa-spinner'></span></span> {% endblock %} {% block collapse_content %} diff --git a/InvenTree/templates/InvenTree/starred_parts.html b/InvenTree/templates/InvenTree/starred_parts.html index a0801566c2..ae8b9ecc1d 100644 --- a/InvenTree/templates/InvenTree/starred_parts.html +++ b/InvenTree/templates/InvenTree/starred_parts.html @@ -4,7 +4,7 @@ {% block collapse_title %} <span class='fas fa-star icon-header'></span> -{% trans "Starred Parts" %}<span class='badge' id='starred-parts-count'><span class='fas fa-spin fa-hourglass-half'></span></span> +{% trans "Starred Parts" %}<span class='badge' id='starred-parts-count'><span class='fas fa-spin fa-spinner'></span></span> {% endblock %} {% block collapse_content %} diff --git a/InvenTree/templates/attachment_table.html b/InvenTree/templates/attachment_table.html index f7d5411290..d13b7b33b1 100644 --- a/InvenTree/templates/attachment_table.html +++ b/InvenTree/templates/attachment_table.html @@ -3,7 +3,9 @@ {% if roles.stock.change %} <div id='attachment-buttons'> <div class='btn-group'> - <button type='button' class='btn btn-success' id='new-attachment'>{% trans "Add Attachment" %}</button> + <button type='button' class='btn btn-success' id='new-attachment'> + <span class='fas fa-plus-circle'></span> {% trans "Add Attachment" %} + </button> </div> </div> {% endif %} diff --git a/InvenTree/templates/js/barcode.html b/InvenTree/templates/js/barcode.js similarity index 100% rename from InvenTree/templates/js/barcode.html rename to InvenTree/templates/js/barcode.js diff --git a/InvenTree/templates/js/bom.html b/InvenTree/templates/js/bom.js similarity index 81% rename from InvenTree/templates/js/bom.html rename to InvenTree/templates/js/bom.js index b804453ca2..c55429faba 100644 --- a/InvenTree/templates/js/bom.html +++ b/InvenTree/templates/js/bom.js @@ -103,6 +103,29 @@ function loadBomTable(table, options) { * BOM data are retrieved from the server via AJAX query */ + var params = { + part: options.parent_id, + ordering: 'name', + } + + if (options.part_detail) { + params.part_detail = true; + } + + params.sub_part_detail = true; + + var filters = {}; + + if (!options.disableFilters) { + filters = loadTableFilters('bom'); + } + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList('bom', $(table)); + // Construct the table columns var cols = []; @@ -127,8 +150,12 @@ function loadBomTable(table, options) { var url = `/part/${row.sub_part}/`; var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, url); + var sub_part = row.sub_part_detail; + + html += makePartIcons(row.sub_part_detail); + // Display an extra icon if this part is an assembly - if (row.sub_part_detail.assembly) { + if (sub_part.assembly) { var text = `<span title='{% trans "Open subassembly" %}' class='fas fa-stream label-right'></span>`; html += renderLink(text, `/part/${row.sub_part}/bom/`); @@ -266,21 +293,6 @@ function loadBomTable(table, options) { }); } - // Configure the table (bootstrap-table) - - var params = { - part: options.parent_id, - ordering: 'name', - } - - if (options.part_detail) { - params.part_detail = true; - } - - if (options.sub_part_detail) { - params.sub_part_detail = true; - } - // Function to request BOM data for sub-items // This function may be called recursively for multi-level BOMs function requestSubItems(bom_pk, part_pk) { @@ -331,9 +343,10 @@ function loadBomTable(table, options) { return {classes: 'rowinvalid'}; } }, - formatNoMatches: function() { return "{% trans "No BOM items found" %}"; }, + formatNoMatches: function() { return '{% trans "No BOM items found" %}'; }, clickToSelect: true, - queryParams: params, + queryParams: filters, + original: params, columns: cols, url: options.bom_url, onPostBody: function() { @@ -427,4 +440,86 @@ function loadBomTable(table, options) { ); }); } -} \ No newline at end of file +} + +function loadUsedInTable(table, options) { + /* Load a table which displays all the parts that the given part is used in. + */ + + var params = { + sub_part: options.part_id, + ordering: 'name', + } + + if (options.part_detail) { + params.part_detail = true; + } + + if (options.sub_part_detail) { + params.sub_part_detail = true; + } + + var filters = {}; + + if (!options.disableFilters) { + filters = loadTableFilters("usedin"); + } + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList("usedin", $(table)); + + // Columns to display in the table + var cols = [ + { + field: 'pk', + title: 'ID', + visible: false, + switchable: false, + }, + { + field: 'part_detail.full_name', + title: '{% trans "Part" %}', + sortable: true, + formatter: function(value, row, index, field) { + var link = `/part/${row.part}/bom/`; + var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, link); + + if (!row.part_detail.active) { + html += "<span class='label label-warning' style='float: right;'>{% trans 'INACTIVE' %}</span>"; + } + + return html; + } + }, + { + field: 'part_detail.description', + title: '{% trans "Description" %}', + sortable: true, + }, + { + sortable: true, + field: 'quantity', + title: '{% trans "Uses" %}', + formatter: function(value, row, index, field) { + return parseFloat(value); + }, + } + ]; + + // Load the table + $(table).inventreeTable({ + url: "{% url 'api-bom-list' %}", + formatNoMatches: function() { + return '{% trans "No matching parts found" %}'; + }, + columns: cols, + showColumns: true, + sortable: true, + serach: true, + queryParams: filters, + original: params, + }); +} diff --git a/InvenTree/templates/js/build.html b/InvenTree/templates/js/build.html deleted file mode 100644 index 1f577802b2..0000000000 --- a/InvenTree/templates/js/build.html +++ /dev/null @@ -1,198 +0,0 @@ -{% load i18n %} -{% load inventree_extras %} - -function loadBuildTable(table, options) { - // Display a table of Build objects - - var params = options.params || {}; - - var filters = {}; - - if (!options.disableFilters) { - loadTableFilters("build"); - } - - for (var key in params) { - filters[key] = params[key]; - } - - setupFilterList("build", table); - - $(table).inventreeTable({ - method: 'get', - formatNoMatches: function() { - return "{% trans "No builds matching query" %}"; - }, - url: options.url, - queryParams: filters, - groupBy: false, - name: 'builds', - original: params, - columns: [ - { - field: 'pk', - title: 'ID', - visible: false, - switchable: false, - }, - { - field: 'reference', - title: '{% trans "Build" %}', - sortable: true, - switchable: false, - formatter: function(value, row, index, field) { - - var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}"; - - if (prefix) { - value = `${prefix}${value}`; - } - - return renderLink(value, '/build/' + row.pk + '/'); - } - }, - { - field: 'title', - title: '{% trans "Description" %}', - sortable: true, - }, - { - field: 'part', - title: '{% trans "Part" %}', - sortable: true, - formatter: function(value, row, index, field) { - - var name = row.part_detail.full_name; - - return imageHoverIcon(row.part_detail.thumbnail) + renderLink(name, '/part/' + row.part + '/'); - } - }, - { - field: 'quantity', - title: '{% trans "Quantity" %}', - sortable: true, - }, - { - field: 'status', - title: '{% trans "Status" %}', - sortable: true, - formatter: function(value, row, index, field) { - return buildStatusDisplay(value); - }, - }, - { - field: 'creation_date', - title: '{% trans "Created" %}', - sortable: true, - }, - { - field: 'completion_date', - title: '{% trans "Completed" %}', - sortable: true, - }, - ], - }); -} - - -function updateAllocationTotal(id, count, required) { - - count = parseFloat(count); - - $('#allocation-total-'+id).html(count); - - var el = $("#allocation-panel-" + id); - el.removeClass('part-allocation-pass part-allocation-underallocated part-allocation-overallocated'); - - if (count < required) { - el.addClass('part-allocation-underallocated'); - } else if (count > required) { - el.addClass('part-allocation-overallocated'); - } else { - el.addClass('part-allocation-pass'); - } -} - -function loadAllocationTable(table, part_id, part, url, required, button) { - - // Load the allocation table - table.bootstrapTable({ - url: url, - sortable: false, - formatNoMatches: function() { return '{% trans "No parts allocated for" %} ' + part; }, - columns: [ - { - field: 'stock_item_detail', - title: '{% trans "Stock Item" %}', - formatter: function(value, row, index, field) { - return '' + parseFloat(value.quantity) + ' x ' + value.part_name + ' @ ' + value.location_name; - } - }, - { - field: 'stock_item_detail.quantity', - title: '{% trans "Available" %}', - formatter: function(value, row, index, field) { - return parseFloat(value); - } - }, - { - field: 'quantity', - title: '{% trans "Allocated" %}', - formatter: function(value, row, index, field) { - var html = parseFloat(value); - - var bEdit = "<button class='btn item-edit-button btn-sm' type='button' title='{% trans "Edit stock allocation" %}' url='/build/item/" + row.pk + "/edit/'><span class='fas fa-edit'></span></button>"; - var bDel = "<button class='btn item-del-button btn-sm' type='button' title='{% trans "Delete stock allocation" %}' url='/build/item/" + row.pk + "/delete/'><span class='fas fa-trash-alt icon-red'></span></button>"; - - html += "<div class='btn-group' style='float: right;'>" + bEdit + bDel + "</div>"; - - return html; - } - } - ], - }); - - // Callback for 'new-item' button - button.click(function() { - launchModalForm(button.attr('url'), { - success: function() { - table.bootstrapTable('refresh'); - }, - }); - }); - - table.on('load-success.bs.table', function(data) { - // Extract table data - var results = table.bootstrapTable('getData'); - - var count = 0; - - for (var i = 0; i < results.length; i++) { - count += parseFloat(results[i].quantity); - } - - updateAllocationTotal(part_id, count, required); - }); - - // Button callbacks for editing and deleting the allocations - table.on('click', '.item-edit-button', function() { - var button = $(this); - - launchModalForm(button.attr('url'), { - success: function() { - table.bootstrapTable('refresh'); - } - }); - }); - - table.on('click', '.item-del-button', function() { - var button = $(this); - - launchModalForm(button.attr('url'), { - success: function() { - table.bootstrapTable('refresh'); - } - }); - }); - -} \ No newline at end of file diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js new file mode 100644 index 0000000000..ab6f0e4e0a --- /dev/null +++ b/InvenTree/templates/js/build.js @@ -0,0 +1,796 @@ +{% load i18n %} +{% load inventree_extras %} + +function newBuildOrder(options={}) { + /* Launch modal form to create a new BuildOrder. + */ + + launchModalForm( + "{% url 'build-create' %}", + { + follow: true, + data: options.data || {}, + callback: [ + { + field: 'part', + action: function(value) { + inventreeGet( + `/api/part/${value}/`, {}, + { + success: function(response) { + + //enableField('serial_numbers', response.trackable); + //clearField('serial_numbers'); + } + } + ); + }, + } + ], + } + ) +} + + +function makeBuildOutputActionButtons(output, buildInfo) { + /* Generate action buttons for a build output. + */ + + var buildId = buildInfo.pk; + var outputId = output.pk; + + var panel = `#allocation-panel-${outputId}`; + + function reloadTable() { + $(panel).find(`#allocation-table-${outputId}`).bootstrapTable('refresh'); + } + + // Find the div where the buttons will be displayed + var buildActions = $(panel).find(`#output-actions-${outputId}`); + + var html = `<div class='btn-group float-right' role='group'>`; + + // Add a button to "auto allocate" against the build + html += makeIconButton( + 'fa-magic icon-blue', 'button-output-auto', outputId, + '{% trans "Auto-allocate stock items to this output" %}', + ); + + // Add a button to "complete" the particular build output + html += makeIconButton( + 'fa-check icon-green', 'button-output-complete', outputId, + '{% trans "Complete build output" %}', + { + //disabled: true + } + ); + + // Add a button to "cancel" the particular build output (unallocate) + html += makeIconButton( + 'fa-minus-circle icon-red', 'button-output-unallocate', outputId, + '{% trans "Unallocate stock from build output" %}', + ); + + // Add a button to "delete" the particular build output + html += makeIconButton( + 'fa-trash-alt icon-red', 'button-output-delete', outputId, + '{% trans "Delete build output" %}', + ); + + // Add a button to "destroy" the particular build output (mark as damaged, scrap) + // TODO + + html += '</div>'; + + buildActions.html(html); + + // Add callbacks for the buttons + $(panel).find(`#button-output-auto-${outputId}`).click(function() { + // Launch modal dialog to perform auto-allocation + launchModalForm(`/build/${buildId}/auto-allocate/`, + { + data: { + output: outputId, + }, + success: reloadTable, + } + ); + }); + + $(panel).find(`#button-output-complete-${outputId}`).click(function() { + launchModalForm( + `/build/${buildId}/complete-output/`, + { + data: { + output: outputId, + }, + reload: true, + } + ); + }); + + $(panel).find(`#button-output-unallocate-${outputId}`).click(function() { + launchModalForm( + `/build/${buildId}/unallocate/`, + { + success: reloadTable, + data: { + output: outputId, + } + } + ); + }); + + $(panel).find(`#button-output-delete-${outputId}`).click(function() { + launchModalForm( + `/build/${buildId}/delete-output/`, + { + reload: true, + data: { + output: outputId + } + } + ); + }); +} + + +function loadBuildOutputAllocationTable(buildInfo, output, options={}) { + /* + * Load the "allocation table" for a particular build output. + * + * Args: + * - buildId: The PK of the Build object + * - partId: The PK of the Part object + * - output: The StockItem object which is the "output" of the build + * - options: + * -- table: The #id of the table (will be auto-calculated if not provided) + */ + + var buildId = buildInfo.pk; + var partId = buildInfo.part; + + var outputId = null; + + outputId = output.pk; + + var table = options.table; + + if (options.table == null) { + table = `#allocation-table-${outputId}`; + } + + function reloadTable() { + // Reload the entire build allocation table + $(table).bootstrapTable('refresh'); + } + + function requiredQuantity(row) { + // Return the requied quantity for a given row + + return row.quantity * output.quantity; + } + + function sumAllocations(row) { + // Calculat total allocations for a given row + if (!row.allocations) { + return 0; + } + + var quantity = 0; + + row.allocations.forEach(function(item) { + quantity += item.quantity; + }); + + return quantity; + } + + function setupCallbacks() { + // Register button callbacks once table data are loaded + + // Callback for 'allocate' button + $(table).find(".button-add").click(function() { + + // Primary key of the 'sub_part' + var pk = $(this).attr('pk'); + + // Launch form to allocate new stock against this output + launchModalForm("{% url 'build-item-create' %}", { + success: reloadTable, + data: { + part: pk, + build: buildId, + install_into: outputId, + }, + secondary: [ + { + field: 'stock_item', + label: '{% trans "New Stock Item" %}', + title: '{% trans "Create new Stock Item" %}', + url: '{% url "stock-item-create" %}', + data: { + part: pk, + }, + }, + ], + callback: [ + { + field: 'stock_item', + action: function(value) { + inventreeGet( + `/api/stock/${value}/`, {}, + { + success: function(response) { + + // How many items are actually available for the given stock item? + var available = response.quantity - response.allocated; + + var field = getFieldByName('#modal-form', 'quantity'); + + // Allocation quantity initial value + var initial = field.attr('value'); + + if (available < initial) { + field.val(available); + } + } + } + ) + } + } + ] + }); + }); + + // Callback for 'build' button + $(table).find('.button-build').click(function() { + var pk = $(this).attr('pk'); + + // Extract row data from the table + var idx = $(this).closest('tr').attr('data-index'); + var row = $(table).bootstrapTable('getData')[idx]; + + // Launch form to create a new build order + launchModalForm('{% url "build-create" %}', { + follow: true, + data: { + part: pk, + parent: buildId, + quantity: requiredQuantity(row) - sumAllocations(row), + } + }); + }); + + // Callback for 'unallocate' button + $(table).find('.button-unallocate').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm(`/build/${buildId}/unallocate/`, + { + success: reloadTable, + data: { + output: outputId, + part: pk, + } + } + ); + }); + } + + // Load table of BOM items + $(table).inventreeTable({ + url: "{% url 'api-bom-list' %}", + queryParams: { + part: partId, + sub_part_detail: true, + }, + formatNoMatches: function() { + return '{% trans "No BOM items found" %}'; + }, + name: 'build-allocation', + uniqueId: 'sub_part', + onPostBody: setupCallbacks, + onLoadSuccess: function(tableData) { + // Once the BOM data are loaded, request allocation data for this build output + + inventreeGet('/api/build/item/', + { + build: buildId, + output: outputId, + }, + { + success: function(data) { + // Iterate through the returned data, and group by the part they point to + var allocations = {}; + + // Total number of line items + var totalLines = tableData.length; + + // Total number of "completely allocated" lines + var allocatedLines = 0; + + data.forEach(function(item) { + + // Group BuildItem objects by part + var part = item.part; + var key = parseInt(part); + + if (!(key in allocations)) { + allocations[key] = new Array(); + } + + allocations[key].push(item); + }); + + // Now update the allocations for each row in the table + for (var key in allocations) { + + // Select the associated row in the table + var tableRow = $(table).bootstrapTable('getRowByUniqueId', key); + + if (!tableRow) { + continue; + } + + // Set the allocation list for that row + tableRow.allocations = allocations[key]; + + // Calculate the total allocated quantity + var allocatedQuantity = sumAllocations(tableRow); + + // Is this line item fully allocated? + if (allocatedQuantity >= (tableRow.quantity * output.quantity)) { + allocatedLines += 1; + } + + // Push the updated row back into the main table + $(table).bootstrapTable('updateByUniqueId', key, tableRow, true); + } + + // Update the total progress for this build output + var buildProgress = $(`#allocation-panel-${outputId}`).find($(`#output-progress-${outputId}`)); + + var progress = makeProgressBar( + allocatedLines, + totalLines + ); + + buildProgress.html(progress); + + // Update the available actions for this build output + + makeBuildOutputActionButtons(output, buildInfo); + } + } + ); + }, + sortable: true, + showColumns: false, + detailViewByClick: true, + detailView: true, + detailFilter: function(index, row) { + return row.allocations != null; + }, + detailFormatter: function(index, row, element) { + // Contruct an 'inner table' which shows which stock items have been allocated + + var subTableId = `allocation-table-${row.pk}`; + + var html = `<div class='sub-table'><table class='table table-condensed table-striped' id='${subTableId}'></table></div>`; + + element.html(html); + + var lineItem = row; + + var subTable = $(`#${subTableId}`); + + subTable.bootstrapTable({ + data: row.allocations, + showHeader: true, + columns: [ + { + width: '50%', + field: 'quantity', + title: '{% trans "Assigned Stock" %}', + formatter: function(value, row, index, field) { + var text = ''; + + var url = ''; + + if (row.serial && row.quantity == 1) { + text = `{% trans "Serial Number" %}: ${row.serial}`; + } else { + text = `{% trans "Quantity" %}: ${row.quantity}`; + } + + {% if build.status == BuildStatus.COMPLETE %} + url = `/stock/item/${row.pk}/`; + {% else %} + url = `/stock/item/${row.stock_item}/`; + {% endif %} + + return renderLink(text, url); + } + }, + { + field: 'location', + title: '{% trans "Location" %}', + formatter: function(value, row, index, field) { + if (row.stock_item_detail.location) { + var text = row.stock_item_detail.location_name; + var url = `/stock/location/${row.stock_item_detail.location}/`; + + return renderLink(text, url); + } else { + return '<i>{% trans "No location set" %}</i>'; + } + } + }, + { + field: 'actions', + formatter: function(value, row, index, field) { + /* Actions available for a particular stock item allocation: + * + * - Edit the allocation quantity + * - Delete the allocation + */ + + var pk = row.pk; + + var html = `<div class='btn-group float-right' role='group'>`; + + html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); + + html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); + + html += `</div>`; + + return html; + } + } + ] + }); + + // Assign button callbacks to the newly created allocation buttons + subTable.find('.button-allocation-edit').click(function() { + var pk = $(this).attr('pk'); + launchModalForm(`/build/item/${pk}/edit/`, { + success: reloadTable, + }); + }); + + subTable.find('.button-allocation-delete').click(function() { + var pk = $(this).attr('pk'); + launchModalForm(`/build/item/${pk}/delete/`, { + success: reloadTable, + }); + }); + }, + columns: [ + { + field: 'pk', + visible: false, + }, + { + field: 'sub_part_detail.full_name', + title: '{% trans "Required Part" %}', + sortable: true, + formatter: function(value, row, index, field) { + var url = `/part/${row.sub_part}/`; + var thumb = row.sub_part_detail.thumbnail; + var name = row.sub_part_detail.full_name; + + var html = imageHoverIcon(thumb) + renderLink(name, url); + + html += makePartIcons(row.sub_part_detail); + + return html; + } + }, + { + field: 'reference', + title: '{% trans "Reference" %}', + sortable: true, + }, + { + field: 'quantity', + title: '{% trans "Quantity Per" %}', + sortable: true, + }, + { + field: 'sub_part_detail.stock', + title: '{% trans "Available" %}', + }, + { + field: 'allocated', + title: '{% trans "Allocated" %}', + sortable: true, + formatter: function(value, row, index, field) { + var allocated = 0; + + if (row.allocations) { + row.allocations.forEach(function(item) { + allocated += item.quantity; + }); + } + + var required = requiredQuantity(row); + + return makeProgressBar(allocated, required); + }, + sorter: function(valA, valB, rowA, rowB) { + var aA = sumAllocations(rowA); + var aB = sumAllocations(rowB); + + var qA = rowA.quantity; + var qB = rowB.quantity; + + qA *= output.quantity; + qB *= output.quantity; + + // Handle the case where both numerators are zero + if ((aA == 0) && (aB == 0)) { + return (qA > qB) ? 1 : -1; + } + + // Handle the case where either denominator is zero + if ((qA == 0) || (qB == 0)) { + return 1; + } + + var progressA = parseFloat(aA) / qA; + var progressB = parseFloat(aB) / qB; + + // Handle the case where both ratios are equal + if (progressA == progressB) { + return (qA < qB) ? 1 : -1; + } + + return (progressA < progressB) ? 1 : -1; + } + }, + { + field: 'actions', + title: '{% trans "Actions" %}', + formatter: function(value, row, index, field) { + // Generate action buttons for this build output + var html = `<div class='btn-group float-right' role='group'>`; + + if (sumAllocations(row) < requiredQuantity(row)) { + if (row.sub_part_detail.assembly) { + html += makeIconButton('fa-tools icon-blue', 'button-build', row.sub_part, '{% trans "Build stock" %}'); + } + + if (row.sub_part_detail.purchaseable) { + html += makeIconButton('fa-shopping-cart icon-blue', 'button-buy', row.sub_part, '{% trans "Order stock" %}', {disabled: true}); + } + + html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', row.sub_part, '{% trans "Allocate stock" %}'); + } + + html += makeIconButton( + 'fa-minus-circle icon-red', 'button-unallocate', row.sub_part, + '{% trans "Unallocate stock" %}', + { + disabled: row.allocations == null + } + ); + + html += '</div>'; + + return html; + } + }, + ] + }); +} + + +function loadBuildTable(table, options) { + // Display a table of Build objects + + var params = options.params || {}; + + var filters = {}; + + if (!options.disableFilters) { + filters = loadTableFilters("build"); + } + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList("build", table); + + $(table).inventreeTable({ + method: 'get', + formatNoMatches: function() { + return '{% trans "No builds matching query" %}'; + }, + url: options.url, + queryParams: filters, + groupBy: false, + name: 'builds', + original: params, + columns: [ + { + field: 'pk', + title: 'ID', + visible: false, + switchable: false, + }, + { + field: 'reference', + title: '{% trans "Build" %}', + sortable: true, + switchable: false, + formatter: function(value, row, index, field) { + + var prefix = "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}"; + + if (prefix) { + value = `${prefix}${value}`; + } + + return renderLink(value, '/build/' + row.pk + '/'); + } + }, + { + field: 'title', + title: '{% trans "Description" %}', + sortable: true, + }, + { + field: 'part', + title: '{% trans "Part" %}', + sortable: true, + formatter: function(value, row, index, field) { + + var html = imageHoverIcon(row.part_detail.thumbnail); + + html += renderLink(row.part_detail.full_name, `/part/${row.part}/`); + html += makePartIcons(row.part_detail); + + return html; + } + }, + { + field: 'quantity', + title: '{% trans "Completed" %}', + sortable: true, + formatter: function(value, row, index, field) { + return makeProgressBar( + row.completed, + row.quantity, + { + //style: 'max', + } + ); + } + }, + { + field: 'status', + title: '{% trans "Status" %}', + sortable: true, + formatter: function(value, row, index, field) { + return buildStatusDisplay(value); + }, + }, + { + field: 'creation_date', + title: '{% trans "Created" %}', + sortable: true, + }, + { + field: 'completion_date', + title: '{% trans "Completed" %}', + sortable: true, + }, + ], + }); +} + + +function updateAllocationTotal(id, count, required) { + + count = parseFloat(count); + + $('#allocation-total-'+id).html(count); + + var el = $("#allocation-panel-" + id); + el.removeClass('part-allocation-pass part-allocation-underallocated part-allocation-overallocated'); + + if (count < required) { + el.addClass('part-allocation-underallocated'); + } else if (count > required) { + el.addClass('part-allocation-overallocated'); + } else { + el.addClass('part-allocation-pass'); + } +} + +function loadAllocationTable(table, part_id, part, url, required, button) { + + // Load the allocation table + table.bootstrapTable({ + url: url, + sortable: false, + formatNoMatches: function() { return '{% trans "No parts allocated for" %} ' + part; }, + columns: [ + { + field: 'stock_item_detail', + title: '{% trans "Stock Item" %}', + formatter: function(value, row, index, field) { + return '' + parseFloat(value.quantity) + ' x ' + value.part_name + ' @ ' + value.location_name; + } + }, + { + field: 'stock_item_detail.quantity', + title: '{% trans "Available" %}', + formatter: function(value, row, index, field) { + return parseFloat(value); + } + }, + { + field: 'quantity', + title: '{% trans "Allocated" %}', + formatter: function(value, row, index, field) { + var html = parseFloat(value); + + var bEdit = "<button class='btn item-edit-button btn-sm' type='button' title='{% trans "Edit stock allocation" %}' url='/build/item/" + row.pk + "/edit/'><span class='fas fa-edit'></span></button>"; + var bDel = "<button class='btn item-del-button btn-sm' type='button' title='{% trans "Delete stock allocation" %}' url='/build/item/" + row.pk + "/delete/'><span class='fas fa-trash-alt icon-red'></span></button>"; + + html += "<div class='btn-group' style='float: right;'>" + bEdit + bDel + "</div>"; + + return html; + } + } + ], + }); + + // Callback for 'new-item' button + button.click(function() { + launchModalForm(button.attr('url'), { + success: function() { + table.bootstrapTable('refresh'); + }, + }); + }); + + table.on('load-success.bs.table', function(data) { + // Extract table data + var results = table.bootstrapTable('getData'); + + var count = 0; + + for (var i = 0; i < results.length; i++) { + count += parseFloat(results[i].quantity); + } + + updateAllocationTotal(part_id, count, required); + }); + + // Button callbacks for editing and deleting the allocations + table.on('click', '.item-edit-button', function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + success: function() { + table.bootstrapTable('refresh'); + } + }); + }); + + table.on('click', '.item-del-button', function() { + var button = $(this); + + launchModalForm(button.attr('url'), { + success: function() { + table.bootstrapTable('refresh'); + } + }); + }); + +} \ No newline at end of file diff --git a/InvenTree/templates/js/company.html b/InvenTree/templates/js/company.js similarity index 98% rename from InvenTree/templates/js/company.html rename to InvenTree/templates/js/company.js index f904c0f506..164329aee4 100644 --- a/InvenTree/templates/js/company.html +++ b/InvenTree/templates/js/company.js @@ -110,7 +110,7 @@ function loadSupplierPartTable(table, url, options) { // Query parameters var params = options.params || {}; - // Load 'user' filters + // Load filters var filters = loadTableFilters("supplier-part"); for (var key in params) { @@ -122,6 +122,7 @@ function loadSupplierPartTable(table, url, options) { $(table).inventreeTable({ url: url, method: 'get', + original: params, queryParams: filters, name: 'supplierparts', groupBy: false, @@ -135,6 +136,7 @@ function loadSupplierPartTable(table, url, options) { sortable: true, field: 'part_detail.full_name', title: '{% trans "Part" %}', + switchable: false, formatter: function(value, row, index, field) { var url = `/part/${row.part}/`; diff --git a/InvenTree/templates/js/order.html b/InvenTree/templates/js/order.js similarity index 100% rename from InvenTree/templates/js/order.html rename to InvenTree/templates/js/order.js diff --git a/InvenTree/templates/js/part.html b/InvenTree/templates/js/part.js similarity index 86% rename from InvenTree/templates/js/part.html rename to InvenTree/templates/js/part.js index e5fafef070..8c00625dfc 100644 --- a/InvenTree/templates/js/part.html +++ b/InvenTree/templates/js/part.js @@ -61,13 +61,61 @@ function toggleStar(options) { } -function loadPartVariantTable(table, partId, options) { +function makePartIcons(part, options={}) { + /* Render a set of icons for the given part. + */ + + var html = ''; + + if (part.trackable) { + html += makeIconBadge('fa-directions', '{% trans "Trackable part" %}'); + } + + if (part.virtual) { + html += makeIconBadge('fa-ghost', '{% trans "Virtual part" %}'); + } + + if (part.is_template) { + html += makeIconBadge('fa-clone', '{% trans "Template part" %}'); + } + + if (part.assembly) { + html += makeIconBadge('fa-tools', '{% trans "Assembled part" %}'); + } + + if (part.starred) { + html += makeIconBadge('fa-star', '{% trans "Starred part" %}'); + } + + if (part.salable) { + html += makeIconBadge('fa-dollar-sign', title='{% trans "Salable part" %}'); + } + + if (!part.active) { + html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`; + } + + return html; + +} + + +function loadPartVariantTable(table, partId, options={}) { /* Load part variant table */ - var params = { - ancestor: partId, - }; + var params = options.params || {}; + + params.ancestor = partId; + + // Load filters + var filters = loadTableFilters("variants"); + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList("variants", $(table)); var cols = [ { @@ -104,16 +152,36 @@ function loadPartVariantTable(table, partId, options) { html += imageHoverIcon(row.thumbnail); html += renderLink(name, `/part/${row.pk}/`); + if (row.trackable) { + html += makeIconBadge('fa-directions', '{% trans "Trackable part" %}'); + } + + if (row.virtual) { + html += makeIconBadge('fa-ghost', '{% trans "Virtual part" %}'); + } + + if (row.is_template) { + html += makeIconBadge('fa-clone', '{% trans "Template part" %}'); + } + + if (row.assembly) { + html += makeIconBadge('fa-tools', '{% trans "Assembled part" %}'); + } + + if (!row.active) { + html += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`; + } + return html; }, }, { field: 'IPN', - title: '{% trans 'IPN' %}', + title: '{% trans "IPN" %}', }, { field: 'revision', - title: '{% trans 'Revision' %}', + title: '{% trans "Revision" %}', }, { field: 'description', @@ -133,7 +201,7 @@ function loadPartVariantTable(table, partId, options) { name: 'partvariants', showColumns: true, original: params, - queryParams: params, + queryParams: filters, formatNoMatches: function() { return "{% trans "No variants found" %}"; }, columns: cols, treeEnable: true, @@ -272,7 +340,7 @@ function loadPartTable(table, url, options={}) { if (options.checkbox) { columns.push({ checkbox: true, - title: '{% trans 'Select' %}', + title: '{% trans "Select" %}', searchable: false, switchable: false, }); @@ -286,8 +354,9 @@ function loadPartTable(table, url, options={}) { columns.push({ field: 'name', - title: '{% trans 'Part' %}', + title: '{% trans "Part" %}', sortable: true, + switchable: false, formatter: function(value, row, index, field) { var name = ''; @@ -310,31 +379,8 @@ function loadPartTable(table, url, options={}) { var display = imageHoverIcon(row.thumbnail) + renderLink(name, '/part/' + row.pk + '/'); - if (row.is_template) { - display += `<span class='fas fa-clone label-right' title='{% trans "Template part" %}'></span>`; - } - - if (row.assembly) { - display += `<span class='fas fa-tools label-right' title='{% trans "Assembled part" %}'></span>`; - } + display += makePartIcons(row); - if (row.starred) { - display += `<span class='fas fa-star label-right' title='{% trans "Starred part" %}'></span>`; - } - - if (row.salable) { - display += `<span class='fas fa-dollar-sign label-right' title='{% trans "Salable part" %}'></span>`; - } - - /* - if (row.component) { - display = display + `<span class='fas fa-cogs label-right' title='Component part'></span>`; - } - */ - - if (!row.active) { - display += `<span class='label label-warning label-right'>{% trans "Inactive" %}</span>`; - } return display; } }); @@ -342,7 +388,7 @@ function loadPartTable(table, url, options={}) { columns.push({ sortable: true, field: 'description', - title: '{% trans 'Description' %}', + title: '{% trans "Description" %}', formatter: function(value, row, index, field) { if (row.is_template) { @@ -356,7 +402,7 @@ function loadPartTable(table, url, options={}) { columns.push({ sortable: true, field: 'category_detail', - title: '{% trans 'Category' %}', + title: '{% trans "Category" %}', formatter: function(value, row, index, field) { if (row.category) { return renderLink(value.pathstring, "/part/category/" + row.category + "/"); diff --git a/InvenTree/templates/js/stock.html b/InvenTree/templates/js/stock.js similarity index 89% rename from InvenTree/templates/js/stock.html rename to InvenTree/templates/js/stock.js index c4209733ef..e59bf5d469 100644 --- a/InvenTree/templates/js/stock.html +++ b/InvenTree/templates/js/stock.js @@ -257,6 +257,56 @@ function loadStockTable(table, options) { filters[key] = params[key]; } + function locationDetail(row) { + /* + * Function to display a "location" of a StockItem. + * + * Complicating factors: A StockItem may not actually *be* in a location! + * - Could be at a customer + * - Could be installed in another stock item + * - Could be assigned to a sales order + * - Could be currently in production! + * + * So, instead of being naive, we'll check! + */ + + // Display text + var text = ''; + + // URL (optional) + var url = ''; + + if (row.is_building && row.build) { + // StockItem is currently being built! + text = "{% trans "In production" %}"; + url = `/build/${row.build}/`; + } else if (row.belongs_to) { + // StockItem is installed inside a different StockItem + text = `{% trans "Installed in Stock Item" %} ${row.belongs_to}`; + url = `/stock/item/${row.belongs_to}/installed/`; + } else if (row.customer) { + // StockItem has been assigned to a customer + text = "{% trans "Shipped to customer" %}"; + url = `/company/${row.customer}/assigned-stock/`; + } else if (row.sales_order) { + // StockItem has been assigned to a sales order + text = "{% trans "Assigned to Sales Order" %}"; + url = `/order/sales-order/${row.sales_order}/`; + } else if (row.location) { + text = row.location_detail.pathstring; + url = `/stock/location/${row.location}/`; + } else { + text = "<i>{% trans "No stock location set" %}</i>"; + url = ''; + } + + if (url) { + return renderLink(text, url); + } else { + return text; + } + } + table.inventreeTable({ method: 'get', formatNoMatches: function() { @@ -274,11 +324,16 @@ function loadStockTable(table, options) { var row = data[0]; - if (field == 'part_detail.name') { + if (field == 'part_detail.full_name') { - var name = row.part_detail.full_name; + var html = imageHoverIcon(row.part_detail.thumbnail); - return imageHoverIcon(row.part_detail.thumbnail) + name + ' <i>(' + data.length + ' items)</i>'; + html += row.part_detail.full_name; + html += ` <i>(${data.length} items)</i>`; + + html += makePartIcons(row.part_detail); + + return html; } else if (field == 'part_detail.IPN') { return row.part_detail.IPN; @@ -353,28 +408,20 @@ function loadStockTable(table, options) { data.forEach(function(item) { - var loc = null; + var detail = locationDetail(item); - if (item.location_detail) { - loc = item.location_detail.pathstring; - } else { - loc = "{% trans "Undefined location" %}"; - } - - if (!locations.includes(loc)) { - locations.push(loc); + if (!locations.includes(detail)) { + locations.push(detail); } }); - if (locations.length > 1) { + if (locations.length == 1) { + // Single location, easy! + return locations[0]; + } else if (locations.length > 1) { return "In " + locations.length + " locations"; } else { - // A single location! - if (row.location_detail) { - return renderLink(row.location_detail.pathstring, `/stock/location/${row.location}/`); - } else { - return "<i>{% trans "Undefined location" %}</i>"; - } + return "<i>{% trans "Undefined location" %}</i>"; } } else if (field == 'notes') { var notes = []; @@ -417,7 +464,7 @@ function loadStockTable(table, options) { switchable: false, }, { - field: 'part_detail.name', + field: 'part_detail.full_name', title: '{% trans "Part" %}', sortable: true, switchable: false, @@ -429,6 +476,8 @@ function loadStockTable(table, options) { html = imageHoverIcon(thumb) + renderLink(name, url); + html += makePartIcons(row.part_detail); + return html; } }, @@ -465,33 +514,35 @@ function loadStockTable(table, options) { var html = renderLink(val, `/stock/item/${row.pk}/`); - if (row.allocated) { - html += `<span class='fas fa-bookmark label-right' title='{% trans "Stock item has been allocated" %}'></span>`; + if (row.is_building) { + html += makeIconBadge('fa-tools', '{% trans "Stock item is in production" %}'); + } + + if (row.sales_order) { + // Stock item has been assigned to a sales order + html += makeIconBadge('fa-truck', '{% trans "Stock item assigned to sales order" %}'); + } else if (row.customer) { + // StockItem has been assigned to a customer + html += makeIconBadge('fa-user', '{% trans "Stock item assigned to customer" %}'); } - if (row.customer) { - html += `<span class='fas fa-user-tie label-right' title='{% trans "Stock item has been assigned to customer" %}'></span>`; - } else { - if (row.build_order) { - html += `<span class='fas fa-tools label-right' title='{% trans "Stock item was assigned to a build order" %}'></span>`; - } else if (row.sales_order) { - html += `<span class='fas fa-dollar-sign label-right' title='{% trans "Stock item was assigned to a sales order" %}'></span>`; - } + if (row.allocated) { + html += makeIconBadge('fa-bookmark', '{% trans "Stock item has been allocated" %}'); } if (row.belongs_to) { - html += `<span class='fas fa-box label-right' title='{% trans "Stock item has been installed in another item" %}'></span>`; + html += makeIconBadge('fa-box', '{% trans "Stock item has been installed in another item" %}'); } // Special stock status codes // 65 = "REJECTED" if (row.status == 65) { - html += `<span class='fas fa-times-circle label-right' title='{% trans "Stock item has been rejected" %}'></span>`; + html += makeIconButton('fa-times-circle icon-red', '{% trans "Stock item has been rejected" %}'); } // 70 = "LOST" else if (row.status == 70) { - html += `<span class='fas fa-question-circle label-right' title='{% trans "Stock item is lost" %}'></span>`; + html += makeIconButton('fa-question-circle','{% trans "Stock item is lost" %}'); } if (row.quantity <= 0) { @@ -519,24 +570,7 @@ function loadStockTable(table, options) { title: '{% trans "Location" %}', sortable: true, formatter: function(value, row, index, field) { - if (row.belongs_to) { - var text = "{% trans 'Installed in Stock Item ' %}" + row.belongs_to; - var url = `/stock/item/${row.belongs_to}/installed/`; - - return renderLink(text, url); - } else if (row.customer) { - var text = "{% trans "Shipped to customer" %}"; - return renderLink(text, `/company/${row.customer}/assigned-stock/`); - } else if (row.sales_order) { - var text = `{% trans "Assigned to sales order" %}`; - return renderLink(text, `/order/sales-order/${row.sales_order}/`); - } - else if (value) { - return renderLink(value, `/stock/location/${row.location}/`); - } - else { - return '<i>{% trans "No stock location set" %}</i>'; - } + return locationDetail(row); } }, { @@ -769,6 +803,7 @@ function createNewStockItem(options) { field: 'part', action: function(value) { + // Reload options for supplier part reloadFieldOptions( 'supplier_part', { @@ -782,6 +817,18 @@ function createNewStockItem(options) { } } ); + + // Disable serial number field if the part is not trackable + inventreeGet( + `/api/part/${value}/`, {}, + { + success: function(response) { + + enableField('serial_numbers', response.trackable); + clearField('serial_numbers'); + } + } + ); } }, ]; @@ -868,7 +915,7 @@ function loadInstalledInTable(table, options) { url: "{% url 'api-bom-list' %}", queryParams: { part: options.part, - trackable: true, + sub_part_trackable: true, sub_part_detail: true, }, showColumns: false, diff --git a/InvenTree/templates/js/table_filters.html b/InvenTree/templates/js/table_filters.js similarity index 71% rename from InvenTree/templates/js/table_filters.html rename to InvenTree/templates/js/table_filters.js index a97d358828..153411d4e9 100644 --- a/InvenTree/templates/js/table_filters.html +++ b/InvenTree/templates/js/table_filters.js @@ -11,6 +11,52 @@ function getAvailableTableFilters(tableKey) { tableKey = tableKey.toLowerCase(); + // Filters for "variant" table + if (tableKey == "variants") { + return { + active: { + type: 'bool', + title: '{% trans "Active" %}', + }, + template: { + type: 'bool', + title: '{% trans "Template" %}', + }, + virtual: { + type: 'bool', + title: '{% trans "Virtual" %}', + }, + trackable: { + type: 'bool', + title: '{% trans "Trackable" %}', + }, + }; + } + + // Filters for Bill of Materials table + if (tableKey == "bom") { + return { + sub_part_trackable: { + type: 'bool', + title: '{% trans "Trackable Part" %}' + }, + validated: { + type: 'bool', + title: '{% trans "Validated" %}', + }, + }; + } + + // Filters for the "used in" table + if (tableKey == 'usedin') { + return { + 'part_active': { + type: 'bool', + title: '{% trans "Active" %}', + }, + }; + } + // Filters for the "customer stock" table (really a subset of "stock") if (tableKey == "customerstock") { return { @@ -19,16 +65,16 @@ function getAvailableTableFilters(tableKey) { title: '{% trans "Is Serialized" %}', }, serial_gte: { - title: "{% trans "Serial number GTE" %}", - description: "{% trans "Serial number greater than or equal to" %}" + title: '{% trans "Serial number GTE" %}', + description: '{% trans "Serial number greater than or equal to" %}' }, serial_lte: { - title: "{% trans "Serial number LTE" %}", - description: "{% trans "Serial number less than or equal to" %}", + title: '{% trans "Serial number LTE" %}', + description: '{% trans "Serial number less than or equal to" %}', }, serial: { - title: "{% trans "Serial number" %}", - description: "{% trans "Serial number" %}" + title: '{% trans "Serial number" %}', + description: '{% trans "Serial number" %}' }, batch: { title: '{% trans "Batch" %}', @@ -65,6 +111,11 @@ function getAvailableTableFilters(tableKey) { title: '{% trans "In Stock" %}', description: '{% trans "Show items which are in stock" %}', }, + is_building: { + type: 'bool', + title: '{% trans "In Production" %}', + description: '{% trans "Show items which are in production" %}', + }, installed: { type: 'bool', title: '{% trans "Installed" %}', @@ -80,16 +131,16 @@ function getAvailableTableFilters(tableKey) { title: '{% trans "Is Serialized" %}', }, serial: { - title: "{% trans "Serial number" %}", - description: "{% trans "Serial number" %}" + title: '{% trans "Serial number" %}', + description: '{% trans "Serial number" %}' }, serial_gte: { - title: "{% trans "Serial number GTE" %}", - description: "{% trans "Serial number greater than or equal to" %}" + title: '{% trans "Serial number GTE" %}', + description: '{% trans "Serial number greater than or equal to" %}' }, serial_lte: { - title: "{% trans "Serial number LTE" %}", - description: "{% trans "Serial number less than or equal to" %}", + title: '{% trans "Serial number LTE" %}', + description: '{% trans "Serial number less than or equal to" %}', }, status: { options: stockCodes, @@ -108,7 +159,7 @@ function getAvailableTableFilters(tableKey) { return { result: { type: 'bool', - title: "{% trans 'Test result' %}", + title: '{% trans "Test result" %}', }, }; } @@ -118,7 +169,7 @@ function getAvailableTableFilters(tableKey) { return { required: { type: 'bool', - title: "{% trans "Required" %}", + title: '{% trans "Required" %}', } }; } @@ -130,9 +181,9 @@ function getAvailableTableFilters(tableKey) { title: '{% trans "Build status" %}', options: buildCodes, }, - pending: { + active: { type: 'bool', - title: '{% trans "Pending" %}', + title: '{% trans "Active" %}', } }; } @@ -165,6 +216,15 @@ function getAvailableTableFilters(tableKey) { }; } + if (tableKey == 'supplier-part') { + return { + active: { + type: 'bool', + title: '{% trans "Active parts" %}', + } + }; + } + // Filters for the "Parts" table if (tableKey == "parts") { return { diff --git a/InvenTree/templates/modals.html b/InvenTree/templates/modals.html index 9423b14569..a50cb65369 100644 --- a/InvenTree/templates/modals.html +++ b/InvenTree/templates/modals.html @@ -1,3 +1,5 @@ +{% load i18n %} + <div class='modal fade modal-fixed-footer modal-primary' tabindex='-1' role='dialog' id='modal-form'> <div class='modal-dialog'> <div class='modal-content'> @@ -5,13 +7,16 @@ <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> - <h3 id='modal-title'>Form Title Here</h3> + <h3 id='modal-title'><i>Form Title Here</i></h3> + </div> + <div class='alert alert-block alert-danger' id='form-validation-warning' style='display: none;'> + {% trans "Form errors exist" %} </div> <div class='modal-form-content'> </div> <div class='modal-footer'> - <button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>Close</button> - <button type='button' class='btn btn-primary' id='modal-form-submit'>Submit</button> + <button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Close" %}</button> + <button type='button' class='btn btn-primary' id='modal-form-submit'>{% trans "Submit" %}</button> </div> </div> </div> @@ -24,13 +29,16 @@ <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> - <h3 id='modal-title'>Form Title Here</h3> + <h3 id='modal-title'><i>Form Title Here</i></h3> + </div> + <div class='alert alert-block alert-danger' id='form-validation-warning' style="display: none;"> + {% trans "Form errors exist" %} </div> <div class='modal-form-content'> </div> <div class='modal-footer'> - <button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>Close</button> - <button type='button' class='btn btn-primary' id='modal-form-submit'>Submit</button> + <button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Close" %}</button> + <button type='button' class='btn btn-primary' id='modal-form-submit'>{% trans "Submit" %}</button> </div> </div> </div> diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html index b4b1b9d50f..51f7c277db 100644 --- a/InvenTree/templates/stock_table.html +++ b/InvenTree/templates/stock_table.html @@ -3,25 +3,29 @@ <div id='button-toolbar'> <div class='button-toolbar container-fluid' style='float: right;'> <div class='btn-group'> - <button class='btn btn-default' id='stock-export' title='{% trans "Export Stock Information" %}'>{% trans "Export" %}</button> + <button class='btn btn-default' id='stock-export' title='{% trans "Export Stock Information" %}'> + <span class='fas fa-file-download'></span> {% trans "Export" %} + </button> {% if read_only %} {% else %} {% if roles.stock.add %} - <button class="btn btn-success" id='item-create'>{% trans "New Stock Item" %}</button> + <button class="btn btn-success" id='item-create'> + <span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %} + </button> {% endif %} {% if roles.stock.change or roles.stock.delete %} <div class="btn-group"> <button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">{% trans "Options" %}<span class="caret"></span></button> <ul class="dropdown-menu"> {% if roles.stock.change %} - <li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'>{% trans "Add stock" %}</a></li> - <li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'>{% trans "Remove stock" %}</a></li> - <li><a href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'>{% trans "Count stock" %}</a></li> - <li><a href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'>{% trans "Move stock" %}</a></li> - <li><a href='#' id='multi-item-order' title='{% trans "Order selected items" %}'>{% trans "Order stock" %}</a></li> + <li><a href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle'></span> {% trans "Add stock" %}</a></li> + <li><a href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle'></span> {% trans "Remove stock" %}</a></li> + <li><a href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle'></span> {% trans "Count stock" %}</a></li> + <li><a href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt'></span> {% trans "Move stock" %}</a></li> + <li><a href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li> {% endif %} {% if roles.stock.delete %} - <li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'>{% trans "Delete Stock" %}</a></li> + <li><a href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt'></span> {% trans "Delete Stock" %}</a></li> {% endif %} </ul> </div> @@ -35,4 +39,4 @@ </div> <table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='stock-table'> -</table> \ No newline at end of file +</table> diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py index e4974a3f7a..4614e5bc4c 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -3,12 +3,13 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ -from django.contrib import admin +from django.contrib import admin, messages from django import forms from django.contrib.auth import get_user_model from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth.models import Group from django.contrib.auth.admin import UserAdmin +from django.utils.safestring import mark_safe from users.models import RuleSet @@ -94,15 +95,37 @@ class RoleGroupAdmin(admin.ModelAdmin): filter_horizontal = ['permissions'] - # Save inlines before model - # https://stackoverflow.com/a/14860703/12794913 def save_model(self, request, obj, form, change): - pass # don't actually save the parent instance + """ + This method serves two purposes: + - show warning message whenever the group users belong to multiple groups + - skip saving of the group instance model as inlines needs to be saved before. + """ + + # Get form cleaned data + users = form.cleaned_data['users'] + + # Check for users who are members of multiple groups + warning_message = '' + for user in users: + if user.groups.all().count() > 1: + warning_message += f'<br>- <b>{user.username}</b> is member of: ' + for idx, group in enumerate(user.groups.all()): + warning_message += f'<b>{group.name}</b>' + if idx < len(user.groups.all()) - 1: + warning_message += ', ' + + # If any, display warning message when group is saved + if warning_message: + warning_message = mark_safe(_(f'The following users are members of multiple groups:' + f'{warning_message}')) + messages.add_message(request, messages.WARNING, warning_message) def save_formset(self, request, form, formset, change): - formset.save() # this will save the children - # update_fields is required to trigger permissions update - form.instance.save(update_fields=['name']) # form.instance is the parent + # Save inline Rulesets + formset.save() + # Save Group instance and update permissions + form.instance.save(update_fields=['name']) class InvenTreeUserAdmin(UserAdmin): diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index d3c713d07d..9dd117f1bd 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -57,6 +57,7 @@ class RuleSet(models.Model): 'part_parttesttemplate', 'part_partparametertemplate', 'part_partparameter', + 'part_partrelated', ], 'stock': [ 'stock_stockitem', @@ -71,6 +72,7 @@ class RuleSet(models.Model): 'part_bomitem', 'build_build', 'build_builditem', + 'build_buildorderattachment', 'stock_stockitem', 'stock_stocklocation', ], diff --git a/tasks.py b/tasks.py index df386633e9..49f3f9445b 100644 --- a/tasks.py +++ b/tasks.py @@ -171,7 +171,8 @@ def translate(c): or after adding translations for existing strings. """ - manage(c, "makemessages") + # Translate applicable .py / .html / .js files + manage(c, "makemessages -e py -e html -e js") manage(c, "compilemessages") @task